package services import ( "git.saqut.com/saqut/mwse/internal/protocol" "git.saqut.com/saqut/mwse/internal/ws" ) func registerRoom(hub *ws.Hub) { // Every client gets a private room named after its own id on connect, and is // ejected from all rooms on disconnect. hub.OnConnect(func(c *ws.Client) { room := ws.NewRoom(hub) room.ID = c.ID room.AccessType = "private" room.JoinType = "notify" room.Description = "Private room" room.Name = "Your Room | " + c.ID room.OwnerID = c.ID room.Publish() room.Join(c) }) hub.OnDisconnect(func(c *ws.Client) { if room, ok := hub.Room(c.ID); ok { room.Eject(c) } for _, rid := range c.Rooms() { if r, ok := hub.Room(rid); ok { r.Eject(c) } } // Clear any pending invites so a room's waiting list cannot retain the id // of a client that has gone away. for _, rid := range c.WaitingRooms() { if r, ok := hub.Room(rid); ok { r.RemoveWaiting(c.ID) } } }) hub.Register("myroom-info", func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(c.ID) if !ok { return fail("NOT-FOUND-ROOM") } return map[string]any{"status": "success", "room": room.ToJSON(false)} }) hub.Register("room-peers", func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(m.Str("roomId")) if !ok { return map[string]any{"status": "fail"} } return map[string]any{"status": "success", "peers": ids(room.FilterPeers(toMap(m.Get("filter"))))} }) hub.Register("room/peer-count", func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(m.Str("roomId")) if !ok { return map[string]any{"status": "fail"} } return map[string]any{"status": "success", "count": len(room.FilterPeers(toMap(m.Get("filter"))))} }) hub.Register("room-info", func(c *ws.Client, m protocol.Message) any { if room, ok := hub.RoomByName(m.Str("name")); ok { return map[string]any{"status": "success", "room": room.ToJSON(false)} } return fail("NOT-FOUND-ROOM") }) hub.Register("joinedrooms", func(c *ws.Client, m protocol.Message) any { var rooms []map[string]any for _, rid := range c.Rooms() { if r, ok := hub.Room(rid); ok { rooms = append(rooms, r.ToJSON(false)) } } return rooms }) hub.Register("closeroom", func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(m.Str("roomId")) if !ok { return map[string]any{"status": "fail"} } if room.OwnerID == c.ID { room.Down() return success() } return map[string]any{"status": "fail"} }) hub.Register("create-room", func(c *ws.Client, m protocol.Message) any { if msg := validateCreateRoom(m); msg != "" { return map[string]any{"status": "fail", "messages": msg} } name := m.Str("name") if existing, exists := hub.RoomByName(name); exists { // ifexistsJoin: instead of failing on a name clash, join the existing // room (so "create or join" is a single round trip). if m.Truthy("ifexistsJoin") { existing.Join(c) return map[string]any{"status": "success", "room": existing.ToJSON(false)} } return fail("ALREADY-EXISTS") } room := ws.NewRoom(hub) room.AccessType = m.Str("accessType") room.NotifyActionInvite = m.Truthy("notifyActionInvite") room.NotifyActionJoined = m.Truthy("notifyActionJoined") room.NotifyActionEjected = m.Truthy("notifyActionEjected") room.JoinType = m.Str("joinType") room.Description = m.Str("description") room.Name = name room.OwnerID = c.ID if cred := m.Str("credential"); cred != "" { room.Credential = sha256hex(cred) } room.Publish() room.Join(c) return map[string]any{"status": "success", "room": room.ToJSON(false)} }) hub.Register("joinroom", func(c *ws.Client, m protocol.Message) any { room, ok := hub.RoomByName(m.Str("name")) if !ok { return fail("NOT-FOUND-ROOM") } fetchInfo := func(resp map[string]any) map[string]any { if m.Truthy("autoFetchInfo") { resp["info"] = room.Info() } return resp } switch room.JoinType { case "lock": return fail("LOCKED-ROOM") case "password": if room.Credential == sha256hex(m.Str("credential")) { room.Join(c) return fetchInfo(map[string]any{"status": "success", "room": room.ToJSON(false)}) } return map[string]any{"status": "fail", "message": "WRONG-PASSWORD", "area": "credential"} case "free": room.Join(c) return fetchInfo(map[string]any{"status": "success", "room": room.ToJSON(false)}) case "invite": room.AddWaiting(c.ID) c.AddWaitingRoom(room.ID) invite := map[string]any{"id": c.ID} if room.NotifyActionInvite { room.Broadcast("room/invite", invite, "", nil) } else if owner, ok := hub.Client(room.OwnerID); ok { owner.Signal("room/invite", invite) } return map[string]any{"status": "success", "message": "INVITE-REQUESTED"} } return fail("NOT-FOUND-ROOM") }) hub.Register("ejectroom", func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(m.Str("roomId")) if !ok { return fail("NOT-FOUND-ROOM") } if !room.Has(c.ID) { return fail("ALREADY-ROOM-OUT") } room.Eject(c) return success() }) hub.Register("accept/invite-room", inviteDecision(hub, true)) hub.Register("reject/invite-room", inviteDecision(hub, false)) hub.Register("room/list", func(c *ws.Client, m protocol.Message) any { var rooms []map[string]any for _, room := range hub.Rooms() { if room.AccessType == "public" { rooms = append(rooms, map[string]any{ "name": room.Name, "joinType": room.JoinType, "description": room.Description, "id": room.ID, }) } } return map[string]any{"type": "public/rooms", "rooms": rooms} }) hub.Register("room/info", func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(m.Str("roomId")) if !ok { return fail("NOT-FOUND-ROOM") } if !c.InRoom(room.ID) { return fail("NO-JOINED-ROOM") } if name := m.Str("name"); name != "" { v, _ := room.InfoValue(name) return map[string]any{"status": "success", "value": v} } return map[string]any{"status": "success", "value": room.Info()} }) hub.Register("room/setinfo", func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(m.Str("roomId")) if !ok { return fail("NOT-FOUND-ROOM") } if !c.InRoom(room.ID) { return fail("NO-JOINED-ROOM") } name := m.Str("name") value := m.Get("value") room.SetInfo(name, value) room.Broadcast( "room/info", map[string]any{"name": name, "value": value, "roomId": room.ID}, c.ID, (*ws.Client).RoomInfoNotifiable, ) return success() }) } // inviteDecision builds the accept/reject invite handlers. The original code was // non-functional here (it called Array methods on a Set and inverted the joinType // check); this implements the intended flow: only the rooms a member belongs to, // only invite rooms, only ids actually on the waiting list. func inviteDecision(hub *ws.Hub, accept bool) handler { return func(c *ws.Client, m protocol.Message) any { room, ok := hub.Room(m.Str("roomId")) if !ok { return fail("NOT-FOUND-ROOM") } if !c.InRoom(room.ID) { return fail("FORBIDDEN-INVITE-ACTIONS") } if room.JoinType != "invite" { return fail("INVALID-DATA") } clientID := m.Str("clientId") if !room.IsWaiting(clientID) { return fail("NO-WAITING-INVITED") } joinClient, ok := hub.Client(clientID) if !ok { room.RemoveWaiting(clientID) return fail("NO-CLIENT") } room.RemoveWaiting(clientID) joinClient.RemoveWaitingRoom(room.ID) if accept { room.Join(joinClient) joinClient.Signal("room/invite/status", map[string]any{"status": "accepted"}) } else { room.Broadcast("room/invite/status", map[string]any{"id": clientID, "roomId": room.ID}, "", nil) joinClient.Signal("room/invite/status", map[string]any{"status": "rejected"}) } return success() } } // validateCreateRoom checks the create-room payload against the same constraints // the original joi schema described. It returns an empty string when valid. func validateCreateRoom(m protocol.Message) string { if !m.Has("type") { return "type is required" } switch m.Str("accessType") { case "public", "private": default: return "accessType must be public or private" } switch m.Str("joinType") { case "free", "invite", "password", "lock": default: return "joinType must be one of free, invite, password, lock" } if !m.Has("notifyActionInvite") || !m.Has("notifyActionJoined") || !m.Has("notifyActionEjected") { return "notify flags are required" } if m.Str("description") == "" { return "description is required" } if m.Str("name") == "" { return "name is required" } return "" }