- PUT /messages/:msgId now returns {ok, edited:true}
- WS pushes 'message_edited' event to both chat members
- Mobile UI: show 'ред.' marker when edited=true
- Audit log: message.edit action
- Repo: GetMessage helper for fetching after edit
360 lines
11 KiB
Go
360 lines
11 KiB
Go
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()})
|
|
}
|
|
|
|
_ = h.Audit.Log(c.UserContext(), &audit.Event{
|
|
UserID: &me, Action: "message.edit", TargetType: "message", TargetID: msgID.String(),
|
|
IP: c.IP(), UserAgent: c.Get("User-Agent"),
|
|
})
|
|
|
|
// Push WS — обоим участникам чата
|
|
m, err := h.Chat.GetMessage(c.UserContext(), msgID)
|
|
if err == nil && m != nil {
|
|
ch, _ := h.Chat.GetByID(c.UserContext(), m.ChatID)
|
|
if ch != nil {
|
|
other := ch.UserA
|
|
if other == me {
|
|
other = ch.UserB
|
|
}
|
|
for _, uid := range []uuid.UUID{me, other} {
|
|
h.Hub.SendTo(uid, ws.Outgoing{
|
|
Type: "message_edited",
|
|
Payload: fiber.Map{
|
|
"id": m.ID,
|
|
"chat_id": m.ChatID,
|
|
"body": m.Body,
|
|
"edited": m.Edited,
|
|
"updated_at": m.UpdatedAt,
|
|
},
|
|
Time: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true, "edited": 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})
|
|
}
|