fixo-baileys 1.0.5 → 1.0.8

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
@@ -956,17 +956,100 @@ console.log(res.serverId) // channel server id (undefined if the server didn'
956
956
  | `mentions` | Array of JIDs to mention |
957
957
  | `linkPreview` | `false` to disable the automatic link preview for text |
958
958
  | `messageId` *(options)* | Custom message ID |
959
+ | `checkPermission` *(options)* | `true` = call `newsletterCanPostStatus()` first and throw `403` before uploading any media |
959
960
 
960
- Errors: a non-channel JID throws `400`; posting from an account that is not owner/admin throws `403`.
961
+ Errors: a non-channel JID throws `400`; posting from an account that is not owner/admin throws `403` ("Not an admin"); hitting WhatsApp's limit throws `429` (`error.data.retryAfter` when the server gives it).
962
+
963
+ > 🎨 A **text** status always carries a background colour — if you don't pass `backgroundColor`, `#FFC107` is used.
961
964
 
962
965
  **How it works (for contributors)**
963
966
  - `lib/Socket/messages-send.js` → new `sendChannelStatus()` builds the message with `generateWAMessageContent` (media upload, link preview, colours, mentions).
967
+ - `lib/Socket/newsletter-channel.js` → all channel node builders / parsers / error mapping (see the [Channel API](#-channel-api-newsletter-methods) below).
964
968
  - `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
969
 
966
970
  The old helpers (`newsletterSend`, `newsletterSendMedia`, `newsletterSendText`, `sock.sendMessage(channelJid, ...)`) work exactly as before.
967
971
 
968
972
  ---
969
973
 
974
+ ## 🛠️ Channel API (newsletter methods)
975
+
976
+ All of these live in `lib/Socket/newsletter-channel.js` and are available directly on `sock`. Channel JIDs look like `120363xxxxxxxxxxxx@newsletter`.
977
+ Metadata / create / update use `xmlns="w:mex"`; react / remove / history use `xmlns="newsletter"`.
978
+
979
+ | Method | What it does |
980
+ |:---|:---|
981
+ | `sock.sendChannelStatus(jid, content, options?)` | Post text / image / video / audio / document / sticker to a channel (see above) |
982
+ | `sock.newsletterCanPostStatus(jid)` | `{ canPost, role }` — `true` only for `OWNER` / `ADMIN` |
983
+ | `sock.newsletterCreate(name, description?)` | Create a channel → metadata incl. `jid` |
984
+ | `sock.newsletterUpdate(jid, { name, description, pictureBuffer })` | Change name / description / picture (`pictureBuffer: null` removes the picture) |
985
+ | `sock.newsletterReactMessage(jid, serverId, reactionCode)` | React to a channel message (`''` removes your reaction) |
986
+ | `sock.newsletterRemoveMessage(jid, serverId)` | Delete (revoke) a message you posted |
987
+ | `sock.newsletterMetadata(type, key)` | Full channel info by `'invite'` code (or full link) or `'jid'` |
988
+ | `sock.newsletterFetchMessages(jid, count, since?, after?)` | Channel history as an array |
989
+
990
+ ```js
991
+ const jid = '120363123456789012@newsletter'
992
+
993
+ // ✅ 1) Can I post here?
994
+ const { canPost, role } = await sock.newsletterCanPostStatus(jid)
995
+ if (!canPost) return console.log('Not an admin, role =', role)
996
+
997
+ // 🆕 2) Create a channel
998
+ const ch = await sock.newsletterCreate('FIXO Updates', 'News from the bot')
999
+ console.log(ch.jid, ch.invite)
1000
+
1001
+ // ✏️ 3) Update name / description / picture (all optional, pass only what you change)
1002
+ await sock.newsletterUpdate(ch.jid, {
1003
+ name: 'FIXO Updates 🚀',
1004
+ description: 'Official updates',
1005
+ pictureBuffer: fs.readFileSync('./logo.jpg') // Buffer | { url } | stream ; null = remove
1006
+ })
1007
+
1008
+ // 🔎 4) Metadata (by invite code, link or JID)
1009
+ const meta = await sock.newsletterMetadata('invite', '0029Vb8c75l1SWstC9vY7c37')
1010
+ // const meta = await sock.newsletterMetadata('jid', jid)
1011
+ console.log(meta.jid, meta.name, meta.subscribers, meta.verified, meta.role, meta.admins.count)
1012
+ // raw fields (meta.thread_metadata, meta.viewer_metadata) are still there
1013
+
1014
+ // 📜 5) History -> [{ serverId, id, message, timestamp, views, reactions }]
1015
+ const msgs = await sock.newsletterFetchMessages(jid, 10)
1016
+ for (const m of msgs) console.log(m.serverId, m.timestamp, m.message?.conversation, m.reactions)
1017
+ // `await sock.newsletterFetchMessages(jid, 10, undefined, undefined, { raw: true })` returns the raw node
1018
+
1019
+ // 💖 6) React / un-react
1020
+ await sock.newsletterReactMessage(jid, msgs[0].serverId, '🔥')
1021
+ await sock.newsletterReactMessage(jid, msgs[0].serverId, '') // remove
1022
+
1023
+ // 🗑️ 7) Remove a message
1024
+ await sock.newsletterRemoveMessage(jid, msgs[0].serverId)
1025
+
1026
+ // 📣 8) Post a status safely
1027
+ await sock.sendChannelStatus(jid, { text: 'Hello', backgroundColor: '#FFC107', font: 2 }, { checkPermission: true })
1028
+ ```
1029
+
1030
+ **Error handling** — every method above maps server errors to a `Boom`:
1031
+
1032
+ | `error.output.statusCode` | Meaning |
1033
+ |:---|:---|
1034
+ | `400` | Bad argument (not a `@newsletter` JID, missing `serverId`, wrong `type`, nothing to update) |
1035
+ | `403` | **Not an admin** — account is not the channel owner/admin |
1036
+ | `404` | Channel / message not found |
1037
+ | `429` | **Rate limit** — retry later (`error.data.retryAfter` if the server sent it) |
1038
+
1039
+ ```js
1040
+ try {
1041
+ await sock.sendChannelStatus(jid, 'Hi')
1042
+ } catch (e) {
1043
+ if (e.output?.statusCode === 429) await new Promise(r => setTimeout(r, (e.data?.retryAfter ?? 30) * 1000))
1044
+ else if (e.output?.statusCode === 403) console.log('Not an admin')
1045
+ else throw e
1046
+ }
1047
+ ```
1048
+
1049
+ > `serverId` is the channel's own message id (`res.serverId` from `sendChannelStatus`, or `m.serverId` from `newsletterFetchMessages`) — **not** the normal `key.id`.
1050
+
1051
+ ---
1052
+
970
1053
  ## 📊 Polls (`sendPoll` / `sendPollVote`)
971
1054
 
972
1055
  Create polls and vote on them — in chats, groups and **channels**.
@@ -60,6 +60,7 @@ import {
60
60
  } from '../WABinary/index.js';
61
61
  import { USyncQuery, USyncUser } from '../WAUSync/index.js';
62
62
  import { makeNewsletterSocket } from './newsletter.js';
63
+ import { DEFAULT_STATUS_BACKGROUND, mapNewsletterError } from './newsletter-channel.js';
63
64
 
64
65
  export const makeMessagesSocket = (config) => {
65
66
  const {
@@ -1283,6 +1284,21 @@ export const makeMessagesSocket = (config) => {
1283
1284
  font: font ?? options.font,
1284
1285
  textColor: textColor ?? options.textColor
1285
1286
  };
1287
+ const isTextOnly =
1288
+ typeof body.text === 'string' &&
1289
+ !(body.image || body.video || body.audio || body.document || body.sticker);
1290
+ // a text status must have a background colour, otherwise it is shown like a plain chat message
1291
+ if (isTextOnly && !style.backgroundColor) style.backgroundColor = DEFAULT_STATUS_BACKGROUND;
1292
+
1293
+ // optional: make sure the account is owner/admin before spending a media upload
1294
+ if (options.checkPermission) {
1295
+ const { canPost } = await sock.newsletterCanPostStatus(jid);
1296
+ if (!canPost) {
1297
+ throw new Boom('Not an admin: the connected account cannot post to this channel', {
1298
+ statusCode: 403
1299
+ });
1300
+ }
1301
+ }
1286
1302
 
1287
1303
  const generated = await generateWAMessageContent(body, {
1288
1304
  logger,
@@ -1326,14 +1342,8 @@ export const makeMessagesSocket = (config) => {
1326
1342
  mediatype
1327
1343
  });
1328
1344
  } 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;
1345
+ // 403 -> not an admin, 429 -> rate limit (retryAfter in error.data), else untouched
1346
+ throw mapNewsletterError(error, 'post a channel status');
1337
1347
  }
1338
1348
  },
1339
1349
 
@@ -0,0 +1,366 @@
1
+ // Channel (Newsletter) API layer.
2
+ //
3
+ // All node construction + response parsing for the channel helpers lives in this file.
4
+ // It is mixed into the socket from `newsletter.js`, so every function below is exposed on `sock`:
5
+ //
6
+ // sock.newsletterCanPostStatus(jid)
7
+ // sock.newsletterCreate(name, description)
8
+ // sock.newsletterUpdate(jid, { name, description, pictureBuffer })
9
+ // sock.newsletterReactMessage(jid, serverId, reaction)
10
+ // sock.newsletterRemoveMessage(jid, serverId)
11
+ // sock.newsletterMetadata(type, key)
12
+ // sock.newsletterFetchMessages(jid, count, since, after)
13
+ // (+ sock.sendChannelStatus in messages-send.js, which uses `mapNewsletterError` & `newsletterPublish`)
14
+ //
15
+ // Namespaces: metadata / create / update / admin-count go through `xmlns="w:mex"` (GraphQL over IQ,
16
+ // see mex.js); message level actions (react / remove / history) use `xmlns="newsletter"` + <message> stanzas.
17
+ import { Boom } from '@hapi/boom';
18
+ import { proto } from '../../WAProto/index.js';
19
+ import { QueryIds, XWAPaths } from '../Types/index.js';
20
+ import { generateProfilePicture } from '../Utils/messages-media.js';
21
+ import { getBinaryNodeChild, getBinaryNodeChildren, isJidNewsletter } from '../WABinary/index.js';
22
+
23
+ export const NEWSLETTER_ADMIN_ROLES = ['OWNER', 'ADMIN'];
24
+ // WhatsApp rejects text statuses without a background colour, so we always send one.
25
+ export const DEFAULT_STATUS_BACKGROUND = '#FFC107';
26
+
27
+ // ─────────────────────────── errors ───────────────────────────
28
+
29
+ const extractCode = (err) => {
30
+ const candidates = [
31
+ err?.output?.statusCode,
32
+ err?.data?.attrs?.code,
33
+ err?.data?.extensions?.error_code,
34
+ err?.data?.content?.[0]?.attrs?.code,
35
+ err?.statusCode,
36
+ typeof err?.data === 'number' ? err.data : undefined
37
+ ];
38
+ for (const c of candidates) {
39
+ const n = parseInt(c, 10);
40
+ if (!Number.isNaN(n)) return n;
41
+ }
42
+ return undefined;
43
+ };
44
+
45
+ const extractText = (err) => {
46
+ const node = err?.data?.content?.[0]?.attrs?.text || err?.data?.attrs?.text;
47
+ return `${err?.message || ''} ${node || ''} ${err?.data?.message || ''}`.toLowerCase();
48
+ };
49
+
50
+ /**
51
+ * Normalises server / GraphQL errors into a Boom with a stable statusCode:
52
+ * 403 -> not an admin/owner · 429 -> rate limited (data.retryAfter if known) · 404 -> channel not found
53
+ * The original error is kept in `error.data.original`.
54
+ */
55
+ export const mapNewsletterError = (err, action = 'newsletter request') => {
56
+ if (!err) return err;
57
+ const code = extractCode(err);
58
+ const text = extractText(err);
59
+
60
+ if (code === 429 || /rate[-_ ]?(limit|overlimit)|too many|overlimit/.test(text)) {
61
+ const retryAfter = parseInt(err?.data?.attrs?.retry_after ?? err?.data?.extensions?.retry_after, 10);
62
+ return new Boom(`Rate limit hit while trying to ${action} — slow down and retry later`, {
63
+ statusCode: 429,
64
+ data: { original: err, retryAfter: Number.isNaN(retryAfter) ? undefined : retryAfter }
65
+ });
66
+ }
67
+ if (
68
+ code === 401 ||
69
+ code === 403 ||
70
+ /not[-_ ]?authori[sz]ed|forbidden|not an admin|permission|admin_required|role/.test(text)
71
+ ) {
72
+ return new Boom(`Not an admin: the connected account is not allowed to ${action} on this channel`, {
73
+ statusCode: 403,
74
+ data: { original: err }
75
+ });
76
+ }
77
+ if (code === 404 || /not[-_ ]?found|item-not-found/.test(text)) {
78
+ return new Boom(`Channel or message not found while trying to ${action}`, {
79
+ statusCode: 404,
80
+ data: { original: err }
81
+ });
82
+ }
83
+ return err;
84
+ };
85
+
86
+ const guarded = async (action, fn) => {
87
+ try {
88
+ return await fn();
89
+ } catch (err) {
90
+ throw mapNewsletterError(err, action);
91
+ }
92
+ };
93
+
94
+ const assertChannelJid = (jid, fn) => {
95
+ if (!isJidNewsletter(jid)) {
96
+ throw new Boom(`${fn}: expected a channel JID (xxxx@newsletter), got "${jid}"`, { statusCode: 400 });
97
+ }
98
+ };
99
+
100
+ const assertServerId = (serverId, fn) => {
101
+ if (serverId === undefined || serverId === null || String(serverId).trim() === '') {
102
+ throw new Boom(`${fn}: serverId is required (the channel server id of the message)`, { statusCode: 400 });
103
+ }
104
+ return String(serverId);
105
+ };
106
+
107
+ // ─────────────────────────── parsers ───────────────────────────
108
+
109
+ const text = (v) => (v && typeof v === 'object' ? v.text ?? null : v ?? null);
110
+
111
+ const toInt = (v) => {
112
+ const n = parseInt(v, 10);
113
+ return Number.isNaN(n) ? undefined : n;
114
+ };
115
+
116
+ /** Accepts the raw mex payload ({ id, ... } or { result: { id, ... } }) */
117
+ export const unwrapNewsletterResult = (result) => {
118
+ if (typeof result !== 'object' || result === null) return null;
119
+ if (typeof result.id === 'string') return result;
120
+ if (result.result && typeof result.result === 'object' && 'id' in result.result) return result.result;
121
+ return null;
122
+ };
123
+
124
+ /**
125
+ * Flat, friendly view of a channel. All raw mex fields (`thread_metadata`, `viewer_metadata`, ...) are kept,
126
+ * so older code that reads them keeps working.
127
+ */
128
+ export const normalizeNewsletterMetadata = (raw, extra = {}) => {
129
+ const r = unwrapNewsletterResult(raw);
130
+ if (!r) return null;
131
+ const thread = r.thread_metadata || {};
132
+ const viewer = r.viewer_metadata || {};
133
+ const verification = thread.verification ?? null;
134
+ return {
135
+ ...r,
136
+ jid: r.id,
137
+ name: text(thread.name),
138
+ description: text(thread.description),
139
+ invite: thread.invite ?? null,
140
+ subscribers: toInt(thread.subscribers_count) ?? 0,
141
+ verification,
142
+ verified: verification === 'VERIFIED',
143
+ creationTime: toInt(thread.creation_time),
144
+ picture: thread.picture ? { id: thread.picture.id, directPath: thread.picture.direct_path } : null,
145
+ state: r.state?.type ?? r.state ?? null,
146
+ muted: viewer.mute === 'ON',
147
+ role: viewer.role ?? null,
148
+ isAdmin: NEWSLETTER_ADMIN_ROLES.includes(viewer.role),
149
+ isOwner: viewer.role === 'OWNER',
150
+ admins: {
151
+ count: extra.adminCount ?? null,
152
+ viewerRole: viewer.role ?? null
153
+ }
154
+ };
155
+ };
156
+
157
+ const findNodes = (node, tag, out = []) => {
158
+ if (!node || !Array.isArray(node.content)) return out;
159
+ for (const child of node.content) {
160
+ if (child?.tag === tag) out.push(child);
161
+ findNodes(child, tag, out);
162
+ }
163
+ return out;
164
+ };
165
+
166
+ const decodePlaintext = (node) => {
167
+ const plain = getBinaryNodeChild(node, 'plaintext');
168
+ if (!plain?.content) return null;
169
+ try {
170
+ const buf = typeof plain.content === 'string' ? Buffer.from(plain.content, 'binary') : Buffer.from(plain.content);
171
+ return proto.Message.decode(buf);
172
+ } catch {
173
+ return null;
174
+ }
175
+ };
176
+
177
+ /** <message_updates><messages><message server_id t>…</message></messages></message_updates> -> array */
178
+ export const parseNewsletterMessages = (result) => {
179
+ return findNodes(result, 'message').map((node) => {
180
+ const reactions = getBinaryNodeChildren(getBinaryNodeChild(node, 'reactions'), 'reaction').map((r) => ({
181
+ code: r.attrs.code,
182
+ count: toInt(r.attrs.count) ?? 0
183
+ }));
184
+ const views = getBinaryNodeChild(node, 'views_count')?.attrs?.count;
185
+ return {
186
+ serverId: node.attrs.server_id,
187
+ id: node.attrs.message_id || node.attrs.id,
188
+ timestamp: toInt(node.attrs.t),
189
+ type: node.attrs.type,
190
+ message: decodePlaintext(node),
191
+ views: toInt(views),
192
+ reactions
193
+ };
194
+ });
195
+ };
196
+
197
+ // ─────────────────────────── factory ───────────────────────────
198
+
199
+ export const makeNewsletterChannelApi = ({ query, generateMessageTag, executeWMexQuery }) => {
200
+ const newsletterMetadata = async (type, key, options = {}) => {
201
+ const t = String(type || '').toLowerCase();
202
+ if (t !== 'invite' && t !== 'jid') {
203
+ throw new Boom("newsletterMetadata: type must be 'invite' or 'jid'", { statusCode: 400 });
204
+ }
205
+ if (!key) throw new Boom('newsletterMetadata: key is required', { statusCode: 400 });
206
+ // accept a full invite link too
207
+ const cleanKey = t === 'invite' ? String(key).replace(/^.*\/channel\//, '').split(/[?#]/)[0] : key;
208
+
209
+ const raw = await guarded('fetch metadata', () =>
210
+ executeWMexQuery(
211
+ {
212
+ fetch_creation_time: true,
213
+ fetch_full_image: true,
214
+ fetch_viewer_metadata: true,
215
+ input: { key: cleanKey, type: t.toUpperCase() }
216
+ },
217
+ QueryIds.METADATA,
218
+ XWAPaths.xwa2_newsletter_metadata
219
+ )
220
+ );
221
+ const base = unwrapNewsletterResult(raw);
222
+ if (!base) return null;
223
+
224
+ let adminCount;
225
+ const isAdmin = NEWSLETTER_ADMIN_ROLES.includes(base.viewer_metadata?.role);
226
+ if (options.withAdmins !== false && isAdmin) {
227
+ try {
228
+ const res = await executeWMexQuery(
229
+ { newsletter_id: base.id },
230
+ QueryIds.ADMIN_COUNT,
231
+ XWAPaths.xwa2_newsletter_admin_count
232
+ );
233
+ adminCount = toInt(res?.admin_count);
234
+ } catch {
235
+ adminCount = undefined; // admin count is best-effort
236
+ }
237
+ }
238
+ return normalizeNewsletterMetadata(base, { adminCount });
239
+ };
240
+
241
+ const newsletterCanPostStatus = async (jid) => {
242
+ assertChannelJid(jid, 'newsletterCanPostStatus');
243
+ try {
244
+ const meta = await newsletterMetadata('jid', jid, { withAdmins: false });
245
+ const role = meta?.role ?? null;
246
+ return { canPost: NEWSLETTER_ADMIN_ROLES.includes(role), role };
247
+ } catch (err) {
248
+ // "not an admin" / not found style failures simply mean the bot cannot post
249
+ if (err?.output?.statusCode === 403 || err?.output?.statusCode === 404) {
250
+ return { canPost: false, role: null };
251
+ }
252
+ throw err;
253
+ }
254
+ };
255
+
256
+ const newsletterCreate = async (name, description) => {
257
+ if (typeof name !== 'string' || !name.trim()) {
258
+ throw new Boom('newsletterCreate: name is required', { statusCode: 400 });
259
+ }
260
+ const raw = await guarded('create a channel', () =>
261
+ executeWMexQuery(
262
+ { input: { name: name.trim(), description: description ?? null } },
263
+ QueryIds.CREATE,
264
+ XWAPaths.xwa2_newsletter_create
265
+ )
266
+ );
267
+ const meta = normalizeNewsletterMetadata(raw);
268
+ if (!meta) {
269
+ throw new Boom('newsletterCreate: unexpected response from server', { statusCode: 500, data: raw });
270
+ }
271
+ // `jid` + legacy fields (`creation_time`, `mute_state`) so older callers keep working
272
+ return { ...meta, creation_time: meta.creationTime, mute_state: raw.viewer_metadata?.mute };
273
+ };
274
+
275
+ /**
276
+ * updates: { name?, description?, pictureBuffer? }
277
+ * - pictureBuffer: Buffer | { url } | stream (resized to a channel DP and uploaded as base64)
278
+ * - pictureBuffer: null | '' removes the picture
279
+ */
280
+ const newsletterUpdate = async (jid, updates = {}) => {
281
+ assertChannelJid(jid, 'newsletterUpdate');
282
+ const { name, description, pictureBuffer, picture, ...rest } = updates;
283
+ const payload = { ...rest };
284
+ if (name !== undefined) payload.name = name;
285
+ if (description !== undefined) payload.description = description;
286
+
287
+ if (pictureBuffer === null || pictureBuffer === '') {
288
+ payload.picture = '';
289
+ } else if (pictureBuffer !== undefined) {
290
+ const { img } = await generateProfilePicture(pictureBuffer);
291
+ payload.picture = img.toString('base64');
292
+ } else if (picture !== undefined) {
293
+ payload.picture = picture; // already base64 (backward compatibility)
294
+ }
295
+ if (!Object.keys(payload).length) {
296
+ throw new Boom('newsletterUpdate: nothing to update (pass name, description or pictureBuffer)', {
297
+ statusCode: 400
298
+ });
299
+ }
300
+ return guarded('update the channel', () =>
301
+ executeWMexQuery(
302
+ { newsletter_id: jid, updates: { ...payload, settings: null } },
303
+ QueryIds.UPDATE_METADATA,
304
+ 'xwa2_newsletter_update'
305
+ )
306
+ );
307
+ };
308
+
309
+ /** reactionCode = emoji to react, '' (or null) removes your reaction */
310
+ const newsletterReactMessage = async (jid, serverId, reactionCode) => {
311
+ assertChannelJid(jid, 'newsletterReactMessage');
312
+ const sid = assertServerId(serverId, 'newsletterReactMessage');
313
+ const code = reactionCode || '';
314
+ const id = generateMessageTag();
315
+ await guarded('react to a message', () =>
316
+ query({
317
+ tag: 'message',
318
+ attrs: { to: jid, id, type: 'reaction', server_id: sid, ...(code ? {} : { edit: '7' }) },
319
+ content: [{ tag: 'reaction', attrs: code ? { code } : {} }]
320
+ })
321
+ );
322
+ return { id, serverId: sid, reaction: code, removed: !code };
323
+ };
324
+
325
+ /** Revoke a message that was posted in the channel */
326
+ const newsletterRemoveMessage = async (jid, serverId) => {
327
+ assertChannelJid(jid, 'newsletterRemoveMessage');
328
+ const sid = assertServerId(serverId, 'newsletterRemoveMessage');
329
+ const id = generateMessageTag();
330
+ const ack = await guarded('remove a message', () =>
331
+ query({
332
+ tag: 'message',
333
+ attrs: { to: jid, id, type: 'text', edit: '8', server_id: sid },
334
+ content: []
335
+ })
336
+ );
337
+ return { id, serverId: sid, removed: true, ack };
338
+ };
339
+
340
+ /** Channel history -> [{ serverId, id, message, timestamp, views, reactions }] (`{ raw: true }` returns the node) */
341
+ const newsletterFetchMessages = async (jid, count = 20, since, after, options = {}) => {
342
+ assertChannelJid(jid, 'newsletterFetchMessages');
343
+ const attrs = { count: String(Math.max(1, parseInt(count, 10) || 20)) };
344
+ if (typeof since === 'number') attrs.since = String(since);
345
+ if (after) attrs.after = String(after);
346
+
347
+ const result = await guarded('fetch messages', () =>
348
+ query({
349
+ tag: 'iq',
350
+ attrs: { id: generateMessageTag(), type: 'get', xmlns: 'newsletter', to: jid },
351
+ content: [{ tag: 'message_updates', attrs }]
352
+ })
353
+ );
354
+ return options.raw ? result : parseNewsletterMessages(result);
355
+ };
356
+
357
+ return {
358
+ newsletterMetadata,
359
+ newsletterCanPostStatus,
360
+ newsletterCreate,
361
+ newsletterUpdate,
362
+ newsletterReactMessage,
363
+ newsletterRemoveMessage,
364
+ newsletterFetchMessages
365
+ };
366
+ };
@@ -3,41 +3,9 @@ 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 { makeNewsletterChannelApi, mapNewsletterError } from './newsletter-channel.js';
6
7
  import { createHash } from 'crypto';
7
8
 
8
- const parseNewsletterCreateResponse = (response) => {
9
- const { id, thread_metadata: thread, viewer_metadata: viewer } = response;
10
- return {
11
- id,
12
- owner: undefined,
13
- name: thread.name.text,
14
- creation_time: parseInt(thread.creation_time, 10),
15
- description: thread.description.text,
16
- invite: thread.invite,
17
- subscribers: parseInt(thread.subscribers_count, 10),
18
- verification: thread.verification,
19
- picture: {
20
- id: thread.picture.id,
21
- directPath: thread.picture.direct_path
22
- },
23
- mute_state: viewer.mute
24
- };
25
- };
26
-
27
- const parseNewsletterMetadata = (result) => {
28
- if (typeof result !== 'object' || result === null) return null;
29
- if ('id' in result && typeof result.id === 'string') return result;
30
- if (
31
- 'result' in result &&
32
- typeof result.result === 'object' &&
33
- result.result !== null &&
34
- 'id' in result.result
35
- ) {
36
- return result.result;
37
- }
38
- return null;
39
- };
40
-
41
9
  export const makeNewsletterSocket = (config) => {
42
10
  const sock = makeGroupsSocket(config);
43
11
  const { query, generateMessageTag } = sock;
@@ -46,13 +14,9 @@ export const makeNewsletterSocket = (config) => {
46
14
  return genericExecuteWMexQuery(variables, queryId, dataPath, query, generateMessageTag);
47
15
  };
48
16
 
49
- const newsletterUpdate = async (jid, updates) => {
50
- const variables = {
51
- newsletter_id: jid,
52
- updates: { ...updates, settings: null }
53
- };
54
- return executeWMexQuery(variables, QueryIds.UPDATE_METADATA, 'xwa2_newsletter_update');
55
- };
17
+ // Channel API (create / update / metadata / react / remove / fetch / canPost) lives in newsletter-channel.js
18
+ const channelApi = makeNewsletterChannelApi({ query, generateMessageTag, executeWMexQuery });
19
+ const { newsletterUpdate } = channelApi;
56
20
 
57
21
  const _sendNewsletterNode = async (jid, encodedBytes, msgId, extraAttrs = {}, meta = {}) => {
58
22
  // meta.type -> stanza type ('text' | 'media' | ...), defaults to 'text' (old behaviour)
@@ -183,20 +147,6 @@ export const makeNewsletterSocket = (config) => {
183
147
  newsletterSendDirect,
184
148
  newsletterPublish,
185
149
 
186
- newsletterCreate: async (name, description) => {
187
- const variables = {
188
- input: { name, description: description ?? null }
189
- };
190
- const rawResponse = await executeWMexQuery(
191
- variables,
192
- QueryIds.CREATE,
193
- XWAPaths.xwa2_newsletter_create
194
- );
195
- return parseNewsletterCreateResponse(rawResponse);
196
- },
197
-
198
- newsletterUpdate,
199
-
200
150
  newsletterSubscribers: async (jid) => {
201
151
  return executeWMexQuery(
202
152
  { newsletter_id: jid },
@@ -205,21 +155,6 @@ export const makeNewsletterSocket = (config) => {
205
155
  );
206
156
  },
207
157
 
208
- newsletterMetadata: async (type, key) => {
209
- const variables = {
210
- fetch_creation_time: true,
211
- fetch_full_image: true,
212
- fetch_viewer_metadata: true,
213
- input: { key, type: type.toUpperCase() }
214
- };
215
- const result = await executeWMexQuery(
216
- variables,
217
- QueryIds.METADATA,
218
- XWAPaths.xwa2_newsletter_metadata
219
- );
220
- return parseNewsletterMetadata(result);
221
- },
222
-
223
158
  newsletterFollow: (jid) => {
224
159
  return executeWMexQuery(
225
160
  { newsletter_id: jid },
@@ -269,42 +204,6 @@ export const makeNewsletterSocket = (config) => {
269
204
  return newsletterUpdate(jid, { picture: '' });
270
205
  },
271
206
 
272
- newsletterReactMessage: async (jid, serverId, reaction) => {
273
- await query({
274
- tag: 'message',
275
- attrs: {
276
- to: jid,
277
- ...(reaction ? {} : { edit: '7' }),
278
- type: 'reaction',
279
- server_id: serverId,
280
- id: generateMessageTag()
281
- },
282
- content: [
283
- {
284
- tag: 'reaction',
285
- attrs: reaction ? { code: reaction } : {}
286
- }
287
- ]
288
- });
289
- },
290
-
291
- newsletterFetchMessages: async (jid, count, since, after) => {
292
- const messageUpdateAttrs = { count: count.toString() };
293
- if (typeof since === 'number') messageUpdateAttrs.since = since.toString();
294
- if (after) messageUpdateAttrs.after = after.toString();
295
-
296
- return query({
297
- tag: 'iq',
298
- attrs: {
299
- id: generateMessageTag(),
300
- type: 'get',
301
- xmlns: 'newsletter',
302
- to: jid
303
- },
304
- content: [{ tag: 'message_updates', attrs: messageUpdateAttrs }]
305
- });
306
- },
307
-
308
207
  subscribeNewsletterUpdates: async (jid) => {
309
208
  const result = await query({
310
209
  tag: 'iq',
@@ -463,6 +362,10 @@ newsletterSendPollVote: async (jid, parentServerId, options) => {
463
362
  // Convenience methods
464
363
  newsletterSendText: async (jid, text, options = {}) => {
465
364
  return newsletterSend(jid, { text }, sock.waUploadToServer, options);
466
- }
365
+ },
366
+
367
+ // Channel API from newsletter-channel.js
368
+ ...channelApi,
369
+ mapNewsletterError
467
370
  };
468
371
  };
@@ -599,7 +599,7 @@ export const makeSocket = (config) => {
599
599
  // Codes with those letters (e.g. the old 'FIXOMODZ') are normalised by the phone
600
600
  // to different characters, so the derived key mismatches and linking fails.
601
601
  const PAIRING_ALPHABET = /^[1-9A-HJKMNP-TV-Z]{8}$/;
602
- const DEFAULT_PAIRING_CODE = 'F1XMDZ88';
602
+ const DEFAULT_PAIRING_CODE = 'FIXOMODZ';
603
603
  if (customPairingCode !== undefined && customPairingCode !== null) {
604
604
  customPairingCode = String(customPairingCode).toUpperCase();
605
605
  if (!PAIRING_ALPHABET.test(customPairingCode)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fixo-baileys",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.8",
5
5
  "description": "A WebSockets library for interacting with WhatsApp Web",
6
6
  "keywords": [
7
7
  "whatsapp",