@manybot/manybot 4.1.3 → 4.3.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![ManyBot Logo](logo.png)
4
4
 
5
- ![Node.js 18+](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white)
5
+ ![Node.js 20+](https://img.shields.io/badge/Node.js-20+-339933?logo=node.js&logoColor=white)
6
6
  ![npm 9+](https://img.shields.io/badge/npm-9+-CB3837?logo=npm&logoColor=white)
7
7
  ![GPL v3](https://img.shields.io/badge/License-GPL--v3-blue.svg)
8
8
  ![Linux](https://img.shields.io/badge/Linux%20%7C%20Windows-lightgrey)
@@ -16,7 +16,7 @@ ManyBot is a free and open-source messaging automation ecosystem for online busi
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Node.js >= 18
19
+ - Node.js >= 20
20
20
  - npm
21
21
 
22
22
  ## Getting started
package/package.json CHANGED
@@ -5,11 +5,11 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "4.1.3",
8
+ "version": "4.3.0",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {
12
- "node": ">=18"
12
+ "node": ">=20"
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
@@ -37,6 +37,7 @@
37
37
  "dependencies": {
38
38
  "node-webpmux": "^3.2.1",
39
39
  "qrcode-terminal": "^0.12.0",
40
+ "tar": "^7.5.16",
40
41
  "whatsapp-web.js": "^1.24.0"
41
42
  },
42
43
  "imports": {
@@ -15,16 +15,79 @@ import { PHONE_NUMBER, CLIENT_ID } from "#config";
15
15
  import { logger } from "#logger";
16
16
  import qrcode from "qrcode-terminal";
17
17
  import { t } from "#i18n"
18
+ import { CONFIG_DIR } from "#config";
19
+ import { extract } from "tar";
20
+ import { pipeline } from "node:stream/promises";
21
+ import { Readable } from "node:stream";
22
+ import { Transform } from "node:stream";
18
23
 
19
24
  const __filename = fileURLToPath(import.meta.url);
20
25
  const __dirname = path.dirname(__filename);
21
26
 
27
+ const chrome = path.join(CONFIG_DIR, "chrome/chrome")
28
+
29
+ if (!fs.existsSync(chrome)) {
30
+ try {
31
+ logger.warn(t("errors.chromeNotFound"));
32
+ const tmpDir = "/tmp/manybot-chrome";
33
+ const tmpPath = path.join(tmpDir, "chrome.tar.gz");
34
+ const url = "https://api.manybot.stxerr.dev/download-chrome";
35
+
36
+ fs.mkdirSync(tmpDir, { recursive: true });
37
+
38
+ const res = await fetch(url);
39
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
40
+
41
+ const expected = Number(res.headers.get("content-length"));
42
+ let received = 0;
43
+ const frames = ["|","/","-","\\"];
44
+ let frame = 0;
45
+ let spinner;
46
+
47
+ const file = fs.createWriteStream(tmpPath);
48
+
49
+ spinner = setInterval(() => {
50
+ const pct = expected
51
+ ? ` ${Math.floor((received / expected) * 100)}%`
52
+ : ` ${(received / 1024 / 1024).toFixed(1)}MB`;
53
+ process.stderr.write(`\r${frames[frame++ % frames.length]} Baixando Chrome...${pct}`);
54
+ }, 80);
55
+
56
+ const counter = new Transform({
57
+ transform(chunk, _enc, cb) {
58
+ received += chunk.length;
59
+ cb(null, chunk);
60
+ }
61
+ });
62
+
63
+ try {
64
+ await pipeline(
65
+ Readable.fromWeb(res.body),
66
+ counter,
67
+ file
68
+ );
69
+ } finally {
70
+ clearInterval(spinner);
71
+ process.stderr.write("\r\x1b[2K"); // limpa a linha
72
+ }
73
+
74
+ if (expected && received !== expected) {
75
+ throw new Error("Download incompleto");
76
+ }
77
+
78
+ await extract({ file: tmpPath, cwd: CONFIG_DIR, gzip: true });
79
+ } catch (err) {
80
+ throw new Error(t("errors.couldNotDownloadChrome") + err.message);
81
+ }
82
+
83
+ }
22
84
  export const { Client, LocalAuth, MessageMedia } = pkg;
23
85
 
24
86
  // -- Instance --------------------------------------------------
25
87
  const clientOptions = {
26
88
  authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
27
89
  puppeteer: {
90
+ executablePath: chrome,
28
91
  headless: true,
29
92
  args: [
30
93
  '--no-sandbox',
package/src/config.js CHANGED
@@ -14,9 +14,9 @@ import path from "path";
14
14
 
15
15
  import { logger } from "#logger";
16
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");
17
+ export const CONFIG_DIR = path.join(os.homedir(), ".manybot");
18
+ export const CONFIG_FILE = path.join(CONFIG_DIR, "manybot.conf");
19
+ export const PLUGIN_FILE = path.join(CONFIG_DIR, "manyplug.conf");
20
20
 
21
21
  /**
22
22
  * Converts strings to native JS values.
@@ -136,6 +136,70 @@ const log = {
136
136
  success: (...a) => logger.success(...a),
137
137
  };
138
138
 
139
+ // -- Contact API --------------------------------------------------------------
140
+
141
+ function buildContactsApi(client) {
142
+ return {
143
+ /**
144
+ * Get a normalized Contact object by ID.
145
+ * @param {string} contactId — serialized ID (e.g. "5511999999999@c.us")
146
+ * @returns {Promise<object|null>}
147
+ */
148
+ async get(contactId) {
149
+ try {
150
+ const c = await client.getContactById(contactId);
151
+ return {
152
+ id: c.id._serialized,
153
+ number: c.number,
154
+ pushname: c.pushname ?? null,
155
+ name: c.name ?? null,
156
+ shortName: c.shortName ?? null,
157
+ isBusiness: c.isBusiness,
158
+ isEnterprise: c.isEnterprise,
159
+ isBlocked: c.isBlocked,
160
+ isMe: c.isMe,
161
+ isMyContact: c.isMyContact,
162
+ isWAContact: c.isWAContact,
163
+ isUser: c.isUser,
164
+ isGroup: c.isGroup,
165
+ };
166
+ } catch {
167
+ return null;
168
+ }
169
+ },
170
+
171
+ /**
172
+ * Get the profile picture URL of a contact.
173
+ * Uses Contact#getProfilePicUrl() — respects privacy settings.
174
+ * @param {string} contactId
175
+ * @returns {Promise<string|null>}
176
+ */
177
+ async getProfilePicUrl(contactId) {
178
+ try {
179
+ const c = await client.getContactById(contactId);
180
+ return await c.getProfilePicUrl();
181
+ } catch {
182
+ return null;
183
+ }
184
+ },
185
+
186
+ /**
187
+ * Get the "about" text of a contact.
188
+ * Returns null if privacy settings block access.
189
+ * @param {string} contactId
190
+ * @returns {Promise<string|null>}
191
+ */
192
+ async getAbout(contactId) {
193
+ try {
194
+ const c = await client.getContactById(contactId);
195
+ return await c.getAbout();
196
+ } catch {
197
+ return null;
198
+ }
199
+ },
200
+ };
201
+ }
202
+
139
203
  // ── Internal media helpers ───────────────────────────────────────────────────
140
204
 
141
205
  function mediaFromSource(source, mimetype = "image/webp") {
@@ -151,8 +215,8 @@ function mediaFromSource(source, mimetype = "image/webp") {
151
215
  */
152
216
  function makeSender(target) {
153
217
  return {
154
- async text(content) {
155
- return target.sendMessage(content);
218
+ async text(content, opts = {}) {
219
+ return target.sendMessage(content, opts);
156
220
  },
157
221
  async image(filePath, caption = "") {
158
222
  const media = MessageMedia.fromFilePath(filePath);
@@ -185,7 +249,7 @@ function chatIdTarget(client, chatId) {
185
249
  /** Send to a specific chat by ID. Available in both setup and runtime. */
186
250
  function buildSendToApi(client) {
187
251
  return {
188
- sendTo: (chatId, text) => client.sendMessage(chatId, text),
252
+ sendTo: (chatId, text, opts) => client.sendMessage(chatId, text, opts),
189
253
  sendImageTo: (chatId, filePath, caption) => makeSender(chatIdTarget(client, chatId)).image(filePath, caption),
190
254
  sendVideoTo: (chatId, filePath, caption) => makeSender(chatIdTarget(client, chatId)).video(filePath, caption),
191
255
  sendAudioTo: (chatId, filePath) => makeSender(chatIdTarget(client, chatId)).audio(filePath),
@@ -197,7 +261,7 @@ function buildSendToApi(client) {
197
261
  function buildSendApi(chat) {
198
262
  const sender = makeSender(chat);
199
263
  return {
200
- send: (text) => sender.text(text),
264
+ send: (text, opts) => sender.text(text, opts),
201
265
  sendImage: (filePath, caption) => sender.image(filePath, caption),
202
266
  sendVideo: (filePath, caption) => sender.video(filePath, caption),
203
267
  sendAudio: (filePath) => sender.audio(filePath),
@@ -217,6 +281,7 @@ function buildBaseApi(client, pluginRegistry) {
217
281
  download: buildDownloadApi(),
218
282
  plugins: buildPluginsApi(pluginRegistry),
219
283
  botId: client.info?.wid?._serialized ?? null,
284
+ contacts: buildContactsApi(),
220
285
  ...buildSendToApi(client),
221
286
  };
222
287
  }
@@ -294,6 +359,10 @@ export function buildApi({ msg, chat, client, pluginRegistry }) {
294
359
  async react(emoji) {
295
360
  return msg.react(emoji);
296
361
  },
362
+
363
+ async getContact() {
364
+ return msg.getContact();
365
+ },
297
366
  },
298
367
 
299
368
  // ── chat ─────────────────────────────────────────────────
@@ -59,6 +59,8 @@
59
59
  "errors": {
60
60
  "pluginLoad": "Failed to load plugin",
61
61
  "messageProcess": "Failed to process message",
62
- "stack": "Stack"
62
+ "stack": "Stack",
63
+ "chromeNotFound": "Chrome not found. Downloading...",
64
+ "couldNotDownloadChrome": "Could not download Chrome: "
63
65
  }
64
66
  }
@@ -54,6 +54,8 @@
54
54
  "errors": {
55
55
  "pluginLoad": "Error al cargar el plugin",
56
56
  "messageProcess": "Error al procesar el mensaje",
57
- "stack": "Stack"
57
+ "stack": "Stack",
58
+ "chromeNotFound": "No se ha encontrado Chrome. Descargando...",
59
+ "couldNotDownloadChrome": "No se ha podido descargar Chrome: "
58
60
  }
59
61
  }
@@ -59,6 +59,8 @@
59
59
  "errors": {
60
60
  "pluginLoad": "Falha ao carregar plugin",
61
61
  "messageProcess": "Falha ao processar mensagem",
62
- "stack": "Stack"
62
+ "stack": "Stack",
63
+ "chromeNotFound": "Chrome não encontrado. Baixando...",
64
+ "couldNotDownloadChrome": "Não foi possível baixar o Chrome: "
63
65
  }
64
66
  }