254 lines
6.3 KiB
Go
254 lines
6.3 KiB
Go
package ws
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Room is a set of clients that can be addressed together. Access types and join
|
|
// types mirror the original service.
|
|
type Room struct {
|
|
ID string
|
|
Name string
|
|
Description string
|
|
OwnerID string
|
|
CreatedAt time.Time
|
|
AccessType string // "public" | "private"
|
|
JoinType string // "free" | "invite" | "password" | "lock" | "notify"
|
|
|
|
NotifyActionInvite bool
|
|
NotifyActionJoined bool
|
|
NotifyActionEjected bool
|
|
|
|
Credential string // sha256 hex, or "" when none
|
|
|
|
hub *Hub
|
|
|
|
mu sync.RWMutex
|
|
clients map[string]*Client
|
|
waitingInvited map[string]struct{}
|
|
info map[string]any
|
|
}
|
|
|
|
// NewRoom creates an empty, unpublished room with a fresh id and the same field
|
|
// defaults the Node constructor used.
|
|
func NewRoom(hub *Hub) *Room {
|
|
return &Room{
|
|
ID: newUUID(),
|
|
CreatedAt: time.Now(),
|
|
JoinType: "invite",
|
|
NotifyActionJoined: true,
|
|
NotifyActionEjected: true,
|
|
hub: hub,
|
|
clients: make(map[string]*Client),
|
|
waitingInvited: make(map[string]struct{}),
|
|
info: make(map[string]any),
|
|
}
|
|
}
|
|
|
|
// Publish registers the room in the hub so it can be looked up by id.
|
|
func (r *Room) Publish() { r.hub.addRoom(r) }
|
|
|
|
// Size returns the current member count.
|
|
func (r *Room) Size() int {
|
|
r.mu.RLock()
|
|
n := len(r.clients)
|
|
r.mu.RUnlock()
|
|
return n
|
|
}
|
|
|
|
// Has reports membership by client id.
|
|
func (r *Room) Has(id string) bool {
|
|
r.mu.RLock()
|
|
_, ok := r.clients[id]
|
|
r.mu.RUnlock()
|
|
return ok
|
|
}
|
|
|
|
// snapshot returns the current members as a slice. The room lock is held only for
|
|
// the cheap copy; callers then send without holding any lock, so a member that
|
|
// disconnects mid-broadcast cannot deadlock or race the send (Client.Send drops
|
|
// safely once the peer is closing).
|
|
func (r *Room) snapshot() []*Client {
|
|
r.mu.RLock()
|
|
out := make([]*Client, 0, len(r.clients))
|
|
for _, c := range r.clients {
|
|
out = append(out, c)
|
|
}
|
|
r.mu.RUnlock()
|
|
return out
|
|
}
|
|
|
|
// Broadcast sends signal `name` with `payload` to every member except exceptID
|
|
// (pass "" to include everyone). If filter is non-nil, only members for which it
|
|
// returns true receive the message.
|
|
func (r *Room) Broadcast(name string, payload any, exceptID string, filter func(*Client) bool) {
|
|
for _, c := range r.snapshot() {
|
|
if c.ID == exceptID {
|
|
continue
|
|
}
|
|
if filter != nil && !filter(c) {
|
|
continue
|
|
}
|
|
c.Signal(name, payload)
|
|
}
|
|
}
|
|
|
|
// Members returns a snapshot of the room's current members.
|
|
func (r *Room) Members() []*Client { return r.snapshot() }
|
|
|
|
// FilterPeers returns the members whose info matches filter.
|
|
func (r *Room) FilterPeers(filter map[string]any) []*Client {
|
|
var out []*Client
|
|
for _, c := range r.snapshot() {
|
|
if c.Match(filter) {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Join adds a client to the room. Existing members are notified first (so the new
|
|
// member does not receive its own join), then membership is recorded on both the
|
|
// room and the client.
|
|
func (r *Room) Join(c *Client) {
|
|
if r.NotifyActionJoined {
|
|
r.Broadcast(
|
|
"room/joined",
|
|
map[string]any{"id": c.ID, "roomid": r.ID, "ownerid": r.OwnerID},
|
|
"",
|
|
(*Client).PeerInfoNotifiable,
|
|
)
|
|
}
|
|
r.mu.Lock()
|
|
r.clients[c.ID] = c
|
|
r.mu.Unlock()
|
|
c.addRoom(r.ID)
|
|
}
|
|
|
|
// Eject removes a client from the room, notifying the remaining members. When the
|
|
// room empties it is taken down.
|
|
func (r *Room) Eject(c *Client) {
|
|
if r.NotifyActionEjected {
|
|
r.Broadcast(
|
|
"room/ejected",
|
|
map[string]any{"id": c.ID, "roomid": r.ID, "ownerid": r.OwnerID},
|
|
c.ID,
|
|
(*Client).PeerInfoNotifiable,
|
|
)
|
|
}
|
|
c.removeRoom(r.ID)
|
|
r.mu.Lock()
|
|
delete(r.clients, c.ID)
|
|
empty := len(r.clients) == 0
|
|
r.mu.Unlock()
|
|
|
|
if empty {
|
|
r.Down()
|
|
}
|
|
}
|
|
|
|
// Down closes the room: members are told, the room is unregistered, and each
|
|
// member's membership record is cleared.
|
|
func (r *Room) Down() {
|
|
members := r.snapshot()
|
|
for _, c := range members {
|
|
c.Signal("room/closed", map[string]any{"roomid": r.ID, "ownerid": r.OwnerID})
|
|
c.removeRoom(r.ID)
|
|
}
|
|
r.hub.removeRoom(r.ID)
|
|
}
|
|
|
|
// ---- room info -----------------------------------------------------------
|
|
|
|
// SetInfo stores a room-level value.
|
|
func (r *Room) SetInfo(name string, value any) {
|
|
r.mu.Lock()
|
|
r.info[name] = value
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
// InfoValue returns a single room value.
|
|
func (r *Room) InfoValue(name string) (any, bool) {
|
|
r.mu.RLock()
|
|
v, ok := r.info[name]
|
|
r.mu.RUnlock()
|
|
return v, ok
|
|
}
|
|
|
|
// Info returns a copy of all room values.
|
|
func (r *Room) Info() map[string]any {
|
|
r.mu.RLock()
|
|
out := make(map[string]any, len(r.info))
|
|
for k, v := range r.info {
|
|
out[k] = v
|
|
}
|
|
r.mu.RUnlock()
|
|
return out
|
|
}
|
|
|
|
// ---- invitations ---------------------------------------------------------
|
|
|
|
// AddWaiting records a client awaiting an invite decision.
|
|
func (r *Room) AddWaiting(id string) {
|
|
r.mu.Lock()
|
|
r.waitingInvited[id] = struct{}{}
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
// RemoveWaiting drops a client from the invite waiting list.
|
|
func (r *Room) RemoveWaiting(id string) {
|
|
r.mu.Lock()
|
|
delete(r.waitingInvited, id)
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
// IsWaiting reports whether a client is on the invite waiting list.
|
|
func (r *Room) IsWaiting(id string) bool {
|
|
r.mu.RLock()
|
|
_, ok := r.waitingInvited[id]
|
|
r.mu.RUnlock()
|
|
return ok
|
|
}
|
|
|
|
func (r *Room) waitingList() []string {
|
|
r.mu.RLock()
|
|
out := make([]string, 0, len(r.waitingInvited))
|
|
for id := range r.waitingInvited {
|
|
out = append(out, id)
|
|
}
|
|
r.mu.RUnlock()
|
|
return out
|
|
}
|
|
|
|
// ---- serialization -------------------------------------------------------
|
|
|
|
// ToJSON renders the room the way the SDK expects. When detailed is true the
|
|
// sensitive/owner-only fields are included.
|
|
func (r *Room) ToJSON(detailed bool) map[string]any {
|
|
obj := map[string]any{
|
|
"id": r.ID,
|
|
"accessType": r.AccessType,
|
|
"createdAt": r.CreatedAt,
|
|
"description": r.Description,
|
|
"joinType": r.JoinType,
|
|
"name": r.Name,
|
|
"owner": r.OwnerID,
|
|
"waitingInvited": r.waitingList(),
|
|
}
|
|
if detailed {
|
|
obj["credential"] = r.Credential
|
|
obj["notifyActionInvite"] = r.NotifyActionInvite
|
|
obj["notifyActionJoined"] = r.NotifyActionJoined
|
|
obj["notifyActionEjected"] = r.NotifyActionEjected
|
|
r.mu.RLock()
|
|
ids := make([]string, 0, len(r.clients))
|
|
for id := range r.clients {
|
|
ids = append(ids, id)
|
|
}
|
|
r.mu.RUnlock()
|
|
obj["clients"] = ids
|
|
}
|
|
return obj
|
|
}
|