MWSE/internal/services/room.go

297 lines
8.5 KiB
Go

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 ""
}