talon-agent 3.24.1 → 3.25.1
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/README.md +123 -25
- package/package.json +4 -1
- package/prompts/whatsapp.md +58 -0
- package/src/backend/shared/delivery-contract.ts +1 -0
- package/src/core/doctor.ts +5 -0
- package/src/core/frontend-runtime/builtins.ts +8 -0
- package/src/core/mcp-hub/talon-server.ts +1 -0
- package/src/core/prompt/embedded-prompts.ts +2 -0
- package/src/core/tools/chat.ts +4 -4
- package/src/core/tools/history.ts +5 -5
- package/src/core/tools/media.ts +1 -1
- package/src/core/tools/members.ts +4 -4
- package/src/core/tools/messaging.ts +12 -11
- package/src/core/tools/moderation.ts +2 -2
- package/src/core/tools/scheduling.ts +2 -2
- package/src/core/tools/types.ts +1 -1
- package/src/frontend/factories.ts +1 -0
- package/src/frontend/whatsapp/actions/chat-info.ts +228 -0
- package/src/frontend/whatsapp/actions/index.ts +78 -0
- package/src/frontend/whatsapp/actions/media.ts +248 -0
- package/src/frontend/whatsapp/actions/messaging.ts +281 -0
- package/src/frontend/whatsapp/actions/moderation.ts +231 -0
- package/src/frontend/whatsapp/actions/shared.ts +196 -0
- package/src/frontend/whatsapp/actions/types.ts +34 -0
- package/src/frontend/whatsapp/factory.ts +12 -0
- package/src/frontend/whatsapp/formatting.ts +172 -0
- package/src/frontend/whatsapp/identity.ts +150 -0
- package/src/frontend/whatsapp/index.ts +553 -0
- package/src/frontend/whatsapp/media-store.ts +97 -0
- package/src/frontend/whatsapp/message-store.ts +140 -0
- package/src/frontend/whatsapp/pins.ts +52 -0
- package/src/frontend/whatsapp/registry.ts +92 -0
- package/src/util/chat-id.ts +5 -0
- package/src/util/config.ts +43 -0
- package/src/util/log.ts +1 -0
- package/src/util/paths.ts +2 -0
|
@@ -110,7 +110,7 @@ Notes:
|
|
|
110
110
|
// src/backend/claude-sdk/options.ts:turnTerminatorHook.
|
|
111
111
|
return throwIfFailed(result, "end_turn");
|
|
112
112
|
},
|
|
113
|
-
frontends: ["telegram", "teams", "discord", "native"],
|
|
113
|
+
frontends: ["telegram", "teams", "discord", "native", "whatsapp"],
|
|
114
114
|
tag: "messaging",
|
|
115
115
|
endsTurn: true,
|
|
116
116
|
delivery: true,
|
|
@@ -473,7 +473,7 @@ Examples:
|
|
|
473
473
|
return { ok: false, error: `Unknown type: ${type}` };
|
|
474
474
|
}
|
|
475
475
|
},
|
|
476
|
-
frontends: ["telegram", "discord"],
|
|
476
|
+
frontends: ["telegram", "discord", "whatsapp"],
|
|
477
477
|
tag: "messaging",
|
|
478
478
|
delivery: true,
|
|
479
479
|
},
|
|
@@ -490,7 +490,7 @@ Examples:
|
|
|
490
490
|
text: z.string().describe("Message text. Supports Markdown."),
|
|
491
491
|
},
|
|
492
492
|
execute: (params, bridge) => bridge("send_message", params),
|
|
493
|
-
frontends: ["teams", "native"],
|
|
493
|
+
frontends: ["teams", "native", "whatsapp"],
|
|
494
494
|
tag: "messaging",
|
|
495
495
|
delivery: true,
|
|
496
496
|
},
|
|
@@ -515,7 +515,7 @@ Example: send_message_with_buttons(text="Choose:", rows=[[{"text":"Docs","url":"
|
|
|
515
515
|
.describe("Button rows"),
|
|
516
516
|
},
|
|
517
517
|
execute: (params, bridge) => bridge("send_message_with_buttons", params),
|
|
518
|
-
frontends: ["teams", "native"],
|
|
518
|
+
frontends: ["teams", "native", "whatsapp"],
|
|
519
519
|
tag: "messaging",
|
|
520
520
|
delivery: true,
|
|
521
521
|
},
|
|
@@ -572,7 +572,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
572
572
|
const result = await bridge("react", rest);
|
|
573
573
|
return throwIfFailed(result, "react");
|
|
574
574
|
},
|
|
575
|
-
frontends: ["telegram", "discord", "native"],
|
|
575
|
+
frontends: ["telegram", "discord", "native", "whatsapp"],
|
|
576
576
|
tag: "messaging",
|
|
577
577
|
endsTurn: true,
|
|
578
578
|
delivery: true,
|
|
@@ -592,7 +592,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
592
592
|
.describe("Edit the media caption rather than message text (Telegram)"),
|
|
593
593
|
},
|
|
594
594
|
execute: (params, bridge) => bridge("edit_message", params),
|
|
595
|
-
frontends: ["telegram", "discord", "native"],
|
|
595
|
+
frontends: ["telegram", "discord", "native", "whatsapp"],
|
|
596
596
|
tag: "messaging",
|
|
597
597
|
},
|
|
598
598
|
|
|
@@ -609,7 +609,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
609
609
|
.describe("Bulk delete these message IDs (Telegram)"),
|
|
610
610
|
},
|
|
611
611
|
execute: (params, bridge) => bridge("delete_message", params),
|
|
612
|
-
frontends: ["telegram", "discord", "native"],
|
|
612
|
+
frontends: ["telegram", "discord", "native", "whatsapp"],
|
|
613
613
|
tag: "messaging",
|
|
614
614
|
},
|
|
615
615
|
|
|
@@ -632,7 +632,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
632
632
|
.describe("Destination chat (default: current chat)"),
|
|
633
633
|
},
|
|
634
634
|
execute: (params, bridge) => bridge("forward_message", params),
|
|
635
|
-
frontends: ["telegram", "discord"],
|
|
635
|
+
frontends: ["telegram", "discord", "whatsapp"],
|
|
636
636
|
tag: "messaging",
|
|
637
637
|
},
|
|
638
638
|
|
|
@@ -648,7 +648,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
648
648
|
to_chat_id: chatIdSchema.optional(),
|
|
649
649
|
},
|
|
650
650
|
execute: (params, bridge) => bridge("copy_message", params),
|
|
651
|
-
frontends: ["telegram"],
|
|
651
|
+
frontends: ["telegram", "whatsapp"],
|
|
652
652
|
tag: "messaging",
|
|
653
653
|
},
|
|
654
654
|
|
|
@@ -658,7 +658,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
658
658
|
description: "Pin a message.",
|
|
659
659
|
schema: { message_id: snowflakeOrIdSchema },
|
|
660
660
|
execute: (params, bridge) => bridge("pin_message", params),
|
|
661
|
-
frontends: ["telegram", "discord"],
|
|
661
|
+
frontends: ["telegram", "discord", "whatsapp"],
|
|
662
662
|
tag: "messaging",
|
|
663
663
|
},
|
|
664
664
|
|
|
@@ -668,7 +668,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
668
668
|
description: "Unpin a message.",
|
|
669
669
|
schema: { message_id: snowflakeOrIdSchema.optional() },
|
|
670
670
|
execute: (params, bridge) => bridge("unpin_message", params),
|
|
671
|
-
frontends: ["telegram", "discord"],
|
|
671
|
+
frontends: ["telegram", "discord", "whatsapp"],
|
|
672
672
|
tag: "messaging",
|
|
673
673
|
},
|
|
674
674
|
|
|
@@ -681,6 +681,7 @@ Valid emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉
|
|
|
681
681
|
message_id: idSchema.describe("Message ID of the poll to stop"),
|
|
682
682
|
},
|
|
683
683
|
execute: (params, bridge) => bridge("stop_poll", params),
|
|
684
|
+
// No WhatsApp equivalent: its polls run until the message is deleted.
|
|
684
685
|
frontends: ["telegram"],
|
|
685
686
|
tag: "messaging",
|
|
686
687
|
},
|
|
@@ -112,7 +112,7 @@ Forum topics (supergroups with topics):
|
|
|
112
112
|
.describe("Target chat ID. Omit for the current chat."),
|
|
113
113
|
},
|
|
114
114
|
execute: (params, bridge) => bridge("moderate", params),
|
|
115
|
-
frontends: ["telegram"],
|
|
115
|
+
frontends: ["telegram", "whatsapp"],
|
|
116
116
|
tag: "moderation",
|
|
117
117
|
},
|
|
118
118
|
|
|
@@ -125,7 +125,7 @@ Forum topics (supergroups with topics):
|
|
|
125
125
|
limit: z.number().optional().describe("Max photos to return (default 5)"),
|
|
126
126
|
},
|
|
127
127
|
execute: (params, bridge) => bridge("get_user_profile_photos", params),
|
|
128
|
-
frontends: ["telegram"],
|
|
128
|
+
frontends: ["telegram", "whatsapp"],
|
|
129
129
|
tag: "moderation",
|
|
130
130
|
},
|
|
131
131
|
];
|
|
@@ -11,7 +11,7 @@ export const schedulingTools: ToolDefinition[] = [
|
|
|
11
11
|
description: "Cancel a scheduled message.",
|
|
12
12
|
schema: { schedule_id: z.string() },
|
|
13
13
|
execute: (params, bridge) => bridge("cancel_scheduled", params),
|
|
14
|
-
frontends: ["telegram", "discord"],
|
|
14
|
+
frontends: ["telegram", "discord", "whatsapp"],
|
|
15
15
|
tag: "scheduling",
|
|
16
16
|
},
|
|
17
17
|
|
|
@@ -21,7 +21,7 @@ export const schedulingTools: ToolDefinition[] = [
|
|
|
21
21
|
"List pending scheduled messages in this chat (id, seconds until send, text). Scheduled messages persist across restarts.",
|
|
22
22
|
schema: {},
|
|
23
23
|
execute: (_params, bridge) => bridge("list_scheduled", {}),
|
|
24
|
-
frontends: ["telegram", "discord"],
|
|
24
|
+
frontends: ["telegram", "discord", "whatsapp"],
|
|
25
25
|
tag: "scheduling",
|
|
26
26
|
},
|
|
27
27
|
|
package/src/core/tools/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { ZodRawShape } from "zod";
|
|
|
9
9
|
|
|
10
10
|
/** Which frontends a tool is available on. "all" = every frontend. */
|
|
11
11
|
export type ToolFrontend =
|
|
12
|
-
"telegram" | "teams" | "terminal" | "discord" | "native" | "all";
|
|
12
|
+
"telegram" | "teams" | "terminal" | "discord" | "native" | "whatsapp" | "all";
|
|
13
13
|
|
|
14
14
|
/** Domain tags for runtime filtering and grouping. */
|
|
15
15
|
export type ToolTag =
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat- and member-inspection actions.
|
|
3
|
+
*
|
|
4
|
+
* Group data comes from `groupMetadata`, which WhatsApp serves per JID
|
|
5
|
+
* and Baileys caches; DMs have no metadata endpoint, so their answers are
|
|
6
|
+
* assembled from the registry and the profile endpoints.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { downloadMediaMessage } from "baileys";
|
|
10
|
+
import { lookupMessage } from "../message-store.js";
|
|
11
|
+
import { toUserJid, tryAction } from "./shared.js";
|
|
12
|
+
import type { WhatsAppActionHandlers } from "./types.js";
|
|
13
|
+
|
|
14
|
+
/** Render one participant the way the other frontends render members. */
|
|
15
|
+
function describeParticipant(p: {
|
|
16
|
+
id: string;
|
|
17
|
+
admin?: string | null;
|
|
18
|
+
name?: string | null;
|
|
19
|
+
}): string {
|
|
20
|
+
const role =
|
|
21
|
+
p.admin === "superadmin" ? " (owner)" : p.admin ? " (admin)" : "";
|
|
22
|
+
const handle = p.id.split("@")[0];
|
|
23
|
+
return p.name ? `${p.name} [${handle}]${role}` : `${handle}${role}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const chatInfoHandlers: WhatsAppActionHandlers = {
|
|
27
|
+
get_chat_info: (_body, chatId, ctx) =>
|
|
28
|
+
tryAction("get_chat_info", async () => {
|
|
29
|
+
const chat = ctx.chat!;
|
|
30
|
+
if (!chat.isGroup) {
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
id: chatId,
|
|
34
|
+
type: "private",
|
|
35
|
+
title: chat.title ?? chat.jid.split("@")[0],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const meta = await ctx.sock.groupMetadata(chat.jid);
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
id: chatId,
|
|
42
|
+
type: "group",
|
|
43
|
+
title: meta.subject,
|
|
44
|
+
description: meta.desc ?? undefined,
|
|
45
|
+
member_count: meta.participants.length,
|
|
46
|
+
owner: meta.owner ?? undefined,
|
|
47
|
+
created: meta.creation ?? undefined,
|
|
48
|
+
announce_only: Boolean(meta.announce),
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
get_chat_admins: (_body, _chatId, ctx) =>
|
|
53
|
+
tryAction("get_chat_admins", async () => {
|
|
54
|
+
const chat = ctx.chat!;
|
|
55
|
+
if (!chat.isGroup) {
|
|
56
|
+
return { ok: true, text: "Direct message — no admins." };
|
|
57
|
+
}
|
|
58
|
+
const meta = await ctx.sock.groupMetadata(chat.jid);
|
|
59
|
+
const admins = meta.participants.filter((p) => p.admin);
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
text: admins.length
|
|
63
|
+
? admins.map(describeParticipant).join("\n")
|
|
64
|
+
: "No admins.",
|
|
65
|
+
};
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
get_chat_member_count: (_body, _chatId, ctx) =>
|
|
69
|
+
tryAction("get_chat_member_count", async () => {
|
|
70
|
+
const chat = ctx.chat!;
|
|
71
|
+
if (!chat.isGroup) return { ok: true, count: 2, text: "2" };
|
|
72
|
+
const meta = await ctx.sock.groupMetadata(chat.jid);
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
count: meta.participants.length,
|
|
76
|
+
text: String(meta.participants.length),
|
|
77
|
+
};
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
list_chat_members: (body, _chatId, ctx) =>
|
|
81
|
+
tryAction("list_chat_members", async () => {
|
|
82
|
+
const chat = ctx.chat!;
|
|
83
|
+
if (!chat.isGroup) {
|
|
84
|
+
return { ok: true, text: "Direct message — just the two of you." };
|
|
85
|
+
}
|
|
86
|
+
const limit = Math.min(500, Number(body.limit ?? 100));
|
|
87
|
+
const meta = await ctx.sock.groupMetadata(chat.jid);
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
text: meta.participants
|
|
91
|
+
.slice(0, limit)
|
|
92
|
+
.map(describeParticipant)
|
|
93
|
+
.join("\n"),
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
get_member_info: (body, _chatId, ctx) =>
|
|
98
|
+
tryAction("get_member_info", async () => {
|
|
99
|
+
const jid = toUserJid(body.user_id ?? body.user_name);
|
|
100
|
+
if (!jid)
|
|
101
|
+
return { ok: false, error: "get_member_info: user_id required" };
|
|
102
|
+
const chat = ctx.chat!;
|
|
103
|
+
const lines: string[] = [`jid: ${jid}`];
|
|
104
|
+
if (chat.isGroup) {
|
|
105
|
+
const meta = await ctx.sock.groupMetadata(chat.jid);
|
|
106
|
+
const participant = meta.participants.find(
|
|
107
|
+
(p) => p.id.split("@")[0] === jid.split("@")[0],
|
|
108
|
+
);
|
|
109
|
+
if (!participant) {
|
|
110
|
+
return { ok: false, error: `${jid} is not in this group` };
|
|
111
|
+
}
|
|
112
|
+
lines.push(
|
|
113
|
+
`role: ${participant.admin === "superadmin" ? "owner" : (participant.admin ?? "member")}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
// Profile lookups are best-effort: privacy settings routinely hide
|
|
117
|
+
// both, and a missing status is not an error.
|
|
118
|
+
const status: unknown = await ctx.sock
|
|
119
|
+
.fetchStatus(jid)
|
|
120
|
+
.catch(() => undefined);
|
|
121
|
+
// fetchStatus has returned both a bare object and a one-element
|
|
122
|
+
// array across Baileys versions; accept either shape.
|
|
123
|
+
const entry = (Array.isArray(status) ? status[0] : status) as
|
|
124
|
+
{ status?: string | { status?: string } } | undefined;
|
|
125
|
+
const statusText =
|
|
126
|
+
typeof entry?.status === "string"
|
|
127
|
+
? entry.status
|
|
128
|
+
: entry?.status?.status;
|
|
129
|
+
if (statusText) lines.push(`about: ${statusText}`);
|
|
130
|
+
const picture = await ctx.sock
|
|
131
|
+
.profilePictureUrl(jid, "image")
|
|
132
|
+
.catch(() => undefined);
|
|
133
|
+
if (picture) lines.push(`photo: ${picture}`);
|
|
134
|
+
return { ok: true, text: lines.join("\n") };
|
|
135
|
+
}),
|
|
136
|
+
|
|
137
|
+
get_user_profile_photos: (body, _chatId, ctx) =>
|
|
138
|
+
tryAction("get_user_profile_photos", async () => {
|
|
139
|
+
const jid = toUserJid(body.user_id) ?? ctx.chat!.jid;
|
|
140
|
+
const url = await ctx.sock
|
|
141
|
+
.profilePictureUrl(jid, "image")
|
|
142
|
+
.catch(() => undefined);
|
|
143
|
+
return url
|
|
144
|
+
? { ok: true, text: url }
|
|
145
|
+
: {
|
|
146
|
+
ok: true,
|
|
147
|
+
text: "No profile photo (or hidden by privacy settings).",
|
|
148
|
+
};
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
/** WhatsApp exposes no presence roster; presence is per-subscription. */
|
|
152
|
+
online_count: (_body, _chatId, ctx) =>
|
|
153
|
+
tryAction("online_count", async () => {
|
|
154
|
+
const chat = ctx.chat!;
|
|
155
|
+
if (!chat.isGroup) {
|
|
156
|
+
return { ok: true, text: "Presence is only visible per contact." };
|
|
157
|
+
}
|
|
158
|
+
const meta = await ctx.sock.groupMetadata(chat.jid);
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
text:
|
|
162
|
+
`WhatsApp does not report a group online count; ` +
|
|
163
|
+
`${meta.participants.length} members total.`,
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
166
|
+
|
|
167
|
+
set_chat_title: (body, _chatId, ctx) =>
|
|
168
|
+
tryAction("set_chat_title", async () => {
|
|
169
|
+
const chat = ctx.chat!;
|
|
170
|
+
if (!chat.isGroup) {
|
|
171
|
+
return { ok: false, error: "Only group subjects can be changed" };
|
|
172
|
+
}
|
|
173
|
+
const title = String(body.title ?? "").trim();
|
|
174
|
+
if (!title) return { ok: false, error: "set_chat_title: title required" };
|
|
175
|
+
await ctx.sock.groupUpdateSubject(chat.jid, title);
|
|
176
|
+
chat.title = title;
|
|
177
|
+
return { ok: true };
|
|
178
|
+
}),
|
|
179
|
+
|
|
180
|
+
set_chat_description: (body, _chatId, ctx) =>
|
|
181
|
+
tryAction("set_chat_description", async () => {
|
|
182
|
+
const chat = ctx.chat!;
|
|
183
|
+
if (!chat.isGroup) {
|
|
184
|
+
return { ok: false, error: "Only groups have a description" };
|
|
185
|
+
}
|
|
186
|
+
await ctx.sock.groupUpdateDescription(
|
|
187
|
+
chat.jid,
|
|
188
|
+
String(body.description ?? ""),
|
|
189
|
+
);
|
|
190
|
+
return { ok: true };
|
|
191
|
+
}),
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Re-download a message's media on demand. Inbound media is already
|
|
195
|
+
* saved to the workspace as it arrives; this covers anything the model
|
|
196
|
+
* wants to pull again by id.
|
|
197
|
+
*/
|
|
198
|
+
download_media: (body, _chatId, ctx) =>
|
|
199
|
+
tryAction("download_media", async () => {
|
|
200
|
+
const raw = body.message_id;
|
|
201
|
+
const msgId = typeof raw === "number" ? raw : Number(raw);
|
|
202
|
+
const stored = Number.isFinite(msgId) ? lookupMessage(msgId) : undefined;
|
|
203
|
+
if (!stored || stored.chatId !== ctx.chat!.chatId) {
|
|
204
|
+
return { ok: false, error: `Unknown message_id ${String(raw)}` };
|
|
205
|
+
}
|
|
206
|
+
if (!stored.message) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error: `Message ${msgId} is no longer retained in full`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const { saveInboundMedia } = await import("../media-store.js");
|
|
213
|
+
const saved = await saveInboundMedia(
|
|
214
|
+
stored.message,
|
|
215
|
+
ctx.chat!.chatId,
|
|
216
|
+
msgId,
|
|
217
|
+
stored.senderName,
|
|
218
|
+
);
|
|
219
|
+
return saved
|
|
220
|
+
? { ok: true, text: saved.filePath, file_path: saved.filePath }
|
|
221
|
+
: {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: `Message ${msgId} carries no downloadable media`,
|
|
224
|
+
};
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export { downloadMediaMessage };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp-specific action handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handles the MCP tool actions that need the WhatsApp socket.
|
|
5
|
+
* Platform-agnostic actions (cron, fetch_url, in-memory history) are
|
|
6
|
+
* served by core/engine/gateway-actions before this is reached, and any
|
|
7
|
+
* action this module doesn't claim returns null so that fallback still
|
|
8
|
+
* applies — which is how `read_history` and friends work here without a
|
|
9
|
+
* WhatsApp-specific implementation.
|
|
10
|
+
*
|
|
11
|
+
* Split by responsibility (mirrors the telegram/discord actions/ layout):
|
|
12
|
+
* - shared — tryAction, media/quote resolution, send helpers
|
|
13
|
+
* - messaging — text, replies, reactions, edits, deletes, pins, presence
|
|
14
|
+
* - media — images, video, audio, documents, polls, locations, …
|
|
15
|
+
* - chat-info — group metadata, members, titles, media download
|
|
16
|
+
* - moderation — the `moderate` op switch
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { WASocket } from "baileys";
|
|
20
|
+
import type { Gateway } from "../../../core/engine/gateway.js";
|
|
21
|
+
import type { ActionResult } from "../../../core/types.js";
|
|
22
|
+
import { lookupWhatsAppChat } from "../registry.js";
|
|
23
|
+
import { chatInfoHandlers } from "./chat-info.js";
|
|
24
|
+
import { mediaHandlers } from "./media.js";
|
|
25
|
+
import { messagingHandlers } from "./messaging.js";
|
|
26
|
+
import { moderationHandlers } from "./moderation.js";
|
|
27
|
+
import type { WhatsAppActionContext, WhatsAppActionHandlers } from "./types.js";
|
|
28
|
+
|
|
29
|
+
// Null-prototype so a request `action` of "toString" / "constructor" can't
|
|
30
|
+
// resolve an inherited Object.prototype method via `handlers[action]`.
|
|
31
|
+
const handlers: WhatsAppActionHandlers = Object.assign(Object.create(null), {
|
|
32
|
+
...messagingHandlers,
|
|
33
|
+
...mediaHandlers,
|
|
34
|
+
...chatInfoHandlers,
|
|
35
|
+
...moderationHandlers,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Actions that operate on the store alone and need no resolved chat. */
|
|
39
|
+
const CHATLESS_ACTIONS = new Set(["cancel_scheduled", "list_scheduled"]);
|
|
40
|
+
|
|
41
|
+
export function createWhatsAppActionHandler(
|
|
42
|
+
getSock: () => WASocket | null,
|
|
43
|
+
gateway: Gateway,
|
|
44
|
+
) {
|
|
45
|
+
const scheduledMessages = new Map<string, ReturnType<typeof setTimeout>>();
|
|
46
|
+
|
|
47
|
+
return async (
|
|
48
|
+
body: Record<string, unknown>,
|
|
49
|
+
chatId: number,
|
|
50
|
+
): Promise<ActionResult | null> => {
|
|
51
|
+
const action = body.action as string;
|
|
52
|
+
const handler = handlers[action];
|
|
53
|
+
if (!handler) return null; // not a WhatsApp action — let the core try
|
|
54
|
+
|
|
55
|
+
const sock = getSock();
|
|
56
|
+
if (!sock) {
|
|
57
|
+
return { ok: false, error: "WhatsApp socket is not connected" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const chat = lookupWhatsAppChat(chatId) ?? null;
|
|
61
|
+
if (!chat && !CHATLESS_ACTIONS.has(action)) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
error:
|
|
65
|
+
`No WhatsApp chat known for id ${chatId} — Talon learns a chat's ` +
|
|
66
|
+
`JID from its first message.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ctx: WhatsAppActionContext = {
|
|
71
|
+
sock,
|
|
72
|
+
gateway,
|
|
73
|
+
chat,
|
|
74
|
+
scheduledMessages,
|
|
75
|
+
};
|
|
76
|
+
return handler(body, chatId, ctx);
|
|
77
|
+
};
|
|
78
|
+
}
|