mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
chore: remove legacy code
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] || { }
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user