@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/LICENSE +674 -0
- package/README.md +18 -0
- package/package.json +53 -0
- package/src/client/banner.js +57 -0
- package/src/client/whatsappClient.js +103 -0
- package/src/config.js +196 -0
- package/src/download/queue.js +55 -0
- package/src/i18n/index.js +235 -0
- package/src/kernel/messageHandler.js +39 -0
- package/src/kernel/pluginApi.js +303 -0
- package/src/kernel/pluginGuard.js +37 -0
- package/src/kernel/pluginLoader.js +112 -0
- package/src/kernel/pluginState.js +99 -0
- package/src/kernel/scheduler.js +48 -0
- package/src/locales/en.json +64 -0
- package/src/locales/es.json +59 -0
- package/src/locales/pt.json +64 -0
- package/src/logger/logger.js +31 -0
- package/src/main.js +105 -0
- package/src/utils/file.js +9 -0
- package/src/utils/getChatId.js +3 -0
- package/src/utils/get_id.js +177 -0
- package/src/utils/pluginI18n.js +129 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**Just a cool open source WhatsApp bot**
|
|
6
|
+
|
|
7
|
+
[🇧🇷 Português](README.md) · [🇺🇸 English](README_EN.md)
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
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
|
+
}
|