173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
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))
|
|
}
|