mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
fix(guest): tear down guest WS on guest-mode config change
An open guest WebSocket stamped isGuest/botScope once at upgrade and
never rechecked them, so it kept streaming bot state after an admin
disabled guest mode or narrowed guestMode.bots. setupWebSocket now
returns { cleanup, refreshGuestPolicy }; POST /api/bot/settings invokes
refreshGuestPolicy after saving a guestMode change, force-closing guest
sockets when disabled and live re-scoping them otherwise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c1d73b6ba8
commit
43c0175334
4 files changed
+120
-8
No files matched your search
+11
-2
@@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import type { BotManager } from "../../bot/manager.js";
|
||||
import type { BotConfig } from "../../data/config.js";
|
||||
import type { BotConfig, GuestModeConfig } from "../../data/config.js";
|
||||
import { saveConfig } from "../../data/config.js";
|
||||
import type { Logger } from "../../logger.js";
|
||||
import type { BotDatabase } from "../../data/database.js";
|
||||
@@ -16,6 +16,7 @@ export function createBotRouter(
|
||||
logger: Logger,
|
||||
botDb: BotDatabase,
|
||||
avatarStore: AvatarStore,
|
||||
onGuestPolicyChanged?: (cfg: GuestModeConfig) => void,
|
||||
): Router {
|
||||
const router = Router();
|
||||
|
||||
@@ -55,7 +56,8 @@ export function createBotRouter(
|
||||
if (hasIdle) config.idleTimeoutMinutes = idleTimeoutMinutes;
|
||||
if (hasAutoPause) config.autoPauseOnEmpty = autoPauseOnEmpty;
|
||||
|
||||
if (guestMode !== undefined && guestMode !== null && typeof guestMode === "object") {
|
||||
const hasGuestMode = guestMode !== undefined && guestMode !== null && typeof guestMode === "object";
|
||||
if (hasGuestMode) {
|
||||
const gm = config.guestMode;
|
||||
if (typeof guestMode.enabled === "boolean") gm.enabled = guestMode.enabled;
|
||||
if (guestMode.bots === "all") {
|
||||
@@ -74,6 +76,13 @@ export function createBotRouter(
|
||||
|
||||
saveConfig(configPath, config);
|
||||
|
||||
// Guest-mode changed: tear down / re-scope in-flight guest WS sockets so a
|
||||
// disabled or narrowed scope takes effect immediately (matches requireAuth's
|
||||
// "disabling immediately invalidates in-flight guest sessions" invariant).
|
||||
if (hasGuestMode) {
|
||||
onGuestPolicyChanged?.(config.guestMode);
|
||||
}
|
||||
|
||||
// 通知所有 bot 实例更新
|
||||
for (const bot of botManager.getAllBots()) {
|
||||
if (hasIdle) bot.updateIdleTimeout(config.idleTimeoutMinutes);
|
||||
|
||||
+10
-3
@@ -6,7 +6,7 @@ import { WebSocketServer } from "ws";
|
||||
import type { BotManager } from "../bot/manager.js";
|
||||
import type { MusicProvider } from "../music/provider.js";
|
||||
import type { BotDatabase } from "../data/database.js";
|
||||
import type { BotConfig } from "../data/config.js";
|
||||
import type { BotConfig, GuestModeConfig } from "../data/config.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import type { CookieStore } from "../music/auth.js";
|
||||
import type { AvatarStore } from "../data/avatars.js";
|
||||
@@ -104,6 +104,11 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
||||
app.use("/api", requireAuth);
|
||||
|
||||
// ─── Protected routes ───────────────────────────────────────────────────
|
||||
// The bot router is mounted BEFORE setupWebSocket runs, but its /settings
|
||||
// handler needs to trigger a guest-policy refresh on the (later-created) WS
|
||||
// controller. Bridge the two with a mutable indirection that starts as a
|
||||
// no-op and is wired to the real refreshGuestPolicy once the WS is set up.
|
||||
let onGuestPolicyChanged: (cfg: GuestModeConfig) => void = () => {};
|
||||
app.use(
|
||||
"/api/bot",
|
||||
createBotRouter(
|
||||
@@ -113,6 +118,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
||||
logger,
|
||||
options.database,
|
||||
options.avatarStore,
|
||||
(cfg) => onGuestPolicyChanged(cfg),
|
||||
)
|
||||
);
|
||||
app.use(
|
||||
@@ -195,7 +201,8 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
||||
wss.emit("connection", ws, req);
|
||||
});
|
||||
});
|
||||
const cleanupWs = setupWebSocket(wss, options.botManager, logger);
|
||||
const controller = setupWebSocket(wss, options.botManager, logger);
|
||||
onGuestPolicyChanged = controller.refreshGuestPolicy;
|
||||
|
||||
// ─── Session cleanup interval ──────────────────────────────────────────
|
||||
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -221,7 +228,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
||||
clearInterval(cleanupTimer);
|
||||
cleanupTimer = null;
|
||||
}
|
||||
cleanupWs();
|
||||
controller.cleanup();
|
||||
wss.close();
|
||||
server.close();
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("WebSocket guest bot scope", () => {
|
||||
off: () => {},
|
||||
removeListener: () => {},
|
||||
};
|
||||
const cleanup = setupWebSocket(fakeWss, botManager, {
|
||||
const { cleanup } = setupWebSocket(fakeWss, botManager, {
|
||||
debug() {},
|
||||
error() {},
|
||||
info() {},
|
||||
@@ -114,3 +114,68 @@ describe("WebSocket guest bot scope", () => {
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebSocket refreshGuestPolicy", () => {
|
||||
function makeHarness() {
|
||||
const clients: any[] = [];
|
||||
const fakeWss: any = {
|
||||
on: (ev: string, cb: any) => {
|
||||
if (ev === "connection") fakeWss._conn = cb;
|
||||
},
|
||||
};
|
||||
const botManager: any = {
|
||||
getAllBots: () => [],
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
removeListener: () => {},
|
||||
};
|
||||
const logger = { debug() {}, error() {}, info() {}, warn() {} } as any;
|
||||
const controller = setupWebSocket(fakeWss, botManager, logger);
|
||||
// Connect fake sockets via the connection handler so they land in `clients`.
|
||||
const connect = (ws: any) => {
|
||||
clients.push(ws);
|
||||
fakeWss._conn(ws);
|
||||
};
|
||||
return { controller, connect };
|
||||
}
|
||||
|
||||
function makeFakeWs(opts: { isGuest: boolean; botScope?: "all" | Set<string> }) {
|
||||
const closeCalls: Array<{ code?: number; reason?: string }> = [];
|
||||
const ws: any = {
|
||||
readyState: 1,
|
||||
isGuest: opts.isGuest,
|
||||
botScope: opts.botScope,
|
||||
send: () => {},
|
||||
on: () => {},
|
||||
close: (code?: number, reason?: string) => closeCalls.push({ code, reason }),
|
||||
};
|
||||
return { ws, closeCalls };
|
||||
}
|
||||
|
||||
it("disabling guest mode closes guest sockets but leaves non-guest sockets open", () => {
|
||||
const { controller, connect } = makeHarness();
|
||||
const guest = makeFakeWs({ isGuest: true, botScope: new Set(["bot1"]) });
|
||||
const member = makeFakeWs({ isGuest: false, botScope: "all" });
|
||||
connect(guest.ws);
|
||||
connect(member.ws);
|
||||
|
||||
controller.refreshGuestPolicy({ enabled: false, bots: "all" });
|
||||
|
||||
expect(guest.closeCalls.length).toBe(1);
|
||||
expect(guest.closeCalls[0].code).toBe(1008);
|
||||
expect(member.closeCalls.length).toBe(0);
|
||||
});
|
||||
|
||||
it("narrowing the guest scope live re-scopes open guest sockets", () => {
|
||||
const { controller, connect } = makeHarness();
|
||||
const guest = makeFakeWs({ isGuest: true, botScope: new Set(["bot1"]) });
|
||||
connect(guest.ws);
|
||||
|
||||
controller.refreshGuestPolicy({ enabled: true, bots: ["bot2"] });
|
||||
|
||||
expect(guest.closeCalls.length).toBe(0);
|
||||
expect(guest.ws.botScope instanceof Set).toBe(true);
|
||||
expect(guest.ws.botScope.has("bot2")).toBe(true);
|
||||
expect(guest.ws.botScope.has("bot1")).toBe(false);
|
||||
});
|
||||
});
|
||||
+33
-2
@@ -3,11 +3,21 @@ import type { BotManager } from "../bot/manager.js";
|
||||
import type { BotInstance } from "../bot/instance.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
|
||||
export interface WebSocketController {
|
||||
cleanup: () => void;
|
||||
/**
|
||||
* Re-apply the current guest-mode policy to every already-open guest socket.
|
||||
* If guest mode is disabled, in-flight guest sockets are force-closed; otherwise
|
||||
* each guest socket is live re-scoped so out-of-scope bots stop streaming.
|
||||
*/
|
||||
refreshGuestPolicy: (cfg: { enabled: boolean; bots: "all" | string[] }) => void;
|
||||
}
|
||||
|
||||
export function setupWebSocket(
|
||||
wss: WebSocketServer,
|
||||
botManager: BotManager,
|
||||
logger: Logger
|
||||
): () => void {
|
||||
): WebSocketController {
|
||||
const clients = new Set<WebSocket>();
|
||||
|
||||
/**
|
||||
@@ -150,7 +160,7 @@ export function setupWebSocket(
|
||||
}, 5000);
|
||||
ensureAllBotsAttached();
|
||||
|
||||
return () => {
|
||||
const cleanup = () => {
|
||||
clearInterval(intervalId);
|
||||
botManager.removeListener("botInstance", onBotInstance);
|
||||
botManager.removeListener("botInstanceRemoved", onBotInstanceRemoved);
|
||||
@@ -159,4 +169,25 @@ export function setupWebSocket(
|
||||
detachBotListener(id);
|
||||
}
|
||||
};
|
||||
|
||||
// When the admin changes guestMode (disable / narrow scope), already-open guest
|
||||
// sockets must stop streaming immediately — their isGuest/botScope were stamped
|
||||
// once at upgrade and would otherwise keep receiving bot state.
|
||||
const refreshGuestPolicy = (cfg: { enabled: boolean; bots: "all" | string[] }) => {
|
||||
for (const ws of clients) {
|
||||
const w = ws as unknown as { isGuest?: boolean; botScope?: "all" | Set<string> };
|
||||
if (!w.isGuest) continue;
|
||||
if (!cfg.enabled) {
|
||||
try {
|
||||
ws.close(1008, "guest mode disabled");
|
||||
} catch {
|
||||
// socket may already be closing; ignore
|
||||
}
|
||||
} else {
|
||||
w.botScope = cfg.bots === "all" ? "all" : new Set(cfg.bots);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { cleanup, refreshGuestPolicy };
|
||||
}
|
||||
Reference in new issue
Block a user