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:
@@ -2,9 +2,14 @@ name: Update WhatsApp Version
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
# Run on the 1st of every month at 02:00 UTC
|
# Run daily at 06:00 UTC (when WhatsApp typically deploys updates)
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 6 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
force:
|
||||||
|
description: 'Force update even if version is the same'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -16,10 +21,10 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node and Corepack
|
- name: Setup Node and Corepack
|
||||||
uses: actions/setup-node@v3.6.0
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
@@ -29,7 +34,7 @@ jobs:
|
|||||||
corepack prepare yarn@4.x --activate
|
corepack prepare yarn@4.x --activate
|
||||||
|
|
||||||
- name: Restore Yarn Cache
|
- name: Restore Yarn Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
path: .yarn/cache
|
path: .yarn/cache
|
||||||
@@ -69,7 +74,9 @@ jobs:
|
|||||||
|
|
||||||
# Create new branch, commit, and push
|
# Create new branch, commit, and push
|
||||||
git checkout -b "$BRANCH_NAME"
|
git checkout -b "$BRANCH_NAME"
|
||||||
git add src/Defaults/baileys-version.json src/Defaults/index.ts src/Utils/generics.ts
|
|
||||||
|
# Only add the single source of truth file
|
||||||
|
git add src/Defaults/baileys-version.json
|
||||||
git commit -m "chore: update WhatsApp Web version"
|
git commit -m "chore: update WhatsApp Web version"
|
||||||
git push origin "$BRANCH_NAME"
|
git push origin "$BRANCH_NAME"
|
||||||
|
|
||||||
@@ -80,9 +87,19 @@ jobs:
|
|||||||
|
|
||||||
This PR updates the WhatsApp Web client revision to the latest version fetched from \`web.whatsapp.com\`.
|
This PR updates the WhatsApp Web client revision to the latest version fetched from \`web.whatsapp.com\`.
|
||||||
|
|
||||||
**Files updated:**
|
**Single source of truth:**
|
||||||
- \`src/Defaults/baileys-version.json\`
|
- \`src/Defaults/baileys-version.json\` (other files import from here)
|
||||||
- \`src/Defaults/index.ts\`
|
|
||||||
- \`src/Utils/generics.ts\`" \
|
**Update frequency:** Daily at 06:00 UTC" \
|
||||||
--base master \
|
--base master \
|
||||||
--head "$BRANCH_NAME" || echo "PR already exists or could not be created"
|
--head "$BRANCH_NAME" || echo "PR already exists or could not be created"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
run: |
|
||||||
|
echo "### WhatsApp Version Update" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${{ steps.check_changes.outputs.has_changes }}" == "true" ]; then
|
||||||
|
echo "✅ New version detected and PR created" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "ℹ️ Already up to date" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|||||||
+197
-49
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Script to update WhatsApp Web version.
|
* Script to update WhatsApp Web version with robust error handling.
|
||||||
* Fetches the latest version from web.whatsapp.com and updates:
|
* Fetches the latest version from web.whatsapp.com with:
|
||||||
* - src/Defaults/baileys-version.json (SINGLE SOURCE OF TRUTH)
|
* - Retry with exponential backoff
|
||||||
|
* - Multiple source endpoints for redundancy
|
||||||
|
* - Version validation before updating
|
||||||
*
|
*
|
||||||
* Other files (index.ts, generics.ts) import from this JSON file,
|
* Updates: src/Defaults/baileys-version.json (SINGLE SOURCE OF TRUTH)
|
||||||
* so only one file needs to be updated.
|
|
||||||
*
|
*
|
||||||
* Usage: yarn update:version
|
* Usage: yarn update:version
|
||||||
*/
|
*/
|
||||||
@@ -24,60 +25,188 @@ type WAVersion = [number, number, number]
|
|||||||
interface VersionResult {
|
interface VersionResult {
|
||||||
version: WAVersion
|
version: WAVersion
|
||||||
isLatest: boolean
|
isLatest: boolean
|
||||||
|
source?: string
|
||||||
error?: unknown
|
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
|
* Sleep for specified milliseconds
|
||||||
* Extracted here to avoid circular dependency with generics.ts
|
*/
|
||||||
|
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> {
|
async function fetchLatestWaWebVersion(): Promise<VersionResult> {
|
||||||
// Read current version as fallback
|
// Read current version as fallback
|
||||||
const currentContent = readFileSync(VERSION_FILE_PATH, 'utf-8')
|
const currentContent = readFileSync(VERSION_FILE_PATH, 'utf-8')
|
||||||
const fallbackVersion = JSON.parse(currentContent).version as WAVersion
|
const fallbackVersion = JSON.parse(currentContent).version as WAVersion
|
||||||
|
|
||||||
try {
|
const sources = [
|
||||||
const headers = {
|
{ name: 'Service Worker', fetch: fetchFromServiceWorker },
|
||||||
'sec-fetch-site': 'none',
|
{ name: 'Bootstrap Page', fetch: fetchFromBootstrap },
|
||||||
'user-agent':
|
]
|
||||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch('https://web.whatsapp.com/sw.js', {
|
const errors: string[] = []
|
||||||
method: 'GET',
|
|
||||||
headers
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
for (const source of sources) {
|
||||||
throw new Error(`Failed to fetch sw.js: ${response.statusText}`)
|
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()
|
if (attempt < CONFIG.maxRetries) {
|
||||||
const regex = /\\?"client_revision\\?":\s*(\d+)/
|
const delay = CONFIG.retryDelayMs * attempt
|
||||||
const match = data.match(regex)
|
console.log(` Waiting ${delay}ms before retry...`)
|
||||||
|
await sleep(delay)
|
||||||
if (!match?.[1]) {
|
}
|
||||||
return {
|
|
||||||
version: fallbackVersion,
|
|
||||||
isLatest: false,
|
|
||||||
error: { message: 'Could not find client revision in the fetched content' }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
console.log('\n⚠ All sources failed, using fallback version')
|
||||||
version: [2, 3000, +match[1]] as WAVersion,
|
return {
|
||||||
isLatest: true
|
version: fallbackVersion,
|
||||||
}
|
isLatest: false,
|
||||||
} catch (error) {
|
error: { message: 'All fetch attempts failed', details: errors }
|
||||||
return {
|
|
||||||
version: fallbackVersion,
|
|
||||||
isLatest: false,
|
|
||||||
error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBaileysVersionJson(version: WAVersion): boolean {
|
function updateBaileysVersionJson(version: WAVersion, source?: string): boolean {
|
||||||
const content = { version }
|
const content = { version }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +224,10 @@ function updateBaileysVersionJson(version: WAVersion): boolean {
|
|||||||
|
|
||||||
writeFileSync(VERSION_FILE_PATH, JSON.stringify(content) + '\n')
|
writeFileSync(VERSION_FILE_PATH, JSON.stringify(content) + '\n')
|
||||||
console.log(`✓ Updated baileys-version.json: [${currentVersion.join(', ')}] → [${version.join(', ')}]`)
|
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
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`✗ Failed to update baileys-version.json:`, error)
|
console.error(`✗ Failed to update baileys-version.json:`, error)
|
||||||
@@ -104,33 +236,49 @@ function updateBaileysVersionJson(version: WAVersion): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
console.log('╔════════════════════════════════════════════════╗')
|
||||||
|
console.log('║ WhatsApp Web Version Update Script ║')
|
||||||
|
console.log('╚════════════════════════════════════════════════╝\n')
|
||||||
|
|
||||||
console.log('Fetching latest WhatsApp Web version...\n')
|
console.log('Fetching latest WhatsApp Web version...\n')
|
||||||
|
|
||||||
const result = await fetchLatestWaWebVersion()
|
const result = await fetchLatestWaWebVersion()
|
||||||
|
|
||||||
if (!result.isLatest) {
|
console.log('')
|
||||||
console.error('Failed to fetch latest version:', result.error)
|
if (result.isLatest) {
|
||||||
process.exit(1)
|
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, result.source)
|
||||||
|
|
||||||
const hasUpdates = updateBaileysVersionJson(result.version)
|
|
||||||
|
|
||||||
console.log('')
|
console.log('')
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
|
console.log('═══════════════════════════════════════════════')
|
||||||
console.log('Version update complete!')
|
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) {
|
if (process.env.GITHUB_OUTPUT) {
|
||||||
appendFileSync(process.env.GITHUB_OUTPUT, `updated=true\n`)
|
appendFileSync(process.env.GITHUB_OUTPUT, `updated=true\n`)
|
||||||
appendFileSync(process.env.GITHUB_OUTPUT, `version=${result.version.join('.')}\n`)
|
appendFileSync(process.env.GITHUB_OUTPUT, `version=${result.version.join('.')}\n`)
|
||||||
|
appendFileSync(process.env.GITHUB_OUTPUT, `source=${result.source || 'fallback'}\n`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Already up to date.')
|
console.log('Already up to date. No changes needed.')
|
||||||
if (process.env.GITHUB_OUTPUT) {
|
if (process.env.GITHUB_OUTPUT) {
|
||||||
appendFileSync(process.env.GITHUB_OUTPUT, `updated=false\n`)
|
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 => {
|
main().catch(error => {
|
||||||
|
|||||||
Reference in New Issue
Block a user