Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
capspkg "chat-switchboard/capabilities"
|
||||
"chat-switchboard/crypto"
|
||||
"chat-switchboard/database"
|
||||
"chat-switchboard/events"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/providers"
|
||||
@@ -153,7 +156,7 @@ func (h *MessageHandler) GetActivePath(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"messages": path})
|
||||
c.JSON(http.StatusOK, gin.H{"data": path})
|
||||
}
|
||||
|
||||
// ── Create Message (manual) ─────────────────
|
||||
@@ -218,6 +221,11 @@ func (h *MessageHandler) CreateMessage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Broadcast via WS to channel participants
|
||||
if h.hub != nil && msg.Role == "user" {
|
||||
broadcastUserMessage(c.Request.Context(), h.hub, channelID, msg.ID, msg.Content, userID)
|
||||
}
|
||||
|
||||
resp := messageResponse{
|
||||
ID: msg.ID,
|
||||
ChannelID: msg.ChannelID,
|
||||
@@ -579,7 +587,7 @@ func (h *MessageHandler) UpdateCursor(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"messages": path, "active_leaf_id": leafID})
|
||||
c.JSON(http.StatusOK, gin.H{"data": path, "active_leaf_id": leafID})
|
||||
}
|
||||
|
||||
// ── List Siblings ───────────────────────────
|
||||
@@ -638,3 +646,157 @@ func userOwnsChannel(c *gin.Context, channelID, userID string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// broadcastUserMessage publishes a message.created event to all user
|
||||
// participants in the channel via WebSocket.
|
||||
func broadcastUserMessage(ctx context.Context, hub *events.Hub, channelID, msgID, content, senderID string) {
|
||||
// Look up sender display name
|
||||
var displayName, username string
|
||||
_ = database.DB.QueryRowContext(ctx, database.Q(`
|
||||
SELECT COALESCE(display_name, ''), COALESCE(username, '') FROM users WHERE id = $1
|
||||
`), senderID).Scan(&displayName, &username)
|
||||
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"id": msgID,
|
||||
"channel_id": channelID,
|
||||
"role": "user",
|
||||
"content": content,
|
||||
"user_id": senderID,
|
||||
"display_name": displayName,
|
||||
"username": username,
|
||||
"created_at": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
evt := events.Event{
|
||||
Label: "message.created",
|
||||
Payload: payload,
|
||||
Ts: time.Now().UnixMilli(),
|
||||
}
|
||||
// Send to all user participants in the channel
|
||||
rows, err := database.DB.QueryContext(ctx, database.Q(`
|
||||
SELECT participant_id FROM channel_participants
|
||||
WHERE channel_id = $1 AND participant_type = 'user'
|
||||
`), channelID)
|
||||
if err != nil {
|
||||
// Fallback: at least send to the sender
|
||||
hub.PublishToUser(senderID, evt)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var pid string
|
||||
if rows.Scan(&pid) == nil {
|
||||
hub.PublishToUser(pid, evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// broadcastAssistantMessage publishes a message.created event for an assistant
|
||||
// message to all user participants in the channel (except the requesting user,
|
||||
// who already received the response via SSE).
|
||||
func broadcastAssistantMessage(ctx context.Context, hub *events.Hub, channelID, msgID, content, model, senderUserID, personaID string) {
|
||||
// Look up persona display name if available
|
||||
var displayName, avatar string
|
||||
if personaID != "" {
|
||||
_ = database.DB.QueryRowContext(ctx, database.Q(`
|
||||
SELECT COALESCE(name, ''), COALESCE(avatar, '') FROM personas WHERE id = $1
|
||||
`), personaID).Scan(&displayName, &avatar)
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = model
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"id": msgID,
|
||||
"channel_id": channelID,
|
||||
"role": "assistant",
|
||||
"content": content,
|
||||
"model": model,
|
||||
"participant_type": "persona",
|
||||
"participant_id": personaID,
|
||||
"display_name": displayName,
|
||||
"avatar": avatar,
|
||||
"created_at": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
|
||||
})
|
||||
evt := events.Event{
|
||||
Label: "message.created",
|
||||
Payload: payload,
|
||||
Ts: time.Now().UnixMilli(),
|
||||
}
|
||||
// Send to all user participants except the sender (who got SSE)
|
||||
rows, err := database.DB.QueryContext(ctx, database.Q(`
|
||||
SELECT participant_id FROM channel_participants
|
||||
WHERE channel_id = $1 AND participant_type = 'user'
|
||||
`), channelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var pid string
|
||||
if rows.Scan(&pid) == nil && pid != senderUserID {
|
||||
hub.PublishToUser(pid, evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete Message ──────────────────────────────────────────
|
||||
|
||||
// DeleteMessage soft-deletes a message.
|
||||
// DELETE /channels/:id/messages/:msgId
|
||||
func (h *MessageHandler) DeleteMessage(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
channelID := c.Param("id")
|
||||
msgID := c.Param("msgId")
|
||||
|
||||
if !userCanAccessChannel(c, h.stores, channelID, userID) {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify message belongs to this channel and is not already deleted
|
||||
var exists bool
|
||||
_ = database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT EXISTS(SELECT 1 FROM messages WHERE id = $1 AND channel_id = $2 AND deleted_at IS NULL)
|
||||
`), msgID, channelID).Scan(&exists)
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.stores.Messages.Delete(c.Request.Context(), msgID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete message"})
|
||||
return
|
||||
}
|
||||
|
||||
// Broadcast to channel participants (exclude sender — they already removed it)
|
||||
broadcastMessageDeleted(c.Request.Context(), h.hub, channelID, msgID, userID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// broadcastMessageDeleted publishes a message.deleted event to all user
|
||||
// participants in the channel except the sender.
|
||||
func broadcastMessageDeleted(ctx context.Context, hub *events.Hub, channelID, msgID, senderID string) {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"id": msgID,
|
||||
"channel_id": channelID,
|
||||
})
|
||||
evt := events.Event{
|
||||
Label: "message.deleted",
|
||||
Payload: payload,
|
||||
Ts: time.Now().UnixMilli(),
|
||||
}
|
||||
rows, err := database.DB.QueryContext(ctx, database.Q(`
|
||||
SELECT participant_id FROM channel_participants
|
||||
WHERE channel_id = $1 AND participant_type = 'user'
|
||||
`), channelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var pid string
|
||||
if rows.Scan(&pid) == nil && pid != senderID {
|
||||
hub.PublishToUser(pid, evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user