File size: 3,427 Bytes
820c4b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""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()