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