akdNIKY commited on
Commit
fd480d8
·
verified ·
1 Parent(s): 472cf7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +347 -51
app.py CHANGED
@@ -23,9 +23,10 @@ from pydub import AudioSegment
23
  # --- 1. تنظیمات و متغیرهای سراسری ---
24
 
25
  # ⚠️⚠️⚠️ بسیار مهم: این توکن را با توکن واقعی ربات خود که از BotFather دریافت کرده‌اید جایگزین کنید.
26
- TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "YOUR_TELEGRAM_BOT_TOKEN_HERE")
27
- if TOKEN == "YOUR_TELEGRAM_BOT_TOKEN_HERE":
28
- print("Error: TELEGRAM_BOT_TOKEN environment variable not set, and no default token provided.", file=sys.stderr)
 
29
  sys.exit(1)
30
 
31
  # نام کاربری ربات و آیدی ادمین
@@ -50,7 +51,7 @@ def load_required_channels():
50
  with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
51
  return json.load(f)
52
  except json.JSONDecodeError:
53
- print(f"Error decoding JSON from {CHANNELS_FILE}. Starting with empty list.", file=sys.stderr)
54
  return []
55
  return []
56
 
@@ -65,11 +66,11 @@ REQUIRED_CHANNELS = load_required_channels()
65
  # --- 3. تعریف حالت‌های مکالمه و پیام‌ها ---
66
 
67
  # تعریف حالت‌ها
68
- LANGUAGE_SELECTION, MAIN_MENU, CONVERT_AUDIO, CUT_AUDIO_FILE, CUT_AUDIO_RANGE, \
69
- VIDEO_CONVERSION_MODE, WAITING_FOR_MEMBERSHIP, \
70
- ADMIN_MENU, ADD_CHANNEL, LIST_REMOVE_CHANNELS = range(10)
71
 
72
- # دیکشنری پیام‌ها (کپی شده از اسکریپت Colab)
73
  MESSAGES = {
74
  'fa': {
75
  'start_welcome': "سلام! من یک ربات تبدیل فرمت صوتی و ویدیویی هستم.\n\nبرای شروع، از منوی زیر یک قابلیت را انتخاب کنید.",
@@ -83,7 +84,7 @@ MESSAGES = {
83
  'error_voice_to_mp3': "❌ خطا در تبدیل ویس تلگرام به MP3: ",
84
  'general_error': "متاسفم، مشکلی پیش آمد. لطفاً دوباره تلاش کنید.",
85
  'main_menu_prompt': "چه کاری می‌خواهید روی فایل خود انجام دهید؟",
86
- 'btn_convert_format': "تغییر فرمت صدا 🎵",
87
  'btn_cut_audio': "برش قسمتی از صدا ✂️",
88
  'btn_video_conversion': "تبدیل ویدیو دایره‌ای 🎥",
89
  'convert_mode_active': "شما در حالت 'تغییر فرمت صدا' هستید. حالا فایل صوتی (ویس یا MP3) خود را برای من ارسال کنید.",
@@ -126,7 +127,58 @@ MESSAGES = {
126
  'bot_not_admin_in_channel': "ربات ادمین کانال '{channel_id}' نیست یا مجوزهای کافی برای بررسی عضویت را ندارد. لطفاً ربات را به عنوان ادمین با مجوز 'بررسی وضعیت اعضا' در کانال اضافه کنید."
127
  },
128
  'en': {
129
- # ... پیام‌های انگلیسی را برای اختصار حذف کردم، اما شما باید آنها را اینجا کپی کنید ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
131
  }
132
 
@@ -135,7 +187,8 @@ MESSAGES = {
135
  def get_message(context, key, **kwargs):
136
  """پیام مناسب را بر اساس زبان کاربر برمی‌گرداند."""
137
  lang = context.user_data.get('language', 'fa')
138
- message_template = MESSAGES.get(lang, MESSAGES['fa']).get(key, MESSAGES['fa'].get(key, "Message not found"))
 
139
  return message_template.format(**kwargs)
140
 
141
  def parse_time_to_ms(time_str):
@@ -158,8 +211,9 @@ async def check_user_membership(update: Update, context):
158
  chat_member = await context.bot.get_chat_member(chat_id=channel_id, user_id=user_id)
159
  if chat_member.status not in ['member', 'administrator', 'creator']:
160
  return False
161
- except Exception:
162
- return False # اگر خطایی رخ دهد (مثلاً ربات ادمین نباشد)، دسترسی را رد می‌کنیم
 
163
  return True
164
 
165
  async def show_membership_required_message(update: Update, context):
@@ -173,13 +227,14 @@ async def show_membership_required_message(update: Update, context):
173
  chat = await context.bot.get_chat(chat_id=channel_id)
174
  url = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
175
  if url:
176
- keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_join_channel')} {chat.title or channel_id}", url=url)])
177
  except Exception as e:
178
- print(f"Could not get chat info for {channel_id}: {e}", file=sys.stderr)
 
179
 
180
  keyboard.append([InlineKeyboardButton(get_message(context, 'btn_check_membership'), callback_data='check_membership')])
181
  reply_markup = InlineKeyboardMarkup(keyboard)
182
-
183
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.effective_message.reply_text
184
  await message_sender(get_message(context, 'membership_required'), reply_markup=reply_markup)
185
  return WAITING_FOR_MEMBERSHIP
@@ -228,14 +283,11 @@ async def show_main_menu(update: Update, context):
228
  [InlineKeyboardButton(get_message(context, 'btn_video_conversion'), callback_data='select_video_conversion')]
229
  ]
230
  reply_markup = InlineKeyboardMarkup(keyboard)
231
-
232
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.message.reply_text
233
  await message_sender(text=get_message(context, 'main_menu_prompt'), reply_markup=reply_markup)
234
  return MAIN_MENU
235
 
236
- # ... (تمام توابع دیگر مانند change_format_selected, handle_audio, handle_voice, handle_cut_audio_file و غیره باید اینجا کپی شوند)
237
- # برای جلوگیری از طولانی شدن بیش از حد، فقط چند تابع کلیدی را می‌آورم و شما باید بقیه را به همین شکل اضافه کنید.
238
-
239
  async def change_format_selected(update: Update, context):
240
  query = update.callback_query
241
  await query.answer()
@@ -256,23 +308,254 @@ async def video_conversion_selected(update: Update, context):
256
  return VIDEO_CONVERSION_MODE
257
 
258
  async def handle_audio(update: Update, context):
 
259
  async def _perform_conversion(update, context):
260
- # منطق کامل تابع handle_audio شما از فایل Colab
261
  file_id = update.message.audio.file_id
262
- download_path = os.path.join(DOWNLOAD_DIR, f"audio_{file_id}.mp3")
263
- output_ogg_path = os.path.join(OUTPUT_DIR, f"voice_{file_id}.ogg")
264
- # بقیه کد...
265
- await update.message.reply_text("Audio conversion logic goes here.") # Placeholder
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  return CONVERT_AUDIO
267
  return await process_feature_or_check_membership(update, context, _perform_conversion)
268
-
269
- # شما باید تمام توابع دیگر را به همین شکل کامل کنید
270
- # handle_voice, handle_cut_audio_file, handle_cut_audio_range, handle_video_conversion
271
- # admin_link_command, admin_add_channel_prompt, و غیره
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
  async def cancel(update: Update, context):
274
  """عملیات را لغو کرده و به منوی اصلی برمی‌گردد."""
275
- await update.message.reply_text(get_message(context, 'cancel_message'))
 
276
  context.user_data.clear()
277
  return await show_main_menu(update, context)
278
 
@@ -280,7 +563,6 @@ async def error_handler(update: object, context: object) -> None:
280
  """لاگ کردن خطاها."""
281
  print(f"Update {update} caused error {context.error}", file=sys.stderr)
282
 
283
-
284
  # --- 6. بخش مربوط به Flask و Webhook ---
285
 
286
  app = Flask(__name__)
@@ -293,12 +575,8 @@ async def get_telegram_application():
293
  print("Initializing Telegram bot Application for this worker...")
294
  application = Application.builder().token(TOKEN).build()
295
 
296
- # ConversationHandler کامل شده از اسکریپت Colab
297
  conv_handler = ConversationHandler(
298
- entry_points=[
299
- CommandHandler("start", start),
300
- # CommandHandler("link", admin_link_command) # این را هم اضافه کنید
301
- ],
302
  states={
303
  LANGUAGE_SELECTION: [CallbackQueryHandler(set_language, pattern='^set_lang_')],
304
  MAIN_MENU: [
@@ -307,12 +585,21 @@ async def get_telegram_application():
307
  CallbackQueryHandler(video_conversion_selected, pattern='^select_video_conversion$'),
308
  ],
309
  CONVERT_AUDIO: [
310
- MessageHandler(filters.AUDIO, handle_audio),
311
- # MessageHandler(filters.VOICE, handle_voice),
 
 
 
 
 
 
 
 
312
  ],
313
- # ... بقیه state ها
 
314
  },
315
- fallbacks=[CommandHandler("cancel", cancel)],
316
  allow_reentry=True
317
  )
318
 
@@ -326,30 +613,39 @@ async def get_telegram_application():
326
  @app.route("/")
327
  def index():
328
  """یک صفحه ساده برای اطمینان از بالا بودن سرور."""
329
- return "Hello, I am the Telegram bot server."
330
 
331
  @app.route("/webhook", methods=["POST"])
332
  async def webhook():
333
  """این مسیر آپدیت‌ها را از تلگرام دریافت می‌کند."""
334
- application = await get_telegram_application()
335
- update = Update.de_json(request.get_json(force=True), application.bot)
336
- await application.process_update(update)
337
- return "ok"
 
 
 
 
338
 
339
  @app.route("/set_webhook", methods=["GET"])
340
  async def set_webhook_route():
341
  """یک مسیر برای تنظیم وبهوک به صورت خودکار."""
342
- application = await get_telegram_application()
343
  webhook_url = os.getenv("WEBHOOK_URL")
344
  if not webhook_url:
345
- return "WEBHOOK_URL environment variable not set.", 500
346
-
347
  if not webhook_url.endswith("/webhook"):
348
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
349
 
350
- await application.bot.set_webhook(url=webhook_url)
351
- return f"Webhook set to {webhook_url}"
 
 
 
 
 
352
 
353
  if __name__ == "__main__":
354
  # این بخش برای اجرای محلی (local) سرور Flask است
355
- app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 8080)))
 
 
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)
30
  sys.exit(1)
31
 
32
  # نام کاربری ربات و آیدی ادمین
 
51
  with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
52
  return json.load(f)
53
  except json.JSONDecodeError:
54
+ print(f"خطا در خواندن فایل JSON کانال‌ها: {CHANNELS_FILE}", file=sys.stderr)
55
  return []
56
  return []
57
 
 
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برای شروع، از منوی زیر یک قابلیت را انتخاب کنید.",
 
84
  'error_voice_to_mp3': "❌ خطا در تبدیل ویس تلگرام به MP3: ",
85
  'general_error': "متاسفم، مشکلی پیش آمد. لطفاً دوباره تلاش کنید.",
86
  'main_menu_prompt': "چه کاری می‌خواهید روی فایل خود انجام دهید؟",
87
+ 'btn_convert_format': "تغییر فرمت صدا ",
88
  'btn_cut_audio': "برش قسمتی از صدا ✂️",
89
  'btn_video_conversion': "تبدیل ویدیو دایره‌ای 🎥",
90
  'convert_mode_active': "شما در حالت 'تغییر فرمت صدا' هستید. حالا فایل صوتی (ویس یا MP3) خود را برای من ارسال کنید.",
 
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
 
 
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):
 
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):
 
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
 
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
290
 
 
 
 
291
  async def change_format_selected(update: Update, context):
292
  query = update.callback_query
293
  await query.answer()
 
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)
336
+ if os.path.exists(output_ogg_path): os.remove(output_ogg_path)
337
  return CONVERT_AUDIO
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)
365
+ if os.path.exists(output_mp3_path): os.remove(output_mp3_path)
366
+ return CONVERT_AUDIO
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)
380
+ context.user_data['audio_for_cut_path'] = download_path
381
+ context.user_data['audio_for_cut_type'] = ext
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}")
420
+ await processing_message.delete()
421
+ return await show_main_menu(update, context)
422
+ except ValueError:
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:
430
+ if os.path.exists(audio_path): os.remove(audio_path)
431
+ if os.path.exists(output_cut_path): os.remove(output_cut_path)
432
+ context.user_data.pop('audio_for_cut_path', None)
433
+ context.user_data.pop('audio_for_cut_type', None)
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'))
465
+ else:
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)
476
+ if os.path.exists(output_path): os.remove(output_path)
477
+ return VIDEO_CONVERSION_MODE
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)
485
+ if is_member:
486
+ context.user_data['is_member'] = True
487
+ await query.edit_message_text(get_message(context, 'membership_success'))
488
+ return await show_main_menu(update, context)
489
+ else:
490
+ await query.answer(get_message(context, 'membership_failed'), show_alert=True)
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
498
+ keyboard = [
499
+ [InlineKeyboardButton(get_message(context, 'btn_add_channel'), callback_data='admin_add_channel')],
500
+ [InlineKeyboardButton(get_message(context, 'btn_list_channels'), callback_data='admin_list_channels')]
501
+ ]
502
+ await update.message.reply_text(get_message(context, 'admin_menu_prompt'), reply_markup=InlineKeyboardMarkup(keyboard))
503
+ return ADMIN_MENU
504
+
505
+ async def admin_add_channel_prompt(update: Update, context):
506
+ query = update.callback_query
507
+ await query.answer()
508
+ await query.edit_message_text(get_message(context, 'send_channel_link'))
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)
522
+ await update.message.reply_text(get_message(context, 'channel_added', channel_id=channel_input))
523
+ else:
524
+ await update.message.reply_text(get_message(context, 'channel_already_exists'))
525
+ except Exception as e:
526
+ await update.message.reply_text(get_message(context, 'bot_not_admin_in_channel', channel_id=channel_input) + f"\nError: {e}")
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:
534
+ await query.edit_message_text(get_message(context, 'no_channels_configured'))
535
+ return ADMIN_MENU
536
+ keyboard = []
537
+ for channel_id in REQUIRED_CHANNELS:
538
+ keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_remove_channel')} {channel_id}", callback_data=f'remove_channel_{channel_id}')])
539
+ await query.edit_message_text(get_message(context, 'channel_list_prompt'), reply_markup=InlineKeyboardMarkup(keyboard))
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_', '')
547
+ if channel_id_to_remove in REQUIRED_CHANNELS:
548
+ REQUIRED_CHANNELS.remove(channel_id_to_remove)
549
+ save_required_channels(REQUIRED_CHANNELS)
550
+ await query.edit_message_text(get_message(context, 'channel_removed', channel_id=channel_id_to_remove))
551
+ else:
552
+ await query.edit_message_text(get_message(context, 'channel_not_found'))
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
 
 
563
  """لاگ کردن خطاها."""
564
  print(f"Update {update} caused error {context.error}", file=sys.stderr)
565
 
 
566
  # --- 6. بخش مربوط به Flask و Webhook ---
567
 
568
  app = Flask(__name__)
 
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: [
 
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
 
 
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)))