mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix(profile): move the now-playing channel description with the bot (#159)
When the bot was moved to another channel, the channel it left kept the now-playing description forever: updateChannelDescription always targeted getChannelId(), which by then already reported the new channel. Remember which channel we last wrote to. On a self clientMoved event, clear that channel and, if a song is playing, write the description to the new one. Stop now clears the channel we actually wrote to, so a missed move event can't leave a stale description behind either. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8ff51ea6e0
commit
faf6ac09ec
3 files changed
+115
-6
No files matched your search
+6
-1
@@ -456,11 +456,16 @@ export class BotInstance extends EventEmitter {
|
|||||||
this.voiceDucking.removeSpeaker(event.id);
|
this.voiceDucking.removeSpeaker(event.id);
|
||||||
void this.refreshOccupancy();
|
void this.refreshOccupancy();
|
||||||
});
|
});
|
||||||
this.tsClient.on("clientMoved", (event: { id: number }) => {
|
this.tsClient.on("clientMoved", (event: { id: number; targetChannelID: bigint }) => {
|
||||||
if (event.id === this.tsClient.getClientId()) {
|
if (event.id === this.tsClient.getClientId()) {
|
||||||
// Moving the bot invalidates every activity deadline from its old
|
// Moving the bot invalidates every activity deadline from its old
|
||||||
// channel even if no individual leave events arrive.
|
// channel even if no individual leave events arrive.
|
||||||
this.voiceDucking.reset(false);
|
this.voiceDucking.reset(false);
|
||||||
|
// Carry the now-playing channel description over to the new
|
||||||
|
// channel instead of leaving it stale in the old one (#159).
|
||||||
|
this.profileManager.onChannelMoved(event.targetChannelID).catch((err) => {
|
||||||
|
this.logger.warn({ err }, "Channel description move update failed");
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.voiceDucking.removeSpeaker(event.id);
|
this.voiceDucking.removeSpeaker(event.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,3 +202,65 @@ describe("BotProfileManager loadCustomAvatar (pre-connect load, #148)", () => {
|
|||||||
expect(ts.uploadCalls[0].equals(Buffer.from([2, 2]))).toBe(true);
|
expect(ts.uploadCalls[0].equals(Buffer.from([2, 2]))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("BotProfileManager channel description follows the bot (#159)", () => {
|
||||||
|
const cfgChannelDesc = { ...cfgOff, channelDescEnabled: true };
|
||||||
|
let ts: ReturnType<typeof makeMockTs> & { cid: bigint };
|
||||||
|
let channelEdits: () => string[];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ts = makeMockTs() as any;
|
||||||
|
ts.cid = 5n;
|
||||||
|
(ts as any).getChannelId = () => ts.cid;
|
||||||
|
channelEdits = () =>
|
||||||
|
(ts.sendCommandNoWait as any).mock.calls
|
||||||
|
.map((c: any[]) => c[0] as string)
|
||||||
|
.filter((cmd: string) => cmd.startsWith("channeledit"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the old channel and fills the new one when moved while playing", async () => {
|
||||||
|
const pm = new BotProfileManager(ts as any, noopLogger, cfgChannelDesc, "Bot");
|
||||||
|
await pm.onSongChange(fakeSong);
|
||||||
|
expect(channelEdits()).toEqual([
|
||||||
|
expect.stringMatching(/^channeledit cid=5 channel_description=\S+/),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ts.cid = 9n;
|
||||||
|
await pm.onChannelMoved(9n);
|
||||||
|
|
||||||
|
const edits = channelEdits();
|
||||||
|
expect(edits[1]).toBe("channeledit cid=5 channel_description=");
|
||||||
|
expect(edits[2]).toMatch(/^channeledit cid=9 channel_description=\S+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stopping after a move clears the channel the bot is in now, not the old one", async () => {
|
||||||
|
const pm = new BotProfileManager(ts as any, noopLogger, cfgChannelDesc, "Bot");
|
||||||
|
await pm.onSongChange(fakeSong);
|
||||||
|
ts.cid = 9n;
|
||||||
|
await pm.onChannelMoved(9n);
|
||||||
|
await pm.onSongChange(null);
|
||||||
|
expect(channelEdits().at(-1)).toBe("channeledit cid=9 channel_description=");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a move while idle touches no channel description", async () => {
|
||||||
|
const pm = new BotProfileManager(ts as any, noopLogger, cfgChannelDesc, "Bot");
|
||||||
|
ts.cid = 9n;
|
||||||
|
await pm.onChannelMoved(9n);
|
||||||
|
expect(channelEdits()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a move is ignored when the channel description feature is off", async () => {
|
||||||
|
const pm = new BotProfileManager(ts as any, noopLogger, cfgOff, "Bot");
|
||||||
|
await pm.onSongChange(fakeSong);
|
||||||
|
ts.cid = 9n;
|
||||||
|
await pm.onChannelMoved(9n);
|
||||||
|
expect(channelEdits()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("an event for the channel the description is already in is a no-op", async () => {
|
||||||
|
const pm = new BotProfileManager(ts as any, noopLogger, cfgChannelDesc, "Bot");
|
||||||
|
await pm.onSongChange(fakeSong);
|
||||||
|
await pm.onChannelMoved(5n);
|
||||||
|
expect(channelEdits()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
+47
-5
@@ -32,6 +32,13 @@ export class BotProfileManager {
|
|||||||
* pushed immediately (idle) or wait for the next stop event (playing).
|
* pushed immediately (idle) or wait for the next stop event (playing).
|
||||||
*/
|
*/
|
||||||
private currentSong: QueuedSong | null = null;
|
private currentSong: QueuedSong | null = null;
|
||||||
|
/**
|
||||||
|
* Channel whose description currently holds our now-playing text, or null
|
||||||
|
* if we have not written one. Remembered so that when the bot is moved we
|
||||||
|
* can still clean up the channel it was taken out of (#159) — by then
|
||||||
|
* getChannelId() already reports the new channel.
|
||||||
|
*/
|
||||||
|
private channelDescCid: bigint | null = null;
|
||||||
|
|
||||||
/** Per-feature permission-denied flags. Reset on reconnect. */
|
/** Per-feature permission-denied flags. Reset on reconnect. */
|
||||||
private permDenied = {
|
private permDenied = {
|
||||||
@@ -134,6 +141,8 @@ export class BotProfileManager {
|
|||||||
onConnect(): void {
|
onConnect(): void {
|
||||||
this.generation++;
|
this.generation++;
|
||||||
this.currentSong = null;
|
this.currentSong = null;
|
||||||
|
// Channel ids are per-server; never carry one across a (re)connect.
|
||||||
|
this.channelDescCid = null;
|
||||||
this.permDenied = {
|
this.permDenied = {
|
||||||
avatar: false,
|
avatar: false,
|
||||||
description: false,
|
description: false,
|
||||||
@@ -150,6 +159,31 @@ export class BotProfileManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the bot itself has been moved to another channel (#159).
|
||||||
|
* Clears the now-playing text from the channel it left and, if a song is
|
||||||
|
* playing, writes it to the channel it is in now.
|
||||||
|
*/
|
||||||
|
async onChannelMoved(newChannelId: bigint): Promise<void> {
|
||||||
|
if (!this.config.channelDescEnabled || this.permDenied.channelDesc) return;
|
||||||
|
const oldChannelId = this.channelDescCid;
|
||||||
|
if (oldChannelId === newChannelId) return;
|
||||||
|
try {
|
||||||
|
if (oldChannelId !== null) {
|
||||||
|
await this.tsClient.sendCommandNoWait(
|
||||||
|
`channeledit cid=${oldChannelId} channel_description=`,
|
||||||
|
);
|
||||||
|
this.channelDescCid = null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.handleFeatureError("channelDesc", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.currentSong) {
|
||||||
|
await this.updateChannelDescription(this.currentSong, newChannelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getConfig(): ProfileConfig {
|
getConfig(): ProfileConfig {
|
||||||
return { ...this.config };
|
return { ...this.config };
|
||||||
}
|
}
|
||||||
@@ -400,18 +434,25 @@ export class BotProfileManager {
|
|||||||
return str.slice(0, end) + ellipsis;
|
return str.slice(0, end) + ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateChannelDescription(song: QueuedSong | null): Promise<void> {
|
private async updateChannelDescription(
|
||||||
|
song: QueuedSong | null,
|
||||||
|
targetChannelId?: bigint,
|
||||||
|
): Promise<void> {
|
||||||
if (!this.config.channelDescEnabled || this.permDenied.channelDesc) return;
|
if (!this.config.channelDescEnabled || this.permDenied.channelDesc) return;
|
||||||
try {
|
try {
|
||||||
const channelId = this.tsClient.getChannelId();
|
const channelId = targetChannelId ?? this.tsClient.getChannelId();
|
||||||
if (channelId === 0n) return; // unknown channel
|
|
||||||
|
|
||||||
if (!song) {
|
if (!song) {
|
||||||
|
// Prefer the channel we actually wrote to, in case a move event
|
||||||
|
// was missed and the bot is somewhere else now.
|
||||||
|
const target = this.channelDescCid ?? channelId;
|
||||||
|
if (target === 0n) return; // unknown channel
|
||||||
await this.tsClient.sendCommandNoWait(
|
await this.tsClient.sendCommandNoWait(
|
||||||
`channeledit cid=${channelId} channel_description=`,
|
`channeledit cid=${target} channel_description=`,
|
||||||
);
|
);
|
||||||
|
this.channelDescCid = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (channelId === 0n) return; // unknown channel
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`\u266A \u6B63\u5728\u64AD\u653E: ${song.name} - ${song.artist}`, // ♪ 正在播放:
|
`\u266A \u6B63\u5728\u64AD\u653E: ${song.name} - ${song.artist}`, // ♪ 正在播放:
|
||||||
@@ -422,6 +463,7 @@ export class BotProfileManager {
|
|||||||
await this.tsClient.sendCommandNoWait(
|
await this.tsClient.sendCommandNoWait(
|
||||||
`channeledit cid=${channelId} channel_description=${escapeTS3(desc)}`,
|
`channeledit cid=${channelId} channel_description=${escapeTS3(desc)}`,
|
||||||
);
|
);
|
||||||
|
this.channelDescCid = channelId;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.handleFeatureError("channelDesc", err);
|
this.handleFeatureError("channelDesc", err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user