fix(whatsapp): refresh bridge when source changes
This commit is contained in:
@@ -17,7 +17,7 @@ import { Boom } from '@hapi/boom';
|
|||||||
import qrcode from 'qrcode-terminal';
|
import qrcode from 'qrcode-terminal';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import { readFile, writeFile, mkdir } from 'fs/promises';
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
||||||
import { join, basename } from 'path';
|
import { join, basename, resolve, sep } from 'path';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
const VERSION = '0.1.0';
|
const VERSION = '0.1.0';
|
||||||
@@ -196,17 +196,18 @@ export class WhatsAppClient {
|
|||||||
|
|
||||||
let outFilename: string;
|
let outFilename: string;
|
||||||
if (fileName) {
|
if (fileName) {
|
||||||
// Documents have a filename — use it with a unique prefix to avoid collisions
|
const safeName = basename(fileName).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
const prefix = `wa_${Date.now()}_${randomBytes(4).toString('hex')}_`;
|
outFilename = `wa_${Date.now()}_${randomBytes(4).toString('hex')}_${safeName}`;
|
||||||
outFilename = prefix + fileName;
|
|
||||||
} else {
|
} else {
|
||||||
const mime = mimetype || 'application/octet-stream';
|
const mime = mimetype || 'application/octet-stream';
|
||||||
// Derive extension from mimetype subtype (e.g. "image/png" → ".png", "application/pdf" → ".pdf")
|
|
||||||
const ext = '.' + (mime.split('/').pop()?.split(';')[0] || 'bin');
|
const ext = '.' + (mime.split('/').pop()?.split(';')[0] || 'bin');
|
||||||
outFilename = `wa_${Date.now()}_${randomBytes(4).toString('hex')}${ext}`;
|
outFilename = `wa_${Date.now()}_${randomBytes(4).toString('hex')}${ext}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = join(mediaDir, outFilename);
|
const filepath = resolve(mediaDir, outFilename);
|
||||||
|
if (!filepath.startsWith(resolve(mediaDir) + sep)) {
|
||||||
|
throw new Error(`Path traversal blocked: ${outFilename}`);
|
||||||
|
}
|
||||||
await writeFile(filepath, buffer);
|
await writeFile(filepath, buffer);
|
||||||
|
|
||||||
return filepath;
|
return filepath;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""WhatsApp channel implementation using Node.js bridge."""
|
"""WhatsApp channel implementation using Node.js bridge."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
@@ -316,13 +317,7 @@ def _ensure_bridge_setup() -> Path:
|
|||||||
from nanobot.config.paths import get_bridge_install_dir
|
from nanobot.config.paths import get_bridge_install_dir
|
||||||
|
|
||||||
user_bridge = get_bridge_install_dir()
|
user_bridge = get_bridge_install_dir()
|
||||||
|
stamp_file = user_bridge / ".nanobot-bridge-source-hash"
|
||||||
if (user_bridge / "dist" / "index.js").exists():
|
|
||||||
return user_bridge
|
|
||||||
|
|
||||||
npm_path = shutil.which("npm")
|
|
||||||
if not npm_path:
|
|
||||||
raise RuntimeError("npm not found. Please install Node.js >= 18.")
|
|
||||||
|
|
||||||
# Find source bridge
|
# Find source bridge
|
||||||
current_file = Path(__file__)
|
current_file = Path(__file__)
|
||||||
@@ -341,6 +336,33 @@ def _ensure_bridge_setup() -> Path:
|
|||||||
"Try reinstalling: pip install --force-reinstall nanobot"
|
"Try reinstalling: pip install --force-reinstall nanobot"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def source_hash(root: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for path in sorted(root.rglob("*")):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(root)
|
||||||
|
if rel.parts and rel.parts[0] in {"node_modules", "dist"}:
|
||||||
|
continue
|
||||||
|
digest.update(rel.as_posix().encode("utf-8"))
|
||||||
|
digest.update(b"\0")
|
||||||
|
digest.update(path.read_bytes())
|
||||||
|
digest.update(b"\0")
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
expected_hash = source_hash(source)
|
||||||
|
current_hash = stamp_file.read_text().strip() if stamp_file.exists() else None
|
||||||
|
|
||||||
|
if (user_bridge / "dist" / "index.js").exists() and current_hash == expected_hash:
|
||||||
|
return user_bridge
|
||||||
|
|
||||||
|
if (user_bridge / "dist" / "index.js").exists() and current_hash != expected_hash:
|
||||||
|
logger.info("WhatsApp bridge source changed; rebuilding bridge...")
|
||||||
|
|
||||||
|
npm_path = shutil.which("npm")
|
||||||
|
if not npm_path:
|
||||||
|
raise RuntimeError("npm not found. Please install Node.js >= 18.")
|
||||||
|
|
||||||
logger.info("Setting up WhatsApp bridge...")
|
logger.info("Setting up WhatsApp bridge...")
|
||||||
user_bridge.parent.mkdir(parents=True, exist_ok=True)
|
user_bridge.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if user_bridge.exists():
|
if user_bridge.exists():
|
||||||
@@ -352,6 +374,7 @@ def _ensure_bridge_setup() -> Path:
|
|||||||
|
|
||||||
logger.info(" Building...")
|
logger.info(" Building...")
|
||||||
subprocess.run([npm_path, "run", "build"], cwd=user_bridge, check=True, capture_output=True)
|
subprocess.run([npm_path, "run", "build"], cwd=user_bridge, check=True, capture_output=True)
|
||||||
|
stamp_file.write_text(expected_hash + "\n")
|
||||||
|
|
||||||
logger.info("Bridge ready")
|
logger.info("Bridge ready")
|
||||||
return user_bridge
|
return user_bridge
|
||||||
|
|||||||
+33
-10
@@ -1271,6 +1271,7 @@ def channels_status(
|
|||||||
|
|
||||||
def _get_bridge_dir() -> Path:
|
def _get_bridge_dir() -> Path:
|
||||||
"""Get the bridge directory, setting it up if needed."""
|
"""Get the bridge directory, setting it up if needed."""
|
||||||
|
import hashlib
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
@@ -1278,16 +1279,7 @@ def _get_bridge_dir() -> Path:
|
|||||||
from nanobot.config.paths import get_bridge_install_dir
|
from nanobot.config.paths import get_bridge_install_dir
|
||||||
|
|
||||||
user_bridge = get_bridge_install_dir()
|
user_bridge = get_bridge_install_dir()
|
||||||
|
stamp_file = user_bridge / ".nanobot-bridge-source-hash"
|
||||||
# Check if already built
|
|
||||||
if (user_bridge / "dist" / "index.js").exists():
|
|
||||||
return user_bridge
|
|
||||||
|
|
||||||
# Check for npm
|
|
||||||
npm_path = shutil.which("npm")
|
|
||||||
if not npm_path:
|
|
||||||
console.print("[red]npm not found. Please install Node.js >= 18.[/red]")
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
# Find source bridge: first check package data, then source dir
|
# Find source bridge: first check package data, then source dir
|
||||||
pkg_bridge = Path(__file__).parent.parent / "bridge" # nanobot/bridge (installed)
|
pkg_bridge = Path(__file__).parent.parent / "bridge" # nanobot/bridge (installed)
|
||||||
@@ -1304,6 +1296,36 @@ def _get_bridge_dir() -> Path:
|
|||||||
console.print("Try reinstalling: pip install --force-reinstall nanobot")
|
console.print("Try reinstalling: pip install --force-reinstall nanobot")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
def source_hash(root: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for path in sorted(root.rglob("*")):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(root)
|
||||||
|
if rel.parts and rel.parts[0] in {"node_modules", "dist"}:
|
||||||
|
continue
|
||||||
|
digest.update(rel.as_posix().encode("utf-8"))
|
||||||
|
digest.update(b"\0")
|
||||||
|
digest.update(path.read_bytes())
|
||||||
|
digest.update(b"\0")
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
expected_hash = source_hash(source)
|
||||||
|
current_hash = stamp_file.read_text().strip() if stamp_file.exists() else None
|
||||||
|
|
||||||
|
# Reuse only a bridge built from the currently installed source.
|
||||||
|
if (user_bridge / "dist" / "index.js").exists() and current_hash == expected_hash:
|
||||||
|
return user_bridge
|
||||||
|
|
||||||
|
if (user_bridge / "dist" / "index.js").exists() and current_hash != expected_hash:
|
||||||
|
console.print(f"{__logo__} WhatsApp bridge source changed; rebuilding bridge...")
|
||||||
|
|
||||||
|
# Check for npm
|
||||||
|
npm_path = shutil.which("npm")
|
||||||
|
if not npm_path:
|
||||||
|
console.print("[red]npm not found. Please install Node.js >= 18.[/red]")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
console.print(f"{__logo__} Setting up bridge...")
|
console.print(f"{__logo__} Setting up bridge...")
|
||||||
|
|
||||||
# Copy to user directory
|
# Copy to user directory
|
||||||
@@ -1319,6 +1341,7 @@ def _get_bridge_dir() -> Path:
|
|||||||
|
|
||||||
console.print(" Building...")
|
console.print(" Building...")
|
||||||
subprocess.run([npm_path, "run", "build"], cwd=user_bridge, check=True, capture_output=True)
|
subprocess.run([npm_path, "run", "build"], cwd=user_bridge, check=True, capture_output=True)
|
||||||
|
stamp_file.write_text(expected_hash + "\n")
|
||||||
|
|
||||||
console.print("[green]✓[/green] Bridge ready\n")
|
console.print("[green]✓[/green] Bridge ready\n")
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user