@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.
@@ -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';
@@ -1,15 +1,17 @@
1
1
  import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
2
- import { makeCommunitiesSocket } from './communities.js';
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 = makeCommunitiesSocket(newConfig);
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 {Record<string, any>} tree Root node, e.g. `{ component: 'Column', children: [...] }`.
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