- Implement full MessageHandler dispatch with all 18 incoming message types - Add handler_messages.go: SendMessage, EditMessage, DeleteMessage with ownership checks - Add handler_rooms.go: JoinConversation, LeaveConversation - Add handler_history.go: FetchHistory (cursor-based), SearchMessages (ILIKE), SyncMessages - Add handler_realtime.go: Typing, MarkAsRead, Delivered, AddReaction, RemoveReaction - Add handler_calls.go: WebRTC signaling relay (CallOffer/Answer/ICE/Hangup/Reject) - Add PermissionService: CanRead/CanSend/CanJoin/CanModerate based on room_members - Add RateLimiter: per-user per-action sliding window (in-memory) - Wire all dependencies in router.go setupChatWebSocket
152 lines
4.4 KiB
Go
152 lines
4.4 KiB
Go
package chat
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/models"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func (h *MessageHandler) HandleTyping(ctx context.Context, client *Client, msg *IncomingMessage) {
|
|
if msg.ConversationID == nil {
|
|
client.SendJSON(NewErrorResponse("conversation_id is required"))
|
|
return
|
|
}
|
|
|
|
if !h.rateLimiter.Allow(client.UserID, "typing") {
|
|
return
|
|
}
|
|
|
|
isTyping := true
|
|
if msg.IsTyping != nil {
|
|
isTyping = *msg.IsTyping
|
|
}
|
|
|
|
outgoing := NewUserTypingResponse(*msg.ConversationID, client.UserID, isTyping)
|
|
data, _ := json.Marshal(outgoing)
|
|
h.hub.BroadcastToRoom(*msg.ConversationID, data, client)
|
|
|
|
client.SendJSON(NewActionConfirmedResponse("typing_indicator", true))
|
|
}
|
|
|
|
func (h *MessageHandler) HandleMarkAsRead(ctx context.Context, client *Client, msg *IncomingMessage) {
|
|
if msg.ConversationID == nil || msg.MessageID == nil {
|
|
client.SendJSON(NewErrorResponse("conversation_id and message_id are required"))
|
|
return
|
|
}
|
|
|
|
receipt := &models.ReadReceipt{
|
|
ID: uuid.New(),
|
|
UserID: client.UserID,
|
|
MessageID: *msg.MessageID,
|
|
ReadAt: time.Now(),
|
|
}
|
|
|
|
if err := h.readRepo.MarkRead(ctx, receipt); err != nil {
|
|
h.logger.Error("Failed to mark as read", zap.Error(err))
|
|
client.SendJSON(NewErrorResponse("failed to mark as read"))
|
|
return
|
|
}
|
|
|
|
client.SendJSON(NewActionConfirmedResponse("marked_as_read", true))
|
|
|
|
outgoing := NewMessageReadResponse(*msg.MessageID, client.UserID, *msg.ConversationID, receipt.ReadAt)
|
|
data, _ := json.Marshal(outgoing)
|
|
h.hub.BroadcastToRoom(*msg.ConversationID, data, client)
|
|
|
|
if h.pubsub != nil {
|
|
_ = h.pubsub.Publish(ctx, *msg.ConversationID, data)
|
|
}
|
|
}
|
|
|
|
func (h *MessageHandler) HandleDelivered(ctx context.Context, client *Client, msg *IncomingMessage) {
|
|
if msg.ConversationID == nil || msg.MessageID == nil {
|
|
client.SendJSON(NewErrorResponse("conversation_id and message_id are required"))
|
|
return
|
|
}
|
|
|
|
status := &models.DeliveredStatus{
|
|
ID: uuid.New(),
|
|
UserID: client.UserID,
|
|
MessageID: *msg.MessageID,
|
|
DeliveredAt: time.Now(),
|
|
}
|
|
|
|
if err := h.deliveredRepo.MarkDelivered(ctx, status); err != nil {
|
|
h.logger.Error("Failed to mark as delivered", zap.Error(err))
|
|
client.SendJSON(NewErrorResponse("failed to mark as delivered"))
|
|
return
|
|
}
|
|
|
|
client.SendJSON(NewActionConfirmedResponse("marked_as_delivered", true))
|
|
|
|
outgoing := NewMessageDeliveredResponse(*msg.MessageID, client.UserID, *msg.ConversationID, status.DeliveredAt)
|
|
data, _ := json.Marshal(outgoing)
|
|
h.hub.BroadcastToRoom(*msg.ConversationID, data, client)
|
|
|
|
if h.pubsub != nil {
|
|
_ = h.pubsub.Publish(ctx, *msg.ConversationID, data)
|
|
}
|
|
}
|
|
|
|
func (h *MessageHandler) HandleAddReaction(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.Emoji == "" {
|
|
client.SendJSON(NewErrorResponse("emoji is required"))
|
|
return
|
|
}
|
|
|
|
reaction := &models.MessageReaction{
|
|
ID: uuid.New(),
|
|
UserID: client.UserID,
|
|
MessageID: *msg.MessageID,
|
|
Emoji: msg.Emoji,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
if err := h.reactionRepo.Add(ctx, reaction); err != nil {
|
|
h.logger.Error("Failed to add reaction", zap.Error(err))
|
|
client.SendJSON(NewErrorResponse("failed to add reaction"))
|
|
return
|
|
}
|
|
|
|
client.SendJSON(NewActionConfirmedResponse("reaction_added", true))
|
|
|
|
outgoing := NewReactionAddedResponse(*msg.MessageID, *msg.ConversationID, client.UserID, msg.Emoji)
|
|
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) HandleRemoveReaction(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 err := h.reactionRepo.Remove(ctx, client.UserID, *msg.MessageID); err != nil {
|
|
h.logger.Error("Failed to remove reaction", zap.Error(err))
|
|
client.SendJSON(NewErrorResponse("failed to remove reaction"))
|
|
return
|
|
}
|
|
|
|
client.SendJSON(NewActionConfirmedResponse("reaction_removed", true))
|
|
|
|
outgoing := NewReactionRemovedResponse(*msg.MessageID, *msg.ConversationID, client.UserID)
|
|
data, _ := json.Marshal(outgoing)
|
|
h.hub.BroadcastToRoom(*msg.ConversationID, data, nil)
|
|
|
|
if h.pubsub != nil {
|
|
_ = h.pubsub.Publish(ctx, *msg.ConversationID, data)
|
|
}
|
|
}
|