@manybot/manybot 5.0.0 → 5.2.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.
@@ -1,524 +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
-
21
- const { MessageMedia } = pkg;
22
-
23
- // ── Storage API ──────────────────────────────────────────────────────────────
24
-
25
- export function buildStorageApi(pluginName) {
26
- const dir = path.join(CONFIG_DIR, "data", pluginName);
27
- mkdirSync(dir, { recursive: true });
28
-
29
- return {
30
- dir,
31
-
32
- /**
33
- * Resolves a path inside data directory.
34
- * Make subdirectories automatically.
35
- * @param {string} relativePath
36
- * @returns {string}
37
- */
38
- path(relativePath) {
39
- const resolved = path.join(dir, relativePath);
40
- mkdirSync(path.dirname(resolved), { recursive: true });
41
- return resolved;
42
- },
43
- };
44
- }
45
-
46
- // ── Config API ───────────────────────────────────────────────────────────────
47
-
48
- function buildConfigApi() {
49
- return {
50
- /**
51
- * Get a config value with optional default.
52
- * @param {string} key
53
- * @param {any} [defaultValue]
54
- */
55
- get(key, defaultValue = null) {
56
- return CONFIG[key] ?? defaultValue;
57
- },
58
-
59
- /** Full config object — read only. */
60
- all: CONFIG,
61
- };
62
- }
63
-
64
- // ── i18n API ─────────────────────────────────────────────────────────────────
65
-
66
- function buildI18nApi() {
67
- return {
68
- /** Translate a core key. */
69
- t,
70
-
71
- /**
72
- * Create a scoped t() for a plugin's own locale files.
73
- * @param {string} pluginMetaUrl — pass import.meta.url from the plugin
74
- */
75
- createT: createPluginT,
76
-
77
- /** Reload all translations (e.g. after language change). */
78
- reload: reloadTranslations,
79
-
80
- /** Returns current language code. */
81
- getCurrentLang,
82
- };
83
- }
84
-
85
- // ── Utils API ────────────────────────────────────────────────────────────────
86
-
87
- function buildUtilsApi() {
88
- return {
89
- /**
90
- * Empty a folder's contents without removing the folder itself.
91
- * @param {string} folder
92
- */
93
- emptyFolder,
94
-
95
- /**
96
- * Get the serialized chat ID from a chat object.
97
- * @param {import("whatsapp-web.js").Chat} chat
98
- */
99
- getChatId,
100
- };
101
- }
102
-
103
- // ── Download API ─────────────────────────────────────────────────────────────
104
-
105
- function buildDownloadApi() {
106
- return {
107
- /**
108
- * Enqueue a download work function.
109
- * @param {Function} workFn
110
- * @param {Function} [errorFn]
111
- */
112
- enqueue,
113
- };
114
- }
115
-
116
- // ── Plugin registry API ──────────────────────────────────────────────────────
117
-
118
- function buildPluginsApi(pluginRegistry) {
119
- return {
120
- /**
121
- * Return public API of another plugin, or null if not active.
122
- * @param {string} name
123
- * @returns {any|null}
124
- */
125
- get(name) {
126
- return pluginRegistry.get(name)?.exports ?? null;
127
- },
128
-
129
- /**
130
- * Return public API of another plugin, or throw if not active.
131
- * Analogous to require() — use when the dependency is mandatory.
132
- * @param {string} name
133
- * @returns {any}
134
- */
135
- require(name) {
136
- const plugin = pluginRegistry.get(name);
137
- if (!plugin || plugin.status !== "active") {
138
- throw new Error(`Plugin dependency "${name}" does not exist or is not active.`);
139
- }
140
- return plugin.exports;
141
- },
142
-
143
- /**
144
- * Check if a plugin is active.
145
- * @param {string} name
146
- * @returns {boolean}
147
- */
148
- exists(name) {
149
- return pluginRegistry.get(name)?.status === "active";
150
- },
151
- };
152
- }
153
-
154
- // ── Log API ──────────────────────────────────────────────────────────────────
155
-
156
- const log = {
157
- info: (...a) => logger.info(...a),
158
- warn: (...a) => logger.warn(...a),
159
- error: (...a) => logger.error(...a),
160
- success: (...a) => logger.success(...a),
161
- };
162
-
163
- // ── Contact API ──────────────────────────────────────────────────────────────
164
-
165
- /**
166
- * Normalizes a raw whatsapp-web.js Contact into a plain object.
167
- * Used internally so both ctx.contacts and ctx.msg.getContact()
168
- * always return the same shape.
169
- * @param {import("whatsapp-web.js").Contact} c
170
- * @returns {object}
171
- */
172
- function normalizeContact(c) {
173
- return {
174
- id: c.id._serialized,
175
- number: c.number,
176
- pushname: c.pushname ?? null,
177
- name: c.name ?? null,
178
- shortName: c.shortName ?? null,
179
- isBusiness: c.isBusiness,
180
- isEnterprise: c.isEnterprise,
181
- isBlocked: c.isBlocked,
182
- isMe: c.isMe,
183
- isMyContact: c.isMyContact,
184
- isWAContact: c.isWAContact,
185
- isUser: c.isUser,
186
- isGroup: c.isGroup,
187
- };
188
- }
189
-
190
- function buildContactsApi(client) {
191
- return {
192
- /**
193
- * Get a normalized Contact object by ID.
194
- * @param {string} contactId — serialized ID (e.g. "5511999999999@c.us")
195
- * @returns {Promise<object|null>}
196
- */
197
- async get(contactId) {
198
- try {
199
- const c = await client.getContactById(contactId);
200
- return normalizeContact(c);
201
- } catch {
202
- return null;
203
- }
204
- },
205
-
206
- /**
207
- * Get the profile picture URL of a contact.
208
- * Respects privacy settings — may return null.
209
- * @param {string} contactId
210
- * @returns {Promise<string|null>}
211
- */
212
- async getProfilePicUrl(contactId) {
213
- try {
214
- const c = await client.getContactById(contactId);
215
- return await c.getProfilePicUrl();
216
- } catch {
217
- return null;
218
- }
219
- },
220
-
221
- /**
222
- * Get the "about" text of a contact.
223
- * Returns null if privacy settings block access.
224
- * @param {string} contactId
225
- * @returns {Promise<string|null>}
226
- */
227
- async getAbout(contactId) {
228
- try {
229
- const c = await client.getContactById(contactId);
230
- return await c.getAbout();
231
- } catch {
232
- return null;
233
- }
234
- },
235
- };
236
- }
237
-
238
- // ── Internal media helpers ───────────────────────────────────────────────────
239
-
240
- /**
241
- * @param {string|Buffer} source
242
- * @param {string} mimetype — required, no ambiguous default
243
- */
244
- function mediaFromSource(source, mimetype) {
245
- return typeof source === "string"
246
- ? MessageMedia.fromFilePath(source)
247
- : new MessageMedia(mimetype, source.toString("base64"));
248
- }
249
-
250
- /**
251
- * Returns send methods bound to a target that exposes `.sendMessage()`.
252
- * @param {{ sendMessage: Function }} target
253
- */
254
- function makeSender(target) {
255
- return {
256
- async text(content, opts = {}) {
257
- return target.sendMessage(content, opts);
258
- },
259
- async image(filePath, caption = "") {
260
- const media = MessageMedia.fromFilePath(filePath);
261
- return target.sendMessage(media, { caption });
262
- },
263
- async video(filePath, caption = "") {
264
- const media = MessageMedia.fromFilePath(filePath);
265
- return target.sendMessage(media, { caption });
266
- },
267
- async audio(filePath, { asVoice = true } = {}) {
268
- const media = MessageMedia.fromFilePath(filePath);
269
- return target.sendMessage(media, { sendAudioAsVoice: asVoice });
270
- },
271
- async sticker(source) {
272
- const media = mediaFromSource(source, "image/webp");
273
- return target.sendMessage(media, { sendMediaAsSticker: true });
274
- },
275
- };
276
- }
277
-
278
- /** Adapts client.sendMessage(chatId, ...) to the makeSender interface. */
279
- function chatIdTarget(client, chatId) {
280
- return {
281
- sendMessage: (content, opts) => client.sendMessage(chatId, content, opts),
282
- };
283
- }
284
-
285
- // ── Send API ─────────────────────────────────────────────────────────────────
286
-
287
- /**
288
- * Runtime send API — current chat + .to() for other chats.
289
- *
290
- * ctx.send.text("oi")
291
- * ctx.send.image("./foto.jpg", "legenda")
292
- * ctx.send.to("5511@c.us").text("oi")
293
- */
294
- function buildSendApi(chat, client) {
295
- const current = makeSender(chat);
296
-
297
- return {
298
- send: {
299
- text: (text, opts) => current.text(text, opts),
300
- image: (filePath, caption) => current.image(filePath, caption),
301
- video: (filePath, caption) => current.video(filePath, caption),
302
- audio: (filePath, opts) => current.audio(filePath, opts),
303
- sticker: (source) => current.sticker(source),
304
-
305
- /**
306
- * Returns a sender bound to another chat.
307
- * @param {string} chatId
308
- * @returns {{ text, image, video, audio, sticker }}
309
- */
310
- to: (chatId) => makeSender(chatIdTarget(client, chatId)),
311
- },
312
- };
313
- }
314
-
315
- /**
316
- * Setup send API — no current chat, only .to().
317
- *
318
- * ctx.send.to(adminChatId).text("bot iniciado")
319
- */
320
- function buildSetupSendApi(client) {
321
- return {
322
- send: {
323
- to: (chatId) => makeSender(chatIdTarget(client, chatId)),
324
- },
325
- };
326
- }
327
-
328
- // ── Events API (setup only) ───────────────────────────────────────────────────
329
-
330
- function buildEventsApi(client) {
331
- return {
332
- /**
333
- * Registers a persistent listener for a client event.
334
- * Returns an 'off()' function to cancel the listener when the plugin wants.
335
- *
336
- * @param {string} event
337
- * @param {Function} handler
338
- * @returns {Function} off
339
- *
340
- * @example
341
- * const off = ctx.events.on("group_join", (notification) => { ... });
342
- * // cancels when the plugin doesn't want anymore:
343
- * off();
344
- */
345
- on(event, handler) {
346
- client.on(event, handler);
347
- return () => client.off(event, handler);
348
- },
349
-
350
- /**
351
- * Returns a Promise that resolves on the next ocurrence of the event.
352
- * Useful for waiting a specific event without having to manage listeners manually.
353
- *
354
- * @param {string} event
355
- * @returns {Promise<any>}
356
- *
357
- * @example
358
- * const notification = await ctx.events.once("group_join");
359
- */
360
- once(event) {
361
- return new Promise((resolve) => client.once(event, resolve));
362
- }
363
- };
364
- }
365
-
366
- // ── Base API (shared between setup and runtime) ───────────────────────────────
367
-
368
- function buildBaseApi(client, pluginRegistry, pluginName) {
369
- const botId = client.info?.wid?._serialized ?? null;
370
- if (!botId) logger.warn("[pluginApi] botId is null - client may not be ready yet.");
371
-
372
- return {
373
- log,
374
- t,
375
- config: buildConfigApi(),
376
- i18n: buildI18nApi(),
377
- utils: buildUtilsApi(),
378
- download: buildDownloadApi(),
379
- plugins: buildPluginsApi(pluginRegistry),
380
- contacts: buildContactsApi(client),
381
- storage: buildStorageApi(pluginName),
382
- botId,
383
- };
384
- }
385
-
386
- // ── Setup API ────────────────────────────────────────────────────────────────
387
-
388
- /**
389
- * Setup API — without message context.
390
- * Passed to plugin.setup(ctx) during initialization.
391
- *
392
- * @param {import("whatsapp-web.js").Client} client
393
- * @param {Map<string, any>} pluginRegistry
394
- * @returns {object}
395
- */
396
- export function buildSetupApi(client, pluginRegistry, pluginName) {
397
- return {
398
- ...buildBaseApi(client, pluginRegistry, pluginName),
399
- ...buildSetupSendApi(client),
400
- events: buildEventsApi(client),
401
- };
402
- }
403
-
404
- // ── Runtime API ──────────────────────────────────────────────────────────────
405
-
406
- /**
407
- * Runtime API — full context with message and chat.
408
- * Passed to plugin.default(ctx) on every message.
409
- *
410
- * @param {object} params
411
- * @param {import("whatsapp-web.js").Message} params.msg
412
- * @param {import("whatsapp-web.js").Chat} params.chat
413
- * @param {import("whatsapp-web.js").Client} params.client
414
- * @param {Map<string, any>} params.pluginRegistry
415
- * @returns {object} ctx
416
- */
417
- export function buildApi({ msg, chat, client, pluginRegistry }) {
418
- const prefix = CONFIG.CMD_PREFIX ?? "!";
419
- const rawArgs = msg.body?.trim().split(/\s+/) ?? [];
420
- const command = rawArgs[0]?.toLowerCase().startsWith(prefix)
421
- ? rawArgs[0].slice(prefix.length).toLowerCase()
422
- : rawArgs[0]?.toLowerCase() ?? "";
423
-
424
- return {
425
- ...buildBaseApi(client, pluginRegistry),
426
- ...buildSendApi(chat, client),
427
-
428
- // ── msg ──────────────────────────────────────────────────
429
-
430
- msg: {
431
- body: msg.body ?? "",
432
- type: msg.type,
433
- fromMe: msg.fromMe,
434
- sender: msg.author || msg.from,
435
- senderName: msg._data?.notifyName || String(msg.from).replace(/(:\d+)?@.*$/, ""),
436
-
437
- /** Command token without prefix (e.g. "play" for "!play foo"). */
438
- command,
439
-
440
- /** Arguments after the command token. */
441
- args: rawArgs.slice(1),
442
-
443
- /**
444
- * Check if message is a given command.
445
- * @param {string} cmd — without prefix
446
- */
447
- is(cmd) {
448
- return command === cmd.toLowerCase();
449
- },
450
-
451
- hasMedia: msg.hasMedia,
452
- isGif: msg._data?.isGif ?? false,
453
-
454
- async downloadMedia() {
455
- const media = await msg.downloadMedia();
456
- if (!media) return null;
457
- return { mimetype: media.mimetype, data: media.data };
458
- },
459
-
460
- hasReply: msg.hasQuotedMsg,
461
-
462
- async getReply() {
463
- if (!msg.hasQuotedMsg) return null;
464
- return msg.getQuotedMessage();
465
- },
466
-
467
- async reply(text) {
468
- return msg.reply(text);
469
- },
470
-
471
- async react(emoji) {
472
- return msg.react(emoji);
473
- },
474
-
475
- /**
476
- * Get the sender as a normalized Contact object.
477
- * Same shape as ctx.contacts.get().
478
- * @returns {Promise<object|null>}
479
- */
480
- async getContact() {
481
- try {
482
- const c = await msg.getContact();
483
- return normalizeContact(c);
484
- } catch {
485
- return null;
486
- }
487
- },
488
- },
489
-
490
- // ── chat ─────────────────────────────────────────────────
491
-
492
- chat: {
493
- id: chat.id._serialized,
494
- name: chat.name || chat.id.user,
495
- isGroup: /@g\.us$/.test(chat.id._serialized),
496
-
497
- /**
498
- * List of group participants.
499
- * Returns [] for non-group chats.
500
- * @returns {Promise<Array<{ id: string, isAdmin: boolean, isSuperAdmin: boolean }>>}
501
- */
502
- async getParticipants() {
503
- if (!chat.participants) return [];
504
- return chat.participants.map((p) => ({
505
- id: p.id._serialized,
506
- isAdmin: p.isAdmin,
507
- isSuperAdmin: p.isSuperAdmin,
508
- }));
509
- },
510
-
511
- /**
512
- * Check if a contact is an admin of this group.
513
- * Always returns false for non-group chats.
514
- * @param {string} contactId
515
- * @returns {Promise<boolean>}
516
- */
517
- async isAdmin(contactId) {
518
- return chat.participants?.some(
519
- (p) => p.id._serialized === contactId && (p.isAdmin || p.isSuperAdmin)
520
- ) ?? false;
521
- },
522
- },
523
- };
524
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * pluginGuard.js
3
- *
4
- * Runs a plugin safely.
5
- * If plugin throws an error:
6
- * - Logs error with context
7
- * - Marks plugin as "error" in registry
8
- * - Never crashes the bot
9
- *
10
- * Disabled or errored plugins are silently ignored.
11
- */
12
-
13
- import { logger } from "#logger";
14
- import { t } from "#i18n";
15
- import { pluginRegistry } from "#kernel/pluginLoader";
16
-
17
- /**
18
- * @param {object} plugin — pluginRegistry entry
19
- * @param {object} context — buildApi ctx
20
- */
21
- export async function runPlugin(plugin, context) {
22
- if (plugin.status !== "active") return;
23
-
24
- try {
25
- await plugin.run(context);
26
- } catch (err) {
27
- // Disable plugin to prevent further breakage
28
- plugin.status = "error";
29
- plugin.error = err;
30
- pluginRegistry.set(plugin.name, plugin);
31
-
32
- logger.error(
33
- t("system.pluginDisabledAfterError", { name: plugin.name, message: err.message }),
34
- `\n ${t("errors.stack")}: ${err.stack?.split("\n")[1]?.trim() ?? ""}`
35
- );
36
- }
37
- }
@@ -1,134 +0,0 @@
1
- /**
2
- * pluginLoader.js
3
- *
4
- * Responsible for:
5
- * 1. Reading active plugins (config.js imports this module and give the list)
6
- * 2. Loading each plugin from ~/.manybot/plugins folder
7
- * 3. Registering in pluginRegistry with status and public exports
8
- * 4. Exposing pluginRegistry to kernel and pluginApi
9
- *
10
- */
11
-
12
- import fs from "fs";
13
- import path from "path";
14
- import { logger } from "#logger";
15
- import { t } from "#i18n";
16
- import { pathToFileURL } from "url";
17
- import { PATHS } from "#config";
18
- import { buildStorageApi } from "#manyapi";
19
-
20
- const PLUGINS_DIR = path.join(PATHS.HOME, "plugins");
21
-
22
- /**
23
- * Each entry in registry:
24
- * {
25
- * name: string,
26
- * status: "active" | "disabled" | "error",
27
- * run: async function({ msg, chat, api }) — plugin default function
28
- * exports: any — what plugin exposed via `export const api = { ... }`
29
- * error: Error | null
30
- * }
31
- *
32
- * @type {Map<string, object>}
33
- */
34
- export const pluginRegistry = new Map();
35
-
36
- /**
37
- * Load all active plugins listed in `activePlugins`.
38
- * Called once during bot initialization.
39
- *
40
- * @param {string[]} activePlugins — active plugin names (from .conf)
41
- */
42
- export async function loadPlugins(activePlugins) {
43
- if (!fs.existsSync(PLUGINS_DIR)) {
44
- fs.mkdirSync(PLUGINS_DIR, { recursive: true });
45
- }
46
-
47
- for (const name of activePlugins) {
48
- await loadPlugin(name);
49
- }
50
-
51
- const total = pluginRegistry.size;
52
- const ativos = [...pluginRegistry.values()].filter(p => p.status === "active").length;
53
- const erros = total - ativos;
54
-
55
- logger.success(t("system.pluginsLoaded", {
56
- count: ativos,
57
- errors: erros ? t("system.pluginsLoadedWithErrors", { count: erros }) : ""
58
- }));
59
- }
60
-
61
- /**
62
- * Call setup(api) on all plugins that export it.
63
- * Executed once after bot connects to WhatsApp.
64
- *
65
- * @param {object} api — api without message context (only sendTo, log, schedule...)
66
- */
67
- export async function setupPlugins(baseApi) {
68
- for (const plugin of pluginRegistry.values()) {
69
- if (plugin.status !== "active" || !plugin.setup) continue;
70
- try {
71
- const api = { ...baseApi, storage: buildStorageApi(plugin.name) };
72
- await plugin.setup(api);
73
- } catch (err) {
74
- logger.error(t("system.pluginSetupFailed", { name: plugin.name, message: err.message }));
75
- }
76
- }
77
- }
78
-
79
- async function findPluginPath(name) {
80
- // key direto: synt-xerror/figurinha
81
- const direct = path.join(PLUGINS_DIR, name, "index.js");
82
- if (fs.existsSync(direct)) return direct;
83
-
84
- // nome simples em subdir: plugins/synt-xerror/figurinha
85
- if (!fs.existsSync(PLUGINS_DIR)) return null;
86
- for (const entry of fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })) {
87
- if (!entry.isDirectory()) continue;
88
- const nested = path.join(PLUGINS_DIR, entry.name, name, "index.js");
89
- if (fs.existsSync(nested)) return nested;
90
- }
91
-
92
- return null;
93
- }
94
- /**
95
- * Carrega um único plugin pelo nome.
96
- * @param {string} name
97
- */
98
- async function loadPlugin(name) {
99
- const pluginPath = await findPluginPath(name);
100
- if (!pluginPath) {
101
- logger.warn(t("system.pluginNotFound", { name, path: path.join(PLUGINS_DIR, name) }));
102
- pluginRegistry.set(name, { name, status: "disabled", run: null, exports: null, error: null });
103
- return;
104
- }
105
-
106
- if (!fs.existsSync(pluginPath)) {
107
- logger.warn(t("system.pluginNotFound", { name, path: pluginPath }));
108
- pluginRegistry.set(name, { name, status: "disabled", run: null, exports: null, error: null });
109
- return;
110
- }
111
-
112
- try {
113
- const mod = await import(pathToFileURL(pluginPath).href);
114
-
115
- // Plugin must export a default function — this is called on every message
116
- if (typeof mod.default !== "function") {
117
- throw new Error(`Plugin "${name}" does not export a default function`);
118
- }
119
-
120
- pluginRegistry.set(name, {
121
- name,
122
- status: "active",
123
- run: mod.default,
124
- setup: mod.setup ?? null, // opcional — chamado uma vez na inicialização
125
- exports: mod.api ?? null,
126
- error: null,
127
- });
128
-
129
- logger.info(t("system.pluginLoaded", { name }));
130
- } catch (err) {
131
- logger.error(t("system.pluginLoadFailed", { name, message: err.message }));
132
- pluginRegistry.set(name, { name, status: "error", run: null, exports: null, error: err });
133
- }
134
- }