buhapp-backend/internal/ws/hub.go
ga 4533ceaf2d Sprint 3: chat with WebSocket
- 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
2026-08-20 15:51:05 +00:00

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 отправим обратно клиенту для подтверждения
}
}
}