| """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") |
|
|
| |
| |
| 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() |
|
|