package handlers import ( "context" // "encoding/base64" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/google/uuid" "gitlab.com/patrickbritton3/sojorn/go-backend/internal/realtime" "gitlab.com/patrickbritton3/sojorn/go-backend/internal/repository" "gitlab.com/patrickbritton3/sojorn/go-backend/internal/services" "github.com/rs/zerolog/log" ) type ChatHandler struct { chatRepo *repository.ChatRepository notificationService *services.NotificationService hub *realtime.Hub } func NewChatHandler(chatRepo *repository.ChatRepository, notificationService *services.NotificationService, hub *realtime.Hub) *ChatHandler { return &ChatHandler{ chatRepo: chatRepo, notificationService: notificationService, hub: hub, } } func (h *ChatHandler) GetConversations(c *gin.Context) { userIDStr, _ := c.Get("user_id") conversations, err := h.chatRepo.GetConversations(c.Request.Context(), userIDStr.(string)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch conversations"}) return } c.JSON(http.StatusOK, gin.H{"conversations": conversations}) } func (h *ChatHandler) GetOrCreateConversation(c *gin.Context) { userIDStr, _ := c.Get("user_id") otherUserID := c.Query("other_user_id") if otherUserID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "other_user_id is required"}) return } id, err := h.chatRepo.GetOrCreateConversation(c.Request.Context(), userIDStr.(string), otherUserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to handle conversation"}) return } c.JSON(http.StatusOK, gin.H{"conversation_id": id}) } func (h *ChatHandler) SendMessage(c *gin.Context) { senderIDStr, _ := c.Get("user_id") senderID, err := uuid.Parse(senderIDStr.(string)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid sender ID"}) return } var req struct { ConversationID string `json:"conversation_id" binding:"required"` ReceiverID string `json:"receiver_id"` Ciphertext string `json:"ciphertext" binding:"required"` IV string `json:"iv"` KeyVersion string `json:"key_version"` MessageHeader string `json:"message_header"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } convID, err := uuid.Parse(req.ConversationID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid conversation ID"}) return } pA, pB, err := h.chatRepo.GetParticipants(c.Request.Context(), req.ConversationID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to authenticate conversation participants"}) return } if senderID.String() != pA && senderID.String() != pB { c.JSON(http.StatusForbidden, gin.H{"error": "You are not a participant in this conversation"}) return } otherParticipant := pA if senderID.String() == pA { otherParticipant = pB } var receiverID uuid.UUID if req.ReceiverID != "" { receiverID, err = uuid.Parse(req.ReceiverID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid receiver ID"}) return } if receiverID.String() != otherParticipant { c.JSON(http.StatusForbidden, gin.H{"error": "Receiver is not a participant in this conversation"}) return } } else { receiverID, _ = uuid.Parse(otherParticipant) } // Persist blind ciphertext to DB msg, err := h.chatRepo.CreateMessage(c.Request.Context(), senderID, receiverID, convID, req.Ciphertext, req.IV, req.KeyVersion, req.MessageHeader) if err != nil { log.Error().Err(err).Msg("Failed to persist secure message") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send message"}) return } // Prepare Real-time Payload rtPayload := gin.H{ "type": "new_message", "payload": gin.H{ "id": msg.ID, "conversation_id": msg.ConversationID, "sender_id": msg.SenderID, "receiver_id": msg.ReceiverID, "ciphertext": msg.Ciphertext, "iv": msg.IV, "key_version": msg.KeyVersion, // e.g. "x3dh" "message_header": msg.MessageHeader, "created_at": msg.CreatedAt, }, } // 1. Send via WebSocket (Best Effort, Immediate) h.hub.SendToUser(receiverID.String(), rtPayload) // 2. Send via Notification Service (Background, Reliable) if h.notificationService != nil { go func(recipID string, senderID string, convID string) { _ = h.notificationService.NotifyMessage(context.Background(), recipID, senderID, convID) }(receiverID.String(), senderID.String(), msg.ConversationID.String()) } c.JSON(http.StatusCreated, msg) } func (h *ChatHandler) GetMessages(c *gin.Context) { convIDStr := c.Param("id") convID, err := uuid.Parse(convIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid conversation ID"}) return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) messages, err := h.chatRepo.GetMessages(c.Request.Context(), convID, limit, offset) if err != nil { log.Error().Err(err).Str("conversation_id", convIDStr).Msg("Failed to fetch messages") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch messages"}) return } c.JSON(http.StatusOK, gin.H{"messages": messages}) } func (h *ChatHandler) GetMutualFollows(c *gin.Context) { userIDStr, _ := c.Get("user_id") profiles, err := h.chatRepo.GetMutualFollows(c.Request.Context(), userIDStr.(string)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch mutual follows"}) return } c.JSON(http.StatusOK, gin.H{"profiles": profiles}) } func (h *ChatHandler) DeleteConversation(c *gin.Context) { userIDStr, _ := c.Get("user_id") convIDStr := c.Param("id") convID, err := uuid.Parse(convIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid conversation ID"}) return } err = h.chatRepo.DeleteConversation(c.Request.Context(), convID, userIDStr.(string)) if err != nil { log.Error().Err(err).Str("conversation_id", convIDStr).Msg("Failed to delete conversation") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete conversation"}) return } // Broadcast deletion to current user via WebSocket deleteEvent := map[string]interface{}{ "type": "conversation_deleted", "payload": map[string]interface{}{ "conversation_id": convID.String(), "deleted_by": userIDStr, }, } // Send to current user (all their devices) _ = h.hub.SendToUser(userIDStr.(string), deleteEvent) c.JSON(http.StatusOK, gin.H{"success": true, "message": "Conversation permanently deleted"}) } func (h *ChatHandler) DeleteMessage(c *gin.Context) { userIDStr, _ := c.Get("user_id") msgIDStr := c.Param("id") msgID, err := uuid.Parse(msgIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid message ID"}) return } // Get conversation and recipient info before deleting var conversationID, senderID, receiverID uuid.UUID err = h.chatRepo.GetMessageInfo(c.Request.Context(), msgID, &conversationID, &senderID, &receiverID) if err != nil { log.Error().Err(err).Str("message_id", msgIDStr).Msg("Failed to get message info") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get message info"}) return } err = h.chatRepo.DeleteMessage(c.Request.Context(), msgID, userIDStr.(string)) if err != nil { log.Error().Err(err).Str("message_id", msgIDStr).Msg("Failed to delete message") c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete message"}) return } // Broadcast deletion to both participants via WebSocket deleteEvent := map[string]interface{}{ "type": "message_deleted", "payload": map[string]interface{}{ "message_id": msgID.String(), "conversation_id": conversationID.String(), "deleted_by": userIDStr, }, } // Send to sender (all their devices) _ = h.hub.SendToUser(senderID.String(), deleteEvent) // Send to receiver (all their devices) _ = h.hub.SendToUser(receiverID.String(), deleteEvent) c.JSON(http.StatusOK, gin.H{"success": true, "message": "Message permanently deleted"}) }