46ffaf027c
Fixes from code review: 1. Fix #1,6: Use connection.update event instead of overriding sock.end() - Listens for 'close' event to cleanup interval - Handles both explicit close and internal disconnections 2. Fix #3: Exit code 2 when fetch fails (not 0) - Allows CI to distinguish success/error/fetch-failed - Properly signals fetch failures to workflows 3. Fix #4: Document revision bounds + add env vars - Added detailed comments explaining min/max revision values - Made configurable via WA_MIN_REVISION/WA_MAX_REVISION env vars 4. Fix #5,9: Remove unused fetchLatestVersion option - Removed from SocketConfig and defaults - Updated versionCheckIntervalMs docs to clarify it's only for makeWASocketAutoVersion 5. Fix #7: Use separate variable for version tracking - trackedVersion instead of mutating mergedConfig - Prevents unexpected side effects 6. Fix #8: Check socket state before emitting events - isSocketClosed flag to prevent race conditions - Double-check after async operations 7. Fix #10: Implement force parameter in workflow - Creates PR even without version changes when force=true - Useful for re-triggering updates manually Note: Test coverage (Fix #2) deferred to separate PR due to ESM mocking complexity with Jest.
328 lines
10 KiB
JavaScript
328 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 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
|
|
*
|
|
* Updates: src/Defaults/baileys-version.json (SINGLE SOURCE OF TRUTH)
|
|
*
|
|
* Usage: yarn update:version
|
|
*
|
|
* Environment variables:
|
|
* - WA_MIN_REVISION: Minimum valid revision number (default: 1000000000)
|
|
* - WA_MAX_REVISION: Maximum valid revision number (default: 9999999999)
|
|
*
|
|
* Exit codes:
|
|
* - 0: Success (version updated or already up to date)
|
|
* - 1: Fatal error (script failure)
|
|
* - 2: Fetch failed (all sources failed, fallback version used)
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, appendFileSync } from 'fs'
|
|
import { dirname, join } from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = dirname(__filename)
|
|
const ROOT_DIR = join(__dirname, '..')
|
|
const VERSION_FILE_PATH = join(ROOT_DIR, 'src/Defaults/baileys-version.json')
|
|
|
|
type WAVersion = [number, number, number]
|
|
|
|
interface VersionResult {
|
|
version: WAVersion
|
|
isLatest: boolean
|
|
source?: string
|
|
error?: unknown
|
|
}
|
|
|
|
/**
|
|
* Configuration for the version update script.
|
|
*
|
|
* Revision bounds explanation:
|
|
* - WhatsApp Web uses a numeric "client_revision" that increments with each release
|
|
* - As of 2024, revisions are in the 10-digit range (e.g., 1032141294)
|
|
* - minRevision (1000000000) ensures we don't accept clearly invalid small numbers
|
|
* that could result from parsing errors
|
|
* - maxRevision (9999999999) is the maximum 10-digit number, providing an upper sanity check
|
|
*
|
|
* These bounds can be overridden via environment variables if WhatsApp changes their
|
|
* versioning scheme in the future.
|
|
*/
|
|
const CONFIG = {
|
|
maxRetries: 3,
|
|
retryDelayMs: 2000, // Base delay, will be multiplied by attempt number
|
|
requestTimeoutMs: 10000,
|
|
// Version sanity checks - can be overridden via environment variables
|
|
minRevision: parseInt(process.env.WA_MIN_REVISION || '1000000000', 10),
|
|
maxRevision: parseInt(process.env.WA_MAX_REVISION || '9999999999', 10),
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* WhatsApp Web revisions are typically 10-digit numbers.
|
|
*
|
|
* @param revision - The revision number to validate
|
|
* @returns true if the revision is within expected bounds
|
|
*/
|
|
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
|
|
|
|
const sources = [
|
|
{ name: 'Service Worker', fetch: fetchFromServiceWorker },
|
|
{ name: 'Bootstrap Page', fetch: fetchFromBootstrap },
|
|
]
|
|
|
|
const errors: string[] = []
|
|
|
|
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}`)
|
|
|
|
if (attempt < CONFIG.maxRetries) {
|
|
const delay = CONFIG.retryDelayMs * attempt
|
|
console.log(` Waiting ${delay}ms before retry...`)
|
|
await sleep(delay)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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, source?: string): boolean {
|
|
const content = { version }
|
|
|
|
try {
|
|
const currentContent = readFileSync(VERSION_FILE_PATH, 'utf-8')
|
|
const currentVersion = JSON.parse(currentContent).version as number[]
|
|
|
|
if (
|
|
currentVersion[0] === version[0] &&
|
|
currentVersion[1] === version[1] &&
|
|
currentVersion[2] === version[2]
|
|
) {
|
|
console.log(`✓ baileys-version.json already up to date`)
|
|
return false
|
|
}
|
|
|
|
writeFileSync(VERSION_FILE_PATH, JSON.stringify(content) + '\n')
|
|
console.log(`✓ Updated baileys-version.json: [${currentVersion.join(', ')}] → [${version.join(', ')}]`)
|
|
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)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log('╔════════════════════════════════════════════════╗')
|
|
console.log('║ WhatsApp Web Version Update Script ║')
|
|
console.log('╚════════════════════════════════════════════════╝\n')
|
|
|
|
// Log configuration
|
|
console.log('Configuration:')
|
|
console.log(` Min revision: ${CONFIG.minRevision}`)
|
|
console.log(` Max revision: ${CONFIG.maxRevision}`)
|
|
console.log(` Max retries: ${CONFIG.maxRetries}`)
|
|
console.log(` Request timeout: ${CONFIG.requestTimeoutMs}ms\n`)
|
|
|
|
console.log('Fetching latest WhatsApp Web version...\n')
|
|
|
|
const result = await fetchLatestWaWebVersion()
|
|
|
|
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`)
|
|
}
|
|
|
|
const hasUpdates = updateBaileysVersionJson(result.version, result.source)
|
|
|
|
console.log('')
|
|
if (hasUpdates) {
|
|
console.log('═══════════════════════════════════════════════')
|
|
console.log('Version update complete!')
|
|
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. No changes needed.')
|
|
if (process.env.GITHUB_OUTPUT) {
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `updated=false\n`)
|
|
}
|
|
}
|
|
|
|
// Exit with code 2 if fetch failed (Fix #3)
|
|
// This allows CI to distinguish between:
|
|
// - 0: Success
|
|
// - 1: Script error
|
|
// - 2: Fetch failed but fallback used
|
|
if (!result.isLatest) {
|
|
console.log('\n⚠ Warning: Could not fetch latest version from WhatsApp servers')
|
|
console.log('Exit code 2: Fetch failed, fallback version used')
|
|
if (process.env.GITHUB_OUTPUT) {
|
|
appendFileSync(process.env.GITHUB_OUTPUT, `fetch_failed=true\n`)
|
|
}
|
|
process.exit(2)
|
|
}
|
|
}
|
|
|
|
main().catch(error => {
|
|
console.error('Fatal error:', error)
|
|
process.exit(1)
|
|
})
|