feat: improve version update robustness and frequency

Changes:
- Update frequency: weekly → daily (06:00 UTC)
- Add retry with exponential backoff (3 attempts per source)
- Add multiple source endpoints (sw.js + bootstrap page)
- Add version validation (sanity checks on revision numbers)
- Add fetch timeout to prevent hanging
- Add detailed logging for debugging
- Simplify workflow to only update baileys-version.json

This makes the version update more reliable and responsive
to WhatsApp Web changes.
This commit is contained in:
Claude
2026-01-21 16:22:46 +00:00
parent 8a134d2420
commit b9a7c2cabc
2 changed files with 224 additions and 59 deletions
+197 -49
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env node
/**
* Script to update WhatsApp Web version.
* Fetches the latest version from web.whatsapp.com and updates:
* - src/Defaults/baileys-version.json (SINGLE SOURCE OF TRUTH)
* Script to update WhatsApp Web version with robust error handling.
* Fetches the latest version from web.whatsapp.com with:
* - Retry with exponential backoff
* - Multiple source endpoints for redundancy
* - Version validation before updating
*
* Other files (index.ts, generics.ts) import from this JSON file,
* so only one file needs to be updated.
* Updates: src/Defaults/baileys-version.json (SINGLE SOURCE OF TRUTH)
*
* Usage: yarn update:version
*/
@@ -24,60 +25,188 @@ type WAVersion = [number, number, number]
interface VersionResult {
version: WAVersion
isLatest: boolean
source?: string
error?: unknown
}
// Configuration
const CONFIG = {
maxRetries: 3,
retryDelayMs: 2000, // Base delay, will be multiplied by attempt number
requestTimeoutMs: 10000,
// Version sanity checks
minRevision: 1000000000, // Minimum expected revision (to catch parsing errors)
maxRevision: 9999999999, // Maximum expected revision
}
/**
* Fetches the latest WhatsApp Web version from web.whatsapp.com
* Extracted here to avoid circular dependency with generics.ts
* Sleep for specified milliseconds
*/
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* Fetch with timeout
*/
async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number): Promise<Response> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
return response
} finally {
clearTimeout(timeout)
}
}
/**
* Validates that a revision number looks reasonable
*/
function isValidRevision(revision: number): boolean {
return (
Number.isInteger(revision) &&
revision >= CONFIG.minRevision &&
revision <= CONFIG.maxRevision
)
}
/**
* Fetches version from WhatsApp Web Service Worker (primary source)
*/
async function fetchFromServiceWorker(): Promise<VersionResult> {
const url = 'https://web.whatsapp.com/sw.js'
const headers = {
'sec-fetch-site': 'none',
'sec-fetch-mode': 'navigate',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'accept': '*/*',
'accept-language': 'en-US,en;q=0.9',
}
const response = await fetchWithTimeout(url, { method: 'GET', headers }, CONFIG.requestTimeoutMs)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.text()
// Try multiple patterns for robustness
const patterns = [
/\\?"client_revision\\?":\s*(\d+)/,
/"client_revision":\s*(\d+)/,
/client_revision["']?\s*:\s*(\d+)/,
]
for (const regex of patterns) {
const match = data.match(regex)
if (match?.[1]) {
const revision = parseInt(match[1], 10)
if (isValidRevision(revision)) {
return {
version: [2, 3000, revision] as WAVersion,
isLatest: true,
source: 'sw.js'
}
}
}
}
throw new Error('Could not find valid client_revision in sw.js')
}
/**
* Fetches version from WhatsApp Web bootstrap (backup source)
*/
async function fetchFromBootstrap(): Promise<VersionResult> {
const url = 'https://web.whatsapp.com/'
const headers = {
'sec-fetch-site': 'none',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'accept': 'text/html,application/xhtml+xml',
}
const response = await fetchWithTimeout(url, { method: 'GET', headers }, CONFIG.requestTimeoutMs)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.text()
// Look for version in HTML/JS bootstrap
const patterns = [
/\"client_revision\":(\d+)/,
/clientRevision['":\s]+(\d+)/i,
]
for (const regex of patterns) {
const match = data.match(regex)
if (match?.[1]) {
const revision = parseInt(match[1], 10)
if (isValidRevision(revision)) {
return {
version: [2, 3000, revision] as WAVersion,
isLatest: true,
source: 'bootstrap'
}
}
}
}
throw new Error('Could not find valid client_revision in bootstrap page')
}
/**
* Fetches the latest WhatsApp Web version with retry and fallback
*/
async function fetchLatestWaWebVersion(): Promise<VersionResult> {
// Read current version as fallback
const currentContent = readFileSync(VERSION_FILE_PATH, 'utf-8')
const fallbackVersion = JSON.parse(currentContent).version as WAVersion
try {
const headers = {
'sec-fetch-site': 'none',
'user-agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
}
const sources = [
{ name: 'Service Worker', fetch: fetchFromServiceWorker },
{ name: 'Bootstrap Page', fetch: fetchFromBootstrap },
]
const response = await fetch('https://web.whatsapp.com/sw.js', {
method: 'GET',
headers
})
const errors: string[] = []
if (!response.ok) {
throw new Error(`Failed to fetch sw.js: ${response.statusText}`)
}
for (const source of sources) {
for (let attempt = 1; attempt <= CONFIG.maxRetries; attempt++) {
try {
console.log(` Attempting ${source.name} (attempt ${attempt}/${CONFIG.maxRetries})...`)
const result = await source.fetch()
console.log(` ✓ Success from ${source.name}`)
return result
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
errors.push(`${source.name} attempt ${attempt}: ${errorMsg}`)
console.log(`${source.name} failed: ${errorMsg}`)
const data = await response.text()
const regex = /\\?"client_revision\\?":\s*(\d+)/
const match = data.match(regex)
if (!match?.[1]) {
return {
version: fallbackVersion,
isLatest: false,
error: { message: 'Could not find client revision in the fetched content' }
if (attempt < CONFIG.maxRetries) {
const delay = CONFIG.retryDelayMs * attempt
console.log(` Waiting ${delay}ms before retry...`)
await sleep(delay)
}
}
}
}
return {
version: [2, 3000, +match[1]] as WAVersion,
isLatest: true
}
} catch (error) {
return {
version: fallbackVersion,
isLatest: false,
error
}
console.log('\n⚠ All sources failed, using fallback version')
return {
version: fallbackVersion,
isLatest: false,
error: { message: 'All fetch attempts failed', details: errors }
}
}
function updateBaileysVersionJson(version: WAVersion): boolean {
function updateBaileysVersionJson(version: WAVersion, source?: string): boolean {
const content = { version }
try {
@@ -95,7 +224,10 @@ function updateBaileysVersionJson(version: WAVersion): boolean {
writeFileSync(VERSION_FILE_PATH, JSON.stringify(content) + '\n')
console.log(`✓ Updated baileys-version.json: [${currentVersion.join(', ')}] → [${version.join(', ')}]`)
console.log(` (index.ts and generics.ts will automatically use the new version)`)
if (source) {
console.log(` Source: ${source}`)
}
console.log(` (index.ts and generics.ts automatically use the new version via import)`)
return true
} catch (error) {
console.error(`✗ Failed to update baileys-version.json:`, error)
@@ -104,33 +236,49 @@ function updateBaileysVersionJson(version: WAVersion): boolean {
}
async function main() {
console.log('╔════════════════════════════════════════════════╗')
console.log('║ WhatsApp Web Version Update Script ║')
console.log('╚════════════════════════════════════════════════╝\n')
console.log('Fetching latest WhatsApp Web version...\n')
const result = await fetchLatestWaWebVersion()
if (!result.isLatest) {
console.error('Failed to fetch latest version:', result.error)
process.exit(1)
console.log('')
if (result.isLatest) {
console.log(`Latest version: [${result.version.join(', ')}]`)
if (result.source) {
console.log(`Source: ${result.source}\n`)
}
} else {
console.log(`⚠ Using fallback version: [${result.version.join(', ')}]`)
console.log(`Reason: ${JSON.stringify(result.error)}\n`)
}
console.log(`Latest version: [${result.version.join(', ')}]\n`)
const hasUpdates = updateBaileysVersionJson(result.version)
const hasUpdates = updateBaileysVersionJson(result.version, result.source)
console.log('')
if (hasUpdates) {
console.log('═══════════════════════════════════════════════')
console.log('Version update complete!')
console.log('Note: Only baileys-version.json needs updating - other files import from it.')
console.log('═══════════════════════════════════════════════')
if (process.env.GITHUB_OUTPUT) {
appendFileSync(process.env.GITHUB_OUTPUT, `updated=true\n`)
appendFileSync(process.env.GITHUB_OUTPUT, `version=${result.version.join('.')}\n`)
appendFileSync(process.env.GITHUB_OUTPUT, `source=${result.source || 'fallback'}\n`)
}
} else {
console.log('Already up to date.')
console.log('Already up to date. No changes needed.')
if (process.env.GITHUB_OUTPUT) {
appendFileSync(process.env.GITHUB_OUTPUT, `updated=false\n`)
}
}
// Exit with error if we couldn't fetch latest (so CI knows)
if (!result.isLatest) {
console.log('\n⚠ Warning: Could not fetch latest version from WhatsApp servers')
process.exit(0) // Don't fail the workflow, just warn
}
}
main().catch(error => {