@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.
- package/README.md +15 -1
- package/dist/client/cache.js +1 -1
- package/dist/client/store.js +9 -0
- package/dist/config.js +230 -12
- package/dist/drivers/baileys/adapter.js +629 -0
- package/dist/drivers/{whatsapp → baileys}/api/index.js +553 -393
- package/dist/drivers/baileys/index.js +594 -0
- package/dist/drivers/{whatsapp → baileys}/loginPrompt.js +2 -0
- package/dist/drivers/{whatsapp → baileys}/messageHandler.js +46 -16
- package/dist/drivers/{whatsapp → baileys}/sdk/baileysSock.js +1 -30
- package/dist/drivers/jid.js +31 -0
- package/dist/drivers/types.js +14 -0
- package/dist/drivers/whatsmeow/client.js +252 -0
- package/dist/drivers/whatsmeow/index.js +79 -0
- package/dist/drivers/whatsmeow/installer.js +86 -0
- package/dist/drivers/whatsmeow/supervisor.js +328 -0
- package/dist/drivers/whatsmeow/whatsmeow.proto +64 -0
- package/dist/i18n/index.js +8 -12
- package/dist/kernel/alerts.js +190 -0
- package/dist/kernel/contactAutoSave.js +200 -0
- package/dist/kernel/driverManager.js +117 -0
- package/dist/kernel/pluginApi.js +25 -7
- package/dist/kernel/pluginLoader.js +12 -12
- package/dist/kernel/sendFallbackGuard.js +183 -0
- package/dist/kernel/sendGuard.js +143 -33
- package/dist/kernel/statusServer.js +39 -0
- package/dist/kernel/updateCheck.js +88 -0
- package/dist/kernel/waContract.js +16 -0
- package/dist/locales/en.json +16 -1
- package/dist/locales/es.json +16 -1
- package/dist/locales/pt.json +16 -1
- package/dist/main.js +100 -5
- package/dist/types.js +18 -11
- package/package.json +6 -8
- package/dist/core/adapter.js +0 -12
- package/dist/core/capabilities.js +0 -16
- package/dist/core/types.js +0 -6
- package/dist/drivers/index.js +0 -14
- package/dist/drivers/whatsapp/adapter.js +0 -7
- package/dist/drivers/whatsapp/index.js +0 -382
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* drivers/
|
|
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,
|
|
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 {
|
|
39
|
-
// ──
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return "image";
|
|
70
|
-
|
|
71
|
-
return "
|
|
72
|
-
|
|
73
|
-
return "
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
236
|
+
* Build a WAChat adapter from a BotMessage + store.
|
|
157
237
|
* Exposed for use in messageHandler.ts.
|
|
158
238
|
*
|
|
159
|
-
* @param {
|
|
160
|
-
* @param {
|
|
161
|
-
* @param {
|
|
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,
|
|
165
|
-
const rawJid = msg.
|
|
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(
|
|
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
|
|
381
|
-
* a profile only for WhatsApp Business accounts, undefined otherwise.
|
|
382
|
-
* @param {string}
|
|
383
|
-
* @param {
|
|
384
|
-
* @param {string|null}
|
|
385
|
-
* @param {
|
|
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,
|
|
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 &&
|
|
477
|
+
if (!isWAAccount && contract && !jid.endsWith("@g.us")) {
|
|
398
478
|
try {
|
|
399
|
-
const results = await
|
|
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 (
|
|
493
|
+
if (contract && !jid.endsWith("@g.us")) {
|
|
414
494
|
try {
|
|
415
|
-
isBusiness = Boolean(await
|
|
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(
|
|
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).
|
|
485
|
-
// the
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
654
|
-
|
|
746
|
+
? (() => {
|
|
747
|
+
const decoded = decodeContent(contextInfo.quotedMessage);
|
|
748
|
+
return {
|
|
749
|
+
id: contextInfo.stanzaId ?? "",
|
|
750
|
+
chatId: msg.chatId,
|
|
655
751
|
fromMe: false,
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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.
|
|
665
|
-
timestamp:
|
|
783
|
+
id: msg.id,
|
|
784
|
+
timestamp: msg.timestamp || 0,
|
|
666
785
|
body,
|
|
667
786
|
type: getMsgType(msg),
|
|
668
|
-
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
|
-
|
|
681
|
-
|
|
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
|
|
804
|
+
const raw = rawMsgOf(msg, store);
|
|
805
|
+
const isAnimatedSticker = !!(raw?.message?.stickerMessage?.isAnimated);
|
|
684
806
|
if (opts.asMp4 && isAnimatedSticker) {
|
|
685
|
-
const mp4 = await stickerToMp4(
|
|
807
|
+
const mp4 = await stickerToMp4(result.data);
|
|
686
808
|
return { mimetype: "video/mp4", data: mp4.toString("base64") };
|
|
687
809
|
}
|
|
688
|
-
return { mimetype:
|
|
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: !!(
|
|
817
|
+
hasReply: !!(msg.quotedKey),
|
|
695
818
|
async getReply() {
|
|
696
819
|
if (!quotedRaw)
|
|
697
820
|
return null;
|
|
698
|
-
return buildMessageContext(quotedRaw,
|
|
821
|
+
return buildMessageContext(quotedRaw, contract, store, { cooldown: false, jitter: false });
|
|
699
822
|
},
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
845
|
+
if (!msg.fromMe) {
|
|
711
846
|
throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
|
|
712
847
|
}
|
|
713
|
-
|
|
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.
|
|
727
|
-
?? store.contacts[denormalizeJid(store.resolveJid(msg.
|
|
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,
|
|
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
|
-
|
|
888
|
+
_contract;
|
|
741
889
|
_store;
|
|
742
890
|
_jid = null;
|
|
743
891
|
_guardOptions;
|
|
744
892
|
rawPromise;
|
|
745
|
-
constructor(promise,
|
|
893
|
+
constructor(promise, contract, store, guardOptions) {
|
|
746
894
|
this.rawPromise = promise;
|
|
747
|
-
this.
|
|
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.
|
|
755
|
-
return buildMessageContext(msg,
|
|
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.
|
|
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
|
|
933
|
+
if (!msg)
|
|
786
934
|
return;
|
|
787
|
-
const jid = msg.key.remoteJid;
|
|
788
935
|
if (forEveryone) {
|
|
789
|
-
|
|
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
|
|
947
|
+
if (!msg)
|
|
796
948
|
return;
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
959
|
+
if (!msg)
|
|
805
960
|
return;
|
|
806
|
-
if (!msg.
|
|
961
|
+
if (!msg.fromMe) {
|
|
807
962
|
throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
|
|
808
963
|
}
|
|
809
|
-
|
|
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 {
|
|
940
|
-
* @param {
|
|
941
|
-
* @param {string}
|
|
942
|
-
* @param {
|
|
943
|
-
* @param {object}
|
|
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(
|
|
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
|
-
|
|
952
|
-
|
|
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
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
|
|
972
|
-
|
|
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
|
|
982
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1162
|
+
const quotedRef = await resolveQuoted();
|
|
983
1163
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
984
|
-
await simulateState(
|
|
1164
|
+
await simulateState(contract, jid, mediaDuration(caption), "typing");
|
|
985
1165
|
const buffer = await resolveMediaBuffer(source);
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
1000
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1180
|
+
const quotedRef = await resolveQuoted();
|
|
1001
1181
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1002
|
-
await simulateState(
|
|
1182
|
+
await simulateState(contract, jid, mediaDuration(caption), "typing");
|
|
1003
1183
|
const buffer = await resolveMediaBuffer(source);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
|
1026
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1206
|
+
const quotedRef = await resolveQuoted();
|
|
1027
1207
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1028
|
-
await simulateState(
|
|
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
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1045
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1227
|
+
const quotedRef = await resolveQuoted();
|
|
1046
1228
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1047
|
-
await simulateState(
|
|
1229
|
+
await simulateState(contract, jid, mediaDuration(), "recording");
|
|
1048
1230
|
const buffer = await resolveMediaBuffer(source);
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
|
1060
|
-
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
1242
|
+
const quotedRef = await resolveQuoted();
|
|
1061
1243
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1062
|
-
await simulateState(
|
|
1244
|
+
await simulateState(contract, jid, mediaDuration(), "typing");
|
|
1063
1245
|
const buffer = await resolveMediaBuffer(source);
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
|
1070
|
-
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
1254
|
+
const quotedRef = await resolveQuoted();
|
|
1071
1255
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1072
|
-
await simulateState(
|
|
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
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
|
1098
|
-
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
1280
|
+
const quotedRef = await resolveQuoted();
|
|
1099
1281
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
})(),
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1317
|
+
function buildSetupSendApi(contract, store) {
|
|
1136
1318
|
return {
|
|
1137
1319
|
send: {
|
|
1138
|
-
to: (targetJid) => makeSender(
|
|
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,
|
|
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 {
|
|
1159
|
-
* @param {string}
|
|
1341
|
+
* @param {WaContract} contract
|
|
1342
|
+
* @param {string} pluginName
|
|
1160
1343
|
*/
|
|
1161
|
-
function buildEventsApi(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
1278
|
-
* WhatsApp rejected some (or all) of the requested participants —
|
|
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 `
|
|
1300
|
-
* opaque
|
|
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
|
|
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
|
|
1518
|
+
return contract.groupUpdateSubject(chatJid, name);
|
|
1355
1519
|
},
|
|
1356
1520
|
/** @param {string} text */
|
|
1357
1521
|
async setDescription(text) {
|
|
1358
1522
|
requireChat();
|
|
1359
|
-
return
|
|
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
|
|
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
|
|
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
|
|
1540
|
+
return contract.groupRevokeInvite(chatJid);
|
|
1377
1541
|
},
|
|
1378
1542
|
};
|
|
1379
1543
|
}
|
|
1380
1544
|
// ── Me API ────────────────────────────────────────────────────────────────────
|
|
1381
|
-
/** @param {
|
|
1382
|
-
function buildMeApi(
|
|
1545
|
+
/** @param {WaContract} contract */
|
|
1546
|
+
function buildMeApi(contract) {
|
|
1383
1547
|
return {
|
|
1384
1548
|
/** @param {string} name */
|
|
1385
1549
|
async setName(name) {
|
|
1386
|
-
return
|
|
1550
|
+
return contract.updateProfileName(name);
|
|
1387
1551
|
},
|
|
1388
1552
|
/** @param {string} text */
|
|
1389
1553
|
async setAbout(text) {
|
|
1390
|
-
return
|
|
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 =
|
|
1396
|
-
return
|
|
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.
|
|
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 {
|
|
1467
|
-
* @param {
|
|
1468
|
-
* @param {string}
|
|
1469
|
-
* @param {object}
|
|
1470
|
-
* @param {string}
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1626
|
-
const botJid =
|
|
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(
|
|
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 {
|
|
1650
|
-
* @param {
|
|
1651
|
-
* @param {Map<string, any>}
|
|
1652
|
-
* @param {string}
|
|
1823
|
+
* @param {WaContract} contract
|
|
1824
|
+
* @param {BotStore} store
|
|
1825
|
+
* @param {Map<string, any>} pluginRegistry
|
|
1826
|
+
* @param {string} pluginName
|
|
1653
1827
|
*/
|
|
1654
|
-
export function buildSetupApi(
|
|
1655
|
-
bindGroupMetaInvalidation(
|
|
1828
|
+
export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
|
|
1829
|
+
bindGroupMetaInvalidation(contract);
|
|
1656
1830
|
return {
|
|
1657
|
-
...buildBaseApi(
|
|
1658
|
-
...buildSetupSendApi(
|
|
1659
|
-
admin: buildAdminApi(
|
|
1660
|
-
events: buildEventsApi(
|
|
1661
|
-
me: buildMeApi(
|
|
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 {
|
|
1845
|
+
* @param {BotMessage} params.msg
|
|
1672
1846
|
* @param {WAChat} params.chat
|
|
1673
|
-
* @param {
|
|
1674
|
-
* @param {
|
|
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,
|
|
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.
|
|
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(
|
|
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
|
|
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(
|
|
1727
|
-
...buildSendApi(
|
|
1886
|
+
...buildBaseApi(contract, store, pluginRegistry, pluginName),
|
|
1887
|
+
...buildSendApi(contract, store, rawJid, guardOptions),
|
|
1728
1888
|
// ── msg ──────────────────────────────────────────────────────────────────
|
|
1729
|
-
msg: buildMessageContext(msg,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
1793
|
-
|
|
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
|
|
1808
|
-
const
|
|
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(
|
|
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(
|
|
1989
|
+
admin: buildAdminApi(contract, rawJid),
|
|
1826
1990
|
// ── me ────────────────────────────────────────────────────────────────────
|
|
1827
|
-
me: buildMeApi(
|
|
1991
|
+
me: buildMeApi(contract),
|
|
1828
1992
|
// ── poll ──────────────────────────────────────────────────────────────────
|
|
1829
|
-
poll: buildPollApi(
|
|
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
|
-
|
|
1998
|
+
contract,
|
|
1835
1999
|
store,
|
|
1836
2000
|
msg,
|
|
1837
2001
|
downloadMedia: async (opts = {}) => {
|
|
1838
2002
|
try {
|
|
1839
|
-
const
|
|
1840
|
-
if (!
|
|
2003
|
+
const result = await contract.downloadMedia(msg, opts);
|
|
2004
|
+
if (!result)
|
|
1841
2005
|
return null;
|
|
1842
|
-
|
|
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
|
}
|