新增了完整的Signal渠道适配器实现,包含RPC客户端、守护进程管理、安全策略、消息处理、安装配置工具等全套功能,支持通过signal-cli与Signal网络进行通信,包含账户管理、消息收发、反应处理、媒体分析、健康检查等能力。
76 lines
2.8 KiB
Python
76 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import random
|
|
from collections.abc import Callable, Awaitable
|
|
|
|
import aiohttp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_RECONNECT_DELAY = 60.0
|
|
INITIAL_RECONNECT_DELAY = 1.0
|
|
JITTER = 0.1
|
|
|
|
|
|
async def sse_event_stream(
|
|
url: str,
|
|
params: dict,
|
|
on_event: Callable[[str], Awaitable[None]],
|
|
) -> None:
|
|
reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
last_event_id: str | None = None
|
|
unauth_backoff = False
|
|
|
|
while True:
|
|
try:
|
|
headers: dict[str, str] = {"Accept": "text/event-stream"}
|
|
if last_event_id:
|
|
headers["Last-Event-ID"] = last_event_id
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(url, params=params, headers=headers) as response:
|
|
if response.status == 401:
|
|
unauth_backoff = True
|
|
logger.error("SSE connection received 401 Unauthorized, backing off for 30s")
|
|
await asyncio.sleep(30.0)
|
|
reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
continue
|
|
|
|
if response.status != 200:
|
|
logger.error(f"SSE connection failed: {response.status}")
|
|
await asyncio.sleep(reconnect_delay)
|
|
reconnect_delay = min(reconnect_delay * 2, MAX_RECONNECT_DELAY)
|
|
continue
|
|
|
|
if unauth_backoff:
|
|
logger.info("SSE reconnected successfully after 401 backoff")
|
|
unauth_backoff = False
|
|
|
|
reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
|
|
async for line in response.content:
|
|
line_text = line.decode("utf-8").strip()
|
|
|
|
if line_text.startswith("id:"):
|
|
last_event_id = line_text.removeprefix("id:").strip()
|
|
continue
|
|
|
|
if line_text.startswith("data:"):
|
|
event_data = line_text.removeprefix("data:").strip()
|
|
if event_data:
|
|
try:
|
|
await on_event(event_data)
|
|
except Exception:
|
|
logger.exception("Error handling SSE event")
|
|
|
|
except (TimeoutError, aiohttp.ClientError) as e:
|
|
logger.warning(f"SSE connection lost: {e}, reconnecting in {reconnect_delay:.1f}s")
|
|
jitter_ms = reconnect_delay * JITTER * random.random()
|
|
await asyncio.sleep(reconnect_delay + jitter_ms)
|
|
reconnect_delay = min(reconnect_delay * 2, MAX_RECONNECT_DELAY)
|
|
except asyncio.CancelledError:
|
|
logger.info("SSE event stream cancelled")
|
|
break
|