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,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Contact tools (5 tools).
|
|
3
|
+
*
|
|
4
|
+
* check_phone_number, get_contact_info, get_profile_picture,
|
|
5
|
+
* manage_block, get_business_profile
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { phoneToJid, jidToPhone, isGroupJid, okResult, errResult } = require("../helpers");
|
|
11
|
+
|
|
12
|
+
module.exports = [
|
|
13
|
+
// 1. check_phone_number
|
|
14
|
+
{
|
|
15
|
+
definition: {
|
|
16
|
+
name: "check_phone_number",
|
|
17
|
+
description:
|
|
18
|
+
"Check if one or more phone numbers are registered on WhatsApp." +
|
|
19
|
+
" Returns the JID for each number that is on WhatsApp.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
phones: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: { type: "string" },
|
|
26
|
+
description: "Array of phone numbers to check (e.g. ['33612345678', '+1555000123']).",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["phones"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
handler: async ({ phones }, { sock }) => {
|
|
33
|
+
if (!phones || phones.length === 0) {
|
|
34
|
+
return errResult("At least one phone number is required.");
|
|
35
|
+
}
|
|
36
|
+
// Normalize phone numbers to JIDs
|
|
37
|
+
const ids = phones.map((p) => phoneToJid(p));
|
|
38
|
+
const result = await sock.onWhatsApp(...ids);
|
|
39
|
+
const formatted = result.map((r) => ({
|
|
40
|
+
phone: jidToPhone(r.jid),
|
|
41
|
+
jid: r.jid,
|
|
42
|
+
exists: r.exists,
|
|
43
|
+
}));
|
|
44
|
+
return okResult({
|
|
45
|
+
total: phones.length,
|
|
46
|
+
on_whatsapp: formatted.filter((r) => r.exists).length,
|
|
47
|
+
results: formatted,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// 2. get_contact_info
|
|
53
|
+
{
|
|
54
|
+
definition: {
|
|
55
|
+
name: "get_contact_info",
|
|
56
|
+
description:
|
|
57
|
+
"Get info about a contact: name, about/status text, and profile picture URL." +
|
|
58
|
+
" Combines data from the local store and live API calls.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
jid: { type: "string", description: "Contact JID or phone number." },
|
|
63
|
+
},
|
|
64
|
+
required: ["jid"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
handler: async ({ jid }, { sock, store }) => {
|
|
68
|
+
const contactJid = phoneToJid(jid);
|
|
69
|
+
const info = { jid: contactJid };
|
|
70
|
+
|
|
71
|
+
// From store
|
|
72
|
+
const storeContact = store.getContact(contactJid);
|
|
73
|
+
if (storeContact) {
|
|
74
|
+
info.name = storeContact.name || storeContact.notify || storeContact.verifiedName || null;
|
|
75
|
+
info.short_name = storeContact.short || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Live: status/about
|
|
79
|
+
try {
|
|
80
|
+
const status = await sock.fetchStatus(contactJid);
|
|
81
|
+
info.about = status?.status || null;
|
|
82
|
+
info.about_set_at = status?.setAt ? Number(status.setAt) : null;
|
|
83
|
+
} catch {
|
|
84
|
+
info.about = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Live: profile picture
|
|
88
|
+
try {
|
|
89
|
+
info.profile_picture_url = await sock.profilePictureUrl(contactJid, "image");
|
|
90
|
+
} catch {
|
|
91
|
+
info.profile_picture_url = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return okResult(info);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// 3. get_profile_picture
|
|
99
|
+
{
|
|
100
|
+
definition: {
|
|
101
|
+
name: "get_profile_picture",
|
|
102
|
+
description: "Get the profile picture URL for any JID (contact, group, or your own).",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
jid: { type: "string", description: "JID or phone number. Use 'me' for your own picture." },
|
|
107
|
+
type: {
|
|
108
|
+
type: "string",
|
|
109
|
+
enum: ["image", "preview"],
|
|
110
|
+
description: "Resolution: 'image' for full size, 'preview' for thumbnail. Default 'image'.",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
required: ["jid"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
handler: async ({ jid, type }, { sock }) => {
|
|
117
|
+
let targetJid;
|
|
118
|
+
if (jid === "me") {
|
|
119
|
+
const { user } = sock;
|
|
120
|
+
targetJid = user?.id;
|
|
121
|
+
if (!targetJid) return errResult("Cannot determine own JID. Are you connected?");
|
|
122
|
+
} else {
|
|
123
|
+
targetJid = phoneToJid(jid);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const url = await sock.profilePictureUrl(targetJid, type || "image");
|
|
127
|
+
return okResult({ jid: targetJid, profile_picture_url: url });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err.message?.includes("404") || err.message?.includes("not-authorized")) {
|
|
130
|
+
return okResult({ jid: targetJid, profile_picture_url: null, note: "No profile picture or not authorized." });
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// 4. manage_block
|
|
138
|
+
{
|
|
139
|
+
definition: {
|
|
140
|
+
name: "manage_block",
|
|
141
|
+
description: "Block, unblock a contact, or list blocked contacts.",
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
action: {
|
|
146
|
+
type: "string",
|
|
147
|
+
enum: ["block", "unblock", "list"],
|
|
148
|
+
description: "Action to perform.",
|
|
149
|
+
},
|
|
150
|
+
jid: {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Contact JID or phone number (required for block/unblock).",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
required: ["action"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
handler: async ({ action, jid }, { sock }) => {
|
|
159
|
+
if (action === "list") {
|
|
160
|
+
const blocked = await sock.fetchBlocklist();
|
|
161
|
+
return okResult({
|
|
162
|
+
count: blocked.length,
|
|
163
|
+
blocked: blocked.map((b) => ({ jid: b, phone: jidToPhone(b) })),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (!jid) return errResult(`JID is required for ${action} action.`);
|
|
167
|
+
const contactJid = phoneToJid(jid);
|
|
168
|
+
if (action === "block") {
|
|
169
|
+
await sock.updateBlockStatus(contactJid, "block");
|
|
170
|
+
return okResult({ status: "blocked", jid: contactJid });
|
|
171
|
+
}
|
|
172
|
+
if (action === "unblock") {
|
|
173
|
+
await sock.updateBlockStatus(contactJid, "unblock");
|
|
174
|
+
return okResult({ status: "unblocked", jid: contactJid });
|
|
175
|
+
}
|
|
176
|
+
return errResult(`Unknown action: ${action}`);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// 5. get_business_profile
|
|
181
|
+
{
|
|
182
|
+
definition: {
|
|
183
|
+
name: "get_business_profile",
|
|
184
|
+
description:
|
|
185
|
+
"Get the WhatsApp Business profile of a contact." +
|
|
186
|
+
" Returns business info: description, category, website, email, etc.",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: "object",
|
|
189
|
+
properties: {
|
|
190
|
+
jid: { type: "string", description: "Business contact JID or phone number." },
|
|
191
|
+
},
|
|
192
|
+
required: ["jid"],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
handler: async ({ jid }, { sock }) => {
|
|
196
|
+
const contactJid = phoneToJid(jid);
|
|
197
|
+
try {
|
|
198
|
+
const profile = await sock.getBusinessProfile(contactJid);
|
|
199
|
+
return okResult({
|
|
200
|
+
jid: contactJid,
|
|
201
|
+
business_profile: profile || null,
|
|
202
|
+
});
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return okResult({
|
|
205
|
+
jid: contactJid,
|
|
206
|
+
business_profile: null,
|
|
207
|
+
note: "Could not retrieve business profile. Contact may not be a business account.",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
// 6. list_contacts
|
|
214
|
+
{
|
|
215
|
+
definition: {
|
|
216
|
+
name: "list_contacts",
|
|
217
|
+
description:
|
|
218
|
+
"List contacts from the local store with optional filtering by name, tag, or type." +
|
|
219
|
+
" Returns contact info including custom tags if any are assigned.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
limit: { type: "integer", description: "Max contacts to return (default 100, max 1000)." },
|
|
224
|
+
offset: { type: "integer", description: "Offset for pagination (default 0)." },
|
|
225
|
+
name: { type: "string", description: "Filter by name (case-insensitive substring match)." },
|
|
226
|
+
tag: { type: "string", description: "Filter to contacts with this custom tag." },
|
|
227
|
+
has_tags: { type: "boolean", description: "If true, only contacts with tags; if false, only without tags." },
|
|
228
|
+
exclude_groups: { type: "boolean", description: "Exclude group JIDs from results (default true)." },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
handler: async ({ limit, offset, name, tag, has_tags, exclude_groups }, { store }) => {
|
|
233
|
+
let contacts = store.listContacts({ name, tag, has_tags });
|
|
234
|
+
|
|
235
|
+
// Exclude groups by default
|
|
236
|
+
if (exclude_groups !== false) {
|
|
237
|
+
contacts = contacts.filter((c) => !isGroupJid(c.id));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const total = contacts.length;
|
|
241
|
+
const off = offset || 0;
|
|
242
|
+
const lim = Math.min(limit || 100, 1000);
|
|
243
|
+
const page = contacts.slice(off, off + lim);
|
|
244
|
+
|
|
245
|
+
return okResult({
|
|
246
|
+
total,
|
|
247
|
+
offset: off,
|
|
248
|
+
count: page.length,
|
|
249
|
+
contacts: page.map((c) => ({
|
|
250
|
+
jid: c.id,
|
|
251
|
+
phone: jidToPhone(c.id),
|
|
252
|
+
name: c.name || c.notify || c.verifiedName || null,
|
|
253
|
+
short_name: c.short || null,
|
|
254
|
+
tags: store.getContactTags(c.id),
|
|
255
|
+
})),
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
];
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Digest tools (2 tools).
|
|
3
|
+
*
|
|
4
|
+
* get_messages_multi, daily_digest
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const { phoneToJid, isGroupJid, okResult, errResult, formatMessage } = require("../helpers");
|
|
10
|
+
|
|
11
|
+
module.exports = [
|
|
12
|
+
// 1. get_messages_multi
|
|
13
|
+
{
|
|
14
|
+
definition: {
|
|
15
|
+
name: "get_messages_multi",
|
|
16
|
+
description:
|
|
17
|
+
"Get messages from multiple chats in one call." +
|
|
18
|
+
" Specify JIDs directly or use a named watchlist from config." +
|
|
19
|
+
" Supports time range and message type filters.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
jids: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: { type: "string" },
|
|
26
|
+
description: "Array of chat JIDs or phone numbers to fetch messages from.",
|
|
27
|
+
},
|
|
28
|
+
watchlist: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Name of a watchlist defined in config.json (e.g. 'groups', 'family'). Used if jids is empty.",
|
|
31
|
+
},
|
|
32
|
+
limit_per_chat: {
|
|
33
|
+
type: "integer",
|
|
34
|
+
description: "Max messages per chat (default 50, max 200).",
|
|
35
|
+
},
|
|
36
|
+
since: {
|
|
37
|
+
type: "integer",
|
|
38
|
+
description: "Unix timestamp: only include messages at or after this time.",
|
|
39
|
+
},
|
|
40
|
+
until: {
|
|
41
|
+
type: "integer",
|
|
42
|
+
description: "Unix timestamp: only include messages at or before this time.",
|
|
43
|
+
},
|
|
44
|
+
include_types: {
|
|
45
|
+
type: "array",
|
|
46
|
+
items: { type: "string" },
|
|
47
|
+
description: "Only include messages of these types.",
|
|
48
|
+
},
|
|
49
|
+
exclude_types: {
|
|
50
|
+
type: "array",
|
|
51
|
+
items: { type: "string" },
|
|
52
|
+
description: "Exclude messages of these types.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
handler: async (
|
|
58
|
+
{ jids, watchlist, limit_per_chat, since, until, include_types, exclude_types },
|
|
59
|
+
{ store, config },
|
|
60
|
+
) => {
|
|
61
|
+
// Resolve JIDs from watchlist or explicit list
|
|
62
|
+
let resolvedJids;
|
|
63
|
+
if (jids && jids.length > 0) {
|
|
64
|
+
resolvedJids = jids.map(phoneToJid);
|
|
65
|
+
} else if (watchlist) {
|
|
66
|
+
const wlJids = store.resolveWatchlist(watchlist, config?.watchlists);
|
|
67
|
+
if (wlJids) {
|
|
68
|
+
resolvedJids = wlJids.map(phoneToJid);
|
|
69
|
+
} else {
|
|
70
|
+
const all = [...new Set([
|
|
71
|
+
...Object.keys(store.listWatchlists()),
|
|
72
|
+
...Object.keys(config?.watchlists || {}),
|
|
73
|
+
])];
|
|
74
|
+
return errResult(`Watchlist '${watchlist}' not found. Available: ${all.join(", ") || "none"}`);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
return errResult("Provide either 'jids' array or a 'watchlist' name.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const lim = Math.min(limit_per_chat || 50, 200);
|
|
81
|
+
const filterOpts = {
|
|
82
|
+
since: since || undefined,
|
|
83
|
+
until: until || undefined,
|
|
84
|
+
types: include_types || undefined,
|
|
85
|
+
excludeTypes: exclude_types || undefined,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const chats = [];
|
|
89
|
+
let totalMessages = 0;
|
|
90
|
+
|
|
91
|
+
for (const jid of resolvedJids) {
|
|
92
|
+
const messages = store.getMessages(jid, lim, undefined, filterOpts);
|
|
93
|
+
const formatted = messages.map(formatMessage).filter(Boolean);
|
|
94
|
+
const chat = store.getChat(jid);
|
|
95
|
+
const contact = store.getContact(jid);
|
|
96
|
+
|
|
97
|
+
chats.push({
|
|
98
|
+
jid,
|
|
99
|
+
name: chat?.name || chat?.subject || contact?.name || contact?.notify || jid,
|
|
100
|
+
is_group: isGroupJid(jid),
|
|
101
|
+
count: formatted.length,
|
|
102
|
+
messages: formatted,
|
|
103
|
+
});
|
|
104
|
+
totalMessages += formatted.length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return okResult({
|
|
108
|
+
total_chats: chats.length,
|
|
109
|
+
total_messages: totalMessages,
|
|
110
|
+
filters: { since, until, include_types, exclude_types, limit_per_chat: lim },
|
|
111
|
+
chats,
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// 2. daily_digest
|
|
117
|
+
{
|
|
118
|
+
definition: {
|
|
119
|
+
name: "daily_digest",
|
|
120
|
+
description:
|
|
121
|
+
"Generate a structured daily digest of messages across specified chats." +
|
|
122
|
+
" Defaults to the last 24 hours if no time range is given." +
|
|
123
|
+
" Perfect for evening summaries: shows per-chat message counts, active participants, and messages." +
|
|
124
|
+
" Chats with zero messages in the period are excluded.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
jids: {
|
|
129
|
+
type: "array",
|
|
130
|
+
items: { type: "string" },
|
|
131
|
+
description: "Array of chat JIDs or phone numbers.",
|
|
132
|
+
},
|
|
133
|
+
watchlist: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "Name of a watchlist from config. Used if jids is empty.",
|
|
136
|
+
},
|
|
137
|
+
since: {
|
|
138
|
+
type: "integer",
|
|
139
|
+
description: "Unix timestamp for period start. Default: 24 hours ago.",
|
|
140
|
+
},
|
|
141
|
+
until: {
|
|
142
|
+
type: "integer",
|
|
143
|
+
description: "Unix timestamp for period end. Default: now.",
|
|
144
|
+
},
|
|
145
|
+
limit_per_chat: {
|
|
146
|
+
type: "integer",
|
|
147
|
+
description: "Max messages per chat (default 100, max 500).",
|
|
148
|
+
},
|
|
149
|
+
exclude_types: {
|
|
150
|
+
type: "array",
|
|
151
|
+
items: { type: "string" },
|
|
152
|
+
description: "Exclude these message types from the digest (e.g. reaction, protocol).",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
handler: async (
|
|
158
|
+
{ jids, watchlist, since, until, limit_per_chat, exclude_types },
|
|
159
|
+
{ store, config },
|
|
160
|
+
) => {
|
|
161
|
+
// Time range defaults: last 24 hours
|
|
162
|
+
const now = Math.floor(Date.now() / 1000);
|
|
163
|
+
const effectiveSince = since || (now - 86400);
|
|
164
|
+
const effectiveUntil = until || now;
|
|
165
|
+
|
|
166
|
+
// Resolve JIDs
|
|
167
|
+
let resolvedJids;
|
|
168
|
+
if (jids && jids.length > 0) {
|
|
169
|
+
resolvedJids = jids.map(phoneToJid);
|
|
170
|
+
} else if (watchlist) {
|
|
171
|
+
const wlJids = store.resolveWatchlist(watchlist, config?.watchlists);
|
|
172
|
+
if (wlJids) {
|
|
173
|
+
resolvedJids = wlJids.map(phoneToJid);
|
|
174
|
+
} else {
|
|
175
|
+
const all = [...new Set([
|
|
176
|
+
...Object.keys(store.listWatchlists()),
|
|
177
|
+
...Object.keys(config?.watchlists || {}),
|
|
178
|
+
])];
|
|
179
|
+
return errResult(`Watchlist '${watchlist}' not found. Available: ${all.join(", ") || "none"}`);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// Default: all chats with messages
|
|
183
|
+
resolvedJids = Array.from(store.messages.keys());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const lim = Math.min(limit_per_chat || 100, 500);
|
|
187
|
+
const filterOpts = {
|
|
188
|
+
since: effectiveSince,
|
|
189
|
+
until: effectiveUntil,
|
|
190
|
+
excludeTypes: exclude_types || undefined,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const chatDigests = [];
|
|
194
|
+
let totalMessages = 0;
|
|
195
|
+
let totalFromMe = 0;
|
|
196
|
+
let totalFromOthers = 0;
|
|
197
|
+
|
|
198
|
+
for (const jid of resolvedJids) {
|
|
199
|
+
const messages = store.getMessages(jid, lim, undefined, filterOpts);
|
|
200
|
+
const formatted = messages.map(formatMessage).filter(Boolean);
|
|
201
|
+
if (formatted.length === 0) continue;
|
|
202
|
+
|
|
203
|
+
const chat = store.getChat(jid);
|
|
204
|
+
const contact = store.getContact(jid);
|
|
205
|
+
const fromMe = formatted.filter((m) => m.from_me).length;
|
|
206
|
+
|
|
207
|
+
// Collect unique active participants
|
|
208
|
+
const participants = new Set();
|
|
209
|
+
for (const m of formatted) {
|
|
210
|
+
if (m.sender) participants.add(m.sender);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
chatDigests.push({
|
|
214
|
+
jid,
|
|
215
|
+
name: chat?.name || chat?.subject || contact?.name || contact?.notify || jid,
|
|
216
|
+
is_group: isGroupJid(jid),
|
|
217
|
+
message_count: formatted.length,
|
|
218
|
+
from_me: fromMe,
|
|
219
|
+
from_others: formatted.length - fromMe,
|
|
220
|
+
active_participants: participants.size,
|
|
221
|
+
messages: formatted,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
totalMessages += formatted.length;
|
|
225
|
+
totalFromMe += fromMe;
|
|
226
|
+
totalFromOthers += formatted.length - fromMe;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Sort: most active chats first
|
|
230
|
+
chatDigests.sort((a, b) => b.message_count - a.message_count);
|
|
231
|
+
|
|
232
|
+
return okResult({
|
|
233
|
+
period: {
|
|
234
|
+
since: effectiveSince,
|
|
235
|
+
until: effectiveUntil,
|
|
236
|
+
since_iso: new Date(effectiveSince * 1000).toISOString(),
|
|
237
|
+
until_iso: new Date(effectiveUntil * 1000).toISOString(),
|
|
238
|
+
},
|
|
239
|
+
summary: {
|
|
240
|
+
total_chats: chatDigests.length,
|
|
241
|
+
total_messages: totalMessages,
|
|
242
|
+
total_from_me: totalFromMe,
|
|
243
|
+
total_from_others: totalFromOthers,
|
|
244
|
+
},
|
|
245
|
+
chats: chatDigests,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
];
|