This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/messages.go
2026-02-16 01:01:33 +00:00

175 lines
4.6 KiB
Go

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
}