@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * drivers/whatsapp/api/index.ts
2
+ * drivers/baileys/api/index.ts
3
3
  *
4
4
  * WhatsApp plugin context builder.
5
5
  * Exports buildApi() and buildSetupApi() for the WhatsApp driver.
@@ -8,23 +8,15 @@
8
8
  * All wwjs types have been replaced with Baileys equivalents.
9
9
  * The ctx surface area is preserved so existing plugins stay compatible.
10
10
  */
11
+ import { toBotMessage } from "#drivers/baileys/index.js";
12
+ import { decodeContent } from "#drivers/baileys/adapter.js";
11
13
  import { logger } from "#logger";
12
14
  import { t, createPluginT, reloadTranslations, getCurrentLang } from "#i18n";
13
15
  import { CONFIG, CONFIG_DIR } from "#config";
14
16
  import { enqueue } from "#download";
15
17
  import { schedule, cancelPlugin } from "#kernel/scheduler.js";
16
18
  import { emptyFolder } from "#utils/file.js";
17
- import { normalizeJid, toPresenceCapable } from "../sdk/baileysSock.js";
18
- // store.contacts is keyed by whatever raw JID Baileys handed the store
19
- // (e.g. "...@s.whatsapp.net" for a normal DM) — never run through
20
- // normalizeJid. Every lookup here works in normalized ("...@c.us") form,
21
- // so a plain `store.contacts[normalizedId]` misses for any non-@lid
22
- // contact. @lid contacts don't hit this: normalizeJid doesn't touch
23
- // "@lid", so the raw store key already matches. Used as a fallback key
24
- // alongside the normalized one wherever we read store.contacts directly.
25
- function denormalizeJid(jid) {
26
- return jid.replace(/@c\.us$/, "@s.whatsapp.net");
27
- }
19
+ import { normalizeJid, denormalizeJid, toWireJid } from "#drivers/jid.js";
28
20
  import { mkdirSync } from "fs";
29
21
  import { readFile, writeFile, unlink, mkdtemp, rm } from "fs/promises";
30
22
  import { readFileSync } from "fs";
@@ -32,92 +24,175 @@ import path from "path";
32
24
  import os from "os";
33
25
  import { spawn } from "child_process";
34
26
  import { randomUUID } from "crypto";
35
- import { waitForSendSlot, simulateState, typingDuration, mediaDuration } from "#sendguard";
27
+ import { waitForSendSlot, simulateState, typingDuration, mediaDuration, waitForEditSlot } from "#sendguard";
28
+ import { sendWithFallback } from "#kernel/sendFallbackGuard.js";
36
29
  import { buildSettingsApi } from "#settingsdb";
37
30
  import WebP from "node-webpmux";
38
- import { downloadMediaMessage, getAggregateVotesInPollMessage, decryptPollVote, jidNormalizedUser, normalizeMessageContent } from "@whiskeysockets/baileys";
39
- // ── Message body / type helpers ───────────────────────────────────────────────
40
- // WhatsApp wraps media in ephemeralMessage/viewOnceMessage/etc when sent as
41
- // disappearing or view-once the actual imageMessage/videoMessage/... sits
42
- // one level deeper. Reading msg.message directly misses all of that (this
43
- // was the cause of hasMedia silently returning false for such messages).
44
- function unwrap(msg) {
45
- return normalizeMessageContent(msg.message) ?? undefined;
31
+ import { getAggregateVotesInPollMessage, decryptPollVote, jidNormalizedUser, } from "@whiskeysockets/baileys";
32
+ // ── Raw-Baileys escape hatch ─────────────────────────────────────────────────
33
+ //
34
+ // This whole file is the Baileys driver's plugin-context builder, so the
35
+ // few operations that don't have a driver-neutral equivalent yet (poll
36
+ // decryption, gif detection, message envelope decoding for the helpers
37
+ // above) need direct access to the underlying WASocket. The WaContract
38
+ // intentionally doesn't expose the raw socket — but the adapter that
39
+ // builds it from a real Baileys socket hangs the socket off a private
40
+ // symbol so we can pull it back here without leaking Baileys types
41
+ // through the kernel/pluginApi surface.
42
+ //
43
+ // Defined as a separate symbol so the rest of the kernel can still
44
+ // import the WaContract without ever knowing this exists.
45
+ const RAW_SOCK = Symbol.for("manybot.baileys.rawSocket");
46
+ function rawSocketOf(contract) {
47
+ const sock = contract[RAW_SOCK];
48
+ if (!sock) {
49
+ throw new Error("[baileys/api] WaContract has no raw socket attached — only the Baileys adapter provides one.");
50
+ }
51
+ return sock;
52
+ }
53
+ /**
54
+ * Pull a Baileys-shaped message envelope off a `BotMessage._raw` when an
55
+ * api-layer helper needs fields the neutral envelope intentionally
56
+ * doesn't model (poll enc key, gifPlayback flag, …). The store still
57
+ * stores the raw `WAMessage` keyed by jid+id, so the common case is
58
+ * to read it back from there.
59
+ */
60
+ function rawMsgOf(msg, store) {
61
+ const raw = store.messages.get(msg.chatId)?.get(msg.id);
62
+ return raw;
63
+ }
64
+ /**
65
+ * Convert a `BotMessage` (driver-neutral incoming envelope) into a
66
+ * `BotQuotedRef` (driver-neutral outgoing reference), used as the
67
+ * `quoted` argument to the contract's send methods. The fields the
68
+ * adapter pre-extracts (id, chatId, fromMe, participantAlt/fromLid/
69
+ * fromPn) are enough for the protocol — no raw Baileys access needed.
70
+ */
71
+ function quotedRefFromMsg(msg) {
72
+ return {
73
+ id: msg.id,
74
+ remoteJid: msg.chatId,
75
+ fromMe: msg.fromMe,
76
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
77
+ };
46
78
  }
79
+ /**
80
+ * Read the just-sent message off the store after a `contract.sendX()`
81
+ * call resolves with a `SentMessageRef` (id + chatId + timestamp).
82
+ * Mirrors the text path's polling window — the driver's own
83
+ * `messages.upsert` handler writes the freshly-sent WAMessage into
84
+ * the store keyed by chatId+id, but that write may not have landed
85
+ * on the very next tick.
86
+ */
87
+ async function refToBotMessage(ref, store) {
88
+ const deadline = Date.now() + 200;
89
+ while (Date.now() < deadline) {
90
+ const stored = store.messages.get(ref.chatId)?.get(ref.id);
91
+ if (stored)
92
+ return toBotMessage(stored);
93
+ await new Promise(r => setTimeout(r, 20));
94
+ }
95
+ const final = store.messages.get(ref.chatId)?.get(ref.id);
96
+ return final ? toBotMessage(final) : undefined;
97
+ }
98
+ // ── Message body / type helpers ───────────────────────────────────────────────
99
+ //
100
+ // Helpers read the neutral `BotMessage` envelope. Fields the neutral
101
+ // envelope intentionally doesn't model (poll enc key, gifPlayback,
102
+ // messageSecret) are pulled out of the raw store entry when needed —
103
+ // the adapter fills `BotMessage._raw` with the bits a Baileys-side
104
+ // consumer might need (poll enc key) and leaves the rest on the raw
105
+ // WAMessage in the store.
47
106
  function getMsgBody(msg) {
48
- const m = unwrap(msg);
49
- if (!m)
50
- return "";
51
- return (m.conversation ??
52
- m.extendedTextMessage?.text ??
53
- m.imageMessage?.caption ??
54
- m.videoMessage?.caption ??
55
- m.documentMessage?.caption ??
56
- m.buttonsResponseMessage?.selectedDisplayText ??
57
- m.listResponseMessage?.title ??
58
- "");
107
+ // The adapter pre-extracts the body into `BotMessage.body`, so the
108
+ // common path is one field read. Fall back to the raw store entry only
109
+ // if the body wasn't populated (older code paths that built the
110
+ // envelope by hand).
111
+ if (msg.body !== undefined)
112
+ return msg.body;
113
+ return msg.body ?? "";
59
114
  }
60
115
  function getMsgType(msg) {
61
- const m = unwrap(msg);
62
- if (!m)
63
- return "unknown";
64
- if (m.conversation || m.extendedTextMessage)
65
- return "chat";
66
- if (m.buttonsResponseMessage || m.listResponseMessage)
67
- return "chat";
68
- if (m.imageMessage)
69
- return "image";
70
- if (m.videoMessage)
71
- return "video";
72
- if (m.audioMessage)
73
- return "audio";
74
- if (m.stickerMessage)
75
- return "sticker";
76
- if (m.documentMessage)
77
- return "document";
78
- if (m.pollCreationMessage ||
79
- m.pollCreationMessageV2 ||
80
- m.pollCreationMessageV3)
81
- return "poll";
82
- if (m.locationMessage || m.liveLocationMessage)
83
- return "location";
84
- if (m.contactMessage)
85
- return "vcard";
86
- if (m.contactsArrayMessage)
87
- return "multi_vcard";
88
- if (m.protocolMessage?.type === 0)
89
- return "revoked"; // REVOKE
90
- return "unknown";
116
+ // body-style text vs extendedTextMessage, buttons vs list response, etc.
117
+ // — these distinctions live on the raw envelope, since the neutral
118
+ // `type` only models the high-level kind (text/image/video/...).
119
+ const raw = msg._raw?.kind;
120
+ if (raw)
121
+ return raw;
122
+ switch (msg.type) {
123
+ case "text": return "chat";
124
+ case "image": return "image";
125
+ case "video": return "video";
126
+ case "audio": return "audio";
127
+ case "sticker": return "sticker";
128
+ case "document": return "document";
129
+ default: return "unknown";
130
+ }
91
131
  }
92
132
  function msgHasMedia(msg) {
93
- const m = unwrap(msg);
94
- return !!(m?.imageMessage || m?.videoMessage || m?.audioMessage || m?.documentMessage || m?.stickerMessage);
133
+ return msg.type === "image" || msg.type === "video" || msg.type === "audio" || msg.type === "document" || msg.type === "sticker";
95
134
  }
96
- function msgIsGif(msg) {
97
- return !!(unwrap(msg)?.videoMessage?.gifPlayback);
135
+ function msgIsGif(msg, store) {
136
+ const raw = rawMsgOf(msg, store);
137
+ if (!raw)
138
+ return false;
139
+ // gifPlayback is a Baileys-specific flag — read it off the raw
140
+ // WAMessage directly.
141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
+ return !!raw.message?.videoMessage?.gifPlayback;
98
143
  }
99
144
  /** Sender JID — group participant or DM remote JID, normalized. */
100
145
  function getMsgSender(msg, store) {
101
- const raw = normalizeJid(msg.key.participant || msg.key.remoteJid || "");
102
- return normalizeJid(store.resolveJid(raw));
146
+ // Prefer the @s.whatsapp.net form when we know it (Baileys-advisor split
147
+ // exposes both forms on incoming messages). The adapter fills
148
+ // `fromPn` when known; fall back to `chatId` for DMs.
149
+ const participant = msg._raw?.key?.participant
150
+ ?? msg.fromPn;
151
+ const raw = participant ?? msg.chatId;
152
+ return normalizeJid(store.resolveJid(normalizeJid(raw)));
153
+ }
154
+ /** Quoted-message metadata as the rest of the api uses it. */
155
+ function getQuotedContext(msg) {
156
+ if (!msg.quotedKey)
157
+ return null;
158
+ return {
159
+ id: msg.quotedKey.id ?? null,
160
+ remoteJid: msg.chatId,
161
+ fromMe: false,
162
+ participant: msg._raw?.contextInfo?.participant ?? null,
163
+ };
103
164
  }
104
165
  function getContextInfo(msg) {
105
- const m = unwrap(msg);
106
- return (m?.extendedTextMessage?.contextInfo ??
107
- m?.imageMessage?.contextInfo ??
108
- m?.videoMessage?.contextInfo ??
109
- m?.audioMessage?.contextInfo ??
110
- m?.documentMessage?.contextInfo ??
111
- null);
166
+ // The context info lives on the raw envelope (it carries mentions,
167
+ // stanzaId, etc.). The adapter doesn't pre-extract it because no
168
+ // neutral consumer needs it — pull it on demand from the store.
169
+ const raw = msg._raw?.contextInfo;
170
+ return raw ?? null;
171
+ }
172
+ /** True if the message has any @mention at all. */
173
+ function hasMention(contextInfo) {
174
+ const ci = contextInfo;
175
+ return !!(ci?.mentionedJid && Array.isArray(ci.mentionedJid) && ci.mentionedJid.length > 0);
176
+ }
177
+ /** True if the bot's own JID (PN or LID) is in contextInfo.mentionedJid. */
178
+ function hasBotMention(contextInfo, sock, store) {
179
+ const ci = contextInfo;
180
+ const mentioned = ci?.mentionedJid;
181
+ if (!mentioned || mentioned.length === 0)
182
+ return false;
183
+ const botLid = sock.user?.lid;
184
+ const botCandidates = [sock.user?.id, botLid]
185
+ .filter((v) => !!v)
186
+ .map(v => normalizeJid(store.resolveJid(normalizeJid(v))));
187
+ if (botCandidates.length === 0)
188
+ return false;
189
+ return mentioned.some(jid => {
190
+ const resolved = normalizeJid(store.resolveJid(normalizeJid(jid)));
191
+ return botCandidates.includes(resolved);
192
+ });
112
193
  }
113
194
  function getMsgMimetype(msg) {
114
- const m = unwrap(msg);
115
- return (m?.imageMessage?.mimetype ??
116
- m?.videoMessage?.mimetype ??
117
- m?.audioMessage?.mimetype ??
118
- m?.documentMessage?.mimetype ??
119
- m?.stickerMessage?.mimetype ??
120
- "application/octet-stream");
195
+ return msg.mimetype ?? "application/octet-stream";
121
196
  }
122
197
  // ── Chat adapter builder ──────────────────────────────────────────────────────
123
198
  // Group subjects only land in `store.chats` once Baileys fires chats.upsert
@@ -131,19 +206,24 @@ const GROUP_NAME_CACHE_TTL_MS = 5 * 60 * 1000;
131
206
  const groupNameCache = new Map();
132
207
  const GROUP_META_CACHE_TTL_MS = 5 * 60 * 1000;
133
208
  const groupMetaCache = new Map();
134
- async function getGroupMetadataCached(sock, jid) {
209
+ async function getGroupMetadataCached(contract, jid) {
135
210
  const cached = groupMetaCache.get(jid);
136
211
  if (cached && Date.now() - cached.at < GROUP_META_CACHE_TTL_MS)
137
212
  return cached.meta;
213
+ // Go through the raw sock for the Baileys-flavored metadata (carries
214
+ // pn/phoneNumber which the neutral contract drops). The neutral
215
+ // contract's groupMetadata is used everywhere the kernel asks for it.
216
+ const sock = rawSocketOf(contract);
138
217
  const meta = await sock.groupMetadata(jid);
139
218
  groupMetaCache.set(jid, { meta, at: Date.now() });
140
219
  return meta;
141
220
  }
142
221
  let groupMetaInvalidationBound = false;
143
- function bindGroupMetaInvalidation(sock) {
222
+ function bindGroupMetaInvalidation(contract) {
144
223
  if (groupMetaInvalidationBound)
145
224
  return;
146
225
  groupMetaInvalidationBound = true;
226
+ const sock = rawSocketOf(contract);
147
227
  const ev = sock.ev;
148
228
  ev.on("group-participants.update", (u) => groupMetaCache.delete(u.id));
149
229
  ev.on("groups.update", (updates) => {
@@ -153,17 +233,17 @@ function bindGroupMetaInvalidation(sock) {
153
233
  });
154
234
  }
155
235
  /**
156
- * Build a WAChat adapter from a Baileys message + store.
236
+ * Build a WAChat adapter from a BotMessage + store.
157
237
  * Exposed for use in messageHandler.ts.
158
238
  *
159
- * @param {WAProtoMsg} msg
160
- * @param {WAStore} store
161
- * @param {WASocket} sock
239
+ * @param {BotMessage} msg
240
+ * @param {BotStore} store
241
+ * @param {WaContract} contract
162
242
  * @returns {Promise<WAChat>}
163
243
  */
164
- export async function buildChatFromMsg(msg, store, sock) {
165
- const rawJid = msg.key.remoteJid ?? "";
166
- const jid = normalizeJid(rawJid);
244
+ export async function buildChatFromMsg(msg, store, contract) {
245
+ const rawJid = msg.chatId;
246
+ const jid = normalizeJid(store.resolveJid(rawJid));
167
247
  const user = jid.split("@")[0];
168
248
  const isGroup = rawJid.endsWith("@g.us");
169
249
  // Try to get name from store
@@ -176,7 +256,7 @@ export async function buildChatFromMsg(msg, store, sock) {
176
256
  }
177
257
  else {
178
258
  try {
179
- const meta = await getGroupMetadataCached(sock, rawJid);
259
+ const meta = await getGroupMetadataCached(contract, rawJid);
180
260
  if (meta?.subject) {
181
261
  name = meta.subject;
182
262
  groupNameCache.set(rawJid, { name: meta.subject, at: Date.now() });
@@ -377,14 +457,14 @@ function mentionDisplayName(jid) {
377
457
  }
378
458
  /**
379
459
  * Build a normalized contact object from a JID and optional store metadata.
380
- * isBusiness is resolved via sock.getBusinessProfile(jid) — it resolves to
381
- * a profile only for WhatsApp Business accounts, undefined otherwise.
382
- * @param {string} jid
383
- * @param {WAStoreContact} [info]
384
- * @param {string|null} [botJid]
385
- * @param {WASocket} [sock]
460
+ * isBusiness is resolved via contract.getBusinessProfile(jid) — it resolves
461
+ * to a profile only for WhatsApp Business accounts, undefined otherwise.
462
+ * @param {string} jid
463
+ * @param {RawStoreContact} [info]
464
+ * @param {string|null} [botJid]
465
+ * @param {WaContract} [contract]
386
466
  */
387
- async function normalizeContact(jid, info, botJid, sock) {
467
+ async function normalizeContact(jid, info, botJid, contract) {
388
468
  const number = jid.split("@")[0];
389
469
  let isBusiness = false;
390
470
  // We already have a contact record for this jid (learned from a real
@@ -394,9 +474,9 @@ async function normalizeContact(jid, info, botJid, sock) {
394
474
  // raw @lid we've never resolved to a phone number (returns a false
395
475
  // "doesn't exist" instead of throwing).
396
476
  let isWAAccount = Boolean(info);
397
- if (!isWAAccount && sock && !jid.endsWith("@g.us")) {
477
+ if (!isWAAccount && contract && !jid.endsWith("@g.us")) {
398
478
  try {
399
- const results = await sock.onWhatsApp(jid);
479
+ const results = await contract.onWhatsApp(jid);
400
480
  isWAAccount = Boolean(results?.[0]?.exists);
401
481
  }
402
482
  catch {
@@ -410,9 +490,9 @@ async function normalizeContact(jid, info, botJid, sock) {
410
490
  // resolved to null for an unknown ID instead of returning a hollow object.
411
491
  if (!jid.endsWith("@g.us") && !isWAAccount)
412
492
  return null;
413
- if (sock && !jid.endsWith("@g.us")) {
493
+ if (contract && !jid.endsWith("@g.us")) {
414
494
  try {
415
- isBusiness = Boolean(await sock.getBusinessProfile(jid));
495
+ isBusiness = Boolean(await contract.getBusinessProfile(jid));
416
496
  }
417
497
  catch {
418
498
  isBusiness = false;
@@ -453,7 +533,7 @@ function buildChatsApi(store) {
453
533
  };
454
534
  }
455
535
  // ── Contact API ───────────────────────────────────────────────────────────────
456
- function buildContactsApi(sock, store, botJid) {
536
+ function buildContactsApi(contract, store, botJid) {
457
537
  return {
458
538
  /**
459
539
  * Get a normalized contact object by JID.
@@ -481,24 +561,23 @@ function buildContactsApi(sock, store, botJid) {
481
561
  let freshPn;
482
562
  // 1) Baileys' own protocol-level LID↔PN store — populated as part of
483
563
  // real Signal session/identity handling, so it's authoritative when
484
- // it has an answer (unlike our own heuristic lidMap). Not part of
485
- // the public v6.7 TS surface, hence the cast; guarded with a
486
- // typeof check since older/patched Baileys builds may not have it.
487
- try {
488
- const lidMapping = sock.signalRepository?.lidMapping;
489
- if (typeof lidMapping?.getPNForLID === "function") {
490
- freshPn = await lidMapping.getPNForLID(contactId);
564
+ // it has an answer (unlike our own heuristic lidMap). Routed through
565
+ // the contract's optional resolveLid() helper (drivers without one
566
+ // e.g. the future whatsmeow client fall through to step 2).
567
+ if (contract.resolveLid) {
568
+ try {
569
+ freshPn = await contract.resolveLid(contactId);
570
+ }
571
+ catch (err) {
572
+ logger.warn(`[contacts.get] contract.resolveLid cross-check failed for "${contactId}" — ${err.message}`);
491
573
  }
492
- }
493
- catch (err) {
494
- logger.warn(`[contacts.get] signalRepository.lidMapping cross-check failed for "${contactId}" — ${err.message}`);
495
574
  }
496
575
  // 2) Fall back to live groupMetadata() when we know the group and
497
576
  // the signal repository didn't have an answer. Best-effort — see
498
577
  // the Baileys 6.7.x caveat in the doc comment above.
499
578
  if (!freshPn && opts?.groupId) {
500
579
  try {
501
- const meta = await sock.groupMetadata(opts.groupId);
580
+ const meta = await getGroupMetadataCached(contract, opts.groupId);
502
581
  const participant = meta.participants.find((p) => normalizeJid(p.id) === normalizeJid(contactId));
503
582
  freshPn = participant?.phoneNumber;
504
583
  }
@@ -546,7 +625,7 @@ function buildContactsApi(sock, store, botJid) {
546
625
  notify: resolvedInfo?.notify ?? raw?.notify,
547
626
  verifiedName: resolvedInfo?.verifiedName ?? raw?.verifiedName,
548
627
  } : undefined;
549
- return normalizeContact(resolved, info, botJid, sock);
628
+ return normalizeContact(resolved, info, botJid, contract);
550
629
  },
551
630
  /**
552
631
  * Get the profile picture URL of a contact.
@@ -556,7 +635,7 @@ function buildContactsApi(sock, store, botJid) {
556
635
  async getPfpUrl(contactId) {
557
636
  const resolved = normalizeJid(store.resolveJid(normalizeJid(contactId)));
558
637
  try {
559
- const url = await sock.profilePictureUrl(resolved, "image");
638
+ const url = await contract.profilePictureUrl(resolved);
560
639
  return url ?? null;
561
640
  }
562
641
  catch {
@@ -594,7 +673,7 @@ function buildContactsApi(sock, store, botJid) {
594
673
  // (PN when known) rather than a raw @lid, which it may not accept.
595
674
  const resolved = normalizeJid(store.resolveJid(normalizeJid(contactId)));
596
675
  try {
597
- const res = await sock.fetchStatus(resolved);
676
+ const res = await contract.fetchStatus(resolved);
598
677
  // Current Baileys versions return a USync result array
599
678
  // (`[{ id, status: { status, setAt } }]`) instead of the legacy
600
679
  // single `{ status }` object — handle both shapes.
@@ -602,7 +681,10 @@ function buildContactsApi(sock, store, botJid) {
602
681
  const entry = res.find(r => normalizeJid(r.id) === resolved) ?? res[0];
603
682
  return entry?.status?.status ?? null;
604
683
  }
605
- return res?.status ?? null;
684
+ if (res && typeof res === "object") {
685
+ return res.status ?? null;
686
+ }
687
+ return null;
606
688
  }
607
689
  catch (err) {
608
690
  // Previously swallowed silently, which made "always null" look
@@ -616,14 +698,14 @@ function buildContactsApi(sock, store, botJid) {
616
698
  * @param {string} contactId
617
699
  */
618
700
  async block(contactId) {
619
- return sock.updateBlockStatus(contactId, "block");
701
+ await contract.updateBlockStatus(contactId, "block");
620
702
  },
621
703
  /**
622
704
  * Unblock a contact.
623
705
  * @param {string} contactId
624
706
  */
625
707
  async unblock(contactId) {
626
- return sock.updateBlockStatus(contactId, "unblock");
708
+ await contract.updateBlockStatus(contactId, "unblock");
627
709
  },
628
710
  };
629
711
  }
@@ -636,36 +718,73 @@ function makeHistoryArray(entries, store) {
636
718
  };
637
719
  return arr;
638
720
  }
639
- export function buildMessageContext(msg, sock, store, guardOptions = {}) {
721
+ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
722
+ const sock = rawSocketOf(contract);
640
723
  const body = getMsgBody(msg);
641
724
  const prefix = CONFIG.CMD_PREFIX;
642
725
  const rawArgs = body.trim().split(/\s+/);
643
726
  const first = rawArgs[0]?.toLowerCase() ?? "";
644
727
  const hasPrefix = first.startsWith(prefix);
645
728
  const command = hasPrefix ? first.slice(prefix.length) : "";
646
- const rawJid = msg.key.remoteJid ?? "";
729
+ const rawJid = msg.chatId;
647
730
  const sender = getMsgSender(msg, store);
648
731
  const cooldown = guardOptions.cooldown ?? true;
649
732
  const jitter = guardOptions.jitter ?? true;
650
733
  const contextInfo = getContextInfo(msg);
734
+ // Build a synthetic quoted BotMessage when the original envelope carries
735
+ // a quotedMessage (the adapter pre-decodes the full IContextInfo into
736
+ // msg._raw.contextInfo). The synthetic uses the same decodeContent
737
+ // helper as toBotMessage so type/body/mimetype reflect what's actually
738
+ // in the quoted payload — without this, msgHasMedia()/downloadMedia()
739
+ // on the result of getReply() always reported type=other / no media,
740
+ // even when the quoted message was an image/video/document/etc.
741
+ //
742
+ // We carry the same _raw.contextInfo on the synthetic so a recursive
743
+ // getReply().getReply() keeps working (the inner call re-reads
744
+ // getContextInfo off _raw.contextInfo).
651
745
  const quotedRaw = contextInfo?.quotedMessage
652
- ? {
653
- key: {
654
- remoteJid: rawJid,
746
+ ? (() => {
747
+ const decoded = decodeContent(contextInfo.quotedMessage);
748
+ return {
749
+ id: contextInfo.stanzaId ?? "",
750
+ chatId: msg.chatId,
655
751
  fromMe: false,
656
- id: contextInfo.stanzaId ?? undefined,
657
- participant: contextInfo.participant ?? undefined,
658
- },
659
- message: contextInfo.quotedMessage,
660
- pushName: null,
661
- }
662
- : null;
752
+ type: decoded.type,
753
+ contentHash: "",
754
+ timestamp: 0,
755
+ body: decoded.body,
756
+ mimetype: decoded.mimetype,
757
+ _raw: {
758
+ contextInfo: {
759
+ stanzaId: contextInfo.stanzaId,
760
+ participant: contextInfo.participant,
761
+ mentionedJid: contextInfo.mentionedJid,
762
+ quotedMessage: contextInfo.quotedMessage,
763
+ },
764
+ },
765
+ };
766
+ })()
767
+ : msg.quotedKey
768
+ // No embedded quotedMessage (older envelopes, evicted from store, or
769
+ // the quoted message pre-dates contextInfo-quoting). Fall back to a
770
+ // key-only synthetic so hasReply()/getReply() still work, but
771
+ // hasMedia/downloadMedia on the result will degrade gracefully
772
+ // (type=other, mimetype=undefined).
773
+ ? {
774
+ id: msg.quotedKey.id ?? "",
775
+ chatId: msg.chatId,
776
+ fromMe: false,
777
+ type: "other",
778
+ contentHash: "",
779
+ timestamp: 0,
780
+ }
781
+ : null;
663
782
  return {
664
- id: msg.key.id ?? "",
665
- timestamp: Number(msg.messageTimestamp) || 0,
783
+ id: msg.id,
784
+ timestamp: msg.timestamp || 0,
666
785
  body,
667
786
  type: getMsgType(msg),
668
- fromMe: !!(msg.key.fromMe),
787
+ fromMe: msg.fromMe,
669
788
  sender,
670
789
  senderName: msg.pushName ?? sender.replace(/(:\d+)?@.*$/, ""),
671
790
  command,
@@ -674,43 +793,66 @@ export function buildMessageContext(msg, sock, store, guardOptions = {}) {
674
793
  return hasPrefix && command === cmd.toLowerCase();
675
794
  },
676
795
  hasMedia: msgHasMedia(msg),
677
- isGif: msgIsGif(msg),
796
+ isGif: msgIsGif(msg, store),
678
797
  async downloadMedia(opts = {}) {
679
798
  try {
680
- const buffer = await downloadMediaMessage(msg, "buffer", {});
681
- if (!buffer || !Buffer.isBuffer(buffer))
799
+ // contract.downloadMedia handles reupload internally via the
800
+ // driver's own protocol knowledge (Baileys: sock.updateMediaMessage).
801
+ const result = await contract.downloadMedia(msg, {});
802
+ if (!result)
682
803
  return null;
683
- const isAnimatedSticker = !!unwrap(msg)?.stickerMessage?.isAnimated;
804
+ const raw = rawMsgOf(msg, store);
805
+ const isAnimatedSticker = !!(raw?.message?.stickerMessage?.isAnimated);
684
806
  if (opts.asMp4 && isAnimatedSticker) {
685
- const mp4 = await stickerToMp4(buffer);
807
+ const mp4 = await stickerToMp4(result.data);
686
808
  return { mimetype: "video/mp4", data: mp4.toString("base64") };
687
809
  }
688
- return { mimetype: getMsgMimetype(msg), data: buffer.toString("base64") };
810
+ return { mimetype: result.mimetype, data: result.data.toString("base64") };
689
811
  }
690
- catch {
812
+ catch (err) {
813
+ logger.warn(`[whatsapp] downloadMedia failed: ${err.message}`);
691
814
  return null;
692
815
  }
693
816
  },
694
- hasReply: !!(contextInfo?.quotedMessage),
817
+ hasReply: !!(msg.quotedKey),
695
818
  async getReply() {
696
819
  if (!quotedRaw)
697
820
  return null;
698
- return buildMessageContext(quotedRaw, sock, store, { cooldown: false, jitter: false });
821
+ return buildMessageContext(quotedRaw, contract, store, { cooldown: false, jitter: false });
699
822
  },
700
- reply: makeSender(sock, store, rawJid, msg, { cooldown, jitter }),
823
+ hasMention: hasMention(contextInfo),
824
+ hasBotMention: hasBotMention(contextInfo, sock, store),
825
+ reply: makeSender(contract, store, rawJid, msg, { cooldown, jitter }),
701
826
  async react(emoji) {
702
- return sock.sendMessage(rawJid, { react: { text: emoji, key: msg.key } });
827
+ await contract.react(rawJid, {
828
+ id: msg.id,
829
+ remoteJid: msg.chatId,
830
+ fromMe: msg.fromMe,
831
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
832
+ }, emoji);
703
833
  },
704
834
  async delete(forEveryone = true) {
705
835
  if (forEveryone) {
706
- return sock.sendMessage(rawJid, { delete: msg.key });
836
+ await contract.deleteMessage(rawJid, {
837
+ id: msg.id,
838
+ remoteJid: msg.chatId,
839
+ fromMe: msg.fromMe,
840
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
841
+ }, true);
707
842
  }
708
843
  },
709
844
  async edit(text) {
710
- if (!msg.key.fromMe) {
845
+ if (!msg.fromMe) {
711
846
  throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
712
847
  }
713
- return sock.sendMessage(rawJid, { text, edit: msg.key });
848
+ if (!msg.id || !(await waitForEditSlot(msg.id)))
849
+ return;
850
+ await contract.editMessage(rawJid, {
851
+ id: msg.id,
852
+ remoteJid: msg.chatId,
853
+ fromMe: msg.fromMe,
854
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
855
+ }, text);
714
856
  },
715
857
  async pin(_duration) {
716
858
  logger.warn("[pluginApi] pin() is not supported with Baileys");
@@ -723,10 +865,10 @@ export function buildMessageContext(msg, sock, store, guardOptions = {}) {
723
865
  async getContact() {
724
866
  const info = store.contacts[sender]
725
867
  ?? store.contacts[denormalizeJid(sender)]
726
- ?? store.contacts[store.resolveJid(msg.key.participant ?? "")]
727
- ?? store.contacts[denormalizeJid(store.resolveJid(msg.key.participant ?? ""))];
868
+ ?? store.contacts[store.resolveJid(msg.fromPn ?? "")]
869
+ ?? store.contacts[denormalizeJid(store.resolveJid(msg.fromPn ?? ""))];
728
870
  const botJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null;
729
- return normalizeContact(sender, info, botJid, sock);
871
+ return normalizeContact(sender, info, botJid, contract);
730
872
  },
731
873
  };
732
874
  }
@@ -734,25 +876,31 @@ export function buildMessageContext(msg, sock, store, guardOptions = {}) {
734
876
  /**
735
877
  * Wraps a pending send and exposes chainable post-send actions.
736
878
  * Thenable: `await ctx.send.text("hi")` resolves to the message context.
879
+ *
880
+ * The wrapped `rawPromise` now resolves to a `BotMessage` (driver-neutral)
881
+ * instead of a raw `WAMessage` — the driver writes its own sent message
882
+ * into the in-memory store under jid+id, and the api reads it back as a
883
+ * BotMessage (built from the adapter's WAMessage→BotMessage translator).
884
+ * This keeps the rest of the api from depending on Baileys' wire shape.
737
885
  */
738
886
  class MessageHandle {
739
887
  _p;
740
- _sock;
888
+ _contract;
741
889
  _store;
742
890
  _jid = null;
743
891
  _guardOptions;
744
892
  rawPromise;
745
- constructor(promise, sock, store, guardOptions) {
893
+ constructor(promise, contract, store, guardOptions) {
746
894
  this.rawPromise = promise;
747
- this._sock = sock;
895
+ this._contract = contract;
748
896
  this._store = store;
749
897
  this._guardOptions = guardOptions ?? {};
750
898
  this._p = promise.then(msg => {
751
899
  if (!msg)
752
900
  return undefined;
753
901
  if (!this._jid)
754
- this._jid = msg.key.remoteJid || null;
755
- return buildMessageContext(msg, sock, store, guardOptions);
902
+ this._jid = msg.chatId || null;
903
+ return buildMessageContext(msg, this._contract, store, guardOptions);
756
904
  });
757
905
  }
758
906
  then(onfulfilled, onrejected) {
@@ -773,7 +921,7 @@ class MessageHandle {
773
921
  * const reply = await audio.reply.text("here it is!");
774
922
  */
775
923
  get reply() {
776
- return makeSender(this._sock, this._store, this._jid || "", this.rawPromise, this._guardOptions);
924
+ return makeSender(this._contract, this._store, this._jid || "", this.rawPromise, this._guardOptions);
777
925
  }
778
926
  /** Pin the sent message. */
779
927
  async pin(_duration) {
@@ -782,31 +930,45 @@ class MessageHandle {
782
930
  /** Delete the sent message. */
783
931
  async delete(forEveryone = true) {
784
932
  const msg = await this.rawPromise;
785
- if (!msg?.key)
933
+ if (!msg)
786
934
  return;
787
- const jid = msg.key.remoteJid;
788
935
  if (forEveryone) {
789
- return this._sock.sendMessage(jid, { delete: msg.key });
936
+ await this._contract.deleteMessage(msg.chatId, {
937
+ id: msg.id,
938
+ remoteJid: msg.chatId,
939
+ fromMe: msg.fromMe,
940
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
941
+ }, true);
790
942
  }
791
943
  }
792
944
  /** React to the sent message. */
793
945
  async react(emoji) {
794
946
  const msg = await this.rawPromise;
795
- if (!msg?.key)
947
+ if (!msg)
796
948
  return;
797
- return this._sock.sendMessage(msg.key.remoteJid, {
798
- react: { text: emoji, key: msg.key },
799
- });
949
+ await this._contract.react(msg.chatId, {
950
+ id: msg.id,
951
+ remoteJid: msg.chatId,
952
+ fromMe: msg.fromMe,
953
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
954
+ }, emoji);
800
955
  }
801
956
  /** Edit the sent message's text. Only works on the bot's own messages. */
802
957
  async edit(text) {
803
958
  const msg = await this.rawPromise;
804
- if (!msg?.key)
959
+ if (!msg)
805
960
  return;
806
- if (!msg.key.fromMe) {
961
+ if (!msg.fromMe) {
807
962
  throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
808
963
  }
809
- return this._sock.sendMessage(msg.key.remoteJid, { text, edit: msg.key });
964
+ if (!msg.id || !(await waitForEditSlot(msg.id)))
965
+ return;
966
+ await this._contract.editMessage(msg.chatId, {
967
+ id: msg.id,
968
+ remoteJid: msg.chatId,
969
+ fromMe: msg.fromMe,
970
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
971
+ }, text);
810
972
  }
811
973
  }
812
974
  // ── Sender factory ────────────────────────────────────────────────────────────
@@ -936,81 +1098,100 @@ async function stickerToMp4(webpBuffer) {
936
1098
  /**
937
1099
  * Returns send methods bound to a specific JID.
938
1100
  *
939
- * @param {WASocket} sock
940
- * @param {WAStore} store
941
- * @param {string} jid — destination JID (raw, not normalized)
942
- * @param {WAProtoMsg | Promise<WAProtoMsg | null>} [quoted] — message to quote (can be Promise)
943
- * @param {object} [guard]
1101
+ * @param {WaContract} contract
1102
+ * @param {BotStore} store
1103
+ * @param {string} jid — destination JID (raw, not normalized)
1104
+ * @param {BotMessage | Promise<BotMessage | null | undefined> | null} [quoted] — message to quote (can be Promise)
1105
+ * @param {object} [guard]
944
1106
  */
945
- function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter = true } = {}) {
1107
+ function makeSender(contract, store, jid, quoted = null, { cooldown = true, jitter = true } = {}) {
946
1108
  const normJid = normalizeJid(jid);
947
- // Helper: resolve quoted message if it's a Promise
1109
+ // Helper: resolve quoted message if it's a Promise, then turn it into a
1110
+ // BotQuotedRef the contract's send methods accept.
948
1111
  const resolveQuoted = async () => {
949
1112
  if (!quoted)
950
1113
  return undefined;
951
- if (quoted instanceof Promise) {
952
- const result = await quoted;
953
- return result || undefined;
954
- }
955
- return quoted;
1114
+ const msg = quoted instanceof Promise ? (await quoted) || undefined : quoted;
1115
+ return msg ? quotedRefFromMsg(msg) : undefined;
956
1116
  };
957
1117
  return {
958
1118
  text(content, opts = {}) {
1119
+ // The text path goes through sendFallbackGuard:
1120
+ // try the active driver, verify the message actually landed in the
1121
+ // driver's history, and on failure swap to the other driver. sendMedia
1122
+ // and react below still go straight to the contract on purpose — media
1123
+ // fallback is out of scope for this phase, react is one-shot and
1124
+ // already idempotent at the protocol level.
959
1125
  return new MessageHandle((async () => {
960
- const quotedMsg = await resolveQuoted();
961
- const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
962
- // mentions/linkPreview are part of the message CONTENT, not the send
963
- // options — Baileys reads contextInfo.mentionedJid off the content
964
- // object. Putting them on sendOpts (3rd arg) is silently ignored: the
965
- // message sends fine, "@name" shows as plain text, and nobody actually
966
- // gets tagged/notified.
967
- const messageContent = { text: content };
968
- if (opts.linkPreview === false) {
969
- messageContent.linkPreview = null;
1126
+ const quotedRef = await resolveQuoted();
1127
+ const mentionsResolved = opts.mentions?.length
1128
+ ? await resolveMentionJids(contract, store, jid, opts.mentions)
1129
+ : undefined;
1130
+ // Pre-send bookkeeping the Baileys path used to do inline:
1131
+ // waitForSendSlot is already invoked by sendFallbackGuard on the
1132
+ // primary attempt, so we don't repeat it here. simulateState is
1133
+ // best-effort and per-driver, so it stays on this side.
1134
+ await simulateState(contract, jid, typingDuration(content), "typing");
1135
+ const ref = await sendWithFallback(jid, content, {
1136
+ quoted: quotedRef ?? undefined,
1137
+ mentions: mentionsResolved,
1138
+ });
1139
+ // The guard returns a SentMessageRef (id + chatId + timestamp) but
1140
+ // the rest of the sender API (MessageHandle, buildPollApi's
1141
+ // rawMsg.chatId, etc.) expects the full BotMessage. The driver's
1142
+ // messages.upsert handler stores own sent messages into the store
1143
+ // keyed by jid+id, so we can read it back. Give the store a tiny
1144
+ // window to catch up — a freshly-sent message may not be in the
1145
+ // map yet on the very same tick — and return undefined if it
1146
+ // never lands; downstream code already tolerates that (e.g.
1147
+ // .reply / .delete are no-ops when rawPromise resolves to
1148
+ // undefined).
1149
+ const deadline = Date.now() + 200;
1150
+ while (Date.now() < deadline) {
1151
+ const stored = store.messages.get(ref.chatId)?.get(ref.id);
1152
+ if (stored)
1153
+ return toBotMessage(stored);
1154
+ await new Promise(r => setTimeout(r, 20));
970
1155
  }
971
- if (opts.mentions?.length) {
972
- messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
973
- }
974
- await waitForSendSlot(normJid, { cooldown, jitter });
975
- await simulateState(toPresenceCapable(sock), jid, typingDuration(content), "typing");
976
- return sock.sendMessage(jid, messageContent, sendOpts);
977
- })(), sock, store, { cooldown, jitter });
1156
+ const final = store.messages.get(ref.chatId)?.get(ref.id);
1157
+ return final ? toBotMessage(final) : undefined;
1158
+ })(), contract, store, { cooldown, jitter });
978
1159
  },
979
1160
  image(source, caption = "", opts = {}) {
980
1161
  return new MessageHandle((async () => {
981
- const quotedMsg = await resolveQuoted();
982
- const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
1162
+ const quotedRef = await resolveQuoted();
983
1163
  await waitForSendSlot(normJid, { cooldown, jitter });
984
- await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
1164
+ await simulateState(contract, jid, mediaDuration(caption), "typing");
985
1165
  const buffer = await resolveMediaBuffer(source);
986
- // viewOnce/mentions are part of the message CONTENT — see note above.
987
- const messageContent = { image: buffer, caption };
988
- if (opts.viewOnce) {
989
- messageContent.viewOnce = true;
990
- }
991
- if (opts.mentions?.length) {
992
- messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
993
- }
994
- return sock.sendMessage(jid, messageContent, sendOpts);
995
- })(), sock, store, { cooldown, jitter });
1166
+ const mentionsResolved = opts.mentions?.length
1167
+ ? await resolveMentionJids(contract, store, jid, opts.mentions)
1168
+ : undefined;
1169
+ const ref = await contract.sendImage(jid, buffer, {
1170
+ caption,
1171
+ quoted: quotedRef ?? undefined,
1172
+ mentions: mentionsResolved,
1173
+ viewOnce: opts.viewOnce,
1174
+ });
1175
+ return refToBotMessage(ref, store);
1176
+ })(), contract, store, { cooldown, jitter });
996
1177
  },
997
1178
  video(source, caption = "", opts = {}) {
998
1179
  return new MessageHandle((async () => {
999
- const quotedMsg = await resolveQuoted();
1000
- const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
1180
+ const quotedRef = await resolveQuoted();
1001
1181
  await waitForSendSlot(normJid, { cooldown, jitter });
1002
- await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
1182
+ await simulateState(contract, jid, mediaDuration(caption), "typing");
1003
1183
  const buffer = await resolveMediaBuffer(source);
1004
- // viewOnce/mentions are part of the message CONTENT — see note above.
1005
- const messageContent = { video: buffer, caption };
1006
- if (opts.viewOnce) {
1007
- messageContent.viewOnce = true;
1008
- }
1009
- if (opts.mentions?.length) {
1010
- messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
1011
- }
1012
- return sock.sendMessage(jid, messageContent, sendOpts);
1013
- })(), sock, store, { cooldown, jitter });
1184
+ const mentionsResolved = opts.mentions?.length
1185
+ ? await resolveMentionJids(contract, store, jid, opts.mentions)
1186
+ : undefined;
1187
+ const ref = await contract.sendVideo(jid, buffer, {
1188
+ caption,
1189
+ quoted: quotedRef ?? undefined,
1190
+ mentions: mentionsResolved,
1191
+ viewOnce: opts.viewOnce,
1192
+ });
1193
+ return refToBotMessage(ref, store);
1194
+ })(), contract, store, { cooldown, jitter });
1014
1195
  },
1015
1196
  /**
1016
1197
  * Send a GIF. WhatsApp has no native GIF format — this sends an mp4
@@ -1022,54 +1203,57 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
1022
1203
  */
1023
1204
  gif(source, caption = "", opts = {}) {
1024
1205
  return new MessageHandle((async () => {
1025
- const quotedMsg = await resolveQuoted();
1026
- const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
1206
+ const quotedRef = await resolveQuoted();
1027
1207
  await waitForSendSlot(normJid, { cooldown, jitter });
1028
- await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
1208
+ await simulateState(contract, jid, mediaDuration(caption), "typing");
1029
1209
  const buffer = needsGifConversion(source)
1030
1210
  ? await gifToMp4(source)
1031
1211
  : await resolveMediaBuffer(source);
1032
- const messageContent = { video: buffer, caption, gifPlayback: true };
1033
- if (opts.viewOnce) {
1034
- messageContent.viewOnce = true;
1035
- }
1036
- if (opts.mentions?.length) {
1037
- messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
1038
- }
1039
- return sock.sendMessage(jid, messageContent, sendOpts);
1040
- })(), sock, store, { cooldown, jitter });
1212
+ const mentionsResolved = opts.mentions?.length
1213
+ ? await resolveMentionJids(contract, store, jid, opts.mentions)
1214
+ : undefined;
1215
+ const ref = await contract.sendVideo(jid, buffer, {
1216
+ caption,
1217
+ quoted: quotedRef ?? undefined,
1218
+ mentions: mentionsResolved,
1219
+ viewOnce: opts.viewOnce,
1220
+ gifPlayback: true,
1221
+ });
1222
+ return refToBotMessage(ref, store);
1223
+ })(), contract, store, { cooldown, jitter });
1041
1224
  },
1042
1225
  audio(source, { asVoice = true, viewOnce = false } = {}) {
1043
1226
  return new MessageHandle((async () => {
1044
- const quotedMsg = await resolveQuoted();
1045
- const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
1227
+ const quotedRef = await resolveQuoted();
1046
1228
  await waitForSendSlot(normJid, { cooldown, jitter });
1047
- await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "recording");
1229
+ await simulateState(contract, jid, mediaDuration(), "recording");
1048
1230
  const buffer = await resolveMediaBuffer(source);
1049
- // viewOnce is part of the message CONTENT — see note above.
1050
- const messageContent = { audio: buffer, mimetype: "audio/mp4", ptt: asVoice };
1051
- if (viewOnce) {
1052
- messageContent.viewOnce = true;
1053
- }
1054
- return sock.sendMessage(jid, messageContent, sendOpts);
1055
- })(), sock, store, { cooldown, jitter });
1231
+ const ref = await contract.sendAudio(jid, buffer, {
1232
+ quoted: quotedRef ?? undefined,
1233
+ viewOnce: viewOnce,
1234
+ ptt: asVoice,
1235
+ mimetype: "audio/mp4",
1236
+ });
1237
+ return refToBotMessage(ref, store);
1238
+ })(), contract, store, { cooldown, jitter });
1056
1239
  },
1057
1240
  sticker(source) {
1058
1241
  return new MessageHandle((async () => {
1059
- const quotedMsg = await resolveQuoted();
1060
- const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
1242
+ const quotedRef = await resolveQuoted();
1061
1243
  await waitForSendSlot(normJid, { cooldown, jitter });
1062
- await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
1244
+ await simulateState(contract, jid, mediaDuration(), "typing");
1063
1245
  const buffer = await resolveMediaBuffer(source);
1064
- return sock.sendMessage(jid, { sticker: buffer }, qOpts);
1065
- })(), sock, store, { cooldown, jitter });
1246
+ const ref = await contract.sendSticker(jid, buffer, {
1247
+ quoted: quotedRef ?? undefined,
1248
+ });
1249
+ return refToBotMessage(ref, store);
1250
+ })(), contract, store, { cooldown, jitter });
1066
1251
  },
1067
1252
  file(source, filename) {
1068
1253
  return new MessageHandle((async () => {
1069
- const quotedMsg = await resolveQuoted();
1070
- const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
1254
+ const quotedRef = await resolveQuoted();
1071
1255
  await waitForSendSlot(normJid, { cooldown, jitter });
1072
- await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
1256
+ await simulateState(contract, jid, mediaDuration(), "typing");
1073
1257
  const isBuffer = Buffer.isBuffer(source);
1074
1258
  const buffer = await resolveMediaBuffer(source);
1075
1259
  // With a path we can always infer the mimetype/name from it. With a
@@ -1078,12 +1262,11 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
1078
1262
  // a generic octet-stream document named "file".
1079
1263
  const mimetype = filename ? mimeFromPath(filename) : (isBuffer ? "application/octet-stream" : mimeFromPath(source));
1080
1264
  const resolvedFilename = filename ?? (isBuffer ? "file" : path.basename(source));
1081
- return sock.sendMessage(jid, {
1082
- document: buffer,
1083
- mimetype,
1084
- fileName: resolvedFilename,
1085
- }, qOpts);
1086
- })(), sock, store, { cooldown, jitter });
1265
+ const ref = await contract.sendDocument(jid, buffer, resolvedFilename, mimetype, {
1266
+ quoted: quotedRef ?? undefined,
1267
+ });
1268
+ return refToBotMessage(ref, store);
1269
+ })(), contract, store, { cooldown, jitter });
1087
1270
  },
1088
1271
  /**
1089
1272
  * Send a poll.
@@ -1094,25 +1277,24 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
1094
1277
  */
1095
1278
  poll(question, options, { allowMultipleAnswers = false } = {}) {
1096
1279
  return new MessageHandle((async () => {
1097
- const quotedMsg = await resolveQuoted();
1098
- const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
1280
+ const quotedRef = await resolveQuoted();
1099
1281
  await waitForSendSlot(normJid, { cooldown, jitter });
1100
- return sock.sendMessage(jid, {
1101
- poll: {
1102
- name: question,
1103
- values: options,
1104
- selectableCount: allowMultipleAnswers ? 0 : 1,
1105
- },
1106
- }, qOpts);
1107
- })(), sock, store, { cooldown, jitter });
1282
+ const ref = await contract.sendPoll(jid, {
1283
+ name: question,
1284
+ values: options,
1285
+ selectableCount: allowMultipleAnswers ? 0 : 1,
1286
+ quoted: quotedRef ?? undefined,
1287
+ });
1288
+ return refToBotMessage(ref, store);
1289
+ })(), contract, store, { cooldown, jitter });
1108
1290
  },
1109
1291
  };
1110
1292
  }
1111
1293
  // ── Send API ──────────────────────────────────────────────────────────────────
1112
- function buildSendApi(sock, store, rawJid, guardOptions = {}) {
1294
+ function buildSendApi(contract, store, rawJid, guardOptions = {}) {
1113
1295
  const cooldown = (guardOptions.cooldown ?? true);
1114
1296
  const jitter = (guardOptions.jitter ?? true);
1115
- const current = makeSender(sock, store, rawJid, null, { cooldown, jitter });
1297
+ const current = makeSender(contract, store, rawJid, null, { cooldown, jitter });
1116
1298
  return {
1117
1299
  send: {
1118
1300
  text: (text, opts) => current.text(text, opts),
@@ -1127,21 +1309,22 @@ function buildSendApi(sock, store, rawJid, guardOptions = {}) {
1127
1309
  * Returns a sender bound to another chat.
1128
1310
  * @param {string} targetJid
1129
1311
  */
1130
- to: (targetJid) => makeSender(sock, store, targetJid, null, { cooldown: false, jitter: false }),
1312
+ to: (targetJid) => makeSender(contract, store, targetJid, null, { cooldown: false, jitter: false }),
1131
1313
  },
1132
1314
  };
1133
1315
  }
1134
1316
  /** Setup send API — no current chat, only .to(). */
1135
- function buildSetupSendApi(sock, store) {
1317
+ function buildSetupSendApi(contract, store) {
1136
1318
  return {
1137
1319
  send: {
1138
- to: (targetJid) => makeSender(sock, store, targetJid),
1320
+ to: (targetJid) => makeSender(contract, store, targetJid),
1139
1321
  },
1140
1322
  };
1141
1323
  }
1142
1324
  // ── Events API ────────────────────────────────────────────────────────────────
1143
1325
  const listenerRegistry = new Map();
1144
- export function cleanupPluginEvents(pluginName, sock) {
1326
+ export function cleanupPluginEvents(pluginName, contract) {
1327
+ const sock = rawSocketOf(contract);
1145
1328
  const list = listenerRegistry.get(pluginName);
1146
1329
  if (list) {
1147
1330
  for (const { event, handler } of list) {
@@ -1155,10 +1338,11 @@ export function cleanupPluginEvents(pluginName, sock) {
1155
1338
  cancelPlugin(pluginName);
1156
1339
  }
1157
1340
  /**
1158
- * @param {WASocket} sock
1159
- * @param {string} pluginName
1341
+ * @param {WaContract} contract
1342
+ * @param {string} pluginName
1160
1343
  */
1161
- function buildEventsApi(sock, pluginName) {
1344
+ function buildEventsApi(contract, pluginName) {
1345
+ const sock = rawSocketOf(contract);
1162
1346
  return {
1163
1347
  on(event, handler) {
1164
1348
  sock.ev.on(event, handler);
@@ -1193,30 +1377,6 @@ function buildEventsApi(sock, pluginName) {
1193
1377
  * @param {WASocket} sock
1194
1378
  * @param {string|null} chatJid — raw JID of the current group (null in setup context)
1195
1379
  */
1196
- /**
1197
- * Normalize any identifier to the wire JID format WhatsApp's servers and
1198
- * protocol actually expect (`...@s.whatsapp.net`). Used anywhere a JID is
1199
- * handed straight to Baileys — `groupParticipantsUpdate()` and message
1200
- * `mentions` — both of which silently misbehave otherwise: a malformed
1201
- * group-update JID gets the whole IQ query rejected ("bad-request"), and
1202
- * a malformed mentions entry just doesn't tag/notify anyone (the message
1203
- * still sends fine, so it looks like nothing is wrong until you check).
1204
- * Handles this framework's own normalized `@c.us` form (`contact.id`,
1205
- * `getMsgSender()`'s output — see normalizeJid()/denormalizeJid()), a bare
1206
- * phone number (with or without "+", spaces or dashes), and already-valid
1207
- * wire JIDs (`@s.whatsapp.net`, `@lid`, `@g.us`), which pass through
1208
- * unchanged.
1209
- * @param {string} id
1210
- */
1211
- function toWireJid(id) {
1212
- const trimmed = id.trim();
1213
- if (/@(s\.whatsapp\.net|lid|g\.us)$/.test(trimmed))
1214
- return trimmed;
1215
- if (trimmed.endsWith("@c.us"))
1216
- return denormalizeJid(trimmed);
1217
- const digits = trimmed.replace(/\D/g, "");
1218
- return `${digits}@s.whatsapp.net`;
1219
- }
1220
1380
  /**
1221
1381
  * Resolve mentions to the exact JID form the destination group uses for
1222
1382
  * that participant — not whatever store.resolveJid() happens to have
@@ -1226,7 +1386,7 @@ function toWireJid(id) {
1226
1386
  * member. DMs have no participant list to check against, so just wire-
1227
1387
  * normalize as before (mentions aren't a real thing in DMs anyway).
1228
1388
  */
1229
- async function resolveMentionJids(sock, store, jid, mentions) {
1389
+ async function resolveMentionJids(contract, store, jid, mentions) {
1230
1390
  const result = [];
1231
1391
  for (const m of mentions) {
1232
1392
  const wire = toWireJid(m);
@@ -1244,7 +1404,11 @@ async function resolveMentionJids(sock, store, jid, mentions) {
1244
1404
  }
1245
1405
  let meta;
1246
1406
  try {
1247
- meta = await getGroupMetadataCached(sock, jid);
1407
+ // getGroupMetadataCached uses the raw sock internally for the
1408
+ // Baileys-flavored participant field shape (admin: "admin" |
1409
+ // "superadmin", etc.); the neutral contract's groupMetadata doesn't
1410
+ // expose that yet.
1411
+ meta = await getGroupMetadataCached(contract, jid);
1248
1412
  }
1249
1413
  catch {
1250
1414
  return Array.from(new Set(result.filter(Boolean)));
@@ -1261,22 +1425,22 @@ async function resolveMentionJids(sock, store, jid, mentions) {
1261
1425
  }
1262
1426
  return Array.from(new Set(result.filter(Boolean)));
1263
1427
  }
1264
- function buildAdminApi(sock, chatJid) {
1428
+ function buildAdminApi(contract, chatJid) {
1265
1429
  const norm = (v) => (Array.isArray(v) ? v : [v]).map(toWireJid);
1266
1430
  function requireChat() {
1267
1431
  if (!chatJid)
1268
1432
  throw new Error("This admin operation requires a runtime group context.");
1269
1433
  }
1270
1434
  async function getGroup(jid) {
1271
- const meta = await sock.groupMetadata(jid);
1435
+ const meta = await contract.groupMetadata(jid);
1272
1436
  if (!meta)
1273
1437
  throw new Error(`Group not found: ${jid}`);
1274
1438
  return meta;
1275
1439
  }
1276
1440
  /**
1277
- * Baileys' `groupParticipantsUpdate()` resolves normally even when
1278
- * WhatsApp rejected some (or all) of the requested participants — it
1279
- * returns an array with a per-participant `status` code (`'200'` =
1441
+ * The contract's `groupParticipantsUpdate()` resolves normally even
1442
+ * when WhatsApp rejected some (or all) of the requested participants —
1443
+ * it returns an array with a per-participant `status` code (`'200'` =
1280
1444
  * success; anything else is a rejection — e.g. `'403'`/`'401'` not
1281
1445
  * authorized, often because the target's privacy settings block being
1282
1446
  * added by a non-contact; `'409'` already a participant; `'408'`
@@ -1296,15 +1460,15 @@ function buildAdminApi(sock, chatJid) {
1296
1460
  }
1297
1461
  }
1298
1462
  /**
1299
- * Thin wrapper around `sock.groupParticipantsUpdate()` that turns an
1300
- * opaque Boom rejection (e.g. the whole IQ query bounced with
1463
+ * Thin wrapper around `contract.groupParticipantsUpdate()` that turns
1464
+ * an opaque rejection (e.g. the whole IQ query bounced with
1301
1465
  * "bad-request") into an error that names the group/action/participants
1302
1466
  * involved, then still runs the per-participant status check above.
1303
1467
  */
1304
1468
  async function runParticipantsUpdate(jid, users, action) {
1305
1469
  let results;
1306
1470
  try {
1307
- results = await sock.groupParticipantsUpdate(jid, users, action);
1471
+ results = await contract.groupParticipantsUpdate(jid, users, action);
1308
1472
  }
1309
1473
  catch (err) {
1310
1474
  throw new Error(`groupParticipantsUpdate("${action}") falhou para o grupo "${jid}" com participantes [${users.join(", ")}]: ${err.message}`);
@@ -1351,49 +1515,49 @@ function buildAdminApi(sock, chatJid) {
1351
1515
  /** @param {string} name */
1352
1516
  async setSubject(name) {
1353
1517
  requireChat();
1354
- return sock.groupUpdateSubject(chatJid, name);
1518
+ return contract.groupUpdateSubject(chatJid, name);
1355
1519
  },
1356
1520
  /** @param {string} text */
1357
1521
  async setDescription(text) {
1358
1522
  requireChat();
1359
- return sock.groupUpdateDescription(chatJid, text);
1523
+ return contract.groupUpdateDescription(chatJid, text);
1360
1524
  },
1361
1525
  /** @param {string|Buffer} source */
1362
1526
  async setProfilePic(source) {
1363
1527
  requireChat();
1364
1528
  const buffer = Buffer.isBuffer(source) ? source : readFileSync(source);
1365
- return sock.updateProfilePicture(chatJid, buffer);
1529
+ return contract.updateProfilePicture(chatJid, buffer);
1366
1530
  },
1367
1531
  async getInviteLink(groupId) {
1368
1532
  const jid = groupId ?? chatJid;
1369
1533
  if (!jid)
1370
1534
  throw new Error("This admin operation requires a runtime group context.");
1371
- const code = await sock.groupInviteCode(jid);
1535
+ const code = await contract.groupInviteCode(jid);
1372
1536
  return `https://chat.whatsapp.com/${code}`;
1373
1537
  },
1374
1538
  async revokeInvite() {
1375
1539
  requireChat();
1376
- return sock.groupRevokeInvite(chatJid);
1540
+ return contract.groupRevokeInvite(chatJid);
1377
1541
  },
1378
1542
  };
1379
1543
  }
1380
1544
  // ── Me API ────────────────────────────────────────────────────────────────────
1381
- /** @param {WASocket} sock */
1382
- function buildMeApi(sock) {
1545
+ /** @param {WaContract} contract */
1546
+ function buildMeApi(contract) {
1383
1547
  return {
1384
1548
  /** @param {string} name */
1385
1549
  async setName(name) {
1386
- return sock.updateProfileName(name);
1550
+ return contract.updateProfileName(name);
1387
1551
  },
1388
1552
  /** @param {string} text */
1389
1553
  async setAbout(text) {
1390
- return sock.updateProfileStatus(text);
1554
+ return contract.updateProfileStatus(text);
1391
1555
  },
1392
1556
  /** @param {string|Buffer} source */
1393
1557
  async setProfilePic(source) {
1394
1558
  const buffer = Buffer.isBuffer(source) ? source : readFileSync(source);
1395
- const jid = sock.user?.id ?? "";
1396
- return sock.updateProfilePicture(jid, buffer);
1559
+ const jid = contract.me().id ?? "";
1560
+ return contract.updateProfilePicture(jid, buffer);
1397
1561
  },
1398
1562
  };
1399
1563
  }
@@ -1408,13 +1572,13 @@ const pollListenersBySocket = new WeakMap();
1408
1572
  * Tracks votes for an active poll.
1409
1573
  * Obtained via ctx.poll.create().
1410
1574
  */
1411
- class PollHandle {
1575
+ export class PollHandle {
1412
1576
  msgId;
1413
1577
  _options;
1414
1578
  _callbacks;
1415
1579
  _registry;
1416
1580
  constructor(msg, options, registry) {
1417
- this.msgId = msg.key.id ?? "";
1581
+ this.msgId = msg.id ?? "";
1418
1582
  this._options = new Map(options.map(o => [o, new Set()]));
1419
1583
  this._callbacks = [];
1420
1584
  this._registry = registry;
@@ -1463,13 +1627,13 @@ class PollHandle {
1463
1627
  }
1464
1628
  }
1465
1629
  /**
1466
- * @param {WASocket} sock
1467
- * @param {WAStore} store
1468
- * @param {string} rawJid — destination JID (not normalized)
1469
- * @param {object} guardOptions
1470
- * @param {string} pluginName
1630
+ * @param {WaContract} contract
1631
+ * @param {BotStore} store
1632
+ * @param {string} rawJid — destination JID (not normalized)
1633
+ * @param {object} guardOptions
1634
+ * @param {string} pluginName
1471
1635
  */
1472
- function buildPollApi(sock, store, rawJid, guardOptions, pluginName) {
1636
+ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
1473
1637
  if (!pollRegistry.has(pluginName))
1474
1638
  pollRegistry.set(pluginName, new Map());
1475
1639
  const registry = pollRegistry.get(pluginName);
@@ -1479,6 +1643,11 @@ function buildPollApi(sock, store, rawJid, guardOptions, pluginName) {
1479
1643
  // with no dedup — so we must keep only the latest entry per voter ourselves,
1480
1644
  // or retracted/changed votes keep counting alongside the new one.
1481
1645
  const pollVotesByCreationId = new Map();
1646
+ // Poll decryption needs the raw Baileys socket (sock.ev for
1647
+ // messages.upsert, sock.user for self-JID shape), so we lift it off the
1648
+ // contract here. The WeakMap key is the socket itself so old listeners
1649
+ // fall off automatically when the socket is replaced (reconnect).
1650
+ const sock = rawSocketOf(contract);
1482
1651
  let boundPlugins = pollListenersBySocket.get(sock);
1483
1652
  if (!boundPlugins) {
1484
1653
  boundPlugins = new Set();
@@ -1592,7 +1761,7 @@ function buildPollApi(sock, store, rawJid, guardOptions, pluginName) {
1592
1761
  * @returns {Promise<PollHandle>}
1593
1762
  */
1594
1763
  async create(question, options, opts = {}) {
1595
- const sender = makeSender(sock, store, rawJid, null, { cooldown, jitter });
1764
+ const sender = makeSender(contract, store, rawJid, null, { cooldown, jitter });
1596
1765
  const handlePromise = sender.poll(question, options, opts);
1597
1766
  const rawMsg = await handlePromise.rawPromise;
1598
1767
  if (!rawMsg)
@@ -1602,7 +1771,12 @@ function buildPollApi(sock, store, rawJid, guardOptions, pluginName) {
1602
1771
  // Ensure the poll message is in the store before any vote arrives —
1603
1772
  // messages.upsert isn't guaranteed to fire (or land in time) for the
1604
1773
  // bot's own sent messages, which previously dropped every vote.
1605
- const remoteJid = rawMsg.key.remoteJid;
1774
+ // The store still keeps raw WAMessages keyed by jid+id (the
1775
+ // poll-decryption path below reads the raw envelope off the store),
1776
+ // so push a raw entry back in here too. refToBotMessage keeps the
1777
+ // driver-neutral envelope current; rawStore keeps the Baileys shape
1778
+ // the decryption helper still needs.
1779
+ const remoteJid = rawMsg.chatId;
1606
1780
  if (remoteJid) {
1607
1781
  if (!store.messages.has(remoteJid))
1608
1782
  store.messages.set(remoteJid, new Map());
@@ -1622,8 +1796,8 @@ function buildPollApi(sock, store, rawJid, guardOptions, pluginName) {
1622
1796
  };
1623
1797
  }
1624
1798
  // ── Base API (shared between setup and runtime) ───────────────────────────────
1625
- function buildBaseApi(sock, store, pluginRegistry, pluginName) {
1626
- const botJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null;
1799
+ function buildBaseApi(contract, store, pluginRegistry, pluginName) {
1800
+ const botJid = contract.me().id ? jidNormalizedUser(contract.me().id) : null;
1627
1801
  if (!botJid)
1628
1802
  logger.warn("[pluginApi] botId is null — socket may not be ready yet.");
1629
1803
  return {
@@ -1636,7 +1810,7 @@ function buildBaseApi(sock, store, pluginRegistry, pluginName) {
1636
1810
  scheduler: buildSchedulerApi(pluginName),
1637
1811
  plugins: buildPluginsApi(pluginRegistry),
1638
1812
  chats: buildChatsApi(store),
1639
- contacts: buildContactsApi(sock, store, botJid),
1813
+ contacts: buildContactsApi(contract, store, botJid),
1640
1814
  storage: buildStorageApi(pluginName),
1641
1815
  botId: botJid,
1642
1816
  };
@@ -1646,19 +1820,19 @@ function buildBaseApi(sock, store, pluginRegistry, pluginName) {
1646
1820
  * Setup API — without message context.
1647
1821
  * Passed to plugin.setup(ctx) during initialization.
1648
1822
  *
1649
- * @param {WASocket} sock
1650
- * @param {WAStore} store
1651
- * @param {Map<string, any>} pluginRegistry
1652
- * @param {string} pluginName
1823
+ * @param {WaContract} contract
1824
+ * @param {BotStore} store
1825
+ * @param {Map<string, any>} pluginRegistry
1826
+ * @param {string} pluginName
1653
1827
  */
1654
- export function buildSetupApi(sock, store, pluginRegistry, pluginName) {
1655
- bindGroupMetaInvalidation(sock);
1828
+ export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
1829
+ bindGroupMetaInvalidation(contract);
1656
1830
  return {
1657
- ...buildBaseApi(sock, store, pluginRegistry, pluginName),
1658
- ...buildSetupSendApi(sock, store),
1659
- admin: buildAdminApi(sock, null),
1660
- events: buildEventsApi(sock, pluginName),
1661
- me: buildMeApi(sock),
1831
+ ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1832
+ ...buildSetupSendApi(contract, store),
1833
+ admin: buildAdminApi(contract, null),
1834
+ events: buildEventsApi(contract, pluginName),
1835
+ me: buildMeApi(contract),
1662
1836
  settings: { global: buildSettingsApi(pluginName, "_global").global },
1663
1837
  };
1664
1838
  }
@@ -1668,44 +1842,30 @@ export function buildSetupApi(sock, store, pluginRegistry, pluginName) {
1668
1842
  * Passed to plugin.default(ctx) on every message.
1669
1843
  *
1670
1844
  * @param {object} params
1671
- * @param {WAProtoMsg} params.msg
1845
+ * @param {BotMessage} params.msg
1672
1846
  * @param {WAChat} params.chat
1673
- * @param {WASocket} params.sock
1674
- * @param {WAStore} params.store
1847
+ * @param {WaContract} params.contract
1848
+ * @param {BotStore} params.store
1675
1849
  * @param {Map} params.pluginRegistry
1676
1850
  * @param {string} params.pluginName
1677
1851
  * @param {object} [params.guardOptions]
1678
1852
  */
1679
- export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, guardOptions = {}, }) {
1853
+ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginName, guardOptions = {}, }) {
1680
1854
  const prefix = CONFIG.CMD_PREFIX;
1681
1855
  const body = getMsgBody(msg);
1682
1856
  const rawArgs = body.trim().split(/\s+/);
1683
1857
  const first = rawArgs[0]?.toLowerCase() ?? "";
1684
1858
  const hasPrefix = first.startsWith(prefix);
1685
1859
  const command = hasPrefix ? first.slice(prefix.length) : "";
1686
- const rawJid = msg.key.remoteJid ?? "";
1860
+ const rawJid = msg.chatId;
1687
1861
  const normJid = normalizeJid(rawJid);
1688
1862
  const sender = getMsgSender(msg, store);
1689
1863
  const cooldown = (guardOptions.cooldown ?? true);
1690
1864
  const jitter = (guardOptions.jitter ?? true);
1691
- bindGroupMetaInvalidation(sock);
1692
- // Sender for quoted messages
1693
- const contextInfo = getContextInfo(msg);
1694
- const quotedRaw = contextInfo?.quotedMessage
1695
- ? {
1696
- key: {
1697
- remoteJid: rawJid,
1698
- fromMe: false,
1699
- id: contextInfo.stanzaId ?? undefined,
1700
- participant: contextInfo.participant ?? undefined,
1701
- },
1702
- message: contextInfo.quotedMessage,
1703
- pushName: null,
1704
- }
1705
- : null;
1865
+ bindGroupMetaInvalidation(contract);
1706
1866
  // Group participant JIDs come back in whatever addressing mode the group
1707
1867
  // uses (@lid or @s.whatsapp.net/@c.us) — same issue as poll vote decryption.
1708
- // "sender" and "sock.user.id" are usually PN-normalized, so a straight
1868
+ // "sender" and the bot's own JID are usually PN-normalized, so a straight
1709
1869
  // `=== ` against an @lid participant list silently never matches, even
1710
1870
  // when the person genuinely is an admin. Compare every known form
1711
1871
  // (raw + store-resolved) on both sides instead of trusting a single shape.
@@ -1723,10 +1883,10 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1723
1883
  return false;
1724
1884
  }
1725
1885
  return {
1726
- ...buildBaseApi(sock, store, pluginRegistry, pluginName),
1727
- ...buildSendApi(sock, store, rawJid, guardOptions),
1886
+ ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1887
+ ...buildSendApi(contract, store, rawJid, guardOptions),
1728
1888
  // ── msg ──────────────────────────────────────────────────────────────────
1729
- msg: buildMessageContext(msg, sock, store, { cooldown, jitter }),
1889
+ msg: buildMessageContext(msg, contract, store, { cooldown, jitter }),
1730
1890
  // ── chat ─────────────────────────────────────────────────────────────────
1731
1891
  chat: {
1732
1892
  id: normJid,
@@ -1741,7 +1901,7 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1741
1901
  get history() {
1742
1902
  const chatMsgs = store.messages.get(rawJid);
1743
1903
  const entries = chatMsgs
1744
- ? [...chatMsgs.values()].map((m) => buildMessageContext(m, sock, store, { cooldown: false, jitter: false }))
1904
+ ? [...chatMsgs.values()].map((m) => buildMessageContext(toBotMessage(m), contract, store, { cooldown: false, jitter: false }))
1745
1905
  : [];
1746
1906
  return makeHistoryArray(entries, store);
1747
1907
  },
@@ -1754,7 +1914,7 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1754
1914
  if (!chat.isGroup)
1755
1915
  return [];
1756
1916
  try {
1757
- const meta = await getGroupMetadataCached(sock, rawJid);
1917
+ const meta = await getGroupMetadataCached(contract, rawJid);
1758
1918
  return meta.participants.map(p => ({
1759
1919
  id: normalizeJid(p.id),
1760
1920
  isAdmin: p.admin === "admin" || p.admin === "superadmin",
@@ -1774,7 +1934,7 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1774
1934
  if (!chat.isGroup)
1775
1935
  return false;
1776
1936
  try {
1777
- const meta = await getGroupMetadataCached(sock, rawJid);
1937
+ const meta = await getGroupMetadataCached(contract, rawJid);
1778
1938
  return meta.participants.some(p => matchesParticipant([contactId], p.id) && (p.admin === "admin" || p.admin === "superadmin"));
1779
1939
  }
1780
1940
  catch {
@@ -1789,8 +1949,11 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1789
1949
  if (!chat.isGroup)
1790
1950
  return false;
1791
1951
  try {
1792
- const meta = await getGroupMetadataCached(sock, rawJid);
1793
- const rawSenderParticipant = msg.key.participant ?? msg.key.remoteJid ?? "";
1952
+ const meta = await getGroupMetadataCached(contract, rawJid);
1953
+ // The adapter's pre-extracted participant fields give us both the
1954
+ // LID and PN forms of the sender; match either against the group's
1955
+ // own participant list.
1956
+ const rawSenderParticipant = msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? msg.chatId ?? "";
1794
1957
  return meta.participants.some(p => matchesParticipant([sender, rawSenderParticipant], p.id) && (p.admin === "admin" || p.admin === "superadmin"));
1795
1958
  }
1796
1959
  catch {
@@ -1804,12 +1967,13 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1804
1967
  async isBotAdmin() {
1805
1968
  if (!chat.isGroup)
1806
1969
  return false;
1807
- const botLid = sock.user?.lid;
1808
- const botCandidates = [sock.user?.id, botLid];
1970
+ const me = contract.me();
1971
+ const botLid = me?.lid;
1972
+ const botCandidates = [me.id, botLid];
1809
1973
  if (!botCandidates.some(Boolean))
1810
1974
  return false;
1811
1975
  try {
1812
- const meta = await getGroupMetadataCached(sock, rawJid);
1976
+ const meta = await getGroupMetadataCached(contract, rawJid);
1813
1977
  return meta.participants.some(p => matchesParticipant(botCandidates, p.id) && (p.admin === "admin" || p.admin === "superadmin"));
1814
1978
  }
1815
1979
  catch {
@@ -1822,31 +1986,27 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1822
1986
  },
1823
1987
  },
1824
1988
  // ── admin ─────────────────────────────────────────────────────────────────
1825
- admin: buildAdminApi(sock, rawJid),
1989
+ admin: buildAdminApi(contract, rawJid),
1826
1990
  // ── me ────────────────────────────────────────────────────────────────────
1827
- me: buildMeApi(sock),
1991
+ me: buildMeApi(contract),
1828
1992
  // ── poll ──────────────────────────────────────────────────────────────────
1829
- poll: buildPollApi(sock, store, rawJid, guardOptions, pluginName),
1993
+ poll: buildPollApi(contract, store, rawJid, guardOptions, pluginName),
1830
1994
  // ── settings ──────────────────────────────────────────────────────────────
1831
1995
  settings: buildSettingsApi(pluginName, normJid),
1832
1996
  // ── isolated platform contexts ────────────────────────────────────────────
1833
1997
  wa: {
1834
- sock,
1998
+ contract,
1835
1999
  store,
1836
2000
  msg,
1837
2001
  downloadMedia: async (opts = {}) => {
1838
2002
  try {
1839
- const buffer = await downloadMediaMessage(msg, "buffer", {});
1840
- if (!buffer || !Buffer.isBuffer(buffer))
2003
+ const result = await contract.downloadMedia(msg, opts);
2004
+ if (!result)
1841
2005
  return null;
1842
- const isAnimatedSticker = !!unwrap(msg)?.stickerMessage?.isAnimated;
1843
- if (opts.asMp4 && isAnimatedSticker) {
1844
- const mp4 = await stickerToMp4(buffer);
1845
- return { mimetype: "video/mp4", data: mp4.toString("base64") };
1846
- }
1847
- return { mimetype: getMsgMimetype(msg), data: buffer.toString("base64") };
2006
+ return { mimetype: result.mimetype, data: result.data.toString("base64") };
1848
2007
  }
1849
- catch {
2008
+ catch (err) {
2009
+ logger.warn(`[whatsapp] downloadMedia failed: ${err.message}`);
1850
2010
  return null;
1851
2011
  }
1852
2012
  }