Files
Baileys/src/Utils/generics.ts
João Lucas de Oliveira Lopes 8391c02e0b [READY FOR MERGE] Implement newsletter (#1532)
* feat: implement basic newsletter functionality with socket integration and event handling

* feat: enhance media handling for newsletters with raw media upload support

* feat: working updatePicture, removePicure, adminCount, mute, Unmute

* fix: fetchMessages

* chore: cleanup

* fix: update newsletter metadata path and query ID for consistency. newsletterMetadata works now

* chore: enhance newsletter metadata parsing and error handling

* fix: correct DELETE QueryId value in Newsletter.ts

* chore: split mex stuffs to own file

* chore: remove as any
2025-06-30 23:22:09 -03:00

450 lines
12 KiB
TypeScript

import { Boom } from '@hapi/boom'
import axios, { AxiosRequestConfig } from 'axios'
import { createHash, randomBytes } from 'crypto'
import { platform, release } from 'os'
import { proto } from '../../WAProto'
import { version as baileysVersion } from '../Defaults/baileys-version.json'
import {
BaileysEventEmitter,
BaileysEventMap,
BrowsersMap,
ConnectionState,
DisconnectReason,
WACallUpdateType,
WAVersion
} from '../Types'
import { BinaryNode, getAllBinaryNodeChildren, jidDecode } from '../WABinary'
const PLATFORM_MAP = {
aix: 'AIX',
darwin: 'Mac OS',
win32: 'Windows',
android: 'Android',
freebsd: 'FreeBSD',
openbsd: 'OpenBSD',
sunos: 'Solaris'
}
export const Browsers: BrowsersMap = {
ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
macOS: browser => ['Mac OS', browser, '14.4.1'],
baileys: browser => ['Baileys', browser, '6.5.0'],
windows: browser => ['Windows', browser, '10.0.22631'],
/** The appropriate browser based on your OS & release */
appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
}
export const getPlatformId = (browser: string) => {
const platformType = proto.DeviceProps.PlatformType[browser.toUpperCase()]
return platformType ? platformType.toString() : '1' //chrome
}
export const BufferJSON = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 getKeyAuthor = (key: proto.IMessageKey | undefined | null, meId = 'me') =>
(key?.fromMe ? meId : key?.participant || key?.remoteJid) || ''
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 | null | undefined): number =>
typeof t === 'object' && t ? ('toNumber' in t ? t.toNumber() : (t as Long).low) : t || 0
/** unix timestamp of a date in seconds */
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime() / 1000)
export type DebouncedTimeout = ReturnType<typeof debouncedTimeout>
export const debouncedTimeout = (intervalMs = 1000, task?: () => void) => {
let timeout: NodeJS.Timeout | undefined
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<void> = 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<T>(
ms: number | undefined,
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 <ms> 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<T>
}
// inspired from whatsmeow code
// https://github.com/tulir/whatsmeow/blob/64bc969fbe78d31ae0dd443b8d4c80a5d026d07a/send.go#L42
export const generateMessageIDV2 = (userId?: string): string => {
const data = Buffer.alloc(8 + 20 + 16)
data.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 1000)))
if (userId) {
const id = jidDecode(userId)
if (id?.user) {
data.write(id.user, 8)
data.write('@c.us', 8 + id.user.length)
}
}
const random = randomBytes(16)
random.copy(data, 28)
const hash = createHash('sha256').update(data).digest()
return '3EB0' + hash.toString('hex').toUpperCase().substring(0, 18)
}
// generate a random ID to attach to a message
export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase()
export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEventEmitter, event: T) {
return async (check: (u: BaileysEventMap[T]) => Promise<boolean | undefined>, timeoutMs?: number) => {
let listener: (item: BaileysEventMap[T]) => void
let closeListener: (state: Partial<ConnectionState>) => void
await promiseTimeout<void>(timeoutMs, (resolve, reject) => {
closeListener = ({ connection, lastDisconnect }) => {
if (connection === 'close') {
reject(
lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
)
}
}
ev.on('connection.update', closeListener)
listener = async update => {
if (await check(update)) {
resolve()
}
}
ev.on(event, listener)
}).finally(() => {
ev.off(event, listener)
ev.off('connection.update', closeListener)
})
}
}
export const bindWaitForConnectionUpdate = (ev: BaileysEventEmitter) => bindWaitForEvent(ev, 'connection.update')
/**
* 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 (options: AxiosRequestConfig<{}> = {}) => {
const URL = 'https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/Defaults/baileys-version.json'
try {
const result = await axios.get<{ version: WAVersion }>(URL, {
...options,
responseType: 'json'
})
return {
version: result.data.version,
isLatest: true
}
} catch (error) {
return {
version: baileysVersion as WAVersion,
isLatest: false,
error
}
}
}
/**
* A utility that fetches the latest web version of whatsapp.
* Use to ensure your WA connection is always on the latest version
*/
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
try {
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
...options,
responseType: 'json'
})
const regex = /\\?"client_revision\\?":\s*(\d+)/
const match = data.match(regex)
if (!match?.[1]) {
return {
version: baileysVersion as WAVersion,
isLatest: false,
error: {
message: 'Could not find client revision in the fetched content'
}
}
}
const clientRevision = match[1]
return {
version: [2, 3000, +clientRevision] as WAVersion,
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.Status } = {
sender: proto.WebMessageInfo.Status.SERVER_ACK,
played: proto.WebMessageInfo.Status.PLAYED,
read: proto.WebMessageInfo.Status.READ,
'read-self': proto.WebMessageInfo.Status.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.Status.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 {
//fired when accepted/rejected/timeout/caller hangs up
status = 'terminate'
}
break
case 'reject':
status = 'reject'
break
case 'accept':
status = 'accept'
break
default:
status = 'ringing'
break
}
return status
}
const UNEXPECTED_SERVER_CODE_TEXT = 'Unexpected server response: '
export const getCodeFromWSError = (error: Error) => {
let statusCode = 500
if (error?.message?.includes(UNEXPECTED_SERVER_CODE_TEXT)) {
const code = +error?.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length)
if (!Number.isNaN(code) && code >= 400) {
statusCode = code
}
} else if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any)?.code?.startsWith('E') ||
error?.message?.includes('timed out')
) {
// handle ETIMEOUT, ENOTFOUND etc
statusCode = 408
}
return statusCode
}
/**
* Is the given platform WA business
* @param platform AuthenticationCreds.platform
*/
export const isWABusinessPlatform = (platform: string) => {
return platform === 'smbi' || platform === 'smba'
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function trimUndefined(obj: { [_: string]: any }) {
for (const key in obj) {
if (typeof obj[key] === 'undefined') {
delete obj[key]
}
}
return obj
}
const CROCKFORD_CHARACTERS = '123456789ABCDEFGHJKLMNPQRSTVWXYZ'
export function bytesToCrockford(buffer: Buffer): string {
let value = 0
let bitCount = 0
const crockford: string[] = []
for (const element of buffer) {
value = (value << 8) | (element & 0xff)
bitCount += 8
while (bitCount >= 5) {
crockford.push(CROCKFORD_CHARACTERS.charAt((value >>> (bitCount - 5)) & 31))
bitCount -= 5
}
}
if (bitCount > 0) {
crockford.push(CROCKFORD_CHARACTERS.charAt((value << (5 - bitCount)) & 31))
}
return crockford.join('')
}
export function encodeNewsletterMessage(message: proto.IMessage): Uint8Array {
return proto.Message.encode(message).finish()
}