mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Merge pull request #2 from SamuelScheit/master
This commit is contained in:
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
|
import parsePhoneNumber from 'libphonenumber-js'
|
||||||
import NodeCache from 'node-cache'
|
import NodeCache from 'node-cache'
|
||||||
import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, getAggregateVotesInPollMessage, makeCacheableSignalKeyStore, makeInMemoryStore, proto, useMultiFileAuthState, WAMessageContent, WAMessageKey } from '../src'
|
import readline from 'readline'
|
||||||
|
import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, getAggregateVotesInPollMessage, makeCacheableSignalKeyStore, makeInMemoryStore, PHONENUMBER_MCC, proto, useMultiFileAuthState, WAMessageContent, WAMessageKey } from '../src'
|
||||||
import MAIN_LOGGER from '../src/Utils/logger'
|
import MAIN_LOGGER from '../src/Utils/logger'
|
||||||
|
|
||||||
const logger = MAIN_LOGGER.child({})
|
const logger = MAIN_LOGGER.child({})
|
||||||
@@ -8,6 +10,7 @@ logger.level = 'trace'
|
|||||||
|
|
||||||
const useStore = !process.argv.includes('--no-store')
|
const useStore = !process.argv.includes('--no-store')
|
||||||
const doReplies = !process.argv.includes('--no-reply')
|
const doReplies = !process.argv.includes('--no-reply')
|
||||||
|
const useMobile = process.argv.includes('--mobile')
|
||||||
|
|
||||||
// external map to store retry counts of messages when decryption/encryption fails
|
// external map to store retry counts of messages when decryption/encryption fails
|
||||||
// keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts
|
// keep this out of the socket itself, so as to prevent a message decryption/encryption loop across socket restarts
|
||||||
@@ -33,6 +36,7 @@ const startSock = async() => {
|
|||||||
version,
|
version,
|
||||||
logger,
|
logger,
|
||||||
printQRInTerminal: true,
|
printQRInTerminal: true,
|
||||||
|
mobile: useMobile,
|
||||||
auth: {
|
auth: {
|
||||||
creds: state.creds,
|
creds: state.creds,
|
||||||
/** caching makes the store faster to send/recv messages */
|
/** caching makes the store faster to send/recv messages */
|
||||||
@@ -49,6 +53,67 @@ const startSock = async() => {
|
|||||||
|
|
||||||
store?.bind(sock.ev)
|
store?.bind(sock.ev)
|
||||||
|
|
||||||
|
// If mobile was chosen, ask for the code
|
||||||
|
if(useMobile && !sock.authState.creds.registered) {
|
||||||
|
const question = (text: string) => new Promise<string>((resolve) => rl.question(text, resolve))
|
||||||
|
|
||||||
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||||
|
const { registration } = sock.authState.creds || { registration: {} }
|
||||||
|
|
||||||
|
if(!registration.phoneNumber) {
|
||||||
|
registration.phoneNumber = await question('Please enter your mobile phone number:\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneNumber = parsePhoneNumber(registration!.phoneNumber)
|
||||||
|
if(!phoneNumber?.isValid()) {
|
||||||
|
throw new Error('Invalid phone number: ' + registration!.phoneNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
registration.phoneNumber = phoneNumber.format('E.164')
|
||||||
|
registration.phoneNumberCountryCode = phoneNumber.countryCallingCode
|
||||||
|
registration.phoneNumberNationalNumber = phoneNumber.nationalNumber
|
||||||
|
const mcc = PHONENUMBER_MCC[phoneNumber.countryCallingCode]
|
||||||
|
if(!mcc) {
|
||||||
|
throw new Error('Could not find MCC for phone number: ' + registration!.phoneNumber + '\nPlease specify the MCC manually.')
|
||||||
|
}
|
||||||
|
|
||||||
|
registration.phoneNumberMobileCountryCode = mcc
|
||||||
|
|
||||||
|
async function enterCode() {
|
||||||
|
try {
|
||||||
|
const code = await question('Please enter the one time code:\n')
|
||||||
|
const response = await sock.register(code.replace(/["']/g, '').trim().toLowerCase())
|
||||||
|
console.log('Successfully registered your phone number.')
|
||||||
|
console.log(response)
|
||||||
|
rl.close()
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Failed to register your phone number. Please try again.\n', error)
|
||||||
|
await askForOTP()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askForOTP() {
|
||||||
|
let code = await question('How would you like to receive the one time code for registration? "sms" or "voice"\n')
|
||||||
|
code = code.replace(/["']/g, '').trim().toLowerCase()
|
||||||
|
|
||||||
|
if(code !== 'sms' && code !== 'voice') {
|
||||||
|
return await askForOTP()
|
||||||
|
}
|
||||||
|
|
||||||
|
registration.method = code
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sock.requestRegistrationCode(registration)
|
||||||
|
await enterCode()
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Failed to request registration code. Please try again.\n', error)
|
||||||
|
await askForOTP()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
askForOTP()
|
||||||
|
}
|
||||||
|
|
||||||
const sendMessageWTyping = async(msg: AnyMessageContent, jid: string) => {
|
const sendMessageWTyping = async(msg: AnyMessageContent, jid: string) => {
|
||||||
await sock.presenceSubscribe(jid)
|
await sock.presenceSubscribe(jid)
|
||||||
await delay(500)
|
await delay(500)
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -41,7 +41,9 @@ import makeWASocket from '@whiskeysockets/baileys'
|
|||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
## Connecting
|
## Connecting multi device (recommended)
|
||||||
|
|
||||||
|
WhatsApp provides a multi-device API that allows Baileys to be authenticated as a second WhatsApp client by scanning a QR code with WhatsApp on your phone.
|
||||||
|
|
||||||
``` ts
|
``` ts
|
||||||
import makeWASocket, { DisconnectReason } from '@whiskeysockets/baileys'
|
import makeWASocket, { DisconnectReason } from '@whiskeysockets/baileys'
|
||||||
@@ -82,6 +84,12 @@ If the connection is successful, you will see a QR code printed on your terminal
|
|||||||
|
|
||||||
**Note:** the code to support the legacy version of WA Web (pre multi-device) has been removed in v5. Only the standard multi-device connection is now supported. This is done as WA seems to have completely dropped support for the legacy version.
|
**Note:** the code to support the legacy version of WA Web (pre multi-device) has been removed in v5. Only the standard multi-device connection is now supported. This is done as WA seems to have completely dropped support for the legacy version.
|
||||||
|
|
||||||
|
## Connecting native mobile api
|
||||||
|
|
||||||
|
Baileys also supports the native mobile API, which allows users to authenticate as a standalone WhatsApp client using their phone number.
|
||||||
|
|
||||||
|
Run the [example](Example/example.ts) file with ``--mobile`` cli flag to use the native mobile API.
|
||||||
|
|
||||||
## Configuring the Connection
|
## Configuring the Connection
|
||||||
|
|
||||||
You can configure the connection by passing a `SocketConfig` object.
|
You can configure the connection by passing a `SocketConfig` object.
|
||||||
|
|||||||
@@ -45,11 +45,13 @@
|
|||||||
"@hapi/boom": "^9.1.3",
|
"@hapi/boom": "^9.1.3",
|
||||||
"axios": "^1.3.3",
|
"axios": "^1.3.3",
|
||||||
"futoin-hkdf": "^1.5.1",
|
"futoin-hkdf": "^1.5.1",
|
||||||
|
"libphonenumber-js": "^1.10.20",
|
||||||
"libsignal": "https://github.com/adiwajshing/libsignal-node.git",
|
"libsignal": "https://github.com/adiwajshing/libsignal-node.git",
|
||||||
"music-metadata": "^7.12.3",
|
"music-metadata": "^7.12.3",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"pino": "^7.0.0",
|
"pino": "^7.0.0",
|
||||||
"protobufjs": "^6.11.3",
|
"protobufjs": "^6.11.3",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"ws": "^8.0.0"
|
"ws": "^8.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -4,22 +4,36 @@ import type { AuthenticationState, MediaType, SocketConfig, WAVersion } from '..
|
|||||||
import { Browsers } from '../Utils'
|
import { Browsers } from '../Utils'
|
||||||
import logger from '../Utils/logger'
|
import logger from '../Utils/logger'
|
||||||
import { version } from './baileys-version.json'
|
import { version } from './baileys-version.json'
|
||||||
|
import phoneNumberMCC from './phonenumber-mcc.json'
|
||||||
|
|
||||||
export const UNAUTHORIZED_CODES = [401, 403, 419]
|
export const UNAUTHORIZED_CODES = [401, 403, 419]
|
||||||
|
|
||||||
|
export const PHONENUMBER_MCC = phoneNumberMCC
|
||||||
|
|
||||||
export const DEFAULT_ORIGIN = 'https://web.whatsapp.com'
|
export const DEFAULT_ORIGIN = 'https://web.whatsapp.com'
|
||||||
|
export const MOBILE_ENDPOINT = 'g.whatsapp.net'
|
||||||
|
export const MOBILE_PORT = 443
|
||||||
export const DEF_CALLBACK_PREFIX = 'CB:'
|
export const DEF_CALLBACK_PREFIX = 'CB:'
|
||||||
export const DEF_TAG_PREFIX = 'TAG:'
|
export const DEF_TAG_PREFIX = 'TAG:'
|
||||||
export const PHONE_CONNECTION_CB = 'CB:Pong'
|
export const PHONE_CONNECTION_CB = 'CB:Pong'
|
||||||
|
|
||||||
export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60
|
export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60
|
||||||
|
|
||||||
|
export const MOBILE_TOKEN = Buffer.from('0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM4174c0243f5277a5d7720ce842cc4ae6')
|
||||||
|
export const MOBILE_REGISTRATION_ENDPOINT = 'https://v.whatsapp.net/v2'
|
||||||
|
export const MOBILE_USERAGENT = 'WhatsApp/2.22.24.81 iOS/15.3.1 Device/Apple-iPhone_7'
|
||||||
|
export const REGISTRATION_PUBLIC_KEY = Buffer.from([
|
||||||
|
5, 142, 140, 15, 116, 195, 235, 197, 215, 166, 134, 92, 108, 60, 132, 56, 86, 176, 97, 33, 204, 232, 234, 119, 77,
|
||||||
|
34, 251, 111, 18, 37, 18, 48, 45,
|
||||||
|
])
|
||||||
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
|
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
|
||||||
export const DICT_VERSION = 2
|
export const DICT_VERSION = 2
|
||||||
export const KEY_BUNDLE_TYPE = Buffer.from([5])
|
export const KEY_BUNDLE_TYPE = Buffer.from([5])
|
||||||
export const NOISE_WA_HEADER = Buffer.from(
|
export const NOISE_WA_HEADER = Buffer.from(
|
||||||
[ 87, 65, 6, DICT_VERSION ]
|
[ 87, 65, 6, DICT_VERSION ]
|
||||||
) // last is "DICT_VERSION"
|
) // last is "DICT_VERSION"
|
||||||
|
export const PROTOCOL_VERSION = [5, 2]
|
||||||
|
export const MOBILE_NOISE_HEADER = Buffer.concat([Buffer.from('WA'), Buffer.from(PROTOCOL_VERSION)])
|
||||||
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
|
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
|
||||||
export const URL_REGEX = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/
|
export const URL_REGEX = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/
|
||||||
export const URL_EXCLUDE_REGEX = /.*@.*/
|
export const URL_EXCLUDE_REGEX = /.*@.*/
|
||||||
@@ -74,7 +88,8 @@ export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
|||||||
sticker: '/mms/image',
|
sticker: '/mms/image',
|
||||||
'thumbnail-link': '/mms/image',
|
'thumbnail-link': '/mms/image',
|
||||||
'product-catalog-image': '/product/image',
|
'product-catalog-image': '/product/image',
|
||||||
'md-app-state': ''
|
'md-app-state': '',
|
||||||
|
'md-msg-hist': '/mms/md-app-state',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_HKDF_KEY_MAPPING = {
|
export const MEDIA_HKDF_KEY_MAPPING = {
|
||||||
|
|||||||
223
src/Defaults/phonenumber-mcc.json
Normal file
223
src/Defaults/phonenumber-mcc.json
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
{
|
||||||
|
"93": 412,
|
||||||
|
"355": 276,
|
||||||
|
"213": 603,
|
||||||
|
"1-684": 544,
|
||||||
|
"376": 213,
|
||||||
|
"244": 631,
|
||||||
|
"1-264": 365,
|
||||||
|
"1-268": 344,
|
||||||
|
"54": 722,
|
||||||
|
"374": 283,
|
||||||
|
"297": 363,
|
||||||
|
"61": 505,
|
||||||
|
"43": 232,
|
||||||
|
"994": 400,
|
||||||
|
"1-242": 364,
|
||||||
|
"973": 426,
|
||||||
|
"880": 470,
|
||||||
|
"1-246": 342,
|
||||||
|
"375": 257,
|
||||||
|
"32": 206,
|
||||||
|
"501": 702,
|
||||||
|
"229": 616,
|
||||||
|
"1-441": 350,
|
||||||
|
"975": 402,
|
||||||
|
"591": 736,
|
||||||
|
"387": 218,
|
||||||
|
"267": 652,
|
||||||
|
"55": 724,
|
||||||
|
"1-284": 348,
|
||||||
|
"673": 528,
|
||||||
|
"359": 284,
|
||||||
|
"226": 613,
|
||||||
|
"257": 642,
|
||||||
|
"855": 456,
|
||||||
|
"237": 624,
|
||||||
|
"238": 625,
|
||||||
|
"1-345": 346,
|
||||||
|
"236": 623,
|
||||||
|
"235": 622,
|
||||||
|
"56": 730,
|
||||||
|
"86": 454,
|
||||||
|
"57": 732,
|
||||||
|
"269": 654,
|
||||||
|
"682": 548,
|
||||||
|
"506": 712,
|
||||||
|
"385": 219,
|
||||||
|
"53": 368,
|
||||||
|
"357": 280,
|
||||||
|
"420": 230,
|
||||||
|
"243": 630,
|
||||||
|
"45": 238,
|
||||||
|
"253": 638,
|
||||||
|
"1-767": 366,
|
||||||
|
"1-809": 370,
|
||||||
|
"1-849": 370,
|
||||||
|
"1-829": 370,
|
||||||
|
"593": 740,
|
||||||
|
"20": 602,
|
||||||
|
"503": 706,
|
||||||
|
"240": 627,
|
||||||
|
"291": 657,
|
||||||
|
"372": 248,
|
||||||
|
"251": 636,
|
||||||
|
"500": 750,
|
||||||
|
"298": 288,
|
||||||
|
"679": 542,
|
||||||
|
"358": 244,
|
||||||
|
"33": 208,
|
||||||
|
"689": 547,
|
||||||
|
"241": 628,
|
||||||
|
"220": 607,
|
||||||
|
"995": 282,
|
||||||
|
"49": 262,
|
||||||
|
"233": 620,
|
||||||
|
"350": 266,
|
||||||
|
"30": 202,
|
||||||
|
"299": 290,
|
||||||
|
"1-473": 352,
|
||||||
|
"1-671": 535,
|
||||||
|
"502": 704,
|
||||||
|
"224": 537,
|
||||||
|
"592": 738,
|
||||||
|
"509": 372,
|
||||||
|
"504": 708,
|
||||||
|
"852": 454,
|
||||||
|
"36": 216,
|
||||||
|
"354": 274,
|
||||||
|
"91": 404,
|
||||||
|
"62": 510,
|
||||||
|
"98": 432,
|
||||||
|
"964": 418,
|
||||||
|
"353": 234,
|
||||||
|
"972": 425,
|
||||||
|
"39": 222,
|
||||||
|
"225": 612,
|
||||||
|
"1-876": 338,
|
||||||
|
"81": 440,
|
||||||
|
"962": 416,
|
||||||
|
"254": 639,
|
||||||
|
"686": 545,
|
||||||
|
"383": 221,
|
||||||
|
"965": 419,
|
||||||
|
"371": 247,
|
||||||
|
"961": 415,
|
||||||
|
"266": 651,
|
||||||
|
"231": 618,
|
||||||
|
"218": 606,
|
||||||
|
"423": 295,
|
||||||
|
"370": 246,
|
||||||
|
"352": 270,
|
||||||
|
"389": 294,
|
||||||
|
"261": 646,
|
||||||
|
"265": 650,
|
||||||
|
"60": 502,
|
||||||
|
"960": 472,
|
||||||
|
"223": 610,
|
||||||
|
"356": 278,
|
||||||
|
"692": 551,
|
||||||
|
"222": 609,
|
||||||
|
"230": 617,
|
||||||
|
"52": 334,
|
||||||
|
"691": 550,
|
||||||
|
"373": 259,
|
||||||
|
"377": 212,
|
||||||
|
"976": 428,
|
||||||
|
"382": 297,
|
||||||
|
"1-664": 354,
|
||||||
|
"212": 604,
|
||||||
|
"258": 643,
|
||||||
|
"95": 414,
|
||||||
|
"264": 649,
|
||||||
|
"674": 536,
|
||||||
|
"977": 429,
|
||||||
|
"31": 204,
|
||||||
|
"687": 546,
|
||||||
|
"64": 530,
|
||||||
|
"505": 710,
|
||||||
|
"227": 614,
|
||||||
|
"234": 621,
|
||||||
|
"683": 555,
|
||||||
|
"1-670": 534,
|
||||||
|
"47": 242,
|
||||||
|
"968": 226,
|
||||||
|
"92": 410,
|
||||||
|
"680": 552,
|
||||||
|
"970": 423,
|
||||||
|
"507": 714,
|
||||||
|
"675": 537,
|
||||||
|
"595": 744,
|
||||||
|
"51": 716,
|
||||||
|
"63": 515,
|
||||||
|
"48": 260,
|
||||||
|
"351": 268,
|
||||||
|
"1-787, 1-939": 330,
|
||||||
|
"974": 427,
|
||||||
|
"242": 630,
|
||||||
|
"40": 226,
|
||||||
|
"7": 250,
|
||||||
|
"250": 635,
|
||||||
|
"290": 658,
|
||||||
|
"1-869": 356,
|
||||||
|
"1-758": 358,
|
||||||
|
"508": 308,
|
||||||
|
"1-784": 360,
|
||||||
|
"685": 544,
|
||||||
|
"378": 292,
|
||||||
|
"239": 626,
|
||||||
|
"966": 420,
|
||||||
|
"221": 608,
|
||||||
|
"381": 220,
|
||||||
|
"248": 633,
|
||||||
|
"232": 619,
|
||||||
|
"65": 525,
|
||||||
|
"386": 293,
|
||||||
|
"677": 540,
|
||||||
|
"27": 655,
|
||||||
|
"211": 659,
|
||||||
|
"34": 214,
|
||||||
|
"94": 413,
|
||||||
|
"249": 634,
|
||||||
|
"597": 746,
|
||||||
|
"268": 653,
|
||||||
|
"46": 240,
|
||||||
|
"41": 228,
|
||||||
|
"963": 417,
|
||||||
|
"886": 466,
|
||||||
|
"992": 436,
|
||||||
|
"255": 640,
|
||||||
|
"66": 520,
|
||||||
|
"228": 615,
|
||||||
|
"690": 554,
|
||||||
|
"676": 539,
|
||||||
|
"1-868": 374,
|
||||||
|
"216": 605,
|
||||||
|
"90": 286,
|
||||||
|
"993": 438,
|
||||||
|
"1-649": 376,
|
||||||
|
"688": 553,
|
||||||
|
"1-340": 332,
|
||||||
|
"256": 641,
|
||||||
|
"380": 255,
|
||||||
|
"971": 424,
|
||||||
|
"44": 234,
|
||||||
|
"1": 310,
|
||||||
|
"598": 748,
|
||||||
|
"998": 434,
|
||||||
|
"678": 541,
|
||||||
|
"379": 225,
|
||||||
|
"58": 734,
|
||||||
|
"681": 543,
|
||||||
|
"967": 421,
|
||||||
|
"260": 645,
|
||||||
|
"263": 648,
|
||||||
|
"670": 514,
|
||||||
|
"245": 632,
|
||||||
|
"856": 457,
|
||||||
|
"599": 362,
|
||||||
|
"850": 467,
|
||||||
|
"262": 647,
|
||||||
|
"82": 450,
|
||||||
|
"84": 452
|
||||||
|
}
|
||||||
@@ -335,8 +335,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAccountSyncTimestamp = async(fromTimestamp: number | string) => {
|
const cleanDirtyBits = async(type: 'account_sync' | 'groups', fromTimestamp?: number | string) => {
|
||||||
logger.info({ fromTimestamp }, 'requesting account sync')
|
logger.info({ fromTimestamp }, 'clean dirty bits ' + type)
|
||||||
await sendNode({
|
await sendNode({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -349,8 +349,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
{
|
{
|
||||||
tag: 'clean',
|
tag: 'clean',
|
||||||
attrs: {
|
attrs: {
|
||||||
type: 'account_sync',
|
type,
|
||||||
timestamp: fromTimestamp.toString(),
|
...(fromTimestamp ? { timestamp: fromTimestamp.toString() } : null),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -920,13 +920,16 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
if(attrs.timestamp) {
|
if(attrs.timestamp) {
|
||||||
let { lastAccountSyncTimestamp } = authState.creds
|
let { lastAccountSyncTimestamp } = authState.creds
|
||||||
if(lastAccountSyncTimestamp) {
|
if(lastAccountSyncTimestamp) {
|
||||||
await updateAccountSyncTimestamp(lastAccountSyncTimestamp)
|
await cleanDirtyBits('account_sync', lastAccountSyncTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAccountSyncTimestamp = +attrs.timestamp
|
lastAccountSyncTimestamp = +attrs.timestamp
|
||||||
ev.emit('creds.update', { lastAccountSyncTimestamp })
|
ev.emit('creds.update', { lastAccountSyncTimestamp })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'groups':
|
||||||
|
// handled in groups.ts
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
logger.info({ node }, 'received unknown sync')
|
logger.info({ node }, 'received unknown sync')
|
||||||
@@ -953,7 +956,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
// if we don't have the app state key
|
// if we don't have the app state key
|
||||||
// we keep buffering events until we finally have
|
// we keep buffering events until we finally have
|
||||||
// the key and can sync the messages
|
// the key and can sync the messages
|
||||||
if(!authState.creds?.myAppStateKeyId) {
|
if(!authState.creds?.myAppStateKeyId && !config.mobile) {
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
needToFlushWithAppStateSync = true
|
needToFlushWithAppStateSync = true
|
||||||
}
|
}
|
||||||
@@ -987,6 +990,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
|||||||
getBusinessProfile,
|
getBusinessProfile,
|
||||||
resyncAppState,
|
resyncAppState,
|
||||||
chatModify,
|
chatModify,
|
||||||
|
cleanDirtyBits,
|
||||||
addChatLabel,
|
addChatLabel,
|
||||||
removeChatLabel,
|
removeChatLabel,
|
||||||
addMessageLabel,
|
addMessageLabel,
|
||||||
|
|||||||
@@ -29,6 +29,55 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
return extractGroupMetadata(result)
|
return extractGroupMetadata(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const groupFetchAllParticipating = async() => {
|
||||||
|
const result = await query({
|
||||||
|
tag: 'iq',
|
||||||
|
attrs: {
|
||||||
|
to: '@g.us',
|
||||||
|
xmlns: 'w:g2',
|
||||||
|
type: 'get',
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
tag: 'participating',
|
||||||
|
attrs: { },
|
||||||
|
content: [
|
||||||
|
{ tag: 'participants', attrs: { } },
|
||||||
|
{ tag: 'description', attrs: { } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const data: { [_: string]: GroupMetadata } = { }
|
||||||
|
const groupsChild = getBinaryNodeChild(result, 'groups')
|
||||||
|
if(groupsChild) {
|
||||||
|
const groups = getBinaryNodeChildren(groupsChild, 'group')
|
||||||
|
for(const groupNode of groups) {
|
||||||
|
const meta = extractGroupMetadata({
|
||||||
|
tag: 'result',
|
||||||
|
attrs: { },
|
||||||
|
content: [groupNode]
|
||||||
|
})
|
||||||
|
data[meta.id] = meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sock.ev.emit('groups.update', Object.values(data))
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
sock.ws.on('CB:ib,,dirty', async(node: BinaryNode) => {
|
||||||
|
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
||||||
|
if(attrs.type !== 'groups') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await groupFetchAllParticipating()
|
||||||
|
await sock.cleanDirtyBits('groups')
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sock,
|
...sock,
|
||||||
groupMetadata,
|
groupMetadata,
|
||||||
@@ -211,41 +260,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
groupSettingUpdate: async(jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
|
groupSettingUpdate: async(jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
|
||||||
await groupQuery(jid, 'set', [ { tag: setting, attrs: { } } ])
|
await groupQuery(jid, 'set', [ { tag: setting, attrs: { } } ])
|
||||||
},
|
},
|
||||||
groupFetchAllParticipating: async() => {
|
groupFetchAllParticipating
|
||||||
const result = await query({
|
|
||||||
tag: 'iq',
|
|
||||||
attrs: {
|
|
||||||
to: '@g.us',
|
|
||||||
xmlns: 'w:g2',
|
|
||||||
type: 'get',
|
|
||||||
},
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
tag: 'participating',
|
|
||||||
attrs: { },
|
|
||||||
content: [
|
|
||||||
{ tag: 'participants', attrs: { } },
|
|
||||||
{ tag: 'description', attrs: { } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const data: { [_: string]: GroupMetadata } = { }
|
|
||||||
const groupsChild = getBinaryNodeChild(result, 'groups')
|
|
||||||
if(groupsChild) {
|
|
||||||
const groups = getBinaryNodeChildren(groupsChild, 'group')
|
|
||||||
for(const groupNode of groups) {
|
|
||||||
const meta = extractGroupMetadata({
|
|
||||||
tag: 'result',
|
|
||||||
attrs: { },
|
|
||||||
content: [groupNode]
|
|
||||||
})
|
|
||||||
data[meta.id] = meta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||||
import { UserFacingSocketConfig } from '../Types'
|
import { UserFacingSocketConfig } from '../Types'
|
||||||
import { makeBusinessSocket as _makeSocket } from './business'
|
import { makeRegistrationSocket as _makeSocket } from './registration'
|
||||||
|
|
||||||
// export the last socket layer
|
// export the last socket layer
|
||||||
const makeWASocket = (config: UserFacingSocketConfig) => (
|
const makeWASocket = (config: UserFacingSocketConfig) => (
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
if(msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
|
if(msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
|
||||||
retryMutex.mutex(
|
retryMutex.mutex(
|
||||||
async() => {
|
async() => {
|
||||||
if(ws.readyState === ws.OPEN) {
|
if(ws.isOpen) {
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode)
|
||||||
if(retryRequestDelayMs) {
|
if(retryRequestDelayMs) {
|
||||||
|
|||||||
46
src/Socket/mobile-socket.ts
Normal file
46
src/Socket/mobile-socket.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
this.on('data', (d) => {
|
||||||
|
this.emit('message', d)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override connect() {
|
||||||
|
return super.connect({
|
||||||
|
host: MOBILE_ENDPOINT,
|
||||||
|
port: MOBILE_PORT,
|
||||||
|
}, () => {
|
||||||
|
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, undefined, cb as ((err?: Error | undefined) => void))
|
||||||
|
}
|
||||||
|
}
|
||||||
250
src/Socket/registration.ts
Normal file
250
src/Socket/registration.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
/* 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'
|
||||||
|
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 }, 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)
|
||||||
|
|
||||||
|
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 }, 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'
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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) {
|
||||||
|
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('&')}`
|
||||||
|
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'
|
||||||
|
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,13 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import WebSocket from 'ws'
|
|
||||||
import { proto } from '../../WAProto'
|
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 { 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 { 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'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to WA servers and performs:
|
* Connects to WA servers and performs:
|
||||||
@@ -14,37 +15,40 @@ import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, getBinaryNodeChild,
|
|||||||
* - listen to messages and emit events
|
* - listen to messages and emit events
|
||||||
* - query phone connection
|
* - query phone connection
|
||||||
*/
|
*/
|
||||||
export const makeSocket = ({
|
|
||||||
waWebSocketUrl,
|
export const makeSocket = (config: SocketConfig) => {
|
||||||
connectTimeoutMs,
|
const {
|
||||||
logger,
|
connectTimeoutMs,
|
||||||
agent,
|
logger,
|
||||||
keepAliveIntervalMs,
|
keepAliveIntervalMs,
|
||||||
version,
|
browser,
|
||||||
browser,
|
auth: authState,
|
||||||
auth: authState,
|
printQRInTerminal,
|
||||||
printQRInTerminal,
|
defaultQueryTimeoutMs,
|
||||||
defaultQueryTimeoutMs,
|
transactionOpts,
|
||||||
syncFullHistory,
|
qrTimeout,
|
||||||
transactionOpts,
|
makeSignalRepository,
|
||||||
qrTimeout,
|
} = config
|
||||||
options,
|
|
||||||
makeSignalRepository
|
config.mobile = config.mobile || config.auth.creds.registered
|
||||||
}: SocketConfig) => {
|
const ws = new MobileSocket(config)
|
||||||
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
ws.setMaxListeners?.(0)
|
||||||
origin: DEFAULT_ORIGIN,
|
|
||||||
headers: options.headers as {},
|
// if not mobile or already registered -> auto connect
|
||||||
handshakeTimeout: connectTimeoutMs,
|
if(!config.mobile || config.auth.creds.registered) {
|
||||||
timeout: connectTimeoutMs,
|
ws.connect()
|
||||||
agent
|
}
|
||||||
})
|
|
||||||
ws.setMaxListeners(0)
|
|
||||||
|
|
||||||
const ev = makeEventBuffer(logger)
|
const ev = makeEventBuffer(logger)
|
||||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||||
const ephemeralKeyPair = Curve.generateKeyPair()
|
const ephemeralKeyPair = Curve.generateKeyPair()
|
||||||
/** WA noise protocol wrapper */
|
/** 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
|
const { creds } = authState
|
||||||
// add transaction capability
|
// add transaction capability
|
||||||
@@ -63,7 +67,7 @@ export const makeSocket = ({
|
|||||||
const sendPromise = promisify<void>(ws.send)
|
const sendPromise = promisify<void>(ws.send)
|
||||||
/** send a raw buffer */
|
/** send a raw buffer */
|
||||||
const sendRawMessage = async(data: Uint8Array | Buffer) => {
|
const sendRawMessage = async(data: Uint8Array | Buffer) => {
|
||||||
if(ws.readyState !== ws.OPEN) {
|
if(!ws.isOpen) {
|
||||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ export const makeSocket = ({
|
|||||||
/** send a binary node */
|
/** send a binary node */
|
||||||
const sendNode = (frame: BinaryNode) => {
|
const sendNode = (frame: BinaryNode) => {
|
||||||
if(logger.level === 'trace') {
|
if(logger.level === 'trace') {
|
||||||
logger.trace({ msgId: frame.attrs.id, fromMe: true, frame }, 'communication')
|
logger.trace(binaryNodeToString(frame), 'xml send')
|
||||||
}
|
}
|
||||||
|
|
||||||
const buff = encodeBinaryNode(frame)
|
const buff = encodeBinaryNode(frame)
|
||||||
@@ -101,7 +105,7 @@ export const makeSocket = ({
|
|||||||
|
|
||||||
/** await the next incoming message */
|
/** await the next incoming message */
|
||||||
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
|
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
|
||||||
if(ws.readyState !== ws.OPEN) {
|
if(!ws.isOpen) {
|
||||||
throw new Boom('Connection Closed', {
|
throw new Boom('Connection Closed', {
|
||||||
statusCode: DisconnectReason.connectionClosed
|
statusCode: DisconnectReason.connectionClosed
|
||||||
})
|
})
|
||||||
@@ -186,21 +190,21 @@ export const makeSocket = ({
|
|||||||
}
|
}
|
||||||
helloMsg = proto.HandshakeMessage.fromObject(helloMsg)
|
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 init = proto.HandshakeMessage.encode(helloMsg).finish()
|
||||||
|
|
||||||
const result = await awaitNextMessage<Uint8Array>(init)
|
const result = await awaitNextMessage<Uint8Array>(init)
|
||||||
const handshake = proto.HandshakeMessage.decode(result)
|
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 keyEnc = noise.processHandshake(handshake, creds.noiseKey)
|
||||||
|
|
||||||
const config = { version, browser, syncFullHistory }
|
|
||||||
|
|
||||||
let node: proto.IClientPayload
|
let node: proto.IClientPayload
|
||||||
if(!creds.me) {
|
if(config.mobile) {
|
||||||
|
node = generateMobileNode(config)
|
||||||
|
} else if(!creds.me) {
|
||||||
node = generateRegistrationNode(creds, config)
|
node = generateRegistrationNode(creds, config)
|
||||||
logger.info({ node }, 'not logged in, attempting registration...')
|
logger.info({ node }, 'not logged in, attempting registration...')
|
||||||
} else {
|
} else {
|
||||||
@@ -276,7 +280,7 @@ export const makeSocket = ({
|
|||||||
const msgId = frame.attrs.id
|
const msgId = frame.attrs.id
|
||||||
|
|
||||||
if(logger.level === 'trace') {
|
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 */
|
/* Check if this is a response to a message we sent */
|
||||||
@@ -321,7 +325,7 @@ export const makeSocket = ({
|
|||||||
ws.removeAllListeners('open')
|
ws.removeAllListeners('open')
|
||||||
ws.removeAllListeners('message')
|
ws.removeAllListeners('message')
|
||||||
|
|
||||||
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
|
if(!ws.isClosed && !ws.isClosing) {
|
||||||
try {
|
try {
|
||||||
ws.close()
|
ws.close()
|
||||||
} catch{ }
|
} catch{ }
|
||||||
@@ -338,11 +342,11 @@ export const makeSocket = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const waitForSocketOpen = async() => {
|
const waitForSocketOpen = async() => {
|
||||||
if(ws.readyState === ws.OPEN) {
|
if(ws.isOpen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
if(ws.isClosed || ws.isClosing) {
|
||||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +379,7 @@ export const makeSocket = ({
|
|||||||
*/
|
*/
|
||||||
if(diff > keepAliveIntervalMs + 5000) {
|
if(diff > keepAliveIntervalMs + 5000) {
|
||||||
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
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
|
// if its all good, send a keep alive request
|
||||||
query(
|
query(
|
||||||
{
|
{
|
||||||
@@ -472,7 +476,7 @@ export const makeSocket = ({
|
|||||||
|
|
||||||
let qrMs = qrTimeout || 60_000 // time to let a QR live
|
let qrMs = qrTimeout || 60_000 // time to let a QR live
|
||||||
const genPairQR = () => {
|
const genPairQR = () => {
|
||||||
if(ws.readyState !== ws.OPEN) {
|
if(!ws.isOpen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +627,7 @@ function mapWebSocketError(handler: (err: Error) => void) {
|
|||||||
return (error: Error) => {
|
return (error: Error) => {
|
||||||
handler(
|
handler(
|
||||||
new Boom(
|
new Boom(
|
||||||
`WebSocket Error (${error.message})`,
|
`WebSocket Error (${error?.message})`,
|
||||||
{ statusCode: getCodeFromWSError(error), data: error }
|
{ statusCode: getCodeFromWSError(error), data: error }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { proto } from '../../WAProto'
|
import type { proto } from '../../WAProto'
|
||||||
|
import { RegistrationOptions } from '../Socket/registration'
|
||||||
import type { Contact } from './Contact'
|
import type { Contact } from './Contact'
|
||||||
import type { MinimalMessage } from './Message'
|
import type { MinimalMessage } from './Message'
|
||||||
|
|
||||||
@@ -58,6 +59,13 @@ export type AuthenticationCreds = SignalCreds & {
|
|||||||
/** number of times history & app state has been synced */
|
/** number of times history & app state has been synced */
|
||||||
accountSyncCounter: number
|
accountSyncCounter: number
|
||||||
accountSettings: AccountSettings
|
accountSettings: AccountSettings
|
||||||
|
// mobile creds
|
||||||
|
deviceId: string
|
||||||
|
phoneId: string
|
||||||
|
identityId: Buffer
|
||||||
|
registered: boolean
|
||||||
|
backupToken: Buffer
|
||||||
|
registration: RegistrationOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignalDataTypeMap = {
|
export type SignalDataTypeMap = {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export type SocketConfig = {
|
|||||||
defaultQueryTimeoutMs: number | undefined
|
defaultQueryTimeoutMs: number | undefined
|
||||||
/** ping-pong interval for WS connection */
|
/** ping-pong interval for WS connection */
|
||||||
keepAliveIntervalMs: number
|
keepAliveIntervalMs: number
|
||||||
|
/** should baileys use the mobile api instead of the multi device api */
|
||||||
|
mobile?: boolean
|
||||||
/** proxy agent */
|
/** proxy agent */
|
||||||
agent?: Agent
|
agent?: Agent
|
||||||
/** pino logger */
|
/** pino logger */
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import NodeCache from 'node-cache'
|
import NodeCache from 'node-cache'
|
||||||
import type { Logger } from 'pino'
|
import type { Logger } from 'pino'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { DEFAULT_CACHE_TTLS } from '../Defaults'
|
import { DEFAULT_CACHE_TTLS } from '../Defaults'
|
||||||
import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
|
import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
|
||||||
import { Curve, signedKeyPair } from './crypto'
|
import { Curve, signedKeyPair } from './crypto'
|
||||||
@@ -205,6 +206,13 @@ export const initAuthCreds = (): AuthenticationCreds => {
|
|||||||
accountSyncCounter: 0,
|
accountSyncCounter: 0,
|
||||||
accountSettings: {
|
accountSettings: {
|
||||||
unarchiveChats: false
|
unarchiveChats: false
|
||||||
}
|
},
|
||||||
|
// mobile creds
|
||||||
|
deviceId: Buffer.from(uuidv4().replace(/-/g, ''), 'hex').toString('base64url'),
|
||||||
|
phoneId: uuidv4(),
|
||||||
|
identityId: randomBytes(20),
|
||||||
|
registered: false,
|
||||||
|
backupToken: randomBytes(20),
|
||||||
|
registration: {} as never
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +107,10 @@ export function sha256(buffer: Buffer) {
|
|||||||
return createHash('sha256').update(buffer).digest()
|
return createHash('sha256').update(buffer).digest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function md5(buffer: Buffer) {
|
||||||
|
return createHash('md5').update(buffer).digest()
|
||||||
|
}
|
||||||
|
|
||||||
// HKDF key expansion
|
// HKDF key expansion
|
||||||
export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) {
|
export function hkdf(buffer: Uint8Array | Buffer, expandedLength: number, info: { salt?: Buffer, info?: string }) {
|
||||||
return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info)
|
return HKDF(!Buffer.isBuffer(buffer) ? Buffer.from(buffer) : buffer, expandedLength, info)
|
||||||
|
|||||||
@@ -355,13 +355,13 @@ const UNEXPECTED_SERVER_CODE_TEXT = 'Unexpected server response: '
|
|||||||
|
|
||||||
export const getCodeFromWSError = (error: Error) => {
|
export const getCodeFromWSError = (error: Error) => {
|
||||||
let statusCode = 500
|
let statusCode = 500
|
||||||
if(error.message.includes(UNEXPECTED_SERVER_CODE_TEXT)) {
|
if(error?.message?.includes(UNEXPECTED_SERVER_CODE_TEXT)) {
|
||||||
const code = +error.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length)
|
const code = +error?.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length)
|
||||||
if(!Number.isNaN(code) && code >= 400) {
|
if(!Number.isNaN(code) && code >= 400) {
|
||||||
statusCode = code
|
statusCode = code
|
||||||
}
|
}
|
||||||
} else if(
|
} else if(
|
||||||
(error as any).code?.startsWith('E')
|
(error as any)?.code?.startsWith('E')
|
||||||
|| error?.message?.includes('timed out')
|
|| error?.message?.includes('timed out')
|
||||||
) { // handle ETIMEOUT, ENOTFOUND etc
|
) { // handle ETIMEOUT, ENOTFOUND etc
|
||||||
statusCode = 408
|
statusCode = 408
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { Logger } from 'pino'
|
import { Logger } from 'pino'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { NOISE_MODE, NOISE_WA_HEADER, WA_CERT_DETAILS } from '../Defaults'
|
import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults'
|
||||||
import { KeyPair } from '../Types'
|
import { KeyPair } from '../Types'
|
||||||
import { BinaryNode, decodeBinaryNode } from '../WABinary'
|
import { BinaryNode, decodeBinaryNode } from '../WABinary'
|
||||||
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto'
|
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto'
|
||||||
@@ -13,10 +13,17 @@ const generateIV = (counter: number) => {
|
|||||||
return new Uint8Array(iv)
|
return new Uint8Array(iv)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeNoiseHandler = (
|
export const makeNoiseHandler = ({
|
||||||
{ public: publicKey, private: privateKey }: KeyPair,
|
keyPair: { private: privateKey, public: publicKey },
|
||||||
|
NOISE_HEADER,
|
||||||
|
mobile,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
keyPair: KeyPair
|
||||||
|
NOISE_HEADER: Uint8Array
|
||||||
|
mobile: boolean
|
||||||
logger: Logger
|
logger: Logger
|
||||||
) => {
|
}) => {
|
||||||
logger = logger.child({ class: 'ns' })
|
logger = logger.child({ class: 'ns' })
|
||||||
|
|
||||||
const authenticate = (data: Uint8Array) => {
|
const authenticate = (data: Uint8Array) => {
|
||||||
@@ -86,7 +93,7 @@ export const makeNoiseHandler = (
|
|||||||
|
|
||||||
let inBytes = Buffer.alloc(0)
|
let inBytes = Buffer.alloc(0)
|
||||||
|
|
||||||
authenticate(NOISE_WA_HEADER)
|
authenticate(NOISE_HEADER)
|
||||||
authenticate(publicKey)
|
authenticate(publicKey)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -103,12 +110,17 @@ export const makeNoiseHandler = (
|
|||||||
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
||||||
|
|
||||||
const certDecoded = decrypt(serverHello!.payload!)
|
const certDecoded = decrypt(serverHello!.payload!)
|
||||||
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
|
|
||||||
|
|
||||||
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
|
if(mobile) {
|
||||||
|
proto.CertChain.NoiseCertificate.decode(certDecoded)
|
||||||
|
} else {
|
||||||
|
const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded)
|
||||||
|
|
||||||
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
|
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
|
||||||
throw new Boom('certification match failed', { statusCode: 400 })
|
|
||||||
|
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
|
||||||
|
throw new Boom('certification match failed', { statusCode: 400 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyEnc = encrypt(noiseKey.public)
|
const keyEnc = encrypt(noiseKey.public)
|
||||||
@@ -121,11 +133,11 @@ export const makeNoiseHandler = (
|
|||||||
data = encrypt(data)
|
data = encrypt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const introSize = sentIntro ? 0 : NOISE_WA_HEADER.length
|
const introSize = sentIntro ? 0 : NOISE_HEADER.length
|
||||||
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
|
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
|
||||||
|
|
||||||
if(!sentIntro) {
|
if(!sentIntro) {
|
||||||
frame.set(NOISE_WA_HEADER)
|
frame.set(NOISE_HEADER)
|
||||||
sentIntro = true
|
sentIntro = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,53 +8,67 @@ import { Curve, hmacSign } from './crypto'
|
|||||||
import { encodeBigEndian } from './generics'
|
import { encodeBigEndian } from './generics'
|
||||||
import { createSignalIdentity } from './signal'
|
import { createSignalIdentity } from './signal'
|
||||||
|
|
||||||
type ClientPayloadConfig = Pick<SocketConfig, 'version' | 'browser' | 'syncFullHistory'>
|
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
||||||
|
const osVersion = config.mobile ? '15.3.1' : '0.1'
|
||||||
|
const version = config.mobile ? [2, 22, 24] : config.version
|
||||||
|
const device = config.mobile ? 'iPhone_7' : 'Desktop'
|
||||||
|
const manufacturer = config.mobile ? 'Apple' : ''
|
||||||
|
const platform = config.mobile ? proto.ClientPayload.UserAgent.Platform.IOS : proto.ClientPayload.UserAgent.Platform.MACOS
|
||||||
|
const phoneId = config.mobile ? { phoneId: config.auth.creds.phoneId } : {}
|
||||||
|
|
||||||
const getUserAgent = ({ version }: ClientPayloadConfig): proto.ClientPayload.IUserAgent => {
|
|
||||||
const osVersion = '0.1'
|
|
||||||
return {
|
return {
|
||||||
appVersion: {
|
appVersion: {
|
||||||
primary: version[0],
|
primary: version[0],
|
||||||
secondary: version[1],
|
secondary: version[1],
|
||||||
tertiary: version[2],
|
tertiary: version[2],
|
||||||
},
|
},
|
||||||
platform: proto.ClientPayload.UserAgent.Platform.WEB,
|
platform,
|
||||||
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
||||||
mcc: '000',
|
mcc: config.auth.creds.registration?.phoneNumberMobileCountryCode || '000',
|
||||||
mnc: '000',
|
mnc: config.auth.creds.registration?.phoneNumberMobileNetworkCode || '000',
|
||||||
osVersion: osVersion,
|
osVersion: osVersion,
|
||||||
manufacturer: '',
|
manufacturer,
|
||||||
device: 'Desktop',
|
device,
|
||||||
osBuildNumber: osVersion,
|
osBuildNumber: osVersion,
|
||||||
localeLanguageIso6391: 'en',
|
localeLanguageIso6391: 'en',
|
||||||
localeCountryIso31661Alpha2: 'US',
|
localeCountryIso31661Alpha2: 'US',
|
||||||
|
...phoneId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PLATFORM_MAP = {
|
const getClientPayload = (config: SocketConfig) => {
|
||||||
'Mac OS': proto.ClientPayload.WebInfo.WebSubPlatform.DARWIN,
|
const payload: proto.IClientPayload = {
|
||||||
'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN32
|
|
||||||
}
|
|
||||||
|
|
||||||
const getWebInfo = (config: ClientPayloadConfig): proto.ClientPayload.IWebInfo => {
|
|
||||||
let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER
|
|
||||||
if(config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) {
|
|
||||||
webSubPlatform = PLATFORM_MAP[config.browser[0]]
|
|
||||||
}
|
|
||||||
|
|
||||||
return { webSubPlatform }
|
|
||||||
}
|
|
||||||
|
|
||||||
const getClientPayload = (config: ClientPayloadConfig): proto.IClientPayload => {
|
|
||||||
return {
|
|
||||||
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
|
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
|
||||||
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
|
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
|
||||||
userAgent: getUserAgent(config),
|
userAgent: getUserAgent(config),
|
||||||
webInfo: getWebInfo(config),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateLoginNode = (userJid: string, config: ClientPayloadConfig): proto.IClientPayload => {
|
export const generateMobileNode = (config: SocketConfig): proto.IClientPayload => {
|
||||||
|
if(!config.auth.creds) {
|
||||||
|
throw new Boom('No registration data found', { data: config })
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: proto.IClientPayload = {
|
||||||
|
...getClientPayload(config),
|
||||||
|
sessionId: Math.floor(Math.random() * 999999999 + 1),
|
||||||
|
shortConnect: true,
|
||||||
|
connectAttemptCount: 0,
|
||||||
|
device: 0,
|
||||||
|
dnsSource: {
|
||||||
|
appCached: false,
|
||||||
|
dnsMethod: proto.ClientPayload.DNSSource.DNSResolutionMethod.SYSTEM,
|
||||||
|
},
|
||||||
|
passive: false, // XMPP heartbeat setting (false: server actively pings) (true: client actively pings)
|
||||||
|
pushName: 'test',
|
||||||
|
username: Number(`${config.auth.creds.registration.phoneNumberCountryCode}${config.auth.creds.registration.phoneNumberNationalNumber}`),
|
||||||
|
}
|
||||||
|
return proto.ClientPayload.fromObject(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
|
||||||
const { user, device } = jidDecode(userJid)!
|
const { user, device } = jidDecode(userJid)!
|
||||||
const payload: proto.IClientPayload = {
|
const payload: proto.IClientPayload = {
|
||||||
...getClientPayload(config),
|
...getClientPayload(config),
|
||||||
@@ -67,24 +81,17 @@ export const generateLoginNode = (userJid: string, config: ClientPayloadConfig):
|
|||||||
|
|
||||||
export const generateRegistrationNode = (
|
export const generateRegistrationNode = (
|
||||||
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
|
{ registrationId, signedPreKey, signedIdentityKey }: SignalCreds,
|
||||||
config: ClientPayloadConfig
|
config: SocketConfig
|
||||||
) => {
|
) => {
|
||||||
// the app version needs to be md5 hashed
|
// the app version needs to be md5 hashed
|
||||||
// and passed in
|
// and passed in
|
||||||
const appVersionBuf = createHash('md5')
|
const appVersionBuf = createHash('md5')
|
||||||
.update(config.version.join('.')) // join as string
|
.update(config.version.join('.')) // join as string
|
||||||
.digest()
|
.digest()
|
||||||
const browserVersion = config.browser[2].split('.')
|
|
||||||
|
|
||||||
const companion: proto.IDeviceProps = {
|
const companion: proto.IDeviceProps = {
|
||||||
os: config.browser[0],
|
os: config.browser[0],
|
||||||
version: {
|
platformType: proto.DeviceProps.PlatformType.DESKTOP,
|
||||||
primary: +(browserVersion[0] || 0),
|
|
||||||
secondary: +(browserVersion[1] || 1),
|
|
||||||
tertiary: +(browserVersion[2] || 0),
|
|
||||||
},
|
|
||||||
platformType: proto.DeviceProps.PlatformType[config.browser[1].toUpperCase()]
|
|
||||||
|| proto.DeviceProps.PlatformType.UNKNOWN,
|
|
||||||
requireFullSync: config.syncFullHistory,
|
requireFullSync: config.syncFullHistory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,4 +87,35 @@ function bufferToUInt(e: Uint8Array | Buffer, t: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return a
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = (n: number) => '\t'.repeat(n)
|
||||||
|
|
||||||
|
export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i = 0) {
|
||||||
|
if(!node) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof node === 'string') {
|
||||||
|
return tabs(i) + node
|
||||||
|
}
|
||||||
|
|
||||||
|
if(node instanceof Uint8Array) {
|
||||||
|
return tabs(i) + Buffer.from(node).toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Array.isArray(node)) {
|
||||||
|
return node.map((x) => tabs(i + 1) + binaryNodeToString(x, i + 1)).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = binaryNodeToString(node.content, i + 1)
|
||||||
|
|
||||||
|
const tag = `<${node.tag} ${Object.entries(node.attrs || {})
|
||||||
|
.filter(([, v]) => v !== undefined)
|
||||||
|
.map(([k, v]) => `${k}='${v}'`)
|
||||||
|
.join(' ')}`
|
||||||
|
|
||||||
|
const content: string = children ? `>\n${children}\n${tabs(i)}</${node.tag}>` : '/>'
|
||||||
|
|
||||||
|
return tag + content
|
||||||
}
|
}
|
||||||
57
yarn.lock
57
yarn.lock
@@ -1298,6 +1298,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz"
|
resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz"
|
||||||
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
|
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
|
||||||
|
|
||||||
|
"@types/uuid@^9.0.0":
|
||||||
|
version "9.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.1.tgz#98586dc36aee8dacc98cc396dbca8d0429647aa6"
|
||||||
|
integrity sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==
|
||||||
|
|
||||||
"@types/ws@^8.0.0":
|
"@types/ws@^8.0.0":
|
||||||
version "8.5.3"
|
version "8.5.3"
|
||||||
resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz"
|
resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz"
|
||||||
@@ -2142,6 +2147,11 @@ color@^4.2.3:
|
|||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
color-string "^1.9.0"
|
color-string "^1.9.0"
|
||||||
|
|
||||||
|
colorette@^2.0.7:
|
||||||
|
version "2.0.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||||
|
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||||
|
|
||||||
combined-stream@^1.0.6, combined-stream@^1.0.8:
|
combined-stream@^1.0.6, combined-stream@^1.0.8:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
|
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
|
||||||
@@ -3121,6 +3131,11 @@ fast-redact@^3.0.0:
|
|||||||
resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.1.tgz"
|
resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.1.tgz"
|
||||||
integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A==
|
integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A==
|
||||||
|
|
||||||
|
fast-safe-stringify@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
|
||||||
|
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||||
|
|
||||||
fastq@^1.6.0:
|
fastq@^1.6.0:
|
||||||
version "1.15.0"
|
version "1.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
|
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
|
||||||
@@ -4654,6 +4669,11 @@ jimp@^0.16.1:
|
|||||||
"@jimp/types" "^0.16.1"
|
"@jimp/types" "^0.16.1"
|
||||||
regenerator-runtime "^0.13.3"
|
regenerator-runtime "^0.13.3"
|
||||||
|
|
||||||
|
joycon@^3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
|
||||||
|
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
|
||||||
|
|
||||||
jpeg-js@0.4.2:
|
jpeg-js@0.4.2:
|
||||||
version "0.4.2"
|
version "0.4.2"
|
||||||
resolved "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz"
|
resolved "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz"
|
||||||
@@ -4822,6 +4842,11 @@ levn@~0.3.0:
|
|||||||
prelude-ls "~1.1.2"
|
prelude-ls "~1.1.2"
|
||||||
type-check "~0.3.2"
|
type-check "~0.3.2"
|
||||||
|
|
||||||
|
libphonenumber-js@^1.10.20:
|
||||||
|
version "1.10.28"
|
||||||
|
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.28.tgz#cae7e929cad96cee5ecc9449027192ecba39ee72"
|
||||||
|
integrity sha512-1eAgjLrZA0+2Wgw4hs+4Q/kEBycxQo8ZLYnmOvZ3AlM8ImAVAJgDPlZtISLEzD1vunc2q8s2Pn7XwB7I8U3Kzw==
|
||||||
|
|
||||||
"libsignal@https://github.com/adiwajshing/libsignal-node.git":
|
"libsignal@https://github.com/adiwajshing/libsignal-node.git":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://github.com/adiwajshing/libsignal-node.git#11dbd962ea108187c79a7c46fe4d6f790e23da97"
|
resolved "https://github.com/adiwajshing/libsignal-node.git#11dbd962ea108187c79a7c46fe4d6f790e23da97"
|
||||||
@@ -5403,6 +5428,11 @@ on-exit-leak-free@^0.2.0:
|
|||||||
resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz"
|
resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz"
|
||||||
integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==
|
integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==
|
||||||
|
|
||||||
|
on-exit-leak-free@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
|
||||||
|
integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==
|
||||||
|
|
||||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
||||||
@@ -5730,6 +5760,26 @@ pino-abstract-transport@v0.5.0:
|
|||||||
duplexify "^4.1.2"
|
duplexify "^4.1.2"
|
||||||
split2 "^4.0.0"
|
split2 "^4.0.0"
|
||||||
|
|
||||||
|
pino-pretty@^9.4.0:
|
||||||
|
version "9.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.4.0.tgz#fc4026e83c87272cbdfb7afed121770e6000940c"
|
||||||
|
integrity sha512-NIudkNLxnl7MGj1XkvsqVyRgo6meFP82ECXF2PlOI+9ghmbGuBUUqKJ7IZPIxpJw4vhhSva0IuiDSAuGh6TV9g==
|
||||||
|
dependencies:
|
||||||
|
colorette "^2.0.7"
|
||||||
|
dateformat "^4.6.3"
|
||||||
|
fast-copy "^3.0.0"
|
||||||
|
fast-safe-stringify "^2.1.1"
|
||||||
|
help-me "^4.0.1"
|
||||||
|
joycon "^3.1.1"
|
||||||
|
minimist "^1.2.6"
|
||||||
|
on-exit-leak-free "^2.1.0"
|
||||||
|
pino-abstract-transport "^1.0.0"
|
||||||
|
pump "^3.0.0"
|
||||||
|
readable-stream "^4.0.0"
|
||||||
|
secure-json-parse "^2.4.0"
|
||||||
|
sonic-boom "^3.0.0"
|
||||||
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
pino-std-serializers@^4.0.0:
|
pino-std-serializers@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz"
|
resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz"
|
||||||
@@ -6526,6 +6576,13 @@ sonic-boom@^2.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
atomic-sleep "^1.0.0"
|
atomic-sleep "^1.0.0"
|
||||||
|
|
||||||
|
sonic-boom@^3.0.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c"
|
||||||
|
integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==
|
||||||
|
dependencies:
|
||||||
|
atomic-sleep "^1.0.0"
|
||||||
|
|
||||||
source-map-support@^0.5.6:
|
source-map-support@^0.5.6:
|
||||||
version "0.5.21"
|
version "0.5.21"
|
||||||
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz"
|
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user