veza/veza-backend-api/internal/websocket/chat/handler_messages.go
senke eb2862092d
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Storybook Audit / Build & audit Storybook (push) Failing after 0s
feat(v0.10.6): Livestreaming basique F471-F476
- Backend: callbacks on_publish/on_publish_done, UpdateStreamURL, GetByStreamKey
- Nginx-RTMP: config infra, docker-compose service (profil live)
- Frontend: stream_url dans LiveStream, HLS.js dans LiveViewPlayer, état Stream terminé
- Chat: rate limit send_live_message 1 msg/3s pour rooms live_streams
- Env: RTMP_CALLBACK_SECRET, STREAM_HLS_BASE_URL, NGINX_RTMP_HOST
- Roadmap v0.10.6 marquée DONE
2026-03-10 10:21:57 +01:00

225 lines
5.7 KiB
Go

package chat
import (
"context"
"encoding/json"
"regexp"
"strings"
"time"
"veza-backend-api/internal/models"
"github.com/google/uuid"
"go.uber.org/zap"
)
var mentionRegex = regexp.MustCompile(`@(\w+)`)
func (h *MessageHandler) HandleSendMessage(ctx context.Context, client *Client, msg *IncomingMessage) {
if msg.ConversationID == nil {
client.SendJSON(NewErrorResponse("conversation_id is required"))
return
}
if msg.Content == "" {
client.SendJSON(NewErrorResponse("content is required"))
return
}
// F474: live chat rate limit 1 msg/3s when conversation is a live stream room
action := "send_message"
if h.permissions.IsLiveRoom(ctx, *msg.ConversationID) {
action = "send_live_message"
}
if !h.rateLimiter.Allow(client.UserID, action) {
client.SendJSON(NewErrorResponse("rate limit exceeded"))
return
}
if !h.permissions.CanSend(ctx, client.UserID, *msg.ConversationID) {
client.SendJSON(NewErrorResponse("not allowed to send messages in this conversation"))
return
}
md := make(map[string]interface{})
if len(msg.Attachments) > 0 {
md["attachments"] = msg.Attachments
}
var mentionIDs []string
if h.userRepo != nil {
matches := mentionRegex.FindAllStringSubmatch(msg.Content, -1)
seen := make(map[string]bool)
for _, m := range matches {
if len(m) < 2 || seen[m[1]] {
continue
}
username := strings.TrimSpace(m[1])
if username == "" {
continue
}
u, err := h.userRepo.GetUserByUsername(ctx, username)
if err != nil || u == nil {
continue
}
if u.ID != client.UserID && u.ID != uuid.Nil {
mentionIDs = append(mentionIDs, u.ID.String())
seen[m[1]] = true
}
}
if len(mentionIDs) > 0 {
md["mentions"] = mentionIDs
}
}
var metadata []byte
if len(md) > 0 {
metadata, _ = json.Marshal(md)
}
chatMsg := &models.ChatMessage{
ID: uuid.New(),
ConversationID: *msg.ConversationID,
SenderID: client.UserID,
Content: msg.Content,
MessageType: "text",
ParentMessageID: msg.ParentMessageID,
ReplyToID: msg.ParentMessageID, // DB column; same as parent for threading
Status: "sent",
Metadata: metadata,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := h.msgRepo.Create(ctx, chatMsg); err != nil {
h.logger.Error("Failed to save message",
zap.Error(err),
zap.String("user_id", client.UserID.String()))
client.SendJSON(NewErrorResponse("failed to send message"))
return
}
client.SendJSON(NewActionConfirmedResponse("message_sent", true))
outgoing := NewNewMessageResponse(
chatMsg.ConversationID,
chatMsg.ID,
chatMsg.SenderID,
client.Username,
chatMsg.Content,
chatMsg.CreatedAt,
msg.Attachments,
chatMsg.ParentMessageID,
)
data, _ := json.Marshal(outgoing)
h.hub.BroadcastToRoom(*msg.ConversationID, data, nil)
if h.pubsub != nil {
_ = h.pubsub.Publish(ctx, *msg.ConversationID, data)
}
// v0.9.6: Notify mentioned users
for _, idStr := range mentionIDs {
mentionedID, err := uuid.Parse(idStr)
if err != nil {
continue
}
mentionEvt := NewMentionedInMessageResponse(
chatMsg.ConversationID,
chatMsg.ID,
client.UserID,
chatMsg.Content,
)
mentionData, _ := json.Marshal(mentionEvt)
h.hub.SendToUser(mentionedID, mentionData)
}
}
func (h *MessageHandler) HandleEditMessage(ctx context.Context, client *Client, msg *IncomingMessage) {
if msg.MessageID == nil || msg.ConversationID == nil {
client.SendJSON(NewErrorResponse("message_id and conversation_id are required"))
return
}
if msg.NewContent == "" {
client.SendJSON(NewErrorResponse("new_content is required"))
return
}
chatMsg, err := h.msgRepo.GetByID(ctx, *msg.MessageID)
if err != nil {
client.SendJSON(NewErrorResponse("message not found"))
return
}
if chatMsg.SenderID != client.UserID {
client.SendJSON(NewErrorResponse("can only edit your own messages"))
return
}
now := time.Now()
chatMsg.Content = msg.NewContent
chatMsg.IsEdited = true
chatMsg.EditedAt = &now
if err := h.msgRepo.Update(ctx, chatMsg); err != nil {
h.logger.Error("Failed to update message", zap.Error(err))
client.SendJSON(NewErrorResponse("failed to edit message"))
return
}
client.SendJSON(NewActionConfirmedResponse("message_edited", true))
outgoing := NewMessageEditedResponse(
chatMsg.ID,
chatMsg.ConversationID,
client.UserID,
now,
msg.NewContent,
)
data, _ := json.Marshal(outgoing)
h.hub.BroadcastToRoom(*msg.ConversationID, data, nil)
if h.pubsub != nil {
_ = h.pubsub.Publish(ctx, *msg.ConversationID, data)
}
}
func (h *MessageHandler) HandleDeleteMessage(ctx context.Context, client *Client, msg *IncomingMessage) {
if msg.MessageID == nil || msg.ConversationID == nil {
client.SendJSON(NewErrorResponse("message_id and conversation_id are required"))
return
}
chatMsg, err := h.msgRepo.GetByID(ctx, *msg.MessageID)
if err != nil {
client.SendJSON(NewErrorResponse("message not found"))
return
}
isModerator := h.permissions.CanModerate(ctx, client.UserID, *msg.ConversationID)
if chatMsg.SenderID != client.UserID && !isModerator {
client.SendJSON(NewErrorResponse("can only delete your own messages"))
return
}
if err := h.msgRepo.SoftDelete(ctx, *msg.MessageID); err != nil {
h.logger.Error("Failed to delete message", zap.Error(err))
client.SendJSON(NewErrorResponse("failed to delete message"))
return
}
client.SendJSON(NewActionConfirmedResponse("message_deleted", true))
outgoing := NewMessageDeletedResponse(
*msg.MessageID,
*msg.ConversationID,
client.UserID,
time.Now(),
)
data, _ := json.Marshal(outgoing)
h.hub.BroadcastToRoom(*msg.ConversationID, data, nil)
if h.pubsub != nil {
_ = h.pubsub.Publish(ctx, *msg.ConversationID, data)
}
}