@blckrose/baileys 1.1.1 → 1.1.5

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.
@@ -89,7 +89,8 @@ export const MEDIA_PATH_MAP = {
89
89
  'product-catalog-image': '/product/image',
90
90
  'md-app-state': '',
91
91
  'md-msg-hist': '/mms/md-app-state',
92
- 'biz-cover-photo': '/pps/biz-cover-photo'
92
+ 'biz-cover-photo': '/pps/biz-cover-photo',
93
+ 'sticker-pack': '/mms/sticker'
93
94
  };
94
95
  export const MEDIA_HKDF_KEY_MAPPING = {
95
96
  audio: 'Audio',
@@ -110,7 +111,8 @@ export const MEDIA_HKDF_KEY_MAPPING = {
110
111
  'product-catalog-image': '',
111
112
  'payment-bg-image': 'Payment Background',
112
113
  ptv: 'Video',
113
- 'biz-cover-photo': 'Image'
114
+ 'biz-cover-photo': 'Image',
115
+ 'sticker-pack': 'Sticker Pack'
114
116
  };
115
117
  export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP);
116
118
  export const MIN_PREKEY_COUNT = 5;
@@ -3,14 +3,37 @@ import * as libsignal from 'libsignal';
3
3
  // @ts-ignore
4
4
  import { PreKeyWhisperMessage } from 'libsignal/src/protobufs.js';
5
5
  import { LRUCache } from 'lru-cache';
6
- // ── Suppress libsignal "Closing session: SessionEntry {...}" console spam ─────
7
- const _$origLog = console.log;
8
- console.log = (...args) => {
9
- const first = typeof args[0] === 'string' ? args[0] : (args[0] instanceof Object ? args[0]?.constructor?.name : '');
10
- if (first === 'Closing session:' || (typeof args[0] === 'object' && args[0]?.constructor?.name === 'SessionEntry')) return;
11
- if (typeof args[0] === 'string' && (args[0].startsWith('Closing session') || args[0].includes('SessionEntry'))) return;
12
- _$origLog.apply(console, args);
13
- };
6
+ // ── Suppress libsignal "Closing session: SessionEntry {...}" noise ────────────
7
+ // ES module imports di-hoist, jadi suppressor harus dipasang setelah semua import.
8
+ // Cara paling efektif: patch console + process.stdout setelah module loaded.
9
+ ;(function _suppressSessionNoise() {
10
+ const _isNoise = (...args) => {
11
+ if (typeof args[0] === 'string') {
12
+ const s = args[0];
13
+ if (s.startsWith('Closing session') || s.includes('SessionEntry') ||
14
+ s.includes('pendingPreKey') || s.includes('currentRatchet') ||
15
+ s.includes('registrationId:') || s.includes('indexInfo:')) return true;
16
+ }
17
+ if (args[1] && typeof args[1] === 'object' &&
18
+ args[1]?.constructor?.name === 'SessionEntry') return true;
19
+ if (typeof args[0] === 'object' &&
20
+ args[0]?.constructor?.name === 'SessionEntry') return true;
21
+ return false;
22
+ };
23
+ const _wrapFn = (orig) => (...args) => { if (_isNoise(...args)) return; orig(...args); };
24
+ console.log = _wrapFn(console.log.bind(console));
25
+ console.warn = _wrapFn(console.warn.bind(console));
26
+ console.info = _wrapFn(console.info.bind(console));
27
+ // Intercept process.stdout.write untuk kasus libsignal bypass console
28
+ const _origWrite = process.stdout.write.bind(process.stdout);
29
+ process.stdout.write = function(chunk, ...rest) {
30
+ if (typeof chunk === 'string' && (
31
+ chunk.includes('Closing session') || chunk.includes('SessionEntry') ||
32
+ chunk.includes('pendingPreKey') || chunk.includes('currentRatchet')
33
+ )) return true;
34
+ return _origWrite(chunk, ...rest);
35
+ };
36
+ })();
14
37
  // ─────────────────────────────────────────────────────────────────────────────
15
38
  import { generateSignalPubKey } from '../Utils/index.js';
16
39
  import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary/index.js';
@@ -8,7 +8,7 @@ import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeS
8
8
  import { makeMutex } from '../Utils/make-mutex.js';
9
9
  import processMessage from '../Utils/process-message.js';
10
10
  import { buildTcTokenFromJid } from '../Utils/tc-token-utils.js';
11
- import { getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
11
+ import { getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidEncode, jidNormalizedUser, isLidUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
12
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
13
13
  import { makeSocket } from './socket.js';
14
14
  const MAX_SYNC_ATTEMPTS = 2;
@@ -152,6 +152,23 @@ export const makeChatsSocket = (config) => {
152
152
  }
153
153
  return botList;
154
154
  };
155
+ const getLidUser = async (jid) => {
156
+ if (!jid) throw new Boom('Please input a jid user');
157
+ if (!jid.endsWith('@s.whatsapp.net') && !jid.endsWith('@lid')) {
158
+ throw new Boom('Invalid JID: Not a user JID!');
159
+ }
160
+ const targetJid = jid.includes('@') ? jid : `${jid}@s.whatsapp.net`;
161
+ const usyncQuery = new USyncQuery();
162
+ usyncQuery.protocols.push({
163
+ name: 'lid',
164
+ getQueryElement: () => ({ tag: 'lid', attrs: {}, content: undefined }),
165
+ getUserElement: () => null,
166
+ parser: (node) => node.attrs.val
167
+ });
168
+ usyncQuery.users.push({ id: targetJid });
169
+ const result = await sock.executeUSyncQuery(usyncQuery);
170
+ if (result) return result.list;
171
+ };
155
172
  const fetchStatus = async (...jids) => {
156
173
  const usyncQuery = new USyncQuery().withStatusProtocol();
157
174
  for (const jid of jids) {
@@ -258,6 +275,24 @@ export const makeChatsSocket = (config) => {
258
275
  return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid);
259
276
  };
260
277
  const updateBlockStatus = async (jid, action) => {
278
+ jid = jidNormalizedUser(jid);
279
+ // Jika input adalah LID, resolve ke PN dulu
280
+ if (jid.endsWith('@lid')) {
281
+ try {
282
+ const pn = await signalRepository?.lidMapping?.getPNForLID?.(jid).catch(() => null);
283
+ if (pn) jid = jidNormalizedUser(pn);
284
+ } catch {}
285
+ }
286
+ const dhash = String(Date.now());
287
+ const itemAttrs = {
288
+ dhash,
289
+ action,
290
+ jid, // selalu PN
291
+ };
292
+ // Block: tambah pn_jid juga
293
+ if (action === 'block') {
294
+ itemAttrs.pn_jid = jid;
295
+ }
261
296
  await query({
262
297
  tag: 'iq',
263
298
  attrs: {
@@ -268,10 +303,7 @@ export const makeChatsSocket = (config) => {
268
303
  content: [
269
304
  {
270
305
  tag: 'item',
271
- attrs: {
272
- action,
273
- jid
274
- }
306
+ attrs: itemAttrs
275
307
  }
276
308
  ]
277
309
  });
@@ -803,6 +835,60 @@ export const makeChatsSocket = (config) => {
803
835
  await Promise.all([fetchProps(), fetchBlocklist(), fetchPrivacySettings()]);
804
836
  };
805
837
  const upsertMessage = ev.createBufferedFunction(async (msg, type) => {
838
+ // ── Auto-inject isAdmin, isBotAdmin, metadata ─────────────────────────
839
+ try {
840
+ const remoteJid = msg.key?.remoteJid || '';
841
+ const isGroup = remoteJid.endsWith('@g.us');
842
+ if (isGroup) {
843
+ const _normalizeJid = (jid) => {
844
+ if (!jid) return null;
845
+ try { return jidNormalizedUser(jid).split('@')[0]; }
846
+ catch { return String(jid).split('@')[0]; }
847
+ };
848
+ // Fetch metadata (pakai cache jika tersedia)
849
+ let meta = null;
850
+ try { meta = await sock.groupMetadata(remoteJid); } catch {}
851
+ if (meta && Array.isArray(meta.participants)) {
852
+ msg.metadata = meta;
853
+ const botJid = authState.creds?.me?.id;
854
+ const senderJid = msg.key.fromMe
855
+ ? authState.creds?.me?.id
856
+ : (msg.key.participant || remoteJid);
857
+ const senderNorm = _normalizeJid(senderJid);
858
+ const botNorm = _normalizeJid(botJid);
859
+ // isAdmin
860
+ msg.isAdmin = meta.participants.some(p => {
861
+ const pid = _normalizeJid(p.jid || p.id || p.lid);
862
+ return pid === senderNorm && (p.admin === 'admin' || p.admin === 'superadmin');
863
+ });
864
+ // isBotAdmin: via m.isBotAdmin jika sudah di-set
865
+ let isBotAdmin = typeof msg.isBotAdmin === 'boolean' ? msg.isBotAdmin : false;
866
+ // fallback via owner
867
+ if (!isBotAdmin) {
868
+ const owners = [meta.owner, meta.subjectOwner, meta.ownerPn]
869
+ .filter(Boolean).map(_normalizeJid);
870
+ if (owners.includes(botNorm)) isBotAdmin = true;
871
+ }
872
+ // fallback via participants
873
+ if (!isBotAdmin) {
874
+ isBotAdmin = meta.participants.some(p => {
875
+ const pid = _normalizeJid(p.jid || p.id || p.lid);
876
+ return pid === botNorm && (p.admin === 'admin' || p.admin === 'superadmin');
877
+ });
878
+ }
879
+ msg.isBotAdmin = isBotAdmin;
880
+ } else {
881
+ msg.metadata = {};
882
+ msg.isAdmin = false;
883
+ msg.isBotAdmin = false;
884
+ }
885
+ } else {
886
+ msg.metadata = {};
887
+ msg.isAdmin = false;
888
+ msg.isBotAdmin = false;
889
+ }
890
+ } catch {}
891
+ // ─────────────────────────────────────────────────────────────────────
806
892
  ev.emit('messages.upsert', { messages: [msg], type });
807
893
  if (!!msg.pushName) {
808
894
  let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid;
@@ -1031,7 +1117,17 @@ export const makeChatsSocket = (config) => {
1031
1117
  removeMessageLabel,
1032
1118
  star,
1033
1119
  addOrEditQuickReply,
1034
- removeQuickReply
1120
+ removeQuickReply,
1121
+ clearMessage: (jid, key, timeStamp) => {
1122
+ return chatModify({
1123
+ delete: true,
1124
+ lastMessages: [{
1125
+ key: key,
1126
+ messageTimestamp: timeStamp
1127
+ }]
1128
+ }, jid);
1129
+ },
1130
+ getLidUser
1035
1131
  };
1036
1132
  };
1037
1133
  //# sourceMappingURL=chats.js.map
@@ -40,28 +40,49 @@ export const makeGroupsSocket = (config) => {
40
40
  }
41
41
  });
42
42
  // Update cache saat participant berubah
43
+ // Debounce map to prevent duplicate refresh calls for same group
44
+ const _refreshDebounce = new Map();
45
+ const _refreshGroupMetadata = async (jid) => {
46
+ // Debounce: skip jika refresh sudah dijadwalkan dalam 2 detik terakhir
47
+ if (_refreshDebounce.has(jid)) return;
48
+ _refreshDebounce.set(jid, true);
49
+ setTimeout(() => _refreshDebounce.delete(jid), 2000);
50
+ try {
51
+ const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }]);
52
+ const meta = extractGroupMetadata(result);
53
+ setCachedGroupMetadata(jid, meta);
54
+ // Emit groups.update agar subscriber luar (makeInMemoryStore dll) ikut terupdate
55
+ ev.emit('groups.update', [meta]);
56
+ } catch (e) {
57
+ // Ignore jika gagal (bot mungkin sudah keluar dari grup)
58
+ }
59
+ };
43
60
  ev.on('group-participants.update', ({ id, participants, action }) => {
44
61
  const entry = groupMetadataCache.get(id);
45
- if (!entry) return;
46
- const meta = { ...entry.data };
47
- if (!Array.isArray(meta.participants)) return;
48
- if (action === 'add') {
49
- const existing = new Set(meta.participants.map(p => p.id));
50
- for (const jid of participants) {
51
- if (!existing.has(jid)) meta.participants.push({ id: jid, admin: null });
62
+ if (entry && Array.isArray(entry.data?.participants)) {
63
+ // Fast-path: update cache lokal secara optimistis tanpa tunggu network
64
+ const meta = { ...entry.data, participants: [...entry.data.participants] };
65
+ if (action === 'add') {
66
+ const existing = new Set(meta.participants.map(p => p.id));
67
+ for (const jid of participants) {
68
+ if (!existing.has(jid)) meta.participants.push({ id: jid, admin: null });
69
+ }
70
+ } else if (action === 'remove') {
71
+ meta.participants = meta.participants.filter(p => !participants.includes(p.id));
72
+ } else if (action === 'promote') {
73
+ meta.participants = meta.participants.map(p =>
74
+ participants.includes(p.id) ? { ...p, admin: 'admin' } : p
75
+ );
76
+ } else if (action === 'demote') {
77
+ meta.participants = meta.participants.map(p =>
78
+ participants.includes(p.id) ? { ...p, admin: null } : p
79
+ );
52
80
  }
53
- } else if (action === 'remove') {
54
- meta.participants = meta.participants.filter(p => !participants.includes(p.id));
55
- } else if (action === 'promote') {
56
- meta.participants = meta.participants.map(p =>
57
- participants.includes(p.id) ? { ...p, admin: 'admin' } : p
58
- );
59
- } else if (action === 'demote') {
60
- meta.participants = meta.participants.map(p =>
61
- participants.includes(p.id) ? { ...p, admin: null } : p
62
- );
81
+ groupMetadataCache.set(id, { data: meta, ts: entry.ts });
63
82
  }
64
- groupMetadataCache.set(id, { data: meta, ts: entry.ts });
83
+ // Auto-refresh dari network untuk semua aksi participant
84
+ // Ini memastikan data selalu akurat dari server WA
85
+ _refreshGroupMetadata(id);
65
86
  });
66
87
  // ── End group metadata cache ───────────────────────────────────────────────
67
88
  const groupQuery = async (jid, type, content) => query({
@@ -330,7 +351,47 @@ export const makeGroupsSocket = (config) => {
330
351
  { tag: 'membership_approval_mode', attrs: {}, content: [{ tag: 'group_join', attrs: { state: mode } }] }
331
352
  ]);
332
353
  },
333
- groupFetchAllParticipating
354
+ groupFetchAllParticipating,
355
+ /**
356
+ * Auto detect isAdmin & isBotAdmin dari groupMetadata
357
+ * @param {string} groupJid - JID grup
358
+ * @param {string} senderJid - JID pengirim pesan
359
+ * @returns {Promise<{isAdmin: boolean, isBotAdmin: boolean}>}
360
+ */
361
+ getAdminStatus: async (groupJid, senderJid) => {
362
+ const normalizeJid = (jid) => {
363
+ if (!jid) return null;
364
+ try {
365
+ return jidNormalizedUser(jid).split('@')[0];
366
+ } catch {
367
+ return String(jid).split('@')[0];
368
+ }
369
+ };
370
+ const botJid = sock.authState?.creds?.me?.id;
371
+ const meta = await sock.groupMetadata(groupJid).catch(() => null);
372
+ if (!meta || !Array.isArray(meta.participants)) {
373
+ return { isAdmin: false, isBotAdmin: false };
374
+ }
375
+ const senderNorm = normalizeJid(senderJid);
376
+ const botNorm = normalizeJid(botJid);
377
+ const isAdmin = meta.participants.some(p => {
378
+ const pid = normalizeJid(p.jid || p.id || p.lid);
379
+ return pid === senderNorm && (p.admin === 'admin' || p.admin === 'superadmin');
380
+ });
381
+ // Cek isBotAdmin: via participants
382
+ let isBotAdmin = meta.participants.some(p => {
383
+ const pid = normalizeJid(p.jid || p.id || p.lid);
384
+ return pid === botNorm && (p.admin === 'admin' || p.admin === 'superadmin');
385
+ });
386
+ // Fallback: cek via owner/subjectOwner
387
+ if (!isBotAdmin) {
388
+ const owners = [meta.owner, meta.subjectOwner, meta.ownerPn]
389
+ .filter(Boolean)
390
+ .map(normalizeJid);
391
+ if (owners.includes(botNorm)) isBotAdmin = true;
392
+ }
393
+ return { isAdmin, isBotAdmin };
394
+ }
334
395
  };
335
396
  };
336
397
  export const extractGroupMetadata = (result) => {
@@ -993,7 +993,47 @@ export const makeMessagesRecvSocket = (config) => {
993
993
  }
994
994
  return null;
995
995
  };
996
+ // Resolve semua contextInfo termasuk yang ada di dalam quotedMessage
997
+ const getAllContextInfos = (content, results = []) => {
998
+ if (!content || typeof content !== 'object') return results;
999
+ if (content.contextInfo) {
1000
+ results.push(content.contextInfo);
1001
+ // Juga cari di dalam quotedMessage secara rekursif
1002
+ if (content.contextInfo.quotedMessage) {
1003
+ getAllContextInfos(content.contextInfo.quotedMessage, results);
1004
+ }
1005
+ }
1006
+ for (const val of Object.values(content)) {
1007
+ if (val && typeof val === 'object' && !results.includes(val)) {
1008
+ getAllContextInfos(val, results);
1009
+ }
1010
+ }
1011
+ return results;
1012
+ };
996
1013
  const contextInfo = getContextInfo(msgContent);
1014
+ // Resolve participant LID di semua contextInfo (termasuk quotedMessage)
1015
+ const allContextInfos = getAllContextInfos(msgContent);
1016
+ for (const ctx of allContextInfos) {
1017
+ if (ctx?.participant?.endsWith('@lid')) {
1018
+ try {
1019
+ const pn = await lidMapping.getPNForLID(ctx.participant);
1020
+ if (pn) {
1021
+ logger.debug({ lid: ctx.participant, pn }, 'resolved nested contextInfo.participant LID → PN');
1022
+ ctx.participant = pn;
1023
+ }
1024
+ } catch { }
1025
+ }
1026
+ // Resolve mentionedJid di quotedMessage contextInfo juga
1027
+ if (ctx !== contextInfo && ctx?.mentionedJid?.length) {
1028
+ const lids = ctx.mentionedJid.filter(j => j?.endsWith('@lid'));
1029
+ for (const lid of lids) {
1030
+ try {
1031
+ const pn = await lidMapping.getPNForLID(lid);
1032
+ if (pn) ctx.mentionedJid = ctx.mentionedJid.map(j => j === lid ? pn : j);
1033
+ } catch { }
1034
+ }
1035
+ }
1036
+ }
997
1037
  // resolve contextInfo.participant (sender of quoted message) jika masih LID
998
1038
  if (contextInfo?.participant?.endsWith('@lid')) {
999
1039
  try {
@@ -1007,6 +1047,33 @@ export const makeMessagesRecvSocket = (config) => {
1007
1047
  }
1008
1048
  if (!contextInfo?.mentionedJid?.length)
1009
1049
  return;
1050
+ // Fix text yang masih mengandung LID number meskipun mentionedJid sudah resolved ke PN
1051
+ // Kasus: mentionedJid sudah jadi @s.whatsapp.net tapi text masih "@165159209271535" (LID num)
1052
+ const textFieldEarly = getTextField(msgContent);
1053
+ if (textFieldEarly) {
1054
+ let earlyText = textFieldEarly.obj[textFieldEarly.key] || '';
1055
+ // Cek apakah text mengandung @angka panjang (>12 digit = kemungkinan LID number bukan nomor HP)
1056
+ const lidNumPattern = /@(\d{13,20})/g;
1057
+ const lidNumMatches = [...earlyText.matchAll(lidNumPattern)];
1058
+ if (lidNumMatches.length > 0) {
1059
+ // Untuk setiap mentionedJid yang sudah resolved, cek apakah ada LID number di text
1060
+ for (const resolvedJid of contextInfo.mentionedJid) {
1061
+ if (resolvedJid?.endsWith('@lid')) continue; // belum resolved, skip dulu
1062
+ const pnNum = resolvedJid.split('@')[0].split(':')[0];
1063
+ if (!pnNum) continue;
1064
+ // Cari LID number di text yang paling mungkin match (berdasarkan urutan)
1065
+ for (const match of lidNumMatches) {
1066
+ const lidNum = match[1];
1067
+ if (earlyText.includes(`@${lidNum}`)) {
1068
+ earlyText = earlyText.split(`@${lidNum}`).join(`@${pnNum}`);
1069
+ logger.debug({ lidNum, pnNum }, 'replaced LID number in text with PN number');
1070
+ break;
1071
+ }
1072
+ }
1073
+ }
1074
+ textFieldEarly.obj[textFieldEarly.key] = earlyText;
1075
+ }
1076
+ }
1010
1077
  const hasLid = contextInfo.mentionedJid.some((j) => j?.endsWith('@lid'));
1011
1078
  if (!hasLid)
1012
1079
  return;
@@ -1039,7 +1106,14 @@ export const makeMessagesRecvSocket = (config) => {
1039
1106
  if (result?.list) {
1040
1107
  const mappings = [];
1041
1108
  for (const item of result.list) {
1042
- const lidJid = stillUnresolved.find(l => l === item.id || l.startsWith((item.id.split('@')[0] ?? '')));
1109
+ // item.id bisa berupa PN (@s.whatsapp.net) atau LID (@lid)
1110
+ // stillUnresolved berisi LID. Match berdasarkan numeric prefix
1111
+ const itemNum = (item.id ?? '').split('@')[0].split(':')[0];
1112
+ const lidJid = stillUnresolved.find(l => {
1113
+ if (l === item.id) return true;
1114
+ const lNum = l.split('@')[0].split(':')[0];
1115
+ return itemNum && lNum && itemNum === lNum;
1116
+ });
1043
1117
  if (lidJid && item.id && !item.id.endsWith('@lid')) {
1044
1118
  resolveMap.set(lidJid, item.id);
1045
1119
  mappings.push({ lid: lidJid, pn: item.id });
@@ -1055,6 +1129,34 @@ export const makeMessagesRecvSocket = (config) => {
1055
1129
  logger.debug({ err: e }, 'USync LID resolve failed, using cache only');
1056
1130
  }
1057
1131
  }
1132
+ // ── Text-based PN extraction ─────────────────────────────────────────────
1133
+ // WA kadang sudah resolve nomor HP di text (@6285133801810) tapi mentionedJid
1134
+ // masih berisi LID. Kita ekstrak semua @nomor dari text lalu matching ke LID
1135
+ // yang masih unresolved berdasarkan urutan kemunculan di mentionedJid.
1136
+ const textField = getTextField(msgContent);
1137
+ const stillUnresolvedAfterUSync = lidJids.filter(l => !resolveMap.has(l));
1138
+ if (stillUnresolvedAfterUSync.length > 0 && textField) {
1139
+ const rawText = textField.obj[textField.key] || '';
1140
+ // Ekstrak semua mention @nomor dari text (hanya angka, min 7 digit)
1141
+ const mentionMatches = [...rawText.matchAll(/@(\d{7,15})/g)].map(m => m[1]);
1142
+ if (mentionMatches.length > 0) {
1143
+ // Match LID ke nomor berdasarkan urutan index di mentionedJid
1144
+ const lidOrder = contextInfo.mentionedJid
1145
+ .map((jid, idx) => ({ jid, idx }))
1146
+ .filter(({ jid }) => stillUnresolvedAfterUSync.includes(jid));
1147
+ for (let i = 0; i < lidOrder.length && i < mentionMatches.length; i++) {
1148
+ const lidJid = lidOrder[i].jid;
1149
+ const phoneNum = mentionMatches[i];
1150
+ const pnJid = `${phoneNum}@s.whatsapp.net`;
1151
+ resolveMap.set(lidJid, pnJid);
1152
+ // Simpan mapping baru ini ke store untuk request berikutnya
1153
+ lidMapping.storeLIDPNMappings([{ lid: lidJid, pn: pnJid }]).catch(() => {});
1154
+ logger.debug({ lid: lidJid, pn: pnJid }, 'text-extracted PN for LID → PN');
1155
+ }
1156
+ }
1157
+ }
1158
+ // ── End text-based PN extraction ─────────────────────────────────────────
1159
+
1058
1160
  contextInfo.mentionedJid = contextInfo.mentionedJid.map((jid) => {
1059
1161
  if (!jid?.endsWith('@lid'))
1060
1162
  return jid;
@@ -1063,13 +1165,14 @@ export const makeMessagesRecvSocket = (config) => {
1063
1165
  logger.debug({ lid: jid, pn: resolved }, 'resolved mentionedJid LID → PN');
1064
1166
  return resolved;
1065
1167
  }
1168
+ // Tetap kembalikan LID jika benar-benar tidak bisa di-resolve
1066
1169
  return jid;
1067
1170
  });
1068
- const textField = getTextField(msgContent);
1069
1171
  if (textField) {
1070
1172
  let text = textField.obj[textField.key];
1173
+ // Replace LID number di text dengan PN number yang sudah di-resolve
1071
1174
  for (const [lidJid, pnJid] of resolveMap) {
1072
- const lidNum = lidJid.replace('@lid', '').split(':')[0] ?? '';
1175
+ const lidNum = lidJid.split('@')[0].split(':')[0] ?? '';
1073
1176
  const pnNum = pnJid.replace('@s.whatsapp.net', '').split(':')[0] ?? '';
1074
1177
  if (lidNum && pnNum && text.includes(lidNum)) {
1075
1178
  text = text.split(lidNum).join(pnNum);
@@ -1268,6 +1371,97 @@ export const makeMessagesRecvSocket = (config) => {
1268
1371
  }
1269
1372
  cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid);
1270
1373
  await resolveMentionedLIDs(msg, signalRepository.lidMapping);
1374
+ // ── Post-resolve: fix semua LID yang tersisa di message object ────────
1375
+ // Setelah resolveMentionedLIDs:
1376
+ // 1. Fix text yang masih "@LIDnum" → "@PNnum"
1377
+ // 2. Fix contextInfo.participant yang masih @lid (sender quoted)
1378
+ // 3. Fix mentionedJid & text di dalam quotedMessage
1379
+ try {
1380
+ const _safeSet = (obj, key, val) => {
1381
+ try { obj[key] = val; } catch (_) {
1382
+ // proto readonly fallback
1383
+ const proto = Object.getPrototypeOf(obj);
1384
+ const fresh = Object.assign(Object.create(proto), obj, { [key]: val });
1385
+ // replace reference di parent jika bisa — caller handles
1386
+ return fresh;
1387
+ }
1388
+ return obj;
1389
+ };
1390
+ const _fixTextLid = (innerMsg, mentionedJids, msgTypeLabel) => {
1391
+ const pnNumbers = (mentionedJids || [])
1392
+ .filter(j => j && !j.endsWith('@lid'))
1393
+ .map(j => j.split('@')[0].split(':')[0])
1394
+ .filter(Boolean);
1395
+ if (!pnNumbers.length) return;
1396
+ for (const textKey of ['text', 'caption', 'conversation']) {
1397
+ const originalText = innerMsg[textKey];
1398
+ if (typeof originalText !== 'string') continue;
1399
+ const lidPattern = /@(\d{13,20})/g;
1400
+ let newText = originalText;
1401
+ let match;
1402
+ let pnIdx = 0;
1403
+ while ((match = lidPattern.exec(originalText)) !== null) {
1404
+ if (pnIdx >= pnNumbers.length) break;
1405
+ const lidNum = match[1];
1406
+ const pnNum = pnNumbers[pnIdx++];
1407
+ newText = newText.split(`@${lidNum}`).join(`@${pnNum}`);
1408
+ logger.debug({ lidNum, pnNum, type: msgTypeLabel, textKey }, 'post-resolve: replaced LID num in text');
1409
+ }
1410
+ if (newText !== originalText) _safeSet(innerMsg, textKey, newText);
1411
+ }
1412
+ };
1413
+ const msgObj = msg.message;
1414
+ if (msgObj) {
1415
+ for (const msgType of Object.keys(msgObj)) {
1416
+ const innerMsg = msgObj[msgType];
1417
+ if (!innerMsg || typeof innerMsg !== 'object') continue;
1418
+ const ctxInfo = innerMsg.contextInfo;
1419
+ if (!ctxInfo) continue;
1420
+ // 1. Fix text LID → PN (pesan utama)
1421
+ if (ctxInfo.mentionedJid?.length) {
1422
+ _fixTextLid(innerMsg, ctxInfo.mentionedJid, msgType);
1423
+ }
1424
+ // 2. Fix contextInfo.participant (sender quoted) jika masih LID
1425
+ if (ctxInfo.participant?.endsWith('@lid')) {
1426
+ try {
1427
+ const pn = await signalRepository.lidMapping.getPNForLID(ctxInfo.participant);
1428
+ if (pn) {
1429
+ _safeSet(ctxInfo, 'participant', pn);
1430
+ logger.debug({ lid: ctxInfo.participant, pn }, 'post-resolve: fixed contextInfo.participant LID');
1431
+ }
1432
+ } catch (_) {}
1433
+ }
1434
+ // 3. Fix mentionedJid & text di dalam quotedMessage
1435
+ if (ctxInfo.quotedMessage) {
1436
+ for (const qType of Object.keys(ctxInfo.quotedMessage)) {
1437
+ const qInner = ctxInfo.quotedMessage[qType];
1438
+ if (!qInner || typeof qInner !== 'object') continue;
1439
+ const qCtx = qInner.contextInfo;
1440
+ if (!qCtx?.mentionedJid?.length) continue;
1441
+ // Resolve LID di mentionedJid quoted
1442
+ const resolvedQ = await Promise.all(
1443
+ qCtx.mentionedJid.map(async (jid) => {
1444
+ if (!jid?.endsWith('@lid')) return jid;
1445
+ try {
1446
+ const pn = await signalRepository.lidMapping.getPNForLID(jid);
1447
+ return pn || jid;
1448
+ } catch (_) { return jid; }
1449
+ })
1450
+ );
1451
+ if (resolvedQ.some((j, i) => j !== qCtx.mentionedJid[i])) {
1452
+ _safeSet(qCtx, 'mentionedJid', resolvedQ);
1453
+ logger.debug({ resolvedQ }, 'post-resolve: fixed quotedMessage mentionedJid');
1454
+ }
1455
+ // Fix text LID → PN di quoted
1456
+ _fixTextLid(qInner, resolvedQ, `quoted.${qType}`);
1457
+ }
1458
+ }
1459
+ }
1460
+ }
1461
+ } catch (lidFixErr) {
1462
+ logger.debug({ err: lidFixErr }, 'post-resolve LID fix failed (non-critical)');
1463
+ }
1464
+ // ── End post-resolve fix ──────────────────────────────────────────────
1271
1465
  await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1272
1466
  });
1273
1467
  }