chore: format everything

This commit is contained in:
canove
2025-05-06 12:10:19 -03:00
parent 04afa20244
commit fa706d0b50
76 changed files with 8241 additions and 7142 deletions

View File

@@ -1,2 +1,2 @@
export * from './types'
export * from './websocket'
export * from './websocket'

View File

@@ -8,12 +8,15 @@ export abstract class AbstractSocketClient extends EventEmitter {
abstract get isClosing(): boolean
abstract get isConnecting(): boolean
constructor(public url: URL, public config: SocketConfig) {
constructor(
public url: URL,
public config: SocketConfig
) {
super()
this.setMaxListeners(0)
}
abstract connect(): Promise<void>
abstract close(): Promise<void>
abstract send(str: Uint8Array | string, cb?: (err?: Error) => void): boolean;
}
abstract send(str: Uint8Array | string, cb?: (err?: Error) => void): boolean
}

View File

@@ -3,7 +3,6 @@ import { DEFAULT_ORIGIN } from '../../Defaults'
import { AbstractSocketClient } from './types'
export class WebSocketClient extends AbstractSocketClient {
protected socket: WebSocket | null = null
get isOpen(): boolean {
@@ -20,7 +19,7 @@ export class WebSocketClient extends AbstractSocketClient {
}
async connect(): Promise<void> {
if(this.socket) {
if (this.socket) {
return
}
@@ -29,20 +28,20 @@ export class WebSocketClient extends AbstractSocketClient {
headers: this.config.options?.headers as {},
handshakeTimeout: this.config.connectTimeoutMs,
timeout: this.config.connectTimeoutMs,
agent: this.config.agent,
agent: this.config.agent
})
this.socket.setMaxListeners(0)
const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
for(const event of events) {
for (const event of events) {
this.socket?.on(event, (...args: any[]) => this.emit(event, ...args))
}
}
async close(): Promise<void> {
if(!this.socket) {
if (!this.socket) {
return
}

View File

@@ -1,43 +1,46 @@
import { GetCatalogOptions, ProductCreate, ProductUpdate, SocketConfig } from '../Types'
import { parseCatalogNode, parseCollectionsNode, parseOrderDetailsNode, parseProductNode, toProductNode, uploadingNecessaryImagesOfProduct } from '../Utils/business'
import {
parseCatalogNode,
parseCollectionsNode,
parseOrderDetailsNode,
parseProductNode,
toProductNode,
uploadingNecessaryImagesOfProduct
} from '../Utils/business'
import { BinaryNode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary'
import { getBinaryNodeChild } from '../WABinary/generic-utils'
import { makeMessagesRecvSocket } from './messages-recv'
export const makeBusinessSocket = (config: SocketConfig) => {
const sock = makeMessagesRecvSocket(config)
const {
authState,
query,
waUploadToServer
} = sock
const { authState, query, waUploadToServer } = sock
const getCatalog = async({ jid, limit, cursor }: GetCatalogOptions) => {
const getCatalog = async ({ jid, limit, cursor }: GetCatalogOptions) => {
jid = jid || authState.creds.me?.id
jid = jidNormalizedUser(jid)
const queryParamNodes: BinaryNode[] = [
{
tag: 'limit',
attrs: { },
attrs: {},
content: Buffer.from((limit || 10).toString())
},
{
tag: 'width',
attrs: { },
attrs: {},
content: Buffer.from('100')
},
{
tag: 'height',
attrs: { },
attrs: {},
content: Buffer.from('100')
},
}
]
if(cursor) {
if (cursor) {
queryParamNodes.push({
tag: 'after',
attrs: { },
attrs: {},
content: cursor
})
}
@@ -54,7 +57,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
tag: 'product_catalog',
attrs: {
jid,
'allow_shop_source': 'true'
allow_shop_source: 'true'
},
content: queryParamNodes
}
@@ -63,7 +66,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
return parseCatalogNode(result)
}
const getCollections = async(jid?: string, limit = 51) => {
const getCollections = async (jid?: string, limit = 51) => {
jid = jid || authState.creds.me?.id
jid = jidNormalizedUser(jid)
const result = await query({
@@ -72,33 +75,33 @@ export const makeBusinessSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'w:biz:catalog',
'smax_id': '35'
smax_id: '35'
},
content: [
{
tag: 'collections',
attrs: {
'biz_jid': jid,
biz_jid: jid
},
content: [
{
tag: 'collection_limit',
attrs: { },
attrs: {},
content: Buffer.from(limit.toString())
},
{
tag: 'item_limit',
attrs: { },
attrs: {},
content: Buffer.from(limit.toString())
},
{
tag: 'width',
attrs: { },
attrs: {},
content: Buffer.from('100')
},
{
tag: 'height',
attrs: { },
attrs: {},
content: Buffer.from('100')
}
]
@@ -109,14 +112,14 @@ export const makeBusinessSocket = (config: SocketConfig) => {
return parseCollectionsNode(result)
}
const getOrderDetails = async(orderId: string, tokenBase64: string) => {
const getOrderDetails = async (orderId: string, tokenBase64: string) => {
const result = await query({
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'fb:thrift_iq',
'smax_id': '5'
smax_id: '5'
},
content: [
{
@@ -128,23 +131,23 @@ export const makeBusinessSocket = (config: SocketConfig) => {
content: [
{
tag: 'image_dimensions',
attrs: { },
attrs: {},
content: [
{
tag: 'width',
attrs: { },
attrs: {},
content: Buffer.from('100')
},
{
tag: 'height',
attrs: { },
attrs: {},
content: Buffer.from('100')
}
]
},
{
tag: 'token',
attrs: { },
attrs: {},
content: Buffer.from(tokenBase64)
}
]
@@ -155,7 +158,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
return parseOrderDetailsNode(result)
}
const productUpdate = async(productId: string, update: ProductUpdate) => {
const productUpdate = async (productId: string, update: ProductUpdate) => {
update = await uploadingNecessaryImagesOfProduct(update, waUploadToServer)
const editNode = toProductNode(productId, update)
@@ -174,12 +177,12 @@ export const makeBusinessSocket = (config: SocketConfig) => {
editNode,
{
tag: 'width',
attrs: { },
attrs: {},
content: '100'
},
{
tag: 'height',
attrs: { },
attrs: {},
content: '100'
}
]
@@ -193,7 +196,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
return parseProductNode(productNode!)
}
const productCreate = async(create: ProductCreate) => {
const productCreate = async (create: ProductCreate) => {
// ensure isHidden is defined
create.isHidden = !!create.isHidden
create = await uploadingNecessaryImagesOfProduct(create, waUploadToServer)
@@ -214,12 +217,12 @@ export const makeBusinessSocket = (config: SocketConfig) => {
createNode,
{
tag: 'width',
attrs: { },
attrs: {},
content: '100'
},
{
tag: 'height',
attrs: { },
attrs: {},
content: '100'
}
]
@@ -233,7 +236,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
return parseProductNode(productNode!)
}
const productDelete = async(productIds: string[]) => {
const productDelete = async (productIds: string[]) => {
const result = await query({
tag: 'iq',
attrs: {
@@ -245,19 +248,17 @@ export const makeBusinessSocket = (config: SocketConfig) => {
{
tag: 'product_catalog_delete',
attrs: { v: '1' },
content: productIds.map(
id => ({
tag: 'product',
attrs: { },
content: [
{
tag: 'id',
attrs: { },
content: Buffer.from(id)
}
]
})
)
content: productIds.map(id => ({
tag: 'product',
attrs: {},
content: [
{
tag: 'id',
attrs: {},
content: Buffer.from(id)
}
]
}))
}
]
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +1,70 @@
import { proto } from '../../WAProto'
import { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey, WAMessageStubType } from '../Types'
import {
GroupMetadata,
GroupParticipant,
ParticipantAction,
SocketConfig,
WAMessageKey,
WAMessageStubType
} from '../Types'
import { generateMessageIDV2, unixTimestampSeconds } from '../Utils'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, jidEncode, jidNormalizedUser } from '../WABinary'
import {
BinaryNode,
getBinaryNodeChild,
getBinaryNodeChildren,
getBinaryNodeChildString,
jidEncode,
jidNormalizedUser
} from '../WABinary'
import { makeChatsSocket } from './chats'
export const makeGroupsSocket = (config: SocketConfig) => {
const sock = makeChatsSocket(config)
const { authState, ev, query, upsertMessage } = sock
const groupQuery = async(jid: string, type: 'get' | 'set', content: BinaryNode[]) => (
const groupQuery = async (jid: string, type: 'get' | 'set', content: BinaryNode[]) =>
query({
tag: 'iq',
attrs: {
type,
xmlns: 'w:g2',
to: jid,
to: jid
},
content
})
)
const groupMetadata = async(jid: string) => {
const result = await groupQuery(
jid,
'get',
[ { tag: 'query', attrs: { request: 'interactive' } } ]
)
const groupMetadata = async (jid: string) => {
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
return extractGroupMetadata(result)
}
const groupFetchAllParticipating = async() => {
const groupFetchAllParticipating = async () => {
const result = await query({
tag: 'iq',
attrs: {
to: '@g.us',
xmlns: 'w:g2',
type: 'get',
type: 'get'
},
content: [
{
tag: 'participating',
attrs: { },
attrs: {},
content: [
{ tag: 'participants', attrs: { } },
{ tag: 'description', attrs: { } }
{ tag: 'participants', attrs: {} },
{ tag: 'description', attrs: {} }
]
}
]
})
const data: { [_: string]: GroupMetadata } = { }
const data: { [_: string]: GroupMetadata } = {}
const groupsChild = getBinaryNodeChild(result, 'groups')
if(groupsChild) {
if (groupsChild) {
const groups = getBinaryNodeChildren(groupsChild, 'group')
for(const groupNode of groups) {
for (const groupNode of groups) {
const meta = extractGroupMetadata({
tag: 'result',
attrs: { },
attrs: {},
content: [groupNode]
})
data[meta.id] = meta
@@ -68,9 +76,9 @@ export const makeGroupsSocket = (config: SocketConfig) => {
return data
}
sock.ws.on('CB:ib,,dirty', async(node: BinaryNode) => {
sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
const { attrs } = getBinaryNodeChild(node, 'dirty')!
if(attrs.type !== 'groups') {
if (attrs.type !== 'groups') {
return
}
@@ -81,89 +89,69 @@ export const makeGroupsSocket = (config: SocketConfig) => {
return {
...sock,
groupMetadata,
groupCreate: async(subject: string, participants: string[]) => {
groupCreate: async (subject: string, participants: string[]) => {
const key = generateMessageIDV2()
const result = await groupQuery(
'@g.us',
'set',
[
{
tag: 'create',
attrs: {
subject,
key
},
content: participants.map(jid => ({
tag: 'participant',
attrs: { jid }
}))
}
]
)
const result = await groupQuery('@g.us', 'set', [
{
tag: 'create',
attrs: {
subject,
key
},
content: participants.map(jid => ({
tag: 'participant',
attrs: { jid }
}))
}
])
return extractGroupMetadata(result)
},
groupLeave: async(id: string) => {
await groupQuery(
'@g.us',
'set',
[
{
tag: 'leave',
attrs: { },
content: [
{ tag: 'group', attrs: { id } }
]
}
]
)
groupLeave: async (id: string) => {
await groupQuery('@g.us', 'set', [
{
tag: 'leave',
attrs: {},
content: [{ tag: 'group', attrs: { id } }]
}
])
},
groupUpdateSubject: async(jid: string, subject: string) => {
await groupQuery(
jid,
'set',
[
{
tag: 'subject',
attrs: { },
content: Buffer.from(subject, 'utf-8')
}
]
)
groupUpdateSubject: async (jid: string, subject: string) => {
await groupQuery(jid, 'set', [
{
tag: 'subject',
attrs: {},
content: Buffer.from(subject, 'utf-8')
}
])
},
groupRequestParticipantsList: async(jid: string) => {
const result = await groupQuery(
jid,
'get',
[
{
tag: 'membership_approval_requests',
attrs: {}
}
]
)
groupRequestParticipantsList: async (jid: string) => {
const result = await groupQuery(jid, 'get', [
{
tag: 'membership_approval_requests',
attrs: {}
}
])
const node = getBinaryNodeChild(result, 'membership_approval_requests')
const participants = getBinaryNodeChildren(node, 'membership_approval_request')
return participants.map(v => v.attrs)
},
groupRequestParticipantsUpdate: async(jid: string, participants: string[], action: 'approve' | 'reject') => {
const result = await groupQuery(
jid,
'set',
[{
groupRequestParticipantsUpdate: async (jid: string, participants: string[], action: 'approve' | 'reject') => {
const result = await groupQuery(jid, 'set', [
{
tag: 'membership_requests_action',
attrs: {},
content: [
content: [
{
tag: action,
attrs: { },
attrs: {},
content: participants.map(jid => ({
tag: 'participant',
attrs: { jid }
}))
}
]
}]
)
}
])
const node = getBinaryNodeChild(result, 'membership_requests_action')
const nodeAction = getBinaryNodeChild(node, action)
const participantsAffected = getBinaryNodeChildren(nodeAction, 'participant')
@@ -171,63 +159,49 @@ export const makeGroupsSocket = (config: SocketConfig) => {
return { status: p.attrs.error || '200', jid: p.attrs.jid }
})
},
groupParticipantsUpdate: async(
jid: string,
participants: string[],
action: ParticipantAction
) => {
const result = await groupQuery(
jid,
'set',
[
{
tag: action,
attrs: { },
content: participants.map(jid => ({
tag: 'participant',
attrs: { jid }
}))
}
]
)
groupParticipantsUpdate: async (jid: string, participants: string[], action: ParticipantAction) => {
const result = await groupQuery(jid, 'set', [
{
tag: action,
attrs: {},
content: participants.map(jid => ({
tag: 'participant',
attrs: { jid }
}))
}
])
const node = getBinaryNodeChild(result, action)
const participantsAffected = getBinaryNodeChildren(node, 'participant')
return participantsAffected.map(p => {
return { status: p.attrs.error || '200', jid: p.attrs.jid, content: p }
})
},
groupUpdateDescription: async(jid: string, description?: string) => {
groupUpdateDescription: async (jid: string, description?: string) => {
const metadata = await groupMetadata(jid)
const prev = metadata.descId ?? null
await groupQuery(
jid,
'set',
[
{
tag: 'description',
attrs: {
...(description ? { id: generateMessageIDV2() } : { delete: 'true' }),
...(prev ? { prev } : {})
},
content: description ? [
{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }
] : undefined
}
]
)
await groupQuery(jid, 'set', [
{
tag: 'description',
attrs: {
...(description ? { id: generateMessageIDV2() } : { delete: 'true' }),
...(prev ? { prev } : {})
},
content: description ? [{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }] : undefined
}
])
},
groupInviteCode: async(jid: string) => {
groupInviteCode: async (jid: string) => {
const result = await groupQuery(jid, 'get', [{ tag: 'invite', attrs: {} }])
const inviteNode = getBinaryNodeChild(result, 'invite')
return inviteNode?.attrs.code
},
groupRevokeInvite: async(jid: string) => {
groupRevokeInvite: async (jid: string) => {
const result = await groupQuery(jid, 'set', [{ tag: 'invite', attrs: {} }])
const inviteNode = getBinaryNodeChild(result, 'invite')
return inviteNode?.attrs.code
},
groupAcceptInvite: async(code: string) => {
groupAcceptInvite: async (code: string) => {
const results = await groupQuery('@g.us', 'set', [{ tag: 'invite', attrs: { code } }])
const result = getBinaryNodeChild(results, 'group')
return result?.attrs.jid
@@ -239,8 +213,10 @@ export const makeGroupsSocket = (config: SocketConfig) => {
* @param invitedJid jid of person you invited
* @returns true if successful
*/
groupRevokeInviteV4: async(groupJid: string, invitedJid: string) => {
const result = await groupQuery(groupJid, 'set', [{ tag: 'revoke', attrs: {}, content: [{ tag: 'participant', attrs: { jid: invitedJid } }] }])
groupRevokeInviteV4: async (groupJid: string, invitedJid: string) => {
const result = await groupQuery(groupJid, 'set', [
{ tag: 'revoke', attrs: {}, content: [{ tag: 'participant', attrs: { jid: invitedJid } }] }
])
return !!result
},
@@ -249,87 +225,90 @@ export const makeGroupsSocket = (config: SocketConfig) => {
* @param key the key of the invite message, or optionally only provide the jid of the person who sent the invite
* @param inviteMessage the message to accept
*/
groupAcceptInviteV4: ev.createBufferedFunction(async(key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
key = typeof key === 'string' ? { remoteJid: key } : key
const results = await groupQuery(inviteMessage.groupJid!, 'set', [{
tag: 'accept',
attrs: {
code: inviteMessage.inviteCode!,
expiration: inviteMessage.inviteExpiration!.toString(),
admin: key.remoteJid!
}
}])
// if we have the full message key
// update the invite message to be expired
if(key.id) {
// create new invite message that is expired
inviteMessage = proto.Message.GroupInviteMessage.fromObject(inviteMessage)
inviteMessage.inviteExpiration = 0
inviteMessage.inviteCode = ''
ev.emit('messages.update', [
groupAcceptInviteV4: ev.createBufferedFunction(
async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
key = typeof key === 'string' ? { remoteJid: key } : key
const results = await groupQuery(inviteMessage.groupJid!, 'set', [
{
key,
update: {
message: {
groupInviteMessage: inviteMessage
}
tag: 'accept',
attrs: {
code: inviteMessage.inviteCode!,
expiration: inviteMessage.inviteExpiration!.toString(),
admin: key.remoteJid!
}
}
])
}
// generate the group add message
await upsertMessage(
{
key: {
remoteJid: inviteMessage.groupJid,
id: generateMessageIDV2(sock.user?.id),
fromMe: false,
// if we have the full message key
// update the invite message to be expired
if (key.id) {
// create new invite message that is expired
inviteMessage = proto.Message.GroupInviteMessage.fromObject(inviteMessage)
inviteMessage.inviteExpiration = 0
inviteMessage.inviteCode = ''
ev.emit('messages.update', [
{
key,
update: {
message: {
groupInviteMessage: inviteMessage
}
}
}
])
}
// generate the group add message
await upsertMessage(
{
key: {
remoteJid: inviteMessage.groupJid,
id: generateMessageIDV2(sock.user?.id),
fromMe: false,
participant: key.remoteJid
},
messageStubType: WAMessageStubType.GROUP_PARTICIPANT_ADD,
messageStubParameters: [authState.creds.me!.id],
participant: key.remoteJid,
messageTimestamp: unixTimestampSeconds()
},
messageStubType: WAMessageStubType.GROUP_PARTICIPANT_ADD,
messageStubParameters: [
authState.creds.me!.id
],
participant: key.remoteJid,
messageTimestamp: unixTimestampSeconds()
},
'notify'
)
'notify'
)
return results.attrs.from
}),
groupGetInviteInfo: async(code: string) => {
return results.attrs.from
}
),
groupGetInviteInfo: async (code: string) => {
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
return extractGroupMetadata(results)
},
groupToggleEphemeral: async(jid: string, ephemeralExpiration: number) => {
const content: BinaryNode = ephemeralExpiration ?
{ tag: 'ephemeral', attrs: { expiration: ephemeralExpiration.toString() } } :
{ tag: 'not_ephemeral', attrs: { } }
groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => {
const content: BinaryNode = ephemeralExpiration
? { tag: 'ephemeral', attrs: { expiration: ephemeralExpiration.toString() } }
: { tag: 'not_ephemeral', attrs: {} }
await groupQuery(jid, 'set', [content])
},
groupSettingUpdate: async(jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
await groupQuery(jid, 'set', [ { tag: setting, attrs: { } } ])
groupSettingUpdate: async (jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
await groupQuery(jid, 'set', [{ tag: setting, attrs: {} }])
},
groupMemberAddMode: async(jid: string, mode: 'admin_add' | 'all_member_add') => {
await groupQuery(jid, 'set', [ { tag: 'member_add_mode', attrs: { }, content: mode } ])
groupMemberAddMode: async (jid: string, mode: 'admin_add' | 'all_member_add') => {
await groupQuery(jid, 'set', [{ tag: 'member_add_mode', attrs: {}, content: mode }])
},
groupJoinApprovalMode: async(jid: string, mode: 'on' | 'off') => {
await groupQuery(jid, 'set', [ { tag: 'membership_approval_mode', attrs: { }, content: [ { tag: 'group_join', attrs: { state: mode } } ] } ])
groupJoinApprovalMode: async (jid: string, mode: 'on' | 'off') => {
await groupQuery(jid, 'set', [
{ tag: 'membership_approval_mode', attrs: {}, content: [{ tag: 'group_join', attrs: { state: mode } }] }
])
},
groupFetchAllParticipating
}
}
export const extractGroupMetadata = (result: BinaryNode) => {
const group = getBinaryNodeChild(result, 'group')!
const descChild = getBinaryNodeChild(group, 'description')
let desc: string | undefined
let descId: string | undefined
if(descChild) {
if (descChild) {
desc = getBinaryNodeChildString(descChild, 'body')
descId = descChild.attrs.id
}
@@ -355,14 +334,12 @@ export const extractGroupMetadata = (result: BinaryNode) => {
isCommunityAnnounce: !!getBinaryNodeChild(group, 'default_sub_group'),
joinApprovalMode: !!getBinaryNodeChild(group, 'membership_approval_mode'),
memberAddMode,
participants: getBinaryNodeChildren(group, 'participant').map(
({ attrs }) => {
return {
id: attrs.jid,
admin: (attrs.type || null) as GroupParticipant['admin'],
}
participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
return {
id: attrs.jid,
admin: (attrs.type || null) as GroupParticipant['admin']
}
),
}),
ephemeralDuration: eph ? +eph : undefined
}
return metadata

View File

@@ -3,11 +3,10 @@ import { UserFacingSocketConfig } from '../Types'
import { makeBusinessSocket } from './business'
// export the last socket layer
const makeWASocket = (config: UserFacingSocketConfig) => (
const makeWASocket = (config: UserFacingSocketConfig) =>
makeBusinessSocket({
...DEFAULT_CONNECTION_CONFIG,
...config
})
)
export default makeWASocket
export default makeWASocket

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@ import {
getPlatformId,
makeEventBuffer,
makeNoiseHandler,
promiseTimeout,
promiseTimeout
} from '../Utils'
import {
assertNodeErrorFree,
@@ -61,21 +61,22 @@ export const makeSocket = (config: SocketConfig) => {
defaultQueryTimeoutMs,
transactionOpts,
qrTimeout,
makeSignalRepository,
makeSignalRepository
} = config
if(printQRInTerminal) {
console.warn('⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.')
if (printQRInTerminal) {
console.warn(
'⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.'
)
}
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
if(config.mobile || url.protocol === 'tcp:') {
if (config.mobile || url.protocol === 'tcp:') {
throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut })
}
if(url.protocol === 'wss' && authState?.creds?.routingInfo) {
if (url.protocol === 'wss' && authState?.creds?.routingInfo) {
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'))
}
@@ -110,28 +111,25 @@ export const makeSocket = (config: SocketConfig) => {
const sendPromise = promisify(ws.send)
/** send a raw buffer */
const sendRawMessage = async(data: Uint8Array | Buffer) => {
if(!ws.isOpen) {
const sendRawMessage = async (data: Uint8Array | Buffer) => {
if (!ws.isOpen) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
}
const bytes = noise.encodeFrame(data)
await promiseTimeout<void>(
connectTimeoutMs,
async(resolve, reject) => {
try {
await sendPromise.call(ws, bytes)
resolve()
} catch(error) {
reject(error)
}
await promiseTimeout<void>(connectTimeoutMs, async (resolve, reject) => {
try {
await sendPromise.call(ws, bytes)
resolve()
} catch (error) {
reject(error)
}
)
})
}
/** send a binary node */
const sendNode = (frame: BinaryNode) => {
if(logger.level === 'trace') {
if (logger.level === 'trace') {
logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' })
}
@@ -141,15 +139,12 @@ export const makeSocket = (config: SocketConfig) => {
/** log & process any unexpected errors */
const onUnexpectedError = (err: Error | Boom, msg: string) => {
logger.error(
{ err },
`unexpected error in '${msg}'`
)
logger.error({ err }, `unexpected error in '${msg}'`)
}
/** await the next incoming message */
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
if(!ws.isOpen) {
const awaitNextMessage = async <T>(sendMsg?: Uint8Array) => {
if (!ws.isOpen) {
throw new Boom('Connection Closed', {
statusCode: DisconnectReason.connectionClosed
})
@@ -164,14 +159,13 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('frame', onOpen)
ws.on('close', onClose)
ws.on('error', onClose)
}).finally(() => {
ws.off('frame', onOpen)
ws.off('close', onClose)
ws.off('error', onClose)
})
.finally(() => {
ws.off('frame', onOpen)
ws.off('close', onClose)
ws.off('error', onClose)
})
if(sendMsg) {
if (sendMsg) {
sendRawMessage(sendMsg).catch(onClose!)
}
@@ -183,22 +177,20 @@ export const makeSocket = (config: SocketConfig) => {
* @param msgId the message tag to await
* @param timeoutMs timeout after which the promise will reject
*/
const waitForMessage = async<T>(msgId: string, timeoutMs = defaultQueryTimeoutMs) => {
const waitForMessage = async <T>(msgId: string, timeoutMs = defaultQueryTimeoutMs) => {
let onRecv: (json) => void
let onErr: (err) => void
try {
const result = await promiseTimeout<T>(timeoutMs,
(resolve, reject) => {
onRecv = resolve
onErr = err => {
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
}
const result = await promiseTimeout<T>(timeoutMs, (resolve, reject) => {
onRecv = resolve
onErr = err => {
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
}
ws.on(`TAG:${msgId}`, onRecv)
ws.on('close', onErr) // if the socket closes, you'll never receive the message
ws.off('error', onErr)
},
)
ws.on(`TAG:${msgId}`, onRecv)
ws.on('close', onErr) // if the socket closes, you'll never receive the message
ws.off('error', onErr)
})
return result as any
} finally {
@@ -209,19 +201,16 @@ export const makeSocket = (config: SocketConfig) => {
}
/** send a query, and wait for its response. auto-generates message ID if not provided */
const query = async(node: BinaryNode, timeoutMs?: number) => {
if(!node.attrs.id) {
const query = async (node: BinaryNode, timeoutMs?: number) => {
if (!node.attrs.id) {
node.attrs.id = generateMessageTag()
}
const msgId = node.attrs.id
const [result] = await Promise.all([
waitForMessage(msgId, timeoutMs),
sendNode(node)
])
const [result] = await Promise.all([waitForMessage(msgId, timeoutMs), sendNode(node)])
if('tag' in result) {
if ('tag' in result) {
assertNodeErrorFree(result)
}
@@ -229,7 +218,7 @@ export const makeSocket = (config: SocketConfig) => {
}
/** connection handshake */
const validateConnection = async() => {
const validateConnection = async () => {
let helloMsg: proto.IHandshakeMessage = {
clientHello: { ephemeral: ephemeralKeyPair.public }
}
@@ -247,7 +236,7 @@ export const makeSocket = (config: SocketConfig) => {
const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
let node: proto.IClientPayload
if(!creds.me) {
if (!creds.me) {
node = generateRegistrationNode(creds, config)
logger.info({ node }, 'not logged in, attempting registration...')
} else {
@@ -255,22 +244,20 @@ export const makeSocket = (config: SocketConfig) => {
logger.info({ node }, 'logging in...')
}
const payloadEnc = noise.encrypt(
proto.ClientPayload.encode(node).finish()
)
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
await sendRawMessage(
proto.HandshakeMessage.encode({
clientFinish: {
static: keyEnc,
payload: payloadEnc,
},
payload: payloadEnc
}
}).finish()
)
noise.finishInit()
startKeepAliveRequest()
}
const getAvailablePreKeysOnServer = async() => {
const getAvailablePreKeysOnServer = async () => {
const result = await query({
tag: 'iq',
attrs: {
@@ -279,33 +266,29 @@ export const makeSocket = (config: SocketConfig) => {
type: 'get',
to: S_WHATSAPP_NET
},
content: [
{ tag: 'count', attrs: {} }
]
content: [{ tag: 'count', attrs: {} }]
})
const countChild = getBinaryNodeChild(result, 'count')
return +countChild!.attrs.value
}
/** generates and uploads a set of pre-keys to the server */
const uploadPreKeys = async(count = INITIAL_PREKEY_COUNT) => {
await keys.transaction(
async() => {
logger.info({ count }, 'uploading pre-keys')
const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
const uploadPreKeys = async (count = INITIAL_PREKEY_COUNT) => {
await keys.transaction(async () => {
logger.info({ count }, 'uploading pre-keys')
const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
await query(node)
ev.emit('creds.update', update)
await query(node)
ev.emit('creds.update', update)
logger.info({ count }, 'uploaded pre-keys')
}
)
logger.info({ count }, 'uploaded pre-keys')
})
}
const uploadPreKeysToServerIfRequired = async() => {
const uploadPreKeysToServerIfRequired = async () => {
const preKeyCount = await getAvailablePreKeysOnServer()
logger.info(`${preKeyCount} pre-keys found on server`)
if(preKeyCount <= MIN_PREKEY_COUNT) {
if (preKeyCount <= MIN_PREKEY_COUNT) {
await uploadPreKeys()
}
}
@@ -319,10 +302,10 @@ export const makeSocket = (config: SocketConfig) => {
anyTriggered = ws.emit('frame', frame)
// if it's a binary node
if(!(frame instanceof Uint8Array)) {
if (!(frame instanceof Uint8Array)) {
const msgId = frame.attrs.id
if(logger.level === 'trace') {
if (logger.level === 'trace') {
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' })
}
@@ -333,7 +316,7 @@ export const makeSocket = (config: SocketConfig) => {
const l1 = frame.attrs || {}
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ''
for(const key of Object.keys(l1)) {
for (const key of Object.keys(l1)) {
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered
@@ -342,7 +325,7 @@ export const makeSocket = (config: SocketConfig) => {
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered
if(!anyTriggered && logger.level === 'debug') {
if (!anyTriggered && logger.level === 'debug') {
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv')
}
}
@@ -350,16 +333,13 @@ export const makeSocket = (config: SocketConfig) => {
}
const end = (error: Error | undefined) => {
if(closed) {
if (closed) {
logger.trace({ trace: error?.stack }, 'connection already closed')
return
}
closed = true
logger.info(
{ trace: error?.stack },
error ? 'connection errored' : 'connection closed'
)
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
clearInterval(keepAliveReq)
clearTimeout(qrTimer)
@@ -369,10 +349,10 @@ export const makeSocket = (config: SocketConfig) => {
ws.removeAllListeners('open')
ws.removeAllListeners('message')
if(!ws.isClosed && !ws.isClosing) {
if (!ws.isClosed && !ws.isClosing) {
try {
ws.close()
} catch{ }
} catch {}
}
ev.emit('connection.update', {
@@ -385,12 +365,12 @@ export const makeSocket = (config: SocketConfig) => {
ev.removeAllListeners('connection.update')
}
const waitForSocketOpen = async() => {
if(ws.isOpen) {
const waitForSocketOpen = async () => {
if (ws.isOpen) {
return
}
if(ws.isClosed || ws.isClosing) {
if (ws.isClosed || ws.isClosing) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
}
@@ -402,17 +382,16 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('open', onOpen)
ws.on('close', onClose)
ws.on('error', onClose)
}).finally(() => {
ws.off('open', onOpen)
ws.off('close', onClose)
ws.off('error', onClose)
})
.finally(() => {
ws.off('open', onOpen)
ws.off('close', onClose)
ws.off('error', onClose)
})
}
const startKeepAliveRequest = () => (
keepAliveReq = setInterval(() => {
if(!lastDateRecv) {
const startKeepAliveRequest = () =>
(keepAliveReq = setInterval(() => {
if (!lastDateRecv) {
lastDateRecv = new Date()
}
@@ -421,49 +400,42 @@ export const makeSocket = (config: SocketConfig) => {
check if it's been a suspicious amount of time since the server responded with our last seen
it could be that the network is down
*/
if(diff > keepAliveIntervalMs + 5000) {
if (diff > keepAliveIntervalMs + 5000) {
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
} else if(ws.isOpen) {
} else if (ws.isOpen) {
// if its all good, send a keep alive request
query(
{
tag: 'iq',
attrs: {
id: generateMessageTag(),
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'w:p',
},
content: [{ tag: 'ping', attrs: {} }]
}
)
.catch(err => {
logger.error({ trace: err.stack }, 'error in sending keep alive')
})
query({
tag: 'iq',
attrs: {
id: generateMessageTag(),
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'w:p'
},
content: [{ tag: 'ping', attrs: {} }]
}).catch(err => {
logger.error({ trace: err.stack }, 'error in sending keep alive')
})
} else {
logger.warn('keep alive called when WS not open')
}
}, keepAliveIntervalMs)
)
}, keepAliveIntervalMs))
/** i have no idea why this exists. pls enlighten me */
const sendPassiveIq = (tag: 'passive' | 'active') => (
const sendPassiveIq = (tag: 'passive' | 'active') =>
query({
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
xmlns: 'passive',
type: 'set',
type: 'set'
},
content: [
{ tag, attrs: {} }
]
content: [{ tag, attrs: {} }]
})
)
/** logout & invalidate connection */
const logout = async(msg?: string) => {
const logout = async (msg?: string) => {
const jid = authState.creds.me?.id
if(jid) {
if (jid) {
await sendNode({
tag: 'iq',
attrs: {
@@ -487,7 +459,7 @@ export const makeSocket = (config: SocketConfig) => {
end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
}
const requestPairingCode = async(phoneNumber: string): Promise<string> => {
const requestPairingCode = async (phoneNumber: string): Promise<string> => {
authState.creds.pairingCode = bytesToCrockford(randomBytes(5))
authState.creds.me = {
id: jidEncode(phoneNumber, 's.whatsapp.net'),
@@ -572,10 +544,10 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('message', onMessageReceived)
ws.on('open', async() => {
ws.on('open', async () => {
try {
await validateConnection()
} catch(err) {
} catch (err) {
logger.error({ err }, 'error in validating connection')
end(err)
}
@@ -583,15 +555,17 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('error', mapWebSocketError(end))
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
// the server terminated the connection
ws.on('CB:xmlstreamend', () => end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })))
ws.on('CB:xmlstreamend', () =>
end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
)
// QR gen
ws.on('CB:iq,type:set,pair-device', async(stanza: BinaryNode) => {
ws.on('CB:iq,type:set,pair-device', async (stanza: BinaryNode) => {
const iq: BinaryNode = {
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
type: 'result',
id: stanza.attrs.id,
id: stanza.attrs.id
}
}
await sendNode(iq)
@@ -604,12 +578,12 @@ export const makeSocket = (config: SocketConfig) => {
let qrMs = qrTimeout || 60_000 // time to let a QR live
const genPairQR = () => {
if(!ws.isOpen) {
if (!ws.isOpen) {
return
}
const refNode = refNodes.shift()
if(!refNode) {
if (!refNode) {
end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }))
return
}
@@ -627,7 +601,7 @@ export const makeSocket = (config: SocketConfig) => {
})
// device paired for the first time
// if device pairs successfully, the server asks to restart the connection
ws.on('CB:iq,,pair-success', async(stanza: BinaryNode) => {
ws.on('CB:iq,,pair-success', async (stanza: BinaryNode) => {
logger.debug('pair success recv')
try {
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
@@ -641,13 +615,13 @@ export const makeSocket = (config: SocketConfig) => {
ev.emit('connection.update', { isNewLogin: true, qr: undefined })
await sendNode(reply)
} catch(error) {
} catch (error) {
logger.info({ trace: error.stack }, 'error in pairing')
end(error)
}
})
// login complete
ws.on('CB:success', async(node: BinaryNode) => {
ws.on('CB:success', async (node: BinaryNode) => {
await uploadPreKeysToServerIfRequired()
await sendPassiveIq('active')
@@ -677,7 +651,7 @@ export const makeSocket = (config: SocketConfig) => {
})
ws.on('CB:ib,,offline_preview', (node: BinaryNode) => {
logger.info('offline preview received', JSON.stringify(node))
logger.info('offline preview received', JSON.stringify(node))
sendNode({
tag: 'ib',
attrs: {},
@@ -688,7 +662,7 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('CB:ib,,edge_routing', (node: BinaryNode) => {
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing')
const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info')
if(routingInfo?.content) {
if (routingInfo?.content) {
authState.creds.routingInfo = Buffer.from(routingInfo?.content as Uint8Array)
ev.emit('creds.update', authState.creds)
}
@@ -696,7 +670,7 @@ export const makeSocket = (config: SocketConfig) => {
let didStartBuffer = false
process.nextTick(() => {
if(creds.me?.id) {
if (creds.me?.id) {
// start buffering important events
// if we're logged in
ev.buffer()
@@ -712,7 +686,7 @@ export const makeSocket = (config: SocketConfig) => {
const offlineNotifs = +(child?.attrs.count || 0)
logger.info(`handled ${offlineNotifs} offline messages/notifications`)
if(didStartBuffer) {
if (didStartBuffer) {
ev.flush()
logger.trace('flushed events for initial buffer')
}
@@ -724,21 +698,19 @@ export const makeSocket = (config: SocketConfig) => {
ev.on('creds.update', update => {
const name = update.me?.name
// if name has just been received
if(creds.me?.name !== name) {
if (creds.me?.name !== name) {
logger.debug({ name }, 'updated pushName')
sendNode({
tag: 'presence',
attrs: { name: name! }
}).catch(err => {
logger.warn({ trace: err.stack }, 'error in sending presence update on name change')
})
.catch(err => {
logger.warn({ trace: err.stack }, 'error in sending presence update on name change')
})
}
Object.assign(creds, update)
})
return {
type: 'md' as 'md',
ws,
@@ -762,7 +734,7 @@ export const makeSocket = (config: SocketConfig) => {
requestPairingCode,
/** Waits for the connection to WA to reach a state */
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
sendWAMBuffer,
sendWAMBuffer
}
}
@@ -772,11 +744,6 @@ export const makeSocket = (config: SocketConfig) => {
* */
function mapWebSocketError(handler: (err: Error) => void) {
return (error: Error) => {
handler(
new Boom(
`WebSocket Error (${error?.message})`,
{ statusCode: getCodeFromWSError(error), data: error }
)
)
handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }))
}
}

View File

@@ -7,13 +7,10 @@ import { makeSocket } from './socket'
export const makeUSyncSocket = (config: SocketConfig) => {
const sock = makeSocket(config)
const {
generateMessageTag,
query,
} = sock
const { generateMessageTag, query } = sock
const executeUSyncQuery = async(usyncQuery: USyncQuery) => {
if(usyncQuery.protocols.length === 0) {
const executeUSyncQuery = async (usyncQuery: USyncQuery) => {
if (usyncQuery.protocols.length === 0) {
throw new Boom('USyncQuery must have at least one protocol')
}
@@ -21,15 +18,13 @@ export const makeUSyncSocket = (config: SocketConfig) => {
// variable below has only validated users
const validUsers = usyncQuery.users
const userNodes = validUsers.map((user) => {
const userNodes = validUsers.map(user => {
return {
tag: 'user',
attrs: {
jid: !user.phone ? user.id : undefined,
jid: !user.phone ? user.id : undefined
},
content: usyncQuery.protocols
.map((a) => a.getUserElement(user))
.filter(a => a !== null)
content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
} as BinaryNode
})
@@ -42,14 +37,14 @@ export const makeUSyncSocket = (config: SocketConfig) => {
const queryNode: BinaryNode = {
tag: 'query',
attrs: {},
content: usyncQuery.protocols.map((a) => a.getQueryElement())
content: usyncQuery.protocols.map(a => a.getQueryElement())
}
const iq = {
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'usync',
xmlns: 'usync'
},
content: [
{
@@ -59,14 +54,11 @@ export const makeUSyncSocket = (config: SocketConfig) => {
mode: usyncQuery.mode,
sid: generateMessageTag(),
last: 'true',
index: '0',
index: '0'
},
content: [
queryNode,
listNode
]
content: [queryNode, listNode]
}
],
]
}
const result = await query(iq)
@@ -76,6 +68,6 @@ export const makeUSyncSocket = (config: SocketConfig) => {
return {
...sock,
executeUSyncQuery,
executeUSyncQuery
}
}
}