akdNIKY commited on
Commit
1b3dbce
·
verified ·
1 Parent(s): d57e440

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -200
app.py CHANGED
@@ -5,6 +5,7 @@ import re
5
  import subprocess
6
  import json
7
  from io import BytesIO
 
8
 
9
  from flask import Flask, request, jsonify
10
 
@@ -23,7 +24,6 @@ from pydub import AudioSegment
23
  # --- 1. تنظیمات و متغیرهای سراسری ---
24
 
25
  # ⚠️⚠️⚠️ بسیار مهم: این توکن را با توکن واقعی ربات خود که از BotFather دریافت کرده‌اید جایگزین کنید.
26
- # می‌توانید توکن را به عنوان یک متغیر محیطی (Environment Variable) به نام TELEGRAM_BOT_TOKEN نیز تعریف کنید.
27
  TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8035336072:AAHG9REotvM4u8DgreC7hu8gIAroxhS-N-M")
28
  if "YOUR_TELEGRAM_BOT_TOKEN_HERE" in TOKEN:
29
  print("خطا: لطفا توکن ربات تلگرام خود را در کد جایگزین کنید.", file=sys.stderr)
@@ -35,17 +35,15 @@ BOT_USERNAME = os.getenv("TELEGRAM_BOT_USERNAME", "Voice2mp3_RoBot")
35
  ADMIN_ID = int(os.getenv("TELEGRAM_ADMIN_ID", "684173337"))
36
 
37
  # مسیرها
38
- # در محیط‌های سرورلس مانند Hugging Face Spaces، استفاده از /tmp توصیه می‌شود
39
  DOWNLOAD_DIR = "/tmp/downloads"
40
  OUTPUT_DIR = "/tmp/outputs"
41
  CHANNELS_FILE = "/tmp/channels.json"
42
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
43
  os.makedirs(OUTPUT_DIR, exist_ok=True)
44
 
45
- # --- 2. توابع مدیریت کانال‌های عضویت اجباری ---
46
 
47
  def load_required_channels():
48
- """کانال‌های مورد نیاز را از فایل JSON بارگذاری می‌کند."""
49
  if os.path.exists(CHANNELS_FILE):
50
  try:
51
  with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
@@ -56,21 +54,16 @@ def load_required_channels():
56
  return []
57
 
58
  def save_required_channels(channels):
59
- """کانال‌های مورد نیاز را در فایل JSON ذخیره می‌کند."""
60
  with open(CHANNELS_FILE, 'w', encoding='utf-8') as f:
61
  json.dump(channels, f, indent=4, ensure_ascii=False)
62
 
63
  REQUIRED_CHANNELS = load_required_channels()
64
 
65
-
66
  # --- 3. تعریف حالت‌های مکالمه و پیام‌ها ---
67
-
68
- # تعریف حالت‌ها
69
  (LANGUAGE_SELECTION, MAIN_MENU, CONVERT_AUDIO, CUT_AUDIO_FILE, CUT_AUDIO_RANGE,
70
  VIDEO_CONVERSION_MODE, WAITING_FOR_MEMBERSHIP, ADMIN_MENU, ADD_CHANNEL,
71
  LIST_REMOVE_CHANNELS) = range(10)
72
 
73
- # دیکشنری پیام‌ها
74
  MESSAGES = {
75
  'fa': {
76
  'start_welcome': "سلام! من یک ربات تبدیل فرمت صوتی و ویدیویی هستم.\n\nبرای شروع، از منوی زیر یک قابلیت را انتخاب کنید.",
@@ -127,123 +120,55 @@ MESSAGES = {
127
  'bot_not_admin_in_channel': "ربات ادمین کانال '{channel_id}' نیست یا مجوزهای کافی برای بررسی عضویت را ندارد. لطفاً ربات را به عنوان ادمین با مجوز 'بررسی وضعیت اعضا' در کانال اضافه کنید."
128
  },
129
  'en': {
130
- 'start_welcome': "Hello! I am an audio and video format conversion bot.\n\nTo start, select a feature from the menu below.",
131
- 'choose_language': "Choose your preferred language:",
132
- 'processing_start': "⏳ Starting processing...",
133
- 'file_received': "⬇️ File received. Processing...",
134
- 'conversion_done': "⚙️ Conversion complete. Sending...",
135
- 'mp3_to_voice_reply': "Your Telegram voice (converted from MP3)",
136
- 'voice_to_mp3_caption': "Your MP3 file (converted from Telegram voice)",
137
- 'error_mp3_to_voice': "❌ Error converting MP3 to Telegram voice: ",
138
- 'error_voice_to_mp3': "❌ Error converting Telegram voice to MP3: ",
139
- 'general_error': "Sorry, something went wrong. Please try again.",
140
- 'main_menu_prompt': "What would you like to do with your file?",
141
- 'btn_convert_format': "Change Audio Format 🎵",
142
- 'btn_cut_audio': "Cut Part of Audio ✂️",
143
- 'btn_video_conversion': "Convert Circular Video 🎥",
144
- 'convert_mode_active': "You are now in 'Change Audio Format' mode. Send me your audio file (voice or MP3).",
145
- 'cut_mode_active_file': "You are in the 'Cut Audio' section.\n\nFirst, send your audio file (MP3 or voice).",
146
- 'cut_mode_active_range': "Now send the desired time range for cutting in 'MM.SS-MM.SS' format (example: 00.21-00.54).",
147
- 'invalid_time_format': "Invalid time format. Please use 'MM.SS-MM.SS' format. (example: 00.21-00.54)",
148
- 'invalid_time_range': "Invalid time range or end time is less than start time. Please enter a valid range.",
149
- 'audio_cut_success': "✅ Audio cut successfully. Your file is ready.",
150
- 'no_audio_for_cut': "No audio file found for cutting. Please send the audio file first.",
151
- 'cut_processing': "✂️ Cutting audio...",
152
- 'returning_to_main_menu': "Returning to main menu...",
153
- 'cancel_message': "Operation cancelled. Returned to main menu.",
154
- 'video_conversion_mode_active': "You are in 'Circular Video Conversion' mode.\n\nSend me a regular video or a circular video message (Video Message).",
155
- 'file_received_video': "⬇️ Video file received. Processing...",
156
- 'converting_video_note_to_video': "🔄 Converting circular video to regular video...",
157
- 'converting_video_to_video_note': "🔄 Converting regular video to circular video...",
158
- 'conversion_done_video': "✅ Video conversion successful. Sending...",
159
- 'video_note_to_video_caption': "Your regular video (converted from circular video)",
160
- 'video_to_video_note_reply': "Your circular video (converted from regular video)",
161
- 'error_video_conversion': "❌ Error converting video: ",
162
- 'invalid_file_type_video': "Please send a video file or a video message.",
163
- 'membership_required': "To continue using the bot and access unlimited features, please join the following channels first:",
164
- 'btn_join_channel': "Join Channel 🤝",
165
- 'btn_check_membership': "Check Membership ✅",
166
- 'membership_success': "✅ Your membership has been verified! You can now use the bot unlimitedly.",
167
- 'membership_failed': "❌ Sorry, you are not yet a member of all required channels. Please join first and then press 'Check Membership' again.",
168
- 'not_admin': "You do not have permission to access this section.",
169
- 'admin_menu_prompt': "Welcome to the link management panel:",
170
- 'btn_add_channel': "Add Channel Link ➕",
171
- 'btn_list_channels': "List Channels & Remove 🗑️",
172
- 'send_channel_link': "Please send the channel link (e.g., @mychannel) or numeric ID:",
173
- 'channel_added': "✅ Channel '{channel_id}' successfully added.",
174
- 'channel_already_exists': "❗️ This channel has already been added.",
175
- 'no_channels_configured': "No channels configured for membership.",
176
- 'channel_list_prompt': "Current list of channels for mandatory membership:",
177
- 'btn_remove_channel': "Remove ❌",
178
- 'channel_removed': "✅ Channel '{channel_id}' successfully removed.",
179
- 'channel_not_found': "❗️ Channel not found.",
180
- 'invalid_channel_id': "Invalid channel ID/link. Please send @username or numeric ID (e.g., -1001234567890).",
181
- 'bot_not_admin_in_channel': "The bot is not an admin in channel '{channel_id}' or does not have sufficient permissions to check membership. Please add the bot as an admin with 'Check members' permission in the channel."
182
  }
183
  }
184
 
185
- # --- 4. تمام توابع کمکی و منطقی ربات ---
186
 
187
  def get_message(context, key, **kwargs):
188
- """پیام مناسب را بر اساس زبان کاربر برمی‌گرداند."""
189
  lang = context.user_data.get('language', 'fa')
190
- # Fallback to Persian if language or key is not found
191
  message_template = MESSAGES.get(lang, MESSAGES['fa']).get(key, MESSAGES['fa'].get(key, "Message key not found"))
192
  return message_template.format(**kwargs)
193
 
194
  def parse_time_to_ms(time_str):
195
- """رشته 'MM.SS' را به میلی‌ثانیه تبدیل می‌کند."""
196
  match = re.match(r'^(\d{2})\.(\d{2})$', time_str)
197
- if not match:
198
- raise ValueError("Invalid time format")
199
  minutes, seconds = int(match.group(1)), int(match.group(2))
200
- if seconds >= 60:
201
- raise ValueError("Seconds must be between 00 and 59")
202
  return (minutes * 60 + seconds) * 1000
203
 
204
  async def check_user_membership(update: Update, context):
205
- """عضویت کاربر در کانال‌های ضروری را بررسی می‌کند."""
206
  user_id = update.effective_user.id
207
- if not REQUIRED_CHANNELS:
208
- return True
209
  for channel_id in REQUIRED_CHANNELS:
210
  try:
211
  chat_member = await context.bot.get_chat_member(chat_id=channel_id, user_id=user_id)
212
- if chat_member.status not in ['member', 'administrator', 'creator']:
213
- return False
214
  except Exception as e:
215
  print(f"خطا در بررسی عضویت کانال {channel_id}: {e}", file=sys.stderr)
216
  return False
217
  return True
218
 
219
  async def show_membership_required_message(update: Update, context):
220
- """پیام درخواست عضویت در کانال را نمایش می‌دهد."""
221
  keyboard = []
222
- if not REQUIRED_CHANNELS:
223
- return await show_main_menu(update, context)
224
-
225
  for channel_id in REQUIRED_CHANNELS:
226
  try:
227
  chat = await context.bot.get_chat(chat_id=channel_id)
228
  url = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
229
- if url:
230
- keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_join_channel')} {chat.title or channel_id}", url=url)])
231
  except Exception as e:
232
  print(f"ناتوان در دریافت اطلاعات کانال {channel_id}: {e}", file=sys.stderr)
233
- keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_join_channel')} {channel_id}", callback_data=f"no_link_{channel_id}")])
234
-
235
  keyboard.append([InlineKeyboardButton(get_message(context, 'btn_check_membership'), callback_data='check_membership')])
236
  reply_markup = InlineKeyboardMarkup(keyboard)
237
-
238
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.effective_message.reply_text
239
  await message_sender(get_message(context, 'membership_required'), reply_markup=reply_markup)
240
  return WAITING_FOR_MEMBERSHIP
241
 
242
  async def process_feature_or_check_membership(update: Update, context, feature_func, *args, **kwargs):
243
- """یک میان‌افزار برای بررسی عضویت قبل از اجرای قابلیت‌ها."""
244
  if update.effective_user.id == ADMIN_ID or not REQUIRED_CHANNELS:
245
  return await feature_func(update, context, *args, **kwargs)
246
-
247
  is_member = await check_user_membership(update, context)
248
  if is_member:
249
  context.user_data['is_member'] = True
@@ -252,23 +177,15 @@ async def process_feature_or_check_membership(update: Update, context, feature_f
252
  context.user_data['is_member'] = False
253
  return await show_membership_required_message(update, context)
254
 
255
- # --- 5. تمام توابع Handler (مدیریت دستورات و پیام‌ها) ---
256
 
257
  async def start(update: Update, context):
258
- """دستور /start را مدیریت کرده و درخواست انتخاب زبان می‌کند."""
259
- keyboard = [
260
- [InlineKeyboardButton("فارسی 🇮🇷", callback_data='set_lang_fa')],
261
- [InlineKeyboardButton("English 🇬🇧", callback_data='set_lang_en')]
262
- ]
263
  reply_markup = InlineKeyboardMarkup(keyboard)
264
- await update.message.reply_text(
265
- "زبان مورد نظر خود را انتخاب کنید:\nChoose your preferred language:",
266
- reply_markup=reply_markup
267
- )
268
  return LANGUAGE_SELECTION
269
 
270
  async def set_language(update: Update, context):
271
- """زبان کاربر را تنظیم کرده و به منوی اصلی می‌رود."""
272
  query = update.callback_query
273
  await query.answer()
274
  context.user_data['language'] = query.data.replace('set_lang_', '')
@@ -276,14 +193,12 @@ async def set_language(update: Update, context):
276
  return await show_main_menu(update, context)
277
 
278
  async def show_main_menu(update: Update, context):
279
- """منوی اصلی ربات را نمایش می‌دهد."""
280
  keyboard = [
281
  [InlineKeyboardButton(get_message(context, 'btn_convert_format'), callback_data='select_convert_format')],
282
  [InlineKeyboardButton(get_message(context, 'btn_cut_audio'), callback_data='select_cut_audio')],
283
  [InlineKeyboardButton(get_message(context, 'btn_video_conversion'), callback_data='select_video_conversion')]
284
  ]
285
  reply_markup = InlineKeyboardMarkup(keyboard)
286
-
287
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.message.reply_text
288
  await message_sender(text=get_message(context, 'main_menu_prompt'), reply_markup=reply_markup)
289
  return MAIN_MENU
@@ -308,28 +223,22 @@ async def video_conversion_selected(update: Update, context):
308
  return VIDEO_CONVERSION_MODE
309
 
310
  async def handle_audio(update: Update, context):
311
- """فایل MP3 را به ویس تلگرام تبدیل می‌کند."""
312
  async def _perform_conversion(update, context):
313
  file_id = update.message.audio.file_id
314
- file_name = update.message.audio.file_name or f"audio_{file_id}.mp3"
315
  processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
316
  download_path = os.path.join(DOWNLOAD_DIR, f"in_{file_id}.mp3")
317
  output_ogg_path = os.path.join(OUTPUT_DIR, f"out_{file_id}.ogg")
318
-
319
  try:
320
  new_file = await context.bot.get_file(file_id)
321
  await new_file.download_to_drive(download_path)
322
  await processing_message.edit_text(get_message(context, 'file_received'))
323
-
324
  audio = AudioSegment.from_file(download_path)
325
  audio.export(output_ogg_path, format="ogg", codec="libopus", parameters=["-b:a", "32k"])
326
  await processing_message.edit_text(get_message(context, 'conversion_done'))
327
-
328
  with open(output_ogg_path, 'rb') as f:
329
- await update.message.reply_voice(f, reply_to_message_id=update.message.message_id)
330
  await processing_message.delete()
331
  except Exception as e:
332
- print(f"خطا در تبدیل MP3 به ویس: {e}", file=sys.stderr)
333
  await processing_message.edit_text(get_message(context, 'error_mp3_to_voice') + str(e))
334
  finally:
335
  if os.path.exists(download_path): os.remove(download_path)
@@ -338,27 +247,22 @@ async def handle_audio(update: Update, context):
338
  return await process_feature_or_check_membership(update, context, _perform_conversion)
339
 
340
  async def handle_voice(update: Update, context):
341
- """ویس تلگرام را به فایل MP3 تبدیل می‌کند."""
342
  async def _perform_conversion(update, context):
343
  file_id = update.message.voice.file_id
344
  processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
345
  download_path = os.path.join(DOWNLOAD_DIR, f"in_{file_id}.ogg")
346
  output_mp3_path = os.path.join(OUTPUT_DIR, f"@{BOT_USERNAME}_{file_id}.mp3")
347
-
348
  try:
349
  new_file = await context.bot.get_file(file_id)
350
  await new_file.download_to_drive(download_path)
351
  await processing_message.edit_text(get_message(context, 'file_received'))
352
-
353
  audio = AudioSegment.from_file(download_path, format="ogg")
354
  audio.export(output_mp3_path, format="mp3", tags={'album': BOT_USERNAME, 'artist': BOT_USERNAME})
355
  await processing_message.edit_text(get_message(context, 'conversion_done'))
356
-
357
  with open(output_mp3_path, 'rb') as f:
358
- await update.message.reply_audio(f, caption=get_message(context, 'voice_to_mp3_caption'), reply_to_message_id=update.message.message_id)
359
  await processing_message.delete()
360
  except Exception as e:
361
- print(f"خطا در تبدیل ویس به MP3: {e}", file=sys.stderr)
362
  await processing_message.edit_text(get_message(context, 'error_voice_to_mp3') + str(e))
363
  finally:
364
  if os.path.exists(download_path): os.remove(download_path)
@@ -367,13 +271,11 @@ async def handle_voice(update: Update, context):
367
  return await process_feature_or_check_membership(update, context, _perform_conversion)
368
 
369
  async def handle_cut_audio_file(update: Update, context):
370
- """فایل صوتی برای برش را دریافت می‌کند."""
371
  async def _perform_file_receive(update, context):
372
  audio_file = update.message.audio or update.message.voice
373
  file_id = audio_file.file_id
374
  ext = 'mp3' if update.message.audio else 'ogg'
375
  download_path = os.path.join(DOWNLOAD_DIR, f"cut_in_{file_id}.{ext}")
376
-
377
  try:
378
  new_file = await context.bot.get_file(file_id)
379
  await new_file.download_to_drive(download_path)
@@ -382,38 +284,30 @@ async def handle_cut_audio_file(update: Update, context):
382
  await update.message.reply_text(get_message(context, 'cut_mode_active_range'))
383
  return CUT_AUDIO_RANGE
384
  except Exception as e:
385
- print(f"خطا در دریافت فایل صوتی برای برش: {e}", file=sys.stderr)
386
  await update.message.reply_text(get_message(context, 'general_error'))
387
  return CUT_AUDIO_FILE
388
  return await process_feature_or_check_membership(update, context, _perform_file_receive)
389
 
390
  async def handle_cut_audio_range(update: Update, context):
391
- """بازه زمانی را دریافت کرده و صدا را برش می‌دهد."""
392
  async def _perform_cut(update, context):
393
  time_range_str = update.message.text
394
  audio_path = context.user_data.get('audio_for_cut_path')
395
  audio_type = context.user_data.get('audio_for_cut_type')
396
-
397
  if not audio_path or not os.path.exists(audio_path):
398
  await update.message.reply_text(get_message(context, 'no_audio_for_cut'))
399
  return CUT_AUDIO_FILE
400
-
401
  processing_message = await update.message.reply_text(get_message(context, 'cut_processing'))
402
  output_cut_path = os.path.join(OUTPUT_DIR, f"cut_out_{os.path.basename(audio_path)}.mp3")
403
-
404
  try:
405
  start_time_str, end_time_str = time_range_str.split('-')
406
  start_ms = parse_time_to_ms(start_time_str.strip())
407
  end_ms = parse_time_to_ms(end_time_str.strip())
408
-
409
  if start_ms >= end_ms:
410
  await processing_message.edit_text(get_message(context, 'invalid_time_range'))
411
  return CUT_AUDIO_RANGE
412
-
413
  audio = AudioSegment.from_file(audio_path, format=audio_type)
414
  cut_audio = audio[start_ms:end_ms]
415
  cut_audio.export(output_cut_path, format="mp3")
416
-
417
  await processing_message.edit_text(get_message(context, 'audio_cut_success'))
418
  with open(output_cut_path, 'rb') as f:
419
  await update.message.reply_audio(f, caption=f"برش از {start_time_str} تا {end_time_str}")
@@ -423,7 +317,6 @@ async def handle_cut_audio_range(update: Update, context):
423
  await processing_message.edit_text(get_message(context, 'invalid_time_format'))
424
  return CUT_AUDIO_RANGE
425
  except Exception as e:
426
- print(f"خطا در برش صدا: {e}", file=sys.stderr)
427
  await processing_message.edit_text(get_message(context, 'general_error'))
428
  return await show_main_menu(update, context)
429
  finally:
@@ -434,31 +327,25 @@ async def handle_cut_audio_range(update: Update, context):
434
  return await process_feature_or_check_membership(update, context, _perform_cut)
435
 
436
  async def handle_video_conversion(update: Update, context):
437
- """تبدیل بین ویدیو معمولی و دایره‌ای را انجام می‌دهد."""
438
  async def _perform_video_conversion(update, context):
439
  is_video_note = bool(update.message.video_note)
440
  file_to_process = update.message.video_note if is_video_note else update.message.video
441
-
442
  file_id = file_to_process.file_id
443
  download_path = os.path.join(DOWNLOAD_DIR, f"vid_in_{file_id}.mp4")
444
  output_path = os.path.join(OUTPUT_DIR, f"vid_out_{file_id}.mp4")
445
  processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
446
-
447
  try:
448
  new_file = await context.bot.get_file(file_id)
449
  await new_file.download_to_drive(download_path)
450
  await processing_message.edit_text(get_message(context, 'file_received_video'))
451
-
452
  if is_video_note:
453
  await processing_message.edit_text(get_message(context, 'converting_video_note_to_video'))
454
- ffmpeg_command = ['ffmpeg', '-i', download_path, '-c:v', 'libx264', '-crf', '23', '-preset', 'medium', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', output_path]
455
  else:
456
  await processing_message.edit_text(get_message(context, 'converting_video_to_video_note'))
457
- ffmpeg_command = ['ffmpeg', '-i', download_path, '-vf', 'crop=min(iw\,ih):min(iw\,ih),scale=640:640', '-c:v', 'libx264', '-crf', '28', '-preset', 'veryfast', '-an', output_path]
458
-
459
- subprocess.run(ffmpeg_command, check=True, capture_output=True)
460
  await processing_message.edit_text(get_message(context, 'conversion_done_video'))
461
-
462
  with open(output_path, 'rb') as f:
463
  if is_video_note:
464
  await update.message.reply_video(f, caption=get_message(context, 'video_note_to_video_caption'))
@@ -466,10 +353,8 @@ async def handle_video_conversion(update: Update, context):
466
  await update.message.reply_video_note(f)
467
  await processing_message.delete()
468
  except subprocess.CalledProcessError as e:
469
- print(f"خطای FFmpeg: {e.stderr.decode()}", file=sys.stderr)
470
- await processing_message.edit_text(get_message(context, 'error_video_conversion') + "خطای FFmpeg")
471
  except Exception as e:
472
- print(f"خطای کلی تبدیل ویدیو: {e}", file=sys.stderr)
473
  await processing_message.edit_text(get_message(context, 'error_video_conversion') + str(e))
474
  finally:
475
  if os.path.exists(download_path): os.remove(download_path)
@@ -478,7 +363,6 @@ async def handle_video_conversion(update: Update, context):
478
  return await process_feature_or_check_membership(update, context, _perform_video_conversion)
479
 
480
  async def check_membership_callback(update: Update, context):
481
- """دکمه 'بررسی عضویت' را مدیریت می‌کند."""
482
  query = update.callback_query
483
  await query.answer()
484
  is_member = await check_user_membership(update, context)
@@ -491,7 +375,6 @@ async def check_membership_callback(update: Update, context):
491
  return WAITING_FOR_MEMBERSHIP
492
 
493
  async def admin_link_command(update: Update, context):
494
- """دستور /link برای ادمین."""
495
  if update.effective_user.id != ADMIN_ID:
496
  await update.message.reply_text(get_message(context, 'not_admin'))
497
  return ConversationHandler.END
@@ -509,13 +392,12 @@ async def admin_add_channel_prompt(update: Update, context):
509
  return ADD_CHANNEL
510
 
511
  async def admin_handle_add_channel(update: Update, context):
512
- """کانال جدی�� را از ادمین دریافت و اضافه می‌کند."""
513
  channel_input = update.message.text.strip()
514
  if not (channel_input.startswith('@') or channel_input.startswith('-100')):
515
  await update.message.reply_text(get_message(context, 'invalid_channel_id'))
516
  return ADD_CHANNEL
517
  try:
518
- await context.bot.get_chat(channel_input) # Check if bot can access the channel
519
  if channel_input not in REQUIRED_CHANNELS:
520
  REQUIRED_CHANNELS.append(channel_input)
521
  save_required_channels(REQUIRED_CHANNELS)
@@ -527,7 +409,6 @@ async def admin_handle_add_channel(update: Update, context):
527
  return await admin_link_command(update, context)
528
 
529
  async def admin_list_channels(update: Update, context):
530
- """لیست کانال‌ها را با دکمه حذف نمایش می‌دهد."""
531
  query = update.callback_query
532
  await query.answer()
533
  if not REQUIRED_CHANNELS:
@@ -540,7 +421,6 @@ async def admin_list_channels(update: Update, context):
540
  return LIST_REMOVE_CHANNELS
541
 
542
  async def admin_handle_remove_channel(update: Update, context):
543
- """یک کانال را از لیست حذف می‌کند."""
544
  query = update.callback_query
545
  await query.answer()
546
  channel_id_to_remove = query.data.replace('remove_channel_', '')
@@ -553,99 +433,119 @@ async def admin_handle_remove_channel(update: Update, context):
553
  return await admin_link_command(update, context)
554
 
555
  async def cancel(update: Update, context):
556
- """عملیات را لغو کرده و به منوی اصلی برمی‌گردد."""
557
  message = update.message or update.callback_query.message
558
  await message.reply_text(get_message(context, 'cancel_message'))
559
  context.user_data.clear()
560
  return await show_main_menu(update, context)
561
 
562
  async def error_handler(update: object, context: object) -> None:
563
- """لاگ کردن خطاها."""
564
  print(f"Update {update} caused error {context.error}", file=sys.stderr)
565
 
566
- # --- 6. بخش مربوط به Flask و Webhook ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
 
568
  app = Flask(__name__)
569
- _application_instance = None
570
-
571
- async def get_telegram_application():
572
- """اپلیکیشن تلگرام را مقداردهی اولیه کرده و ConversationHandler کامل را به آن اضافه می‌کند."""
573
- global _application_instance
574
- if _application_instance is None:
575
- print("Initializing Telegram bot Application for this worker...")
576
- application = Application.builder().token(TOKEN).build()
577
-
578
- conv_handler = ConversationHandler(
579
- entry_points=[CommandHandler("start", start), CommandHandler("link", admin_link_command)],
580
- states={
581
- LANGUAGE_SELECTION: [CallbackQueryHandler(set_language, pattern='^set_lang_')],
582
- MAIN_MENU: [
583
- CallbackQueryHandler(change_format_selected, pattern='^select_convert_format$'),
584
- CallbackQueryHandler(cut_audio_selected, pattern='^select_cut_audio$'),
585
- CallbackQueryHandler(video_conversion_selected, pattern='^select_video_conversion$'),
586
- ],
587
- CONVERT_AUDIO: [
588
- MessageHandler(filters.AUDIO & ~filters.COMMAND, handle_audio),
589
- MessageHandler(filters.VOICE & ~filters.COMMAND, handle_voice),
590
- ],
591
- CUT_AUDIO_FILE: [MessageHandler((filters.AUDIO | filters.VOICE) & ~filters.COMMAND, handle_cut_audio_file)],
592
- CUT_AUDIO_RANGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, handle_cut_audio_range)],
593
- VIDEO_CONVERSION_MODE: [MessageHandler((filters.VIDEO | filters.VIDEO_NOTE) & ~filters.COMMAND, handle_video_conversion)],
594
- WAITING_FOR_MEMBERSHIP: [CallbackQueryHandler(check_membership_callback, pattern='^check_membership$')],
595
- ADMIN_MENU: [
596
- CallbackQueryHandler(admin_add_channel_prompt, pattern='^admin_add_channel$'),
597
- CallbackQueryHandler(admin_list_channels, pattern='^admin_list_channels$'),
598
- ],
599
- ADD_CHANNEL: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_handle_add_channel)],
600
- LIST_REMOVE_CHANNELS: [CallbackQueryHandler(admin_handle_remove_channel, pattern='^remove_channel_')],
601
- },
602
- fallbacks=[CommandHandler("cancel", cancel), CommandHandler("start", start)],
603
- allow_reentry=True
604
- )
605
-
606
- application.add_handler(conv_handler)
607
- application.add_error_handler(error_handler)
608
- await application.initialize()
609
- _application_instance = application
610
- print("Telegram bot Application initialized.")
611
- return _application_instance
612
 
613
  @app.route("/")
614
  def index():
615
- """یک صفحه ساده برای اطمینان از بالا بودن سرور."""
616
- return "Hello, I am the Telegram bot server. The bot is running."
617
 
618
  @app.route("/webhook", methods=["POST"])
619
  async def webhook():
620
- """این مسیر آپدیت‌ها را از تلگرام دریافت می‌کند."""
621
  try:
622
- application = await get_telegram_application()
623
- update = Update.de_json(request.get_json(force=True), application.bot)
624
- await application.process_update(update)
625
  return "ok"
626
  except Exception as e:
627
- print(f"Error in webhook: {e}", file=sys.stderr)
628
- return "error", 500
 
 
 
 
 
 
 
 
629
 
630
  @app.route("/set_webhook", methods=["GET"])
631
  async def set_webhook_route():
632
- """یک مسیر برای تنظیم وبهوک به صورت خودکار."""
633
  webhook_url = os.getenv("WEBHOOK_URL")
634
  if not webhook_url:
635
  return jsonify({"status": "error", "message": "WEBHOOK_URL environment variable not set."}), 500
636
-
637
  if not webhook_url.endswith("/webhook"):
638
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
639
 
640
  try:
641
- application = await get_telegram_application()
642
- await application.bot.set_webhook(url=webhook_url)
 
 
 
643
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
644
  except Exception as e:
645
  print(f"Failed to set webhook: {e}", file=sys.stderr)
646
  return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
647
 
648
  if __name__ == "__main__":
649
- # این بخش برای اجرای محلی (local) سرور Flask است
650
- # در محیط‌های production مانند Hugging Face، یک وب سرور مانند Gunicorn این فایل را اجرا می‌کند.
651
  app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
 
5
  import subprocess
6
  import json
7
  from io import BytesIO
8
+ import asyncio
9
 
10
  from flask import Flask, request, jsonify
11
 
 
24
  # --- 1. تنظیمات و متغیرهای سراسری ---
25
 
26
  # ⚠️⚠️⚠️ بسیار مهم: این توکن را با توکن واقعی ربات خود که از BotFather دریافت کرده‌اید جایگزین کنید.
 
27
  TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8035336072:AAHG9REotvM4u8DgreC7hu8gIAroxhS-N-M")
28
  if "YOUR_TELEGRAM_BOT_TOKEN_HERE" in TOKEN:
29
  print("خطا: لطفا توکن ربات تلگرام خود را در کد جایگزین کنید.", file=sys.stderr)
 
35
  ADMIN_ID = int(os.getenv("TELEGRAM_ADMIN_ID", "684173337"))
36
 
37
  # مسیرها
 
38
  DOWNLOAD_DIR = "/tmp/downloads"
39
  OUTPUT_DIR = "/tmp/outputs"
40
  CHANNELS_FILE = "/tmp/channels.json"
41
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
42
  os.makedirs(OUTPUT_DIR, exist_ok=True)
43
 
44
+ # --- 2. توابع مدیریت کانال‌ها ---
45
 
46
  def load_required_channels():
 
47
  if os.path.exists(CHANNELS_FILE):
48
  try:
49
  with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
 
54
  return []
55
 
56
  def save_required_channels(channels):
 
57
  with open(CHANNELS_FILE, 'w', encoding='utf-8') as f:
58
  json.dump(channels, f, indent=4, ensure_ascii=False)
59
 
60
  REQUIRED_CHANNELS = load_required_channels()
61
 
 
62
  # --- 3. تعریف حالت‌های مکالمه و پیام‌ها ---
 
 
63
  (LANGUAGE_SELECTION, MAIN_MENU, CONVERT_AUDIO, CUT_AUDIO_FILE, CUT_AUDIO_RANGE,
64
  VIDEO_CONVERSION_MODE, WAITING_FOR_MEMBERSHIP, ADMIN_MENU, ADD_CHANNEL,
65
  LIST_REMOVE_CHANNELS) = range(10)
66
 
 
67
  MESSAGES = {
68
  'fa': {
69
  'start_welcome': "سلام! من یک ربات تبدیل فرمت صوتی و ویدیویی هستم.\n\nبرای شروع، از منوی زیر یک قابلیت را انتخاب کنید.",
 
120
  'bot_not_admin_in_channel': "ربات ادمین کانال '{channel_id}' نیست یا مجوزهای کافی برای بررسی عضویت را ندارد. لطفاً ربات را به عنوان ادمین با مجوز 'بررسی وضعیت اعضا' در کانال اضافه کنید."
121
  },
122
  'en': {
123
+ # English messages go here
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
  }
126
 
127
+ # --- 4. توابع کمکی ---
128
 
129
  def get_message(context, key, **kwargs):
 
130
  lang = context.user_data.get('language', 'fa')
 
131
  message_template = MESSAGES.get(lang, MESSAGES['fa']).get(key, MESSAGES['fa'].get(key, "Message key not found"))
132
  return message_template.format(**kwargs)
133
 
134
  def parse_time_to_ms(time_str):
 
135
  match = re.match(r'^(\d{2})\.(\d{2})$', time_str)
136
+ if not match: raise ValueError("Invalid time format")
 
137
  minutes, seconds = int(match.group(1)), int(match.group(2))
138
+ if seconds >= 60: raise ValueError("Seconds must be between 00 and 59")
 
139
  return (minutes * 60 + seconds) * 1000
140
 
141
  async def check_user_membership(update: Update, context):
 
142
  user_id = update.effective_user.id
143
+ if not REQUIRED_CHANNELS: return True
 
144
  for channel_id in REQUIRED_CHANNELS:
145
  try:
146
  chat_member = await context.bot.get_chat_member(chat_id=channel_id, user_id=user_id)
147
+ if chat_member.status not in ['member', 'administrator', 'creator']: return False
 
148
  except Exception as e:
149
  print(f"خطا در بررسی عضویت کانال {channel_id}: {e}", file=sys.stderr)
150
  return False
151
  return True
152
 
153
  async def show_membership_required_message(update: Update, context):
 
154
  keyboard = []
155
+ if not REQUIRED_CHANNELS: return await show_main_menu(update, context)
 
 
156
  for channel_id in REQUIRED_CHANNELS:
157
  try:
158
  chat = await context.bot.get_chat(chat_id=channel_id)
159
  url = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
160
+ if url: keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_join_channel')} {chat.title or channel_id}", url=url)])
 
161
  except Exception as e:
162
  print(f"ناتوان در دریافت اطلاعات کانال {channel_id}: {e}", file=sys.stderr)
 
 
163
  keyboard.append([InlineKeyboardButton(get_message(context, 'btn_check_membership'), callback_data='check_membership')])
164
  reply_markup = InlineKeyboardMarkup(keyboard)
 
165
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.effective_message.reply_text
166
  await message_sender(get_message(context, 'membership_required'), reply_markup=reply_markup)
167
  return WAITING_FOR_MEMBERSHIP
168
 
169
  async def process_feature_or_check_membership(update: Update, context, feature_func, *args, **kwargs):
 
170
  if update.effective_user.id == ADMIN_ID or not REQUIRED_CHANNELS:
171
  return await feature_func(update, context, *args, **kwargs)
 
172
  is_member = await check_user_membership(update, context)
173
  if is_member:
174
  context.user_data['is_member'] = True
 
177
  context.user_data['is_member'] = False
178
  return await show_membership_required_message(update, context)
179
 
180
+ # --- 5. توابع Handler (مدیریت دستورات و پیام‌ها) ---
181
 
182
  async def start(update: Update, context):
183
+ keyboard = [[InlineKeyboardButton("فارسی 🇮🇷", callback_data='set_lang_fa')], [InlineKeyboardButton("English 🇬🇧", callback_data='set_lang_en')]]
 
 
 
 
184
  reply_markup = InlineKeyboardMarkup(keyboard)
185
+ await update.message.reply_text("زبان مورد نظر خود را انتخاب کنید:\nChoose your preferred language:", reply_markup=reply_markup)
 
 
 
186
  return LANGUAGE_SELECTION
187
 
188
  async def set_language(update: Update, context):
 
189
  query = update.callback_query
190
  await query.answer()
191
  context.user_data['language'] = query.data.replace('set_lang_', '')
 
193
  return await show_main_menu(update, context)
194
 
195
  async def show_main_menu(update: Update, context):
 
196
  keyboard = [
197
  [InlineKeyboardButton(get_message(context, 'btn_convert_format'), callback_data='select_convert_format')],
198
  [InlineKeyboardButton(get_message(context, 'btn_cut_audio'), callback_data='select_cut_audio')],
199
  [InlineKeyboardButton(get_message(context, 'btn_video_conversion'), callback_data='select_video_conversion')]
200
  ]
201
  reply_markup = InlineKeyboardMarkup(keyboard)
 
202
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.message.reply_text
203
  await message_sender(text=get_message(context, 'main_menu_prompt'), reply_markup=reply_markup)
204
  return MAIN_MENU
 
223
  return VIDEO_CONVERSION_MODE
224
 
225
  async def handle_audio(update: Update, context):
 
226
  async def _perform_conversion(update, context):
227
  file_id = update.message.audio.file_id
 
228
  processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
229
  download_path = os.path.join(DOWNLOAD_DIR, f"in_{file_id}.mp3")
230
  output_ogg_path = os.path.join(OUTPUT_DIR, f"out_{file_id}.ogg")
 
231
  try:
232
  new_file = await context.bot.get_file(file_id)
233
  await new_file.download_to_drive(download_path)
234
  await processing_message.edit_text(get_message(context, 'file_received'))
 
235
  audio = AudioSegment.from_file(download_path)
236
  audio.export(output_ogg_path, format="ogg", codec="libopus", parameters=["-b:a", "32k"])
237
  await processing_message.edit_text(get_message(context, 'conversion_done'))
 
238
  with open(output_ogg_path, 'rb') as f:
239
+ await update.message.reply_voice(f)
240
  await processing_message.delete()
241
  except Exception as e:
 
242
  await processing_message.edit_text(get_message(context, 'error_mp3_to_voice') + str(e))
243
  finally:
244
  if os.path.exists(download_path): os.remove(download_path)
 
247
  return await process_feature_or_check_membership(update, context, _perform_conversion)
248
 
249
  async def handle_voice(update: Update, context):
 
250
  async def _perform_conversion(update, context):
251
  file_id = update.message.voice.file_id
252
  processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
253
  download_path = os.path.join(DOWNLOAD_DIR, f"in_{file_id}.ogg")
254
  output_mp3_path = os.path.join(OUTPUT_DIR, f"@{BOT_USERNAME}_{file_id}.mp3")
 
255
  try:
256
  new_file = await context.bot.get_file(file_id)
257
  await new_file.download_to_drive(download_path)
258
  await processing_message.edit_text(get_message(context, 'file_received'))
 
259
  audio = AudioSegment.from_file(download_path, format="ogg")
260
  audio.export(output_mp3_path, format="mp3", tags={'album': BOT_USERNAME, 'artist': BOT_USERNAME})
261
  await processing_message.edit_text(get_message(context, 'conversion_done'))
 
262
  with open(output_mp3_path, 'rb') as f:
263
+ await update.message.reply_audio(f, caption=get_message(context, 'voice_to_mp3_caption'))
264
  await processing_message.delete()
265
  except Exception as e:
 
266
  await processing_message.edit_text(get_message(context, 'error_voice_to_mp3') + str(e))
267
  finally:
268
  if os.path.exists(download_path): os.remove(download_path)
 
271
  return await process_feature_or_check_membership(update, context, _perform_conversion)
272
 
273
  async def handle_cut_audio_file(update: Update, context):
 
274
  async def _perform_file_receive(update, context):
275
  audio_file = update.message.audio or update.message.voice
276
  file_id = audio_file.file_id
277
  ext = 'mp3' if update.message.audio else 'ogg'
278
  download_path = os.path.join(DOWNLOAD_DIR, f"cut_in_{file_id}.{ext}")
 
279
  try:
280
  new_file = await context.bot.get_file(file_id)
281
  await new_file.download_to_drive(download_path)
 
284
  await update.message.reply_text(get_message(context, 'cut_mode_active_range'))
285
  return CUT_AUDIO_RANGE
286
  except Exception as e:
 
287
  await update.message.reply_text(get_message(context, 'general_error'))
288
  return CUT_AUDIO_FILE
289
  return await process_feature_or_check_membership(update, context, _perform_file_receive)
290
 
291
  async def handle_cut_audio_range(update: Update, context):
 
292
  async def _perform_cut(update, context):
293
  time_range_str = update.message.text
294
  audio_path = context.user_data.get('audio_for_cut_path')
295
  audio_type = context.user_data.get('audio_for_cut_type')
 
296
  if not audio_path or not os.path.exists(audio_path):
297
  await update.message.reply_text(get_message(context, 'no_audio_for_cut'))
298
  return CUT_AUDIO_FILE
 
299
  processing_message = await update.message.reply_text(get_message(context, 'cut_processing'))
300
  output_cut_path = os.path.join(OUTPUT_DIR, f"cut_out_{os.path.basename(audio_path)}.mp3")
 
301
  try:
302
  start_time_str, end_time_str = time_range_str.split('-')
303
  start_ms = parse_time_to_ms(start_time_str.strip())
304
  end_ms = parse_time_to_ms(end_time_str.strip())
 
305
  if start_ms >= end_ms:
306
  await processing_message.edit_text(get_message(context, 'invalid_time_range'))
307
  return CUT_AUDIO_RANGE
 
308
  audio = AudioSegment.from_file(audio_path, format=audio_type)
309
  cut_audio = audio[start_ms:end_ms]
310
  cut_audio.export(output_cut_path, format="mp3")
 
311
  await processing_message.edit_text(get_message(context, 'audio_cut_success'))
312
  with open(output_cut_path, 'rb') as f:
313
  await update.message.reply_audio(f, caption=f"برش از {start_time_str} تا {end_time_str}")
 
317
  await processing_message.edit_text(get_message(context, 'invalid_time_format'))
318
  return CUT_AUDIO_RANGE
319
  except Exception as e:
 
320
  await processing_message.edit_text(get_message(context, 'general_error'))
321
  return await show_main_menu(update, context)
322
  finally:
 
327
  return await process_feature_or_check_membership(update, context, _perform_cut)
328
 
329
  async def handle_video_conversion(update: Update, context):
 
330
  async def _perform_video_conversion(update, context):
331
  is_video_note = bool(update.message.video_note)
332
  file_to_process = update.message.video_note if is_video_note else update.message.video
 
333
  file_id = file_to_process.file_id
334
  download_path = os.path.join(DOWNLOAD_DIR, f"vid_in_{file_id}.mp4")
335
  output_path = os.path.join(OUTPUT_DIR, f"vid_out_{file_id}.mp4")
336
  processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
 
337
  try:
338
  new_file = await context.bot.get_file(file_id)
339
  await new_file.download_to_drive(download_path)
340
  await processing_message.edit_text(get_message(context, 'file_received_video'))
 
341
  if is_video_note:
342
  await processing_message.edit_text(get_message(context, 'converting_video_note_to_video'))
343
+ ffmpeg_command = ['ffmpeg', '-y', '-i', download_path, '-c:v', 'libx264', '-crf', '23', '-preset', 'medium', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', output_path]
344
  else:
345
  await processing_message.edit_text(get_message(context, 'converting_video_to_video_note'))
346
+ ffmpeg_command = ['ffmpeg', '-y', '-i', download_path, '-vf', 'crop=min(iw\,ih):min(iw\,ih),scale=640:640,setsar=1', '-an', output_path]
347
+ subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True)
 
348
  await processing_message.edit_text(get_message(context, 'conversion_done_video'))
 
349
  with open(output_path, 'rb') as f:
350
  if is_video_note:
351
  await update.message.reply_video(f, caption=get_message(context, 'video_note_to_video_caption'))
 
353
  await update.message.reply_video_note(f)
354
  await processing_message.delete()
355
  except subprocess.CalledProcessError as e:
356
+ await processing_message.edit_text(get_message(context, 'error_video_conversion') + f"FFmpeg Error: {e.stderr}")
 
357
  except Exception as e:
 
358
  await processing_message.edit_text(get_message(context, 'error_video_conversion') + str(e))
359
  finally:
360
  if os.path.exists(download_path): os.remove(download_path)
 
363
  return await process_feature_or_check_membership(update, context, _perform_video_conversion)
364
 
365
  async def check_membership_callback(update: Update, context):
 
366
  query = update.callback_query
367
  await query.answer()
368
  is_member = await check_user_membership(update, context)
 
375
  return WAITING_FOR_MEMBERSHIP
376
 
377
  async def admin_link_command(update: Update, context):
 
378
  if update.effective_user.id != ADMIN_ID:
379
  await update.message.reply_text(get_message(context, 'not_admin'))
380
  return ConversationHandler.END
 
392
  return ADD_CHANNEL
393
 
394
  async def admin_handle_add_channel(update: Update, context):
 
395
  channel_input = update.message.text.strip()
396
  if not (channel_input.startswith('@') or channel_input.startswith('-100')):
397
  await update.message.reply_text(get_message(context, 'invalid_channel_id'))
398
  return ADD_CHANNEL
399
  try:
400
+ await context.bot.get_chat(channel_input)
401
  if channel_input not in REQUIRED_CHANNELS:
402
  REQUIRED_CHANNELS.append(channel_input)
403
  save_required_channels(REQUIRED_CHANNELS)
 
409
  return await admin_link_command(update, context)
410
 
411
  async def admin_list_channels(update: Update, context):
 
412
  query = update.callback_query
413
  await query.answer()
414
  if not REQUIRED_CHANNELS:
 
421
  return LIST_REMOVE_CHANNELS
422
 
423
  async def admin_handle_remove_channel(update: Update, context):
 
424
  query = update.callback_query
425
  await query.answer()
426
  channel_id_to_remove = query.data.replace('remove_channel_', '')
 
433
  return await admin_link_command(update, context)
434
 
435
  async def cancel(update: Update, context):
 
436
  message = update.message or update.callback_query.message
437
  await message.reply_text(get_message(context, 'cancel_message'))
438
  context.user_data.clear()
439
  return await show_main_menu(update, context)
440
 
441
  async def error_handler(update: object, context: object) -> None:
 
442
  print(f"Update {update} caused error {context.error}", file=sys.stderr)
443
 
444
+
445
+ # --- 6. بخش آماده‌سازی و مقداردهی اولیه ربات ---
446
+
447
+ def setup_telegram_app():
448
+ print("در حال ساختن اپلیکیشن تلگرام...")
449
+ application = Application.builder().token(TOKEN).build()
450
+
451
+ conv_handler = ConversationHandler(
452
+ entry_points=[CommandHandler("start", start), CommandHandler("link", admin_link_command)],
453
+ states={
454
+ LANGUAGE_SELECTION: [CallbackQueryHandler(set_language, pattern='^set_lang_')],
455
+ MAIN_MENU: [
456
+ CallbackQueryHandler(change_format_selected, pattern='^select_convert_format$'),
457
+ CallbackQueryHandler(cut_audio_selected, pattern='^select_cut_audio$'),
458
+ CallbackQueryHandler(video_conversion_selected, pattern='^select_video_conversion$'),
459
+ ],
460
+ CONVERT_AUDIO: [
461
+ MessageHandler(filters.AUDIO & ~filters.COMMAND, handle_audio),
462
+ MessageHandler(filters.VOICE & ~filters.COMMAND, handle_voice),
463
+ ],
464
+ CUT_AUDIO_FILE: [MessageHandler((filters.AUDIO | filters.VOICE) & ~filters.COMMAND, handle_cut_audio_file)],
465
+ CUT_AUDIO_RANGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, handle_cut_audio_range)],
466
+ VIDEO_CONVERSION_MODE: [MessageHandler((filters.VIDEO | filters.VIDEO_NOTE) & ~filters.COMMAND, handle_video_conversion)],
467
+ WAITING_FOR_MEMBERSHIP: [CallbackQueryHandler(check_membership_callback, pattern='^check_membership$')],
468
+ ADMIN_MENU: [
469
+ CallbackQueryHandler(admin_add_channel_prompt, pattern='^admin_add_channel$'),
470
+ CallbackQueryHandler(admin_list_channels, pattern='^admin_list_channels$'),
471
+ ],
472
+ ADD_CHANNEL: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_handle_add_channel)],
473
+ LIST_REMOVE_CHANNELS: [CallbackQueryHandler(admin_handle_remove_channel, pattern='^remove_channel_')],
474
+ },
475
+ fallbacks=[CommandHandler("cancel", cancel), CommandHandler("start", start)],
476
+ allow_reentry=True
477
+ )
478
+
479
+ application.add_handler(conv_handler)
480
+ application.add_error_handler(error_handler)
481
+
482
+ print("اپلیکیشن تلگرام با موفقیت ساخته شد.")
483
+ return application
484
+
485
+ # --- 7. بخش مربوط به Flask و Webhook (اصلاح شده) ---
486
 
487
  app = Flask(__name__)
488
+ telegram_app = setup_telegram_app()
489
+
490
+ @app.before_first_request
491
+ def initialize_bot():
492
+ """
493
+ این تابع قبل از اولین درخواست به سرور اجرا می‌شود.
494
+ ما از این فرصت برای مقداردهی اولیه ربات استفاده می‌کنیم.
495
+ """
496
+ print("در حال مقداردهی اولیه ربات قبل از اولین درخواست...")
497
+ # از آنجایی که این محیط همزمان است، از asyncio.run برای اجرای تابع async استفاده می‌کنیم
498
+ try:
499
+ asyncio.run(telegram_app.initialize())
500
+ print("ربات با موفقیت مقداردهی اولیه شد.")
501
+ except Exception as e:
502
+ print(f"خطا در هنگام مقداردهی اولیه ربات: {e}", file=sys.stderr)
503
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
  @app.route("/")
506
  def index():
507
+ return "Hello, I am the Telegram bot server. The bot is ready."
 
508
 
509
  @app.route("/webhook", methods=["POST"])
510
  async def webhook():
 
511
  try:
512
+ # دیگر نیازی به بررسی initialize در اینجا نیست چون before_first_request آن را انجام داده
513
+ update = Update.de_json(request.get_json(force=True), telegram_app.bot)
514
+ await telegram_app.process_update(update)
515
  return "ok"
516
  except Exception as e:
517
+ # حتی اگر before_first_request ناموفق باشد، به عنوان آخرین راه‌حل اینجا initialize می‌کنیم
518
+ if "This Application was not initialized" in str(e):
519
+ print("مقداردهی اولیه مجدد در وبهوک...")
520
+ await telegram_app.initialize()
521
+ update = Update.de_json(request.get_json(force=True), telegram_app.bot)
522
+ await telegram_app.process_update(update)
523
+ return "ok"
524
+ else:
525
+ print(f"Error in webhook: {e}", file=sys.stderr)
526
+ return "error", 500
527
 
528
  @app.route("/set_webhook", methods=["GET"])
529
  async def set_webhook_route():
 
530
  webhook_url = os.getenv("WEBHOOK_URL")
531
  if not webhook_url:
532
  return jsonify({"status": "error", "message": "WEBHOOK_URL environment variable not set."}), 500
533
+
534
  if not webhook_url.endswith("/webhook"):
535
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
536
 
537
  try:
538
+ # اطمینان از اینکه ربات قبل از تنظیم وبهوک مقداردهی اولیه شده
539
+ if not telegram_app.initialized:
540
+ await telegram_app.initialize()
541
+
542
+ await telegram_app.bot.set_webhook(url=webhook_url, allowed_updates=Update.ALL_TYPES)
543
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
544
  except Exception as e:
545
  print(f"Failed to set webhook: {e}", file=sys.stderr)
546
  return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
547
 
548
  if __name__ == "__main__":
549
+ # این بخش فقط برای اجرای محلی (local) با استفاده از وب‌سرور خود Flask است
550
+ # در این حالت، before_first_request به درستی کار می‌کند
551
  app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))