@manybot/manybot 4.0.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 ADDED
@@ -0,0 +1,18 @@
1
+ <div align="center">
2
+
3
+ ![ManyBot Logo](logo.png)
4
+
5
+ **Just a cool open source WhatsApp bot**
6
+
7
+ [🇧🇷 Português](README.md) · [🇺🇸 English](README_EN.md)
8
+
9
+ ![Node.js 18+](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white)
10
+ ![npm 9+](https://img.shields.io/badge/npm-9+-CB3837?logo=npm&logoColor=white)
11
+ ![GPL v3](https://img.shields.io/badge/License-GPL--v3-blue.svg)
12
+ ![Linux](https://img.shields.io/badge/Linux%20%7C%20Windows-lightgrey)
13
+
14
+ ---
15
+
16
+ This repository is dedicated to contributions related to the project.
17
+
18
+ website: https://manybot.stxerr.dev
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@manybot/manybot",
3
+ "description": "Free and open source WhatsApp bot",
4
+ "author": {
5
+ "name": "SyntaxError!",
6
+ "email": "me@stxerr.dev"
7
+ },
8
+ "version": "4.0.1",
9
+ "license": "GPL-3.0-only",
10
+ "private": false,
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/many-bot/manybot.git"
17
+ },
18
+ "type": "module",
19
+ "bin": {
20
+ "manybot": "./src/main.js"
21
+ },
22
+ "files": [
23
+ "src/client",
24
+ "src/config.js",
25
+ "src/download",
26
+ "src/i18n",
27
+ "src/kernel",
28
+ "src/locales",
29
+ "src/logger",
30
+ "src/main.js",
31
+ "src/utils",
32
+ "README.md",
33
+ "README_EN.md",
34
+ "latest",
35
+ "LICENSE"
36
+ ],
37
+ "dependencies": {
38
+ "node-webpmux": "^3.2.1",
39
+ "qrcode-terminal": "^0.12.0",
40
+ "whatsapp-web.js": "^1.24.0"
41
+ },
42
+ "imports": {
43
+ "#client/*": "./src/client/*.js",
44
+ "#kernel/*": "./src/kernel/*.js",
45
+ "#manyapi": "./src/kernel/pluginApi.js",
46
+ "#logger": "./src/logger/logger.js",
47
+ "#utils/*": "./src/utils/*.js",
48
+ "#i18n": "./src/i18n/index.js",
49
+ "#download": "./src/download/queue.js",
50
+ "#config": "./src/config.js",
51
+ "#main": "./src/main.js"
52
+ }
53
+ }
@@ -0,0 +1,57 @@
1
+ const C = {
2
+ reset: "\x1b[0m",
3
+ bold: "\x1b[1m",
4
+
5
+ blue: "\x1b[94m",
6
+ magenta: "\x1b[95m",
7
+ cyan: "\x1b[96m",
8
+ gray: "\x1b[90m",
9
+ yellow: "\x1b[93m",
10
+ };
11
+
12
+ import { readFileSync } from "fs";
13
+ import { fileURLToPath } from "url";
14
+ import path from "path";
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ const v = JSON.parse(
20
+ readFileSync(
21
+ path.join(__dirname, "../../package.json"),
22
+ "utf8"
23
+ )
24
+ ).version;
25
+
26
+ export function printBanner() {
27
+ const banner = [
28
+ ` _ _ `,
29
+ ` | | | | `,
30
+ ` _ __ ___ __ _ _ __ _ _| |__ ___ | |_`,
31
+ `| '_ \` _ \\ / _\` | '_ \\| | | | '_ \\ / _ \\| __`,
32
+ `| | | | | | (_| | | | | |_| | |_) | (_) | |_`,
33
+ `|_| |_| |_|\\__,_|_| |_|\\__, |_.__/ \\___/ \\__`,
34
+ ` __/ | `,
35
+ ` ${v} |___/ `
36
+ ];
37
+
38
+ console.log(`${C.bold}${C.blue}`);
39
+ console.log(banner.join("\n"));
40
+ console.log(C.reset);
41
+
42
+ console.log(
43
+ ` made with ${C.magenta}<3${C.reset} by ${C.bold}${C.cyan}SyntaxError!${C.reset} ${C.gray}<me@stxerr.dev>${C.reset}`
44
+ );
45
+
46
+ console.log();
47
+
48
+ console.log(
49
+ ` ${C.gray}website${C.reset} : ${C.yellow}https://manybot.stxerr.dev${C.reset}`
50
+ );
51
+
52
+ console.log(
53
+ ` ${C.gray}github ${C.reset} : ${C.yellow}https://github.com/many-bot${C.reset}`
54
+ );
55
+
56
+ console.log();
57
+ }
@@ -0,0 +1,103 @@
1
+ /* whatsappClient
2
+ *
3
+ * Initialize client and connect to WhatsApp
4
+ *
5
+ * if PHONE_NUMBER is set on config, it will request a verficiation code
6
+ * but if it is not, it will display a QR Code on the screen to scan using your phone
7
+ *
8
+ * */
9
+
10
+ import pkg from "whatsapp-web.js";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { PHONE_NUMBER, CLIENT_ID } from "#config";
15
+ import { logger } from "#logger";
16
+ import qrcode from "qrcode-terminal";
17
+ import { t } from "#i18n"
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+
22
+ export const { Client, LocalAuth, MessageMedia } = pkg;
23
+
24
+ // -- Instance --------------------------------------------------
25
+ const clientOptions = {
26
+ authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
27
+ puppeteer: {
28
+ headless: true,
29
+ args: [
30
+ '--no-sandbox',
31
+ '--disable-setuid-sandbox'
32
+ ],
33
+ },
34
+ };
35
+
36
+ // -- Qr Handle --------------------------------------------------
37
+ export function handleQR(qr) {
38
+ logger.info(t("system.qrScan"));
39
+ qrcode.generate(qr, { small: true });
40
+ }
41
+
42
+ // -- Handle pairing code ---------------------------------------
43
+ export function handlePairingCode(code) {
44
+ logger.info(t("system.pairingCodeTitle"));
45
+ logger.info(t("system.pairingCodeValue", { code: code }));
46
+ logger.info(t("system.pairingCodeInstructions"));
47
+ }
48
+
49
+ // -- Phone Number Validation ------------------------------------
50
+ const AUTH_STATE_PATH = path.join(__dirname, `../../.auth_${CLIENT_ID}.json`);
51
+
52
+ // Validates if phone string has 10-15 characters
53
+ function isValidPhoneNumber(phone) {
54
+ if (!phone || typeof phone !== 'string') return false;
55
+ const phoneRegex = /^\d{10,15}$/;
56
+ return phoneRegex.test(phone);
57
+ }
58
+
59
+ // Checks if phone number changed since last authentication
60
+ function hasPhoneNumberChanged(currentPhone) {
61
+ try {
62
+ if (!fs.existsSync(AUTH_STATE_PATH)) return false;
63
+ const state = JSON.parse(fs.readFileSync(AUTH_STATE_PATH, 'utf8'));
64
+ const storedPhone = state.phoneNumber || null;
65
+ return storedPhone !== currentPhone;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ // Saves phone number to auth state file
72
+ function savePhoneNumber(phone) {
73
+ try {
74
+ fs.writeFileSync(AUTH_STATE_PATH, JSON.stringify({ phoneNumber: phone, savedAt: new Date().toISOString() }));
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ // Check if phone number changed and force re-authentication if needed
81
+ if (PHONE_NUMBER && hasPhoneNumberChanged(PHONE_NUMBER)) {
82
+ // Delete auth folder to force fresh authentication
83
+ const authPath = path.join(__dirname, `../../.wwebjs_auth/session-${CLIENT_ID}`);
84
+ if (fs.existsSync(authPath)) {
85
+ fs.rmSync(authPath, { recursive: true, force: true });
86
+ }
87
+ }
88
+
89
+ // Add phone number pairing if PHONE_NUMBER is configured and valid
90
+ if (PHONE_NUMBER) {
91
+ if (isValidPhoneNumber(PHONE_NUMBER)) {
92
+ clientOptions.pairWithPhoneNumber = {
93
+ phoneNumber: PHONE_NUMBER,
94
+ showNotification: true,
95
+ };
96
+ }
97
+ savePhoneNumber(PHONE_NUMBER);
98
+ }
99
+
100
+ export const client = new Client(clientOptions);
101
+
102
+
103
+ export default client;
package/src/config.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * config.js
3
+ *
4
+ * Loads:
5
+ * ~/.manybot/manybot.conf
6
+ * ~/.manybot/manyplug.conf
7
+ *
8
+ * Merges both files into a single configuration object.
9
+ */
10
+
11
+ import fs from "fs/promises";
12
+ import os from "os";
13
+ import path from "path";
14
+
15
+ import { logger } from "#logger";
16
+
17
+ const CONFIG_DIR = path.join(os.homedir(), ".manybot");
18
+ const CONFIG_FILE = path.join(CONFIG_DIR, "manybot.conf");
19
+ const PLUGIN_FILE = path.join(CONFIG_DIR, "manyplug.conf");
20
+
21
+ /**
22
+ * Converts strings to native JS values.
23
+ */
24
+ function parseValue(value) {
25
+ value = value.trim();
26
+
27
+ if (
28
+ (value.startsWith('"') && value.endsWith('"')) ||
29
+ (value.startsWith("'") && value.endsWith("'"))
30
+ ) {
31
+ value = value.slice(1, -1);
32
+ }
33
+
34
+ if (value === "true")
35
+ return true;
36
+
37
+ if (value === "false")
38
+ return false;
39
+
40
+ return value;
41
+ }
42
+
43
+ /**
44
+ * Reads comments safely.
45
+ * Ignores # inside quoted strings.
46
+ */
47
+ function stripInlineComment(line) {
48
+ let result = "";
49
+ let quote = null;
50
+
51
+ for (let i = 0; i < line.length; i++) {
52
+ const ch = line[i];
53
+
54
+ if ((ch === '"' || ch === "'") && line[i - 1] !== "\\") {
55
+ if (quote === ch)
56
+ quote = null;
57
+ else if (!quote)
58
+ quote = ch;
59
+ }
60
+
61
+ if (ch === "#" && !quote)
62
+ break;
63
+
64
+ result += ch;
65
+ }
66
+
67
+ return result.trim();
68
+ }
69
+
70
+ /**
71
+ * Parses manybot.conf syntax.
72
+ */
73
+ function parseConf(raw) {
74
+ const lines = raw.split(/\r?\n/);
75
+
76
+ const mergedLines = [];
77
+
78
+ let insideList = false;
79
+ let buffer = "";
80
+
81
+ for (let line of lines) {
82
+ line = stripInlineComment(line);
83
+
84
+ if (!line)
85
+ continue;
86
+
87
+ if (!insideList) {
88
+ if (/=\s*\[$/.test(line)) {
89
+ insideList = true;
90
+ buffer = line;
91
+ } else {
92
+ mergedLines.push(line);
93
+ }
94
+ } else {
95
+ buffer += " " + line;
96
+
97
+ if (line.includes("]")) {
98
+ mergedLines.push(buffer);
99
+ buffer = "";
100
+ insideList = false;
101
+ }
102
+ }
103
+ }
104
+
105
+ const config = {};
106
+
107
+ for (const line of mergedLines) {
108
+ const idx = line.indexOf("=");
109
+
110
+ if (idx === -1)
111
+ continue;
112
+
113
+ const key = line.slice(0, idx).trim();
114
+ let value = line.slice(idx + 1).trim();
115
+
116
+ if (value.startsWith("[") && value.endsWith("]")) {
117
+ config[key] = value
118
+ .slice(1, -1)
119
+ .split(",")
120
+ .map(v => parseValue(v))
121
+ .filter(v => v !== "");
122
+ continue;
123
+ }
124
+
125
+ config[key] = parseValue(value);
126
+ }
127
+
128
+ return config;
129
+ }
130
+
131
+ async function readFileSafe(file) {
132
+ try {
133
+ return await fs.readFile(file, "utf-8");
134
+ } catch (err) {
135
+ if (err.code !== "ENOENT") {
136
+ logger.warn(`Error reading ${file}: ${err.message}`);
137
+ }
138
+ return "";
139
+ }
140
+ }
141
+
142
+ const defaultConfig =
143
+ `
144
+ # Many bot configuration file
145
+ # See https://manybot.stxerr.dev/docs/config to learn more
146
+
147
+ CLIENT_ID="manybot"
148
+ CMD_PREFIX="!"
149
+ CHATS=[]
150
+ LANGUAGE=en
151
+ PHONE_NUMBER=
152
+ `;
153
+
154
+ try {
155
+ await fs.stat(CONFIG_FILE);
156
+ } catch {
157
+ logger.warn("Configuration file not found: ", CONFIG_FILE, ". Creating a new one.");
158
+
159
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
160
+ await fs.writeFile(CONFIG_FILE, defaultConfig);
161
+ }
162
+
163
+ const baseConfig = await readFileSafe(CONFIG_FILE);
164
+ const pluginConfig = await readFileSafe(PLUGIN_FILE);
165
+
166
+ export const CONFIG = parseConf(baseConfig + "\n" + pluginConfig);
167
+
168
+ /**
169
+ * Common exports.
170
+ */
171
+ export const CLIENT_ID =
172
+ CONFIG.CLIENT_ID ?? "manybot";
173
+
174
+ export const CMD_PREFIX =
175
+ CONFIG.CMD_PREFIX ?? "!";
176
+
177
+ export const CHATS =
178
+ CONFIG.CHATS ?? [];
179
+
180
+ export const PLUGINS =
181
+ CONFIG.PLUGINS ?? [];
182
+
183
+ export const LANGUAGE =
184
+ CONFIG.LANGUAGE ?? "en";
185
+
186
+ export const PHONE_NUMBER =
187
+ CONFIG.PHONE_NUMBER ?? null;
188
+
189
+ /**
190
+ * Useful paths for plugins/modules.
191
+ */
192
+ export const PATHS = {
193
+ HOME: CONFIG_DIR,
194
+ CONFIG_FILE,
195
+ PLUGIN_FILE
196
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * src/download/queue.js
3
+ *
4
+ * Sequential execution queue for heavy jobs (downloads, conversions).
5
+ * Ensures only one job runs at a time — without overloading yt-dlp or ffmpeg.
6
+ *
7
+ * Plugin passes a `workFn` that does everything: download, convert, send.
8
+ * Queue only handles sequence and error handling.
9
+ *
10
+ * Usage:
11
+ * import { enqueue } from "../../src/download/queue.js";
12
+ * enqueue(async () => { ... all plugin logic ... }, onError);
13
+ */
14
+
15
+ import { logger } from "#logger";
16
+ import { t } from "#i18n";
17
+
18
+ /**
19
+ * @typedef {{
20
+ * workFn: () => Promise<void>,
21
+ * errorFn: (err: Error) => Promise<void>,
22
+ * }} Job
23
+ */
24
+
25
+ /** @type {Job[]} */
26
+ let queue = [];
27
+ let processing = false;
28
+
29
+ /**
30
+ * Add job to queue and start processing if idle.
31
+ *
32
+ * @param {Function} workFn — async () => void — all plugin logic
33
+ * @param {Function} errorFn — async (err) => void — called if workFn throws
34
+ */
35
+ export function enqueue(workFn, errorFn) {
36
+ queue.push({ workFn, errorFn });
37
+ if (!processing) processQueue();
38
+ }
39
+
40
+ async function processQueue() {
41
+ processing = true;
42
+ while (queue.length) {
43
+ await processJob(queue.shift());
44
+ }
45
+ processing = false;
46
+ }
47
+
48
+ async function processJob({ workFn, errorFn }) {
49
+ try {
50
+ await workFn();
51
+ } catch (err) {
52
+ logger.error(t("system.downloadJobFailed", { message: err.message }));
53
+ try { await errorFn(err); } catch { }
54
+ }
55
+ }