Charan5775 commited on
Commit
299980f
Β·
verified Β·
1 Parent(s): 6490e29

Create telegram_bot.py

Browse files
Files changed (1) hide show
  1. telegram_bot.py +581 -0
telegram_bot.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from pyrogram import Client, filters, enums
4
+ import requests
5
+ import tempfile
6
+ from urllib.parse import urljoin
7
+ from config import API_ID, API_HASH, BOT_TOKEN, API_BASE_URL
8
+ import time
9
+ import math
10
+ import asyncio
11
+ import re
12
+ from collections import defaultdict
13
+ from datetime import datetime, timedelta
14
+ import urllib3
15
+ from asyncio import Lock
16
+
17
+ # Enable logging
18
+ logging.basicConfig(
19
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
20
+ level=logging.INFO
21
+ )
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Disable SSL warnings
25
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
26
+
27
+ # Initialize the Pyrogram Client first
28
+ app = Client(
29
+ "file_sharing_bot",
30
+ api_id=API_ID,
31
+ api_hash=API_HASH,
32
+ bot_token=BOT_TOKEN,
33
+ in_memory=True
34
+ )
35
+
36
+ # Set timeout for requests
37
+ TIMEOUT = 30 # seconds
38
+
39
+ # User-specific progress tracking
40
+ user_progress = defaultdict(dict)
41
+
42
+ # Rate limiting per user
43
+ rate_limit = defaultdict(lambda: {'last_update': 0, 'count': 0})
44
+
45
+ # Add after other global variables
46
+ upload_locks = {}
47
+
48
+ class RateLimiter:
49
+ def __init__(self, interval=1):
50
+ self.interval = interval
51
+ self.last_check = defaultdict(float)
52
+
53
+ async def can_proceed(self, user_id):
54
+ now = time.time()
55
+ if now - self.last_check[user_id] < self.interval:
56
+ return False
57
+ self.last_check[user_id] = now
58
+ return True
59
+
60
+ rate_limiter = RateLimiter(interval=2)
61
+
62
+ async def progress(current, total, message, start_time, action="Uploading", user_id=None):
63
+ """Progress callback with per-user rate limiting"""
64
+ try:
65
+ if not user_id:
66
+ user_id = message.chat.id
67
+
68
+ now = time.time()
69
+
70
+ # Check rate limit for this user
71
+ if not await rate_limiter.can_proceed(user_id):
72
+ return
73
+
74
+ elapsed_time = now - start_time
75
+ if elapsed_time == 0:
76
+ return
77
+
78
+ # Store progress for this user
79
+ user_progress[user_id] = {
80
+ 'current': current,
81
+ 'total': total,
82
+ 'speed': current / elapsed_time,
83
+ 'progress': (current * 100) / total
84
+ }
85
+
86
+ progress_data = user_progress[user_id]
87
+
88
+ # Format progress bar
89
+ bar_length = 20
90
+ filled_length = int(progress_data['progress'] / 100 * bar_length)
91
+ bar = 'β–ˆ' * filled_length + 'β–‘' * (bar_length - filled_length)
92
+
93
+ # Calculate ETA
94
+ eta_seconds = (total - current) / progress_data['speed'] if progress_data['speed'] > 0 else 0
95
+
96
+ text = (
97
+ f"{action} File...\n"
98
+ f"[{bar}] {progress_data['progress']:.1f}%\n"
99
+ f"Size: {format_size(current)}/{format_size(total)}\n"
100
+ f"Speed: {format_size(progress_data['speed'])}/s\n"
101
+ f"ETA: {int(eta_seconds)}s"
102
+ )
103
+
104
+ try:
105
+ await message.edit_text(text)
106
+ except Exception as e:
107
+ logger.debug(f"Progress update failed: {e}")
108
+
109
+ except Exception as e:
110
+ logger.error(f"Progress error for user {user_id}: {e}")
111
+
112
+ # Add this class after other imports
113
+ class ProgressTracker:
114
+ def __init__(self, message, action="Uploading"):
115
+ self.message = message
116
+ self.action = action
117
+ self.start_time = time.time()
118
+ self.last_update_time = 0
119
+ self.edit_failed = False
120
+
121
+ async def update(self, current, total):
122
+ now = time.time()
123
+ # Update only every 1 second
124
+ if now - self.last_update_time < 1:
125
+ return
126
+
127
+ self.last_update_time = now
128
+ elapsed_time = now - self.start_time
129
+ if elapsed_time == 0:
130
+ return
131
+
132
+ speed = current / elapsed_time
133
+ progress_percent = (current * 100) / total
134
+
135
+ # Calculate ETA
136
+ remaining_bytes = total - current
137
+ eta_seconds = remaining_bytes / speed if speed > 0 else 0
138
+
139
+ # Format progress bar
140
+ bar_length = 20
141
+ filled_length = int(progress_percent / 100 * bar_length)
142
+ bar = 'β–ˆ' * filled_length + 'β–‘' * (bar_length - filled_length)
143
+
144
+ text = (
145
+ f"{self.action} to server...\n"
146
+ f"[{bar}] {progress_percent:.1f}%\n"
147
+ f"Size: {format_size(current)}/{format_size(total)}\n"
148
+ f"Speed: {format_size(speed)}/s\n"
149
+ f"ETA: {int(eta_seconds)}s"
150
+ )
151
+
152
+ # If upload is complete, update the message
153
+ if current >= total:
154
+ text = "βœ… Upload complete! Processing..."
155
+
156
+ try:
157
+ if not self.edit_failed:
158
+ try:
159
+ await self.message.edit_text(text)
160
+ except Exception as e:
161
+ logger.debug(f"Edit failed, switching to new messages: {e}")
162
+ self.edit_failed = True
163
+ # Delete old message
164
+ try:
165
+ await self.message.delete()
166
+ except:
167
+ pass
168
+ # Send new message
169
+ self.message = await self.message.reply_text(text)
170
+ else:
171
+ # Delete old message and send new one
172
+ try:
173
+ await self.message.delete()
174
+ except:
175
+ pass
176
+ self.message = await self.message.reply_text(text)
177
+
178
+ except Exception as e:
179
+ logger.debug(f"Progress update failed: {e}")
180
+
181
+ class ProgressFile:
182
+ def __init__(self, file, size, progress_callback):
183
+ self.file = file
184
+ self.size = size
185
+ self.progress_callback = progress_callback
186
+ self.uploaded = 0
187
+
188
+ def read(self, chunk_size=-1):
189
+ data = self.file.read(chunk_size)
190
+ if data:
191
+ self.uploaded += len(data)
192
+ asyncio.create_task(
193
+ self.progress_callback(self.uploaded, self.size)
194
+ )
195
+ return data
196
+
197
+ def seek(self, offset, whence=0):
198
+ return self.file.seek(offset, whence)
199
+
200
+ def tell(self):
201
+ return self.file.tell()
202
+
203
+ def close(self):
204
+ return self.file.close()
205
+
206
+ def fileno(self):
207
+ return self.file.fileno()
208
+
209
+ def readable(self):
210
+ return True
211
+
212
+ def seekable(self):
213
+ return True
214
+
215
+ def writable(self):
216
+ return False
217
+
218
+ # Modify document handler for concurrent processing
219
+ @app.on_message(filters.document)
220
+ async def handle_document(client, message):
221
+ user_id = message.from_user.id
222
+
223
+ # Check if user is already uploading
224
+ if user_id in upload_locks:
225
+ await message.reply_text("⚠️ Please wait for your current upload to finish.")
226
+ return
227
+
228
+ upload_locks[user_id] = Lock()
229
+
230
+ try:
231
+ async with upload_locks[user_id]:
232
+ status_msg = await message.reply_text("Starting file processing...")
233
+
234
+ # Create user-specific temp directory
235
+ user_temp_dir = os.path.join("temp", str(user_id))
236
+ os.makedirs(user_temp_dir, exist_ok=True)
237
+
238
+ # Get original filename
239
+ original_filename = message.document.file_name
240
+ safe_filename = sanitize_filename(original_filename)
241
+ file_path = os.path.join(user_temp_dir, safe_filename)
242
+
243
+ try:
244
+ # Download with user-specific progress tracking
245
+ start_time = time.time()
246
+ await message.download(
247
+ file_name=file_path,
248
+ progress=progress,
249
+ progress_args=(status_msg, start_time, "Downloading", user_id)
250
+ )
251
+
252
+ # Get file size for upload progress
253
+ file_size = os.path.getsize(file_path)
254
+ progress_tracker = ProgressTracker(status_msg, "Uploading")
255
+
256
+ with open(file_path, 'rb') as f:
257
+ # Create a wrapper for the file to track upload progress
258
+ progress_file = ProgressFile(f, file_size, progress_tracker.update)
259
+ files = {'file': (original_filename, progress_file)}
260
+
261
+ try:
262
+ response = requests.post(
263
+ f"{API_BASE_URL}/upload/",
264
+ files=files,
265
+ params={"user_id": str(user_id)},
266
+ timeout=60,
267
+ verify=False
268
+ )
269
+
270
+ if response.status_code == 200:
271
+ result = response.json()
272
+ file_url = urljoin(API_BASE_URL, result['access_code'])
273
+
274
+ # Send a new message instead of editing
275
+ await status_msg.delete()
276
+ await message.reply_text(
277
+ f"βœ… File uploaded successfully!\n\n"
278
+ f"πŸ“„ Filename: {original_filename}\n"
279
+ f"πŸ”‘ Access Code: <code>{result['access_code']}</code>\n"
280
+ f"πŸ”— Direct Link: {file_url}\n\n"
281
+ f"Anyone with this link can download the file.",
282
+ parse_mode=enums.ParseMode.HTML
283
+ )
284
+ else:
285
+ error_msg = response.json().get('detail', 'Unknown error')
286
+ await message.reply_text(f"❌ Upload failed: {error_msg}")
287
+
288
+ except Exception as upload_error:
289
+ logger.error(f"Upload error detail: {upload_error}")
290
+ await message.reply_text("❌ Upload failed at final stage. Please try again.")
291
+
292
+ except Exception as e:
293
+ logger.error(f"File handling error: {e}")
294
+ await message.reply_text("❌ Error processing file. Please try again.")
295
+
296
+ except Exception as e:
297
+ logger.error(f"Error for user {user_id}: {e}")
298
+ await message.reply_text("❌ Sorry, something went wrong. Please try again.")
299
+ finally:
300
+ # Cleanup user-specific temp files
301
+ try:
302
+ if os.path.exists(user_temp_dir):
303
+ for file in os.listdir(user_temp_dir):
304
+ os.remove(os.path.join(user_temp_dir, file))
305
+ os.rmdir(user_temp_dir)
306
+ except Exception as e:
307
+ logger.error(f"Cleanup error for user {user_id}: {e}")
308
+
309
+ # Remove the lock
310
+ if user_id in upload_locks:
311
+ del upload_locks[user_id]
312
+
313
+ def format_size(size):
314
+ """Format size in bytes to human readable format"""
315
+ units = ['B', 'KB', 'MB', 'GB']
316
+ size = float(size)
317
+ unit = 0
318
+ while size >= 1024 and unit < len(units) - 1:
319
+ size /= 1024
320
+ unit += 1
321
+ return f"{size:.2f} {units[unit]}"
322
+
323
+ @app.on_message(filters.command("start"))
324
+ async def start_command(client, message):
325
+ """Handle the /start command"""
326
+ await message.reply_text(
327
+ 'Hi! I can help you share files.\n'
328
+ 'Just send me any file and I will give you a link to share it.\n\n'
329
+ 'Commands:\n'
330
+ '/start - Show this help message\n'
331
+ '/list - List all uploaded files\n'
332
+ '/delete <code> - Delete a file using access code\n'
333
+ '/stats - View your usage statistics\n\n'
334
+ 'πŸ’‘ You can also send me an access code directly to get the file!'
335
+ )
336
+
337
+ @app.on_message(filters.command("list"))
338
+ async def list_command(client, message):
339
+ """Handle the /list command"""
340
+ try:
341
+ # Get user's Telegram ID
342
+ user_id = str(message.from_user.id)
343
+
344
+ # Get only this user's files
345
+ response = requests.get(f"{API_BASE_URL}/files/{user_id}")
346
+ files = response.json()
347
+
348
+ if not files['files']:
349
+ await message.reply_text("πŸ“‚ You haven't uploaded any files yet.")
350
+ return
351
+
352
+ async def send_long_message(text, parse_mode=None):
353
+ MAX_LENGTH = 4000
354
+ messages = []
355
+ current_msg = "πŸ“‚ Your Files:\n\n"
356
+
357
+ for line in text.split('\n'):
358
+ if len(current_msg + line + '\n') > MAX_LENGTH:
359
+ messages.append(current_msg)
360
+ current_msg = "πŸ“‚ Your Files (continued):\n\n" + line + '\n'
361
+ else:
362
+ current_msg += line + '\n'
363
+
364
+ if current_msg:
365
+ messages.append(current_msg)
366
+
367
+ for i, msg_text in enumerate(messages, 1):
368
+ if len(messages) > 1:
369
+ msg_text += f"\nπŸ“ƒ Page {i}/{len(messages)}"
370
+ await message.reply_text(msg_text, parse_mode=parse_mode)
371
+
372
+ # Prepare combined message with better formatting
373
+ files_msg = ""
374
+ for i, file in enumerate(files['files'], 1):
375
+ file_url = urljoin(API_BASE_URL, file['access_code'])
376
+ files_msg += f"<b>{i}. {file['filename']}</b>\n"
377
+ files_msg += f" β”œβ”€ πŸ”— <a href='{file_url}'>Direct Link</a>\n"
378
+ files_msg += f" └─ πŸ”‘ Code: <code>{file['access_code']}</code>\n\n"
379
+
380
+ # Send message with HTML formatting
381
+ await send_long_message(files_msg, parse_mode=enums.ParseMode.HTML)
382
+
383
+ except Exception as e:
384
+ logger.error(f"Error listing files: {e}")
385
+ await message.reply_text("❌ Sorry, couldn't fetch your files.")
386
+
387
+ def sanitize_filename(filename):
388
+ """Remove invalid characters from filename"""
389
+ # Remove invalid characters
390
+ filename = re.sub(r'[<>:"/\\|?*]', '', filename)
391
+ # Remove any leading/trailing spaces and dots
392
+ filename = filename.strip('. ')
393
+ # If filename is empty after sanitization, use a default name
394
+ if not filename:
395
+ filename = 'downloaded_file'
396
+ return filename
397
+
398
+ @app.on_message(filters.command("delete"))
399
+ async def delete_command(client, message):
400
+ """Handle the /delete command"""
401
+ try:
402
+ # Check if access code is provided
403
+ command_parts = message.text.split()
404
+ if len(command_parts) != 2:
405
+ await message.reply_text(
406
+ "❌ Please provide an access code.\n"
407
+ "Usage: /delete <access_code>"
408
+ )
409
+ return
410
+
411
+ user_id = str(message.from_user.id)
412
+ access_code = command_parts[1]
413
+
414
+ # Try to delete the file
415
+ response = requests.delete(
416
+ f"{API_BASE_URL}/delete/{access_code}",
417
+ params={"user_id": user_id}, # Add user_id to verify ownership
418
+ timeout=TIMEOUT,
419
+ verify=False
420
+ )
421
+
422
+ if response.status_code == 200:
423
+ await message.reply_text("βœ… File deleted successfully!")
424
+ else:
425
+ error_msg = response.json().get('detail', 'Unknown error')
426
+ await message.reply_text(f"❌ Error: {error_msg}")
427
+
428
+ except Exception as e:
429
+ logger.error(f"Error deleting file: {e}")
430
+ await message.reply_text("❌ Sorry, couldn't delete the file.")
431
+
432
+ @app.on_message(filters.command(["stats", "stats@your_bot_username"])) # Add your bot's username
433
+ async def stats_command(client, message):
434
+ """Handle the /stats command"""
435
+ print("stats command received") # Debug print
436
+
437
+ # Check if it's a private chat
438
+ if message.chat.type != enums.ChatType.PRIVATE:
439
+ await message.reply_text("Please use this command in private chat.")
440
+ return
441
+
442
+ try:
443
+ user_id = str(message.from_user.id)
444
+ print(f"Processing stats for user_id: {user_id}") # Debug print
445
+
446
+ # Get stats from server
447
+ response = requests.get(
448
+ f"{API_BASE_URL}/stats/{user_id}",
449
+ timeout=TIMEOUT,
450
+ verify=False
451
+ )
452
+ print(f"Server response: {response.text}") # Debug print
453
+
454
+ if response.status_code == 200:
455
+ stats = response.json()
456
+
457
+ # Format the statistics message with better error handling
458
+ uploads = stats.get('uploads', 0)
459
+ downloads = stats.get('downloads', 0)
460
+ bytes_uploaded = stats.get('bytes_uploaded', 0)
461
+ bytes_downloaded = stats.get('bytes_downloaded', 0)
462
+ last_activity = stats.get('last_activity', 'No activity')
463
+
464
+ stats_msg = (
465
+ "πŸ“Š Your File Sharing Statistics\n\n"
466
+ f"πŸ“€ Uploads: {uploads}\n"
467
+ f"πŸ“₯ Downloads: {downloads}\n"
468
+ f"πŸ“ˆ Total Uploaded: {format_size(bytes_uploaded)}\n"
469
+ f"πŸ“‰ Total Downloaded: {format_size(bytes_downloaded)}\n"
470
+ f"πŸ•’ Last Activity: {last_activity}"
471
+ )
472
+
473
+ await message.reply_text(stats_msg)
474
+ else:
475
+ logger.error(f"Stats error: {response.text}") # Add error logging
476
+ await message.reply_text("❌ Couldn't fetch your statistics.")
477
+
478
+ except Exception as e:
479
+ logger.error(f"Error fetching stats: {e}")
480
+ await message.reply_text("❌ Sorry, something went wrong while fetching your statistics.")
481
+
482
+ @app.on_message(filters.text & filters.private & ~filters.via_bot & ~filters.forwarded)
483
+ async def handle_text(client, message):
484
+ """Handle text messages as potential access codes"""
485
+ if message.text.startswith('/'):
486
+ return
487
+
488
+ try:
489
+ user_id = str(message.from_user.id)
490
+ access_code = message.text.strip()
491
+ if len(access_code) != 8:
492
+ return
493
+
494
+ status_msg = await message.reply_text("πŸ” Fetching file...")
495
+
496
+ response = requests.get(
497
+ f"{API_BASE_URL}/download/{access_code}",
498
+ stream=True,
499
+ timeout=TIMEOUT
500
+ )
501
+
502
+ if response.status_code == 200:
503
+ # Get and sanitize filename from headers
504
+ content_disposition = response.headers.get('content-disposition', '')
505
+ if 'filename=' in content_disposition:
506
+ filename = content_disposition.split('filename=')[-1].strip('"\'')
507
+ # URL decode the filename
508
+ filename = requests.utils.unquote(filename)
509
+ else:
510
+ filename = f"file_{access_code}"
511
+
512
+ # Sanitize the filename
513
+ filename = sanitize_filename(filename)
514
+
515
+ # Save file temporarily
516
+ temp_path = os.path.join("temp", filename)
517
+ os.makedirs("temp", exist_ok=True)
518
+
519
+ try:
520
+ #Download file with progress
521
+ total_size = int(response.headers.get('content-length', 0))
522
+ current_size = 0
523
+ start_time = time.time()
524
+
525
+ with open(temp_path, 'wb') as f:
526
+ for chunk in response.iter_content(chunk_size=8192):
527
+ if chunk:
528
+ f.write(chunk)
529
+ current_size += len(chunk)
530
+ await progress(
531
+ current_size,
532
+ total_size,
533
+ status_msg,
534
+ start_time,
535
+ "Downloading"
536
+ )
537
+
538
+ # Send file to user
539
+ await status_msg.edit_text("πŸ“€ Sending file to you...")
540
+ await message.reply_document(
541
+ temp_path,
542
+ caption=f"πŸ“„ File fetched using access code: {access_code}"
543
+ )
544
+ await status_msg.delete()
545
+
546
+ # Log the download
547
+ file_size = int(response.headers.get('content-length', 0))
548
+ requests.post(
549
+ f"{API_BASE_URL}/log_download",
550
+ json={
551
+ "user_id": user_id,
552
+ "file_size": file_size,
553
+ "filename": filename
554
+ },
555
+ timeout=TIMEOUT
556
+ )
557
+
558
+ finally:
559
+ # Clean up
560
+ try:
561
+ os.remove(temp_path)
562
+ except:
563
+ pass
564
+
565
+ else:
566
+ error_msg = response.json().get('detail', 'Unknown error')
567
+ await status_msg.edit_text(f"❌ Invalid access code or file not found")
568
+
569
+ except Exception as e:
570
+ logger.error(f"Error fetching file: {e}")
571
+ await message.reply_text("❌ Sorry, something went wrong while fetching the file.")
572
+
573
+
574
+
575
+ def main():
576
+ """Start the bot"""
577
+ print("Starting bot...")
578
+ app.run()
579
+
580
+ if __name__ == '__main__':
581
+ main()