Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d28d8a4596 | |||
| 17e3bd5d36 |
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
package proto;
|
||||
|
||||
/// WhatsApp Version: 2.3000.1034302344
|
||||
/// WhatsApp Version: 2.3000.1034274421
|
||||
|
||||
message ADVDeviceIdentity {
|
||||
optional uint32 rawId = 1;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":[2,3000,1034382497]}
|
||||
{"version":[2,3000,1034279434]}
|
||||
|
||||
+1
-24
@@ -56,33 +56,10 @@ 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.
|
||||
* Default: Android companion (SMB_ANDROID) — matches upstream PR #2201.
|
||||
* Pair code auto-detects Android and falls back to Chrome in socket.ts.
|
||||
*
|
||||
* unset / 'android' → Browsers.android('14')
|
||||
* 'android:15' → Browsers.android('15')
|
||||
* 'chrome' / 'macos' → Browsers.macOS('Chrome')
|
||||
*/
|
||||
const resolveDefaultBrowser = (): [string, string, string] => {
|
||||
const env = process.env.BAILEYS_BROWSER?.trim().toLowerCase()
|
||||
if (env === 'chrome' || env === 'macos') {
|
||||
return Browsers.macOS('Chrome')
|
||||
}
|
||||
|
||||
if (env?.startsWith('android:')) {
|
||||
const apiLevel = env.split(':')[1] || '14'
|
||||
return Browsers.android(apiLevel)
|
||||
}
|
||||
|
||||
return Browsers.android('14')
|
||||
}
|
||||
|
||||
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
||||
version: version as WAVersion,
|
||||
versionCheckIntervalMs: SIX_HOURS_MS,
|
||||
browser: resolveDefaultBrowser(),
|
||||
browser: Browsers.macOS('Chrome'),
|
||||
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
|
||||
connectTimeoutMs: 20_000,
|
||||
keepAliveIntervalMs: 15_000,
|
||||
|
||||
@@ -69,7 +69,7 @@ import {
|
||||
recordMessageReceived,
|
||||
recordMessageRetry
|
||||
} from '../Utils/prometheus-metrics.js'
|
||||
import { isTcTokenExpired, resolveTcTokenJid, storeTcTokensFromIqResult } from '../Utils/tc-token-utils'
|
||||
import { isTcTokenExpired, resolveTcTokenJid } from '../Utils/tc-token-utils'
|
||||
import {
|
||||
areJidsSameUser,
|
||||
type BinaryNode,
|
||||
@@ -1453,7 +1453,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
})
|
||||
|
||||
// When a session is refreshed (identity change), re-issue tctoken fire-and-forget
|
||||
// WABA Android: reissue stores senderTimestamp + realIssueTimestamp after IQ success
|
||||
if (result.action === 'session_refreshed') {
|
||||
const normalizedJid = jidNormalizedUser(from)
|
||||
resolveTcTokenJid(normalizedJid, getLIDForPN)
|
||||
@@ -1463,36 +1462,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
if (entry?.token?.length && !isTcTokenExpired(entry.timestamp)) {
|
||||
const senderTs = unixTimestampSeconds()
|
||||
logTcToken('reissue', { jid: normalizedJid, reason: 'session_refreshed' })
|
||||
getPrivacyTokens([normalizedJid], senderTs)
|
||||
.then(async (iqResult) => {
|
||||
await storeTcTokensFromIqResult({
|
||||
result: iqResult,
|
||||
fallbackJid: normalizedJid,
|
||||
keys: authState.keys,
|
||||
getLIDForPN,
|
||||
onNewJidStored: (storedJid) => {
|
||||
tcTokenKnownJids.add(storedJid)
|
||||
scheduleTcTokenIndexSave()
|
||||
}
|
||||
})
|
||||
// Persist senderTimestamp + realIssueTimestamp after IQ success
|
||||
const currentData = await authState.keys.get('tctoken', [tcJid])
|
||||
const currentEntry = currentData[tcJid]
|
||||
await authState.keys.set({
|
||||
tctoken: {
|
||||
[tcJid]: {
|
||||
...currentEntry,
|
||||
token: currentEntry?.token ?? Buffer.alloc(0),
|
||||
senderTimestamp: senderTs,
|
||||
realIssueTimestamp: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
logTcToken('reissue_ok', { jid: normalizedJid, reason: 'session_refreshed' })
|
||||
})
|
||||
.catch(err => {
|
||||
logTcToken('reissue_fail', { jid: normalizedJid, error: err?.message })
|
||||
})
|
||||
getPrivacyTokens([normalizedJid], senderTs).catch(err => {
|
||||
logTcToken('reissue_fail', { jid: normalizedJid, error: err?.message })
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1940,10 +1912,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
[storageJid]: {
|
||||
...existing,
|
||||
token: Buffer.from(content),
|
||||
timestamp,
|
||||
// WABA Android: resets real_issue_timestamp when a new incoming token arrives
|
||||
// (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null)
|
||||
realIssueTimestamp: null
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2854,24 +2823,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const jid = jidNormalizedUser(attrs.from)
|
||||
logTcToken('error_463', { jid, msgId })
|
||||
|
||||
// WABA Android: error 463 triggers getPrivacyTokens() fire-and-forget
|
||||
// to ensure token is available for the retry below
|
||||
getPrivacyTokens([jid])
|
||||
.then(async (result) => {
|
||||
await storeTcTokensFromIqResult({
|
||||
result,
|
||||
fallbackJid: jid,
|
||||
keys: authState.keys,
|
||||
getLIDForPN,
|
||||
onNewJidStored: (storedJid) => {
|
||||
tcTokenKnownJids.add(storedJid)
|
||||
scheduleTcTokenIndexSave()
|
||||
}
|
||||
})
|
||||
logTcToken('fetched', { jid, reason: 'error_463' })
|
||||
})
|
||||
.catch(() => { /* fire-and-forget */ })
|
||||
|
||||
// Single-retry: wait 1.5s for the server's tctoken notification to arrive,
|
||||
// then resend. A Set prevents infinite retry loops.
|
||||
// Composite key (jid:msgId) ensures retries are isolated per destination.
|
||||
@@ -2907,24 +2858,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
return
|
||||
}
|
||||
} else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
||||
const jid479 = jidNormalizedUser(attrs.from)
|
||||
logTcToken('error_479', { jid: jid479, msgId: attrs.id })
|
||||
// WABA Android: error 479 (SmaxInvalid) also triggers token re-fetch
|
||||
getPrivacyTokens([jid479])
|
||||
.then(async (result) => {
|
||||
await storeTcTokensFromIqResult({
|
||||
result,
|
||||
fallbackJid: jid479,
|
||||
keys: authState.keys,
|
||||
getLIDForPN,
|
||||
onNewJidStored: (storedJid) => {
|
||||
tcTokenKnownJids.add(storedJid)
|
||||
scheduleTcTokenIndexSave()
|
||||
}
|
||||
})
|
||||
logTcToken('fetched', { jid: jid479, reason: 'error_479' })
|
||||
})
|
||||
.catch(() => { /* fire-and-forget */ })
|
||||
logTcToken('error_479', { jid: attrs.from, msgId: attrs.id })
|
||||
} else {
|
||||
logger.warn({ attrs }, 'received error in ack')
|
||||
}
|
||||
|
||||
@@ -1611,8 +1611,6 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
|
||||
// Persist senderTimestamp unconditionally — WA Web stores it in the chat table
|
||||
// regardless of whether a token exists. Spread preserves token+timestamp if present.
|
||||
// WABA Android: INSERT INTO wa_trusted_contacts_send (jid, sent_tc_token_timestamp, real_issue_timestamp)
|
||||
// VALUES (?, ?, 0) — realIssueTimestamp=0 means issued but not yet confirmed by server
|
||||
const currentData = await authState.keys.get('tctoken', [tcTokenJid])
|
||||
const currentEntry = currentData[tcTokenJid]
|
||||
await authState.keys.set({
|
||||
@@ -1620,8 +1618,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
[tcTokenJid]: {
|
||||
...currentEntry,
|
||||
token: currentEntry?.token ?? Buffer.alloc(0),
|
||||
senderTimestamp: issueTimestamp,
|
||||
realIssueTimestamp: 0
|
||||
senderTimestamp: issueTimestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+6
-25
@@ -38,7 +38,7 @@ import {
|
||||
signedKeyPair,
|
||||
xmppSignedPreKey
|
||||
} from '../Utils'
|
||||
import { getPlatformId, isAndroidBrowser } from '../Utils/browser-utils'
|
||||
import { getPlatformId } from '../Utils/browser-utils'
|
||||
import {
|
||||
CircuitBreaker,
|
||||
CircuitOpenError,
|
||||
@@ -634,6 +634,8 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
}
|
||||
helloMsg = proto.HandshakeMessage.fromObject(helloMsg)
|
||||
|
||||
logger.info({ browser, helloMsg }, 'connected to WA')
|
||||
|
||||
const init = proto.HandshakeMessage.encode(helloMsg).finish()
|
||||
|
||||
const result = await awaitNextMessage<Uint8Array>(init)
|
||||
@@ -1350,25 +1352,6 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
||||
name: '~'
|
||||
}
|
||||
|
||||
// Pair code companion_platform_id must be Chrome (1) when using Android
|
||||
// browser preset. ANDROID_PHONE (16) causes silent timeout (server ignores),
|
||||
// UWP (21) causes "cannot connect device" rejection. Only Chrome (1) works
|
||||
// for pair code via web protocol (WA\x06\x03). The device still appears as
|
||||
// "Android" in linked devices because DeviceProps.platformType=ANDROID_PHONE
|
||||
// is set separately in the registration node.
|
||||
const isAndroid = isAndroidBrowser(browser)
|
||||
const pairPlatformId = isAndroid ? getPlatformId('Chrome') : getPlatformId(browser[1])
|
||||
const pairPlatformDisplay = isAndroid ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})`
|
||||
|
||||
logger.info({
|
||||
pairCode: pairingCode,
|
||||
jid: authState.creds.me.id,
|
||||
companionPlatformId: pairPlatformId,
|
||||
companionPlatformDisplay: pairPlatformDisplay,
|
||||
isAndroid,
|
||||
}, `pair code requested | companion: ${pairPlatformDisplay} | ${isAndroid ? 'android override -> Chrome' : 'native platform'}`)
|
||||
|
||||
ev.emit('creds.update', authState.creds)
|
||||
await sendNode({
|
||||
tag: 'iq',
|
||||
@@ -1401,12 +1384,12 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
{
|
||||
tag: 'companion_platform_id',
|
||||
attrs: {},
|
||||
content: pairPlatformId
|
||||
content: getPlatformId(browser[1])
|
||||
},
|
||||
{
|
||||
tag: 'companion_platform_display',
|
||||
attrs: {},
|
||||
content: pairPlatformDisplay
|
||||
content: `${browser[1]} (${browser[0]})`
|
||||
},
|
||||
{
|
||||
tag: 'link_code_pairing_nonce',
|
||||
@@ -1536,9 +1519,7 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
})
|
||||
// login complete
|
||||
ws.on('CB:success', async (node: BinaryNode) => {
|
||||
const isAndroid = isAndroidBrowser(browser)
|
||||
const phoneId = authState.creds.me?.id?.split(':')[0]?.split('@')[0] || 'new session'
|
||||
logger.info(`${isAndroid ? '\uD83D\uDCF1' : '\uD83D\uDDA5\uFE0F'} Connected to WA | ${phoneId} | platform: ${isAndroid ? 'SMB_ANDROID' : 'MACOS'} | device: ${isAndroid ? 'Android' : 'Desktop'} | platformType: ${isAndroid ? 'ANDROID_PHONE' : 'CHROME'}`)
|
||||
logger.info('opened connection to WA')
|
||||
clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try
|
||||
|
||||
ev.emit('creds.update', { me: { ...authState.creds.me!, lid: node.attrs.lid } })
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ export type SignalDataTypeMap = {
|
||||
'app-state-sync-version': LTHashState
|
||||
'lid-mapping': string
|
||||
'device-list': string[]
|
||||
tctoken: { token: Buffer; timestamp?: string; senderTimestamp?: number; realIssueTimestamp?: number | null }
|
||||
tctoken: { token: Buffer; timestamp?: string; senderTimestamp?: number }
|
||||
/** Identity key for Signal Protocol - used for detecting contact reinstalls */
|
||||
'identity-key': Uint8Array
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ 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,13 +94,7 @@ const BROWSER_TO_PLATFORM_ID: ReadonlyMap<string, string> = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return new Map(entries)
|
||||
})()
|
||||
|
||||
// ============================================================
|
||||
@@ -337,8 +331,7 @@ 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()],
|
||||
android: (apiLevel: string): [string, string, string] => [apiLevel, 'Android', '']
|
||||
appropriate: (browser: string): [string, string, string] => [getPlatformName(), browser, getAppropriateVersion()]
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -385,7 +378,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', 'android')
|
||||
* @returns True if value is a valid browser preset key ('ubuntu', 'macOS', 'windows', 'baileys', 'appropriate')
|
||||
*
|
||||
* @example
|
||||
* isValidBrowserPreset('ubuntu') // true
|
||||
@@ -398,16 +391,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -159,10 +159,7 @@ export async function storeTcTokensFromIqResult({
|
||||
[storageJid]: {
|
||||
...existingEntry,
|
||||
token: Buffer.from(tokenNode.content),
|
||||
timestamp: tokenNode.attrs.t,
|
||||
// WABA Android: resets real_issue_timestamp to null when storing a new token
|
||||
// (UPDATE wa_trusted_contacts_send SET real_issue_timestamp=null)
|
||||
realIssueTimestamp: null
|
||||
timestamp: tokenNode.attrs.t
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,12 +14,6 @@ import { encodeBigEndian } from './generics'
|
||||
import { createSignalIdentity } from './signal'
|
||||
|
||||
const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
||||
// Always use MACOS/Desktop identity for UserAgent — we connect via web
|
||||
// protocol (WA\x06\x03) so the server expects a web-like UserAgent.
|
||||
// Using SMB_ANDROID here causes pair code registration to fail with
|
||||
// "não é possível conectar" even though the phone shows the confirmation.
|
||||
// Android identity is only set in DeviceProps (registration node) which
|
||||
// determines the display name in "Linked Devices".
|
||||
return {
|
||||
appVersion: {
|
||||
primary: config.version[0],
|
||||
@@ -32,6 +26,7 @@ const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
||||
device: 'Desktop',
|
||||
osBuildNumber: '0.1',
|
||||
localeLanguageIso6391: 'en',
|
||||
|
||||
mnc: '000',
|
||||
mcc: '000',
|
||||
localeCountryIso31661Alpha2: config.countryCode
|
||||
@@ -60,10 +55,11 @@ const getClientPayload = (config: SocketConfig) => {
|
||||
const payload: proto.IClientPayload = {
|
||||
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
|
||||
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
|
||||
userAgent: getUserAgent(config),
|
||||
webInfo: getWebInfo(config)
|
||||
userAgent: getUserAgent(config)
|
||||
}
|
||||
|
||||
payload.webInfo = getWebInfo(config)
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -88,10 +84,6 @@ 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