@manybot/manybot 5.1.0 → 5.2.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 +5 -7
- package/dist/client/banner.js +35 -0
- package/dist/client/store.js +135 -0
- package/dist/config.js +363 -0
- package/dist/core/adapter.js +12 -0
- package/dist/core/capabilities.js +16 -0
- package/dist/core/types.js +6 -0
- package/dist/download/queue.js +49 -0
- package/dist/drivers/index.js +14 -0
- package/dist/drivers/patches/index.js +9 -0
- package/dist/drivers/patches/libsignal.js +16 -0
- package/dist/drivers/patches/patch.js +1 -0
- package/dist/drivers/whatsapp/adapter.js +7 -0
- package/dist/drivers/whatsapp/api/index.js +1311 -0
- package/dist/drivers/whatsapp/index.js +164 -0
- package/dist/drivers/whatsapp/loginPrompt.js +81 -0
- package/dist/drivers/whatsapp/messageHandler.js +111 -0
- package/dist/drivers/whatsapp/sdk/baileysSock.js +124 -0
- package/dist/i18n/index.js +202 -0
- package/dist/kernel/pluginApi.js +11 -0
- package/dist/kernel/pluginGuard.js +88 -0
- package/dist/kernel/pluginLoader.js +322 -0
- package/dist/kernel/scheduler.js +110 -0
- package/dist/kernel/sendGuard.js +121 -0
- package/dist/kernel/settingsDb.js +205 -0
- package/{src → dist}/locales/en.json +18 -33
- package/dist/locales/es.json +52 -0
- package/{src → dist}/locales/pt.json +18 -34
- package/dist/logger/logger.js +16 -0
- package/dist/main.js +82 -0
- package/dist/types.js +16 -0
- package/{src → dist}/utils/file.js +3 -3
- package/package.json +35 -26
- package/src/client/banner.js +0 -57
- package/src/client/whatsappClient.js +0 -185
- package/src/config.js +0 -288
- package/src/download/queue.js +0 -55
- package/src/i18n/index.js +0 -235
- package/src/kernel/messageHandler.js +0 -88
- package/src/kernel/pluginApi.js +0 -630
- package/src/kernel/pluginGuard.js +0 -78
- package/src/kernel/pluginLoader.js +0 -177
- package/src/kernel/pluginState.js +0 -99
- package/src/kernel/scheduler.js +0 -48
- package/src/kernel/sendGuard.js +0 -166
- package/src/locales/es.json +0 -62
- package/src/logger/logger.js +0 -32
- package/src/main.js +0 -136
- package/src/utils/getChatId.js +0 -3
- package/src/utils/get_id.js +0 -177
- package/src/utils/pluginI18n.js +0 -129
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drivers/whatsapp/api/index.ts
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp plugin context builder.
|
|
5
|
+
* Exports buildApi() and buildSetupApi() for the WhatsApp driver.
|
|
6
|
+
*
|
|
7
|
+
* Plugins receive a `ctx` object built by buildApi().
|
|
8
|
+
* All wwjs types have been replaced with Baileys equivalents.
|
|
9
|
+
* The ctx surface area is preserved so existing plugins stay compatible.
|
|
10
|
+
*/
|
|
11
|
+
import { logger } from "#logger";
|
|
12
|
+
import { t, createPluginT, reloadTranslations, getCurrentLang } from "#i18n";
|
|
13
|
+
import { CONFIG, CONFIG_DIR } from "#config";
|
|
14
|
+
import { enqueue } from "#download";
|
|
15
|
+
import { schedule, cancelPlugin } from "#kernel/scheduler.js";
|
|
16
|
+
import { emptyFolder } from "#utils/file.js";
|
|
17
|
+
import { normalizeJid, toPresenceCapable } from "../sdk/baileysSock.js";
|
|
18
|
+
import { mkdirSync } from "fs";
|
|
19
|
+
import { readFile, writeFile } from "fs/promises";
|
|
20
|
+
import { readFileSync } from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import { waitForSendSlot, simulateState, typingDuration, mediaDuration } from "#sendguard";
|
|
23
|
+
import { buildSettingsApi } from "#settingsdb";
|
|
24
|
+
import { downloadMediaMessage, getAggregateVotesInPollMessage, decryptPollVote, jidNormalizedUser } from "@whiskeysockets/baileys";
|
|
25
|
+
// ── Message body / type helpers ───────────────────────────────────────────────
|
|
26
|
+
function getMsgBody(msg) {
|
|
27
|
+
const m = msg.message;
|
|
28
|
+
if (!m)
|
|
29
|
+
return "";
|
|
30
|
+
return (m.conversation ??
|
|
31
|
+
m.extendedTextMessage?.text ??
|
|
32
|
+
m.imageMessage?.caption ??
|
|
33
|
+
m.videoMessage?.caption ??
|
|
34
|
+
m.documentMessage?.caption ??
|
|
35
|
+
m.buttonsResponseMessage?.selectedDisplayText ??
|
|
36
|
+
m.listResponseMessage?.title ??
|
|
37
|
+
"");
|
|
38
|
+
}
|
|
39
|
+
function getMsgType(msg) {
|
|
40
|
+
const m = msg.message;
|
|
41
|
+
if (!m)
|
|
42
|
+
return "unknown";
|
|
43
|
+
if (m.conversation || m.extendedTextMessage)
|
|
44
|
+
return "chat";
|
|
45
|
+
if (m.imageMessage)
|
|
46
|
+
return "image";
|
|
47
|
+
if (m.videoMessage)
|
|
48
|
+
return "video";
|
|
49
|
+
if (m.audioMessage)
|
|
50
|
+
return "audio";
|
|
51
|
+
if (m.stickerMessage)
|
|
52
|
+
return "sticker";
|
|
53
|
+
if (m.documentMessage)
|
|
54
|
+
return "document";
|
|
55
|
+
if (m.pollCreationMessage ||
|
|
56
|
+
m.pollCreationMessageV2 ||
|
|
57
|
+
m.pollCreationMessageV3)
|
|
58
|
+
return "poll";
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
function msgHasMedia(msg) {
|
|
62
|
+
const m = msg.message;
|
|
63
|
+
return !!(m?.imageMessage || m?.videoMessage || m?.audioMessage || m?.documentMessage || m?.stickerMessage);
|
|
64
|
+
}
|
|
65
|
+
function msgIsGif(msg) {
|
|
66
|
+
return !!(msg.message?.videoMessage?.gifPlayback);
|
|
67
|
+
}
|
|
68
|
+
/** Sender JID — group participant or DM remote JID, normalized. */
|
|
69
|
+
function getMsgSender(msg, store) {
|
|
70
|
+
const raw = normalizeJid(msg.key.participant || msg.key.remoteJid || "");
|
|
71
|
+
return normalizeJid(store.resolveJid(raw));
|
|
72
|
+
}
|
|
73
|
+
function getContextInfo(msg) {
|
|
74
|
+
const m = msg.message;
|
|
75
|
+
return (m?.extendedTextMessage?.contextInfo ??
|
|
76
|
+
m?.imageMessage?.contextInfo ??
|
|
77
|
+
m?.videoMessage?.contextInfo ??
|
|
78
|
+
m?.audioMessage?.contextInfo ??
|
|
79
|
+
m?.documentMessage?.contextInfo ??
|
|
80
|
+
null);
|
|
81
|
+
}
|
|
82
|
+
function getMsgMimetype(msg) {
|
|
83
|
+
const m = msg.message;
|
|
84
|
+
return (m?.imageMessage?.mimetype ??
|
|
85
|
+
m?.videoMessage?.mimetype ??
|
|
86
|
+
m?.audioMessage?.mimetype ??
|
|
87
|
+
m?.documentMessage?.mimetype ??
|
|
88
|
+
m?.stickerMessage?.mimetype ??
|
|
89
|
+
"application/octet-stream");
|
|
90
|
+
}
|
|
91
|
+
// ── Chat adapter builder ──────────────────────────────────────────────────────
|
|
92
|
+
// Group subjects only land in `store.chats` once Baileys fires chats.upsert
|
|
93
|
+
// / chats.update for that chat — which depends on history sync and doesn't
|
|
94
|
+
// always happen promptly (or at all) for a group the bot just joined. When
|
|
95
|
+
// that hasn't happened yet, `chat.name` silently fell back to the numeric
|
|
96
|
+
// group ID. sock.groupMetadata() always has the real subject, straight from
|
|
97
|
+
// WhatsApp, so use it as a fallback — cached briefly so we're not hitting it
|
|
98
|
+
// on every single incoming message from the same group.
|
|
99
|
+
const GROUP_NAME_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
100
|
+
const groupNameCache = new Map();
|
|
101
|
+
/**
|
|
102
|
+
* Build a WAChat adapter from a Baileys message + store.
|
|
103
|
+
* Exposed for use in messageHandler.ts.
|
|
104
|
+
*
|
|
105
|
+
* @param {WAProtoMsg} msg
|
|
106
|
+
* @param {WAStore} store
|
|
107
|
+
* @param {WASocket} sock
|
|
108
|
+
* @returns {Promise<WAChat>}
|
|
109
|
+
*/
|
|
110
|
+
export async function buildChatFromMsg(msg, store, sock) {
|
|
111
|
+
const rawJid = msg.key.remoteJid ?? "";
|
|
112
|
+
const jid = normalizeJid(rawJid);
|
|
113
|
+
const user = jid.split("@")[0];
|
|
114
|
+
const isGroup = rawJid.endsWith("@g.us");
|
|
115
|
+
// Try to get name from store
|
|
116
|
+
const stored = store.chats.get(rawJid);
|
|
117
|
+
let name = stored?.name;
|
|
118
|
+
if (!name && isGroup) {
|
|
119
|
+
const cached = groupNameCache.get(rawJid);
|
|
120
|
+
if (cached && Date.now() - cached.at < GROUP_NAME_CACHE_TTL_MS) {
|
|
121
|
+
name = cached.name;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
try {
|
|
125
|
+
const meta = await sock.groupMetadata(rawJid);
|
|
126
|
+
if (meta?.subject) {
|
|
127
|
+
name = meta.subject;
|
|
128
|
+
groupNameCache.set(rawJid, { name: meta.subject, at: Date.now() });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// fall through to the numeric fallback below
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { id: { _serialized: jid, user }, name: name ?? user, isGroup };
|
|
137
|
+
}
|
|
138
|
+
// ── MIME map for file sends ───────────────────────────────────────────────────
|
|
139
|
+
const MIME_MAP = {
|
|
140
|
+
".pdf": "application/pdf",
|
|
141
|
+
".doc": "application/msword",
|
|
142
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
143
|
+
".xls": "application/vnd.ms-excel",
|
|
144
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
145
|
+
".zip": "application/zip",
|
|
146
|
+
".mp3": "audio/mpeg",
|
|
147
|
+
".mp4": "video/mp4",
|
|
148
|
+
".jpg": "image/jpeg",
|
|
149
|
+
".jpeg": "image/jpeg",
|
|
150
|
+
".png": "image/png",
|
|
151
|
+
".gif": "image/gif",
|
|
152
|
+
".webp": "image/webp",
|
|
153
|
+
".txt": "text/plain",
|
|
154
|
+
".csv": "text/csv",
|
|
155
|
+
};
|
|
156
|
+
function mimeFromPath(filePath) {
|
|
157
|
+
return MIME_MAP[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
158
|
+
}
|
|
159
|
+
// ── Storage API ───────────────────────────────────────────────────────────────
|
|
160
|
+
export function buildStorageApi(pluginName) {
|
|
161
|
+
if (typeof pluginName !== "string" || pluginName.trim() === "") {
|
|
162
|
+
throw new Error("[storage] buildStorageApi: pluginName must be a non-empty string");
|
|
163
|
+
}
|
|
164
|
+
const dir = path.join(CONFIG_DIR, "data", pluginName);
|
|
165
|
+
mkdirSync(dir, { recursive: true });
|
|
166
|
+
return {
|
|
167
|
+
dir,
|
|
168
|
+
/**
|
|
169
|
+
* Resolves a path inside the plugin's data directory.
|
|
170
|
+
* Creates subdirectories automatically.
|
|
171
|
+
* @param {string} relativePath
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
resolve(relativePath) {
|
|
175
|
+
if (!relativePath || typeof relativePath !== "string")
|
|
176
|
+
throw new Error(`[storage] resolve() requires a non-empty string, got: ${typeof relativePath}`);
|
|
177
|
+
if (relativePath.includes(".."))
|
|
178
|
+
throw new Error(`[storage] path traversal detected in: "${relativePath}"`);
|
|
179
|
+
if (path.isAbsolute(relativePath))
|
|
180
|
+
throw new Error(`[storage] absolute paths are not allowed: "${relativePath}"`);
|
|
181
|
+
if (relativePath.includes("\\"))
|
|
182
|
+
throw new Error(`[storage] Windows-style paths are not allowed: "${relativePath}"`);
|
|
183
|
+
const resolved = path.join(dir, relativePath);
|
|
184
|
+
if (!resolved.startsWith(path.resolve(dir) + path.sep))
|
|
185
|
+
throw new Error(`[storage] resolved path escapes plugin data dir: "${resolved}"`);
|
|
186
|
+
mkdirSync(path.dirname(resolved), { recursive: true });
|
|
187
|
+
return resolved;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// ── Config API ────────────────────────────────────────────────────────────────
|
|
192
|
+
function buildConfigApi() {
|
|
193
|
+
return {
|
|
194
|
+
/**
|
|
195
|
+
* Get a config value with optional default.
|
|
196
|
+
* @param {string} key
|
|
197
|
+
* @param {any} [defaultValue]
|
|
198
|
+
*/
|
|
199
|
+
get(key, defaultValue = null) {
|
|
200
|
+
return CONFIG[key] ?? defaultValue;
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// ── i18n API ──────────────────────────────────────────────────────────────────
|
|
205
|
+
function buildI18nApi() {
|
|
206
|
+
return {
|
|
207
|
+
t,
|
|
208
|
+
/**
|
|
209
|
+
* Create a scoped t() for a plugin's own locale files.
|
|
210
|
+
* @param {string} pluginMetaUrl — pass import.meta.url from the plugin
|
|
211
|
+
*/
|
|
212
|
+
createT: createPluginT,
|
|
213
|
+
reload: reloadTranslations,
|
|
214
|
+
getCurrentLang,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
// ── Utils API ─────────────────────────────────────────────────────────────────
|
|
218
|
+
function buildUtilsApi() {
|
|
219
|
+
return { emptyFolder };
|
|
220
|
+
}
|
|
221
|
+
// ── Download API ──────────────────────────────────────────────────────────────
|
|
222
|
+
function buildDownloadApi() {
|
|
223
|
+
return {
|
|
224
|
+
/**
|
|
225
|
+
* Enqueue a download work function.
|
|
226
|
+
* @param {Function} workFn
|
|
227
|
+
* @param {Function} [errorFn]
|
|
228
|
+
*/
|
|
229
|
+
enqueue,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// ── Scheduler API ─────────────────────────────────────────────────────────────
|
|
233
|
+
function buildSchedulerApi(pluginName) {
|
|
234
|
+
return {
|
|
235
|
+
/**
|
|
236
|
+
* Register a cron task, scoped to this plugin.
|
|
237
|
+
* Re-registering the same expression replaces the previous task
|
|
238
|
+
* instead of stacking a new one.
|
|
239
|
+
* @param {string} expression — cron expression, e.g. "0 9 * * 1"
|
|
240
|
+
* @param {Function} fn — async function to run on schedule
|
|
241
|
+
* @returns {{ stop(): void }}
|
|
242
|
+
*/
|
|
243
|
+
schedule(expression, fn) {
|
|
244
|
+
return schedule(expression, fn, pluginName);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// ── Plugin registry API ───────────────────────────────────────────────────────
|
|
249
|
+
function buildPluginsApi(pluginRegistry) {
|
|
250
|
+
return {
|
|
251
|
+
/**
|
|
252
|
+
* Return public API of another plugin, or null if not active.
|
|
253
|
+
* @param {string} name
|
|
254
|
+
*/
|
|
255
|
+
get(name) {
|
|
256
|
+
return pluginRegistry.get(name)?.exports ?? null;
|
|
257
|
+
},
|
|
258
|
+
/**
|
|
259
|
+
* Return public API of another plugin, or throw if not active.
|
|
260
|
+
* @param {string} name
|
|
261
|
+
*/
|
|
262
|
+
require(name) {
|
|
263
|
+
const plugin = pluginRegistry.get(name);
|
|
264
|
+
if (!plugin || plugin.status !== "active")
|
|
265
|
+
throw new Error(`[plugins] dependency "${name}" does not exist or is not active`);
|
|
266
|
+
return plugin.exports;
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* Check if a plugin is active.
|
|
270
|
+
* @param {string} name
|
|
271
|
+
*/
|
|
272
|
+
exists(name) {
|
|
273
|
+
return pluginRegistry.get(name)?.status === "active";
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// ── Log API ───────────────────────────────────────────────────────────────────
|
|
278
|
+
const log = {
|
|
279
|
+
info: (...a) => logger.info(...a),
|
|
280
|
+
warn: (...a) => logger.warn(...a),
|
|
281
|
+
error: (...a) => logger.error(...a),
|
|
282
|
+
success: (...a) => logger.success(...a),
|
|
283
|
+
};
|
|
284
|
+
// ── Contact normalization ─────────────────────────────────────────────────────
|
|
285
|
+
/**
|
|
286
|
+
* Build a normalized contact object from a JID and optional store metadata.
|
|
287
|
+
* isBusiness is resolved via sock.getBusinessProfile(jid) — it resolves to
|
|
288
|
+
* a profile only for WhatsApp Business accounts, undefined otherwise.
|
|
289
|
+
* @param {string} jid
|
|
290
|
+
* @param {WAStoreContact} [info]
|
|
291
|
+
* @param {string|null} [botJid]
|
|
292
|
+
* @param {WASocket} [sock]
|
|
293
|
+
*/
|
|
294
|
+
async function normalizeContact(jid, info, botJid, sock) {
|
|
295
|
+
const number = jid.split("@")[0];
|
|
296
|
+
let isBusiness = false;
|
|
297
|
+
// We already have a contact record for this jid (learned from a real
|
|
298
|
+
// contacts.upsert or an actual message from them) — that alone proves
|
|
299
|
+
// it's a real WhatsApp account. Only fall back to the onWhatsApp() query
|
|
300
|
+
// when we don't, since that query is PN-oriented and unreliable for a
|
|
301
|
+
// raw @lid we've never resolved to a phone number (returns a false
|
|
302
|
+
// "doesn't exist" instead of throwing).
|
|
303
|
+
let isWAAccount = Boolean(info);
|
|
304
|
+
if (!isWAAccount && sock && !jid.endsWith("@g.us")) {
|
|
305
|
+
try {
|
|
306
|
+
const results = await sock.onWhatsApp(jid);
|
|
307
|
+
isWAAccount = Boolean(results?.[0]?.exists);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Couldn't verify — leave as false rather than claiming certainty.
|
|
311
|
+
isWAAccount = false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (sock && !jid.endsWith("@g.us")) {
|
|
315
|
+
try {
|
|
316
|
+
isBusiness = Boolean(await sock.getBusinessProfile(jid));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
isBusiness = false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
id: jid,
|
|
324
|
+
number,
|
|
325
|
+
pushname: info?.notify ?? null,
|
|
326
|
+
name: info?.name ?? info?.verifiedName ?? null,
|
|
327
|
+
// Baileys' Contact type has no "short name" equivalent (that's a
|
|
328
|
+
// whatsapp-web.js/vCard concept) — always null here, not a bug.
|
|
329
|
+
shortName: null,
|
|
330
|
+
isBusiness,
|
|
331
|
+
isEnterprise: false,
|
|
332
|
+
isBlocked: false,
|
|
333
|
+
isMe: botJid ? jid === normalizeJid(botJid) : false,
|
|
334
|
+
isWAAccount,
|
|
335
|
+
isUser: !jid.endsWith("@g.us"),
|
|
336
|
+
isGroup: jid.endsWith("@g.us"),
|
|
337
|
+
mention: { text: `@${number}`, mentions: [jid] },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
// ── Contact API ───────────────────────────────────────────────────────────────
|
|
341
|
+
function buildContactsApi(sock, store, botJid) {
|
|
342
|
+
return {
|
|
343
|
+
/**
|
|
344
|
+
* Get a normalized contact object by JID.
|
|
345
|
+
* @param {string} contactId
|
|
346
|
+
* @returns {Promise<object|null>}
|
|
347
|
+
*/
|
|
348
|
+
async get(contactId) {
|
|
349
|
+
const resolved = normalizeJid(store.resolveJid(normalizeJid(contactId)));
|
|
350
|
+
// The same person's data can be split across the raw (e.g. @lid) and
|
|
351
|
+
// store-resolved (PN) keys — one may have `notify` and the other
|
|
352
|
+
// `verifiedName`/`name`. Merge instead of taking whichever key
|
|
353
|
+
// happens to exist first, or we silently drop real fields.
|
|
354
|
+
const raw = store.contacts[contactId];
|
|
355
|
+
const resolvedInfo = store.contacts[resolved];
|
|
356
|
+
const info = (raw || resolvedInfo) ? { ...raw, ...resolvedInfo } : undefined;
|
|
357
|
+
return normalizeContact(resolved, info, botJid, sock);
|
|
358
|
+
},
|
|
359
|
+
/**
|
|
360
|
+
* Get the profile picture URL of a contact.
|
|
361
|
+
* @param {string} contactId
|
|
362
|
+
* @returns {Promise<string|null>}
|
|
363
|
+
*/
|
|
364
|
+
async getPfpUrl(contactId) {
|
|
365
|
+
const resolved = normalizeJid(store.resolveJid(normalizeJid(contactId)));
|
|
366
|
+
try {
|
|
367
|
+
const url = await sock.profilePictureUrl(resolved, "image");
|
|
368
|
+
return url ?? null;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
/**
|
|
375
|
+
* Download a contact's profile picture to a local path.
|
|
376
|
+
* @param {string} contactId
|
|
377
|
+
* @param {string} destPath — absolute path (e.g. via ctx.storage.resolve)
|
|
378
|
+
* @returns {Promise<string|null>}
|
|
379
|
+
*/
|
|
380
|
+
async getPfpPath(contactId, destPath) {
|
|
381
|
+
const url = await this.getPfpUrl(contactId);
|
|
382
|
+
if (!url)
|
|
383
|
+
return null;
|
|
384
|
+
try {
|
|
385
|
+
const res = await fetch(url);
|
|
386
|
+
if (!res.ok)
|
|
387
|
+
return null;
|
|
388
|
+
await writeFile(destPath, Buffer.from(await res.arrayBuffer()));
|
|
389
|
+
return destPath;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
/**
|
|
396
|
+
* Get the "about" / status text of a contact.
|
|
397
|
+
* @param {string} contactId
|
|
398
|
+
* @returns {Promise<string|null>}
|
|
399
|
+
*/
|
|
400
|
+
async getAbout(contactId) {
|
|
401
|
+
// fetchStatus is a legacy/USync query — pass the store-resolved JID
|
|
402
|
+
// (PN when known) rather than a raw @lid, which it may not accept.
|
|
403
|
+
const resolved = normalizeJid(store.resolveJid(normalizeJid(contactId)));
|
|
404
|
+
try {
|
|
405
|
+
const res = await sock.fetchStatus(resolved);
|
|
406
|
+
// Current Baileys versions return a USync result array
|
|
407
|
+
// (`[{ id, status: { status, setAt } }]`) instead of the legacy
|
|
408
|
+
// single `{ status }` object — handle both shapes.
|
|
409
|
+
if (Array.isArray(res)) {
|
|
410
|
+
const entry = res.find(r => normalizeJid(r.id) === resolved) ?? res[0];
|
|
411
|
+
return entry?.status?.status ?? null;
|
|
412
|
+
}
|
|
413
|
+
return res?.status ?? null;
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
// Previously swallowed silently, which made "always null" look
|
|
417
|
+
// identical to "no status set" — log so real failures are visible.
|
|
418
|
+
logger.warn(`[contacts] getAbout(${contactId}) failed: ${err}`);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
/**
|
|
423
|
+
* Block a contact.
|
|
424
|
+
* @param {string} contactId
|
|
425
|
+
*/
|
|
426
|
+
async block(contactId) {
|
|
427
|
+
return sock.updateBlockStatus(contactId, "block");
|
|
428
|
+
},
|
|
429
|
+
/**
|
|
430
|
+
* Unblock a contact.
|
|
431
|
+
* @param {string} contactId
|
|
432
|
+
*/
|
|
433
|
+
async unblock(contactId) {
|
|
434
|
+
return sock.updateBlockStatus(contactId, "unblock");
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
export function buildMessageContext(msg, sock, store, guardOptions = {}) {
|
|
439
|
+
const body = getMsgBody(msg);
|
|
440
|
+
const prefix = CONFIG.CMD_PREFIX;
|
|
441
|
+
const rawArgs = body.trim().split(/\s+/);
|
|
442
|
+
const first = rawArgs[0]?.toLowerCase() ?? "";
|
|
443
|
+
const hasPrefix = first.startsWith(prefix);
|
|
444
|
+
const command = hasPrefix ? first.slice(prefix.length) : "";
|
|
445
|
+
const rawJid = msg.key.remoteJid ?? "";
|
|
446
|
+
const sender = getMsgSender(msg, store);
|
|
447
|
+
const cooldown = guardOptions.cooldown ?? true;
|
|
448
|
+
const jitter = guardOptions.jitter ?? true;
|
|
449
|
+
const contextInfo = getContextInfo(msg);
|
|
450
|
+
const quotedRaw = contextInfo?.quotedMessage
|
|
451
|
+
? {
|
|
452
|
+
key: {
|
|
453
|
+
remoteJid: rawJid,
|
|
454
|
+
fromMe: false,
|
|
455
|
+
id: contextInfo.stanzaId ?? undefined,
|
|
456
|
+
participant: contextInfo.participant ?? undefined,
|
|
457
|
+
},
|
|
458
|
+
message: contextInfo.quotedMessage,
|
|
459
|
+
pushName: null,
|
|
460
|
+
}
|
|
461
|
+
: null;
|
|
462
|
+
return {
|
|
463
|
+
body,
|
|
464
|
+
type: getMsgType(msg),
|
|
465
|
+
fromMe: !!(msg.key.fromMe),
|
|
466
|
+
sender,
|
|
467
|
+
senderName: msg.pushName ?? sender.replace(/(:\d+)?@.*$/, ""),
|
|
468
|
+
command,
|
|
469
|
+
args: rawArgs.slice(1),
|
|
470
|
+
is(cmd) {
|
|
471
|
+
return hasPrefix && command === cmd.toLowerCase();
|
|
472
|
+
},
|
|
473
|
+
hasMedia: msgHasMedia(msg),
|
|
474
|
+
isGif: msgIsGif(msg),
|
|
475
|
+
async downloadMedia() {
|
|
476
|
+
try {
|
|
477
|
+
const buffer = await downloadMediaMessage(msg, "buffer", {});
|
|
478
|
+
if (!buffer || !Buffer.isBuffer(buffer))
|
|
479
|
+
return null;
|
|
480
|
+
return { mimetype: getMsgMimetype(msg), data: buffer.toString("base64") };
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
hasReply: !!(contextInfo?.quotedMessage),
|
|
487
|
+
async getReply() {
|
|
488
|
+
return quotedRaw;
|
|
489
|
+
},
|
|
490
|
+
reply: makeSender(sock, store, rawJid, msg, { cooldown, jitter }),
|
|
491
|
+
async react(emoji) {
|
|
492
|
+
return sock.sendMessage(rawJid, { react: { text: emoji, key: msg.key } });
|
|
493
|
+
},
|
|
494
|
+
async delete(forEveryone = true) {
|
|
495
|
+
if (forEveryone) {
|
|
496
|
+
return sock.sendMessage(rawJid, { delete: msg.key });
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
async pin(_duration) {
|
|
500
|
+
logger.warn("[pluginApi] pin() is not supported with Baileys");
|
|
501
|
+
},
|
|
502
|
+
hasPrefix,
|
|
503
|
+
async getContact() {
|
|
504
|
+
const info = store.contacts[sender]
|
|
505
|
+
?? store.contacts[store.resolveJid(msg.key.participant ?? "")];
|
|
506
|
+
const botJid = sock.user?.id ? normalizeJid(sock.user.id) : null;
|
|
507
|
+
return normalizeContact(sender, info, botJid, sock);
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// ── MessageHandle ─────────────────────────────────────────────────────────────
|
|
512
|
+
/**
|
|
513
|
+
* Wraps a pending send and exposes chainable post-send actions.
|
|
514
|
+
* Thenable: `await ctx.send.text("hi")` resolves to the message context.
|
|
515
|
+
*/
|
|
516
|
+
class MessageHandle {
|
|
517
|
+
_p;
|
|
518
|
+
_sock;
|
|
519
|
+
_store;
|
|
520
|
+
_jid = null;
|
|
521
|
+
_guardOptions;
|
|
522
|
+
rawPromise;
|
|
523
|
+
constructor(promise, sock, store, guardOptions) {
|
|
524
|
+
this.rawPromise = promise;
|
|
525
|
+
this._sock = sock;
|
|
526
|
+
this._store = store;
|
|
527
|
+
this._guardOptions = guardOptions ?? {};
|
|
528
|
+
this._p = promise.then(msg => {
|
|
529
|
+
if (!msg)
|
|
530
|
+
return undefined;
|
|
531
|
+
if (!this._jid)
|
|
532
|
+
this._jid = msg.key.remoteJid || null;
|
|
533
|
+
return buildMessageContext(msg, sock, store, guardOptions);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
then(onfulfilled, onrejected) {
|
|
537
|
+
return this._p.then(onfulfilled, onrejected);
|
|
538
|
+
}
|
|
539
|
+
catch(onrejected) {
|
|
540
|
+
return this._p.catch(onrejected);
|
|
541
|
+
}
|
|
542
|
+
finally(onfinally) {
|
|
543
|
+
return this._p.finally(onfinally);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Reply to the sent message.
|
|
547
|
+
* Returns a sender that will quote this message when methods are called.
|
|
548
|
+
*
|
|
549
|
+
* Usage:
|
|
550
|
+
* const audio = await ctx.send.audio("file.mp3");
|
|
551
|
+
* const reply = await audio.reply.text("here it is!");
|
|
552
|
+
*/
|
|
553
|
+
get reply() {
|
|
554
|
+
return makeSender(this._sock, this._store, this._jid || "", this.rawPromise, this._guardOptions);
|
|
555
|
+
}
|
|
556
|
+
/** Pin the sent message. */
|
|
557
|
+
async pin(_duration) {
|
|
558
|
+
logger.warn("[pluginApi] pin() is not supported yet");
|
|
559
|
+
}
|
|
560
|
+
/** Delete the sent message. */
|
|
561
|
+
async delete(forEveryone = true) {
|
|
562
|
+
const msg = await this.rawPromise;
|
|
563
|
+
if (!msg?.key)
|
|
564
|
+
return;
|
|
565
|
+
const jid = msg.key.remoteJid;
|
|
566
|
+
if (forEveryone) {
|
|
567
|
+
return this._sock.sendMessage(jid, { delete: msg.key });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/** React to the sent message. */
|
|
571
|
+
async react(emoji) {
|
|
572
|
+
const msg = await this.rawPromise;
|
|
573
|
+
if (!msg?.key)
|
|
574
|
+
return;
|
|
575
|
+
return this._sock.sendMessage(msg.key.remoteJid, {
|
|
576
|
+
react: { text: emoji, key: msg.key },
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// ── Sender factory ────────────────────────────────────────────────────────────
|
|
581
|
+
/**
|
|
582
|
+
* Returns send methods bound to a specific JID.
|
|
583
|
+
*
|
|
584
|
+
* @param {WASocket} sock
|
|
585
|
+
* @param {WAStore} store
|
|
586
|
+
* @param {string} jid — destination JID (raw, not normalized)
|
|
587
|
+
* @param {WAProtoMsg | Promise<WAProtoMsg | null>} [quoted] — message to quote (can be Promise)
|
|
588
|
+
* @param {object} [guard]
|
|
589
|
+
*/
|
|
590
|
+
function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter = true } = {}) {
|
|
591
|
+
const normJid = normalizeJid(jid);
|
|
592
|
+
// Helper: resolve quoted message if it's a Promise
|
|
593
|
+
const resolveQuoted = async () => {
|
|
594
|
+
if (!quoted)
|
|
595
|
+
return undefined;
|
|
596
|
+
if (quoted instanceof Promise) {
|
|
597
|
+
const result = await quoted;
|
|
598
|
+
return result || undefined;
|
|
599
|
+
}
|
|
600
|
+
return quoted;
|
|
601
|
+
};
|
|
602
|
+
return {
|
|
603
|
+
text(content, opts = {}) {
|
|
604
|
+
return new MessageHandle((async () => {
|
|
605
|
+
const quotedMsg = await resolveQuoted();
|
|
606
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
607
|
+
if (opts.linkPreview === false) {
|
|
608
|
+
sendOpts.linkPreview = false;
|
|
609
|
+
}
|
|
610
|
+
if (opts.mentions?.length) {
|
|
611
|
+
sendOpts.mentions = opts.mentions;
|
|
612
|
+
}
|
|
613
|
+
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
614
|
+
await simulateState(toPresenceCapable(sock), jid, typingDuration(content), "typing");
|
|
615
|
+
return sock.sendMessage(jid, { text: content }, sendOpts);
|
|
616
|
+
})(), sock, store, { cooldown, jitter });
|
|
617
|
+
},
|
|
618
|
+
image(filePath, caption = "", opts = {}) {
|
|
619
|
+
return new MessageHandle((async () => {
|
|
620
|
+
const quotedMsg = await resolveQuoted();
|
|
621
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
622
|
+
if (opts.viewOnce) {
|
|
623
|
+
sendOpts.viewOnce = true;
|
|
624
|
+
}
|
|
625
|
+
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
626
|
+
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
627
|
+
const buffer = await readFile(filePath);
|
|
628
|
+
return sock.sendMessage(jid, { image: buffer, caption }, sendOpts);
|
|
629
|
+
})(), sock, store, { cooldown, jitter });
|
|
630
|
+
},
|
|
631
|
+
video(filePath, caption = "", opts = {}) {
|
|
632
|
+
return new MessageHandle((async () => {
|
|
633
|
+
const quotedMsg = await resolveQuoted();
|
|
634
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
635
|
+
if (opts.viewOnce) {
|
|
636
|
+
sendOpts.viewOnce = true;
|
|
637
|
+
}
|
|
638
|
+
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
639
|
+
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
640
|
+
const buffer = await readFile(filePath);
|
|
641
|
+
return sock.sendMessage(jid, { video: buffer, caption }, sendOpts);
|
|
642
|
+
})(), sock, store, { cooldown, jitter });
|
|
643
|
+
},
|
|
644
|
+
audio(filePath, { asVoice = true, viewOnce = false } = {}) {
|
|
645
|
+
return new MessageHandle((async () => {
|
|
646
|
+
const quotedMsg = await resolveQuoted();
|
|
647
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
648
|
+
if (viewOnce) {
|
|
649
|
+
sendOpts.viewOnce = true;
|
|
650
|
+
}
|
|
651
|
+
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
652
|
+
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "recording");
|
|
653
|
+
const buffer = await readFile(filePath);
|
|
654
|
+
return sock.sendMessage(jid, { audio: buffer, mimetype: "audio/mp4", ptt: asVoice }, sendOpts);
|
|
655
|
+
})(), sock, store, { cooldown, jitter });
|
|
656
|
+
},
|
|
657
|
+
sticker(source) {
|
|
658
|
+
return new MessageHandle((async () => {
|
|
659
|
+
const quotedMsg = await resolveQuoted();
|
|
660
|
+
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
661
|
+
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
662
|
+
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
663
|
+
const buffer = Buffer.isBuffer(source) ? source : await readFile(source);
|
|
664
|
+
return sock.sendMessage(jid, { sticker: buffer }, qOpts);
|
|
665
|
+
})(), sock, store, { cooldown, jitter });
|
|
666
|
+
},
|
|
667
|
+
file(filePath, filename) {
|
|
668
|
+
return new MessageHandle((async () => {
|
|
669
|
+
const quotedMsg = await resolveQuoted();
|
|
670
|
+
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
671
|
+
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
672
|
+
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
673
|
+
const buffer = await readFile(filePath);
|
|
674
|
+
const mimetype = mimeFromPath(filePath);
|
|
675
|
+
return sock.sendMessage(jid, {
|
|
676
|
+
document: buffer,
|
|
677
|
+
mimetype,
|
|
678
|
+
fileName: filename ?? path.basename(filePath),
|
|
679
|
+
}, qOpts);
|
|
680
|
+
})(), sock, store, { cooldown, jitter });
|
|
681
|
+
},
|
|
682
|
+
/**
|
|
683
|
+
* Send a poll.
|
|
684
|
+
* @param {string} question
|
|
685
|
+
* @param {string[]} options
|
|
686
|
+
* @param {object} [opts]
|
|
687
|
+
* @param {boolean} [opts.allowMultipleAnswers=false]
|
|
688
|
+
*/
|
|
689
|
+
poll(question, options, { allowMultipleAnswers = false } = {}) {
|
|
690
|
+
return new MessageHandle((async () => {
|
|
691
|
+
const quotedMsg = await resolveQuoted();
|
|
692
|
+
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
693
|
+
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
694
|
+
return sock.sendMessage(jid, {
|
|
695
|
+
poll: {
|
|
696
|
+
name: question,
|
|
697
|
+
values: options,
|
|
698
|
+
selectableCount: allowMultipleAnswers ? 0 : 1,
|
|
699
|
+
},
|
|
700
|
+
}, qOpts);
|
|
701
|
+
})(), sock, store, { cooldown, jitter });
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
// ── Send API ──────────────────────────────────────────────────────────────────
|
|
706
|
+
function buildSendApi(sock, store, rawJid, guardOptions = {}) {
|
|
707
|
+
const cooldown = (guardOptions.cooldown ?? true);
|
|
708
|
+
const jitter = (guardOptions.jitter ?? true);
|
|
709
|
+
const current = makeSender(sock, store, rawJid, null, { cooldown, jitter });
|
|
710
|
+
return {
|
|
711
|
+
send: {
|
|
712
|
+
text: (text, opts) => current.text(text, opts),
|
|
713
|
+
image: (filePath, caption) => current.image(filePath, caption),
|
|
714
|
+
video: (filePath, caption) => current.video(filePath, caption),
|
|
715
|
+
audio: (filePath, opts) => current.audio(filePath, opts),
|
|
716
|
+
sticker: (source) => current.sticker(source),
|
|
717
|
+
file: (filePath, filename) => current.file(filePath, filename),
|
|
718
|
+
poll: (q, opts, cfg) => current.poll(q, opts, cfg),
|
|
719
|
+
/**
|
|
720
|
+
* Returns a sender bound to another chat.
|
|
721
|
+
* @param {string} targetJid
|
|
722
|
+
*/
|
|
723
|
+
to: (targetJid) => makeSender(sock, store, targetJid, null, { cooldown: false, jitter: false }),
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/** Setup send API — no current chat, only .to(). */
|
|
728
|
+
function buildSetupSendApi(sock, store) {
|
|
729
|
+
return {
|
|
730
|
+
send: {
|
|
731
|
+
to: (targetJid) => makeSender(sock, store, targetJid),
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// ── Events API ────────────────────────────────────────────────────────────────
|
|
736
|
+
const listenerRegistry = new Map();
|
|
737
|
+
export function cleanupPluginEvents(pluginName, sock) {
|
|
738
|
+
const list = listenerRegistry.get(pluginName);
|
|
739
|
+
if (list) {
|
|
740
|
+
for (const { event, handler } of list) {
|
|
741
|
+
try {
|
|
742
|
+
sock.ev.off(event, handler);
|
|
743
|
+
}
|
|
744
|
+
catch { }
|
|
745
|
+
}
|
|
746
|
+
listenerRegistry.delete(pluginName);
|
|
747
|
+
}
|
|
748
|
+
cancelPlugin(pluginName);
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* @param {WASocket} sock
|
|
752
|
+
* @param {string} pluginName
|
|
753
|
+
*/
|
|
754
|
+
function buildEventsApi(sock, pluginName) {
|
|
755
|
+
return {
|
|
756
|
+
on(event, handler) {
|
|
757
|
+
sock.ev.on(event, handler);
|
|
758
|
+
if (!listenerRegistry.has(pluginName))
|
|
759
|
+
listenerRegistry.set(pluginName, new Set());
|
|
760
|
+
const ref = { event, handler };
|
|
761
|
+
listenerRegistry.get(pluginName).add(ref);
|
|
762
|
+
return () => {
|
|
763
|
+
sock.ev.off(event, handler);
|
|
764
|
+
listenerRegistry.get(pluginName)?.delete(ref);
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
once(event) {
|
|
768
|
+
return new Promise(resolve => {
|
|
769
|
+
const off = this.on(event, (data) => { off(); resolve(data); });
|
|
770
|
+
});
|
|
771
|
+
},
|
|
772
|
+
cleanup() {
|
|
773
|
+
const list = listenerRegistry.get(pluginName);
|
|
774
|
+
if (!list)
|
|
775
|
+
return;
|
|
776
|
+
for (const { event, handler } of list)
|
|
777
|
+
sock.ev.off(event, handler);
|
|
778
|
+
listenerRegistry.delete(pluginName);
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
// ── Admin API ─────────────────────────────────────────────────────────────────
|
|
783
|
+
/**
|
|
784
|
+
* Group administration actions.
|
|
785
|
+
*
|
|
786
|
+
* @param {WASocket} sock
|
|
787
|
+
* @param {string|null} chatJid — raw JID of the current group (null in setup context)
|
|
788
|
+
*/
|
|
789
|
+
function buildAdminApi(sock, chatJid) {
|
|
790
|
+
const norm = (v) => Array.isArray(v) ? v : [v];
|
|
791
|
+
function requireChat() {
|
|
792
|
+
if (!chatJid)
|
|
793
|
+
throw new Error("This admin operation requires a runtime group context.");
|
|
794
|
+
}
|
|
795
|
+
async function getGroup(jid) {
|
|
796
|
+
const meta = await sock.groupMetadata(jid);
|
|
797
|
+
if (!meta)
|
|
798
|
+
throw new Error(`Group not found: ${jid}`);
|
|
799
|
+
return meta;
|
|
800
|
+
}
|
|
801
|
+
function createTargetableAction(action, memberIds) {
|
|
802
|
+
const users = norm(memberIds);
|
|
803
|
+
const executeCurrent = async () => { requireChat(); return action(chatJid, users); };
|
|
804
|
+
return {
|
|
805
|
+
async to(targetJid) { await getGroup(targetJid); return action(targetJid, users); },
|
|
806
|
+
then(onfulfilled, onrejected) {
|
|
807
|
+
return executeCurrent().then(onfulfilled, onrejected);
|
|
808
|
+
},
|
|
809
|
+
catch(onrejected) {
|
|
810
|
+
return executeCurrent().catch(onrejected);
|
|
811
|
+
},
|
|
812
|
+
finally(onfinally) {
|
|
813
|
+
return executeCurrent().finally(onfinally);
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
/** @param {string|string[]} memberIds */
|
|
819
|
+
add(memberIds) {
|
|
820
|
+
return createTargetableAction((jid, users) => sock.groupParticipantsUpdate(jid, users, "add"), memberIds);
|
|
821
|
+
},
|
|
822
|
+
/** @param {string|string[]} memberIds */
|
|
823
|
+
async kick(memberIds) {
|
|
824
|
+
requireChat();
|
|
825
|
+
return sock.groupParticipantsUpdate(chatJid, norm(memberIds), "remove");
|
|
826
|
+
},
|
|
827
|
+
/** @param {string|string[]} memberIds */
|
|
828
|
+
async promote(memberIds) {
|
|
829
|
+
requireChat();
|
|
830
|
+
return sock.groupParticipantsUpdate(chatJid, norm(memberIds), "promote");
|
|
831
|
+
},
|
|
832
|
+
/** @param {string|string[]} memberIds */
|
|
833
|
+
async demote(memberIds) {
|
|
834
|
+
requireChat();
|
|
835
|
+
return sock.groupParticipantsUpdate(chatJid, norm(memberIds), "demote");
|
|
836
|
+
},
|
|
837
|
+
/** @param {string} name */
|
|
838
|
+
async setSubject(name) {
|
|
839
|
+
requireChat();
|
|
840
|
+
return sock.groupUpdateSubject(chatJid, name);
|
|
841
|
+
},
|
|
842
|
+
/** @param {string} text */
|
|
843
|
+
async setDescription(text) {
|
|
844
|
+
requireChat();
|
|
845
|
+
return sock.groupUpdateDescription(chatJid, text);
|
|
846
|
+
},
|
|
847
|
+
/** @param {string|Buffer} source */
|
|
848
|
+
async setProfilePic(source) {
|
|
849
|
+
requireChat();
|
|
850
|
+
const buffer = Buffer.isBuffer(source) ? source : readFileSync(source);
|
|
851
|
+
return sock.updateProfilePicture(chatJid, buffer);
|
|
852
|
+
},
|
|
853
|
+
async getInviteLink() {
|
|
854
|
+
requireChat();
|
|
855
|
+
const code = await sock.groupInviteCode(chatJid);
|
|
856
|
+
return `https://chat.whatsapp.com/${code}`;
|
|
857
|
+
},
|
|
858
|
+
async revokeInvite() {
|
|
859
|
+
requireChat();
|
|
860
|
+
return sock.groupRevokeInvite(chatJid);
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
// ── Me API ────────────────────────────────────────────────────────────────────
|
|
865
|
+
/** @param {WASocket} sock */
|
|
866
|
+
function buildMeApi(sock) {
|
|
867
|
+
return {
|
|
868
|
+
/** @param {string} name */
|
|
869
|
+
async setName(name) {
|
|
870
|
+
return sock.updateProfileName(name);
|
|
871
|
+
},
|
|
872
|
+
/** @param {string} text */
|
|
873
|
+
async setAbout(text) {
|
|
874
|
+
return sock.updateProfileStatus(text);
|
|
875
|
+
},
|
|
876
|
+
/** @param {string|Buffer} source */
|
|
877
|
+
async setProfilePic(source) {
|
|
878
|
+
const buffer = Buffer.isBuffer(source) ? source : readFileSync(source);
|
|
879
|
+
const jid = sock.user?.id ?? "";
|
|
880
|
+
return sock.updateProfilePicture(jid, buffer);
|
|
881
|
+
},
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
// ── Poll API ──────────────────────────────────────────────────────────────────
|
|
885
|
+
const pollRegistry = new Map();
|
|
886
|
+
const pollListeners = new Set();
|
|
887
|
+
/**
|
|
888
|
+
* Tracks votes for an active poll.
|
|
889
|
+
* Obtained via ctx.poll.create().
|
|
890
|
+
*/
|
|
891
|
+
class PollHandle {
|
|
892
|
+
msgId;
|
|
893
|
+
_options;
|
|
894
|
+
_callbacks;
|
|
895
|
+
_registry;
|
|
896
|
+
constructor(msg, options, registry) {
|
|
897
|
+
this.msgId = msg.key.id ?? "";
|
|
898
|
+
this._options = new Map(options.map(o => [o, new Set()]));
|
|
899
|
+
this._callbacks = [];
|
|
900
|
+
this._registry = registry;
|
|
901
|
+
}
|
|
902
|
+
/** Update from Baileys aggregated vote result. */
|
|
903
|
+
_updateFromAggregated(aggregated) {
|
|
904
|
+
// Reset all counts then rebuild from aggregate
|
|
905
|
+
for (const voters of this._options.values())
|
|
906
|
+
voters.clear();
|
|
907
|
+
for (const { name, voters } of aggregated) {
|
|
908
|
+
if (this._options.has(name))
|
|
909
|
+
this._options.set(name, new Set(voters));
|
|
910
|
+
}
|
|
911
|
+
for (const cb of this._callbacks)
|
|
912
|
+
cb(this.results(), aggregated);
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Register a callback invoked on every vote change.
|
|
916
|
+
* @param cb Receives (results, raw)
|
|
917
|
+
*/
|
|
918
|
+
onVote(cb) {
|
|
919
|
+
this._callbacks.push(cb);
|
|
920
|
+
return this;
|
|
921
|
+
}
|
|
922
|
+
/** Current tally as a plain object. */
|
|
923
|
+
results() {
|
|
924
|
+
const out = {};
|
|
925
|
+
for (const [name, voters] of this._options)
|
|
926
|
+
out[name] = voters.size;
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
/** Name(s) of the leading option(s). Returns [] if no votes yet. */
|
|
930
|
+
winner() {
|
|
931
|
+
const res = this.results();
|
|
932
|
+
const counts = Object.values(res);
|
|
933
|
+
if (!counts.length)
|
|
934
|
+
return [];
|
|
935
|
+
const max = Math.max(...counts);
|
|
936
|
+
if (max === 0)
|
|
937
|
+
return [];
|
|
938
|
+
return Object.entries(res).filter(([, v]) => v === max).map(([k]) => k);
|
|
939
|
+
}
|
|
940
|
+
/** Remove this poll from tracking. */
|
|
941
|
+
close() {
|
|
942
|
+
this._registry.delete(this.msgId);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* @param {WASocket} sock
|
|
947
|
+
* @param {WAStore} store
|
|
948
|
+
* @param {string} rawJid — destination JID (not normalized)
|
|
949
|
+
* @param {object} guardOptions
|
|
950
|
+
* @param {string} pluginName
|
|
951
|
+
*/
|
|
952
|
+
function buildPollApi(sock, store, rawJid, guardOptions, pluginName) {
|
|
953
|
+
if (!pollRegistry.has(pluginName))
|
|
954
|
+
pollRegistry.set(pluginName, new Map());
|
|
955
|
+
const registry = pollRegistry.get(pluginName);
|
|
956
|
+
// Keyed by creationId -> (voterKey -> latest vote entry). WhatsApp resends
|
|
957
|
+
// the *entire current selection* on every tap (not a diff), and Baileys'
|
|
958
|
+
// getAggregateVotesInPollMessage() replays whatever pollUpdates you give it
|
|
959
|
+
// with no dedup — so we must keep only the latest entry per voter ourselves,
|
|
960
|
+
// or retracted/changed votes keep counting alongside the new one.
|
|
961
|
+
const pollVotesByCreationId = new Map();
|
|
962
|
+
if (!pollListeners.has(pluginName)) {
|
|
963
|
+
pollListeners.add(pluginName);
|
|
964
|
+
const meId = sock.user?.id ? jidNormalizedUser(sock.user.id) : "me";
|
|
965
|
+
// WhatsApp doesn't consistently use the same JID shape (LID vs PN) for
|
|
966
|
+
// pollCreatorJid/voterJid when deriving the poll-vote decryption key —
|
|
967
|
+
// it depends on addressingMode, 1:1 vs group, and which side sent last.
|
|
968
|
+
// Trying to compute "the" correct JID up front (as the old resolveAuthor
|
|
969
|
+
// did, always preferring participantPn) causes AES-GCM auth failures
|
|
970
|
+
// whenever WhatsApp actually used the LID for that message. Instead,
|
|
971
|
+
// gather every plausible JID for each side and brute-force combinations
|
|
972
|
+
// until one decrypts successfully — see
|
|
973
|
+
// https://github.com/WhiskeySockets/Baileys/issues/2342 and #1678.
|
|
974
|
+
function jidCandidates(key) {
|
|
975
|
+
const cands = [];
|
|
976
|
+
if (key.fromMe) {
|
|
977
|
+
const selfLid = sock.user?.lid;
|
|
978
|
+
if (selfLid)
|
|
979
|
+
cands.push(jidNormalizedUser(selfLid));
|
|
980
|
+
if (sock.user?.id)
|
|
981
|
+
cands.push(jidNormalizedUser(sock.user.id));
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
const rawParticipant = key.participant ?? key.remoteJid;
|
|
985
|
+
if (rawParticipant)
|
|
986
|
+
cands.push(jidNormalizedUser(rawParticipant));
|
|
987
|
+
if (key.participantPn)
|
|
988
|
+
cands.push(jidNormalizedUser(key.participantPn));
|
|
989
|
+
if (rawParticipant?.endsWith("@lid")) {
|
|
990
|
+
const resolved = store.resolveJid(rawParticipant);
|
|
991
|
+
if (resolved && resolved !== rawParticipant)
|
|
992
|
+
cands.push(jidNormalizedUser(resolved));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return Array.from(new Set(cands.filter(Boolean)));
|
|
996
|
+
}
|
|
997
|
+
sock.ev.on("messages.upsert", async ({ messages: msgs }) => {
|
|
998
|
+
for (const msg of msgs) {
|
|
999
|
+
const pum = msg.message?.pollUpdateMessage;
|
|
1000
|
+
if (!pum)
|
|
1001
|
+
continue;
|
|
1002
|
+
const creationKey = pum.pollCreationMessageKey;
|
|
1003
|
+
const creationId = creationKey?.id ?? "";
|
|
1004
|
+
const handle = registry.get(creationId);
|
|
1005
|
+
if (!handle)
|
|
1006
|
+
continue;
|
|
1007
|
+
const storeMsg = store.messages.get(creationKey?.remoteJid ?? "")?.get(creationId);
|
|
1008
|
+
const pollEncKeyRaw = storeMsg?.message?.messageContextInfo?.messageSecret;
|
|
1009
|
+
if (!storeMsg || !pollEncKeyRaw || !pum.vote)
|
|
1010
|
+
continue;
|
|
1011
|
+
try {
|
|
1012
|
+
const pollEncKey = Buffer.isBuffer(pollEncKeyRaw)
|
|
1013
|
+
? pollEncKeyRaw
|
|
1014
|
+
: Buffer.from(pollEncKeyRaw, "base64");
|
|
1015
|
+
const creatorCandidates = jidCandidates((creationKey ?? {}));
|
|
1016
|
+
const voterCandidates = jidCandidates(msg.key);
|
|
1017
|
+
let decryptedVote;
|
|
1018
|
+
for (const pollCreatorJid of creatorCandidates) {
|
|
1019
|
+
for (const voterJid of voterCandidates) {
|
|
1020
|
+
try {
|
|
1021
|
+
decryptedVote = decryptPollVote(pum.vote, {
|
|
1022
|
+
pollEncKey,
|
|
1023
|
+
pollCreatorJid,
|
|
1024
|
+
pollMsgId: creationId,
|
|
1025
|
+
voterJid,
|
|
1026
|
+
});
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// try next JID combination
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (decryptedVote)
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
if (!decryptedVote) {
|
|
1037
|
+
throw new Error(`all JID combinations failed (creator=${JSON.stringify(creatorCandidates)}, voter=${JSON.stringify(voterCandidates)})`);
|
|
1038
|
+
}
|
|
1039
|
+
const voterKey = msg.key.fromMe
|
|
1040
|
+
? meId
|
|
1041
|
+
: jidNormalizedUser(msg.key.participant ?? msg.key.remoteJid ?? "");
|
|
1042
|
+
const votesByVoter = pollVotesByCreationId.get(creationId) ?? new Map();
|
|
1043
|
+
votesByVoter.set(voterKey, {
|
|
1044
|
+
pollUpdateMessageKey: msg.key,
|
|
1045
|
+
vote: decryptedVote,
|
|
1046
|
+
senderTimestampMs: pum.senderTimestampMs,
|
|
1047
|
+
});
|
|
1048
|
+
pollVotesByCreationId.set(creationId, votesByVoter);
|
|
1049
|
+
const aggregated = getAggregateVotesInPollMessage({ message: storeMsg.message, pollUpdates: Array.from(votesByVoter.values()) }, meId);
|
|
1050
|
+
handle._updateFromAggregated(aggregated);
|
|
1051
|
+
}
|
|
1052
|
+
catch (err) {
|
|
1053
|
+
logger.error(`[poll] erro ao decriptar voto: ${err}`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
const cooldown = (guardOptions.cooldown ?? true);
|
|
1059
|
+
const jitter = (guardOptions.jitter ?? true);
|
|
1060
|
+
return {
|
|
1061
|
+
/**
|
|
1062
|
+
* Send a poll and start tracking votes.
|
|
1063
|
+
* @param {string} question
|
|
1064
|
+
* @param {string[]} options
|
|
1065
|
+
* @param {object} [opts]
|
|
1066
|
+
* @param {boolean} [opts.allowMultipleAnswers=false]
|
|
1067
|
+
* @returns {Promise<PollHandle>}
|
|
1068
|
+
*/
|
|
1069
|
+
async create(question, options, opts = {}) {
|
|
1070
|
+
const sender = makeSender(sock, store, rawJid, null, { cooldown, jitter });
|
|
1071
|
+
const handlePromise = sender.poll(question, options, opts);
|
|
1072
|
+
const rawMsg = await handlePromise.rawPromise;
|
|
1073
|
+
if (!rawMsg)
|
|
1074
|
+
throw new Error("[poll] failed to send poll message");
|
|
1075
|
+
const handle = new PollHandle(rawMsg, options, registry);
|
|
1076
|
+
registry.set(handle.msgId, handle);
|
|
1077
|
+
// Ensure the poll message is in the store before any vote arrives —
|
|
1078
|
+
// messages.upsert isn't guaranteed to fire (or land in time) for the
|
|
1079
|
+
// bot's own sent messages, which previously dropped every vote.
|
|
1080
|
+
const remoteJid = rawMsg.key.remoteJid;
|
|
1081
|
+
if (remoteJid) {
|
|
1082
|
+
if (!store.messages.has(remoteJid))
|
|
1083
|
+
store.messages.set(remoteJid, new Map());
|
|
1084
|
+
if (!store.messages.get(remoteJid).has(handle.msgId)) {
|
|
1085
|
+
store.messages.get(remoteJid).set(handle.msgId, rawMsg);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return handle;
|
|
1089
|
+
},
|
|
1090
|
+
/**
|
|
1091
|
+
* Retrieve an active PollHandle by its message ID.
|
|
1092
|
+
* @param {string} msgId
|
|
1093
|
+
*/
|
|
1094
|
+
get(msgId) {
|
|
1095
|
+
return registry.get(msgId) ?? null;
|
|
1096
|
+
},
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
// ── Base API (shared between setup and runtime) ───────────────────────────────
|
|
1100
|
+
function buildBaseApi(sock, store, pluginRegistry, pluginName) {
|
|
1101
|
+
const botJid = sock.user?.id ? normalizeJid(sock.user.id) : null;
|
|
1102
|
+
if (!botJid)
|
|
1103
|
+
logger.warn("[pluginApi] botId is null — socket may not be ready yet.");
|
|
1104
|
+
return {
|
|
1105
|
+
log,
|
|
1106
|
+
t,
|
|
1107
|
+
config: buildConfigApi(),
|
|
1108
|
+
i18n: buildI18nApi(),
|
|
1109
|
+
utils: buildUtilsApi(),
|
|
1110
|
+
download: buildDownloadApi(),
|
|
1111
|
+
scheduler: buildSchedulerApi(pluginName),
|
|
1112
|
+
plugins: buildPluginsApi(pluginRegistry),
|
|
1113
|
+
contacts: buildContactsApi(sock, store, botJid),
|
|
1114
|
+
storage: buildStorageApi(pluginName),
|
|
1115
|
+
botId: botJid,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
// ── Setup API ─────────────────────────────────────────────────────────────────
|
|
1119
|
+
/**
|
|
1120
|
+
* Setup API — without message context.
|
|
1121
|
+
* Passed to plugin.setup(ctx) during initialization.
|
|
1122
|
+
*
|
|
1123
|
+
* @param {WASocket} sock
|
|
1124
|
+
* @param {WAStore} store
|
|
1125
|
+
* @param {Map<string, any>} pluginRegistry
|
|
1126
|
+
* @param {string} pluginName
|
|
1127
|
+
*/
|
|
1128
|
+
export function buildSetupApi(sock, store, pluginRegistry, pluginName) {
|
|
1129
|
+
return {
|
|
1130
|
+
...buildBaseApi(sock, store, pluginRegistry, pluginName),
|
|
1131
|
+
...buildSetupSendApi(sock, store),
|
|
1132
|
+
admin: buildAdminApi(sock, null),
|
|
1133
|
+
events: buildEventsApi(sock, pluginName),
|
|
1134
|
+
me: buildMeApi(sock),
|
|
1135
|
+
settings: { global: buildSettingsApi(pluginName, "_global").global },
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
// ── Runtime API ───────────────────────────────────────────────────────────────
|
|
1139
|
+
/**
|
|
1140
|
+
* Runtime API — full context with message and chat.
|
|
1141
|
+
* Passed to plugin.default(ctx) on every message.
|
|
1142
|
+
*
|
|
1143
|
+
* @param {object} params
|
|
1144
|
+
* @param {WAProtoMsg} params.msg
|
|
1145
|
+
* @param {WAChat} params.chat
|
|
1146
|
+
* @param {WASocket} params.sock
|
|
1147
|
+
* @param {WAStore} params.store
|
|
1148
|
+
* @param {Map} params.pluginRegistry
|
|
1149
|
+
* @param {string} params.pluginName
|
|
1150
|
+
* @param {object} [params.guardOptions]
|
|
1151
|
+
*/
|
|
1152
|
+
export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, guardOptions = {}, }) {
|
|
1153
|
+
const prefix = CONFIG.CMD_PREFIX;
|
|
1154
|
+
const body = getMsgBody(msg);
|
|
1155
|
+
const rawArgs = body.trim().split(/\s+/);
|
|
1156
|
+
const first = rawArgs[0]?.toLowerCase() ?? "";
|
|
1157
|
+
const hasPrefix = first.startsWith(prefix);
|
|
1158
|
+
const command = hasPrefix ? first.slice(prefix.length) : "";
|
|
1159
|
+
const rawJid = msg.key.remoteJid ?? "";
|
|
1160
|
+
const normJid = normalizeJid(rawJid);
|
|
1161
|
+
const sender = getMsgSender(msg, store);
|
|
1162
|
+
const cooldown = (guardOptions.cooldown ?? true);
|
|
1163
|
+
const jitter = (guardOptions.jitter ?? true);
|
|
1164
|
+
// Sender for quoted messages
|
|
1165
|
+
const contextInfo = getContextInfo(msg);
|
|
1166
|
+
const quotedRaw = contextInfo?.quotedMessage
|
|
1167
|
+
? {
|
|
1168
|
+
key: {
|
|
1169
|
+
remoteJid: rawJid,
|
|
1170
|
+
fromMe: false,
|
|
1171
|
+
id: contextInfo.stanzaId ?? undefined,
|
|
1172
|
+
participant: contextInfo.participant ?? undefined,
|
|
1173
|
+
},
|
|
1174
|
+
message: contextInfo.quotedMessage,
|
|
1175
|
+
pushName: null,
|
|
1176
|
+
}
|
|
1177
|
+
: null;
|
|
1178
|
+
// Group participant JIDs come back in whatever addressing mode the group
|
|
1179
|
+
// uses (@lid or @s.whatsapp.net/@c.us) — same issue as poll vote decryption.
|
|
1180
|
+
// "sender" and "sock.user.id" are usually PN-normalized, so a straight
|
|
1181
|
+
// `=== ` against an @lid participant list silently never matches, even
|
|
1182
|
+
// when the person genuinely is an admin. Compare every known form
|
|
1183
|
+
// (raw + store-resolved) on both sides instead of trusting a single shape.
|
|
1184
|
+
function matchesParticipant(candidates, participantId) {
|
|
1185
|
+
const pRaw = normalizeJid(participantId);
|
|
1186
|
+
const pResolved = normalizeJid(store.resolveJid(pRaw));
|
|
1187
|
+
for (const raw of candidates) {
|
|
1188
|
+
if (!raw)
|
|
1189
|
+
continue;
|
|
1190
|
+
const c = normalizeJid(raw);
|
|
1191
|
+
const cResolved = normalizeJid(store.resolveJid(c));
|
|
1192
|
+
if (c === pRaw || c === pResolved || cResolved === pRaw || cResolved === pResolved)
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
...buildBaseApi(sock, store, pluginRegistry, pluginName),
|
|
1199
|
+
...buildSendApi(sock, store, rawJid, guardOptions),
|
|
1200
|
+
// ── msg ──────────────────────────────────────────────────────────────────
|
|
1201
|
+
msg: buildMessageContext(msg, sock, store, { cooldown, jitter }),
|
|
1202
|
+
// ── chat ─────────────────────────────────────────────────────────────────
|
|
1203
|
+
chat: {
|
|
1204
|
+
id: normJid,
|
|
1205
|
+
name: chat.name,
|
|
1206
|
+
isGroup: chat.isGroup,
|
|
1207
|
+
/**
|
|
1208
|
+
* List of group participants.
|
|
1209
|
+
* Returns [] for non-group chats.
|
|
1210
|
+
* @returns {Promise<Array<{ id: string, isAdmin: boolean, isSuperAdmin: boolean }>>}
|
|
1211
|
+
*/
|
|
1212
|
+
async getParticipants() {
|
|
1213
|
+
if (!chat.isGroup)
|
|
1214
|
+
return [];
|
|
1215
|
+
try {
|
|
1216
|
+
const meta = await sock.groupMetadata(rawJid);
|
|
1217
|
+
return meta.participants.map(p => ({
|
|
1218
|
+
id: normalizeJid(p.id),
|
|
1219
|
+
isAdmin: p.admin === "admin" || p.admin === "superadmin",
|
|
1220
|
+
isSuperAdmin: p.admin === "superadmin",
|
|
1221
|
+
}));
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
return [];
|
|
1225
|
+
}
|
|
1226
|
+
},
|
|
1227
|
+
/**
|
|
1228
|
+
* Check if a contact is an admin of this group.
|
|
1229
|
+
* @param {string} contactId
|
|
1230
|
+
* @returns {Promise<boolean>}
|
|
1231
|
+
*/
|
|
1232
|
+
async isAdmin(contactId) {
|
|
1233
|
+
if (!chat.isGroup)
|
|
1234
|
+
return false;
|
|
1235
|
+
try {
|
|
1236
|
+
const meta = await sock.groupMetadata(rawJid);
|
|
1237
|
+
return meta.participants.some(p => matchesParticipant([contactId], p.id) && (p.admin === "admin" || p.admin === "superadmin"));
|
|
1238
|
+
}
|
|
1239
|
+
catch {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
/**
|
|
1244
|
+
* Check if the message sender is an admin of this group.
|
|
1245
|
+
* @returns {Promise<boolean>}
|
|
1246
|
+
*/
|
|
1247
|
+
async isSenderAdmin() {
|
|
1248
|
+
if (!chat.isGroup)
|
|
1249
|
+
return false;
|
|
1250
|
+
try {
|
|
1251
|
+
const meta = await sock.groupMetadata(rawJid);
|
|
1252
|
+
const rawSenderParticipant = msg.key.participant ?? msg.key.remoteJid ?? "";
|
|
1253
|
+
return meta.participants.some(p => matchesParticipant([sender, rawSenderParticipant], p.id) && (p.admin === "admin" || p.admin === "superadmin"));
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
},
|
|
1259
|
+
/**
|
|
1260
|
+
* Check if the bot is an admin of this group.
|
|
1261
|
+
* @returns {Promise<boolean>}
|
|
1262
|
+
*/
|
|
1263
|
+
async isBotAdmin() {
|
|
1264
|
+
if (!chat.isGroup)
|
|
1265
|
+
return false;
|
|
1266
|
+
const botLid = sock.user?.lid;
|
|
1267
|
+
const botCandidates = [sock.user?.id, botLid];
|
|
1268
|
+
if (!botCandidates.some(Boolean))
|
|
1269
|
+
return false;
|
|
1270
|
+
try {
|
|
1271
|
+
const meta = await sock.groupMetadata(rawJid);
|
|
1272
|
+
return meta.participants.some(p => matchesParticipant(botCandidates, p.id) && (p.admin === "admin" || p.admin === "superadmin"));
|
|
1273
|
+
}
|
|
1274
|
+
catch {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
/** Clear all messages in this chat — not supported in Baileys. */
|
|
1279
|
+
async clearMessages() {
|
|
1280
|
+
logger.warn("[pluginApi] clearMessages() is not supported with Baileys");
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
// ── admin ─────────────────────────────────────────────────────────────────
|
|
1284
|
+
admin: buildAdminApi(sock, rawJid),
|
|
1285
|
+
// ── me ────────────────────────────────────────────────────────────────────
|
|
1286
|
+
me: buildMeApi(sock),
|
|
1287
|
+
// ── poll ──────────────────────────────────────────────────────────────────
|
|
1288
|
+
poll: buildPollApi(sock, store, rawJid, guardOptions, pluginName),
|
|
1289
|
+
// ── settings ──────────────────────────────────────────────────────────────
|
|
1290
|
+
settings: buildSettingsApi(pluginName, normJid),
|
|
1291
|
+
// ── isolated platform contexts ────────────────────────────────────────────
|
|
1292
|
+
wa: {
|
|
1293
|
+
sock,
|
|
1294
|
+
store,
|
|
1295
|
+
msg,
|
|
1296
|
+
downloadMedia: async () => {
|
|
1297
|
+
try {
|
|
1298
|
+
const buffer = await downloadMediaMessage(msg, "buffer", {});
|
|
1299
|
+
if (!buffer || !Buffer.isBuffer(buffer))
|
|
1300
|
+
return null;
|
|
1301
|
+
return { mimetype: getMsgMimetype(msg), data: buffer.toString("base64") };
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
},
|
|
1308
|
+
tg: null,
|
|
1309
|
+
dc: null,
|
|
1310
|
+
};
|
|
1311
|
+
}
|