miaw-core 1.2.6 → 1.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/CHANGELOG.md +101 -1
- package/README.md +4 -4
- package/bin/miaw-cli.ts +6 -0
- package/dist/cli/commands/commands-index.d.ts +1 -1
- package/dist/cli/commands/commands-index.d.ts.map +1 -1
- package/dist/cli/commands/commands-index.js +1 -1
- package/dist/cli/commands/commands-index.js.map +1 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +46 -3
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/send.d.ts +35 -0
- package/dist/cli/commands/send.d.ts.map +1 -1
- package/dist/cli/commands/send.js +83 -0
- package/dist/cli/commands/send.js.map +1 -1
- package/dist/cli/repl.d.ts +1 -0
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +3 -3
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/utils/session.d.ts +2 -0
- package/dist/cli/utils/session.d.ts.map +1 -1
- package/dist/cli/utils/session.js +1 -0
- package/dist/cli/utils/session.js.map +1 -1
- package/dist/client/MiawClient.d.ts +122 -1
- package/dist/client/MiawClient.d.ts.map +1 -1
- package/dist/client/MiawClient.js +538 -20
- package/dist/client/MiawClient.js.map +1 -1
- package/dist/handlers/MessageHandler.d.ts.map +1 -1
- package/dist/handlers/MessageHandler.js +11 -4
- package/dist/handlers/MessageHandler.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/baileys.d.ts +4 -1
- package/dist/types/baileys.d.ts.map +1 -1
- package/dist/types/baileys.js.map +1 -1
- package/dist/types/index.d.ts +131 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/proxy-agent.d.ts +35 -0
- package/dist/utils/proxy-agent.d.ts.map +1 -0
- package/dist/utils/proxy-agent.js +73 -0
- package/dist/utils/proxy-agent.js.map +1 -0
- package/docs/BAILEYS_MIGRATION_v7.md +2 -0
- package/docs/BAILEYS_VS_MIAW_COMPARISON.md +30 -29
- package/docs/CLI.md +7 -0
- package/docs/LID_RESOLUTION.md +60 -6
- package/docs/MIGRATION.md +39 -3
- package/docs/TEST_COVERAGE_ANALYSIS.md +3 -1
- package/docs/USAGE.md +173 -1
- package/package.json +20 -18
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import makeWASocket, { DisconnectReason, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, Browsers, downloadMediaMessage, jidNormalizedUser, } from "@whiskeysockets/baileys";
|
|
1
|
+
import makeWASocket, { DisconnectReason, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, Browsers, downloadMediaMessage, jidNormalizedUser, getAggregateVotesInPollMessage, } from "@whiskeysockets/baileys";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { createFilteredLogger } from "../utils/filtered-logger.js";
|
|
4
4
|
import { enableConsoleFilter, disableConsoleFilter, } from "../utils/console-filter.js";
|
|
@@ -9,6 +9,7 @@ import { MessageHandler } from "../handlers/MessageHandler.js";
|
|
|
9
9
|
import { TIMEOUTS } from "../constants/timeouts.js";
|
|
10
10
|
import { CACHE_CONFIG } from "../constants/cache.js";
|
|
11
11
|
import { validatePhoneNumber, validateJID, validateMessageText, validateGroupName, validatePhoneNumbers, } from "../utils/validation.js";
|
|
12
|
+
import { createProxyAgents, validateProxyConfig, } from "../utils/proxy-agent.js";
|
|
12
13
|
/**
|
|
13
14
|
* LRU Cache for LID to JID mappings
|
|
14
15
|
* Prevents unbounded memory growth while maintaining frequently used mappings
|
|
@@ -131,6 +132,11 @@ export class MiawClient extends EventEmitter {
|
|
|
131
132
|
qrScanTimeout: options.qrScanTimeout || TIMEOUTS.QR_SCAN_TIMEOUT,
|
|
132
133
|
connectionTimeout: options.connectionTimeout || TIMEOUTS.CONNECTION_TIMEOUT,
|
|
133
134
|
syncFullHistory: options.syncFullHistory !== false,
|
|
135
|
+
proxy: options.proxy,
|
|
136
|
+
agent: options.agent,
|
|
137
|
+
fetchAgent: options.fetchAgent,
|
|
138
|
+
usePairingCode: options.usePairingCode,
|
|
139
|
+
phoneNumber: options.phoneNumber,
|
|
134
140
|
};
|
|
135
141
|
// Use the initialized logger
|
|
136
142
|
this.logger = logger;
|
|
@@ -179,6 +185,8 @@ export class MiawClient extends EventEmitter {
|
|
|
179
185
|
this.authState = { creds: state.creds };
|
|
180
186
|
// Get latest Baileys version
|
|
181
187
|
const { version } = await fetchLatestBaileysVersion();
|
|
188
|
+
// Resolve proxy agents if configured
|
|
189
|
+
const proxyAgents = await this.resolveProxyAgents();
|
|
182
190
|
// Create socket
|
|
183
191
|
const debugMode = this.options.debug;
|
|
184
192
|
const logger = this.logger;
|
|
@@ -192,6 +200,9 @@ export class MiawClient extends EventEmitter {
|
|
|
192
200
|
},
|
|
193
201
|
browser: Browsers.macOS("Desktop"), // Desktop for more history
|
|
194
202
|
generateHighQualityLinkPreview: true,
|
|
203
|
+
// Auto-recreate signal sessions on retry and migrate PN<->LID sessions
|
|
204
|
+
// (Baileys rc13 default; set explicitly to document the LID reliance).
|
|
205
|
+
enableAutoSessionRecreation: true,
|
|
195
206
|
// Enable full history sync to populate stores (controlled by syncFullHistory option)
|
|
196
207
|
syncFullHistory: this.options.syncFullHistory,
|
|
197
208
|
fireInitQueries: true,
|
|
@@ -202,9 +213,15 @@ export class MiawClient extends EventEmitter {
|
|
|
202
213
|
}
|
|
203
214
|
return this.options.syncFullHistory;
|
|
204
215
|
},
|
|
216
|
+
// Proxy support: agent for WebSocket, fetchAgent for media HTTP requests
|
|
217
|
+
...(proxyAgents?.wsAgent ? { agent: proxyAgents.wsAgent } : {}),
|
|
218
|
+
...(proxyAgents?.fetchAgent ? { fetchAgent: proxyAgents.fetchAgent } : {}),
|
|
205
219
|
});
|
|
206
220
|
// Register event handlers
|
|
207
221
|
this.registerSocketEvents(saveCreds);
|
|
222
|
+
// Pairing-code auth (alternative to QR): on a fresh session, request an
|
|
223
|
+
// 8-char code for the configured phone number and emit it.
|
|
224
|
+
this.maybeRequestPairingCode(Boolean(state.creds.registered));
|
|
208
225
|
this.logger.info("Connection initiated");
|
|
209
226
|
}
|
|
210
227
|
catch (error) {
|
|
@@ -215,6 +232,77 @@ export class MiawClient extends EventEmitter {
|
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Request a pairing code when pairing-code auth is configured and the session
|
|
237
|
+
* is not yet registered. Non-blocking; emits `pairing_code` on success.
|
|
238
|
+
* @param registered - Whether the current session creds are already registered
|
|
239
|
+
*/
|
|
240
|
+
maybeRequestPairingCode(registered) {
|
|
241
|
+
if (!this.options.usePairingCode ||
|
|
242
|
+
!this.options.phoneNumber ||
|
|
243
|
+
registered ||
|
|
244
|
+
!this.socket) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.socket
|
|
248
|
+
.requestPairingCode(this.options.phoneNumber)
|
|
249
|
+
.then((code) => {
|
|
250
|
+
if (this.options.debug) {
|
|
251
|
+
this.logger.debug(`Pairing code: ${code}`);
|
|
252
|
+
}
|
|
253
|
+
this.emit("pairing_code", code);
|
|
254
|
+
})
|
|
255
|
+
.catch((err) => {
|
|
256
|
+
this.logger.error("Failed to request pairing code:", err);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Resolves proxy agents from configuration.
|
|
261
|
+
* Direct agent/fetchAgent options take priority over proxy config.
|
|
262
|
+
*/
|
|
263
|
+
async resolveProxyAgents() {
|
|
264
|
+
// Direct agent options take priority
|
|
265
|
+
if (this.options.agent || this.options.fetchAgent) {
|
|
266
|
+
return {
|
|
267
|
+
wsAgent: this.options.agent,
|
|
268
|
+
fetchAgent: this.options.fetchAgent,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// Create agents from proxy config
|
|
272
|
+
if (this.options.proxy) {
|
|
273
|
+
if (!validateProxyConfig(this.options.proxy)) {
|
|
274
|
+
throw new Error(`Invalid proxy configuration: ${typeof this.options.proxy === "string" ? this.options.proxy : this.options.proxy.url}`);
|
|
275
|
+
}
|
|
276
|
+
const agents = await createProxyAgents(this.options.proxy);
|
|
277
|
+
this.logger.info("Proxy agent initialized");
|
|
278
|
+
return agents;
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get current proxy configuration info (URL with credentials masked).
|
|
284
|
+
* Returns null if no proxy is configured.
|
|
285
|
+
*/
|
|
286
|
+
getProxyInfo() {
|
|
287
|
+
const proxyUrl = typeof this.options.proxy === "string"
|
|
288
|
+
? this.options.proxy
|
|
289
|
+
: this.options.proxy?.url;
|
|
290
|
+
if (!proxyUrl)
|
|
291
|
+
return null;
|
|
292
|
+
try {
|
|
293
|
+
const parsed = new URL(proxyUrl);
|
|
294
|
+
if (parsed.password) {
|
|
295
|
+
parsed.password = "****";
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
url: parsed.toString(),
|
|
299
|
+
protocol: parsed.protocol.replace(":", ""),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return { url: proxyUrl, protocol: "unknown" };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
218
306
|
/**
|
|
219
307
|
* Register socket event handlers
|
|
220
308
|
*/
|
|
@@ -224,8 +312,8 @@ export class MiawClient extends EventEmitter {
|
|
|
224
312
|
// Connection updates
|
|
225
313
|
this.socket.ev.on("connection.update", async (update) => {
|
|
226
314
|
const { connection, lastDisconnect, qr } = update;
|
|
227
|
-
// Handle QR code
|
|
228
|
-
if (qr) {
|
|
315
|
+
// Handle QR code (suppressed when authenticating via pairing code)
|
|
316
|
+
if (qr && !this.options.usePairingCode) {
|
|
229
317
|
this.updateConnectionState("qr_required");
|
|
230
318
|
this.emit("qr", qr);
|
|
231
319
|
}
|
|
@@ -314,14 +402,17 @@ export class MiawClient extends EventEmitter {
|
|
|
314
402
|
this.updateChatsStore(chats);
|
|
315
403
|
});
|
|
316
404
|
// History sync - populates contacts, chats, and messages from history
|
|
317
|
-
this.socket.ev.on("messaging-history.set", ({ chats, contacts, messages, isLatest, peerDataRequestSessionId }) => {
|
|
405
|
+
this.socket.ev.on("messaging-history.set", ({ chats, contacts, messages, lidPnMappings, isLatest, peerDataRequestSessionId }) => {
|
|
318
406
|
if (this.options.debug) {
|
|
319
407
|
this.logger.debug("\n========== MESSAGING HISTORY SYNC ==========");
|
|
320
|
-
this.logger.debug(`Contacts: ${contacts.length}, Chats: ${chats.length}, Messages: ${messages.length}`);
|
|
408
|
+
this.logger.debug(`Contacts: ${contacts.length}, Chats: ${chats.length}, Messages: ${messages.length}, LID maps: ${lidPnMappings?.length ?? 0}`);
|
|
321
409
|
this.logger.debug(`Is Latest: ${isLatest}, SessionId: ${peerDataRequestSessionId || 'none'}`);
|
|
322
410
|
this.logger.debug("============================================\n");
|
|
323
411
|
}
|
|
324
|
-
// Build LID mappings FIRST (before store population)
|
|
412
|
+
// Build LID mappings FIRST (before store population). Baileys rc13 hands
|
|
413
|
+
// us a dedicated, high-confidence LID<->PN array on history sync; ingest
|
|
414
|
+
// it before the contact/chat-derived mappings.
|
|
415
|
+
this.ingestLidPnMappings(lidPnMappings);
|
|
325
416
|
this.updateLidToJidMapping(contacts);
|
|
326
417
|
this.updateLidFromChats(chats);
|
|
327
418
|
// Update contacts from history
|
|
@@ -367,12 +458,14 @@ export class MiawClient extends EventEmitter {
|
|
|
367
458
|
this.handleProtocolMessage(msg, protocolMessage);
|
|
368
459
|
continue; // Don't process as regular message
|
|
369
460
|
}
|
|
370
|
-
// Build LID
|
|
371
|
-
//
|
|
372
|
-
//
|
|
461
|
+
// Build LID<->PN mapping from incoming message keys. Baileys rc13 keys
|
|
462
|
+
// carry the alternate JID (remoteJidAlt for DMs, participantAlt for
|
|
463
|
+
// groups): when the primary id is a privacy LID, the alt holds the phone
|
|
464
|
+
// JID (and vice versa). Capture whichever side is the @lid.
|
|
373
465
|
const msgKey = msg.key;
|
|
374
|
-
if (!msgKey.fromMe
|
|
375
|
-
this.
|
|
466
|
+
if (!msgKey.fromMe) {
|
|
467
|
+
this.captureLidPnPair(msgKey.remoteJid, msgKey.remoteJidAlt);
|
|
468
|
+
this.captureLidPnPair(msgKey.participant, msgKey.participantAlt);
|
|
376
469
|
}
|
|
377
470
|
const normalized = MessageHandler.normalize({ messages: [msg], type: "notify" }, this.logger);
|
|
378
471
|
if (normalized) {
|
|
@@ -400,6 +493,25 @@ export class MiawClient extends EventEmitter {
|
|
|
400
493
|
}
|
|
401
494
|
}
|
|
402
495
|
}
|
|
496
|
+
// Native-store fallback (Baileys rc13): if the local cache couldn't
|
|
497
|
+
// resolve the sender's @lid, ask the signal repository's LID store.
|
|
498
|
+
// resolvePnFromNativeStore returns a normalized phone JID, which we
|
|
499
|
+
// assign as the resolved sender JID (group -> participant, DM -> from).
|
|
500
|
+
if (!normalized.senderPhone) {
|
|
501
|
+
const lidJid = normalized.isGroup ? normalized.participant : normalized.from;
|
|
502
|
+
if (lidJid?.endsWith("@lid")) {
|
|
503
|
+
const pn = await this.resolvePnFromNativeStore(lidJid);
|
|
504
|
+
if (pn) {
|
|
505
|
+
if (normalized.isGroup) {
|
|
506
|
+
normalized.participant = pn;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
normalized.from = pn;
|
|
510
|
+
}
|
|
511
|
+
normalized.senderPhone = MessageHandler.formatJidToPhone(pn);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
403
515
|
// Store message in messagesStore
|
|
404
516
|
const chatJid = normalized.from;
|
|
405
517
|
if (!this.messagesStore.has(chatJid)) {
|
|
@@ -430,6 +542,20 @@ export class MiawClient extends EventEmitter {
|
|
|
430
542
|
this.emit("message_reaction", reactionData);
|
|
431
543
|
}
|
|
432
544
|
});
|
|
545
|
+
// Poll votes (messages.update carries pollUpdates when someone votes)
|
|
546
|
+
this.socket.ev.on("messages.update", (updates) => {
|
|
547
|
+
for (const { key, update } of updates) {
|
|
548
|
+
const pollUpdates = update.pollUpdates;
|
|
549
|
+
if (!pollUpdates?.length) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
this.handlePollVote(key, pollUpdates).catch((err) => {
|
|
553
|
+
if (this.options.debug) {
|
|
554
|
+
this.logger.debug("Failed to process poll vote:", err);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
});
|
|
433
559
|
// Presence updates
|
|
434
560
|
this.socket.ev.on("presence.update", (presence) => {
|
|
435
561
|
const jid = presence.id;
|
|
@@ -515,6 +641,7 @@ export class MiawClient extends EventEmitter {
|
|
|
515
641
|
this.socket.ev.removeAllListeners("messaging-history.set");
|
|
516
642
|
this.socket.ev.removeAllListeners("messages.upsert");
|
|
517
643
|
this.socket.ev.removeAllListeners("messages.reaction");
|
|
644
|
+
this.socket.ev.removeAllListeners("messages.update");
|
|
518
645
|
this.socket.ev.removeAllListeners("presence.update");
|
|
519
646
|
this.socket.ev.removeAllListeners("labels.edit");
|
|
520
647
|
this.socket.ev.removeAllListeners("labels.association");
|
|
@@ -572,6 +699,22 @@ export class MiawClient extends EventEmitter {
|
|
|
572
699
|
* @param jid - The JID to map to (phone number)
|
|
573
700
|
* @param source - Source of the mapping for debug logs
|
|
574
701
|
*/
|
|
702
|
+
/**
|
|
703
|
+
* Capture a LID<->PN mapping from a (primary, alternate) JID pair.
|
|
704
|
+
* Baileys rc13 message keys expose alternate addressing (remoteJidAlt /
|
|
705
|
+
* participantAlt); whichever side is a privacy LID (@lid) is mapped to the
|
|
706
|
+
* phone (@s.whatsapp.net) side.
|
|
707
|
+
*/
|
|
708
|
+
captureLidPnPair(a, b) {
|
|
709
|
+
if (!a || !b)
|
|
710
|
+
return;
|
|
711
|
+
if (a.endsWith("@lid") && b.endsWith("@s.whatsapp.net")) {
|
|
712
|
+
this.addLidMapping(a, b, "Message");
|
|
713
|
+
}
|
|
714
|
+
else if (b.endsWith("@lid") && a.endsWith("@s.whatsapp.net")) {
|
|
715
|
+
this.addLidMapping(b, a, "Message");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
575
718
|
addLidMapping(lid, jid, source) {
|
|
576
719
|
if (!lid || !jid) {
|
|
577
720
|
return;
|
|
@@ -592,18 +735,80 @@ export class MiawClient extends EventEmitter {
|
|
|
592
735
|
this.saveLidMappingsToFile();
|
|
593
736
|
}
|
|
594
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Baileys rc13 native LID<->PN store, exposed on the signal repository.
|
|
740
|
+
* Provides server-grade resolution (USync-backed, with its own cache) that
|
|
741
|
+
* complements our local LRU cache. Null when the socket is not connected.
|
|
742
|
+
*/
|
|
743
|
+
get lidStore() {
|
|
744
|
+
return this.socket ? this.socket.signalRepository.lidMapping : null;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Resolve a LID to its phone JID via Baileys' native LID store, used as a
|
|
748
|
+
* fallback when our local cache misses. On a hit the mapping is back-filled
|
|
749
|
+
* into the local cache (and persisted). Never throws.
|
|
750
|
+
* @param lidJid - A '@lid' JID
|
|
751
|
+
* @returns The phone JID (@s.whatsapp.net) or null if unresolved
|
|
752
|
+
*/
|
|
753
|
+
async resolvePnFromNativeStore(lidJid) {
|
|
754
|
+
if (!lidJid?.endsWith("@lid")) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const store = this.lidStore;
|
|
758
|
+
if (!store) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
const pn = await store.getPNForLID(lidJid);
|
|
763
|
+
if (pn) {
|
|
764
|
+
// The native store returns device-specific JIDs (e.g. '123:0@s.whatsapp.net');
|
|
765
|
+
// normalize to a clean user JID so cache entries (and normalized.from /
|
|
766
|
+
// messagesStore keys) match every other resolution path.
|
|
767
|
+
const normalizedPn = jidNormalizedUser(pn);
|
|
768
|
+
this.addLidMapping(lidJid, normalizedPn, "NativeStore");
|
|
769
|
+
return normalizedPn;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
if (this.options.debug) {
|
|
774
|
+
this.logger.debug(`[LID NativeStore] getPNForLID failed for ${lidJid}:`, error);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Ingest the dedicated LID<->PN mapping array Baileys provides on history
|
|
781
|
+
* sync (rc13 `messaging-history.set.lidPnMappings`). This is the highest-
|
|
782
|
+
* confidence mapping source, so it is applied before contact/chat-derived
|
|
783
|
+
* mappings.
|
|
784
|
+
* @param mappings - Array of { lid, pn } pairs, or null/undefined
|
|
785
|
+
*/
|
|
786
|
+
ingestLidPnMappings(mappings) {
|
|
787
|
+
if (!mappings?.length) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
for (const entry of mappings) {
|
|
791
|
+
// Proto-decoded arrays can contain null/undefined entries; skip them.
|
|
792
|
+
if (!entry) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
this.addLidMapping(entry.lid, entry.pn, "History");
|
|
796
|
+
}
|
|
797
|
+
}
|
|
595
798
|
/**
|
|
596
799
|
* Update LID to JID mapping from contacts
|
|
597
800
|
*/
|
|
598
801
|
updateLidToJidMapping(contacts) {
|
|
599
802
|
for (const contact of contacts) {
|
|
600
|
-
//
|
|
803
|
+
// id is a phone JID and lid is present - map lid -> phone
|
|
601
804
|
if (contact.lid && contact.id && !contact.id.endsWith("@lid")) {
|
|
602
805
|
this.addLidMapping(contact.lid, contact.id, "Contact");
|
|
603
806
|
}
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
807
|
+
// id is the LID - map id -> phone. Baileys rc10 removed Contact.jid in
|
|
808
|
+
// favour of Contact.phoneNumber; keep .jid as a fallback for older data.
|
|
809
|
+
const contactPn = contact.phoneNumber || contact.jid;
|
|
810
|
+
if (contact.id?.endsWith("@lid") && contactPn) {
|
|
811
|
+
this.addLidMapping(contact.id, contactPn, "Contact");
|
|
607
812
|
}
|
|
608
813
|
}
|
|
609
814
|
}
|
|
@@ -612,13 +817,15 @@ export class MiawClient extends EventEmitter {
|
|
|
612
817
|
*/
|
|
613
818
|
updateLidFromChats(chats) {
|
|
614
819
|
for (const chat of chats) {
|
|
615
|
-
//
|
|
820
|
+
// id is a phone JID and lidJid is present - map lid -> phone
|
|
616
821
|
if (chat.lidJid && chat.id && !chat.id.endsWith("@lid")) {
|
|
617
822
|
this.addLidMapping(chat.lidJid, chat.id, "Chat");
|
|
618
823
|
}
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
824
|
+
// id is the LID - map id -> phone. Chats carry the phone JID on pnJid
|
|
825
|
+
// (Baileys rc10); keep the removed .jid field as a fallback for older data.
|
|
826
|
+
const chatPn = chat.pnJid || chat.jid;
|
|
827
|
+
if (chat.id?.endsWith("@lid") && chatPn) {
|
|
828
|
+
this.addLidMapping(chat.id, chatPn, "Chat");
|
|
622
829
|
}
|
|
623
830
|
}
|
|
624
831
|
}
|
|
@@ -642,6 +849,33 @@ export class MiawClient extends EventEmitter {
|
|
|
642
849
|
const resolved = this.resolveLidToJid(jid);
|
|
643
850
|
return MessageHandler.formatJidToPhone(resolved);
|
|
644
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* Resolve a LID to its phone JID, consulting Baileys' native LID store
|
|
854
|
+
* (rc13) when the local cache misses. Unlike the synchronous
|
|
855
|
+
* {@link resolveLidToJid}, this can perform a server-backed lookup.
|
|
856
|
+
* @param lid - The LID format JID (e.g., '12345@lid')
|
|
857
|
+
* @returns The resolved phone JID, or the original input if unresolved
|
|
858
|
+
*/
|
|
859
|
+
async resolveLidToJidAsync(lid) {
|
|
860
|
+
const resolved = this.resolveLidToJid(lid);
|
|
861
|
+
// Already resolved from cache, or not a LID at all.
|
|
862
|
+
if (!resolved.endsWith("@lid")) {
|
|
863
|
+
return resolved;
|
|
864
|
+
}
|
|
865
|
+
const pn = await this.resolvePnFromNativeStore(resolved);
|
|
866
|
+
return pn || resolved;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get the phone number for any JID, consulting Baileys' native LID store
|
|
870
|
+
* (rc13) when the local cache misses. Async counterpart of
|
|
871
|
+
* {@link getPhoneFromJid}.
|
|
872
|
+
* @param jid - Any JID format
|
|
873
|
+
* @returns Phone number string or undefined
|
|
874
|
+
*/
|
|
875
|
+
async getPhoneFromJidAsync(jid) {
|
|
876
|
+
const resolved = await this.resolveLidToJidAsync(jid);
|
|
877
|
+
return MessageHandler.formatJidToPhone(resolved);
|
|
878
|
+
}
|
|
645
879
|
/**
|
|
646
880
|
* Manually register a LID to phone number mapping
|
|
647
881
|
* Useful for persisting mappings across sessions
|
|
@@ -683,6 +917,87 @@ export class MiawClient extends EventEmitter {
|
|
|
683
917
|
clearLidCache() {
|
|
684
918
|
this.lidToJidMap.clear();
|
|
685
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* Resolve multiple LIDs to phone numbers in one call. Cache hits are served
|
|
922
|
+
* locally; the remaining misses are resolved in a single batched query
|
|
923
|
+
* against Baileys' native LID store (rc13 `getPNsForLIDs`), and any results
|
|
924
|
+
* are back-filled into the local cache.
|
|
925
|
+
* @param lids - LID JIDs (e.g., ['12345@lid', ...])
|
|
926
|
+
* @returns A map of input LID -> phone number string, or null if unresolved
|
|
927
|
+
*/
|
|
928
|
+
async resolveLidsToPhones(lids) {
|
|
929
|
+
const result = {};
|
|
930
|
+
const misses = [];
|
|
931
|
+
// Resolve from the local cache first; collect the misses.
|
|
932
|
+
for (const lid of lids) {
|
|
933
|
+
if (!lid?.endsWith("@lid")) {
|
|
934
|
+
// Not a LID — extract the phone directly when possible.
|
|
935
|
+
result[lid] = MessageHandler.formatJidToPhone(lid) || null;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
const cached = this.resolveLidToJid(lid);
|
|
939
|
+
if (cached.endsWith("@lid")) {
|
|
940
|
+
result[lid] = null;
|
|
941
|
+
misses.push(lid);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
result[lid] = MessageHandler.formatJidToPhone(cached) || null;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// One batched native lookup for the misses.
|
|
948
|
+
const store = this.lidStore;
|
|
949
|
+
if (store && misses.length > 0) {
|
|
950
|
+
try {
|
|
951
|
+
const pairs = await store.getPNsForLIDs(misses);
|
|
952
|
+
if (pairs) {
|
|
953
|
+
for (const { lid, pn } of pairs) {
|
|
954
|
+
if (lid && pn) {
|
|
955
|
+
const normalizedPn = jidNormalizedUser(pn);
|
|
956
|
+
this.addLidMapping(lid, normalizedPn, "NativeStore");
|
|
957
|
+
result[lid] = MessageHandler.formatJidToPhone(normalizedPn) || null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
if (this.options.debug) {
|
|
964
|
+
this.logger.debug("[LID NativeStore] getPNsForLIDs failed:", error);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Reverse-resolve a phone number to its LID via Baileys' native LID store
|
|
972
|
+
* (rc13 `getLIDForPN`). On a hit the mapping is also seeded into the local
|
|
973
|
+
* cache. Requires an active connection.
|
|
974
|
+
* @param phone - A phone number (e.g., '6281234567890') or phone JID
|
|
975
|
+
* @returns The LID JID (e.g., '12345@lid') or null if unresolved
|
|
976
|
+
*/
|
|
977
|
+
async getLidForPhone(phone) {
|
|
978
|
+
const store = this.lidStore;
|
|
979
|
+
if (!store) {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
const pnJid = phone.includes("@") ? phone : MessageHandler.formatPhoneToJid(phone);
|
|
983
|
+
try {
|
|
984
|
+
const lid = await store.getLIDForPN(pnJid);
|
|
985
|
+
if (lid) {
|
|
986
|
+
// Normalize the (possibly device-specific) LID before caching so it
|
|
987
|
+
// matches the bare LID form that arrives in message keys.
|
|
988
|
+
const normalizedLid = jidNormalizedUser(lid);
|
|
989
|
+
// Seed the reverse mapping into our cache for future LID -> PN lookups.
|
|
990
|
+
this.addLidMapping(normalizedLid, pnJid, "NativeStore");
|
|
991
|
+
return normalizedLid;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
if (this.options.debug) {
|
|
996
|
+
this.logger.debug(`[LID NativeStore] getLIDForPN failed for ${pnJid}:`, error);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
686
1001
|
/**
|
|
687
1002
|
* Update in-memory contacts store
|
|
688
1003
|
* @param contacts - Array of contact objects from Baileys
|
|
@@ -792,6 +1107,12 @@ export class MiawClient extends EventEmitter {
|
|
|
792
1107
|
* @param msg - Baileys message object
|
|
793
1108
|
*/
|
|
794
1109
|
addMessageToStore(msg) {
|
|
1110
|
+
// Capture LID<->PN mapping from the history message key (rc13 alt fields)
|
|
1111
|
+
// so @lid from/participant resolve consistently with the live message path.
|
|
1112
|
+
if (msg?.key && !msg.key.fromMe) {
|
|
1113
|
+
this.captureLidPnPair(msg.key.remoteJid, msg.key.remoteJidAlt);
|
|
1114
|
+
this.captureLidPnPair(msg.key.participant, msg.key.participantAlt);
|
|
1115
|
+
}
|
|
795
1116
|
const normalized = MessageHandler.normalize({ messages: [msg], type: "notify" }, this.logger);
|
|
796
1117
|
if (!normalized)
|
|
797
1118
|
return;
|
|
@@ -964,7 +1285,11 @@ export class MiawClient extends EventEmitter {
|
|
|
964
1285
|
const sendOptions = options?.quoted?.raw
|
|
965
1286
|
? { quoted: options.quoted.raw }
|
|
966
1287
|
: undefined;
|
|
967
|
-
const
|
|
1288
|
+
const content = { text };
|
|
1289
|
+
if (options?.mentions?.length) {
|
|
1290
|
+
content.mentions = options.mentions.map((m) => MessageHandler.formatPhoneToJid(m));
|
|
1291
|
+
}
|
|
1292
|
+
const result = await this.socket.sendMessage(jid, content, sendOptions);
|
|
968
1293
|
return {
|
|
969
1294
|
success: true,
|
|
970
1295
|
messageId: result?.key?.id || undefined,
|
|
@@ -999,6 +1324,9 @@ export class MiawClient extends EventEmitter {
|
|
|
999
1324
|
caption: options?.caption,
|
|
1000
1325
|
viewOnce: options?.viewOnce,
|
|
1001
1326
|
};
|
|
1327
|
+
if (options?.mentions?.length) {
|
|
1328
|
+
imageContent.mentions = options.mentions.map((m) => MessageHandler.formatPhoneToJid(m));
|
|
1329
|
+
}
|
|
1002
1330
|
// Build send options (for quoting/replying)
|
|
1003
1331
|
const sendOptions = options?.quoted?.raw
|
|
1004
1332
|
? { quoted: options.quoted.raw }
|
|
@@ -1091,6 +1419,9 @@ export class MiawClient extends EventEmitter {
|
|
|
1091
1419
|
gifPlayback: options?.gifPlayback,
|
|
1092
1420
|
ptv: options?.ptv,
|
|
1093
1421
|
};
|
|
1422
|
+
if (options?.mentions?.length) {
|
|
1423
|
+
videoContent.mentions = options.mentions.map((m) => MessageHandler.formatPhoneToJid(m));
|
|
1424
|
+
}
|
|
1094
1425
|
// Build send options (for quoting/replying)
|
|
1095
1426
|
const sendOptions = options?.quoted?.raw
|
|
1096
1427
|
? { quoted: options.quoted.raw }
|
|
@@ -1153,6 +1484,193 @@ export class MiawClient extends EventEmitter {
|
|
|
1153
1484
|
};
|
|
1154
1485
|
}
|
|
1155
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Send a location message
|
|
1489
|
+
* @param to - Recipient phone number or JID
|
|
1490
|
+
* @param latitude - Latitude in decimal degrees
|
|
1491
|
+
* @param longitude - Longitude in decimal degrees
|
|
1492
|
+
* @param options - Optional name/address and quoted message
|
|
1493
|
+
*/
|
|
1494
|
+
async sendLocation(to, latitude, longitude, options) {
|
|
1495
|
+
try {
|
|
1496
|
+
if (!this.socket) {
|
|
1497
|
+
throw new Error("Not connected. Call connect() first.");
|
|
1498
|
+
}
|
|
1499
|
+
if (this.connectionState !== "connected") {
|
|
1500
|
+
throw new Error(`Cannot send message. Connection state: ${this.connectionState}`);
|
|
1501
|
+
}
|
|
1502
|
+
const jid = MessageHandler.formatPhoneToJid(to);
|
|
1503
|
+
const content = {
|
|
1504
|
+
location: {
|
|
1505
|
+
degreesLatitude: latitude,
|
|
1506
|
+
degreesLongitude: longitude,
|
|
1507
|
+
name: options?.name,
|
|
1508
|
+
address: options?.address,
|
|
1509
|
+
},
|
|
1510
|
+
};
|
|
1511
|
+
const sendOptions = options?.quoted?.raw
|
|
1512
|
+
? { quoted: options.quoted.raw }
|
|
1513
|
+
: undefined;
|
|
1514
|
+
const result = await this.socket.sendMessage(jid, content, sendOptions);
|
|
1515
|
+
return { success: true, messageId: result?.key?.id || undefined };
|
|
1516
|
+
}
|
|
1517
|
+
catch (error) {
|
|
1518
|
+
this.logger.error("Failed to send location:", error);
|
|
1519
|
+
return { success: false, error: error.message };
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Build a vCard (version 3.0) string from a contact card.
|
|
1524
|
+
*/
|
|
1525
|
+
buildVCard(contact) {
|
|
1526
|
+
const phoneJid = MessageHandler.formatPhoneToJid(contact.phone);
|
|
1527
|
+
const waid = MessageHandler.formatJidToPhone(phoneJid) || contact.phone;
|
|
1528
|
+
const lines = ["BEGIN:VCARD", "VERSION:3.0", `FN:${contact.fullName}`];
|
|
1529
|
+
if (contact.organization) {
|
|
1530
|
+
lines.push(`ORG:${contact.organization}`);
|
|
1531
|
+
}
|
|
1532
|
+
lines.push(`TEL;type=CELL;type=VOICE;waid=${waid}:+${waid}`);
|
|
1533
|
+
lines.push("END:VCARD");
|
|
1534
|
+
return lines.join("\n");
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Send one or more contact cards (vCards)
|
|
1538
|
+
* @param to - Recipient phone number or JID
|
|
1539
|
+
* @param contacts - A single contact card or an array of them
|
|
1540
|
+
* @param options - Optional quoted message
|
|
1541
|
+
*/
|
|
1542
|
+
async sendContact(to, contacts, options) {
|
|
1543
|
+
try {
|
|
1544
|
+
if (!this.socket) {
|
|
1545
|
+
throw new Error("Not connected. Call connect() first.");
|
|
1546
|
+
}
|
|
1547
|
+
if (this.connectionState !== "connected") {
|
|
1548
|
+
throw new Error(`Cannot send message. Connection state: ${this.connectionState}`);
|
|
1549
|
+
}
|
|
1550
|
+
const list = Array.isArray(contacts) ? contacts : [contacts];
|
|
1551
|
+
if (list.length === 0) {
|
|
1552
|
+
return { success: false, error: "No contacts provided" };
|
|
1553
|
+
}
|
|
1554
|
+
const jid = MessageHandler.formatPhoneToJid(to);
|
|
1555
|
+
const content = {
|
|
1556
|
+
contacts: {
|
|
1557
|
+
displayName: list.length === 1 ? list[0].fullName : `${list.length} contacts`,
|
|
1558
|
+
contacts: list.map((c) => ({ vcard: this.buildVCard(c) })),
|
|
1559
|
+
},
|
|
1560
|
+
};
|
|
1561
|
+
const sendOptions = options?.quoted?.raw
|
|
1562
|
+
? { quoted: options.quoted.raw }
|
|
1563
|
+
: undefined;
|
|
1564
|
+
const result = await this.socket.sendMessage(jid, content, sendOptions);
|
|
1565
|
+
return { success: true, messageId: result?.key?.id || undefined };
|
|
1566
|
+
}
|
|
1567
|
+
catch (error) {
|
|
1568
|
+
this.logger.error("Failed to send contact:", error);
|
|
1569
|
+
return { success: false, error: error.message };
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Send a sticker (WebP image)
|
|
1574
|
+
* @param to - Recipient phone number or JID
|
|
1575
|
+
* @param sticker - WebP sticker source (file path, URL, or Buffer)
|
|
1576
|
+
* @param options - Optional quoted message
|
|
1577
|
+
*/
|
|
1578
|
+
async sendSticker(to, sticker, options) {
|
|
1579
|
+
try {
|
|
1580
|
+
if (!this.socket) {
|
|
1581
|
+
throw new Error("Not connected. Call connect() first.");
|
|
1582
|
+
}
|
|
1583
|
+
if (this.connectionState !== "connected") {
|
|
1584
|
+
throw new Error(`Cannot send message. Connection state: ${this.connectionState}`);
|
|
1585
|
+
}
|
|
1586
|
+
const jid = MessageHandler.formatPhoneToJid(to);
|
|
1587
|
+
const content = {
|
|
1588
|
+
sticker: Buffer.isBuffer(sticker) ? sticker : { url: sticker },
|
|
1589
|
+
};
|
|
1590
|
+
const sendOptions = options?.quoted?.raw
|
|
1591
|
+
? { quoted: options.quoted.raw }
|
|
1592
|
+
: undefined;
|
|
1593
|
+
const result = await this.socket.sendMessage(jid, content, sendOptions);
|
|
1594
|
+
return { success: true, messageId: result?.key?.id || undefined };
|
|
1595
|
+
}
|
|
1596
|
+
catch (error) {
|
|
1597
|
+
this.logger.error("Failed to send sticker:", error);
|
|
1598
|
+
return { success: false, error: error.message };
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Send a poll
|
|
1603
|
+
* @param to - Recipient phone number or JID
|
|
1604
|
+
* @param name - Poll question/title
|
|
1605
|
+
* @param pollOptions - Answer options (2-12)
|
|
1606
|
+
* @param options - selectableCount (default 1) and quoted message
|
|
1607
|
+
*/
|
|
1608
|
+
async sendPoll(to, name, pollOptions, options) {
|
|
1609
|
+
try {
|
|
1610
|
+
if (!this.socket) {
|
|
1611
|
+
throw new Error("Not connected. Call connect() first.");
|
|
1612
|
+
}
|
|
1613
|
+
if (this.connectionState !== "connected") {
|
|
1614
|
+
throw new Error(`Cannot send message. Connection state: ${this.connectionState}`);
|
|
1615
|
+
}
|
|
1616
|
+
if (!pollOptions || pollOptions.length < 2) {
|
|
1617
|
+
return { success: false, error: "A poll needs at least 2 options" };
|
|
1618
|
+
}
|
|
1619
|
+
const jid = MessageHandler.formatPhoneToJid(to);
|
|
1620
|
+
const content = {
|
|
1621
|
+
poll: {
|
|
1622
|
+
name,
|
|
1623
|
+
values: pollOptions,
|
|
1624
|
+
selectableCount: options?.selectableCount ?? 1,
|
|
1625
|
+
},
|
|
1626
|
+
};
|
|
1627
|
+
const sendOptions = options?.quoted?.raw
|
|
1628
|
+
? { quoted: options.quoted.raw }
|
|
1629
|
+
: undefined;
|
|
1630
|
+
const result = await this.socket.sendMessage(jid, content, sendOptions);
|
|
1631
|
+
return { success: true, messageId: result?.key?.id || undefined };
|
|
1632
|
+
}
|
|
1633
|
+
catch (error) {
|
|
1634
|
+
this.logger.error("Failed to send poll:", error);
|
|
1635
|
+
return { success: false, error: error.message };
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Decode a poll vote update and emit `poll_vote` with the aggregated tally.
|
|
1640
|
+
* Requires the original poll-creation message to be in `messagesStore` (so its
|
|
1641
|
+
* encryption secret is available). Best-effort; never throws to the caller.
|
|
1642
|
+
*/
|
|
1643
|
+
async handlePollVote(key, pollUpdates) {
|
|
1644
|
+
const chatId = key.remoteJid || "";
|
|
1645
|
+
const pollMessageId = key.id || "";
|
|
1646
|
+
if (!chatId || !pollMessageId) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
// Find the original poll-creation message we stored.
|
|
1650
|
+
const stored = (this.messagesStore.get(chatId) || []).find((m) => m.id === pollMessageId || m.raw?.key?.id === pollMessageId);
|
|
1651
|
+
if (!stored?.raw?.message) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
// Accumulate incoming vote updates onto the stored message (in-memory) so
|
|
1655
|
+
// the aggregate reflects the running tally across multiple update events.
|
|
1656
|
+
const raw = stored.raw;
|
|
1657
|
+
raw.pollUpdates = [...(raw.pollUpdates || []), ...pollUpdates];
|
|
1658
|
+
const aggregated = getAggregateVotesInPollMessage({ message: raw.message, pollUpdates: raw.pollUpdates }, this.socket?.user?.id);
|
|
1659
|
+
const vote = {
|
|
1660
|
+
pollMessageId,
|
|
1661
|
+
chatId,
|
|
1662
|
+
results: (aggregated || []).map((a) => ({
|
|
1663
|
+
option: a.name,
|
|
1664
|
+
voters: a.voters,
|
|
1665
|
+
})),
|
|
1666
|
+
};
|
|
1667
|
+
if (this.options.debug) {
|
|
1668
|
+
this.logger.debug("\n========== POLL VOTE ==========");
|
|
1669
|
+
this.logger.debug(JSON.stringify(vote, null, 2));
|
|
1670
|
+
this.logger.debug("===============================\n");
|
|
1671
|
+
}
|
|
1672
|
+
this.emit("poll_vote", vote);
|
|
1673
|
+
}
|
|
1156
1674
|
/**
|
|
1157
1675
|
* Download media from a received message
|
|
1158
1676
|
* @param message - The MiawMessage containing media (must have raw field with original Baileys message)
|