baha-99 commited on
Commit
c2ca7ac
Β·
verified Β·
1 Parent(s): 3cd2bd3

Upload 7 files

Browse files
Files changed (7) hide show
  1. Dockerfile +25 -0
  2. README.md +11 -10
  3. app.py +26 -0
  4. bot_telegram.py +410 -0
  5. digram.io +0 -0
  6. requirements.freeze.txt +5 -0
  7. requirements.txt +5 -0
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ RUN useradd -m -u 1000 chameleon
4
+
5
+ USER chameleon
6
+
7
+ ENV PATH="/home/chameleon/.local/bin:$PATH"
8
+
9
+ WORKDIR /app
10
+
11
+ COPY --chown=chameleon requirements.txt requirements.txt
12
+
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=chameleon . .
16
+
17
+ # HuggingFace Space port
18
+ ENV PORT=7860
19
+
20
+ # Expose the port
21
+ EXPOSE 7860
22
+
23
+ # Start command for loop
24
+ # CMD ["sh", "-c", "python bot_telegram.py & tail -f /dev/null"]
25
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,11 @@
1
- ---
2
- title: Bot
3
- emoji: πŸŒ–
4
- colorFrom: purple
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
+ ---
2
+ title: Telegram Bot
3
+ emoji: πŸ”₯
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ short_description: it is making not accessible for other people
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import logging
3
+
4
+ from bot_telegram import init_bot
5
+
6
+ app = FastAPI()
7
+ bot = None
8
+
9
+ @app.on_event("startup")
10
+ async def startup_event():
11
+ """Start the Telegram bot when the FastAPI application starts."""
12
+ global bot
13
+ bot = init_bot()
14
+ await bot.run()
15
+
16
+ @app.on_event("shutdown")
17
+ async def shutdown_event():
18
+ """Stop the Telegram bot when the FastAPI application stops."""
19
+ global bot
20
+ if bot:
21
+ logging.info("Stopping bot...")
22
+ await bot.bot_stop()
23
+
24
+ @app.get("/")
25
+ def greet_json():
26
+ return {"Hello": "World!"}
bot_telegram.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import requests
4
+ from telegram import Update
5
+ from telegram.ext import Application, CommandHandler, MessageHandler, filters, CallbackContext
6
+ import aiohttp
7
+ import io
8
+ import asyncio
9
+ from concurrent.futures import ThreadPoolExecutor
10
+ import time
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
14
+
15
+ # Load environment variables from Hugging Face Secrets
16
+ BOT_SECRET_PASSWORD = os.getenv("BOT_SECRET_PASSWORD")
17
+ BOT_TOKEN = os.getenv("BOT_TOKEN")
18
+ BASE_URL = os.getenv("BASE_URL")
19
+ HF_TOKEN = os.getenv("HF_TOKEN")
20
+ # API_USERNAME = os.getenv("API_USERNAME")
21
+ # API_PASSWORD = os.getenv("API_PASSWORD")
22
+
23
+ # Set the secret password for authentication
24
+ SECRET_PASSWORD = BOT_SECRET_PASSWORD
25
+
26
+ # Dictionary to store authenticated users
27
+ AUTHENTICATED_USERS = set()
28
+ AWAITING_PASSWORD = set()
29
+
30
+ logging.info("Bot starting... Version 1.0.1") # Add version number
31
+
32
+ class TelegramBot:
33
+ """A Telegram bot with password-based authentication."""
34
+
35
+ def __init__(self, bot_token, base_url):
36
+ """Initialize the bot with Telegram API token, API credentials, and authentication."""
37
+ self.bot_token = bot_token
38
+ self.base_url = base_url
39
+ # self.username = username
40
+ # self.password = password
41
+ # self.auth_token = None
42
+
43
+ # API Endpoints
44
+ self.login_url = f"{self.base_url}/api/v1/auth/login"
45
+ self.ai_url = f"{self.base_url}/api/v1/questions/text"
46
+ self.excel_url = f"{self.base_url}/api/v1/questions/excel"
47
+
48
+ # Executors (keep only one instance of each)
49
+ self.text_executor = ThreadPoolExecutor(max_workers=5, thread_name_prefix="text_worker")
50
+ self.excel_executor = ThreadPoolExecutor(max_workers=5, thread_name_prefix="excel_worker")
51
+ self.excel_semaphore = asyncio.Semaphore(10)
52
+
53
+ # Track active processes
54
+ self.active_requests = {}
55
+ self.active_excel_files = {}
56
+
57
+ # Start Telegram Bot
58
+ self.app = Application.builder().token(self.bot_token).build()
59
+ self.setup_handlers()
60
+
61
+ # Authenticate with API
62
+ logging.info("Authenticating with API...")
63
+ # self.authenticate()
64
+
65
+ # Create a ThreadPoolExecutor for handling concurrent requests
66
+ self.executor = ThreadPoolExecutor(max_workers=10)
67
+
68
+ # Increase Excel workers for more concurrent processing
69
+ self.excel_executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix="excel_worker")
70
+ self.excel_semaphore = asyncio.Semaphore(10) # Allow 10 concurrent Excel processes
71
+ self.active_excel_files = {}
72
+
73
+ # def authenticate(self):
74
+ # """Authenticate with the API and retrieve an access token."""
75
+ # payload = {"username": self.username, "password": self.password}
76
+ # headers = {"Content-Type": "application/json", "accept": "application/json"}
77
+ #
78
+ # try:
79
+ # response = requests.post(self.login_url, headers=headers, json=payload)
80
+ #
81
+ # if response.status_code == 200:
82
+ # self.auth_token = response.json().get("access_token")
83
+ # logging.info("Successfully authenticated with API")
84
+ # else:
85
+ # logging.error(f"Authentication failed: {response.status_code} - {response.text}")
86
+ #
87
+ # except Exception as e:
88
+ # logging.error(f"Authentication Error: {e}")
89
+
90
+ async def start_command(self, update: Update, context: CallbackContext):
91
+ """Handles the /start command and asks for a password if the user is not authenticated."""
92
+ user_id = update.message.from_user.id
93
+
94
+ if user_id in AUTHENTICATED_USERS:
95
+ await update.message.reply_text(
96
+ "βœ… You are already authenticated!\n\n"
97
+ "You can:\n"
98
+ "1. Send me any question as text\n"
99
+ "2. Send me an Excel file with questions (must have a 'question' column in 'rfp' sheet)\n\n"
100
+ "Note: Excel files must contain no more than 50 questions.\n\n"
101
+ "type '/status' to check the status of the request"
102
+ )
103
+ else:
104
+ AWAITING_PASSWORD.add(user_id)
105
+ await update.message.reply_text("πŸ”‘ Please enter the secret password to access the bot.")
106
+
107
+ async def handle_message(self, update: Update, context: CallbackContext):
108
+ """Handles all incoming messages concurrently"""
109
+ user_id = update.message.from_user.id
110
+ user_message = update.message.text.strip()
111
+ message_id = update.message.message_id
112
+
113
+ # If user is waiting to enter a password, validate it
114
+ if user_id in AWAITING_PASSWORD:
115
+ await self.check_password(update, context)
116
+ return
117
+
118
+ # If user is authenticated, process AI request
119
+ if user_id in AUTHENTICATED_USERS:
120
+ # Create task for processing
121
+ asyncio.create_task(self.chat_with_ai(update, context))
122
+ else:
123
+ await update.message.reply_text("❌ You are not authenticated. Please type /start to authenticate and then enter the password.")
124
+
125
+ async def check_password(self, update: Update, context: CallbackContext):
126
+ """Checks if the password is correct and authenticates the user."""
127
+ user_id = update.message.from_user.id
128
+ user_message = update.message.text.strip()
129
+
130
+ if user_message == SECRET_PASSWORD:
131
+ AUTHENTICATED_USERS.add(user_id)
132
+ AWAITING_PASSWORD.discard(user_id)
133
+ logging.info(f"User {user_id} authenticated successfully.")
134
+ await update.message.reply_text(
135
+ "βœ… Authentication successful!\n\n"
136
+ "You can:\n"
137
+ "1. Send me any question as text\n"
138
+ "2. Send me an Excel file with questions (must have a 'question' column in 'rfp' sheet)\n\n"
139
+ "Note: Excel files must contain no more than 50 questions."
140
+ )
141
+ else:
142
+ await update.message.reply_text("❌ Wrong password. Try again.")
143
+
144
+ async def chat_with_ai(self, update: Update, context: CallbackContext):
145
+ """Process text messages asynchronously"""
146
+ message_id = update.message.message_id
147
+ user_message = update.message.text
148
+
149
+ try:
150
+ # Send immediate acknowledgment
151
+ processing_msg = await update.message.reply_text(
152
+ f"πŸ€” Processing your request...\n"
153
+ f"Request ID: #{message_id}"
154
+ )
155
+
156
+ # Track this request
157
+ self.active_requests[message_id] = {
158
+ 'type': 'text',
159
+ 'status': 'processing',
160
+ 'start_time': time.time()
161
+ }
162
+
163
+ # Process in thread pool
164
+ response = await asyncio.get_event_loop().run_in_executor(
165
+ self.text_executor,
166
+ self._make_api_request,
167
+ user_message
168
+ )
169
+
170
+ # Update with response
171
+ await processing_msg.edit_text(
172
+ f"βœ… Response for #{message_id}:\n{response}"
173
+ )
174
+
175
+ except Exception as e:
176
+ logging.error(f"Error processing text request: {e}")
177
+ await processing_msg.edit_text(
178
+ f"❌ Error processing request #{message_id}: {str(e)}"
179
+ )
180
+ finally:
181
+ if message_id in self.active_requests:
182
+ self.active_requests[message_id]['status'] = 'completed'
183
+ self.active_requests[message_id]['end_time'] = time.time()
184
+
185
+ def _make_api_request(self, user_message):
186
+ """Make API request"""
187
+ try:
188
+ headers = {
189
+ "Authorization": f"Bearer {HF_TOKEN}",
190
+ "accept": "application/json"
191
+ }
192
+
193
+ json_payload = {"question": user_message}
194
+ form_payload = {"question": user_message}
195
+
196
+ response = requests.post(
197
+ self.ai_url,
198
+ headers={**headers, "Content-Type": "application/json"},
199
+ json=json_payload
200
+ )
201
+
202
+ if response.status_code == 422:
203
+ response = requests.post(
204
+ self.ai_url,
205
+ headers={**headers, "Content-Type": "application/x-www-form-urlencoded"},
206
+ data=form_payload
207
+ )
208
+
209
+ if response.status_code == 200:
210
+ return response.json().get("answer", "I didn't understand that.")
211
+ else:
212
+ return f"Error: {response.status_code}"
213
+
214
+ except Exception as e:
215
+ return f"Connection error: {e}"
216
+
217
+ async def handle_excel(self, update: Update, context: CallbackContext):
218
+ """Handle Excel files concurrently"""
219
+ message_id = update.message.message_id
220
+
221
+ # Create task for processing
222
+ asyncio.create_task(self._process_excel_file(update, context, message_id))
223
+
224
+ async def _process_excel_file(self, update, context, message_id):
225
+ """Process Excel file with progress updates"""
226
+ try:
227
+ document = update.message.document
228
+
229
+ # Send initial processing message
230
+ processing_msg = await update.message.reply_text(
231
+ f"πŸ“Š Starting Excel processing...\n"
232
+ f"File: {document.file_name}\n"
233
+ f"Request ID: #{message_id}"
234
+ )
235
+
236
+ # Step 1: Download file - ADD THIS PART
237
+ try:
238
+ file = await context.bot.get_file(document.file_id)
239
+ file_bytes = await file.download_as_bytearray() # Define file_bytes here
240
+ except Exception as e:
241
+ logging.error(f"Error downloading file: {e}")
242
+ await processing_msg.edit_text(
243
+ f"❌ Error downloading file: {document.file_name}\n"
244
+ f"Please try again or contact support."
245
+ )
246
+ return
247
+
248
+ # Step 2: Update status to processing
249
+ self.active_excel_files[message_id] = {
250
+ 'filename': document.file_name,
251
+ 'status': 'processing',
252
+ 'start_time': time.time()
253
+ }
254
+ await processing_msg.edit_text(
255
+ f"βš™οΈ Processing Excel file...\n"
256
+ f"File: {document.file_name}\n"
257
+ f"Request ID: #{message_id}"
258
+ )
259
+
260
+ # Step 3: Process in thread pool
261
+ result = await asyncio.get_event_loop().run_in_executor(
262
+ self.excel_executor,
263
+ self._process_excel_sync,
264
+ file_bytes, # Now file_bytes is defined
265
+ document.file_name
266
+ )
267
+
268
+ if result is None:
269
+ await processing_msg.edit_text(
270
+ f"❌ Failed to process file\n"
271
+ f"File: {document.file_name}\n"
272
+ f"Please check the file format and try again."
273
+ )
274
+ return
275
+
276
+ # Send processed file
277
+ await context.bot.send_document(
278
+ chat_id=update.effective_chat.id,
279
+ document=io.BytesIO(result),
280
+ filename=f'processed_{document.file_name}',
281
+ caption=f"βœ… Excel processing completed!\nRequest ID: #{message_id}"
282
+ )
283
+ await processing_msg.delete()
284
+
285
+ except Exception as e:
286
+ logging.error(f"Error processing file: {e}")
287
+ await processing_msg.edit_text(
288
+ f"❌ Error processing file\n"
289
+ f"File: {document.file_name}\n"
290
+ f"Error: {str(e)}"
291
+ )
292
+ finally:
293
+ if message_id in self.active_excel_files:
294
+ self.active_excel_files[message_id]['status'] = 'completed'
295
+ self.active_excel_files[message_id]['end_time'] = time.time()
296
+
297
+ def _process_excel_sync(self, file_bytes, filename):
298
+ """Synchronous function to process Excel file"""
299
+ try:
300
+ headers = {
301
+ "Authorization": f"Bearer {HF_TOKEN}",
302
+ "accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
303
+ }
304
+
305
+ files = {
306
+ 'file': (
307
+ filename,
308
+ file_bytes,
309
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
310
+ )
311
+ }
312
+
313
+ response = requests.post(
314
+ self.excel_url,
315
+ headers=headers,
316
+ files=files
317
+ )
318
+
319
+ if response.status_code == 200:
320
+ return response.content
321
+ else:
322
+ logging.error(f"Excel API Error: {response.status_code} - {response.text}")
323
+ return None
324
+
325
+ except Exception as e:
326
+ logging.error(f"Excel processing error: {e}")
327
+ return None
328
+
329
+ async def _update_progress(self, message, message_id, filename):
330
+ """Update progress message periodically"""
331
+ try:
332
+ while message_id in self.active_excel_files:
333
+ elapsed_time = time.time() - self.active_excel_files[message_id]['start_time']
334
+ hours = int(elapsed_time // 3600)
335
+ minutes = int((elapsed_time % 3600) // 60)
336
+
337
+ await message.edit_text(
338
+ f"βš™οΈ Processing Excel file...\n"
339
+ f"File: {filename}\n"
340
+ f"Request ID: #{message_id}\n"
341
+ f"Time elapsed: {hours}h {minutes}m\n"
342
+ f"Status: {self.active_excel_files[message_id]['status']}"
343
+ )
344
+
345
+ # Update every 5 minutes
346
+ await asyncio.sleep(300)
347
+ except Exception as e:
348
+ logging.error(f"Error updating progress: {e}")
349
+
350
+ async def status_command(self, update: Update, context: CallbackContext):
351
+ """Show status of all active processes"""
352
+ status_message = "Current Status:\n\n"
353
+
354
+ # Text requests status
355
+ if self.active_requests:
356
+ status_message += "πŸ“ Text Requests:\n"
357
+ for msg_id, info in self.active_requests.items():
358
+ current_time = time.time()
359
+ processing_time = current_time - info['start_time']
360
+ status_message += (
361
+ f"Request #{msg_id}:\n"
362
+ f"β”œβ”€ Status: {info['status']}\n"
363
+ f"└─ Time: {processing_time:.1f}s\n\n"
364
+ )
365
+
366
+ # Excel files status
367
+ if self.active_excel_files:
368
+ status_message += "πŸ“Š Excel Files:\n"
369
+ for msg_id, info in self.active_excel_files.items():
370
+ current_time = time.time()
371
+ processing_time = current_time - info['start_time']
372
+ status_message += (
373
+ f"File #{msg_id}:\n"
374
+ f"β”œβ”€ Name: {info['filename']}\n"
375
+ f"β”œβ”€ Status: {info['status']}\n"
376
+ f"└─ Time: {processing_time:.1f}s\n\n"
377
+ )
378
+
379
+ if not self.active_requests and not self.active_excel_files:
380
+ status_message += "No active processes"
381
+
382
+ await update.message.reply_text(status_message)
383
+
384
+ def setup_handlers(self):
385
+ """Set up Telegram command and message handlers."""
386
+ self.app.add_handler(CommandHandler("start", self.start_command))
387
+ self.app.add_handler(CommandHandler("status", self.status_command))
388
+ self.app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
389
+ self.app.add_handler(MessageHandler(
390
+ filters.Document.FileExtension("xlsx") | filters.Document.FileExtension("xls"),
391
+ self.handle_excel
392
+ ))
393
+
394
+ async def run(self):
395
+ """Start the bot and listen for messages."""
396
+ logging.info("Starting Telegram bot...")
397
+ await self.app.initialize()
398
+ await self.app.start()
399
+ await self.app.updater.start_polling()
400
+
401
+ async def bot_stop(self):
402
+ """Stop the bot."""
403
+ logging.info("Stopping Telegram bot...")
404
+ if self.app.updater:
405
+ await self.app.updater.stop()
406
+ await self.app.stop()
407
+ await self.app.shutdown()
408
+
409
+ def init_bot():
410
+ return TelegramBot(bot_token=BOT_TOKEN,base_url=BASE_URL)
digram.io ADDED
File without changes
requirements.freeze.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ python-telegram-bot==20.7
2
+ requests==2.31.0
3
+ aiohttp==3.9.1
4
+ fastapi==0.105.0
5
+ uvicorn[standard]==0.24.0.post1
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ python-telegram-bot==20.7
2
+ requests==2.31.0
3
+ aiohttp==3.9.1
4
+ fastapi==0.105.0
5
+ uvicorn[standard]==0.24.0.post1