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

Switch Telegram bot to webhook mode for HF Spaces compatibility

Browse files
Files changed (2) hide show
  1. app/main.py +29 -9
  2. app/services/telegram_bot.py +73 -30
app/main.py CHANGED
@@ -3,6 +3,7 @@ MEXC Funding Rate Report - Backend API
3
 
4
  FastAPI application for providing funding rate data and analysis.
5
  """
 
6
  from contextlib import asynccontextmanager
7
  from fastapi import FastAPI
8
  from fastapi.middleware.cors import CORSMiddleware
@@ -17,6 +18,7 @@ from app.api import (
17
  watchlist_router,
18
  telegram_router,
19
  )
 
20
 
21
  # Initialize settings
22
  settings = get_settings()
@@ -51,6 +53,23 @@ async def scheduled_telegram_notify():
51
  await service.send_watchlist_notification()
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  @asynccontextmanager
55
  async def lifespan(app: FastAPI):
56
  """Application lifespan manager."""
@@ -60,14 +79,14 @@ async def lifespan(app: FastAPI):
60
  # Initial data fetch
61
  from app.services import get_mexc_client
62
  client = get_mexc_client()
63
- await client.refresh_data() # Initial fetch on startup
64
 
65
- # Start Telegram bot
66
  from app.services.telegram_bot import TelegramBot
67
- await TelegramBot.start()
 
68
 
69
  # Setup scheduler
70
- # Data refresh every 5 minutes
71
  scheduler.add_job(
72
  scheduled_data_refresh,
73
  'interval',
@@ -76,7 +95,6 @@ async def lifespan(app: FastAPI):
76
  replace_existing=True,
77
  )
78
 
79
- # FR snapshot for watchlist
80
  scheduler.add_job(
81
  scheduled_fr_snapshot,
82
  'interval',
@@ -85,7 +103,6 @@ async def lifespan(app: FastAPI):
85
  replace_existing=True,
86
  )
87
 
88
- # Telegram notifications
89
  scheduler.add_job(
90
  scheduled_telegram_notify,
91
  'interval',
@@ -101,7 +118,7 @@ async def lifespan(app: FastAPI):
101
 
102
  # Shutdown
103
  scheduler.shutdown()
104
- await TelegramBot.stop()
105
  await Database.disconnect()
106
 
107
 
@@ -131,12 +148,14 @@ app.include_router(stats_router, prefix="/api/v1")
131
  app.include_router(alerts_router, prefix="/api/v1")
132
  app.include_router(watchlist_router, prefix="/api/v1")
133
  app.include_router(telegram_router, prefix="/api/v1")
 
134
 
135
 
136
  @app.get("/")
137
  async def root():
138
  """Health check endpoint."""
139
  from app.services import get_mexc_client
 
140
  client = get_mexc_client()
141
  cache_age = client.get_cache_age_seconds()
142
 
@@ -145,7 +164,7 @@ async def root():
145
  "message": "MEXC FR Report API is running",
146
  "version": "2.0.0",
147
  "cache_age_seconds": cache_age,
148
- "data_refresh": "every 5 minutes",
149
  }
150
 
151
 
@@ -154,6 +173,7 @@ async def health():
154
  """Health check endpoint for monitoring."""
155
  from app.database import get_database
156
  from app.services import get_mexc_client
 
157
 
158
  db = get_database()
159
  client = get_mexc_client()
@@ -163,11 +183,11 @@ async def health():
163
  "database": "connected" if db is not None else "disconnected",
164
  "mexc_cache_age": client.get_cache_age_seconds(),
165
  "mexc_tickers_count": len(client.get_cached_tickers()),
 
166
  }
167
 
168
 
169
  if __name__ == "__main__":
170
- import os
171
  import uvicorn
172
  port = int(os.environ.get("PORT", 8000))
173
  uvicorn.run("app.main:app", host="0.0.0.0", port=port, reload=True)
 
3
 
4
  FastAPI application for providing funding rate data and analysis.
5
  """
6
+ import os
7
  from contextlib import asynccontextmanager
8
  from fastapi import FastAPI
9
  from fastapi.middleware.cors import CORSMiddleware
 
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
  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."""
 
79
  # Initial data fetch
80
  from app.services import get_mexc_client
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(
91
  scheduled_data_refresh,
92
  'interval',
 
95
  replace_existing=True,
96
  )
97
 
 
98
  scheduler.add_job(
99
  scheduled_fr_snapshot,
100
  'interval',
 
103
  replace_existing=True,
104
  )
105
 
 
106
  scheduler.add_job(
107
  scheduled_telegram_notify,
108
  'interval',
 
118
 
119
  # Shutdown
120
  scheduler.shutdown()
121
+ await TelegramBot.shutdown()
122
  await Database.disconnect()
123
 
124
 
 
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
  "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
  """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
  "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
 
190
  if __name__ == "__main__":
 
191
  import uvicorn
192
  port = int(os.environ.get("PORT", 8000))
193
  uvicorn.run("app.main:app", host="0.0.0.0", port=port, reload=True)
app/services/telegram_bot.py CHANGED
@@ -1,30 +1,34 @@
1
- """Telegram Bot with webhook for /start command."""
2
- import asyncio
3
  from telegram import Update, Bot
4
  from telegram.ext import Application, CommandHandler, ContextTypes
 
5
  from app.config import get_settings
6
 
 
 
7
 
8
  class TelegramBot:
9
  """Telegram bot with webhook handlers."""
10
 
11
- _instance = None
12
  _app: Application = None
13
  _bot: Bot = None
14
- _started = False
 
15
 
16
  @classmethod
17
- async def start(cls):
18
- """Start the Telegram bot (graceful - won't crash on failure)."""
19
  settings = get_settings()
20
 
21
  if not settings.telegram_bot_token:
22
  print("Telegram bot: Not configured (no token)")
23
- return
24
 
25
  try:
26
- cls._bot = Bot(token=settings.telegram_bot_token)
27
  cls._app = Application.builder().token(settings.telegram_bot_token).build()
 
28
 
29
  # Add handlers
30
  cls._app.add_handler(CommandHandler("start", cls._handle_start))
@@ -33,30 +37,50 @@ class TelegramBot:
33
  cls._app.add_handler(CommandHandler("top", cls._handle_top))
34
  cls._app.add_handler(CommandHandler("watchlist", cls._handle_watchlist))
35
 
36
- # Start polling in background
37
  await cls._app.initialize()
38
- await cls._app.start()
39
- await cls._app.updater.start_polling(drop_pending_updates=True)
40
 
41
- cls._started = True
42
- print("Telegram bot: Started with polling")
 
 
 
 
 
 
 
 
43
  except Exception as e:
44
- print(f"Telegram bot: Failed to start ({e.__class__.__name__})")
45
  cls._app = None
46
  cls._bot = None
47
- cls._started = False
 
48
 
49
  @classmethod
50
- async def stop(cls):
51
- """Stop the Telegram bot."""
52
- if cls._app and cls._started:
53
  try:
54
- await cls._app.updater.stop()
55
- await cls._app.stop()
 
56
  await cls._app.shutdown()
57
- print("Telegram bot: Stopped")
58
  except Exception as e:
59
- print(f"Telegram bot: Error stopping ({e})")
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  @classmethod
62
  async def _handle_start(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -65,7 +89,7 @@ class TelegramBot:
65
  message = (
66
  f"🚀 <b>MEXC FR Report Bot</b>\n\n"
67
  f"📍 Your Chat ID: <code>{chat_id}</code>\n\n"
68
- f"Copy this Chat ID and add it to your .env file:\n"
69
  f"<code>TELEGRAM_CHAT_ID={chat_id}</code>\n\n"
70
  f"<b>Commands:</b>\n"
71
  f"/top - Get Top 5 FR coins\n"
@@ -104,20 +128,17 @@ class TelegramBot:
104
 
105
  @classmethod
106
  async def _handle_top(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
107
- """Handle /top command - send top FR coins."""
108
  from app.services.telegram_service import get_telegram_service
109
  service = get_telegram_service()
110
-
111
- # Override chat_id for this message
112
  chat_id = update.effective_chat.id
113
  await service.send_top_fr_notification(chat_id=chat_id)
114
 
115
  @classmethod
116
  async def _handle_watchlist(cls, update: Update, context: ContextTypes.DEFAULT_TYPE):
117
- """Handle /watchlist command - send watchlist coins."""
118
  from app.services.telegram_service import get_telegram_service
119
  service = get_telegram_service()
120
-
121
  chat_id = update.effective_chat.id
122
  success = await service.send_watchlist_notification(chat_id=chat_id)
123
 
@@ -129,6 +150,28 @@ class TelegramBot:
129
  )
130
 
131
 
132
- # Create singleton getter
133
- def get_telegram_bot() -> TelegramBot:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  return TelegramBot
 
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:
25
  print("Telegram bot: Not configured (no token)")
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
  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:
67
+ await cls._bot.delete_webhook()
68
  await cls._app.shutdown()
69
+ print("Telegram bot: Shutdown complete")
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):
 
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
 
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
  )
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