chore: remove legacy code

This commit is contained in:
Adhiraj Singh
2022-09-19 14:59:35 +05:30
parent d73585ede9
commit e7f6838fa4
23 changed files with 91 additions and 2921 deletions

View File

@@ -1,4 +1,4 @@
import type { CommonSocketConfig, LegacySocketConfig, MediaType, SocketConfig } from '../Types'
import type { MediaType, SocketConfig } from '../Types'
import { Browsers } from '../Utils'
import logger from '../Utils/logger'
import { version } from './baileys-version.json'
@@ -26,10 +26,9 @@ export const WA_CERT_DETAILS = {
SERIAL: 0,
}
const BASE_CONNECTION_CONFIG: CommonSocketConfig = {
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
version: version as any,
browser: Browsers.baileys('Chrome'),
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
connectTimeoutMs: 20_000,
keepAliveIntervalMs: 15_000,
@@ -38,11 +37,7 @@ const BASE_CONNECTION_CONFIG: CommonSocketConfig = {
emitOwnEvents: true,
defaultQueryTimeoutMs: 60_000,
customUploadHosts: [],
retryRequestDelayMs: 250
}
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
...BASE_CONNECTION_CONFIG,
retryRequestDelayMs: 250,
fireInitQueries: true,
auth: undefined as any,
downloadHistory: true,
@@ -54,13 +49,6 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
getMessage: async() => undefined
}
export const DEFAULT_LEGACY_CONNECTION_CONFIG: LegacySocketConfig = {
...BASE_CONNECTION_CONFIG,
waWebSocketUrl: 'wss://web.whatsapp.com/ws',
phoneResponseTimeMs: 20_000,
expectResponseTimeout: 60_000,
}
export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
image: '/mms/image',
video: '/mms/video',

View File

@@ -1,251 +0,0 @@
import { Boom } from '@hapi/boom'
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 {
logger,
version,
browser,
connectTimeoutMs,
printQRInTerminal,
auth: initialAuthInfo
} = config
const ev = new EventEmitter() as LegacyBaileysEventEmitter
const authInfo = initialAuthInfo || newLegacyAuthCreds()
const state: ConnectionState = {
legacy: {
phoneConnected: false,
},
connection: 'connecting',
}
const socket = makeSocket(config)
const { ws } = socket
let curveKeys: CurveKeyPair
let initTimeout: NodeJS.Timeout | undefined
ws.on('phone-connection', ({ value: phoneConnected }) => {
updateState({ legacy: { ...state.legacy, phoneConnected } })
})
// add close listener
ws.on('ws-close', (error: Boom | Error) => {
logger.info({ error }, 'closed connection to WhatsApp')
initTimeout && clearTimeout(initTimeout)
// if no reconnects occur
// send close event
updateState({
connection: 'close',
qr: undefined,
lastDisconnect: {
error,
date: new Date()
}
})
})
/** Can you login to WA without scanning the QR */
const canLogin = () => !!authInfo?.encKey && !!authInfo?.macKey
const updateState = (update: Partial<ConnectionState>) => {
Object.assign(state, update)
ev.emit('connection.update', update)
}
/**
* Logs you out from WA
* If connected, invalidates the credentials with the server
*/
const logout = async() => {
if(state.connection === 'open') {
await socket.sendNode({
json: ['admin', 'Conn', 'disconnect'],
tag: 'goodbye'
})
}
// will call state update to close connection
socket?.end(
new Boom('Logged Out', { statusCode: DisconnectReason.loggedOut })
)
}
const updateEncKeys = () => {
// update the keys so we can decrypt traffic
socket.updateKeys({ encKey: authInfo!.encKey, macKey: authInfo!.macKey })
}
const generateKeysForAuth = async(ref: string, ttl?: number) => {
curveKeys = Curve.generateKeyPair()
const publicKey = Buffer.from(curveKeys.public).toString('base64')
const qrLoop = ttl => {
const qr = [ref, publicKey, authInfo.clientID].join(',')
updateState({ qr })
initTimeout = setTimeout(async() => {
if(state.connection !== 'connecting') {
return
}
logger.debug('regenerating QR')
try {
// request new QR
const { ref: newRef, ttl: newTTL } = await socket.query({
json: ['admin', 'Conn', 'reref'],
expect200: true,
longTag: true,
requiresPhoneConnection: false
})
ttl = newTTL
ref = newRef
} catch(error) {
logger.error({ error }, 'error in QR gen')
if(error.output?.statusCode === 429) { // too many QR requests
socket.end(error)
return
}
}
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
if(!canDoLogin) {
generateKeysForAuth(ref, ttl)
}
})()
let loginTag: string | undefined
if(canDoLogin) {
updateEncKeys()
// if we have the info to restore a closed session
const json = [
'admin',
'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({
json,
tag: loginTag
})
initTimeout = setTimeout(sendLoginReq, 10_000)
}
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
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
Object.assign(authInfo, auth)
updateEncKeys()
logger.info({ user }, 'logged in')
ev.emit('creds.update', auth)
updateState({
connection: 'open',
legacy: {
phoneConnected: true,
user,
},
isNewLogin,
qr: undefined
})
}
ws.once('open', async() => {
try {
await onOpen()
} catch(error) {
socket.end(error)
}
})
if(printQRInTerminal) {
printQRIfNecessaryListener(ev, logger)
}
process.nextTick(() => {
ev.emit('connection.update', {
...state
})
})
return {
...socket,
state,
authInfo,
ev,
canLogin,
logout,
/** Waits for the connection to WA to reach a state */
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev)
}
}
export default makeAuthSocket

View File

@@ -1,199 +0,0 @@
import { LegacySocketConfig, OrderDetails } from '../Types'
import { CatalogResult, Product, ProductCreate, ProductCreateResult, ProductUpdate } from '../Types'
import { uploadingNecessaryImages } from '../Utils/business'
import makeGroupsSocket from './groups'
const makeBusinessSocket = (config: LegacySocketConfig) => {
const sock = makeGroupsSocket(config)
const {
query,
generateMessageTag,
waUploadToServer,
state
} = sock
const getCatalog = async(jid?: string, limit = 10) => {
jid = jid || state.legacy?.user?.id
const result: CatalogResult = await query({
expect200: true,
json: [
'query',
'bizCatalog',
{
allowShopSource: false,
catalogWid: jid,
height: 100,
width: 100,
limit,
stanza_id: generateMessageTag(true),
type: 'get_product_catalog_reh',
}
]
})
const products = result.data.data.map<Product>(
mapProduct
)
return {
beforeCursor: result.data.paging.cursors.before,
products
}
}
const productCreate = async(product: ProductCreate) => {
const result: ProductCreateResult = await query({
expect200: true,
json: [
'action',
'addProduct_reh',
await mapProductCreate(product)
]
})
return mapProduct(result.data.product)
}
const productDelete = async(productIds: string[]) => {
const result = await query({
expect200: true,
json: [
'action',
'deleteProduct_reh',
{
product_ids: productIds,
stanza_id: generateMessageTag(true),
}
]
})
return {
deleted: result.data.deleted_count
}
}
const productUpdate = async(productId: string, update: ProductUpdate) => {
const productCreate = await mapProductCreate(
{ ...update, originCountryCode: undefined },
false
)
const result: ProductCreateResult = await query({
expect200: true,
json: [
'action',
'editProduct_reh',
{
product_id: productId,
...productCreate
}
]
})
return mapProduct(result.data.product)
}
const getOrderDetails = async(orderId: string, tokenBase64: string) => {
const result = await query({
expect200: true,
json: [
'query',
'order',
{
id: generateMessageTag(true),
orderId,
imageWidth: '80',
imageHeight: '80',
token: tokenBase64
}
]
})
const data = result.data
const order: OrderDetails = {
price: {
currency: data.price.currency,
total: data.price.total,
},
products: data.products.map(
p => ({
id: p.id,
imageUrl: p.image?.url,
name: p.name,
quantity: +p.quantity,
currency: p.currency,
price: +p.price
})
)
}
return order
}
// maps product create to send to WA
const mapProductCreate = async(product: ProductCreate, mapCompliance = true) => {
const imgs = (
await uploadingNecessaryImages(product.images, waUploadToServer)
).map(img => img.url)
const result: any = {
name: product.name,
description: product.description,
image_url: imgs[0],
url: product.url || '',
additional_image_urls: imgs.slice(1),
retailer_id: product.retailerId || '',
width: '100',
height: '100',
stanza_id: generateMessageTag(true),
price: product.price.toString(),
currency: product.currency
}
if(mapCompliance) {
Object.assign(result, {
compliance_category: product.originCountryCode
? undefined :
'COUNTRY_ORIGIN_EXEMPT',
compliance_info: product.originCountryCode
? { country_code_origin: product.originCountryCode }
: undefined
})
}
return result
}
return {
...sock,
getOrderDetails,
getCatalog,
productCreate,
productDelete,
productUpdate
}
}
const mapProduct = (item: any): Product => ({
id: item.id,
name: item.name,
retailerId: item.retailer_id,
price: +item.price,
description: item.description,
currency: item.currency,
imageUrls: item.image_cdn_urls.reduce(
(dict, { key, value }) => {
dict[key] = value
return dict
}, { }
),
reviewStatus: item.capability_to_review_status.reduce(
(dict, { key, value }) => {
dict[key] = value
return dict
}, { }
),
isHidden: item.is_hidden,
availability: item.availability
})
export default makeBusinessSocket

View File

@@ -1,556 +0,0 @@
import { BaileysEventMap, Chat, ChatModification, Contact, LastMessageList, LegacySocketConfig, PresenceData, WABusinessProfile, WAFlag, WAMessageKey, WAMessageUpdate, WAMetric, WAPresence } from '../Types'
import { generateProfilePicture } from '../Utils'
import { debouncedTimeout, unixTimestampSeconds } from '../Utils/generics'
import { BinaryNode, jidNormalizedUser } from '../WABinary'
import makeAuthSocket from './auth'
const makeChatsSocket = (config: LegacySocketConfig) => {
const { logger } = config
const sock = makeAuthSocket(config)
const {
ev,
ws: socketEvents,
currentEpoch,
setQuery,
query,
sendNode,
state
} = sock
const chatsDebounceTimeout = debouncedTimeout(10_000, () => sendChatsQuery(1))
const sendChatsQuery = (epoch: number) => (
sendNode({
json: {
tag: 'query',
attrs: { type: 'chat', epoch: epoch.toString() }
},
binaryTag: [ WAMetric.queryChat, WAFlag.ignore ]
})
)
const profilePictureUrl = async(jid: string, timeoutMs?: number) => {
const response = await query({
json: ['query', 'ProfilePicThumb', jid],
expect200: false,
requiresPhoneConnection: false,
timeoutMs
})
return response.eurl as string | undefined
}
const executeChatModification = (node: BinaryNode) => {
const { attrs: attributes } = node
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 }
})
)
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 presence: PresenceData = {
lastSeen: update.t ? +update.t : undefined,
lastKnownPresence: update.type as WAPresence
}
return { id, presences: { [participant]: presence } }
}
const chatRead = async(fromMessage: WAMessageKey, count: number) => {
await setQuery (
[
{
tag: 'read',
attrs: {
jid: fromMessage.remoteJid!,
count: count.toString(),
index: fromMessage.id!,
owner: fromMessage.fromMe ? 'true' : 'false'
}
}
],
[ WAMetric.read, WAFlag.ignore ]
)
if(config.emitOwnEvents) {
ev.emit('chats.update', [{ id: fromMessage.remoteJid!, unreadCount: count < 0 ? -1 : 0 }])
}
}
ev.on('connection.update', async({ connection }) => {
if(connection !== 'open') {
return
}
try {
await Promise.all([
sendNode({
json: { tag: 'query', attrs: { type: 'contacts', epoch: '1' } },
binaryTag: [ WAMetric.queryContact, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: { type: 'status', epoch: '1' } },
binaryTag: [ WAMetric.queryStatus, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: { type: 'quick_reply', epoch: '1' } },
binaryTag: [ WAMetric.queryQuickReply, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: { type: 'label', epoch: '1' } },
binaryTag: [ WAMetric.queryLabel, WAFlag.ignore ]
}),
sendNode({
json: { tag: 'query', attrs: { type: 'emoji', epoch: '1' } },
binaryTag: [ WAMetric.queryEmoji, WAFlag.ignore ]
}),
sendNode({
json: {
tag: 'action',
attrs: { type: 'set', epoch: '1' },
content: [
{ tag: 'presence', attrs: { type: 'available' } }
]
},
binaryTag: [ WAMetric.presence, WAFlag.available ]
})
])
chatsDebounceTimeout.start()
logger.debug('sent init queries')
} catch(error) {
logger.error(`error in sending init queries: ${error}`)
}
})
socketEvents.on('CB:response,type:chat', async({ content: data }: BinaryNode) => {
chatsDebounceTimeout.cancel()
if(Array.isArray(data)) {
const contacts: Contact[] = []
const chats = data.map(({ attrs }): Chat => {
const id = jidNormalizedUser(attrs.jid)
if(attrs.name) {
contacts.push({ id, name: attrs.name })
}
return {
id: jidNormalizedUser(attrs.jid),
conversationTimestamp: attrs.t ? +attrs.t : undefined,
unreadCount: +attrs.count,
archive: attrs.archive === 'true' ? true : undefined,
pin: attrs.pin ? +attrs.pin : undefined,
mute: attrs.mute ? +attrs.mute : undefined,
notSpam: !(attrs.spam === 'true'),
name: attrs.name,
ephemeralExpiration: attrs.ephemeral ? +attrs.ephemeral : undefined,
ephemeralSettingTimestamp: attrs.eph_setting_ts ? +attrs.eph_setting_ts : undefined,
readOnly: attrs.read_only === 'true' ? true : undefined,
}
})
logger.info(`got ${chats.length} chats, extracted ${contacts.length} contacts with name`)
ev.emit('chats.set', { chats, isLatest: true })
}
})
// got all contacts from phone
socketEvents.on('CB:response,type:contacts', async({ content: data }: BinaryNode) => {
if(Array.isArray(data)) {
const contacts = data.map(({ attrs }): Contact => {
return {
id: jidNormalizedUser(attrs.jid),
name: attrs.name,
notify: attrs.notify,
verifiedName: attrs.verify === '2' ? attrs.vname : undefined
}
})
logger.info(`got ${contacts.length} contacts`)
ev.emit('contacts.set', { contacts, isLatest: true })
}
})
// status updates
socketEvents.on('CB:Status,status', json => {
const id = jidNormalizedUser(json[1].id)
ev.emit('contacts.update', [ { id, status: json[1].status } ])
})
// User Profile Name Updates
socketEvents.on('CB:Conn,pushname', json => {
const { legacy, connection } = state
const { user } = legacy!
if(connection === 'open' && json[1].pushname !== user!.name) {
user!.name = json[1].pushname
ev.emit('connection.update', { legacy: { ...legacy!, user } })
}
})
// read updates
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
}
ev.emit('chats.update', [update])
}
})
socketEvents.on('CB:Cmd,type:picture', async json => {
json = json[1]
const id = jidNormalizedUser(json.jid)
const imgUrl = await profilePictureUrl(id).catch(() => '')
ev.emit('contacts.update', [ { id, imgUrl } ])
})
// chat archive, pin etc.
socketEvents.on('CB:action,,chat', ({ content }: BinaryNode) => {
if(Array.isArray(content)) {
const [node] = content
executeChatModification(node)
}
})
socketEvents.on('CB:action,,user', (json: BinaryNode) => {
if(Array.isArray(json.content)) {
const user = json.content[0].attrs
if(user.id) {
user.id = jidNormalizedUser(user.id)
//ev.emit('contacts.upsert', [user])
} else {
logger.warn({ json }, 'recv unknown action')
}
}
})
// presence updates
socketEvents.on('CB:Presence', json => {
const update = applyingPresenceUpdate(json[1])
ev.emit('presence.update', update)
})
// blocklist updates
socketEvents.on('CB:Blocklist', json => {
json = json[1]
const blocklist = json.blocklist
ev.emit('blocklist.set', { blocklist })
})
socketEvents.on('ws-close', () => {
chatsDebounceTimeout.cancel()
})
return {
...sock,
sendChatsQuery,
profilePictureUrl,
chatRead,
/**
* Modify a given chat (archive, pin etc.)
* @param jid the ID of the person/group you are modifiying
*/
chatModify: async(modification: ChatModification, jid: string, chatInfo: Pick<Chat, 'mute' | 'pin'>, timestampNow?: number) => {
const chatAttrs: BinaryNode['attrs'] = { jid: jid }
let data: BinaryNode[] | undefined = undefined
timestampNow = timestampNow || unixTimestampSeconds()
const getIndexKey = (list: LastMessageList) => {
if(Array.isArray(list)) {
return list[list.length - 1].key
}
return list.messages?.[list.messages?.length - 1]?.key
}
if('archive' in modification) {
chatAttrs.type = modification.archive ? 'archive' : 'unarchive'
} else if('pin' in modification) {
chatAttrs.type = 'pin'
if(modification.pin) {
chatAttrs.pin = timestampNow.toString()
} else {
chatAttrs.previous = chatInfo.pin!.toString()
}
} else if('mute' in modification) {
chatAttrs.type = 'mute'
if(modification.mute) {
chatAttrs.mute = (timestampNow + modification.mute).toString()
} else {
chatAttrs.previous = chatInfo.mute!.toString()
}
} else if('clear' in modification) {
chatAttrs.type = 'clear'
chatAttrs.modify_tag = Math.round(Math.random() * 1000000).toString()
if(modification.clear !== 'all') {
data = modification.clear.messages.map(({ id, fromMe }) => (
{
tag: 'item',
attrs: { owner: (!!fromMe).toString(), index: id }
}
))
}
} else if('star' in modification) {
chatAttrs.type = modification.star.star ? 'star' : 'unstar'
data = modification.star.messages.map(({ id, fromMe }) => (
{
tag: 'item',
attrs: { owner: (!!fromMe).toString(), index: id }
}
))
} else if('markRead' in modification) {
const indexKey = getIndexKey(modification.lastMessages)!
return chatRead(indexKey, modification.markRead ? 0 : -1)
} else if('delete' in modification) {
chatAttrs.type = 'delete'
}
if('lastMessages' in modification) {
const indexKey = getIndexKey(modification.lastMessages)
if(indexKey) {
chatAttrs.index = indexKey.id!
chatAttrs.owner = indexKey.fromMe ? 'true' : 'false'
}
}
const node = { tag: 'chat', attrs: chatAttrs, content: data }
const response = await setQuery([node], [ WAMetric.chat, WAFlag.ignore ])
if(config.emitOwnEvents) {
// apply it and emit events
executeChatModification(node)
}
return response
},
/**
* Query whether a given number is registered on WhatsApp
* @param str phone number/jid you want to check for
* @returns undefined if the number doesn't exists, otherwise the correctly formatted jid
*/
onWhatsApp: async(str: string) => {
const { status, jid, biz } = await query({
json: ['query', 'exist', str],
requiresPhoneConnection: false
})
if(status === 200) {
return {
exists: true,
jid: jidNormalizedUser(jid),
isBusiness: biz as boolean
}
}
},
/**
* Tell someone about your presence -- online, typing, offline etc.
* @param jid the ID of the person/group who you are updating
* @param type your presence
*/
sendPresenceUpdate: (type: WAPresence, toJid?: string) => (
sendNode({
binaryTag: [WAMetric.presence, WAFlag[type]], // weird stuff WA does
json: {
tag: 'action',
attrs: { epoch: currentEpoch().toString(), type: 'set' },
content: [
{
tag: 'presence',
attrs: { type: type, to: toJid! }
}
]
}
})
),
/**
* Request updates on the presence of a user
* this returns nothing, you'll receive updates in chats.update event
* */
presenceSubscribe: async(jid: string) => (
sendNode({ json: ['action', 'presence', 'subscribe', jid] })
),
/** Query the status of the person (see groupMetadata() for groups) */
getStatus: async(jid: string) => {
const status: { status: string } = await query({ json: ['query', 'Status', jid], requiresPhoneConnection: false })
return status
},
setStatus: async(status: string) => {
const response = await setQuery(
[
{
tag: 'status',
attrs: {},
content: Buffer.from (status, 'utf-8')
}
]
)
ev.emit('contacts.update', [{ id: state.legacy!.user!.id, status }])
return response
},
/** Updates business profile. */
updateBusinessProfile: async(profile: WABusinessProfile) => {
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 }]
await query({ json, expect200: true, requiresPhoneConnection: true })
},
updateProfileName: async(name: string) => {
const response = (await setQuery(
[
{
tag: 'profile',
attrs: { name }
}
]
)) as any as {status: number, pushname: string}
if(config.emitOwnEvents) {
const user = { ...state.legacy!.user!, name }
ev.emit('connection.update', { legacy: {
...state.legacy!, user
} })
ev.emit('contacts.update', [{ id: user.id, name }])
}
return response
},
/**
* Update the profile picture
* @param jid
* @param img
*/
async updateProfilePicture(jid: string, imgBuffer: Buffer) {
jid = jidNormalizedUser (jid)
const { img } = await generateProfilePicture(imgBuffer)
const tag = this.generateMessageTag ()
const query: BinaryNode = {
tag: 'picture',
attrs: { jid: jid, id: tag, type: 'set' },
content: [
{ tag: 'image', attrs: {}, content: img },
{ tag: 'preview', attrs: {}, content: img }
]
}
const user = state.legacy?.user
const { eurl } = await this.setQuery ([query], [WAMetric.picture, 136], tag) as { eurl: string, status: number }
if(config.emitOwnEvents) {
if(jid === user?.id) {
user.imgUrl = eurl
ev.emit('connection.update', {
legacy: {
...state.legacy!,
user
}
})
}
ev.emit('contacts.update', [ { id: jid, imgUrl: eurl } ])
}
},
/**
* Add or remove user from blocklist
* @param jid the ID of the person who you are blocking/unblocking
* @param type type of operation
*/
blockUser: async(jid: string, type: 'add' | 'remove' = 'add') => {
const json = {
tag: 'block',
attrs: { type },
content: [ { tag: 'user', attrs: { jid } } ]
}
await setQuery([json], [WAMetric.block, WAFlag.ignore])
if(config.emitOwnEvents) {
ev.emit('blocklist.update', { blocklist: [jid], type })
}
},
/**
* Query Business Profile (Useful for VCards)
* @param jid Business Jid
* @returns profile object or undefined if not business account
*/
getBusinessProfile: async(jid: string) => {
jid = jidNormalizedUser(jid)
const {
profiles: [{
profile,
wid
}]
} = await query({
json: [
'query', 'businessProfile',
[ { 'wid': jid.replace('@s.whatsapp.net', '@c.us') } ],
84
],
expect200: true,
requiresPhoneConnection: false,
})
return {
...profile,
wid: jidNormalizedUser(wid)
} as WABusinessProfile
}
}
}
export default makeChatsSocket

View File

@@ -1,259 +0,0 @@
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
const sock = makeMessagesSocket(config)
const {
ev,
ws: socketEvents,
query,
generateMessageTag,
currentEpoch,
setQuery,
state
} = 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 ([
{
tag: 'group',
attrs: {
author: state.legacy?.user?.id!,
id: tag,
type: type,
jid: jid!,
subject: subject!,
},
content: participants ?
participants.map(jid => (
{ tag: 'participant', attrs: { jid } }
)) :
additionalNodes
}
], [WAMetric.group, 136], tag)
return result
}
/** Get the metadata of the group from WA */
const groupMetadataFull = async(jid: string) => {
const metadata = await query({
json: ['query', 'GroupMetadata', jid],
expect200: true
})
const meta: GroupMetadata = {
id: metadata.id,
subject: metadata.subject,
creation: +metadata.creation,
owner: metadata.owner ? jidNormalizedUser(metadata.owner) : undefined,
desc: metadata.desc,
descOwner: metadata.descOwner,
participants: metadata.participants.map(
p => ({
id: jidNormalizedUser(p.id),
admin: p.isSuperAdmin ? 'super-admin' : p.isAdmin ? 'admin' : undefined
})
),
ephemeralDuration: metadata.ephemeralDuration
}
return meta
}
/** 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() }
},
binaryTag: [WAMetric.group, WAFlag.ignore],
expect200: true
})
const participants: GroupParticipant[] = []
let desc: string | undefined
if(Array.isArray(content) && Array.isArray(content[0].content)) {
const nodes = content[0].content
for(const item of nodes) {
if(item.tag === 'participant') {
participants.push({
id: item.attrs.jid,
isAdmin: item.attrs.type === 'admin',
isSuperAdmin: false
})
} else if(item.tag === 'description') {
desc = (item.content as Buffer).toString('utf-8')
}
}
}
const meta: GroupMetadata = {
id: jid,
owner: attrs?.creator,
creation: +attrs?.create,
subject: '',
desc,
participants
}
return meta
}
socketEvents.on('CB:Chat,cmd:action', () => {
/*const data = json[1].data
if (data) {
const emitGroupParticipantsUpdate = (action: WAParticipantAction) => this.emitParticipantsUpdate
(json[1].id, data[2].participants.map(whatsappID), action)
const emitGroupUpdate = (data: Partial<WAGroupMetadata>) => this.emitGroupUpdate(json[1].id, data)
switch (data[0]) {
case "promote":
emitGroupParticipantsUpdate('promote')
break
case "demote":
emitGroupParticipantsUpdate('demote')
break
case "desc_add":
emitGroupUpdate({ ...data[2], descOwner: data[1] })
break
default:
this.logger.debug({ unhandled: true }, json)
break
}
}*/
})
return {
...sock,
groupMetadata: async(jid: string, minimal: boolean) => {
let result: GroupMetadata
if(minimal) {
result = await groupMetadataMinimal(jid)
} else {
result = await groupMetadataFull(jid)
}
return result
},
/**
* Create a group
* @param title like, the title of the group
* @param participants people to include in the group
*/
groupCreate: async(title: string, participants: string[]) => {
const response = await groupQuery('create', undefined, title, participants) as WAGroupCreateResponse
const gid = response.gid!
let metadata: GroupMetadata
try {
metadata = await groupMetadataFull(gid)
} catch(error) {
logger.warn (`error in group creation: ${error}, switching gid & checking`)
// if metadata is not available
const comps = gid.replace ('@g.us', '').split ('-')
response.gid = `${comps[0]}-${+comps[1] + 1}@g.us`
metadata = await groupMetadataFull(gid)
logger.warn (`group ID switched from ${gid} to ${response.gid}`)
}
ev.emit('chats.upsert', [
{
id: response.gid!,
name: title,
conversationTimestamp: unixTimestampSeconds(),
unreadCount: 0
}
])
return metadata
},
/**
* Leave a group
* @param jid the ID of the group
*/
groupLeave: async(id: string) => {
await groupQuery('leave', id)
ev.emit('chats.update', [ { id, readOnly: true } ])
},
/**
* Update the subject of the group
* @param {string} jid the ID of the group
* @param {string} title the new title of the group
*/
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 } ])
ev.emit('groups.update', [ { id: id, subject: title } ])
},
/**
* Update the group description
* @param {string} jid the ID of the group
* @param {string} title the new title of the group
*/
groupUpdateDescription: async(jid: string, description: string) => {
const metadata = await groupMetadataFull(jid)
const node: BinaryNode = {
tag: 'description',
attrs: { id: generateMessageID(), prev: metadata?.descId! },
content: Buffer.from(description, 'utf-8')
}
const response = await groupQuery('description', jid, undefined, undefined, [node])
ev.emit('groups.update', [ { id: jid, desc: description } ])
return response
},
/**
* Update participants in the group
* @param jid the ID of the group
* @param participants the people to add
*/
groupParticipantsUpdate: async(id: string, participants: string[], action: ParticipantAction) => {
const result: GroupModificationResponse = await groupQuery(action, id, undefined, participants)
const jids = Object.keys(result.participants || {})
ev.emit('group-participants.update', { id, participants: jids, action })
return Object.keys(result.participants || {}).map(
jid => ({ jid, status: result.participants?.[jid] })
)
},
/** Query broadcast list info */
getBroadcastListInfo: async(jid: string) => {
interface WABroadcastListInfo {
status: number
name: string
recipients?: {id: string}[]
}
const result = await query({
json: ['query', 'contact', jid],
expect200: true,
requiresPhoneConnection: true
}) as WABroadcastListInfo
const metadata: GroupMetadata = {
subject: result.name,
id: jid,
owner: state.legacy?.user?.id,
participants: result.recipients!.map(({ id }) => (
{ id: jidNormalizedUser(id), isAdmin: false, isSuperAdmin: false }
))
}
return metadata
},
groupInviteCode: async(jid: string) => {
const response = await sock.query({
json: ['query', 'inviteCode', jid],
expect200: true,
requiresPhoneConnection: false
})
return response.code as string
}
}
}
export default makeGroupsSocket

View File

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

View File

@@ -1,560 +0,0 @@
import { proto } from '../../WAProto'
import { WA_DEFAULT_EPHEMERAL } from '../Defaults'
import { AnyMessageContent, Chat, GroupMetadata, LegacySocketConfig, MediaConnInfo, MessageUpsertType, MessageUserReceipt, MessageUserReceiptUpdate, MiscMessageGenerationOptions, ParticipantAction, WAFlag, WAMessage, WAMessageCursor, WAMessageKey, WAMessageStatus, WAMessageStubType, WAMessageUpdate, WAMetric, WAUrlInfo } from '../Types'
import { assertMediaContent, downloadMediaMessage, generateWAMessage, getWAUploadToServer, MediaDownloadOptions, normalizeMessageContent, 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
} as { [_: string]: WAMessageStatus }
const makeMessagesSocket = (config: LegacySocketConfig) => {
const { logger } = config
const sock = makeChatsSocket(config)
const {
ev,
ws: socketEvents,
query,
generateMessageTag,
currentEpoch,
setQuery,
state
} = sock
let mediaConn: Promise<MediaConnInfo>
const refreshMediaConn = async(forceGet = false) => {
const media = await mediaConn
if(!media || forceGet || (new Date().getTime() - media.fetchDate.getTime()) > media.ttl * 1000) {
mediaConn = (async() => {
const { media_conn } = await query({
json: ['query', 'mediaConn'],
requiresPhoneConnection: false,
expect200: true
})
media_conn.fetchDate = new Date()
return media_conn as MediaConnInfo
})()
}
return mediaConn
}
const fetchMessagesFromWA = async(
jid: string,
count: number,
cursor?: WAMessageCursor
) => {
let key: WAMessageKey | undefined
if(cursor) {
key = 'before' in cursor ? cursor.before : cursor.after
}
const { content }:BinaryNode = await query({
json: {
tag: 'query',
attrs: {
epoch: currentEpoch().toString(),
type: 'message',
jid: jid,
kind: !cursor || 'before' in cursor ? 'before' : 'after',
count: count.toString(),
index: key?.id!,
owner: key?.fromMe === false ? 'false' : 'true',
}
},
binaryTag: [WAMetric.queryMessages, WAFlag.ignore],
expect200: false,
requiresPhoneConnection: true
})
if(Array.isArray(content)) {
return content.map(data => proto.WebMessageInfo.decode(data.content as Buffer))
}
return []
}
const updateMediaMessage = async(message: WAMessage) => {
const content = assertMediaContent(message.message)
const response: BinaryNode = await query ({
json: {
tag: 'query',
attrs: {
type: 'media',
index: message.key.id!,
owner: message.key.fromMe ? 'true' : 'false',
jid: message.key.remoteJid!,
epoch: currentEpoch().toString()
}
},
binaryTag: [WAMetric.queryMedia, WAFlag.ignore],
expect200: true,
requiresPhoneConnection: true
})
const attrs = response.attrs
Object.assign(content, attrs) // update message
ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }])
return message
}
const onMessage = (message: WAMessage, type: MessageUpsertType) => {
const jid = message.key.remoteJid!
// store chat updates in this
const chatUpdate: Partial<Chat> = {
id: jid,
}
const emitGroupUpdate = (update: Partial<GroupMetadata>) => {
ev.emit('groups.update', [ { id: jid, ...update } ])
}
const normalizedContent = normalizeMessageContent(message.message)
const protocolMessage = normalizedContent?.protocolMessage
if(
!!normalizedContent
&& !normalizedContent?.protocolMessage
&& !normalizedContent?.reactionMessage
) {
chatUpdate.conversationTimestamp = +toNumber(message.messageTimestamp)
// add to count if the message isn't from me & there exists a message
if(!message.key.fromMe) {
chatUpdate.unreadCount = 1
const participant = jidNormalizedUser(message.participant || jid)
ev.emit(
'presence.update',
{
id: jid,
presences: { [participant]: { lastKnownPresence: 'available' } }
}
)
}
}
if(normalizedContent?.reactionMessage) {
const reaction: proto.IReaction = {
...normalizedContent.reactionMessage,
key: message.key,
}
ev.emit(
'messages.reaction',
[{ reaction, key: normalizedContent.reactionMessage!.key! }]
)
}
// if it's a message to delete another message
if(protocolMessage) {
switch (protocolMessage.type) {
case proto.Message.ProtocolMessage.Type.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: key!
}
])
return
case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING:
chatUpdate.ephemeralSettingTimestamp = message.messageTimestamp
chatUpdate.ephemeralExpiration = protocolMessage.ephemeralExpiration
if(isJidGroup(jid)) {
emitGroupUpdate({ ephemeralDuration: protocolMessage.ephemeralExpiration || 0 })
}
break
default:
break
}
}
// check if the message is an action
if(message.messageStubType) {
const { user } = state.legacy!
//let actor = jidNormalizedUser (message.participant)
let participants: string[]
const emitParticipantsUpdate = (action: ParticipantAction) => (
ev.emit('group-participants.update', { id: jid, participants, action })
)
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] || 0) })
}
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
}
}
if(Object.keys(chatUpdate).length > 1) {
ev.emit('chats.update', [chatUpdate])
}
ev.emit('messages.upsert', { messages: [message], type })
}
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({
json: {
tag: 'query',
attrs: {
type: 'url',
url: text,
epoch: currentEpoch().toString()
}
},
binaryTag: [26, WAFlag.ignore],
expect200: true,
requiresPhoneConnection: false
})
const urlInfo = { ...response.attrs } as any as WAUrlInfo
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 json: BinaryNode = {
tag: 'action',
attrs: { epoch: currentEpoch().toString(), type: 'relay' },
content: [
{
tag: 'message',
attrs: {},
content: proto.WebMessageInfo.encode(message).finish()
}
]
}
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 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
})
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
.then(() => emitUpdate(finalState))
.catch(() => emitUpdate(WAMessageStatus.ERROR))
}
if(config.emitOwnEvents) {
onMessage(message, 'append')
}
}
// messages received
const messagesUpdate = (node: BinaryNode, isLatest: boolean) => {
const messages = getBinaryNodeMessages(node)
messages.reverse()
ev.emit('messages.set', { messages, isLatest })
}
socketEvents.on('CB:action,add:last', json => messagesUpdate(json, true))
socketEvents.on('CB:action,add:unread', json => messagesUpdate(json, false))
socketEvents.on('CB:action,add:before', json => messagesUpdate(json, false))
// new messages
socketEvents.on('CB:action,add:relay,message', (node: BinaryNode) => {
const msgs = getBinaryNodeMessages(node)
for(const msg of msgs) {
onMessage(msg, 'notify')
}
})
// If a message has been updated
// usually called when a video message gets its upload url, or live locations or ciphertext message gets fixed
socketEvents.on ('CB:action,add:update,message', (node: BinaryNode) => {
const msgs = getBinaryNodeMessages(node)
for(const msg of msgs) {
onMessage(msg, 'append')
}
})
// message status updates
const onMessageStatusUpdate = ({ content }: BinaryNode) => {
if(Array.isArray(content)) {
const updates: WAMessageUpdate[] = []
for(const { attrs: json } of content) {
const key: WAMessageKey = {
remoteJid: jidNormalizedUser(json.jid),
id: json.index,
fromMe: json.owner === 'true'
}
const status = STATUS_MAP[json.type]
if(status) {
updates.push({ key, update: { status } })
} else {
logger.warn({ content, key }, 'got unknown status update for message')
}
}
ev.emit('messages.update', updates)
}
}
const onMessageInfoUpdate = ([, attributes]: [string, {[_: string]: any}]) => {
let ids = attributes.id as string[] | string
if(typeof ids === 'string') {
ids = [ids]
}
let updateKey: keyof MessageUserReceipt
switch (attributes.ack.toString()) {
case '2':
updateKey = 'receiptTimestamp'
break
case '3':
updateKey = 'readTimestamp'
break
case '4':
updateKey = 'playedTimestamp'
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 || ''),
}
const userJid = jidNormalizedUser(attributes.participant || attributes.to)
const updates = ids.map<MessageUserReceiptUpdate>(id => ({
key: { ...keyPartial, id },
receipt: {
userJid,
[updateKey]: +attributes.t
}
}))
ev.emit('message-receipt.update', updates)
// for individual messages
// it means the message is marked read/delivered
if(!isJidGroup(keyPartial.remoteJid)) {
ev.emit('messages.update', ids.map(id => (
{
key: { ...keyPartial, id },
update: {
status: updateKey === 'receiptTimestamp' ? WAMessageStatus.DELIVERY_ACK : WAMessageStatus.READ
}
}
)))
}
}
socketEvents.on('CB:action,add:relay,received', onMessageStatusUpdate)
socketEvents.on('CB:action,,received', onMessageStatusUpdate)
socketEvents.on('CB:Msg', onMessageInfoUpdate)
socketEvents.on('CB:MsgInfo', onMessageInfoUpdate)
return {
...sock,
relayMessage,
waUploadToServer,
generateUrlInfo,
messageInfo: async(jid: string, messageID: string) => {
const { content }: BinaryNode = await query({
json: {
tag: 'query',
attrs: {
type: 'message_info',
index: messageID,
jid: jid,
epoch: currentEpoch().toString()
}
},
binaryTag: [WAMetric.queryRead, WAFlag.ignore],
expect200: true,
requiresPhoneConnection: true
})
const info: { [jid: string]: MessageUserReceipt } = { }
if(Array.isArray(content)) {
for(const { tag, content: innerData } of content) {
const [{ attrs }] = (innerData as BinaryNode[])
const jid = jidNormalizedUser(attrs.jid)
const recp = info[jid] || { userJid: jid }
const date = +attrs.t
switch (tag) {
case 'read':
recp.readTimestamp = date
break
case 'delivery':
recp.receiptTimestamp = date
break
}
info[jid] = recp
}
}
return Object.values(info)
},
downloadMediaMessage: async(message: WAMessage, type: 'buffer' | 'stream' = 'buffer', options: MediaDownloadOptions = { }) => {
try {
const result = await downloadMediaMessage(message, type, options)
return result
} catch(error) {
if(error.message.includes('404')) { // media needs to be updated
logger.info (`updating media of message: ${message.key.id}`)
await updateMediaMessage(message)
const result = await downloadMediaMessage(message, type, options)
return result
}
throw error
}
},
updateMediaMessage,
fetchMessagesFromWA,
/** Load a single message specified by the ID */
loadMessageFromWA: async(jid: string, id: string) => {
// 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 } }))
}
// the message after the loaded message is the message required
const [actual] = await fetchMessagesFromWA(jid, 1, { after: messages[0] && messages[0].key })
return actual
},
searchMessages: async(txt: string, inJid: string | null, count: number, page: number) => {
const node: BinaryNode = await query({
json: {
tag: 'query',
attrs: {
epoch: currentEpoch().toString(),
type: 'search',
search: txt,
count: count.toString(),
page: page.toString(),
jid: inJid!
}
},
binaryTag: [24, WAFlag.ignore],
expect200: true
}) // encrypt and send off
return {
last: node.attrs?.last === 'true',
messages: getBinaryNodeMessages(node)
}
},
sendMessage: async(
jid: string,
content: AnyMessageContent,
options: MiscMessageGenerationOptions & { waitForAck?: boolean } = { waitForAck: true }
) => {
const userJid = state.legacy?.user?.id
if(
typeof content === 'object' &&
'disappearingMessagesInChat' in content &&
typeof content['disappearingMessagesInChat'] !== 'undefined' &&
isJidGroup(jid)
) {
const { disappearingMessagesInChat } = content
const value = typeof disappearingMessagesInChat === 'boolean' ?
(disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
disappearingMessagesInChat
const tag = generateMessageTag(true)
await setQuery([
{
tag: 'group',
attrs: { id: tag, jid, type: 'prop', author: userJid! },
content: [
{ tag: 'ephemeral', attrs: { value: value.toString() } }
]
}
])
} else {
const msg = await generateWAMessage(
jid,
content,
{
logger,
userJid: userJid!,
getUrlInfo: generateUrlInfo,
upload: waUploadToServer,
mediaCache: config.mediaCache,
...options,
}
)
await relayMessage(msg, { waitForAck: !!options.waitForAck })
return msg
}
}
}
}
export default makeMessagesSocket

View File

@@ -1,431 +0,0 @@
import { Boom } from '@hapi/boom'
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:
* - simple queries (no retry mechanism, wait for connection establishment)
* - listen to messages and emit events
* - query phone connection
*/
export const makeSocket = ({
waWebSocketUrl,
connectTimeoutMs,
phoneResponseTimeMs,
logger,
agent,
keepAliveIntervalMs,
expectResponseTimeout,
}: LegacySocketConfig) => {
// for generating tags
const referenceDateSeconds = unixTimestampSeconds(new Date())
const ws = new WebSocket(waWebSocketUrl, undefined, {
origin: DEFAULT_ORIGIN,
timeout: connectTimeoutMs,
agent,
headers: {
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache',
'Host': 'web.whatsapp.com',
'Pragma': 'no-cache',
'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
}
})
ws.setMaxListeners(0)
let lastDateRecv: Date
let epoch = 0
let authInfo: { encKey: Buffer, macKey: Buffer }
let keepAliveReq: NodeJS.Timeout
let phoneCheckInterval: NodeJS.Timeout | undefined
let phoneCheckListeners = 0
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 })
}
return sendPromise.call(ws, data) as Promise<void>
}
/**
* Send a message to the WA servers
* @returns the tag attached in the message
* */
const sendNode = async(
{ json, binaryTag, tag, longTag }: SocketSendMessageOptions
) => {
tag = tag || generateMessageTag(longTag)
let data: Buffer | string
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(!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
const sign = hmacSign(buff, authInfo.macKey) // sign the message using HMAC and our macKey
data = Buffer.concat([
Buffer.from(tag + ','), // generate & prefix the message tag
Buffer.from(binaryTag), // prefix some bytes that tell whatsapp what the message is about
sign, // the HMAC sign of the message
buff, // the actual encrypted buffer
])
} else {
data = `${tag},${JSON.stringify(json)}`
}
await sendRawMessage(data)
return tag
}
const end = (error: Error | undefined) => {
logger.info({ error }, 'connection closed')
ws.removeAllListeners('close')
ws.removeAllListeners('error')
ws.removeAllListeners('open')
ws.removeAllListeners('message')
phoneCheckListeners = 0
clearInterval(keepAliveReq)
clearPhoneCheckInterval()
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
try {
ws.close()
} catch{ }
}
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)
return
}
//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')
}
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
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')
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()
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'] })
/**
* 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 })
}
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 }))
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.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
) => {
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 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()
/*
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)
)
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', () => {
startKeepAliveRequest()
logger.info('Opened WS connection to WhatsApp Web')
})
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 }
))
})
return {
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
}
return query({
json,
binaryTag,
tag,
expect200: true,
requiresPhoneConnection: true
}) as Promise<{ status: number }>
},
currentEpoch: () => epoch,
end
}
}

View File

@@ -3,15 +3,13 @@ import type { Comparable } from '@adiwajshing/keyed-db/lib/Types'
import type { Logger } from 'pino'
import { proto } from '../../WAProto'
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
import type makeLegacySocket from '../LegacySocket'
import type makeMDSocket from '../Socket'
import type { BaileysEventEmitter, Chat, ConnectionState, Contact, GroupMetadata, PresenceData, WAMessage, WAMessageCursor, WAMessageKey } from '../Types'
import { toNumber, updateMessageWithReaction, updateMessageWithReceipt } from '../Utils'
import { jidNormalizedUser } from '../WABinary'
import makeOrderedDictionary from './make-ordered-dictionary'
type LegacyWASocket = ReturnType<typeof makeLegacySocket>
type AnyWASocket = ReturnType<typeof makeMDSocket>
type WASocket = ReturnType<typeof makeMDSocket>
export const waChatKey = (pin: boolean) => ({
key: (c: Chat) => (pin ? (c.pin ? '1' : '0') : '') + (c.archive ? '0' : '1') + (c.conversationTimestamp ? c.conversationTimestamp.toString(16).padStart(8, '0') : '') + c.id,
@@ -269,13 +267,8 @@ export default (
presences,
bind,
/** loads messages from the store, if not found -- uses the legacy connection */
loadMessages: async(jid: string, count: number, cursor: WAMessageCursor, sock: LegacyWASocket | undefined) => {
loadMessages: async(jid: string, count: number, cursor: WAMessageCursor) => {
const list = assertMessageList(jid)
const retrieve = async(count: number, cursor: WAMessageCursor) => {
const result = await sock?.fetchMessagesFromWA(jid, count, cursor)
return result || []
}
const mode = !cursor || 'before' in cursor ? 'before' : 'after'
const cursorKey = !!cursor ? ('before' in cursor ? cursor.before : cursor.after) : undefined
const cursorValue = cursorKey ? list.get(cursorKey.id!) : undefined
@@ -292,41 +285,19 @@ export default (
const diff = count - messages.length
if(diff < 0) {
messages = messages.slice(-count) // get the last X messages
} else if(diff > 0) {
const [fMessage] = messages
const cursor = { before: fMessage?.key || cursorKey }
const extra = await retrieve (diff, cursor)
// add to DB
for(let i = extra.length - 1; i >= 0;i--) {
list.upsert(extra[i], 'prepend')
}
messages.splice(0, 0, ...extra)
}
} else {
messages = await retrieve(count, cursor)
messages = []
}
return messages
},
loadMessage: async(jid: string, id: string, sock: LegacyWASocket | undefined) => {
let message = messages[jid]?.get(id)
if(!message) {
message = await sock?.loadMessageFromWA(jid, id)
}
loadMessage: async(jid: string, id: string) => messages[jid]?.get(id),
mostRecentMessage: async(jid: string) => {
const message: WAMessage | undefined = messages[jid]?.array.slice(-1)[0]
return message
},
mostRecentMessage: async(jid: string, sock: LegacyWASocket | undefined) => {
let message: WAMessage | undefined = messages[jid]?.array.slice(-1)[0]
if(!message) {
const items = await sock?.fetchMessagesFromWA(jid, 1, undefined)
message = items?.[0]
}
return message
},
fetchImageUrl: async(jid: string, sock: AnyWASocket | undefined) => {
fetchImageUrl: async(jid: string, sock: WASocket | undefined) => {
const contact = contacts[jid]
if(!contact) {
return sock?.profilePictureUrl(jid)
@@ -338,7 +309,7 @@ export default (
return contact.imgUrl
},
fetchGroupMetadata: async(jid: string, sock: AnyWASocket | undefined) => {
fetchGroupMetadata: async(jid: string, sock: WASocket | undefined) => {
if(!groupMetadata[jid]) {
const metadata = await sock?.groupMetadata(jid)
if(metadata) {
@@ -348,28 +319,20 @@ export default (
return groupMetadata[jid]
},
fetchBroadcastListInfo: async(jid: string, sock: LegacyWASocket | undefined) => {
if(!groupMetadata[jid]) {
const metadata = await sock?.getBroadcastListInfo(jid)
if(metadata) {
groupMetadata[jid] = metadata
}
}
// fetchBroadcastListInfo: async(jid: string, sock: WASocket | undefined) => {
// if(!groupMetadata[jid]) {
// const metadata = await sock?.getBroadcastListInfo(jid)
// if(metadata) {
// groupMetadata[jid] = metadata
// }
// }
return groupMetadata[jid]
},
fetchMessageReceipts: async({ remoteJid, id }: WAMessageKey, sock: LegacyWASocket | undefined) => {
// return groupMetadata[jid]
// },
fetchMessageReceipts: async({ remoteJid, id }: WAMessageKey) => {
const list = messages[remoteJid!]
const msg = list?.get(id!)
let receipts = msg?.userReceipt
if(!receipts) {
receipts = await sock?.messageInfo(remoteJid!, id!)
if(msg) {
msg.userReceipt = receipts
}
}
return receipts
return msg?.userReceipt
},
toJSON,
fromJSON,

View File

@@ -8,11 +8,11 @@ import { GroupMetadata, ParticipantAction } from './GroupMetadata'
import { MessageUpsertType, MessageUserReceiptUpdate, WAMessage, WAMessageKey, WAMessageUpdate } from './Message'
import { ConnectionState } from './State'
export type BaileysEventMap<T> = {
export type BaileysEventMap = {
/** connection state has been updated -- WS closed, opened, connecting etc. */
'connection.update': Partial<ConnectionState>
/** credentials updated -- some metadata, keys or something */
'creds.update': Partial<T>
'creds.update': Partial<AuthenticationCreds>
/** set chats (history sync), chats are reverse chronologically sorted */
'chats.set': { chats: Chat[], isLatest: boolean }
/** set messages (history sync), messages are reverse chronologically sorted */
@@ -69,13 +69,11 @@ export type BufferedEventData = {
groupUpdates: { [jid: string]: Partial<GroupMetadata> }
}
export type BaileysEvent = keyof BaileysEventMap<any>
export type BaileysEvent = keyof BaileysEventMap
export interface CommonBaileysEventEmitter<Creds> {
on<T extends keyof BaileysEventMap<Creds>>(event: T, listener: (arg: BaileysEventMap<Creds>[T]) => void): void
off<T extends keyof BaileysEventMap<Creds>>(event: T, listener: (arg: BaileysEventMap<Creds>[T]) => void): void
removeAllListeners<T extends keyof BaileysEventMap<Creds>>(event: T): void
emit<T extends keyof BaileysEventMap<Creds>>(event: T, arg: BaileysEventMap<Creds>[T]): boolean
}
export type BaileysEventEmitter = CommonBaileysEventEmitter<AuthenticationCreds>
export interface BaileysEventEmitter {
on<T extends keyof BaileysEventMap>(event: T, listener: (arg: BaileysEventMap[T]) => void): void
off<T extends keyof BaileysEventMap>(event: T, listener: (arg: BaileysEventMap[T]) => void): void
removeAllListeners<T extends keyof BaileysEventMap>(event: T): void
emit<T extends keyof BaileysEventMap>(event: T, arg: BaileysEventMap[T]): boolean
}

View File

@@ -1,82 +0,0 @@
import { BinaryNode } from '../WABinary'
import { CommonBaileysEventEmitter } from './Events'
import { CommonSocketConfig } from './Socket'
export interface LegacyAuthenticationCreds {
clientID: string
serverToken: string
clientToken: string
encKey: Buffer
macKey: Buffer
}
/** used for binary messages */
export enum WAMetric {
debugLog = 1,
queryResume = 2,
liveLocation = 3,
queryMedia = 4,
queryChat = 5,
queryContact = 6,
queryMessages = 7,
presence = 8,
presenceSubscribe = 9,
group = 10,
read = 11,
chat = 12,
received = 13,
picture = 14,
status = 15,
message = 16,
queryActions = 17,
block = 18,
queryGroup = 19,
queryPreview = 20,
queryEmoji = 21,
queryRead = 22,
queryVCard = 29,
queryStatus = 30,
queryStatusUpdate = 31,
queryLiveLocation = 33,
queryLabel = 36,
queryQuickReply = 39
}
/** used for binary messages */
export enum WAFlag {
available = 160,
other = 136, // don't know this one
ignore = 1 << 7,
acknowledge = 1 << 6,
unavailable = 1 << 4,
expires = 1 << 3,
composing = 1 << 2,
recording = 1 << 2,
paused = 1 << 2
}
/** Tag used with binary queries */
export type WATag = [WAMetric, WAFlag]
export type SocketSendMessageOptions = {
json: BinaryNode | any[]
binaryTag?: WATag
tag?: string
longTag?: boolean
}
export type SocketQueryOptions = SocketSendMessageOptions & {
timeoutMs?: number
expect200?: boolean
requiresPhoneConnection?: boolean
}
export type LegacySocketConfig = CommonSocketConfig & {
auth?: LegacyAuthenticationCreds
/** max time for the phone to respond to a connectivity test */
phoneResponseTimeMs: number
/** max time for WA server to respond before error with 422 */
expectResponseTimeout: number
}
export type LegacyBaileysEventEmitter = CommonBaileysEventEmitter<LegacyAuthenticationCreds>

View File

@@ -3,12 +3,16 @@ import type { Agent } from 'https'
import type NodeCache from 'node-cache'
import type { Logger } from 'pino'
import type { URL } from 'url'
import { proto } from '../../WAProto'
import { AuthenticationState, TransactionCapabilityOptions } from './Auth'
import { MediaConnInfo } from './Message'
export type WAVersion = [number, number, number]
export type WABrowserDescription = [string, string, string]
export type CommonSocketConfig = {
export type MessageRetryMap = { [msgId: string]: number }
export type SocketConfig = {
/** the WS url to connect to WA */
waWebSocketUrl: string | URL
/** Fails the connection if the socket times out in this interval */
@@ -39,4 +43,34 @@ export type CommonSocketConfig = {
retryRequestDelayMs: number
/** time to wait for the generation of the next QR in ms */
qrTimeout?: number;
/** provide an auth state object to maintain the auth state */
auth: AuthenticationState
/** By default true, should history messages be downloaded and processed */
downloadHistory: boolean
/** transaction capability options for SignalKeyStore */
transactionOpts: TransactionCapabilityOptions
/** provide a cache to store a user's device list */
userDevicesCache?: NodeCache
/** marks the client as online whenever the socket successfully connects */
markOnlineOnConnect: boolean
/**
* map to store the retry counts for failed messages;
* used to determine whether to retry a message or not */
msgRetryCounterMap?: MessageRetryMap
/** width for link preview images */
linkPreviewImageThumbnailWidth: number
/** Should Baileys ask the phone for full history, will be received async */
syncFullHistory: boolean
/** Should baileys fire init queries automatically, default true */
fireInitQueries: boolean
/**
* generate a high quality link preview,
* entails uploading the jpegThumbnail to WA
* */
generateHighQualityLinkPreview: boolean
/**
* fetch a message from your store
* implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried
* */
getMessage: (key: proto.IMessageKey) => Promise<proto.IMessage | undefined>
}

View File

@@ -4,51 +4,13 @@ export * from './Chat'
export * from './Contact'
export * from './State'
export * from './Message'
export * from './Legacy'
export * from './Socket'
export * from './Events'
export * from './Product'
export * from './Call'
import type NodeCache from 'node-cache'
import { proto } from '../../WAProto'
import { AuthenticationState, TransactionCapabilityOptions } from './Auth'
import { CommonSocketConfig } from './Socket'
export type MessageRetryMap = { [msgId: string]: number }
export type SocketConfig = CommonSocketConfig & {
/** provide an auth state object to maintain the auth state */
auth: AuthenticationState
/** By default true, should history messages be downloaded and processed */
downloadHistory: boolean
/** transaction capability options for SignalKeyStore */
transactionOpts: TransactionCapabilityOptions
/** provide a cache to store a user's device list */
userDevicesCache?: NodeCache
/** marks the client as online whenever the socket successfully connects */
markOnlineOnConnect: boolean
/**
* map to store the retry counts for failed messages;
* used to determine whether to retry a message or not */
msgRetryCounterMap?: MessageRetryMap
/** width for link preview images */
linkPreviewImageThumbnailWidth: number
/** Should Baileys ask the phone for full history, will be received async */
syncFullHistory: boolean
/** Should baileys fire init queries automatically, default true */
fireInitQueries: boolean
/**
* generate a high quality link preview,
* entails uploading the jpegThumbnail to WA
* */
generateHighQualityLinkPreview: boolean
/**
* fetch a message from your store
* implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried
* */
getMessage: (key: proto.IMessageKey) => Promise<proto.IMessage | undefined>
}
import { AuthenticationState } from './Auth'
import { SocketConfig } from './Socket'
export type UserFacingSocketConfig = Partial<SocketConfig> & { auth: AuthenticationState }

View File

@@ -2,7 +2,7 @@ import EventEmitter from 'events'
import { createReadStream } from 'fs'
import { writeFile } from 'fs/promises'
import { createInterface } from 'readline'
import type { CommonBaileysEventEmitter } from '../Types'
import type { BaileysEventEmitter } from '../Types'
import { delay } from './generics'
import { makeMutex } from './make-mutex'
@@ -11,7 +11,7 @@ import { makeMutex } from './make-mutex'
* @param ev The event emitter to read events from
* @param filename File to save to
*/
export const captureEventStream = (ev: CommonBaileysEventEmitter<any>, filename: string) => {
export const captureEventStream = (ev: BaileysEventEmitter, filename: string) => {
const oldEmit = ev.emit
// write mutex so data is appended in order
const writeMutex = makeMutex()
@@ -36,7 +36,7 @@ export const captureEventStream = (ev: CommonBaileysEventEmitter<any>, filename:
* @param delayIntervalMs delay between each event emit
*/
export const readAndEmitEventStream = (filename: string, delayIntervalMs: number = 0) => {
const ev = new EventEmitter() as CommonBaileysEventEmitter<any>
const ev = new EventEmitter() as BaileysEventEmitter
const fireEvents = async() => {
// from: https://stackoverflow.com/questions/6156501/read-a-file-one-line-at-a-time-in-node-js

View File

@@ -1,7 +1,7 @@
import EventEmitter from 'events'
import { Logger } from 'pino'
import { proto } from '../../WAProto'
import { AuthenticationCreds, BaileysEvent, BaileysEventEmitter, BaileysEventMap, BufferedEventData, Chat, Contact, WAMessage, WAMessageStatus } from '../Types'
import { BaileysEvent, BaileysEventEmitter, BaileysEventMap, BufferedEventData, Chat, Contact, WAMessage, WAMessageStatus } from '../Types'
import { updateMessageWithReaction, updateMessageWithReceipt } from './messages'
import { isRealMessage, shouldIncrementChatUnread } from './process-message'
@@ -28,7 +28,7 @@ type BufferableEvent = typeof BUFFERABLE_EVENT[number]
* this can make processing events extremely efficient -- since everything
* can be done in a single transaction
*/
type BaileysEventData = Partial<BaileysEventMap<AuthenticationCreds>>
type BaileysEventData = Partial<BaileysEventMap>
const BUFFERABLE_EVENT_SET = new Set<BaileysEvent>(BUFFERABLE_EVENT)
@@ -109,7 +109,7 @@ export const makeEventBuffer = (logger: Logger): BaileysBufferableEventEmitter =
ev.off('event', listener)
}
},
emit<T extends BaileysEvent>(event: BaileysEvent, evData: BaileysEventMap<AuthenticationCreds>[T]) {
emit<T extends BaileysEvent>(event: BaileysEvent, evData: BaileysEventMap[T]) {
if(isBuffering && BUFFERABLE_EVENT_SET.has(event)) {
append(data, event as any, evData, logger)
return true
@@ -233,7 +233,7 @@ function append<E extends BufferableEvent>(
break
case 'contacts.update':
const contactUpdates = eventData as BaileysEventMap<any>['contacts.update']
const contactUpdates = eventData as BaileysEventMap['contacts.update']
for(const update of contactUpdates) {
const id = update.id!
// merge into prior upsert
@@ -249,7 +249,7 @@ function append<E extends BufferableEvent>(
break
case 'messages.upsert':
const { messages, type } = eventData as BaileysEventMap<any>['messages.upsert']
const { messages, type } = eventData as BaileysEventMap['messages.upsert']
for(const message of messages) {
const key = stringifyMessageKey(message.key)
const existing = data.messageUpserts[key]
@@ -273,7 +273,7 @@ function append<E extends BufferableEvent>(
break
case 'messages.update':
const msgUpdates = eventData as BaileysEventMap<any>['messages.update']
const msgUpdates = eventData as BaileysEventMap['messages.update']
for(const { key, update } of msgUpdates) {
const keyStr = stringifyMessageKey(key)
const existing = data.messageUpserts[keyStr]
@@ -294,7 +294,7 @@ function append<E extends BufferableEvent>(
break
case 'messages.delete':
const deleteData = eventData as BaileysEventMap<any>['messages.delete']
const deleteData = eventData as BaileysEventMap['messages.delete']
if('keys' in deleteData) {
const { keys } = deleteData
for(const key of keys) {
@@ -315,7 +315,7 @@ function append<E extends BufferableEvent>(
break
case 'messages.reaction':
const reactions = eventData as BaileysEventMap<any>['messages.reaction']
const reactions = eventData as BaileysEventMap['messages.reaction']
for(const { key, reaction } of reactions) {
const keyStr = stringifyMessageKey(key)
const existing = data.messageUpserts[keyStr]
@@ -330,7 +330,7 @@ function append<E extends BufferableEvent>(
break
case 'message-receipt.update':
const receipts = eventData as BaileysEventMap<any>['message-receipt.update']
const receipts = eventData as BaileysEventMap['message-receipt.update']
for(const { key, receipt } of receipts) {
const keyStr = stringifyMessageKey(key)
const existing = data.messageUpserts[keyStr]
@@ -345,7 +345,7 @@ function append<E extends BufferableEvent>(
break
case 'groups.update':
const groupUpdates = eventData as BaileysEventMap<any>['groups.update']
const groupUpdates = eventData as BaileysEventMap['groups.update']
for(const update of groupUpdates) {
const id = update.id!
const groupUpdate = data.groupUpdates[id] || { }

View File

@@ -5,7 +5,7 @@ import { platform, release } from 'os'
import { Logger } from 'pino'
import { proto } from '../../WAProto'
import { version as baileysVersion } from '../Defaults/baileys-version.json'
import { BaileysEventMap, CommonBaileysEventEmitter, DisconnectReason, WACallUpdateType, WAVersion } from '../Types'
import { BaileysEventEmitter, BaileysEventMap, DisconnectReason, WACallUpdateType, WAVersion } from '../Types'
import { BinaryNode, getAllBinaryNodeChildren } from '../WABinary'
const PLATFORM_MAP = {
@@ -165,9 +165,9 @@ export async function promiseTimeout<T>(ms: number | undefined, promise: (resolv
// generate a random ID to attach to a message
export const generateMessageID = () => 'BAE5' + randomBytes(6).toString('hex').toUpperCase()
export function bindWaitForEvent<T extends keyof BaileysEventMap<any>>(ev: CommonBaileysEventEmitter<any>, event: T) {
return async(check: (u: BaileysEventMap<any>[T]) => boolean | undefined, timeoutMs?: number) => {
let listener: (item: BaileysEventMap<any>[T]) => void
export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEventEmitter, event: T) {
return async(check: (u: BaileysEventMap[T]) => boolean | undefined, timeoutMs?: number) => {
let listener: (item: BaileysEventMap[T]) => void
let closeListener: any
await (
promiseTimeout(
@@ -200,9 +200,9 @@ export function bindWaitForEvent<T extends keyof BaileysEventMap<any>>(ev: Commo
}
}
export const bindWaitForConnectionUpdate = (ev: CommonBaileysEventEmitter<any>) => bindWaitForEvent(ev, 'connection.update')
export const bindWaitForConnectionUpdate = (ev: BaileysEventEmitter) => bindWaitForEvent(ev, 'connection.update')
export const printQRIfNecessaryListener = (ev: CommonBaileysEventEmitter<any>, logger: Logger) => {
export const printQRIfNecessaryListener = (ev: BaileysEventEmitter, logger: Logger) => {
ev.on('connection.update', async({ qr }) => {
if(qr) {
const QR = await import('qrcode-terminal')

View File

@@ -10,7 +10,6 @@ export * from './history'
export * from './chat-utils'
export * from './lt-hash'
export * from './auth-utils'
export * from './legacy-msgs'
export * from './baileys-event-stream'
export * from './use-single-file-auth-state'
export * from './use-multi-file-auth-state'

View File

@@ -1,198 +0,0 @@
import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto'
import { AuthenticationCreds, Contact, CurveKeyPair, DisconnectReason, LegacyAuthenticationCreds, WATag } from '../Types'
import { decodeBinaryNodeLegacy, jidNormalizedUser } from '../WABinary'
import { aesDecrypt, Curve, hkdf, hmacSign } from './crypto'
import { BufferJSON } from './generics'
export const newLegacyAuthCreds = () => ({
clientID: randomBytes(16).toString('base64')
}) as LegacyAuthenticationCreds
export const decodeWAMessage = (
message: Buffer | string,
auth: { macKey: Buffer, encKey: Buffer },
fromMe: boolean = false
) => {
let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message
if(commaIndex < 0) {
throw new Boom('invalid message', { data: message })
} // if there was no comma, then this message must be not be valid
if(message[commaIndex + 1] === ',') {
commaIndex += 1
}
let data = message.slice(commaIndex + 1, message.length)
// get the message tag.
// If a query was done, the server will respond with the same message tag we sent the query with
const messageTag: string = message.slice(0, commaIndex).toString()
let json: any
let tags: WATag | undefined
if(data.length) {
const possiblyEnc = (data.length > 32 && data.length % 16 === 0)
if(typeof data === 'string' || !possiblyEnc) {
json = JSON.parse(data.toString()) // parse the JSON
} else {
try {
json = JSON.parse(data.toString())
} catch{
const { macKey, encKey } = auth || {}
if(!macKey || !encKey) {
throw new Boom('recieved encrypted buffer when auth creds unavailable', { data: message, statusCode: DisconnectReason.badSession })
}
/*
If the data recieved was not a JSON, then it must be an encrypted message.
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
*/
if(fromMe) {
tags = [data[0], data[1]]
data = data.slice(2, data.length)
}
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
data = data.slice(32, data.length) // the actual message
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey
if(checksum.equals(computedChecksum)) {
// the checksum the server sent, must match the one we computed for the message to be valid
const decrypted = aesDecrypt(data, encKey) // decrypt using AES
json = decodeBinaryNodeLegacy(decrypted, { index: 0 }) // decode the binary message into a JSON array
} else {
throw new Boom('Bad checksum', {
data: {
received: checksum.toString('hex'),
computed: computedChecksum.toString('hex'),
data: data.slice(0, 80).toString(),
tag: messageTag,
message: message.slice(0, 80).toString()
},
statusCode: DisconnectReason.badSession
})
}
}
}
}
return [messageTag, json, tags] as const
}
/**
* Once the QR code is scanned and we can validate our connection, or we resolved the challenge when logging back in
* @private
* @param json
*/
export const validateNewConnection = (
json: { [_: string]: any },
auth: LegacyAuthenticationCreds,
curveKeys: CurveKeyPair
) => {
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
const onValidationSuccess = () => {
const user: Contact = {
id: jidNormalizedUser(json.wid),
name: json.pushname
}
return { user, auth, phone: json.phone }
}
if(!json.secret) {
// if we didn't get a secret, we don't need it, we're validated
if(json.clientToken && json.clientToken !== auth.clientToken) {
auth = { ...auth, clientToken: json.clientToken }
}
if(json.serverToken && json.serverToken !== auth.serverToken) {
auth = { ...auth, serverToken: json.serverToken }
}
return onValidationSuccess()
}
const secret = Buffer.from(json.secret, 'base64')
if(secret.length !== 144) {
throw new Error ('incorrect secret length received: ' + secret.length)
}
// generate shared key from our private key & the secret shared by the server
const sharedKey = Curve.sharedKey(curveKeys.private, secret.slice(0, 32))
// expand the key to 80 bytes using HKDF
const expandedKey = hkdf(sharedKey as Buffer, 80, { })
// perform HMAC validation.
const hmacValidationKey = expandedKey.slice(32, 64)
const hmacValidationMessage = Buffer.concat([secret.slice(0, 32), secret.slice(64, secret.length)])
const hmac = hmacSign(hmacValidationMessage, hmacValidationKey)
if(!hmac.equals(secret.slice(32, 64))) {
// if the checksums didn't match
throw new Boom('HMAC validation failed', { statusCode: 400 })
}
// computed HMAC should equal secret[32:64]
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
// they are encrypted using key: expandedKey[0:32]
const encryptedAESKeys = Buffer.concat([
expandedKey.slice(64, expandedKey.length),
secret.slice(64, secret.length),
])
const decryptedKeys = aesDecrypt(encryptedAESKeys, expandedKey.slice(0, 32))
// set the credentials
auth = {
encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages
macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages
clientToken: json.clientToken,
serverToken: json.serverToken,
clientID: auth.clientID,
}
return onValidationSuccess()
}
export const computeChallengeResponse = (challenge: string, auth: LegacyAuthenticationCreds) => {
const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string
const signed = hmacSign(bytes, auth.macKey).toString('base64') // sign the challenge string with our macKey
return ['admin', 'challenge', signed, auth.serverToken, auth.clientID] // prepare to send this signed string with the serverToken & clientID
}
export const useSingleFileLegacyAuthState = (file: string) => {
// require fs here so that in case "fs" is not available -- the app does not crash
const { readFileSync, writeFileSync, existsSync } = require('fs')
let state: LegacyAuthenticationCreds
if(existsSync(file)) {
state = JSON.parse(
readFileSync(file, { encoding: 'utf-8' }),
BufferJSON.reviver
)
if(typeof state.encKey === 'string') {
state.encKey = Buffer.from(state.encKey, 'base64')
}
if(typeof state.macKey === 'string') {
state.macKey = Buffer.from(state.macKey, 'base64')
}
} else {
state = newLegacyAuthCreds()
}
return {
state,
saveState: () => {
const str = JSON.stringify(state, BufferJSON.replacer, 2)
writeFileSync(file, str)
}
}
}
export const getAuthenticationCredsType = (creds: LegacyAuthenticationCreds | AuthenticationCreds) => {
if('clientID' in creds && !!creds.clientID) {
return 'legacy'
}
if('noiseKey' in creds && !!creds.noiseKey) {
return 'md'
}
}

View File

@@ -12,7 +12,7 @@ import { Readable, Transform } from 'stream'
import { URL } from 'url'
import { proto } from '../../WAProto'
import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP } from '../Defaults'
import { BaileysEventMap, CommonSocketConfig, DownloadableMessage, MediaConnInfo, MediaDecryptionKeyInfo, MediaType, MessageType, WAGenericMediaMessage, WAMediaUpload, WAMediaUploadFunction, WAMessageContent } from '../Types'
import { BaileysEventMap, DownloadableMessage, MediaConnInfo, MediaDecryptionKeyInfo, MediaType, MessageType, SocketConfig, WAGenericMediaMessage, WAMediaUpload, WAMediaUploadFunction, WAMessageContent } from '../Types'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, jidNormalizedUser } from '../WABinary'
import { aesDecryptGCM, aesEncryptGCM, hkdf } from './crypto'
import { generateMessageID } from './generics'
@@ -514,7 +514,7 @@ export function extensionForMediaMessage(message: WAMessageContent) {
return extension
}
export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }: CommonSocketConfig, refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>): WAMediaUploadFunction => {
export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }: SocketConfig, refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>): WAMediaUploadFunction => {
return async(stream, { mediaType, fileEncSha256B64, timeoutMs }) => {
const { default: axios } = await import('axios')
// send a query JSON to obtain the url & auth token to upload our media
@@ -646,7 +646,7 @@ export const encryptMediaRetryRequest = (
export const decodeMediaRetryNode = (node: BinaryNode) => {
const rmrNode = getBinaryNodeChild(node, 'rmr')!
const event: BaileysEventMap<any>['messages.media-update'][number] = {
const event: BaileysEventMap['messages.media-update'][number] = {
key: {
id: node.attrs.id,
remoteJid: rmrNode.attrs.jid,

View File

@@ -1,205 +0,0 @@
export const TAGS = {
LIST_EMPTY: 0,
STREAM_END: 2,
DICTIONARY_0: 236,
DICTIONARY_1: 237,
DICTIONARY_2: 238,
DICTIONARY_3: 239,
LIST_8: 248,
LIST_16: 249,
JID_PAIR: 250,
HEX_8: 251,
BINARY_8: 252,
BINARY_20: 253,
BINARY_32: 254,
NIBBLE_8: 255,
SINGLE_BYTE_MAX: 256,
PACKED_MAX: 254,
AD_JID: 247,
}
export const DOUBLE_BYTE_TOKENS = []
export const SINGLE_BYTE_TOKENS = [
null,
null,
null,
'200',
'400',
'404',
'500',
'501',
'502',
'action',
'add',
'after',
'archive',
'author',
'available',
'battery',
'before',
'body',
'broadcast',
'chat',
'clear',
'code',
'composing',
'contacts',
'count',
'create',
'debug',
'delete',
'demote',
'duplicate',
'encoding',
'error',
'false',
'filehash',
'from',
'g.us',
'group',
'groups_v2',
'height',
'id',
'image',
'in',
'index',
'invis',
'item',
'jid',
'kind',
'last',
'leave',
'live',
'log',
'media',
'message',
'mimetype',
'missing',
'modify',
'name',
'notification',
'notify',
'out',
'owner',
'participant',
'paused',
'picture',
'played',
'presence',
'preview',
'promote',
'query',
'raw',
'read',
'receipt',
'received',
'recipient',
'recording',
'relay',
'remove',
'response',
'resume',
'retry',
's.whatsapp.net',
'seconds',
'set',
'size',
'status',
'subject',
'subscribe',
't',
'text',
'to',
'true',
'type',
'unarchive',
'unavailable',
'url',
'user',
'value',
'web',
'width',
'mute',
'read_only',
'admin',
'creator',
'short',
'update',
'powersave',
'checksum',
'epoch',
'block',
'previous',
'409',
'replaced',
'reason',
'spam',
'modify_tag',
'message_info',
'delivery',
'emoji',
'title',
'description',
'canonical-url',
'matched-text',
'star',
'unstar',
'media_key',
'filename',
'identity',
'unread',
'page',
'page_count',
'search',
'media_message',
'security',
'call_log',
'profile',
'ciphertext',
'invite',
'gif',
'vcard',
'frequent',
'privacy',
'blacklist',
'whitelist',
'verify',
'location',
'document',
'elapsed',
'revoke_invite',
'expiration',
'unsubscribe',
'disable',
'vname',
'old_jid',
'new_jid',
'announcement',
'locked',
'prop',
'label',
'color',
'call',
'offer',
'call-id',
'quick_reply',
'sticker',
'pay_t',
'accept',
'reject',
'sticker_pack',
'invalid',
'canceled',
'missed',
'connected',
'result',
'audio',
'video',
'recent',
]
export const TOKEN_MAP: { [token: string]: { dict?: number, index: number } } = { }
for(let i = 0;i < SINGLE_BYTE_TOKENS.length;i++) {
TOKEN_MAP[SINGLE_BYTE_TOKENS[i]!] = { index: i }
}

View File

@@ -1,13 +0,0 @@
import { decodeDecompressedBinaryNode } from '../decode'
import { encodeBinaryNode } from '../encode'
import type { BinaryNode } from '../types'
import * as constants from './constants'
export const encodeBinaryNodeLegacy = (node: BinaryNode) => {
return encodeBinaryNode(node, constants, [])
}
export const decodeBinaryNodeLegacy = (data: Buffer, indexRef: { index: number }) => {
return decodeDecompressedBinaryNode(data, constants, indexRef)
}

View File

@@ -2,5 +2,4 @@ export * from './encode'
export * from './decode'
export * from './generic-utils'
export * from './jid-utils'
export * from './types'
export * from './Legacy'
export * from './types'

View File

@@ -1,4 +1,3 @@
import makeWALegacySocket from './LegacySocket'
import makeWASocket from './Socket'
export * from '../WAProto'
@@ -8,12 +7,6 @@ export * from './Store'
export * from './Defaults'
export * from './WABinary'
export type WALegacySocket = ReturnType<typeof makeWALegacySocket>
export { makeWALegacySocket }
export type WASocket = ReturnType<typeof makeWASocket>
export type AnyWASocket = WASocket | WALegacySocket
export default makeWASocket