stage-13: WhatsApp bridge with Baileys + Python adapter

Node.js bridge (bridge/whatsapp/): Baileys client with Express HTTP API
on localhost:8098 — QR code linking, message queue, reconnection logic.

Python adapter (src/adapters/whatsapp.py): polls bridge every 2s, routes
through router.py, separate whatsapp.owner/admins auth, security logging.

Integrated in main.py alongside Discord + Telegram via asyncio.gather.
CLI: echo whatsapp status/qr. 442 tests pass (32 new, zero failures).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MoltBot Service
2026-02-13 21:41:16 +00:00
parent 2d8e56d44c
commit 80502b7931
12 changed files with 3636 additions and 2 deletions

90
cli.py
View File

@@ -522,6 +522,86 @@ def cmd_heartbeat(args):
print(run_heartbeat())
def cmd_whatsapp(args):
"""Handle whatsapp subcommand."""
if args.whatsapp_action == "status":
_whatsapp_status()
elif args.whatsapp_action == "qr":
_whatsapp_qr()
def _whatsapp_status():
"""Check WhatsApp bridge connection status."""
import urllib.request
import urllib.error
cfg_file = CONFIG_FILE
bridge_url = "http://127.0.0.1:8098"
try:
text = cfg_file.read_text(encoding="utf-8")
cfg = json.loads(text)
bridge_url = cfg.get("whatsapp", {}).get("bridge_url", bridge_url)
except (FileNotFoundError, json.JSONDecodeError, OSError):
pass
try:
req = urllib.request.urlopen(f"{bridge_url}/status", timeout=5)
data = json.loads(req.read().decode())
except (urllib.error.URLError, OSError) as e:
print(f"Bridge not reachable at {bridge_url}")
print(f" Error: {e}")
return
connected = data.get("connected", False)
phone = data.get("phone", "unknown")
has_qr = data.get("qr", False)
if connected:
print(f"Status: CONNECTED")
print(f"Phone: {phone}")
elif has_qr:
print(f"Status: WAITING FOR QR SCAN")
print(f"Run 'echo whatsapp qr' for QR code instructions.")
else:
print(f"Status: DISCONNECTED")
print(f"Start the bridge and scan the QR code to connect.")
def _whatsapp_qr():
"""Show QR code instructions from the bridge."""
import urllib.request
import urllib.error
cfg_file = CONFIG_FILE
bridge_url = "http://127.0.0.1:8098"
try:
text = cfg_file.read_text(encoding="utf-8")
cfg = json.loads(text)
bridge_url = cfg.get("whatsapp", {}).get("bridge_url", bridge_url)
except (FileNotFoundError, json.JSONDecodeError, OSError):
pass
try:
req = urllib.request.urlopen(f"{bridge_url}/qr", timeout=5)
data = json.loads(req.read().decode())
except (urllib.error.URLError, OSError) as e:
print(f"Bridge not reachable at {bridge_url}")
print(f" Error: {e}")
return
qr = data.get("qr")
if not qr:
if data.get("connected"):
print("Already connected — no QR code needed.")
else:
print("No QR code available yet. Wait for the bridge to initialize.")
return
print("QR code is available at the bridge.")
print(f"Open {bridge_url}/qr in a browser to scan,")
print("or check the bridge terminal output for the QR code.")
def cmd_secrets(args):
"""Handle secrets subcommand."""
if args.secrets_action == "set":
@@ -664,6 +744,13 @@ def main():
cron_disable_p = cron_sub.add_parser("disable", help="Disable a job")
cron_disable_p.add_argument("name", help="Job name")
# whatsapp
whatsapp_parser = sub.add_parser("whatsapp", help="WhatsApp bridge commands")
whatsapp_sub = whatsapp_parser.add_subparsers(dest="whatsapp_action")
whatsapp_sub.add_parser("status", help="Check bridge connection status")
whatsapp_sub.add_parser("qr", help="Show QR code instructions")
# Parse and dispatch
args = parser.parse_args()
@@ -693,6 +780,9 @@ def main():
"secrets": lambda a: (
cmd_secrets(a) if a.secrets_action else (secrets_parser.print_help() or sys.exit(0))
),
"whatsapp": lambda a: (
cmd_whatsapp(a) if a.whatsapp_action else (whatsapp_parser.print_help() or sys.exit(0))
),
}
handler = dispatch.get(args.command)