@manybot/manybot 5.5.3 → 5.6.0

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