109 lines
3.5 KiB
Python
109 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import hmac
|
|
import json
|
|
import os
|
|
import threading
|
|
from typing import Any
|
|
|
|
from fastapi import HTTPException, Request
|
|
from loguru import logger
|
|
|
|
_WEBHOOK_BOT: Any | None = None
|
|
_WEBHOOK_BOT_LOCK = threading.Lock()
|
|
|
|
|
|
def _configured_secret() -> str:
|
|
return str(os.getenv("TELEGRAM_WEBHOOK_SECRET") or "").strip()
|
|
|
|
|
|
def _configured_header_secret() -> str:
|
|
return str(os.getenv("TELEGRAM_WEBHOOK_HEADER_SECRET") or "").strip()
|
|
|
|
|
|
def _parse_update(payload: dict[str, Any]) -> Any:
|
|
from telebot import types # type: ignore
|
|
|
|
return types.Update.de_json(json.dumps(payload))
|
|
|
|
|
|
def _build_webhook_bot() -> Any:
|
|
import telebot # type: ignore
|
|
|
|
from src.bot.handlers.activity import ActivityHandler
|
|
from src.bot.handlers.basic import BasicCommandHandler
|
|
from src.bot.io_layer import BotIOLayer
|
|
from src.bot.runtime_coordinator import StartupCoordinator
|
|
from src.database.db_manager import DBManager
|
|
from src.utils.config_loader import load_config
|
|
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
|
|
|
token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
|
|
if not token:
|
|
raise RuntimeError("TELEGRAM_BOT_TOKEN is not configured")
|
|
|
|
config = load_config()
|
|
bot = telebot.TeleBot(token)
|
|
io_layer = BotIOLayer(bot=bot, db=DBManager())
|
|
startup_coordinator = StartupCoordinator(
|
|
bot=bot,
|
|
config=config,
|
|
command_access_mode="public",
|
|
protected_commands=[],
|
|
required_group_chat_id=",".join(get_telegram_chat_ids_from_env()),
|
|
)
|
|
BasicCommandHandler(
|
|
bot=bot,
|
|
io_layer=io_layer,
|
|
runtime_status_provider=startup_coordinator.get_runtime_status,
|
|
config=config,
|
|
).register()
|
|
ActivityHandler(bot=bot, io_layer=io_layer).register()
|
|
return bot
|
|
|
|
|
|
def _get_webhook_bot() -> Any:
|
|
global _WEBHOOK_BOT
|
|
if _WEBHOOK_BOT is not None:
|
|
return _WEBHOOK_BOT
|
|
with _WEBHOOK_BOT_LOCK:
|
|
if _WEBHOOK_BOT is None:
|
|
_WEBHOOK_BOT = _build_webhook_bot()
|
|
return _WEBHOOK_BOT
|
|
|
|
|
|
def _verify_webhook_secret(path_secret: str, request: Request) -> None:
|
|
expected = _configured_secret()
|
|
if not expected:
|
|
raise HTTPException(status_code=503, detail="telegram webhook is not configured")
|
|
if not hmac.compare_digest(str(path_secret or ""), expected):
|
|
raise HTTPException(status_code=403, detail="invalid telegram webhook secret")
|
|
|
|
expected_header = _configured_header_secret()
|
|
if expected_header:
|
|
actual_header = request.headers.get("x-telegram-bot-api-secret-token", "")
|
|
if not hmac.compare_digest(actual_header, expected_header):
|
|
raise HTTPException(status_code=403, detail="invalid telegram webhook header")
|
|
|
|
|
|
async def handle_telegram_webhook(path_secret: str, request: Request) -> dict[str, bool]:
|
|
_verify_webhook_secret(path_secret, request)
|
|
try:
|
|
payload = await request.json()
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail="invalid telegram webhook payload") from exc
|
|
if not isinstance(payload, dict):
|
|
raise HTTPException(status_code=400, detail="invalid telegram webhook payload")
|
|
|
|
update = _parse_update(payload)
|
|
if update is None:
|
|
logger.warning("telegram webhook ignored unparsable update payload")
|
|
return {"ok": True}
|
|
|
|
try:
|
|
_get_webhook_bot().process_new_updates([update])
|
|
except Exception:
|
|
logger.exception("telegram webhook dispatch failed")
|
|
raise
|
|
return {"ok": True}
|