First pass at adding a waiting room.

This commit is contained in:
Storm Dragon
2026-08-30 16:56:26 -04:00
parent 9125e76d43
commit ae55982a40
17 changed files with 1254 additions and 100 deletions
+330
View File
@@ -0,0 +1,330 @@
package rtpconn
import (
"errors"
"sync"
"time"
"git.stormux.org/storm/skald/hall"
)
type waitingEntry struct {
client *webClient
hall *hall.Hall
auth hall.AuthenticatedClient
autoJoin bool
approved bool
manualJoin bool
}
type waitingEventAction struct {
message clientMessage
}
type waitingState struct {
Hall string `json:"hall"`
DisplayName string `json:"displayName"`
Username string `json:"username"`
Locked bool `json:"locked"`
Deadline string `json:"deadline,omitempty"`
}
type waitingUser struct {
Username string `json:"username"`
Approved bool `json:"approved,omitempty"`
}
var waitingRooms = struct {
sync.Mutex
entries map[*hall.Hall][]*waitingEntry
}{
entries: make(map[*hall.Hall][]*waitingEntry),
}
var errWaitingUnavailable = errors.New("waiting room unavailable")
func waitingDisplayName(g *hall.Hall) string {
name := g.Status(true, nil).DisplayName
if name == "" {
name = g.Name()
}
return name
}
func currentWaitingState(g *hall.Hall, username string) waitingState {
locked, _ := g.Locked()
state := waitingState{
Hall: g.Name(), DisplayName: waitingDisplayName(g),
Username: username, Locked: locked,
}
if deadline, ok := scheduledUnlockDeadline(g); ok {
state.Deadline = deadline.Format(time.RFC3339)
}
return state
}
func sendWaiting(c *webClient, kind string, value interface{}) {
c.action(waitingEventAction{clientMessage{
Type: "waiting", Kind: kind, Value: value,
}})
}
func sendWaitingList(c *webClient, kind string, value interface{}) {
c.action(waitingEventAction{clientMessage{
Type: "waiting-list", Kind: kind, Value: value,
}})
}
func waitingSnapshotLocked(g *hall.Hall) []waitingUser {
entries := waitingRooms.entries[g]
users := make([]waitingUser, 0, len(entries))
for _, entry := range entries {
users = append(users, waitingUser{entry.auth.Username, entry.approved})
}
return users
}
func waitingClientHall(c *webClient) *hall.Hall {
waitingRooms.Lock()
defer waitingRooms.Unlock()
if c.waiting == nil {
return nil
}
return c.waiting.hall
}
func notifyWaitingOperators(g *hall.Hall, kind string, value interface{}) {
for _, client := range g.GetClients(nil) {
c, ok := client.(*webClient)
if ok && member("op", c.Permissions()) {
sendWaitingList(c, kind, value)
}
}
}
func addWaitingClient(c *webClient, g *hall.Hall, auth hall.AuthenticatedClient) error {
waitingRooms.Lock()
offered, err := g.ReserveUsernameWhileWaiting(auth.Username, c)
if err != nil {
waitingRooms.Unlock()
return err
}
if !offered {
waitingRooms.Unlock()
return errWaitingUnavailable
}
entry := &waitingEntry{client: c, hall: g, auth: auth}
waitingRooms.entries[g] = append(waitingRooms.entries[g], entry)
c.waiting = entry
c.SetUsername(auth.Username)
sendWaiting(c, "enter", currentWaitingState(g, auth.Username))
notifyWaitingOperators(g, "add", waitingUser{Username: auth.Username})
waitingRooms.Unlock()
return nil
}
func removeWaitingClient(c *webClient, notify bool) bool {
waitingRooms.Lock()
entry := c.waiting
if entry == nil {
waitingRooms.Unlock()
return false
}
removeWaitingEntryLocked(entry)
if notify {
notifyWaitingOperators(entry.hall, "remove", waitingUser{Username: entry.auth.Username})
}
waitingRooms.Unlock()
return true
}
func removeWaitingEntryLocked(entry *waitingEntry) {
entries := waitingRooms.entries[entry.hall]
for i, candidate := range entries {
if candidate == entry {
entries = append(entries[:i], entries[i+1:]...)
break
}
}
if len(entries) == 0 {
delete(waitingRooms.entries, entry.hall)
} else {
waitingRooms.entries[entry.hall] = entries
}
entry.client.waiting = nil
entry.hall.ReleaseUsername(entry.auth.Username, entry.client)
}
func cancelWaiting(g *hall.Hall, message string) {
waitingRooms.Lock()
cancelWaitingLocked(g, message)
waitingRooms.Unlock()
}
func cancelWaitingLocked(g *hall.Hall, message string) {
entries := append([]*waitingEntry(nil), waitingRooms.entries[g]...)
delete(waitingRooms.entries, g)
for _, entry := range entries {
entry.client.waiting = nil
entry.hall.ReleaseUsername(entry.auth.Username, entry.client)
entry.client.SetPermissions(nil)
entry.client.SetUsername("")
sendWaiting(entry.client, "cancel", message)
}
}
func waitingOperatorJoined(g *hall.Hall, c *webClient) {
if !member("op", c.Permissions()) {
return
}
waitingRooms.Lock()
snapshot := waitingSnapshotLocked(g)
sendWaitingList(c, "snapshot", snapshot)
waitingRooms.Unlock()
}
func waitingHallChanged(g *hall.Hall) {
waitingRooms.Lock()
locked, hasOperator := g.WaitingStatus()
if locked && !hasOperator {
cancelWaitingLocked(g, "Waiting ended because the last operator left the hall.")
waitingRooms.Unlock()
return
}
entries := waitingRooms.entries[g]
if locked {
for _, entry := range entries {
entry.manualJoin = false
}
}
kind := "update"
if !locked {
kind = "open"
}
for _, entry := range entries {
sendWaiting(entry.client, kind, currentWaitingState(g, entry.auth.Username))
}
waitingRooms.Unlock()
processWaiting(g)
}
func processWaiting(g *hall.Hall) {
for {
waitingRooms.Lock()
entries := waitingRooms.entries[g]
locked, hasOperator := g.WaitingStatus()
if locked && !hasOperator {
cancelWaitingLocked(g, "Waiting ended because the last operator left the hall.")
waitingRooms.Unlock()
return
}
var entry *waitingEntry
for _, candidate := range entries {
if candidate.approved || (!locked && (candidate.autoJoin || candidate.manualJoin)) {
entry = candidate
break
}
}
if entry == nil {
waitingRooms.Unlock()
return
}
// Publish the target hall before AddAuthenticatedClient enqueues its
// joined callback; admission may run on a different client's goroutine.
entry.client.hall = g
err := hall.AddAuthenticatedClient(g, entry.client, entry.auth, entry.approved)
if err != nil {
entry.client.hall = nil
if lockedNow, _ := g.Locked(); lockedNow && !entry.approved {
entry.manualJoin = false
waitingRooms.Unlock()
sendWaiting(entry.client, "update", currentWaitingState(g, entry.auth.Username))
return
}
if errors.Is(err, hall.UserError("too many users")) || err.Error() == "too many users" {
waitingRooms.Unlock()
return
}
removeWaitingEntryLocked(entry)
entry.client.SetPermissions(nil)
entry.client.SetUsername("")
notifyWaitingOperators(g, "remove", waitingUser{Username: entry.auth.Username})
waitingRooms.Unlock()
sendWaiting(entry.client, "cancel", "Admission cannot continue: "+err.Error())
continue
}
removeWaitingEntryLocked(entry)
notifyWaitingOperators(g, "remove", waitingUser{Username: entry.auth.Username})
waitingRooms.Unlock()
waitingOperatorJoined(g, entry.client)
}
}
// WaitingCapacityChanged retries pending admissions after a non-WebSocket
// client leaves the hall.
func WaitingCapacityChanged(g *hall.Hall) {
processWaiting(g)
}
func updateWaitingPreference(c *webClient, autoJoin bool) error {
waitingRooms.Lock()
if c.waiting == nil {
waitingRooms.Unlock()
return hall.UserError("you are not in a waiting room")
}
c.waiting.autoJoin = autoJoin
g := c.waiting.hall
waitingRooms.Unlock()
if autoJoin {
processWaiting(g)
}
return nil
}
func requestWaitingAdmission(c *webClient) error {
waitingRooms.Lock()
if c.waiting == nil {
waitingRooms.Unlock()
return hall.UserError("you are not in a waiting room")
}
g := c.waiting.hall
waitingRooms.Unlock()
locked, _ := g.Locked()
if locked {
return hall.UserError("the hall is still locked")
}
waitingRooms.Lock()
if c.waiting != nil {
c.waiting.manualJoin = true
}
waitingRooms.Unlock()
processWaiting(g)
return nil
}
func admitWaitingClient(operator *webClient, username string) error {
if operator.hall == nil || !member("op", operator.Permissions()) {
return hall.UserError("not authorised")
}
g := operator.hall
waitingRooms.Lock()
var found *waitingEntry
for _, entry := range waitingRooms.entries[g] {
if entry.auth.Username == username {
entry.approved = true
found = entry
break
}
}
snapshot := waitingSnapshotLocked(g)
if found != nil {
notifyWaitingOperators(g, "snapshot", snapshot)
}
waitingRooms.Unlock()
if found == nil {
return hall.UserError("no such waiting user")
}
sendWaiting(found.client, "approved", currentWaitingState(g, username))
processWaiting(g)
return nil
}