package api import ( "encoding/json" "net/http" "strconv" "time" "messenger/internal/realtime" "messenger/internal/store" ) type conversationJSON struct { ID int64 `json:"id"` Type string `json:"type"` CreatedAt time.Time `json:"created_at"` LastMessage *messageJSON `json:"last_message,omitempty"` UnreadCount int64 `json:"unread_count"` } type messageJSON struct { ID int64 `json:"id"` ConversationID int64 `json:"conversation_id"` SenderID int64 `json:"sender_id"` Body string `json:"body"` CreatedAt time.Time `json:"created_at"` EditedAt *time.Time `json:"edited_at,omitempty"` DeletedAt *time.Time `json:"deleted_at,omitempty"` } func toMessageJSON(m *store.Message) messageJSON { return messageJSON{ ID: m.ID, ConversationID: m.ConversationID, SenderID: m.SenderID, Body: m.Body, CreatedAt: m.CreatedAt, EditedAt: m.EditedAt, DeletedAt: m.DeletedAt, } } func (h *apiHandler) listConversations(w http.ResponseWriter, r *http.Request) { userID, _ := userIDFrom(r.Context()) summaries, err := h.store.ListConversationsForUser(r.Context(), userID) if err != nil { writeError(w, http.StatusInternalServerError, "internal server error") return } out := make([]conversationJSON, 0, len(summaries)) for _, s := range summaries { item := conversationJSON{ ID: s.ID, Type: s.Type, CreatedAt: s.CreatedAt, UnreadCount: s.UnreadCount, } if s.LastMessage != nil { msg := toMessageJSON(s.LastMessage) item.LastMessage = &msg } out = append(out, item) } writeJSON(w, http.StatusOK, out) } func (h *apiHandler) createDM(w http.ResponseWriter, r *http.Request) { userID, _ := userIDFrom(r.Context()) var req struct { UserID int64 `json:"user_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.UserID <= 0 { writeError(w, http.StatusBadRequest, "invalid user_id") return } if req.UserID == userID { writeError(w, http.StatusBadRequest, "cannot chat with yourself") return } conv, err := h.store.CreateDM(r.Context(), userID, req.UserID) if err != nil { writeError(w, http.StatusInternalServerError, "internal server error") return } writeJSON(w, http.StatusCreated, conversationJSON{ID: conv.ID, Type: conv.Type, CreatedAt: conv.CreatedAt}) } func (h *apiHandler) listMessages(w http.ResponseWriter, r *http.Request) { userID, _ := userIDFrom(r.Context()) convID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil || convID <= 0 { writeError(w, http.StatusBadRequest, "invalid conversation id") return } member, err := h.store.IsMember(r.Context(), convID, userID) if err != nil || !member { writeError(w, http.StatusForbidden, "not a member") return } limit := 50 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 100 { limit = n } } var before int64 if v := r.URL.Query().Get("before"); v != "" { before, _ = strconv.ParseInt(v, 10, 64) } messages, err := h.store.ListMessages(r.Context(), convID, before, limit) if err != nil { writeError(w, http.StatusInternalServerError, "internal server error") return } out := make([]messageJSON, 0, len(messages)) for i := range messages { out = append(out, toMessageJSON(&messages[i])) } writeJSON(w, http.StatusOK, out) } type sendMessageRequest struct { Body string `json:"body"` TempID string `json:"temp_id"` } func (h *apiHandler) sendMessage(w http.ResponseWriter, r *http.Request) { userID, _ := userIDFrom(r.Context()) convID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil || convID <= 0 { writeError(w, http.StatusBadRequest, "invalid conversation id") return } member, err := h.store.IsMember(r.Context(), convID, userID) if err != nil || !member { writeError(w, http.StatusForbidden, "not a member") return } var req sendMessageRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } if req.Body == "" { writeError(w, http.StatusBadRequest, "body is required") return } if len(req.Body) > 4000 { writeError(w, http.StatusBadRequest, "body too long (max 4000)") return } msg, err := h.store.InsertMessage(r.Context(), convID, userID, req.Body) if err != nil { writeError(w, http.StatusInternalServerError, "internal server error") return } h.hub.PublishMessageCreated(convID, realtime.MessageCreated{ TempID: req.TempID, ConversationID: convID, MessageID: msg.ID, SenderID: userID, Body: msg.Body, CreatedAt: msg.CreatedAt, }) writeJSON(w, http.StatusCreated, toMessageJSON(msg)) }