mobile: deprecation.

This commit is contained in:
Rajeh Taher
2024-10-14 03:39:46 +03:00
parent 647f8d767f
commit 61a0ff3178
18 changed files with 47 additions and 533 deletions

View File

@@ -1,3 +1,2 @@
export * from './abstract-socket-client'
export * from './mobile-socket-client'
export * from './web-socket-client'
export * from './types'
export * from './websocket'

View File

@@ -1,66 +0,0 @@
import { connect, Socket } from 'net'
import { AbstractSocketClient } from './abstract-socket-client'
export class MobileSocketClient extends AbstractSocketClient {
protected socket: Socket | null = null
get isOpen(): boolean {
return this.socket?.readyState === 'open'
}
get isClosed(): boolean {
return this.socket === null || this.socket?.readyState === 'closed'
}
get isClosing(): boolean {
return this.socket === null || this.socket?.readyState === 'closed'
}
get isConnecting(): boolean {
return this.socket?.readyState === 'opening'
}
async connect(): Promise<void> {
if(this.socket) {
return
}
if(this.config.agent) {
throw new Error('There are not support for proxy agent for mobile connection')
} else {
this.socket = connect({
host: this.url.hostname,
port: Number(this.url.port) || 443
})
}
this.socket.setMaxListeners(0)
const events = ['close', 'connect', 'data', 'drain', 'end', 'error', 'lookup', 'ready', 'timeout']
for(const event of events) {
this.socket?.on(event, (...args: any[]) => this.emit(event, ...args))
}
this.socket.on('data', (...args: any[]) => this.emit('message', ...args))
this.socket.on('ready', (...args: any[]) => this.emit('open', ...args))
}
async close(): Promise<void> {
if(!this.socket) {
return
}
return new Promise<void>(resolve => {
this.socket!.end(resolve)
this.socket = null
})
}
send(str: string | Uint8Array, cb?: (err?: Error) => void): boolean {
if(this.socket === null) {
return false
}
return this.socket.write(str, undefined, cb)
}
}

View File

@@ -1,6 +1,6 @@
import WebSocket from 'ws'
import { DEFAULT_ORIGIN } from '../../Defaults'
import { AbstractSocketClient } from './abstract-socket-client'
import { AbstractSocketClient } from './types'
export class WebSocketClient extends AbstractSocketClient {

View File

@@ -3,12 +3,12 @@ import NodeCache from 'node-cache'
import { proto } from '../../WAProto'
import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults'
import { ALL_WA_PATCH_NAMES, ChatModification, ChatMutation, LTHashState, MessageUpsertType, PresenceData, SocketConfig, WABusinessHoursConfig, WABusinessProfile, WAMediaUpload, WAMessage, WAPatchCreate, WAPatchName, WAPresence, WAPrivacyCallValue, WAPrivacyGroupAddValue, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '../Types'
import { LabelActionBody } from '../Types/Label'
import { chatModificationToAppPatch, ChatMutationMap, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils'
import { makeMutex } from '../Utils/make-mutex'
import processMessage from '../Utils/process-message'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary'
import { makeSocket } from './socket'
import { Label, LabelActionBody } from '../Types/Label'
const MAX_SYNC_ATTEMPTS = 2
@@ -221,7 +221,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
/** update the profile picture for yourself or a group */
const updateProfilePicture = async(jid: string, content: WAMediaUpload) => {
let targetJid;
let targetJid
if(!jid) {
throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update')
}
@@ -251,7 +251,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
/** remove the profile picture for yourself or a group */
const removeProfilePicture = async(jid: string) => {
let targetJid;
let targetJid
if(!jid) {
throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update')
}
@@ -777,6 +777,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
authState.creds.lastPropHash = propsNode?.attrs?.hash
ev.emit('creds.update', authState.creds)
}
props = reduceBinaryNodeToDictionary(propsNode, 'prop')
}
@@ -1002,7 +1003,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
// if we don't have the app state key
// we keep buffering events until we finally have
// the key and can sync the messages
if(!authState.creds?.myAppStateKeyId && !config.mobile) {
// todo scrutinize
if(!authState.creds?.myAppStateKeyId) {
ev.buffer()
needToFlushWithAppStateSync = true
}

View File

@@ -1,10 +1,10 @@
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
import { UserFacingSocketConfig } from '../Types'
import { makeRegistrationSocket as _makeSocket } from './registration'
import { makeBusinessSocket } from './business'
// export the last socket layer
const makeWASocket = (config: UserFacingSocketConfig) => (
_makeSocket({
makeBusinessSocket({
...DEFAULT_CONNECTION_CONFIG,
...config
})

View File

@@ -469,17 +469,15 @@ export const makeMessagesSocket = (config: SocketConfig) => {
await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } })
} else {
const { user: meUser, device: meDevice } = jidDecode(meId)!
const { user: meUser } = jidDecode(meId)!
if(!participant) {
devices.push({ user })
// do not send message to self if the device is 0 (mobile)
if(!(additionalAttributes?.['category'] === 'peer' && user === meUser)) {
if(meDevice !== undefined && meDevice !== 0) {
devices.push({ user: meUser })
}
if(user !== meUser) {
devices.push({ user: meUser })
}
if(additionalAttributes?.['category'] !== 'peer') {
const additionalDevices = await getUSyncDevices([ meId, jid ], !!useUserDevicesCache, true)
devices.push(...additionalDevices)
}

View File

@@ -1,250 +0,0 @@
/* eslint-disable camelcase */
import axios, { AxiosRequestConfig } from 'axios'
import { MOBILE_REGISTRATION_ENDPOINT, MOBILE_TOKEN, MOBILE_USERAGENT, REGISTRATION_PUBLIC_KEY } from '../Defaults'
import { KeyPair, SignedKeyPair, SocketConfig } from '../Types'
import { aesEncryptGCM, Curve, md5 } from '../Utils/crypto'
import { jidEncode } from '../WABinary'
import { makeBusinessSocket } from './business'
function urlencode(str: string) {
return str.replace(/-/g, '%2d').replace(/_/g, '%5f').replace(/~/g, '%7e')
}
const validRegistrationOptions = (config: RegistrationOptions) => config?.phoneNumberCountryCode &&
config.phoneNumberNationalNumber &&
config.phoneNumberMobileCountryCode
export const makeRegistrationSocket = (config: SocketConfig) => {
const sock = makeBusinessSocket(config)
const register = async(code: string) => {
if(!validRegistrationOptions(config.auth.creds.registration)) {
throw new Error('please specify the registration options')
}
const result = await mobileRegister({ ...sock.authState.creds, ...sock.authState.creds.registration as RegistrationOptions, code }, config.options)
sock.authState.creds.me = {
id: jidEncode(result.login!, 's.whatsapp.net'),
name: '~'
}
sock.authState.creds.registered = true
sock.ev.emit('creds.update', sock.authState.creds)
return result
}
const requestRegistrationCode = async(registrationOptions?: RegistrationOptions) => {
registrationOptions = registrationOptions || config.auth.creds.registration
if(!validRegistrationOptions(registrationOptions)) {
throw new Error('Invalid registration options')
}
sock.authState.creds.registration = registrationOptions
sock.ev.emit('creds.update', sock.authState.creds)
return mobileRegisterCode({ ...config.auth.creds, ...registrationOptions }, config.options)
}
return {
...sock,
register,
requestRegistrationCode,
}
}
// Backup_token: Base64.getEncoder().encodeToString(Arrays.copyOfRange(Base64.getDecoder().decode(UUID.randomUUID().toString().replace('-','')),0,15))
export interface RegistrationData {
registrationId: number
signedPreKey: SignedKeyPair
noiseKey: KeyPair
signedIdentityKey: KeyPair
identityId: Buffer
phoneId: string
deviceId: string
backupToken: Buffer
}
export interface RegistrationOptions {
/** your phone number */
phoneNumber?: string
/** the country code of your phone number */
phoneNumberCountryCode: string
/** your phone number without country code */
phoneNumberNationalNumber: string
/** the country code of your mobile network
* @see {@link https://de.wikipedia.org/wiki/Mobile_Country_Code}
*/
phoneNumberMobileCountryCode: string
/** the network code of your mobile network
* @see {@link https://de.wikipedia.org/wiki/Mobile_Network_Code}
*/
phoneNumberMobileNetworkCode: string
/**
* How to send the one time code
*/
method?: 'sms' | 'voice' | 'captcha'
/**
* The captcha code if it was requested
*/
captcha?: string
}
export type RegistrationParams = RegistrationData & RegistrationOptions
function convertBufferToUrlHex(buffer: Buffer) {
var id = ''
buffer.forEach((x) => {
// encode random identity_id buffer as percentage url encoding
id += `%${x.toString(16).padStart(2, '0').toLowerCase()}`
})
return id
}
export function registrationParams(params: RegistrationParams) {
const e_regid = Buffer.alloc(4)
e_regid.writeInt32BE(params.registrationId)
const e_skey_id = Buffer.alloc(3)
e_skey_id.writeInt16BE(params.signedPreKey.keyId)
params.phoneNumberCountryCode = params.phoneNumberCountryCode.replace('+', '').trim()
params.phoneNumberNationalNumber = params.phoneNumberNationalNumber.replace(/[/-\s)(]/g, '').trim()
return {
cc: params.phoneNumberCountryCode,
in: params.phoneNumberNationalNumber,
Rc: '0',
lg: 'en',
lc: 'GB',
mistyped: '6',
authkey: Buffer.from(params.noiseKey.public).toString('base64url'),
e_regid: e_regid.toString('base64url'),
e_keytype: 'BQ',
e_ident: Buffer.from(params.signedIdentityKey.public).toString('base64url'),
// e_skey_id: e_skey_id.toString('base64url'),
e_skey_id: 'AAAA',
e_skey_val: Buffer.from(params.signedPreKey.keyPair.public).toString('base64url'),
e_skey_sig: Buffer.from(params.signedPreKey.signature).toString('base64url'),
fdid: params.phoneId,
network_ratio_type: '1',
expid: params.deviceId,
simnum: '1',
hasinrc: '1',
pid: Math.floor(Math.random() * 1000).toString(),
id: convertBufferToUrlHex(params.identityId),
backup_token: convertBufferToUrlHex(params.backupToken),
token: md5(Buffer.concat([MOBILE_TOKEN, Buffer.from(params.phoneNumberNationalNumber)])).toString('hex'),
fraud_checkpoint_code: params.captcha,
}
}
/**
* Requests a registration code for the given phone number.
*/
export function mobileRegisterCode(params: RegistrationParams, fetchOptions?: AxiosRequestConfig) {
return mobileRegisterFetch('/code', {
params: {
...registrationParams(params),
mcc: `${params.phoneNumberMobileCountryCode}`.padStart(3, '0'),
mnc: `${params.phoneNumberMobileNetworkCode || '001'}`.padStart(3, '0'),
sim_mcc: '000',
sim_mnc: '000',
method: params?.method || 'sms',
reason: '',
hasav: '1'
},
...fetchOptions,
})
}
export function mobileRegisterExists(params: RegistrationParams, fetchOptions?: AxiosRequestConfig) {
return mobileRegisterFetch('/exist', {
params: registrationParams(params),
...fetchOptions
})
}
/**
* Registers the phone number on whatsapp with the received OTP code.
*/
export async function mobileRegister(params: RegistrationParams & { code: string }, fetchOptions?: AxiosRequestConfig) {
//const result = await mobileRegisterFetch(`/reg_onboard_abprop?cc=${params.phoneNumberCountryCode}&in=${params.phoneNumberNationalNumber}&rc=0`)
return mobileRegisterFetch('/register', {
params: { ...registrationParams(params), code: params.code.replace('-', '') },
...fetchOptions,
})
}
/**
* Encrypts the given string as AEAD aes-256-gcm with the public whatsapp key and a random keypair.
*/
export function mobileRegisterEncrypt(data: string) {
const keypair = Curve.generateKeyPair()
const key = Curve.sharedKey(keypair.private, REGISTRATION_PUBLIC_KEY)
const buffer = aesEncryptGCM(Buffer.from(data), new Uint8Array(key), Buffer.alloc(12), Buffer.alloc(0))
return Buffer.concat([Buffer.from(keypair.public), buffer]).toString('base64url')
}
export async function mobileRegisterFetch(path: string, opts: AxiosRequestConfig = {}) {
let url = `${MOBILE_REGISTRATION_ENDPOINT}${path}`
if(opts.params) {
const parameter = [] as string[]
for(const param in opts.params) {
if(opts.params[param] !== null && opts.params[param] !== undefined) {
parameter.push(param + '=' + urlencode(opts.params[param]))
}
}
url += `?${parameter.join('&')}`
delete opts.params
}
if(!opts.headers) {
opts.headers = {}
}
opts.headers['User-Agent'] = MOBILE_USERAGENT
const response = await axios(url, opts)
var json = response.data
if(response.status > 300 || json.reason) {
throw json
}
if(json.status && !['ok', 'sent'].includes(json.status)) {
throw json
}
return json as ExistsResponse
}
export interface ExistsResponse {
status: 'fail' | 'sent'
voice_length?: number
voice_wait?: number
sms_length?: number
sms_wait?: number
reason?: 'incorrect' | 'missing_param' | 'code_checkpoint'
login?: string
flash_type?: number
ab_hash?: string
ab_key?: string
exp_cfg?: string
lid?: string
image_blob?: string
audio_blob?: string
}

View File

@@ -8,9 +8,6 @@ import {
DEF_TAG_PREFIX,
INITIAL_PREKEY_COUNT,
MIN_PREKEY_COUNT,
MOBILE_ENDPOINT,
MOBILE_NOISE_HEADER,
MOBILE_PORT,
NOISE_WA_HEADER
} from '../Defaults'
import { DisconnectReason, SocketConfig } from '../Types'
@@ -24,7 +21,6 @@ import {
derivePairingCodeKey,
generateLoginNode,
generateMdTagPrefix,
generateMobileNode,
generateRegistrationNode,
getCodeFromWSError,
getErrorCodeFromStreamError,
@@ -45,7 +41,7 @@ import {
jidEncode,
S_WHATSAPP_NET
} from '../WABinary'
import { MobileSocketClient, WebSocketClient } from './Client'
import { WebSocketClient } from './Client'
/**
* Connects to WA servers and performs:
@@ -69,19 +65,18 @@ export const makeSocket = (config: SocketConfig) => {
makeSignalRepository,
} = config
let url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
config.mobile = config.mobile || url.protocol === 'tcp:'
if(config.mobile && url.protocol !== 'tcp:') {
url = new URL(`tcp://${MOBILE_ENDPOINT}:${MOBILE_PORT}`)
if(config.mobile || url.protocol === 'tcp:') {
throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut })
}
if(!config.mobile && url.protocol === 'wss' && authState?.creds?.routingInfo) {
if(url.protocol === 'wss' && authState?.creds?.routingInfo) {
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'))
}
const ws = config.socket ? config.socket : config.mobile ? new MobileSocketClient(url, config) : new WebSocketClient(url, config)
const ws = config.socket ? config.socket : new WebSocketClient(url, config)
ws.connect()
@@ -91,8 +86,7 @@ export const makeSocket = (config: SocketConfig) => {
/** WA noise protocol wrapper */
const noise = makeNoiseHandler({
keyPair: ephemeralKeyPair,
NOISE_HEADER: config.mobile ? MOBILE_NOISE_HEADER : NOISE_WA_HEADER,
mobile: config.mobile,
NOISE_HEADER: NOISE_WA_HEADER,
logger,
routingInfo: authState?.creds?.routingInfo
})
@@ -247,9 +241,7 @@ export const makeSocket = (config: SocketConfig) => {
const keyEnc = noise.processHandshake(handshake, creds.noiseKey)
let node: proto.IClientPayload
if(config.mobile) {
node = generateMobileNode(config)
} else if(!creds.me) {
if(!creds.me) {
node = generateRegistrationNode(creds, config)
logger.info({ node }, 'not logged in, attempting registration...')
} else {