fix(ts6): update the real music client profile

This commit is contained in:
razaxq committed 2026-10-01 21:32:21 +08:00
1 parent 87fca6d8b7
commit af4ca56fd3
2 files changed
+187 -52

No files matched your search

+142 -52
View File
@@ -288,32 +288,38 @@ export class BotProfileManager {
private async updateDescription(song: QueuedSong | null): Promise<void> { private async updateDescription(song: QueuedSong | null): Promise<void> {
if (!this.config.descriptionEnabled || this.permDenied.description) return; if (!this.config.descriptionEnabled || this.permDenied.description) return;
try { try {
const text = song const text = song
? `${song.name} - ${song.artist} [${song.album}]` ? `${song.name} - ${song.artist} [${song.album}]`
: ""; : "";
const clid = this.tsClient.getClientId();
if (clid <= 0) return;
const httpQuery = this.tsClient.getHttpQuery(); const httpQuery = this.tsClient.getHttpQuery();
if (httpQuery) { if (httpQuery) {
// TS6 HTTP API: send the raw (unescaped) text. clientUpdate // IMPORTANT:
// throws HttpQueryError on non-2xx so a silent 400/403 cannot // clientUpdate() would modify the HTTP Query/serveradmin client.
// be misreported as success. // Explicitly edit the real visible music client instead.
const result = await httpQuery.clientUpdate({ client_description: text }); const result = await httpQuery.clientEdit(clid, {
this.logger.info({ status: result.status }, "Description updated"); client_description: text,
});
this.logger.info(
{ status: result.status, clid },
"Description updated",
);
} else { } else {
// clientupdate rejects client_description (error 1538).
// Use clientedit on our own clid instead — this is what
// TS3AudioBot does via TSLib's ChangeDescription().
const clid = this.tsClient.getClientId();
if (clid <= 0) return;
// Use a 5s timeout — if clientedit hangs, don't block the
// remaining profile updates (channeledit, now-playing msg).
await this.withTimeout( await this.withTimeout(
this.tsClient.execCommand( this.tsClient.execCommand(
`clientedit clid=${clid} client_description=${escapeTS3(text)}`, `clientedit clid=${clid} client_description=${escapeTS3(text)}`,
), ),
5000, 5000,
); );
this.logger.info("Description updated");
this.logger.info({ clid }, "Description updated");
} }
} catch (err) { } catch (err) {
this.handleFeatureError("description", err); this.handleFeatureError("description", err);
@@ -353,37 +359,33 @@ export class BotProfileManager {
rawProps.client_away = 0; rawProps.client_away = 0;
} else { } else {
rawProps.client_away = 1; rawProps.client_away = 1;
rawProps.client_away_message = "\u7B49\u5F85\u64AD\u653E"; rawProps.client_away_message = "等待播放";
} }
} }
if (Object.keys(rawProps).length === 0) return; if (Object.keys(rawProps).length === 0) return;
try { try {
const httpQuery = this.tsClient.getHttpQuery(); // clientupdate modifies whichever connection sends the command.
if (httpQuery) { // Therefore it must be sent by the real full client, NOT HTTP Query.
// TS6: send raw values as JSON. Throws HttpQueryError on 4xx/5xx. const parts = Object.entries(rawProps).map(([key, value]) =>
const result = await httpQuery.clientUpdate(rawProps); typeof value === "string"
this.logger.info( ? `${key}=${escapeTS3(value)}`
{ status: result.status, props: Object.keys(rawProps) }, : `${key}=${value}`,
"Client properties updated (nickname + away)", );
);
} else { await this.tsClient.sendCommandNoWait(
// TS3 wire protocol: escape string values inline. `clientupdate ${parts.join(" ")}`,
// sendCommandNoWait: the TS3 full-client protocol often );
// doesn't return a timely error response for clientupdate,
// causing execCommand to time out after 10s. this.logger.info(
const parts = Object.entries(rawProps).map(([k, v]) => {
typeof v === "string" ? `${k}=${escapeTS3(v)}` : `${k}=${v}`, clid: this.tsClient.getClientId(),
); props: Object.keys(rawProps),
await this.tsClient.sendCommandNoWait(`clientupdate ${parts.join(" ")}`); },
this.logger.info( "Client properties updated (nickname + away)",
{ props: Object.keys(rawProps) }, );
"Client properties updated (nickname + away)",
);
}
} catch (err) { } catch (err) {
// Flag both features on permission error
this.handleFeatureError("nickname", err); this.handleFeatureError("nickname", err);
this.handleFeatureError("awayStatus", err); this.handleFeatureError("awayStatus", err);
} }
@@ -439,30 +441,118 @@ export class BotProfileManager {
targetChannelId?: bigint, targetChannelId?: bigint,
): Promise<void> { ): Promise<void> {
if (!this.config.channelDescEnabled || this.permDenied.channelDesc) return; if (!this.config.channelDescEnabled || this.permDenied.channelDesc) return;
try { try {
const channelId = targetChannelId ?? this.tsClient.getChannelId(); let channelId = targetChannelId ?? this.tsClient.getChannelId();
// TS6 full-client may report channelID() as 0 even after the
// visible music client has already joined a channel.
// Fall back to HTTP Query and resolve our real clid -> cid.
if (channelId === 0n) {
const httpQuery = this.tsClient.getHttpQuery();
const clid = this.tsClient.getClientId();
if (httpQuery && clid > 0) {
const result = await httpQuery.clientList();
const payload = result.body as {
body?: Array<Record<string, string>>;
};
const me = payload.body?.find(
(client) => Number(client.clid) === clid,
);
if (me?.cid) {
channelId = BigInt(me.cid);
this.logger.info(
{
clid,
cid: channelId.toString(),
},
"Resolved channel ID via HTTP Query",
);
}
}
}
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; const target = this.channelDescCid ?? channelId;
if (target === 0n) return; // unknown channel
await this.tsClient.sendCommandNoWait( if (target === 0n) return;
`channeledit cid=${target} channel_description=`,
); const httpQuery = this.tsClient.getHttpQuery();
if (httpQuery) {
const result = await httpQuery.channelEdit(Number(target), {
channel_description: "",
});
this.logger.info(
{
status: result.status,
cid: target.toString(),
},
"Channel description cleared",
);
} else {
await this.withTimeout(
this.tsClient.execCommand(
`channeledit cid=${target} channel_description=`,
),
5000,
);
this.logger.info(
{ cid: target.toString() },
"Channel description cleared",
);
}
this.channelDescCid = null; this.channelDescCid = null;
return; return;
} }
if (channelId === 0n) return; // unknown channel
if (channelId === 0n) return;
const lines = [ const lines = [
`\u266A \u6B63\u5728\u64AD\u653E: ${song.name} - ${song.artist}`, // ♪ 正在播放: `♪ 正在播放: ${song.name} - ${song.artist}`,
`\u4E13\u8F91: ${song.album}`, // 专辑: `专辑: ${song.album}`,
`\u5E73\u53F0: ${song.platform}`, // 平台: `平台: ${song.platform}`,
]; ];
const desc = lines.join("\\n");
await this.tsClient.sendCommandNoWait( // HTTP Query uses a normal JSON string, so use real newlines here.
`channeledit cid=${channelId} channel_description=${escapeTS3(desc)}`, const desc = lines.join("\n");
);
const httpQuery = this.tsClient.getHttpQuery();
if (httpQuery) {
const result = await httpQuery.channelEdit(Number(channelId), {
channel_description: desc,
});
this.logger.info(
{
status: result.status,
cid: channelId.toString(),
},
"Channel description updated",
);
} else {
await this.withTimeout(
this.tsClient.execCommand(
`channeledit cid=${channelId} channel_description=${escapeTS3(desc)}`,
),
5000,
);
this.logger.info(
{ cid: channelId.toString() },
"Channel description updated",
);
}
this.channelDescCid = channelId; this.channelDescCid = channelId;
} catch (err) { } catch (err) {
this.handleFeatureError("channelDesc", err); this.handleFeatureError("channelDesc", err);
+45
View File
@@ -209,6 +209,51 @@ export class TS6HttpQuery {
return result; return result;
} }
/**
* Edit a specific connected client.
*
* clientUpdate() modifies the HTTP Query client itself.
* clientEdit() explicitly targets the supplied clid.
*/
async clientEdit(
clid: number,
properties: Record<string, string | number>,
sid = 1,
): Promise<HttpQueryResult> {
const path = `/1/clientedit?sid=${sid}`;
const result = await this.request("POST", path, {
clid,
...properties,
});
if (result.status < 200 || result.status >= 300) {
throw new HttpQueryError(path, result.status, result.body);
}
return result;
}
/**
* Edit a specific channel.
*/
async channelEdit(
cid: number,
properties: Record<string, string | number>,
sid = 1,
): Promise<HttpQueryResult> {
const path = `/1/channeledit?sid=${sid}`;
const result = await this.request("POST", path, {
cid,
...properties,
});
if (result.status < 200 || result.status >= 300) {
throw new HttpQueryError(path, result.status, result.body);
}
return result;
}
/** Move a client to a channel */ /** Move a client to a channel */
async clientMove( async clientMove(
clid: number, clid: number,