diff --git a/.eslintrc.json b/.eslintrc.json index 258042d2df7..d5c41ceac5b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,13 +21,6 @@ "@typescript-eslint/no-unnecessary-type-assertion": [ "warn" ], - "no-restricted-syntax": [ - "warn", - { - "selector": "TSEnumDeclaration", - "message": "Don't declare enums, use literals instead" - } - ], "keyword-spacing": [ "warn" ] diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 6e64dd69fa8..922f50d2037 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -4,7 +4,7 @@ import { randomBytes } from 'crypto' import NodeCache from 'node-cache' import { proto } from '../../WAProto' import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults' -import { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, SocketConfig, WACallEvent, WAMessageKey, WAMessageStatus, WAMessageStubType, WAPatchName } from '../Types' +import { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, MexOperations, NewsletterSettingsUpdate, SocketConfig, SubscriberAction, WACallEvent, WAMessageKey, WAMessageStatus, WAMessageStubType, WAPatchName, XWAPaths } from '../Types' import { aesDecryptCTR, aesEncryptGCM, @@ -322,7 +322,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { break case 'membership_approval_mode': - const approvalMode: any = getBinaryNodeChild(child, 'group_join') + const approvalMode = getBinaryNodeChild(child, 'group_join') if(approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE msg.messageStubParameters = [ approvalMode.attrs.state ] @@ -341,6 +341,57 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } } + const handleNewsletterNotification = (id: string, node: BinaryNode) => { + const messages = getBinaryNodeChild(node, 'messages') + const message = getBinaryNodeChild(messages, 'message')! + + const serverId = message.attrs.server_id + + const reactionsList = getBinaryNodeChild(message, 'reactions') + const viewsList = getBinaryNodeChildren(message, 'views_count') + + if(reactionsList) { + const reactions = getBinaryNodeChildren(reactionsList, 'reaction') + if(reactions.length === 0) { + ev.emit('newsletter.reaction', { id, 'server_id': serverId, reaction: { removed: true } }) + } + + reactions.forEach(item => { + ev.emit('newsletter.reaction', { id, 'server_id': serverId, reaction: { code: item.attrs?.code, count: +item.attrs?.count } }) + }) + } + + if(viewsList.length) { + viewsList.forEach(item => { + ev.emit('newsletter.view', { id, 'server_id': serverId, count: +item.attrs.count }) + }) + } + } + + const handleMexNewsletterNotification = (id: string, node: BinaryNode) => { + const operation = node?.attrs.op_name + const content = JSON.parse(node?.content?.toString()!) + + let contentPath + + if(operation === MexOperations.UPDATE) { + contentPath = content.data[XWAPaths.METADATA_UPDATE] + ev.emit('newsletter-settings.update', { id, update: contentPath.thread_metadata.settings as NewsletterSettingsUpdate }) + } else { + let action: SubscriberAction + + if(operation === MexOperations.PROMOTE) { + action = 'promote' + contentPath = content.data[XWAPaths.PROMOTE] + } else { + action = 'demote' + contentPath = content.data[XWAPaths.DEMOTE] + } + + ev.emit('newsletter-participants.update', { id, author: contentPath.actor.pn, user: contentPath.user.pn, 'new_role': contentPath.user_new_role, action }) + } + } + const processNotification = async(node: BinaryNode) => { const result: Partial = { } const [child] = getAllBinaryNodeChildren(node) @@ -362,6 +413,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { logger.debug({ jid }, 'got privacy token update') } + break + case 'newsletter': + handleNewsletterNotification(node.attrs.from, child) + break + case 'mex': + handleMexNewsletterNotification(node.attrs.from, child) break case 'w:gp2': handleGroupNotification(node.attrs.participant, child, result) @@ -699,7 +756,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } const handleMessage = async(node: BinaryNode) => { - if(shouldIgnoreJid(node.attrs.from!) && node.attrs.from! !== '@s.whatsapp.net') { + if(shouldIgnoreJid(node.attrs.from) && node.attrs.from !== '@s.whatsapp.net') { logger.debug({ key: node.attrs.key }, 'ignored message') await sendMessageAck(node) return @@ -814,7 +871,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { } const handleBadAck = async({ attrs }: BinaryNode) => { - const key: WAMessageKey = { remoteJid: attrs.from, fromMe: true, id: attrs.id } + const key: WAMessageKey = { remoteJid: attrs.from, fromMe: true, id: attrs.id, 'server_id': attrs?.server_id } // current hypothesis is that if pash is sent in the ack // it means -- the message hasn't reached all devices yet // we'll retry sending the message here diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index eded651f8ed..b59ffa48dd8 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -4,10 +4,10 @@ import NodeCache from 'node-cache' import { proto } from '../../WAProto' import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults' import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig, WAMessageKey } from '../Types' -import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils' +import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils' import { getUrlInfo } from '../Utils/link-preview' import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary' -import { makeGroupsSocket } from './groups' +import { makeNewsletterSocket } from './newsletter' import ListType = proto.Message.ListMessage.ListType; export const makeMessagesSocket = (config: SocketConfig) => { @@ -18,7 +18,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { options: axiosOptions, patchMessageBeforeSending, } = config - const sock = makeGroupsSocket(config) + const sock = makeNewsletterSocket(config) const { ev, authState, @@ -315,6 +315,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { const { user, server } = jidDecode(jid)! const statusJid = 'status@broadcast' const isGroup = server === 'g.us' + const isNewsletter = server === 'newsletter' const isStatus = jid === statusJid const isLid = server === 'lid' @@ -322,7 +323,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { useUserDevicesCache = useUserDevicesCache !== false const participants: BinaryNode[] = [] - const destinationJid = (!isStatus) ? jidEncode(user, isLid ? 'lid' : isGroup ? 'g.us' : 's.whatsapp.net') : statusJid + const destinationJid = (!isStatus) ? jidEncode(user, isLid ? 'lid' : isGroup ? 'g.us' : isNewsletter ? 'newsletter' : 's.whatsapp.net') : statusJid const binaryNodeContent: BinaryNode[] = [] const devices: JidWithDevice[] = [] @@ -431,6 +432,15 @@ export const makeMessagesSocket = (config: SocketConfig) => { }) await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } }) + } else if(isNewsletter) { + const patched = await patchMessageBeforeSending(message, []) + const bytes = encodeNewsletterMessage(patched) + + binaryNodeContent.push({ + tag: 'plaintext', + attrs: {}, + content: bytes + }) } else { const { user: meUser, device: meDevice } = jidDecode(meId)! @@ -679,7 +689,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { } content.directPath = media.directPath - content.url = getUrlFromDirectPath(content.directPath!) + content.url = getUrlFromDirectPath(content.directPath) logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful') } catch(err) { diff --git a/src/Socket/newsletter.ts b/src/Socket/newsletter.ts new file mode 100644 index 00000000000..309efb49c42 --- /dev/null +++ b/src/Socket/newsletter.ts @@ -0,0 +1,266 @@ +import { NewsletterFetchedUpdate, NewsletterMetadata, NewsletterReaction, NewsletterReactionMode, NewsletterViewRole, QueryIds, SocketConfig, WAMediaUpload, XWAPaths } from '../Types' +import { decryptMessageNode, generateMessageID, generateProfilePicture } from '../Utils' +import { BinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary' +import { makeGroupsSocket } from './groups' + +export const makeNewsletterSocket = (config: SocketConfig) => { + const sock = makeGroupsSocket(config) + const { authState, signalRepository, query, generateMessageTag } = sock + + const encoder = new TextEncoder() + + const newsletterQuery = async(jid: string, type: 'get' | 'set', content: BinaryNode[]) => ( + query({ + tag: 'iq', + attrs: { + id: generateMessageTag(), + type, + xmlns: 'newsletter', + to: jid, + }, + content + }) + ) + + const newsletterWMexQuery = async(jid: string | undefined, queryId: QueryIds, content?: object) => ( + query({ + tag: 'iq', + attrs: { + id: generateMessageTag(), + type: 'get', + xmlns: 'w:mex', + to: S_WHATSAPP_NET, + }, + content: [ + { + tag: 'query', + attrs: { 'query_id': queryId }, + content: encoder.encode( + JSON.stringify({ + variables: { + 'newsletter_id': jid, + ...content + } + }) + ) + } + ] + }) + ) + + const parseFetchedUpdates = async(node: BinaryNode, type: 'messages' | 'updates') => { + let child + + if(type === 'messages') { + child = getBinaryNodeChild(node, 'messages') + } else { + const parent = getBinaryNodeChild(node, 'message_updates') + child = getBinaryNodeChild(parent, 'messages') + } + + return await Promise.all(getAllBinaryNodeChildren(child).map(async messageNode => { + messageNode.attrs.from = child?.attrs.jid as string + + const views = parseInt(getBinaryNodeChild(messageNode, 'views_count')?.attrs?.count || '0') + const reactionNode = getBinaryNodeChild(messageNode, 'reactions') + const reactions = getBinaryNodeChildren(reactionNode, 'reaction') + .map(({ attrs }) => ({ count: +attrs.count, code: attrs.code } as NewsletterReaction)) + + const data: NewsletterFetchedUpdate = { + 'server_id': messageNode.attrs.server_id, + views, + reactions + } + + if(type === 'messages') { + const { fullMessage: message, decrypt } = await decryptMessageNode( + messageNode, + authState.creds.me!.id, + authState.creds.me!.lid || '', + signalRepository, + config.logger + ) + + await decrypt() + + data.message = message + } + + return data + })) + } + + return { + ...sock, + subscribeNewsletterUpdates: async(jid: string) => { + const result = await newsletterQuery(jid, 'set', [{ tag: 'live_updates', attrs: {}, content: [] }]) + + return getBinaryNodeChild(result, 'live_updates')?.attrs as {duration: string} + }, + + newsletterReactionMode: async(jid: string, mode: NewsletterReactionMode) => { + await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, { + updates: { settings: { 'reaction_codes': { value: mode } } } + }) + }, + + newsletterUpdateDescription: async(jid: string, description?: string) => { + await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, { + updates: { description: description || '', settings: null } + }) + }, + + newsletterUpdateName: async(jid: string, name: string) => { + await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, { + updates: { name, settings: null } + }) + }, + + newsletterUpdatePicture: async(jid: string, content: WAMediaUpload) => { + const { img } = await generateProfilePicture(content) + + await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, { + updates: { picture: img.toString('base64'), settings: null } + }) + }, + + newsletterRemovePicture: async(jid: string) => { + await newsletterWMexQuery(jid, QueryIds.JOB_MUTATION, { + updates: { picture: '', settings: null } + }) + }, + + newsletterAction: async(jid: string, type: 'follow' | 'unfollow' | 'mute' | 'unmute') => { + await newsletterWMexQuery(jid, type.toUpperCase() as QueryIds) + }, + + newsletterCreate: async(name: string, description: string) => { + //TODO: Implement TOS system wide for Meta AI, communities, and here etc. + /**tos query */ + await query({ + tag: 'iq', + attrs: { + to: S_WHATSAPP_NET, + xmlns: 'tos', + id: generateMessageTag(), + type: 'set' + }, + content: [ + { + tag: 'notice', + attrs: { + id: '20601218', + stage: '5' + }, + content: [] + } + ] + }) + const result = await newsletterWMexQuery(undefined, QueryIds.CREATE, { + input: { name, description } + }) + + return extractNewsletterMetadata(result, true) + }, + + newsletterMetadata: async(type: 'invite' | 'jid', key: string, role?: NewsletterViewRole) => { + const result = await newsletterWMexQuery(undefined, QueryIds.METADATA, { + input: { + key, + type: type.toUpperCase(), + 'view_role': role || 'GUEST' + }, + 'fetch_viewer_metadata': true, + 'fetch_full_image': true, + 'fetch_creation_time': true + }) + + return extractNewsletterMetadata(result) + }, + + newsletterAdminCount: async(jid: string) => { + const result = await newsletterWMexQuery(jid, QueryIds.ADMIN_COUNT) + + const buff = getBinaryNodeChild(result, 'result')?.content?.toString() + + return JSON.parse(buff!).data[XWAPaths.ADMIN_COUNT].admin_count as number + }, + + /**user is Lid, not Jid */ + newsletterChangeOwner: async(jid: string, user: string) => { + await newsletterWMexQuery(jid, QueryIds.CHANGE_OWNER, { + 'user_id': user + }) + }, + + /**user is Lid, not Jid */ + newsletterDemote: async(jid: string, user: string) => { + await newsletterWMexQuery(jid, QueryIds.DEMOTE, { + 'user_id': user + }) + }, + + newsletterDelete: async(jid: string) => { + await newsletterWMexQuery(jid, QueryIds.DELETE) + }, + + /**if code wasn't passed, the reaction will be removed (if is reacted) */ + newsletterReactMessage: async(jid: string, serverId: string, code?: string) => { + await query({ + tag: 'message', + attrs: { to: jid, ...(!code ? { edit: '7' } : {}), type: 'reaction', 'server_id': serverId, id: generateMessageID() }, + content: [{ + tag: 'reaction', + attrs: code ? { code } : {} + }] + }) + }, + + newsletterFetchMessages: async(type: 'invite' | 'jid', key: string, count: number, after?: number) => { + const result = await newsletterQuery(S_WHATSAPP_NET, 'get', [ + { + tag: 'messages', + attrs: { type, ...(type === 'invite' ? { key } : { jid: key }), count: count.toString(), after: after?.toString() || '100' } + } + ]) + + return await parseFetchedUpdates(result, 'messages') + }, + + newsletterFetchUpdates: async(jid: string, count: number, after?: number, since?: number) => { + const result = await newsletterQuery(jid, 'get', [ + { + tag: 'message_updates', + attrs: { count: count.toString(), after: after?.toString() || '100', since: since?.toString() || '0' } + } + ]) + + return await parseFetchedUpdates(result, 'updates') + } + } +} + +export const extractNewsletterMetadata = (node: BinaryNode, isCreate?: boolean) => { + const result = getBinaryNodeChild(node, 'result')?.content?.toString() + const metadataPath = JSON.parse(result!).data[isCreate ? XWAPaths.CREATE : XWAPaths.NEWSLETTER] + + const metadata: NewsletterMetadata = { + id: metadataPath.id, + state: metadataPath.state.type, + 'creation_time': +metadataPath.thread_metadata.creation_time, + name: metadataPath.thread_metadata.name.text, + nameTime: +metadataPath.thread_metadata.name.update_time, + description: metadataPath.thread_metadata.description.text, + descriptionTime: +metadataPath.thread_metadata.description.update_time, + invite: metadataPath.thread_metadata.invite, + handle: metadataPath.thread_metadata.handle, + picture: metadataPath.thread_metadata.picture.direct_path || null, + preview: metadataPath.thread_metadata.preview.direct_path || null, + 'reaction_codes': metadataPath.thread_metadata?.settings?.reaction_codes?.value, + subscribers: +metadataPath.thread_metadata.subscribers_count, + verification: metadataPath.thread_metadata.verification, + 'viewer_metadata': metadataPath.viewer_metadata + } + + return metadata +} \ No newline at end of file diff --git a/src/Types/Events.ts b/src/Types/Events.ts index e10aad74f74..7d1df1ba786 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -8,6 +8,7 @@ import { GroupMetadata, ParticipantAction, RequestJoinAction, RequestJoinMethod import { Label } from './Label' import { LabelAssociation } from './LabelAssociation' import { MessageUpsertType, MessageUserReceiptUpdate, WAMessage, WAMessageKey, WAMessageUpdate } from './Message' +import { NewsletterSettingsUpdate, NewsletterViewRole, SubscriberAction } from './Newsletter' import { ConnectionState } from './State' export type BaileysEventMap = { @@ -54,6 +55,12 @@ export type BaileysEventMap = { 'group-participants.update': { id: string, author: string, participants: string[], action: ParticipantAction } 'group.join-request': { id: string, author: string, participant: string, action: RequestJoinAction, method: RequestJoinMethod } + 'newsletter.reaction': { id: string, server_id: string, reaction: {code?: string, count?: number, removed?: boolean}} + 'newsletter.view': { id: string, server_id: string, count: number} + /**don't handles subscribe/unsubscribe actions */ + 'newsletter-participants.update': { id: string, author: string, user: string, new_role: NewsletterViewRole, action: SubscriberAction} + 'newsletter-settings.update': { id: string, update: NewsletterSettingsUpdate} + 'blocklist.set': { blocklist: string[] } 'blocklist.update': { blocklist: string[], type: 'add' | 'remove' } diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 62d2f8b0304..e8f3bbcb40e 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -13,7 +13,7 @@ export type WAMessage = proto.IWebMessageInfo export type WAMessageContent = proto.IMessage export type WAContactMessage = proto.Message.IContactMessage export type WAContactsArrayMessage = proto.Message.IContactsArrayMessage -export type WAMessageKey = proto.IMessageKey +export type WAMessageKey = proto.IMessageKey & {server_id?: string} export type WATextMessage = proto.Message.IExtendedTextMessage export type WAContextInfo = proto.IContextInfo export type WALocationMessage = proto.Message.ILocationMessage diff --git a/src/Types/Newsletter.ts b/src/Types/Newsletter.ts new file mode 100644 index 00000000000..59a261f0e46 --- /dev/null +++ b/src/Types/Newsletter.ts @@ -0,0 +1,98 @@ +import { proto } from '../../WAProto' + +export type NewsletterReactionMode = 'ALL' | 'BASIC' | 'NONE' + +export type NewsletterState = 'ACTIVE' | 'GEOSUSPENDED' | 'SUSPENDED' + +export type NewsletterVerification = 'VERIFIED' | 'UNVERIFIED' + +export type NewsletterMute = 'ON' | 'OFF' | 'UNDEFINED' + +export type NewsletterViewRole = 'ADMIN' | 'GUEST' | 'OWNER' | 'SUBSCRIBER' + +export type NewsletterViewerMetadata = { + mute: NewsletterMute + view_role: NewsletterViewRole +} + +export type NewsletterMetadata = { + /**jid of newsletter */ + id: string + /**state of newsletter */ + state: NewsletterState + /**creation timestamp of newsletter */ + creation_time: number + /**name of newsletter */ + name: string + /**timestamp of last name modification of newsletter */ + nameTime: number + /**description of newsletter */ + description: string + /**timestamp of last description modification of newsletter */ + descriptionTime: number + /**invite code of newsletter */ + invite: string + /**i dont know */ + handle: null + /**direct path of picture */ + picture: string | null + /**direct path of picture preview (lower quality) */ + preview: string | null + /**reaction mode of newsletter */ + reaction_codes?: NewsletterReactionMode + /**subscribers count of newsletter */ + subscribers: number + /**verification state of newsletter */ + verification: NewsletterVerification + /**viewer metadata */ + viewer_metadata: NewsletterViewerMetadata +} + +export type SubscriberAction = 'promote' | 'demote' + +export type ReactionModeUpdate = {reaction_codes: {blocked_codes: null, enabled_ts_sec: null, value: NewsletterReactionMode}} + +/**only exists reaction mode update */ +export type NewsletterSettingsUpdate = ReactionModeUpdate + +export type NewsletterReaction = {count: number, code: string} + +export type NewsletterFetchedUpdate = { + /**id of message in newsletter, starts from 100 */ + server_id: string + /**count of views in this message */ + views?: number + /**reactions in this message */ + reactions: NewsletterReaction[] + /**the message, if you requested only updates, you will not receive message */ + message?: proto.IWebMessageInfo +} + +export enum MexOperations { + PROMOTE = 'NotificationNewsletterAdminPromote', + DEMOTE = 'NotificationNewsletterAdminDemote', + UPDATE = 'NotificationNewsletterUpdate' +} + +export enum XWAPaths { + PROMOTE = 'xwa2_notify_newsletter_admin_promote', + DEMOTE = 'xwa2_notify_newsletter_admin_demote', + ADMIN_COUNT = 'xwa2_newsletter_admin', + CREATE = 'xwa2_newsletter_create', + NEWSLETTER = 'xwa2_newsletter', + METADATA_UPDATE = 'xwa2_notify_newsletter_on_metadata_update' +} + +export enum QueryIds { + JOB_MUTATION = '7150902998257522', + METADATA = '6620195908089573', + UNFOLLOW = '7238632346214362', + FOLLOW = '7871414976211147', + UNMUTE = '7337137176362961', + MUTE = '25151904754424642', + CREATE = '6996806640408138', + ADMIN_COUNT = '7130823597031706', + CHANGE_OWNER = '7341777602580933', + DELETE = '8316537688363079', + DEMOTE = '6551828931592903' +} \ No newline at end of file diff --git a/src/Types/index.ts b/src/Types/index.ts index fe50887e81c..3e96000e61e 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -1,5 +1,6 @@ export * from './Auth' export * from './GroupMetadata' +export * from './Newsletter' export * from './Chat' export * from './Contact' export * from './State' diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index 3760865c73a..406cd10fc1c 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -2,12 +2,12 @@ import { Boom } from '@hapi/boom' import { Logger } from 'pino' import { proto } from '../../WAProto' import { SignalRepository, WAMessageKey } from '../Types' -import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidStatusBroadcast, isJidUser, isLidUser } from '../WABinary' +import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isJidUser, isLidUser } from '../WABinary' import { BufferJSON, unpadRandomMax16 } from './generics' const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node' -type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status' +type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status' | 'newsletter' /** * Decode the received node as a message. @@ -64,6 +64,10 @@ export function decodeMessageNode( msgType = 'group' author = participant chatId = from + } else if(isJidNewsletter(from)) { + msgType = 'newsletter' + author = from + chatId = from } else if(isJidBroadcast(from)) { if(!participant) { throw new Boom('No participant in group message') @@ -78,18 +82,23 @@ export function decodeMessageNode( chatId = from author = participant + } else if(isJidNewsletter(from)) { + msgType = 'newsletter' + chatId = from + author = from } else { throw new Boom('Unknown message type', { data: stanza }) } - const fromMe = (isLidUser(from) ? isMeLid : isMe)(stanza.attrs.participant || stanza.attrs.from) - const pushname = stanza.attrs.notify + const fromMe = isJidNewsletter(from) ? !!stanza.attrs?.is_sender || false : (isLidUser(from) ? isMeLid : isMe)(stanza.attrs.participant || stanza.attrs.from) + const pushname = stanza?.attrs?.notify const key: WAMessageKey = { remoteJid: chatId, fromMe, id: msgId, - participant + participant, + 'server_id': stanza.attrs?.server_id } const fullMessage: proto.IWebMessageInfo = { @@ -132,7 +141,7 @@ export const decryptMessageNode = ( fullMessage.verifiedBizName = details.verifiedName } - if(tag !== 'enc') { + if(tag !== 'enc' && tag !== 'plaintext') { continue } @@ -145,7 +154,7 @@ export const decryptMessageNode = ( let msgBuffer: Uint8Array try { - const e2eType = attrs.type + const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type switch (e2eType) { case 'skmsg': msgBuffer = await repository.decryptGroupMessage({ @@ -163,21 +172,24 @@ export const decryptMessageNode = ( ciphertext: content }) break + case 'plaintext': + msgBuffer = content + break default: throw new Error(`Unknown e2e type: ${e2eType}`) } - let msg: proto.IMessage = proto.Message.decode(unpadRandomMax16(msgBuffer)) + let msg: proto.IMessage = proto.Message.decode(e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer) msg = msg.deviceSentMessage?.message || msg if(msg.senderKeyDistributionMessage) { - try { + try { await repository.processSenderKeyDistributionMessage({ authorJid: author, item: msg.senderKeyDistributionMessage }) } catch(err) { logger.error({ key: fullMessage.key, err }, 'failed to decrypt message') - } + } } if(fullMessage.message) { diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index c99dbb43ee9..94a18cf9077 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -87,6 +87,10 @@ export const encodeWAMessage = (message: proto.IMessage) => ( ) ) +export const encodeNewsletterMessage = (message: proto.IMessage) => ( + proto.Message.encode(message).finish() +) + export const generateRegistrationId = (): number => { return Uint16Array.from(randomBytes(2))[0] & 16383 } diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 12360917d86..394140689cc 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -24,7 +24,7 @@ import { WAProto, WATextMessage, } from '../Types' -import { isJidGroup, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary' +import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary' import { sha256 } from './crypto' import { generateMessageID, getKeyAuthor, unixTimestampSeconds } from './generics' import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, MediaDownloadOptions } from './messages-media' @@ -126,7 +126,7 @@ export const prepareWAMessageMedia = async( !!uploadData.media.url && !!options.mediaCache && ( // generate the key - mediaType + ':' + uploadData.media.url!.toString() + mediaType + ':' + uploadData.media.url.toString() ) if(mediaType === 'document' && !uploadData.fileName) { @@ -583,7 +583,8 @@ export const generateWAMessageFromContent = ( const timestamp = unixTimestampSeconds(options.timestamp) const { quoted, userJid } = options - if(quoted) { + // only set quoted if isn't a newsletter message + if(quoted && !isJidNewsletter(jid)) { const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid) let quotedMsg = normalizeMessageContent(quoted.message)! @@ -616,7 +617,9 @@ export const generateWAMessageFromContent = ( // and it's not a protocol message -- delete, toggle disappear message key !== 'protocolMessage' && // already not converted to disappearing message - key !== 'ephemeralMessage' + key !== 'ephemeralMessage' && + // newsletter not accept disappearing messages + !isJidNewsletter(jid) ) { innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), @@ -730,7 +733,7 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu content = normalizeMessageContent(content) if(content?.buttonsMessage) { - return extractFromTemplateMessage(content.buttonsMessage!) + return extractFromTemplateMessage(content.buttonsMessage) } if(content?.templateMessage?.hydratedFourRowTemplate) { diff --git a/src/WABinary/jid-utils.ts b/src/WABinary/jid-utils.ts index cb58eefd7a4..0d0dafca746 100644 --- a/src/WABinary/jid-utils.ts +++ b/src/WABinary/jid-utils.ts @@ -4,7 +4,7 @@ export const SERVER_JID = 'server@c.us' export const PSA_WID = '0@c.us' export const STORIES_JID = 'status@broadcast' -export type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call' | 'lid' +export type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call' | 'lid' | 'newsletter' export type JidWithDevice = { user: string @@ -12,7 +12,7 @@ export type JidWithDevice = { } export type FullJid = JidWithDevice & { - server: JidServer | string + server: JidServer domainType?: number } @@ -26,7 +26,7 @@ export const jidDecode = (jid: string | undefined): FullJid | undefined => { return undefined } - const server = jid!.slice(sepIdx + 1) + const server = jid!.slice(sepIdx + 1) as JidServer const userCombined = jid!.slice(0, sepIdx) const [userAgent, device] = userCombined.split(':') @@ -52,6 +52,8 @@ export const isLidUser = (jid: string | undefined) => (jid?.endsWith('@lid')) export const isJidBroadcast = (jid: string | undefined) => (jid?.endsWith('@broadcast')) /** is the jid a group */ export const isJidGroup = (jid: string | undefined) => (jid?.endsWith('@g.us')) +/** is the jid a newsletter */ +export const isJidNewsletter = (jid: string | undefined) => (jid?.endsWith('@newsletter')) /** is the jid the status broadcast */ export const isJidStatusBroadcast = (jid: string) => jid === 'status@broadcast'