feat: centralize WhatsApp version + improve update robustness
feat: centralize WhatsApp version + improve update robustness
This commit is contained in:
@@ -2,9 +2,15 @@ name: Update WhatsApp Version
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run on the 1st of every month at 02:00 UTC
|
||||
- cron: '0 0 * * 0'
|
||||
# Run daily at 06:00 UTC (when WhatsApp typically deploys updates)
|
||||
- cron: '0 6 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force:
|
||||
description: 'Force create PR even if version unchanged (useful for re-triggering)'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -16,10 +22,10 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node and Corepack
|
||||
uses: actions/setup-node@v3.6.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
@@ -29,7 +35,7 @@ jobs:
|
||||
corepack prepare yarn@4.x --activate
|
||||
|
||||
- name: Restore Yarn Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: .yarn/cache
|
||||
@@ -42,23 +48,52 @@ jobs:
|
||||
|
||||
- name: Update WhatsApp Web Version
|
||||
id: update_version
|
||||
run: yarn update:version
|
||||
run: |
|
||||
# Run the update script, capture exit code
|
||||
# Exit codes: 0=success, 1=error, 2=fetch failed (fallback used)
|
||||
set +e
|
||||
yarn update:version
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
|
||||
|
||||
# Only fail on fatal errors (exit code 1)
|
||||
if [ $EXIT_CODE -eq 1 ]; then
|
||||
echo "::error::Script failed with fatal error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Exit code 2 means fetch failed but fallback was used - this is a warning
|
||||
if [ $EXIT_CODE -eq 2 ]; then
|
||||
echo "::warning::Could not fetch latest version from WhatsApp servers"
|
||||
fi
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
FORCE="${{ inputs.force }}"
|
||||
|
||||
if git diff --quiet; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
if [ "$FORCE" == "true" ]; then
|
||||
echo "force_mode=true" >> $GITHUB_OUTPUT
|
||||
echo "::notice::Force mode enabled - will create PR even without changes"
|
||||
else
|
||||
echo "force_mode=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "force_mode=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
if: steps.check_changes.outputs.has_changes == 'true' || steps.check_changes.outputs.force_mode == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BRANCH_NAME="update-version/stable"
|
||||
FORCE="${{ inputs.force }}"
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
@@ -67,22 +102,61 @@ jobs:
|
||||
# Check if branch exists and delete it
|
||||
git push origin --delete "$BRANCH_NAME" 2>/dev/null || true
|
||||
|
||||
# Create new branch, commit, and push
|
||||
# Create new branch
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
git add src/Defaults/baileys-version.json src/Defaults/index.ts src/Utils/generics.ts
|
||||
git commit -m "chore: update WhatsApp Web version"
|
||||
|
||||
# Only add the single source of truth file
|
||||
git add src/Defaults/baileys-version.json
|
||||
|
||||
# Create commit (allow empty if force mode)
|
||||
if [ "$FORCE" == "true" ] && git diff --cached --quiet; then
|
||||
git commit --allow-empty -m "chore: force WhatsApp Web version check"
|
||||
else
|
||||
git commit -m "chore: update WhatsApp Web version"
|
||||
fi
|
||||
|
||||
git push origin "$BRANCH_NAME"
|
||||
|
||||
# Create PR using GitHub CLI
|
||||
gh pr create \
|
||||
--title "chore: update WhatsApp Web version" \
|
||||
--body "Automated WhatsApp Web version update.
|
||||
PR_BODY="Automated WhatsApp Web version update.
|
||||
|
||||
This PR updates the WhatsApp Web client revision to the latest version fetched from \`web.whatsapp.com\`.
|
||||
|
||||
**Files updated:**
|
||||
- \`src/Defaults/baileys-version.json\`
|
||||
- \`src/Defaults/index.ts\`
|
||||
- \`src/Utils/generics.ts\`" \
|
||||
**Single source of truth:**
|
||||
- \`src/Defaults/baileys-version.json\` (other files import from here)
|
||||
|
||||
**Update frequency:** Daily at 06:00 UTC"
|
||||
|
||||
if [ "$FORCE" == "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
|
||||
> **Note:** This PR was created with force mode enabled."
|
||||
fi
|
||||
|
||||
gh pr create \
|
||||
--title "chore: update WhatsApp Web version" \
|
||||
--body "$PR_BODY" \
|
||||
--base master \
|
||||
--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
|
||||
|
||||
EXIT_CODE="${{ steps.update_version.outputs.exit_code }}"
|
||||
HAS_CHANGES="${{ steps.check_changes.outputs.has_changes }}"
|
||||
FORCE_MODE="${{ steps.check_changes.outputs.force_mode }}"
|
||||
|
||||
if [ "$EXIT_CODE" == "2" ]; then
|
||||
echo "⚠️ Warning: Could not fetch latest version from WhatsApp servers" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [ "$HAS_CHANGES" == "true" ]; then
|
||||
echo "✅ New version detected and PR created" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "$FORCE_MODE" == "true" ]; then
|
||||
echo "🔄 Force mode: PR created (no version change)" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "ℹ️ Already up to date" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
+272
-94
@@ -1,40 +1,258 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Script to update WhatsApp Web version across the codebase.
|
||||
* Fetches the latest version from web.whatsapp.com and updates:
|
||||
* - src/Defaults/baileys-version.json
|
||||
* - src/Defaults/index.ts
|
||||
* - src/Utils/generics.ts
|
||||
* 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 } from 'fs'
|
||||
import { readFileSync, writeFileSync, appendFileSync } from 'fs'
|
||||
import { dirname, join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { fetchLatestWaWebVersion } from '../src/Utils/generics.ts'
|
||||
|
||||
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')
|
||||
|
||||
function updateBaileysVersionJson(version: [number, number, number]): boolean {
|
||||
const filePath = join(ROOT_DIR, 'src/Defaults/baileys-version.json')
|
||||
const content = {
|
||||
version
|
||||
}
|
||||
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 currentContent = readFileSync(filePath, 'utf-8')
|
||||
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]) {
|
||||
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(filePath, JSON.stringify(content) + '\n')
|
||||
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)
|
||||
@@ -42,105 +260,65 @@ function updateBaileysVersionJson(version: [number, number, number]): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function updateGenerics(version: [number, number, number]): boolean {
|
||||
const filePath = join(ROOT_DIR, 'src/Utils/generics.ts')
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
const versionRegex = /const baileysVersion = \[(\d+),\s*(\d+),\s*(\d+)\]/
|
||||
const match = content.match(versionRegex)
|
||||
|
||||
if (!match) {
|
||||
throw new Error('Could not find baileysVersion declaration in generics.ts')
|
||||
}
|
||||
|
||||
const currentVersion = [+match[1]!, +match[2]!, +match[3]!]
|
||||
|
||||
if (currentVersion[0] === version[0] && currentVersion[1] === version[1] && currentVersion[2] === version[2]) {
|
||||
console.log(`✓ src/Utils/generics.ts already up to date`)
|
||||
return false
|
||||
}
|
||||
|
||||
const newContent = content.replace(
|
||||
versionRegex,
|
||||
`const baileysVersion = [${version[0]}, ${version[1]}, ${version[2]}]`
|
||||
)
|
||||
|
||||
writeFileSync(filePath, newContent)
|
||||
console.log(`✓ Updated src/Utils/generics.ts: [${currentVersion.join(', ')}] → [${version.join(', ')}]`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to update src/Utils/generics.ts:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function updateIndex(version: [number, number, number]): boolean {
|
||||
const filePath = join(ROOT_DIR, 'src/Defaults/index.ts')
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
const versionRegex = /const version = \[(\d+),\s*(\d+),\s*(\d+)\]/
|
||||
const match = content.match(versionRegex)
|
||||
|
||||
if (!match) {
|
||||
throw new Error('Could not find version declaration in index.ts')
|
||||
}
|
||||
|
||||
const currentVersion = [+match[1]!, +match[2]!, +match[3]!]
|
||||
|
||||
if (currentVersion[0] === version[0] && currentVersion[1] === version[1] && currentVersion[2] === version[2]) {
|
||||
console.log(`✓ src/Defaults/index.ts already up to date`)
|
||||
return false
|
||||
}
|
||||
|
||||
const newContent = content.replace(versionRegex, `const version = [${version[0]}, ${version[1]}, ${version[2]}]`)
|
||||
|
||||
writeFileSync(filePath, newContent)
|
||||
console.log(`✓ Updated src/Defaults/index.ts: [${currentVersion.join(', ')}] → [${version.join(', ')}]`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to update src/Defaults/index.ts:`, 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()
|
||||
|
||||
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 updates = [
|
||||
updateBaileysVersionJson(result.version),
|
||||
updateGenerics(result.version),
|
||||
updateIndex(result.version)
|
||||
]
|
||||
|
||||
const hasUpdates = updates.some(Boolean)
|
||||
const hasUpdates = updateBaileysVersionJson(result.version, result.source)
|
||||
|
||||
console.log('')
|
||||
if (hasUpdates) {
|
||||
console.log('═══════════════════════════════════════════════')
|
||||
console.log('Version update complete!')
|
||||
// Set GitHub Actions output if running in CI
|
||||
console.log('═══════════════════════════════════════════════')
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
const { appendFileSync } = await import('fs')
|
||||
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('All files are already up to date.')
|
||||
console.log('Already up to date. No changes needed.')
|
||||
if (process.env.GITHUB_OUTPUT) {
|
||||
const { appendFileSync } = await import('fs')
|
||||
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 => {
|
||||
|
||||
@@ -3,8 +3,10 @@ import { makeLibSignalRepository } from '../Signal/libsignal'
|
||||
import type { AuthenticationState, SocketConfig, WAVersion } from '../Types'
|
||||
import { Browsers } from '../Utils/browser-utils'
|
||||
import logger from '../Utils/logger'
|
||||
// Single source of truth for WhatsApp Web version - imported from JSON
|
||||
import baileysVersionData from './baileys-version.json'
|
||||
|
||||
const version = [2, 3000, 1032141294]
|
||||
const version = baileysVersionData.version
|
||||
|
||||
export const UNAUTHORIZED_CODES = [401, 403, 419]
|
||||
|
||||
@@ -45,8 +47,12 @@ export const PROCESSABLE_HISTORY_TYPES = [
|
||||
proto.HistorySync.HistorySyncType.INITIAL_STATUS_V3
|
||||
]
|
||||
|
||||
// 6 hours in milliseconds
|
||||
const SIX_HOURS_MS = 6 * 60 * 60 * 1000
|
||||
|
||||
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
||||
version: version as WAVersion,
|
||||
versionCheckIntervalMs: SIX_HOURS_MS,
|
||||
browser: Browsers.macOS('Chrome'),
|
||||
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
|
||||
connectTimeoutMs: 20_000,
|
||||
|
||||
+165
-1
@@ -1,7 +1,24 @@
|
||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||
import type { UserFacingSocketConfig } from '../Types'
|
||||
import type { UserFacingSocketConfig, WAVersion } from '../Types'
|
||||
import { fetchLatestWaWebVersion } from '../Utils/generics'
|
||||
import { makeCommunitiesSocket } from './communities'
|
||||
|
||||
/**
|
||||
* Compares two WhatsApp versions
|
||||
* @returns true if versions are different
|
||||
*/
|
||||
const versionsAreDifferent = (v1: WAVersion, v2: WAVersion): boolean => {
|
||||
return v1[0] !== v2[0] || v1[1] !== v2[1] || v1[2] !== v2[2]
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version change is critical (major or minor version changed)
|
||||
*/
|
||||
const isCriticalVersionChange = (oldVersion: WAVersion, newVersion: WAVersion): boolean => {
|
||||
// Major version change (index 0) or minor version change (index 1)
|
||||
return oldVersion[0] !== newVersion[0] || oldVersion[1] !== newVersion[1]
|
||||
}
|
||||
|
||||
// export the last socket layer
|
||||
const makeWASocket = (config: UserFacingSocketConfig) => {
|
||||
const newConfig = {
|
||||
@@ -19,4 +36,151 @@ const makeWASocket = (config: UserFacingSocketConfig) => {
|
||||
return makeCommunitiesSocket(newConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a WhatsApp socket connection with automatic version fetching
|
||||
* and periodic version checks (soft update - transparent to user).
|
||||
*
|
||||
* Features:
|
||||
* - Fetches latest version on connect
|
||||
* - Checks for new versions every 6 hours (configurable)
|
||||
* - Updates version on next natural reconnection (transparent)
|
||||
* - Emits 'version.update' event when new version is detected
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sock = await makeWASocketAutoVersion({
|
||||
* auth: state,
|
||||
* versionCheckIntervalMs: 6 * 60 * 60 * 1000 // 6 hours (default)
|
||||
* })
|
||||
*
|
||||
* // Listen for version updates
|
||||
* sock.ev.on('version.update', ({ currentVersion, newVersion, isCritical }) => {
|
||||
* console.log(`New version detected: ${newVersion.join('.')}`)
|
||||
* // Version will be used on next reconnection automatically
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const makeWASocketAutoVersion = async (config: UserFacingSocketConfig) => {
|
||||
const mergedConfig = {
|
||||
...DEFAULT_CONNECTION_CONFIG,
|
||||
...config
|
||||
}
|
||||
|
||||
const logger = mergedConfig.logger
|
||||
|
||||
// Track version separately to avoid mutating config (Fix #7)
|
||||
let trackedVersion: WAVersion = [...mergedConfig.version] as WAVersion
|
||||
let versionCheckInterval: ReturnType<typeof setInterval> | null = null
|
||||
let isSocketClosed = false
|
||||
|
||||
/**
|
||||
* Cleans up the version check interval
|
||||
*/
|
||||
const cleanupInterval = () => {
|
||||
if (versionCheckInterval) {
|
||||
clearInterval(versionCheckInterval)
|
||||
versionCheckInterval = null
|
||||
logger?.debug('Stopped periodic version check')
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch latest version before connecting
|
||||
try {
|
||||
logger?.info('Fetching latest WhatsApp Web version...')
|
||||
const result = await fetchLatestWaWebVersion()
|
||||
|
||||
if (result.isLatest) {
|
||||
logger?.info({ version: result.version }, 'Using latest WhatsApp Web version')
|
||||
mergedConfig.version = result.version
|
||||
trackedVersion = [...result.version] as WAVersion
|
||||
} else {
|
||||
logger?.warn(
|
||||
{ error: result.error, fallbackVersion: mergedConfig.version },
|
||||
'Failed to fetch latest version, using bundled version'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger?.warn(
|
||||
{ error, fallbackVersion: mergedConfig.version },
|
||||
'Error fetching latest version, using bundled version'
|
||||
)
|
||||
}
|
||||
|
||||
// Create the socket
|
||||
const sock = makeWASocket(mergedConfig)
|
||||
|
||||
// Listen for connection close to cleanup interval (Fix #1, #6)
|
||||
// This handles both explicit sock.end() and internal disconnections
|
||||
sock.ev.on('connection.update', (update) => {
|
||||
if (update.connection === 'close') {
|
||||
isSocketClosed = true
|
||||
cleanupInterval()
|
||||
} else if (update.connection === 'open') {
|
||||
isSocketClosed = false
|
||||
}
|
||||
})
|
||||
|
||||
// Setup periodic version check if interval > 0
|
||||
const checkIntervalMs = mergedConfig.versionCheckIntervalMs
|
||||
if (checkIntervalMs > 0) {
|
||||
logger?.info(
|
||||
{ intervalHours: checkIntervalMs / (60 * 60 * 1000) },
|
||||
'Starting periodic version check'
|
||||
)
|
||||
|
||||
versionCheckInterval = setInterval(async () => {
|
||||
// Skip if socket is closed (Fix #8 - race condition)
|
||||
if (isSocketClosed) {
|
||||
cleanupInterval()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
logger?.debug('Checking for WhatsApp Web version update...')
|
||||
const result = await fetchLatestWaWebVersion()
|
||||
|
||||
// Double-check socket is still open after async operation (Fix #8)
|
||||
if (isSocketClosed) {
|
||||
cleanupInterval()
|
||||
return
|
||||
}
|
||||
|
||||
if (result.isLatest && versionsAreDifferent(trackedVersion, result.version)) {
|
||||
const isCritical = isCriticalVersionChange(trackedVersion, result.version)
|
||||
const previousVersion = trackedVersion
|
||||
|
||||
logger?.info(
|
||||
{
|
||||
currentVersion: previousVersion,
|
||||
newVersion: result.version,
|
||||
isCritical
|
||||
},
|
||||
'New WhatsApp Web version detected! Will use on next reconnection.'
|
||||
)
|
||||
|
||||
// Update tracked version for next reconnection (Fix #7)
|
||||
trackedVersion = [...result.version] as WAVersion
|
||||
|
||||
// Emit event for user to handle (only if socket still open)
|
||||
if (!isSocketClosed) {
|
||||
sock.ev.emit('version.update', {
|
||||
currentVersion: previousVersion,
|
||||
newVersion: result.version,
|
||||
isCritical
|
||||
})
|
||||
}
|
||||
} else if (result.isLatest) {
|
||||
logger?.debug({ version: trackedVersion }, 'Version is up to date')
|
||||
} else {
|
||||
logger?.warn({ error: result.error }, 'Failed to check for version update')
|
||||
}
|
||||
} catch (error) {
|
||||
logger?.warn({ error }, 'Error checking for version update')
|
||||
}
|
||||
}, checkIntervalMs)
|
||||
}
|
||||
|
||||
return sock
|
||||
}
|
||||
|
||||
export default makeWASocket
|
||||
|
||||
@@ -107,6 +107,18 @@ export type BaileysEventMap = {
|
||||
|
||||
/** Settings and actions sync events */
|
||||
'chats.lock': { id: string; locked: boolean }
|
||||
/**
|
||||
* Emitted when a new WhatsApp Web version is detected.
|
||||
* The new version will be used on the next reconnection (soft update).
|
||||
*/
|
||||
'version.update': {
|
||||
/** Previous version */
|
||||
currentVersion: [number, number, number]
|
||||
/** New version detected */
|
||||
newVersion: [number, number, number]
|
||||
/** Whether the update is critical (major version change) */
|
||||
isCritical: boolean
|
||||
}
|
||||
'settings.update':
|
||||
| { setting: 'unarchiveChats'; value: boolean }
|
||||
| { setting: 'locale'; value: string }
|
||||
|
||||
@@ -49,6 +49,17 @@ export type SocketConfig = {
|
||||
logger: ILogger
|
||||
/** version to connect with */
|
||||
version: WAVersion
|
||||
/**
|
||||
* Interval in milliseconds to check for new WhatsApp Web versions.
|
||||
* When a new version is detected, it will be used on the next reconnection.
|
||||
* Set to 0 to disable periodic checks.
|
||||
*
|
||||
* Note: This option is only used by `makeWASocketAutoVersion()`.
|
||||
* The standard `makeWASocket()` does not perform automatic version checks.
|
||||
*
|
||||
* @default 21600000 (6 hours)
|
||||
*/
|
||||
versionCheckIntervalMs: number
|
||||
/** override browser config */
|
||||
browser: WABrowserDescription
|
||||
/** agent used for fetch requests -- uploading/downloading media */
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import { proto } from '../../WAProto/index.js'
|
||||
const baileysVersion = [2, 3000, 1032141294]
|
||||
// Single source of truth for WhatsApp Web version - imported from JSON
|
||||
import baileysVersionData from '../Defaults/baileys-version.json'
|
||||
|
||||
const baileysVersion = baileysVersionData.version
|
||||
import type {
|
||||
BaileysEventEmitter,
|
||||
BaileysEventMap,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import makeWASocket from './Socket/index'
|
||||
import makeWASocket, { makeWASocketAutoVersion } from './Socket/index'
|
||||
|
||||
export * from '../WAProto/index.js'
|
||||
export * from './Utils/index'
|
||||
@@ -9,5 +9,5 @@ export * from './WAM/index'
|
||||
export * from './WAUSync/index'
|
||||
|
||||
export type WASocket = ReturnType<typeof makeWASocket>
|
||||
export { makeWASocket }
|
||||
export { makeWASocket, makeWASocketAutoVersion }
|
||||
export default makeWASocket
|
||||
|
||||
Reference in New Issue
Block a user