diff --git a/src/bot/instance.ts b/src/bot/instance.ts index 41c5e55..408c823 100755 --- a/src/bot/instance.ts +++ b/src/bot/instance.ts @@ -159,6 +159,22 @@ export class BotInstance extends EventEmitter { this.autoPaused = false; this._startIdlePoller(); }); + + // React near-instantly to channel membership changes. The 30s idle + // poller remains the fallback if any of these events are missed. + this.tsClient.on("clientEnter", () => void this.refreshOccupancy()); + this.tsClient.on("clientLeave", () => void this.refreshOccupancy()); + this.tsClient.on("clientMoved", () => void this.refreshOccupancy()); + } + + private async refreshOccupancy(): Promise { + if (!this.connected) return; + try { + const clients = await this.tsClient.getClientsInChannel(); + this.handleOccupancy(clients.length - 1); + } catch { + // ignore — the 30s poll is the fallback + } } async connect(): Promise { diff --git a/src/ts-protocol/client.ts b/src/ts-protocol/client.ts index 591507c..383c6fc 100644 --- a/src/ts-protocol/client.ts +++ b/src/ts-protocol/client.ts @@ -12,6 +12,8 @@ import { type Identity, type TextMessage, type ClientInfo, + type ClientLeftViewEvent, + type ClientMovedEvent, type FileUploadInfo, } from "@honeybbq/teamspeak-client"; import type { Logger } from "../logger.js"; @@ -221,6 +223,20 @@ export class TS3Client extends EventEmitter { { nickname: info.nickname, id: info.id }, "Client entered" ); + this.emit("clientEnter", info); + }); + + this.client.on("clientLeave", (ev: ClientLeftViewEvent) => { + this.logger.debug({ id: ev.id }, "Client left"); + this.emit("clientLeave", ev); + }); + + this.client.on("clientMoved", (ev: ClientMovedEvent) => { + this.logger.debug( + { id: ev.id, targetChannelID: ev.targetChannelID.toString() }, + "Client moved" + ); + this.emit("clientMoved", ev); }); await this.client.connect();