sauruspro-bail 5.0.2-alpha.spr.test → 5.0.3
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/README.md +233 -48
- package/docs/CHANGELOG.md +44 -30
- package/docs/SPR.md +1 -1
- package/lib/Socket/index.js +37 -6
- package/lib/Socket/socket.js +80 -50
- package/lib/Utils/crypto.js +25 -1
- 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 +6 -0
- 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/lib/index.js +14 -4
- package/package.json +7 -3
- package/docs/NEWSLETTER-STATUS.md +0 -120
package/lib/Utils/crypto.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto';
|
|
2
2
|
import * as curve from 'libsignal/src/curve.js';
|
|
3
3
|
import { KEY_BUNDLE_TYPE } from '../Defaults/index.js';
|
|
4
|
+
import { getNativeCrypto } from './native-backend.js';
|
|
4
5
|
export { md5, hkdf } from 'whatsapp-rust-bridge';
|
|
5
6
|
// insure browser & node compatibility
|
|
6
7
|
const { subtle } = globalThis.crypto;
|
|
@@ -16,11 +17,34 @@ export const Curve = {
|
|
|
16
17
|
};
|
|
17
18
|
},
|
|
18
19
|
sharedKey: (privateKey, publicKey) => {
|
|
20
|
+
const native = getNativeCrypto().x25519;
|
|
21
|
+
if (native) {
|
|
22
|
+
try {
|
|
23
|
+
return Buffer.from(native(privateKey, publicKey.length === 33 ? publicKey.subarray(1) : publicKey));
|
|
24
|
+
}
|
|
25
|
+
catch { /* fallback ke JS */ }
|
|
26
|
+
}
|
|
19
27
|
const shared = curve.calculateAgreement(generateSignalPubKey(publicKey), privateKey);
|
|
20
28
|
return Buffer.from(shared);
|
|
21
29
|
},
|
|
22
|
-
sign: (privateKey, buf) =>
|
|
30
|
+
sign: (privateKey, buf) => {
|
|
31
|
+
const native = getNativeCrypto().xeddsaSign;
|
|
32
|
+
if (native) {
|
|
33
|
+
try {
|
|
34
|
+
return Buffer.from(native(privateKey, buf));
|
|
35
|
+
}
|
|
36
|
+
catch { /* fallback ke JS */ }
|
|
37
|
+
}
|
|
38
|
+
return curve.calculateSignature(privateKey, buf);
|
|
39
|
+
},
|
|
23
40
|
verify: (pubKey, message, signature) => {
|
|
41
|
+
const native = getNativeCrypto().xeddsaVerify;
|
|
42
|
+
if (native) {
|
|
43
|
+
try {
|
|
44
|
+
return !!native(pubKey.length === 33 ? pubKey.subarray(1) : pubKey, message, signature);
|
|
45
|
+
}
|
|
46
|
+
catch { /* fallback ke JS */ }
|
|
47
|
+
}
|
|
24
48
|
try {
|
|
25
49
|
curve.verifySignature(generateSignalPubKey(pubKey), message, signature);
|
|
26
50
|
return true;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface GreetingCardOptions {
|
|
2
|
+
text?: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
greeting?: string;
|
|
5
|
+
imageUrl?: string;
|
|
6
|
+
contactUrl?: string;
|
|
7
|
+
thumbnail?: Buffer;
|
|
8
|
+
sourceApp?: string;
|
|
9
|
+
ref?: string;
|
|
10
|
+
mentions?: string[];
|
|
11
|
+
/** override field externalAdReply apa pun */
|
|
12
|
+
adReply?: Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
export declare const buildGreetingCardContent: (opts?: GreetingCardOptions) => any;
|
|
15
|
+
export declare const sendGreetingCard: (sock: any, jid: string, opts?: GreetingCardOptions, sendOptions?: Record<string, any>) => Promise<any>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Greeting Card (/buss) - diadaptasi dari zapo-js.
|
|
2
|
+
// Pesan teks + contextInfo.externalAdReply. Kartu sapaan hanya terlihat di WhatsApp Business;
|
|
3
|
+
// user WhatsApp biasa hanya melihat teks chat.
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
import { generateWAMessageFromContent } from './messages.js';
|
|
6
|
+
|
|
7
|
+
export const buildGreetingCardContent = (opts = {}) => {
|
|
8
|
+
const {
|
|
9
|
+
text = '',
|
|
10
|
+
title = '',
|
|
11
|
+
greeting = 'Hi Business',
|
|
12
|
+
imageUrl = '',
|
|
13
|
+
contactUrl = '',
|
|
14
|
+
thumbnail,
|
|
15
|
+
sourceApp = 'myapp',
|
|
16
|
+
ref = 'referencia',
|
|
17
|
+
mentions,
|
|
18
|
+
adReply = {}
|
|
19
|
+
} = opts;
|
|
20
|
+
return {
|
|
21
|
+
extendedTextMessage: {
|
|
22
|
+
text,
|
|
23
|
+
contextInfo: {
|
|
24
|
+
...(mentions?.length ? { mentionedJid: mentions } : {}),
|
|
25
|
+
externalAdReply: {
|
|
26
|
+
title,
|
|
27
|
+
body: greeting,
|
|
28
|
+
mediaType: 1,
|
|
29
|
+
thumbnailUrl: imageUrl,
|
|
30
|
+
thumbnail: thumbnail ?? Buffer.alloc(0),
|
|
31
|
+
sourceType: 'whatsapp',
|
|
32
|
+
sourceId: String(randomBytes(4).readUInt32BE(0)),
|
|
33
|
+
sourceUrl: contactUrl,
|
|
34
|
+
containsAutoReply: false,
|
|
35
|
+
renderLargerThumbnail: false,
|
|
36
|
+
showAdAttribution: false,
|
|
37
|
+
ctwaClid: `clid${randomBytes(8).toString('hex')}`,
|
|
38
|
+
ref,
|
|
39
|
+
clickToWhatsappCall: true,
|
|
40
|
+
adContextPreviewDismissed: false,
|
|
41
|
+
sourceApp,
|
|
42
|
+
automatedGreetingMessageShown: true,
|
|
43
|
+
greetingMessageBody: greeting,
|
|
44
|
+
ctaPayload: `payload${randomBytes(8).toString('hex')}`,
|
|
45
|
+
disableNudge: true,
|
|
46
|
+
originalImageUrl: imageUrl,
|
|
47
|
+
wtwaAdFormat: false,
|
|
48
|
+
containsCtwaFlowsAutoReply: false,
|
|
49
|
+
agmThumbnailStrategy: 0,
|
|
50
|
+
agmTitleStrategy: 0,
|
|
51
|
+
agmSubtitleStrategy: 0,
|
|
52
|
+
agmHeaderInteractionStrategy: 0,
|
|
53
|
+
...adReply
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const sendGreetingCard = async (sock, jid, opts = {}, sendOptions = {}) => {
|
|
61
|
+
const content = buildGreetingCardContent(opts);
|
|
62
|
+
const userJid = sock.authState?.creds?.me?.id ?? sock.user?.id;
|
|
63
|
+
const msg = generateWAMessageFromContent(jid, content, {
|
|
64
|
+
userJid,
|
|
65
|
+
...(sendOptions.messageId ? { messageId: sendOptions.messageId } : {}),
|
|
66
|
+
...(sendOptions.quoted ? { quoted: sendOptions.quoted } : {})
|
|
67
|
+
});
|
|
68
|
+
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id, ...sendOptions });
|
|
69
|
+
return msg;
|
|
70
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const GROUP_HISTORY_FIELDS: Readonly<{ MESSAGES: 1; OUT_OF_WINDOW_PINNED_MESSAGES: 4 }>;
|
|
2
|
+
export declare function encodeGroupHistoryBundle(messages: any[], outOfWindowPinnedMessages?: any[]): Promise<{ compressed: Buffer; encoded: Uint8Array }>;
|
|
3
|
+
export declare function decodeGroupHistoryBundle(blob: Uint8Array): Promise<any>;
|
|
4
|
+
export declare function streamGroupHistoryBundle(inflated: Uint8Array, onMessage: (msg: any, isOutOfWindowPinned: boolean) => void | Promise<void>, maxRecordBytes?: number): Promise<void>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Group History Bundle - diadaptasi dari zapo-js (MIT).
|
|
2
|
+
// Serialize/deserialize riwayat pesan grup yang dibagikan ke anggota baru.
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { deflate, unzip } from 'zlib';
|
|
5
|
+
import $protobuf from 'protobufjs/minimal.js';
|
|
6
|
+
import { proto } from '../../WAProto/index.js';
|
|
7
|
+
|
|
8
|
+
const deflateAsync = promisify(deflate);
|
|
9
|
+
const unzipAsync = promisify(unzip);
|
|
10
|
+
|
|
11
|
+
export const GROUP_HISTORY_FIELDS = Object.freeze({
|
|
12
|
+
MESSAGES: 1,
|
|
13
|
+
OUT_OF_WINDOW_PINNED_MESSAGES: 4
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Encode pesan -> payload `GroupHistory` terkompres (zlib). */
|
|
17
|
+
export async function encodeGroupHistoryBundle(messages, outOfWindowPinnedMessages) {
|
|
18
|
+
const encoded = proto.GroupHistory.encode({
|
|
19
|
+
messages,
|
|
20
|
+
outOfWindowPinnedMessages: outOfWindowPinnedMessages?.length ? outOfWindowPinnedMessages : undefined
|
|
21
|
+
}).finish();
|
|
22
|
+
const compressed = await deflateAsync(encoded);
|
|
23
|
+
return { compressed, encoded };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Inflate + decode blob bundle hasil download. */
|
|
27
|
+
export async function decodeGroupHistoryBundle(blob) {
|
|
28
|
+
const inflated = await unzipAsync(blob);
|
|
29
|
+
return proto.GroupHistory.decode(inflated);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Baca bundle (sudah di-inflate) satu pesan per satu pesan, tanpa decode seluruh bundle sekaligus.
|
|
34
|
+
* onMessage(webMessageInfo, isOutOfWindowPinned)
|
|
35
|
+
*/
|
|
36
|
+
export async function streamGroupHistoryBundle(inflated, onMessage, maxRecordBytes) {
|
|
37
|
+
const reader = $protobuf.Reader.create(inflated);
|
|
38
|
+
while (reader.pos < reader.len) {
|
|
39
|
+
const tag = reader.uint32();
|
|
40
|
+
const fieldNumber = tag >>> 3;
|
|
41
|
+
const wireType = tag & 7;
|
|
42
|
+
if (wireType === 2) {
|
|
43
|
+
const bytes = reader.bytes();
|
|
44
|
+
if (maxRecordBytes !== undefined && bytes.length > maxRecordBytes) {
|
|
45
|
+
throw new Error(`group history record exceeds ${maxRecordBytes} bytes`);
|
|
46
|
+
}
|
|
47
|
+
if (fieldNumber === GROUP_HISTORY_FIELDS.MESSAGES) {
|
|
48
|
+
await onMessage(proto.WebMessageInfo.decode(bytes), false);
|
|
49
|
+
}
|
|
50
|
+
else if (fieldNumber === GROUP_HISTORY_FIELDS.OUT_OF_WINDOW_PINNED_MESSAGES) {
|
|
51
|
+
await onMessage(proto.WebMessageInfo.decode(bytes), true);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
reader.skipType(wireType);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
package/lib/Utils/index.js
CHANGED
|
@@ -23,6 +23,12 @@ export * from './stanza-ack.js';
|
|
|
23
23
|
export * from './message-composer.js';
|
|
24
24
|
export * from './newsletter-status.js';
|
|
25
25
|
export { sPR, selectiveRelay, sPRPrivate, sPRBroadcast, getSPRTargets, loadSPRAvatars, createOneShotSenderKey, SPR_LIMITS, SPR_TEXT } from './dinonic-spr.js';
|
|
26
|
+
export * from './group-history.js';
|
|
27
|
+
export { definePlugin, makePluginHost } from './plugins.js';
|
|
28
|
+
export { makePresence, makeProfile } from './presence-profile.js';
|
|
29
|
+
export { resolveNativeBackend, getNativeCrypto, isNativeCryptoActive } from './native-backend.js';
|
|
30
|
+
export * as MetaAIBot from './meta-ai-bot.js';
|
|
31
|
+
export { sendGreetingCard, buildGreetingCardContent } from './greeting-card.js';
|
|
26
32
|
//# sourceMappingURL=index.js.map
|
|
27
33
|
export * from './a2ui.js';
|
|
28
34
|
export * from './html-app.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const BOT_SERVER: 'bot';
|
|
2
|
+
export declare const BOT_KNOWN_JIDS: Readonly<Record<'META_AI_PN' | 'META_AI_FBID' | 'META_AI_TEE_FBID' | 'META_AI_SIDECHAT_FBID' | 'MANUS_FBID' | 'HATCH_FBID', string>>;
|
|
3
|
+
export declare const isBotServerJid: (jid: string) => boolean;
|
|
4
|
+
export declare function genBotMsgSecret(parentMessageSecret: Uint8Array): Buffer;
|
|
5
|
+
export declare function deriveBotChunkKey(input: { botMsgSecret: Uint8Array; saltId: string; targetSenderJid: string; authorJid: string }): Buffer;
|
|
6
|
+
export declare function decryptBotChunk(input: { parentMessageSecret: Uint8Array; saltId: string; targetSenderJid: string; authorJid: string; encIv: Uint8Array; encPayload: Uint8Array }): Buffer;
|
|
7
|
+
export declare function attachBotMetadata(message: any, input?: { personaId?: string; invokerJid?: string; capabilities?: any[] }): any;
|
|
8
|
+
export declare function attachBotThread(message: any, input: { remoteJid: string; aiThreadId: string; threadType?: number }): any;
|
|
9
|
+
export declare function extractInvokedBotJid(message: any): string | null;
|
|
10
|
+
export declare function buildBotInvokeProtoCopy(message: any, botMessageSecret?: Uint8Array): any;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Meta AI Bot helpers - diadaptasi dari zapo-js (MIT).
|
|
2
|
+
// Metadata bot, thread AI, dekripsi chunk balasan bot, deteksi bot yang di-invoke.
|
|
3
|
+
import { hkdfSync } from 'crypto';
|
|
4
|
+
import { proto } from '../../WAProto/index.js';
|
|
5
|
+
import { aesDecryptGCM } from './crypto.js';
|
|
6
|
+
|
|
7
|
+
export const BOT_SERVER = 'bot';
|
|
8
|
+
export const BOT_HKDF_INFO = Buffer.from('Bot Message');
|
|
9
|
+
export const BOT_MSG_SECRET_BYTES = 32;
|
|
10
|
+
export const BOT_KNOWN_JIDS = Object.freeze({
|
|
11
|
+
META_AI_PN: '13135550002@s.whatsapp.net',
|
|
12
|
+
META_AI_FBID: `867051314767696@${BOT_SERVER}`,
|
|
13
|
+
META_AI_TEE_FBID: `1273596044787272@${BOT_SERVER}`,
|
|
14
|
+
META_AI_SIDECHAT_FBID: `867051314767696555@${BOT_SERVER}`,
|
|
15
|
+
MANUS_FBID: `1807055946647696@${BOT_SERVER}`,
|
|
16
|
+
HATCH_FBID: `1807055946647697@${BOT_SERVER}`
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const isBotServerJid = (jid) => typeof jid === 'string' && jid.endsWith(`@${BOT_SERVER}`);
|
|
20
|
+
|
|
21
|
+
const hkdf = (ikm, info) => Buffer.from(hkdfSync('sha256', ikm, Buffer.alloc(0), info, BOT_MSG_SECRET_BYTES));
|
|
22
|
+
|
|
23
|
+
export function genBotMsgSecret(parentMessageSecret) {
|
|
24
|
+
const ikm = Buffer.from(parentMessageSecret);
|
|
25
|
+
if (ikm.length !== 32) {
|
|
26
|
+
throw new Error(`bot parent message secret must be 32 bytes (got ${ikm.length})`);
|
|
27
|
+
}
|
|
28
|
+
return hkdf(ikm, BOT_HKDF_INFO);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function deriveBotChunkKey({ botMsgSecret, saltId, targetSenderJid, authorJid }) {
|
|
32
|
+
if (!saltId) throw new Error('bot chunk salt id must be a non-empty string');
|
|
33
|
+
if (!targetSenderJid) throw new Error('bot chunk target sender jid must be a non-empty string');
|
|
34
|
+
if (!authorJid) throw new Error('bot chunk author jid must be a non-empty string');
|
|
35
|
+
return hkdf(Buffer.from(botMsgSecret), Buffer.from(saltId + targetSenderJid + authorJid));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Dekripsi chunk balasan bot (AES-GCM, AAD = saltId + NUL + authorJid). */
|
|
39
|
+
export function decryptBotChunk({ parentMessageSecret, saltId, targetSenderJid, authorJid, encIv, encPayload }) {
|
|
40
|
+
const botMsgSecret = genBotMsgSecret(parentMessageSecret);
|
|
41
|
+
const key = deriveBotChunkKey({ botMsgSecret, saltId, targetSenderJid, authorJid });
|
|
42
|
+
const aad = Buffer.from(`${saltId}\u0000${authorJid}`);
|
|
43
|
+
return aesDecryptGCM(Buffer.from(encPayload), key, Buffer.from(encIv), aad);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function attachBotMetadata(message, { personaId, invokerJid, capabilities } = {}) {
|
|
47
|
+
if (!personaId && !invokerJid && !capabilities) return message;
|
|
48
|
+
const botMetadata = {};
|
|
49
|
+
if (personaId) botMetadata.personaId = personaId;
|
|
50
|
+
if (invokerJid) botMetadata.invokerJid = invokerJid;
|
|
51
|
+
if (capabilities?.length) botMetadata.capabilityMetadata = { capabilities };
|
|
52
|
+
return {
|
|
53
|
+
...message,
|
|
54
|
+
messageContextInfo: { ...(message.messageContextInfo ?? {}), botMetadata }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function attachBotThread(message, { remoteJid, aiThreadId, threadType }) {
|
|
59
|
+
const ctx = message.messageContextInfo ?? {};
|
|
60
|
+
const type = threadType ?? proto.AIThreadInfo.AIThreadClientInfo.AIThreadType.DEFAULT;
|
|
61
|
+
return {
|
|
62
|
+
...message,
|
|
63
|
+
messageContextInfo: {
|
|
64
|
+
...ctx,
|
|
65
|
+
threadId: [
|
|
66
|
+
{
|
|
67
|
+
threadType: proto.ThreadID.ThreadType.AI_THREAD,
|
|
68
|
+
threadKey: { remoteJid, fromMe: true, id: aiThreadId }
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
botMetadata: { ...(ctx.botMetadata ?? {}), botThreadInfo: { clientInfo: { type } } }
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** JID bot yang di-mention di dalam botInvokeMessage, atau null. */
|
|
77
|
+
export function extractInvokedBotJid(message) {
|
|
78
|
+
const inner = message.botInvokeMessage?.message;
|
|
79
|
+
if (!inner) return null;
|
|
80
|
+
const mentioned = inner.extendedTextMessage?.contextInfo?.mentionedJid ??
|
|
81
|
+
inner.imageMessage?.contextInfo?.mentionedJid ??
|
|
82
|
+
inner.videoMessage?.contextInfo?.mentionedJid ??
|
|
83
|
+
inner.documentMessage?.contextInfo?.mentionedJid;
|
|
84
|
+
if (!mentioned) return null;
|
|
85
|
+
return mentioned.find(isBotServerJid) ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildBotInvokeProtoCopy(message, botMessageSecret) {
|
|
89
|
+
const { messageContextInfo, ...rest } = message;
|
|
90
|
+
if (!messageContextInfo && !botMessageSecret) return rest;
|
|
91
|
+
const next = { ...(messageContextInfo ?? {}), messageSecret: undefined };
|
|
92
|
+
if (botMessageSecret) next.botMessageSecret = botMessageSecret;
|
|
93
|
+
return { ...rest, messageContextInfo: next };
|
|
94
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const resolveNativeBackend: () => any | null;
|
|
2
|
+
export declare const getNativeCrypto: () => { x25519: ((priv: Uint8Array, pub: Uint8Array) => Uint8Array) | null; xeddsaSign: ((priv: Uint8Array, msg: Uint8Array) => Uint8Array) | null; xeddsaVerify: ((pub: Uint8Array, msg: Uint8Array, sig: Uint8Array) => boolean) | null };
|
|
3
|
+
export declare const isNativeCryptoActive: () => boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Akselerator kripto native opsional (@zapo-js/native, MIT) - diadaptasi dari zapo-js.
|
|
2
|
+
// Mempercepat X25519 ECDH dan XEdDSA sign/verify. Otomatis dipakai jika paket terpasang,
|
|
3
|
+
// jika tidak ada atau gagal load -> fallback ke implementasi JS (libsignal), tanpa perubahan kode.
|
|
4
|
+
// Env: SAURUS_NATIVE_BACKEND (atau ZAPO_NATIVE_BACKEND) = auto | napi | wasm | js | none
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const WASM_MODULE = '@zapo-js/native/wasm/pkg/zapo_native_wasm.js';
|
|
10
|
+
const WASM_BINARY = '@zapo-js/native/wasm/pkg/zapo_native_wasm_bg.wasm';
|
|
11
|
+
|
|
12
|
+
const isAbsence = (error) => {
|
|
13
|
+
const code = error?.code;
|
|
14
|
+
const message = String(error?.message ?? '');
|
|
15
|
+
return (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND' || message.includes('Cannot find native binding')) &&
|
|
16
|
+
(message.includes('@zapo-js/native') || message.includes('zapo_native_wasm') || message.includes('Cannot find native binding'));
|
|
17
|
+
};
|
|
18
|
+
const warn = (backend, error) => process.emitWarning('failed to load native crypto accelerator, falling back to JS', {
|
|
19
|
+
code: 'SAURUS_NATIVE_LOAD_FAILED',
|
|
20
|
+
detail: `${backend}: ${error?.message ?? error}`
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
let cached;
|
|
24
|
+
export const resolveNativeBackend = () => {
|
|
25
|
+
if (cached !== undefined) return cached;
|
|
26
|
+
const mode = (process.env.SAURUS_NATIVE_BACKEND ?? process.env.ZAPO_NATIVE_BACKEND ?? 'auto').toLowerCase();
|
|
27
|
+
cached = null;
|
|
28
|
+
if (mode === 'js' || mode === 'none') return cached;
|
|
29
|
+
if (mode === 'napi' || mode === 'auto') {
|
|
30
|
+
try { cached = require('@zapo-js/native'); }
|
|
31
|
+
catch (error) { if (!isAbsence(error)) warn('napi', error); }
|
|
32
|
+
}
|
|
33
|
+
if (cached === null && (mode === 'wasm' || mode === 'auto')) {
|
|
34
|
+
try {
|
|
35
|
+
const mod = require(WASM_MODULE);
|
|
36
|
+
mod.initSync({ module: readFileSync(require.resolve(WASM_BINARY)) });
|
|
37
|
+
cached = mod;
|
|
38
|
+
}
|
|
39
|
+
catch (error) { if (!isAbsence(error)) warn('wasm', error); }
|
|
40
|
+
}
|
|
41
|
+
return cached;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Fungsi native yang tersedia (atau null jika memakai JS). */
|
|
45
|
+
export const getNativeCrypto = () => {
|
|
46
|
+
const mod = resolveNativeBackend();
|
|
47
|
+
return {
|
|
48
|
+
x25519: mod && typeof mod.x25519ScalarMult === 'function' ? mod.x25519ScalarMult : null,
|
|
49
|
+
xeddsaSign: mod && typeof mod.xeddsaSign === 'function' ? mod.xeddsaSign : null,
|
|
50
|
+
xeddsaVerify: mod && typeof mod.xeddsaVerify === 'function' ? mod.xeddsaVerify : null
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export const isNativeCryptoActive = () => {
|
|
54
|
+
const n = getNativeCrypto();
|
|
55
|
+
return !!(n.x25519 || n.xeddsaSign || n.xeddsaVerify);
|
|
56
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PluginContext {
|
|
2
|
+
sock: any; config: any; logger?: any; ev: any;
|
|
3
|
+
on: (...args: any[]) => void; off: (...args: any[]) => void;
|
|
4
|
+
registerDispose: (fn: () => void | Promise<void>) => void;
|
|
5
|
+
extend: (members: Record<string, any>) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface PluginDefinition<T = any> {
|
|
8
|
+
id: string;
|
|
9
|
+
exposeAs?: string;
|
|
10
|
+
setup: (ctx: PluginContext) => T;
|
|
11
|
+
dispose?: (instance: T, ctx: PluginContext) => void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare const definePlugin: <T = any>(input: PluginDefinition<T>) => Readonly<PluginDefinition<T>>;
|
|
14
|
+
export declare const makePluginHost: (sock: any, config?: any) => {
|
|
15
|
+
use: (plugin: PluginDefinition) => any;
|
|
16
|
+
removePlugin: (id: string) => Promise<boolean>;
|
|
17
|
+
hasPlugin: (id: string) => boolean;
|
|
18
|
+
listPlugins: () => string[];
|
|
19
|
+
disposeAll: () => Promise<void>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Plugin system: sock.use(plugin) - terinspirasi defineWaClientPlugin milik zapo-js (MIT).
|
|
2
|
+
// Plugin = { id, exposeAs?, setup(ctx), dispose?(instance, ctx) }.
|
|
3
|
+
// - exposeAs: hasil setup() dipasang di sock[exposeAs]
|
|
4
|
+
// - ctx.extend({ nama: nilai }): tambah banyak member ke sock (otomatis dicabut saat dispose)
|
|
5
|
+
export const definePlugin = (input) => {
|
|
6
|
+
if (!input || typeof input.id !== 'string' || !input.id) {
|
|
7
|
+
throw new Error('plugin requires a non-empty string id');
|
|
8
|
+
}
|
|
9
|
+
if (typeof input.setup !== 'function') {
|
|
10
|
+
throw new Error(`plugin ${input.id} requires a setup function`);
|
|
11
|
+
}
|
|
12
|
+
return Object.freeze({ id: input.id, exposeAs: input.exposeAs, setup: input.setup, dispose: input.dispose });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const makePluginHost = (sock, config = {}) => {
|
|
16
|
+
const installed = new Map(); // id -> { plugin, instance, ctx, disposers, members }
|
|
17
|
+
const disabled = new Set(config.disablePlugins ?? []);
|
|
18
|
+
const define = (name, descriptor) => {
|
|
19
|
+
Object.defineProperty(sock, name, { configurable: true, enumerable: true, ...descriptor });
|
|
20
|
+
};
|
|
21
|
+
const use = (input) => {
|
|
22
|
+
const plugin = definePlugin(input);
|
|
23
|
+
if (disabled.has(plugin.id)) {
|
|
24
|
+
return sock;
|
|
25
|
+
}
|
|
26
|
+
if (installed.has(plugin.id)) {
|
|
27
|
+
throw new Error(`duplicate plugin id: ${plugin.id}`);
|
|
28
|
+
}
|
|
29
|
+
const members = [];
|
|
30
|
+
const disposers = [];
|
|
31
|
+
const claim = (name) => {
|
|
32
|
+
if (name in sock) {
|
|
33
|
+
throw new Error(`plugin ${plugin.id}: member "${name}" collides with an existing socket member`);
|
|
34
|
+
}
|
|
35
|
+
members.push(name);
|
|
36
|
+
};
|
|
37
|
+
const ctx = {
|
|
38
|
+
sock,
|
|
39
|
+
config,
|
|
40
|
+
logger: config.logger?.child ? config.logger.child({ plugin: plugin.id }) : config.logger,
|
|
41
|
+
ev: sock.ev,
|
|
42
|
+
on: (...a) => sock.ev.on(...a),
|
|
43
|
+
off: (...a) => sock.ev.off(...a),
|
|
44
|
+
registerDispose: (fn) => disposers.push(fn),
|
|
45
|
+
extend: (obj) => {
|
|
46
|
+
for (const [name, value] of Object.entries(obj)) {
|
|
47
|
+
claim(name);
|
|
48
|
+
define(name, { value, writable: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const instance = plugin.setup(ctx);
|
|
53
|
+
if (plugin.exposeAs) {
|
|
54
|
+
claim(plugin.exposeAs);
|
|
55
|
+
define(plugin.exposeAs, { value: instance, writable: true });
|
|
56
|
+
}
|
|
57
|
+
installed.set(plugin.id, { plugin, instance, ctx, disposers, members });
|
|
58
|
+
return sock;
|
|
59
|
+
};
|
|
60
|
+
const remove = async (id) => {
|
|
61
|
+
const entry = installed.get(id);
|
|
62
|
+
if (!entry) return false;
|
|
63
|
+
installed.delete(id);
|
|
64
|
+
for (let i = entry.disposers.length - 1; i >= 0; i--) {
|
|
65
|
+
try { await entry.disposers[i](); } catch { /* abaikan */ }
|
|
66
|
+
}
|
|
67
|
+
try { await entry.plugin.dispose?.(entry.instance, entry.ctx); } catch { /* abaikan */ }
|
|
68
|
+
for (const name of entry.members) delete sock[name];
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
use,
|
|
73
|
+
removePlugin: remove,
|
|
74
|
+
hasPlugin: (id) => installed.has(id),
|
|
75
|
+
listPlugins: () => [...installed.keys()],
|
|
76
|
+
disposeAll: async () => { for (const id of [...installed.keys()].reverse()) await remove(id); }
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface PresenceApi {
|
|
2
|
+
setOnline(): Promise<void>; setOffline(): Promise<void>;
|
|
3
|
+
typing(jid: string): Promise<void>; recording(jid: string): Promise<void>; paused(jid: string): Promise<void>;
|
|
4
|
+
subscribe(jid: string): Promise<void>;
|
|
5
|
+
typeFor(jid: string, ms?: number): Promise<void>;
|
|
6
|
+
withTyping<T>(jid: string, fn: () => Promise<T> | T, kind?: 'composing' | 'recording'): Promise<T>;
|
|
7
|
+
}
|
|
8
|
+
export interface ProfileApi {
|
|
9
|
+
getPicture(jid: string, type?: 'preview' | 'image', timeoutMs?: number): Promise<string | undefined>;
|
|
10
|
+
setPicture(jid: string, content: any, dimensions?: { width: number; height: number }): Promise<void>;
|
|
11
|
+
removePicture(jid: string): Promise<void>;
|
|
12
|
+
getStatus(...jids: string[]): Promise<any>;
|
|
13
|
+
setStatus(text: string): Promise<void>;
|
|
14
|
+
setName(name: string): Promise<void>;
|
|
15
|
+
username: { get(): Promise<any>; set(username: string, options?: any): Promise<any>; delete(): Promise<any>; check(username: string): Promise<any>; recommendations(source?: string): Promise<any>; setPin(pin: string): Promise<any>; find(username: string, pin?: string): Promise<any> };
|
|
16
|
+
}
|
|
17
|
+
export declare const makePresence: (sock: any) => PresenceApi;
|
|
18
|
+
export declare const makeProfile: (sock: any) => ProfileApi;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Presence & Profile coordinator - API rapi di atas fungsi socket yang sudah ada.
|
|
2
|
+
// Terinspirasi client.presence / client.profile milik zapo-js (MIT).
|
|
3
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
|
|
5
|
+
export const makePresence = (sock) => {
|
|
6
|
+
const send = (type, jid) => sock.sendPresenceUpdate(type, jid);
|
|
7
|
+
return {
|
|
8
|
+
setOnline: () => send('available'),
|
|
9
|
+
setOffline: () => send('unavailable'),
|
|
10
|
+
typing: (jid) => send('composing', jid),
|
|
11
|
+
recording: (jid) => send('recording', jid),
|
|
12
|
+
paused: (jid) => send('paused', jid),
|
|
13
|
+
subscribe: (jid) => sock.presenceSubscribe(jid),
|
|
14
|
+
/** composing -> tunggu ms -> paused */
|
|
15
|
+
async typeFor(jid, ms = 1000) {
|
|
16
|
+
await send('composing', jid);
|
|
17
|
+
await sleep(ms);
|
|
18
|
+
await send('paused', jid);
|
|
19
|
+
},
|
|
20
|
+
/** Tampilkan "mengetik" selama fn berjalan, lalu paused (tetap paused walau fn error). */
|
|
21
|
+
async withTyping(jid, fn, kind = 'composing') {
|
|
22
|
+
await send(kind, jid);
|
|
23
|
+
try {
|
|
24
|
+
return await fn();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await send('paused', jid).catch(() => { });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const makeProfile = (sock) => ({
|
|
34
|
+
getPicture: (jid, type = 'preview', timeoutMs) => sock.profilePictureUrl(jid, type, timeoutMs),
|
|
35
|
+
setPicture: (jid, content, dimensions) => sock.updateProfilePicture(jid, content, dimensions),
|
|
36
|
+
removePicture: (jid) => sock.removeProfilePicture(jid),
|
|
37
|
+
getStatus: (...jids) => sock.fetchStatus(...jids),
|
|
38
|
+
setStatus: (text) => sock.updateProfileStatus(text),
|
|
39
|
+
setName: (name) => sock.updateProfileName(name),
|
|
40
|
+
/** Username (jika modul username tersedia di socket) */
|
|
41
|
+
username: {
|
|
42
|
+
get: () => sock.getMyUsername?.(),
|
|
43
|
+
set: (username, options) => sock.setUsername(username, options),
|
|
44
|
+
delete: () => sock.deleteUsername(),
|
|
45
|
+
check: (username) => sock.checkUsername(username),
|
|
46
|
+
recommendations: (source) => sock.getUsernameRecommendations(source),
|
|
47
|
+
setPin: (pin) => sock.setUsernamePin(pin),
|
|
48
|
+
find: (username, pin) => sock.findUserByUsername?.(username, pin)
|
|
49
|
+
}
|
|
50
|
+
});
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import makeWASocket from './Socket/index.js';
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
// Versi diambil dari package.json supaya selalu sinkron.
|
|
7
|
+
export const packageVersion = require("../package.json").version;
|
|
3
8
|
|
|
4
9
|
const ascii = [
|
|
5
10
|
"⠀⠀⠀⠀⢀⡠⠤⠔⢲⢶⡖⠒⠤⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀",
|
|
@@ -22,13 +27,16 @@ const ascii = [
|
|
|
22
27
|
const info = [
|
|
23
28
|
{ label: "", value: "Baileys Saurus Pro", title: true },
|
|
24
29
|
{ label: "", value: "" },
|
|
25
|
-
{ label: "✦", value: "
|
|
26
|
-
{ label: "✦", value: "
|
|
27
|
-
{ label: "✦", value: "Rich Message Support" },
|
|
30
|
+
{ label: "✦", value: "Pairing Code & QR (kode acak, sudah diperbaiki)" },
|
|
31
|
+
{ label: "✦", value: "Rich Message (AIRich, Carousel, Button)" },
|
|
28
32
|
{ label: "✦", value: "SPR (Selective Relay)" },
|
|
29
33
|
{ label: "✦", value: "Newsletter Status" },
|
|
34
|
+
{ label: "✦", value: "Greeting Card /buss" },
|
|
35
|
+
{ label: "✦", value: "Plugin System (sock.use)" },
|
|
36
|
+
{ label: "✦", value: "Presence & Profile API" },
|
|
37
|
+
{ label: "✦", value: "Native Crypto (opsional)" },
|
|
30
38
|
{ label: "", value: "" },
|
|
31
|
-
{ label: "Version", value:
|
|
39
|
+
{ label: "Version", value: `v${packageVersion}` },
|
|
32
40
|
{ label: "Owner", value: "Saurus / Dinonic" },
|
|
33
41
|
{ label: "Telegram", value: "t.me/lordsaurus" },
|
|
34
42
|
{ label: "WA Owner", value: "+62 895-6390-43495" }
|
|
@@ -45,6 +53,7 @@ const infoLines = info.map(({ label, value, title }) => {
|
|
|
45
53
|
});
|
|
46
54
|
|
|
47
55
|
const rows = Math.max(ascii.length, infoLines.length);
|
|
56
|
+
if (!process.env.SAURUS_NO_BANNER) {
|
|
48
57
|
console.log("\n");
|
|
49
58
|
for (let i = 0; i < rows; i++) {
|
|
50
59
|
const art = ascii[i] || "".padEnd(24);
|
|
@@ -52,6 +61,7 @@ for (let i = 0; i < rows; i++) {
|
|
|
52
61
|
console.log(" " + purple(art) + " " + text);
|
|
53
62
|
}
|
|
54
63
|
console.log("\n");
|
|
64
|
+
}
|
|
55
65
|
export * from '../WAProto/index.js';
|
|
56
66
|
export * from './Utils/index.js';
|
|
57
67
|
export * from './Types/index.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sauruspro-bail",
|
|
3
|
-
"version": "5.0.
|
|
4
|
-
"description": "Sauruspro Baileys -
|
|
3
|
+
"version": "5.0.3",
|
|
4
|
+
"description": "Sauruspro Baileys - WhatsApp Web API (Baileys) dengan SPR (Selective Relay), Newsletter Status, Rich Messages, Greeting Card, plugin system, dan pairing code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"baileys",
|
|
7
7
|
"whatsapp",
|
|
@@ -96,7 +96,8 @@
|
|
|
96
96
|
"audio-decode": "^2.1.3",
|
|
97
97
|
"jimp": "^1.6.1",
|
|
98
98
|
"link-preview-js": "^3.0.0",
|
|
99
|
-
"sharp": "*"
|
|
99
|
+
"sharp": "*",
|
|
100
|
+
"@zapo-js/native": "*"
|
|
100
101
|
},
|
|
101
102
|
"resolutions": {
|
|
102
103
|
"ajv@npm:^6.12.4": "^6.14.0",
|
|
@@ -136,6 +137,9 @@
|
|
|
136
137
|
},
|
|
137
138
|
"link-preview-js": {
|
|
138
139
|
"optional": true
|
|
140
|
+
},
|
|
141
|
+
"@zapo-js/native": {
|
|
142
|
+
"optional": true
|
|
139
143
|
}
|
|
140
144
|
},
|
|
141
145
|
"packageManager": "yarn@4.9.2",
|