Cuong2004 commited on
Commit
8120515
·
1 Parent(s): ddf7e4d

Switch Telegram bot to webhook mode for HF Spaces

Browse files
app/api/routes/telegram.py CHANGED
@@ -110,3 +110,16 @@ async def record_fr_snapshot():
110
  "message": f"Recorded FR snapshot for {count} coins",
111
  "count": count
112
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  "message": f"Recorded FR snapshot for {count} coins",
111
  "count": count
112
  }
113
+
114
+
115
+ @router.post("/webhook")
116
+ async def telegram_webhook(request: dict):
117
+ """
118
+ Webhook endpoint for Telegram bot updates.
119
+ Telegram will POST updates here instead of polling.
120
+ """
121
+ from app.services.telegram_bot import TelegramBot
122
+
123
+ success = await TelegramBot.process_update(request)
124
+
125
+ return {"ok": success}
app/main.py CHANGED
@@ -18,7 +18,6 @@ from app.api import (
18
  watchlist_router,
19
  telegram_router,
20
  )
21
- from app.services.telegram_bot import router as telegram_webhook_router
22
 
23
  # Initialize settings
24
  settings = get_settings()
@@ -53,23 +52,6 @@ async def scheduled_telegram_notify():
53
  await service.send_watchlist_notification()
54
 
55
 
56
- def get_webhook_base_url() -> str:
57
- """Get the base URL for webhook based on environment."""
58
- # Check for HF Spaces
59
- space_id = os.environ.get("SPACE_ID")
60
- if space_id:
61
- # Format: username-spacename
62
- return f"https://{space_id.replace('/', '-')}.hf.space"
63
-
64
- # Check for explicit WEBHOOK_URL
65
- webhook_url = os.environ.get("WEBHOOK_URL")
66
- if webhook_url:
67
- return webhook_url
68
-
69
- # Local development - no webhook
70
- return None
71
-
72
-
73
  @asynccontextmanager
74
  async def lifespan(app: FastAPI):
75
  """Application lifespan manager."""
@@ -81,10 +63,12 @@ async def lifespan(app: FastAPI):
81
  client = get_mexc_client()
82
  await client.refresh_data()
83
 
84
- # Initialize Telegram bot with webhook
85
  from app.services.telegram_bot import TelegramBot
86
- webhook_url = get_webhook_base_url()
87
- await TelegramBot.init(webhook_base_url=webhook_url)
 
 
88
 
89
  # Setup scheduler
90
  scheduler.add_job(
@@ -94,7 +78,6 @@ async def lifespan(app: FastAPI):
94
  id='data_refresh',
95
  replace_existing=True,
96
  )
97
-
98
  scheduler.add_job(
99
  scheduled_fr_snapshot,
100
  'interval',
@@ -102,7 +85,6 @@ async def lifespan(app: FastAPI):
102
  id='fr_snapshot',
103
  replace_existing=True,
104
  )
105
-
106
  scheduler.add_job(
107
  scheduled_telegram_notify,
108
  'interval',
@@ -148,14 +130,12 @@ app.include_router(stats_router, prefix="/api/v1")
148
  app.include_router(alerts_router, prefix="/api/v1")
149
  app.include_router(watchlist_router, prefix="/api/v1")
150
  app.include_router(telegram_router, prefix="/api/v1")
151
- app.include_router(telegram_webhook_router) # Telegram webhook at /telegram-webhook
152
 
153
 
154
  @app.get("/")
155
  async def root():
156
  """Health check endpoint."""
157
  from app.services import get_mexc_client
158
- from app.services.telegram_bot import TelegramBot
159
  client = get_mexc_client()
160
  cache_age = client.get_cache_age_seconds()
161
 
@@ -164,7 +144,7 @@ async def root():
164
  "message": "MEXC FR Report API is running",
165
  "version": "2.0.0",
166
  "cache_age_seconds": cache_age,
167
- "telegram_webhook": TelegramBot._initialized,
168
  }
169
 
170
 
@@ -173,7 +153,6 @@ async def health():
173
  """Health check endpoint for monitoring."""
174
  from app.database import get_database
175
  from app.services import get_mexc_client
176
- from app.services.telegram_bot import TelegramBot
177
 
178
  db = get_database()
179
  client = get_mexc_client()
@@ -183,7 +162,6 @@ async def health():
183
  "database": "connected" if db is not None else "disconnected",
184
  "mexc_cache_age": client.get_cache_age_seconds(),
185
  "mexc_tickers_count": len(client.get_cached_tickers()),
186
- "telegram": "webhook" if TelegramBot._initialized else "disabled",
187
  }
188
 
189
 
 
18
  watchlist_router,
19
  telegram_router,
20
  )
 
21
 
22
  # Initialize settings
23
  settings = get_settings()
 
52
  await service.send_watchlist_notification()
53
 
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  @asynccontextmanager
56
  async def lifespan(app: FastAPI):
57
  """Application lifespan manager."""
 
63
  client = get_mexc_client()
64
  await client.refresh_data()
65
 
66
+ # Setup Telegram bot with webhook
67
  from app.services.telegram_bot import TelegramBot
68
+ webhook_url = os.environ.get("SPACE_HOST")
69
+ if webhook_url:
70
+ webhook_url = f"https://{webhook_url}"
71
+ await TelegramBot.setup(webhook_base_url=webhook_url)
72
 
73
  # Setup scheduler
74
  scheduler.add_job(
 
78
  id='data_refresh',
79
  replace_existing=True,
80
  )
 
81
  scheduler.add_job(
82
  scheduled_fr_snapshot,
83
  'interval',
 
85
  id='fr_snapshot',
86
  replace_existing=True,
87
  )
 
88
  scheduler.add_job(
89
  scheduled_telegram_notify,
90
  'interval',
 
130
  app.include_router(alerts_router, prefix="/api/v1")
131
  app.include_router(watchlist_router, prefix="/api/v1")
132
  app.include_router(telegram_router, prefix="/api/v1")
 
133
 
134
 
135
  @app.get("/")
136
  async def root():
137
  """Health check endpoint."""
138
  from app.services import get_mexc_client
 
139
  client = get_mexc_client()
140
  cache_age = client.get_cache_age_seconds()
141
 
 
144
  "message": "MEXC FR Report API is running",
145
  "version": "2.0.0",
146
  "cache_age_seconds": cache_age,
147
+ "data_refresh": "every 5 minutes",
148
  }
149
 
150
 
 
153
  """Health check endpoint for monitoring."""
154
  from app.database import get_database
155
  from app.services import get_mexc_client
 
156
 
157
  db = get_database()
158
  client = get_mexc_client()
 
162
  "database": "connected" if db is not None else "disconnected",
163
  "mexc_cache_age": client.get_cache_age_seconds(),
164
  "mexc_tickers_count": len(client.get_cached_tickers()),
 
165
  }
166
 
167
 
app/services/telegram_bot.py CHANGED
@@ -1,24 +1,25 @@
1
- """Telegram Bot using Webhook mode for HF Spaces compatibility."""
2
- import os
3
  from telegram import Update, Bot
4
  from telegram.ext import Application, CommandHandler, ContextTypes
5
- from fastapi import APIRouter, Request, Response
6
  from app.config import get_settings
7
 
8
- router = APIRouter(prefix="/telegram-webhook", tags=["Telegram Webhook"])
9
-
10
 
11
  class TelegramBot:
12
  """Telegram bot with webhook handlers."""
13
 
14
  _app: Application = None
15
  _bot: Bot = None
16
- _initialized = False
17
  _webhook_url: str = None
18
 
19
  @classmethod
20
- async def init(cls, webhook_base_url: str = None):
21
- """Initialize the Telegram bot with webhook."""
 
 
 
22
  settings = get_settings()
23
 
24
  if not settings.telegram_bot_token:
@@ -26,9 +27,8 @@ class TelegramBot:
26
  return False
27
 
28
  try:
29
- # Build application
30
  cls._app = Application.builder().token(settings.telegram_bot_token).build()
31
- cls._bot = cls._app.bot
32
 
33
  # Add handlers
34
  cls._app.add_handler(CommandHandler("start", cls._handle_start))
@@ -37,30 +37,44 @@ class TelegramBot:
37
  cls._app.add_handler(CommandHandler("top", cls._handle_top))
38
  cls._app.add_handler(CommandHandler("watchlist", cls._handle_watchlist))
39
 
40
- # Initialize (but don't start polling)
41
  await cls._app.initialize()
42
 
43
  # Set webhook if URL provided
44
  if webhook_base_url:
45
- cls._webhook_url = f"{webhook_base_url}/telegram-webhook/update"
46
  await cls._bot.set_webhook(url=cls._webhook_url)
47
  print(f"Telegram bot: Webhook set to {cls._webhook_url}")
48
 
49
- cls._initialized = True
50
- print("Telegram bot: Initialized in webhook mode")
51
  return True
52
 
53
  except Exception as e:
54
- print(f"Telegram bot: Failed to initialize ({e.__class__.__name__}: {e})")
55
  cls._app = None
56
  cls._bot = None
57
- cls._initialized = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  return False
59
 
60
  @classmethod
61
  async def shutdown(cls):
62
  """Shutdown the bot."""
63
- if cls._app and cls._initialized:
64
  try:
65
  # Remove webhook
66
  if cls._bot:
@@ -70,18 +84,6 @@ class TelegramBot:
70
  except Exception as e:
71
  print(f"Telegram bot: Error during shutdown ({e})")
72
 
73
- @classmethod
74
- async def process_update(cls, update_data: dict):
75
- """Process incoming webhook update."""
76
- if not cls._initialized or not cls._app:
77
- return
78
-
79
- try:
80
- update = Update.de_json(update_data, cls._bot)
81
- await cls._app.process_update(update)
82
- except Exception as e:
83
- print(f"Telegram bot: Error processing update ({e})")
84
-
85
  @classmethod
86
  async def _handle_start(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
87
  """Handle /start command."""
@@ -89,7 +91,7 @@ class TelegramBot:
89
  message = (
90
  f"🚀 <b>MEXC FR Report Bot</b>\n\n"
91
  f"📍 Your Chat ID: <code>{chat_id}</code>\n\n"
92
- f"Copy this Chat ID and add it to your environment:\n"
93
  f"<code>TELEGRAM_CHAT_ID={chat_id}</code>\n\n"
94
  f"<b>Commands:</b>\n"
95
  f"/top - Get Top 5 FR coins\n"
@@ -128,17 +130,19 @@ class TelegramBot:
128
 
129
  @classmethod
130
  async def _handle_top(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
131
- """Handle /top command."""
132
  from app.services.telegram_service import get_telegram_service
133
  service = get_telegram_service()
 
134
  chat_id = update.effective_chat.id
135
  await service.send_top_fr_notification(chat_id=chat_id)
136
 
137
  @classmethod
138
  async def _handle_watchlist(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
139
- """Handle /watchlist command."""
140
  from app.services.telegram_service import get_telegram_service
141
  service = get_telegram_service()
 
142
  chat_id = update.effective_chat.id
143
  success = await service.send_watchlist_notification(chat_id=chat_id)
144
 
@@ -150,28 +154,5 @@ class TelegramBot:
150
  )
151
 
152
 
153
- # Webhook endpoint
154
- @router.post("/update")
155
- async def telegram_webhook(request: Request):
156
- """Handle incoming Telegram webhook updates."""
157
- try:
158
- update_data = await request.json()
159
- await TelegramBot.process_update(update_data)
160
- return Response(status_code=200)
161
- except Exception as e:
162
- print(f"Webhook error: {e}")
163
- return Response(status_code=200) # Always return 200 to Telegram
164
-
165
-
166
- @router.get("/status")
167
- async def webhook_status():
168
- """Check webhook status."""
169
- return {
170
- "initialized": TelegramBot._initialized,
171
- "webhook_url": TelegramBot._webhook_url,
172
- "mode": "webhook"
173
- }
174
-
175
-
176
  def get_telegram_bot():
177
  return TelegramBot
 
1
+ """Telegram Bot with webhook mode for HF Spaces."""
2
+ import asyncio
3
  from telegram import Update, Bot
4
  from telegram.ext import Application, CommandHandler, ContextTypes
5
+ from fastapi import Request
6
  from app.config import get_settings
7
 
 
 
8
 
9
  class TelegramBot:
10
  """Telegram bot with webhook handlers."""
11
 
12
  _app: Application = None
13
  _bot: Bot = None
14
+ _started = False
15
  _webhook_url: str = None
16
 
17
  @classmethod
18
+ async def setup(cls, webhook_base_url: str = None):
19
+ """
20
+ Setup the Telegram bot (webhook mode).
21
+ Call this during app startup.
22
+ """
23
  settings = get_settings()
24
 
25
  if not settings.telegram_bot_token:
 
27
  return False
28
 
29
  try:
30
+ cls._bot = Bot(token=settings.telegram_bot_token)
31
  cls._app = Application.builder().token(settings.telegram_bot_token).build()
 
32
 
33
  # Add handlers
34
  cls._app.add_handler(CommandHandler("start", cls._handle_start))
 
37
  cls._app.add_handler(CommandHandler("top", cls._handle_top))
38
  cls._app.add_handler(CommandHandler("watchlist", cls._handle_watchlist))
39
 
40
+ # Initialize app (but don't start polling)
41
  await cls._app.initialize()
42
 
43
  # Set webhook if URL provided
44
  if webhook_base_url:
45
+ cls._webhook_url = f"{webhook_base_url}/api/v1/telegram/webhook"
46
  await cls._bot.set_webhook(url=cls._webhook_url)
47
  print(f"Telegram bot: Webhook set to {cls._webhook_url}")
48
 
49
+ cls._started = True
50
+ print("Telegram bot: Initialized successfully")
51
  return True
52
 
53
  except Exception as e:
54
+ print(f"Telegram bot: Failed to setup ({e.__class__.__name__}: {e})")
55
  cls._app = None
56
  cls._bot = None
57
+ cls._started = False
58
+ return False
59
+
60
+ @classmethod
61
+ async def process_update(cls, update_data: dict):
62
+ """Process incoming webhook update."""
63
+ if not cls._app or not cls._started:
64
+ return False
65
+
66
+ try:
67
+ update = Update.de_json(update_data, cls._bot)
68
+ await cls._app.process_update(update)
69
+ return True
70
+ except Exception as e:
71
+ print(f"Telegram bot: Error processing update ({e})")
72
  return False
73
 
74
  @classmethod
75
  async def shutdown(cls):
76
  """Shutdown the bot."""
77
+ if cls._app and cls._started:
78
  try:
79
  # Remove webhook
80
  if cls._bot:
 
84
  except Exception as e:
85
  print(f"Telegram bot: Error during shutdown ({e})")
86
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  @classmethod
88
  async def _handle_start(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
89
  """Handle /start command."""
 
91
  message = (
92
  f"🚀 <b>MEXC FR Report Bot</b>\n\n"
93
  f"📍 Your Chat ID: <code>{chat_id}</code>\n\n"
94
+ f"Copy this Chat ID and add it to your .env file:\n"
95
  f"<code>TELEGRAM_CHAT_ID={chat_id}</code>\n\n"
96
  f"<b>Commands:</b>\n"
97
  f"/top - Get Top 5 FR coins\n"
 
130
 
131
  @classmethod
132
  async def _handle_top(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
133
+ """Handle /top command - send top FR coins."""
134
  from app.services.telegram_service import get_telegram_service
135
  service = get_telegram_service()
136
+
137
  chat_id = update.effective_chat.id
138
  await service.send_top_fr_notification(chat_id=chat_id)
139
 
140
  @classmethod
141
  async def _handle_watchlist(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
142
+ """Handle /watchlist command - send watchlist coins."""
143
  from app.services.telegram_service import get_telegram_service
144
  service = get_telegram_service()
145
+
146
  chat_id = update.effective_chat.id
147
  success = await service.send_watchlist_notification(chat_id=chat_id)
148
 
 
154
  )
155
 
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  def get_telegram_bot():
158
  return TelegramBot