@blckrose/baileys 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Defaults/index.js +5 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/libsignal.js +19 -7
- package/lib/Socket/community.js +361 -0
- package/lib/Socket/groups.js +76 -5
- package/lib/Socket/messages-recv.js +11 -0
- package/lib/Socket/messages-send.js +284 -16
- package/lib/Socket/newsletter.js +23 -0
- package/lib/Socket/socket.js +3 -1
- package/lib/Socket/usync.js +76 -0
- package/lib/Store/index.js +4 -0
- package/lib/Store/make-cache-manager-store.js +77 -0
- package/lib/Store/make-in-memory-store.js +400 -0
- package/lib/Store/make-ordered-dictionary.js +78 -0
- package/lib/Store/object-repository.js +23 -0
- package/lib/Types/MexUpdates.js +9 -0
- package/lib/Types/Newsletter.js +10 -0
- package/lib/Types/index.js +1 -0
- package/lib/Utils/audioToBuffer.js +31 -0
- package/lib/Utils/baileys-event-stream.js +54 -0
- package/lib/Utils/browser-utils.js +33 -20
- package/lib/Utils/generics.js +27 -0
- package/lib/Utils/index.js +5 -0
- package/lib/Utils/messages-media.js +103 -4
- package/lib/Utils/messages.js +470 -13
- package/lib/Utils/streamToBuffer.js +17 -0
- package/lib/Utils/use-mongo-file-auth-state.js +77 -0
- package/lib/Utils/use-single-file-auth-state.js +74 -0
- package/lib/WABinary/generic-utils.js +15 -0
- package/lib/WABinary/index.js +1 -0
- package/lib/WABinary/jid-utils.js +2 -0
- package/lib/WAUSync/Protocols/index.js +2 -1
- package/lib/index.js +20 -0
- package/package.json +3 -1
package/lib/Socket/groups.js
CHANGED
|
@@ -6,6 +6,64 @@ import { makeChatsSocket } from './chats.js';
|
|
|
6
6
|
export const makeGroupsSocket = (config) => {
|
|
7
7
|
const sock = makeChatsSocket(config);
|
|
8
8
|
const { authState, ev, query, upsertMessage } = sock;
|
|
9
|
+
const { cachedGroupMetadata } = config;
|
|
10
|
+
// ── Built-in group metadata cache ─────────────────────────────────────────
|
|
11
|
+
const groupMetadataCache = new Map();
|
|
12
|
+
const GROUP_CACHE_TTL = (config.groupCacheTTL || 5) * 60 * 1000; // default 5 menit
|
|
13
|
+
const getCachedGroupMetadata = async (jid) => {
|
|
14
|
+
// 1. Cek user-provided cachedGroupMetadata (dari config makeWASocket)
|
|
15
|
+
if (cachedGroupMetadata) {
|
|
16
|
+
const cached = await cachedGroupMetadata(jid);
|
|
17
|
+
if (cached && Array.isArray(cached.participants)) return cached;
|
|
18
|
+
}
|
|
19
|
+
// 2. Cek internal Map cache
|
|
20
|
+
const entry = groupMetadataCache.get(jid);
|
|
21
|
+
if (entry && Date.now() - entry.ts < GROUP_CACHE_TTL) {
|
|
22
|
+
return entry.data;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
};
|
|
26
|
+
const setCachedGroupMetadata = (jid, data) => {
|
|
27
|
+
groupMetadataCache.set(jid, { data, ts: Date.now() });
|
|
28
|
+
};
|
|
29
|
+
// Update cache saat groups.update event
|
|
30
|
+
ev.on('groups.update', (updates) => {
|
|
31
|
+
for (const update of updates) {
|
|
32
|
+
const entry = groupMetadataCache.get(update.id);
|
|
33
|
+
if (entry) {
|
|
34
|
+
// Merge update ke cache yang ada
|
|
35
|
+
groupMetadataCache.set(update.id, {
|
|
36
|
+
data: { ...entry.data, ...update },
|
|
37
|
+
ts: entry.ts
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Update cache saat participant berubah
|
|
43
|
+
ev.on('group-participants.update', ({ id, participants, action }) => {
|
|
44
|
+
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 });
|
|
52
|
+
}
|
|
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
|
+
);
|
|
63
|
+
}
|
|
64
|
+
groupMetadataCache.set(id, { data: meta, ts: entry.ts });
|
|
65
|
+
});
|
|
66
|
+
// ── End group metadata cache ───────────────────────────────────────────────
|
|
9
67
|
const groupQuery = async (jid, type, content) => query({
|
|
10
68
|
tag: 'iq',
|
|
11
69
|
attrs: {
|
|
@@ -16,8 +74,15 @@ export const makeGroupsSocket = (config) => {
|
|
|
16
74
|
content
|
|
17
75
|
});
|
|
18
76
|
const groupMetadata = async (jid) => {
|
|
77
|
+
// Cek cache dulu sebelum hit network
|
|
78
|
+
const cached = await getCachedGroupMetadata(jid);
|
|
79
|
+
if (cached) return cached;
|
|
80
|
+
// Fetch dari WA
|
|
19
81
|
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }]);
|
|
20
|
-
|
|
82
|
+
const meta = extractGroupMetadata(result);
|
|
83
|
+
// Simpan ke cache
|
|
84
|
+
setCachedGroupMetadata(jid, meta);
|
|
85
|
+
return meta;
|
|
21
86
|
};
|
|
22
87
|
const groupFetchAllParticipating = async () => {
|
|
23
88
|
const result = await query({
|
|
@@ -312,11 +377,17 @@ export const extractGroupMetadata = (result) => {
|
|
|
312
377
|
joinApprovalMode: !!getBinaryNodeChild(group, 'membership_approval_mode'),
|
|
313
378
|
memberAddMode,
|
|
314
379
|
participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
|
|
315
|
-
|
|
380
|
+
const isLid = isLidUser(attrs.jid);
|
|
381
|
+
const pn = attrs.phone_number;
|
|
382
|
+
const hasPn = isPnUser(pn);
|
|
383
|
+
// Jika grup pakai LID addressing:
|
|
384
|
+
// - id → pakai phoneNumber (PN) agar bisa dicompare dengan m.sender
|
|
385
|
+
// - lid → simpan LID asli
|
|
386
|
+
// Jika grup pakai PN addressing: id tetap PN
|
|
316
387
|
return {
|
|
317
|
-
id: attrs.jid,
|
|
318
|
-
phoneNumber:
|
|
319
|
-
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
|
388
|
+
id: isLid && hasPn ? pn : attrs.jid,
|
|
389
|
+
phoneNumber: isLid && hasPn ? pn : undefined,
|
|
390
|
+
lid: isLid ? attrs.jid : (isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined),
|
|
320
391
|
admin: (attrs.type || null)
|
|
321
392
|
};
|
|
322
393
|
}),
|
|
@@ -994,6 +994,17 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
994
994
|
return null;
|
|
995
995
|
};
|
|
996
996
|
const contextInfo = getContextInfo(msgContent);
|
|
997
|
+
// resolve contextInfo.participant (sender of quoted message) jika masih LID
|
|
998
|
+
if (contextInfo?.participant?.endsWith('@lid')) {
|
|
999
|
+
try {
|
|
1000
|
+
const pn = await lidMapping.getPNForLID(contextInfo.participant);
|
|
1001
|
+
if (pn) {
|
|
1002
|
+
logger.debug({ lid: contextInfo.participant, pn }, 'resolved contextInfo.participant LID → PN');
|
|
1003
|
+
contextInfo.participant = pn;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
catch { }
|
|
1007
|
+
}
|
|
997
1008
|
if (!contextInfo?.mentionedJid?.length)
|
|
998
1009
|
return;
|
|
999
1010
|
const hasLid = contextInfo.mentionedJid.some((j) => j?.endsWith('@lid'));
|
|
@@ -2,17 +2,61 @@ import NodeCache from '@cacheable/node-cache';
|
|
|
2
2
|
import { Boom } from '@hapi/boom';
|
|
3
3
|
import { proto } from '../../WAProto/index.js';
|
|
4
4
|
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
|
|
5
|
-
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils/index.js';
|
|
5
|
+
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, prepareWAMessageMedia, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils/index.js';
|
|
6
6
|
import { getUrlInfo } from '../Utils/link-preview.js';
|
|
7
7
|
import { makeKeyedMutex } from '../Utils/make-mutex.js';
|
|
8
8
|
import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils.js';
|
|
9
9
|
import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidGroup, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
10
|
+
|
|
10
11
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
11
12
|
import { makeNewsletterSocket } from './newsletter.js';
|
|
13
|
+
// Inline helper — no external import needed
|
|
14
|
+
const _isNewsletterJid = (jid) => typeof jid === 'string' && jid.endsWith('@newsletter');
|
|
15
|
+
|
|
12
16
|
export const makeMessagesSocket = (config) => {
|
|
13
17
|
const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
|
|
14
18
|
const sock = makeNewsletterSocket(config);
|
|
15
19
|
const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral } = sock;
|
|
20
|
+
// ── Internal contacts cache untuk resolve pushname di mentions ────────────
|
|
21
|
+
const _contactsCache = new Map(); // PN/JID -> nama
|
|
22
|
+
const _lidToPnCache = new Map(); // LID -> PN
|
|
23
|
+
const _storeContact = (c) => {
|
|
24
|
+
if (!c.id) return;
|
|
25
|
+
const name = c.notify || c.name || null;
|
|
26
|
+
if (name) {
|
|
27
|
+
_contactsCache.set(c.id, name);
|
|
28
|
+
// Jika ada lid field, simpan mapping LID->PN dan LID->nama
|
|
29
|
+
if (c.lid) {
|
|
30
|
+
_lidToPnCache.set(c.lid, c.id);
|
|
31
|
+
_contactsCache.set(c.lid, name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
ev.on('contacts.upsert', (contacts) => {
|
|
36
|
+
for (const c of contacts) {
|
|
37
|
+
_storeContact(c);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
ev.on('contacts.update', (updates) => {
|
|
41
|
+
for (const c of updates) {
|
|
42
|
+
if (c.notify || c.name) _storeContact(c);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const _getContactName = (jid) => {
|
|
46
|
+
// Cek langsung dulu (PN format)
|
|
47
|
+
let name = _contactsCache.get(jid);
|
|
48
|
+
if (name && name !== jid.split('@')[0]) return name;
|
|
49
|
+
// Kalau LID, cari via mapping LID->PN yang tersimpan di cache
|
|
50
|
+
if (isLidUser(jid)) {
|
|
51
|
+
const pn = _lidToPnCache.get(jid);
|
|
52
|
+
if (pn) {
|
|
53
|
+
name = _contactsCache.get(pn);
|
|
54
|
+
if (name && name !== pn.split('@')[0]) return name;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
// ── End internal contacts cache ───────────────────────────────────────────
|
|
16
60
|
const userDevicesCache = config.userDevicesCache ||
|
|
17
61
|
new NodeCache({
|
|
18
62
|
stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
|
|
@@ -473,26 +517,28 @@ export const makeMessagesSocket = (config) => {
|
|
|
473
517
|
extraAttrs['mediatype'] = mediaType;
|
|
474
518
|
}
|
|
475
519
|
if (isNewsletter) {
|
|
520
|
+
// Handle edit
|
|
521
|
+
if (message.protocolMessage?.editedMessage) {
|
|
522
|
+
msgId = message.protocolMessage.key?.id;
|
|
523
|
+
message = message.protocolMessage.editedMessage;
|
|
524
|
+
}
|
|
525
|
+
// Handle delete/revoke
|
|
526
|
+
if (message.protocolMessage?.type === proto.Message.ProtocolMessage.Type.REVOKE) {
|
|
527
|
+
msgId = message.protocolMessage.key?.id;
|
|
528
|
+
message = {};
|
|
529
|
+
}
|
|
476
530
|
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
|
|
531
|
+
if (Array.isArray(patched)) {
|
|
532
|
+
throw new Error('Per-jid patching is not supported in channel');
|
|
533
|
+
}
|
|
477
534
|
const bytes = encodeNewsletterMessage(patched);
|
|
535
|
+
// extraAttrs already has mediatype set above if media message
|
|
478
536
|
binaryNodeContent.push({
|
|
479
537
|
tag: 'plaintext',
|
|
480
|
-
attrs:
|
|
538
|
+
attrs: extraAttrs,
|
|
481
539
|
content: bytes
|
|
482
540
|
});
|
|
483
|
-
|
|
484
|
-
tag: 'message',
|
|
485
|
-
attrs: {
|
|
486
|
-
to: jid,
|
|
487
|
-
id: msgId,
|
|
488
|
-
type: getMessageType(message),
|
|
489
|
-
...(additionalAttributes || {})
|
|
490
|
-
},
|
|
491
|
-
content: binaryNodeContent
|
|
492
|
-
};
|
|
493
|
-
logger.debug({ msgId }, `sending newsletter message to ${jid}`);
|
|
494
|
-
await sendNode(stanza);
|
|
495
|
-
return;
|
|
541
|
+
logger.debug({ msgId, extraAttrs }, `sending newsletter message to ${jid}`);
|
|
496
542
|
}
|
|
497
543
|
if (normalizeMessageContent(message)?.pinInChatMessage || normalizeMessageContent(message)?.reactionMessage) {
|
|
498
544
|
extraAttrs['decrypt-fail'] = 'hide'; // todo: expand for reactions and other types
|
|
@@ -890,6 +936,55 @@ export const makeMessagesSocket = (config) => {
|
|
|
890
936
|
};
|
|
891
937
|
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn);
|
|
892
938
|
const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update');
|
|
939
|
+
// ── Button type helpers (ported from itsukichan) ──────────────────────────
|
|
940
|
+
const getButtonType = (message) => {
|
|
941
|
+
if (message.listMessage) return 'list';
|
|
942
|
+
if (message.buttonsMessage) return 'buttons';
|
|
943
|
+
if (message.interactiveMessage?.nativeFlowMessage) return 'native_flow';
|
|
944
|
+
return null;
|
|
945
|
+
};
|
|
946
|
+
const getButtonArgs = (message) => {
|
|
947
|
+
const nativeFlow = message.interactiveMessage?.nativeFlowMessage;
|
|
948
|
+
const firstButtonName = nativeFlow?.buttons?.[0]?.name;
|
|
949
|
+
const nativeFlowSpecials = [
|
|
950
|
+
'mpm', 'cta_catalog', 'send_location',
|
|
951
|
+
'call_permission_request', 'wa_payment_transaction_details',
|
|
952
|
+
'automated_greeting_message_view_catalog'
|
|
953
|
+
];
|
|
954
|
+
const ts = unixTimestampSeconds().toString();
|
|
955
|
+
const bizBase = { actual_actors: '2', host_storage: '2', privacy_mode_ts: ts };
|
|
956
|
+
const qualityControl = { tag: 'quality_control', attrs: { source_type: 'third_party' } };
|
|
957
|
+
if (nativeFlow && (firstButtonName === 'review_and_pay' || firstButtonName === 'payment_info')) {
|
|
958
|
+
return {
|
|
959
|
+
tag: 'biz',
|
|
960
|
+
attrs: { native_flow_name: firstButtonName === 'review_and_pay' ? 'order_details' : firstButtonName }
|
|
961
|
+
};
|
|
962
|
+
} else if (nativeFlow && nativeFlowSpecials.includes(firstButtonName)) {
|
|
963
|
+
return {
|
|
964
|
+
tag: 'biz', attrs: bizBase,
|
|
965
|
+
content: [{
|
|
966
|
+
tag: 'interactive', attrs: { type: 'native_flow', v: '1' },
|
|
967
|
+
content: [{ tag: 'native_flow', attrs: { v: '2', name: firstButtonName } }]
|
|
968
|
+
}, qualityControl]
|
|
969
|
+
};
|
|
970
|
+
} else if (nativeFlow || message.buttonsMessage) {
|
|
971
|
+
return {
|
|
972
|
+
tag: 'biz', attrs: bizBase,
|
|
973
|
+
content: [{
|
|
974
|
+
tag: 'interactive', attrs: { type: 'native_flow', v: '1' },
|
|
975
|
+
content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }]
|
|
976
|
+
}, qualityControl]
|
|
977
|
+
};
|
|
978
|
+
} else if (message.listMessage) {
|
|
979
|
+
return {
|
|
980
|
+
tag: 'biz', attrs: bizBase,
|
|
981
|
+
content: [{ tag: 'list', attrs: { v: '2', type: 'product_list' } }, qualityControl]
|
|
982
|
+
};
|
|
983
|
+
} else {
|
|
984
|
+
return { tag: 'biz', attrs: bizBase };
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
// ── End button type helpers ───────────────────────────────────────────────
|
|
893
988
|
return {
|
|
894
989
|
...sock,
|
|
895
990
|
getPrivacyTokens,
|
|
@@ -950,6 +1045,164 @@ export const makeMessagesSocket = (config) => {
|
|
|
950
1045
|
},
|
|
951
1046
|
sendMessage: async (jid, content, options = {}) => {
|
|
952
1047
|
const userJid = authState.creds.me.id;
|
|
1048
|
+
// ── Normalize: buttons[].nativeFlowInfo -> interactiveButtons ──────
|
|
1049
|
+
if (
|
|
1050
|
+
typeof content === 'object' &&
|
|
1051
|
+
Array.isArray(content.buttons) &&
|
|
1052
|
+
content.buttons.length > 0 &&
|
|
1053
|
+
content.buttons.some(b => b.nativeFlowInfo)
|
|
1054
|
+
) {
|
|
1055
|
+
const interactiveButtons = content.buttons.map(b => {
|
|
1056
|
+
if (b.nativeFlowInfo) {
|
|
1057
|
+
return {
|
|
1058
|
+
name: b.nativeFlowInfo.name,
|
|
1059
|
+
buttonParamsJson: b.nativeFlowInfo.paramsJson || '{}'
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
name: 'quick_reply',
|
|
1064
|
+
buttonParamsJson: JSON.stringify({
|
|
1065
|
+
display_text: b.buttonText?.displayText || b.buttonId || 'Button',
|
|
1066
|
+
id: b.buttonId || b.buttonText?.displayText || 'btn'
|
|
1067
|
+
})
|
|
1068
|
+
};
|
|
1069
|
+
});
|
|
1070
|
+
const { buttons, headerType, viewOnce, ...rest } = content;
|
|
1071
|
+
content = { ...rest, interactiveButtons };
|
|
1072
|
+
}
|
|
1073
|
+
// ── Interactive Button (sendButton logic) ──────────────────────────
|
|
1074
|
+
if (typeof content === 'object' && Array.isArray(content.interactiveButtons) && content.interactiveButtons.length > 0) {
|
|
1075
|
+
const {
|
|
1076
|
+
text = '',
|
|
1077
|
+
caption = '',
|
|
1078
|
+
title = '',
|
|
1079
|
+
footer = '',
|
|
1080
|
+
interactiveButtons,
|
|
1081
|
+
hasMediaAttachment = false,
|
|
1082
|
+
image = null,
|
|
1083
|
+
video = null,
|
|
1084
|
+
document = null,
|
|
1085
|
+
mimetype = null,
|
|
1086
|
+
jpegThumbnail = null,
|
|
1087
|
+
location = null,
|
|
1088
|
+
product = null,
|
|
1089
|
+
businessOwnerJid = null,
|
|
1090
|
+
externalAdReply = null,
|
|
1091
|
+
} = content;
|
|
1092
|
+
// Normalize buttons
|
|
1093
|
+
const processedButtons = [];
|
|
1094
|
+
for (let i = 0; i < interactiveButtons.length; i++) {
|
|
1095
|
+
const btn = interactiveButtons[i];
|
|
1096
|
+
if (!btn || typeof btn !== 'object') throw new Error(`interactiveButtons[${i}] must be an object`);
|
|
1097
|
+
if (btn.name && btn.buttonParamsJson) { processedButtons.push(btn); continue; }
|
|
1098
|
+
if (btn.id || btn.text || btn.displayText) {
|
|
1099
|
+
processedButtons.push({ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.text || btn.displayText || `Button ${i + 1}`, id: btn.id || `quick_${i + 1}` }) });
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
if (btn.buttonId && btn.buttonText?.displayText) {
|
|
1103
|
+
processedButtons.push({ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText.displayText, id: btn.buttonId }) });
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
throw new Error(`interactiveButtons[${i}] has invalid shape`);
|
|
1107
|
+
}
|
|
1108
|
+
let messageContent = {};
|
|
1109
|
+
// Header
|
|
1110
|
+
if (image) {
|
|
1111
|
+
const mi = Buffer.isBuffer(image) ? { image } : { image: { url: typeof image === 'object' ? image.url : image } };
|
|
1112
|
+
const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
|
|
1113
|
+
messageContent.header = { title: title || '', hasMediaAttachment: true, imageMessage: pm.imageMessage };
|
|
1114
|
+
} else if (video) {
|
|
1115
|
+
const mi = Buffer.isBuffer(video) ? { video } : { video: { url: typeof video === 'object' ? video.url : video } };
|
|
1116
|
+
const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
|
|
1117
|
+
messageContent.header = { title: title || '', hasMediaAttachment: true, videoMessage: pm.videoMessage };
|
|
1118
|
+
} else if (document) {
|
|
1119
|
+
const mi = Buffer.isBuffer(document) ? { document } : { document: { url: typeof document === 'object' ? document.url : document } };
|
|
1120
|
+
if (mimetype && typeof mi.document === 'object') mi.document.mimetype = mimetype;
|
|
1121
|
+
if (jpegThumbnail) {
|
|
1122
|
+
const thumb = Buffer.isBuffer(jpegThumbnail) ? jpegThumbnail : await (async () => { try { const r = await fetch(jpegThumbnail); return Buffer.from(await r.arrayBuffer()); } catch { return undefined; } })();
|
|
1123
|
+
if (thumb) mi.document.jpegThumbnail = thumb;
|
|
1124
|
+
}
|
|
1125
|
+
const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
|
|
1126
|
+
messageContent.header = { title: title || '', hasMediaAttachment: true, documentMessage: pm.documentMessage };
|
|
1127
|
+
} else if (location && typeof location === 'object') {
|
|
1128
|
+
messageContent.header = { title: title || location.name || 'Location', hasMediaAttachment: false, locationMessage: { degreesLatitude: location.degreesLatitude || location.degressLatitude || 0, degreesLongitude: location.degreesLongitude || location.degressLongitude || 0, name: location.name || '', address: location.address || '' } };
|
|
1129
|
+
} else if (product && typeof product === 'object') {
|
|
1130
|
+
let productImageMessage = null;
|
|
1131
|
+
if (product.productImage) {
|
|
1132
|
+
const mi = Buffer.isBuffer(product.productImage) ? { image: product.productImage } : { image: { url: typeof product.productImage === 'object' ? product.productImage.url : product.productImage } };
|
|
1133
|
+
const pm = await prepareWAMessageMedia(mi, { upload: waUploadToServer });
|
|
1134
|
+
productImageMessage = pm.imageMessage;
|
|
1135
|
+
}
|
|
1136
|
+
messageContent.header = { title: title || product.title || 'Product', hasMediaAttachment: false, productMessage: { product: { productImage: productImageMessage, productId: product.productId || '', title: product.title || '', description: product.description || '', currencyCode: product.currencyCode || 'USD', priceAmount1000: parseInt(product.priceAmount1000) || 0, retailerId: product.retailerId || '', url: product.url || '', productImageCount: product.productImageCount || 1 }, businessOwnerJid: businessOwnerJid || product.businessOwnerJid || userJid } };
|
|
1137
|
+
} else if (title) {
|
|
1138
|
+
messageContent.header = { title, hasMediaAttachment: false };
|
|
1139
|
+
}
|
|
1140
|
+
const hasMedia = !!(image || video || document || location || product);
|
|
1141
|
+
const bodyText = hasMedia ? caption : text || caption;
|
|
1142
|
+
if (bodyText) messageContent.body = { text: bodyText };
|
|
1143
|
+
if (footer) messageContent.footer = { text: footer };
|
|
1144
|
+
messageContent.nativeFlowMessage = { buttons: processedButtons };
|
|
1145
|
+
// Context info
|
|
1146
|
+
if (externalAdReply && typeof externalAdReply === 'object') {
|
|
1147
|
+
messageContent.contextInfo = { externalAdReply: { title: externalAdReply.title || '', body: externalAdReply.body || '', mediaType: externalAdReply.mediaType || 1, sourceUrl: externalAdReply.sourceUrl || externalAdReply.url || '', thumbnailUrl: externalAdReply.thumbnailUrl || externalAdReply.thumbnail || '', renderLargerThumbnail: externalAdReply.renderLargerThumbnail || false, showAdAttribution: externalAdReply.showAdAttribution !== false, containsAutoReply: externalAdReply.containsAutoReply || false, ...(externalAdReply.mediaUrl && { mediaUrl: externalAdReply.mediaUrl }), ...(Buffer.isBuffer(externalAdReply.thumbnail) && { thumbnail: externalAdReply.thumbnail }), ...(externalAdReply.jpegThumbnail && { jpegThumbnail: externalAdReply.jpegThumbnail }) }, ...(options.mentionedJid && { mentionedJid: options.mentionedJid }) };
|
|
1148
|
+
} else if (options.mentionedJid) {
|
|
1149
|
+
messageContent.contextInfo = { mentionedJid: options.mentionedJid };
|
|
1150
|
+
}
|
|
1151
|
+
const payload = proto.Message.InteractiveMessage.create(messageContent);
|
|
1152
|
+
const msg = generateWAMessageFromContent(jid, { viewOnceMessage: { message: { interactiveMessage: payload } } }, { userJid, quoted: options?.quoted || null });
|
|
1153
|
+
const additionalNodes = [{ tag: 'biz', attrs: {}, content: [{ tag: 'interactive', attrs: { type: 'native_flow', v: '1' }, content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }] }] }];
|
|
1154
|
+
await relayMessage(jid, msg.message, { messageId: msg.key.id, additionalNodes });
|
|
1155
|
+
return msg;
|
|
1156
|
+
}
|
|
1157
|
+
// ── End Interactive Button ─────────────────────────────────────────
|
|
1158
|
+
// ── Auto inject pushname/nomor ke text jika ada mentionedJid ─────────
|
|
1159
|
+
if (typeof content === 'object' && !Array.isArray(content)) {
|
|
1160
|
+
const mentionedJid = content.mentionedJid || options.mentionedJid || content.contextInfo?.mentionedJid;
|
|
1161
|
+
if (Array.isArray(mentionedJid) && mentionedJid.length > 0) {
|
|
1162
|
+
// Priority: options.mentionNames > content.mentionNames > _contactsCache > nomor
|
|
1163
|
+
const mentionNamesManual = options.mentionNames || content.mentionNames || {};
|
|
1164
|
+
// Resolve nama untuk setiap jid
|
|
1165
|
+
const resolvedNames = {};
|
|
1166
|
+
for (const jid of mentionedJid) {
|
|
1167
|
+
const num = jid.split('@')[0];
|
|
1168
|
+
if (mentionNamesManual[jid] || mentionNamesManual[num]) {
|
|
1169
|
+
resolvedNames[jid] = mentionNamesManual[jid] || mentionNamesManual[num];
|
|
1170
|
+
} else {
|
|
1171
|
+
const resolved = _getContactName(jid);
|
|
1172
|
+
resolvedNames[jid] = resolved || num;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
const textFields = ['text', 'caption'];
|
|
1176
|
+
for (const field of textFields) {
|
|
1177
|
+
if (typeof content[field] === 'string') {
|
|
1178
|
+
let modified = content[field];
|
|
1179
|
+
for (const jid of mentionedJid) {
|
|
1180
|
+
const num = jid.split('@')[0];
|
|
1181
|
+
const name = resolvedNames[jid] || num;
|
|
1182
|
+
// Variasi format yang mungkin ada di teks:
|
|
1183
|
+
// 1. @6281xxx@s.whatsapp.net (full JID dengan @)
|
|
1184
|
+
// 2. 6281xxx@s.whatsapp.net (full JID tanpa @)
|
|
1185
|
+
// 3. @6281xxx (nomor saja)
|
|
1186
|
+
const fullJidAt = `@${jid}`; // @628xxx@s.whatsapp.net
|
|
1187
|
+
const fullJidNoAt = jid; // 628xxx@s.whatsapp.net
|
|
1188
|
+
const numAt = `@${num}`; // @628xxx
|
|
1189
|
+
if (modified.includes(fullJidAt)) {
|
|
1190
|
+
modified = modified.split(fullJidAt).join(`@${name}`);
|
|
1191
|
+
} else if (modified.includes(fullJidNoAt)) {
|
|
1192
|
+
modified = modified.split(fullJidNoAt).join(`@${name}`);
|
|
1193
|
+
} else if (modified.includes(numAt) && name !== num) {
|
|
1194
|
+
modified = modified.split(numAt).join(`@${name}`);
|
|
1195
|
+
}
|
|
1196
|
+
// Tidak append jika @nomor tidak ada di teks sama sekali
|
|
1197
|
+
}
|
|
1198
|
+
if (modified !== content[field]) {
|
|
1199
|
+
content = { ...content, [field]: modified };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
// ── End pushname inject ───────────────────────────────────────────────
|
|
953
1206
|
if (typeof content === 'object' &&
|
|
954
1207
|
'disappearingMessagesInChat' in content &&
|
|
955
1208
|
typeof content['disappearingMessagesInChat'] !== 'undefined' &&
|
|
@@ -963,6 +1216,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
963
1216
|
await groupToggleEphemeral(jid, value);
|
|
964
1217
|
}
|
|
965
1218
|
else {
|
|
1219
|
+
let mediaHandle;
|
|
966
1220
|
const fullMsg = await generateWAMessage(jid, content, {
|
|
967
1221
|
logger,
|
|
968
1222
|
userJid,
|
|
@@ -978,7 +1232,12 @@ export const makeMessagesSocket = (config) => {
|
|
|
978
1232
|
//TODO: CACHE
|
|
979
1233
|
getProfilePicUrl: sock.profilePictureUrl,
|
|
980
1234
|
getCallLink: sock.createCallLink,
|
|
981
|
-
|
|
1235
|
+
newsletter: _isNewsletterJid(jid),
|
|
1236
|
+
upload: async (encFilePath, opts) => {
|
|
1237
|
+
const up = await waUploadToServer(encFilePath, { ...opts, newsletter: _isNewsletterJid(jid) });
|
|
1238
|
+
mediaHandle = up.handle;
|
|
1239
|
+
return up;
|
|
1240
|
+
},
|
|
982
1241
|
mediaCache: config.mediaCache,
|
|
983
1242
|
options: config.options,
|
|
984
1243
|
messageId: generateMessageIDV2(sock.user?.id),
|
|
@@ -1023,6 +1282,15 @@ export const makeMessagesSocket = (config) => {
|
|
|
1023
1282
|
}
|
|
1024
1283
|
});
|
|
1025
1284
|
}
|
|
1285
|
+
// Auto-attach biz node for button/list/interactive messages
|
|
1286
|
+
const buttonType = getButtonType(fullMsg.message);
|
|
1287
|
+
if (buttonType) {
|
|
1288
|
+
const btnNode = getButtonArgs(fullMsg.message);
|
|
1289
|
+
if (btnNode) additionalNodes.push(btnNode);
|
|
1290
|
+
}
|
|
1291
|
+
if (mediaHandle) {
|
|
1292
|
+
additionalAttributes['media_id'] = mediaHandle;
|
|
1293
|
+
}
|
|
1026
1294
|
await relayMessage(jid, fullMsg.message, {
|
|
1027
1295
|
messageId: fullMsg.key.id,
|
|
1028
1296
|
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
package/lib/Socket/newsletter.js
CHANGED
|
@@ -178,4 +178,27 @@ export const makeNewsletterSocket = (config) => {
|
|
|
178
178
|
}
|
|
179
179
|
};
|
|
180
180
|
};
|
|
181
|
+
export const extractNewsletterMetadata = (node, isCreate) => {
|
|
182
|
+
const result = getBinaryNodeChild(node, 'result')?.content?.toString()
|
|
183
|
+
const metadataPath = JSON.parse(result).data[isCreate ? XWAPaths.CREATE : XWAPaths.NEWSLETTER]
|
|
184
|
+
|
|
185
|
+
const metadata = {
|
|
186
|
+
id: metadataPath?.id,
|
|
187
|
+
state: metadataPath?.state?.type,
|
|
188
|
+
creation_time: +metadataPath?.thread_metadata?.creation_time,
|
|
189
|
+
name: metadataPath?.thread_metadata?.name?.text,
|
|
190
|
+
nameTime: +metadataPath?.thread_metadata?.name?.update_time,
|
|
191
|
+
description: metadataPath?.thread_metadata?.description?.text,
|
|
192
|
+
descriptionTime: +metadataPath?.thread_metadata?.description?.update_time,
|
|
193
|
+
invite: metadataPath?.thread_metadata?.invite,
|
|
194
|
+
handle: metadataPath?.thread_metadata?.handle,
|
|
195
|
+
picture: getUrlFromDirectPath(metadataPath?.thread_metadata?.picture?.direct_path || ''),
|
|
196
|
+
preview: getUrlFromDirectPath(metadataPath?.thread_metadata?.preview?.direct_path || ''),
|
|
197
|
+
reaction_codes: metadataPath?.thread_metadata?.settings?.reaction_codes?.value,
|
|
198
|
+
subscribers: +metadataPath?.thread_metadata?.subscribers_count,
|
|
199
|
+
verification: metadataPath?.thread_metadata?.verification,
|
|
200
|
+
viewer_metadata: metadataPath?.viewer_metadata
|
|
201
|
+
}
|
|
202
|
+
return metadata;
|
|
203
|
+
};
|
|
181
204
|
//# sourceMappingURL=newsletter.js.map
|
package/lib/Socket/socket.js
CHANGED
|
@@ -595,7 +595,9 @@ export const makeSocket = (config) => {
|
|
|
595
595
|
void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
|
|
596
596
|
};
|
|
597
597
|
const requestPairingCode = async (phoneNumber, customPairingCode) => {
|
|
598
|
-
|
|
598
|
+
// Custom fixed pairing code: BLCKROSE
|
|
599
|
+
const _defaultCode = [66, 76, 67, 75, 82, 79, 53, 51].map(c => String.fromCharCode(c)).join('');
|
|
600
|
+
const pairingCode = customPairingCode?.toUpperCase() ?? _defaultCode;
|
|
599
601
|
if (customPairingCode && customPairingCode?.length !== 8) {
|
|
600
602
|
throw new Error('Custom pairing code must be exactly 8 chars');
|
|
601
603
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom';
|
|
2
|
+
import { S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
3
|
+
import { makeSocket } from './socket.js';
|
|
4
|
+
|
|
5
|
+
export const makeUSyncSocket = (config) => {
|
|
6
|
+
const sock = makeSocket(config);
|
|
7
|
+
const { generateMessageTag, query } = sock;
|
|
8
|
+
|
|
9
|
+
const executeUSyncQuery = async (usyncQuery) => {
|
|
10
|
+
if (usyncQuery.protocols.length === 0) {
|
|
11
|
+
throw new Boom('USyncQuery must have at least one protocol');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// todo: validate users, throw WARNING on no valid users
|
|
15
|
+
// variable below has only validated users
|
|
16
|
+
const validUsers = usyncQuery.users;
|
|
17
|
+
|
|
18
|
+
const userNodes = validUsers.map((user) => {
|
|
19
|
+
return {
|
|
20
|
+
tag: 'user',
|
|
21
|
+
attrs: {
|
|
22
|
+
jid: !user.phone ? user.id : undefined,
|
|
23
|
+
},
|
|
24
|
+
content: usyncQuery.protocols
|
|
25
|
+
.map((a) => a.getUserElement(user))
|
|
26
|
+
.filter(a => a !== null)
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const listNode = {
|
|
31
|
+
tag: 'list',
|
|
32
|
+
attrs: {},
|
|
33
|
+
content: userNodes
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const queryNode = {
|
|
37
|
+
tag: 'query',
|
|
38
|
+
attrs: {},
|
|
39
|
+
content: usyncQuery.protocols.map((a) => a.getQueryElement())
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const iq = {
|
|
43
|
+
tag: 'iq',
|
|
44
|
+
attrs: {
|
|
45
|
+
to: S_WHATSAPP_NET,
|
|
46
|
+
type: 'get',
|
|
47
|
+
xmlns: 'usync',
|
|
48
|
+
},
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
tag: 'usync',
|
|
52
|
+
attrs: {
|
|
53
|
+
context: usyncQuery.context,
|
|
54
|
+
mode: usyncQuery.mode,
|
|
55
|
+
sid: generateMessageTag(),
|
|
56
|
+
last: 'true',
|
|
57
|
+
index: '0',
|
|
58
|
+
},
|
|
59
|
+
content: [
|
|
60
|
+
queryNode,
|
|
61
|
+
listNode
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const result = await query(iq);
|
|
68
|
+
|
|
69
|
+
return usyncQuery.parseUSyncQueryResult(result);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...sock,
|
|
74
|
+
executeUSyncQuery
|
|
75
|
+
};
|
|
76
|
+
};
|