@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
package/src/kernel/pluginApi.js
DELETED
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pluginApi.js
|
|
3
|
-
*
|
|
4
|
-
* Builds the `ctx` object each plugin receives.
|
|
5
|
-
* Plugins can only do what's here — never touch client directly.
|
|
6
|
-
*
|
|
7
|
-
* `chat` is already filtered by kernel (only allowed chats from .conf),
|
|
8
|
-
* so plugins don't need and can't choose destination, unless they use sendTo.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { logger } from "#logger";
|
|
12
|
-
import { t, createPluginT, reloadTranslations,
|
|
13
|
-
getCurrentLang } from "#i18n";
|
|
14
|
-
import { CONFIG, CONFIG_DIR } from "#config";
|
|
15
|
-
import { enqueue } from "#download";
|
|
16
|
-
import { emptyFolder } from "#utils/file";
|
|
17
|
-
import { getChatId } from "#utils/getChatId";
|
|
18
|
-
import pkg from "whatsapp-web.js";
|
|
19
|
-
import { mkdirSync } from "fs";
|
|
20
|
-
import path from "path";
|
|
21
|
-
import { waitForSendSlot, simulateState,
|
|
22
|
-
typingDuration, mediaDuration } from "#sendguard";
|
|
23
|
-
|
|
24
|
-
const { MessageMedia } = pkg;
|
|
25
|
-
|
|
26
|
-
// ── Storage API ──────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
export function buildStorageApi(pluginName) {
|
|
29
|
-
if (typeof pluginName !== "string" || pluginName.trim() === "") {
|
|
30
|
-
throw new Error("[storage] buildStorageApi: pluginName must be a non-empty string");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const dir = path.join(CONFIG_DIR, "data", pluginName);
|
|
34
|
-
mkdirSync(dir, { recursive: true });
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
dir,
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolves a path inside the plugin's data directory.
|
|
41
|
-
* Creates subdirectories automatically.
|
|
42
|
-
* @param {string} relativePath
|
|
43
|
-
* @returns {string}
|
|
44
|
-
*/
|
|
45
|
-
resolve(relativePath) {
|
|
46
|
-
if (!relativePath || typeof relativePath !== "string") {
|
|
47
|
-
throw new Error(`[storage] resolve() requires a non-empty string, got: ${typeof relativePath}`);
|
|
48
|
-
}
|
|
49
|
-
if (relativePath.includes("..")) {
|
|
50
|
-
throw new Error(`[storage] path traversal detected in: "${relativePath}"`);
|
|
51
|
-
}
|
|
52
|
-
if (path.isAbsolute(relativePath)) {
|
|
53
|
-
throw new Error(`[storage] absolute paths are not allowed: "${relativePath}"`);
|
|
54
|
-
}
|
|
55
|
-
if (relativePath.includes("\\")) {
|
|
56
|
-
throw new Error(`[storage] Windows-style paths are not allowed: "${relativePath}"`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const resolved = path.join(dir, relativePath);
|
|
60
|
-
|
|
61
|
-
if (!resolved.startsWith(path.resolve(dir) + path.sep)) {
|
|
62
|
-
throw new Error(`[storage] resolved path escapes plugin data dir: "${resolved}"`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
mkdirSync(path.dirname(resolved), { recursive: true });
|
|
66
|
-
return resolved;
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ── Config API ───────────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
function buildConfigApi() {
|
|
74
|
-
return {
|
|
75
|
-
/**
|
|
76
|
-
* Get a config value with optional default.
|
|
77
|
-
* @param {string} key
|
|
78
|
-
* @param {any} [defaultValue]
|
|
79
|
-
*/
|
|
80
|
-
get(key, defaultValue = null) {
|
|
81
|
-
return CONFIG[key] ?? defaultValue;
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── i18n API ─────────────────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
function buildI18nApi() {
|
|
89
|
-
return {
|
|
90
|
-
/** Translate a core key. */
|
|
91
|
-
t,
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Create a scoped t() for a plugin's own locale files.
|
|
95
|
-
* @param {string} pluginMetaUrl — pass import.meta.url from the plugin
|
|
96
|
-
*/
|
|
97
|
-
createT: createPluginT,
|
|
98
|
-
|
|
99
|
-
/** Reload all translations (e.g. after language change). */
|
|
100
|
-
reload: reloadTranslations,
|
|
101
|
-
|
|
102
|
-
/** Returns current language code. */
|
|
103
|
-
getCurrentLang,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── Utils API ────────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
function buildUtilsApi() {
|
|
110
|
-
return {
|
|
111
|
-
/**
|
|
112
|
-
* Empty a folder's contents without removing the folder itself.
|
|
113
|
-
* @param {string} folder
|
|
114
|
-
*/
|
|
115
|
-
emptyFolder,
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Get the serialized chat ID from a chat object.
|
|
119
|
-
* @param {import("whatsapp-web.js").Chat} chat
|
|
120
|
-
*/
|
|
121
|
-
getChatId,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ── Download API ─────────────────────────────────────────────────────────────
|
|
126
|
-
|
|
127
|
-
function buildDownloadApi() {
|
|
128
|
-
return {
|
|
129
|
-
/**
|
|
130
|
-
* Enqueue a download work function.
|
|
131
|
-
* @param {Function} workFn
|
|
132
|
-
* @param {Function} [errorFn]
|
|
133
|
-
*/
|
|
134
|
-
enqueue,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── Plugin registry API ──────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
function buildPluginsApi(pluginRegistry) {
|
|
141
|
-
return {
|
|
142
|
-
/**
|
|
143
|
-
* Return public API of another plugin, or null if not active.
|
|
144
|
-
* @param {string} name
|
|
145
|
-
* @returns {any|null}
|
|
146
|
-
*/
|
|
147
|
-
get(name) {
|
|
148
|
-
return pluginRegistry.get(name)?.exports ?? null;
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Return public API of another plugin, or throw if not active.
|
|
153
|
-
* Analogous to require() — use when the dependency is mandatory.
|
|
154
|
-
* @param {string} name
|
|
155
|
-
* @returns {any}
|
|
156
|
-
*/
|
|
157
|
-
require(name) {
|
|
158
|
-
const plugin = pluginRegistry.get(name);
|
|
159
|
-
if (!plugin || plugin.status !== "active") {
|
|
160
|
-
throw new Error(`[plugins] dependency "${name}" does not exist or is not active`);
|
|
161
|
-
}
|
|
162
|
-
return plugin.exports;
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Check if a plugin is active.
|
|
167
|
-
* @param {string} name
|
|
168
|
-
* @returns {boolean}
|
|
169
|
-
*/
|
|
170
|
-
exists(name) {
|
|
171
|
-
return pluginRegistry.get(name)?.status === "active";
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ── Log API ──────────────────────────────────────────────────────────────────
|
|
177
|
-
|
|
178
|
-
const log = {
|
|
179
|
-
info: (...a) => logger.info(...a),
|
|
180
|
-
warn: (...a) => logger.warn(...a),
|
|
181
|
-
error: (...a) => logger.error(...a),
|
|
182
|
-
success: (...a) => logger.success(...a),
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// ── Contact API ──────────────────────────────────────────────────────────────
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Normalizes a raw whatsapp-web.js Contact into a plain object.
|
|
189
|
-
* Used internally so both ctx.contacts and ctx.msg.getContact()
|
|
190
|
-
* always return the same shape.
|
|
191
|
-
* @param {import("whatsapp-web.js").Contact} c
|
|
192
|
-
* @returns {object}
|
|
193
|
-
*/
|
|
194
|
-
function normalizeContact(c) {
|
|
195
|
-
return {
|
|
196
|
-
id: c.id._serialized,
|
|
197
|
-
number: c.number,
|
|
198
|
-
pushname: c.pushname ?? null,
|
|
199
|
-
name: c.name ?? null,
|
|
200
|
-
shortName: c.shortName ?? null,
|
|
201
|
-
isBusiness: c.isBusiness,
|
|
202
|
-
isEnterprise: c.isEnterprise,
|
|
203
|
-
isBlocked: c.isBlocked,
|
|
204
|
-
isMe: c.isMe,
|
|
205
|
-
isMyContact: c.isMyContact,
|
|
206
|
-
isWAContact: c.isWAContact,
|
|
207
|
-
isUser: c.isUser,
|
|
208
|
-
isGroup: c.isGroup,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function buildContactsApi(client) {
|
|
213
|
-
return {
|
|
214
|
-
/**
|
|
215
|
-
* Get a normalized Contact object by ID.
|
|
216
|
-
* @param {string} contactId — serialized ID (e.g. "5511999999999@c.us")
|
|
217
|
-
* @returns {Promise<object|null>}
|
|
218
|
-
*/
|
|
219
|
-
async get(contactId) {
|
|
220
|
-
try {
|
|
221
|
-
const c = await client.getContactById(contactId);
|
|
222
|
-
return normalizeContact(c);
|
|
223
|
-
} catch {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Get the profile picture URL of a contact.
|
|
230
|
-
* Respects privacy settings — may return null.
|
|
231
|
-
* @param {string} contactId
|
|
232
|
-
* @returns {Promise<string|null>}
|
|
233
|
-
*/
|
|
234
|
-
async getProfilePicUrl(contactId) {
|
|
235
|
-
try {
|
|
236
|
-
const c = await client.getContactById(contactId);
|
|
237
|
-
return await c.getProfilePicUrl();
|
|
238
|
-
} catch {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Get the "about" text of a contact.
|
|
245
|
-
* Returns null if privacy settings block access.
|
|
246
|
-
* @param {string} contactId
|
|
247
|
-
* @returns {Promise<string|null>}
|
|
248
|
-
*/
|
|
249
|
-
async getAbout(contactId) {
|
|
250
|
-
try {
|
|
251
|
-
const c = await client.getContactById(contactId);
|
|
252
|
-
return await c.getAbout();
|
|
253
|
-
} catch {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── Internal media helpers ───────────────────────────────────────────────────
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* @param {string|Buffer} source
|
|
264
|
-
* @param {string} mimetype — required, no ambiguous default
|
|
265
|
-
*/
|
|
266
|
-
function mediaFromSource(source, mimetype) {
|
|
267
|
-
return typeof source === "string"
|
|
268
|
-
? MessageMedia.fromFilePath(source)
|
|
269
|
-
: new MessageMedia(mimetype, source.toString("base64"));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Returns send methods bound to a target that exposes `.sendMessage()`.
|
|
274
|
-
*
|
|
275
|
-
* @param {{ sendMessage: Function }} target
|
|
276
|
-
* @param {object} [extraOpts] — merged into every sendMessage call (e.g. { quoted: msg })
|
|
277
|
-
* @param {string|null} [chatId] — serialized chat ID; enables sendGuard when set
|
|
278
|
-
* @param {object|null} [chatObj] — real Chat object; enables typing indicator when set
|
|
279
|
-
*/
|
|
280
|
-
function makeSender(target, extraOpts = {}, chatId = null, chatObj = null, { cooldown = true, jitter = true } = {}) {
|
|
281
|
-
return {
|
|
282
|
-
async text(content, opts = {}) {
|
|
283
|
-
if (chatId) {
|
|
284
|
-
await waitForSendSlot(chatId, { cooldown, jitter });
|
|
285
|
-
await simulateState(chatObj, typingDuration(content), "typing");
|
|
286
|
-
}
|
|
287
|
-
return target.sendMessage(content, { ...extraOpts, ...opts });
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
async image(filePath, caption = "") {
|
|
291
|
-
if (chatId) {
|
|
292
|
-
await waitForSendSlot(chatId, { cooldown, jitter });
|
|
293
|
-
await simulateState(chatObj, mediaDuration(), "typing");
|
|
294
|
-
}
|
|
295
|
-
const media = MessageMedia.fromFilePath(filePath);
|
|
296
|
-
return target.sendMessage(media, { caption, ...extraOpts });
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
async video(filePath, caption = "") {
|
|
300
|
-
if (chatId) {
|
|
301
|
-
await waitForSendSlot(chatId, { cooldown, jitter });
|
|
302
|
-
await simulateState(chatObj, mediaDuration(), "typing");
|
|
303
|
-
}
|
|
304
|
-
const media = MessageMedia.fromFilePath(filePath);
|
|
305
|
-
return target.sendMessage(media, { caption, ...extraOpts });
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
async audio(filePath, { asVoice = true } = {}) {
|
|
309
|
-
if (chatId) {
|
|
310
|
-
await waitForSendSlot(chatId, { cooldown, jitter });
|
|
311
|
-
// "gravando áudio…" é mais convincente que "digitando…" para áudio
|
|
312
|
-
await simulateState(chatObj, mediaDuration(), "recording");
|
|
313
|
-
}
|
|
314
|
-
const media = MessageMedia.fromFilePath(filePath);
|
|
315
|
-
return target.sendMessage(media, { sendAudioAsVoice: asVoice, ...extraOpts });
|
|
316
|
-
},
|
|
317
|
-
|
|
318
|
-
async sticker(source) {
|
|
319
|
-
if (chatId) {
|
|
320
|
-
await waitForSendSlot(chatId, { cooldown, jitter });
|
|
321
|
-
await simulateState(chatObj, mediaDuration(), "typing");
|
|
322
|
-
}
|
|
323
|
-
const media = mediaFromSource(source, "image/webp");
|
|
324
|
-
return target.sendMessage(media, { sendMediaAsSticker: true, ...extraOpts });
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
async file(filePath, filename) {
|
|
328
|
-
if (chatId) {
|
|
329
|
-
await waitForSendSlot(chatId, { cooldown, jitter });
|
|
330
|
-
await simulateState(chatObj, mediaDuration(), "typing");
|
|
331
|
-
}
|
|
332
|
-
const media = MessageMedia.fromFilePath(filePath);
|
|
333
|
-
return target.sendMessage(media, {
|
|
334
|
-
sendMediaAsDocument: true,
|
|
335
|
-
filename: filename ?? path.basename(filePath),
|
|
336
|
-
...extraOpts,
|
|
337
|
-
});
|
|
338
|
-
},
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** Adapts client.sendMessage(chatId, ...) to the makeSender interface. */
|
|
343
|
-
function chatIdTarget(client, chatId) {
|
|
344
|
-
return {
|
|
345
|
-
sendMessage: (content, opts) => client.sendMessage(chatId, content, opts),
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// ── Send API ─────────────────────────────────────────────────────────────────
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Runtime send API — current chat + .to() for other chats.
|
|
353
|
-
*
|
|
354
|
-
* ctx.send.text("oi")
|
|
355
|
-
* ctx.send.image("./foto.jpg", "legenda")
|
|
356
|
-
* ctx.send.to("5511@c.us").text("oi")
|
|
357
|
-
*/
|
|
358
|
-
function buildSendApi(chat, client, guardOptions = {}) {
|
|
359
|
-
const chatId = chat.id._serialized;
|
|
360
|
-
const { cooldown = true, jitter = true } = guardOptions;
|
|
361
|
-
const current = makeSender(chat, {}, chatId, chat, { cooldown, jitter });
|
|
362
|
-
return {
|
|
363
|
-
send: {
|
|
364
|
-
text: (text, opts) => current.text(text, opts),
|
|
365
|
-
image: (filePath, caption) => current.image(filePath, caption),
|
|
366
|
-
video: (filePath, caption) => current.video(filePath, caption),
|
|
367
|
-
audio: (filePath, opts) => current.audio(filePath, opts),
|
|
368
|
-
sticker: (source) => current.sticker(source),
|
|
369
|
-
file: (filePath, filename) => current.file(filePath, filename),
|
|
370
|
-
/**
|
|
371
|
-
* Returns a sender bound to another chat.
|
|
372
|
-
* Typing simulation is skipped (no Chat object available without a fetch).
|
|
373
|
-
* @param {string} targetChatId
|
|
374
|
-
*/
|
|
375
|
-
to: (targetChatId) =>
|
|
376
|
-
makeSender(chatIdTarget(client, targetChatId), {}, targetChatId, null),
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Setup send API — no current chat, only .to().
|
|
382
|
-
*
|
|
383
|
-
* ctx.send.to(adminChatId).text("bot iniciado")
|
|
384
|
-
*/
|
|
385
|
-
function buildSetupSendApi(client) {
|
|
386
|
-
return {
|
|
387
|
-
send: {
|
|
388
|
-
to: (targetChatId) =>
|
|
389
|
-
makeSender(chatIdTarget(client, targetChatId), {}, targetChatId, null),
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ── Events API (setup only) ───────────────────────────────────────────────────
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* @param {import("whatsapp-web.js").Client} client
|
|
398
|
-
* @param {string} pluginName
|
|
399
|
-
*/
|
|
400
|
-
const listenerRegistry = new Map();
|
|
401
|
-
|
|
402
|
-
function buildEventsApi(client, pluginName) {
|
|
403
|
-
return {
|
|
404
|
-
on(event, handler) {
|
|
405
|
-
client.on(event, handler);
|
|
406
|
-
|
|
407
|
-
if (!listenerRegistry.has(pluginName)) {
|
|
408
|
-
listenerRegistry.set(pluginName, new Set());
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const ref = { event, handler };
|
|
412
|
-
|
|
413
|
-
listenerRegistry
|
|
414
|
-
.get(pluginName)
|
|
415
|
-
.add(ref);
|
|
416
|
-
|
|
417
|
-
return () => {
|
|
418
|
-
client.off(event, handler);
|
|
419
|
-
|
|
420
|
-
listenerRegistry
|
|
421
|
-
.get(pluginName)
|
|
422
|
-
?.delete(ref);
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
listenerRegistry
|
|
426
|
-
.get(pluginName)
|
|
427
|
-
?.size === 0
|
|
428
|
-
) {
|
|
429
|
-
listenerRegistry.delete(pluginName);
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
},
|
|
433
|
-
|
|
434
|
-
once(event) {
|
|
435
|
-
return new Promise((resolve) => {
|
|
436
|
-
const off =
|
|
437
|
-
this.on(
|
|
438
|
-
event,
|
|
439
|
-
(data) => {
|
|
440
|
-
off();
|
|
441
|
-
resolve(data);
|
|
442
|
-
}
|
|
443
|
-
);
|
|
444
|
-
});
|
|
445
|
-
},
|
|
446
|
-
|
|
447
|
-
cleanup() {
|
|
448
|
-
const list =
|
|
449
|
-
listenerRegistry.get(pluginName);
|
|
450
|
-
|
|
451
|
-
if (!list) return;
|
|
452
|
-
|
|
453
|
-
for (const {
|
|
454
|
-
event,
|
|
455
|
-
handler
|
|
456
|
-
} of list) {
|
|
457
|
-
client.off(
|
|
458
|
-
event,
|
|
459
|
-
handler
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
listenerRegistry.delete(pluginName);
|
|
463
|
-
},
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// ── Base API (shared between setup and runtime) ───────────────────────────────
|
|
468
|
-
|
|
469
|
-
function buildBaseApi(client, pluginRegistry, pluginName) {
|
|
470
|
-
const botId = client.info?.wid?._serialized ?? null;
|
|
471
|
-
if (!botId) logger.warn("[pluginApi] botId is null — client may not be ready yet.");
|
|
472
|
-
|
|
473
|
-
return {
|
|
474
|
-
log,
|
|
475
|
-
t,
|
|
476
|
-
config: buildConfigApi(),
|
|
477
|
-
i18n: buildI18nApi(),
|
|
478
|
-
utils: buildUtilsApi(),
|
|
479
|
-
download: buildDownloadApi(),
|
|
480
|
-
plugins: buildPluginsApi(pluginRegistry),
|
|
481
|
-
contacts: buildContactsApi(client),
|
|
482
|
-
storage: buildStorageApi(pluginName),
|
|
483
|
-
botId,
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ── Setup API ────────────────────────────────────────────────────────────────
|
|
488
|
-
|
|
489
|
-
/**
|
|
490
|
-
* Setup API — without message context.
|
|
491
|
-
* Passed to plugin.setup(ctx) during initialization.
|
|
492
|
-
*
|
|
493
|
-
* @param {import("whatsapp-web.js").Client} client
|
|
494
|
-
* @param {Map<string, any>} pluginRegistry
|
|
495
|
-
* @param {string} pluginName
|
|
496
|
-
* @returns {object}
|
|
497
|
-
*/
|
|
498
|
-
export function buildSetupApi(client, pluginRegistry, pluginName ) {
|
|
499
|
-
return {
|
|
500
|
-
...buildBaseApi(client, pluginRegistry, pluginName),
|
|
501
|
-
...buildSetupSendApi(client),
|
|
502
|
-
events: buildEventsApi(client, pluginName),
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// ── Runtime API ──────────────────────────────────────────────────────────────
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Runtime API — full context with message and chat.
|
|
510
|
-
* Passed to plugin.default(ctx) on every message.
|
|
511
|
-
*
|
|
512
|
-
* @param {object} params
|
|
513
|
-
* @param {import("whatsapp-web.js").Message} params.msg
|
|
514
|
-
* @param {import("whatsapp-web.js").Chat} params.chat
|
|
515
|
-
* @param {import("whatsapp-web.js").Client} params.client
|
|
516
|
-
* @param {Map<string, any>} params.pluginRegistry
|
|
517
|
-
* @param {string} params.pluginName
|
|
518
|
-
* @returns {object} ctx
|
|
519
|
-
*/
|
|
520
|
-
export function buildApi({ msg, chat, client, pluginRegistry, pluginName, guardOptions = {} }) {
|
|
521
|
-
const prefix = CONFIG.CMD_PREFIX;
|
|
522
|
-
const rawArgs = msg.body?.trim().split(/\s+/) ?? [];
|
|
523
|
-
const first = rawArgs[0]?.toLowerCase() ?? "";
|
|
524
|
-
const hasPrefix = first.startsWith(prefix)
|
|
525
|
-
const command = hasPrefix ? first.slice(prefix.length) : "";
|
|
526
|
-
|
|
527
|
-
const chatId = chat.id._serialized;
|
|
528
|
-
const { cooldown = true, jitter = true } = guardOptions;
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
...buildBaseApi(client, pluginRegistry, pluginName),
|
|
532
|
-
...buildSendApi(chat, client),
|
|
533
|
-
|
|
534
|
-
// ── msg ──────────────────────────────────────────────────
|
|
535
|
-
|
|
536
|
-
msg: {
|
|
537
|
-
body: msg.body ?? "",
|
|
538
|
-
type: msg.type,
|
|
539
|
-
fromMe: msg.fromMe,
|
|
540
|
-
sender: msg.author || msg.from,
|
|
541
|
-
senderName: msg._data?.notifyName || String(msg.from).replace(/(:\d+)?@.*$/, ""),
|
|
542
|
-
|
|
543
|
-
/** Command token without prefix (e.g. "play" for "!play foo"). */
|
|
544
|
-
command,
|
|
545
|
-
|
|
546
|
-
/** Arguments after the command token. */
|
|
547
|
-
args: rawArgs.slice(1),
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Check if message matches a given command.
|
|
551
|
-
* @param {string} cmd
|
|
552
|
-
*/
|
|
553
|
-
is(cmd) {
|
|
554
|
-
return this.hasPrefix && command === cmd.toLowerCase();
|
|
555
|
-
},
|
|
556
|
-
|
|
557
|
-
hasMedia: msg.hasMedia,
|
|
558
|
-
isGif: msg._data?.isGif ?? false,
|
|
559
|
-
|
|
560
|
-
async downloadMedia() {
|
|
561
|
-
const media = await msg.downloadMedia();
|
|
562
|
-
if (!media) return null;
|
|
563
|
-
return { mimetype: media.mimetype, data: media.data };
|
|
564
|
-
},
|
|
565
|
-
|
|
566
|
-
hasReply: msg.hasQuotedMsg,
|
|
567
|
-
|
|
568
|
-
async getReply() {
|
|
569
|
-
if (!msg.hasQuotedMsg) return null;
|
|
570
|
-
return msg.getQuotedMessage();
|
|
571
|
-
},
|
|
572
|
-
|
|
573
|
-
reply: makeSender(chat, { quotedMessageId: msg.id._serialized }, chatId, chat, { cooldown, jitter }),
|
|
574
|
-
|
|
575
|
-
async react(emoji) {
|
|
576
|
-
return msg.react(emoji);
|
|
577
|
-
},
|
|
578
|
-
|
|
579
|
-
hasPrefix,
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Get the sender as a normalized Contact object.
|
|
583
|
-
* Same shape as ctx.contacts.get().
|
|
584
|
-
* @returns {Promise<object|null>}
|
|
585
|
-
*/
|
|
586
|
-
async getContact() {
|
|
587
|
-
try {
|
|
588
|
-
const c = await msg.getContact();
|
|
589
|
-
return normalizeContact(c);
|
|
590
|
-
} catch {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
},
|
|
594
|
-
},
|
|
595
|
-
|
|
596
|
-
// ── chat ─────────────────────────────────────────────────
|
|
597
|
-
|
|
598
|
-
chat: {
|
|
599
|
-
id: chatId,
|
|
600
|
-
name: chat.name || chat.id.user,
|
|
601
|
-
isGroup: /@g\.us$/.test(chatId),
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* List of group participants.
|
|
605
|
-
* Returns [] for non-group chats.
|
|
606
|
-
* @returns {Promise<Array<{ id: string, isAdmin: boolean, isSuperAdmin: boolean }>>}
|
|
607
|
-
*/
|
|
608
|
-
async getParticipants() {
|
|
609
|
-
if (!chat.participants) return [];
|
|
610
|
-
return chat.participants.map((p) => ({
|
|
611
|
-
id: p.id._serialized,
|
|
612
|
-
isAdmin: p.isAdmin,
|
|
613
|
-
isSuperAdmin: p.isSuperAdmin,
|
|
614
|
-
}));
|
|
615
|
-
},
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Check if a contact is an admin of this group.
|
|
619
|
-
* Always returns false for non-group chats.
|
|
620
|
-
* @param {string} contactId
|
|
621
|
-
* @returns {Promise<boolean>}
|
|
622
|
-
*/
|
|
623
|
-
async isAdmin(contactId) {
|
|
624
|
-
return chat.participants?.some(
|
|
625
|
-
(p) => p.id._serialized === contactId && (p.isAdmin || p.isSuperAdmin)
|
|
626
|
-
) ?? false;
|
|
627
|
-
},
|
|
628
|
-
},
|
|
629
|
-
};
|
|
630
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pluginGuard.js
|
|
3
|
-
*
|
|
4
|
-
* Runs a plugin safely.
|
|
5
|
-
*
|
|
6
|
-
* Protections:
|
|
7
|
-
* - Hard timeout per plugin run (prevents infinite hangs from locking the queue)
|
|
8
|
-
* - Catches and logs all errors with structured context
|
|
9
|
-
* - Marks errored plugins so they are silently skipped from then on
|
|
10
|
-
* - Never crashes the bot
|
|
11
|
-
*
|
|
12
|
-
* Per-plugin overrides:
|
|
13
|
-
* Plugins may export a `guardOptions` object to opt out of specific
|
|
14
|
-
* protections. The pluginLoader is responsible for reading this export
|
|
15
|
-
* and storing it as `plugin.guardOptions` in the registry entry.
|
|
16
|
-
*
|
|
17
|
-
* Supported keys:
|
|
18
|
-
* timeout {boolean} — set to `false` to disable the hard timeout.
|
|
19
|
-
* Use only for plugins that intentionally block
|
|
20
|
-
* (e.g. heavy media processing, sticker generation).
|
|
21
|
-
*/
|
|
22
|
-
import { logger } from "#logger";
|
|
23
|
-
import { pluginRegistry } from "#kernel/pluginLoader";
|
|
24
|
-
|
|
25
|
-
/** Max ms a single plugin run is allowed to take before it's force-aborted. */
|
|
26
|
-
const PLUGIN_TIMEOUT_MS = 120_000;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Races `promise` against a timeout rejection.
|
|
30
|
-
* @param {Promise} promise
|
|
31
|
-
* @param {number} ms
|
|
32
|
-
* @param {string} pluginName
|
|
33
|
-
*/
|
|
34
|
-
function withTimeout(promise, ms, pluginName) {
|
|
35
|
-
return Promise.race([
|
|
36
|
-
promise,
|
|
37
|
-
new Promise((_, reject) =>
|
|
38
|
-
setTimeout(
|
|
39
|
-
() => reject(new Error(`timed out after ${ms}ms`)),
|
|
40
|
-
ms
|
|
41
|
-
)
|
|
42
|
-
),
|
|
43
|
-
]);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @param {object} plugin — pluginRegistry entry
|
|
48
|
-
* @param {object} context — buildApi ctx
|
|
49
|
-
*
|
|
50
|
-
* plugin.guardOptions (optional, read from plugin's own export):
|
|
51
|
-
* @param {boolean} [plugin.guardOptions.timeout=true]
|
|
52
|
-
*/
|
|
53
|
-
export async function runPlugin(plugin, context) {
|
|
54
|
-
if (plugin.status !== "active") return;
|
|
55
|
-
|
|
56
|
-
const useTimeout = plugin.guardOptions?.timeout !== false;
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const run = plugin.run(context);
|
|
60
|
-
await (useTimeout ? withTimeout(run, PLUGIN_TIMEOUT_MS, plugin.name) : run);
|
|
61
|
-
} catch (err) {
|
|
62
|
-
plugin.status = "error";
|
|
63
|
-
plugin.error = err;
|
|
64
|
-
pluginRegistry.set(plugin.name, plugin);
|
|
65
|
-
|
|
66
|
-
const isTimeout = useTimeout && err.message?.startsWith("timed out");
|
|
67
|
-
const headline = isTimeout
|
|
68
|
-
? `[pluginGuard] Plugin "${plugin.name}" forcibly stopped: ${err.message}`
|
|
69
|
-
: `[pluginGuard] Plugin "${plugin.name}" threw an unhandled error and was disabled`;
|
|
70
|
-
|
|
71
|
-
logger.error(headline);
|
|
72
|
-
logger.error(` message : ${err.message}`);
|
|
73
|
-
if (!isTimeout) {
|
|
74
|
-
const frame = err.stack?.split("\n")[1]?.trim() ?? "(no stack)";
|
|
75
|
-
logger.error(` at : ${frame}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|