whatsapp-pi 1.0.61 → 1.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/i18n.ts +15 -0
- package/src/services/incoming-message.resolver.ts +17 -0
- package/src/services/reaction.sender.ts +65 -0
- package/whatsapp-pi.ts +79 -40
package/package.json
CHANGED
package/src/i18n.ts
CHANGED
|
@@ -189,6 +189,11 @@ const fallback = {
|
|
|
189
189
|
"incoming.media.contact": "[Contact]",
|
|
190
190
|
"incoming.media.location": "[Location]",
|
|
191
191
|
"incoming.media.unsupported": "[Unsupported Message Type: {typeName}]",
|
|
192
|
+
"incoming.media.reaction": "{emoji} Reacted to message",
|
|
193
|
+
"incoming.media.reactionRemoved": "Removed reaction",
|
|
194
|
+
"tool.sendReaction.label": "Send WhatsApp Reaction",
|
|
195
|
+
"tool.sendReaction.description": "React to a WhatsApp message with an emoji",
|
|
196
|
+
"tool.sendReaction.error.invalidEmoji": "Invalid emoji provided",
|
|
192
197
|
"incoming.media.audioTranscribing": "[WhatsApp-Pi] Transcribing audio from {pushName}...",
|
|
193
198
|
"incoming.media.audioTranscribed": "[Transcribed Audio]: {transcription}",
|
|
194
199
|
"incoming.media.imageDownloading": "[WhatsApp-Pi] Downloading image from {pushName}...",
|
|
@@ -387,6 +392,11 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
387
392
|
"incoming.media.documentDownloading": "[WhatsApp-Pi] Baixando documento de {pushName}: {fileName}...",
|
|
388
393
|
"incoming.media.documentSaved": "[WhatsApp-Pi] Documento salvo em {relativePath} ({size} bytes)",
|
|
389
394
|
"incoming.media.documentReceived": "[Documento recebido: {fileName}]",
|
|
395
|
+
"incoming.media.reaction": "{emoji} Reagiu à mensagem",
|
|
396
|
+
"incoming.media.reactionRemoved": "Reação removida",
|
|
397
|
+
"tool.sendReaction.label": "Enviar Reação no WhatsApp",
|
|
398
|
+
"tool.sendReaction.description": "Reagir a uma mensagem do WhatsApp com um emoji",
|
|
399
|
+
"tool.sendReaction.error.invalidEmoji": "Emoji inválido",
|
|
390
400
|
"incoming.media.documentMimeType": "Tipo MIME: {mimeType}",
|
|
391
401
|
"incoming.media.documentSize": "Tamanho: {size}",
|
|
392
402
|
"incoming.media.documentLocation": "Local: {relativePath}",
|
|
@@ -531,6 +541,11 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
531
541
|
"session.manager.failedSaveConfig": "Error al guardar config:",
|
|
532
542
|
"session.manager.invalidNumber": "[SessionManager] Intento de añadir número inválido:",
|
|
533
543
|
"session.manager.failedDeleteAuthState": "Error al eliminar el estado de autenticación:",
|
|
544
|
+
"incoming.media.reaction": "{emoji} Reaccionó al mensaje",
|
|
545
|
+
"incoming.media.reactionRemoved": "Reacción eliminada",
|
|
546
|
+
"tool.sendReaction.label": "Enviar Reacción en WhatsApp",
|
|
547
|
+
"tool.sendReaction.description": "Reaccionar a un mensaje de WhatsApp con un emoji",
|
|
548
|
+
"tool.sendReaction.error.invalidEmoji": "Emoji inválido",
|
|
534
549
|
},
|
|
535
550
|
fr: {
|
|
536
551
|
"tool.label": "Envoyer un message WhatsApp",
|
|
@@ -9,6 +9,7 @@ export type IncomingResolution =
|
|
|
9
9
|
| { kind: 'contact'; text: string }
|
|
10
10
|
| { kind: 'location'; text: string }
|
|
11
11
|
| { kind: 'system'; text: string }
|
|
12
|
+
| { kind: 'reaction'; text: string; reactionMessage: any }
|
|
12
13
|
| { kind: 'unsupported'; text: string };
|
|
13
14
|
|
|
14
15
|
const protocolTypes: Record<number, keyof typeof protocolLabels> = {
|
|
@@ -146,5 +147,21 @@ export const extractIncomingText = (message: any): IncomingResolution => {
|
|
|
146
147
|
return { kind: 'text', text: resolved.templateButtonReplyMessage.selectedDisplayText };
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
if (resolved?.reactionMessage) {
|
|
151
|
+
const emoji = resolved.reactionMessage.text;
|
|
152
|
+
if (emoji) {
|
|
153
|
+
return {
|
|
154
|
+
kind: 'reaction',
|
|
155
|
+
text: t('incoming.media.reaction', { emoji }),
|
|
156
|
+
reactionMessage: resolved.reactionMessage
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
kind: 'reaction',
|
|
161
|
+
text: t('incoming.media.reactionRemoved'),
|
|
162
|
+
reactionMessage: resolved.reactionMessage
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
return { kind: 'unsupported', text: t('incoming.media.unsupported', { typeName }) };
|
|
150
167
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { t } from '../i18n.js';
|
|
2
|
+
|
|
3
|
+
export interface SendReactionOptions {
|
|
4
|
+
jid: string;
|
|
5
|
+
messageId: string;
|
|
6
|
+
emoji: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SendReactionResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
messageId?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Socket interface that matches what we need from Baileys
|
|
16
|
+
interface ReactionSocket {
|
|
17
|
+
sendMessage: (jid: string, content: { react: { text: string; key: { remoteJid: string; id: string; fromMe: boolean } } }) => Promise<{ key?: { id?: string | null } } | undefined>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ReactionSender {
|
|
21
|
+
constructor(private socket: ReactionSocket | null) {}
|
|
22
|
+
|
|
23
|
+
async sendReaction(options: SendReactionOptions): Promise<SendReactionResult> {
|
|
24
|
+
if (!this.socket) {
|
|
25
|
+
return { success: false, error: t('service.whatsapp.notConnected') };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Validate emoji (must be single emoji or empty string to remove)
|
|
29
|
+
if (!this.isValidEmoji(options.emoji)) {
|
|
30
|
+
return { success: false, error: t('tool.sendReaction.error.invalidEmoji') };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const result = await this.socket.sendMessage(options.jid, {
|
|
35
|
+
react: {
|
|
36
|
+
text: options.emoji,
|
|
37
|
+
key: {
|
|
38
|
+
remoteJid: options.jid,
|
|
39
|
+
id: options.messageId,
|
|
40
|
+
fromMe: false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
messageId: result?.key?.id ?? undefined
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private isValidEmoji(emoji: string): boolean {
|
|
58
|
+
// Allow empty string (removes reaction)
|
|
59
|
+
if (emoji === '') return true;
|
|
60
|
+
// Check if single emoji using Unicode emoji regex
|
|
61
|
+
// This matches single Unicode emojis including variation selectors
|
|
62
|
+
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)$/u;
|
|
63
|
+
return emojiRegex.test(emoji);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/whatsapp-pi.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { AudioService } from './src/services/audio.service.js';
|
|
|
8
8
|
import { extractIncomingText } from './src/services/incoming-message.resolver.js';
|
|
9
9
|
import { IncomingMediaService } from './src/services/incoming-media.service.js';
|
|
10
10
|
import { WhatsAppPiLogger } from './src/services/whatsapp-pi.logger.js';
|
|
11
|
+
import { ReactionSender } from './src/services/reaction.sender.js';
|
|
11
12
|
import { initI18n, t } from './src/i18n.js';
|
|
12
13
|
|
|
13
14
|
const shutdownState = globalThis as typeof globalThis & {
|
|
@@ -205,16 +206,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
206
|
ctx.ui.notify('WhatsApp: Session reset via /new is now fully supported.', 'info');
|
|
206
207
|
});
|
|
207
208
|
|
|
208
|
-
// Track whether send_wa_message tool already sent a reply this turn
|
|
209
|
-
let toolSentToJid: string | null = null;
|
|
210
|
-
|
|
211
|
-
const toRecentSenderNumber = (recipientJid: string): string => {
|
|
212
|
-
if (recipientJid.endsWith('@g.us')) {
|
|
213
|
-
return recipientJid;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return `+${recipientJid.split('@')[0]}`;
|
|
217
|
-
};
|
|
209
|
+
// Track whether send_wa_message tool already sent a reply this turn
|
|
210
|
+
let toolSentToJid: string | null = null;
|
|
211
|
+
|
|
212
|
+
const toRecentSenderNumber = (recipientJid: string): string => {
|
|
213
|
+
if (recipientJid.endsWith('@g.us')) {
|
|
214
|
+
return recipientJid;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return `+${recipientJid.split('@')[0]}`;
|
|
218
|
+
};
|
|
218
219
|
|
|
219
220
|
// Handle incoming messages by injecting them as user prompts
|
|
220
221
|
whatsappService.setMessageCallback(async (m) => {
|
|
@@ -326,15 +327,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
327
|
formattedMessage
|
|
327
328
|
].join('\n'));
|
|
328
329
|
|
|
329
|
-
const outboundJid = whatsappService.resolveOutboundRecipientJid(resolvedJid);
|
|
330
|
-
const result = await whatsappService.sendMessage(outboundJid, params.message);
|
|
330
|
+
const outboundJid = whatsappService.resolveOutboundRecipientJid(resolvedJid);
|
|
331
|
+
const result = await whatsappService.sendMessage(outboundJid, params.message);
|
|
331
332
|
|
|
332
333
|
if (result.success) {
|
|
333
334
|
// Mark that tool already sent to this JID — prevents message_end from re-sending
|
|
334
|
-
toolSentToJid = outboundJid;
|
|
335
|
-
await recentsService.recordMessage({
|
|
336
|
-
messageId: result.messageId!,
|
|
337
|
-
senderNumber: toRecentSenderNumber(outboundJid),
|
|
335
|
+
toolSentToJid = outboundJid;
|
|
336
|
+
await recentsService.recordMessage({
|
|
337
|
+
messageId: result.messageId!,
|
|
338
|
+
senderNumber: toRecentSenderNumber(outboundJid),
|
|
338
339
|
text: params.message,
|
|
339
340
|
direction: 'outgoing',
|
|
340
341
|
timestamp: Date.now()
|
|
@@ -362,6 +363,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
362
363
|
}
|
|
363
364
|
});
|
|
364
365
|
|
|
366
|
+
// Register send_reaction tool (LLM-callable)
|
|
367
|
+
pi.registerTool({
|
|
368
|
+
name: "send_reaction",
|
|
369
|
+
label: t("tool.sendReaction.label"),
|
|
370
|
+
description: t("tool.sendReaction.description"),
|
|
371
|
+
promptSnippet: "send_reaction(jid, messageId, emoji) - React to a WhatsApp message with an emoji. The 'jid' is the chat JID (e.g. 5511999998888@s.whatsapp.net), 'messageId' is the ID of the message to react to, and 'emoji' is the emoji to react with (e.g., 👍, ❤️, 😂).",
|
|
372
|
+
parameters: Type.Object({
|
|
373
|
+
jid: Type.String({ description: "WhatsApp JID of the chat (e.g. 5511999998888@s.whatsapp.net or 120363012345@g.us)" }),
|
|
374
|
+
messageId: Type.String({ description: "ID of the message to react to" }),
|
|
375
|
+
emoji: Type.String({ description: "Emoji to react with (e.g., 👍, ❤️, 😂). Use empty string to remove reaction." })
|
|
376
|
+
}),
|
|
377
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
378
|
+
// Get socket from WhatsApp service
|
|
379
|
+
const socket = whatsappService.getSocket();
|
|
380
|
+
if (!socket) {
|
|
381
|
+
return {
|
|
382
|
+
isError: true,
|
|
383
|
+
details: undefined,
|
|
384
|
+
content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: t("service.whatsapp.notConnected") }) }]
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Create sender with the socket
|
|
389
|
+
const sender = new ReactionSender(socket as any);
|
|
390
|
+
const result = await sender.sendReaction({
|
|
391
|
+
jid: params.jid,
|
|
392
|
+
messageId: params.messageId,
|
|
393
|
+
emoji: params.emoji
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
isError: !result.success,
|
|
398
|
+
details: undefined,
|
|
399
|
+
content: [{ type: "text" as const, text: JSON.stringify({ success: result.success, messageId: result.messageId, error: result.error }) }]
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
365
404
|
// Suppress automatic message_end reply when tool already sent
|
|
366
405
|
// This is checked by the message_end handler below
|
|
367
406
|
|
|
@@ -385,10 +424,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
385
424
|
// Handle outgoing messages (Agent -> WhatsApp)
|
|
386
425
|
pi.on("agent_start", async (_event, _ctx) => {
|
|
387
426
|
if (sessionManager.getStatus() !== 'connected') return;
|
|
388
|
-
const lastJid = whatsappService.getLastRemoteJid();
|
|
389
|
-
if (lastJid) {
|
|
390
|
-
await whatsappService.sendPresence(whatsappService.resolveOutboundRecipientJid(lastJid), 'composing');
|
|
391
|
-
}
|
|
427
|
+
const lastJid = whatsappService.getLastRemoteJid();
|
|
428
|
+
if (lastJid) {
|
|
429
|
+
await whatsappService.sendPresence(whatsappService.resolveOutboundRecipientJid(lastJid), 'composing');
|
|
430
|
+
}
|
|
392
431
|
});
|
|
393
432
|
|
|
394
433
|
pi.on("message_end", async (event, ctx) => {
|
|
@@ -396,26 +435,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
396
435
|
|
|
397
436
|
const { message } = event;
|
|
398
437
|
// Only reply if it's the assistant and we have a valid target
|
|
399
|
-
if (message.role === "assistant") {
|
|
400
|
-
const lastJid = whatsappService.getLastRemoteJid();
|
|
401
|
-
const text = message.content.filter(c => c.type === "text").map(c => c.text).join("\n");
|
|
402
|
-
const outboundJid = lastJid
|
|
403
|
-
? whatsappService.resolveOutboundRecipientJid(lastJid)
|
|
404
|
-
: null;
|
|
405
|
-
|
|
406
|
-
// Skip if send_wa_message tool already sent a reply to this JID
|
|
407
|
-
if (toolSentToJid === outboundJid) {
|
|
408
|
-
toolSentToJid = null;
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (outboundJid && text) {
|
|
413
|
-
try {
|
|
414
|
-
const result = await whatsappService.sendMessage(outboundJid, text);
|
|
415
|
-
if (result.success) {
|
|
416
|
-
await recentsService.recordMessage({
|
|
417
|
-
messageId: result.messageId ?? `${Date.now()}`,
|
|
418
|
-
senderNumber: toRecentSenderNumber(outboundJid),
|
|
438
|
+
if (message.role === "assistant") {
|
|
439
|
+
const lastJid = whatsappService.getLastRemoteJid();
|
|
440
|
+
const text = message.content.filter(c => c.type === "text").map(c => c.text).join("\n");
|
|
441
|
+
const outboundJid = lastJid
|
|
442
|
+
? whatsappService.resolveOutboundRecipientJid(lastJid)
|
|
443
|
+
: null;
|
|
444
|
+
|
|
445
|
+
// Skip if send_wa_message tool already sent a reply to this JID
|
|
446
|
+
if (toolSentToJid === outboundJid) {
|
|
447
|
+
toolSentToJid = null;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (outboundJid && text) {
|
|
452
|
+
try {
|
|
453
|
+
const result = await whatsappService.sendMessage(outboundJid, text);
|
|
454
|
+
if (result.success) {
|
|
455
|
+
await recentsService.recordMessage({
|
|
456
|
+
messageId: result.messageId ?? `${Date.now()}`,
|
|
457
|
+
senderNumber: toRecentSenderNumber(outboundJid),
|
|
419
458
|
text,
|
|
420
459
|
direction: 'outgoing',
|
|
421
460
|
timestamp: Date.now()
|