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 { ServerProtocol } from "../ts-protocol/client.js";
|
||||||
import type { AvatarStore } from "../data/avatars.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
|
* 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 config: BotConfig;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private avatarStore: AvatarStore;
|
private avatarStore: AvatarStore;
|
||||||
|
private permissions: PermissionStore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
neteaseProvider: MusicProvider,
|
neteaseProvider: MusicProvider,
|
||||||
@@ -84,7 +86,8 @@ export class BotManager extends EventEmitter {
|
|||||||
database: BotDatabase,
|
database: BotDatabase,
|
||||||
config: BotConfig,
|
config: BotConfig,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
avatarStore: AvatarStore
|
avatarStore: AvatarStore,
|
||||||
|
permissions: PermissionStore
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.neteaseProvider = neteaseProvider;
|
this.neteaseProvider = neteaseProvider;
|
||||||
@@ -95,6 +98,7 @@ export class BotManager extends EventEmitter {
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.avatarStore = avatarStore;
|
this.avatarStore = avatarStore;
|
||||||
|
this.permissions = permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createBot(params: CreateBotParams): Promise<BotInstance> {
|
async createBot(params: CreateBotParams): Promise<BotInstance> {
|
||||||
@@ -152,6 +156,7 @@ export class BotManager extends EventEmitter {
|
|||||||
this.bots.delete(id);
|
this.bots.delete(id);
|
||||||
}
|
}
|
||||||
this.database.deleteBotInstance(id);
|
this.database.deleteBotInstance(id);
|
||||||
|
this.permissions.pruneBot(id);
|
||||||
this.emit("botInstanceRemoved", id);
|
this.emit("botInstanceRemoved", id);
|
||||||
this.logger.info({ botId: id }, "Bot instance removed");
|
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 { BiliBiliProvider } from "./music/bilibili.js";
|
||||||
import { createCookieStore } from "./music/auth.js";
|
import { createCookieStore } from "./music/auth.js";
|
||||||
import { createAvatarStore } from "./data/avatars.js";
|
import { createAvatarStore } from "./data/avatars.js";
|
||||||
|
import { createPermissionStore } from "./data/permissions.js";
|
||||||
import { BotManager } from "./bot/manager.js";
|
import { BotManager } from "./bot/manager.js";
|
||||||
import { createWebServer } from "./web/server.js";
|
import { createWebServer } from "./web/server.js";
|
||||||
|
|
||||||
@@ -56,6 +57,8 @@ async function main() {
|
|||||||
const bilibiliCookie = cookieStore.load("bilibili");
|
const bilibiliCookie = cookieStore.load("bilibili");
|
||||||
if (bilibiliCookie) bilibiliProvider.setCookie(bilibiliCookie);
|
if (bilibiliCookie) bilibiliProvider.setCookie(bilibiliCookie);
|
||||||
|
|
||||||
|
const permissions = createPermissionStore(db.db);
|
||||||
|
|
||||||
const botManager = new BotManager(
|
const botManager = new BotManager(
|
||||||
neteaseProvider,
|
neteaseProvider,
|
||||||
qqProvider,
|
qqProvider,
|
||||||
@@ -63,7 +66,8 @@ async function main() {
|
|||||||
db,
|
db,
|
||||||
config,
|
config,
|
||||||
logger,
|
logger,
|
||||||
avatarStore
|
avatarStore,
|
||||||
|
permissions
|
||||||
);
|
);
|
||||||
await botManager.loadSavedBots();
|
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 {
|
): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/", (_req, res) => {
|
router.get("/", (req, res) => {
|
||||||
const bots = botManager.getAllBots().map((b) => b.getStatus());
|
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 });
|
res.json({ bots });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user