From d0f78b1295ad0ce33b3a47c078476005e25d64e0 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 30 Aug 2026 21:27:21 -0400 Subject: [PATCH] Fix bugs in waiting room. --- hall/hall.go | 4 +++- rtpconn/unlock_test.go | 30 ++++++++++++++++++++++++++ rtpconn/waiting_test.go | 22 +++++++++++++++++++ rtpconn/webclient.go | 2 +- skald-protocol.md | 1 + static/skald.js | 2 ++ webserver/static_accessibility_test.go | 2 ++ 7 files changed, 61 insertions(+), 2 deletions(-) diff --git a/hall/hall.go b/hall/hall.go index 494d293..20d4572 100644 --- a/hall/hall.go +++ b/hall/hall.go @@ -1201,6 +1201,7 @@ type Status struct { AuthServer string `json:"authServer,omitempty"` AuthPortal string `json:"authPortal,omitempty"` Locked bool `json:"locked,omitempty"` + WaitingRoom bool `json:"waitingRoom,omitempty"` Recording bool `json:"recording,omitempty"` ClientCount *int `json:"clientCount,omitempty"` CanChangePassword bool `json:"canChangePassword,omitempty"` @@ -1255,9 +1256,10 @@ func (g *Hall) Status(authentified bool, base *url.URL) Status { if authentified || desc.Public { // these are considered private information - locked, _ := g.Locked() + locked, hasOperator := g.WaitingStatus() count := g.ClientCount() d.Locked = locked + d.WaitingRoom = locked && hasOperator d.ClientCount = &count } if authentified { diff --git a/rtpconn/unlock_test.go b/rtpconn/unlock_test.go index 7953565..16d937b 100644 --- a/rtpconn/unlock_test.go +++ b/rtpconn/unlock_test.go @@ -296,6 +296,36 @@ func TestScheduledUnlockCompletesWhileIssuerIsOperator(t *testing.T) { } } +func TestScheduledUnlockTimerCompletesWhileIssuerIsOperator(t *testing.T) { + g, hallName := newWaitingTestHall(t, 0) + operator := addTestWebClient(t, hallName, "Operator", "op") + g.SetLocked(true, "") + waiter := joinWaitingTestClient(t, hallName, "Alice") + if err := updateWaitingPreference(waiter, true); err != nil { + t.Fatalf("enable automatic waiting-room admission: %v", err) + } + + now := time.Now() + if err := scheduleHallUnlock(g, operator, now.Add(20*time.Millisecond), now); err != nil { + t.Fatalf("scheduleHallUnlock: %v", err) + } + deadline := time.Now().Add(time.Second) + for { + locked, _ := g.Locked() + if !locked && waitingClientHall(waiter) == nil { + break + } + if time.Now().After(deadline) { + t.Fatal("scheduled timer left hall locked while issuing operator was present") + } + time.Sleep(time.Millisecond) + } + drainActions(t, waiter) + if waiter.hall != g || waiter.waiting != nil { + t.Fatalf("waiting user was not admitted after scheduled unlock: hall=%v waiting=%v", waiter.hall, waiter.waiting) + } +} + func TestScheduledUnlockRequiresOperatorPermissionAtDeadline(t *testing.T) { hallName := newChalkboardTestHall(t) operator := addTestWebClient(t, hallName, "Operator", "op") diff --git a/rtpconn/waiting_test.go b/rtpconn/waiting_test.go index 6935cb1..ea9e3df 100644 --- a/rtpconn/waiting_test.go +++ b/rtpconn/waiting_test.go @@ -70,6 +70,9 @@ func TestWaitingClientAuthenticatedWithoutMembership(t *testing.T) { drainActions(t, operator) waiter := joinWaitingTestClient(t, name, "Alice") + if status := g.Status(true, nil); !status.WaitingRoom { + t.Fatalf("locked hall with operator did not report waiting-room availability: %#v", status) + } if waiter.hall != nil || waiter.waiting == nil { t.Fatalf("waiting client membership: hall=%v waiting=%v", waiter.hall, waiter.waiting) } @@ -108,6 +111,25 @@ func TestWaitingClientAuthenticatedWithoutMembership(t *testing.T) { } } +func TestWaitingClientLivenessMessagesDoNotReportAdmissionError(t *testing.T) { + g, name := newWaitingTestHall(t, 0) + operator := addTestWebClient(t, name, "Operator", "op") + g.SetLocked(true, "") + drainActions(t, operator) + + waiter := joinWaitingTestClient(t, name, "Alice") + for _, messageType := range []string{"ping", "pong"} { + if err := handleClientMessage(waiter, clientMessage{Type: messageType}); err != nil { + t.Fatalf("waiting %s: %v", messageType, err) + } + } + for _, message := range drainMessages(waiter) { + if message.Type == "usermessage" && message.Kind == "error" { + t.Fatalf("waiting liveness message produced error: %#v", message) + } + } +} + func TestWaitingManualAdmissionAndLastOperatorCancellation(t *testing.T) { g, name := newWaitingTestHall(t, 0) operator := addTestWebClient(t, name, "Operator", "op") diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go index dde8524..2117c9b 100644 --- a/rtpconn/webclient.go +++ b/rtpconn/webclient.go @@ -1467,7 +1467,7 @@ func handleClientMessage(c *webClient, m clientMessage) error { } } } - if waitingClientHall(c) != nil && m.Type != "waiting" && m.Type != "join" && m.Type != "pong" { + if waitingClientHall(c) != nil && m.Type != "waiting" && m.Type != "join" && m.Type != "ping" && m.Type != "pong" { return c.error(hall.UserError("you have not been admitted to the hall")) } diff --git a/skald-protocol.md b/skald-protocol.md index 37142ac..b6354af 100644 --- a/skald-protocol.md +++ b/skald-protocol.md @@ -49,6 +49,7 @@ the following fields: - `authServer`: the URL of the authentication server, if any; - `authPortal`: the uRL of the authentication portal, if any; - `locked`: true if the hall is locked; + - `waitingRoom`: true if the hall is locked and its waiting room is available; - `clientCount`: the number of clients currently in the hall. All fields are optional except `name`, `location` and `endpoint`. diff --git a/static/skald.js b/static/skald.js index 42133cb..9a22bd2 100644 --- a/static/skald.js +++ b/static/skald.js @@ -4386,6 +4386,8 @@ async function start() { if(window.location.search) window.history.replaceState(null, '', window.location.pathname); setTitle(hallStatus.displayName || capitalise(hall)); + getInputElement('connectbutton').value = hallStatus.waitingRoom ? + 'Join waiting room' : 'Connect'; await setMediaChoices(false); reflectSettings(); diff --git a/webserver/static_accessibility_test.go b/webserver/static_accessibility_test.go index 79ae95f..8d9750e 100644 --- a/webserver/static_accessibility_test.go +++ b/webserver/static_accessibility_test.go @@ -156,6 +156,8 @@ func TestWaitingRoomUsesAccessibleNativeControlsAndLiveRegions(t *testing.T) { requireContains(t, html, `id="waiting-leave" type="button"`, "leave button") requireContains(t, html, `aria-labelledby="waiting-list-heading"`, "operator waiting list") requireFunctionContains(t, js, "showWaitingRoom", "waiting-heading').focus") + requireContains(t, js, "hallStatus.waitingRoom ?", "waiting-room submit label condition") + requireContains(t, js, "'Join waiting room' : 'Connect'", "waiting-room submit label") requireFunctionContains(t, js, "updateWaitingStatus", "Math.ceil") requireFunctionContains(t, js, "updateWaitingStatus", "announceChat(status)") requireFunctionContains(t, js, "gotWaiting", "announceUrgent")