- POST /api/v1/chats (ensure chat between users, normalized pair) - GET /api/v1/chats (list with last message + unread) - GET /api/v1/chats/:id/messages - POST /api/v1/chats/:id/messages (send text/photo, push via WS) - PUT /api/v1/chats/:id/read (mark all read) - PUT /api/v1/messages/:msgId (edit, owner only) - DELETE /api/v1/messages/:msgId (soft delete) - POST /api/v1/blocks, DELETE /api/v1/blocks/:id - POST /api/v1/reports - WS /ws?token= (real-time message push, ping/pong) - migrations 0003: chats, messages, blocks, reports
113 lines
2.6 KiB
Go
113 lines
2.6 KiB
Go
package ws
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gofiber/contrib/websocket"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Hub — WebSocket connection hub
|
|
type Hub struct {
|
|
mu sync.RWMutex
|
|
conns map[uuid.UUID]map[*websocket.Conn]struct{} // user_id -> set of conns
|
|
}
|
|
|
|
func NewHub() *Hub {
|
|
return &Hub{conns: make(map[uuid.UUID]map[*websocket.Conn]struct{})}
|
|
}
|
|
|
|
func (h *Hub) add(userID uuid.UUID, c *websocket.Conn) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if h.conns[userID] == nil {
|
|
h.conns[userID] = make(map[*websocket.Conn]struct{})
|
|
}
|
|
h.conns[userID][c] = struct{}{}
|
|
}
|
|
|
|
func (h *Hub) remove(userID uuid.UUID, c *websocket.Conn) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if set, ok := h.conns[userID]; ok {
|
|
delete(set, c)
|
|
if len(set) == 0 {
|
|
delete(h.conns, userID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *Hub) IsOnline(userID uuid.UUID) bool {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
_, ok := h.conns[userID]
|
|
return ok
|
|
}
|
|
|
|
// SendTo — отправить сообщение всем коннектам пользователя
|
|
func (h *Hub) SendTo(userID uuid.UUID, payload any) {
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
log.Printf("ws: marshal: %v", err)
|
|
return
|
|
}
|
|
h.mu.RLock()
|
|
conns := make([]*websocket.Conn, 0, 4)
|
|
for c := range h.conns[userID] {
|
|
conns = append(conns, c)
|
|
}
|
|
h.mu.RUnlock()
|
|
for _, c := range conns {
|
|
if err := c.WriteMessage(websocket.TextMessage, data); err != nil {
|
|
log.Printf("ws: write: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClientMessage — входящее от клиента
|
|
type ClientMessage struct {
|
|
Type string `json:"type"` // "ping", "typing", "read"
|
|
Payload json.RawMessage `json:"payload,omitempty"`
|
|
}
|
|
|
|
type Outgoing struct {
|
|
Type string `json:"type"`
|
|
Payload any `json:"payload"`
|
|
Time time.Time `json:"time"`
|
|
}
|
|
|
|
// Handle — обработчик WebSocket-соединения
|
|
func (h *Hub) Handle(userID uuid.UUID, c *websocket.Conn) {
|
|
h.add(userID, c)
|
|
defer func() {
|
|
h.remove(userID, c)
|
|
_ = c.Close()
|
|
}()
|
|
|
|
// уведомим контактов что онлайн? для MVP — пропустим
|
|
_ = h
|
|
|
|
// отправим pong при подключении
|
|
_ = c.WriteJSON(Outgoing{Type: "connected", Payload: map[string]any{"user_id": userID}, Time: time.Now()})
|
|
|
|
for {
|
|
_, msg, err := c.ReadMessage()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var cm ClientMessage
|
|
if err := json.Unmarshal(msg, &cm); err != nil {
|
|
continue
|
|
}
|
|
switch cm.Type {
|
|
case "ping":
|
|
_ = c.WriteJSON(Outgoing{Type: "pong", Time: time.Now()})
|
|
case "typing":
|
|
// можно broadcastить — но для MVP отправим обратно клиенту для подтверждения
|
|
}
|
|
}
|
|
}
|