From 5f1f1181d0a5534b4227b7619ac4dba5e5e76e92 Mon Sep 17 00:00:00 2001 From: vinikjkkj Date: Fri, 5 Jun 2026 00:19:01 -0300 Subject: [PATCH] fix(newsletter): parse metadata and fetch-messages responses `newsletterMetadata`: transform the raw server response into the flat `NewsletterMetadata` shape instead of casting it, and fall back to `image`/`preview` when `picture` is absent (preview-only responses for non-followers). Fixes #2204. `newsletterFetchMessages`: send the WA-Web-shaped query (``) instead of `` addressed to the channel jid, which timed out, then parse the `` response into messages carrying `reactions`, `pollVotes`, views/forwards/responses counts, edit timestamps, media `rcat`, and the decoded plaintext `proto.Message`. Fixes #2555. `Mex.ts`: remove the duplicated `NewsletterCreateResponse` interface. All changes were validated end-to-end against live WhatsApp. `newsletterMetadata` and `newsletterFetchMessages` were verified on a real public channel (~109k subscribers): metadata returns the flat shape with the channel picture (resolved from `preview`), and fetch returns real messages with decoded `proto.Message` content, `reactions`, and `forwards` counts (both previously timed out or returned `null`). --- src/Socket/newsletter.ts | 100 +++++++++++++++++++++++++++++++++------ src/Types/Mex.ts | 19 -------- 2 files changed, 85 insertions(+), 34 deletions(-) diff --git a/src/Socket/newsletter.ts b/src/Socket/newsletter.ts index 5aa3d4fad74..9ff1a1d9139 100644 --- a/src/Socket/newsletter.ts +++ b/src/Socket/newsletter.ts @@ -1,8 +1,9 @@ +import { proto } from '../../WAProto/index.js' import type { NewsletterCreateResponse, SocketConfig, WAMediaUpload } from '../Types' import type { NewsletterMetadata, NewsletterUpdate } from '../Types' import { QueryIds, XWAPaths } from '../Types' import { generateProfilePicture } from '../Utils/messages-media' -import { getBinaryNodeChild } from '../WABinary' +import { type BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary' import { makeGroupsSocket } from './groups' import { executeWMexQuery as genericExecuteWMexQuery } from './mex' @@ -30,15 +31,80 @@ const parseNewsletterMetadata = (result: unknown): NewsletterMetadata | null => return null } - if ('id' in result && typeof result.id === 'string') { - return result as NewsletterMetadata + const raw = result as Record + const node = (raw.result && typeof raw.result === 'object' ? raw.result : raw) as Record + + if (typeof node.id !== 'string') { + return null } - if ('result' in result && typeof result.result === 'object' && result.result !== null && 'id' in result.result) { - return result.result as NewsletterMetadata + const thread = node.thread_metadata ?? {} + const viewer = node.viewer_metadata ?? {} + const pic = thread.picture ?? thread.image ?? thread.preview + + return { + id: node.id, + name: thread.name?.text ?? '', + description: thread.description?.text, + invite: thread.invite, + creation_time: thread.creation_time ? parseInt(thread.creation_time, 10) : undefined, + subscribers: thread.subscribers_count ? parseInt(thread.subscribers_count, 10) : undefined, + picture: pic ? { id: pic.id, directPath: pic.direct_path } : undefined, + verification: thread.verification, + mute_state: viewer.mute } +} + +const parseFetchedNewsletterMessage = (node: BinaryNode) => { + const plaintext = getBinaryNodeChild(node, 'plaintext') + const plaintextContent = plaintext?.content + const meta = getBinaryNodeChild(node, 'meta') + const viewsCount = getBinaryNodeChild(node, 'views_count') + const forwardsCount = getBinaryNodeChild(node, 'forwards_count') + const responsesCount = getBinaryNodeChild(node, 'responses_count') + const rcat = getBinaryNodeChild(node, 'rcat') + + const reactionsNode = getBinaryNodeChild(node, 'reactions') + const reactions = reactionsNode + ? getBinaryNodeChildren(reactionsNode, 'reaction').map(r => ({ + code: r.attrs.code, + count: r.attrs.count ? parseInt(r.attrs.count, 10) : 0 + })) + : [] - return null + const votesNode = getBinaryNodeChild(node, 'votes') + const pollVotes = votesNode + ? getBinaryNodeChildren(votesNode, 'vote').map(v => ({ + count: v.attrs.count ? parseInt(v.attrs.count, 10) : 0, + hash: v.content instanceof Uint8Array ? v.content : undefined + })) + : [] + + let message: proto.IMessage | undefined + if (plaintextContent instanceof Uint8Array) { + try { + message = proto.Message.decode(plaintextContent) + } catch { + message = undefined + } + } + + return { + id: node.attrs.id, + serverId: node.attrs.server_id, + type: node.attrs.type, + timestamp: node.attrs.t ? parseInt(node.attrs.t, 10) : undefined, + isSender: node.attrs.is_sender === 'true', + views: viewsCount?.attrs?.count ? parseInt(viewsCount.attrs.count, 10) : undefined, + forwards: forwardsCount?.attrs?.count ? parseInt(forwardsCount.attrs.count, 10) : undefined, + responses: responsesCount?.attrs?.count ? parseInt(responsesCount.attrs.count, 10) : undefined, + editTimestamp: meta?.attrs?.msg_edit_t ? parseInt(meta.attrs.msg_edit_t, 10) : undefined, + originalTimestamp: meta?.attrs?.original_msg_t ? parseInt(meta.attrs.original_msg_t, 10) : undefined, + mediaRcat: rcat?.content instanceof Uint8Array ? rcat.content : undefined, + reactions, + pollVotes, + message + } } export const makeNewsletterSocket = (config: SocketConfig) => { @@ -154,15 +220,17 @@ export const makeNewsletterSocket = (config: SocketConfig) => { }, newsletterFetchMessages: async (jid: string, count: number, since: number, after: number) => { - const messageUpdateAttrs: { count: string; since?: string; after?: string } = { + const messagesAttrs: { type: string; jid: string; count: string; before?: string; after?: string } = { + type: 'jid', + jid, count: count.toString() } - if (typeof since === 'number') { - messageUpdateAttrs.since = since.toString() + if (typeof since === 'number' && since) { + messagesAttrs.before = since.toString() } if (after) { - messageUpdateAttrs.after = after.toString() + messagesAttrs.after = after.toString() } const result = await query({ @@ -170,17 +238,19 @@ export const makeNewsletterSocket = (config: SocketConfig) => { attrs: { id: generateMessageTag(), type: 'get', - xmlns: 'newsletter', - to: jid + to: S_WHATSAPP_NET, + xmlns: 'newsletter' }, content: [ { - tag: 'message_updates', - attrs: messageUpdateAttrs + tag: 'messages', + attrs: messagesAttrs } ] }) - return result + + const messagesNode = getBinaryNodeChild(result, 'messages') + return getBinaryNodeChildren(messagesNode, 'message').map(parseFetchedNewsletterMessage) }, subscribeNewsletterUpdates: async (jid: string): Promise<{ duration: string } | null> => { diff --git a/src/Types/Mex.ts b/src/Types/Mex.ts index 6e3e63f4b03..75592c113c5 100644 --- a/src/Types/Mex.ts +++ b/src/Types/Mex.ts @@ -57,25 +57,6 @@ export interface NewsletterCreateResponse { role: NewsletterViewRole } } -export interface NewsletterCreateResponse { - id: string - state: { type: string } - thread_metadata: { - creation_time: string - description: { id: string; text: string; update_time: string } - handle: string | null - invite: string - name: { id: string; text: string; update_time: string } - picture: { direct_path: string; id: string; type: string } - preview: { direct_path: string; id: string; type: string } - subscribers_count: string - verification: 'VERIFIED' | 'UNVERIFIED' - } - viewer_metadata: { - mute: 'ON' | 'OFF' - role: NewsletterViewRole - } -} export type NewsletterViewRole = 'ADMIN' | 'GUEST' | 'OWNER' | 'SUBSCRIBER' export interface NewsletterMetadata { id: string