@manybot/manybot 5.5.4 → 5.6.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 +15 -1
- package/dist/client/cache.js +1 -1
- package/dist/client/store.js +9 -0
- package/dist/config.js +230 -12
- package/dist/drivers/baileys/adapter.js +556 -0
- package/dist/drivers/{whatsapp → baileys}/api/index.js +533 -386
- package/dist/drivers/baileys/index.js +560 -0
- package/dist/drivers/{whatsapp → baileys}/loginPrompt.js +2 -0
- package/dist/drivers/{whatsapp → baileys}/messageHandler.js +46 -16
- package/dist/drivers/{whatsapp → baileys}/sdk/baileysSock.js +0 -29
- package/dist/drivers/jid.js +31 -0
- package/dist/drivers/types.js +14 -0
- package/dist/drivers/whatsmeow/client.js +203 -0
- package/dist/drivers/whatsmeow/index.js +79 -0
- package/dist/drivers/whatsmeow/installer.js +70 -0
- package/dist/drivers/whatsmeow/supervisor.js +309 -0
- package/dist/drivers/whatsmeow/whatsmeow.proto +64 -0
- package/dist/i18n/index.js +8 -12
- package/dist/kernel/alerts.js +190 -0
- package/dist/kernel/contactAutoSave.js +200 -0
- package/dist/kernel/driverManager.js +117 -0
- package/dist/kernel/pluginApi.js +25 -7
- package/dist/kernel/pluginLoader.js +12 -12
- package/dist/kernel/sendFallbackGuard.js +173 -0
- package/dist/kernel/sendGuard.js +143 -33
- package/dist/kernel/statusServer.js +39 -0
- package/dist/kernel/updateCheck.js +88 -0
- package/dist/kernel/waContract.js +16 -0
- package/dist/locales/en.json +15 -1
- package/dist/locales/es.json +15 -1
- package/dist/locales/pt.json +15 -1
- package/dist/main.js +77 -5
- package/dist/types.js +18 -11
- package/package.json +6 -8
- package/dist/core/adapter.js +0 -12
- package/dist/core/capabilities.js +0 -16
- package/dist/core/types.js +0 -6
- package/dist/drivers/index.js +0 -14
- package/dist/drivers/whatsapp/adapter.js +0 -7
- package/dist/drivers/whatsapp/index.js +0 -382
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* drivers/baileys/index.ts
|
|
3
|
+
*
|
|
4
|
+
* Main WhatsApp driver entry point (Baileys implementation).
|
|
5
|
+
*
|
|
6
|
+
* Owns the socket state machine (connect / disconnect / reconnect /
|
|
7
|
+
* circuit breaker) and exposes the live `WaContract` adapter to the
|
|
8
|
+
* rest of the kernel. Plugins, sendFallbackGuard, messageHandler,
|
|
9
|
+
* pluginLoader, contactAutoSave and sendGuard all consume the
|
|
10
|
+
* `WaContract` returned here — they never touch the raw Baileys socket.
|
|
11
|
+
*
|
|
12
|
+
* The contract's per-call methods (sendText, react, presence, etc.)
|
|
13
|
+
* come from `./adapter.ts` (createBaileysAdapter). The lifecycle
|
|
14
|
+
* methods (connect / disconnect / isReady) live HERE because they
|
|
15
|
+
* also drive the chat-cache persistence, plugin reload, alert
|
|
16
|
+
* registration, and the reconnect circuit breaker — none of which
|
|
17
|
+
* the adapter should know about.
|
|
18
|
+
*
|
|
19
|
+
* For the verification-required path (sendFallbackGuard), the contract
|
|
20
|
+
* also exposes `getHistory?` — which the adapter populates from the
|
|
21
|
+
* Baileys store.messages map on demand.
|
|
22
|
+
*/
|
|
23
|
+
import { createSocket, AUTH_DIR, store as sharedStore } from "./sdk/baileysSock.js";
|
|
24
|
+
import { createBaileysAdapter } from "./adapter.js";
|
|
25
|
+
import { handleMessage } from "./messageHandler.js";
|
|
26
|
+
import { normalizeJid } from "#drivers/jid.js";
|
|
27
|
+
import { loadPlugins, setupPlugins } from "#kernel/pluginLoader.js";
|
|
28
|
+
import { runContactRefreshSweep } from "#kernel/contactAutoSave.js";
|
|
29
|
+
import { registerAlertSockProvider, sendAlert } from "#kernel/alerts.js";
|
|
30
|
+
import { startUpdateCheckSchedule, stopUpdateCheckSchedule } from "#kernel/updateCheck.js";
|
|
31
|
+
import { setStatus } from "#kernel/statusServer.js";
|
|
32
|
+
import { logger } from "#logger";
|
|
33
|
+
import { PLUGINS, CLIENT_ID } from "#config";
|
|
34
|
+
import { t } from "#i18n";
|
|
35
|
+
import { printBanner } from "#client/banner.js";
|
|
36
|
+
import { loadChatCache, saveChatCache, isCacheFresh } from "#client/cache.js";
|
|
37
|
+
import { DisconnectReason } from "@whiskeysockets/baileys";
|
|
38
|
+
import fs from "fs/promises";
|
|
39
|
+
import * as clack from "@clack/prompts";
|
|
40
|
+
import { copyToClipboard } from "#utils/clipboard.js";
|
|
41
|
+
import { applyPatches } from "../patches/index.js";
|
|
42
|
+
applyPatches();
|
|
43
|
+
let state = "BOOT";
|
|
44
|
+
let shuttingDown = false;
|
|
45
|
+
let currentSock = null;
|
|
46
|
+
let currentStore = null;
|
|
47
|
+
let currentAdapter = null;
|
|
48
|
+
let reconnectTimer = null;
|
|
49
|
+
let connecting = false;
|
|
50
|
+
let reconnectAttempts = 0;
|
|
51
|
+
let halted = false;
|
|
52
|
+
let cacheHydrated = false;
|
|
53
|
+
let cacheSaveTimer = null;
|
|
54
|
+
let contactRefreshTimer = null;
|
|
55
|
+
registerAlertSockProvider(() => currentSock);
|
|
56
|
+
// ── Per-chat message queue ──────────────────────────────────────────────────
|
|
57
|
+
// Messages from the same chat are processed one at a time (in order), but
|
|
58
|
+
// different chats run concurrently — a slow plugin in one chat (e.g. sticker
|
|
59
|
+
// generation) no longer blocks replies in every other chat.
|
|
60
|
+
const chatQueues = new Map();
|
|
61
|
+
function enqueueForChat(jid, task) {
|
|
62
|
+
const prev = chatQueues.get(jid) ?? Promise.resolve();
|
|
63
|
+
const settled = prev.catch(() => { }).then(task).catch((e) => {
|
|
64
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
65
|
+
logger.error(`${err.message}\n${err.stack}`);
|
|
66
|
+
});
|
|
67
|
+
chatQueues.set(jid, settled);
|
|
68
|
+
settled.finally(() => {
|
|
69
|
+
if (chatQueues.get(jid) === settled)
|
|
70
|
+
chatQueues.delete(jid);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Messages older than this (WhatsApp's own delivery delay — e.g. backlog
|
|
74
|
+
// dumped after the bot reconnects) are skipped. Checked at arrival time,
|
|
75
|
+
// so time spent waiting in chatQueues never counts against a message.
|
|
76
|
+
const MAX_MESSAGE_AGE_SECONDS = 60;
|
|
77
|
+
function isMessageStale(timestamp) {
|
|
78
|
+
if (!timestamp)
|
|
79
|
+
return false;
|
|
80
|
+
const nowInSeconds = Math.floor(Date.now() / 1000);
|
|
81
|
+
const age = nowInSeconds - timestamp;
|
|
82
|
+
if (age > MAX_MESSAGE_AGE_SECONDS)
|
|
83
|
+
return true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const RECONNECT_BASE_MS = 1000;
|
|
87
|
+
const RECONNECT_MAX_MS = 60000;
|
|
88
|
+
// Circuit breaker: after this many consecutive failed reconnects, stop
|
|
89
|
+
// retrying automatically instead of hammering the connection forever.
|
|
90
|
+
// Rapid repeated attempts are themselves a ban signal (WhatsApp's abuse
|
|
91
|
+
// detection treats connect/disconnect loops as suspicious automation and
|
|
92
|
+
// each failed re-pairing attempt during a cooldown resets its timer), so
|
|
93
|
+
// silently retrying past this point can make a restriction last longer.
|
|
94
|
+
const MAX_RECONNECT_ATTEMPTS = 6;
|
|
95
|
+
const CACHE_SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5min
|
|
96
|
+
/**
|
|
97
|
+
* Loads the on-disk cache and merges it into `store` (union, never
|
|
98
|
+
* overwrite — see client/cache.ts). Runs once per process: the shared
|
|
99
|
+
* store singleton already accumulates across reconnects, so re-hydrating
|
|
100
|
+
* later would just redo a no-op merge.
|
|
101
|
+
*/
|
|
102
|
+
async function hydrateFromCache(store) {
|
|
103
|
+
if (cacheHydrated)
|
|
104
|
+
return;
|
|
105
|
+
cacheHydrated = true;
|
|
106
|
+
const snapshot = await loadChatCache();
|
|
107
|
+
if (!snapshot)
|
|
108
|
+
return;
|
|
109
|
+
store.hydrate(snapshot);
|
|
110
|
+
const fresh = await isCacheFresh();
|
|
111
|
+
const count = snapshot.chats.length;
|
|
112
|
+
const key = fresh ? "system.cacheLoaded" : "system.cacheLoadedStale";
|
|
113
|
+
logger.info(`[cache] ${t(key, { count })}`);
|
|
114
|
+
}
|
|
115
|
+
function startCacheAutosave(store) {
|
|
116
|
+
if (cacheSaveTimer)
|
|
117
|
+
return;
|
|
118
|
+
cacheSaveTimer = setInterval(() => { saveChatCache(store); }, CACHE_SAVE_INTERVAL_MS);
|
|
119
|
+
}
|
|
120
|
+
function stopCacheAutosave() {
|
|
121
|
+
if (!cacheSaveTimer)
|
|
122
|
+
return;
|
|
123
|
+
clearInterval(cacheSaveTimer);
|
|
124
|
+
cacheSaveTimer = null;
|
|
125
|
+
}
|
|
126
|
+
// Runs a few times a day, each time touching only a couple of stale
|
|
127
|
+
// contacts (see REFRESH_SWEEP_SAMPLE) — deliberately slow and staggered,
|
|
128
|
+
// same anti-detection reasoning as everything else in sendGuard.
|
|
129
|
+
const CONTACT_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
130
|
+
function startContactRefreshSweep(contract) {
|
|
131
|
+
if (contactRefreshTimer)
|
|
132
|
+
return;
|
|
133
|
+
contactRefreshTimer = setInterval(() => {
|
|
134
|
+
runContactRefreshSweep(contract).catch(() => { });
|
|
135
|
+
}, CONTACT_REFRESH_INTERVAL_MS);
|
|
136
|
+
}
|
|
137
|
+
function stopContactRefreshSweep() {
|
|
138
|
+
if (!contactRefreshTimer)
|
|
139
|
+
return;
|
|
140
|
+
clearInterval(contactRefreshTimer);
|
|
141
|
+
contactRefreshTimer = null;
|
|
142
|
+
}
|
|
143
|
+
function nextBackoffMs() {
|
|
144
|
+
const delay = RECONNECT_BASE_MS * 2 ** reconnectAttempts;
|
|
145
|
+
reconnectAttempts++;
|
|
146
|
+
return Math.min(delay, RECONNECT_MAX_MS);
|
|
147
|
+
}
|
|
148
|
+
function teardownSock(sock) {
|
|
149
|
+
if (!sock)
|
|
150
|
+
return;
|
|
151
|
+
try {
|
|
152
|
+
sock.ev.removeAllListeners();
|
|
153
|
+
}
|
|
154
|
+
catch { }
|
|
155
|
+
try {
|
|
156
|
+
sock.end(undefined);
|
|
157
|
+
}
|
|
158
|
+
catch { }
|
|
159
|
+
}
|
|
160
|
+
function scheduleReconnect(delayMs) {
|
|
161
|
+
if (shuttingDown)
|
|
162
|
+
return;
|
|
163
|
+
if (reconnectTimer)
|
|
164
|
+
clearTimeout(reconnectTimer);
|
|
165
|
+
reconnectTimer = setTimeout(() => {
|
|
166
|
+
reconnectTimer = null;
|
|
167
|
+
startBot();
|
|
168
|
+
}, delayMs);
|
|
169
|
+
}
|
|
170
|
+
async function startBot() {
|
|
171
|
+
if (connecting)
|
|
172
|
+
return;
|
|
173
|
+
connecting = true;
|
|
174
|
+
await hydrateFromCache(sharedStore);
|
|
175
|
+
const previousSock = currentSock;
|
|
176
|
+
const { sock, store } = await createSocket();
|
|
177
|
+
teardownSock(previousSock);
|
|
178
|
+
currentSock = sock;
|
|
179
|
+
currentStore = store;
|
|
180
|
+
// Build the driver-neutral WaContract adapter on top of this socket.
|
|
181
|
+
// Everything in the kernel (setupPlugins, handleMessage, sendGuard,
|
|
182
|
+
// contactAutoSave, ...) talks to the contract, never to the raw sock
|
|
183
|
+
// directly — that's how the whatsmeow driver plugs in as a second
|
|
184
|
+
// adapter without any of them knowing which one is active.
|
|
185
|
+
const adapter = createBaileysAdapter({ sock, store });
|
|
186
|
+
currentAdapter = adapter;
|
|
187
|
+
const contract = adapter.contract;
|
|
188
|
+
connecting = false;
|
|
189
|
+
let pluginsReady = false;
|
|
190
|
+
// ── Normal bot mode ─────────────────────────────────────────────────────────
|
|
191
|
+
sock.ev.on("connection.update", async (update) => {
|
|
192
|
+
const { connection, lastDisconnect } = update;
|
|
193
|
+
if (connection === "open") {
|
|
194
|
+
state = "READY_INIT";
|
|
195
|
+
reconnectAttempts = 0;
|
|
196
|
+
setStatus(true);
|
|
197
|
+
logger.success(t("system.connected"));
|
|
198
|
+
logger.info(t("system.clientId", { id: CLIENT_ID }));
|
|
199
|
+
printBanner();
|
|
200
|
+
if (!pluginsReady) {
|
|
201
|
+
pluginsReady = true;
|
|
202
|
+
await loadPlugins(PLUGINS);
|
|
203
|
+
await setupPlugins(contract, store);
|
|
204
|
+
}
|
|
205
|
+
startCacheAutosave(store);
|
|
206
|
+
startContactRefreshSweep(contract);
|
|
207
|
+
startUpdateCheckSchedule();
|
|
208
|
+
// buffer anti-replay / sync ghost messages
|
|
209
|
+
setTimeout(() => { state = "READY"; }, 2000);
|
|
210
|
+
}
|
|
211
|
+
if (connection === "close") {
|
|
212
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
213
|
+
const loggedOut = code === DisconnectReason.loggedOut;
|
|
214
|
+
state = "BOOT";
|
|
215
|
+
setStatus(false, String(code));
|
|
216
|
+
logger.warn(t("system.disconnected", { reason: String(code) }));
|
|
217
|
+
if (loggedOut) {
|
|
218
|
+
logger.warn(t("system.sessionExpired"));
|
|
219
|
+
try {
|
|
220
|
+
await fs.rm(AUTH_DIR, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
logger.error(`[whatsapp] Failed to remove session dir: ${e.message}`);
|
|
224
|
+
}
|
|
225
|
+
scheduleReconnect(1000);
|
|
226
|
+
}
|
|
227
|
+
else if (!shuttingDown) {
|
|
228
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
229
|
+
halted = true;
|
|
230
|
+
logger.error(t("system.reconnectHalted", { attempts: reconnectAttempts }));
|
|
231
|
+
sendAlert({
|
|
232
|
+
level: "critical",
|
|
233
|
+
title: "manybot parou de tentar reconectar",
|
|
234
|
+
message: `Desisti após ${reconnectAttempts} tentativas — possível restrição de conta. Rode connect() manualmente pra tentar de novo.`,
|
|
235
|
+
}).catch(() => { });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const delay = nextBackoffMs();
|
|
239
|
+
logger.info(t("system.reconnecting", { secs: Math.round(delay / 1000) }));
|
|
240
|
+
scheduleReconnect(delay);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
// Subscribe to the contract's translated messages.upsert events so
|
|
245
|
+
// there's a single placeholder where the WAMessage → BotMessage
|
|
246
|
+
// conversion lives (the adapter). The adapter's rebind() handles
|
|
247
|
+
// re-subscribing on the fresh socket on reconnect — we registered
|
|
248
|
+
// once here, that's it.
|
|
249
|
+
contract.on("messages.upsert", ({ messages, type }) => {
|
|
250
|
+
if (state !== "READY")
|
|
251
|
+
return;
|
|
252
|
+
if (type !== "notify" && type !== "append")
|
|
253
|
+
return;
|
|
254
|
+
for (const msg of messages) {
|
|
255
|
+
const tsSec = Math.floor(msg.timestamp / 1000);
|
|
256
|
+
if (type === "append" && !msg.fromMe)
|
|
257
|
+
continue;
|
|
258
|
+
if (isMessageStale(tsSec))
|
|
259
|
+
continue;
|
|
260
|
+
const jid = normalizeJid(msg.chatId);
|
|
261
|
+
enqueueForChat(jid, () => handleMessage(msg, contract, store));
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* isReady() mirrors the "open" event from Baileys — once connect() resolves
|
|
267
|
+
* we don't yet have a session, so we wait for the connection.update === "open"
|
|
268
|
+
* path to flip the flag. isReady() requires true ONLY after both.
|
|
269
|
+
*/
|
|
270
|
+
function sockIsOpen() {
|
|
271
|
+
return state === "READY" || state === "READY_INIT";
|
|
272
|
+
}
|
|
273
|
+
// ── Public driver: WaContract ────────────────────────────────────────────────
|
|
274
|
+
//
|
|
275
|
+
// Drivers expose the full WaContract surface: the kernel plugs in this
|
|
276
|
+
// object, and the lifecycle methods (connect / disconnect / isReady) wrap
|
|
277
|
+
// the startBot state machine. Until connect() resolves, the contract's
|
|
278
|
+
// per-call methods throw — plugins are only loaded after `connection.update
|
|
279
|
+
// === "open"`, so they never see the pre-connect state.
|
|
280
|
+
function requireReady() {
|
|
281
|
+
if (!currentAdapter) {
|
|
282
|
+
throw new Error("[baileys] driver not connected — call connect() first");
|
|
283
|
+
}
|
|
284
|
+
return currentAdapter.contract;
|
|
285
|
+
}
|
|
286
|
+
export const baileysContract = {
|
|
287
|
+
name: "baileys",
|
|
288
|
+
async connect() {
|
|
289
|
+
shuttingDown = false;
|
|
290
|
+
halted = false;
|
|
291
|
+
reconnectAttempts = 0;
|
|
292
|
+
await startBot();
|
|
293
|
+
},
|
|
294
|
+
async disconnect() {
|
|
295
|
+
shuttingDown = true;
|
|
296
|
+
reconnectAttempts = 0;
|
|
297
|
+
setStatus(false);
|
|
298
|
+
if (reconnectTimer) {
|
|
299
|
+
clearTimeout(reconnectTimer);
|
|
300
|
+
reconnectTimer = null;
|
|
301
|
+
}
|
|
302
|
+
stopCacheAutosave();
|
|
303
|
+
stopContactRefreshSweep();
|
|
304
|
+
stopUpdateCheckSchedule();
|
|
305
|
+
if (currentStore)
|
|
306
|
+
await saveChatCache(currentStore);
|
|
307
|
+
teardownSock(currentSock);
|
|
308
|
+
currentSock = null;
|
|
309
|
+
currentStore = null;
|
|
310
|
+
currentAdapter = null;
|
|
311
|
+
},
|
|
312
|
+
isReady() {
|
|
313
|
+
return sockIsOpen();
|
|
314
|
+
},
|
|
315
|
+
// ── event passthrough ───────────────────────────────────────────────────
|
|
316
|
+
on: (...args) => requireReady().on(...args),
|
|
317
|
+
resolveLid: (...args) => {
|
|
318
|
+
const c = requireReady();
|
|
319
|
+
return c.resolveLid ? c.resolveLid(...args) : Promise.resolve(null);
|
|
320
|
+
},
|
|
321
|
+
// ── send ────────────────────────────────────────────────────────────────
|
|
322
|
+
sendText: (...args) => requireReady().sendText(...args),
|
|
323
|
+
sendImage: (...args) => requireReady().sendImage(...args),
|
|
324
|
+
sendVideo: (...args) => requireReady().sendVideo(...args),
|
|
325
|
+
sendAudio: (...args) => requireReady().sendAudio(...args),
|
|
326
|
+
sendSticker: (...args) => requireReady().sendSticker(...args),
|
|
327
|
+
sendDocument: (...args) => requireReady().sendDocument(...args),
|
|
328
|
+
sendPoll: (...args) => requireReady().sendPoll(...args),
|
|
329
|
+
// ── react / edit / delete ───────────────────────────────────────────────
|
|
330
|
+
react: (...args) => requireReady().react(...args),
|
|
331
|
+
deleteMessage: (...args) => requireReady().deleteMessage(...args),
|
|
332
|
+
editMessage: (...args) => requireReady().editMessage(...args),
|
|
333
|
+
// ── presence + read ─────────────────────────────────────────────────────
|
|
334
|
+
sendPresenceUpdate: (...args) => requireReady().sendPresenceUpdate(...args),
|
|
335
|
+
readMessages: (...args) => requireReady().readMessages(...args),
|
|
336
|
+
// ── contacts ────────────────────────────────────────────────────────────
|
|
337
|
+
onWhatsApp: (...args) => requireReady().onWhatsApp(...args),
|
|
338
|
+
getBusinessProfile: (...args) => requireReady().getBusinessProfile(...args),
|
|
339
|
+
profilePictureUrl: (...args) => requireReady().profilePictureUrl(...args),
|
|
340
|
+
fetchStatus: (...args) => requireReady().fetchStatus(...args),
|
|
341
|
+
updateBlockStatus: (...args) => requireReady().updateBlockStatus(...args),
|
|
342
|
+
addOrEditContact: (...args) => requireReady().addOrEditContact(...args),
|
|
343
|
+
removeContact: (...args) => requireReady().removeContact(...args),
|
|
344
|
+
// ── groups ──────────────────────────────────────────────────────────────
|
|
345
|
+
groupMetadata: (...args) => requireReady().groupMetadata(...args),
|
|
346
|
+
groupParticipantsUpdate: (...args) => requireReady().groupParticipantsUpdate(...args),
|
|
347
|
+
groupUpdateSubject: (...args) => requireReady().groupUpdateSubject(...args),
|
|
348
|
+
groupUpdateDescription: (...args) => requireReady().groupUpdateDescription(...args),
|
|
349
|
+
groupInviteCode: (...args) => requireReady().groupInviteCode(...args),
|
|
350
|
+
groupRevokeInvite: (...args) => requireReady().groupRevokeInvite(...args),
|
|
351
|
+
// ── profile ────────────────────────────────────────────────────────────
|
|
352
|
+
updateProfilePicture: (...args) => requireReady().updateProfilePicture(...args),
|
|
353
|
+
updateProfileName: (...args) => requireReady().updateProfileName(...args),
|
|
354
|
+
updateProfileStatus: (...args) => requireReady().updateProfileStatus(...args),
|
|
355
|
+
// ── me ──────────────────────────────────────────────────────────────────
|
|
356
|
+
me: () => requireReady().me(),
|
|
357
|
+
// ── media (download) ────────────────────────────────────────────────────
|
|
358
|
+
downloadMedia: (...args) => requireReady().downloadMedia(...args),
|
|
359
|
+
// ── verification primitive ─────────────────────────────────────────────
|
|
360
|
+
// Delegates to the adapter, which reads from the in-memory Baileys
|
|
361
|
+
// store (store.messages). The adapter guarantees getHistory is defined
|
|
362
|
+
// (the Baileys adapter guarantees getHistory is defined), so the defensive fallback is gone.
|
|
363
|
+
getHistory: (jid, opts) => requireReady().getHistory(jid, opts),
|
|
364
|
+
/**
|
|
365
|
+
* Diagnostic mode: connects on its own session (separate from the
|
|
366
|
+
* running bot's, so it doesn't compete for the same WhatsApp Web
|
|
367
|
+
* slot), waits for the initial chat sync, then shows an interactive
|
|
368
|
+
* list — arrow keys to navigate, Enter to pick. The selected chat's
|
|
369
|
+
* id is normalized, resolved from `@lid` to the real phone-based JID
|
|
370
|
+
* when known, copied to the clipboard, and printed.
|
|
371
|
+
*/
|
|
372
|
+
async getId() {
|
|
373
|
+
logger.info(`[getid] ${t("getid.connecting")}`);
|
|
374
|
+
await hydrateFromCache(sharedStore);
|
|
375
|
+
const CONNECT_TIMEOUT_MS = 25000;
|
|
376
|
+
const MAX_ATTEMPTS = 3;
|
|
377
|
+
const MAX_ROUNDS = 2;
|
|
378
|
+
const getidAuthDir = `${CLIENT_ID}-getid`;
|
|
379
|
+
let sock = null;
|
|
380
|
+
let store = null;
|
|
381
|
+
for (let round = 1; round <= MAX_ROUNDS && !sock; round++) {
|
|
382
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS && !sock; attempt++) {
|
|
383
|
+
const created = await createSocket(getidAuthDir);
|
|
384
|
+
// Not a spinner here on purpose: if this session isn't paired yet,
|
|
385
|
+
// Baileys prints the QR/pairing code through the normal logger
|
|
386
|
+
// right after this — a spinner redrawing the line would bury it,
|
|
387
|
+
// so the person never gets a chance to approve it on their phone
|
|
388
|
+
// and the connection hangs forever waiting for "open".
|
|
389
|
+
const opened = await new Promise((resolve) => {
|
|
390
|
+
const timer = setTimeout(() => resolve(false), CONNECT_TIMEOUT_MS);
|
|
391
|
+
created.sock.ev.on("connection.update", (u) => {
|
|
392
|
+
if (u.connection === "open") {
|
|
393
|
+
clearTimeout(timer);
|
|
394
|
+
resolve(true);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
if (!opened) {
|
|
399
|
+
logger.warn(`[getid] round ${round} attempt ${attempt} timed out waiting for "open"`);
|
|
400
|
+
teardownSock(created.sock);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
sock = created.sock;
|
|
404
|
+
store = created.store;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!sock || !store) {
|
|
408
|
+
logger.error(`[getid] failed to open a session after ${MAX_ROUNDS * MAX_ATTEMPTS} attempts`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
// Wait for the initial chat sync to land in the store.
|
|
412
|
+
await new Promise((resolve) => {
|
|
413
|
+
const timer = setTimeout(resolve, 5000);
|
|
414
|
+
sock.ev.on("messaging-history.set", () => {
|
|
415
|
+
clearTimeout(timer);
|
|
416
|
+
resolve();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
const chats = store.chats.all();
|
|
420
|
+
if (chats.length === 0) {
|
|
421
|
+
logger.warn(`[getid] no chats synced yet — try again in a few seconds`);
|
|
422
|
+
teardownSock(sock);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const sorted = chats
|
|
426
|
+
.map((c) => ({ id: c.id, name: c.name ?? "" }))
|
|
427
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
428
|
+
const picked = await clack.select({
|
|
429
|
+
message: t("getid.pickChat"),
|
|
430
|
+
options: sorted.map((c) => ({ label: c.name || c.id, value: c.id })),
|
|
431
|
+
});
|
|
432
|
+
if (clack.isCancel(picked) || typeof picked !== "string") {
|
|
433
|
+
teardownSock(sock);
|
|
434
|
+
process.exit(0);
|
|
435
|
+
}
|
|
436
|
+
const resolved = await resolveLidForJid(sock, picked);
|
|
437
|
+
const finalJid = resolved ?? picked;
|
|
438
|
+
try {
|
|
439
|
+
await copyToClipboard(finalJid);
|
|
440
|
+
logger.success(`[getid] ${t("getid.copied", { id: finalJid })}`);
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
logger.info(`[getid] ${finalJid}`);
|
|
444
|
+
}
|
|
445
|
+
teardownSock(sock);
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* Try to resolve a `@lid` JID to its real `@s.whatsapp.net` form via the
|
|
450
|
+
* Baileys signal repository. Returns null if the adapter doesn't expose
|
|
451
|
+
* one (very old sessions) or the lookup fails.
|
|
452
|
+
*/
|
|
453
|
+
async function resolveLidForJid(sock, jid) {
|
|
454
|
+
try {
|
|
455
|
+
const repo = sock.signalRepository;
|
|
456
|
+
const fn = repo?.lidMapping?.getPNForLID;
|
|
457
|
+
if (typeof fn === "function") {
|
|
458
|
+
const pn = await fn(jid);
|
|
459
|
+
return pn ?? null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch { }
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Backwards-compat: callers that still import `baileysDriver` get the
|
|
467
|
+
* new contract object. The old `WaDriver` surface is gone — kernel
|
|
468
|
+
* code that needs the driver only ever sees the `WaContract` now.
|
|
469
|
+
*
|
|
470
|
+
* @deprecated Use `baileysContract` directly.
|
|
471
|
+
*/
|
|
472
|
+
export const baileysDriver = baileysContract;
|
|
473
|
+
// ── Baileys-only helpers used by the plugin-context builder ─────────────────
|
|
474
|
+
// The plugin-context layer (drivers/baileys/api/index.ts) still works
|
|
475
|
+
// against Baileys' raw WAMessage shape for things that don't yet have a
|
|
476
|
+
// driver-neutral equivalent (poll decryption, gif detection, history
|
|
477
|
+
// reconstruction from store.messages). These helpers stay here so the
|
|
478
|
+
// api file can convert a raw WAMessage into a driver-neutral BotMessage
|
|
479
|
+
// without re-importing the adapter's private internals.
|
|
480
|
+
import { normalizeMessageContent } from "@whiskeysockets/baileys";
|
|
481
|
+
import { createHash } from "node:crypto";
|
|
482
|
+
/**
|
|
483
|
+
* Map a Baileys WAMessage into the driver-neutral BotMessage envelope.
|
|
484
|
+
* Mirrors the adapter's internal conversion (the two diverged once —
|
|
485
|
+
* keep them in sync if you change one).
|
|
486
|
+
*/
|
|
487
|
+
export function toBotMessage(msg) {
|
|
488
|
+
const m = normalizeMessageContent(msg.message);
|
|
489
|
+
let type = "other";
|
|
490
|
+
let text = "";
|
|
491
|
+
let mimetype;
|
|
492
|
+
if (m?.conversation) {
|
|
493
|
+
type = "text";
|
|
494
|
+
text = m.conversation;
|
|
495
|
+
}
|
|
496
|
+
else if (m?.extendedTextMessage?.text) {
|
|
497
|
+
type = "text";
|
|
498
|
+
text = m.extendedTextMessage.text;
|
|
499
|
+
}
|
|
500
|
+
else if (m?.imageMessage) {
|
|
501
|
+
type = "image";
|
|
502
|
+
text = m.imageMessage.caption ?? "";
|
|
503
|
+
mimetype = m.imageMessage.mimetype ?? undefined;
|
|
504
|
+
}
|
|
505
|
+
else if (m?.videoMessage) {
|
|
506
|
+
type = "video";
|
|
507
|
+
text = m.videoMessage.caption ?? "";
|
|
508
|
+
mimetype = m.videoMessage.mimetype ?? undefined;
|
|
509
|
+
}
|
|
510
|
+
else if (m?.audioMessage) {
|
|
511
|
+
type = "audio";
|
|
512
|
+
text = "";
|
|
513
|
+
mimetype = m.audioMessage.mimetype ?? undefined;
|
|
514
|
+
}
|
|
515
|
+
else if (m?.documentMessage) {
|
|
516
|
+
type = "document";
|
|
517
|
+
text = m.documentMessage.caption ?? "";
|
|
518
|
+
mimetype = m.documentMessage.mimetype ?? undefined;
|
|
519
|
+
}
|
|
520
|
+
else if (m?.stickerMessage) {
|
|
521
|
+
type = "sticker";
|
|
522
|
+
text = "";
|
|
523
|
+
mimetype = m.stickerMessage.mimetype ?? undefined;
|
|
524
|
+
}
|
|
525
|
+
const key = msg.key;
|
|
526
|
+
const contextInfo = m?.extendedTextMessage?.contextInfo ??
|
|
527
|
+
m?.imageMessage?.contextInfo ??
|
|
528
|
+
m?.videoMessage?.contextInfo ??
|
|
529
|
+
m?.audioMessage?.contextInfo ??
|
|
530
|
+
m?.documentMessage?.contextInfo ??
|
|
531
|
+
undefined;
|
|
532
|
+
return {
|
|
533
|
+
id: msg.key?.id ?? "",
|
|
534
|
+
chatId: normalizeJid(msg.key?.remoteJid ?? ""),
|
|
535
|
+
fromMe: !!msg.key?.fromMe,
|
|
536
|
+
type,
|
|
537
|
+
contentHash: hashText(text),
|
|
538
|
+
timestamp: Number(msg.messageTimestamp ?? 0) * 1000,
|
|
539
|
+
body: text || undefined,
|
|
540
|
+
mimetype,
|
|
541
|
+
pushName: msg.pushName ?? undefined,
|
|
542
|
+
mentionedJid: contextInfo?.mentionedJid ?? undefined,
|
|
543
|
+
quotedKey: contextInfo?.stanzaId ? {
|
|
544
|
+
id: contextInfo.stanzaId,
|
|
545
|
+
remoteJid: msg.key?.remoteJid ?? undefined,
|
|
546
|
+
fromMe: false,
|
|
547
|
+
participant: contextInfo.participant ?? undefined,
|
|
548
|
+
} : undefined,
|
|
549
|
+
fromLid: key.participantAlt,
|
|
550
|
+
fromPn: key.participant,
|
|
551
|
+
participantAlt: key.participantAlt,
|
|
552
|
+
remoteJidAlt: key.remoteJidAlt,
|
|
553
|
+
_raw: {
|
|
554
|
+
pollEncKeyRaw: m?.messageContextInfo?.messageSecret ?? undefined,
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function hashText(text) {
|
|
559
|
+
return createHash("sha1").update(text.trim(), "utf8").digest("hex");
|
|
560
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as clack from "@clack/prompts";
|
|
12
12
|
import { CONFIG, persistConfigValue } from "#config";
|
|
13
13
|
import { t } from "#i18n";
|
|
14
|
+
import { promptWhatsmeowInstall } from "#drivers/whatsmeow/installer.js";
|
|
14
15
|
function cancelAndExit() {
|
|
15
16
|
clack.cancel(t("onboarding.cancelled"));
|
|
16
17
|
process.exit(1);
|
|
@@ -72,6 +73,7 @@ export async function resolveLoginMethod() {
|
|
|
72
73
|
clack.intro(t("onboarding.intro"));
|
|
73
74
|
if (needsMethod) {
|
|
74
75
|
method = await promptLoginMethod();
|
|
76
|
+
await promptWhatsmeowInstall();
|
|
75
77
|
}
|
|
76
78
|
if (method === "phone" && !phone) {
|
|
77
79
|
phone = await promptPhoneNumber();
|