diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index 6852b55..93334c4 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -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', diff --git a/src/LegacySocket/auth.ts b/src/LegacySocket/auth.ts deleted file mode 100644 index 0736b54..0000000 --- a/src/LegacySocket/auth.ts +++ /dev/null @@ -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) => { - 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 \ No newline at end of file diff --git a/src/LegacySocket/business.ts b/src/LegacySocket/business.ts deleted file mode 100644 index 566eb24..0000000 --- a/src/LegacySocket/business.ts +++ /dev/null @@ -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( - 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 \ No newline at end of file diff --git a/src/LegacySocket/chats.ts b/src/LegacySocket/chats.ts deleted file mode 100644 index 7318d86..0000000 --- a/src/LegacySocket/chats.ts +++ /dev/null @@ -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['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 = { - 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, 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 \ No newline at end of file diff --git a/src/LegacySocket/groups.ts b/src/LegacySocket/groups.ts deleted file mode 100644 index defd7d3..0000000 --- a/src/LegacySocket/groups.ts +++ /dev/null @@ -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) => 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 \ No newline at end of file diff --git a/src/LegacySocket/index.ts b/src/LegacySocket/index.ts deleted file mode 100644 index 93bb492..0000000 --- a/src/LegacySocket/index.ts +++ /dev/null @@ -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) => ( - _makeLegacySocket({ - ...DEFAULT_LEGACY_CONNECTION_CONFIG, - ...config - }) -) - -export default makeLegacySocket \ No newline at end of file diff --git a/src/LegacySocket/messages.ts b/src/LegacySocket/messages.ts deleted file mode 100644 index 1ad7061..0000000 --- a/src/LegacySocket/messages.ts +++ /dev/null @@ -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 - 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 = { - id: jid, - } - - const emitGroupUpdate = (update: Partial) => { - 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(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 \ No newline at end of file diff --git a/src/LegacySocket/socket.ts b/src/LegacySocket/socket.ts deleted file mode 100644 index 8f6eab8..0000000 --- a/src/LegacySocket/socket.ts +++ /dev/null @@ -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 - } - - /** - * 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 - } -} \ No newline at end of file diff --git a/src/Store/make-in-memory-store.ts b/src/Store/make-in-memory-store.ts index 8d68cc1..03abc3d 100644 --- a/src/Store/make-in-memory-store.ts +++ b/src/Store/make-in-memory-store.ts @@ -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 -type AnyWASocket = ReturnType +type WASocket = ReturnType 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, diff --git a/src/Types/Events.ts b/src/Types/Events.ts index 5d064c0..341a129 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -8,11 +8,11 @@ import { GroupMetadata, ParticipantAction } from './GroupMetadata' import { MessageUpsertType, MessageUserReceiptUpdate, WAMessage, WAMessageKey, WAMessageUpdate } from './Message' import { ConnectionState } from './State' -export type BaileysEventMap = { +export type BaileysEventMap = { /** connection state has been updated -- WS closed, opened, connecting etc. */ 'connection.update': Partial /** credentials updated -- some metadata, keys or something */ - 'creds.update': Partial + 'creds.update': Partial /** 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 } } -export type BaileysEvent = keyof BaileysEventMap +export type BaileysEvent = keyof BaileysEventMap -export interface CommonBaileysEventEmitter { - on>(event: T, listener: (arg: BaileysEventMap[T]) => void): void - off>(event: T, listener: (arg: BaileysEventMap[T]) => void): void - removeAllListeners>(event: T): void - emit>(event: T, arg: BaileysEventMap[T]): boolean -} - -export type BaileysEventEmitter = CommonBaileysEventEmitter \ No newline at end of file +export interface BaileysEventEmitter { + on(event: T, listener: (arg: BaileysEventMap[T]) => void): void + off(event: T, listener: (arg: BaileysEventMap[T]) => void): void + removeAllListeners(event: T): void + emit(event: T, arg: BaileysEventMap[T]): boolean +} \ No newline at end of file diff --git a/src/Types/Legacy.ts b/src/Types/Legacy.ts deleted file mode 100644 index b475070..0000000 --- a/src/Types/Legacy.ts +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index f328b5e..78625d0 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -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 } diff --git a/src/Types/index.ts b/src/Types/index.ts index d1fb641..78ea6f4 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -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 -} +import { AuthenticationState } from './Auth' +import { SocketConfig } from './Socket' export type UserFacingSocketConfig = Partial & { auth: AuthenticationState } diff --git a/src/Utils/baileys-event-stream.ts b/src/Utils/baileys-event-stream.ts index 6518f69..26fa610 100644 --- a/src/Utils/baileys-event-stream.ts +++ b/src/Utils/baileys-event-stream.ts @@ -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, 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, filename: * @param delayIntervalMs delay between each event emit */ export const readAndEmitEventStream = (filename: string, delayIntervalMs: number = 0) => { - const ev = new EventEmitter() as CommonBaileysEventEmitter + 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 diff --git a/src/Utils/event-buffer.ts b/src/Utils/event-buffer.ts index 62ae013..9ed314b 100644 --- a/src/Utils/event-buffer.ts +++ b/src/Utils/event-buffer.ts @@ -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> +type BaileysEventData = Partial const BUFFERABLE_EVENT_SET = new Set(BUFFERABLE_EVENT) @@ -109,7 +109,7 @@ export const makeEventBuffer = (logger: Logger): BaileysBufferableEventEmitter = ev.off('event', listener) } }, - emit(event: BaileysEvent, evData: BaileysEventMap[T]) { + emit(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( break case 'contacts.update': - const contactUpdates = eventData as BaileysEventMap['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( break case 'messages.upsert': - const { messages, type } = eventData as BaileysEventMap['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( break case 'messages.update': - const msgUpdates = eventData as BaileysEventMap['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( break case 'messages.delete': - const deleteData = eventData as BaileysEventMap['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( break case 'messages.reaction': - const reactions = eventData as BaileysEventMap['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( break case 'message-receipt.update': - const receipts = eventData as BaileysEventMap['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( break case 'groups.update': - const groupUpdates = eventData as BaileysEventMap['groups.update'] + const groupUpdates = eventData as BaileysEventMap['groups.update'] for(const update of groupUpdates) { const id = update.id! const groupUpdate = data.groupUpdates[id] || { } diff --git a/src/Utils/generics.ts b/src/Utils/generics.ts index 5d5e005..4fad4e4 100644 --- a/src/Utils/generics.ts +++ b/src/Utils/generics.ts @@ -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(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>(ev: CommonBaileysEventEmitter, event: T) { - return async(check: (u: BaileysEventMap[T]) => boolean | undefined, timeoutMs?: number) => { - let listener: (item: BaileysEventMap[T]) => void +export function bindWaitForEvent(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>(ev: Commo } } -export const bindWaitForConnectionUpdate = (ev: CommonBaileysEventEmitter) => bindWaitForEvent(ev, 'connection.update') +export const bindWaitForConnectionUpdate = (ev: BaileysEventEmitter) => bindWaitForEvent(ev, 'connection.update') -export const printQRIfNecessaryListener = (ev: CommonBaileysEventEmitter, logger: Logger) => { +export const printQRIfNecessaryListener = (ev: BaileysEventEmitter, logger: Logger) => { ev.on('connection.update', async({ qr }) => { if(qr) { const QR = await import('qrcode-terminal') diff --git a/src/Utils/index.ts b/src/Utils/index.ts index e6383de..3aa06bb 100644 --- a/src/Utils/index.ts +++ b/src/Utils/index.ts @@ -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' diff --git a/src/Utils/legacy-msgs.ts b/src/Utils/legacy-msgs.ts deleted file mode 100644 index 7dfd0d5..0000000 --- a/src/Utils/legacy-msgs.ts +++ /dev/null @@ -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' - } -} \ No newline at end of file diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index 9463f72..103aa77 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -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): WAMediaUploadFunction => { +export const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }: SocketConfig, refreshMediaConn: (force: boolean) => Promise): 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['messages.media-update'][number] = { + const event: BaileysEventMap['messages.media-update'][number] = { key: { id: node.attrs.id, remoteJid: rmrNode.attrs.jid, diff --git a/src/WABinary/Legacy/constants.ts b/src/WABinary/Legacy/constants.ts deleted file mode 100644 index 79e4b38..0000000 --- a/src/WABinary/Legacy/constants.ts +++ /dev/null @@ -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 } -} \ No newline at end of file diff --git a/src/WABinary/Legacy/index.ts b/src/WABinary/Legacy/index.ts deleted file mode 100644 index a2a6f4a..0000000 --- a/src/WABinary/Legacy/index.ts +++ /dev/null @@ -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) -} diff --git a/src/WABinary/index.ts b/src/WABinary/index.ts index f22933f..ecf0c9f 100644 --- a/src/WABinary/index.ts +++ b/src/WABinary/index.ts @@ -2,5 +2,4 @@ export * from './encode' export * from './decode' export * from './generic-utils' export * from './jid-utils' -export * from './types' -export * from './Legacy' \ No newline at end of file +export * from './types' \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4fcdeb2..e9eed52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 - -export { makeWALegacySocket } - export type WASocket = ReturnType -export type AnyWASocket = WASocket | WALegacySocket - export default makeWASocket \ No newline at end of file