feat: add Android browser preset for companion device registration (#248)
* feat: add Android browser preset for companion device registration Add Browsers.android(apiLevel) preset that identifies as an Android companion device (SMB_ANDROID platform, ANDROID_PHONE device type). Validated against Frida captures of real WhatsApp Android protocol. * feat: support BAILEYS_BROWSER env var for browser selection Allows selecting Android companion mode via environment variable without code changes. Set BAILEYS_BROWSER=android (or android:15 for a specific API level) to register as an Android device. * fix: auto-detect Android browser in pair code and fall back to Chrome Pair code companion registration fails with Android browser because the WA server rejects the ANDROID_PHONE companion_platform_id over the web Noise protocol. When Android is detected, requestPairingCode() now transparently uses Chrome/macOS platform values for the pair code stanza while QR code pairing continues to work as Android (SMB_ANDROID). * docs: add android to isValidBrowserPreset JSDoc
This commit is contained in:
+17
-1
@@ -56,10 +56,26 @@ export const PROCESSABLE_HISTORY_TYPES = [
|
||||
// 6 hours in milliseconds
|
||||
const SIX_HOURS_MS = 6 * 60 * 60 * 1000
|
||||
|
||||
/**
|
||||
* Resolves the default browser tuple from the BAILEYS_BROWSER env var.
|
||||
* 'android' → Browsers.android('14')
|
||||
* 'android:15' → Browsers.android('15')
|
||||
* unset / other → Browsers.macOS('Chrome')
|
||||
*/
|
||||
const resolveDefaultBrowser = (): [string, string, string] => {
|
||||
const env = process.env.BAILEYS_BROWSER?.trim().toLowerCase()
|
||||
if (env?.startsWith('android')) {
|
||||
const apiLevel = env.split(':')[1] || '14'
|
||||
return Browsers.android(apiLevel)
|
||||
}
|
||||
|
||||
return Browsers.macOS('Chrome')
|
||||
}
|
||||
|
||||
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
||||
version: version as WAVersion,
|
||||
versionCheckIntervalMs: SIX_HOURS_MS,
|
||||
browser: Browsers.macOS('Chrome'),
|
||||
browser: resolveDefaultBrowser(),
|
||||
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
|
||||
connectTimeoutMs: 20_000,
|
||||
keepAliveIntervalMs: 15_000,
|
||||
|
||||
+11
-3
@@ -38,7 +38,7 @@ import {
|
||||
signedKeyPair,
|
||||
xmppSignedPreKey
|
||||
} from '../Utils'
|
||||
import { getPlatformId } from '../Utils/browser-utils'
|
||||
import { getPlatformId, isAndroidBrowser } from '../Utils/browser-utils'
|
||||
import {
|
||||
CircuitBreaker,
|
||||
CircuitOpenError,
|
||||
@@ -1352,6 +1352,14 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
||||
name: '~'
|
||||
}
|
||||
|
||||
// Pair code with Android browser doesn't work (WA server rejects the
|
||||
// companion registration). When Android is detected, fall back to the
|
||||
// Chrome/macOS platform values that are known to work.
|
||||
const isAndroid = isAndroidBrowser(browser)
|
||||
const pairPlatformId = isAndroid ? getPlatformId('Chrome') : getPlatformId(browser[1])
|
||||
const pairPlatformDisplay = isAndroid ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})`
|
||||
|
||||
ev.emit('creds.update', authState.creds)
|
||||
await sendNode({
|
||||
tag: 'iq',
|
||||
@@ -1384,12 +1392,12 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
{
|
||||
tag: 'companion_platform_id',
|
||||
attrs: {},
|
||||
content: getPlatformId(browser[1])
|
||||
content: pairPlatformId
|
||||
},
|
||||
{
|
||||
tag: 'companion_platform_display',
|
||||
attrs: {},
|
||||
content: `${browser[1]} (${browser[0]})`
|
||||
content: pairPlatformDisplay
|
||||
},
|
||||
{
|
||||
tag: 'link_code_pairing_nonce',
|
||||
|
||||
@@ -23,6 +23,7 @@ export type BrowsersMap = {
|
||||
baileys(browser: string): [string, string, string]
|
||||
windows(browser: string): [string, string, string]
|
||||
appropriate(browser: string): [string, string, string]
|
||||
android(apiLevel: string): [string, string, string]
|
||||
}
|
||||
|
||||
export enum DisconnectReason {
|
||||
|
||||
@@ -94,7 +94,13 @@ const BROWSER_TO_PLATFORM_ID: ReadonlyMap<string, string> = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
return new Map(entries)
|
||||
const map = new Map(entries)
|
||||
// ANDROID → ANDROID_PHONE alias (DeviceProps.PlatformType has ANDROID_PHONE but not ANDROID)
|
||||
if (!map.has('ANDROID') && map.has('ANDROID_PHONE')) {
|
||||
map.set('ANDROID', map.get('ANDROID_PHONE')!)
|
||||
}
|
||||
|
||||
return map
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
@@ -331,7 +337,8 @@ export const Browsers: BrowsersMap = {
|
||||
macOS: (browser: string): [string, string, string] => ['Mac OS', browser, OS_VERSIONS.macOS],
|
||||
windows: (browser: string): [string, string, string] => ['Windows', browser, OS_VERSIONS.windows],
|
||||
baileys: (browser: string): [string, string, string] => ['Baileys', browser, OS_VERSIONS.baileys],
|
||||
appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, getAppropriateVersion()]
|
||||
appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, getAppropriateVersion()],
|
||||
android: (apiLevel: string): [string, string, string] => [apiLevel, 'Android', '']
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -378,7 +385,7 @@ export const getPlatformId = (browser: unknown): string => {
|
||||
* properties like 'toString' or 'constructor'.
|
||||
*
|
||||
* @param value - Value to check
|
||||
* @returns True if value is a valid browser preset key ('ubuntu', 'macOS', 'windows', 'baileys', 'appropriate')
|
||||
* @returns True if value is a valid browser preset key ('ubuntu', 'macOS', 'windows', 'baileys', 'appropriate', 'android')
|
||||
*
|
||||
* @example
|
||||
* isValidBrowserPreset('ubuntu') // true
|
||||
@@ -391,6 +398,16 @@ export const getPlatformId = (browser: unknown): string => {
|
||||
* const config = Browsers[userInput]('MyApp')
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* Checks if the browser tuple represents an Android companion device.
|
||||
*
|
||||
* @param browser - Browser tuple [os, platform, version]
|
||||
* @returns True if platform is 'Android' (case-insensitive)
|
||||
*/
|
||||
export const isAndroidBrowser = (browser: [string, string, string]): boolean => {
|
||||
return browser[1]?.toUpperCase() === 'ANDROID'
|
||||
}
|
||||
|
||||
export const isValidBrowserPreset = (value: unknown): value is keyof BrowsersMap => {
|
||||
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(Browsers, value)
|
||||
}
|
||||
|
||||
@@ -9,24 +9,27 @@ import {
|
||||
} from '../Defaults'
|
||||
import type { AuthenticationCreds, SignalCreds, SocketConfig } from '../Types'
|
||||
import { type BinaryNode, getBinaryNodeChild, jidDecode, S_WHATSAPP_NET } from '../WABinary'
|
||||
import { isAndroidBrowser } from './browser-utils'
|
||||
import { Curve, hmacSign } from './crypto'
|
||||
import { encodeBigEndian } from './generics'
|
||||
import { createSignalIdentity } from './signal'
|
||||
|
||||
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
||||
const isAndroid = isAndroidBrowser(config.browser)
|
||||
return {
|
||||
appVersion: {
|
||||
primary: config.version[0],
|
||||
secondary: config.version[1],
|
||||
tertiary: config.version[2]
|
||||
},
|
||||
platform: proto.ClientPayload.UserAgent.Platform.MACOS,
|
||||
platform: isAndroid
|
||||
? proto.ClientPayload.UserAgent.Platform.SMB_ANDROID
|
||||
: proto.ClientPayload.UserAgent.Platform.MACOS,
|
||||
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
||||
osVersion: '0.1',
|
||||
device: 'Desktop',
|
||||
osVersion: isAndroid ? config.browser[0] : '0.1',
|
||||
device: isAndroid ? 'Android' : 'Desktop',
|
||||
osBuildNumber: '0.1',
|
||||
localeLanguageIso6391: 'en',
|
||||
|
||||
mnc: '000',
|
||||
mcc: '000',
|
||||
localeCountryIso31661Alpha2: config.countryCode
|
||||
@@ -58,7 +61,9 @@ const getClientPayload = (config: SocketConfig) => {
|
||||
userAgent: getUserAgent(config)
|
||||
}
|
||||
|
||||
payload.webInfo = getWebInfo(config)
|
||||
if (!isAndroidBrowser(config.browser)) {
|
||||
payload.webInfo = getWebInfo(config)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
@@ -84,6 +89,10 @@ export const generateLoginNode = (userJid: string, config: SocketConfig): proto.
|
||||
|
||||
const getPlatformType = (platform: string): proto.DeviceProps.PlatformType => {
|
||||
const platformType = platform.toUpperCase()
|
||||
if (platformType === 'ANDROID') {
|
||||
return proto.DeviceProps.PlatformType.ANDROID_PHONE
|
||||
}
|
||||
|
||||
return (
|
||||
proto.DeviceProps.PlatformType[platformType as keyof typeof proto.DeviceProps.PlatformType] ||
|
||||
proto.DeviceProps.PlatformType.CHROME
|
||||
|
||||
Reference in New Issue
Block a user