package chat import ( "encoding/json" "time" "github.com/google/uuid" ) // Incoming message types (client -> server) const ( TypeSendMessage = "SendMessage" TypeJoinConversation = "JoinConversation" TypeLeaveConversation = "LeaveConversation" TypeMarkAsRead = "MarkAsRead" TypeTyping = "Typing" TypeDelivered = "Delivered" TypeEditMessage = "EditMessage" TypeDeleteMessage = "DeleteMessage" TypeAddReaction = "AddReaction" TypeRemoveReaction = "RemoveReaction" TypeFetchHistory = "FetchHistory" TypeSearchMessages = "SearchMessages" TypeSyncMessages = "SyncMessages" TypeCallOffer = "CallOffer" TypeCallAnswer = "CallAnswer" TypeICECandidate = "ICECandidate" TypeCallHangup = "CallHangup" TypeCallReject = "CallReject" TypePing = "Ping" ) // Outgoing message types (server -> client) const ( TypeNewMessage = "NewMessage" TypeMessageRead = "MessageRead" TypeMessageDelivered = "MessageDelivered" TypeUserTyping = "UserTyping" TypeMessageEdited = "MessageEdited" TypeMessageDeleted = "MessageDeleted" TypeReactionAdded = "ReactionAdded" TypeReactionRemoved = "ReactionRemoved" TypeHistoryChunk = "HistoryChunk" TypeSearchResults = "SearchResults" TypeSyncChunk = "SyncChunk" TypeActionConfirmed = "ActionConfirmed" TypeError = "Error" TypeOutCallOffer = "CallOffer" TypeOutCallAnswer = "CallAnswer" TypeOutICECandidate = "ICECandidate" TypeOutCallHangup = "CallHangup" TypeCallRejected = "CallRejected" TypePong = "Pong" TypeMentionedInMessage = "MentionedInMessage" ) // IncomingMessage is a flat struct that deserializes any client message. // The "type" field selects which fields are relevant. type IncomingMessage struct { Type string `json:"type"` // Common fields ConversationID *uuid.UUID `json:"conversation_id,omitempty"` MessageID *uuid.UUID `json:"message_id,omitempty"` // SendMessage Content string `json:"content,omitempty"` ParentMessageID *uuid.UUID `json:"parent_message_id,omitempty"` Attachments []MessageAttachment `json:"attachments,omitempty"` // Typing IsTyping *bool `json:"is_typing,omitempty"` // EditMessage NewContent string `json:"new_content,omitempty"` // AddReaction Emoji string `json:"emoji,omitempty"` // FetchHistory Before *string `json:"before,omitempty"` After *string `json:"after,omitempty"` Limit *int `json:"limit,omitempty"` // SearchMessages Query string `json:"query,omitempty"` Offset *int `json:"offset,omitempty"` // SyncMessages Since *string `json:"since,omitempty"` // Call signaling TargetUserID *uuid.UUID `json:"target_user_id,omitempty"` CallerUserID *uuid.UUID `json:"caller_user_id,omitempty"` SDP string `json:"sdp,omitempty"` CallType string `json:"call_type,omitempty"` Candidate string `json:"candidate,omitempty"` } type MessageAttachment struct { FileName string `json:"file_name"` FileType string `json:"file_type"` FileURL string `json:"file_url"` FileSize int64 `json:"file_size"` } // MessageDTO is the JSON representation of a message in history/search/sync responses. type MessageDTO struct { ID uuid.UUID `json:"id"` ConversationID uuid.UUID `json:"conversation_id"` SenderID uuid.UUID `json:"sender_id"` Content string `json:"content"` MessageType string `json:"message_type"` ParentMessageID *uuid.UUID `json:"parent_message_id,omitempty"` ReplyToID *uuid.UUID `json:"reply_to_id,omitempty"` IsPinned bool `json:"is_pinned"` IsEdited bool `json:"is_edited"` IsDeleted bool `json:"is_deleted"` EditedAt *time.Time `json:"edited_at,omitempty"` DeletedAt *time.Time `json:"deleted_at,omitempty"` Status string `json:"status"` Metadata json.RawMessage `json:"metadata,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Attachments []MessageAttachment `json:"attachments,omitempty"` Reactions map[string][]string `json:"reactions,omitempty"` // emoji -> user IDs } // --- Outgoing message factory functions --- func NewNewMessageResponse(conversationID, messageID, senderID uuid.UUID, senderUsername, content string, createdAt time.Time, attachments []MessageAttachment, parentMessageID *uuid.UUID) map[string]interface{} { out := map[string]interface{}{ "type": TypeNewMessage, "conversation_id": conversationID, "message_id": messageID, "sender_id": senderID, "sender_username": senderUsername, "content": content, "created_at": createdAt.UTC().Format(time.RFC3339), "attachments": attachments, } if parentMessageID != nil { out["parent_message_id"] = parentMessageID.String() } return out } func NewMessageReadResponse(messageID, userID, conversationID uuid.UUID, readAt time.Time) map[string]interface{} { return map[string]interface{}{ "type": TypeMessageRead, "message_id": messageID, "user_id": userID, "conversation_id": conversationID, "read_at": readAt.UTC().Format(time.RFC3339), } } func NewMessageDeliveredResponse(messageID, userID, conversationID uuid.UUID, deliveredAt time.Time) map[string]interface{} { return map[string]interface{}{ "type": TypeMessageDelivered, "message_id": messageID, "user_id": userID, "conversation_id": conversationID, "delivered_at": deliveredAt.UTC().Format(time.RFC3339), } } func NewUserTypingResponse(conversationID, userID uuid.UUID, isTyping bool) map[string]interface{} { return map[string]interface{}{ "type": TypeUserTyping, "conversation_id": conversationID, "user_id": userID, "is_typing": isTyping, } } func NewMessageEditedResponse(messageID, conversationID, editorID uuid.UUID, editedAt time.Time, newContent string) map[string]interface{} { return map[string]interface{}{ "type": TypeMessageEdited, "message_id": messageID, "conversation_id": conversationID, "editor_id": editorID, "edited_at": editedAt.UTC().Format(time.RFC3339), "new_content": newContent, } } func NewMessageDeletedResponse(messageID, conversationID, deleterID uuid.UUID, deletedAt time.Time) map[string]interface{} { return map[string]interface{}{ "type": TypeMessageDeleted, "message_id": messageID, "conversation_id": conversationID, "deleter_id": deleterID, "deleted_at": deletedAt.UTC().Format(time.RFC3339), } } func NewReactionAddedResponse(messageID, conversationID, userID uuid.UUID, emoji string) map[string]interface{} { return map[string]interface{}{ "type": TypeReactionAdded, "message_id": messageID, "conversation_id": conversationID, "user_id": userID, "emoji": emoji, } } func NewReactionRemovedResponse(messageID, conversationID, userID uuid.UUID) map[string]interface{} { return NewReactionRemovedResponseWithEmoji(messageID, conversationID, userID, "") } // NewReactionRemovedResponseWithEmoji includes emoji when provided (for remove-by-emoji). func NewReactionRemovedResponseWithEmoji(messageID, conversationID, userID uuid.UUID, emoji string) map[string]interface{} { res := map[string]interface{}{ "type": TypeReactionRemoved, "message_id": messageID, "conversation_id": conversationID, "user_id": userID, } if emoji != "" { res["emoji"] = emoji } return res } func NewHistoryChunkResponse(conversationID uuid.UUID, messages []MessageDTO, hasMoreBefore, hasMoreAfter bool) map[string]interface{} { return map[string]interface{}{ "type": TypeHistoryChunk, "conversation_id": conversationID, "messages": messages, "has_more_before": hasMoreBefore, "has_more_after": hasMoreAfter, } } func NewSearchResultsResponse(conversationID uuid.UUID, messages []MessageDTO, query string, total int64) map[string]interface{} { return map[string]interface{}{ "type": TypeSearchResults, "conversation_id": conversationID, "messages": messages, "query": query, "total": total, } } func NewSyncChunkResponse(conversationID uuid.UUID, messages []MessageDTO, lastSync time.Time) map[string]interface{} { return map[string]interface{}{ "type": TypeSyncChunk, "conversation_id": conversationID, "messages": messages, "last_sync": lastSync.UTC().Format(time.RFC3339), } } func NewActionConfirmedResponse(action string, success bool) map[string]interface{} { return map[string]interface{}{ "type": TypeActionConfirmed, "action": action, "success": success, } } func NewErrorResponse(message string) map[string]interface{} { return map[string]interface{}{ "type": TypeError, "message": message, } } func NewPongResponse() map[string]interface{} { return map[string]interface{}{ "type": TypePong, } } func NewCallOfferResponse(conversationID, callerUserID uuid.UUID, sdp, callType string) map[string]interface{} { return map[string]interface{}{ "type": TypeOutCallOffer, "conversation_id": conversationID, "caller_user_id": callerUserID, "sdp": sdp, "call_type": callType, } } func NewCallAnswerResponse(conversationID, targetUserID, fromUserID uuid.UUID, sdp string) map[string]interface{} { return map[string]interface{}{ "type": TypeOutCallAnswer, "conversation_id": conversationID, "target_user_id": targetUserID, "from_user_id": fromUserID, "sdp": sdp, } } func NewICECandidateResponse(conversationID, fromUserID uuid.UUID, candidate string) map[string]interface{} { return map[string]interface{}{ "type": TypeOutICECandidate, "conversation_id": conversationID, "from_user_id": fromUserID, "candidate": candidate, } } func NewCallHangupResponse(conversationID, userID uuid.UUID) map[string]interface{} { return map[string]interface{}{ "type": TypeOutCallHangup, "conversation_id": conversationID, "user_id": userID, } } func NewCallRejectedResponse(conversationID, userID uuid.UUID) map[string]interface{} { return map[string]interface{}{ "type": TypeCallRejected, "conversation_id": conversationID, "user_id": userID, } } // NewMentionedInMessageResponse notifies a user they were mentioned in a message (v0.9.6) func NewMentionedInMessageResponse(conversationID, messageID, authorID uuid.UUID, contentPreview string) map[string]interface{} { preview := contentPreview if len(preview) > 100 { preview = preview[:97] + "..." } return map[string]interface{}{ "type": TypeMentionedInMessage, "conversation_id": conversationID, "message_id": messageID, "author_id": authorID, "content_preview": preview, } }