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
+6
View File
@@ -133,6 +133,12 @@ type ClientCredentials struct {
Token string
}
// AuthenticatedClient is the credential-free result of authentication.
type AuthenticatedClient struct {
Username string
Permissions []string
}
type Client interface {
Hall() *Hall
Addr() net.Addr
+142 -83
View File
@@ -105,6 +105,7 @@ type Hall struct {
description *Description
locked *string
clients map[string]Client
reserved map[string]Client
history []ChatHistoryEntry
timestamp time.Time
data map[string]interface{}
@@ -232,6 +233,37 @@ func (g *Hall) ClientCount() int {
return g.clientCountUnlocked()
}
// ReserveUsernameWhileWaiting atomically checks whether waiting is available
// and, if so, reserves a username for an authenticated non-member.
func (g *Hall) ReserveUsernameWhileWaiting(username string, client Client) (bool, error) {
g.mu.Lock()
defer g.mu.Unlock()
if g.locked == nil || !g.hasOperatorUnlocked() {
return false, nil
}
for _, c := range g.clients {
if !isSystemClient(c) && c.Username() == username {
return true, UserError("username already in use")
}
}
if holder := g.reserved[username]; holder != nil && holder != client {
return true, UserError("username already in use")
}
if g.reserved == nil {
g.reserved = make(map[string]Client)
}
g.reserved[username] = client
return true, nil
}
func (g *Hall) ReleaseUsername(username string, client Client) {
g.mu.Lock()
defer g.mu.Unlock()
if g.reserved[username] == client {
delete(g.reserved, username)
}
}
func (g *Hall) clientCountUnlocked() int {
count := 0
for _, c := range g.clients {
@@ -252,6 +284,9 @@ func (g *Hall) mayExpire() bool {
if len(g.clients) > 0 {
return false
}
if len(g.reserved) > 0 {
return false
}
return time.Since(g.timestamp) > maxHistoryAge(g.description)
}
@@ -578,7 +613,7 @@ func Delete(name string) bool {
// Called with both halls.mu and g.mu taken.
func deleteUnlocked(g *Hall) bool {
if len(g.clients) != 0 {
if len(g.clients) != 0 || len(g.reserved) != 0 {
return false
}
@@ -599,112 +634,136 @@ func isSystemClient(c Client) bool {
return member("system", c.Permissions())
}
func AddClient(hall string, c Client, creds ClientCredentials) (*Hall, error) {
g, err := Add(hall, nil)
func AuthenticateClient(name string, creds ClientCredentials) (*Hall, AuthenticatedClient, error) {
g, err := Add(name, nil)
if err != nil {
return nil, err
return nil, AuthenticatedClient{}, err
}
g.mu.Lock()
defer g.mu.Unlock()
username, perms, err := g.getPermission(creds)
if err != nil {
return nil, AuthenticatedClient{}, err
}
return g, AuthenticatedClient{username, append([]string(nil), perms...)}, nil
}
// AddAuthenticatedClient admits a previously authenticated non-system client.
// A lock bypass does not bypass time restrictions or capacity.
func AddAuthenticatedClient(g *Hall, c Client, auth AuthenticatedClient, bypassLock bool) error {
if g == nil {
return os.ErrNotExist
}
g.mu.Lock()
defer g.mu.Unlock()
clients := g.getClientsUnlocked(nil)
systemClient := isSystemClient(c)
if !systemClient {
username, perms, err := g.getPermission(creds)
if err != nil {
return nil, err
if holder := g.reserved[auth.Username]; holder != nil && holder != c {
return UserError("username already in use")
}
c.SetUsername(auth.Username)
c.SetPermissions(auth.Permissions)
for _, cc := range clients {
if !isSystemClient(cc) && cc.Username() == auth.Username {
return UserError("username already in use")
}
c.SetUsername(username)
c.SetPermissions(perms)
for _, cc := range clients {
if !isSystemClient(cc) && cc.Username() == username {
return nil, UserError("username already in use")
}
if !member("op", auth.Permissions) {
if !bypassLock && g.locked != nil {
m := *g.locked
if m == "" {
m = "Hall is locked."
}
return UserError(m)
}
now := time.Now()
if g.description.NotBefore != nil && g.description.NotBefore.After(now) {
return UserError("this hall is not open yet")
}
if g.description.Expires != nil && g.description.Expires.Before(now) {
return UserError("this hall is closed")
}
if g.description.Autokick {
ops := false
for _, existing := range clients {
if member("op", existing.Permissions()) {
ops = true
break
}
}
if !ops {
return UserError("there are no operators in this hall")
}
}
if !member("op", perms) {
if g.locked != nil {
m := *g.locked
if m == "" {
m = "Hall is locked."
}
return nil, UserError(m)
}
if g.description.NotBefore != nil ||
g.description.Expires != nil {
now := time.Now()
if g.description.NotBefore != nil &&
g.description.NotBefore.After(now) {
return nil, UserError(
"this hall is not open yet",
)
}
if g.description.Expires != nil &&
g.description.Expires.Before(now) {
return nil, UserError(
"this hall is closed",
)
}
}
if g.description.Autokick {
ops := false
for _, c := range clients {
if member("op", c.Permissions()) {
ops = true
break
}
}
if !ops {
return nil, UserError(
"there are no operators " +
"in this hall",
)
}
}
}
if !member("op", perms) && g.description.MaxClients > 0 {
if g.clientCountUnlocked() >= g.description.MaxClients {
return nil, UserError("too many users")
}
if g.description.MaxClients > 0 && g.clientCountUnlocked() >= g.description.MaxClients {
return UserError("too many users")
}
}
id := c.Id()
if id == "" {
return nil, errors.New("client has empty id")
return errors.New("client has empty id")
}
if g.clients[id] != nil {
return nil, ProtocolError("duplicate client id")
return ProtocolError("duplicate client id")
}
g.clients[id] = c
delete(g.reserved, auth.Username)
g.timestamp = time.Now()
c.Joined(g.Name(), "join")
u := c.Username()
p := c.Permissions()
s := c.Data()
if !systemClient {
c.PushClient(g.Name(), "add", c.Id(), u, p, s)
}
u, p, s := c.Username(), c.Permissions(), c.Data()
c.PushClient(g.Name(), "add", id, u, p, s)
for _, cc := range clients {
pp := cc.Permissions()
uu := cc.Username()
if !isSystemClient(cc) {
c.PushClient(
g.Name(), "add", cc.Id(), uu, pp, cc.Data(),
)
c.PushClient(g.Name(), "add", cc.Id(), cc.Username(), cc.Permissions(), cc.Data())
}
if !systemClient {
cc.PushClient(g.Name(), "add", id, u, p, s)
cc.PushClient(g.Name(), "add", id, u, p, s)
}
return nil
}
func AddClient(name string, c Client, creds ClientCredentials) (*Hall, error) {
if creds.System || isSystemClient(c) {
g, err := Add(name, nil)
if err != nil {
return nil, err
}
g.mu.Lock()
defer g.mu.Unlock()
if c.Id() == "" {
return nil, errors.New("client has empty id")
}
if g.clients[c.Id()] != nil {
return nil, ProtocolError("duplicate client id")
}
g.clients[c.Id()] = c
g.timestamp = time.Now()
c.Joined(g.Name(), "join")
return g, nil
}
g, auth, err := AuthenticateClient(name, creds)
if err != nil {
return nil, err
}
if err := AddAuthenticatedClient(g, c, auth, false); err != nil {
return nil, err
}
return g, nil
}
// WaitingStatus atomically reports the lock and active-operator state.
func (g *Hall) WaitingStatus() (bool, bool) {
g.mu.Lock()
defer g.mu.Unlock()
return g.locked != nil, g.hasOperatorUnlocked()
}
// called locked
func (g *Hall) hasOperatorUnlocked() bool {
for _, c := range g.clients {
if !isSystemClient(c) && member("op", c.Permissions()) {
return true
}
}
return g, nil
return false
}
// called locked