Dmitry Beresnev commited on
Commit
558806d
Β·
1 Parent(s): bb08700

add new tg bot

Browse files
Files changed (3) hide show
  1. .env.example +4 -1
  2. Dockerfile +3 -2
  3. src/tg_bot.py +321 -0
.env.example CHANGED
@@ -2,4 +2,7 @@ TELEGRAM_TOKEN=
2
  FINNHUB_API_TOKEN=
3
  SPACE_ID=
4
  GEMINI_API_TOKEN=
5
- OPENROUTER_API_TOKEN=
 
 
 
 
2
  FINNHUB_API_TOKEN=
3
  SPACE_ID=
4
  GEMINI_API_TOKEN=
5
+ OPENROUTER_API_TOKEN=
6
+ GOOGLE_APPS_SCRIPT_URL=
7
+ WEBHOOK_SECRET=
8
+ SPECE_URL=
Dockerfile CHANGED
@@ -58,7 +58,8 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
58
 
59
  # Run the application when the container starts HF
60
  #CMD ["uvicorn", "src.telegram_bot:app", "--host", "0.0.0.0", "--port", "7860"]
61
- #CMD ["python", "main.py"]
62
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
63
  # Uncomment the line below to run the application locally
64
  #CMD ["python", "-m", "telegram_bot"]
 
58
 
59
  # Run the application when the container starts HF
60
  #CMD ["uvicorn", "src.telegram_bot:app", "--host", "0.0.0.0", "--port", "7860"]
61
+ CMD ["python", "main.py"]
62
+ #CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
63
+ #CMD ["uvicorn", "src.tg_bot:app", "--host", "0.0.0.0", "--port", "7860"]
64
  # Uncomment the line below to run the application locally
65
  #CMD ["python", "-m", "telegram_bot"]
src/tg_bot.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Telegram Bot with Google Apps Script Proxy
3
+
4
+ This implementation uses Google Apps Script as a proxy to bypass
5
+ HuggingFace's restrictions on Telegram API calls.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ from typing import Optional, Dict, Any
11
+ import httpx
12
+ from fastapi import FastAPI, Request, HTTPException
13
+ from pydantic import BaseModel
14
+ import uvicorn
15
+ import asyncio
16
+ from contextlib import asynccontextmanager
17
+
18
+ # Configure logging
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
22
+ )
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ # Configuration
27
+ class Config:
28
+ BOT_TOKEN: str = os.getenv("BOT_TOKEN") or os.getenv("TELEGRAM_TOKEN")
29
+ GOOGLE_APPS_SCRIPT_URL: str = os.getenv("GOOGLE_APPS_SCRIPT_URL", "")
30
+ WEBHOOK_SECRET: str = os.getenv("WEBHOOK_SECRET", "your-secret-key")
31
+ PORT: int = int(os.getenv("PORT", 7860))
32
+
33
+ @classmethod
34
+ def validate(cls) -> bool:
35
+ """Validate required configuration"""
36
+ missing = []
37
+ if not cls.BOT_TOKEN:
38
+ missing.append("BOT_TOKEN")
39
+ if not cls.GOOGLE_APPS_SCRIPT_URL:
40
+ missing.append("GOOGLE_APPS_SCRIPT_URL")
41
+
42
+ if missing:
43
+ logger.error(f"Missing required environment variables: {missing}")
44
+ return False
45
+ return True
46
+
47
+
48
+ # Pydantic models for request/response validation
49
+ class TelegramUpdate(BaseModel):
50
+ update_id: int
51
+ message: Optional[Dict[str, Any]] = None
52
+ callback_query: Optional[Dict[str, Any]] = None
53
+
54
+
55
+ class TelegramMessage(BaseModel):
56
+ message_id: int
57
+ from_user: Dict[str, Any] = {}
58
+ chat: Dict[str, Any]
59
+ text: Optional[str] = None
60
+
61
+
62
+ # Telegram Bot Service
63
+ class TelegramBotService:
64
+ def __init__(self):
65
+ self.config = Config()
66
+ self.http_client: Optional[httpx.AsyncClient] = None
67
+
68
+ async def initialize(self):
69
+ """Initialize HTTP client"""
70
+ self.http_client = httpx.AsyncClient(
71
+ timeout=httpx.Timeout(30.0),
72
+ limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
73
+ )
74
+ logger.info("TelegramBotService initialized")
75
+
76
+ async def cleanup(self):
77
+ """Cleanup resources"""
78
+ if self.http_client:
79
+ await self.http_client.aclose()
80
+ logger.info("TelegramBotService cleaned up")
81
+
82
+ async def send_message_via_proxy(
83
+ self,
84
+ chat_id: int,
85
+ text: str,
86
+ parse_mode: str = "HTML"
87
+ ) -> Dict[str, Any]:
88
+ """
89
+ Send message to Telegram via Google Apps Script proxy
90
+
91
+ Args:
92
+ chat_id: Telegram chat ID
93
+ text: Message text
94
+ parse_mode: Message parse mode (HTML, Markdown, etc.)
95
+
96
+ Returns:
97
+ Response from Telegram API
98
+ """
99
+ if not self.http_client:
100
+ raise RuntimeError("HTTP client not initialized")
101
+
102
+ payload = {
103
+ "method": "sendMessage",
104
+ "bot_token": self.config.BOT_TOKEN,
105
+ "chat_id": chat_id,
106
+ "text": text,
107
+ "parse_mode": parse_mode
108
+ }
109
+
110
+ try:
111
+ logger.info(f"Sending message to chat {chat_id} via proxy")
112
+ response = await self.http_client.post(
113
+ self.config.GOOGLE_APPS_SCRIPT_URL,
114
+ json=payload,
115
+ headers={"Content-Type": "application/json"}
116
+ )
117
+ response.raise_for_status()
118
+
119
+ result = response.json()
120
+ logger.info(f"Message sent successfully: {result.get('ok', False)}")
121
+ return result
122
+
123
+ except httpx.RequestError as e:
124
+ logger.error(f"HTTP request failed: {e}")
125
+ raise HTTPException(status_code=500, detail="Failed to send message")
126
+ except Exception as e:
127
+ logger.error(f"Unexpected error: {e}")
128
+ raise HTTPException(status_code=500, detail="Internal server error")
129
+
130
+ async def process_update(self, update: TelegramUpdate) -> None:
131
+ """Process incoming Telegram update"""
132
+ try:
133
+ if not update.message:
134
+ logger.debug("No message in update, skipping")
135
+ return
136
+
137
+ message = update.message
138
+ chat_id = message.get("chat", {}).get("id")
139
+ text = message.get("text", "").strip()
140
+ user_name = message.get("from", {}).get("first_name", "User")
141
+
142
+ if not chat_id:
143
+ logger.warning("No chat ID found in message")
144
+ return
145
+
146
+ logger.info(f"Processing message: '{text}' from user: {user_name}")
147
+
148
+ # Command handling
149
+ if text.startswith("/"):
150
+ await self._handle_command(chat_id, text, user_name)
151
+ else:
152
+ await self._handle_regular_message(chat_id, text, user_name)
153
+
154
+ except Exception as e:
155
+ logger.error(f"Error processing update: {e}")
156
+ if update.message and update.message.get("chat", {}).get("id"):
157
+ await self.send_message_via_proxy(
158
+ update.message["chat"]["id"],
159
+ "Sorry, something went wrong. Please try again later."
160
+ )
161
+
162
+ async def _handle_command(self, chat_id: int, command: str, user_name: str) -> None:
163
+ """Handle bot commands"""
164
+ command = command.lower().strip()
165
+
166
+ if command in ["/start", "/hello"]:
167
+ response = f"πŸ‘‹ Hello, {user_name}! Welcome to the Financial News Bot!\n\n"
168
+ response += "Available commands:\n"
169
+ response += "/hello - Say hello\n"
170
+ response += "/help - Show help\n"
171
+ response += "/status - Check bot status"
172
+
173
+ elif command == "/help":
174
+ response = "πŸ€– <b>Financial News Bot Help</b>\n\n"
175
+ response += "<b>Commands:</b>\n"
176
+ response += "/start or /hello - Get started\n"
177
+ response += "/help - Show this help message\n"
178
+ response += "/status - Check bot status\n\n"
179
+ response += "<b>About:</b>\n"
180
+ response += "This bot provides financial news and sentiment analysis."
181
+
182
+ elif command == "/status":
183
+ response = "βœ… <b>Bot Status: Online</b>\n\n"
184
+ response += "πŸ”§ System: Running on HuggingFace Spaces\n"
185
+ response += "🌐 Proxy: Google Apps Script\n"
186
+ response += "πŸ“Š Status: All systems operational"
187
+
188
+ else:
189
+ response = f"❓ Unknown command: {command}\n\n"
190
+ response += "Type /help to see available commands."
191
+
192
+ await self.send_message_via_proxy(chat_id, response)
193
+
194
+ async def _handle_regular_message(self, chat_id: int, text: str, user_name: str) -> None:
195
+ """Handle regular (non-command) messages"""
196
+ response = f"Hello {user_name}! πŸ‘‹\n\n"
197
+ response += f"You said: <i>\"{text}\"</i>\n\n"
198
+ response += "I'm a financial news bot. Try these commands:\n"
199
+ response += "/hello - Get started\n"
200
+ response += "/help - Show help\n"
201
+ response += "/status - Check status"
202
+
203
+ await self.send_message_via_proxy(chat_id, response)
204
+
205
+
206
+ # Global bot service instance
207
+ bot_service = TelegramBotService()
208
+
209
+
210
+ # FastAPI lifespan management
211
+ @asynccontextmanager
212
+ async def lifespan(app: FastAPI):
213
+ """Manage application lifespan"""
214
+ # Startup
215
+ logger.info("Starting up application...")
216
+
217
+ if not Config.validate():
218
+ raise RuntimeError("Invalid configuration")
219
+
220
+ await bot_service.initialize()
221
+ logger.info("Application startup complete")
222
+
223
+ yield
224
+
225
+ # Shutdown
226
+ logger.info("Shutting down application...")
227
+ await bot_service.cleanup()
228
+ logger.info("Application shutdown complete")
229
+
230
+
231
+ # FastAPI app
232
+ app = FastAPI(
233
+ title="Financial News Telegram Bot",
234
+ description="A Telegram bot for financial news with Google Apps Script proxy",
235
+ version="1.0.0",
236
+ lifespan=lifespan
237
+ )
238
+
239
+
240
+ @app.get("/")
241
+ async def root():
242
+ """Root endpoint for health checks"""
243
+ return {
244
+ "status": "running",
245
+ "message": "Financial News Telegram Bot",
246
+ "version": "1.0.0",
247
+ "bot_configured": bool(Config.BOT_TOKEN),
248
+ "proxy_configured": bool(Config.GOOGLE_APPS_SCRIPT_URL)
249
+ }
250
+
251
+
252
+ @app.get("/health")
253
+ async def health_check():
254
+ """Detailed health check"""
255
+ return {
256
+ "status": "healthy",
257
+ "timestamp": "2025-01-01T00:00:00Z",
258
+ "services": {
259
+ "bot": "online",
260
+ "proxy": "configured" if Config.GOOGLE_APPS_SCRIPT_URL else "not_configured"
261
+ }
262
+ }
263
+
264
+
265
+ @app.post(f"/webhook/{Config.WEBHOOK_SECRET}")
266
+ async def webhook(request: Request):
267
+ """
268
+ Webhook endpoint for Telegram updates
269
+ URL: https://your-space.hf.space/webhook/{WEBHOOK_SECRET}
270
+ """
271
+ try:
272
+ json_data = await request.json()
273
+ logger.debug(f"Received webhook data: {json_data}")
274
+
275
+ # Validate update structure
276
+ update = TelegramUpdate(**json_data)
277
+
278
+ # Process update asynchronously
279
+ asyncio.create_task(bot_service.process_update(update))
280
+
281
+ return {"ok": True}
282
+
283
+ except Exception as e:
284
+ logger.error(f"Webhook error: {e}")
285
+ raise HTTPException(status_code=400, detail="Invalid update format")
286
+
287
+
288
+ @app.get("/webhook_info")
289
+ async def webhook_info():
290
+ """Get webhook configuration info"""
291
+ return {
292
+ "webhook_url": f"https://your-space-id.hf.space/webhook/{Config.WEBHOOK_SECRET}",
293
+ "instructions": [
294
+ "1. Deploy this app to HuggingFace Spaces",
295
+ "2. Set up Google Apps Script proxy",
296
+ "3. Set webhook URL using Telegram Bot API",
297
+ "4. Test with /hello command"
298
+ ]
299
+ }
300
+
301
+
302
+ @app.post("/test_message")
303
+ async def test_message(chat_id: int, message: str = "Hello World!"):
304
+ """Test endpoint to send a message (for debugging)"""
305
+ try:
306
+ result = await bot_service.send_message_via_proxy(chat_id, message)
307
+ return {"success": True, "result": result}
308
+ except Exception as e:
309
+ logger.error(f"Test message failed: {e}")
310
+ raise HTTPException(status_code=500, detail=str(e))
311
+
312
+
313
+ # Development server
314
+ if __name__ == "__main__":
315
+ logger.info(f"Starting Financial News Bot on port {Config.PORT}")
316
+ uvicorn.run(
317
+ app,
318
+ host="0.0.0.0",
319
+ port=Config.PORT,
320
+ log_level="info"
321
+ )