mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(perm): filter GET /api/bot to allowed bots; prune access on bot delete
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cd6f2c6078
commit
1f0f162f66
4 files changed
+108
-4
No files matched your search
+6
-1
@@ -12,6 +12,7 @@ import type { Logger } from "../logger.js";
|
||||
|
||||
import type { ServerProtocol } from "../ts-protocol/client.js";
|
||||
import type { AvatarStore } from "../data/avatars.js";
|
||||
import type { PermissionStore } from "../data/permissions.js";
|
||||
|
||||
/**
|
||||
* Run bot.connect() with a hard deadline. If the handshake hangs (e.g. the
|
||||
@@ -76,6 +77,7 @@ export class BotManager extends EventEmitter {
|
||||
private config: BotConfig;
|
||||
private logger: Logger;
|
||||
private avatarStore: AvatarStore;
|
||||
private permissions: PermissionStore;
|
||||
|
||||
constructor(
|
||||
neteaseProvider: MusicProvider,
|
||||
@@ -84,7 +86,8 @@ export class BotManager extends EventEmitter {
|
||||
database: BotDatabase,
|
||||
config: BotConfig,
|
||||
logger: Logger,
|
||||
avatarStore: AvatarStore
|
||||
avatarStore: AvatarStore,
|
||||
permissions: PermissionStore
|
||||
) {
|
||||
super();
|
||||
this.neteaseProvider = neteaseProvider;
|
||||
@@ -95,6 +98,7 @@ export class BotManager extends EventEmitter {
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
this.avatarStore = avatarStore;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
async createBot(params: CreateBotParams): Promise<BotInstance> {
|
||||
@@ -152,6 +156,7 @@ export class BotManager extends EventEmitter {
|
||||
this.bots.delete(id);
|
||||
}
|
||||
this.database.deleteBotInstance(id);
|
||||
this.permissions.pruneBot(id);
|
||||
this.emit("botInstanceRemoved", id);
|
||||
this.logger.info({ botId: id }, "Bot instance removed");
|
||||
}
|
||||
|
||||
+5
-1
@@ -9,6 +9,7 @@ import { QQMusicProvider } from "./music/qq.js";
|
||||
import { BiliBiliProvider } from "./music/bilibili.js";
|
||||
import { createCookieStore } from "./music/auth.js";
|
||||
import { createAvatarStore } from "./data/avatars.js";
|
||||
import { createPermissionStore } from "./data/permissions.js";
|
||||
import { BotManager } from "./bot/manager.js";
|
||||
import { createWebServer } from "./web/server.js";
|
||||
|
||||
@@ -56,6 +57,8 @@ async function main() {
|
||||
const bilibiliCookie = cookieStore.load("bilibili");
|
||||
if (bilibiliCookie) bilibiliProvider.setCookie(bilibiliCookie);
|
||||
|
||||
const permissions = createPermissionStore(db.db);
|
||||
|
||||
const botManager = new BotManager(
|
||||
neteaseProvider,
|
||||
qqProvider,
|
||||
@@ -63,7 +66,8 @@ async function main() {
|
||||
db,
|
||||
config,
|
||||
logger,
|
||||
avatarStore
|
||||
avatarStore,
|
||||
permissions
|
||||
);
|
||||
await botManager.loadSavedBots();
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import pino from "pino";
|
||||
import { createBotRouter } from "./bot.js";
|
||||
|
||||
const logger = pino({ level: "silent" });
|
||||
|
||||
// Fake bot whose getStatus() exposes its id, matching the real status shape.
|
||||
function makeFakeBot(id: string) {
|
||||
return {
|
||||
id,
|
||||
getStatus: () => ({ id }),
|
||||
};
|
||||
}
|
||||
|
||||
function makeBotManager() {
|
||||
const b1 = makeFakeBot("b1");
|
||||
const b2 = makeFakeBot("b2");
|
||||
return {
|
||||
getBot: (id: string) => (id === "b1" ? b1 : id === "b2" ? b2 : undefined),
|
||||
getAllBots: () => [b1, b2],
|
||||
getBotConfig: () => undefined,
|
||||
createBot: async () => b1,
|
||||
updateBot: () => {},
|
||||
removeBot: async () => {},
|
||||
startBot: async () => {},
|
||||
stopBot: () => {},
|
||||
} as any;
|
||||
}
|
||||
|
||||
function makeApp(user: any) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => { (req as any).user = user; next(); });
|
||||
app.use(
|
||||
"/api/bot",
|
||||
createBotRouter(
|
||||
makeBotManager(),
|
||||
{ idleTimeoutMinutes: 0 } as any,
|
||||
"/tmp/config.json",
|
||||
logger,
|
||||
{ getBotInstances: () => [], getCustomAvatarPath: () => null, setCustomAvatarPath: () => {} } as any,
|
||||
{ read: () => null, write: () => "x", remove: () => {} } as any,
|
||||
),
|
||||
);
|
||||
return app;
|
||||
}
|
||||
|
||||
const member = (bots: "all" | string[]) => ({
|
||||
id: "u1",
|
||||
username: "alice",
|
||||
role: "member" as const,
|
||||
capabilities: new Set<string>(),
|
||||
bots: bots === "all" ? ("all" as const) : new Set(bots),
|
||||
});
|
||||
|
||||
const admin = {
|
||||
id: "a",
|
||||
username: "admin",
|
||||
role: "admin" as const,
|
||||
capabilities: new Set<string>(),
|
||||
bots: "all" as const,
|
||||
};
|
||||
|
||||
describe("GET /api/bot bot-list filtering", () => {
|
||||
it("member with bots:Set([b1]) sees only b1", async () => {
|
||||
const app = makeApp(member(["b1"]));
|
||||
const res = await request(app).get("/api/bot");
|
||||
expect(res.status).toBe(200);
|
||||
const ids = (res.body.bots as { id: string }[]).map((b) => b.id);
|
||||
expect(ids).toEqual(["b1"]);
|
||||
});
|
||||
|
||||
it("admin sees both b1 and b2", async () => {
|
||||
const app = makeApp(admin);
|
||||
const res = await request(app).get("/api/bot");
|
||||
expect(res.status).toBe(200);
|
||||
const ids = (res.body.bots as { id: string }[]).map((b) => b.id).sort();
|
||||
expect(ids).toEqual(["b1", "b2"]);
|
||||
});
|
||||
|
||||
it("member with bots:'all' sees both b1 and b2", async () => {
|
||||
const app = makeApp(member("all"));
|
||||
const res = await request(app).get("/api/bot");
|
||||
expect(res.status).toBe(200);
|
||||
const ids = (res.body.bots as { id: string }[]).map((b) => b.id).sort();
|
||||
expect(ids).toEqual(["b1", "b2"]);
|
||||
});
|
||||
});
|
||||
+7
-2
@@ -17,8 +17,13 @@ export function createBotRouter(
|
||||
): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/", (_req, res) => {
|
||||
const bots = botManager.getAllBots().map((b) => b.getStatus());
|
||||
router.get("/", (req, res) => {
|
||||
const all = botManager.getAllBots().map((b) => b.getStatus());
|
||||
const u = req.user!;
|
||||
const bots =
|
||||
u.role === "admin" || u.bots === "all"
|
||||
? all
|
||||
: all.filter((b) => u.bots instanceof Set && u.bots.has(b.id));
|
||||
res.json({ bots });
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user