fixo-baileys 1.0.7 → 1.0.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 +111 -1
- package/WAProto/WAProto.proto +1 -0
- package/WAProto/index.d.ts +2 -0
- package/WAProto/index.js +23 -0
- package/lib/Socket/messages-send.js +39 -9
- package/lib/Socket/newsletter-channel.js +366 -0
- package/lib/Socket/newsletter.js +9 -106
- package/lib/Utils/messages.js +2 -1
- package/package.json +3 -2
- package/tools/capture-channel-status.js +29 -0
package/README.md
CHANGED
|
@@ -956,17 +956,127 @@ 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.
|
|
964
|
+
|
|
965
|
+
### ⚠️ `mode: 'status'` (default) vs `mode: 'post'`
|
|
966
|
+
|
|
967
|
+
| `options.mode` | What is sent | Where it shows up |
|
|
968
|
+
|:---|:---|:---|
|
|
969
|
+
| `'status'` *(default, **experimental**)* | the message wrapped in `proto.Message.newsletterAdminProfileStatusMessage` (field 126) and published to the channel JID | meant to be a real **Channel Status** (Updates tab, 24 h) |
|
|
970
|
+
| `'post'` | plain channel message (the old behaviour) | channel chat only — a normal post |
|
|
971
|
+
|
|
972
|
+
> The `'status'` wrapper is based on WhatsApp's own protobuf definition, but the exact stanza WhatsApp uses has **not been verified against a real capture yet**.
|
|
973
|
+
> If it still lands as a normal post, capture the real stanza (below) and open an issue / send the log.
|
|
974
|
+
> Add `rawLog: true` to the 3rd argument to log the final payload: `sock.sendChannelStatus(jid, 'Hi', { rawLog: true })`.
|
|
975
|
+
|
|
976
|
+
```js
|
|
977
|
+
await sock.sendChannelStatus(channelJid, { image: { url: './pic.jpg' }, caption: 'Hi' }) // status (experimental)
|
|
978
|
+
await sock.sendChannelStatus(channelJid, { image: { url: './pic.jpg' }, caption: 'Hi' }, { mode: 'post' }) // old plain post
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
### 🔍 Capturing the real Channel Status stanza
|
|
982
|
+
|
|
983
|
+
```js
|
|
984
|
+
import makeWASocket from 'fixo-baileys'
|
|
985
|
+
import { attachCapture } from 'fixo-baileys/tools/capture-channel-status.js'
|
|
986
|
+
|
|
987
|
+
const sock = makeWASocket({ /* ... */ })
|
|
988
|
+
attachCapture(sock) // writes every received stanza to ./capture.log
|
|
989
|
+
```
|
|
990
|
+
Then post a real Channel Status (one text, one photo) **from your phone** and send `capture.log`.
|
|
961
991
|
|
|
962
992
|
**How it works (for contributors)**
|
|
963
993
|
- `lib/Socket/messages-send.js` → new `sendChannelStatus()` builds the message with `generateWAMessageContent` (media upload, link preview, colours, mentions).
|
|
994
|
+
- `lib/Socket/newsletter-channel.js` → all channel node builders / parsers / error mapping (see the [Channel API](#-channel-api-newsletter-methods) below).
|
|
964
995
|
- `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
996
|
|
|
966
997
|
The old helpers (`newsletterSend`, `newsletterSendMedia`, `newsletterSendText`, `sock.sendMessage(channelJid, ...)`) work exactly as before.
|
|
967
998
|
|
|
968
999
|
---
|
|
969
1000
|
|
|
1001
|
+
## 🛠️ Channel API (newsletter methods)
|
|
1002
|
+
|
|
1003
|
+
All of these live in `lib/Socket/newsletter-channel.js` and are available directly on `sock`. Channel JIDs look like `120363xxxxxxxxxxxx@newsletter`.
|
|
1004
|
+
Metadata / create / update use `xmlns="w:mex"`; react / remove / history use `xmlns="newsletter"`.
|
|
1005
|
+
|
|
1006
|
+
| Method | What it does |
|
|
1007
|
+
|:---|:---|
|
|
1008
|
+
| `sock.sendChannelStatus(jid, content, options?)` | Post text / image / video / audio / document / sticker to a channel (see above) |
|
|
1009
|
+
| `sock.newsletterCanPostStatus(jid)` | `{ canPost, role }` — `true` only for `OWNER` / `ADMIN` |
|
|
1010
|
+
| `sock.newsletterCreate(name, description?)` | Create a channel → metadata incl. `jid` |
|
|
1011
|
+
| `sock.newsletterUpdate(jid, { name, description, pictureBuffer })` | Change name / description / picture (`pictureBuffer: null` removes the picture) |
|
|
1012
|
+
| `sock.newsletterReactMessage(jid, serverId, reactionCode)` | React to a channel message (`''` removes your reaction) |
|
|
1013
|
+
| `sock.newsletterRemoveMessage(jid, serverId)` | Delete (revoke) a message you posted |
|
|
1014
|
+
| `sock.newsletterMetadata(type, key)` | Full channel info by `'invite'` code (or full link) or `'jid'` |
|
|
1015
|
+
| `sock.newsletterFetchMessages(jid, count, since?, after?)` | Channel history as an array |
|
|
1016
|
+
|
|
1017
|
+
```js
|
|
1018
|
+
const jid = '120363123456789012@newsletter'
|
|
1019
|
+
|
|
1020
|
+
// ✅ 1) Can I post here?
|
|
1021
|
+
const { canPost, role } = await sock.newsletterCanPostStatus(jid)
|
|
1022
|
+
if (!canPost) return console.log('Not an admin, role =', role)
|
|
1023
|
+
|
|
1024
|
+
// 🆕 2) Create a channel
|
|
1025
|
+
const ch = await sock.newsletterCreate('FIXO Updates', 'News from the bot')
|
|
1026
|
+
console.log(ch.jid, ch.invite)
|
|
1027
|
+
|
|
1028
|
+
// ✏️ 3) Update name / description / picture (all optional, pass only what you change)
|
|
1029
|
+
await sock.newsletterUpdate(ch.jid, {
|
|
1030
|
+
name: 'FIXO Updates 🚀',
|
|
1031
|
+
description: 'Official updates',
|
|
1032
|
+
pictureBuffer: fs.readFileSync('./logo.jpg') // Buffer | { url } | stream ; null = remove
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
// 🔎 4) Metadata (by invite code, link or JID)
|
|
1036
|
+
const meta = await sock.newsletterMetadata('invite', '0029Vb8c75l1SWstC9vY7c37')
|
|
1037
|
+
// const meta = await sock.newsletterMetadata('jid', jid)
|
|
1038
|
+
console.log(meta.jid, meta.name, meta.subscribers, meta.verified, meta.role, meta.admins.count)
|
|
1039
|
+
// raw fields (meta.thread_metadata, meta.viewer_metadata) are still there
|
|
1040
|
+
|
|
1041
|
+
// 📜 5) History -> [{ serverId, id, message, timestamp, views, reactions }]
|
|
1042
|
+
const msgs = await sock.newsletterFetchMessages(jid, 10)
|
|
1043
|
+
for (const m of msgs) console.log(m.serverId, m.timestamp, m.message?.conversation, m.reactions)
|
|
1044
|
+
// `await sock.newsletterFetchMessages(jid, 10, undefined, undefined, { raw: true })` returns the raw node
|
|
1045
|
+
|
|
1046
|
+
// 💖 6) React / un-react
|
|
1047
|
+
await sock.newsletterReactMessage(jid, msgs[0].serverId, '🔥')
|
|
1048
|
+
await sock.newsletterReactMessage(jid, msgs[0].serverId, '') // remove
|
|
1049
|
+
|
|
1050
|
+
// 🗑️ 7) Remove a message
|
|
1051
|
+
await sock.newsletterRemoveMessage(jid, msgs[0].serverId)
|
|
1052
|
+
|
|
1053
|
+
// 📣 8) Post a status safely
|
|
1054
|
+
await sock.sendChannelStatus(jid, { text: 'Hello', backgroundColor: '#FFC107', font: 2 }, { checkPermission: true })
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
**Error handling** — every method above maps server errors to a `Boom`:
|
|
1058
|
+
|
|
1059
|
+
| `error.output.statusCode` | Meaning |
|
|
1060
|
+
|:---|:---|
|
|
1061
|
+
| `400` | Bad argument (not a `@newsletter` JID, missing `serverId`, wrong `type`, nothing to update) |
|
|
1062
|
+
| `403` | **Not an admin** — account is not the channel owner/admin |
|
|
1063
|
+
| `404` | Channel / message not found |
|
|
1064
|
+
| `429` | **Rate limit** — retry later (`error.data.retryAfter` if the server sent it) |
|
|
1065
|
+
|
|
1066
|
+
```js
|
|
1067
|
+
try {
|
|
1068
|
+
await sock.sendChannelStatus(jid, 'Hi')
|
|
1069
|
+
} catch (e) {
|
|
1070
|
+
if (e.output?.statusCode === 429) await new Promise(r => setTimeout(r, (e.data?.retryAfter ?? 30) * 1000))
|
|
1071
|
+
else if (e.output?.statusCode === 403) console.log('Not an admin')
|
|
1072
|
+
else throw e
|
|
1073
|
+
}
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
> `serverId` is the channel's own message id (`res.serverId` from `sendChannelStatus`, or `m.serverId` from `newsletterFetchMessages`) — **not** the normal `key.id`.
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
970
1080
|
## 📊 Polls (`sendPoll` / `sendPollVote`)
|
|
971
1081
|
|
|
972
1082
|
Create polls and vote on them — in chats, groups and **channels**.
|
package/WAProto/WAProto.proto
CHANGED
|
@@ -2150,6 +2150,7 @@ message Message {
|
|
|
2150
2150
|
optional FutureProofMessage questionMessage = 101;
|
|
2151
2151
|
optional MessageHistoryNotice messageHistoryNotice = 102;
|
|
2152
2152
|
optional FutureProofMessage groupStatusMessageV2 = 103;
|
|
2153
|
+
optional FutureProofMessage newsletterAdminProfileStatusMessage = 126;
|
|
2153
2154
|
optional FutureProofMessage botForwardedMessage = 104;
|
|
2154
2155
|
optional StatusQuestionAnswerMessage statusQuestionAnswerMessage = 105;
|
|
2155
2156
|
optional FutureProofMessage questionReplyMessage = 106;
|
package/WAProto/index.d.ts
CHANGED
|
@@ -5269,6 +5269,7 @@ export namespace proto {
|
|
|
5269
5269
|
questionMessage?: (proto.Message.IFutureProofMessage|null);
|
|
5270
5270
|
messageHistoryNotice?: (proto.Message.IMessageHistoryNotice|null);
|
|
5271
5271
|
groupStatusMessageV2?: (proto.Message.IFutureProofMessage|null);
|
|
5272
|
+
newsletterAdminProfileStatusMessage?: (proto.Message.IFutureProofMessage|null);
|
|
5272
5273
|
botForwardedMessage?: (proto.Message.IFutureProofMessage|null);
|
|
5273
5274
|
statusQuestionAnswerMessage?: (proto.Message.IStatusQuestionAnswerMessage|null);
|
|
5274
5275
|
questionReplyMessage?: (proto.Message.IFutureProofMessage|null);
|
|
@@ -5368,6 +5369,7 @@ export namespace proto {
|
|
|
5368
5369
|
public questionMessage?: (proto.Message.IFutureProofMessage|null);
|
|
5369
5370
|
public messageHistoryNotice?: (proto.Message.IMessageHistoryNotice|null);
|
|
5370
5371
|
public groupStatusMessageV2?: (proto.Message.IFutureProofMessage|null);
|
|
5372
|
+
public newsletterAdminProfileStatusMessage?: (proto.Message.IFutureProofMessage|null);
|
|
5371
5373
|
public botForwardedMessage?: (proto.Message.IFutureProofMessage|null);
|
|
5372
5374
|
public statusQuestionAnswerMessage?: (proto.Message.IStatusQuestionAnswerMessage|null);
|
|
5373
5375
|
public questionReplyMessage?: (proto.Message.IFutureProofMessage|null);
|
package/WAProto/index.js
CHANGED
|
@@ -35552,6 +35552,7 @@ export const proto = $root.proto = (() => {
|
|
|
35552
35552
|
Message.prototype.questionMessage = null;
|
|
35553
35553
|
Message.prototype.messageHistoryNotice = null;
|
|
35554
35554
|
Message.prototype.groupStatusMessageV2 = null;
|
|
35555
|
+
Message.prototype.newsletterAdminProfileStatusMessage = null;
|
|
35555
35556
|
Message.prototype.botForwardedMessage = null;
|
|
35556
35557
|
Message.prototype.statusQuestionAnswerMessage = null;
|
|
35557
35558
|
Message.prototype.questionReplyMessage = null;
|
|
@@ -36080,6 +36081,12 @@ export const proto = $root.proto = (() => {
|
|
|
36080
36081
|
set: $util.oneOfSetter($oneOfFields)
|
|
36081
36082
|
});
|
|
36082
36083
|
|
|
36084
|
+
// Virtual OneOf for proto3 optional field
|
|
36085
|
+
Object.defineProperty(Message.prototype, "_newsletterAdminProfileStatusMessage", {
|
|
36086
|
+
get: $util.oneOfGetter($oneOfFields = ["newsletterAdminProfileStatusMessage"]),
|
|
36087
|
+
set: $util.oneOfSetter($oneOfFields)
|
|
36088
|
+
});
|
|
36089
|
+
|
|
36083
36090
|
// Virtual OneOf for proto3 optional field
|
|
36084
36091
|
Object.defineProperty(Message.prototype, "_botForwardedMessage", {
|
|
36085
36092
|
get: $util.oneOfGetter($oneOfFields = ["botForwardedMessage"]),
|
|
@@ -36313,6 +36320,8 @@ export const proto = $root.proto = (() => {
|
|
|
36313
36320
|
$root.proto.Message.MessageHistoryNotice.encode(m.messageHistoryNotice, w.uint32(818).fork()).ldelim();
|
|
36314
36321
|
if (m.groupStatusMessageV2 != null && Object.hasOwnProperty.call(m, "groupStatusMessageV2"))
|
|
36315
36322
|
$root.proto.Message.FutureProofMessage.encode(m.groupStatusMessageV2, w.uint32(826).fork()).ldelim();
|
|
36323
|
+
if (m.newsletterAdminProfileStatusMessage != null && Object.hasOwnProperty.call(m, "newsletterAdminProfileStatusMessage"))
|
|
36324
|
+
$root.proto.Message.FutureProofMessage.encode(m.newsletterAdminProfileStatusMessage, w.uint32(1010).fork()).ldelim();
|
|
36316
36325
|
if (m.botForwardedMessage != null && Object.hasOwnProperty.call(m, "botForwardedMessage"))
|
|
36317
36326
|
$root.proto.Message.FutureProofMessage.encode(m.botForwardedMessage, w.uint32(834).fork()).ldelim();
|
|
36318
36327
|
if (m.statusQuestionAnswerMessage != null && Object.hasOwnProperty.call(m, "statusQuestionAnswerMessage"))
|
|
@@ -36687,6 +36696,10 @@ export const proto = $root.proto = (() => {
|
|
|
36687
36696
|
m.groupStatusMessageV2 = $root.proto.Message.FutureProofMessage.decode(r, r.uint32());
|
|
36688
36697
|
break;
|
|
36689
36698
|
}
|
|
36699
|
+
case 126: {
|
|
36700
|
+
m.newsletterAdminProfileStatusMessage = $root.proto.Message.FutureProofMessage.decode(r, r.uint32());
|
|
36701
|
+
break;
|
|
36702
|
+
}
|
|
36690
36703
|
case 104: {
|
|
36691
36704
|
m.botForwardedMessage = $root.proto.Message.FutureProofMessage.decode(r, r.uint32());
|
|
36692
36705
|
break;
|
|
@@ -37163,6 +37176,11 @@ export const proto = $root.proto = (() => {
|
|
|
37163
37176
|
throw TypeError(".proto.Message.groupStatusMessageV2: object expected");
|
|
37164
37177
|
m.groupStatusMessageV2 = $root.proto.Message.FutureProofMessage.fromObject(d.groupStatusMessageV2);
|
|
37165
37178
|
}
|
|
37179
|
+
if (d.newsletterAdminProfileStatusMessage != null) {
|
|
37180
|
+
if (typeof d.newsletterAdminProfileStatusMessage !== "object")
|
|
37181
|
+
throw TypeError(".proto.Message.newsletterAdminProfileStatusMessage: object expected");
|
|
37182
|
+
m.newsletterAdminProfileStatusMessage = $root.proto.Message.FutureProofMessage.fromObject(d.newsletterAdminProfileStatusMessage);
|
|
37183
|
+
}
|
|
37166
37184
|
if (d.botForwardedMessage != null) {
|
|
37167
37185
|
if (typeof d.botForwardedMessage !== "object")
|
|
37168
37186
|
throw TypeError(".proto.Message.botForwardedMessage: object expected");
|
|
@@ -37645,6 +37663,11 @@ export const proto = $root.proto = (() => {
|
|
|
37645
37663
|
if (o.oneofs)
|
|
37646
37664
|
d._groupStatusMessageV2 = "groupStatusMessageV2";
|
|
37647
37665
|
}
|
|
37666
|
+
if (m.newsletterAdminProfileStatusMessage != null && m.hasOwnProperty("newsletterAdminProfileStatusMessage")) {
|
|
37667
|
+
d.newsletterAdminProfileStatusMessage = $root.proto.Message.FutureProofMessage.toObject(m.newsletterAdminProfileStatusMessage, o);
|
|
37668
|
+
if (o.oneofs)
|
|
37669
|
+
d._newsletterAdminProfileStatusMessage = "newsletterAdminProfileStatusMessage";
|
|
37670
|
+
}
|
|
37648
37671
|
if (m.botForwardedMessage != null && m.hasOwnProperty("botForwardedMessage")) {
|
|
37649
37672
|
d.botForwardedMessage = $root.proto.Message.FutureProofMessage.toObject(m.botForwardedMessage, o);
|
|
37650
37673
|
if (o.oneofs)
|
|
@@ -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 {
|
|
@@ -1267,6 +1268,14 @@ export const makeMessagesSocket = (config) => {
|
|
|
1267
1268
|
// ── Channel Status (post to a WhatsApp Channel like a user posts a status) ──
|
|
1268
1269
|
// sock.sendChannelStatus(channelJid, 'text' | { text, backgroundColor, font, textColor }
|
|
1269
1270
|
// | { image|video|audio|document|sticker, caption, ... }, options)
|
|
1271
|
+
//
|
|
1272
|
+
// options.mode:
|
|
1273
|
+
// 'status' (default, EXPERIMENTAL) -> the message is wrapped in
|
|
1274
|
+
// proto.Message.newsletterAdminProfileStatusMessage (field 126), which is the
|
|
1275
|
+
// wrapper WhatsApp uses for a channel admin's *status* (Updates tab, 24h).
|
|
1276
|
+
// The exact wire format is not verified against a real capture yet.
|
|
1277
|
+
// 'post' -> old behaviour: plain channel post (shows up inside the channel chat only).
|
|
1278
|
+
// options.rawLog = true -> logs the final proto + stanza attrs (logger.info) for debugging.
|
|
1270
1279
|
sendChannelStatus: async (jid, content, options = {}) => {
|
|
1271
1280
|
if (!isJidNewsletter(jid)) {
|
|
1272
1281
|
throw new Boom('sendChannelStatus needs a channel JID (xxxx@newsletter)', { statusCode: 400 });
|
|
@@ -1283,6 +1292,21 @@ export const makeMessagesSocket = (config) => {
|
|
|
1283
1292
|
font: font ?? options.font,
|
|
1284
1293
|
textColor: textColor ?? options.textColor
|
|
1285
1294
|
};
|
|
1295
|
+
const isTextOnly =
|
|
1296
|
+
typeof body.text === 'string' &&
|
|
1297
|
+
!(body.image || body.video || body.audio || body.document || body.sticker);
|
|
1298
|
+
// a text status must have a background colour, otherwise it is shown like a plain chat message
|
|
1299
|
+
if (isTextOnly && !style.backgroundColor) style.backgroundColor = DEFAULT_STATUS_BACKGROUND;
|
|
1300
|
+
|
|
1301
|
+
// optional: make sure the account is owner/admin before spending a media upload
|
|
1302
|
+
if (options.checkPermission) {
|
|
1303
|
+
const { canPost } = await sock.newsletterCanPostStatus(jid);
|
|
1304
|
+
if (!canPost) {
|
|
1305
|
+
throw new Boom('Not an admin: the connected account cannot post to this channel', {
|
|
1306
|
+
statusCode: 403
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1286
1310
|
|
|
1287
1311
|
const generated = await generateWAMessageContent(body, {
|
|
1288
1312
|
logger,
|
|
@@ -1318,22 +1342,28 @@ export const makeMessagesSocket = (config) => {
|
|
|
1318
1342
|
else if (inner.documentMessage) [type, mediatype] = ['media', 'document'];
|
|
1319
1343
|
else if (inner.stickerMessage) [type, mediatype] = ['media', 'sticker'];
|
|
1320
1344
|
|
|
1345
|
+
const mode = options.mode === 'post' ? 'post' : 'status';
|
|
1346
|
+
const toSend =
|
|
1347
|
+
mode === 'status'
|
|
1348
|
+
? proto.Message.create({
|
|
1349
|
+
newsletterAdminProfileStatusMessage: proto.Message.FutureProofMessage.create({ message })
|
|
1350
|
+
})
|
|
1351
|
+
: message;
|
|
1352
|
+
|
|
1353
|
+
if (options.rawLog) {
|
|
1354
|
+
logger.info({ jid, mode, type, mediatype, proto: toSend.toJSON?.() }, 'sendChannelStatus payload');
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1321
1357
|
try {
|
|
1322
|
-
return await sock.newsletterPublish(jid,
|
|
1358
|
+
return await sock.newsletterPublish(jid, toSend, {
|
|
1323
1359
|
messageId: options.messageId,
|
|
1324
1360
|
additionalAttributes: options.additionalAttributes,
|
|
1325
1361
|
type,
|
|
1326
1362
|
mediatype
|
|
1327
1363
|
});
|
|
1328
1364
|
} catch (error) {
|
|
1329
|
-
|
|
1330
|
-
|
|
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;
|
|
1365
|
+
// 403 -> not an admin, 429 -> rate limit (retryAfter in error.data), else untouched
|
|
1366
|
+
throw mapNewsletterError(error, 'post a channel status');
|
|
1337
1367
|
}
|
|
1338
1368
|
},
|
|
1339
1369
|
|
|
@@ -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
|
+
};
|
package/lib/Socket/newsletter.js
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
};
|
package/lib/Utils/messages.js
CHANGED
|
@@ -816,7 +816,8 @@ export const normalizeMessageContent = (content) => {
|
|
|
816
816
|
message?.editedMessage ||
|
|
817
817
|
message?.associatedChildMessage ||
|
|
818
818
|
message?.groupStatusMessage ||
|
|
819
|
-
message?.groupStatusMessageV2
|
|
819
|
+
message?.groupStatusMessageV2 ||
|
|
820
|
+
message?.newsletterAdminProfileStatusMessage);
|
|
820
821
|
}
|
|
821
822
|
};
|
|
822
823
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fixo-baileys",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.9",
|
|
5
5
|
"description": "A WebSockets library for interacting with WhatsApp Web",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"lib/**/*",
|
|
18
18
|
"WAProto/**/*",
|
|
19
19
|
"engine-requirements.js",
|
|
20
|
-
"assets/**/*"
|
|
20
|
+
"assets/**/*",
|
|
21
|
+
"tools/**/*"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsc && tsc-esm-fix",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// capture-channel-status.js
|
|
2
|
+
// Logs every raw stanza the bot receives/sends into capture.log so the real
|
|
3
|
+
// "Channel Status" wire format can be inspected.
|
|
4
|
+
//
|
|
5
|
+
// Usage: in your bot, right after you create the socket:
|
|
6
|
+
//
|
|
7
|
+
// import { attachCapture } from 'fixo-baileys/tools/capture-channel-status.js'
|
|
8
|
+
// const sock = makeWASocket({ ... })
|
|
9
|
+
// attachCapture(sock)
|
|
10
|
+
//
|
|
11
|
+
// Then post a real Channel Status from the phone (one text, one photo)
|
|
12
|
+
// and send capture.log back (remove phone numbers if you like).
|
|
13
|
+
import fs from 'fs'
|
|
14
|
+
import { binaryNodeToString } from '../lib/WABinary/index.js'
|
|
15
|
+
|
|
16
|
+
export const attachCapture = (sock, file = 'capture.log') => {
|
|
17
|
+
const out = fs.createWriteStream(file, { flags: 'a' })
|
|
18
|
+
sock.ws.on('frame', (frame) => {
|
|
19
|
+
if (frame instanceof Uint8Array) return
|
|
20
|
+
// skip noisy keep-alive / presence traffic
|
|
21
|
+
if (['presence', 'chatstate', 'receipt', 'ack'].includes(frame.tag)) return
|
|
22
|
+
try {
|
|
23
|
+
out.write(`\n[${new Date().toISOString()}] RECV\n${binaryNodeToString(frame)}\n`)
|
|
24
|
+
} catch (e) {
|
|
25
|
+
out.write(`\n[${new Date().toISOString()}] RECV (unprintable) ${e.message}\n`)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
console.log(`capture started -> ${file}`)
|
|
29
|
+
}
|