@blckrose/baileys 1.0.5 → 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/Signal/libsignal.js +19 -7
- package/lib/Socket/groups.js +76 -5
- package/lib/Socket/messages-send.js +284 -16
- package/lib/Socket/socket.js +3 -1
- package/lib/Utils/messages-media.js +69 -13
- package/lib/Utils/messages.js +27 -8
- package/lib/index.js +18 -0
- package/package.json +2 -1
package/lib/Signal/libsignal.js
CHANGED
|
@@ -3,6 +3,15 @@ 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
|
+
};
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
15
|
import { generateSignalPubKey } from '../Utils/index.js';
|
|
7
16
|
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary/index.js';
|
|
8
17
|
import { SenderKeyName } from './Group/sender-key-name.js';
|
|
@@ -85,13 +94,16 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
85
94
|
}
|
|
86
95
|
async function doDecrypt() {
|
|
87
96
|
let result;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
try {
|
|
98
|
+
switch (type) {
|
|
99
|
+
case 'pkmsg':
|
|
100
|
+
result = await session.decryptPreKeyWhisperMessage(ciphertext);
|
|
101
|
+
break;
|
|
102
|
+
case 'msg':
|
|
103
|
+
result = await session.decryptWhisperMessage(ciphertext);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
95
107
|
}
|
|
96
108
|
return result;
|
|
97
109
|
}
|
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
|
}),
|
|
@@ -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/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
|
}
|
|
@@ -38,7 +38,7 @@ export const getRawMediaUploadData = async (media, mediaType, logger) => {
|
|
|
38
38
|
const fileWriteStream = createWriteStream(filePath);
|
|
39
39
|
let fileLength = 0;
|
|
40
40
|
try {
|
|
41
|
-
for await (const data of
|
|
41
|
+
for await (const data of finalStream) {
|
|
42
42
|
fileLength += data.length;
|
|
43
43
|
hasher.update(data);
|
|
44
44
|
if (!fileWriteStream.write(data)) {
|
|
@@ -308,9 +308,53 @@ export const getHttpStream = async (url, options = {}) => {
|
|
|
308
308
|
// @ts-ignore Node18+ Readable.fromWeb exists
|
|
309
309
|
return response.body instanceof Readable ? response.body : Readable.fromWeb(response.body);
|
|
310
310
|
};
|
|
311
|
-
|
|
311
|
+
|
|
312
|
+
// ── convertToOpusBuffer (ported from elaina) ─────────────────────────────────
|
|
313
|
+
const convertToOpusBuffer = (buffer, logger) => new Promise((resolve, reject) => {
|
|
314
|
+
const args = [
|
|
315
|
+
'-i', 'pipe:0',
|
|
316
|
+
'-c:a', 'libopus',
|
|
317
|
+
'-b:a', '64k',
|
|
318
|
+
'-vbr', 'on',
|
|
319
|
+
'-compression_level', '10',
|
|
320
|
+
'-frame_duration', '20',
|
|
321
|
+
'-application', 'voip',
|
|
322
|
+
'-f', 'ogg',
|
|
323
|
+
'pipe:1'
|
|
324
|
+
];
|
|
325
|
+
const ffmpeg = spawn('ffmpeg', args);
|
|
326
|
+
const chunks = [];
|
|
327
|
+
ffmpeg.stdin.write(buffer);
|
|
328
|
+
ffmpeg.stdin.end();
|
|
329
|
+
ffmpeg.stdout.on('data', chunk => chunks.push(chunk));
|
|
330
|
+
ffmpeg.stderr.on('data', () => {});
|
|
331
|
+
ffmpeg.on('close', code => {
|
|
332
|
+
if (code === 0) resolve(Buffer.concat(chunks));
|
|
333
|
+
else reject(new Error(`FFmpeg Opus conversion exited with code ${code}`));
|
|
334
|
+
});
|
|
335
|
+
ffmpeg.on('error', err => reject(err));
|
|
336
|
+
});
|
|
337
|
+
// ── End convertToOpusBuffer ───────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
export const encryptedStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
|
|
312
340
|
const { stream, type } = await getStream(media, opts);
|
|
313
341
|
logger?.debug('fetched media stream');
|
|
342
|
+
let finalStream = stream;
|
|
343
|
+
let opusConverted = false;
|
|
344
|
+
// Auto-convert to Opus if PTT or forceOpus
|
|
345
|
+
if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
|
|
346
|
+
try {
|
|
347
|
+
const buf = await toBuffer(finalStream);
|
|
348
|
+
const opusBuf = await convertToOpusBuffer(buf, logger);
|
|
349
|
+
finalStream = toReadable(opusBuf);
|
|
350
|
+
opusConverted = true;
|
|
351
|
+
logger?.debug('converted audio to Opus for PTT');
|
|
352
|
+
} catch (error) {
|
|
353
|
+
logger?.error('failed to convert audio to Opus, fallback to original');
|
|
354
|
+
const { stream: newStream } = await getStream(media, opts);
|
|
355
|
+
finalStream = newStream;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
314
358
|
const mediaKey = Crypto.randomBytes(32);
|
|
315
359
|
const { cipherKey, iv, macKey } = await getMediaKeys(mediaKey, mediaType);
|
|
316
360
|
const encFilePath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2() + '-enc');
|
|
@@ -375,7 +419,8 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
|
|
|
375
419
|
mac,
|
|
376
420
|
fileEncSha256,
|
|
377
421
|
fileSha256,
|
|
378
|
-
fileLength
|
|
422
|
+
fileLength,
|
|
423
|
+
opusConverted
|
|
379
424
|
};
|
|
380
425
|
}
|
|
381
426
|
catch (error) {
|
|
@@ -774,9 +819,22 @@ const MEDIA_RETRY_STATUS_MAP = {
|
|
|
774
819
|
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
|
|
775
820
|
};
|
|
776
821
|
|
|
777
|
-
export const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts } = {}) => {
|
|
822
|
+
export const prepareStream = async (media, mediaType, { logger, saveOriginalFileIfRequired, opts, isPtt, forceOpus } = {}) => {
|
|
778
823
|
const { stream, type } = await getStream(media, opts);
|
|
779
824
|
logger?.debug('fetched media stream');
|
|
825
|
+
let opusConverted = false;
|
|
826
|
+
let buffer = await toBuffer(stream);
|
|
827
|
+
// Auto-convert to Opus if PTT or forceOpus
|
|
828
|
+
if (mediaType === 'audio' && (isPtt === true || forceOpus === true)) {
|
|
829
|
+
try {
|
|
830
|
+
const opusBuf = await convertToOpusBuffer(buffer, logger);
|
|
831
|
+
buffer = opusBuf;
|
|
832
|
+
opusConverted = true;
|
|
833
|
+
logger?.debug('converted audio to Opus for newsletter PTT');
|
|
834
|
+
} catch (e) {
|
|
835
|
+
logger?.error('failed to convert audio for newsletter PTT');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
780
838
|
const encFilePath = join(tmpdir(), mediaType + generateMessageID() + '-plain');
|
|
781
839
|
const encFileWriteStream = createWriteStream(encFilePath);
|
|
782
840
|
let originalFilePath;
|
|
@@ -790,20 +848,18 @@ export const prepareStream = async (media, mediaType, { logger, saveOriginalFile
|
|
|
790
848
|
let fileLength = 0;
|
|
791
849
|
const hashCtx = createHashCrypto('sha256');
|
|
792
850
|
try {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
851
|
+
// Use buffer (possibly opus-converted) directly
|
|
852
|
+
fileLength = buffer.length;
|
|
853
|
+
hashCtx.update(buffer);
|
|
854
|
+
encFileWriteStream.write(buffer);
|
|
855
|
+
if (originalFileStream) {
|
|
856
|
+
originalFileStream.write(buffer);
|
|
800
857
|
}
|
|
801
858
|
const fileSha256 = hashCtx.digest();
|
|
802
859
|
encFileWriteStream.end();
|
|
803
860
|
originalFileStream?.end?.call(originalFileStream);
|
|
804
|
-
stream.destroy();
|
|
805
861
|
logger?.debug('prepared plain stream successfully');
|
|
806
|
-
return { mediaKey: undefined, originalFilePath, encFilePath, mac: undefined, fileEncSha256: undefined, fileSha256, fileLength };
|
|
862
|
+
return { mediaKey: undefined, originalFilePath, encFilePath, mac: undefined, fileEncSha256: undefined, fileSha256, fileLength, opusConverted };
|
|
807
863
|
} catch (error) {
|
|
808
864
|
encFileWriteStream.destroy();
|
|
809
865
|
originalFileStream?.destroy?.call(originalFileStream);
|
package/lib/Utils/messages.js
CHANGED
|
@@ -8,7 +8,7 @@ import { WAMessageStatus, WAProto } from '../Types/index.js';
|
|
|
8
8
|
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
9
9
|
import { sha256 } from './crypto.js';
|
|
10
10
|
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
|
|
11
|
-
import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
|
|
11
|
+
import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
|
|
12
12
|
import { shouldIncludeReportingToken } from './reporting-utils.js';
|
|
13
13
|
const MIMETYPE_MAP = {
|
|
14
14
|
image: 'image/jpeg',
|
|
@@ -86,6 +86,10 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
86
86
|
if (!uploadData.mimetype) {
|
|
87
87
|
uploadData.mimetype = MIMETYPE_MAP[mediaType];
|
|
88
88
|
}
|
|
89
|
+
// Auto-set PTT for audio if not explicitly defined
|
|
90
|
+
if (mediaType === 'audio' && typeof uploadData.ptt === 'undefined') {
|
|
91
|
+
uploadData.ptt = true;
|
|
92
|
+
}
|
|
89
93
|
if (cacheableKey) {
|
|
90
94
|
const mediaBuff = await options.mediaCache.get(cacheableKey);
|
|
91
95
|
if (mediaBuff) {
|
|
@@ -133,15 +137,23 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
133
137
|
}
|
|
134
138
|
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
|
|
135
139
|
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
|
|
136
|
-
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
|
|
137
|
-
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio'
|
|
138
|
-
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
|
|
139
|
-
const
|
|
140
|
+
const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor);
|
|
141
|
+
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio';
|
|
142
|
+
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation || requiresWaveformProcessing;
|
|
143
|
+
const isNewsletterUpload = !!options.newsletter;
|
|
144
|
+
const streamFn = isNewsletterUpload ? prepareStream : encryptedStream;
|
|
145
|
+
const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength, opusConverted } = await streamFn(uploadData.media, options.mediaTypeOverride || mediaType, {
|
|
140
146
|
logger,
|
|
141
147
|
saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
|
|
142
|
-
opts: options.options
|
|
148
|
+
opts: options.options,
|
|
149
|
+
isPtt: uploadData.ptt,
|
|
150
|
+
forceOpus: (mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'))
|
|
143
151
|
});
|
|
144
|
-
|
|
152
|
+
// Update mimetype if converted to opus
|
|
153
|
+
if (mediaType === 'audio' && opusConverted) {
|
|
154
|
+
uploadData.mimetype = 'audio/ogg; codecs=opus';
|
|
155
|
+
}
|
|
156
|
+
const fileEncSha256B64 = (isNewsletterUpload ? fileSha256 : (fileEncSha256 ?? fileSha256)).toString('base64');
|
|
145
157
|
const [{ mediaUrl, directPath }] = await Promise.all([
|
|
146
158
|
(async () => {
|
|
147
159
|
const result = await options.upload(encFilePath, {
|
|
@@ -169,7 +181,14 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
169
181
|
logger?.debug('computed audio duration');
|
|
170
182
|
}
|
|
171
183
|
if (requiresWaveformProcessing) {
|
|
172
|
-
|
|
184
|
+
try {
|
|
185
|
+
uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
logger?.warn('Failed to generate waveform, using fallback');
|
|
188
|
+
}
|
|
189
|
+
if (!uploadData.waveform) {
|
|
190
|
+
uploadData.waveform = new Uint8Array([0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99,0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99]);
|
|
191
|
+
}
|
|
173
192
|
logger?.debug('processed waveform');
|
|
174
193
|
}
|
|
175
194
|
if (requiresAudioBackground) {
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// ── Blckrose Baileys Banner ───────────────────────────────────────────────────
|
|
4
|
+
const _blck = {
|
|
5
|
+
line : chalk.hex('#8B5CF6')('━'.repeat(60)),
|
|
6
|
+
title: chalk.bold.hex('#A78BFA')('⬡ Blckrose Baileys ') + chalk.hex('#6D28D9')('| Modified Edition'),
|
|
7
|
+
pair : chalk.hex('#7C3AED')('⌘ Pairing Code : ') + chalk.bold.white('BLCKRO53'),
|
|
8
|
+
repo : chalk.hex('#7C3AED')('❖ Report Error : ') + chalk.bold.cyan('@Blckrose0'),
|
|
9
|
+
note : chalk.dim.hex('#A78BFA')(' Laporkan error baileys ke kontak di atas'),
|
|
10
|
+
};
|
|
11
|
+
console.log(_blck.line);
|
|
12
|
+
console.log(_blck.title);
|
|
13
|
+
console.log(_blck.pair);
|
|
14
|
+
console.log(_blck.repo);
|
|
15
|
+
console.log(_blck.note);
|
|
16
|
+
console.log(_blck.line);
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
1
19
|
import makeWASocket from './Socket/index.js';
|
|
2
20
|
export * from '../WAProto/index.js';
|
|
3
21
|
export { proto } from '../WAProto/index.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blckrose/baileys",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"description": "A WebSockets library for interacting with WhatsApp Web",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"@hapi/boom": "^9.1.3",
|
|
27
27
|
"async-mutex": "^0.5.0",
|
|
28
28
|
"cache-manager": "^5.7.6",
|
|
29
|
+
"chalk": "^5.6.2",
|
|
29
30
|
"libsignal": "git+https://github.com/whiskeysockets/libsignal-node",
|
|
30
31
|
"lru-cache": "^11.1.0",
|
|
31
32
|
"music-metadata": "^11.7.0",
|