import { Boom } from '@hapi/boom' import axios from 'axios' import { randomBytes } from 'crypto' import { platform, release } from 'os' import { Logger } from 'pino' import { proto } from '../../WAProto' import { version as baileysVersion } from '../Defaults/baileys-version.json' import { CommonBaileysEventEmitter, ConnectionState, DisconnectReason, WACallUpdateType, WAVersion } from '../Types' import { BinaryNode, getAllBinaryNodeChildren } from '../WABinary' const PLATFORM_MAP = { 'aix': 'AIX', 'darwin': 'Mac OS', 'win32': 'Windows', 'android': 'Android' } export const Browsers = { ubuntu: browser => ['Ubuntu', browser, '20.0.04'] as [string, string, string], macOS: browser => ['Mac OS', browser, '10.15.7'] as [string, string, string], baileys: browser => ['Baileys', browser, '4.0.0'] as [string, string, string], /** The appropriate browser based on your OS & release */ appropriate: browser => [ PLATFORM_MAP[platform()] || 'Ubuntu', browser, release() ] as [string, string, string] } export const BufferJSON = { replacer: (k, value: any) => { if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') { return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') } } return value }, reviver: (_, value: any) => { if(typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) { const val = value.data || value.value return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val || []) } return value } } export const writeRandomPadMax16 = (msg: Uint8Array) => { const pad = randomBytes(1) pad[0] &= 0xf if(!pad[0]) { pad[0] = 0xf } return Buffer.concat([msg, Buffer.alloc(pad[0], pad[0])]) } export const unpadRandomMax16 = (e: Uint8Array | Buffer) => { const t = new Uint8Array(e) if(0 === t.length) { throw new Error('unpadPkcs7 given empty bytes') } var r = t[t.length - 1] if(r > t.length) { throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`) } return new Uint8Array(t.buffer, t.byteOffset, t.length - r) } export const encodeWAMessage = (message: proto.IMessage) => ( writeRandomPadMax16( proto.Message.encode(message).finish() ) ) export const generateRegistrationId = (): number => { return Uint16Array.from(randomBytes(2))[0] & 16383 } export const encodeBigEndian = (e: number, t = 4) => { let r = e const a = new Uint8Array(t) for(let i = t - 1; i >= 0; i--) { a[i] = 255 & r r >>>= 8 } return a } export const toNumber = (t: Long | number) => ((typeof t === 'object' && t) ? ('toNumber' in t ? t.toNumber() : (t as any).low) : t) export function shallowChanges (old: T, current: T, { lookForDeletedKeys }: {lookForDeletedKeys: boolean}): Partial { const changes: Partial = {} for(const key in current) { if(old[key] !== current[key]) { changes[key] = current[key] || null } } if(lookForDeletedKeys) { for(const key in old) { if(!changes[key] && old[key] !== current[key]) { changes[key] = current[key] || null } } } return changes } /** unix timestamp of a date in seconds */ export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime() / 1000) export type DebouncedTimeout = ReturnType export const debouncedTimeout = (intervalMs: number = 1000, task: () => void = undefined) => { let timeout: NodeJS.Timeout return { start: (newIntervalMs?: number, newTask?: () => void) => { task = newTask || task intervalMs = newIntervalMs || intervalMs timeout && clearTimeout(timeout) timeout = setTimeout(task, intervalMs) }, cancel: () => { timeout && clearTimeout(timeout) timeout = undefined }, setTask: (newTask: () => void) => task = newTask, setInterval: (newInterval: number) => intervalMs = newInterval } } export const delay = (ms: number) => delayCancellable (ms).delay export const delayCancellable = (ms: number) => { const stack = new Error().stack let timeout: NodeJS.Timeout let reject: (error) => void const delay: Promise = new Promise((resolve, _reject) => { timeout = setTimeout(resolve, ms) reject = _reject }) const cancel = () => { clearTimeout (timeout) reject( new Boom('Cancelled', { statusCode: 500, data: { stack } }) ) } return { delay, cancel } } export async function promiseTimeout(ms: number, promise: (resolve: (v?: T)=>void, reject: (error) => void) => void) { if(!ms) { return new Promise (promise) } const stack = new Error().stack // Create a promise that rejects in milliseconds const { delay, cancel } = delayCancellable (ms) const p = new Promise ((resolve, reject) => { delay .then(() => reject( new Boom('Timed Out', { statusCode: DisconnectReason.timedOut, data: { stack } }) )) .catch (err => reject(err)) promise (resolve, reject) }) .finally (cancel) return p as Promise } // generate a random ID to attach to a message export const generateMessageID = () => 'BAE5' + randomBytes(6).toString('hex').toUpperCase() export const bindWaitForConnectionUpdate = (ev: CommonBaileysEventEmitter) => ( async(check: (u: Partial) => boolean, timeoutMs?: number) => { let listener: (item: Partial) => void await ( promiseTimeout( timeoutMs, (resolve, reject) => { listener = (update) => { if(check(update)) { resolve() } else if(update.connection === 'close') { reject(update.lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })) } } ev.on('connection.update', listener) } ) .finally(() => ( ev.off('connection.update', listener) )) ) } ) export const printQRIfNecessaryListener = (ev: CommonBaileysEventEmitter, logger: Logger) => { ev.on('connection.update', async({ qr }) => { if(qr) { const QR = await import('qrcode-terminal') .catch(err => { logger.error('QR code terminal not added as dependency') }) QR?.generate(qr, { small: true }) } }) } /** * utility that fetches latest baileys version from the master branch. * Use to ensure your WA connection is always on the latest version */ export const fetchLatestBaileysVersion = async() => { const URL = 'https://raw.githubusercontent.com/adiwajshing/Baileys/master/src/Defaults/baileys-version.json' try { const result = await axios.get<{ version: WAVersion }>(URL, { responseType: 'json' }) return { version: result.data.version, isLatest: true } } catch(error) { return { version: baileysVersion as WAVersion, isLatest: false, error } } } /** unique message tag prefix for MD clients */ export const generateMdTagPrefix = () => { const bytes = randomBytes(4) return `${bytes.readUInt16BE()}.${bytes.readUInt16BE(2)}-` } const STATUS_MAP: { [_: string]: proto.WebMessageInfo.WebMessageInfoStatus } = { 'played': proto.WebMessageInfo.WebMessageInfoStatus.PLAYED, 'read': proto.WebMessageInfo.WebMessageInfoStatus.READ, 'read-self': proto.WebMessageInfo.WebMessageInfoStatus.READ } /** * Given a type of receipt, returns what the new status of the message should be * @param type type from receipt */ export const getStatusFromReceiptType = (type: string | undefined) => { const status = STATUS_MAP[type] if(typeof type === 'undefined') { return proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK } return status } const CODE_MAP: { [_: string]: DisconnectReason } = { conflict: DisconnectReason.connectionReplaced } /** * Stream errors generally provide a reason, map that to a baileys DisconnectReason * @param reason the string reason given, eg. "conflict" */ export const getErrorCodeFromStreamError = (node: BinaryNode) => { const [reasonNode] = getAllBinaryNodeChildren(node) let reason = reasonNode?.tag || 'unknown' const statusCode = +(node.attrs.code || CODE_MAP[reason] || DisconnectReason.badSession) if(statusCode === DisconnectReason.restartRequired) { reason = 'restart required' } return { reason, statusCode } } export const getCallStatusFromNode = ({ tag, attrs }: BinaryNode) => { let status: WACallUpdateType switch (tag) { case 'offer': case 'offer_notice': status = 'offer' break case 'terminate': if(attrs.reason === 'timeout') { status = 'timeout' } else { status = 'reject' } break case 'reject': status = 'reject' break case 'accept': status = 'accept' break default: status = 'ringing' break } return status }