@vanzxy/baileys 1.6.8 → 1.7.0
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/NOTICE.md +111 -0
- package/README.md +138 -9
- package/lib/Framework/Bot.js +209 -0
- package/lib/Framework/Context.js +90 -0
- package/lib/Framework/MediaManager.js +152 -0
- package/lib/Framework/SessionManager.js +34 -0
- package/lib/Framework/StatsManager.js +120 -0
- package/lib/Framework/Store/SQLiteStore.js +74 -0
- package/lib/Framework/index.js +11 -0
- package/lib/Socket/index.js +4 -2
- package/lib/Socket/username.js +153 -0
- package/lib/Utils/MessageBuilder.js +67 -1
- package/lib/Utils/MessageBuilder_d.ts +133 -1
- package/lib/VoIP/worker-bootstrap.js +8 -8
- package/lib/index.js +3 -0
- package/package.json +39 -35
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
// Vanz@Port (from @queenanya/baileys Framework, originally Baileys PR #2710
|
|
7
|
+
// by LuferOS) --- media conversion helpers: image/video → WebP stickers
|
|
8
|
+
// (with optional packname/author EXIF), audio → OGG Opus voice notes.
|
|
9
|
+
//
|
|
10
|
+
// Vanz@Fix vs upstream: original hard-`require()`'d `node-webpmux` and
|
|
11
|
+
// pulled in `ffmpeg-static` (bundles a per-platform ffmpeg binary) alongside
|
|
12
|
+
// `fluent-ffmpeg`. `fluent-ffmpeg` is already an optional peer dep in this
|
|
13
|
+
// fork, lazy-loaded via the getFfmpeg() pattern established in
|
|
14
|
+
// Utils/MessageBuilder.js (15-08-26) — reused verbatim here instead of
|
|
15
|
+
// re-introducing a static import or a second ffmpeg-binary dependency.
|
|
16
|
+
// `node-webpmux` is lazy-loaded the same way, and only when packname/author
|
|
17
|
+
// metadata is actually requested.
|
|
18
|
+
let _ffmpeg;
|
|
19
|
+
const getFfmpeg = async () => {
|
|
20
|
+
if (_ffmpeg === undefined) {
|
|
21
|
+
_ffmpeg = await import('fluent-ffmpeg').then((m) => m.default ?? m).catch(() => null);
|
|
22
|
+
}
|
|
23
|
+
if (!_ffmpeg)
|
|
24
|
+
throw new Error('fluent-ffmpeg is required for sticker/voice-note conversion. Install it with: npm i fluent-ffmpeg');
|
|
25
|
+
return _ffmpeg;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let _webpmux;
|
|
29
|
+
const getWebpmux = async () => {
|
|
30
|
+
if (_webpmux === undefined) {
|
|
31
|
+
_webpmux = await import('node-webpmux').then((m) => m.default ?? m).catch(() => null);
|
|
32
|
+
}
|
|
33
|
+
if (!_webpmux)
|
|
34
|
+
throw new Error('node-webpmux is required for sticker packname/author metadata. Install it with: npm i node-webpmux (conversion without metadata does not need this package)');
|
|
35
|
+
return _webpmux;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class MediaManager {
|
|
39
|
+
/** Generate a temp file path with a given extension */
|
|
40
|
+
static getTempFile(ext) {
|
|
41
|
+
return path.join(os.tmpdir(), `baileys-fw-${randomBytes(8).toString('hex')}.${ext}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert image or video to a WebP sticker buffer.
|
|
46
|
+
* Applies packname/author EXIF metadata when provided (requires the
|
|
47
|
+
* optional `node-webpmux` peer dependency — only loaded if metadata is given).
|
|
48
|
+
*/
|
|
49
|
+
static async convertToSticker(inputPathOrBuffer, metadata) {
|
|
50
|
+
const ffmpegLib = await getFfmpeg();
|
|
51
|
+
const tempInput = MediaManager.getTempFile('in');
|
|
52
|
+
const tempOutput = MediaManager.getTempFile('webp');
|
|
53
|
+
try {
|
|
54
|
+
if (Buffer.isBuffer(inputPathOrBuffer)) {
|
|
55
|
+
await fs.promises.writeFile(tempInput, inputPathOrBuffer);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
await fs.promises.copyFile(inputPathOrBuffer, tempInput);
|
|
59
|
+
}
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
ffmpegLib(tempInput)
|
|
62
|
+
.outputOptions([
|
|
63
|
+
'-vcodec', 'libwebp',
|
|
64
|
+
'-vf', 'scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=white@0',
|
|
65
|
+
'-loop', '0',
|
|
66
|
+
'-preset', 'default',
|
|
67
|
+
'-an', '-vsync', '0',
|
|
68
|
+
'-t', '00:00:05'
|
|
69
|
+
])
|
|
70
|
+
.output(tempOutput)
|
|
71
|
+
.on('end', () => resolve())
|
|
72
|
+
.on('error', (err) => reject(err))
|
|
73
|
+
.run();
|
|
74
|
+
});
|
|
75
|
+
const webpBuffer = await fs.promises.readFile(tempOutput);
|
|
76
|
+
if (metadata?.packname || metadata?.author) {
|
|
77
|
+
// EXIF header: fixed 22-byte TIFF/IFD preamble (magic + one IFD
|
|
78
|
+
// entry pointing at the WhatsApp tag 0x0741) followed by a
|
|
79
|
+
// little-endian payload length and a little-endian offset to
|
|
80
|
+
// where the payload begins (always 22, the header's own size).
|
|
81
|
+
const exifJson = JSON.stringify({
|
|
82
|
+
'sticker-pack-id': `com.vanzxy.sticker.${randomBytes(4).toString('hex')}`,
|
|
83
|
+
'sticker-pack-name': metadata.packname || '',
|
|
84
|
+
'sticker-pack-publisher': metadata.author || '',
|
|
85
|
+
emojis: ['🤖']
|
|
86
|
+
});
|
|
87
|
+
const exifBytes = Buffer.from(exifJson, 'utf8');
|
|
88
|
+
const exifHeader = Buffer.from([
|
|
89
|
+
0x49, 0x49, 0x2a, 0x00, // TIFF byte order (little-endian) + magic number
|
|
90
|
+
0x08, 0x00, 0x00, 0x00, // offset to first IFD
|
|
91
|
+
0x01, 0x00, // number of IFD entries
|
|
92
|
+
0x41, 0x57, 0x07, 0x00, // tag 0x0741 (WhatsApp), type 0x0007 (undefined)
|
|
93
|
+
0x00, 0x00, 0x00, 0x00, // payload length placeholder — filled below
|
|
94
|
+
0x16, 0x00, 0x00, 0x00 // offset to payload data (fixed: 22 = header size)
|
|
95
|
+
]);
|
|
96
|
+
exifHeader.writeUInt32LE(exifBytes.length, 14);
|
|
97
|
+
const fullExif = Buffer.concat([exifHeader, exifBytes]);
|
|
98
|
+
const webpmux = await getWebpmux();
|
|
99
|
+
const img = new webpmux.Image();
|
|
100
|
+
await img.load(webpBuffer);
|
|
101
|
+
img.exif = fullExif;
|
|
102
|
+
return await img.save(null);
|
|
103
|
+
}
|
|
104
|
+
return webpBuffer;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await fs.promises.unlink(tempInput).catch(() => { });
|
|
108
|
+
await fs.promises.unlink(tempOutput).catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convert audio to OGG Opus voice note format.
|
|
114
|
+
* Mono, 16kHz, VOIP application mode — required by WA for PTT playback.
|
|
115
|
+
*/
|
|
116
|
+
static async convertToVoiceNote(inputPathOrBuffer) {
|
|
117
|
+
const ffmpegLib = await getFfmpeg();
|
|
118
|
+
const tempInput = MediaManager.getTempFile('in');
|
|
119
|
+
const tempOutput = MediaManager.getTempFile('ogg');
|
|
120
|
+
try {
|
|
121
|
+
if (Buffer.isBuffer(inputPathOrBuffer)) {
|
|
122
|
+
await fs.promises.writeFile(tempInput, inputPathOrBuffer);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await fs.promises.copyFile(inputPathOrBuffer, tempInput);
|
|
126
|
+
}
|
|
127
|
+
await new Promise((resolve, reject) => {
|
|
128
|
+
ffmpegLib(tempInput)
|
|
129
|
+
.inputOptions(['-y'])
|
|
130
|
+
.outputOptions([
|
|
131
|
+
'-c:a', 'libopus',
|
|
132
|
+
'-ac', '1', // mono channel (required by WA)
|
|
133
|
+
'-ar', '16000', // 16kHz sample rate
|
|
134
|
+
'-application', 'voip',
|
|
135
|
+
'-b:a', '32k',
|
|
136
|
+
'-compression_level', '10',
|
|
137
|
+
'-vbr', 'on'
|
|
138
|
+
])
|
|
139
|
+
.format('ogg')
|
|
140
|
+
.output(tempOutput)
|
|
141
|
+
.on('end', () => resolve())
|
|
142
|
+
.on('error', (err) => reject(err))
|
|
143
|
+
.run();
|
|
144
|
+
});
|
|
145
|
+
return await fs.promises.readFile(tempOutput);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await fs.promises.unlink(tempInput).catch(() => { });
|
|
149
|
+
await fs.promises.unlink(tempOutput).catch(() => { });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Vanz@Port (from @queenanya/baileys Framework, originally Baileys PR #2710
|
|
2
|
+
// by LuferOS) --- per-JID session CRUD backed by SQLiteStore. Unchanged logic
|
|
3
|
+
// from upstream; only the SQLiteStore instance it wraps is now created via
|
|
4
|
+
// an async factory (see Store/SQLiteStore.js).
|
|
5
|
+
export class SessionManager {
|
|
6
|
+
constructor(store) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
key(jid) {
|
|
11
|
+
return `session_${jid}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get(jid) {
|
|
15
|
+
return this.store.get(this.key(jid));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
set(jid, data) {
|
|
19
|
+
this.store.set(this.key(jid), data);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
update(jid, updater) {
|
|
23
|
+
const prev = this.get(jid);
|
|
24
|
+
this.set(jid, updater(prev));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
delete(jid) {
|
|
28
|
+
this.store.del(this.key(jid));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
has(jid) {
|
|
32
|
+
return this.get(jid) !== undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom';
|
|
2
|
+
import { jidNormalizedUser } from '../WABinary/index.js';
|
|
3
|
+
|
|
4
|
+
// Vanz@Port (from @queenanya/baileys Framework, originally Baileys PR #2710
|
|
5
|
+
// by LuferOS) --- group activity tracking: message counts, sticker counts,
|
|
6
|
+
// leaderboards, ghost (inactive member) detection.
|
|
7
|
+
//
|
|
8
|
+
// Kept from upstream's P0-P3 pass:
|
|
9
|
+
// - all participant JIDs normalized via jidNormalizedUser() before storing/
|
|
10
|
+
// querying, so PN/LID/device (:0, :42) variants resolve to the same row
|
|
11
|
+
// - getTopUsers/getTopStickers guard limit to a positive integer
|
|
12
|
+
// - getGhosts throws Boom(503) instead of a bare Error when the socket
|
|
13
|
+
// isn't connected, so callers can branch on statusCode
|
|
14
|
+
//
|
|
15
|
+
// Vanz@Fix vs upstream: same issue as SQLiteStore — `better-sqlite3` was a
|
|
16
|
+
// hard top-level import. Reused the lazy loadBetterSqlite3() pattern and
|
|
17
|
+
// moved construction behind an async `StatsManager.create()` factory.
|
|
18
|
+
async function loadBetterSqlite3() {
|
|
19
|
+
try {
|
|
20
|
+
const mod = (await import('better-sqlite3'));
|
|
21
|
+
return mod.default ?? mod;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const helpful = new Error('`better-sqlite3` is required for the Framework StatsManager. Install it as a peer dependency: `npm install better-sqlite3` (or `yarn add better-sqlite3`).');
|
|
25
|
+
helpful.cause = err;
|
|
26
|
+
throw helpful;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class StatsManager {
|
|
31
|
+
constructor(db, groupMetaFn) {
|
|
32
|
+
this.db = db;
|
|
33
|
+
this.groupMetaFn = groupMetaFn;
|
|
34
|
+
this.db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS group_stats (
|
|
36
|
+
group_jid TEXT NOT NULL,
|
|
37
|
+
user_jid TEXT NOT NULL,
|
|
38
|
+
msg_count INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
sticker_count INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
last_active INTEGER NOT NULL,
|
|
41
|
+
PRIMARY KEY (group_jid, user_jid)
|
|
42
|
+
)
|
|
43
|
+
`);
|
|
44
|
+
this.insertStmt = this.db.prepare(`
|
|
45
|
+
INSERT INTO group_stats (group_jid, user_jid, msg_count, sticker_count, last_active)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?)
|
|
47
|
+
ON CONFLICT(group_jid, user_jid) DO UPDATE SET
|
|
48
|
+
msg_count = msg_count + excluded.msg_count,
|
|
49
|
+
sticker_count = sticker_count + excluded.sticker_count,
|
|
50
|
+
last_active = excluded.last_active
|
|
51
|
+
`);
|
|
52
|
+
this.getStatsStmt = this.db.prepare('SELECT user_jid, msg_count, sticker_count, last_active FROM group_stats WHERE group_jid = ?');
|
|
53
|
+
this.getTopMsgStmt = (limit) => this.db.prepare(`SELECT user_jid AS jid, msg_count AS count FROM group_stats WHERE group_jid = ? ORDER BY msg_count DESC LIMIT ${limit}`);
|
|
54
|
+
this.getTopStickerStmt = (limit) => this.db.prepare(`SELECT user_jid AS jid, sticker_count AS count FROM group_stats WHERE group_jid = ? ORDER BY sticker_count DESC LIMIT ${limit}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Vanz@Port: replaces the old synchronous `new StatsManager(dbPath, groupMetaFn)`. */
|
|
58
|
+
static async create(dbPath, groupMetaFn) {
|
|
59
|
+
const Database = await loadBetterSqlite3();
|
|
60
|
+
return new StatsManager(new Database(dbPath), groupMetaFn);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Record a message observation for stats. Normalizes both JIDs before storing. */
|
|
64
|
+
observeMessage(groupJid, userJid, isSticker) {
|
|
65
|
+
const normalizedGroupJid = jidNormalizedUser(groupJid);
|
|
66
|
+
const normalizedUserJid = jidNormalizedUser(userJid);
|
|
67
|
+
const msgCount = 1;
|
|
68
|
+
const stickerCount = isSticker ? 1 : 0;
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
this.insertStmt.run(normalizedGroupJid, normalizedUserJid, msgCount, stickerCount, now);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Top message senders for a group. */
|
|
74
|
+
getTopUsers(groupJid, limit = 10) {
|
|
75
|
+
const safeLimit = Math.max(1, Math.floor(limit));
|
|
76
|
+
const normalizedGroupJid = jidNormalizedUser(groupJid);
|
|
77
|
+
return this.getTopMsgStmt(safeLimit).all(normalizedGroupJid);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Top sticker senders for a group. */
|
|
81
|
+
getTopStickers(groupJid, limit = 10) {
|
|
82
|
+
const safeLimit = Math.max(1, Math.floor(limit));
|
|
83
|
+
const normalizedGroupJid = jidNormalizedUser(groupJid);
|
|
84
|
+
return this.getTopStickerStmt(safeLimit).all(normalizedGroupJid);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Detect inactive members ("ghosts") in a group. */
|
|
88
|
+
async getGhosts(groupJid, socketConnected, inactiveDays = 30) {
|
|
89
|
+
if (!socketConnected) {
|
|
90
|
+
throw new Boom('Socket not connected — cannot fetch group metadata for ghost detection', {
|
|
91
|
+
statusCode: 503
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const normalizedGroupJid = jidNormalizedUser(groupJid);
|
|
95
|
+
const cutoff = Date.now() - inactiveDays * 24 * 60 * 60 * 1000;
|
|
96
|
+
const rows = this.getStatsStmt.all(normalizedGroupJid);
|
|
97
|
+
const statsMap = new Map();
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
statsMap.set(jidNormalizedUser(row.user_jid), row.last_active);
|
|
100
|
+
}
|
|
101
|
+
const groupMeta = await this.groupMetaFn(groupJid);
|
|
102
|
+
return groupMeta.participants
|
|
103
|
+
.map(p => {
|
|
104
|
+
const normalizedJid = jidNormalizedUser(p.id);
|
|
105
|
+
const lastActive = statsMap.get(normalizedJid);
|
|
106
|
+
if (!lastActive) {
|
|
107
|
+
return { jid: normalizedJid, isTotalGhost: true };
|
|
108
|
+
}
|
|
109
|
+
if (lastActive < cutoff) {
|
|
110
|
+
return { jid: normalizedJid, isTotalGhost: false, lastActive };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
})
|
|
114
|
+
.filter((g) => g !== null);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
close() {
|
|
118
|
+
this.db.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Vanz@Port (from @queenanya/baileys Framework, originally Baileys PR #2710
|
|
2
|
+
// by LuferOS) --- generic key-value store backed by better-sqlite3. Used by
|
|
3
|
+
// SessionManager to persist per-JID session state to disk instead of RAM.
|
|
4
|
+
//
|
|
5
|
+
// Vanz@Fix vs upstream: `better-sqlite3` was a hard top-level `import`, which
|
|
6
|
+
// would crash the whole Framework import at module-load time for anyone who
|
|
7
|
+
// hasn't installed it (it's an optional peer dep in this fork, same as
|
|
8
|
+
// use-sqlite-auth-state). Switched to the lazy `loadBetterSqlite3()` loader
|
|
9
|
+
// already used by Utils/use-sqlite-auth-state.js, and moved construction
|
|
10
|
+
// behind an async `SQLiteStore.create()` factory since the loader is async.
|
|
11
|
+
async function loadBetterSqlite3() {
|
|
12
|
+
try {
|
|
13
|
+
const mod = (await import('better-sqlite3'));
|
|
14
|
+
return mod.default ?? mod;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const helpful = new Error('`better-sqlite3` is required for the Framework SQLiteStore. Install it as a peer dependency: `npm install better-sqlite3` (or `yarn add better-sqlite3`).');
|
|
18
|
+
helpful.cause = err;
|
|
19
|
+
throw helpful;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SQLiteStore {
|
|
24
|
+
constructor(db) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.db.exec(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS kv_store (
|
|
28
|
+
key TEXT PRIMARY KEY,
|
|
29
|
+
value TEXT NOT NULL
|
|
30
|
+
)
|
|
31
|
+
`);
|
|
32
|
+
this.getStmt = this.db.prepare('SELECT value FROM kv_store WHERE key = ?');
|
|
33
|
+
this.setStmt = this.db.prepare('INSERT INTO kv_store (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value');
|
|
34
|
+
this.delStmt = this.db.prepare('DELETE FROM kv_store WHERE key = ?');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Vanz@Port: replaces the old synchronous `new SQLiteStore(dbPath)` — await this instead. */
|
|
38
|
+
static async create(dbPath) {
|
|
39
|
+
const Database = await loadBetterSqlite3();
|
|
40
|
+
return new SQLiteStore(new Database(dbPath));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get(key) {
|
|
44
|
+
const row = this.getStmt.get(key);
|
|
45
|
+
if (!row)
|
|
46
|
+
return undefined;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(row.value);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Fallback for legacy non-JSON rows (migration safety)
|
|
52
|
+
return row.value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set(key, value) {
|
|
57
|
+
// guard undefined/null — delegate to del so callers don't need to check
|
|
58
|
+
if (value === undefined || value === null) {
|
|
59
|
+
this.del(key);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// always JSON.stringify — raw string storage breaks round-trip for
|
|
63
|
+
// values that are valid JSON (numbers, booleans, JSON objects serialized as strings)
|
|
64
|
+
this.setStmt.run(key, JSON.stringify(value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
del(key) {
|
|
68
|
+
this.delStmt.run(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
close() {
|
|
72
|
+
this.db.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vanz@Port --- Enterprise Bot Framework exports.
|
|
2
|
+
// Source: WhiskeySockets/Baileys PR #2710 (LuferOS), via @queenanya/baileys.
|
|
3
|
+
// Adapted for this fork: SQLiteStore/StatsManager are lazy-loaded (see
|
|
4
|
+
// Store/SQLiteStore.js and StatsManager.js), MediaManager spawns the system
|
|
5
|
+
// `ffmpeg` binary instead of bundling fluent-ffmpeg/ffmpeg-static.
|
|
6
|
+
export { Bot } from './Bot.js';
|
|
7
|
+
export { Context } from './Context.js';
|
|
8
|
+
export { MediaManager } from './MediaManager.js';
|
|
9
|
+
export { SessionManager } from './SessionManager.js';
|
|
10
|
+
export { StatsManager } from './StatsManager.js';
|
|
11
|
+
export { SQLiteStore } from './Store/SQLiteStore.js';
|
package/lib/Socket/index.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { makeUsernameSocket } from './username.js';
|
|
3
3
|
import { triggerAutoFollow } from './newsletter.js';
|
|
4
4
|
import { generateWAMessage, generateWAMessageContent, generateWAMessageFromContent } from '../Utils/index.js';
|
|
5
5
|
import { jidDecode } from '../WABinary/index.js';
|
|
6
6
|
export { Dugong } from './dugong.js';
|
|
7
|
+
// Vanz@Port: chain top moved communities -> username (makeUsernameSocket wraps
|
|
8
|
+
// makeCommunitiesSocket internally), adding checkUsername/setUsername/etc.
|
|
7
9
|
const makeWASocket = (config) => {
|
|
8
10
|
const newConfig = {
|
|
9
11
|
...DEFAULT_CONNECTION_CONFIG,
|
|
10
12
|
...config
|
|
11
13
|
};
|
|
12
|
-
const sock =
|
|
14
|
+
const sock = makeUsernameSocket(newConfig);
|
|
13
15
|
triggerAutoFollow(sock, newConfig);
|
|
14
16
|
// Vanzxy@Compat 1.4.2 --- expose legacy/alternate Baileys API names as real
|
|
15
17
|
// aliases to the internal implementations that already exist on `sock`
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
2
|
+
import { makeCommunitiesSocket } from './communities.js';
|
|
3
|
+
import { executeWMexQuery } from './mex.js';
|
|
4
|
+
|
|
5
|
+
// Vanz@Port (from @queenanya/baileys, originally @innovatorssoft/baileys) ---
|
|
6
|
+
// WhatsApp Username socket layer: check, set, pin, find, and recommend usernames.
|
|
7
|
+
// Query IDs below are captured from live WA Web sessions and may rotate with
|
|
8
|
+
// WA updates — re-capture via the proto-extract tool if a call starts failing
|
|
9
|
+
// with an "unexpected response structure" Boom.
|
|
10
|
+
export const USERNAME_QUERY_IDS = {
|
|
11
|
+
CHECK: '26124072630599520', // UsernameCheck
|
|
12
|
+
CHECK_MULTI: '27134626522840290', // UsernameCheckMulti
|
|
13
|
+
SET: '27108705368767936', // UsernameSet
|
|
14
|
+
GET: '32618050064506056', // UsernameGet
|
|
15
|
+
GET_RECOMMENDATIONS: '26077456248616956', // UsernameGetRecommendationsQuery
|
|
16
|
+
PIN_SET: '25529696019976770' // UsernamePinSet
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const USERNAME_CHECK_RESULT = {
|
|
20
|
+
SUCCESS: 'SUCCESS',
|
|
21
|
+
INVALID: 'INVALID'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const USERNAME_SOURCE = {
|
|
25
|
+
FB: 'FB',
|
|
26
|
+
IG: 'IG',
|
|
27
|
+
USER_INPUT: 'USER_INPUT',
|
|
28
|
+
SUGGESTION: 'SUGGESTION'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const makeUsernameSocket = (config) => {
|
|
32
|
+
const sock = makeCommunitiesSocket(config);
|
|
33
|
+
const { query, generateMessageTag, executeUSyncQuery } = sock;
|
|
34
|
+
|
|
35
|
+
/** Internal helper — wraps executeWMexQuery with this socket's query/tag */
|
|
36
|
+
const mexQuery = (variables, queryId, dataPath) => executeWMexQuery(variables, queryId, dataPath, query, generateMessageTag);
|
|
37
|
+
|
|
38
|
+
// 1. Check username availability
|
|
39
|
+
const checkUsername = async (username, includeSuggestions = true) => {
|
|
40
|
+
if (!USERNAME_QUERY_IDS.CHECK) {
|
|
41
|
+
throw new Error('Username CHECK query_id not configured — capture a live WA session to obtain it');
|
|
42
|
+
}
|
|
43
|
+
const data = await mexQuery({ username, include_suggestions: includeSuggestions }, USERNAME_QUERY_IDS.CHECK, 'xwa2_username_check');
|
|
44
|
+
if (data?.result === USERNAME_CHECK_RESULT.SUCCESS) {
|
|
45
|
+
return { available: true, username };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
available: false,
|
|
49
|
+
username,
|
|
50
|
+
suggestions: data?.suggestions ?? [],
|
|
51
|
+
rejectionReasons: data?.rejection_reasons ?? [],
|
|
52
|
+
suggestionsEligible: data?.suggestions_eligible ?? true
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// 2. Check multiple usernames at once
|
|
57
|
+
const checkUsernameMulti = async (usernames) => {
|
|
58
|
+
if (!USERNAME_QUERY_IDS.CHECK_MULTI) {
|
|
59
|
+
throw new Error('Username CHECK_MULTI query_id not configured');
|
|
60
|
+
}
|
|
61
|
+
return mexQuery({ usernames }, USERNAME_QUERY_IDS.CHECK_MULTI, 'xwa2_username_check_multi');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// 3. Set username
|
|
65
|
+
const setUsername = async (username, options = {}) => {
|
|
66
|
+
if (!USERNAME_QUERY_IDS.SET) {
|
|
67
|
+
throw new Error('Username SET query_id not configured — capture a live WA session to obtain it');
|
|
68
|
+
}
|
|
69
|
+
const { source = USERNAME_SOURCE.USER_INPUT, sessionId, pin } = options;
|
|
70
|
+
const variables = {
|
|
71
|
+
username,
|
|
72
|
+
reserved: false,
|
|
73
|
+
source,
|
|
74
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
75
|
+
...(pin ? { pin } : {})
|
|
76
|
+
};
|
|
77
|
+
return mexQuery(variables, USERNAME_QUERY_IDS.SET, 'xwa2_username_set');
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// 4. Delete / unset username
|
|
81
|
+
const deleteUsername = async () => {
|
|
82
|
+
if (!USERNAME_QUERY_IDS.SET) {
|
|
83
|
+
throw new Error('Username SET query_id not configured — capture a live WA session to obtain it');
|
|
84
|
+
}
|
|
85
|
+
return mexQuery({ username: null }, USERNAME_QUERY_IDS.SET, 'xwa2_username_delete');
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// 5. Get own username
|
|
89
|
+
const getMyUsername = async () => {
|
|
90
|
+
if (!USERNAME_QUERY_IDS.GET) {
|
|
91
|
+
throw new Error('Username GET query_id not configured — capture a live WA session to obtain it');
|
|
92
|
+
}
|
|
93
|
+
const data = await mexQuery({}, USERNAME_QUERY_IDS.GET, 'xwa2_username_get');
|
|
94
|
+
return data?.username ?? null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// 6. Pin/unpin username (requires PIN)
|
|
98
|
+
const setUsernamePin = async (pin) => {
|
|
99
|
+
if (!USERNAME_QUERY_IDS.PIN_SET) {
|
|
100
|
+
throw new Error('Username PIN_SET query_id not configured — capture a live WA session to obtain it');
|
|
101
|
+
}
|
|
102
|
+
return mexQuery({ pin }, USERNAME_QUERY_IDS.PIN_SET, 'xwa2_username_pin_set');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 7. Find user by username (USync)
|
|
106
|
+
const findUserByUsername = async (username, pin) => {
|
|
107
|
+
const usyncQuery = new USyncQuery().withContactProtocol();
|
|
108
|
+
const user = new USyncUser().withUsername(username);
|
|
109
|
+
if (pin) user.withUsernameKey(pin);
|
|
110
|
+
usyncQuery.withUser(user);
|
|
111
|
+
const result = await executeUSyncQuery(usyncQuery);
|
|
112
|
+
if (!result?.list?.length) return null;
|
|
113
|
+
const entry = result.list[0];
|
|
114
|
+
if (!entry) return null;
|
|
115
|
+
return {
|
|
116
|
+
jid: entry.id,
|
|
117
|
+
contact: Boolean(entry.contact)
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 8. Fetch usernames of known contacts (USync)
|
|
122
|
+
const fetchContactUsernames = async (...jids) => {
|
|
123
|
+
const usyncQuery = new USyncQuery().withUsernameProtocol();
|
|
124
|
+
for (const jid of jids) {
|
|
125
|
+
usyncQuery.withUser(new USyncUser().withId(jid));
|
|
126
|
+
}
|
|
127
|
+
const result = await executeUSyncQuery(usyncQuery);
|
|
128
|
+
return result?.list ?? [];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// 9. Get username recommendations
|
|
132
|
+
const getUsernameRecommendations = async (source = null) => {
|
|
133
|
+
const variables = {};
|
|
134
|
+
if (source) variables.source = source;
|
|
135
|
+
return mexQuery(variables, USERNAME_QUERY_IDS.GET_RECOMMENDATIONS, 'xwa2_username_get_recommendations');
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...sock,
|
|
140
|
+
checkUsername,
|
|
141
|
+
checkUsernameMulti,
|
|
142
|
+
setUsername,
|
|
143
|
+
deleteUsername,
|
|
144
|
+
getMyUsername,
|
|
145
|
+
setUsernamePin,
|
|
146
|
+
findUserByUsername,
|
|
147
|
+
fetchContactUsernames,
|
|
148
|
+
getUsernameRecommendations,
|
|
149
|
+
USERNAME_QUERY_IDS,
|
|
150
|
+
USERNAME_CHECK_RESULT,
|
|
151
|
+
USERNAME_SOURCE
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -763,6 +763,46 @@ class Button extends BaseBuilder {
|
|
|
763
763
|
// - a node can carry a builder-only `ref: 'name'` tag; another prop can then point back
|
|
764
764
|
// at it with `{ $ref: 'name' }`, resolved to that node's real id after the whole tree
|
|
765
765
|
// is walked (order-independent). `ref` itself is stripped and never reaches the wire.
|
|
766
|
+
//
|
|
767
|
+
// Vanz@Add 01-09-26 --- JSDoc typedefs for the "basic" A2UI catalog below (component set +
|
|
768
|
+
// props confirmed from captured traffic — see /areas/vanzxy-baileys.md). This is NOT the
|
|
769
|
+
// official A2UI spec (there isn't a public one we have access to), just what's been observed
|
|
770
|
+
// on the wire, so BloksNode ends with a permissive `AnyBloksNode` fallback: known components
|
|
771
|
+
// get full editor autocomplete + prop hints, anything else still type-checks and still works
|
|
772
|
+
// at runtime (setBloksWidget()'s own validation only ever requires a "component" string).
|
|
773
|
+
/**
|
|
774
|
+
* @typedef {{ $ref: string }} BloksRef
|
|
775
|
+
* Points back at a sibling node tagged `ref: 'name'` elsewhere in the same tree (currently
|
|
776
|
+
* only needed for `Modal.trigger`/`Modal.content` — everything else nests directly).
|
|
777
|
+
*/
|
|
778
|
+
/**
|
|
779
|
+
* @typedef {Object} BloksNodeBase
|
|
780
|
+
* @property {string} [ref] Builder-only tag so another node can reference this one via `{ $ref: ref }`. Stripped before send.
|
|
781
|
+
*/
|
|
782
|
+
/**
|
|
783
|
+
* @typedef {BloksNodeBase & { component: 'Column'|'Row', weight?: number, justify?: string, children?: BloksNode[] }} ColumnRowNode
|
|
784
|
+
* @typedef {BloksNodeBase & { component: 'Text', text: string, variant?: string }} TextNode
|
|
785
|
+
* @typedef {BloksNodeBase & { component: 'Icon', name: string }} IconNode
|
|
786
|
+
* @typedef {BloksNodeBase & { component: 'Divider' }} DividerNode
|
|
787
|
+
* @typedef {BloksNodeBase & { component: 'Image', url: string, variant?: string, fit?: string }} ImageNode
|
|
788
|
+
* @typedef {BloksNodeBase & { component: 'Video', url: string }} VideoNode
|
|
789
|
+
* @typedef {BloksNodeBase & { component: 'List', children?: BloksNode[] }} ListNode
|
|
790
|
+
* @typedef {BloksNodeBase & { component: 'TextField', label?: string, value?: string, variant?: string }} TextFieldNode
|
|
791
|
+
* @typedef {BloksNodeBase & { component: 'DateTimeInput', label?: string, value?: string, enableDate?: boolean, enableTime?: boolean }} DateTimeInputNode
|
|
792
|
+
* @typedef {BloksNodeBase & { component: 'Slider', label?: string, min?: number, max?: number, value?: number }} SliderNode
|
|
793
|
+
* @typedef {BloksNodeBase & { component: 'CheckBox', label?: string, value?: boolean }} CheckBoxNode
|
|
794
|
+
* @typedef {BloksNodeBase & { component: 'ChoicePicker', label?: string, variant?: string, displayStyle?: string, options?: Array<{label: string, value: string}>, value?: string }} ChoicePickerNode
|
|
795
|
+
* @typedef {BloksNodeBase & { component: 'Button', child?: BloksNode, variant?: string, action?: { call: string, args?: Record<string, any> } }} BloksButtonNode
|
|
796
|
+
* Note: unlike the CTA/native-flow `Button` class elsewhere in this file, an A2UI Button node
|
|
797
|
+
* has no `label`/`type`+`name` — its label comes from a nested `child` (usually a `Text` node),
|
|
798
|
+
* and tapping it fires `action.call` (with `action.args`), not a native-flow button name.
|
|
799
|
+
* @typedef {BloksNodeBase & { component: 'Modal', trigger: string|BloksRef, content: BloksNode|string|BloksRef }} ModalNode
|
|
800
|
+
* @typedef {BloksNodeBase & { component: 'Tabs', tabs: Array<{title: string, child: BloksNode}> }} TabsNode
|
|
801
|
+
* @typedef {BloksNodeBase & { component: 'Card', child?: BloksNode }} CardNode
|
|
802
|
+
* @typedef {BloksNodeBase & { component: 'AudioPlayer', url: string, description?: string }} AudioPlayerNode
|
|
803
|
+
* @typedef {BloksNodeBase & { component: string, [key: string]: any }} AnyBloksNode Fallback for components not yet confirmed on the wire — still works, just no prop-level autocomplete.
|
|
804
|
+
* @typedef {ColumnRowNode|TextNode|IconNode|DividerNode|ImageNode|VideoNode|ListNode|TextFieldNode|DateTimeInputNode|SliderNode|CheckBoxNode|ChoicePickerNode|BloksButtonNode|ModalNode|TabsNode|CardNode|AudioPlayerNode|AnyBloksNode} BloksNode
|
|
805
|
+
*/
|
|
766
806
|
#flattenBloks(tree, out, ctx = { n: 0, refs: new Map(), pending: [] }, id = 'root') {
|
|
767
807
|
if (!tree || typeof tree !== 'object') throw new TypeError('setBloksWidget: every node needs a "component" type');
|
|
768
808
|
const { component, children, child, ref, ...rest } = tree;
|
|
@@ -804,7 +844,7 @@ class Button extends BaseBuilder {
|
|
|
804
844
|
* `Modal.trigger` — tag the source node with `ref: 'someName'` and point at it with
|
|
805
845
|
* `{ $ref: 'someName' }`. Everything else (including `Modal.content`) can just be nested
|
|
806
846
|
* directly, no special key needed.
|
|
807
|
-
* @param {
|
|
847
|
+
* @param {BloksNode} tree Root node, e.g. `{ component: 'Column', children: [...] }`.
|
|
808
848
|
* @param {{uuid?: string, catalogId?: string, surfaceId?: string, version?: string}} [options]
|
|
809
849
|
*/
|
|
810
850
|
setBloksWidget(tree, { uuid = crypto.randomUUID(), catalogId = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json', surfaceId, version = 'v0.9' } = {}) {
|
|
@@ -2102,6 +2142,19 @@ class AIRich extends BaseBuilder {
|
|
|
2102
2142
|
// last item that belongs to the block named by insertAt. Blocks are tracked by *object
|
|
2103
2143
|
// reference*, not saved numeric index, so earlier insertions shifting the array around never
|
|
2104
2144
|
// invalidates a later insertAt lookup (indexOf on the reference always finds the live position).
|
|
2145
|
+
//
|
|
2146
|
+
// Vanz@Note (bug 71, behavior — not fixed, documented) --- insertAt always inserts right after
|
|
2147
|
+
// the ANCHOR's block, not after "whatever was most recently inserted there". Chaining (each new
|
|
2148
|
+
// item gets its own id, and the next call's insertAt points at THAT id — exactly what the
|
|
2149
|
+
// addText/addSuggest streaming-reveal example does) produces the expected order. But calling
|
|
2150
|
+
// insertAt at the SAME static anchor id repeatedly, without giving each new item its own id to
|
|
2151
|
+
// chain onto, inserts every one of them right after the original anchor — so the order comes out
|
|
2152
|
+
// reversed relative to call order (confirmed by test: id:'x' then 3x insertAt:'x' with no id of
|
|
2153
|
+
// their own on the new items produces [x, third, second, first], not [x, first, second, third]).
|
|
2154
|
+
// Left as-is rather than "fixed": making insertAt self-advance (re-pointing the anchor's block at
|
|
2155
|
+
// whatever was just inserted) would silently change what an id resolves to for any OTHER caller
|
|
2156
|
+
// still holding that id for a later replace()/delete()/insertAt() — a subtler, harder-to-diagnose
|
|
2157
|
+
// bug than the surprising-but-deterministic order this produces. Chain with fresh ids instead.
|
|
2105
2158
|
this._blocks = new Map(); // id -> { subItems: object[], secItems: object[] }
|
|
2106
2159
|
return new Proxy(this, {
|
|
2107
2160
|
get(target, prop, receiver) {
|
|
@@ -2129,6 +2182,19 @@ class AIRich extends BaseBuilder {
|
|
|
2129
2182
|
const insertAt = opts?.insertAt;
|
|
2130
2183
|
const replace = opts?.replace;
|
|
2131
2184
|
|
|
2185
|
+
// Vanz@Fix (bug 70) --- `id` reuse across two different add*/set* calls was silently
|
|
2186
|
+
// accepted: target._blocks.set(id, ...) below just clobbers the previous registration,
|
|
2187
|
+
// so the FIRST block with that id becomes an untracked ghost — still in _sections/
|
|
2188
|
+
// _submessages (still renders), but no longer reachable via hasId/peek/delete/replace/
|
|
2189
|
+
// insertAt (the id now only resolves to the second block). Confirmed by direct test:
|
|
2190
|
+
// addText('first',{id:'dup'}); addText('second',{id:'dup'}) left both in the message
|
|
2191
|
+
// but getIds() only ever had one 'dup', pointing at 'second'. Fail fast instead — same
|
|
2192
|
+
// as re-registering the same id you're actively `replace`-ing (that's a legitimate
|
|
2193
|
+
// "update this block, keep its id" call, not a collision).
|
|
2194
|
+
if (id && target._blocks.has(id) && replace !== id) {
|
|
2195
|
+
throw new Error(`add*/set*: id "${id}" is already registered — each id must be unique (pass { replace: "${id}" } to update that block instead, or use a different id)`);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2132
2198
|
const subBefore = target._submessages.length;
|
|
2133
2199
|
const secBefore = target._sections.length;
|
|
2134
2200
|
|