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"` ChatID string `json:"chat_id"` Role string `json:"role"` Content string `json:"content"` Model *string `json:"model"` TokensUsed *int `json:"tokens_used"` 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) chatID := c.Param("id") // Verify ownership if !userOwnsChat(c, chatID, userID) { return } // Count total messages var total int err := database.DB.QueryRow( `SELECT COUNT(*) FROM chat_messages WHERE chat_id = $1`, chatID, ).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, chat_id, role, content, model, tokens_used, created_at FROM chat_messages WHERE chat_id = $1 ORDER BY created_at ASC LIMIT $2 OFFSET $3 `, chatID, 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.ChatID, &msg.Role, &msg.Content, &msg.Model, &msg.TokensUsed, &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) chatID := c.Param("id") // Verify ownership if !userOwnsChat(c, chatID, userID) { return } var msg messageResponse err := database.DB.QueryRow(` INSERT INTO chat_messages (chat_id, role, content, model) VALUES ($1, $2, $3, $4) RETURNING id, chat_id, role, content, model, tokens_used, created_at `, chatID, req.Role, req.Content, req.Model).Scan( &msg.ID, &msg.ChatID, &msg.Role, &msg.Content, &msg.Model, &msg.TokensUsed, &msg.CreatedAt, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create message"}) return } // Touch the chat's updated_at so it sorts to top of list _, _ = database.DB.Exec(`UPDATE chats SET updated_at = NOW() WHERE id = $1`, chatID) 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 userOwnsChat(c *gin.Context, chatID, userID string) bool { var ownerID string err := database.DB.QueryRow( `SELECT user_id FROM chats WHERE id = $1`, chatID, ).Scan(&ownerID) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "chat not found"}) return false } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify chat ownership"}) return false } if ownerID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "not your chat"}) return false } return true }