@manybot/manybot 5.5.4 → 5.6.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.
Files changed (40) hide show
  1. package/README.md +15 -1
  2. package/dist/client/cache.js +1 -1
  3. package/dist/client/store.js +9 -0
  4. package/dist/config.js +230 -12
  5. package/dist/drivers/baileys/adapter.js +629 -0
  6. package/dist/drivers/{whatsapp → baileys}/api/index.js +553 -393
  7. package/dist/drivers/baileys/index.js +594 -0
  8. package/dist/drivers/{whatsapp → baileys}/loginPrompt.js +2 -0
  9. package/dist/drivers/{whatsapp → baileys}/messageHandler.js +46 -16
  10. package/dist/drivers/{whatsapp → baileys}/sdk/baileysSock.js +1 -30
  11. package/dist/drivers/jid.js +31 -0
  12. package/dist/drivers/types.js +14 -0
  13. package/dist/drivers/whatsmeow/client.js +252 -0
  14. package/dist/drivers/whatsmeow/index.js +79 -0
  15. package/dist/drivers/whatsmeow/installer.js +86 -0
  16. package/dist/drivers/whatsmeow/supervisor.js +328 -0
  17. package/dist/drivers/whatsmeow/whatsmeow.proto +64 -0
  18. package/dist/i18n/index.js +8 -12
  19. package/dist/kernel/alerts.js +190 -0
  20. package/dist/kernel/contactAutoSave.js +200 -0
  21. package/dist/kernel/driverManager.js +117 -0
  22. package/dist/kernel/pluginApi.js +25 -7
  23. package/dist/kernel/pluginLoader.js +12 -12
  24. package/dist/kernel/sendFallbackGuard.js +183 -0
  25. package/dist/kernel/sendGuard.js +143 -33
  26. package/dist/kernel/statusServer.js +39 -0
  27. package/dist/kernel/updateCheck.js +88 -0
  28. package/dist/kernel/waContract.js +16 -0
  29. package/dist/locales/en.json +16 -1
  30. package/dist/locales/es.json +16 -1
  31. package/dist/locales/pt.json +16 -1
  32. package/dist/main.js +100 -5
  33. package/dist/types.js +18 -11
  34. package/package.json +6 -8
  35. package/dist/core/adapter.js +0 -12
  36. package/dist/core/capabilities.js +0 -16
  37. package/dist/core/types.js +0 -6
  38. package/dist/drivers/index.js +0 -14
  39. package/dist/drivers/whatsapp/adapter.js +0 -7
  40. package/dist/drivers/whatsapp/index.js +0 -382
@@ -0,0 +1,629 @@
1
+ /**
2
+ * drivers/baileys/adapter.ts
3
+ *
4
+ * Adapter that wraps a Baileys `WASocket` (the raw, driver-specific socket)
5
+ * and the in-memory store behind the driver-neutral `WaContract` declared
6
+ * in src/kernel/waContract.ts.
7
+ *
8
+ * This is the ONLY file outside `drivers/baileys/sdk/baileysSock.ts` that
9
+ * imports from `@whiskeysockets/baileys`. Every other module in the
10
+ * codebase (kernel, pluginApi, pluginLoader, sendGuard, contactAutoSave,
11
+ * messageHandler, api/index.ts) talks to the driver through this contract
12
+ * — never imports Baileys directly.
13
+ *
14
+ * Responsibilities:
15
+ * - Convert Baileys event payloads (WAMessage, Chat, Contact) into the
16
+ * neutral event payloads the contract declares.
17
+ * - Convert BotQuotedRef / BotGroupMetadata / etc. back into the
18
+ * Baileys-shaped arguments each Baileys method expects.
19
+ * - Implement every method WaContract mandates, by delegating to the
20
+ * matching Baileys WASocket method.
21
+ *
22
+ * Returns/dispatches are pure adapters — error semantics, retries, fallbacks
23
+ * all live elsewhere (sendFallbackGuard, sendGuard, pluginGuard).
24
+ */
25
+ import { normalizeMessageContent, downloadMediaMessage, jidNormalizedUser, } from "@whiskeysockets/baileys";
26
+ import { createHash } from "node:crypto";
27
+ import { logger } from "#logger";
28
+ /**
29
+ * Classify a Baileys message-content payload (`WAMessageContent`) into
30
+ * the neutral `(type, body, mimetype)` triple used by `BotMessage`. Pure
31
+ * function — no side effects, no closures — so it's safe to call for
32
+ * both real incoming messages and the embedded `quotedMessage` content
33
+ * carried in `contextInfo` (which can be an `ephemeralMessage`/
34
+ * `viewOnceMessage` wrapper, hence the `normalizeMessageContent` call).
35
+ *
36
+ * Exported so `api/index.ts` can decode the quoted payload when
37
+ * synthesizing the `BotMessage` returned by `getReply()`.
38
+ */
39
+ export function decodeContent(content) {
40
+ const m = normalizeMessageContent(content) ?? undefined;
41
+ let type = "other";
42
+ let body = "";
43
+ let mimetype;
44
+ if (m?.conversation) {
45
+ type = "text";
46
+ body = m.conversation;
47
+ }
48
+ else if (m?.extendedTextMessage?.text) {
49
+ type = "text";
50
+ body = m.extendedTextMessage.text ?? "";
51
+ }
52
+ else if (m?.imageMessage) {
53
+ type = "image";
54
+ body = m.imageMessage.caption ?? "";
55
+ mimetype = m.imageMessage.mimetype ?? undefined;
56
+ }
57
+ else if (m?.videoMessage) {
58
+ type = "video";
59
+ body = m.videoMessage.caption ?? "";
60
+ mimetype = m.videoMessage.mimetype ?? undefined;
61
+ }
62
+ else if (m?.audioMessage) {
63
+ type = "audio";
64
+ mimetype = m.audioMessage.mimetype ?? undefined;
65
+ }
66
+ else if (m?.documentMessage) {
67
+ type = "document";
68
+ body = m.documentMessage.caption ?? "";
69
+ mimetype = m.documentMessage.mimetype ?? undefined;
70
+ }
71
+ else if (m?.stickerMessage) {
72
+ type = "sticker";
73
+ mimetype = m.stickerMessage.mimetype ?? undefined;
74
+ }
75
+ return { type, body, mimetype };
76
+ }
77
+ export function createBaileysAdapter(initial) {
78
+ // mutable so rebind() can swap it; closure-scoped so the contract below
79
+ // always sees the latest sock.
80
+ let sock = initial.sock;
81
+ const store = initial.store;
82
+ // ── Adapter-local helpers ────────────────────────────────────────────────
83
+ /**
84
+ * Build the `quoted` option for `sock.sendMessage(jid, content, opts)`.
85
+ *
86
+ * Baileys expects the `quoted` field to be a full message-shaped object —
87
+ * `{ key: {...}, message: {...} }` — so it can extract both the attribution
88
+ * (stanzaId/participant/fromMe from `key`) and the preview content (from
89
+ * `message`) when generating contextInfo. Passing only `key` makes
90
+ * `generateWAMessageFromContent` call `normalizeMessageContent(undefined)`
91
+ * → `getContentType(undefined)` → `undefined`, then index it with `[]`
92
+ * and throw `Cannot read properties of undefined (reading 'undefined')`.
93
+ *
94
+ * We resolve the full envelope from the in-memory store, indexed by the
95
+ * (remoteJid, id) pair carried in the neutral `BotQuotedRef`. If the
96
+ * envelope is no longer present (evicted past MAX_MSGS_PER_CHAT, lost on
97
+ * restart, etc.) we return `undefined` rather than emit a half-formed
98
+ * `quoted` — degrading to a plain unquoted reply is safer than crashing
99
+ * the calling plugin.
100
+ */
101
+ function buildQuotedOpts(quoted) {
102
+ if (!quoted?.id || !quoted?.remoteJid)
103
+ return undefined;
104
+ const raw = store.messages.get(quoted.remoteJid)?.get(quoted.id);
105
+ const inner = raw?.message;
106
+ if (!inner)
107
+ return undefined; // safe fallback: no quoted at all
108
+ // `quoted` (used by sendMessage for reply-citation) is a
109
+ // message-shaped object: `{ key: {...}, message: {...} }`. Baileys
110
+ // reads `quoted.key.*` to populate contextInfo (stanzaId, participant,
111
+ // fromMe) — passing the fields flat used to misattribute the reply to
112
+ // the bot itself. `quoted.message` is also required: Baileys' internal
113
+ // `generateWAMessageFromContent` calls `normalizeMessageContent(
114
+ // quoted.message)` and indexes the result with `getContentType(...)`,
115
+ // which throws if `.message` is missing.
116
+ return { quoted: { key: toFlatKey(quoted), message: inner } };
117
+ }
118
+ /**
119
+ * Translate a neutral `BotQuotedRef` into the FLAT key shape Baileys
120
+ * expects for `react`/`delete`/`edit`/`readMessages` (proto.IMessageKey
121
+ * — `{ id, remoteJid, fromMe, participant }` directly, NOT nested under
122
+ * a `.key` field). The nested `{key, message}` form is reserved for the
123
+ * `quoted` field on sendMessage — see `buildQuotedOpts` above.
124
+ */
125
+ function toFlatKey(ref) {
126
+ return {
127
+ id: ref.id ?? null,
128
+ remoteJid: ref.remoteJid ?? "",
129
+ fromMe: !!ref.fromMe,
130
+ participant: ref.participant ?? undefined,
131
+ };
132
+ }
133
+ /** Compute sha1-hex of a normalized buffer or string. */
134
+ function sha1(input) {
135
+ const hash = createHash("sha1");
136
+ if (typeof input === "string") {
137
+ hash.update(input, "utf8");
138
+ }
139
+ else {
140
+ hash.update(input);
141
+ }
142
+ return hash.digest("hex");
143
+ }
144
+ /** Translate a Baileys WAMessage into the neutral BotMessage envelope. */
145
+ function toBotMessage(msg) {
146
+ const m = normalizeMessageContent(msg.message) ?? undefined;
147
+ const { type, body, mimetype } = decodeContent(msg.message);
148
+ const key = msg.key;
149
+ const contextInfo = m?.extendedTextMessage?.contextInfo ??
150
+ m?.imageMessage?.contextInfo ??
151
+ m?.videoMessage?.contextInfo ??
152
+ m?.audioMessage?.contextInfo ??
153
+ m?.documentMessage?.contextInfo ??
154
+ undefined;
155
+ const ciTyped = contextInfo;
156
+ return {
157
+ id: msg.key.id ?? "",
158
+ chatId: msg.key.remoteJid ?? "",
159
+ fromMe: !!msg.key.fromMe,
160
+ type,
161
+ contentHash: sha1(body.trim()),
162
+ timestamp: Number(msg.messageTimestamp ?? 0) * 1000,
163
+ body,
164
+ mimetype: mimetype ?? undefined,
165
+ pushName: msg.pushName,
166
+ mentionedJid: ciTyped?.mentionedJid ?? undefined,
167
+ quotedKey: ciTyped?.stanzaId ? {
168
+ id: ciTyped.stanzaId,
169
+ remoteJid: msg.key.remoteJid ?? undefined,
170
+ fromMe: false,
171
+ participant: ciTyped.participant ?? undefined,
172
+ } : undefined,
173
+ fromLid: key.participantAlt,
174
+ fromPn: key.participant,
175
+ participantAlt: key.participantAlt,
176
+ remoteJidAlt: key.remoteJidAlt,
177
+ // Driver-specific escape hatches:
178
+ // - pollEncKeyRaw: poll-decryption key for vote decryption
179
+ // - contextInfo: full IContextInfo (incl. embedded quotedMessage)
180
+ // so quoted-message consumers (api/index.ts
181
+ // buildMessageContext / downloadMedia fallback)
182
+ // can decode the quoted payload without going
183
+ // back through the store.
184
+ _raw: {
185
+ pollEncKeyRaw: m?.messageContextInfo?.messageSecret ?? undefined,
186
+ contextInfo: ciTyped ?? undefined,
187
+ },
188
+ };
189
+ }
190
+ /** Plain summary of a Baileys Chat. */
191
+ function chatSummary(c) {
192
+ return { id: c.id, name: c.name };
193
+ }
194
+ /** Plain summary of a Baileys Contact. */
195
+ function contactSummary(c) {
196
+ return {
197
+ id: c.id,
198
+ name: c.name,
199
+ notify: c.notify,
200
+ verifiedName: c.verifiedName,
201
+ lid: c.lid,
202
+ };
203
+ }
204
+ const subscribers = new Map();
205
+ // Tracks the handlers each socket has registered so we can remove them on a
206
+ // fresh socket after reconnect. Declared up here because the rebind helpers
207
+ // below close over it and the first `bindSockEventsExternal(sock)` call
208
+ // happens before the later declaration site would have executed — accessing
209
+ // it there would hit the temporal dead zone.
210
+ const boundHandlers = new WeakMap();
211
+ function emit(event, payload) {
212
+ const set = subscribers.get(event);
213
+ if (!set)
214
+ return;
215
+ for (const h of set) {
216
+ try {
217
+ h(payload);
218
+ }
219
+ catch (e) {
220
+ logger.debug(`[waContract] handler for "${event}" threw: ${e.message}`);
221
+ }
222
+ }
223
+ }
224
+ bindSockEventsExternal(sock);
225
+ // ── The contract ────────────────────────────────────────────────────────
226
+ const contract = {
227
+ name: "baileys",
228
+ // ── lifecycle ─────────────────────────────────────────────────────────
229
+ async connect() { },
230
+ async disconnect() { },
231
+ isReady() { return false; /* drivers/baileys/index.ts overrides this via the WaContract lifecycle */ },
232
+ async resolveLid(lid) {
233
+ try {
234
+ const repo = sock.signalRepository;
235
+ const fn = repo?.lidMapping?.getPNForLID;
236
+ if (typeof fn === "function") {
237
+ const pn = await fn(lid);
238
+ return pn ?? null;
239
+ }
240
+ }
241
+ catch (err) {
242
+ logger.debug(`[waContract] resolveLid cross-check failed for "${lid}": ${err.message}`);
243
+ }
244
+ return null;
245
+ },
246
+ // ── event subscription ────────────────────────────────────────────────
247
+ on(event, handler) {
248
+ let set = subscribers.get(event);
249
+ if (!set) {
250
+ set = new Set();
251
+ subscribers.set(event, set);
252
+ }
253
+ set.add(handler);
254
+ return () => set.delete(handler);
255
+ },
256
+ // ── send ───────────────────────────────────────────────────────────────
257
+ async sendText(jid, text, opts) {
258
+ const content = { text };
259
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
260
+ const sendOpts = buildQuotedOpts(opts?.quoted);
261
+ if (opts?.mentions?.length)
262
+ content.mentions = opts.mentions;
263
+ const ref = await sock.sendMessage(jid, content, sendOpts);
264
+ return toSentRef(ref, jid);
265
+ },
266
+ async sendImage(jid, buffer, opts) {
267
+ const content = { image: buffer };
268
+ if (opts?.caption)
269
+ content.caption = opts.caption;
270
+ if (opts?.viewOnce)
271
+ content.viewOnce = true;
272
+ if (opts?.mentions?.length)
273
+ content.mentions = opts.mentions;
274
+ const ref = await sock.sendMessage(jid, content, buildQuotedOpts(opts?.quoted));
275
+ return toSentRef(ref, jid);
276
+ },
277
+ async sendVideo(jid, buffer, opts) {
278
+ const content = { video: buffer };
279
+ if (opts?.caption)
280
+ content.caption = opts.caption;
281
+ if (opts?.viewOnce)
282
+ content.viewOnce = true;
283
+ if (opts?.gifPlayback)
284
+ content.gifPlayback = true;
285
+ if (opts?.mentions?.length)
286
+ content.mentions = opts.mentions;
287
+ const ref = await sock.sendMessage(jid, content, buildQuotedOpts(opts?.quoted));
288
+ return toSentRef(ref, jid);
289
+ },
290
+ async sendAudio(jid, buffer, opts) {
291
+ const content = { audio: buffer };
292
+ content.mimetype = opts?.mimetype ?? "audio/mp4";
293
+ if (opts?.ptt)
294
+ content.ptt = true;
295
+ if (opts?.viewOnce)
296
+ content.viewOnce = true;
297
+ const ref = await sock.sendMessage(jid, content, buildQuotedOpts(opts?.quoted));
298
+ return toSentRef(ref, jid);
299
+ },
300
+ async sendSticker(jid, buffer, opts) {
301
+ const ref = await sock.sendMessage(jid, { sticker: buffer }, buildQuotedOpts(opts?.quoted));
302
+ return toSentRef(ref, jid);
303
+ },
304
+ async sendDocument(jid, buffer, filename, mimetype, opts) {
305
+ const ref = await sock.sendMessage(jid, { document: buffer, mimetype, fileName: filename }, buildQuotedOpts(opts?.quoted));
306
+ return toSentRef(ref, jid);
307
+ },
308
+ async sendPoll(jid, opts) {
309
+ const poll = { name: opts.name, values: opts.values, selectableCount: opts.selectableCount ?? 1 };
310
+ const ref = await sock.sendMessage(jid, { poll }, buildQuotedOpts(opts.quoted));
311
+ return toSentRef(ref, jid);
312
+ },
313
+ async react(jid, target, emoji) {
314
+ const key = toFlatKey(target);
315
+ await sock.sendMessage(jid, { react: { text: emoji, key } });
316
+ },
317
+ async deleteMessage(jid, target, forEveryone) {
318
+ if (!forEveryone)
319
+ return;
320
+ const key = toFlatKey(target);
321
+ await sock.sendMessage(jid, { delete: key });
322
+ },
323
+ async editMessage(jid, target, text) {
324
+ const key = toFlatKey(target);
325
+ await sock.sendMessage(jid, { text, edit: key });
326
+ },
327
+ // ── presence + read ───────────────────────────────────────────────────
328
+ async sendPresenceUpdate(state, jid) {
329
+ const baileyState = state === "composing" ? "composing"
330
+ : state === "recording" ? "recording"
331
+ : "paused";
332
+ await sock.sendPresenceUpdate(baileyState, jid);
333
+ },
334
+ async readMessages(keys) {
335
+ const baileyKeys = keys.map((k) => toFlatKey(k));
336
+ await sock.readMessages(baileyKeys);
337
+ },
338
+ // ── contacts ──────────────────────────────────────────────────────────
339
+ async onWhatsApp(jid) {
340
+ const fn = sock.onWhatsApp?.bind(sock);
341
+ if (!fn)
342
+ return null;
343
+ return await fn(jid);
344
+ },
345
+ async getBusinessProfile(jid) {
346
+ try {
347
+ const fn = sock.getBusinessProfile;
348
+ return await fn(jid);
349
+ }
350
+ catch {
351
+ return null;
352
+ }
353
+ },
354
+ async profilePictureUrl(jid) {
355
+ try {
356
+ const url = await sock.profilePictureUrl(jid, "image");
357
+ return url ?? null;
358
+ }
359
+ catch {
360
+ return null;
361
+ }
362
+ },
363
+ async fetchStatus(jid) {
364
+ const fn = sock.fetchStatus?.bind(sock);
365
+ if (!fn)
366
+ return null;
367
+ try {
368
+ const res = await fn(jid);
369
+ if (Array.isArray(res)) {
370
+ const entry = res.find((r) => jidNormalizedUser(r.id) === jidNormalizedUser(jid)) ?? res[0];
371
+ return entry?.status?.status ?? null;
372
+ }
373
+ return res?.status ?? null;
374
+ }
375
+ catch {
376
+ return null;
377
+ }
378
+ },
379
+ async updateBlockStatus(jid, action) {
380
+ const fn = sock.updateBlockStatus;
381
+ await fn(jid, action);
382
+ },
383
+ async addOrEditContact(jid, info) {
384
+ await sock.addOrEditContact(jid, info);
385
+ },
386
+ async removeContact(jid) {
387
+ await sock.removeContact(jid);
388
+ },
389
+ // ── groups ─────────────────────────────────────────────────────────────
390
+ async groupMetadata(jid) {
391
+ const meta = await sock.groupMetadata(jid);
392
+ return {
393
+ subject: meta.subject,
394
+ participants: meta.participants.map(p => ({
395
+ id: jidNormalizedUser(p.id),
396
+ isAdmin: p.admin === "admin" || p.admin === "superadmin",
397
+ isSuperAdmin: p.admin === "superadmin",
398
+ phoneNumber: p.phoneNumber,
399
+ })),
400
+ };
401
+ },
402
+ async groupParticipantsUpdate(jid, users, action) {
403
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
404
+ const res = await sock.groupParticipantsUpdate(jid, users, action);
405
+ return res;
406
+ },
407
+ async groupUpdateSubject(jid, subject) {
408
+ await sock.groupUpdateSubject(jid, subject);
409
+ },
410
+ async groupUpdateDescription(jid, description) {
411
+ await sock.groupUpdateDescription(jid, description);
412
+ },
413
+ async groupInviteCode(jid) {
414
+ return await sock.groupInviteCode(jid);
415
+ },
416
+ async groupRevokeInvite(jid) {
417
+ return await sock.groupRevokeInvite(jid);
418
+ },
419
+ // ── profile ────────────────────────────────────────────────────────────
420
+ async updateProfilePicture(jid, buffer) {
421
+ await sock.updateProfilePicture(jid, buffer);
422
+ },
423
+ async updateProfileName(name) {
424
+ await sock.updateProfileName(name);
425
+ },
426
+ async updateProfileStatus(status) {
427
+ await sock.updateProfileStatus(status);
428
+ },
429
+ // ── me ─────────────────────────────────────────────────────────────────
430
+ me() {
431
+ const u = sock.user;
432
+ const id = u?.id ? jidNormalizedUser(u.id) : "";
433
+ return { id, lid: u?.lid };
434
+ },
435
+ // ── verification primitive ──────────────────────────────────────────
436
+ // sendFallbackGuard calls this right after sendText resolves to confirm
437
+ // the message actually landed. The Baileys in-memory store is updated
438
+ // synchronously by the `messages.upsert` listener (store.ts), and
439
+ // own-sent messages land there as soon as sock.sendMessage returns —
440
+ // so the first lookup is effectively free (no network round-trip).
441
+ async getHistory(jid, opts) {
442
+ const limit = opts?.limit ?? 5;
443
+ const chatMsgs = store.messages.get(jid);
444
+ if (!chatMsgs || chatMsgs.size === 0)
445
+ return [];
446
+ // Newest-first so callers can slice `limit` off the head; we sort
447
+ // by messageTimestamp (seconds) and fall back to insertion order
448
+ // for messages synced together (same timestamp) so ordering stays
449
+ // stable across reads.
450
+ const all = [...chatMsgs.values()];
451
+ all.sort((a, b) => {
452
+ const ta = Number(a.messageTimestamp ?? 0);
453
+ const tb = Number(b.messageTimestamp ?? 0);
454
+ if (ta !== tb)
455
+ return tb - ta;
456
+ return 0;
457
+ });
458
+ return all.slice(0, limit).map(toBotMessage);
459
+ },
460
+ // ── media (download) ───────────────────────────────────────────────────
461
+ async downloadMedia(msg, opts) {
462
+ // Resolve the Baileys message envelope needed by `downloadMediaMessage`.
463
+ // Preferred path: if the caller already carries the embedded message
464
+ // payload (synthetic `BotMessage` for a quoted message, whose
465
+ // `_raw.contextInfo.quotedMessage` is the full WAMessageContent),
466
+ // build the envelope directly from that. This avoids the silent
467
+ // failure mode where the quoted message's original envelope has
468
+ // aged out of the store's per-chat ring buffer.
469
+ //
470
+ // Fallback: the regular case (downloading media for the incoming
471
+ // message itself, or any other BotMessage that has a real envelope
472
+ // in the store) — look it up by (chatId, id).
473
+ const embedded = msg._raw;
474
+ const embeddedContent = embedded?.contextInfo?.quotedMessage;
475
+ const raw = embeddedContent
476
+ ? {
477
+ key: {
478
+ id: embedded.contextInfo?.stanzaId ?? msg.id,
479
+ remoteJid: msg.chatId,
480
+ fromMe: false,
481
+ participant: embedded.contextInfo?.participant ?? undefined,
482
+ },
483
+ message: embeddedContent,
484
+ }
485
+ : store.messages.get(msg.chatId)?.get(msg.id);
486
+ if (!raw)
487
+ return null;
488
+ try {
489
+ const buffer = await downloadMediaMessage(raw, "buffer", {}, {
490
+ logger: silentBaileysLogger,
491
+ reuploadRequest: sock.updateMediaMessage,
492
+ });
493
+ if (!buffer || !Buffer.isBuffer(buffer))
494
+ return null;
495
+ // Animated sticker → mp4 is handled by the caller (api/index.ts
496
+ // wa.downloadMedia) — kept at the api layer for now.
497
+ if (opts?.asMp4) {
498
+ // No animated-sticker conversion here for now (would couple to
499
+ // ffmpeg + node-webpmux). Caller does it.
500
+ }
501
+ return { mimetype: msg.mimetype ?? "application/octet-stream", data: buffer };
502
+ }
503
+ catch (err) {
504
+ logger.warn(`[waContract] downloadMedia failed: ${err.message}`);
505
+ return null;
506
+ }
507
+ },
508
+ };
509
+ // ── Rebind helpers (post-reconnect in drivers/baileys/index.ts) ────────
510
+ //
511
+ // We need to keep references to the handlers we registered on the old
512
+ // sock.ev so we can remove them on a fresh socket — otherwise the old
513
+ // socket would leak listeners and every reconnect would double the
514
+ // fan-out work. The trick is the handler bodies close over `emit` and
515
+ // `toBotMessage`, which read from module-local state that doesn't change.
516
+ function bindSockEventsExternal(s) {
517
+ const ev = s.ev;
518
+ const handlers = new Map();
519
+ function register(event, h) {
520
+ handlers.set(event, h);
521
+ ev.on(event, h);
522
+ }
523
+ register("messages.upsert", (arg) => {
524
+ const { messages, type } = arg;
525
+ emit("messages.upsert", { messages: messages.map(toBotMessage), type });
526
+ });
527
+ register("messages.update", (arg) => {
528
+ const updates = arg;
529
+ emit("messages.update", { updates });
530
+ });
531
+ register("messaging-history.set", (arg) => {
532
+ const { chats, contacts, messages } = arg;
533
+ emit("messaging-history.set", {
534
+ chats: chats.map(chatSummary),
535
+ contacts: contacts.map(contactSummary),
536
+ messages: (messages ?? []).map(toBotMessage),
537
+ });
538
+ });
539
+ register("chats.upsert", (arg) => {
540
+ emit("chats.upsert", { chats: arg.map(chatSummary) });
541
+ });
542
+ register("chats.update", (arg) => {
543
+ emit("chats.update", { updates: arg });
544
+ });
545
+ register("contacts.upsert", (arg) => {
546
+ emit("contacts.upsert", { contacts: arg.map(contactSummary) });
547
+ });
548
+ register("contacts.update", (arg) => {
549
+ emit("contacts.update", { updates: arg.map(contactSummary) });
550
+ });
551
+ register("group-participants.update", (arg) => {
552
+ const { id, participants } = arg;
553
+ emit("group-participants.update", { id, participants });
554
+ });
555
+ register("groups.update", (arg) => {
556
+ emit("groups.update", { updates: arg });
557
+ });
558
+ register("connection.update", (arg) => {
559
+ const { connection, lastDisconnect } = arg;
560
+ emit("connection.update", {
561
+ connection,
562
+ lastDisconnect: lastDisconnect?.error ? { statusCode: lastDisconnect.error.output?.statusCode } : undefined,
563
+ });
564
+ });
565
+ boundHandlers.set(s, handlers);
566
+ }
567
+ function unbindSockEvents(s) {
568
+ const handlers = boundHandlers.get(s);
569
+ if (!handlers)
570
+ return;
571
+ const ev = s.ev;
572
+ for (const [event, h] of handlers) {
573
+ ev.off(event, h);
574
+ }
575
+ boundHandlers.delete(s);
576
+ }
577
+ // ── Handle returned to drivers/baileys/index.ts ────────────────────────
578
+ // Hang the raw `sock` off the contract via a private symbol so
579
+ // `drivers/baileys/api/index.ts` (the Baileys-only plugin-context
580
+ // builder) can pull it back when it needs a Baileys-only operation
581
+ // (poll decryption, gif detection, message envelope decoding for the
582
+ // helpers that don't yet have a driver-neutral equivalent). The symbol
583
+ // is shared via Symbol.for() so the api file and the adapter can agree
584
+ // on the same key without a top-level import. No other module in the
585
+ // codebase sees this — the rest of the kernel talks only to WaContract.
586
+ const RAW_SOCK = Symbol.for("manybot.baileys.rawSocket");
587
+ contract[RAW_SOCK] = sock;
588
+ return {
589
+ contract,
590
+ rebind(newSock) {
591
+ unbindSockEvents(sock);
592
+ sock = newSock;
593
+ bindSockEventsExternal(newSock);
594
+ // Keep the symbol attached to the contract pointing at the new
595
+ // socket too — api/index.ts caches the contract object, so the
596
+ // symbol slot is the only way it sees the rebind.
597
+ contract[RAW_SOCK] = newSock;
598
+ },
599
+ unbind(oldSock) {
600
+ unbindSockEvents(oldSock);
601
+ },
602
+ };
603
+ }
604
+ // ── Helpers used inside the adapter above ────────────────────────────────────
605
+ //
606
+ // `buildQuotedOpts` and `toFlatKey` are defined inside the
607
+ // `createBaileysAdapter` closure (so they can see the `store`) — only
608
+ // `toSentRef` and `silentBaileysLogger` remain module-scoped helpers.
609
+ function toSentRef(raw, fallbackChatId) {
610
+ const r = raw;
611
+ return {
612
+ id: r?.key?.id ?? "",
613
+ chatId: r?.key?.remoteJid ?? fallbackChatId,
614
+ timestamp: Date.now(),
615
+ };
616
+ }
617
+ /**
618
+ * pino-shape shim for Baileys' downloadMediaMessage — see the in-source
619
+ * explanation in api/index.ts (same shape, kept here for the adapter).
620
+ */
621
+ const silentBaileysLogger = {
622
+ level: "silent",
623
+ child() { return silentBaileysLogger; },
624
+ trace() { },
625
+ debug() { },
626
+ info() { },
627
+ warn(obj, msg) { logger.warn(`[baileys]`, msg ?? obj); },
628
+ error(obj, msg) { logger.error(`[baileys]`, msg ?? obj); },
629
+ };