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.
Files changed (51) 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 +3 -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 +122 -1
  24. package/dist/client/MiawClient.d.ts.map +1 -1
  25. package/dist/client/MiawClient.js +538 -20
  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 +131 -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
@@ -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 to JID mapping from incoming messages
371
- // Incoming messages have remoteJid (phone) and senderLid (LID)
372
- // 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.
373
465
  const msgKey = msg.key;
374
- if (!msgKey.fromMe && msgKey.senderLid && msgKey.remoteJid) {
375
- 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);
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
- // Contact has both lid and jid - create mapping
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
- // Also check if id is the LID and jid field exists
605
- if (contact.id?.endsWith("@lid") && contact.jid) {
606
- 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");
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
- // 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
616
821
  if (chat.lidJid && chat.id && !chat.id.endsWith("@lid")) {
617
822
  this.addLidMapping(chat.lidJid, chat.id, "Chat");
618
823
  }
619
- // Reverse: if id is LID and we have a phone JID
620
- if (chat.id?.endsWith("@lid") && chat.jid) {
621
- 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");
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 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);
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)