package handlers import ( "database/sql" "math" "net/http" "github.com/gin-gonic/gin" "git.gobha.me/xcaliber/chat-switchboard/database" ) // ── Request / Response types ──────────────── type createMessageRequest struct { Role string `json:"role" binding:"required,oneof=user assistant system"` Content string `json:"content" binding:"required"` Model string `json:"model,omitempty"` } type messageResponse struct { ID string `json:"id"` ChannelID string `json:"channel_id"` Role string `json:"role"` Content string `json:"content"` Model *string `json:"model"` TokensUsed *int `json:"tokens_used"` ParentID *string `json:"parent_id,omitempty"` CreatedAt string `json:"created_at"` } // MessageHandler holds dependencies for message endpoints. type MessageHandler struct{} // NewMessageHandler creates a new message handler. func NewMessageHandler() *MessageHandler { return &MessageHandler{} } // ── List Messages ─────────────────────────── func (h *MessageHandler) ListMessages(c *gin.Context) { page, perPage, offset := parsePagination(c) userID := getUserID(c) channelID := c.Param("id") // Verify ownership if !userOwnsChannel(c, channelID, userID) { return } // Count total messages var total int err := database.DB.QueryRow( `SELECT COUNT(*) FROM messages WHERE channel_id = $1`, channelID, ).Scan(&total) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to count messages"}) return } // Fetch messages (oldest first for conversation display) rows, err := database.DB.Query(` SELECT id, channel_id, role, content, model, tokens_used, parent_id, created_at FROM messages WHERE channel_id = $1 ORDER BY created_at ASC LIMIT $2 OFFSET $3 `, channelID, perPage, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list messages"}) return } defer rows.Close() messages := make([]messageResponse, 0) for rows.Next() { var msg messageResponse err := rows.Scan( &msg.ID, &msg.ChannelID, &msg.Role, &msg.Content, &msg.Model, &msg.TokensUsed, &msg.ParentID, &msg.CreatedAt, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan message"}) return } messages = append(messages, msg) } c.JSON(http.StatusOK, paginatedResponse{ Data: messages, Page: page, PerPage: perPage, Total: total, TotalPages: int(math.Ceil(float64(total) / float64(perPage))), }) } // ── Create Message ────────────────────────── func (h *MessageHandler) CreateMessage(c *gin.Context) { var req createMessageRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := getUserID(c) channelID := c.Param("id") // Verify ownership if !userOwnsChannel(c, channelID, userID) { return } // Find the current leaf message to set as parent var parentID *string _ = database.DB.QueryRow( `SELECT id FROM messages WHERE channel_id = $1 ORDER BY created_at DESC LIMIT 1`, channelID, ).Scan(&parentID) var msg messageResponse err := database.DB.QueryRow(` INSERT INTO messages (channel_id, role, content, model, parent_id, participant_type, participant_id) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, channel_id, role, content, model, tokens_used, parent_id, created_at `, channelID, req.Role, req.Content, req.Model, parentID, func() string { if req.Role == "assistant" { return "model" } return "user" }(), func() string { if req.Role == "assistant" && req.Model != "" { return req.Model } return userID }(), ).Scan( &msg.ID, &msg.ChannelID, &msg.Role, &msg.Content, &msg.Model, &msg.TokensUsed, &msg.ParentID, &msg.CreatedAt, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create message"}) return } // Touch the channel's updated_at so it sorts to top of list _, _ = database.DB.Exec(`UPDATE channels SET updated_at = NOW() WHERE id = $1`, channelID) c.JSON(http.StatusCreated, msg) } // ── Regenerate (stub for #8 Chat Engine) ──── func (h *MessageHandler) Regenerate(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{ "error": "regenerate requires Chat Engine (ticket #8)", }) } // ── Stream (stub for #8 Chat Engine) ──────── func (h *MessageHandler) Stream(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{ "error": "streaming requires Chat Engine (ticket #8)", }) } // ── Ownership Check ───────────────────────── func userOwnsChannel(c *gin.Context, channelID, userID string) bool { var ownerID string err := database.DB.QueryRow( `SELECT user_id FROM channels WHERE id = $1`, channelID, ).Scan(&ownerID) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"}) return false } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify channel ownership"}) return false } if ownerID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "not your channel"}) return false } return true }