feat: add status reactions across messaging channels

This commit is contained in:
xmanrui 2026-08-25 17:12:28 +08:00
parent aa8fd71b93
commit da01e3acee
30 changed files with 1853 additions and 274 deletions

View file

@ -88,6 +88,12 @@ function snowflake(value, name) {
return id;
}
function reactionEmoji(value) {
const emoji = cleanString(value);
if (!emoji) throw new TypeError('A Discord reaction emoji is required');
return emoji;
}
export function validDiscordToken(value) {
return typeof value === 'string'
&& /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}$/.test(value.trim());
@ -229,6 +235,35 @@ export class DiscordApi {
});
}
async addOwnReaction({ channelId, messageId, emoji, signal } = {}) {
const normalizedEmoji = reactionEmoji(emoji);
await this.#request(
`channels/${snowflake(channelId, 'channel id')}/messages/${snowflake(messageId, 'message id')}`
+ `/reactions/${encodeURIComponent(normalizedEmoji)}/@me`,
{
method: 'PUT',
signal,
expectBody: false,
retry: false,
},
);
return normalizedEmoji;
}
removeOwnReaction({ channelId, messageId, emoji, signal } = {}) {
const normalizedEmoji = reactionEmoji(emoji);
return this.#request(
`channels/${snowflake(channelId, 'channel id')}/messages/${snowflake(messageId, 'message id')}`
+ `/reactions/${encodeURIComponent(normalizedEmoji)}/@me`,
{
method: 'DELETE',
signal,
expectBody: false,
retry: false,
},
);
}
async #request(path, {
method,
body,

View file

@ -4,6 +4,7 @@ export const DISCORD_DESCRIPTOR = Object.freeze({
key: 'discord',
label: 'Discord',
connectionLabel: ' Gateway 长连接',
reactions: Object.freeze({ processing: '👀', success: '✅', error: '❌' }),
});
export class DiscordHarnessBridge extends TextHarnessBridge {

View file

@ -231,6 +231,10 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
channelId: String(message.channel_id),
replyToMessageId: String(message.id),
},
reactionTarget: {
channelId: String(message.channel_id),
messageId: String(message.id),
},
connectionTestTarget: { channelId: String(message.channel_id) },
};
}
@ -350,6 +354,24 @@ export class DiscordBotClient {
});
}
addReaction(target, emoji, { signal } = {}) {
return this.#api.addOwnReaction({
channelId: target.channelId,
messageId: target.messageId,
emoji,
signal: this.#operationSignal(signal),
});
}
removeReaction(target, reactionKey, { signal } = {}) {
return this.#api.removeOwnReaction({
channelId: target.channelId,
messageId: target.messageId,
emoji: reactionKey,
signal: this.#operationSignal(signal),
});
}
async openStream(target) {
const notice = !this.#deliveredNotices.has(target) && target?.notice
? String(target.notice) : null;
@ -381,6 +403,10 @@ export class DiscordBotClient {
});
return stream.start();
}
#operationSignal(signal) {
return signal ?? this.#signal;
}
}
export function createDiscordRuntimeStatus() {