mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
feat: native-mobile-api
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||
import { UserFacingSocketConfig } from '../Types'
|
||||
import { makeBusinessSocket as _makeSocket } from './business'
|
||||
import { makeRegistrationSocket as _makeSocket } from './registration'
|
||||
|
||||
// export the last socket layer
|
||||
const makeWASocket = (config: UserFacingSocketConfig) => (
|
||||
|
||||
47
src/Socket/mobile-socket.ts
Normal file
47
src/Socket/mobile-socket.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Socket } from 'net'
|
||||
import { MOBILE_ENDPOINT, MOBILE_PORT } from '../Defaults'
|
||||
import { SocketConfig } from '../Types'
|
||||
|
||||
export class MobileSocket extends Socket {
|
||||
constructor(public config: SocketConfig) {
|
||||
super()
|
||||
|
||||
if(config.auth.creds.registered) {
|
||||
this.connect()
|
||||
}
|
||||
|
||||
this.on('data', (d) => {
|
||||
this.emit('message', d)
|
||||
})
|
||||
}
|
||||
|
||||
override connect() {
|
||||
return super.connect(MOBILE_PORT, MOBILE_ENDPOINT, () => {
|
||||
this.emit('open')
|
||||
})
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
return this.readyState === 'open'
|
||||
}
|
||||
|
||||
get isClosed(): boolean {
|
||||
return this.readyState === 'closed'
|
||||
}
|
||||
|
||||
get isClosing(): boolean {
|
||||
return this.isClosed
|
||||
}
|
||||
|
||||
get isConnecting(): boolean {
|
||||
return this.readyState === 'opening'
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.end()
|
||||
}
|
||||
|
||||
send(data: unknown, cb?: ((err?: Error | undefined) => void) | undefined) {
|
||||
return super.write(data as Uint8Array | string, cb as ((err?: Error | undefined) => void))
|
||||
}
|
||||
}
|
||||
251
src/Socket/registration.ts
Normal file
251
src/Socket/registration.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/* eslint-disable camelcase */
|
||||
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'
|
||||
import { MobileSocket } from './mobile-socket'
|
||||
|
||||
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 })
|
||||
|
||||
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)
|
||||
|
||||
if(sock.ws instanceof MobileSocket) {
|
||||
sock.ws.connect()
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
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'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a registration code for the given phone number.
|
||||
*/
|
||||
export function mobileRegisterCode(params: RegistrationParams) {
|
||||
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'
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function mobileRegisterExists(params: RegistrationParams) {
|
||||
return mobileRegisterFetch('/exist', {
|
||||
params: registrationParams(params)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the phone number on whatsapp with the received OTP code.
|
||||
*/
|
||||
export async function mobileRegister(params: RegistrationParams & { code: string }) {
|
||||
//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('-', '') },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: { params?: Record<string, string>, headers?: Record<string, string> } = {}) {
|
||||
let url = `${MOBILE_REGISTRATION_ENDPOINT}${path}`
|
||||
|
||||
if(opts.params) {
|
||||
const parameter = [] as string[]
|
||||
|
||||
for(const param in opts.params) {
|
||||
parameter.push(param + '=' + urlencode(opts.params[param]))
|
||||
}
|
||||
|
||||
console.log('parameter', opts.params, parameter)
|
||||
|
||||
// const params = urlencode(mobileRegisterEncrypt(parameter.join('&')))
|
||||
// url += `?ENC=${params}`
|
||||
url += `?${parameter.join('&')}`
|
||||
}
|
||||
|
||||
if(!opts.headers) {
|
||||
opts.headers = {}
|
||||
}
|
||||
|
||||
opts.headers['User-Agent'] = MOBILE_USERAGENT
|
||||
|
||||
const response = await fetch(url, opts)
|
||||
|
||||
const text = await response.text()
|
||||
|
||||
try {
|
||||
var json = JSON.parse(text)
|
||||
} catch(error) {
|
||||
throw text
|
||||
}
|
||||
|
||||
if(!response.ok || json.reason) {
|
||||
throw json
|
||||
}
|
||||
|
||||
if(json.status && !['ok', 'sent'].includes(json.status)) {
|
||||
throw json
|
||||
}
|
||||
|
||||
return json as ExistsResponse
|
||||
}
|
||||
|
||||
|
||||
export interface ExistsResponse {
|
||||
status: 'fail'
|
||||
voice_length?: number
|
||||
voice_wait?: number
|
||||
sms_length?: number
|
||||
sms_wait?: number
|
||||
reason?: 'incorrect' | 'missing_param'
|
||||
login?: string
|
||||
flash_type?: number
|
||||
ab_hash?: string
|
||||
ab_key?: string
|
||||
exp_cfg?: string
|
||||
lid?: string
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { promisify } from 'util'
|
||||
import WebSocket from 'ws'
|
||||
import { proto } from '../../WAProto'
|
||||
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, DEFAULT_ORIGIN, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT } from '../Defaults'
|
||||
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MOBILE_NOISE_HEADER, NOISE_WA_HEADER } from '../Defaults'
|
||||
import { DisconnectReason, SocketConfig } from '../Types'
|
||||
import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils'
|
||||
import { addTransactionCapability, bindWaitForConnectionUpdate, configureSuccessfulPairing, Curve, generateLoginNode, generateMdTagPrefix, generateMobileNode, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeNoiseHandler, printQRIfNecessaryListener, promiseTimeout } from '../Utils'
|
||||
import { makeEventBuffer } from '../Utils/event-buffer'
|
||||
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary'
|
||||
import { assertNodeErrorFree, BinaryNode, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, S_WHATSAPP_NET } from '../WABinary'
|
||||
import { MobileSocket } from './mobile-socket'
|
||||
import { WebSocket } from './web-socket'
|
||||
|
||||
/**
|
||||
* Connects to WA servers and performs:
|
||||
@@ -14,37 +16,35 @@ import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild,
|
||||
* - listen to messages and emit events
|
||||
* - query phone connection
|
||||
*/
|
||||
export const makeSocket = ({
|
||||
waWebSocketUrl,
|
||||
connectTimeoutMs,
|
||||
logger,
|
||||
agent,
|
||||
keepAliveIntervalMs,
|
||||
version,
|
||||
browser,
|
||||
auth: authState,
|
||||
printQRInTerminal,
|
||||
defaultQueryTimeoutMs,
|
||||
syncFullHistory,
|
||||
transactionOpts,
|
||||
qrTimeout,
|
||||
options,
|
||||
makeSignalRepository
|
||||
}: SocketConfig) => {
|
||||
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
||||
origin: DEFAULT_ORIGIN,
|
||||
headers: options.headers as {},
|
||||
handshakeTimeout: connectTimeoutMs,
|
||||
timeout: connectTimeoutMs,
|
||||
agent
|
||||
})
|
||||
|
||||
export const makeSocket = (config: SocketConfig) => {
|
||||
const {
|
||||
connectTimeoutMs,
|
||||
logger,
|
||||
keepAliveIntervalMs,
|
||||
browser,
|
||||
auth: authState,
|
||||
printQRInTerminal,
|
||||
defaultQueryTimeoutMs,
|
||||
transactionOpts,
|
||||
qrTimeout,
|
||||
makeSignalRepository,
|
||||
} = config
|
||||
|
||||
config.mobile = config.mobile || config.auth.creds.registered
|
||||
const ws = config.mobile ? new MobileSocket(config) : new WebSocket(config)
|
||||
ws.setMaxListeners(0)
|
||||
|
||||
const ev = makeEventBuffer(logger)
|
||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||
const ephemeralKeyPair = Curve.generateKeyPair()
|
||||
/** WA noise protocol wrapper */
|
||||
const noise = makeNoiseHandler(ephemeralKeyPair, logger)
|
||||
const noise = makeNoiseHandler({
|
||||
keyPair: ephemeralKeyPair,
|
||||
NOISE_HEADER: config.mobile ? MOBILE_NOISE_HEADER : NOISE_WA_HEADER,
|
||||
mobile: config.mobile,
|
||||
logger
|
||||
})
|
||||
|
||||
const { creds } = authState
|
||||
// add transaction capability
|
||||
@@ -63,7 +63,7 @@ export const makeSocket = ({
|
||||
const sendPromise = promisify<void>(ws.send)
|
||||
/** send a raw buffer */
|
||||
const sendRawMessage = async(data: Uint8Array | Buffer) => {
|
||||
if(ws.readyState !== ws.OPEN) {
|
||||
if(!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const makeSocket = ({
|
||||
/** send a binary node */
|
||||
const sendNode = (frame: BinaryNode) => {
|
||||
if(logger.level === 'trace') {
|
||||
logger.trace({ msgId: frame.attrs.id, fromMe: true, frame }, 'communication')
|
||||
logger.trace(binaryNodeToString(frame), 'xml send')
|
||||
}
|
||||
|
||||
const buff = encodeBinaryNode(frame)
|
||||
@@ -101,7 +101,7 @@ export const makeSocket = ({
|
||||
|
||||
/** await the next incoming message */
|
||||
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
|
||||
if(ws.readyState !== ws.OPEN) {
|
||||
if(!ws.isOpen) {
|
||||
throw new Boom('Connection Closed', {
|
||||
statusCode: DisconnectReason.connectionClosed
|
||||
})
|
||||
@@ -186,21 +186,21 @@ export const makeSocket = ({
|
||||
}
|
||||
helloMsg = proto.HandshakeMessage.fromObject(helloMsg)
|
||||
|
||||
logger.info({ browser, helloMsg }, 'connected to WA Web')
|
||||
logger.info({ browser, helloMsg }, 'connected to WA')
|
||||
|
||||
const init = proto.HandshakeMessage.encode(helloMsg).finish()
|
||||
|
||||
const result = await awaitNextMessage<Uint8Array>(init)
|
||||
const handshake = proto.HandshakeMessage.decode(result)
|
||||
|
||||
logger.trace({ handshake }, 'handshake recv from WA Web')
|
||||
logger.trace({ handshake }, 'handshake recv from WA')
|
||||
|
||||
const keyEnc = noise.processHandshake(handshake, creds.noiseKey)
|
||||
|
||||
const config = { version, browser, syncFullHistory }
|
||||
|
||||
let node: proto.IClientPayload
|
||||
if(!creds.me) {
|
||||
if(config.mobile) {
|
||||
node = generateMobileNode(config)
|
||||
} else if(!creds.me) {
|
||||
node = generateRegistrationNode(creds, config)
|
||||
logger.info({ node }, 'not logged in, attempting registration...')
|
||||
} else {
|
||||
@@ -276,7 +276,7 @@ export const makeSocket = ({
|
||||
const msgId = frame.attrs.id
|
||||
|
||||
if(logger.level === 'trace') {
|
||||
logger.trace({ msgId, fromMe: false, frame }, 'communication')
|
||||
logger.trace(binaryNodeToString(frame), 'recv xml')
|
||||
}
|
||||
|
||||
/* Check if this is a response to a message we sent */
|
||||
@@ -321,7 +321,7 @@ export const makeSocket = ({
|
||||
ws.removeAllListeners('open')
|
||||
ws.removeAllListeners('message')
|
||||
|
||||
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
|
||||
if(!ws.isClosed && !ws.isClosing) {
|
||||
try {
|
||||
ws.close()
|
||||
} catch{ }
|
||||
@@ -338,11 +338,11 @@ export const makeSocket = ({
|
||||
}
|
||||
|
||||
const waitForSocketOpen = async() => {
|
||||
if(ws.readyState === ws.OPEN) {
|
||||
if(ws.isOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
||||
if(ws.isClosed || ws.isClosing) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ export const makeSocket = ({
|
||||
*/
|
||||
if(diff > keepAliveIntervalMs + 5000) {
|
||||
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
||||
} else if(ws.readyState === ws.OPEN) {
|
||||
} else if(ws.isOpen) {
|
||||
// if its all good, send a keep alive request
|
||||
query(
|
||||
{
|
||||
@@ -472,7 +472,7 @@ export const makeSocket = ({
|
||||
|
||||
let qrMs = qrTimeout || 60_000 // time to let a QR live
|
||||
const genPairQR = () => {
|
||||
if(ws.readyState !== ws.OPEN) {
|
||||
if(!ws.isOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
31
src/Socket/web-socket.ts
Normal file
31
src/Socket/web-socket.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { WebSocket as WS } from 'ws'
|
||||
import { DEFAULT_ORIGIN } from '../Defaults'
|
||||
import { SocketConfig } from '../Types'
|
||||
|
||||
export class WebSocket extends WS {
|
||||
constructor(public config: SocketConfig) {
|
||||
super(config.waWebSocketUrl, undefined, {
|
||||
origin: DEFAULT_ORIGIN,
|
||||
headers: config.options.headers as Record<string, string>,
|
||||
handshakeTimeout: config.connectTimeoutMs,
|
||||
timeout: config.connectTimeoutMs,
|
||||
agent: config.agent,
|
||||
})
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return this.readyState === WS.OPEN
|
||||
}
|
||||
|
||||
get isClosed() {
|
||||
return this.readyState === WS.CLOSED
|
||||
}
|
||||
|
||||
get isClosing() {
|
||||
return this.readyState === WS.CLOSING
|
||||
}
|
||||
|
||||
get isConnecting() {
|
||||
return this.readyState === WS.CONNECTING
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user