miaw-core 1.2.5 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +101 -1
  2. package/README.md +4 -4
  3. package/bin/miaw-cli.ts +6 -0
  4. package/dist/cli/commands/commands-index.d.ts +1 -1
  5. package/dist/cli/commands/commands-index.d.ts.map +1 -1
  6. package/dist/cli/commands/commands-index.js +1 -1
  7. package/dist/cli/commands/commands-index.js.map +1 -1
  8. package/dist/cli/commands/index.d.ts.map +1 -1
  9. package/dist/cli/commands/index.js +46 -3
  10. package/dist/cli/commands/index.js.map +1 -1
  11. package/dist/cli/commands/send.d.ts +35 -0
  12. package/dist/cli/commands/send.d.ts.map +1 -1
  13. package/dist/cli/commands/send.js +83 -0
  14. package/dist/cli/commands/send.js.map +1 -1
  15. package/dist/cli/repl.d.ts +1 -0
  16. package/dist/cli/repl.d.ts.map +1 -1
  17. package/dist/cli/repl.js +18 -3
  18. package/dist/cli/repl.js.map +1 -1
  19. package/dist/cli/utils/session.d.ts +2 -0
  20. package/dist/cli/utils/session.d.ts.map +1 -1
  21. package/dist/cli/utils/session.js +1 -0
  22. package/dist/cli/utils/session.js.map +1 -1
  23. package/dist/client/MiawClient.d.ts +134 -1
  24. package/dist/client/MiawClient.d.ts.map +1 -1
  25. package/dist/client/MiawClient.js +562 -23
  26. package/dist/client/MiawClient.js.map +1 -1
  27. package/dist/handlers/MessageHandler.d.ts.map +1 -1
  28. package/dist/handlers/MessageHandler.js +11 -4
  29. package/dist/handlers/MessageHandler.js.map +1 -1
  30. package/dist/index.d.ts +4 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/types/baileys.d.ts +4 -1
  35. package/dist/types/baileys.d.ts.map +1 -1
  36. package/dist/types/baileys.js.map +1 -1
  37. package/dist/types/index.d.ts +133 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js.map +1 -1
  40. package/dist/utils/proxy-agent.d.ts +35 -0
  41. package/dist/utils/proxy-agent.d.ts.map +1 -0
  42. package/dist/utils/proxy-agent.js +73 -0
  43. package/dist/utils/proxy-agent.js.map +1 -0
  44. package/docs/BAILEYS_MIGRATION_v7.md +2 -0
  45. package/docs/BAILEYS_VS_MIAW_COMPARISON.md +30 -29
  46. package/docs/CLI.md +7 -0
  47. package/docs/LID_RESOLUTION.md +60 -6
  48. package/docs/MIGRATION.md +39 -3
  49. package/docs/TEST_COVERAGE_ANALYSIS.md +3 -1
  50. package/docs/USAGE.md +173 -1
  51. package/package.json +20 -18
  52. package/sessions-cli/.cli_history +10 -0
@@ -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
@@ -130,6 +131,12 @@ export class MiawClient extends EventEmitter {
130
131
  qrGracePeriod: options.qrGracePeriod || TIMEOUTS.QR_GRACE_PERIOD,
131
132
  qrScanTimeout: options.qrScanTimeout || TIMEOUTS.QR_SCAN_TIMEOUT,
132
133
  connectionTimeout: options.connectionTimeout || TIMEOUTS.CONNECTION_TIMEOUT,
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,
133
140
  };
134
141
  // Use the initialized logger
135
142
  this.logger = logger;
@@ -178,6 +185,8 @@ export class MiawClient extends EventEmitter {
178
185
  this.authState = { creds: state.creds };
179
186
  // Get latest Baileys version
180
187
  const { version } = await fetchLatestBaileysVersion();
188
+ // Resolve proxy agents if configured
189
+ const proxyAgents = await this.resolveProxyAgents();
181
190
  // Create socket
182
191
  const debugMode = this.options.debug;
183
192
  const logger = this.logger;
@@ -191,19 +200,28 @@ export class MiawClient extends EventEmitter {
191
200
  },
192
201
  browser: Browsers.macOS("Desktop"), // Desktop for more history
193
202
  generateHighQualityLinkPreview: true,
194
- // Enable full history sync to populate stores
195
- syncFullHistory: 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,
206
+ // Enable full history sync to populate stores (controlled by syncFullHistory option)
207
+ syncFullHistory: this.options.syncFullHistory,
196
208
  fireInitQueries: true,
197
209
  // Debug callback to see if history sync notifications are received
198
210
  shouldSyncHistoryMessage: (msg) => {
199
211
  if (debugMode) {
200
212
  logger.debug(`[shouldSyncHistoryMessage] syncType: ${msg.syncType}`);
201
213
  }
202
- return true; // Always sync
214
+ return this.options.syncFullHistory;
203
215
  },
216
+ // Proxy support: agent for WebSocket, fetchAgent for media HTTP requests
217
+ ...(proxyAgents?.wsAgent ? { agent: proxyAgents.wsAgent } : {}),
218
+ ...(proxyAgents?.fetchAgent ? { fetchAgent: proxyAgents.fetchAgent } : {}),
204
219
  });
205
220
  // Register event handlers
206
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));
207
225
  this.logger.info("Connection initiated");
208
226
  }
209
227
  catch (error) {
@@ -214,6 +232,77 @@ export class MiawClient extends EventEmitter {
214
232
  }
215
233
  }
216
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
+ }
217
306
  /**
218
307
  * Register socket event handlers
219
308
  */
@@ -223,8 +312,8 @@ export class MiawClient extends EventEmitter {
223
312
  // Connection updates
224
313
  this.socket.ev.on("connection.update", async (update) => {
225
314
  const { connection, lastDisconnect, qr } = update;
226
- // Handle QR code
227
- if (qr) {
315
+ // Handle QR code (suppressed when authenticating via pairing code)
316
+ if (qr && !this.options.usePairingCode) {
228
317
  this.updateConnectionState("qr_required");
229
318
  this.emit("qr", qr);
230
319
  }
@@ -313,14 +402,17 @@ export class MiawClient extends EventEmitter {
313
402
  this.updateChatsStore(chats);
314
403
  });
315
404
  // History sync - populates contacts, chats, and messages from history
316
- 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 }) => {
317
406
  if (this.options.debug) {
318
407
  this.logger.debug("\n========== MESSAGING HISTORY SYNC ==========");
319
- 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}`);
320
409
  this.logger.debug(`Is Latest: ${isLatest}, SessionId: ${peerDataRequestSessionId || 'none'}`);
321
410
  this.logger.debug("============================================\n");
322
411
  }
323
- // 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);
324
416
  this.updateLidToJidMapping(contacts);
325
417
  this.updateLidFromChats(chats);
326
418
  // Update contacts from history
@@ -366,12 +458,14 @@ export class MiawClient extends EventEmitter {
366
458
  this.handleProtocolMessage(msg, protocolMessage);
367
459
  continue; // Don't process as regular message
368
460
  }
369
- // Build LID to JID mapping from incoming messages
370
- // Incoming messages have remoteJid (phone) and senderLid (LID)
371
- // Note: senderLid may exist at runtime but isn't in Baileys v7 types
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.
372
465
  const msgKey = msg.key;
373
- if (!msgKey.fromMe && msgKey.senderLid && msgKey.remoteJid) {
374
- this.addLidMapping(msgKey.senderLid, msgKey.remoteJid, "Message");
466
+ if (!msgKey.fromMe) {
467
+ this.captureLidPnPair(msgKey.remoteJid, msgKey.remoteJidAlt);
468
+ this.captureLidPnPair(msgKey.participant, msgKey.participantAlt);
375
469
  }
376
470
  const normalized = MessageHandler.normalize({ messages: [msg], type: "notify" }, this.logger);
377
471
  if (normalized) {
@@ -399,6 +493,25 @@ export class MiawClient extends EventEmitter {
399
493
  }
400
494
  }
401
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
+ }
402
515
  // Store message in messagesStore
403
516
  const chatJid = normalized.from;
404
517
  if (!this.messagesStore.has(chatJid)) {
@@ -429,6 +542,20 @@ export class MiawClient extends EventEmitter {
429
542
  this.emit("message_reaction", reactionData);
430
543
  }
431
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
+ });
432
559
  // Presence updates
433
560
  this.socket.ev.on("presence.update", (presence) => {
434
561
  const jid = presence.id;
@@ -514,6 +641,7 @@ export class MiawClient extends EventEmitter {
514
641
  this.socket.ev.removeAllListeners("messaging-history.set");
515
642
  this.socket.ev.removeAllListeners("messages.upsert");
516
643
  this.socket.ev.removeAllListeners("messages.reaction");
644
+ this.socket.ev.removeAllListeners("messages.update");
517
645
  this.socket.ev.removeAllListeners("presence.update");
518
646
  this.socket.ev.removeAllListeners("labels.edit");
519
647
  this.socket.ev.removeAllListeners("labels.association");
@@ -571,6 +699,22 @@ export class MiawClient extends EventEmitter {
571
699
  * @param jid - The JID to map to (phone number)
572
700
  * @param source - Source of the mapping for debug logs
573
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
+ }
574
718
  addLidMapping(lid, jid, source) {
575
719
  if (!lid || !jid) {
576
720
  return;
@@ -591,18 +735,80 @@ export class MiawClient extends EventEmitter {
591
735
  this.saveLidMappingsToFile();
592
736
  }
593
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
+ }
594
798
  /**
595
799
  * Update LID to JID mapping from contacts
596
800
  */
597
801
  updateLidToJidMapping(contacts) {
598
802
  for (const contact of contacts) {
599
- // Contact has both lid and jid - create mapping
803
+ // id is a phone JID and lid is present - map lid -> phone
600
804
  if (contact.lid && contact.id && !contact.id.endsWith("@lid")) {
601
805
  this.addLidMapping(contact.lid, contact.id, "Contact");
602
806
  }
603
- // Also check if id is the LID and jid field exists
604
- if (contact.id?.endsWith("@lid") && contact.jid) {
605
- this.addLidMapping(contact.id, contact.jid, "Contact");
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");
606
812
  }
607
813
  }
608
814
  }
@@ -611,13 +817,15 @@ export class MiawClient extends EventEmitter {
611
817
  */
612
818
  updateLidFromChats(chats) {
613
819
  for (const chat of chats) {
614
- // Chat may have both id (could be phone or LID) and lidJid
820
+ // id is a phone JID and lidJid is present - map lid -> phone
615
821
  if (chat.lidJid && chat.id && !chat.id.endsWith("@lid")) {
616
822
  this.addLidMapping(chat.lidJid, chat.id, "Chat");
617
823
  }
618
- // Reverse: if id is LID and we have a phone JID
619
- if (chat.id?.endsWith("@lid") && chat.jid) {
620
- this.addLidMapping(chat.id, chat.jid, "Chat");
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");
621
829
  }
622
830
  }
623
831
  }
@@ -641,6 +849,33 @@ export class MiawClient extends EventEmitter {
641
849
  const resolved = this.resolveLidToJid(jid);
642
850
  return MessageHandler.formatJidToPhone(resolved);
643
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
+ }
644
879
  /**
645
880
  * Manually register a LID to phone number mapping
646
881
  * Useful for persisting mappings across sessions
@@ -682,6 +917,87 @@ export class MiawClient extends EventEmitter {
682
917
  clearLidCache() {
683
918
  this.lidToJidMap.clear();
684
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
+ }
685
1001
  /**
686
1002
  * Update in-memory contacts store
687
1003
  * @param contacts - Array of contact objects from Baileys
@@ -791,6 +1107,12 @@ export class MiawClient extends EventEmitter {
791
1107
  * @param msg - Baileys message object
792
1108
  */
793
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
+ }
794
1116
  const normalized = MessageHandler.normalize({ messages: [msg], type: "notify" }, this.logger);
795
1117
  if (!normalized)
796
1118
  return;
@@ -963,7 +1285,11 @@ export class MiawClient extends EventEmitter {
963
1285
  const sendOptions = options?.quoted?.raw
964
1286
  ? { quoted: options.quoted.raw }
965
1287
  : undefined;
966
- const result = await this.socket.sendMessage(jid, { text }, sendOptions);
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);
967
1293
  return {
968
1294
  success: true,
969
1295
  messageId: result?.key?.id || undefined,
@@ -998,6 +1324,9 @@ export class MiawClient extends EventEmitter {
998
1324
  caption: options?.caption,
999
1325
  viewOnce: options?.viewOnce,
1000
1326
  };
1327
+ if (options?.mentions?.length) {
1328
+ imageContent.mentions = options.mentions.map((m) => MessageHandler.formatPhoneToJid(m));
1329
+ }
1001
1330
  // Build send options (for quoting/replying)
1002
1331
  const sendOptions = options?.quoted?.raw
1003
1332
  ? { quoted: options.quoted.raw }
@@ -1090,6 +1419,9 @@ export class MiawClient extends EventEmitter {
1090
1419
  gifPlayback: options?.gifPlayback,
1091
1420
  ptv: options?.ptv,
1092
1421
  };
1422
+ if (options?.mentions?.length) {
1423
+ videoContent.mentions = options.mentions.map((m) => MessageHandler.formatPhoneToJid(m));
1424
+ }
1093
1425
  // Build send options (for quoting/replying)
1094
1426
  const sendOptions = options?.quoted?.raw
1095
1427
  ? { quoted: options.quoted.raw }
@@ -1152,6 +1484,193 @@ export class MiawClient extends EventEmitter {
1152
1484
  };
1153
1485
  }
1154
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
+ }
1155
1674
  /**
1156
1675
  * Download media from a received message
1157
1676
  * @param message - The MiawMessage containing media (must have raw field with original Baileys message)
@@ -4398,6 +4917,26 @@ export class MiawClient extends EventEmitter {
4398
4917
  this.disableDebug();
4399
4918
  }
4400
4919
  }
4920
+ /**
4921
+ * Enable history sync for next connection
4922
+ */
4923
+ enableSync() {
4924
+ this.options.syncFullHistory = true;
4925
+ this.logger.info("History sync enabled (will take effect on next connect)");
4926
+ }
4927
+ /**
4928
+ * Disable history sync for next connection
4929
+ */
4930
+ disableSync() {
4931
+ this.options.syncFullHistory = false;
4932
+ this.logger.info("History sync disabled (will take effect on next connect)");
4933
+ }
4934
+ /**
4935
+ * Check if history sync is enabled
4936
+ */
4937
+ isSyncEnabled() {
4938
+ return this.options.syncFullHistory;
4939
+ }
4401
4940
  // TypeScript event emitter type safety
4402
4941
  on(event, listener) {
4403
4942
  return super.on(event, listener);