chore: add linting

This commit is contained in:
Adhiraj Singh
2022-01-19 15:54:02 +05:30
parent f7f86e69d6
commit 8f11f0be76
49 changed files with 5800 additions and 4314 deletions

View File

@@ -1,8 +1,8 @@
import { Boom } from '@hapi/boom'
import EventEmitter from "events"
import { LegacyBaileysEventEmitter, LegacySocketConfig, CurveKeyPair, WAInitResponse, ConnectionState, DisconnectReason } from "../Types"
import { newLegacyAuthCreds, bindWaitForConnectionUpdate, computeChallengeResponse, validateNewConnection, Curve, printQRIfNecessaryListener } from "../Utils"
import { makeSocket } from "./socket"
import EventEmitter from 'events'
import { ConnectionState, CurveKeyPair, DisconnectReason, LegacyBaileysEventEmitter, LegacySocketConfig, WAInitResponse } from '../Types'
import { bindWaitForConnectionUpdate, computeChallengeResponse, Curve, newLegacyAuthCreds, printQRIfNecessaryListener, validateNewConnection } from '../Utils'
import { makeSocket } from './socket'
const makeAuthSocket = (config: LegacySocketConfig) => {
const {
@@ -60,14 +60,15 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
* If connected, invalidates the credentials with the server
*/
const logout = async() => {
if(state.connection === 'open') {
await socket.sendNode({
if(state.connection === 'open') {
await socket.sendNode({
json: ['admin', 'Conn', 'disconnect'],
tag: 'goodbye'
})
}
}
// will call state update to close connection
socket?.end(
socket?.end(
new Boom('Logged Out', { statusCode: DisconnectReason.loggedOut })
)
}
@@ -86,13 +87,15 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
const qr = [ref, publicKey, authInfo.clientID].join(',')
updateState({ qr })
initTimeout = setTimeout(async () => {
if(state.connection !== 'connecting') return
initTimeout = setTimeout(async() => {
if(state.connection !== 'connecting') {
return
}
logger.debug('regenerating QR')
try {
// request new QR
const {ref: newRef, ttl: newTTL} = await socket.query({
const { ref: newRef, ttl: newTTL } = await socket.query({
json: ['admin', 'Conn', 'reref'],
expect200: true,
longTag: true,
@@ -100,94 +103,104 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
})
ttl = newTTL
ref = newRef
} catch (error) {
logger.error({ error }, `error in QR gen`)
if (error.output?.statusCode === 429) { // too many QR requests
} catch(error) {
logger.error({ error }, 'error in QR gen')
if(error.output?.statusCode === 429) { // too many QR requests
socket.end(error)
return
}
}
qrGens += 1
qrLoop(ttl)
}, ttl || 20_000) // default is 20s, on the off-chance ttl is not present
}
qrLoop(ttl)
}
const onOpen = async() => {
const canDoLogin = canLogin()
const initQuery = (async () => {
const {ref, ttl} = await socket.query({
json: ['admin', 'init', version, browser, authInfo.clientID, true],
expect200: true,
longTag: true,
requiresPhoneConnection: false
}) as WAInitResponse
const initQuery = (async() => {
const { ref, ttl } = await socket.query({
json: ['admin', 'init', version, browser, authInfo.clientID, true],
expect200: true,
longTag: true,
requiresPhoneConnection: false
}) as WAInitResponse
if (!canDoLogin) {
generateKeysForAuth(ref, ttl)
}
})();
let loginTag: string
if(canDoLogin) {
if(!canDoLogin) {
generateKeysForAuth(ref, ttl)
}
})()
let loginTag: string
if(canDoLogin) {
updateEncKeys()
// if we have the info to restore a closed session
const json = [
// if we have the info to restore a closed session
const json = [
'admin',
'login',
authInfo.clientToken,
authInfo.serverToken,
authInfo.clientID,
'login',
authInfo.clientToken,
authInfo.serverToken,
authInfo.clientID,
'takeover'
]
loginTag = socket.generateMessageTag(true)
// send login every 10s
const sendLoginReq = () => {
if(state.connection === 'open') {
logger.warn('Received login timeout req when state=open, ignoring...')
return
}
logger.info('sending login request')
socket.sendNode({
]
loginTag = socket.generateMessageTag(true)
// send login every 10s
const sendLoginReq = () => {
if(state.connection === 'open') {
logger.warn('Received login timeout req when state=open, ignoring...')
return
}
logger.info('sending login request')
socket.sendNode({
json,
tag: loginTag
})
initTimeout = setTimeout(sendLoginReq, 10_000)
}
sendLoginReq()
}
await initQuery
initTimeout = setTimeout(sendLoginReq, 10_000)
}
// wait for response with tag "s1"
let response = await Promise.race(
[
sendLoginReq()
}
await initQuery
// wait for response with tag "s1"
let response = await Promise.race(
[
socket.waitForMessage('s1', false, undefined).promise,
...(loginTag ? [socket.waitForMessage(loginTag, false, connectTimeoutMs).promise] : [])
]
)
initTimeout && clearTimeout(initTimeout)
initTimeout = undefined
)
initTimeout && clearTimeout(initTimeout)
initTimeout = undefined
if(response.status && response.status !== 200) {
throw new Boom(`Unexpected error in login`, { data: response, statusCode: response.status })
}
// if its a challenge request (we get it when logging in)
if(response[1]?.challenge) {
if(response.status && response.status !== 200) {
throw new Boom('Unexpected error in login', { data: response, statusCode: response.status })
}
// if its a challenge request (we get it when logging in)
if(response[1]?.challenge) {
const json = computeChallengeResponse(response[1].challenge, authInfo)
logger.info('resolving login challenge')
await socket.query({ json, expect200: true, timeoutMs: connectTimeoutMs })
response = await socket.waitForMessage('s2', true).promise
}
}
if(!response || !response[1]) {
throw new Boom('Received unexpected login response', { data: response })
}
if(response[1].type === 'upgrade_md_prod') {
throw new Boom('Require multi-device edition', { statusCode: DisconnectReason.multideviceMismatch })
}
// validate the new connection
const {user, auth} = validateNewConnection(response[1], authInfo, curveKeys)// validate the connection
const isNewLogin = user.id !== state.legacy!.user?.id
// validate the new connection
const { user, auth } = validateNewConnection(response[1], authInfo, curveKeys)// validate the connection
const isNewLogin = user.id !== state.legacy!.user?.id
Object.assign(authInfo, auth)
updateEncKeys()
@@ -206,6 +219,7 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
qr: undefined
})
}
ws.once('open', async() => {
try {
await onOpen()
@@ -219,10 +233,10 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
}
process.nextTick(() => {
ev.emit('connection.update', {
ev.emit('connection.update', {
...state
})
})
})
return {
...socket,
@@ -231,8 +245,9 @@ const makeAuthSocket = (config: LegacySocketConfig) => {
ev,
canLogin,
logout,
/** Waits for the connection to WA to reach a state */
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev)
/** Waits for the connection to WA to reach a state */
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev)
}
}
export default makeAuthSocket

View File

@@ -1,7 +1,7 @@
import { BinaryNode, jidNormalizedUser } from "../WABinary";
import { Chat, Contact, WAPresence, PresenceData, LegacySocketConfig, WAFlag, WAMetric, WABusinessProfile, ChatModification, WAMessageKey, WAMessageUpdate, BaileysEventMap } from "../Types";
import { debouncedTimeout, unixTimestampSeconds } from "../Utils/generics";
import makeAuthSocket from "./auth";
import { BaileysEventMap, Chat, ChatModification, Contact, LegacySocketConfig, PresenceData, WABusinessProfile, WAFlag, WAMessageKey, WAMessageUpdate, WAMetric, WAPresence } from '../Types'
import { debouncedTimeout, unixTimestampSeconds } from '../Utils/generics'
import { BinaryNode, jidNormalizedUser } from '../WABinary'
import makeAuthSocket from './auth'
const makeChatsSocket = (config: LegacySocketConfig) => {
const { logger } = config
@@ -22,7 +22,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
sendNode({
json: {
tag: 'query',
attrs: {type: 'chat', epoch: epoch.toString()}
attrs: { type: 'chat', epoch: epoch.toString() }
},
binaryTag: [ WAMetric.queryChat, WAFlag.ignore ]
})
@@ -43,62 +43,64 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
const updateType = attributes.type
const jid = jidNormalizedUser(attributes?.jid)
switch(updateType) {
case 'delete':
ev.emit('chats.delete', [jid])
break
case 'clear':
if(node.content) {
const ids = (node.content as BinaryNode[]).map(
({ attrs }) => attrs.index
)
ev.emit('messages.delete', { keys: ids.map(id => ({ id, remoteJid: jid })) })
} else {
ev.emit('messages.delete', { jid, all: true })
}
break
case 'archive':
ev.emit('chats.update', [ { id: jid, archive: true } ])
break
case 'unarchive':
ev.emit('chats.update', [ { id: jid, archive: false } ])
break
case 'pin':
ev.emit('chats.update', [ { id: jid, pin: attributes.pin ? +attributes.pin : null } ])
break
case 'star':
case 'unstar':
const starred = updateType === 'star'
const updates: WAMessageUpdate[] = (node.content as BinaryNode[]).map(
({ attrs }) => ({
key: {
remoteJid: jid,
id: attrs.index,
fromMe: attrs.owner === 'true'
},
update: { starred }
})
switch (updateType) {
case 'delete':
ev.emit('chats.delete', [jid])
break
case 'clear':
if(node.content) {
const ids = (node.content as BinaryNode[]).map(
({ attrs }) => attrs.index
)
ev.emit('messages.update', updates)
break
case 'mute':
if(attributes.mute === '0') {
ev.emit('chats.update', [{ id: jid, mute: null }])
} else {
ev.emit('chats.update', [{ id: jid, mute: +attributes.mute }])
}
break
default:
logger.warn({ node }, `received unrecognized chat update`)
break
ev.emit('messages.delete', { keys: ids.map(id => ({ id, remoteJid: jid })) })
} else {
ev.emit('messages.delete', { jid, all: true })
}
break
case 'archive':
ev.emit('chats.update', [ { id: jid, archive: true } ])
break
case 'unarchive':
ev.emit('chats.update', [ { id: jid, archive: false } ])
break
case 'pin':
ev.emit('chats.update', [ { id: jid, pin: attributes.pin ? +attributes.pin : null } ])
break
case 'star':
case 'unstar':
const starred = updateType === 'star'
const updates: WAMessageUpdate[] = (node.content as BinaryNode[]).map(
({ attrs }) => ({
key: {
remoteJid: jid,
id: attrs.index,
fromMe: attrs.owner === 'true'
},
update: { starred }
})
)
ev.emit('messages.update', updates)
break
case 'mute':
if(attributes.mute === '0') {
ev.emit('chats.update', [{ id: jid, mute: null }])
} else {
ev.emit('chats.update', [{ id: jid, mute: +attributes.mute }])
}
break
default:
logger.warn({ node }, 'received unrecognized chat update')
break
}
}
const applyingPresenceUpdate = (update: BinaryNode['attrs']): BaileysEventMap<any>['presence.update'] => {
const id = jidNormalizedUser(update.id)
const participant = jidNormalizedUser(update.participant || update.id)
const participant = jidNormalizedUser(update.participant || update.id)
const presence: PresenceData = {
const presence: PresenceData = {
lastSeen: update.t ? +update.t : undefined,
lastKnownPresence: update.type as WAPresence
}
@@ -126,27 +128,30 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
}
ev.on('connection.update', async({ connection }) => {
if(connection !== 'open') return
if(connection !== 'open') {
return
}
try {
await Promise.all([
sendNode({
json: { tag: 'query', attrs: {type: 'contacts', epoch: '1'} },
json: { tag: 'query', attrs: { type: 'contacts', epoch: '1' } },
binaryTag: [ WAMetric.queryContact, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: {type: 'status', epoch: '1'} },
json: { tag: 'query', attrs: { type: 'status', epoch: '1' } },
binaryTag: [ WAMetric.queryStatus, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: {type: 'quick_reply', epoch: '1'} },
json: { tag: 'query', attrs: { type: 'quick_reply', epoch: '1' } },
binaryTag: [ WAMetric.queryQuickReply, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: {type: 'label', epoch: '1'} },
json: { tag: 'query', attrs: { type: 'label', epoch: '1' } },
binaryTag: [ WAMetric.queryLabel, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: {type: 'emoji', epoch: '1'} },
json: { tag: 'query', attrs: { type: 'emoji', epoch: '1' } },
binaryTag: [ WAMetric.queryEmoji, WAFlag.ignore ]
}),
sendNode({
@@ -154,7 +159,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
tag: 'action',
attrs: { type: 'set', epoch: '1' },
content: [
{ tag: 'presence', attrs: {type: 'available'} }
{ tag: 'presence', attrs: { type: 'available' } }
]
},
binaryTag: [ WAMetric.presence, WAFlag.available ]
@@ -167,7 +172,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
logger.error(`error in sending init queries: ${error}`)
}
})
socketEvents.on('CB:response,type:chat', async ({ content: data }: BinaryNode) => {
socketEvents.on('CB:response,type:chat', async({ content: data }: BinaryNode) => {
chatsDebounceTimeout.cancel()
if(Array.isArray(data)) {
const contacts: Contact[] = []
@@ -176,6 +181,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
if(attrs.name) {
contacts.push({ id, name: attrs.name })
}
return {
id: jidNormalizedUser(attrs.jid),
conversationTimestamp: attrs.t ? +attrs.t : undefined,
@@ -196,7 +202,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
}
})
// got all contacts from phone
socketEvents.on('CB:response,type:contacts', async ({ content: data }: BinaryNode) => {
socketEvents.on('CB:response,type:contacts', async({ content: data }: BinaryNode) => {
if(Array.isArray(data)) {
const contacts = data.map(({ attrs }): Contact => {
return {
@@ -225,15 +231,18 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
}
})
// read updates
socketEvents.on ('CB:action,,read', async ({ content }: BinaryNode) => {
socketEvents.on ('CB:action,,read', async({ content }: BinaryNode) => {
if(Array.isArray(content)) {
const { attrs } = content[0]
const update: Partial<Chat> = {
id: jidNormalizedUser(attrs.jid)
}
if(attrs.type === 'false') update.unreadCount = -1
else update.unreadCount = 0
if(attrs.type === 'false') {
update.unreadCount = -1
} else {
update.unreadCount = 0
}
ev.emit('chats.update', [update])
}
@@ -295,7 +304,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
* @param jid the ID of the person/group you are modifiying
*/
chatModify: async(modification: ChatModification, jid: string, chatInfo: Pick<Chat, 'mute' | 'pin'>, timestampNow?: number) => {
let chatAttrs: BinaryNode['attrs'] = { jid: jid }
const chatAttrs: BinaryNode['attrs'] = { jid: jid }
let data: BinaryNode[] | undefined = undefined
timestampNow = timestampNow || unixTimestampSeconds()
@@ -356,6 +365,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
// apply it and emit events
executeChatModification(node)
}
return response
},
/**
@@ -381,7 +391,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
* @param jid the ID of the person/group who you are updating
* @param type your presence
*/
sendPresenceUpdate: ( type: WAPresence, jid: string | undefined) => (
sendPresenceUpdate: (type: WAPresence, jid: string | undefined) => (
sendNode({
binaryTag: [WAMetric.presence, WAFlag[type]], // weird stuff WA does
json: {
@@ -400,7 +410,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
* Request updates on the presence of a user
* this returns nothing, you'll receive updates in chats.update event
* */
presenceSubscribe: async (jid: string) => (
presenceSubscribe: async(jid: string) => (
sendNode({ json: ['action', 'presence', 'subscribe', jid] })
),
/** Query the status of the person (see groupMetadata() for groups) */
@@ -423,11 +433,12 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
},
/** Updates business profile. */
updateBusinessProfile: async(profile: WABusinessProfile) => {
if (profile.business_hours?.config) {
if(profile.business_hours?.config) {
profile.business_hours.business_config = profile.business_hours.config
delete profile.business_hours.config
}
const json = ['action', "editBusinessProfile", {...profile, v: 2}]
const json = ['action', 'editBusinessProfile', { ...profile, v: 2 }]
await query({ json, expect200: true, requiresPhoneConnection: true })
},
updateProfileName: async(name: string) => {
@@ -447,6 +458,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
} })
ev.emit('contacts.update', [{ id: user.id, name }])
}
return response
},
/**
@@ -454,7 +466,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
* @param jid
* @param img
*/
async updateProfilePicture (jid: string, img: Buffer) {
async updateProfilePicture(jid: string, img: Buffer) {
jid = jidNormalizedUser (jid)
const data = { img: Buffer.from([]), preview: Buffer.from([]) } //await generateProfilePicture(img) TODO
const tag = this.generateMessageTag ()
@@ -480,6 +492,7 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
}
})
}
ev.emit('contacts.update', [ { id: jid, imgUrl: eurl } ])
}
},
@@ -513,8 +526,8 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
}]
} = await query({
json: [
"query", "businessProfile",
[ { "wid": jid.replace('@s.whatsapp.net', '@c.us') } ],
'query', 'businessProfile',
[ { 'wid': jid.replace('@s.whatsapp.net', '@c.us') } ],
84
],
expect200: true,
@@ -528,4 +541,5 @@ const makeChatsSocket = (config: LegacySocketConfig) => {
}
}
}
export default makeChatsSocket

View File

@@ -1,7 +1,7 @@
import { BinaryNode, jidNormalizedUser } from "../WABinary";
import { LegacySocketConfig, GroupModificationResponse, ParticipantAction, GroupMetadata, WAFlag, WAMetric, WAGroupCreateResponse, GroupParticipant } from "../Types";
import { generateMessageID, unixTimestampSeconds } from "../Utils/generics";
import makeMessagesSocket from "./messages";
import { GroupMetadata, GroupModificationResponse, GroupParticipant, LegacySocketConfig, ParticipantAction, WAFlag, WAGroupCreateResponse, WAMetric } from '../Types'
import { generateMessageID, unixTimestampSeconds } from '../Utils/generics'
import { BinaryNode, jidNormalizedUser } from '../WABinary'
import makeMessagesSocket from './messages'
const makeGroupsSocket = (config: LegacySocketConfig) => {
const { logger } = config
@@ -17,9 +17,9 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
} = sock
/** Generic function for group queries */
const groupQuery = async(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: BinaryNode[]) => {
const tag = generateMessageTag()
const result = await setQuery ([
const groupQuery = async(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: BinaryNode[]) => {
const tag = generateMessageTag()
const result = await setQuery ([
{
tag: 'group',
attrs: {
@@ -36,12 +36,12 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
additionalNodes
}
], [WAMetric.group, 136], tag)
return result
}
return result
}
/** Get the metadata of the group from WA */
const groupMetadataFull = async (jid: string) => {
const metadata = await query({
/** Get the metadata of the group from WA */
const groupMetadataFull = async(jid: string) => {
const metadata = await query({
json: ['query', 'GroupMetadata', jid],
expect200: true
})
@@ -62,13 +62,14 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
}
return meta
}
/** Get the metadata (works after you've left the group also) */
const groupMetadataMinimal = async (jid: string) => {
const { attrs, content }:BinaryNode = await query({
}
/** Get the metadata (works after you've left the group also) */
const groupMetadataMinimal = async(jid: string) => {
const { attrs, content }:BinaryNode = await query({
json: {
tag: 'query',
attrs: {type: 'group', jid: jid, epoch: currentEpoch().toString()}
attrs: { type: 'group', jid: jid, epoch: currentEpoch().toString() }
},
binaryTag: [WAMetric.group, WAFlag.ignore],
expect200: true
@@ -89,16 +90,17 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
}
}
}
const meta: GroupMetadata = {
id: jid,
owner: attrs?.creator,
creation: +attrs?.create,
subject: null,
desc,
participants
}
const meta: GroupMetadata = {
id: jid,
owner: attrs?.creator,
creation: +attrs?.create,
subject: null,
desc,
participants
}
return meta
}
}
socketEvents.on('CB:Chat,cmd:action', (json: BinaryNode) => {
/*const data = json[1].data
@@ -129,8 +131,11 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
groupMetadata: async(jid: string, minimal: boolean) => {
let result: GroupMetadata
if(minimal) result = await groupMetadataMinimal(jid)
else result = await groupMetadataFull(jid)
if(minimal) {
result = await groupMetadataMinimal(jid)
} else {
result = await groupMetadataFull(jid)
}
return result
},
@@ -139,13 +144,13 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
* @param title like, the title of the group
* @param participants people to include in the group
*/
groupCreate: async (title: string, participants: string[]) => {
groupCreate: async(title: string, participants: string[]) => {
const response = await groupQuery('create', null, title, participants) as WAGroupCreateResponse
const gid = response.gid
let metadata: GroupMetadata
try {
metadata = await groupMetadataFull(gid)
} catch (error) {
} catch(error) {
logger.warn (`error in group creation: ${error}, switching gid & checking`)
// if metadata is not available
const comps = gid.replace ('@g.us', '').split ('-')
@@ -154,6 +159,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
metadata = await groupMetadataFull(gid)
logger.warn (`group ID switched from ${gid} to ${response.gid}`)
}
ev.emit('chats.upsert', [
{
id: response.gid!,
@@ -168,7 +174,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
* Leave a group
* @param jid the ID of the group
*/
groupLeave: async (id: string) => {
groupLeave: async(id: string) => {
await groupQuery('leave', id)
ev.emit('chats.update', [ { id, readOnly: true } ])
},
@@ -177,7 +183,7 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
* @param {string} jid the ID of the group
* @param {string} title the new title of the group
*/
groupUpdateSubject: async (id: string, title: string) => {
groupUpdateSubject: async(id: string, title: string) => {
await groupQuery('subject', id, title)
ev.emit('chats.update', [ { id, name: title } ])
ev.emit('contacts.update', [ { id, name: title } ])
@@ -188,11 +194,11 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
* @param {string} jid the ID of the group
* @param {string} title the new title of the group
*/
groupUpdateDescription: async (jid: string, description: string) => {
groupUpdateDescription: async(jid: string, description: string) => {
const metadata = await groupMetadataFull(jid)
const node: BinaryNode = {
tag: 'description',
attrs: {id: generateMessageID(), prev: metadata?.descId},
attrs: { id: generateMessageID(), prev: metadata?.descId },
content: Buffer.from(description, 'utf-8')
}
@@ -247,4 +253,5 @@ const makeGroupsSocket = (config: LegacySocketConfig) => {
}
}
export default makeGroupsSocket

View File

@@ -1,5 +1,5 @@
import { LegacySocketConfig } from '../Types'
import { DEFAULT_LEGACY_CONNECTION_CONFIG } from '../Defaults'
import { LegacySocketConfig } from '../Types'
import _makeLegacySocket from './groups'
// export the last socket layer
const makeLegacySocket = (config: Partial<LegacySocketConfig>) => (

View File

@@ -1,15 +1,15 @@
import { BinaryNode, getBinaryNodeMessages, isJidGroup, jidNormalizedUser, areJidsSameUser } from "../WABinary";
import { Boom } from '@hapi/boom'
import { Chat, WAMessageCursor, WAMessage, LegacySocketConfig, WAMessageKey, ParticipantAction, WAMessageStatus, WAMessageStubType, GroupMetadata, AnyMessageContent, MiscMessageGenerationOptions, WAFlag, WAMetric, WAUrlInfo, MediaConnInfo, MessageUpdateType, MessageInfo, MessageInfoUpdate, WAMessageUpdate } from "../Types";
import { toNumber, generateWAMessage, decryptMediaMessageBuffer, extractMessageContent, getWAUploadToServer } from "../Utils";
import makeChatsSocket from "./chats";
import { WA_DEFAULT_EPHEMERAL } from "../Defaults";
import { proto } from "../../WAProto";
import { proto } from '../../WAProto'
import { WA_DEFAULT_EPHEMERAL } from '../Defaults'
import { AnyMessageContent, Chat, GroupMetadata, LegacySocketConfig, MediaConnInfo, MessageInfo, MessageInfoUpdate, MessageUpdateType, MiscMessageGenerationOptions, ParticipantAction, WAFlag, WAMessage, WAMessageCursor, WAMessageKey, WAMessageStatus, WAMessageStubType, WAMessageUpdate, WAMetric, WAUrlInfo } from '../Types'
import { decryptMediaMessageBuffer, extractMessageContent, generateWAMessage, getWAUploadToServer, toNumber } from '../Utils'
import { areJidsSameUser, BinaryNode, getBinaryNodeMessages, isJidGroup, jidNormalizedUser } from '../WABinary'
import makeChatsSocket from './chats'
const STATUS_MAP = {
read: WAMessageStatus.READ,
message: WAMessageStatus.DELIVERY_ACK,
error: WAMessageStatus.ERROR
error: WAMessageStatus.ERROR
} as { [_: string]: WAMessageStatus }
const makeMessagesSocket = (config: LegacySocketConfig) => {
@@ -27,10 +27,10 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
let mediaConn: Promise<MediaConnInfo>
const refreshMediaConn = async(forceGet = false) => {
let media = await mediaConn
if (!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) {
const media = await mediaConn
if(!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) {
mediaConn = (async() => {
const {media_conn} = await query({
const { media_conn } = await query({
json: ['query', 'mediaConn'],
requiresPhoneConnection: false,
expect200: true
@@ -38,9 +38,10 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
media_conn.fetchDate = new Date()
return media_conn as MediaConnInfo
})()
}
return mediaConn
}
}
return mediaConn
}
const fetchMessagesFromWA = async(
jid: string,
@@ -51,7 +52,8 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(cursor) {
key = 'before' in cursor ? cursor.before : cursor.after
}
const { content }:BinaryNode = await query({
const { content }:BinaryNode = await query({
json: {
tag: 'query',
attrs: {
@@ -71,15 +73,18 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
if(Array.isArray(content)) {
return content.map(data => proto.WebMessageInfo.decode(data.content as Buffer))
}
return []
}
}
const updateMediaMessage = async(message: WAMessage) => {
const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage
if (!content) throw new Boom(
`given message ${message.key.id} is not a media message`,
{ statusCode: 400, data: message }
)
if(!content) {
throw new Boom(
`given message ${message.key.id} is not a media message`,
{ statusCode: 400, data: message }
)
}
const response: BinaryNode = await query ({
json: {
@@ -133,35 +138,36 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
}
const protocolMessage = message.message?.protocolMessage || message.message?.ephemeralMessage?.message?.protocolMessage
// if it's a message to delete another message
if (protocolMessage) {
switch (protocolMessage.type) {
case proto.ProtocolMessage.ProtocolMessageType.REVOKE:
const key = protocolMessage.key
const messageStubType = WAMessageStubType.REVOKE
ev.emit('messages.update', [
{
// the key of the deleted message is updated
update: { message: null, key: message.key, messageStubType },
key
}
])
return
case proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING:
chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp
// if it's a message to delete another message
if(protocolMessage) {
switch (protocolMessage.type) {
case proto.ProtocolMessage.ProtocolMessageType.REVOKE:
const key = protocolMessage.key
const messageStubType = WAMessageStubType.REVOKE
ev.emit('messages.update', [
{
// the key of the deleted message is updated
update: { message: null, key: message.key, messageStubType },
key
}
])
return
case proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING:
chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp
chatUpdate.ephemeralExpiration = protocolMessage.ephemeralExpiration
if(isJidGroup(jid)) {
emitGroupUpdate({ ephemeralDuration: protocolMessage.ephemeralExpiration || null })
}
break
default:
break
}
}
if(isJidGroup(jid)) {
emitGroupUpdate({ ephemeralDuration: protocolMessage.ephemeralExpiration || null })
}
break
default:
break
}
}
// check if the message is an action
if (message.messageStubType) {
if(message.messageStubType) {
const { user } = state.legacy!
//let actor = jidNormalizedUser (message.participant)
let participants: string[]
@@ -170,44 +176,47 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
)
switch (message.messageStubType) {
case WAMessageStubType.CHANGE_EPHEMERAL_SETTING:
chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp
chatUpdate.ephemeralExpiration = +message.messageStubParameters[0]
if(isJidGroup(jid)) {
emitGroupUpdate({ ephemeralDuration: +message.messageStubParameters[0] || null })
}
break
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
participants = message.messageStubParameters.map (jidNormalizedUser)
emitParticipantsUpdate('remove')
// mark the chat read only if you left the group
if (participants.includes(user.id)) {
chatUpdate.readOnly = true
}
break
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
participants = message.messageStubParameters.map (jidNormalizedUser)
if (participants.includes(user.id)) {
chatUpdate.readOnly = null
}
emitParticipantsUpdate('add')
break
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
const announce = message.messageStubParameters[0] === 'on'
emitGroupUpdate({ announce })
break
case WAMessageStubType.GROUP_CHANGE_RESTRICT:
const restrict = message.messageStubParameters[0] === 'on'
emitGroupUpdate({ restrict })
break
case WAMessageStubType.GROUP_CHANGE_SUBJECT:
case WAMessageStubType.GROUP_CREATE:
chatUpdate.name = message.messageStubParameters[0]
emitGroupUpdate({ subject: chatUpdate.name })
break
case WAMessageStubType.CHANGE_EPHEMERAL_SETTING:
chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp
chatUpdate.ephemeralExpiration = +message.messageStubParameters[0]
if(isJidGroup(jid)) {
emitGroupUpdate({ ephemeralDuration: +message.messageStubParameters[0] || null })
}
break
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
participants = message.messageStubParameters.map (jidNormalizedUser)
emitParticipantsUpdate('remove')
// mark the chat read only if you left the group
if(participants.includes(user.id)) {
chatUpdate.readOnly = true
}
break
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
participants = message.messageStubParameters.map (jidNormalizedUser)
if(participants.includes(user.id)) {
chatUpdate.readOnly = null
}
emitParticipantsUpdate('add')
break
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
const announce = message.messageStubParameters[0] === 'on'
emitGroupUpdate({ announce })
break
case WAMessageStubType.GROUP_CHANGE_RESTRICT:
const restrict = message.messageStubParameters[0] === 'on'
emitGroupUpdate({ restrict })
break
case WAMessageStubType.GROUP_CHANGE_SUBJECT:
case WAMessageStubType.GROUP_CREATE:
chatUpdate.name = message.messageStubParameters[0]
emitGroupUpdate({ subject: chatUpdate.name })
break
}
}
@@ -221,8 +230,8 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
const waUploadToServer = getWAUploadToServer(config, refreshMediaConn)
/** Query a string to check if it has a url, if it does, return WAUrlInfo */
const generateUrlInfo = async(text: string) => {
const response: BinaryNode = await query({
const generateUrlInfo = async(text: string) => {
const response: BinaryNode = await query({
json: {
tag: 'query',
attrs: {
@@ -236,14 +245,15 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
requiresPhoneConnection: false
})
const urlInfo = { ...response.attrs } as any as WAUrlInfo
if(response && response.content) {
urlInfo.jpegThumbnail = response.content as Buffer
}
return urlInfo
}
if(response && response.content) {
urlInfo.jpegThumbnail = response.content as Buffer
}
return urlInfo
}
/** Relay (send) a WAMessage; more advanced functionality to send a built WA Message, you may want to stick with sendMessage() */
const relayMessage = async(message: WAMessage, { waitForAck } = { waitForAck: true }) => {
const relayMessage = async(message: WAMessage, { waitForAck } = { waitForAck: true }) => {
const json: BinaryNode = {
tag: 'action',
attrs: { epoch: currentEpoch().toString(), type: 'relay' },
@@ -256,35 +266,37 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
]
}
const isMsgToMe = areJidsSameUser(message.key.remoteJid, state.legacy.user?.id || '')
const flag = isMsgToMe ? WAFlag.acknowledge : WAFlag.ignore // acknowledge when sending message to oneself
const mID = message.key.id
const flag = isMsgToMe ? WAFlag.acknowledge : WAFlag.ignore // acknowledge when sending message to oneself
const mID = message.key.id
const finalState = isMsgToMe ? WAMessageStatus.READ : WAMessageStatus.SERVER_ACK
message.status = WAMessageStatus.PENDING
const promise = query({
json,
binaryTag: [WAMetric.message, flag],
tag: mID,
expect200: true,
requiresPhoneConnection: true
})
message.status = WAMessageStatus.PENDING
const promise = query({
json,
binaryTag: [WAMetric.message, flag],
tag: mID,
expect200: true,
requiresPhoneConnection: true
})
if(waitForAck) {
await promise
if(waitForAck) {
await promise
message.status = finalState
} else {
const emitUpdate = (status: WAMessageStatus) => {
message.status = status
ev.emit('messages.update', [ { key: message.key, update: { status } } ])
}
promise
} else {
const emitUpdate = (status: WAMessageStatus) => {
message.status = status
ev.emit('messages.update', [ { key: message.key, update: { status } } ])
}
promise
.then(() => emitUpdate(finalState))
.catch(() => emitUpdate(WAMessageStatus.ERROR))
}
}
if(config.emitOwnEvents) {
onMessage(message, 'append')
}
}
}
// messages received
const messagesUpdate = (node: BinaryNode, isLatest: boolean) => {
@@ -330,26 +342,30 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
logger.warn({ content, key }, 'got unknown status update for message')
}
}
ev.emit('messages.update', updates)
}
}
const onMessageInfoUpdate = ([,attributes]: [string,{[_: string]: any}]) => {
const onMessageInfoUpdate = ([, attributes]: [string, {[_: string]: any}]) => {
let ids = attributes.id as string[] | string
if(typeof ids === 'string') {
ids = [ids]
}
let updateKey: keyof MessageInfoUpdate['update']
switch(attributes.ack.toString()) {
case '2':
updateKey = 'deliveries'
break
case '3':
updateKey = 'reads'
break
default:
logger.warn({ attributes }, `received unknown message info update`)
return
switch (attributes.ack.toString()) {
case '2':
updateKey = 'deliveries'
break
case '3':
updateKey = 'reads'
break
default:
logger.warn({ attributes }, 'received unknown message info update')
return
}
const keyPartial = {
remoteJid: jidNormalizedUser(attributes.to),
fromMe: areJidsSameUser(attributes.from, state.legacy?.user?.id || ''),
@@ -406,38 +422,43 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
const [{ attrs }] = (innerData as BinaryNode[])
const jid = jidNormalizedUser(attrs.jid)
const date = new Date(+attrs.t * 1000)
switch(tag) {
case 'read':
info.reads[jid] = date
break
case 'delivery':
info.deliveries[jid] = date
break
switch (tag) {
case 'read':
info.reads[jid] = date
break
case 'delivery':
info.deliveries[jid] = date
break
}
}
}
return info
},
downloadMediaMessage: async(message: WAMessage, type: 'buffer' | 'stream' = 'buffer') => {
const downloadMediaMessage = async () => {
let mContent = extractMessageContent(message.message)
if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message })
const downloadMediaMessage = async() => {
const mContent = extractMessageContent(message.message)
if(!mContent) {
throw new Boom('No message present', { statusCode: 400, data: message })
}
const stream = await decryptMediaMessageBuffer(mContent)
if(type === 'buffer') {
let buffer = Buffer.from([])
for await(const chunk of stream) {
for await (const chunk of stream) {
buffer = Buffer.concat([buffer, chunk])
}
return buffer
}
return stream
}
try {
const result = await downloadMediaMessage()
return result
} catch (error) {
} catch(error) {
if(error.message.includes('404')) { // media needs to be updated
logger.info (`updating media of message: ${message.key.id}`)
@@ -446,6 +467,7 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
const result = await downloadMediaMessage()
return result
}
throw error
}
},
@@ -453,15 +475,15 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
fetchMessagesFromWA,
/** Load a single message specified by the ID */
loadMessageFromWA: async(jid: string, id: string) => {
let message: WAMessage
// load the message before the given message
let messages = (await fetchMessagesFromWA(jid, 1, { before: {id, fromMe: true} }))
if(!messages[0]) messages = (await fetchMessagesFromWA(jid, 1, { before: {id, fromMe: false} }))
let messages = (await fetchMessagesFromWA(jid, 1, { before: { id, fromMe: true } }))
if(!messages[0]) {
messages = (await fetchMessagesFromWA(jid, 1, { before: { id, fromMe: false } }))
}
// the message after the loaded message is the message required
const [actual] = await fetchMessagesFromWA(jid, 1, { after: messages[0] && messages[0].key })
message = actual
return message
return actual
},
searchMessages: async(txt: string, inJid: string | null, count: number, page: number) => {
const node: BinaryNode = await query({
@@ -499,8 +521,8 @@ const makeMessagesSocket = (config: LegacySocketConfig) => {
) {
const { disappearingMessagesInChat } = content
const value = typeof disappearingMessagesInChat === 'boolean' ?
(disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
disappearingMessagesInChat
(disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
disappearingMessagesInChat
const tag = generateMessageTag(true)
await setQuery([
{

View File

@@ -1,11 +1,11 @@
import { Boom } from '@hapi/boom'
import { STATUS_CODES } from "http"
import { promisify } from "util"
import WebSocket from "ws"
import { BinaryNode, encodeBinaryNodeLegacy } from "../WABinary"
import { DisconnectReason, LegacySocketConfig, SocketQueryOptions, SocketSendMessageOptions, WAFlag, WAMetric, WATag } from "../Types"
import { aesEncrypt, hmacSign, promiseTimeout, unixTimestampSeconds, decodeWAMessage } from "../Utils"
import { DEFAULT_ORIGIN, DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, PHONE_CONNECTION_CB } from "../Defaults"
import { STATUS_CODES } from 'http'
import { promisify } from 'util'
import WebSocket from 'ws'
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, PHONE_CONNECTION_CB } from '../Defaults'
import { DisconnectReason, LegacySocketConfig, SocketQueryOptions, SocketSendMessageOptions, WAFlag, WAMetric, WATag } from '../Types'
import { aesEncrypt, decodeWAMessage, hmacSign, promiseTimeout, unixTimestampSeconds } from '../Utils'
import { BinaryNode, encodeBinaryNodeLegacy } from '../WABinary'
/**
* Connects to WA servers and performs:
@@ -14,13 +14,13 @@ import { DEFAULT_ORIGIN, DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, PHONE_CONNECTION_C
* - query phone connection
*/
export const makeSocket = ({
waWebSocketUrl,
connectTimeoutMs,
phoneResponseTimeMs,
logger,
agent,
keepAliveIntervalMs,
expectResponseTimeout,
waWebSocketUrl,
connectTimeoutMs,
phoneResponseTimeMs,
logger,
agent,
keepAliveIntervalMs,
expectResponseTimeout,
}: LegacySocketConfig) => {
// for generating tags
const referenceDateSeconds = unixTimestampSeconds(new Date())
@@ -37,33 +37,35 @@ export const makeSocket = ({
'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
}
})
ws.setMaxListeners(0)
ws.setMaxListeners(0)
let lastDateRecv: Date
let epoch = 0
let authInfo: { encKey: Buffer, macKey: Buffer }
let keepAliveReq: NodeJS.Timeout
let phoneCheckInterval: NodeJS.Timeout
let phoneCheckListeners = 0
let phoneCheckInterval: NodeJS.Timeout
let phoneCheckListeners = 0
const phoneConnectionChanged = (value: boolean) => {
ws.emit('phone-connection', { value })
}
const phoneConnectionChanged = (value: boolean) => {
ws.emit('phone-connection', { value })
}
const sendPromise = promisify(ws.send)
/** generate message tag and increment epoch */
const generateMessageTag = (longTag: boolean = false) => {
const tag = `${longTag ? referenceDateSeconds : (referenceDateSeconds%1000)}.--${epoch}`
epoch += 1 // increment message count, it makes the 'epoch' field when sending binary messages
return tag
}
const sendRawMessage = (data: Buffer | string) => {
if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
}
const tag = `${longTag ? referenceDateSeconds : (referenceDateSeconds%1000)}.--${epoch}`
epoch += 1 // increment message count, it makes the 'epoch' field when sending binary messages
return tag
}
const sendRawMessage = (data: Buffer | string) => {
if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
}
return sendPromise.call(ws, data) as Promise<void>
}
return sendPromise.call(ws, data) as Promise<void>
}
/**
* Send a message to the WA servers
* @returns the tag attached in the message
@@ -73,17 +75,19 @@ export const makeSocket = ({
) => {
tag = tag || generateMessageTag(longTag)
let data: Buffer | string
if(logger.level === 'trace') {
logger.trace({ tag, fromMe: true, json, binaryTag }, 'communication')
}
if(logger.level === 'trace') {
logger.trace({ tag, fromMe: true, json, binaryTag }, 'communication')
}
if(binaryTag) {
if(Array.isArray(json)) {
throw new Boom('Expected BinaryNode with binary code', { statusCode: 400 })
}
if(Array.isArray(json)) {
throw new Boom('Expected BinaryNode with binary code', { statusCode: 400 })
}
if(!authInfo) {
throw new Boom('No encryption/mac keys to encrypt node with', { statusCode: 400 })
}
const binary = encodeBinaryNodeLegacy(json) // encode the JSON to the WhatsApp binary format
const buff = aesEncrypt(binary, authInfo.encKey) // encrypt it using AES and our encKey
@@ -98,242 +102,268 @@ export const makeSocket = ({
} else {
data = `${tag},${JSON.stringify(json)}`
}
await sendRawMessage(data)
return tag
}
const end = (error: Error | undefined) => {
logger.info({ error }, 'connection closed')
logger.info({ error }, 'connection closed')
ws.removeAllListeners('close')
ws.removeAllListeners('error')
ws.removeAllListeners('open')
ws.removeAllListeners('message')
ws.removeAllListeners('close')
ws.removeAllListeners('error')
ws.removeAllListeners('open')
ws.removeAllListeners('message')
phoneCheckListeners = 0
clearInterval(keepAliveReq)
clearPhoneCheckInterval()
phoneCheckListeners = 0
clearInterval(keepAliveReq)
clearPhoneCheckInterval()
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
try { ws.close() } catch { }
}
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
try {
ws.close()
} catch{ }
}
ws.emit('ws-close', error)
ws.removeAllListeners('ws-close')
ws.emit('ws-close', error)
ws.removeAllListeners('ws-close')
}
const onMessageRecieved = (message: string | Buffer) => {
if(message[0] === '!' || message[0] === '!'.charCodeAt(0)) {
// when the first character in the message is an '!', the server is sending a pong frame
const timestamp = message.slice(1, message.length).toString()
lastDateRecv = new Date(parseInt(timestamp))
ws.emit('received-pong')
} else {
let messageTag: string
let json: any
try {
const dec = decodeWAMessage(message, authInfo)
messageTag = dec[0]
json = dec[1]
if (!json) return
} catch (error) {
end(error)
if(message[0] === '!' || message[0] === '!'.charCodeAt(0)) {
// when the first character in the message is an '!', the server is sending a pong frame
const timestamp = message.slice(1, message.length).toString()
lastDateRecv = new Date(parseInt(timestamp))
ws.emit('received-pong')
} else {
let messageTag: string
let json: any
try {
const dec = decodeWAMessage(message, authInfo)
messageTag = dec[0]
json = dec[1]
if(!json) {
return
}
} catch(error) {
end(error)
return
}
//if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false })
}
//if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false })
if (logger.level === 'trace') {
logger.trace({ tag: messageTag, fromMe: false, json }, 'communication')
}
if(logger.level === 'trace') {
logger.trace({ tag: messageTag, fromMe: false, json }, 'communication')
}
let anyTriggered = false
/* Check if this is a response to a message we sent */
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${messageTag}`, json)
/* Check if this is a response to a message we are expecting */
const l0 = json.tag || json[0] || ''
const l1 = json?.attrs || json?.[1] || { }
const l2 = json?.content?.[0]?.tag || json[2]?.[0] || ''
let anyTriggered = false
/* Check if this is a response to a message we sent */
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${messageTag}`, json)
/* Check if this is a response to a message we are expecting */
const l0 = json.tag || json[0] || ''
const l1 = json?.attrs || json?.[1] || { }
const l2 = json?.content?.[0]?.tag || json[2]?.[0] || ''
Object.keys(l1).forEach(key => {
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, json) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, json) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, json) || anyTriggered
})
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, json) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, json) || anyTriggered
Object.keys(l1).forEach(key => {
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, json) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, json) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, json) || anyTriggered
})
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, json) || anyTriggered
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, json) || anyTriggered
if (!anyTriggered && logger.level === 'debug') {
logger.debug({ unhandled: true, tag: messageTag, fromMe: false, json }, 'communication recv')
}
}
}
if(!anyTriggered && logger.level === 'debug') {
logger.debug({ unhandled: true, tag: messageTag, fromMe: false, json }, 'communication recv')
}
}
}
/** Exits a query if the phone connection is active and no response is still found */
const exitQueryIfResponseNotExpected = (tag: string, cancel: (error: Boom) => void) => {
let timeout: NodeJS.Timeout
const listener = ([, connected]) => {
if(connected) {
timeout = setTimeout(() => {
logger.info({ tag }, `cancelling wait for message as a response is no longer expected from the phone`)
const exitQueryIfResponseNotExpected = (tag: string, cancel: (error: Boom) => void) => {
let timeout: NodeJS.Timeout
const listener = ([, connected]) => {
if(connected) {
timeout = setTimeout(() => {
logger.info({ tag }, 'cancelling wait for message as a response is no longer expected from the phone')
cancel(new Boom('Not expecting a response', { statusCode: 422 }))
}, expectResponseTimeout)
ws.off(PHONE_CONNECTION_CB, listener)
}
}
ws.on(PHONE_CONNECTION_CB, listener)
return () => {
ws.off(PHONE_CONNECTION_CB, listener)
timeout && clearTimeout(timeout)
}
}
/** interval is started when a query takes too long to respond */
const startPhoneCheckInterval = () => {
phoneCheckListeners += 1
if (!phoneCheckInterval) {
// if its been a long time and we haven't heard back from WA, send a ping
phoneCheckInterval = setInterval(() => {
if(phoneCheckListeners <= 0) {
logger.warn('phone check called without listeners')
return
}
logger.info('checking phone connection...')
sendAdminTest()
}, expectResponseTimeout)
ws.off(PHONE_CONNECTION_CB, listener)
}
}
ws.on(PHONE_CONNECTION_CB, listener)
return () => {
ws.off(PHONE_CONNECTION_CB, listener)
timeout && clearTimeout(timeout)
}
}
/** interval is started when a query takes too long to respond */
const startPhoneCheckInterval = () => {
phoneCheckListeners += 1
if(!phoneCheckInterval) {
// if its been a long time and we haven't heard back from WA, send a ping
phoneCheckInterval = setInterval(() => {
if(phoneCheckListeners <= 0) {
logger.warn('phone check called without listeners')
return
}
logger.info('checking phone connection...')
sendAdminTest()
phoneConnectionChanged(false)
}, phoneResponseTimeMs)
}
}
const clearPhoneCheckInterval = () => {
phoneCheckListeners -= 1
if(phoneCheckListeners <= 0) {
clearInterval(phoneCheckInterval)
phoneCheckInterval = undefined
phoneCheckListeners = 0
}
}
phoneConnectionChanged(false)
}, phoneResponseTimeMs)
}
}
const clearPhoneCheckInterval = () => {
phoneCheckListeners -= 1
if (phoneCheckListeners <= 0) {
clearInterval(phoneCheckInterval)
phoneCheckInterval = undefined
phoneCheckListeners = 0
}
}
/** checks for phone connection */
const sendAdminTest = () => sendNode({ json: ['admin', 'test'] })
/**
const sendAdminTest = () => sendNode({ json: ['admin', 'test'] })
/**
* Wait for a message with a certain tag to be received
* @param tag the message tag to await
* @param json query that was sent
* @param timeoutMs timeout after which the promise will reject
*/
const waitForMessage = (tag: string, requiresPhoneConnection: boolean, timeoutMs?: number) => {
if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection not open', { statusCode: DisconnectReason.connectionClosed })
}
if(ws.readyState !== ws.OPEN) {
throw new Boom('Connection not open', { statusCode: DisconnectReason.connectionClosed })
}
let cancelToken = () => { }
let cancelToken = () => { }
return {
promise: (async() => {
let onRecv: (json) => void
let onErr: (err) => void
let cancelPhoneChecker: () => void
try {
const result = await promiseTimeout(timeoutMs,
(resolve, reject) => {
onRecv = resolve
onErr = err => {
reject(err || new Boom('Intentional Close', { statusCode: DisconnectReason.connectionClosed }))
}
cancelToken = () => onErr(new Boom('Cancelled', { statusCode: 500 }))
return {
promise: (async() => {
let onRecv: (json) => void
let onErr: (err) => void
let cancelPhoneChecker: () => void
try {
const result = await promiseTimeout(timeoutMs,
(resolve, reject) => {
onRecv = resolve
onErr = err => {
reject(err || new Boom('Intentional Close', { statusCode: DisconnectReason.connectionClosed }))
}
cancelToken = () => onErr(new Boom('Cancelled', { statusCode: 500 }))
if(requiresPhoneConnection) {
startPhoneCheckInterval()
cancelPhoneChecker = exitQueryIfResponseNotExpected(tag, onErr)
}
if(requiresPhoneConnection) {
startPhoneCheckInterval()
cancelPhoneChecker = exitQueryIfResponseNotExpected(tag, onErr)
}
ws.on(`TAG:${tag}`, onRecv)
ws.on('ws-close', onErr) // if the socket closes, you'll never receive the message
},
)
return result as any
} finally {
requiresPhoneConnection && clearPhoneCheckInterval()
cancelPhoneChecker && cancelPhoneChecker()
ws.on(`TAG:${tag}`, onRecv)
ws.on('ws-close', onErr) // if the socket closes, you'll never receive the message
},
)
return result as any
} finally {
requiresPhoneConnection && clearPhoneCheckInterval()
cancelPhoneChecker && cancelPhoneChecker()
ws.off(`TAG:${tag}`, onRecv)
ws.off('ws-close', onErr) // if the socket closes, you'll never receive the message
}
})(),
cancelToken: () => { cancelToken() }
}
}
/**
ws.off(`TAG:${tag}`, onRecv)
ws.off('ws-close', onErr) // if the socket closes, you'll never receive the message
}
})(),
cancelToken: () => {
cancelToken()
}
}
}
/**
* Query something from the WhatsApp servers
* @param json the query itself
* @param binaryTags the tags to attach if the query is supposed to be sent encoded in binary
* @param timeoutMs timeout after which the query will be failed (set to null to disable a timeout)
* @param tag the tag to attach to the message
*/
const query = async(
{ json, timeoutMs, expect200, tag, longTag, binaryTag, requiresPhoneConnection }: SocketQueryOptions
) => {
const query = async(
{ json, timeoutMs, expect200, tag, longTag, binaryTag, requiresPhoneConnection }: SocketQueryOptions
) => {
tag = tag || generateMessageTag(longTag)
const { promise, cancelToken } = waitForMessage(tag, requiresPhoneConnection, timeoutMs)
try {
await sendNode({ json, tag, binaryTag })
} catch(error) {
cancelToken()
// swallow error
await promise.catch(() => { })
// throw back the error
throw error
}
const { promise, cancelToken } = waitForMessage(tag, requiresPhoneConnection, timeoutMs)
try {
await sendNode({ json, tag, binaryTag })
} catch(error) {
cancelToken()
// swallow error
await promise.catch(() => { })
// throw back the error
throw error
}
const response = await promise
const responseStatusCode = +(response.status ? response.status : 200) // default status
// read here: http://getstatuscode.com/599
if(responseStatusCode === 599) { // the connection has gone bad
end(new Boom('WA server overloaded', { statusCode: 599, data: { query: json, response } }))
}
if(expect200 && Math.floor(responseStatusCode/100) !== 2) {
const message = STATUS_CODES[responseStatusCode] || 'unknown'
throw new Boom(
`Unexpected status in '${Array.isArray(json) ? json[0] : (json?.tag || 'query')}': ${message}(${responseStatusCode})`,
{ data: { query: json, response }, statusCode: response.status }
)
}
return response
}
const response = await promise
const responseStatusCode = +(response.status ? response.status : 200) // default status
// read here: http://getstatuscode.com/599
if(responseStatusCode === 599) { // the connection has gone bad
end(new Boom('WA server overloaded', { statusCode: 599, data: { query: json, response } }))
}
if(expect200 && Math.floor(responseStatusCode/100) !== 2) {
const message = STATUS_CODES[responseStatusCode] || 'unknown'
throw new Boom(
`Unexpected status in '${Array.isArray(json) ? json[0] : (json?.tag || 'query')}': ${message}(${responseStatusCode})`,
{ data: { query: json, response }, statusCode: response.status }
)
}
return response
}
const startKeepAliveRequest = () => (
keepAliveReq = setInterval(() => {
if (!lastDateRecv) lastDateRecv = new Date()
const diff = Date.now() - lastDateRecv.getTime()
/*
keepAliveReq = setInterval(() => {
if(!lastDateRecv) {
lastDateRecv = new Date()
}
const diff = Date.now() - lastDateRecv.getTime()
/*
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) {
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
} else if(ws.readyState === ws.OPEN) {
sendRawMessage('?,,') // if its all good, send a keep alive request
} else {
logger.warn('keep alive called when WS not open')
}
}, keepAliveIntervalMs)
)
if(diff > keepAliveIntervalMs+5000) {
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
} else if(ws.readyState === ws.OPEN) {
sendRawMessage('?,,') // if its all good, send a keep alive request
} else {
logger.warn('keep alive called when WS not open')
}
}, keepAliveIntervalMs)
)
const waitForSocketOpen = async() => {
if(ws.readyState === ws.OPEN) return
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
throw new Boom('Connection Already Closed', { statusCode: DisconnectReason.connectionClosed })
}
let onOpen: () => void
let onClose: (err: Error) => void
await new Promise((resolve, reject) => {
onOpen = () => resolve(undefined)
onClose = reject
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)
})
}
const waitForSocketOpen = async() => {
if(ws.readyState === ws.OPEN) {
return
}
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
throw new Boom('Connection Already Closed', { statusCode: DisconnectReason.connectionClosed })
}
let onOpen: () => void
let onClose: (err: Error) => void
await new Promise((resolve, reject) => {
onOpen = () => resolve(undefined)
onClose = reject
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)
})
}
ws.on('message', onMessageRecieved)
ws.on('open', () => {
@@ -343,57 +373,58 @@ export const makeSocket = ({
ws.on('error', end)
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionLost })))
ws.on(PHONE_CONNECTION_CB, json => {
if (!json[1]) {
end(new Boom('Connection terminated by phone', { statusCode: DisconnectReason.connectionLost }))
logger.info('Connection terminated by phone, closing...')
} else {
phoneConnectionChanged(true)
}
})
ws.on('CB:Cmd,type:disconnect', json => {
const {kind} = json[1]
let reason: DisconnectReason
switch(kind) {
case 'replaced':
reason = DisconnectReason.connectionReplaced
break
default:
reason = DisconnectReason.connectionLost
break
}
end(new Boom(
`Connection terminated by server: "${kind || 'unknown'}"`,
{ statusCode: reason }
))
})
ws.on(PHONE_CONNECTION_CB, json => {
if(!json[1]) {
end(new Boom('Connection terminated by phone', { statusCode: DisconnectReason.connectionLost }))
logger.info('Connection terminated by phone, closing...')
} else {
phoneConnectionChanged(true)
}
})
ws.on('CB:Cmd,type:disconnect', json => {
const { kind } = json[1]
let reason: DisconnectReason
switch (kind) {
case 'replaced':
reason = DisconnectReason.connectionReplaced
break
default:
reason = DisconnectReason.connectionLost
break
}
end(new Boom(
`Connection terminated by server: "${kind || 'unknown'}"`,
{ statusCode: reason }
))
})
return {
type: 'legacy' as 'legacy',
ws,
sendAdminTest,
updateKeys: (info: { encKey: Buffer, macKey: Buffer }) => authInfo = info,
waitForSocketOpen,
type: 'legacy' as 'legacy',
ws,
sendAdminTest,
updateKeys: (info: { encKey: Buffer, macKey: Buffer }) => authInfo = info,
waitForSocketOpen,
sendNode,
generateMessageTag,
waitForMessage,
query,
/** Generic function for action, set queries */
setQuery: async(nodes: BinaryNode[], binaryTag: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) => {
const json: BinaryNode = {
tag: 'action',
attrs: { epoch: epoch.toString(), type: 'set' },
content: nodes
}
waitForMessage,
query,
/** Generic function for action, set queries */
setQuery: async(nodes: BinaryNode[], binaryTag: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) => {
const json: BinaryNode = {
tag: 'action',
attrs: { epoch: epoch.toString(), type: 'set' },
content: nodes
}
return query({
json,
binaryTag,
tag,
expect200: true,
requiresPhoneConnection: true
}) as Promise<{ status: number }>
},
return query({
json,
binaryTag,
tag,
expect200: true,
requiresPhoneConnection: true
}) as Promise<{ status: number }>
},
currentEpoch: () => epoch,
end
}