#!/usr/bin/env python3 """deepdive_delivery.py — Phase 5: Telegram voice message delivery. Issue: #830 (the-nexus) Delivers synthesized audio briefing as Telegram voice message. """ import argparse import json import os import sys from pathlib import Path import urllib.request class TelegramDeliveryAdapter: """Deliver audio briefing via Telegram bot as voice message.""" def __init__(self, bot_token: str, chat_id: str): self.bot_token = bot_token self.chat_id = chat_id self.api_base = f"https://api.telegram.org/bot{bot_token}" def _api_post(self, method: str, data: dict, files: dict = None): """Call Telegram Bot API.""" import urllib.request import urllib.parse url = f"{self.api_base}/{method}" if files: # Multipart form for file uploads boundary = "----DeepDiveBoundary" body_parts = [] for key, value in data.items(): body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{key}"\r\n\r\n{value}\r\n') for key, (filename, content) in files.items(): body_parts.append( f'--{boundary}\r\n' f'Content-Disposition: form-data; name="{key}"; filename="{filename}"\r\n' f'Content-Type: audio/mpeg\r\n\r\n' ) body_parts.append(content) body_parts.append(f'\r\n') body_parts.append(f'--{boundary}--\r\n') body = b"" for part in body_parts: if isinstance(part, str): body += part.encode() else: body += part req = urllib.request.Request(url, data=body, method="POST") req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}") else: body = urllib.parse.urlencode(data).encode() req = urllib.request.Request(url, data=body, method="POST") req.add_header("Content-Type", "application/x-www-form-urlencoded") try: with urllib.request.urlopen(req, timeout=60) as resp: return json.loads(resp.read().decode()) except urllib.error.HTTPError as e: error_body = e.read().decode() raise RuntimeError(f"Telegram API error: {e.code} - {error_body}") def send_voice(self, audio_path: Path, caption: str = None) -> dict: """Send audio file as voice message.""" audio_bytes = audio_path.read_bytes() files = {"voice": (audio_path.name, audio_bytes)} data = {"chat_id": self.chat_id} if caption: data["caption"] = caption[:1024] # Telegram caption limit result = self._api_post("sendVoice", data, files) if not result.get("ok"): raise RuntimeError(f"Telegram send failed: {result}") return result def send_text_preview(self, text: str) -> dict: """Send text summary before voice (optional).""" data = { "chat_id": self.chat_id, "text": text[:4096] # Telegram message limit } return self._api_post("sendMessage", data) def load_config(): """Load Telegram configuration from environment.""" token = os.environ.get("DEEPDIVE_TELEGRAM_BOT_TOKEN") or os.environ.get("TELEGRAM_BOT_TOKEN") chat_id = os.environ.get("DEEPDIVE_TELEGRAM_CHAT_ID") or os.environ.get("TELEGRAM_CHAT_ID") if not token: raise RuntimeError( "Telegram bot token required. Set DEEPDIVE_TELEGRAM_BOT_TOKEN or TELEGRAM_BOT_TOKEN" ) if not chat_id: raise RuntimeError( "Telegram chat ID required. Set DEEPDIVE_TELEGRAM_CHAT_ID or TELEGRAM_CHAT_ID" ) return token, chat_id def main(): parser = argparse.ArgumentParser(description="Deep Dive Delivery Pipeline") parser.add_argument("--audio", "-a", help="Path to audio file (MP3)") parser.add_argument("--text", "-t", help="Text message to send") parser.add_argument("--caption", "-c", help="Caption for voice message") parser.add_argument("--preview-text", help="Optional text preview sent before voice") parser.add_argument("--bot-token", help="Telegram bot token (overrides env)") parser.add_argument("--chat-id", help="Telegram chat ID (overrides env)") parser.add_argument("--dry-run", action="store_true", help="Validate config without sending") args = parser.parse_args() # Load config try: if args.bot_token and args.chat_id: token, chat_id = args.bot_token, args.chat_id else: token, chat_id = load_config() except RuntimeError as e: print(f"[ERROR] {e}", file=sys.stderr) sys.exit(1) # Validate input if not args.audio and not args.text: print("[ERROR] Either --audio or --text required", file=sys.stderr) sys.exit(1) if args.dry_run: print(f"[DRY RUN] Config valid") print(f" Bot: {token[:10]}...") print(f" Chat: {chat_id}") if args.audio: audio_path = Path(args.audio) print(f" Audio: {audio_path} ({audio_path.stat().st_size} bytes)") if args.text: print(f" Text: {args.text[:100]}...") sys.exit(0) # Deliver adapter = TelegramDeliveryAdapter(token, chat_id) # Send text if provided if args.text: print("[DELIVERY] Sending text message...") result = adapter.send_text_preview(args.text) message_id = result["result"]["message_id"] print(f"[DELIVERY] Text sent! Message ID: {message_id}") # Send audio if provided if args.audio: audio_path = Path(args.audio) if not audio_path.exists(): print(f"[ERROR] Audio file not found: {audio_path}", file=sys.stderr) sys.exit(1) if args.preview_text: print("[DELIVERY] Sending text preview...") adapter.send_text_preview(args.preview_text) print(f"[DELIVERY] Sending voice message: {audio_path}...") result = adapter.send_voice(audio_path, args.caption) message_id = result["result"]["message_id"] print(f"[DELIVERY] Voice sent! Message ID: {message_id}") print(json.dumps({ "success": True, "message_id": message_id, "chat_id": chat_id, "audio_size_bytes": audio_path.stat().st_size })) if __name__ == "__main__": main()