akdNIKY commited on
Commit
b36400f
·
verified ·
1 Parent(s): 6aca208

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +421 -67
app.py CHANGED
@@ -5,7 +5,6 @@ import re
5
  import subprocess
6
  import json
7
  from io import BytesIO
8
- import asyncio
9
 
10
  from flask import Flask, request, jsonify
11
 
@@ -24,6 +23,7 @@ from pydub import AudioSegment
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,15 +35,17 @@ BOT_USERNAME = os.getenv("TELEGRAM_BOT_USERNAME", "Voice2mp3_RoBot")
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,16 +56,21 @@ def load_required_channels():
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,56 +127,123 @@ MESSAGES = {
120
  'bot_not_admin_in_channel': "ربات ادمین کانال '{channel_id}' نیست یا مجوزهای کافی برای بررسی عضویت را ندارد. لطفاً ربات را به عنوان ادمین با مجوز 'بررسی وضعیت اعضا' در کانال اضافه کنید."
121
  },
122
  'en': {
123
- # پیام‌های انگلیسی در اینجا قرار می‌گیرند
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
  }
126
 
127
  # --- 4. تمام توابع کمکی و منطقی ربات ---
128
- # (این بخش بدون تغییر باقی می‌ماند)
129
 
130
  def get_message(context, key, **kwargs):
 
131
  lang = context.user_data.get('language', 'fa')
 
132
  message_template = MESSAGES.get(lang, MESSAGES['fa']).get(key, MESSAGES['fa'].get(key, "Message key not found"))
133
  return message_template.format(**kwargs)
134
 
135
  def parse_time_to_ms(time_str):
 
136
  match = re.match(r'^(\d{2})\.(\d{2})$', time_str)
137
- if not match: raise ValueError("Invalid time format")
 
138
  minutes, seconds = int(match.group(1)), int(match.group(2))
139
- if seconds >= 60: raise ValueError("Seconds must be between 00 and 59")
 
140
  return (minutes * 60 + seconds) * 1000
141
 
142
  async def check_user_membership(update: Update, context):
 
143
  user_id = update.effective_user.id
144
- if not REQUIRED_CHANNELS: return True
 
145
  for channel_id in REQUIRED_CHANNELS:
146
  try:
147
  chat_member = await context.bot.get_chat_member(chat_id=channel_id, user_id=user_id)
148
- if chat_member.status not in ['member', 'administrator', 'creator']: return False
 
149
  except Exception as e:
150
  print(f"خطا در بررسی عضویت کانال {channel_id}: {e}", file=sys.stderr)
151
  return False
152
  return True
153
 
154
  async def show_membership_required_message(update: Update, context):
 
155
  keyboard = []
156
- if not REQUIRED_CHANNELS: return await show_main_menu(update, context)
 
 
157
  for channel_id in REQUIRED_CHANNELS:
158
  try:
159
  chat = await context.bot.get_chat(chat_id=channel_id)
160
  url = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
161
- if url: keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_join_channel')} {chat.title or channel_id}", url=url)])
 
162
  except Exception as e:
163
  print(f"ناتوان در دریافت اطلاعات کانال {channel_id}: {e}", file=sys.stderr)
 
 
164
  keyboard.append([InlineKeyboardButton(get_message(context, 'btn_check_membership'), callback_data='check_membership')])
165
  reply_markup = InlineKeyboardMarkup(keyboard)
 
166
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.effective_message.reply_text
167
  await message_sender(get_message(context, 'membership_required'), reply_markup=reply_markup)
168
  return WAITING_FOR_MEMBERSHIP
169
 
170
  async def process_feature_or_check_membership(update: Update, context, feature_func, *args, **kwargs):
 
171
  if update.effective_user.id == ADMIN_ID or not REQUIRED_CHANNELS:
172
  return await feature_func(update, context, *args, **kwargs)
 
173
  is_member = await check_user_membership(update, context)
174
  if is_member:
175
  context.user_data['is_member'] = True
@@ -179,15 +253,22 @@ async def process_feature_or_check_membership(update: Update, context, feature_f
179
  return await show_membership_required_message(update, context)
180
 
181
  # --- 5. تمام توابع Handler (مدیریت دستورات و پیام‌ها) ---
182
- # (این بخش بدون تغییر باقی می‌ماند)
183
 
184
  async def start(update: Update, context):
185
- keyboard = [[InlineKeyboardButton("فارسی 🇮🇷", callback_data='set_lang_fa')], [InlineKeyboardButton("English 🇬🇧", callback_data='set_lang_en')]]
 
 
 
 
186
  reply_markup = InlineKeyboardMarkup(keyboard)
187
- await update.message.reply_text("زبان مورد نظر خود را انتخاب کنید:\nChoose your preferred language:", reply_markup=reply_markup)
 
 
 
188
  return LANGUAGE_SELECTION
189
 
190
  async def set_language(update: Update, context):
 
191
  query = update.callback_query
192
  await query.answer()
193
  context.user_data['language'] = query.data.replace('set_lang_', '')
@@ -195,76 +276,352 @@ async def set_language(update: Update, context):
195
  return await show_main_menu(update, context)
196
 
197
  async def show_main_menu(update: Update, context):
 
198
  keyboard = [
199
  [InlineKeyboardButton(get_message(context, 'btn_convert_format'), callback_data='select_convert_format')],
200
  [InlineKeyboardButton(get_message(context, 'btn_cut_audio'), callback_data='select_cut_audio')],
201
  [InlineKeyboardButton(get_message(context, 'btn_video_conversion'), callback_data='select_video_conversion')]
202
  ]
203
  reply_markup = InlineKeyboardMarkup(keyboard)
 
204
  message_sender = update.callback_query.edit_message_text if update.callback_query else update.message.reply_text
205
  await message_sender(text=get_message(context, 'main_menu_prompt'), reply_markup=reply_markup)
206
  return MAIN_MENU
207
 
208
- # ... تمام توابع handler دیگر (handle_audio, handle_voice, و غیره) در اینجا قرار می‌گیرند ...
209
- # (کد آنها بدون تغییر باقی می‌ماند)
 
 
 
210
 
211
- async def error_handler(update: object, context: object) -> None:
212
- print(f"Update {update} caused error {context.error}", file=sys.stderr)
 
 
 
 
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- # --- 6. بخش جدید: آماده‌سازی و مقداردهی اولیه ربات ---
216
-
217
- def setup_telegram_app():
218
- """
219
- این تابع کل اپلیکیشن تلگرام را با تمام handler ها می‌سازد.
220
- این تابع فقط یک بار در هنگام شروع به کار سرور اجرا می‌شود.
221
- """
222
- print("در حال ساختن و مقداردهی اولیه اپلیکیشن تلگرام...")
223
- application = Application.builder().token(TOKEN).build()
224
-
225
- conv_handler = ConversationHandler(
226
- entry_points=[CommandHandler("start", start), CommandHandler("link", admin_link_command)],
227
- states={
228
- LANGUAGE_SELECTION: [CallbackQueryHandler(set_language, pattern='^set_lang_')],
229
- MAIN_MENU: [
230
- CallbackQueryHandler(change_format_selected, pattern='^select_convert_format$'),
231
- CallbackQueryHandler(cut_audio_selected, pattern='^select_cut_audio$'),
232
- CallbackQueryHandler(video_conversion_selected, pattern='^select_video_conversion$'),
233
- ],
234
- # ... تمام state های دیگر باید اینجا کپی شوند ...
235
- # CONVERT_AUDIO: [...],
236
- # CUT_AUDIO_FILE: [...],
237
- },
238
- fallbacks=[CommandHandler("cancel", cancel), CommandHandler("start", start)],
239
- allow_reentry=True
240
- )
241
 
242
- application.add_handler(conv_handler)
243
- application.add_error_handler(error_handler)
244
-
245
- # این بخش برای سازگاری با محیط‌های غیرهمزمان (asynchronous) است
246
- # ما اپلیکیشن را initialize نمی‌کنیم، چون در حلقه رویداد اصلی نیستیم.
247
- # کتابخانه خودش این کار را در زمان process_update انجام می‌دهد.
248
- print("اپلیکیشن تلگرام با موفقیت ساخته شد.")
249
- return application
250
 
251
- # --- 7. بخش مربوط به Flask و Webhook (اصلاح شده) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
- app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
- # اپلیکیشن تلگرام را یک بار در هنگام شروع به کار می‌سازیم
256
- telegram_app = setup_telegram_app()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  @app.route("/")
259
  def index():
260
- return "Hello, I am the Telegram bot server. The bot is ready."
 
261
 
262
  @app.route("/webhook", methods=["POST"])
263
  async def webhook():
264
- """این مسیر آپدیت‌ها را از تلگرام دریافت کرده و با اپلیکیشن از پیش ساخته شده پردازش می‌کند."""
265
  try:
266
- update = Update.de_json(request.get_json(force=True), telegram_app.bot)
267
- await telegram_app.process_update(update)
 
268
  return "ok"
269
  except Exception as e:
270
  print(f"Error in webhook: {e}", file=sys.stderr)
@@ -276,22 +633,19 @@ async def set_webhook_route():
276
  webhook_url = os.getenv("WEBHOOK_URL")
277
  if not webhook_url:
278
  return jsonify({"status": "error", "message": "WEBHOOK_URL environment variable not set."}), 500
279
-
280
  if not webhook_url.endswith("/webhook"):
281
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
282
 
283
  try:
284
- # از اپلیکیشن از پیش ساخته شده برای تنظیم وبهوک استفاده می‌کنیم
285
- await telegram_app.bot.set_webhook(url=webhook_url)
286
- # همچنین وبهوک را initialize می‌کنیم تا آماده دریافت آپدیت‌ها باشد
287
- await telegram_app.initialize()
288
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
289
  except Exception as e:
290
  print(f"Failed to set webhook: {e}", file=sys.stderr)
291
  return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
292
 
293
  if __name__ == "__main__":
294
- # این بخش فقط برای اجرای محلی (local) است
295
- # در این حالت، ما وبهوک را به صورت دستی initialize می‌کنیم
296
- asyncio.run(telegram_app.initialize())
297
  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
 
9
  from flask import Flask, request, jsonify
10
 
 
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
  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
  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
  '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
 
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
  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
290
 
291
+ async def change_format_selected(update: Update, context):
292
+ query = update.callback_query
293
+ await query.answer()
294
+ await query.edit_message_text(text=get_message(context, 'convert_mode_active'))
295
+ return CONVERT_AUDIO
296
 
297
+ async def cut_audio_selected(update: Update, context):
298
+ query = update.callback_query
299
+ await query.answer()
300
+ await query.edit_message_text(text=get_message(context, 'cut_mode_active_file'))
301
+ context.user_data.pop('audio_for_cut_path', None)
302
+ return CUT_AUDIO_FILE
303
 
304
+ async def video_conversion_selected(update: Update, context):
305
+ query = update.callback_query
306
+ await query.answer()
307
+ await query.edit_message_text(text=get_message(context, 'video_conversion_mode_active'))
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
+
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)
 
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)))