whats-mcp 0.1.0
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/.dockerignore +12 -0
- package/.gitlab-ci.yml +54 -0
- package/CHANGELOG.md +38 -0
- package/README.md +205 -0
- package/TODO.md +6 -0
- package/config.json +19 -0
- package/package.json +46 -0
- package/src/.env.example +21 -0
- package/src/admin/cli.js +916 -0
- package/src/admin/service.js +271 -0
- package/src/admin/telegram.js +178 -0
- package/src/admin.js +12 -0
- package/src/config.js +147 -0
- package/src/connection.js +334 -0
- package/src/helpers.js +264 -0
- package/src/http_app.js +267 -0
- package/src/index.js +4 -0
- package/src/main.js +71 -0
- package/src/server.js +67 -0
- package/src/store.js +925 -0
- package/src/tools/analytics.js +157 -0
- package/src/tools/channels.js +215 -0
- package/src/tools/chats.js +291 -0
- package/src/tools/contacts.js +259 -0
- package/src/tools/digest.js +249 -0
- package/src/tools/groups.js +529 -0
- package/src/tools/history-support.js +114 -0
- package/src/tools/labels.js +168 -0
- package/src/tools/messaging.js +510 -0
- package/src/tools/overview.js +416 -0
- package/src/tools/profile.js +155 -0
- package/src/tools/registry.js +105 -0
- package/src/tools/tags.js +104 -0
- package/src/tools/utils.js +325 -0
- package/src/tools/watchlists.js +136 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Contact tag management tools (1 tool).
|
|
3
|
+
*
|
|
4
|
+
* manage_contact_tags
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const { phoneToJid, jidToPhone, okResult, errResult } = require("../helpers");
|
|
10
|
+
|
|
11
|
+
module.exports = [
|
|
12
|
+
{
|
|
13
|
+
definition: {
|
|
14
|
+
name: "manage_contact_tags",
|
|
15
|
+
description:
|
|
16
|
+
"Manage custom contact tags/labels for classification." +
|
|
17
|
+
" Actions: set (replace all tags), add (append new tags), remove (remove specific tags)," +
|
|
18
|
+
" get (view contact's tags), list (all tags with counts), list_by_tag (contacts with a specific tag).",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
action: {
|
|
23
|
+
type: "string",
|
|
24
|
+
enum: ["set", "add", "remove", "get", "list", "list_by_tag"],
|
|
25
|
+
description: "Action to perform.",
|
|
26
|
+
},
|
|
27
|
+
jid: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Contact JID or phone number (required for set/add/remove/get).",
|
|
30
|
+
},
|
|
31
|
+
tags: {
|
|
32
|
+
type: "array",
|
|
33
|
+
items: { type: "string" },
|
|
34
|
+
description: "Tags to set/add/remove.",
|
|
35
|
+
},
|
|
36
|
+
tag: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Tag name for list_by_tag action.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ["action"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
handler: async ({ action, jid, tags, tag }, { store }) => {
|
|
45
|
+
switch (action) {
|
|
46
|
+
case "set": {
|
|
47
|
+
if (!jid) return errResult("jid is required for 'set' action.");
|
|
48
|
+
if (!tags || tags.length === 0) return errResult("tags array is required for 'set' action.");
|
|
49
|
+
const contactJid = phoneToJid(jid);
|
|
50
|
+
store.setContactTags(contactJid, tags);
|
|
51
|
+
return okResult({ jid: contactJid, tags: store.getContactTags(contactJid) });
|
|
52
|
+
}
|
|
53
|
+
case "add": {
|
|
54
|
+
if (!jid) return errResult("jid is required for 'add' action.");
|
|
55
|
+
if (!tags || tags.length === 0) return errResult("tags array is required for 'add' action.");
|
|
56
|
+
const contactJid = phoneToJid(jid);
|
|
57
|
+
store.addContactTags(contactJid, tags);
|
|
58
|
+
return okResult({ jid: contactJid, tags: store.getContactTags(contactJid) });
|
|
59
|
+
}
|
|
60
|
+
case "remove": {
|
|
61
|
+
if (!jid) return errResult("jid is required for 'remove' action.");
|
|
62
|
+
if (!tags || tags.length === 0) return errResult("tags array is required for 'remove' action.");
|
|
63
|
+
const contactJid = phoneToJid(jid);
|
|
64
|
+
store.removeContactTags(contactJid, tags);
|
|
65
|
+
return okResult({ jid: contactJid, tags: store.getContactTags(contactJid) });
|
|
66
|
+
}
|
|
67
|
+
case "get": {
|
|
68
|
+
if (!jid) return errResult("jid is required for 'get' action.");
|
|
69
|
+
const contactJid = phoneToJid(jid);
|
|
70
|
+
const contact = store.getContact(contactJid);
|
|
71
|
+
return okResult({
|
|
72
|
+
jid: contactJid,
|
|
73
|
+
name: contact?.name || contact?.notify || null,
|
|
74
|
+
tags: store.getContactTags(contactJid),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
case "list": {
|
|
78
|
+
const allTags = store.getAllTags();
|
|
79
|
+
const counts = {};
|
|
80
|
+
for (const t of allTags) {
|
|
81
|
+
counts[t] = store.listByTag(t).length;
|
|
82
|
+
}
|
|
83
|
+
return okResult({ tags: allTags, counts });
|
|
84
|
+
}
|
|
85
|
+
case "list_by_tag": {
|
|
86
|
+
if (!tag) return errResult("tag is required for 'list_by_tag' action.");
|
|
87
|
+
const jids = store.listByTag(tag);
|
|
88
|
+
const contacts = jids.map((j) => {
|
|
89
|
+
const c = store.getContact(j);
|
|
90
|
+
return {
|
|
91
|
+
jid: j,
|
|
92
|
+
phone: jidToPhone(j),
|
|
93
|
+
name: c?.name || c?.notify || null,
|
|
94
|
+
tags: store.getContactTags(j),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
return okResult({ tag, count: contacts.length, contacts });
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return errResult(`Unknown action: ${action}. Use: set, add, remove, get, list, list_by_tag.`);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Utility tools (6 tools).
|
|
3
|
+
*
|
|
4
|
+
* connection_status, whatsapp_guide, send_presence,
|
|
5
|
+
* read_messages, search_messages, download_media,
|
|
6
|
+
* analytics_overview, analytics_top_chats, analytics_chat_insights,
|
|
7
|
+
* analytics_timeline, analytics_search
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const { phoneToJid, okResult, errResult } = require("../helpers");
|
|
13
|
+
|
|
14
|
+
module.exports = [
|
|
15
|
+
// 1. connection_status
|
|
16
|
+
{
|
|
17
|
+
definition: {
|
|
18
|
+
name: "connection_status",
|
|
19
|
+
description:
|
|
20
|
+
"Check the WhatsApp connection status, account info, and store statistics.",
|
|
21
|
+
inputSchema: { type: "object", properties: {} },
|
|
22
|
+
},
|
|
23
|
+
handler: async (_args, ctx) => {
|
|
24
|
+
// Don't destructure sock — this tool must work even when disconnected
|
|
25
|
+
const info = ctx.connectionInfo();
|
|
26
|
+
return okResult(info);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// 2. whatsapp_guide
|
|
31
|
+
{
|
|
32
|
+
definition: {
|
|
33
|
+
name: "whatsapp_guide",
|
|
34
|
+
description:
|
|
35
|
+
"Get a comprehensive guide on how to use whats-mcp tools." +
|
|
36
|
+
" Optionally filter by category.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
category: {
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: [
|
|
43
|
+
"overview", "messaging", "chats", "contacts",
|
|
44
|
+
"groups", "profile", "channels", "labels", "analytics", "utilities",
|
|
45
|
+
],
|
|
46
|
+
description: "Category to get help for. Default: overview.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
handler: async ({ category }, { toolDefs, config }) => {
|
|
52
|
+
const cat = category || "overview";
|
|
53
|
+
|
|
54
|
+
if (cat === "overview") {
|
|
55
|
+
const categories = {};
|
|
56
|
+
for (const t of toolDefs) {
|
|
57
|
+
// Derive category from tool placement
|
|
58
|
+
const c = _guessCategory(t.name);
|
|
59
|
+
if (!categories[c]) categories[c] = [];
|
|
60
|
+
categories[c].push(t.name);
|
|
61
|
+
}
|
|
62
|
+
return okResult({
|
|
63
|
+
server: config?.server?.name || "whats-mcp",
|
|
64
|
+
version: config?.server?.version || "0.1.0",
|
|
65
|
+
total_tools: toolDefs.length,
|
|
66
|
+
categories,
|
|
67
|
+
tips: [
|
|
68
|
+
"JIDs: Use phone numbers (e.g. 33612345678) or full JIDs (33612345678@s.whatsapp.net).",
|
|
69
|
+
"Groups: Group JIDs end with @g.us (e.g. 120363xxx@g.us).",
|
|
70
|
+
"Channels: Newsletter JIDs end with @newsletter.",
|
|
71
|
+
"Media: Send images/videos/documents via URL, base64, or local file path.",
|
|
72
|
+
"Batch: Use batch_send_text to send the same message to multiple recipients.",
|
|
73
|
+
"Reactions: Use send_reaction with an emoji to react, empty string to remove.",
|
|
74
|
+
"Reply: Use quoted_id parameter in send_* tools to reply to a specific message.",
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Filter tools by category
|
|
80
|
+
const catTools = toolDefs.filter((t) => _guessCategory(t.name) === cat);
|
|
81
|
+
return okResult({
|
|
82
|
+
category: cat,
|
|
83
|
+
tools: catTools.map((t) => ({
|
|
84
|
+
name: t.name,
|
|
85
|
+
description: t.description,
|
|
86
|
+
parameters: t.inputSchema?.properties
|
|
87
|
+
? Object.keys(t.inputSchema.properties)
|
|
88
|
+
: [],
|
|
89
|
+
required: t.inputSchema?.required || [],
|
|
90
|
+
})),
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// 3. send_presence
|
|
96
|
+
{
|
|
97
|
+
definition: {
|
|
98
|
+
name: "send_presence",
|
|
99
|
+
description:
|
|
100
|
+
"Send a presence update or typing indicator." +
|
|
101
|
+
" Presence: 'available' (online), 'unavailable' (offline)." +
|
|
102
|
+
" Typing: 'composing' (typing), 'recording' (recording audio), 'paused' (stopped typing).",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
type: {
|
|
107
|
+
type: "string",
|
|
108
|
+
enum: ["available", "unavailable", "composing", "recording", "paused"],
|
|
109
|
+
description: "Presence type to send.",
|
|
110
|
+
},
|
|
111
|
+
jid: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: "Chat JID for composing/recording/paused (required for typing indicators).",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ["type"],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
handler: async ({ type, jid }, { sock }) => {
|
|
120
|
+
if (type === "available" || type === "unavailable") {
|
|
121
|
+
await sock.sendPresenceUpdate(type);
|
|
122
|
+
return okResult({ status: type });
|
|
123
|
+
}
|
|
124
|
+
// Typing indicators require a JID
|
|
125
|
+
if (!jid) {
|
|
126
|
+
return errResult("JID is required for typing indicators (composing/recording/paused).");
|
|
127
|
+
}
|
|
128
|
+
const chatJid = phoneToJid(jid);
|
|
129
|
+
await sock.sendPresenceUpdate(type, chatJid);
|
|
130
|
+
return okResult({ status: type, jid: chatJid });
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// 4. read_messages
|
|
135
|
+
{
|
|
136
|
+
definition: {
|
|
137
|
+
name: "read_messages",
|
|
138
|
+
description:
|
|
139
|
+
"Mark specific messages as read (send read receipts)." +
|
|
140
|
+
" Provide the chat JID and message IDs to mark as read.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
jid: { type: "string", description: "Chat JID." },
|
|
145
|
+
message_ids: {
|
|
146
|
+
type: "array",
|
|
147
|
+
items: { type: "string" },
|
|
148
|
+
description: "Array of message IDs to mark as read.",
|
|
149
|
+
},
|
|
150
|
+
participant: {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Sender JID (required for group messages to send proper receipts).",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
required: ["jid", "message_ids"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
handler: async ({ jid, message_ids, participant }, { sock }) => {
|
|
159
|
+
const chatJid = phoneToJid(jid);
|
|
160
|
+
const keys = message_ids.map((id) => ({
|
|
161
|
+
remoteJid: chatJid,
|
|
162
|
+
id,
|
|
163
|
+
...(participant ? { participant } : {}),
|
|
164
|
+
}));
|
|
165
|
+
await sock.readMessages(keys);
|
|
166
|
+
return okResult({
|
|
167
|
+
status: "read",
|
|
168
|
+
jid: chatJid,
|
|
169
|
+
count: message_ids.length,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// 5. search_messages
|
|
175
|
+
{
|
|
176
|
+
definition: {
|
|
177
|
+
name: "search_messages",
|
|
178
|
+
description:
|
|
179
|
+
"Search messages in the local store by text content." +
|
|
180
|
+
" Filter by one or multiple chat JIDs, time range, and message types." +
|
|
181
|
+
" Only searches messages already in memory.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
query: { type: "string", description: "Text to search for (case-insensitive)." },
|
|
186
|
+
jid: { type: "string", description: "Optional: limit search to this chat JID or phone number." },
|
|
187
|
+
jids: {
|
|
188
|
+
type: "array",
|
|
189
|
+
items: { type: "string" },
|
|
190
|
+
description: "Optional: search across multiple chat JIDs. Takes precedence over jid.",
|
|
191
|
+
},
|
|
192
|
+
limit: { type: "integer", description: "Max results (default 50, max 200)." },
|
|
193
|
+
since: {
|
|
194
|
+
type: "integer",
|
|
195
|
+
description: "Unix timestamp: only include messages sent at or after this time.",
|
|
196
|
+
},
|
|
197
|
+
until: {
|
|
198
|
+
type: "integer",
|
|
199
|
+
description: "Unix timestamp: only include messages sent at or before this time.",
|
|
200
|
+
},
|
|
201
|
+
include_types: {
|
|
202
|
+
type: "array",
|
|
203
|
+
items: { type: "string" },
|
|
204
|
+
description: "If set, only include messages of these types.",
|
|
205
|
+
},
|
|
206
|
+
exclude_types: {
|
|
207
|
+
type: "array",
|
|
208
|
+
items: { type: "string" },
|
|
209
|
+
description: "Exclude messages of these types.",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
required: ["query"],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
handler: async ({ query, jid, jids, limit, since, until, include_types, exclude_types }, { store }) => {
|
|
216
|
+
let chatJids = null;
|
|
217
|
+
if (jids && jids.length > 0) {
|
|
218
|
+
chatJids = jids.map(phoneToJid);
|
|
219
|
+
} else if (jid) {
|
|
220
|
+
chatJids = phoneToJid(jid);
|
|
221
|
+
}
|
|
222
|
+
const lim = Math.min(limit || 50, 200);
|
|
223
|
+
const results = store.searchMessages(query, chatJids, lim, {
|
|
224
|
+
since: since || undefined,
|
|
225
|
+
until: until || undefined,
|
|
226
|
+
types: include_types || undefined,
|
|
227
|
+
excludeTypes: exclude_types || undefined,
|
|
228
|
+
});
|
|
229
|
+
return okResult({
|
|
230
|
+
query,
|
|
231
|
+
count: results.length,
|
|
232
|
+
messages: results,
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
// 6. download_media
|
|
238
|
+
{
|
|
239
|
+
definition: {
|
|
240
|
+
name: "download_media",
|
|
241
|
+
description:
|
|
242
|
+
"Download media (image, video, audio, document, sticker) from a message." +
|
|
243
|
+
" Returns the media as base64-encoded data." +
|
|
244
|
+
" The message must be in the local store.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
message_id: { type: "string", description: "Message ID containing media." },
|
|
249
|
+
},
|
|
250
|
+
required: ["message_id"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
handler: async ({ message_id }, { sock, store }) => {
|
|
254
|
+
const msg = store.getMessage(message_id);
|
|
255
|
+
if (!msg) {
|
|
256
|
+
return errResult(`Message ${message_id} not found in store.`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Find the media content in the message
|
|
260
|
+
const m = msg.message;
|
|
261
|
+
if (!m) return errResult("Message has no content.");
|
|
262
|
+
|
|
263
|
+
let mediaMsg = null;
|
|
264
|
+
let mediaType = null;
|
|
265
|
+
const mediaTypes = [
|
|
266
|
+
["imageMessage", "image"],
|
|
267
|
+
["videoMessage", "video"],
|
|
268
|
+
["audioMessage", "audio"],
|
|
269
|
+
["documentMessage", "document"],
|
|
270
|
+
["stickerMessage", "sticker"],
|
|
271
|
+
["documentWithCaptionMessage", "document"],
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
for (const [key, type] of mediaTypes) {
|
|
275
|
+
if (m[key]) {
|
|
276
|
+
mediaMsg = m[key];
|
|
277
|
+
mediaType = type;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle nested documentWithCaption
|
|
283
|
+
if (m.documentWithCaptionMessage?.message?.documentMessage) {
|
|
284
|
+
mediaMsg = m.documentWithCaptionMessage.message.documentMessage;
|
|
285
|
+
mediaType = "document";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!mediaMsg) {
|
|
289
|
+
return errResult("Message does not contain downloadable media.");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Use Baileys downloadMediaMessage
|
|
293
|
+
const { downloadMediaMessage } = require("@whiskeysockets/baileys");
|
|
294
|
+
const buffer = await downloadMediaMessage(msg, "buffer", {});
|
|
295
|
+
const base64 = buffer.toString("base64");
|
|
296
|
+
|
|
297
|
+
return okResult({
|
|
298
|
+
message_id,
|
|
299
|
+
media_type: mediaType,
|
|
300
|
+
mimetype: mediaMsg.mimetype || null,
|
|
301
|
+
filename: mediaMsg.fileName || null,
|
|
302
|
+
file_length: mediaMsg.fileLength ? Number(mediaMsg.fileLength) : buffer.length,
|
|
303
|
+
base64_length: base64.length,
|
|
304
|
+
data: base64,
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
// ── Private: guess tool category from name ──────────────────────────────────
|
|
311
|
+
|
|
312
|
+
function _guessCategory(name) {
|
|
313
|
+
if (/channel|newsletter/.test(name)) return "channels";
|
|
314
|
+
if (/label/.test(name)) return "labels";
|
|
315
|
+
if (/^analytics_|^daily_digest/.test(name)) return "analytics";
|
|
316
|
+
if (/^connection_status$|^whatsapp_guide$|^send_presence$|^read_messages$|^search_messages$|^download_media$/.test(name)) {
|
|
317
|
+
return "utilities";
|
|
318
|
+
}
|
|
319
|
+
if (/^send_|^edit_|^delete_|^forward_|^batch_/.test(name)) return "messaging";
|
|
320
|
+
if (/^list_chats|^get_messages|^manage_chat$|^star_|^set_disappearing/.test(name)) return "chats";
|
|
321
|
+
if (/^check_phone|^get_contact|^get_profile_picture|^manage_block|^get_business|^list_contacts|^manage_contact_tags/.test(name)) return "contacts";
|
|
322
|
+
if (/^create_group|^get_group|^list_groups|^update_group|^manage_group|^leave_group|^set_group/.test(name)) return "groups";
|
|
323
|
+
if (/^update_display|^update_about|^update_profile_picture|^manage_privacy/.test(name)) return "profile";
|
|
324
|
+
return "utilities";
|
|
325
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Watchlist management tools (1 tool).
|
|
3
|
+
*
|
|
4
|
+
* manage_watchlist
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const { phoneToJid, okResult, errResult } = require("../helpers");
|
|
10
|
+
|
|
11
|
+
const VALID_ACTIONS = ["set", "add", "remove", "get", "list", "delete"];
|
|
12
|
+
|
|
13
|
+
module.exports = [
|
|
14
|
+
// manage_watchlist
|
|
15
|
+
{
|
|
16
|
+
definition: {
|
|
17
|
+
name: "manage_watchlist",
|
|
18
|
+
description:
|
|
19
|
+
"Dynamically manage personal chat watchlists — named groups of chats to monitor together." +
|
|
20
|
+
" Watchlists persist across sessions and are used by whatsup and daily_digest." +
|
|
21
|
+
" CALL THIS when user says: 'suis ces groupes', 'ajoute X à ma watchlist', 'track these chats'," +
|
|
22
|
+
" 'enlève X de ma watchlist', 'crée une watchlist famille', 'quelles sont mes watchlists'," +
|
|
23
|
+
" 'follow X group', 'add X to my evening list', 'stop tracking X', 'remove X from watchlist'," +
|
|
24
|
+
" 'mets-moi ça dans la watchlist Y', 'je veux surveiller ces chats'." +
|
|
25
|
+
" Actions: set (define/replace entirely), add (append JIDs), remove (remove JIDs)," +
|
|
26
|
+
" get (view one watchlist with chat names), list (view all watchlists), delete (delete).",
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
action: {
|
|
31
|
+
type: "string",
|
|
32
|
+
enum: VALID_ACTIONS,
|
|
33
|
+
description: "Action to perform: set | add | remove | get | list | delete.",
|
|
34
|
+
},
|
|
35
|
+
name: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description:
|
|
38
|
+
"Watchlist name (e.g. 'family', 'x24', 'ai', 'morning', 'evening_digest')." +
|
|
39
|
+
" Required for all actions except list.",
|
|
40
|
+
},
|
|
41
|
+
jids: {
|
|
42
|
+
type: "array",
|
|
43
|
+
items: { type: "string" },
|
|
44
|
+
description: "Array of chat JIDs or phone numbers. Required for set/add/remove.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
required: ["action"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
handler: async ({ action, name, jids }, { store, config }) => {
|
|
51
|
+
if (!VALID_ACTIONS.includes(action)) {
|
|
52
|
+
return errResult(`Unknown action '${action}'. Valid: ${VALID_ACTIONS.join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// list: show all watchlists (store + config merged, store takes precedence)
|
|
56
|
+
if (action === "list") {
|
|
57
|
+
const storeWLs = store.listWatchlists();
|
|
58
|
+
const configWLs = config?.watchlists || {};
|
|
59
|
+
const merged = { ...configWLs, ...storeWLs };
|
|
60
|
+
const entries = Object.entries(merged).map(([n, wjids]) => {
|
|
61
|
+
const chats = wjids.map((jid) => {
|
|
62
|
+
const ch = store.getChat(jid);
|
|
63
|
+
return { jid, name: ch?.name || ch?.subject || jid };
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
name: n,
|
|
67
|
+
count: wjids.length,
|
|
68
|
+
source: storeWLs[n] ? "dynamic" : "config",
|
|
69
|
+
chats,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return okResult({ total: entries.length, watchlists: entries });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// All other actions require a name
|
|
76
|
+
if (!name) {
|
|
77
|
+
return errResult(`Parameter 'name' is required for action '${action}'.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === "get") {
|
|
81
|
+
const wjids = store.resolveWatchlist(name, config?.watchlists);
|
|
82
|
+
if (!wjids) {
|
|
83
|
+
const all = [...new Set([
|
|
84
|
+
...Object.keys(store.listWatchlists()),
|
|
85
|
+
...Object.keys(config?.watchlists || {}),
|
|
86
|
+
])];
|
|
87
|
+
return errResult(`Watchlist '${name}' not found. Available: ${all.join(", ") || "none"}`);
|
|
88
|
+
}
|
|
89
|
+
const chats = wjids.map((jid) => {
|
|
90
|
+
const ch = store.getChat(jid);
|
|
91
|
+
return { jid, name: ch?.name || ch?.subject || jid };
|
|
92
|
+
});
|
|
93
|
+
return okResult({ name, count: wjids.length, chats });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (action === "delete") {
|
|
97
|
+
const existed = store.deleteWatchlist(name);
|
|
98
|
+
return okResult({ status: existed ? "deleted" : "not_found", name });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// set / add / remove require jids
|
|
102
|
+
const resolvedJids = (jids || []).map(phoneToJid);
|
|
103
|
+
if (resolvedJids.length === 0) {
|
|
104
|
+
return errResult(`Parameter 'jids' must be a non-empty array for action '${action}'.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (action === "set") {
|
|
108
|
+
store.setWatchlist(name, resolvedJids);
|
|
109
|
+
const chats = resolvedJids.map((jid) => {
|
|
110
|
+
const ch = store.getChat(jid);
|
|
111
|
+
return { jid, name: ch?.name || ch?.subject || jid };
|
|
112
|
+
});
|
|
113
|
+
return okResult({ status: "set", name, count: resolvedJids.length, chats });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (action === "add") {
|
|
117
|
+
store.addToWatchlist(name, resolvedJids);
|
|
118
|
+
const updated = store.getWatchlist(name) || [];
|
|
119
|
+
const chats = updated.map((jid) => {
|
|
120
|
+
const ch = store.getChat(jid);
|
|
121
|
+
return { jid, name: ch?.name || ch?.subject || jid };
|
|
122
|
+
});
|
|
123
|
+
return okResult({ status: "added", name, added: resolvedJids.length, total: updated.length, chats });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// remove
|
|
127
|
+
store.removeFromWatchlist(name, resolvedJids);
|
|
128
|
+
const updated = store.getWatchlist(name) || [];
|
|
129
|
+
const chats = updated.map((jid) => {
|
|
130
|
+
const ch = store.getChat(jid);
|
|
131
|
+
return { jid, name: ch?.name || ch?.subject || jid };
|
|
132
|
+
});
|
|
133
|
+
return okResult({ status: "removed", name, removed: resolvedJids.length, remaining: updated.length, chats });
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|