Files
wehub-resource-sync 6ede33ccdb
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Has been cancelled
Build and Push Docker Images / finalize_release (push) Has been cancelled
Obsidian Plugin Lint / lint (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / compute_version (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / verify_digests (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:44 +08:00

167 lines
5.8 KiB
Python

"""FastAPI lifespan integration for self-hosted BYO Telegram long-polling."""
from __future__ import annotations
import asyncio
import logging
from sqlalchemy import select
from app.config import config
from app.db import (
ExternalChatAccount,
ExternalChatAccountMode,
ExternalChatPlatform,
async_session_maker,
)
from app.gateway.accounts import account_token
from app.gateway.inbox import persist_inbound_event
from app.gateway.runner import _run_telegram_account
from app.gateway.whatsapp.adapter_baileys import WhatsAppBaileysAdapter
from app.observability.metrics import record_gateway_inbox_write
logger = logging.getLogger(__name__)
_tasks: set[asyncio.Task[None]] = set()
_shutdown_event: asyncio.Event | None = None
async def _sleep_or_shutdown(seconds: float) -> None:
if _shutdown_event is None:
await asyncio.sleep(seconds)
return
try:
await asyncio.wait_for(_shutdown_event.wait(), timeout=seconds)
except TimeoutError:
return
async def _byo_account_supervisor(account_id: int, token: str) -> None:
while _shutdown_event is None or not _shutdown_event.is_set():
try:
await _run_telegram_account(account_id, token)
except asyncio.CancelledError:
raise
except Exception:
logger.exception(
"BYO Telegram long-poll failed account_id=%s; retrying in 30s",
account_id,
)
await _sleep_or_shutdown(30)
async def _whatsapp_baileys_supervisor() -> None:
adapter = WhatsAppBaileysAdapter()
while _shutdown_event is None or not _shutdown_event.is_set():
try:
async for raw_event in adapter.fetch_updates(offset=None):
async with async_session_maker() as session:
result = await session.execute(
select(ExternalChatAccount).where(
ExternalChatAccount.platform
== ExternalChatPlatform.WHATSAPP,
ExternalChatAccount.mode
== ExternalChatAccountMode.SELF_HOST_BYO,
ExternalChatAccount.is_system_account.is_(False),
ExternalChatAccount.suspended_at.is_(None),
)
)
account = result.scalars().first()
if account is None:
continue
message_id = str(raw_event.get("messageId") or "")
if not message_id:
continue
inbox_id = await persist_inbound_event(
session,
account_id=account.id,
platform=ExternalChatPlatform.WHATSAPP,
event_dedupe_key=f"baileys:{message_id}",
external_event_id=message_id,
external_message_id=message_id,
event_kind="message",
raw_payload=raw_event,
)
await session.commit()
record_gateway_inbox_write(
platform="whatsapp",
dedup_skipped=inbox_id is None,
)
except asyncio.CancelledError:
raise
except Exception:
logger.exception("WhatsApp Baileys intake failed; retrying in 10s")
await _sleep_or_shutdown(10)
async def start_byo_long_poll_supervisors() -> None:
"""Start one BYO long-poll supervisor per active non-system Telegram account."""
global _shutdown_event
if not config.GATEWAY_ENABLED:
return
if (
config.GATEWAY_TELEGRAM_INTAKE_MODE != "longpoll"
and config.GATEWAY_WHATSAPP_INTAKE_MODE != "baileys"
):
return
if _tasks:
return
_shutdown_event = asyncio.Event()
if config.GATEWAY_TELEGRAM_INTAKE_MODE == "longpoll":
async with async_session_maker() as session:
result = await session.execute(
select(ExternalChatAccount).where(
ExternalChatAccount.platform == ExternalChatPlatform.TELEGRAM,
ExternalChatAccount.is_system_account.is_(False),
ExternalChatAccount.suspended_at.is_(None),
)
)
accounts = list(result.scalars())
for account in accounts:
token = account_token(account)
if not token:
continue
task = asyncio.create_task(
_byo_account_supervisor(int(account.id), token),
name=f"gateway-byo-telegram-{account.id}",
)
_tasks.add(task)
task.add_done_callback(_tasks.discard)
logger.info(
"Started BYO Telegram long-poll supervisor account_id=%s", account.id
)
if config.GATEWAY_WHATSAPP_INTAKE_MODE == "baileys":
task = asyncio.create_task(
_whatsapp_baileys_supervisor(),
name="gateway-byo-whatsapp-baileys",
)
_tasks.add(task)
task.add_done_callback(_tasks.discard)
logger.info("Started WhatsApp Baileys bridge intake supervisor")
async def stop_byo_long_poll_supervisors() -> None:
"""Cancel and await all BYO long-poll supervisors."""
global _shutdown_event
if _shutdown_event is not None:
_shutdown_event.set()
tasks = list(_tasks)
for task in tasks:
task.cancel()
if tasks:
try:
await asyncio.wait_for(
asyncio.gather(*tasks, return_exceptions=True), timeout=10
)
except TimeoutError:
logger.warning(
"Timed out waiting for BYO Telegram long-poll supervisors to stop"
)
_tasks.clear()
_shutdown_event = None