package handlers import ( "strings" "time" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "github.com/buhapp/backend/internal/audit" "github.com/buhapp/backend/internal/chat" "github.com/buhapp/backend/internal/ws" ) type ChatHandlers struct { Chat *chat.Repo Audit *audit.Repo Hub *ws.Hub } func NewChatHandlers(c *chat.Repo, a *audit.Repo, hub *ws.Hub) *ChatHandlers { return &ChatHandlers{Chat: c, Audit: a, Hub: hub} } type ensureChatRequest struct { OtherID string `json:"other_id"` } func (h *ChatHandlers) EnsureChat(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } var req ensureChatRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"}) } other, err := uuid.Parse(req.OtherID) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad other_id"}) } if me == other { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "cannot chat with yourself"}) } blocked, err := h.Chat.IsBlockedEither(c.UserContext(), me, other) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } if blocked { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "user blocked"}) } chatObj, err := h.Chat.EnsureChat(c.UserContext(), me, other) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } return c.JSON(chatObj) } func (h *ChatHandlers) ListChats(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } limit := c.QueryInt("limit", 50) offset := c.QueryInt("offset", 0) items, err := h.Chat.ListChats(c.UserContext(), me, limit, offset) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } // mark online for i := range items { items[i].OtherID = items[i].OtherID // already set // online info } return c.JSON(fiber.Map{ "chats": items, "count": len(items), }) } type sendMessageRequest struct { Body string `json:"body"` PhotoURL string `json:"photo_url"` } func (h *ChatHandlers) SendMessage(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } chatID, err := uuid.Parse(c.Params("id")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad chat id"}) } var req sendMessageRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"}) } body := strings.TrimSpace(req.Body) photo := strings.TrimSpace(req.PhotoURL) if body == "" && photo == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body or photo_url required"}) } if len(body) > 4000 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body too long (max 4000)"}) } // Проверим что я участник чата ch, err := h.Chat.GetByID(c.UserContext(), chatID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } if ch == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "chat not found"}) } if ch.UserA != me && ch.UserB != me { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "not a chat member"}) } // Проверим блок other := ch.UserA if other == me { other = ch.UserB } blocked, _ := h.Chat.IsBlockedEither(c.UserContext(), me, other) if blocked { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "user blocked"}) } m, err := h.Chat.Send(c.UserContext(), chatID, me, body, photo) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } _ = h.Audit.Log(c.UserContext(), &audit.Event{ UserID: &me, Action: "message.send", TargetType: "chat", TargetID: chatID.String(), IP: c.IP(), UserAgent: c.Get("User-Agent"), }) // push to recipient via WebSocket h.Hub.SendTo(other, ws.Outgoing{ Type: "message", Payload: fiber.Map{ "id": m.ID, "chat_id": m.ChatID, "sender_id": m.SenderID, "body": m.Body, "photo_url": m.PhotoURL, "created_at": m.CreatedAt, }, Time: time.Now(), }) // echo to sender too h.Hub.SendTo(me, ws.Outgoing{ Type: "message", Payload: fiber.Map{ "id": m.ID, "chat_id": m.ChatID, "sender_id": m.SenderID, "body": m.Body, "photo_url": m.PhotoURL, "created_at": m.CreatedAt, }, Time: time.Now(), }) return c.Status(fiber.StatusCreated).JSON(m) } func (h *ChatHandlers) ListMessages(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } chatID, err := uuid.Parse(c.Params("id")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad chat id"}) } ch, err := h.Chat.GetByID(c.UserContext(), chatID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } if ch == nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "chat not found"}) } if ch.UserA != me && ch.UserB != me { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "not a chat member"}) } limit := c.QueryInt("limit", 50) offset := c.QueryInt("offset", 0) msgs, err := h.Chat.ListMessages(c.UserContext(), chatID, limit, offset) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } return c.JSON(fiber.Map{"messages": msgs, "count": len(msgs)}) } type editRequest struct { Body string `json:"body"` } func (h *ChatHandlers) EditMessage(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } msgID, err := uuid.Parse(c.Params("msgId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad msg id"}) } var req editRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"}) } body := strings.TrimSpace(req.Body) if body == "" || len(body) > 4000 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body required, max 4000"}) } if err := h.Chat.EditMessage(c.UserContext(), msgID, me, body); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func (h *ChatHandlers) DeleteMessage(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } msgID, err := uuid.Parse(c.Params("msgId")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad msg id"}) } if err := h.Chat.DeleteMessage(c.UserContext(), msgID, me); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func (h *ChatHandlers) MarkRead(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } chatID, err := uuid.Parse(c.Params("id")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad chat id"}) } if err := h.Chat.MarkRead(c.UserContext(), chatID, me); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } return c.JSON(fiber.Map{"ok": true}) } type blockRequest struct { UserID string `json:"user_id"` } func (h *ChatHandlers) Block(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } var req blockRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"}) } other, err := uuid.Parse(req.UserID) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad user_id"}) } if err := h.Chat.Block(c.UserContext(), me, other); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } _ = h.Audit.Log(c.UserContext(), &audit.Event{ UserID: &me, Action: "user.block", TargetType: "user", TargetID: other.String(), IP: c.IP(), UserAgent: c.Get("User-Agent"), }) return c.JSON(fiber.Map{"ok": true}) } func (h *ChatHandlers) Unblock(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } other, err := uuid.Parse(c.Params("id")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad id"}) } if err := h.Chat.Unblock(c.UserContext(), me, other); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } return c.JSON(fiber.Map{"ok": true}) } type reportRequest struct { TargetType string `json:"target_type"` TargetID string `json:"target_id"` Reason string `json:"reason"` } func (h *ChatHandlers) Report(c *fiber.Ctx) error { me, err := userID(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"}) } var req reportRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"}) } if req.TargetType != "user" && req.TargetType != "message" && req.TargetType != "chat" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad target_type"}) } if req.TargetID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "target_id required"}) } if err := h.Chat.AddReport(c.UserContext(), &chat.Report{ ReporterID: me, TargetType: req.TargetType, TargetID: req.TargetID, Reason: req.Reason, }); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"}) } _ = h.Audit.Log(c.UserContext(), &audit.Event{ UserID: &me, Action: "report.create", TargetType: req.TargetType, TargetID: req.TargetID, IP: c.IP(), UserAgent: c.Get("User-Agent"), }) return c.JSON(fiber.Map{"ok": true}) }