- 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
225 lines
5.7 KiB
Go
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)
|
|
}
|
|
}
|