import logging import os import platform import shutil import subprocess import tempfile from urllib.request import urlopen, Request import json logger = logging.getLogger(__name__) INSTALL_GUIDE_URL = "https://github.com/AsamK/signal-cli/releases" GITHUB_API_RELEASES = "https://api.github.com/repos/AsamK/signal-cli/releases/latest" DEFAULT_MAX_DOWNLOAD_MB = 256 def _get_platform_suffix() -> str | None: system = platform.system().lower() machine = platform.machine().lower() if system == "linux" and machine in ("x86_64", "amd64"): return "linux-x86_64" if system == "linux" and machine in ("aarch64", "arm64"): return "linux-aarch64" return None async def fetch_latest_release_url(prefer_native: bool = True) -> str | None: try: req = Request(GITHUB_API_RELEASES, headers={"User-Agent": "ForcePilot"}) with urlopen(req, timeout=15) as resp: data = json.loads(resp.read().decode()) assets = data.get("assets", []) native_suffix = _get_platform_suffix() if prefer_native else None native_candidate = None universal_candidate = None for asset in assets: name = asset.get("name", "") if native_suffix and native_suffix in name.lower() and name.endswith(".tar.gz") and "signal-cli" in name: native_candidate = asset.get("browser_download_url") if name.endswith(".tar.gz") and "signal-cli" in name: universal_candidate = asset.get("browser_download_url") return native_candidate or universal_candidate except Exception as e: logger.warning(f"Failed to fetch latest signal-cli release: {e}") return None async def auto_install_signal_cli( target_dir: str | None = None, max_download_mb: int = DEFAULT_MAX_DOWNLOAD_MB ) -> bool: url = await fetch_latest_release_url() if not url: logger.error("Could not find signal-cli download URL") return False if target_dir is None: target_dir = os.path.join(os.path.expanduser("~"), ".local", "signal-cli") os.makedirs(target_dir, exist_ok=True) logger.info(f"Downloading signal-cli from {url}") try: req = Request(url, headers={"User-Agent": "ForcePilot"}) with urlopen(req, timeout=300) as resp: content_length = resp.headers.get("Content-Length") if content_length: size_mb = int(content_length) / (1024 * 1024) if size_mb > max_download_mb: max_size_bytes = max_download_mb * 1024 * 1024 raise ValueError( f"signal-cli download size ({size_mb:.1f} MB) exceeds limit ({max_download_mb} MB)." ) data = resp.read() if len(data) > max_download_mb * 1024 * 1024: raise ValueError( f"signal-cli download size ({len(data) / (1024 * 1024):.1f} MB) exceeds limit ({max_download_mb} MB)." ) except Exception as e: logger.error(f"Failed to download signal-cli: {e}") return False with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp: tmp.write(data) tarball = tmp.name try: import tarfile with tarfile.open(tarball, "r:gz") as tar: tar.extractall(target_dir) logger.info(f"signal-cli installed to {target_dir}") return True except Exception as e: logger.error(f"Failed to extract signal-cli: {e}") return False finally: try: os.unlink(tarball) except OSError: pass def check_signal_cli_installed(cli_path: str = "signal-cli") -> tuple[bool, str | None]: if shutil.which(cli_path): try: result = subprocess.run( [cli_path, "--version"], capture_output=True, text=True, timeout=10, ) if result.returncode == 0: version = result.stdout.strip() or result.stderr.strip() logger.info(f"signal-cli found: {version}") return True, version except (subprocess.TimeoutExpired, FileNotFoundError, OSError): pass return False, None def check_java_installed() -> tuple[bool, str | None]: java_home = shutil.which("java") if not java_home: return False, None try: result = subprocess.run( [java_home, "-version"], capture_output=True, text=True, timeout=10, ) version_output = result.stderr or result.stdout logger.info(f"Java found: {version_output.strip().split(chr(10))[0]}") return True, version_output.strip() except (subprocess.TimeoutExpired, FileNotFoundError, OSError): return False, None def get_install_instructions() -> str: system = platform.system().lower() if system == "linux": return ( "Install signal-cli on Linux:\n" " 1. Download the latest release from:\n" f" {INSTALL_GUIDE_URL}\n" " 2. Install the .deb package:\n" " sudo dpkg -i signal-cli_*.deb\n" " Or use the universal tar:\n" " tar xf signal-cli-*.tar.gz -C /usr/local\n" " ln -s /usr/local/signal-cli-*/bin/signal-cli /usr/local/bin/signal-cli\n" "\n" "Requires Java 17+ JRE:\n" " sudo apt install openjdk-17-jre-headless" ) elif system == "darwin": return ( "Install signal-cli on macOS:\n" " brew install signal-cli\n" "\n" "Requires Java 17+ JRE:\n" " brew install openjdk@17" ) else: return ( "Install signal-cli on Windows:\n" f" 1. Download the latest release from {INSTALL_GUIDE_URL}\n" " 2. Extract signal-cli-*.tar.gz\n" " 3. Run signal-cli.bat from the bin directory\n" "\n" "Requires Java 17+ JRE:\n" " Download from https://adoptium.net/" ) def print_install_guide() -> None: java_ok, java_version = check_java_installed() cli_ok, cli_version = check_signal_cli_installed() print("=== Signal CLI Environment Check ===") print(f" Java 17+: {'OK' if java_ok else 'NOT FOUND'}") if java_version: print(f" {java_version.strip().split(chr(10))[0]}") print(f" signal-cli: {'OK' if cli_ok else 'NOT FOUND'}") if cli_version: print(f" version: {cli_version}") print() if not java_ok: print("[!] Java 17+ JRE is required to run signal-cli.") print() if not cli_ok: print("[!] signal-cli is not installed.") print(get_install_instructions()) else: print("signal-cli is ready to use.") print() print("Next steps:") print(" 1. Register your phone number:") print(" signal-cli -a +1234567890 register") print(" 2. Verify with the code received via SMS:") print(" signal-cli -a +1234567890 verify ") print(" 3. Start daemon mode:") print(" signal-cli -a +1234567890 daemon --http-listen 127.0.0.1:8080")