Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Socket/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
import { UserFacingSocketConfig } from '../Types'
import { makeRegistrationSocket as _makeSocket } from './registration'
import { makeNewslettersSocket as _makeSocket } from './newsletters'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Baileys uses factory functions to create the final socket object:

  • makeMessagesRecvSocket
  • makeMessagesRecvSocket
  • makeMessagesSocket
  • makeGroupsSocket
  • makeBusinessSocket
  • makeChatsSocket
  • makeRegistrationSocket

Each one call the next. I'm not sure if the order matters here, so I just put makeNewslettersSocket a the end.

Any insight here?


// export the last socket layer
const makeWASocket = (config: UserFacingSocketConfig) => (
Expand Down
60 changes: 60 additions & 0 deletions src/Socket/newsletters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { SocketConfig } from '../Types'
import { GraphQLResponse, NewsletterMetadata } from '../Types/Newsletter'
import { convertKeysToCamelCase, getBinaryNodeChildBuffer, S_WHATSAPP_NET } from '..'
import { makeRegistrationSocket } from './registration'

export function makeNewslettersSocket(config: SocketConfig) {
const sock = makeRegistrationSocket(config)
const { query } = sock

const mexQuery = async(queryId: string, variables = {}) => {
const response = await query({
tag: 'iq',
attrs: {
type: 'get',
xmlns: 'w:mex',
to: S_WHATSAPP_NET
},
content: [ { tag: 'query', attrs: { 'query_id': queryId }, content: JSON.stringify({ variables }) } ]
})

const result = getBinaryNodeChildBuffer(response, 'result')

if(!result) {
throw new Error('Missing "result" element in response')
}

const gqlResp: GraphQLResponse = JSON.parse(result.toString())

if(gqlResp.errors?.length > 0) {
throw new Error('Graphql Error when fetching newsletters')
}

return gqlResp.data
}

// const queryFetchNewsletter = '6563316087068696'
// const queryFetchNewsletterDehydrated = '7272540469429201'
// const queryRecommendedNewsletters = '7263823273662354' // variables -> input -> {limit: 20, country_codes: [string]}, output: xwa2_newsletters_recommended
// const queryNewslettersDirectory = '6190824427689257' // variables -> input -> {view: "RECOMMENDED", limit: 50, start_cursor: base64, filters: {country_codes: [string]}}
const querySubscribedNewsletters = '6388546374527196' // variables -> empty, output: xwa2_newsletter_subscribed
// const queryNewsletterSubscribers = '9800646650009898' // variables -> input -> {newsletter_id, count}, output: xwa2_newsletter_subscribers -> subscribers -> edges
// const mutationMuteNewsletter = '6274038279359549' // variables -> {newsletter_id, updates->{description, settings}}, output: xwa2_newsletter_update -> NewsletterMetadata without viewer meta
// const mutationUnmuteNewsletter = '6068417879924485'
// const mutationUpdateNewsletter = '7150902998257522'
// const mutationCreateNewsletter = '6234210096708695'
// const mutationUnfollowNewsletter = '6392786840836363'
// const mutationFollowNewsletter = '9926858900719341'
Comment on lines +36 to +47

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the ids for the remaining queries and mutations here for future reference.


return {
...sock,
getSubscribedNewsletters: async() => {
const result = await mexQuery(querySubscribedNewsletters, {})
if(result?.['xwa2_newsletter_subscribed']) {
return convertKeysToCamelCase(result['xwa2_newsletter_subscribed']) as NewsletterMetadata[]
} else {
throw new Error('Missing "xwa2_newsletter_subscribed" in response')
}
},
}
}
78 changes: 78 additions & 0 deletions src/Types/Newsletter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
export interface NewsletterMuted {
muted: boolean
}

interface WrappedNewsletterState {
type: string
}

export interface NewsletterMetadata {
id: string
state: WrappedNewsletterState
threadMetadata: NewsletterThreadMetadata
viewerMetadata: NewsletterViewerMetadata | null
}

interface NewsletterViewerMetadata {
mute: string
role: string
}

interface NewsletterReactionSettings {
value: string
}

interface NewsletterSettings {
reactionCodes: NewsletterReactionSettings
}

interface ProfilePictureInfo {
url: string
id: string
type: string
directPath: string
}

interface NewsletterThreadMetadata {
creationTime: string
invite: string
name: NewsletterText
description: NewsletterText
subscribersCount: number
verification: string
picture: ProfilePictureInfo | null
preview: ProfilePictureInfo
settings: NewsletterSettings
}

interface NewsletterText {
text: string
id: string
updateTime: string
}

export interface NewsletterMessage {
messageServerId: string
viewsCount: number
reactionCounts: { [key: string]: number }
message: any // Replace with actual type
}

interface GraphQLErrorExtensions {
errorCode: number
isRetryable: boolean
severity: string
}

interface GraphQLError {
extensions: GraphQLErrorExtensions
message: string
path: string[]
}

interface GraphQLErrors extends Array<GraphQLError> {}

export interface GraphQLResponse {
data: any // Replace with actual type
errors: GraphQLErrors
}
Comment on lines +1 to +78

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these types (ported from whatsmeow) are not used yet, but they will be once the remaining queries/mutations are implemented.

24 changes: 21 additions & 3 deletions src/Utils/decode-wa-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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.
Expand Down Expand Up @@ -78,6 +78,10 @@ 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 })
}
Expand Down Expand Up @@ -132,7 +136,21 @@ export const decryptMessageNode = (
fullMessage.verifiedBizName = details.verifiedName
}

if(tag !== 'enc') {
if(tag === 'plaintext') {
// plaintext messages are used for newsletter (whatsapp channel) messages
if(content instanceof Uint8Array) {
try {
fullMessage.message = proto.Message.decode(content)
} catch(err) {
logger.error(
{ key: fullMessage.key, err },
'failed to decode plaintext message'
)
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
fullMessage.messageStubParameters = [err.message, 'plaintext']
}
}
} else if(tag !== 'enc') {
continue
}

Expand Down
20 changes: 20 additions & 0 deletions src/WABinary/generic-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,24 @@ export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i =
const content: string = children ? `>\n${children}\n${tabs(i)}</${node.tag}>` : '/>'

return tag + content
}

function toCamelCase(str) {
return str.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', ''))
}

export function convertKeysToCamelCase(obj) {
if(typeof obj !== 'object' || obj === null) {
return obj
}

if(Array.isArray(obj)) {
return obj.map((v) => convertKeysToCamelCase(v))
}

return Object.keys(obj).reduce((result, key) => {
const camelCaseKey = toCamelCase(key)
result[camelCaseKey] = convertKeysToCamelCase(obj[key])
return result
}, {})
}
2 changes: 2 additions & 0 deletions src/WABinary/jid-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (Whatsapp Channel) */
export const isJidNewsletter = (jid: string | undefined) => (jid?.endsWith('@newsletter'))
/** is the jid the status broadcast */
export const isJidStatusBroadcast = (jid: string) => jid === 'status@broadcast'

Expand Down