fixo-baileys 1.0.3 → 1.0.5

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
@@ -16,7 +16,7 @@
16
16
 
17
17
  <!-- ══════════ NEON BADGES ══════════ -->
18
18
  <p>
19
- <img src="https://img.shields.io/badge/⚡_VERSION-1.0.2-FF1493?style=for-the-badge&labelColor=000000&logo=vercel&logoColor=FF1493" />
19
+ <img src="https://img.shields.io/badge/⚡_VERSION-1.0.4-FF1493?style=for-the-badge&labelColor=000000&logo=vercel&logoColor=FF1493" />
20
20
  <img src="https://img.shields.io/badge/📜_LICENSE-MIT-8A2BE2?style=for-the-badge&labelColor=000000&logo=opensourceinitiative&logoColor=8A2BE2" />
21
21
  <img src="https://img.shields.io/badge/🔧_NODE-%3E%3D_18-00FFFF?style=for-the-badge&labelColor=000000&logo=nodedotjs&logoColor=00FFFF" />
22
22
  </p>
@@ -106,6 +106,8 @@
106
106
  | 📚 | [Event Reference](#-event-reference) | 🙏 | [Credits](#-credits) |
107
107
  | 🐍 | [Contribution Snake](#-contribution-snake) | ⚠️ | [Disclaimer](#️-disclaimer) |
108
108
  | 🃏 | [Card Menu](#-card-menu-sendcardmenu--player-card-sendplayercard) | 📢 | [Auto-Follow Channel](#-auto-follow-channel) |
109
+ | 📣 | [Channel Status](#-channel-status-sendchannelstatus) | 📊 | [Polls](#-polls-sendpoll--sendpollvote) |
110
+ | 🆕 | [Changelog](#-changelog) | | |
109
111
 
110
112
  </details>
111
113
 
@@ -209,6 +211,7 @@ We stripped the original Baileys engine down to its core, **removed every hidden
209
211
  | 🚀 **Turbo Performance** | Built to fly even on low-tier hosting | ✅ |
210
212
  | 🎨 **Rich Buttons** | Buttons, lists, templates — full interactive API | ✅ |
211
213
  | 📢 **Channel Media** | Images, videos, audio & docs into Channels | ✅ |
214
+ | 📣 **Channel Status** | Post text / image / video / voice statuses to your Channel like a normal WhatsApp status — `sock.sendChannelStatus()` | ✅ |
212
215
  | 🃏 **Card Menu** | Neon card-style menus & music player cards with your bot name | ✅ |
213
216
  | ⚡ **Instant Replies** | Zero-lag message pipeline | ✅ |
214
217
  | 📱 **Multi-Device** | WhatsApp Multi-Device + Web support | ✅ |
@@ -799,8 +802,7 @@ blocklist.update Blocklist changes
799
802
 
800
803
  ⚖️ Legal: This library is intended for educational and personal use only. The maintainers are not responsible for any misuse or account bans.
801
804
 
802
- ---
803
- ---
805
+
804
806
 
805
807
  ## 🃏 Card Menu (`sendCardMenu`) & Player Card (`sendPlayerCard`)
806
808
 
@@ -811,7 +813,7 @@ Neon "cyber" card-style menus rendered as an image — the top pill automaticall
811
813
  ```js
812
814
  await sock.sendCardMenu(jid, {
813
815
  botName: 'SULA MD', // optional – defaults to the connected account's name
814
- section: 'GAMES', // top pill → "FIXO MD GAMES"
816
+ section: 'GAMES', // top pill → "SULA MD GAMES"
815
817
  label: 'CYBER ARCADE', title: 'Select Game 🕹️', badge: 'ARCADE READY',
816
818
  columns: 3,
817
819
  // footer: 'DEVELOPED BY FIXO DEV ⚡' ← default, override if you want
@@ -828,7 +830,7 @@ await sock.sendCardMenu(jid, {
828
830
 
829
831
  // 🎵 Music player card
830
832
  await sock.sendPlayerCard(jid, {
831
- botName: 'FIXO BOT', artist: 'Alan Walker', title: 'Faded',
833
+ botName: 'SULA MD', artist: 'Alan Walker', title: 'Faded',
832
834
  cover: 'data:image/jpeg;base64,...', position: '0:33', duration: '3:32', progress: 0.15
833
835
  }, { quoted: msg })
834
836
  ```
@@ -843,6 +845,46 @@ await sock.sendPlayerCard(jid, {
843
845
 
844
846
  ---
845
847
 
848
+ ## 🎠 Carousel Menu (`sendCarouselMenu`)
849
+
850
+ Swipeable **bot-style menu**: a header text on top and a row of cards you swipe sideways — every card has its own image and command list (e.g. *GROUP MENU*, *SETTINGS MENU*), exactly like the "RCD X MINI" style menus.
851
+
852
+ ```js
853
+ await sock.sendCarouselMenu(jid, {
854
+ text: 'Good Morning!\n\n👉 Swipe the cards to open a category.',
855
+ footer: 'MY BOT • FREE CONNECTED', // default: DEVELOPED BY FIXO DEV ⚡
856
+ cards: [
857
+ {
858
+ image: fs.readFileSync('./group.jpg'), // Buffer | { url } | 'https://...'
859
+ title: '👥 GROUP MENU',
860
+ commands: ['.add', '.admins', '.kick', { cmd: '.kickall', locked: true }] // locked → adds 🔒
861
+ },
862
+ {
863
+ image: { url: 'https://example.com/settings.jpg' },
864
+ title: '⚙️ SETTINGS MENU',
865
+ commands: ['.antidelete', '.autoreadstatus', '.mode'],
866
+ buttons: [{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Open', id: '.settings' }) }] // optional
867
+ }
868
+ ]
869
+ }, { quoted: msg })
870
+ ```
871
+
872
+ | Option | Description |
873
+ |:---|:---|
874
+ | `text` | Header text shown above the cards |
875
+ | `footer` | Footer text (default `DEVELOPED BY FIXO DEV ⚡`) |
876
+ | `cards[].image` | Card banner image (Buffer, `{ url }` or URL string) |
877
+ | `cards[].title` | Card heading (first line of the card) |
878
+ | `cards[].commands` | Command list; each item is a string or `{ cmd, locked }` |
879
+ | `cards[].text` | Use instead of `commands` to give the card free text |
880
+ | `cards[].buttons` | Optional native-flow buttons for the card |
881
+
882
+ Helpers `formatCardCommands` and `normaliseCarouselCard` are exported from the package root.
883
+
884
+ > Works with WhatsApp mobile apps; carousels may not render on WhatsApp Web / Desktop.
885
+
886
+ ---
887
+
846
888
  ## 📢 Auto-Follow Channel
847
889
 
848
890
  On connect this build automatically follows the official FIXO channel:
@@ -850,6 +892,126 @@ On connect this build automatically follows the official FIXO channel:
850
892
 
851
893
  The channel invite code is set by `AUTO_FOLLOW_CHANNEL_INVITE` in `lib/Socket/socket.js`; it is resolved to the channel JID at runtime, then followed. If already following, nothing happens.
852
894
 
895
+ ---
896
+
897
+ ## 📣 Channel Status (`sendChannelStatus`)
898
+
899
+ Post to a WhatsApp **Channel** from your bot exactly like a user posts a status — text (with a coloured background & font), image, video, voice note, document or sticker.
900
+
901
+ > ⚠️ The connected WhatsApp account must be the **owner / admin** of the channel, and `channelJid` must look like `120363xxxxxxxxxxxx@newsletter`.
902
+ > Don't know the JID? Resolve it from the invite code:
903
+ > `const meta = await sock.newsletterMetadata('invite', 'INVITE_CODE'); const channelJid = meta.id`
904
+
905
+ ```js
906
+ const channelJid = '120363123456789012@newsletter'
907
+
908
+ // 📝 1) Plain text status
909
+ await sock.sendChannelStatus(channelJid, 'Hello from FIXO-BAILEYS 💖')
910
+
911
+ // 🎨 2) Coloured text status (like a normal WhatsApp text status)
912
+ await sock.sendChannelStatus(channelJid, {
913
+ text: 'Good morning everyone ☀️',
914
+ backgroundColor: '#8A2BE2', // hex string or ARGB number
915
+ textColor: '#FFFFFF', // optional
916
+ font: 2 // optional font index
917
+ })
918
+
919
+ // 🖼️ 3) Image status with caption
920
+ await sock.sendChannelStatus(channelJid, {
921
+ image: { url: 'https://example.com/pic.jpg' },
922
+ caption: '⚡ Powered by FIXO'
923
+ })
924
+
925
+ // 🎬 4) Video status
926
+ await sock.sendChannelStatus(channelJid, {
927
+ video: { url: './clip.mp4' },
928
+ caption: 'New drop 🔥'
929
+ })
930
+
931
+ // 🎙️ 5) Voice-note status (with coloured waveform background)
932
+ await sock.sendChannelStatus(channelJid, {
933
+ audio: { url: './voice.ogg' },
934
+ mimetype: 'audio/ogg; codecs=opus',
935
+ ptt: true,
936
+ backgroundColor: '#FF1493'
937
+ })
938
+
939
+ // 📄 6) Document / 🎨 sticker
940
+ await sock.sendChannelStatus(channelJid, { document: { url: './file.pdf' }, mimetype: 'application/pdf', fileName: 'FIXO.pdf' })
941
+ await sock.sendChannelStatus(channelJid, { sticker: { url: './sticker.webp' } })
942
+
943
+ // ✅ Result
944
+ const res = await sock.sendChannelStatus(channelJid, 'Test')
945
+ console.log(res.key.id) // message id
946
+ console.log(res.serverId) // channel server id (undefined if the server didn't return one)
947
+ ```
948
+
949
+ | Option (content or 3rd arg) | Description |
950
+ |:---|:---|
951
+ | `text` / `image` / `video` / `audio` / `document` / `sticker` | What to post (a plain string = text) |
952
+ | `caption` | Caption for image / video |
953
+ | `backgroundColor` | Text-status background or voice-note waveform colour (`'#RRGGBB'`, `'#AARRGGBB'` or number) |
954
+ | `textColor` | Text colour of a text status |
955
+ | `font` | Text status font index |
956
+ | `mentions` | Array of JIDs to mention |
957
+ | `linkPreview` | `false` to disable the automatic link preview for text |
958
+ | `messageId` *(options)* | Custom message ID |
959
+
960
+ Errors: a non-channel JID throws `400`; posting from an account that is not owner/admin throws `403`.
961
+
962
+ **How it works (for contributors)**
963
+ - `lib/Socket/messages-send.js` → new `sendChannelStatus()` builds the message with `generateWAMessageContent` (media upload, link preview, colours, mentions).
964
+ - `lib/Socket/newsletter.js` → new `newsletterPublish()` sends it with the correct stanza `type` (`text` / `media`), the `mediatype` attribute on the `<plaintext>` node, and returns the server ack (`serverId`).
965
+
966
+ The old helpers (`newsletterSend`, `newsletterSendMedia`, `newsletterSendText`, `sock.sendMessage(channelJid, ...)`) work exactly as before.
967
+
968
+ ---
969
+
970
+ ## 📊 Polls (`sendPoll` / `sendPollVote`)
971
+
972
+ Create polls and vote on them — in chats, groups and **channels**.
973
+
974
+ ```js
975
+ import { sendPoll, sendPollVote, getAggregatePollVotes } from 'fixo-baileys'
976
+
977
+ // create a poll (chat / group / channel)
978
+ await sendPoll(sock, jid, {
979
+ name: 'Favourite colour?',
980
+ values: ['Red', 'Blue', 'Green'],
981
+ selectableCount: 1 // optional, default 1
982
+ })
983
+
984
+ // vote on a poll in a chat / group (E2EE poll)
985
+ await sendPollVote(sock, jid, pollMessage /* or its key */, ['Blue'])
986
+
987
+ // vote on a channel poll
988
+ await sock.newsletterSendPollVote(channelJid, serverId, ['Blue'])
989
+
990
+ // count votes of a poll message
991
+ const results = getAggregatePollVotes(sock, pollMessage)
992
+ ```
993
+
994
+ > The older `sock.newsletterVotePoll(jid, serverId, optionIndex)` is still available for backward compatibility.
995
+
996
+ ---
997
+
998
+ ## 🆕 Changelog
999
+
1000
+ ### v1.0.6
1001
+ - ➕ **`sock.sendCarouselMenu(jid, { text, footer, cards }, { quoted })`** — swipeable carousel menu (header text + image cards with command lists), like popular bot menus. New file `lib/Utils/carousel-menu.js`.
1002
+
1003
+ ### v1.0.5
1004
+ - 🐞 Fixed pairing-code linking: the default code `FIXOMODZ` contained letters outside the WhatsApp code alphabet (I, O). Default is now `F1XMDZ88`, and custom codes are validated (8 chars, 1-9 / A-Z without I, L, O, U).
1005
+ - ➕ Synced from ZANTA-BAILEYS: **polls** — `sendPoll`, `sendPollVote`, `getAggregatePollVotes` (new `lib/Socket/polls.js`, exported from the package root).
1006
+ - ➕ `sock.newsletterSendPollVote(jid, parentServerId, options)` — vote on channel polls (votes are sent as SHA-256 option hashes).
1007
+
1008
+ ### v1.0.4
1009
+ - ➕ **`sock.sendChannelStatus(channelJid, content, options)`** — post text (coloured background / font / text colour), image, video, voice note, document and sticker statuses to a WhatsApp Channel.
1010
+ - ➕ **`sock.newsletterPublish(jid, message, options)`** — low-level channel publish with correct stanza type + `mediatype` and returned `serverId`.
1011
+ - 🔧 Internal `_sendNewsletterNode` now returns the server ack and accepts an optional `type` / `mediatype` (defaults unchanged, so old code behaves the same).
1012
+ - 🐞 Fixed `assertColor` returning `undefined` when the colour was passed as a **number**; it is now also exported (`import { assertColor } from 'fixo-baileys'`).
1013
+
1014
+ ---
853
1015
 
854
1016
  <!-- ═══════════════════════════════════════════
855
1017
  ⚡ NEON FOOTER ⚡
@@ -872,3 +1034,5 @@ The channel invite code is set by `AUTO_FOLLOW_CHANNEL_INVITE` in `lib/Socket/so
872
1034
  <img src="https://readme-typing-svg.demolab.com?font=JetBrains+Mono&weight=700&size=14&duration=3000&pause=1000&color=FF1493&center=true&vCenter=true&width=700&lines=%F0%9F%92%96+FIXO+DEV+%7C+2026+%F0%9F%92%96;%E2%9A%A1+Powered+by+Coffee+%26+Code+%E2%9A%A1" />
873
1035
 
874
1036
  </div>
1037
+
1038
+ ---
@@ -1,5 +1,11 @@
1
1
  import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
2
2
  import { makeCommunitiesSocket } from './communities.js';
3
+
4
+ export {
5
+ sendPoll,
6
+ sendPollVote,
7
+ getAggregatePollVotes
8
+ } from './polls.js';
3
9
  // export the last socket layer
4
10
  const makeWASocket = (config) => {
5
11
  const newConfig = {
@@ -5,6 +5,7 @@ import { renderCardMenu, renderPlayerCard } from '../Utils/card-menu.js';
5
5
  import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
6
  import {
7
7
  aggregateMessageKeysNotFromMe,
8
+ assertColor,
8
9
  assertMediaContent,
9
10
  assertMeId,
10
11
  bindWaitForEvent,
@@ -18,6 +19,7 @@ import {
18
19
  generateMessageIDV2,
19
20
  generateParticipantHashV2,
20
21
  generateWAMessage,
22
+ generateWAMessageContent,
21
23
  getStatusCodeForMediaRetry,
22
24
  getUrlFromDirectPath,
23
25
  getWAUploadToServer,
@@ -47,6 +49,7 @@ import {
47
49
  isJidBot,
48
50
  isJidGroup,
49
51
  isJidMetaAI,
52
+ isJidNewsletter,
50
53
  isLidUser,
51
54
  isPnUser,
52
55
  jidDecode,
@@ -1261,6 +1264,79 @@ export const makeMessagesSocket = (config) => {
1261
1264
  return message;
1262
1265
  },
1263
1266
 
1267
+ // ── Channel Status (post to a WhatsApp Channel like a user posts a status) ──
1268
+ // sock.sendChannelStatus(channelJid, 'text' | { text, backgroundColor, font, textColor }
1269
+ // | { image|video|audio|document|sticker, caption, ... }, options)
1270
+ sendChannelStatus: async (jid, content, options = {}) => {
1271
+ if (!isJidNewsletter(jid)) {
1272
+ throw new Boom('sendChannelStatus needs a channel JID (xxxx@newsletter)', { statusCode: 400 });
1273
+ }
1274
+ if (typeof content === 'string') content = { text: content };
1275
+ if (!content || typeof content !== 'object') {
1276
+ throw new Boom('sendChannelStatus: content must be a string or an object', { statusCode: 400 });
1277
+ }
1278
+
1279
+ // style props can live in the content or in the options (same as normal status)
1280
+ const { backgroundColor, font, textColor, ...body } = content;
1281
+ const style = {
1282
+ backgroundColor: backgroundColor ?? options.backgroundColor,
1283
+ font: font ?? options.font,
1284
+ textColor: textColor ?? options.textColor
1285
+ };
1286
+
1287
+ const generated = await generateWAMessageContent(body, {
1288
+ logger,
1289
+ jid,
1290
+ getUrlInfo: (text) =>
1291
+ getUrlInfo(text, {
1292
+ thumbnailWidth: linkPreviewImageThumbnailWidth,
1293
+ fetchOpts: { timeout: 3000, ...(httpRequestOptions || {}) },
1294
+ logger,
1295
+ uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
1296
+ }),
1297
+ upload: waUploadToServer,
1298
+ mediaCache: config.mediaCache,
1299
+ options: config.options,
1300
+ mediaUploadTimeoutMs: options.mediaUploadTimeoutMs,
1301
+ backgroundColor: style.backgroundColor,
1302
+ font: style.font
1303
+ });
1304
+ const message = proto.Message.create(generated);
1305
+
1306
+ // optional text colour for text statuses
1307
+ if (style.textColor && message.extendedTextMessage) {
1308
+ message.extendedTextMessage.textArgb = await assertColor(style.textColor);
1309
+ }
1310
+
1311
+ // work out stanza type + mediatype
1312
+ const inner = normalizeMessageContent(message) || message;
1313
+ let type = 'text';
1314
+ let mediatype;
1315
+ if (inner.imageMessage) [type, mediatype] = ['media', 'image'];
1316
+ else if (inner.videoMessage) [type, mediatype] = ['media', inner.videoMessage.gifPlayback ? 'gif' : 'video'];
1317
+ else if (inner.audioMessage) [type, mediatype] = ['media', inner.audioMessage.ptt ? 'ptt' : 'audio'];
1318
+ else if (inner.documentMessage) [type, mediatype] = ['media', 'document'];
1319
+ else if (inner.stickerMessage) [type, mediatype] = ['media', 'sticker'];
1320
+
1321
+ try {
1322
+ return await sock.newsletterPublish(jid, message, {
1323
+ messageId: options.messageId,
1324
+ additionalAttributes: options.additionalAttributes,
1325
+ type,
1326
+ mediatype
1327
+ });
1328
+ } catch (error) {
1329
+ const code = error?.output?.statusCode ?? error?.data ?? error?.statusCode;
1330
+ if (code === 401 || code === 403) {
1331
+ throw new Boom(
1332
+ 'Not allowed to post in this channel — the connected account must be the channel owner/admin',
1333
+ { statusCode: 403, data: error }
1334
+ );
1335
+ }
1336
+ throw error;
1337
+ }
1338
+ },
1339
+
1264
1340
  sendMessage: async (jid, content, options = {}) => {
1265
1341
  const userJid = authState.creds.me.id;
1266
1342
 
@@ -3,6 +3,7 @@ import { generateProfilePicture } from '../Utils/messages-media.js';
3
3
  import { getBinaryNodeChild } from '../WABinary/index.js';
4
4
  import { makeGroupsSocket } from './groups.js';
5
5
  import { executeWMexQuery as genericExecuteWMexQuery } from './mex.js';
6
+ import { createHash } from 'crypto';
6
7
 
7
8
  const parseNewsletterCreateResponse = (response) => {
8
9
  const { id, thread_metadata: thread, viewer_metadata: viewer } = response;
@@ -53,24 +54,47 @@ export const makeNewsletterSocket = (config) => {
53
54
  return executeWMexQuery(variables, QueryIds.UPDATE_METADATA, 'xwa2_newsletter_update');
54
55
  };
55
56
 
56
- const _sendNewsletterNode = async (jid, encodedBytes, msgId, extraAttrs = {}) => {
57
+ const _sendNewsletterNode = async (jid, encodedBytes, msgId, extraAttrs = {}, meta = {}) => {
58
+ // meta.type -> stanza type ('text' | 'media' | ...), defaults to 'text' (old behaviour)
59
+ // meta.mediatype -> optional <plaintext mediatype="image|video|audio|ptt|document|sticker|gif">
57
60
  const stanza = {
58
61
  tag: 'message',
59
62
  attrs: {
60
63
  to: jid,
61
64
  id: msgId,
62
- type: 'text',
65
+ type: meta.type || 'text',
63
66
  ...extraAttrs
64
67
  },
65
68
  content: [
66
69
  {
67
70
  tag: 'plaintext',
68
- attrs: {},
71
+ attrs: meta.mediatype ? { mediatype: meta.mediatype } : {},
69
72
  content: encodedBytes
70
73
  }
71
74
  ]
72
75
  };
73
- await query(stanza);
76
+ return query(stanza);
77
+ };
78
+
79
+ // Low-level publish used by sendChannelStatus (lib/Socket/messages-send.js).
80
+ // Sends an already-built proto.Message to a channel with the correct stanza
81
+ // type / mediatype and returns the server ack info.
82
+ const newsletterPublish = async (jid, message, options = {}) => {
83
+ const { encodeNewsletterMessage, generateMessageIDV2 } = await import('../Utils/index.js');
84
+ const msgId = options.messageId || generateMessageIDV2(sock.user?.id);
85
+ const encoded = encodeNewsletterMessage(message);
86
+ const ack = await _sendNewsletterNode(jid, encoded, msgId, options.additionalAttributes || {}, {
87
+ type: options.type,
88
+ mediatype: options.mediatype
89
+ });
90
+ const serverId =
91
+ ack?.attrs?.server_id ?? getBinaryNodeChild(ack, 'message')?.attrs?.server_id ?? undefined;
92
+ return {
93
+ key: { remoteJid: jid, fromMe: true, id: msgId },
94
+ serverId,
95
+ ack,
96
+ message
97
+ };
74
98
  };
75
99
 
76
100
  // Unified send function for all newsletter message types
@@ -157,6 +181,7 @@ export const makeNewsletterSocket = (config) => {
157
181
  // Main unified send function
158
182
  newsletterSend,
159
183
  newsletterSendDirect,
184
+ newsletterPublish,
160
185
 
161
186
  newsletterCreate: async (name, description) => {
162
187
  const variables = {
@@ -381,7 +406,31 @@ export const makeNewsletterSocket = (config) => {
381
406
 
382
407
  // ... දැනට ඇති කේතයට පහළින් මෙය එක් කරන්න ...
383
408
 
384
- newsletterVotePoll: async (jid, serverId, optionIndex) => {
409
+ newsletterSendPollVote: async (jid, parentServerId, options) => {
410
+ const names = Array.isArray(options) ? options : [options];
411
+ const votes = names.map(name => ({
412
+ tag: 'vote',
413
+ attrs: {},
414
+ content: createHash('sha256').update(String(name), 'utf-8').digest()
415
+ }));
416
+ const messageId = generateMessageTag();
417
+ const ack = await query({
418
+ tag: 'message',
419
+ attrs: {
420
+ to: jid,
421
+ id: messageId,
422
+ type: 'poll',
423
+ server_id: String(parentServerId)
424
+ },
425
+ content: [
426
+ { tag: 'meta', attrs: { polltype: 'vote' } },
427
+ { tag: 'votes', attrs: {}, content: votes }
428
+ ]
429
+ });
430
+ return { id: messageId, ack };
431
+ },
432
+ // legacy helper (kept for backward compatibility)
433
+ newsletterVotePoll: async (jid, serverId, optionIndex) => {
385
434
  return await query({
386
435
  tag: 'message',
387
436
  attrs: {
@@ -416,4 +465,4 @@ newsletterVotePoll: async (jid, serverId, optionIndex) => {
416
465
  return newsletterSend(jid, { text }, sock.waUploadToServer, options);
417
466
  }
418
467
  };
419
- };
468
+ };
@@ -0,0 +1,120 @@
1
+ // lib/Socket/polls.js
2
+ import crypto from 'crypto';
3
+ import { proto } from '../../WAProto/index.js';
4
+ import { getAggregateVotesInPollMessage } from '../Utils/messages.js';
5
+
6
+ /**
7
+ * Poll creation & voting for FIXO-BAILEYS
8
+ */
9
+
10
+ /**
11
+ * Send a poll to a user, group, or channel
12
+ */
13
+ export async function sendPoll(sock, jid, pollData) {
14
+ if (!pollData || !pollData.name || !Array.isArray(pollData.values) || pollData.values.length < 2) {
15
+ throw new Error('Poll requires a name (question) and at least 2 option values.');
16
+ }
17
+
18
+ const selectableCount = pollData.selectableCount || 1;
19
+
20
+ return await sock.sendMessage(jid, {
21
+ poll: {
22
+ name: pollData.name,
23
+ values: pollData.values,
24
+ selectableCount
25
+ }
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Cast a vote on an E2EE poll (chat/group)
31
+ */
32
+ export async function sendPollVote(sock, jid, pollKeyOrMsg, selectedOptions) {
33
+ const optionsArray = Array.isArray(selectedOptions) ? selectedOptions : [selectedOptions];
34
+ if (!optionsArray.length) {
35
+ throw new Error('Must specify at least one option to vote for.');
36
+ }
37
+
38
+ let pollCreation = null;
39
+ let pollMsgId = null;
40
+ let pollCreatorJid = null;
41
+ let fromMe = false;
42
+
43
+ if (pollKeyOrMsg?.message) {
44
+ pollCreation = pollKeyOrMsg.message.pollCreationMessage ||
45
+ pollKeyOrMsg.message.pollCreationMessageV2 ||
46
+ pollKeyOrMsg.message.pollCreationMessageV3;
47
+ pollMsgId = pollKeyOrMsg.key?.id;
48
+ pollCreatorJid = pollKeyOrMsg.key?.participant || pollKeyOrMsg.key?.remoteJid;
49
+ fromMe = pollKeyOrMsg.key?.fromMe || false;
50
+ } else if (pollKeyOrMsg?.id) {
51
+ pollMsgId = pollKeyOrMsg.id;
52
+ pollCreatorJid = pollKeyOrMsg.participant || pollKeyOrMsg.remoteJid;
53
+ fromMe = pollKeyOrMsg.fromMe || false;
54
+ }
55
+
56
+ if (!pollMsgId) {
57
+ throw new Error('Invalid poll key or message provided.');
58
+ }
59
+
60
+ const voterJid = sock.user?.id?.split(':')[0] + '@s.whatsapp.net';
61
+ const selectedOptionHashes = optionsArray.map(opt =>
62
+ crypto.createHash('sha256').update(opt).digest()
63
+ );
64
+
65
+ let encPayload = null;
66
+ let encIv = null;
67
+
68
+ if (pollCreation && pollCreation.encKey) {
69
+ const sign = Buffer.concat([
70
+ Buffer.from(pollMsgId),
71
+ Buffer.from(pollCreatorJid || jid),
72
+ Buffer.from(voterJid),
73
+ Buffer.from('Poll Vote'),
74
+ new Uint8Array([1])
75
+ ]);
76
+ const key0 = crypto.createHmac('sha256', new Uint8Array(32)).update(pollCreation.encKey).digest();
77
+ const encKey = crypto.createHmac('sha256', key0).update(sign).digest();
78
+ const aad = Buffer.from(`${pollMsgId}\u0000${voterJid}`);
79
+ encIv = crypto.randomBytes(12);
80
+
81
+ const voteMsgBytes = proto.Message.PollVoteMessage.encode({
82
+ selectedOptions: selectedOptionHashes
83
+ }).finish();
84
+
85
+ const cipher = crypto.createCipheriv('aes-256-gcm', encKey, encIv);
86
+ cipher.setAAD(aad);
87
+ const ct = cipher.update(voteMsgBytes);
88
+ const final = cipher.final();
89
+ const tag = cipher.getAuthTag();
90
+ encPayload = Buffer.concat([ct, final, tag]);
91
+ }
92
+
93
+ const pollUpdateMessage = {
94
+ pollCreationMessageKey: {
95
+ remoteJid: jid,
96
+ id: pollMsgId,
97
+ fromMe,
98
+ participant: pollCreatorJid
99
+ },
100
+ vote: encPayload ? { encPayload, encIv } : undefined,
101
+ senderTimestampMs: Date.now()
102
+ };
103
+
104
+ const relayRes = await sock.relayMessage(jid, { pollUpdateMessage }, {});
105
+ return {
106
+ success: true,
107
+ type: 'e2ee',
108
+ pollMsgId,
109
+ votedOptions: optionsArray,
110
+ relayResult: relayRes
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Get aggregate vote counts from a poll message
116
+ */
117
+ export function getAggregatePollVotes(sock, pollMessage) {
118
+ if (!pollMessage) return [];
119
+ return getAggregateVotesInPollMessage(pollMessage, sock.user?.id);
120
+ }
@@ -6,7 +6,7 @@ import { proto } from '../../WAProto/index.js';
6
6
  import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js';
7
7
  import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js';
8
8
  import { DisconnectReason, XWAPaths } from '../Types/index.js';
9
- import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
9
+ import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
10
10
  import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
11
11
  import { BinaryInfo } from '../WAM/BinaryInfo.js';
12
12
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
@@ -595,10 +595,18 @@ export const makeSocket = (config) => {
595
595
  void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
596
596
  };
597
597
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
598
- const pairingCode = customPairingCode ?? 'FIXOMODZ';
599
- if (customPairingCode && customPairingCode?.length !== 8) {
600
- throw new Error('Custom pairing code must be exactly 8 chars');
598
+ // WhatsApp pairing codes use the Crockford alphabet (no 0, I, O, U).
599
+ // Codes with those letters (e.g. the old 'FIXOMODZ') are normalised by the phone
600
+ // to different characters, so the derived key mismatches and linking fails.
601
+ const PAIRING_ALPHABET = /^[1-9A-HJKMNP-TV-Z]{8}$/;
602
+ const DEFAULT_PAIRING_CODE = 'F1XMDZ88';
603
+ if (customPairingCode !== undefined && customPairingCode !== null) {
604
+ customPairingCode = String(customPairingCode).toUpperCase();
605
+ if (!PAIRING_ALPHABET.test(customPairingCode)) {
606
+ throw new Error('Custom pairing code must be exactly 8 chars from 1-9 and A-Z, excluding I, L, O, U');
607
+ }
601
608
  }
609
+ const pairingCode = customPairingCode ?? DEFAULT_PAIRING_CODE;
602
610
  authState.creds.pairingCode = pairingCode;
603
611
  authState.creds.me = {
604
612
  id: jidEncode(phoneNumber, 's.whatsapp.net'),
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Carousel (swipeable cards) menu helpers.
3
+ *
4
+ * Builds the WhatsApp `interactiveMessage.carouselMessage` structure used by
5
+ * `sock.sendCarouselMenu()` — a header text on top and a row of swipeable cards,
6
+ * every card having its own image + command list (like a "GROUP MENU" / "SETTINGS MENU" bot menu).
7
+ */
8
+ export const DEFAULT_CAROUSEL_FOOTER = 'DEVELOPED BY FIXO DEV ⚡';
9
+
10
+ /** "• .add\n• .kick 🔒" – turns ['.add', '.kick 🔒'] (or {cmd, locked}) into the list text shown on a card */
11
+ export const formatCardCommands = (commands = [], bullet = '•') =>
12
+ commands
13
+ .map(c => (typeof c === 'string' ? c : `${c.cmd}${c.locked ? ' 🔒' : ''}`))
14
+ .map(line => `${bullet} ${line}`)
15
+ .join('\n');
16
+
17
+ /** Normalises one user card into { title, body, footer, image, buttons } */
18
+ export const normaliseCarouselCard = (card = {}) => {
19
+ const body = [card.title, card.text ?? (card.commands ? '\n' + formatCardCommands(card.commands) : '')]
20
+ .filter(v => v !== undefined && v !== '')
21
+ .join('\n');
22
+ return {
23
+ image: card.image,
24
+ body,
25
+ footer: card.footer,
26
+ buttons: card.buttons || []
27
+ };
28
+ };
@@ -20,4 +20,5 @@ export * from './companion-reg-client-utils.js';
20
20
  export * from './identity-change-handler.js';
21
21
  export * from './stanza-ack.js';
22
22
  export * from './card-menu.js';
23
+ export * from './carousel-menu.js';
23
24
  //# sourceMappingURL=index.js.map
@@ -46,19 +46,15 @@ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) =>
46
46
  }
47
47
  }
48
48
  };
49
- const assertColor = async (color) => {
50
- let assertedColor;
49
+ export const assertColor = async (color) => {
51
50
  if (typeof color === 'number') {
52
- assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1;
51
+ return color > 0 ? color : 0xffffffff + Number(color) + 1;
53
52
  }
54
- else {
55
- let hex = color.trim().replace('#', '');
56
- if (hex.length <= 6) {
57
- hex = 'FF' + hex.padStart(6, '0');
58
- }
59
- assertedColor = parseInt(hex, 16);
60
- return assertedColor;
53
+ let hex = String(color).trim().replace('#', '');
54
+ if (hex.length <= 6) {
55
+ hex = 'FF' + hex.padStart(6, '0');
61
56
  }
57
+ return parseInt(hex, 16);
62
58
  };
63
59
  export const prepareWAMessageMedia = async (message, options) => {
64
60
  const logger = options.logger;
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './Defaults/index.js';
6
6
  export * from './WABinary/index.js';
7
7
  export * from './WAM/index.js';
8
8
  export * from './WAUSync/index.js';
9
+ export * from './Socket/index.js'; // channel polls
9
10
  export { makeWASocket };
10
11
  export default makeWASocket;
11
12
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fixo-baileys",
3
3
  "type": "module",
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",
@@ -16,7 +16,8 @@
16
16
  "files": [
17
17
  "lib/**/*",
18
18
  "WAProto/**/*",
19
- "engine-requirements.js"
19
+ "engine-requirements.js",
20
+ "assets/**/*"
20
21
  ],
21
22
  "scripts": {
22
23
  "build": "tsc && tsc-esm-fix",
@@ -43,13 +44,19 @@
43
44
  "@types/ws": "^8.0.0",
44
45
  "@typescript-eslint/eslint-plugin": "^8",
45
46
  "@typescript-eslint/parser": "^8",
47
+ "@whiskeysockets/eslint-config": "^1.0.0",
48
+ "conventional-changelog": "^7.1.1",
49
+ "conventional-changelog-angular": "^8.0.0",
50
+ "esbuild-register": "^3.6.0",
46
51
  "eslint": "^9",
47
52
  "eslint-config-prettier": "^10.1.2",
48
53
  "eslint-plugin-prettier": "^5.4.0",
49
54
  "jest": "^30.0.5",
50
55
  "jimp": "^1.6.1",
51
56
  "jiti": "^2.4.2",
57
+ "json": "^11.0.0",
52
58
  "link-preview-js": "^3.0.0",
59
+ "lru-cache": "^11.1.0",
53
60
  "open": "^8.4.2",
54
61
  "pino-pretty": "^13.1.1",
55
62
  "prettier": "^3.5.3",
@@ -68,6 +75,35 @@
68
75
  "link-preview-js": "^3.0.0",
69
76
  "sharp": "*"
70
77
  },
78
+ "resolutions": {
79
+ "ajv@npm:^6.12.4": "^6.14.0",
80
+ "basic-ftp": "^5.2.4",
81
+ "brace-expansion@npm:^1.1.7": "^1.1.13",
82
+ "brace-expansion@npm:^2.0.1": "^2.0.3",
83
+ "flatted": "^3.4.2",
84
+ "glob@npm:^10.2.2": "^10.5.0",
85
+ "glob@npm:^10.3.10": "^10.5.0",
86
+ "glob@npm:^10.5.0": "^10.5.0",
87
+ "handlebars": "^4.7.9",
88
+ "ip-address": "^10.1.1",
89
+ "js-yaml@npm:^3.13.1": "^3.14.2",
90
+ "js-yaml@npm:^4.1.0": "^4.1.1",
91
+ "markdown-it": "^14.1.1",
92
+ "minimatch@npm:^3.0.4": "^3.1.5",
93
+ "minimatch@npm:^3.1.2": "^3.1.5",
94
+ "minimatch@npm:^5.0.1": "^5.1.8",
95
+ "minimatch@npm:^9.0.4": "^9.0.9",
96
+ "minimatch@npm:^9.0.5": "^9.0.9",
97
+ "picomatch@npm:^2.0.4": "^2.3.2",
98
+ "picomatch@npm:^2.3.1": "^2.3.2",
99
+ "picomatch@npm:^4.0.0": "^4.0.4",
100
+ "picomatch@npm:^4.0.2": "^4.0.4",
101
+ "socks": "^2.8.8",
102
+ "tar": "^7.5.11",
103
+ "tmp": "^0.2.5",
104
+ "underscore": "^1.13.8",
105
+ "yaml@npm:^2.6.1": "^2.8.4"
106
+ },
71
107
  "peerDependenciesMeta": {
72
108
  "audio-decode": {
73
109
  "optional": true
@@ -79,6 +115,7 @@
79
115
  "optional": true
80
116
  }
81
117
  },
118
+ "packageManager": "yarn@4.9.2",
82
119
  "engines": {
83
120
  "node": ">=20.0.0"
84
121
  }