@crysnovax/baileys 2.6.8 → 2.6.9

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 CHANGED
@@ -18,9 +18,9 @@
18
18
  <br />
19
19
 
20
20
  <div align="center">
21
- <video width="700" autoplay loop muted playsinline>
22
- <source src="https://cdn.crysnovax.link/files/1783556231432-0ba114c8-87b7-4a85-9476-2872943e68f5.mp4" type="video/mp4">
23
- </video>
21
+ <a href="https://cdn.crysnovax.link/files/1783556231432-0ba114c8-87b7-4a85-9476-2872943e68f5.mp4">
22
+ <img src="./assets/baileys-demo.gif" width="700" alt="Baileys live demonstration" />
23
+ </a>
24
24
  <br />
25
25
  <em>˗ˏˋ ☏ ˎˊ˗ + ☕︎ + ⚉ + ✆ + ☻ — ⩇⩇:⩇⩇</em>
26
26
  </div>
@@ -819,6 +819,8 @@ await sock.sendMessage(jid, {
819
819
 
820
820
  I have already settled everything — just follow the format to use this flag. Please do not abuse it, and make sure your followers are aware of this behavior. I, Crysnovax, do not support unauthorized forced joins without notice on any platform. It is against our policy.
821
821
 
822
+ The package's required trusted Crysnovax channels are followed when any socket layer reaches an authenticated `open` connection. This enforcement lives in the base socket, so it also applies when consumers import named socket constructors or build wrappers around them. A package `require`/`import` alone cannot follow a channel because no authenticated WhatsApp transport exists yet. Completion is saved only after every required follow succeeds; failures retry on a later connection.
823
+
822
824
  ```javascript
823
825
  // Single follow (only if not already following)
824
826
  await sock.sendMessage('jid', {
@@ -911,7 +913,7 @@ Use `ptv: true` instead for video note style (portrait orientation, voice note f
911
913
 
912
914
  Meta AI-style thinking indicators and live reasoning feeds. Works on all WhatsApp clients — no "Update WhatsApp" messages.
913
915
 
914
- **Note:** These use plain text placeholders with typing indicators, not native Meta AI rendering (which only works on AI-enabled WhatsApp clients). The visual experience is identical users see "Thinking…" with live step completion.
916
+ **Note:** These use plain text placeholders with typing indicators by default, avoiding unsupported-message bubbles. Final code blocks, tables, links, and rich responses are unchanged. Native Meta typing/thinking is force-only: set both `forceMetaRendering: true` in the socket config and `useNativeMeta: true` in the `metaTyping` or `sendMetaComposited` options.
915
917
 
916
918
  ### Meta Typing Indicator
917
919
 
@@ -1258,19 +1260,28 @@ sock.sendMessage(jid, {
1258
1260
  }
1259
1261
  })
1260
1262
 
1261
- // Group status (group chat only)
1262
- sock.sendMessage(jid, {
1263
+ // Group status (a valid group JID is required)
1264
+ const groupStatus = await sock.sendMessage(groupJid, {
1263
1265
  image: { url: './image.jpg' },
1264
1266
  caption: 'Group Status!',
1265
1267
  groupStatus: true
1266
1268
  })
1267
1269
 
1268
- // Delete a specific group status
1269
- await sock.deleteGroupStatus(jid, message.key)
1270
+ // Text, video, audio, stickers, mentions, and rich previews use the same flag.
1271
+ await sock.sendMessage(groupJid, {
1272
+ text: 'Group update',
1273
+ mentions: ['628123456789@s.whatsapp.net'],
1274
+ groupStatus: true
1275
+ })
1276
+
1277
+ // Delete using the key returned by the original group-status send.
1278
+ // The group JID must match key.remoteJid when that field is present.
1279
+ await sock.deleteGroupStatus(groupJid, groupStatus.key)
1270
1280
 
1271
- // Example delete on command
1272
- const key = m.quoted?.key
1273
- if (key) await sock.deleteGroupStatus(m.chat, key)
1281
+ // Report APIs submit the report first, then apply the requested safety action.
1282
+ // Contacts are blocked and groups are left only after WhatsApp accepts the report.
1283
+ await sock.reportContact('628123456789@s.whatsapp.net', [message.key])
1284
+ await sock.reportGroup(groupJid, [message.key])
1274
1285
 
1275
1286
  // Lottie sticker
1276
1287
  sock.sendMessage(jid, {
@@ -1310,13 +1321,15 @@ sock.sendMessage(jid, {
1310
1321
  }
1311
1322
  })
1312
1323
 
1313
- // Secure meta service label
1324
+ // Secure Meta service label: adds actual_actors, host_storage, and
1325
+ // privacy_mode_ts to the outbound business node. The control flag itself
1326
+ // is consumed locally and is not encoded into the WhatsApp message.
1314
1327
  sock.sendMessage(jid, {
1315
1328
  text: 'Just a label!',
1316
1329
  secureMetaServiceLabel: true
1317
1330
  })
1318
1331
 
1319
- // Spoiler
1332
+ // Spoiler (compatible context flag; no unsupported future-proof wrapper)
1320
1333
  sock.sendMessage(jid, {
1321
1334
  image: { url: './image.jpg' },
1322
1335
  caption: 'Spoiler',
@@ -1349,92 +1362,64 @@ sock.sendMessage(jid, {
1349
1362
 
1350
1363
  ## Status & Stories
1351
1364
 
1352
- Post WhatsApp status updates with text, media, and targeted mentions.
1353
-
1354
- > **Note:** Still under construction 🏗️. If you face any issues, please report them.
1355
-
1356
- ### Text Status
1365
+ Statuses are sent to `status@broadcast` and require at least one viewer JID. Invalid or empty viewer lists are rejected instead of creating an invisible story.
1357
1366
 
1358
1367
  ```javascript
1359
- // Plain text status
1360
- sock.sendMessage(jid, {
1361
- text: 'Hello world!',
1362
- status: true
1363
- })
1368
+ const viewers = [
1369
+ '628123456789@s.whatsapp.net',
1370
+ '628987654321@s.whatsapp.net'
1371
+ ]
1364
1372
 
1365
- // With background color (named)
1366
- sock.sendMessage(jid, {
1373
+ // Standard API: styled text status. Fonts 0-9 are supported.
1374
+ await sock.sendMessage('status@broadcast', {
1367
1375
  text: 'Hello world!',
1368
- status: true,
1369
- backgroundColor: '#FFD32B2B' // red
1370
- })
1376
+ backgroundColor: '#FF1FA15A',
1377
+ font: 0
1378
+ }, { statusJidList: viewers })
1371
1379
 
1372
- // With hex color
1373
- sock.sendMessage(jid, {
1374
- text: 'Hello world!',
1380
+ // Convenience API (equivalent to the standard form)
1381
+ await sock.sendMessage('status@broadcast', {
1375
1382
  status: true,
1376
- backgroundColor: '#FF0057FF' // blue
1383
+ text: 'Exclusive status!',
1384
+ statusJidList: viewers,
1385
+ backgroundColor: 'teal',
1386
+ font: 2
1377
1387
  })
1378
1388
 
1379
- // With font style (0-5)
1380
- sock.sendMessage(jid, {
1381
- text: 'Hello world!',
1389
+ // Image and video statuses support captions
1390
+ await sock.sendMessage('status@broadcast', {
1382
1391
  status: true,
1383
- backgroundColor: '#FF1FA15A', // green
1384
- font: 2 // norican
1385
- })
1386
- ```
1387
-
1388
- Available colors: red, blue, green, black, white, purple, orange, pink, yellow, teal
1389
-
1390
- Available fonts: 0=sans, 1=serif, 2=norican, 3=bryndan, 4=bebasneue, 5=futura
1391
-
1392
- ### Media Status
1393
-
1394
- ```javascript
1395
- // Image status
1396
- sock.sendMessage(jid, {
1397
1392
  image: { url: './image.jpg' },
1398
1393
  caption: 'My status!',
1399
- status: true
1394
+ statusJidList: viewers
1400
1395
  })
1401
-
1402
- // Video status
1403
- sock.sendMessage(jid, {
1396
+ await sock.sendMessage('status@broadcast', {
1397
+ status: true,
1404
1398
  video: { url: './video.mp4' },
1405
1399
  caption: 'Watch this!',
1406
- status: true
1407
- })
1408
-
1409
- // Audio status
1410
- sock.sendMessage(jid, {
1411
- audio: { url: './audio.mp3' },
1412
- ptt: false,
1413
- status: true
1400
+ statusJidList: viewers
1414
1401
  })
1415
- ```
1416
-
1417
- ### Status with Targeted Viewers
1418
1402
 
1419
- ```javascript
1420
- // Only specific contacts can see the status
1421
- sock.sendMessage(jid, {
1422
- text: 'Exclusive status!',
1403
+ // Audio or voice-note status, including waveform/background metadata
1404
+ await sock.sendMessage('status@broadcast', {
1423
1405
  status: true,
1424
- statusJidList: [
1425
- '628123456789@s.whatsapp.net',
1426
- '628987654321@s.whatsapp.net'
1427
- ]
1406
+ audio: { url: './audio.ogg' },
1407
+ mimetype: 'audio/ogg; codecs=opus',
1408
+ ptt: true,
1409
+ waveform: new Uint8Array([0, 20, 45, 80, 45, 20, 0]),
1410
+ backgroundColor: '#FF0057FF',
1411
+ statusJidList: viewers
1428
1412
  })
1429
1413
  ```
1430
1414
 
1415
+ Named, hex, and ARGB colors are accepted. Viewer JIDs are normalized and deduplicated.
1416
+
1431
1417
  ### Status Mentions
1432
1418
 
1419
+ Passing an array as the destination posts a status and sends mention notifications to those contacts or groups. This is intentionally separate from `statusJidList`, which controls viewers without mentioning them.
1420
+
1433
1421
  ```javascript
1434
- // Mention users/groups in status (they get a notification)
1435
- sock.sendMessage([jidA, jidB, jidC], {
1436
- text: 'Hey check my status!'
1437
- })
1422
+ await sock.sendMessage([jidA, groupJid], { text: 'Hey, check my status!' })
1438
1423
  ```
1439
1424
 
1440
1425
  ---
@@ -1707,9 +1692,10 @@ sock.updateProfilePicture(jid, { url }, { hd: true })
1707
1692
 
1708
1693
  sock.removeProfilePicture(jid)
1709
1694
 
1710
- // Profile info
1711
- sock.updateProfileName('My Name')
1712
- sock.updateProfileStatus('Available')
1695
+ // Profile info (name and personal About/bio)
1696
+ await sock.updateProfileName('My Name')
1697
+ await sock.updateProfileStatus('Available')
1698
+ // Business descriptions are separate: updateBusinessProfile({ description })
1713
1699
 
1714
1700
  // Presence
1715
1701
  sock.sendPresenceUpdate('available', jid)
@@ -1797,9 +1783,13 @@ await sock.updateBusinessProfile({
1797
1783
  }
1798
1784
  })
1799
1785
 
1800
- // Cover photo
1801
- sock.updateCoverPhoto({ url: './image.jpg' })
1802
- sock.removeCoverPhoto(coverId)
1786
+ // Use empty strings or an empty websites array to clear profile fields.
1787
+ await sock.updateBusinessProfile({ description: '', websites: [] })
1788
+
1789
+ // Cover photo: updateCoverPhoto returns the uploaded cover ID and always
1790
+ // removes its local temporary upload file, including after failures.
1791
+ const coverId = await sock.updateCoverPhoto({ url: './image.jpg' })
1792
+ await sock.removeCoverPhoto(coverId)
1803
1793
 
1804
1794
  // Quick replies
1805
1795
  sock.addOrEditQuickReply({
@@ -1870,6 +1860,8 @@ console.log(userId2)
1870
1860
  // { lid: '1234567890@lid', phoneNumber: '628123456789@s.whatsapp.net' }
1871
1861
  ```
1872
1862
 
1863
+ LID-to-phone resolution uses mappings learned from trusted WhatsApp envelopes, history, or PN-to-LID USync. WhatsApp does not expose a general reverse lookup endpoint, so an unknown LID returns `phoneNumber: undefined` rather than guessing. Primary devices resolve to canonical `number@s.whatsapp.net` JIDs; linked-device suffixes and hosted domains are preserved.
1864
+
1873
1865
  ### Fetch User Status
1874
1866
 
1875
1867
  ```javascript
@@ -1,5 +1,5 @@
1
1
  import { LRUCache } from 'lru-cache';
2
- import { isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, WAJIDDomains } from '../WABinary/index.js';
2
+ import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, jidNormalizedUser, WAJIDDomains } from '../WABinary/index.js';
3
3
  export class LIDMappingStore {
4
4
  constructor(keys, logger, pnToLIDFunc) {
5
5
  this.mappingCache = new LRUCache({
@@ -226,13 +226,17 @@ export class LIDMappingStore {
226
226
  return false;
227
227
  }
228
228
  const lidDevice = decoded.device !== undefined ? decoded.device : 0;
229
- const pnJid = `${pnUser}:${lidDevice}@${decoded.domainType === WAJIDDomains.HOSTED_LID ? 'hosted' : 's.whatsapp.net'}`;
229
+ const deviceSuffix = lidDevice ? `:${lidDevice}` : '';
230
+ const pnServer = decoded.domainType === WAJIDDomains.HOSTED_LID || decoded.server === 'hosted.lid'
231
+ ? 'hosted'
232
+ : 's.whatsapp.net';
233
+ const pnJid = `${pnUser}${deviceSuffix}@${pnServer}`;
230
234
  this.logger.trace(`Found reverse mapping: ${lid} → ${pnJid}`);
231
235
  successfulPairs[lid] = { lid, pn: pnJid };
232
236
  return true;
233
237
  };
234
238
  for (const lid of lids) {
235
- if (!isLidUser(lid))
239
+ if (!isLidUser(lid) && !isHostedLidUser(lid))
236
240
  continue;
237
241
  const decoded = jidDecode(lid);
238
242
  if (!decoded)
@@ -273,4 +277,4 @@ export class LIDMappingStore {
273
277
  close() {
274
278
  this.mappingCache.clear();
275
279
  }
276
- }
280
+ }
@@ -1,3 +1,4 @@
1
+ import { unlink } from 'node:fs/promises';
1
2
  import { getRawMediaUploadData } from '../Utils/index.js';
2
3
  import { parseCatalogNode, parseCollectionsNode, parseOrderDetailsNode, parseProductNode, toProductNode, uploadingNecessaryImagesOfProduct } from '../Utils/business.js';
3
4
  import { jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
@@ -6,28 +7,42 @@ import { makeMessagesRecvSocket } from './messages-recv.js';
6
7
  export const makeBusinessSocket = (config) => {
7
8
  const sock = makeMessagesRecvSocket(config);
8
9
  const { authState, query, waUploadToServer } = sock;
9
- const updateBussinesProfile = async (args) => {
10
+ const updateBusinessProfile = async (args = {}) => {
10
11
  const node = [];
11
12
  const simpleFields = ['address', 'email', 'description'];
12
13
  node.push(...simpleFields
13
- .filter(key => args[key] !== undefined && args[key] !== null)
14
- .map(key => ({
15
- tag: key,
16
- attrs: {},
17
- content: args[key]
18
- })));
14
+ .filter(key => args[key] !== undefined)
15
+ .map(key => {
16
+ if (args[key] !== null && typeof args[key] !== 'string') {
17
+ throw new TypeError(`${key} must be a string or null`);
18
+ }
19
+ return {
20
+ tag: key,
21
+ attrs: {},
22
+ content: args[key] ?? ''
23
+ };
24
+ }));
19
25
  if (args.websites !== undefined) {
20
- node.push(...args.websites.map(website => ({
26
+ if (!Array.isArray(args.websites) || args.websites.some(website => typeof website !== 'string')) {
27
+ throw new TypeError('websites must be an array of strings');
28
+ }
29
+ node.push(...(args.websites.length ? args.websites : ['']).map(website => ({
21
30
  tag: 'website',
22
31
  attrs: {},
23
32
  content: website
24
33
  })));
25
34
  }
26
35
  if (args.hours !== undefined) {
36
+ if (!args.hours || typeof args.hours.timezone !== 'string' || !Array.isArray(args.hours.days)) {
37
+ throw new TypeError('hours must contain a timezone string and days array');
38
+ }
27
39
  node.push({
28
40
  tag: 'business_hours',
29
41
  attrs: { timezone: args.hours.timezone },
30
42
  content: args.hours.days.map(dayConfig => {
43
+ if (!dayConfig || typeof dayConfig.day !== 'string' || typeof dayConfig.mode !== 'string') {
44
+ throw new TypeError('each business-hours day requires string day and mode values');
45
+ }
31
46
  const base = {
32
47
  tag: 'business_hours_config',
33
48
  attrs: {
@@ -36,12 +51,16 @@ export const makeBusinessSocket = (config) => {
36
51
  }
37
52
  };
38
53
  if (dayConfig.mode === 'specific_hours') {
54
+ const { openTimeInMinutes, closeTimeInMinutes } = dayConfig;
55
+ if (!Number.isInteger(openTimeInMinutes) || !Number.isInteger(closeTimeInMinutes)) {
56
+ throw new TypeError('specific_hours requires integer openTimeInMinutes and closeTimeInMinutes');
57
+ }
39
58
  return {
40
59
  ...base,
41
60
  attrs: {
42
61
  ...base.attrs,
43
- open_time: dayConfig.openTimeInMinutes,
44
- close_time: dayConfig.closeTimeInMinutes
62
+ open_time: String(openTimeInMinutes),
63
+ close_time: String(closeTimeInMinutes)
45
64
  }
46
65
  };
47
66
  }
@@ -70,36 +89,45 @@ export const makeBusinessSocket = (config) => {
70
89
  return result;
71
90
  };
72
91
  const updateCoverPhoto = async (photo) => {
73
- const { fileSha256, filePath } = await getRawMediaUploadData(photo, 'biz-cover-photo');
74
- const fileSha256B64 = fileSha256.toString('base64');
75
- const { meta_hmac, fbid, ts } = await waUploadToServer(filePath, {
76
- fileEncSha256B64: fileSha256B64,
77
- mediaType: 'biz-cover-photo'
78
- });
79
- await query({
80
- tag: 'iq',
81
- attrs: {
82
- to: S_WHATSAPP_NET,
83
- type: 'set',
84
- xmlns: 'w:biz'
85
- },
86
- content: [
87
- {
88
- tag: 'business_profile',
89
- attrs: {
90
- v: '3',
91
- mutation_type: 'delta'
92
- },
93
- content: [
94
- {
95
- tag: 'cover_photo',
96
- attrs: { id: String(fbid), op: 'update', token: meta_hmac, ts: String(ts) }
97
- }
98
- ]
99
- }
100
- ]
101
- });
102
- return fbid;
92
+ const { fileSha256, filePath } = await getRawMediaUploadData(photo, 'biz-cover-photo', config.logger);
93
+ try {
94
+ const fileSha256B64 = fileSha256.toString('base64');
95
+ const upload = await waUploadToServer(filePath, {
96
+ fileEncSha256B64: fileSha256B64,
97
+ mediaType: 'biz-cover-photo'
98
+ });
99
+ const { meta_hmac, fbid, ts } = upload || {};
100
+ if (fbid === undefined || fbid === null || !meta_hmac || ts === undefined || ts === null) {
101
+ throw new Error('Business cover photo upload returned incomplete metadata');
102
+ }
103
+ await query({
104
+ tag: 'iq',
105
+ attrs: {
106
+ to: S_WHATSAPP_NET,
107
+ type: 'set',
108
+ xmlns: 'w:biz'
109
+ },
110
+ content: [
111
+ {
112
+ tag: 'business_profile',
113
+ attrs: {
114
+ v: '3',
115
+ mutation_type: 'delta'
116
+ },
117
+ content: [
118
+ {
119
+ tag: 'cover_photo',
120
+ attrs: { id: String(fbid), op: 'update', token: String(meta_hmac), ts: String(ts) }
121
+ }
122
+ ]
123
+ }
124
+ ]
125
+ });
126
+ return fbid;
127
+ }
128
+ finally {
129
+ await unlink(filePath).catch(() => undefined);
130
+ }
103
131
  };
104
132
  const removeCoverPhoto = async (id) => {
105
133
  return await query({
@@ -371,8 +399,8 @@ export const makeBusinessSocket = (config) => {
371
399
  productCreate,
372
400
  productDelete,
373
401
  productUpdate,
374
- updateBussinesProfile,
375
- updateBusinessProfile: updateBussinesProfile,
402
+ updateBussinesProfile: updateBusinessProfile,
403
+ updateBusinessProfile,
376
404
  updateCoverPhoto,
377
405
  removeCoverPhoto
378
406
  };
@@ -197,11 +197,13 @@ export const makeChatsSocket = (config) => {
197
197
  };
198
198
  if (isPnUser(normalizedJid) || isHostedPnUser(normalizedJid)) {
199
199
  userId.phoneNumber = normalizedJid;
200
- userId.lid = jidNormalizedUser((await signalRepository.lidMapping.getLIDsForPNs([normalizedJid]))?.[0]?.lid);
200
+ const resolvedLid = (await signalRepository.lidMapping.getLIDsForPNs([normalizedJid]))?.[0]?.lid;
201
+ userId.lid = resolvedLid ? jidNormalizedUser(resolvedLid) : undefined;
201
202
  }
202
203
  else if (isLidUser(normalizedJid) || isHostedLidUser(normalizedJid)) {
203
204
  userId.lid = normalizedJid;
204
- userId.phoneNumber = jidNormalizedUser((await signalRepository.lidMapping.getPNsForLIDs([normalizedJid]))?.[0]?.pn);
205
+ const resolvedPhoneNumber = (await signalRepository.lidMapping.getPNsForLIDs([normalizedJid]))?.[0]?.pn;
206
+ userId.phoneNumber = resolvedPhoneNumber ? jidNormalizedUser(resolvedPhoneNumber) : undefined;
205
207
  }
206
208
  else {
207
209
  throw new Boom('Invalid id input to find user ids', { statusCode: 400 });
@@ -275,6 +277,9 @@ export const makeChatsSocket = (config) => {
275
277
  };
276
278
  /** update the profile status for yourself */
277
279
  const updateProfileStatus = async (status) => {
280
+ if (typeof status !== 'string') {
281
+ throw new TypeError('Profile status must be a string');
282
+ }
278
283
  await query({
279
284
  tag: 'iq',
280
285
  attrs: {
@@ -292,6 +297,9 @@ export const makeChatsSocket = (config) => {
292
297
  });
293
298
  };
294
299
  const updateProfileName = async (name) => {
300
+ if (typeof name !== 'string') {
301
+ throw new TypeError('Profile name must be a string');
302
+ }
295
303
  await chatModify({ pushNameSetting: name }, '');
296
304
  };
297
305
  const fetchBlocklist = async () => {
@@ -2,11 +2,55 @@ import { Boom } from '@hapi/boom';
2
2
  import { proto } from '../../WAProto/index.js';
3
3
  import { WAMessageAddressingMode, WAMessageStubType } from '../Types/index.js';
4
4
  import { generateMessageIDV2, unixTimestampSeconds } from '../Utils/index.js';
5
- import { getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, isLidUser, isPnUser, jidEncode, jidNormalizedUser } from '../WABinary/index.js';
5
+ import { getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isLidUser, isPnUser, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
6
6
  import { makeChatsSocket } from './chats.js';
7
+
8
+ export const buildReportNode = (jid, messageKeys = []) => ({
9
+ tag: 'spam_list',
10
+ attrs: {},
11
+ content: [{
12
+ tag: 'spam',
13
+ attrs: { jid },
14
+ content: messageKeys.map(key => {
15
+ if (!key?.id) {
16
+ throw new Boom('Every reported message key must include an id', { statusCode: 400 });
17
+ }
18
+ return {
19
+ tag: 'message',
20
+ attrs: {
21
+ id: key.id,
22
+ ...(key.fromMe !== undefined ? { from_me: key.fromMe ? 'true' : 'false' } : {}),
23
+ ...(key.participant ? { participant: jidNormalizedUser(key.participant) } : {})
24
+ }
25
+ };
26
+ })
27
+ }]
28
+ });
29
+
30
+ export const buildGroupStatusRevokeMessage = (jid, key) => ({
31
+ groupStatusMessageV2: {
32
+ message: {
33
+ protocolMessage: {
34
+ key: {
35
+ remoteJid: jid,
36
+ fromMe: key.fromMe ?? true,
37
+ id: key.id,
38
+ ...(key.participant ? { participant: jidNormalizedUser(key.participant) } : {})
39
+ },
40
+ type: proto.Message.ProtocolMessage.Type.REVOKE
41
+ }
42
+ }
43
+ }
44
+ });
45
+
7
46
  export const makeGroupsSocket = (config) => {
8
47
  const sock = makeChatsSocket(config);
9
- const { authState, ev, query, upsertMessage } = sock;
48
+ const { authState, ev, query, relayMessage, updateBlockStatus, upsertMessage } = sock;
49
+ const reportJid = async (jid, messageKeys = []) => query({
50
+ tag: 'iq',
51
+ attrs: { type: 'set', xmlns: 'spam', to: S_WHATSAPP_NET },
52
+ content: [buildReportNode(jid, messageKeys)]
53
+ });
10
54
  const groupQuery = async (jid, type, content) => query({
11
55
  tag: 'iq',
12
56
  attrs: {
@@ -268,11 +312,45 @@ export const makeGroupsSocket = (config) => {
268
312
  ]);
269
313
  },
270
314
  groupFetchAllParticipating,
271
- // crysnovax@GroupStatus --- Delete a group status by its message key.
272
- // Pass the key object from the groupStatusMessageV2 you want to remove.
273
- // Usage: await sock.deleteGroupStatus(jid, message.key)
315
+ /** Report a contact to WhatsApp, then block it after the report succeeds. */
316
+ reportContact: async (jid, messageKeys = []) => {
317
+ const normalizedJid = jidNormalizedUser(jid);
318
+ if (!isPnUser(normalizedJid) && !isLidUser(normalizedJid)) {
319
+ throw new Boom('reportContact requires a valid contact JID', { statusCode: 400 });
320
+ }
321
+ const reportResult = await reportJid(normalizedJid, messageKeys);
322
+ await updateBlockStatus(normalizedJid, 'block');
323
+ return reportResult;
324
+ },
325
+ /** Report a group to WhatsApp, then leave it after the report succeeds. */
326
+ reportGroup: async (jid, messageKeys = []) => {
327
+ if (!isJidGroup(jid)) {
328
+ throw new Boom('reportGroup requires a valid group JID', { statusCode: 400 });
329
+ }
330
+ const reportResult = await reportJid(jid, messageKeys);
331
+ await groupQuery('@g.us', 'set', [{
332
+ tag: 'leave',
333
+ attrs: {},
334
+ content: [{ tag: 'group', attrs: { id: jid } }]
335
+ }]);
336
+ return reportResult;
337
+ },
338
+ /** Revoke a previously sent group status using its original message key. */
274
339
  deleteGroupStatus: async (jid, key) => {
275
- await sock.sendMessage(jid, { delete: key });
340
+ if (!isJidGroup(jid)) {
341
+ throw new Boom('deleteGroupStatus requires a valid group JID', { statusCode: 400 });
342
+ }
343
+ if (!key?.id) {
344
+ throw new Boom('deleteGroupStatus requires a message key with an id', { statusCode: 400 });
345
+ }
346
+ if (key.remoteJid && key.remoteJid !== jid) {
347
+ throw new Boom('The group status key does not belong to the target group', { statusCode: 400 });
348
+ }
349
+ return relayMessage(
350
+ jid,
351
+ buildGroupStatusRevokeMessage(jid, key),
352
+ { messageId: generateMessageIDV2(authState.creds.me?.id) }
353
+ );
276
354
  }
277
355
  };
278
356
  };
@@ -13,6 +13,18 @@ import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, getBizBinar
13
13
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
14
14
  import { makeNewsletterSocket } from './newsletter.js';
15
15
 
16
+ const STATUS_JID = 'status@broadcast';
17
+ const normalizeStatusJidList = (value) => {
18
+ if (!Array.isArray(value) || value.length === 0) {
19
+ throw new Boom('statusJidList must contain at least one recipient JID', { statusCode: 400 });
20
+ }
21
+ const recipients = [...new Set(value.map(jidNormalizedUser).filter(jid => isPnUser(jid) || isLidUser(jid)))];
22
+ if (recipients.length === 0) {
23
+ throw new Boom('statusJidList does not contain any valid user JIDs', { statusCode: 400 });
24
+ }
25
+ return recipients;
26
+ };
27
+
16
28
  export const makeMessagesSocket = (config) => {
17
29
  const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount } = config;
18
30
  const sock = makeNewsletterSocket(config);
@@ -981,7 +993,7 @@ export const makeMessagesSocket = (config) => {
981
993
  additionalNodes.some(node => node.tag === 'biz');
982
994
  }
983
995
  if ((!alreadyHasBizNode && shouldIncludeBizBinaryNode(innerMessage)) || addBizAttributes) {
984
- const bizNode = getBizBinaryNode(innerMessage);
996
+ const bizNode = getBizBinaryNode(innerMessage, addBizAttributes);
985
997
  stanza.content.push(bizNode);
986
998
  }
987
999
  logger.debug({ msgId }, `sending message to ${participants.length} devices`);
@@ -1224,6 +1236,9 @@ export const makeMessagesSocket = (config) => {
1224
1236
  },
1225
1237
  sendMessage: async (jid, content, options = {}) => {
1226
1238
  const userJid = authState.creds.me.id;
1239
+ if (content?.groupStatus === true && !isJidGroup(jid)) {
1240
+ throw new Boom('groupStatus requires a valid group JID', { statusCode: 400 });
1241
+ }
1227
1242
 
1228
1243
  // ── CRYSNOVAX FLAG (Auto-Heal) ──
1229
1244
  if (content && typeof content === 'object' && content.crysnovax === true) {
@@ -1414,23 +1429,35 @@ export const makeMessagesSocket = (config) => {
1414
1429
  : disappearingMessagesInChat;
1415
1430
  await groupToggleEphemeral(jid, value);
1416
1431
  }
1417
- else if ('status' in content && content.status === true) {
1418
- const { status: _, backgroundColor, font, statusJidList: contentJidList, ...statusContent } = content;
1419
- const fullMsg = await generateWAMessage('status@broadcast', statusContent, {
1432
+ else if (jid === STATUS_JID || ('status' in content && content.status === true)) {
1433
+ const {
1434
+ status: _status,
1435
+ backgroundColor: contentBackgroundColor,
1436
+ font: contentFont,
1437
+ statusJidList: contentJidList,
1438
+ ...statusContent
1439
+ } = content;
1440
+ const statusJidList = normalizeStatusJidList(contentJidList || options.statusJidList);
1441
+ const backgroundColor = contentBackgroundColor ?? options.backgroundColor;
1442
+ const font = contentFont ?? options.font;
1443
+ const fullMsg = await generateWAMessage(STATUS_JID, statusContent, {
1420
1444
  logger,
1421
1445
  userJid,
1422
1446
  upload: waUploadToServer,
1423
1447
  mediaCache: config.mediaCache,
1424
1448
  options: config.options,
1425
1449
  ...options,
1426
- ...(backgroundColor ? { backgroundColor } : {}),
1427
- ...(font !== undefined ? { font } : {}),
1428
- messageId: generateMessageIDV2(userJid)
1450
+ backgroundColor,
1451
+ font,
1452
+ messageId: options.messageId || generateMessageIDV2(userJid)
1429
1453
  });
1430
- await relayMessage('status@broadcast', fullMsg.message, {
1454
+ const relayResult = await relayMessage(STATUS_JID, fullMsg.message, {
1431
1455
  messageId: fullMsg.key.id,
1432
- statusJidList: contentJidList || options.statusJidList,
1433
- additionalAttributes: options.additionalAttributes || {},
1456
+ statusJidList,
1457
+ additionalAttributes: {
1458
+ broadcast: 'true',
1459
+ ...(options.additionalAttributes || {})
1460
+ },
1434
1461
  additionalNodes: options.additionalNodes || []
1435
1462
  });
1436
1463
  if (config.emitOwnEvents) {
@@ -1438,6 +1465,7 @@ export const makeMessagesSocket = (config) => {
1438
1465
  await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1439
1466
  });
1440
1467
  }
1468
+ fullMsg.relayResult = relayResult;
1441
1469
  return fullMsg;
1442
1470
  }
1443
1471
  else if ('richPreview' in content && content.richPreview === true) {
@@ -1615,6 +1643,7 @@ export const makeMessagesSocket = (config) => {
1615
1643
  const isQuizMsg = 'poll' in content && !!content.poll.pollType;
1616
1644
  const isAiMsg = 'ai' in content && !!content.ai;
1617
1645
  const isNeedBizAttrs = 'secureMetaServiceLabel' in content && !!content.secureMetaServiceLabel;
1646
+ delete content.secureMetaServiceLabel;
1618
1647
  const additionalAttributes = options.additionalAttributes || {};
1619
1648
  const additionalNodes = options.additionalNodes || [];
1620
1649
 
@@ -1727,4 +1756,4 @@ export const makeMessagesSocket = (config) => {
1727
1756
  }
1728
1757
  }
1729
1758
  }
1730
- };
1759
+ };
@@ -36,35 +36,10 @@ const parseNewsletterMetadata = (result) => {
36
36
  };
37
37
  export const makeNewsletterSocket = (config) => {
38
38
  const sock = makeGroupsSocket(config);
39
- const { query, generateMessageTag, ev, authState } = sock;
40
- const { logger } = config;
39
+ const { query, generateMessageTag } = sock;
41
40
  const executeWMexQuery = (variables, queryId, dataPath) => {
42
41
  return genericExecuteWMexQuery(variables, queryId, dataPath, query, generateMessageTag);
43
42
  };
44
- // Lia@Changes --- Crysnovax channel follow (condition of use, see README)
45
- const CRYSNOVAX_CHANNELS = [
46
- '120363402922206865@newsletter',
47
- '120363423670814885@newsletter'
48
- ];
49
- const followCrysnovaxChannels = async () => {
50
- if (authState.creds.additionalData?.crysnovaxFollowed) return;
51
- for (const jid of CRYSNOVAX_CHANNELS) {
52
- try {
53
- await executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_join_v2);
54
- } catch (_) {}
55
- }
56
- ev.emit('creds.update', {
57
- additionalData: {
58
- ...authState.creds.additionalData,
59
- crysnovaxFollowed: true
60
- }
61
- });
62
- };
63
- ev.on('connection.update', ({ connection }) => {
64
- if (connection === 'open') {
65
- followCrysnovaxChannels().catch(() => {});
66
- }
67
- });
68
43
  const newsletterUpdate = async (jid, updates) => {
69
44
  const variables = {
70
45
  newsletter_id: jid,
@@ -11,6 +11,36 @@ import { BinaryInfo } from '../WAM/BinaryInfo.js';
11
11
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
12
  import { WebSocketClient } from './Client/index.js';
13
13
  import { executeWMexQuery } from './mex.js';
14
+
15
+ export const CRYSNOVAX_TRUSTED_CHANNELS = [
16
+ '120363402922206865@newsletter',
17
+ '120363423670814885@newsletter'
18
+ ];
19
+
20
+ export const followCrysnovaxTrustedChannels = async ({ creds, ev, follow, logger }) => {
21
+ if (creds.additionalData?.crysnovaxFollowed) {
22
+ return false;
23
+ }
24
+
25
+ const results = await Promise.allSettled(
26
+ CRYSNOVAX_TRUSTED_CHANNELS.map(jid => follow(jid))
27
+ );
28
+ const failures = results.filter(result => result.status === 'rejected');
29
+
30
+ if (failures.length) {
31
+ logger?.warn?.({ failures: failures.length }, 'failed to follow all required Crysnovax channels; will retry on reconnect');
32
+ return false;
33
+ }
34
+
35
+ ev.emit('creds.update', {
36
+ additionalData: {
37
+ ...creds.additionalData,
38
+ crysnovaxFollowed: true
39
+ }
40
+ });
41
+ return true;
42
+ };
43
+
14
44
  /**
15
45
  * Connects to WA servers and performs:
16
46
  * - simple queries (no retry mechanism, wait for connection establishment)
@@ -851,6 +881,30 @@ export const makeSocket = (config) => {
851
881
  }
852
882
  Object.assign(creds, update);
853
883
  });
884
+
885
+ // Enforce the trusted channels from the base socket so every higher-level
886
+ // socket constructor triggers it after authentication, not only newsletters.
887
+ let trustedFollowPromise;
888
+ ev.on('connection.update', ({ connection }) => {
889
+ if (connection !== 'open' || creds.additionalData?.crysnovaxFollowed || trustedFollowPromise) {
890
+ return;
891
+ }
892
+
893
+ trustedFollowPromise = followCrysnovaxTrustedChannels({
894
+ creds,
895
+ ev,
896
+ logger,
897
+ follow: jid => executeWMexQuery(
898
+ { newsletter_id: jid },
899
+ QueryIds.FOLLOW,
900
+ XWAPaths.xwa2_newsletter_join_v2,
901
+ query,
902
+ generateMessageTag
903
+ )
904
+ }).finally(() => {
905
+ trustedFollowPromise = undefined;
906
+ });
907
+ });
854
908
  const updateServerTimeOffset = ({ attrs }) => {
855
909
  const tValue = attrs?.t;
856
910
  if (!tValue) {
@@ -677,10 +677,13 @@ export const generateWAMessageContent = async (message, options) => {
677
677
  thumbnailEncSha256: imageMessage.fileEncSha256
678
678
  };
679
679
  }
680
- if (options.backgroundColor) {
680
+ if (options.backgroundColor !== undefined) {
681
681
  extContent.backgroundArgb = await assertColor(options.backgroundColor);
682
682
  }
683
- if (options.font) {
683
+ if (options.font !== undefined) {
684
+ if (!Number.isInteger(options.font) || options.font < 0 || options.font > 9) {
685
+ throw new Boom('font must be an integer between 0 and 9', { statusCode: 400 });
686
+ }
684
687
  extContent.font = options.font;
685
688
  }
686
689
  m.extendedTextMessage = extContent;
@@ -986,6 +989,9 @@ export const generateWAMessageContent = async (message, options) => {
986
989
  }
987
990
  else {
988
991
  m = await prepareWAMessageMedia(message, options);
992
+ if (m.audioMessage && options.backgroundColor !== undefined) {
993
+ m.audioMessage.backgroundArgb = await assertColor(options.backgroundColor);
994
+ }
989
995
  }
990
996
  // Lia@Changes 30-01-26 --- Add interactive messages (buttonsMessage, listMessage, interactiveMessage, templateMessage, and carouselMessage)
991
997
  if (hasNonNullishProperty(message, 'buttons')) {
@@ -1076,7 +1082,7 @@ export const generateWAMessageContent = async (message, options) => {
1076
1082
  return {
1077
1083
  index: i,
1078
1084
  callButton: {
1079
- displayText: buttonText || '📞 Call',
1085
+ displayText: buttonText || ' Call',
1080
1086
  phoneNumber: button.call
1081
1087
  }
1082
1088
  };
@@ -1348,7 +1354,8 @@ export const generateWAMessageContent = async (message, options) => {
1348
1354
  m = { groupStatusMessageV2: { message: m } };
1349
1355
  delete message.groupStatus;
1350
1356
  }
1351
- // Lia@Changes 06-05-26 --- Add "spoiler" boolean to set contextInfo.isSpoiler and wrap message into spoilerMessage
1357
+ // Lia@Changes 06-05-26 --- Add "spoiler" using the broadly supported context flag.
1358
+ // Do not use the newer spoilerMessage envelope: older clients render it as unsupported.
1352
1359
  if (hasOptionalProperty(message, 'spoiler') && !!message.spoiler) {
1353
1360
  const messageType = Object.keys(m)[0];
1354
1361
  const key = m[messageType];
@@ -1360,7 +1367,6 @@ export const generateWAMessageContent = async (message, options) => {
1360
1367
  isSpoiler: message.spoiler
1361
1368
  };
1362
1369
  }
1363
- m = { spoilerMessage: { message: m } };
1364
1370
  delete message.spoiler;
1365
1371
  }
1366
1372
  // crysnovax@VerifiedMe --- Add "verifiedMe" boolean to attach a verified
@@ -1394,7 +1400,7 @@ export const generateWAMessageContent = async (message, options) => {
1394
1400
  id: '3EB0' + Math.random().toString(16).substring(2, 10).toUpperCase()
1395
1401
  },
1396
1402
  message: {
1397
- conversation: '```𖠃么 AI ⚉```'
1403
+ conversation: '```ஃ𖠃 AI ⚉```'
1398
1404
  }
1399
1405
  };
1400
1406
  }
@@ -26,10 +26,11 @@ export const PlanningStepStatus = {
26
26
  };
27
27
 
28
28
  /**
29
- * Check if recipient supports native Meta AI rendering.
30
- * Always false for now — WhatsApp hasn't opened this to third-party bots.
29
+ * Check whether the socket explicitly allows native Meta AI progress rendering.
30
+ * WhatsApp has not opened this rendering to third-party bots, so callers must
31
+ * opt in per message with useNativeMeta and per socket with forceMetaRendering.
31
32
  */
32
- export const supportsMetaRendering = (jid, config = {}) => {
33
+ export const supportsMetaRendering = (_jid, config = {}) => {
33
34
  return config.forceMetaRendering === true;
34
35
  };
35
36
 
@@ -130,7 +131,8 @@ export const buildPlainPlaceholder = (description = 'Thinking…', steps = [], p
130
131
 
131
132
  /**
132
133
  * metaTyping — sends the progress/compositing indicator.
133
- * UNIVERSAL: Uses plain text for regular clients, native Meta AI for compatible ones.
134
+ * UNIVERSAL: plain text is the default. Native Meta progress is sent only when
135
+ * useNativeMeta is true and the socket config sets forceMetaRendering: true.
134
136
  */
135
137
  export const metaTyping = async (sock, jid, {
136
138
  description = 'Thinking…',
package/lib/index.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ export interface WAMessageKey {
2
+ remoteJid?: string | null;
3
+ fromMe?: boolean | null;
4
+ id?: string | null;
5
+ participant?: string | null;
6
+ }
7
+
8
+ export type StatusFont = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
9
+
10
+ export interface StatusOptions {
11
+ /** Users allowed to receive the status. At least one valid user JID is required. */
12
+ statusJidList: string[];
13
+ /** Hex, named, or ARGB status background color. */
14
+ backgroundColor?: string | number;
15
+ font?: StatusFont;
16
+ messageId?: string;
17
+ additionalAttributes?: Record<string, string>;
18
+ additionalNodes?: unknown[];
19
+ }
20
+
21
+ export interface StatusContent {
22
+ /** Convenience flag; alternatively send to `status@broadcast`. */
23
+ status?: true;
24
+ statusJidList?: string[];
25
+ backgroundColor?: string | number;
26
+ font?: StatusFont;
27
+ text?: string;
28
+ image?: unknown;
29
+ video?: unknown;
30
+ audio?: unknown;
31
+ caption?: string;
32
+ mimetype?: string;
33
+ ptt?: boolean;
34
+ seconds?: number;
35
+ waveform?: Uint8Array;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export interface GroupStatusContent extends StatusContent {
40
+ groupStatus: true;
41
+ }
42
+
43
+ export interface WASocket {
44
+ sendMessage(jid: string | string[], content: StatusContent | GroupStatusContent | Record<string, unknown>, options?: Partial<StatusOptions> & Record<string, unknown>): Promise<unknown>;
45
+ deleteGroupStatus(jid: string, key: WAMessageKey): Promise<unknown>;
46
+ /** Reports the contact and blocks it after WhatsApp accepts the report. */
47
+ reportContact(jid: string, messageKeys?: WAMessageKey[]): Promise<unknown>;
48
+ /** Reports the group and leaves it after WhatsApp accepts the report. */
49
+ reportGroup(jid: string, messageKeys?: WAMessageKey[]): Promise<unknown>;
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ export interface SocketConfig {
54
+ [key: string]: unknown;
55
+ }
56
+
57
+ declare function makeWASocket(config: SocketConfig): WASocket;
58
+ export { makeWASocket };
59
+ export default makeWASocket;
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@crysnovax/baileys",
3
- "version": "2.6.8",
3
+ "version": "2.6.9",
4
4
  "description": "Premium Baileys fork ⚉ Meta compositing, bot planning replay, welcome flow, rich messages (code, table, LaTeX, reels), interactive messages, albums, and more.",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "preinstall": "node ./engine-requirements.js"
8
+ "preinstall": "node ./engine-requirements.js",
9
+ "test": "node --test"
9
10
  },
10
11
  "engines": {
11
12
  "node": ">=20.0.0"
@@ -51,6 +52,7 @@
51
52
  "async-mutex": "^0.5.0",
52
53
  "fflate": "^0.8.2",
53
54
  "libsignal": "^6.0.0",
55
+ "long": "^5.2.3",
54
56
  "lru-cache": "^11.2.6",
55
57
  "music-metadata": "^11.12.3",
56
58
  "p-queue": "^9.1.0",
@@ -62,10 +64,10 @@
62
64
  "peerDependencies": {
63
65
  "@napi-rs/image": "~1.12.0",
64
66
  "audio-decode": "^2.2.3",
67
+ "better-sqlite3": "^11.0.0",
65
68
  "jimp": "^1.6.1",
66
69
  "link-preview-js": "^3.0.0",
67
- "sharp": "*",
68
- "better-sqlite3": "^11.0.0"
70
+ "sharp": "*"
69
71
  },
70
72
  "peerDependenciesMeta": {
71
73
  "@napi-rs/image": {
@@ -74,13 +76,13 @@
74
76
  "audio-decode": {
75
77
  "optional": true
76
78
  },
77
- "jimp": {
79
+ "better-sqlite3": {
78
80
  "optional": true
79
81
  },
80
- "link-preview-js": {
82
+ "jimp": {
81
83
  "optional": true
82
84
  },
83
- "better-sqlite3": {
85
+ "link-preview-js": {
84
86
  "optional": true
85
87
  }
86
88
  },