@manybot/manybot 5.5.4 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/client/cache.js +1 -1
- package/dist/client/store.js +9 -0
- package/dist/config.js +230 -12
- package/dist/drivers/baileys/adapter.js +556 -0
- package/dist/drivers/{whatsapp → baileys}/api/index.js +533 -386
- package/dist/drivers/baileys/index.js +560 -0
- package/dist/drivers/{whatsapp → baileys}/loginPrompt.js +2 -0
- package/dist/drivers/{whatsapp → baileys}/messageHandler.js +46 -16
- package/dist/drivers/{whatsapp → baileys}/sdk/baileysSock.js +0 -29
- package/dist/drivers/jid.js +31 -0
- package/dist/drivers/types.js +14 -0
- package/dist/drivers/whatsmeow/client.js +203 -0
- package/dist/drivers/whatsmeow/index.js +79 -0
- package/dist/drivers/whatsmeow/installer.js +70 -0
- package/dist/drivers/whatsmeow/supervisor.js +309 -0
- package/dist/drivers/whatsmeow/whatsmeow.proto +64 -0
- package/dist/i18n/index.js +8 -12
- package/dist/kernel/alerts.js +190 -0
- package/dist/kernel/contactAutoSave.js +200 -0
- package/dist/kernel/driverManager.js +117 -0
- package/dist/kernel/pluginApi.js +25 -7
- package/dist/kernel/pluginLoader.js +12 -12
- package/dist/kernel/sendFallbackGuard.js +173 -0
- package/dist/kernel/sendGuard.js +143 -33
- package/dist/kernel/statusServer.js +39 -0
- package/dist/kernel/updateCheck.js +88 -0
- package/dist/kernel/waContract.js +16 -0
- package/dist/locales/en.json +15 -1
- package/dist/locales/es.json +15 -1
- package/dist/locales/pt.json +15 -1
- package/dist/main.js +77 -5
- package/dist/types.js +18 -11
- package/package.json +6 -8
- package/dist/core/adapter.js +0 -12
- package/dist/core/capabilities.js +0 -16
- package/dist/core/types.js +0 -6
- package/dist/drivers/index.js +0 -14
- package/dist/drivers/whatsapp/adapter.js +0 -7
- package/dist/drivers/whatsapp/index.js +0 -382
|
@@ -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,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,
|
|
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 {
|
|
39
|
-
// ──
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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";
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
235
|
+
* Build a WAChat adapter from a BotMessage + store.
|
|
157
236
|
* Exposed for use in messageHandler.ts.
|
|
158
237
|
*
|
|
159
|
-
* @param {
|
|
160
|
-
* @param {
|
|
161
|
-
* @param {
|
|
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,
|
|
165
|
-
const rawJid = msg.
|
|
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(
|
|
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
|
|
381
|
-
* a profile only for WhatsApp Business accounts, undefined otherwise.
|
|
382
|
-
* @param {string}
|
|
383
|
-
* @param {
|
|
384
|
-
* @param {string|null}
|
|
385
|
-
* @param {
|
|
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,
|
|
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 &&
|
|
476
|
+
if (!isWAAccount && contract && !jid.endsWith("@g.us")) {
|
|
398
477
|
try {
|
|
399
|
-
const results = await
|
|
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 (
|
|
492
|
+
if (contract && !jid.endsWith("@g.us")) {
|
|
414
493
|
try {
|
|
415
|
-
isBusiness = Boolean(await
|
|
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(
|
|
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).
|
|
485
|
-
// the
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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.
|
|
665
|
-
timestamp:
|
|
751
|
+
id: msg.id,
|
|
752
|
+
timestamp: msg.timestamp || 0,
|
|
666
753
|
body,
|
|
667
754
|
type: getMsgType(msg),
|
|
668
|
-
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
|
-
|
|
681
|
-
|
|
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
|
|
772
|
+
const raw = rawMsgOf(msg, store);
|
|
773
|
+
const isAnimatedSticker = !!(raw?.message?.stickerMessage?.isAnimated);
|
|
684
774
|
if (opts.asMp4 && isAnimatedSticker) {
|
|
685
|
-
const mp4 = await stickerToMp4(
|
|
775
|
+
const mp4 = await stickerToMp4(result.data);
|
|
686
776
|
return { mimetype: "video/mp4", data: mp4.toString("base64") };
|
|
687
777
|
}
|
|
688
|
-
return { mimetype:
|
|
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: !!(
|
|
785
|
+
hasReply: !!(msg.quotedKey),
|
|
695
786
|
async getReply() {
|
|
696
787
|
if (!quotedRaw)
|
|
697
788
|
return null;
|
|
698
|
-
return buildMessageContext(quotedRaw,
|
|
789
|
+
return buildMessageContext(quotedRaw, contract, store, { cooldown: false, jitter: false });
|
|
699
790
|
},
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
813
|
+
if (!msg.fromMe) {
|
|
711
814
|
throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
|
|
712
815
|
}
|
|
713
|
-
|
|
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.
|
|
727
|
-
?? store.contacts[denormalizeJid(store.resolveJid(msg.
|
|
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,
|
|
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
|
-
|
|
856
|
+
_contract;
|
|
741
857
|
_store;
|
|
742
858
|
_jid = null;
|
|
743
859
|
_guardOptions;
|
|
744
860
|
rawPromise;
|
|
745
|
-
constructor(promise,
|
|
861
|
+
constructor(promise, contract, store, guardOptions) {
|
|
746
862
|
this.rawPromise = promise;
|
|
747
|
-
this.
|
|
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.
|
|
755
|
-
return buildMessageContext(msg,
|
|
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.
|
|
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
|
|
901
|
+
if (!msg)
|
|
786
902
|
return;
|
|
787
|
-
const jid = msg.key.remoteJid;
|
|
788
903
|
if (forEveryone) {
|
|
789
|
-
|
|
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
|
|
915
|
+
if (!msg)
|
|
796
916
|
return;
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
927
|
+
if (!msg)
|
|
805
928
|
return;
|
|
806
|
-
if (!msg.
|
|
929
|
+
if (!msg.fromMe) {
|
|
807
930
|
throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
|
|
808
931
|
}
|
|
809
|
-
|
|
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 {
|
|
940
|
-
* @param {
|
|
941
|
-
* @param {string}
|
|
942
|
-
* @param {
|
|
943
|
-
* @param {object}
|
|
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(
|
|
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
|
-
|
|
952
|
-
|
|
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
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
|
982
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1130
|
+
const quotedRef = await resolveQuoted();
|
|
983
1131
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
984
|
-
await simulateState(
|
|
1132
|
+
await simulateState(contract, jid, mediaDuration(caption), "typing");
|
|
985
1133
|
const buffer = await resolveMediaBuffer(source);
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
|
1000
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1148
|
+
const quotedRef = await resolveQuoted();
|
|
1001
1149
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1002
|
-
await simulateState(
|
|
1150
|
+
await simulateState(contract, jid, mediaDuration(caption), "typing");
|
|
1003
1151
|
const buffer = await resolveMediaBuffer(source);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
|
1026
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1174
|
+
const quotedRef = await resolveQuoted();
|
|
1027
1175
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1028
|
-
await simulateState(
|
|
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
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1045
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
1195
|
+
const quotedRef = await resolveQuoted();
|
|
1046
1196
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1047
|
-
await simulateState(
|
|
1197
|
+
await simulateState(contract, jid, mediaDuration(), "recording");
|
|
1048
1198
|
const buffer = await resolveMediaBuffer(source);
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
|
1060
|
-
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
1210
|
+
const quotedRef = await resolveQuoted();
|
|
1061
1211
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1062
|
-
await simulateState(
|
|
1212
|
+
await simulateState(contract, jid, mediaDuration(), "typing");
|
|
1063
1213
|
const buffer = await resolveMediaBuffer(source);
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
|
1070
|
-
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
1222
|
+
const quotedRef = await resolveQuoted();
|
|
1071
1223
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1072
|
-
await simulateState(
|
|
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
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
|
1098
|
-
const qOpts = quotedMsg ? { quoted: quotedMsg } : undefined;
|
|
1248
|
+
const quotedRef = await resolveQuoted();
|
|
1099
1249
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
})(),
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1285
|
+
function buildSetupSendApi(contract, store) {
|
|
1136
1286
|
return {
|
|
1137
1287
|
send: {
|
|
1138
|
-
to: (targetJid) => makeSender(
|
|
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,
|
|
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 {
|
|
1159
|
-
* @param {string}
|
|
1309
|
+
* @param {WaContract} contract
|
|
1310
|
+
* @param {string} pluginName
|
|
1160
1311
|
*/
|
|
1161
|
-
function buildEventsApi(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
1278
|
-
* WhatsApp rejected some (or all) of the requested participants —
|
|
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 `
|
|
1300
|
-
* opaque
|
|
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
|
|
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
|
|
1486
|
+
return contract.groupUpdateSubject(chatJid, name);
|
|
1355
1487
|
},
|
|
1356
1488
|
/** @param {string} text */
|
|
1357
1489
|
async setDescription(text) {
|
|
1358
1490
|
requireChat();
|
|
1359
|
-
return
|
|
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
|
|
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
|
|
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
|
|
1508
|
+
return contract.groupRevokeInvite(chatJid);
|
|
1377
1509
|
},
|
|
1378
1510
|
};
|
|
1379
1511
|
}
|
|
1380
1512
|
// ── Me API ────────────────────────────────────────────────────────────────────
|
|
1381
|
-
/** @param {
|
|
1382
|
-
function buildMeApi(
|
|
1513
|
+
/** @param {WaContract} contract */
|
|
1514
|
+
function buildMeApi(contract) {
|
|
1383
1515
|
return {
|
|
1384
1516
|
/** @param {string} name */
|
|
1385
1517
|
async setName(name) {
|
|
1386
|
-
return
|
|
1518
|
+
return contract.updateProfileName(name);
|
|
1387
1519
|
},
|
|
1388
1520
|
/** @param {string} text */
|
|
1389
1521
|
async setAbout(text) {
|
|
1390
|
-
return
|
|
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 =
|
|
1396
|
-
return
|
|
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.
|
|
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 {
|
|
1467
|
-
* @param {
|
|
1468
|
-
* @param {string}
|
|
1469
|
-
* @param {object}
|
|
1470
|
-
* @param {string}
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1626
|
-
const botJid =
|
|
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(
|
|
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 {
|
|
1650
|
-
* @param {
|
|
1651
|
-
* @param {Map<string, any>}
|
|
1652
|
-
* @param {string}
|
|
1791
|
+
* @param {WaContract} contract
|
|
1792
|
+
* @param {BotStore} store
|
|
1793
|
+
* @param {Map<string, any>} pluginRegistry
|
|
1794
|
+
* @param {string} pluginName
|
|
1653
1795
|
*/
|
|
1654
|
-
export function buildSetupApi(
|
|
1655
|
-
bindGroupMetaInvalidation(
|
|
1796
|
+
export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
|
|
1797
|
+
bindGroupMetaInvalidation(contract);
|
|
1656
1798
|
return {
|
|
1657
|
-
...buildBaseApi(
|
|
1658
|
-
...buildSetupSendApi(
|
|
1659
|
-
admin: buildAdminApi(
|
|
1660
|
-
events: buildEventsApi(
|
|
1661
|
-
me: buildMeApi(
|
|
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 {
|
|
1813
|
+
* @param {BotMessage} params.msg
|
|
1672
1814
|
* @param {WAChat} params.chat
|
|
1673
|
-
* @param {
|
|
1674
|
-
* @param {
|
|
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,
|
|
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.
|
|
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(
|
|
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
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
|
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(
|
|
1727
|
-
...buildSendApi(
|
|
1873
|
+
...buildBaseApi(contract, store, pluginRegistry, pluginName),
|
|
1874
|
+
...buildSendApi(contract, store, rawJid, guardOptions),
|
|
1728
1875
|
// ── msg ──────────────────────────────────────────────────────────────────
|
|
1729
|
-
msg: buildMessageContext(msg,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
1793
|
-
|
|
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
|
|
1808
|
-
const
|
|
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(
|
|
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(
|
|
1976
|
+
admin: buildAdminApi(contract, rawJid),
|
|
1826
1977
|
// ── me ────────────────────────────────────────────────────────────────────
|
|
1827
|
-
me: buildMeApi(
|
|
1978
|
+
me: buildMeApi(contract),
|
|
1828
1979
|
// ── poll ──────────────────────────────────────────────────────────────────
|
|
1829
|
-
poll: buildPollApi(
|
|
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
|
-
|
|
1985
|
+
contract,
|
|
1835
1986
|
store,
|
|
1836
1987
|
msg,
|
|
1837
1988
|
downloadMedia: async (opts = {}) => {
|
|
1838
1989
|
try {
|
|
1839
|
-
const
|
|
1840
|
-
if (!
|
|
1990
|
+
const result = await contract.downloadMedia(msg, opts);
|
|
1991
|
+
if (!result)
|
|
1841
1992
|
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") };
|
|
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
|
}
|