sauruspro-bail 5.0.1 → 5.0.2
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/docs/CHANGELOG.md +35 -0
- package/docs/SPR.md +61 -24
- package/lib/Socket/index.js +38 -4
- package/lib/Socket/socket.js +3 -2
- package/lib/Utils/crypto.js +25 -1
- package/lib/Utils/dinonic-spr.d.ts +63 -16
- package/lib/Utils/dinonic-spr.js +658 -440
- package/lib/Utils/greeting-card.d.ts +15 -0
- package/lib/Utils/greeting-card.js +70 -0
- package/lib/Utils/group-history.d.ts +4 -0
- package/lib/Utils/group-history.js +58 -0
- package/lib/Utils/index.js +7 -1
- package/lib/Utils/meta-ai-bot.d.ts +10 -0
- package/lib/Utils/meta-ai-bot.js +94 -0
- package/lib/Utils/native-backend.d.ts +3 -0
- package/lib/Utils/native-backend.js +56 -0
- package/lib/Utils/plugins.d.ts +20 -0
- package/lib/Utils/plugins.js +78 -0
- package/lib/Utils/presence-profile.d.ts +18 -0
- package/lib/Utils/presence-profile.js +50 -0
- package/package.json +6 -2
- package/README.md +0 -129
- package/docs/NEWSLETTER-STATUS.md +0 -120
package/lib/Utils/dinonic-spr.js
CHANGED
|
@@ -1,521 +1,739 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SPR - Selective / Private Relay for sauruspro-bail
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Engine kirim pesan grup dengan kontrol siapa yang bisa membaca apa:
|
|
5
|
+
* - public : membaca pesan umum (menerima sender key sekali pakai)
|
|
6
|
+
* - personal : membaca pesan khusus miliknya sendiri (enkripsi 1-lawan-1)
|
|
7
|
+
* - hidden : tidak menerima apa-apa
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
9
|
+
* Fitur kirim-per-penerima (privat, broadcast {name}, foto profil per orang,
|
|
10
|
+
* batas target, cache, timeout) diadaptasi dari plugin zapo-js (MIT).
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
* WhatsApp: wa.me/6285188349341
|
|
11
|
-
* Channel: https://whatsapp.com/channel/0029VbCV1ck8fewpdNb2TY2k
|
|
12
|
-
*
|
|
13
|
-
* Copyright (c) 2026 Nixel
|
|
14
|
-
*
|
|
15
|
-
* Permission is granted to use and modify this library
|
|
16
|
-
* for personal or commercial projects.
|
|
17
|
-
*
|
|
18
|
-
* Reuploading, reselling, relicensing, or redistributing
|
|
19
|
-
* this library as a standalone product is prohibited.
|
|
20
|
-
*
|
|
21
|
-
* Do not claim this project as your own original work.
|
|
12
|
+
* Maintained by Saurus / Dinonic.
|
|
22
13
|
*/
|
|
23
14
|
|
|
24
15
|
"use strict";
|
|
25
16
|
|
|
26
|
-
import
|
|
17
|
+
import crypto from 'crypto';
|
|
18
|
+
import { Boom } from '@hapi/boom';
|
|
27
19
|
import { Curve } from './crypto.js';
|
|
28
|
-
import { generateMessageIDV2, generateParticipantHashV2
|
|
20
|
+
import { encodeWAMessage, generateMessageIDV2, generateParticipantHashV2 } from './generics.js';
|
|
29
21
|
import { encodeSignedDeviceIdentity } from './validate-connection.js';
|
|
30
|
-
import { normalizeMessageContent } from './messages.js';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
22
|
+
import { normalizeMessageContent, prepareWAMessageMedia } from './messages.js';
|
|
23
|
+
import { getMessageReportingToken, shouldIncludeReportingToken } from './reporting-utils.js';
|
|
24
|
+
import { isTcTokenExpired, resolveTcTokenJid } from './tc-token-utils.js';
|
|
25
|
+
import {
|
|
26
|
+
isHostedLidUser,
|
|
27
|
+
isHostedPnUser,
|
|
28
|
+
isJidGroup,
|
|
29
|
+
isLidUser,
|
|
30
|
+
jidDecode,
|
|
31
|
+
jidEncode
|
|
32
|
+
} from '../WABinary/index.js';
|
|
33
|
+
|
|
34
|
+
/* -------------------------------------------------------------------------- */
|
|
35
|
+
/* Konfigurasi & teks default */
|
|
36
|
+
/* -------------------------------------------------------------------------- */
|
|
37
|
+
|
|
38
|
+
export const SPR_LIMITS = Object.freeze({
|
|
39
|
+
targets: 100,
|
|
40
|
+
concurrency: 10,
|
|
41
|
+
encryptConcurrency: 8,
|
|
42
|
+
avatarBudgetMs: 10000,
|
|
43
|
+
avatarHardCapMs: 14000,
|
|
44
|
+
avatarFetchMs: 8000,
|
|
45
|
+
cacheSize: 200
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const SPR_TEXT = Object.freeze({
|
|
49
|
+
publicDefault: 'Pesan ini memiliki isi khusus untuk beberapa penerima.',
|
|
50
|
+
templateDefault: 'Halo {name}, ada pesan untukmu.',
|
|
51
|
+
noTarget: 'Tidak ada anggota yang bisa dijadikan target (bot tidak dihitung).',
|
|
52
|
+
emptyPrivate: 'Isi pesan privat kosong.',
|
|
53
|
+
needGroup: 'Perintah ini hanya untuk grup.',
|
|
54
|
+
encryptFailed: 'Enkripsi per-penerima gagal semua. Coba lagi sebentar lagi.'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/* -------------------------------------------------------------------------- */
|
|
58
|
+
/* Utilitas kecil (safe / timeout / pool / cache) */
|
|
59
|
+
/* -------------------------------------------------------------------------- */
|
|
60
|
+
|
|
61
|
+
const attempt = async (task, fallback = null) => {
|
|
62
|
+
try {
|
|
63
|
+
return await task();
|
|
64
|
+
} catch {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
34
68
|
|
|
69
|
+
const withTimeout = async (promise, ms, fallback) => {
|
|
70
|
+
let timer;
|
|
71
|
+
const limit = new Promise((resolve) => {
|
|
72
|
+
timer = setTimeout(() => resolve(fallback), ms);
|
|
73
|
+
timer.unref?.();
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
return await Promise.race([promise, limit]);
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
35
81
|
|
|
82
|
+
const runPool = async (items, limit, task) => {
|
|
83
|
+
const out = new Array(items.length);
|
|
84
|
+
let cursor = 0;
|
|
85
|
+
const worker = async () => {
|
|
86
|
+
while (cursor < items.length) {
|
|
87
|
+
const index = cursor++;
|
|
88
|
+
out[index] = await attempt(() => task(items[index], index));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, worker));
|
|
92
|
+
return out;
|
|
93
|
+
};
|
|
36
94
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
95
|
+
const makeCache = (size) => {
|
|
96
|
+
const store = new Map();
|
|
97
|
+
return {
|
|
98
|
+
has: (k) => store.has(k),
|
|
99
|
+
get: (k) => store.get(k),
|
|
100
|
+
set: (k, v) => {
|
|
101
|
+
store.set(k, v);
|
|
102
|
+
if (store.size > size) store.delete(store.keys().next().value);
|
|
103
|
+
return v;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
};
|
|
46
107
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const pubBuffer = Buffer.concat([Buffer.from([0x05]), Buffer.from(keys.public)]);
|
|
50
|
-
return { privateKey: Buffer.from(keys.private), publicKey: pubBuffer };
|
|
51
|
-
}
|
|
108
|
+
const avatarBytesCache = makeCache(SPR_LIMITS.cacheSize);
|
|
109
|
+
const avatarMediaCache = makeCache(SPR_LIMITS.cacheSize);
|
|
52
110
|
|
|
53
|
-
|
|
54
|
-
const messageKey = crypto.createHmac("sha256", seed).update(Buffer.from([0x01])).digest();
|
|
55
|
-
const salt = Buffer.alloc(32, 0);
|
|
56
|
-
const info = Buffer.from("WhisperGroup");
|
|
57
|
-
const prk = crypto.createHmac("sha256", salt).update(messageKey).digest();
|
|
58
|
-
const t1 = crypto.createHmac("sha256", prk).update(Buffer.concat([info, Buffer.from([0x01])])).digest();
|
|
59
|
-
const t2 = crypto.createHmac("sha256", prk).update(Buffer.concat([t1, info, Buffer.from([0x02])])).digest();
|
|
60
|
-
const okm = Buffer.concat([t1, t2]);
|
|
61
|
-
const iv = okm.slice(0, 16);
|
|
62
|
-
const cipherKey = okm.slice(16, 48);
|
|
63
|
-
return { iv, cipherKey };
|
|
64
|
-
}
|
|
111
|
+
const userOf = (jid) => (jid ? jidDecode(String(jid))?.user || String(jid).split('@')[0].split(':')[0] : '');
|
|
65
112
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Buffer.from(remoteJid, "utf8"),
|
|
77
|
-
Buffer.from(remoteJid, "utf8"),
|
|
78
|
-
Buffer.from("Report Token", "utf8")
|
|
79
|
-
]);
|
|
80
|
-
const prk = crypto.createHmac("sha256", salt).update(seed).digest();
|
|
81
|
-
const t1 = crypto.createHmac("sha256", prk).update(Buffer.concat([info, Buffer.from([0x01])])).digest();
|
|
82
|
-
const mac = crypto.createHmac("sha256", t1).update(payload).digest();
|
|
83
|
-
|
|
84
|
-
const iv = Buffer.from([0x7a, 0x3b, 0x91, 0xc4, 0x2e, 0x1f, 0xd0, 0x63]);
|
|
85
|
-
const vec = Buffer.from([0x29, 0x6b, 0xc3, 0x8a, 0x67, 0x47, 0x95, 0x2f, 0x2c, 0x0a]);
|
|
86
|
-
const nonce = mac.subarray(0, 4);
|
|
87
|
-
const stream = crypto.createHmac("sha256", iv).update(nonce).digest();
|
|
88
|
-
|
|
89
|
-
const raw = Buffer.alloc(10);
|
|
90
|
-
for (let i = 0; i < 10; i++) {
|
|
91
|
-
raw[i] = vec[i] ^ iv[i & 7];
|
|
113
|
+
/* -------------------------------------------------------------------------- */
|
|
114
|
+
/* Sender key sekali pakai (Signal group cipher) */
|
|
115
|
+
/* -------------------------------------------------------------------------- */
|
|
116
|
+
|
|
117
|
+
const varint = (value) => {
|
|
118
|
+
const out = [];
|
|
119
|
+
let v = value >>> 0;
|
|
120
|
+
while (v >= 0x80) {
|
|
121
|
+
out.push((v & 0x7f) | 0x80);
|
|
122
|
+
v >>>= 7;
|
|
92
123
|
}
|
|
124
|
+
out.push(v);
|
|
125
|
+
return Buffer.from(out);
|
|
126
|
+
};
|
|
93
127
|
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
const fieldVarint = (field, value) => Buffer.concat([varint((field << 3) | 0), varint(value)]);
|
|
129
|
+
const fieldBytes = (field, bytes) => Buffer.concat([varint((field << 3) | 2), varint(bytes.length), bytes]);
|
|
96
130
|
|
|
97
|
-
|
|
98
|
-
block[i] ^= stream[i];
|
|
99
|
-
}
|
|
131
|
+
const SIGNAL_VERSION = Buffer.from([0x33]);
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Buat sender key baru yang hanya hidup untuk satu pesan.
|
|
135
|
+
* Karena kuncinya tidak pernah disimpan, hanya device yang diberi SKDM-nya
|
|
136
|
+
* yang bisa membuka pesan grup ini.
|
|
137
|
+
*/
|
|
138
|
+
export const createOneShotSenderKey = () => {
|
|
139
|
+
const keyId = crypto.randomBytes(4).readUInt32BE(0) & 0x7fffffff;
|
|
140
|
+
const chainKey = crypto.randomBytes(32);
|
|
141
|
+
const pair = Curve.generateKeyPair();
|
|
142
|
+
const signingPublic = Buffer.concat([Buffer.from([0x05]), Buffer.from(pair.public)]);
|
|
143
|
+
|
|
144
|
+
const deriveCipher = () => {
|
|
145
|
+
const seed = crypto.createHmac('sha256', chainKey).update(Buffer.from([0x01])).digest();
|
|
146
|
+
const okm = Buffer.from(crypto.hkdfSync('sha256', seed, Buffer.alloc(32), 'WhisperGroup', 48));
|
|
147
|
+
return { iv: okm.subarray(0, 16), key: okm.subarray(16, 48) };
|
|
148
|
+
};
|
|
103
149
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
return {
|
|
151
|
+
keyId,
|
|
152
|
+
/** Isi senderKeyDistributionMessage.axolotlSenderKeyDistributionMessage */
|
|
153
|
+
distribution() {
|
|
154
|
+
const body = Buffer.concat([
|
|
155
|
+
fieldVarint(1, keyId),
|
|
156
|
+
fieldVarint(2, 0),
|
|
157
|
+
fieldBytes(3, chainKey),
|
|
158
|
+
fieldBytes(4, signingPublic)
|
|
159
|
+
]);
|
|
160
|
+
return Buffer.concat([SIGNAL_VERSION, body]);
|
|
161
|
+
},
|
|
162
|
+
/** Enkripsi payload menjadi SenderKeyMessage (iterasi 0) */
|
|
163
|
+
seal(plaintext) {
|
|
164
|
+
const { iv, key } = deriveCipher();
|
|
165
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
166
|
+
const sealed = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
167
|
+
const body = Buffer.concat([fieldVarint(1, keyId), fieldVarint(2, 0), fieldBytes(3, sealed)]);
|
|
168
|
+
const signed = Buffer.concat([SIGNAL_VERSION, body]);
|
|
169
|
+
const signature = Buffer.from(Curve.sign(pair.private, signed));
|
|
170
|
+
return Buffer.concat([signed, signature]);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
};
|
|
117
174
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
175
|
+
/* -------------------------------------------------------------------------- */
|
|
176
|
+
/* Helper tipe pesan */
|
|
177
|
+
/* -------------------------------------------------------------------------- */
|
|
178
|
+
|
|
179
|
+
const mediaTypeOf = (m) => {
|
|
180
|
+
if (!m) return '';
|
|
181
|
+
if (m.imageMessage) return 'image';
|
|
182
|
+
if (m.videoMessage) return m.videoMessage.gifPlayback ? 'gif' : 'video';
|
|
183
|
+
if (m.audioMessage) return m.audioMessage.ptt ? 'ptt' : 'audio';
|
|
184
|
+
if (m.contactMessage) return 'vcard';
|
|
185
|
+
if (m.documentMessage) return 'document';
|
|
186
|
+
if (m.contactsArrayMessage) return 'contact_array';
|
|
187
|
+
if (m.liveLocationMessage) return 'livelocation';
|
|
188
|
+
if (m.stickerMessage || m.stickerPackMessage) return 'sticker';
|
|
189
|
+
if (m.listMessage) return 'list';
|
|
190
|
+
if (m.listResponseMessage) return 'list_response';
|
|
191
|
+
if (m.buttonsResponseMessage) return 'buttons_response';
|
|
192
|
+
if (m.orderMessage) return 'order';
|
|
193
|
+
if (m.productMessage) return 'product';
|
|
194
|
+
if (m.interactiveResponseMessage) return 'native_flow_response';
|
|
195
|
+
if (m.groupInviteMessage) return 'url';
|
|
196
|
+
return '';
|
|
197
|
+
};
|
|
126
198
|
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
if (!
|
|
130
|
-
if (
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
normalizedMessage.pollUpdateMessage
|
|
136
|
-
) return "poll";
|
|
137
|
-
if (normalizedMessage.eventMessage) return "event";
|
|
138
|
-
if (getMediaType(normalizedMessage) !== "") return "media";
|
|
139
|
-
return "text";
|
|
199
|
+
const stanzaTypeOf = (message) => {
|
|
200
|
+
const m = normalizeMessageContent(message);
|
|
201
|
+
if (!m) return 'text';
|
|
202
|
+
if (m.reactionMessage || m.encReactionMessage) return 'reaction';
|
|
203
|
+
if (m.pollCreationMessage || m.pollCreationMessageV2 || m.pollCreationMessageV3 ||
|
|
204
|
+
m.pollCreationMessageV4 || m.pollCreationMessageV5 || m.pollUpdateMessage) return 'poll';
|
|
205
|
+
if (m.eventMessage) return 'event';
|
|
206
|
+
return mediaTypeOf(m) ? 'media' : 'text';
|
|
140
207
|
};
|
|
141
208
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (message.audioMessage) return message.audioMessage.ptt ? "ptt" : "audio";
|
|
146
|
-
if (message.contactMessage) return "vcard";
|
|
147
|
-
if (message.documentMessage) return "document";
|
|
148
|
-
if (message.contactsArrayMessage) return "contact_array";
|
|
149
|
-
if (message.liveLocationMessage) return "livelocation";
|
|
150
|
-
if (message.stickerMessage) return "sticker";
|
|
151
|
-
if (message.listMessage) return "list";
|
|
152
|
-
if (message.listResponseMessage) return "list_response";
|
|
153
|
-
if (message.buttonsResponseMessage) return "buttons_response";
|
|
154
|
-
if (message.orderMessage) return "order";
|
|
155
|
-
if (message.productMessage) return "product";
|
|
156
|
-
if (message.interactiveResponseMessage) return "native_flow_response";
|
|
157
|
-
if (message.groupInviteMessage) return "url";
|
|
158
|
-
return "";
|
|
209
|
+
const decryptFailHidden = (message, hide) => {
|
|
210
|
+
const m = normalizeMessageContent(message);
|
|
211
|
+
return hide === true || !!(m?.pinInChatMessage || m?.reactionMessage);
|
|
159
212
|
};
|
|
160
213
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
useUserDevicesCache,
|
|
165
|
-
useCachedGroupMetadata,
|
|
166
|
-
statusJidList,
|
|
167
|
-
additionalNodes,
|
|
168
|
-
targetMe,
|
|
169
|
-
mode = "exclude",
|
|
170
|
-
target = [],
|
|
171
|
-
hideDecryptFail = true,
|
|
172
|
-
deviceMode = "all"
|
|
173
|
-
} = {}) {
|
|
174
|
-
const meId = conn.authState?.creds?.me?.id || conn.user?.id || "";
|
|
175
|
-
const meLid = conn.authState?.creds?.me?.lid || conn.user?.lid || "";
|
|
176
|
-
|
|
177
|
-
const getLidForPn = async (pnJid) => {
|
|
178
|
-
try {
|
|
179
|
-
return await conn.signalRepository?.lidMapping?.getLIDForPN(pnJid);
|
|
180
|
-
} catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
};
|
|
214
|
+
/* -------------------------------------------------------------------------- */
|
|
215
|
+
/* Identitas (PN <-> LID) dan pencocokan target */
|
|
216
|
+
/* -------------------------------------------------------------------------- */
|
|
184
217
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return await conn.signalRepository?.lidMapping?.getPNForLID(lidJid);
|
|
188
|
-
} catch {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
};
|
|
218
|
+
const participantAliases = (p) =>
|
|
219
|
+
[p?.id, p?.jid, p?.lid, p?.phoneNumber, p?.pn].filter(Boolean).map(userOf).filter(Boolean);
|
|
192
220
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const targetDevice = hasDevice ? (decoded.device !== undefined ? decoded.device : 0) : null;
|
|
203
|
-
|
|
204
|
-
rules.push({
|
|
205
|
-
user: decoded.user,
|
|
206
|
-
device: targetDevice,
|
|
207
|
-
server: decoded.server
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
if (decoded.server === "s.whatsapp.net") {
|
|
211
|
-
const lidJid = await getLidForPn(item);
|
|
212
|
-
if (lidJid) {
|
|
213
|
-
const lidDecoded = jidDecode(lidJid);
|
|
214
|
-
if (lidDecoded) {
|
|
215
|
-
rules.push({
|
|
216
|
-
user: lidDecoded.user,
|
|
217
|
-
device: targetDevice,
|
|
218
|
-
server: lidDecoded.server
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
} else if (decoded.server === "lid") {
|
|
223
|
-
const pnJid = await getPnForLid(item);
|
|
224
|
-
if (pnJid) {
|
|
225
|
-
const pnDecoded = jidDecode(pnJid);
|
|
226
|
-
if (pnDecoded) {
|
|
227
|
-
rules.push({
|
|
228
|
-
user: pnDecoded.user,
|
|
229
|
-
device: targetDevice,
|
|
230
|
-
server: pnDecoded.server
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return rules;
|
|
237
|
-
};
|
|
221
|
+
/** Peta alias user -> himpunan semua alias user milik orang yang sama */
|
|
222
|
+
const buildAliasIndex = (groupData) => {
|
|
223
|
+
const index = new Map();
|
|
224
|
+
for (const p of groupData?.participants || []) {
|
|
225
|
+
const set = new Set(participantAliases(p));
|
|
226
|
+
for (const alias of set) index.set(alias, set);
|
|
227
|
+
}
|
|
228
|
+
return index;
|
|
229
|
+
};
|
|
238
230
|
|
|
239
|
-
|
|
231
|
+
const lookupMapped = async (conn, jid) => {
|
|
232
|
+
const mapping = conn.signalRepository?.lidMapping;
|
|
233
|
+
if (!mapping) return null;
|
|
234
|
+
const decoded = jidDecode(jid);
|
|
235
|
+
if (!decoded) return null;
|
|
236
|
+
if (decoded.server === 's.whatsapp.net') return attempt(() => mapping.getLIDForPN(jid));
|
|
237
|
+
if (decoded.server === 'lid') return attempt(() => mapping.getPNForLID(jid));
|
|
238
|
+
return null;
|
|
239
|
+
};
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
/** Ubah daftar JID menjadi aturan {user, device|null} lengkap dengan alias PN/LID */
|
|
242
|
+
const buildRules = async (conn, list, aliasIndex) => {
|
|
243
|
+
const rules = [];
|
|
244
|
+
for (const raw of Array.isArray(list) ? list : []) {
|
|
245
|
+
const decoded = raw ? jidDecode(raw) : null;
|
|
246
|
+
if (!decoded) continue;
|
|
247
|
+
const device = String(raw).includes(':') ? (decoded.device ?? 0) : null;
|
|
248
|
+
const users = new Set([decoded.user, ...(aliasIndex.get(decoded.user) || [])]);
|
|
249
|
+
const mapped = await lookupMapped(conn, raw);
|
|
250
|
+
if (mapped) users.add(userOf(mapped));
|
|
251
|
+
for (const user of users) rules.push({ user, device });
|
|
252
|
+
}
|
|
253
|
+
return rules;
|
|
254
|
+
};
|
|
244
255
|
|
|
245
|
-
|
|
256
|
+
const matchesRules = (deviceJid, rules) => {
|
|
257
|
+
const d = jidDecode(deviceJid);
|
|
258
|
+
if (!d) return false;
|
|
259
|
+
const device = d.device ?? 0;
|
|
260
|
+
return rules.some((r) => r.user === d.user && (r.device === null || r.device === device));
|
|
261
|
+
};
|
|
246
262
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Normalisasi opsi `personal` menjadi fungsi (deviceUser) -> message | null.
|
|
265
|
+
* Bentuk yang diterima: function({jid,user}), Map, atau object {jid: message}.
|
|
266
|
+
*/
|
|
267
|
+
const buildPersonalResolver = async (conn, personal, aliasIndex) => {
|
|
268
|
+
if (!personal) return () => null;
|
|
269
|
+
if (typeof personal === 'function') {
|
|
270
|
+
const memo = new Map();
|
|
271
|
+
return (ctx) => {
|
|
272
|
+
if (!memo.has(ctx.user)) memo.set(ctx.user, personal(ctx) || null);
|
|
273
|
+
return memo.get(ctx.user);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const entries = personal instanceof Map ? [...personal.entries()] : Object.entries(personal);
|
|
277
|
+
const byUser = new Map();
|
|
278
|
+
for (const [jid, message] of entries) {
|
|
279
|
+
if (!message) continue;
|
|
280
|
+
const users = new Set([userOf(jid), ...(aliasIndex.get(userOf(jid)) || [])]);
|
|
281
|
+
const mapped = await lookupMapped(conn, jid);
|
|
282
|
+
if (mapped) users.add(userOf(mapped));
|
|
283
|
+
for (const user of users) if (user) byUser.set(user, message);
|
|
284
|
+
}
|
|
285
|
+
return (ctx) => byUser.get(ctx.user) || null;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/* -------------------------------------------------------------------------- */
|
|
289
|
+
/* Inti: sPR */
|
|
290
|
+
/* -------------------------------------------------------------------------- */
|
|
291
|
+
|
|
292
|
+
const isHostedDevice = (device) =>
|
|
293
|
+
isHostedLidUser(device.jid) || isHostedPnUser(device.jid) || device.device === 99;
|
|
294
|
+
|
|
295
|
+
const runEncrypt = (conn, groups, extraFor) =>
|
|
296
|
+
runPool(groups, SPR_LIMITS.encryptConcurrency, async ({ jids, message, dsm }) => {
|
|
297
|
+
const extra = extraFor(message);
|
|
298
|
+
return conn.createParticipantNodes(jids, message, extra, dsm);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Kirim pesan dengan kontrol audiens per device.
|
|
303
|
+
*
|
|
304
|
+
* @param conn socket sauruspro-bail
|
|
305
|
+
* @param jid grup / chat / status@broadcast
|
|
306
|
+
* @param message pesan publik (boleh null jika semua penerima punya pesan personal)
|
|
307
|
+
* @param options
|
|
308
|
+
* mode 'exclude' (default) | 'include' -> berlaku untuk pesan publik
|
|
309
|
+
* target JID yang dikenai mode
|
|
310
|
+
* targetMe true/false memaksa device milik sendiri menerima/tidak
|
|
311
|
+
* deviceMode 'all' | 'primary' | 'secondary'
|
|
312
|
+
* personal function({jid,user}) | Map | {jid: message} -> pesan khusus per orang.
|
|
313
|
+
* Penerima personal TIDAK menerima pesan publik.
|
|
314
|
+
* hideDecryptFail sembunyikan placeholder gagal-dekripsi (default true)
|
|
315
|
+
* messageId, additionalAttributes, additionalNodes, useUserDevicesCache,
|
|
316
|
+
* useCachedGroupMetadata, statusJidList
|
|
317
|
+
* @returns messageId
|
|
318
|
+
*/
|
|
319
|
+
export async function sPR(conn, jid, message, options = {}) {
|
|
320
|
+
let {
|
|
321
|
+
messageId: msgId,
|
|
322
|
+
additionalAttributes,
|
|
323
|
+
additionalNodes,
|
|
324
|
+
useUserDevicesCache,
|
|
325
|
+
useCachedGroupMetadata,
|
|
326
|
+
statusJidList,
|
|
327
|
+
targetMe,
|
|
328
|
+
mode = 'exclude',
|
|
329
|
+
target = [],
|
|
330
|
+
personal,
|
|
331
|
+
hideDecryptFail = true,
|
|
332
|
+
deviceMode = 'all'
|
|
333
|
+
} = options;
|
|
334
|
+
|
|
335
|
+
if (!jid) throw new Boom('sPR: jid wajib diisi', { statusCode: 400 });
|
|
336
|
+
if (!message && !personal) throw new Boom('sPR: message atau personal wajib diisi', { statusCode: 400 });
|
|
337
|
+
|
|
338
|
+
const creds = conn.authState?.creds;
|
|
339
|
+
const meId = creds?.me?.id || conn.user?.id || '';
|
|
340
|
+
const meLid = creds?.me?.lid || conn.user?.lid || '';
|
|
341
|
+
const meUsers = new Set([userOf(meId), userOf(meLid)].filter(Boolean));
|
|
342
|
+
const isSelfDevice = (deviceJid) => meUsers.has(userOf(deviceJid));
|
|
343
|
+
const isSenderDevice = (deviceJid) => deviceJid === meId || (!!meLid && deviceJid === meLid);
|
|
344
|
+
|
|
345
|
+
const statusJid = 'status@broadcast';
|
|
346
|
+
const { server } = jidDecode(jid);
|
|
347
|
+
|
|
348
|
+
// Newsletter & pesan peer tidak punya konsep audiens: serahkan ke relay biasa.
|
|
349
|
+
if (server === 'newsletter' || additionalAttributes?.category === 'peer') {
|
|
350
|
+
return conn.relayMessage(jid, message, { messageId: msgId, additionalAttributes, additionalNodes });
|
|
351
|
+
}
|
|
255
352
|
|
|
256
|
-
|
|
257
|
-
const statusJid = "status@broadcast";
|
|
258
|
-
const { user, server } = jidDecode(jid);
|
|
259
|
-
const isNewsletter = server === "newsletter";
|
|
260
|
-
const isGroup = server === "g.us";
|
|
353
|
+
const isGroup = isJidGroup(jid);
|
|
261
354
|
const isStatus = jid === statusJid;
|
|
262
355
|
const isGroupOrStatus = isGroup || isStatus;
|
|
356
|
+
const destinationJid = isStatus ? statusJid : jid;
|
|
357
|
+
|
|
263
358
|
msgId = msgId || generateMessageIDV2(meId);
|
|
264
359
|
useUserDevicesCache = useUserDevicesCache !== false;
|
|
265
360
|
useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus;
|
|
266
|
-
const participants = [];
|
|
267
|
-
const destinationJid = !isStatus ? jid : statusJid;
|
|
268
|
-
const binaryNodeContent = [];
|
|
269
|
-
const devices = [];
|
|
270
|
-
|
|
271
|
-
const reportingToken = !isNewsletter && !isStatus ? deriveReportingTag(message, msgId, destinationJid) : null;
|
|
272
|
-
|
|
273
|
-
const meMsg = { deviceSentMessage: { destinationJid, message }, messageContextInfo: message.messageContextInfo };
|
|
274
|
-
const extraAttrs = {};
|
|
275
|
-
const messages = normalizeMessageContent(message);
|
|
276
|
-
|
|
277
|
-
if (isNewsletter) {
|
|
278
|
-
const patched = conn.patchMessageBeforeSending ? await conn.patchMessageBeforeSending(message, []) : message;
|
|
279
|
-
const bytes = encodeNewsletterMessage(patched);
|
|
280
|
-
binaryNodeContent.push({ tag: "plaintext", attrs: {}, content: bytes });
|
|
281
|
-
const stanza = {
|
|
282
|
-
tag: "message",
|
|
283
|
-
attrs: {
|
|
284
|
-
to: jid,
|
|
285
|
-
id: msgId,
|
|
286
|
-
type: getMessageType(message),
|
|
287
|
-
...(additionalAttributes || {})
|
|
288
|
-
},
|
|
289
|
-
content: binaryNodeContent
|
|
290
|
-
};
|
|
291
|
-
await conn.sendNode(stanza);
|
|
292
|
-
return msgId;
|
|
293
|
-
}
|
|
294
361
|
|
|
295
|
-
|
|
296
|
-
|
|
362
|
+
const baseExtra = {};
|
|
363
|
+
if (decryptFailHidden(message, hideDecryptFail)) baseExtra['decrypt-fail'] = 'hide';
|
|
364
|
+
const publicMedia = mediaTypeOf(normalizeMessageContent(message));
|
|
365
|
+
if (publicMedia) baseExtra.mediatype = publicMedia;
|
|
366
|
+
|
|
367
|
+
const extraFor = (msg) => {
|
|
368
|
+
const media = mediaTypeOf(normalizeMessageContent(msg));
|
|
369
|
+
const extra = { ...baseExtra };
|
|
370
|
+
if (media) extra.mediatype = media;
|
|
371
|
+
else delete extra.mediatype;
|
|
372
|
+
return extra;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/* ---------- kumpulkan device & grup ---------- */
|
|
376
|
+
let groupData;
|
|
377
|
+
let devices = [];
|
|
378
|
+
if (isGroupOrStatus) {
|
|
379
|
+
groupData = useCachedGroupMetadata && conn.cachedGroupMetadata ? await conn.cachedGroupMetadata(jid) : undefined;
|
|
380
|
+
if (!(groupData && Array.isArray(groupData.participants)) && !isStatus) groupData = await conn.groupMetadata(jid);
|
|
381
|
+
const ids = groupData ? groupData.participants.map((p) => p.id) : [];
|
|
382
|
+
if (isStatus && statusJidList) ids.push(...statusJidList);
|
|
383
|
+
if (groupData?.ephemeralDuration > 0) {
|
|
384
|
+
additionalAttributes = { ...additionalAttributes, expiration: String(groupData.ephemeralDuration) };
|
|
385
|
+
}
|
|
386
|
+
devices = await conn.getUSyncDevices(ids, !!useUserDevicesCache, false);
|
|
387
|
+
if (isGroup) {
|
|
388
|
+
additionalAttributes = { ...additionalAttributes, addressing_mode: groupData?.addressingMode || 'lid' };
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const lidChat = isLidUser(jid) && meLid;
|
|
392
|
+
const self = jidEncode(userOf(lidChat ? meLid : meId), lidChat ? 'lid' : 's.whatsapp.net');
|
|
393
|
+
devices = await conn.getUSyncDevices([self, jid], true, false);
|
|
297
394
|
}
|
|
298
395
|
|
|
299
|
-
const
|
|
300
|
-
|
|
396
|
+
const aliasIndex = buildAliasIndex(groupData);
|
|
397
|
+
const rules = await buildRules(conn, target, aliasIndex);
|
|
398
|
+
const personalFor = await buildPersonalResolver(conn, personal, aliasIndex);
|
|
399
|
+
|
|
400
|
+
/* ---------- klasifikasi tiap device ---------- */
|
|
401
|
+
const publicDevices = [];
|
|
402
|
+
const personalByUser = new Map(); // user -> { message, jids[] }
|
|
403
|
+
|
|
404
|
+
for (const device of devices) {
|
|
405
|
+
const deviceJid = device.jid;
|
|
406
|
+
if (isSenderDevice(deviceJid)) continue;
|
|
407
|
+
|
|
408
|
+
const d = jidDecode(deviceJid);
|
|
409
|
+
const deviceId = d?.device ?? 0;
|
|
410
|
+
if (deviceMode === 'primary' && deviceId !== 0) continue;
|
|
411
|
+
if (deviceMode === 'secondary' && deviceId === 0) continue;
|
|
412
|
+
|
|
413
|
+
const mine = isSelfDevice(deviceJid);
|
|
414
|
+
const own = mine && typeof targetMe === 'boolean' ? targetMe : null;
|
|
415
|
+
if (own === false) continue;
|
|
416
|
+
|
|
417
|
+
const personalMsg = own === null ? personalFor({ jid: deviceJid, user: d?.user }) : null;
|
|
418
|
+
if (personalMsg) {
|
|
419
|
+
const slot = personalByUser.get(d.user) || { message: personalMsg, jids: [] };
|
|
420
|
+
slot.jids.push(deviceJid);
|
|
421
|
+
personalByUser.set(d.user, slot);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
301
424
|
|
|
302
|
-
|
|
303
|
-
|
|
425
|
+
if (!message) continue;
|
|
426
|
+
let allowed = mode === 'include' ? matchesRules(deviceJid, rules) : !matchesRules(deviceJid, rules);
|
|
427
|
+
if (own !== null) allowed = own;
|
|
428
|
+
if (allowed) publicDevices.push(deviceJid);
|
|
304
429
|
}
|
|
305
430
|
|
|
431
|
+
const publicRecipients = isGroupOrStatus
|
|
432
|
+
? publicDevices.filter((j) => !isHostedLidUser(j) && !isHostedPnUser(j))
|
|
433
|
+
: publicDevices;
|
|
434
|
+
const personalGroups = [...personalByUser.values()].map((slot) => ({
|
|
435
|
+
jids: slot.jids,
|
|
436
|
+
message: slot.message
|
|
437
|
+
}));
|
|
438
|
+
const personalRecipients = personalGroups.flatMap((g) => g.jids);
|
|
439
|
+
|
|
440
|
+
await conn.assertSessions([...publicRecipients, ...personalRecipients]);
|
|
441
|
+
|
|
442
|
+
/* ---------- enkripsi ---------- */
|
|
443
|
+
const participantNodes = [];
|
|
444
|
+
const content = [];
|
|
445
|
+
let needsIdentity = false;
|
|
446
|
+
let reportingSource = message;
|
|
447
|
+
|
|
306
448
|
if (isGroupOrStatus) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
decItem.user === jidDecode(pId)?.user ||
|
|
323
|
-
(pLid && decItem.user === jidDecode(pLid)?.user) ||
|
|
324
|
-
(pPn && decItem.user === jidDecode(pPn)?.user)
|
|
325
|
-
) {
|
|
326
|
-
for (const candidate of [pId, pLid, pPn]) {
|
|
327
|
-
const decCand = candidate ? jidDecode(candidate) : null;
|
|
328
|
-
if (decCand) {
|
|
329
|
-
parsedTargets.push({
|
|
330
|
-
user: decCand.user,
|
|
331
|
-
device: targetDevice,
|
|
332
|
-
server: decCand.server
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
}
|
|
449
|
+
if (message) {
|
|
450
|
+
const patched = typeof conn.patchMessageBeforeSending === 'function'
|
|
451
|
+
? await conn.patchMessageBeforeSending(message)
|
|
452
|
+
: message;
|
|
453
|
+
if (Array.isArray(patched)) throw new Boom('Per-jid patching is not supported in groups');
|
|
454
|
+
reportingSource = patched;
|
|
455
|
+
const senderKey = createOneShotSenderKey();
|
|
456
|
+
const skmsg = senderKey.seal(encodeWAMessage(patched));
|
|
457
|
+
|
|
458
|
+
if (publicRecipients.length) {
|
|
459
|
+
const skdm = {
|
|
460
|
+
senderKeyDistributionMessage: {
|
|
461
|
+
groupId: destinationJid,
|
|
462
|
+
axolotlSenderKeyDistributionMessage: senderKey.distribution()
|
|
336
463
|
}
|
|
337
|
-
}
|
|
464
|
+
};
|
|
465
|
+
const res = await conn.createParticipantNodes(publicRecipients, skdm, baseExtra);
|
|
466
|
+
participantNodes.push(...res.nodes);
|
|
467
|
+
needsIdentity ||= res.shouldIncludeDeviceIdentity;
|
|
338
468
|
}
|
|
469
|
+
content.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg', ...baseExtra }, content: skmsg });
|
|
339
470
|
}
|
|
340
471
|
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
472
|
+
const results = await runEncrypt(conn, personalGroups, extraFor);
|
|
473
|
+
for (const res of results) {
|
|
474
|
+
if (!res) continue;
|
|
475
|
+
participantNodes.push(...res.nodes);
|
|
476
|
+
needsIdentity ||= res.shouldIncludeDeviceIdentity;
|
|
344
477
|
}
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
const skdm = createSKDM(keyId, 0, chainSeed, signingPubKey);
|
|
359
|
-
|
|
360
|
-
const senderKeyRecipients = [];
|
|
361
|
-
const mePnUserDec = jidDecode(meId);
|
|
362
|
-
const meLidUserDec = meLid ? jidDecode(meLid) : null;
|
|
363
|
-
|
|
364
|
-
for (const device of devices) {
|
|
365
|
-
const deviceJid = device.jid;
|
|
366
|
-
const isOwnDevice = (jidDecode(deviceJid)?.user === mePnUserDec?.user) || (meLidUserDec && jidDecode(deviceJid)?.user === meLidUserDec?.user);
|
|
367
|
-
|
|
368
|
-
let shouldSendRealKey = true;
|
|
369
|
-
if (mode === "include") {
|
|
370
|
-
shouldSendRealKey = isMatch(deviceJid, parsedTargets);
|
|
371
|
-
} else {
|
|
372
|
-
shouldSendRealKey = !isMatch(deviceJid, parsedTargets);
|
|
373
|
-
}
|
|
478
|
+
if (personalGroups.length && !results.some((r) => r?.nodes?.length) && !publicRecipients.length) {
|
|
479
|
+
throw new Boom(SPR_TEXT.encryptFailed, { statusCode: 500 });
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
const meDsm = (msg) => ({
|
|
483
|
+
deviceSentMessage: { destinationJid, message: msg },
|
|
484
|
+
messageContextInfo: msg?.messageContextInfo
|
|
485
|
+
});
|
|
486
|
+
const groups = [];
|
|
487
|
+
if (message && publicRecipients.length) {
|
|
488
|
+
groups.push({ jids: publicRecipients, message, dsm: meDsm(message) });
|
|
489
|
+
}
|
|
490
|
+
for (const g of personalGroups) groups.push({ ...g, dsm: meDsm(g.message) });
|
|
374
491
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
492
|
+
const results = await runEncrypt(conn, groups, extraFor);
|
|
493
|
+
for (const res of results) {
|
|
494
|
+
if (!res) continue;
|
|
495
|
+
participantNodes.push(...res.nodes);
|
|
496
|
+
needsIdentity ||= res.shouldIncludeDeviceIdentity;
|
|
497
|
+
}
|
|
498
|
+
if (groups.length && !participantNodes.length) {
|
|
499
|
+
throw new Boom(SPR_TEXT.encryptFailed, { statusCode: 500 });
|
|
500
|
+
}
|
|
501
|
+
if (participantNodes.length) {
|
|
502
|
+
additionalAttributes = {
|
|
503
|
+
...additionalAttributes,
|
|
504
|
+
phash: generateParticipantHashV2([...publicRecipients, ...personalRecipients])
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
378
508
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (deviceMode === "secondary" && dId === 0) shouldSendRealKey = false;
|
|
509
|
+
if (!participantNodes.length && !content.length) {
|
|
510
|
+
throw new Boom('sPR: tidak ada penerima yang cocok', { statusCode: 400 });
|
|
511
|
+
}
|
|
383
512
|
|
|
384
|
-
|
|
385
|
-
senderKeyRecipients.push(deviceJid);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
513
|
+
if (participantNodes.length) content.unshift({ tag: 'participants', attrs: {}, content: participantNodes });
|
|
388
514
|
|
|
389
|
-
|
|
515
|
+
/* ---------- susun stanza ---------- */
|
|
516
|
+
const typeSource = message || personalGroups[0]?.message;
|
|
517
|
+
const stanza = {
|
|
518
|
+
tag: 'message',
|
|
519
|
+
attrs: { id: msgId, to: destinationJid, type: stanzaTypeOf(typeSource), ...(additionalAttributes || {}) },
|
|
520
|
+
content
|
|
521
|
+
};
|
|
390
522
|
|
|
391
|
-
|
|
392
|
-
|
|
523
|
+
if (needsIdentity && creds?.account) {
|
|
524
|
+
stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(creds.account, true) });
|
|
525
|
+
}
|
|
393
526
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
527
|
+
const hasReportingNode = additionalNodes?.some((n) => n.tag === 'reporting');
|
|
528
|
+
if (message && reportingSource?.messageContextInfo?.messageSecret && shouldIncludeReportingToken(reportingSource) && !hasReportingNode) {
|
|
529
|
+
const node = await attempt(() =>
|
|
530
|
+
getMessageReportingToken(encodeWAMessage(reportingSource), reportingSource, {
|
|
531
|
+
id: msgId,
|
|
532
|
+
fromMe: true,
|
|
533
|
+
remoteJid: destinationJid
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
if (node) stanza.content.push(node);
|
|
537
|
+
}
|
|
404
538
|
|
|
405
|
-
|
|
406
|
-
|
|
539
|
+
if (!isGroupOrStatus) {
|
|
540
|
+
const tcJid = await attempt(() => resolveTcTokenJid(destinationJid, (pn) => conn.signalRepository?.lidMapping?.getLIDForPN(pn)), destinationJid);
|
|
541
|
+
const entry = (await attempt(() => conn.authState.keys.get('tctoken', [tcJid]), {}))?.[tcJid];
|
|
542
|
+
if (entry?.token?.length && !isTcTokenExpired(entry.timestamp)) {
|
|
543
|
+
stanza.content.push({ tag: 'tctoken', attrs: {}, content: entry.token });
|
|
407
544
|
}
|
|
545
|
+
}
|
|
408
546
|
|
|
409
|
-
|
|
410
|
-
} else {
|
|
411
|
-
let ownId = isLidUser(jid) && meLid ? meLid : meId;
|
|
412
|
-
const { user: ownUser } = jidDecode(ownId);
|
|
413
|
-
const targetUserServer = isLidUser(jid) ? "lid" : "s.whatsapp.net";
|
|
414
|
-
devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) });
|
|
415
|
-
if (user !== ownUser) {
|
|
416
|
-
const ownUserServer = isLidUser(jid) ? "lid" : "s.whatsapp.net";
|
|
417
|
-
const ownUserForAddressing = isLidUser(jid) && meLid ? jidDecode(meLid).user : jidDecode(meId).user;
|
|
418
|
-
devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) });
|
|
419
|
-
}
|
|
420
|
-
if (additionalAttributes?.["category"] !== "peer") {
|
|
421
|
-
devices.length = 0;
|
|
422
|
-
const senderIdentity = isLidUser(jid) && meLid ? jidEncode(jidDecode(meLid)?.user, "lid", undefined) : jidEncode(jidDecode(meId)?.user, "s.whatsapp.net", undefined);
|
|
423
|
-
devices.push(...(await conn.getUSyncDevices([senderIdentity, jid], true, false)));
|
|
424
|
-
}
|
|
547
|
+
if (additionalNodes?.length) stanza.content.push(...additionalNodes);
|
|
425
548
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const mePnUserDec = jidDecode(meId);
|
|
430
|
-
const meLidUserDec = meLid ? jidDecode(meLid) : null;
|
|
431
|
-
|
|
432
|
-
for (const device of devices) {
|
|
433
|
-
const deviceJid = device.jid;
|
|
434
|
-
const isExactSenderDevice = deviceJid === meId || (meLid && deviceJid === meLid);
|
|
435
|
-
if (isExactSenderDevice) continue;
|
|
436
|
-
|
|
437
|
-
let shouldSend = true;
|
|
438
|
-
if (mode === "include") {
|
|
439
|
-
shouldSend = isMatch(deviceJid, parsedTargets);
|
|
440
|
-
} else {
|
|
441
|
-
shouldSend = !isMatch(deviceJid, parsedTargets);
|
|
442
|
-
}
|
|
549
|
+
await conn.sendNode(stanza);
|
|
550
|
+
return msgId;
|
|
551
|
+
}
|
|
443
552
|
|
|
444
|
-
|
|
553
|
+
export const selectiveRelay = sPR;
|
|
445
554
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
555
|
+
/* -------------------------------------------------------------------------- */
|
|
556
|
+
/* Anggota grup (adaptasi readMembers zapo) */
|
|
557
|
+
/* -------------------------------------------------------------------------- */
|
|
449
558
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
559
|
+
/**
|
|
560
|
+
* Baca anggota grup yang layak jadi target: bot tidak dihitung, tanpa duplikat,
|
|
561
|
+
* admin didahulukan, dipotong sesuai `limit`.
|
|
562
|
+
*/
|
|
563
|
+
export async function getSPRTargets(conn, groupJid, { limit = SPR_LIMITS.targets, exclude = [] } = {}) {
|
|
564
|
+
const meta = await conn.groupMetadata(groupJid);
|
|
565
|
+
const creds = conn.authState?.creds;
|
|
566
|
+
const skip = new Set(
|
|
567
|
+
[creds?.me?.id, creds?.me?.lid, conn.user?.id, conn.user?.lid, ...exclude].filter(Boolean).map(userOf)
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
const seen = new Set();
|
|
571
|
+
const members = [];
|
|
572
|
+
for (const p of meta?.participants || []) {
|
|
573
|
+
const aliases = participantAliases(p);
|
|
574
|
+
if (!aliases.length || aliases.some((a) => skip.has(a) || seen.has(a))) continue;
|
|
575
|
+
aliases.forEach((a) => seen.add(a));
|
|
576
|
+
members.push(p);
|
|
577
|
+
}
|
|
454
578
|
|
|
455
|
-
|
|
579
|
+
const rank = (p) => (p.admin === 'superadmin' ? 0 : p.admin ? 1 : 2);
|
|
580
|
+
const ordered = [...members].sort((a, b) => rank(a) - rank(b));
|
|
581
|
+
const targets = ordered.slice(0, Math.max(0, limit));
|
|
582
|
+
return { meta, targets, total: members.length, overflow: Math.max(0, members.length - targets.length) };
|
|
583
|
+
}
|
|
456
584
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
585
|
+
/* -------------------------------------------------------------------------- */
|
|
586
|
+
/* Foto profil per penerima (adaptasi avatar loader zapo) */
|
|
587
|
+
/* -------------------------------------------------------------------------- */
|
|
588
|
+
|
|
589
|
+
const fetchAvatarBytes = async (url, timeoutMs) => {
|
|
590
|
+
if (!url) return null;
|
|
591
|
+
if (avatarBytesCache.has(url)) return avatarBytesCache.get(url);
|
|
592
|
+
return attempt(async () => {
|
|
593
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
594
|
+
if (!res.ok) return null;
|
|
595
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
596
|
+
return bytes.length ? avatarBytesCache.set(url, bytes) : null;
|
|
597
|
+
});
|
|
598
|
+
};
|
|
465
599
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
600
|
+
const uploadAvatar = async (conn, bytes) => {
|
|
601
|
+
if (!bytes?.length || typeof conn.waUploadToServer !== 'function') return null;
|
|
602
|
+
const hash = crypto.createHash('md5').update(bytes).digest('hex');
|
|
603
|
+
if (avatarMediaCache.has(hash)) return avatarMediaCache.get(hash);
|
|
604
|
+
return attempt(async () => {
|
|
605
|
+
const prepared = await prepareWAMessageMedia({ image: bytes }, { upload: conn.waUploadToServer, logger: conn.logger });
|
|
606
|
+
return prepared?.imageMessage ? avatarMediaCache.set(hash, prepared.imageMessage) : null;
|
|
607
|
+
});
|
|
608
|
+
};
|
|
475
609
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
binaryNodeContent.push({ tag: "participants", attrs: {}, content: participants });
|
|
482
|
-
}
|
|
610
|
+
const avatarUrlOf = async (conn, person) => {
|
|
611
|
+
const candidates = [person.id, person.phoneNumber, person.pn, person.lid, person.jid].filter(Boolean);
|
|
612
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
613
|
+
const url = await attempt(() => conn.profilePictureUrl(candidate, 'image'));
|
|
614
|
+
if (url) return url;
|
|
483
615
|
}
|
|
616
|
+
return null;
|
|
617
|
+
};
|
|
484
618
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
619
|
+
/**
|
|
620
|
+
* Ambil foto profil beberapa orang sekaligus (konkuren, ada budget & hard cap).
|
|
621
|
+
* @returns Map<user, imageMessage> (kunci: semua alias user orang tsb)
|
|
622
|
+
*/
|
|
623
|
+
export async function loadSPRAvatars(conn, people, {
|
|
624
|
+
concurrency = SPR_LIMITS.concurrency,
|
|
625
|
+
budgetMs = SPR_LIMITS.avatarBudgetMs,
|
|
626
|
+
hardCapMs = SPR_LIMITS.avatarHardCapMs,
|
|
627
|
+
fetchMs = SPR_LIMITS.avatarFetchMs
|
|
628
|
+
} = {}) {
|
|
629
|
+
const map = new Map();
|
|
630
|
+
if (typeof conn.profilePictureUrl !== 'function') return map;
|
|
631
|
+
const started = Date.now();
|
|
632
|
+
|
|
633
|
+
const job = async () => {
|
|
634
|
+
const rows = await runPool(people, concurrency, async (person) => {
|
|
635
|
+
if (Date.now() - started > budgetMs) return null;
|
|
636
|
+
const url = await avatarUrlOf(conn, person);
|
|
637
|
+
const image = await uploadAvatar(conn, await fetchAvatarBytes(url, fetchMs));
|
|
638
|
+
return image ? { keys: participantAliases(person), image } : null;
|
|
639
|
+
});
|
|
640
|
+
for (const row of rows) {
|
|
641
|
+
if (row) for (const key of row.keys) map.set(key, row.image);
|
|
642
|
+
}
|
|
643
|
+
return map;
|
|
489
644
|
};
|
|
490
645
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
646
|
+
await withTimeout(attempt(job), hardCapMs, null);
|
|
647
|
+
return map;
|
|
648
|
+
};
|
|
494
649
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
650
|
+
/* -------------------------------------------------------------------------- */
|
|
651
|
+
/* Pesan personal (gambar profil + caption + mention) */
|
|
652
|
+
/* -------------------------------------------------------------------------- */
|
|
653
|
+
|
|
654
|
+
const personalMessage = (image, caption, mentions) => {
|
|
655
|
+
const contextInfo = { mentionedJid: mentions };
|
|
656
|
+
return image
|
|
657
|
+
? { imageMessage: { ...image, caption, contextInfo } }
|
|
658
|
+
: { extendedTextMessage: { text: caption, contextInfo } };
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
/* -------------------------------------------------------------------------- */
|
|
662
|
+
/* sPRPrivate (setara `!spr publik | privat` di plugin zapo) */
|
|
663
|
+
/* -------------------------------------------------------------------------- */
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Anggota grup lain melihat `publicText`, `targetJid` melihat `privateText`
|
|
667
|
+
* (dengan foto profilnya sendiri bila `avatar` aktif).
|
|
668
|
+
*/
|
|
669
|
+
export async function sPRPrivate(conn, groupJid, targetJid, {
|
|
670
|
+
publicText = SPR_TEXT.publicDefault,
|
|
671
|
+
privateText,
|
|
672
|
+
avatar = true,
|
|
673
|
+
mentionTarget = false,
|
|
674
|
+
...relayOptions
|
|
675
|
+
} = {}) {
|
|
676
|
+
if (!isJidGroup(groupJid)) throw new Boom(SPR_TEXT.needGroup, { statusCode: 400 });
|
|
677
|
+
if (!targetJid) throw new Boom('sPRPrivate: targetJid wajib diisi', { statusCode: 400 });
|
|
678
|
+
if (!privateText?.trim()) throw new Boom(SPR_TEXT.emptyPrivate, { statusCode: 400 });
|
|
679
|
+
|
|
680
|
+
let image = null;
|
|
681
|
+
if (avatar) {
|
|
682
|
+
const avatars = await loadSPRAvatars(conn, [{ id: targetJid }]);
|
|
683
|
+
image = avatars.get(userOf(targetJid)) || null;
|
|
507
684
|
}
|
|
685
|
+
const mentions = mentionTarget ? [targetJid] : [];
|
|
508
686
|
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
687
|
+
const id = await sPR(conn, groupJid, { extendedTextMessage: { text: publicText, contextInfo: { mentionedJid: [] } } }, {
|
|
688
|
+
...relayOptions,
|
|
689
|
+
personal: new Map([[targetJid, personalMessage(image, privateText, mentions)]])
|
|
690
|
+
});
|
|
691
|
+
return { id, avatar: !!image };
|
|
692
|
+
}
|
|
512
693
|
|
|
513
|
-
|
|
694
|
+
/* -------------------------------------------------------------------------- */
|
|
695
|
+
/* sPRBroadcast (setara `!sbpr` / `!sbrp`) */
|
|
696
|
+
/* -------------------------------------------------------------------------- */
|
|
514
697
|
|
|
515
|
-
|
|
698
|
+
/**
|
|
699
|
+
* Kirim pesan yang dipersonalisasi ke tiap anggota: `{name}` jadi @mention
|
|
700
|
+
* orang itu, lengkap dengan foto profilnya. Anggota di luar batas `limit`
|
|
701
|
+
* melihat `publicText`.
|
|
702
|
+
*/
|
|
703
|
+
export async function sPRBroadcast(conn, groupJid, {
|
|
704
|
+
template = SPR_TEXT.templateDefault,
|
|
705
|
+
publicText,
|
|
706
|
+
limit = SPR_LIMITS.targets,
|
|
707
|
+
exclude = [],
|
|
708
|
+
avatar = true,
|
|
709
|
+
...relayOptions
|
|
710
|
+
} = {}) {
|
|
711
|
+
if (!isJidGroup(groupJid)) throw new Boom(SPR_TEXT.needGroup, { statusCode: 400 });
|
|
712
|
+
const info = await getSPRTargets(conn, groupJid, { limit, exclude });
|
|
713
|
+
if (!info.targets.length) throw new Boom(SPR_TEXT.noTarget, { statusCode: 400 });
|
|
714
|
+
|
|
715
|
+
const text = template || SPR_TEXT.templateDefault;
|
|
716
|
+
const hasSlot = text.includes('{name}');
|
|
717
|
+
const avatars = avatar ? await loadSPRAvatars(conn, info.targets) : new Map();
|
|
718
|
+
|
|
719
|
+
const personal = new Map();
|
|
720
|
+
let withAvatar = 0;
|
|
721
|
+
for (const person of info.targets) {
|
|
722
|
+
const id = person.id || person.jid;
|
|
723
|
+
const anchor = `@${userOf(id)}`;
|
|
724
|
+
const caption = hasSlot ? text.replaceAll('{name}', anchor) : `${anchor} ${text}`;
|
|
725
|
+
const image = avatars.get(userOf(id)) || null;
|
|
726
|
+
if (image) withAvatar++;
|
|
727
|
+
personal.set(id, personalMessage(image, caption, [id]));
|
|
728
|
+
}
|
|
516
729
|
|
|
517
|
-
|
|
730
|
+
const pub = publicText || text.replaceAll('{name}', 'semua');
|
|
731
|
+
const id = await sPR(conn, groupJid, { extendedTextMessage: { text: pub, contextInfo: { mentionedJid: [] } } }, {
|
|
732
|
+
...relayOptions,
|
|
733
|
+
personal
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
return { id, targets: info.targets.length, total: info.total, overflow: info.overflow, withAvatar };
|
|
518
737
|
}
|
|
519
738
|
|
|
520
|
-
export const selectiveRelay = sPR;
|
|
521
739
|
export default sPR;
|