mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-04 22:12:49 +08:00
fix(ts6): update the real music client profile
This commit is contained in:
1 parent
87fca6d8b7
commit
af4ca56fd3
2 files changed
+187
-52
No files matched your search
+142
-52
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user