Dmitry Beresnev
Add Telegram bot control interface for HuggingFace Spaces deployment
820c4b2
Raw
History Blame Contribute Delete
3.43 kB
"""FastAPI app hosting the Telegram webhook on HuggingFace Spaces.
Startup sequence:
1. The platform polling loop starts in a background thread (PlatformController).
2. The async Telegram client connects (through the Google Apps Script proxy
when GOOGLE_APPS_SCRIPT_URL is set — required on HF Spaces).
3. Telegram pushes updates to POST /webhook/{WEBHOOK_SECRET}; register the URL
once via POST /set_webhook after deploying.
"""
import asyncio
import logging
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from .bot_service import TelegramBotService
from .config import BotConfig
from .controller import PlatformController
from .telegram_client import TelegramClient
from .tg_models import TelegramUpdate
logger = logging.getLogger(__name__)
controller = PlatformController()
client = TelegramClient()
bot_service = TelegramBotService(controller, client)
@asynccontextmanager
async def lifespan(app: FastAPI):
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
BotConfig.validate()
controller.start()
await client.initialize()
logger.info("telegram bot server started")
yield
controller.stop()
await client.aclose()
logger.info("telegram bot server stopped")
app = FastAPI(
title="Market Analyzing Platform Bot",
description="Telegram control interface for the market-wide risk radar",
version="0.1.0",
lifespan=lifespan,
)
@app.get("/")
async def root():
"""Health endpoint HuggingFace Spaces pings to keep the Space alive."""
return {
"status": "running",
"platform": controller.status(),
"bot_configured": bool(BotConfig.BOT_TOKEN),
"proxy_configured": bool(BotConfig.PROXY_URL),
}
@app.get("/health")
async def health():
return {"status": "healthy", "platform_state": controller.status()["state"]}
@app.post(f"/webhook/{BotConfig.WEBHOOK_SECRET}")
async def webhook(request: Request):
try:
json_data = await request.json()
update = TelegramUpdate(**json_data)
except Exception as exc:
logger.error("invalid webhook payload: %s", exc)
raise HTTPException(status_code=400, detail="Invalid update format")
# Ack Telegram immediately; process in the background so slow handlers
# don't trigger webhook retries
asyncio.create_task(bot_service.process_update(update))
return {"ok": True}
@app.post("/set_webhook")
async def set_webhook(url: str = ""):
"""Register the webhook with Telegram (via the proxy when configured).
Uses https://{SPACE_HOST}/webhook/{WEBHOOK_SECRET} unless `url` is given.
"""
target = url or BotConfig.webhook_url()
if not target:
raise HTTPException(
status_code=400,
detail="No url given and SPACE_HOST is not set; pass ?url=https://.../webhook/<secret>",
)
result = await client.call("setWebhook", url=target)
return {"webhook_url": target, "telegram_response": result}
@app.get("/webhook_info")
async def webhook_info():
result = await client.call("getWebhookInfo")
return {"expected_url": BotConfig.webhook_url() or "<set SPACE_HOST>", "telegram_response": result}
def main() -> None:
uvicorn.run(app, host="0.0.0.0", port=BotConfig.PORT, log_level="info")
if __name__ == "__main__":
main()