新增了完整的Signal渠道适配器实现,包含RPC客户端、守护进程管理、安全策略、消息处理、安装配置工具等全套功能,支持通过signal-cli与Signal网络进行通信,包含账户管理、消息收发、反应处理、媒体分析、健康检查等能力。
211 lines
7.2 KiB
Python
211 lines
7.2 KiB
Python
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 <CODE>")
|
|
print(" 3. Start daemon mode:")
|
|
print(" signal-cli -a +1234567890 daemon --http-listen 127.0.0.1:8080")
|