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

@ -6,6 +6,7 @@ export const WHATSAPP_DESCRIPTOR = Object.freeze({
label: 'WhatsApp',
// Translated lazily: t() must run after setImHostLanguage, not at import time.
get connectionLabel() { return t(' Web 关联设备'); },
reactions: Object.freeze({ processing: '👀', success: '✅', error: '❌' }),
});
export class WhatsappHarnessBridge extends TextHarnessBridge {

View file

@ -246,6 +246,7 @@ export function normalizeWhatsappMessage(message, accountJid, {
addressed: !group || fromMe || mentioned || replyToSelf,
selfChat,
replyTarget: { jid: remoteJid, quoted: message, selfChat },
reactionTarget: { jid: remoteJid, key: message.key },
};
}
@ -411,6 +412,41 @@ export class WhatsappBotClient {
}, 'image');
}
async addReaction(target, emoji, { signal } = {}) {
if (typeof emoji !== 'string' || !emoji.trim()) {
throw new TypeError('A WhatsApp reaction emoji is required');
}
const reactionKey = emoji.trim();
await this.#sendReaction(target, reactionKey, signal);
return reactionKey;
}
removeReaction(target, _reactionKey, { signal } = {}) {
return this.#sendReaction(target, '', signal);
}
async #sendReaction(target, text, signal) {
if (typeof target?.jid !== 'string' || !target.jid || !target.key?.id) {
throw new TypeError('A WhatsApp reaction target is required');
}
const operationSignal = signal ?? this.#signal;
operationSignal?.throwIfAborted();
const messageId = randomBytes(10).toString('hex').toUpperCase();
if (typeof this.#outboundIds.reserve === 'function') {
this.#outboundIds.reserve(messageId);
} else {
this.#outboundIds.remember(messageId);
}
const pending = this.#socket.sendMessage(
target.jid,
{ react: { text, key: target.key } },
{ messageId },
);
const result = await waitWithSignal(pending, operationSignal);
this.#outboundIds.remember(result?.key?.id);
return result;
}
async #sendArtifact(target, file, content, presentation) {
this.#signal?.throwIfAborted();
await this.#stopTyping(target.jid);