akdNIKY commited on
Commit
d9e09a7
·
verified ·
1 Parent(s): 409c69b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -858
app.py CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  import os
2
  import sys
3
  import re
@@ -5,50 +8,41 @@ import subprocess
5
  import json
6
  from io import BytesIO
7
  from flask import Flask, request, jsonify
8
- from telegram import Update, InputFile, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
9
  from telegram.ext import (
10
- Application,
11
- CommandHandler,
12
- MessageHandler,
13
- CallbackQueryHandler,
14
- filters,
15
- ConversationHandler
16
  )
17
- from telegram.constants import ParseMode, ChatMemberStatus
18
  from pydub import AudioSegment
19
- from pydub.silence import split_on_silence # Optional: for advanced audio processing if needed
20
 
21
- # Environment variables
22
  TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
23
- if not TOKEN:
24
- print("Error: TELEGRAM_BOT_TOKEN environment variable not set.", file=sys.stderr)
25
- sys.exit(1)
26
-
27
  BOT_USERNAME = os.getenv("TELEGRAM_BOT_USERNAME", "Voice2mp3_RoBot")
28
  ADMIN_ID = int(os.getenv("TELEGRAM_ADMIN_ID", "0"))
29
- if ADMIN_ID == 0:
30
- print("Warning: TELEGRAM_ADMIN_ID not set or 0. Admin features will be disabled.", file=sys.stderr)
31
 
32
  DOWNLOAD_DIR = "/tmp/downloads"
33
  OUTPUT_DIR = "/tmp/outputs"
34
  CHANNELS_FILE = "channels.json"
35
-
36
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
37
  os.makedirs(OUTPUT_DIR, exist_ok=True)
38
 
39
- # Telegram states
40
- LANGUAGE_SELECTION, MAIN_MENU, CONVERT_AUDIO_CHOICE, CONVERT_VOICE_TO_MP3, CONVERT_MP3_TO_VOICE, \
41
- CUT_AUDIO_FILE, CUT_AUDIO_RANGE, VIDEO_CONVERSION_MODE, CONVERT_VIDEO_TO_NOTE, CONVERT_NOTE_TO_VIDEO, \
42
- WAITING_FOR_MEMBERSHIP, ADMIN_MENU, ADD_CHANNEL, LIST_REMOVE_CHANNELS = range(14)
 
43
 
44
- # Load and save channels
45
  def load_required_channels():
46
  if os.path.exists(CHANNELS_FILE):
47
  try:
48
  with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
49
  return json.load(f)
50
  except json.JSONDecodeError:
51
- print(f"Error decoding JSON from {CHANNELS_FILE}. Returning empty list.", file=sys.stderr)
52
  return []
53
 
54
  def save_required_channels(channels):
@@ -57,839 +51,74 @@ def save_required_channels(channels):
57
 
58
  REQUIRED_CHANNELS = load_required_channels()
59
 
60
- # --- Language & Messages ---
61
  MESSAGES = {
62
- "en": {
63
- "welcome": "Hello! Please choose your language:",
64
- "language_chosen": "Language set to English.",
65
- "main_menu": "Welcome to the Audio/Video Converter Bot!\nWhat would you like to do?",
66
- "audio_menu_text": "Choose an audio conversion option:",
67
- "cut_audio_intro": "Please send me an audio file (MP3 or Voice message) you want to cut.",
68
- "cut_audio_range_prompt": "Now, send me the time range in 'MM.SS-MM.SS' format (e.g., 00.21-00.54).\n\nMax duration for cutting is 5 minutes for MP3s and 2 minutes for Voice messages. If your file is longer, only the first part will be processed.",
69
- "invalid_time_format": "Invalid time format. Please use 'MM.SS-MM.SS' (e.g., 00.21-00.54).",
70
- "processing": "Processing your request, please wait...",
71
- "audio_converted": "Here is your converted audio:",
72
- "audio_cut_success": "Here is your trimmed audio:",
73
- "no_audio_file": "Please send an audio file (MP3 or Voice message) to proceed.",
74
- "video_menu_text": "Choose a video conversion option:",
75
- "not_a_video": "Please send a video file to convert.",
76
- "video_note_converted": "Here is your converted video note:",
77
- "video_converted": "Here is your converted video:",
78
- "membership_required": "To use this bot without limitations, please join the following channels:",
79
- "membership_check_button": "I have joined!",
80
- "admin_menu": "Welcome to the Admin Panel! What do you want to do?",
81
- "add_channel_prompt": "Please send me the **username** or **ID** of the channel you want to add (e.g., `@mychannel` or `-1001234567890`).\n\n**Important:** The bot must be an admin in this channel with 'Invite Users' and 'Get Chat Member' permissions to verify membership.",
82
- "channel_already_added": "This channel is already in the list.",
83
- "channel_added_success": "Channel '{channel_name}' added successfully! ID: `{channel_id}`\n\n**Bot's Admin Status:** {status}",
84
- "channel_not_found": "Could not find the channel. Please ensure the username/ID is correct and the bot is a member of the channel.",
85
- "channel_permission_error": "Bot is not an admin or lacks necessary permissions (Invite Users, Get Chat Member) in this channel.",
86
- "list_channels": "Current required channels:\n{channels_list}",
87
- "no_channels_added": "No required channels added yet.",
88
- "select_channel_to_remove": "Select a channel to remove:",
89
- "channel_removed_success": "Channel '{channel_name}' removed successfully.",
90
- "cancel_operation": "Operation cancelled. Returning to main menu.",
91
- "unknown_command": "Unknown command. Please use the menu buttons or /start.",
92
- "error_occurred": "An error occurred while processing your request. Please try again later.",
93
- "not_admin": "You are not authorized to access the admin panel.",
94
- "admin_permission_check": "Checking bot's admin status in channel {channel_name}...",
95
- "bot_is_admin": "✅ Bot is an admin with required permissions.",
96
- "bot_not_admin": "❌ Bot is not an admin or lacks 'Invite Users' and 'Get Chat Member' permissions.",
97
- "bot_cannot_check_admin": "⚠️ Could not verify bot's admin status in this channel. Please ensure the bot is an admin with 'Invite Users' and 'Get Chat Member' permissions."
98
- },
99
- "fa": {
100
- "welcome": "سلام! لطفاً زبان خود را انتخاب کنید:",
101
- "language_chosen": "زبان به فارسی تنظیم شد.",
102
- "main_menu": "به ربات تبدیل صدا/ویدیو خوش آمدید!\nچه کاری می‌خواهید انجام دهید؟",
103
- "audio_menu_text": "یک گزینه برای تبدیل صدا انتخاب کنید:",
104
- "cut_audio_intro": "لطفاً یک فایل صوتی (MP3 یا ویس) که می‌خواهید برش دهید را برای من بفرستید.",
105
- "cut_audio_range_prompt": "اکنون، محدوده زمانی مورد نظر را با فرمت 'دقیقه.ثانیه-دقیقه.ثانیه' (مثال: 00.21-00.54) ارسال کنید.\n\nحداکثر زمان برش برای فایل‌های MP3 پنج دقیقه و برای ویس‌های تلگرام دو دقیقه است. اگر فایل شما طولانی‌تر باشد، فقط بخش ابتدایی آن پردازش می‌شود.",
106
- "invalid_time_format": "فرمت زمان نامعتبر است. لطفاً از فرمت 'MM.SS-MM.SS' (مثال: 00.21-00.54) استفاده کنید.",
107
- "processing": "در حال پردازش درخواست شما، لطفاً منتظر بمانید...",
108
- "audio_converted": "فایل صوتی تبدیل شده شما:",
109
- "audio_cut_success": "فایل صوتی برش خورده شما:",
110
- "no_audio_file": "لطفاً یک فایل صوتی (MP3 یا ویس) برای ادامه ارسال کنید.",
111
- "video_menu_text": "یک گزینه برای تبدیل ویدیو انتخاب کنید:",
112
- "not_a_video": "لطفاً یک فایل ویدیویی برای تبدیل ارسال کنید.",
113
- "video_note_converted": "ویدیوی دایره‌ای تبدیل شده شما:",
114
- "video_converted": "ویدیوی تبدیل شده شما:",
115
- "membership_required": "برای استفاده نامحدود از ربات، لطفاً در کانال‌های زیر عضو شوید:",
116
- "membership_check_button": "عضو شدم!",
117
- "admin_menu": "به پنل مدیریت خوش آمدید! چه کاری می‌خواهید انجام دهید؟",
118
- "add_channel_prompt": "لطفاً **یوزرنیم** یا **آیدی** کانالی که می‌خواهید اضافه کنید را ارسال کنید (مثال: `@mychannel` یا `-1001234567890`).\n\n**مهم:** ربات باید در این کانال ادمین باشد و دسترسی‌های 'دعوت کاربر' و 'دریافت اطلاعات اعضا' را داشته باشد تا بتواند عضویت را بررسی کند.",
119
- "channel_already_added": "این کانال از قبل اضافه شده است.",
120
- "channel_added_success": "کانال '{channel_name}' با موفقیت اضافه شد! آیدی: `{channel_id}`\n\n**وضعیت ادمین ربات:** {status}",
121
- "channel_not_found": "کانال پیدا نشد. لطفاً مطمئن شوید یوزرنیم/آیدی صحیح است و ربات عضو کانال است.",
122
- "channel_permission_error": "ربات ادمین نیست یا دسترسی‌های لازم (دعوت کاربر، دریافت اطلاعات اعضا) را در این کانال ندارد.",
123
- "list_channels": "کانال‌های اجباری فعلی:\n{channels_list}",
124
- "no_channels_added": "هنوز کانالی برای عضویت اجباری اضافه نشده است.",
125
- "select_channel_to_remove": "یک کانال برای حذف انتخاب کنید:",
126
- "channel_removed_success": "کانال '{channel_name}' با موفقیت حذف شد.",
127
- "cancel_operation": "عملیات لغو شد. بازگشت به منوی اصلی.",
128
- "unknown_command": "دستور نامشخص. لطفاً از دکمه‌های منو یا /start استفاده کنید.",
129
- "error_occurred": "خطایی در پردازش درخواست شما رخ داد. لطفاً بعداً امتحان کنید.",
130
- "not_admin": "شما اجازه دسترسی به پنل ادمین را ندارید.",
131
- "admin_permission_check": "در حال بررسی وضعیت ادمین ربات در کانال {channel_name}...",
132
- "bot_is_admin": "✅ ربات ادمین است و دسترسی‌های لازم را دارد.",
133
- "bot_not_admin": "❌ ربات ادمین نیست یا دسترسی‌های 'دعوت کاربر' و 'دریافت اطلاعات اعضا' را ندارد.",
134
- "bot_cannot_check_admin": "⚠️ امکان بررسی وضعیت ادمین ربات در این کانال وجود ندارد. لطفاً اطمینان حاصل کنید که ربات ادمین است و دسترسی‌های 'دعوت کاربر' و 'دریافت اطلاعات اعضا' را دارد."
135
  }
136
  }
137
 
138
- # --- Utility Functions ---
139
- async def get_user_language(user_id, context):
140
- user_data = context.user_data
141
- if 'language' not in user_data:
142
- # Default to Persian if not set
143
- user_data['language'] = 'fa'
144
- return user_data['language']
145
-
146
- def get_message(user_lang, key, **kwargs):
147
- return MESSAGES.get(user_lang, MESSAGES["en"]).get(key, MESSAGES["en"][key]).format(**kwargs)
148
-
149
- async def check_membership(update, context):
150
- user_id = update.effective_user.id
151
- user_lang = await get_user_language(user_id, context)
152
-
153
- if not REQUIRED_CHANNELS:
154
- return True # No channels required, proceed
155
-
156
- is_member = True
157
- not_joined_channels = []
158
 
159
- for channel_info in REQUIRED_CHANNELS:
160
- channel_id = channel_info['id']
161
- try:
162
- member = await context.bot.get_chat_member(chat_id=channel_id, user_id=user_id)
163
- if member.status not in [ChatMemberStatus.MEMBER, ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]:
164
- is_member = False
165
- not_joined_channels.append(channel_info)
166
- except Exception as e:
167
- print(f"Error checking membership for channel {channel_id}: {e}", file=sys.stderr)
168
- # If there's an error (e.g., bot not in channel, channel not found), assume not joined
169
- is_member = False
170
- not_joined_channels.append(channel_info)
171
-
172
- if not is_member:
173
- keyboard_buttons = []
174
- message_text = get_message(user_lang, "membership_required") + "\n"
175
- for i, channel in enumerate(not_joined_channels):
176
- channel_name = channel.get('name', channel['id'])
177
- message_text += f"- {channel_name}: {channel['link']}\n"
178
-
179
- keyboard_buttons.append([InlineKeyboardButton(get_message(user_lang, "membership_check_button"), callback_data="check_membership_again")])
180
-
181
- reply_markup = InlineKeyboardMarkup(keyboard_buttons)
182
- await update.message.reply_text(message_text, reply_markup=reply_markup, parse_mode=ParseMode.HTML)
183
- return False
184
- return True
185
-
186
- async def send_main_menu(update, context):
187
- user_lang = await get_user_language(update.effective_user.id, context)
188
- keyboard = [
189
- [InlineKeyboardButton(get_message(user_lang, "audio_menu"), callback_data="audio_menu")],
190
- [InlineKeyboardButton(get_message(user_lang, "video_menu"), callback_data="video_menu")]
191
- ]
192
  reply_markup = InlineKeyboardMarkup(keyboard)
193
- await update.message.reply_text(get_message(user_lang, "main_menu"), reply_markup=reply_markup)
194
- return MAIN_MENU
195
-
196
- # --- Audio Processing ---
197
- async def convert_ogg_to_mp3(ogg_file_path):
198
- output_path = os.path.join(OUTPUT_DIR, os.path.basename(ogg_file_path).replace(".ogg", ".mp3"))
199
- audio = AudioSegment.from_ogg(ogg_file_path)
200
- audio.export(output_path, format="mp3")
201
- return output_path
202
-
203
- async def convert_mp3_to_ogg_opus(mp3_file_path):
204
- output_path = os.path.join(OUTPUT_DIR, os.path.basename(mp3_file_path).replace(".mp3", ".ogg"))
205
- audio = AudioSegment.from_mp3(mp3_file_path)
206
- # Telegram voice notes are OGG Opus. Use a reasonable bitrate.
207
- # pydub's default ogg export uses libvorbis. For opus, ffmpeg might be needed directly.
208
- # Let's use ffmpeg via subprocess for explicit Opus encoding.
209
- command = [
210
- "ffmpeg", "-i", mp3_file_path,
211
- "-c:a", "libopus", "-b:a", "32k", # Typical Opus bitrate for voice notes
212
- "-vbr", "on", "-compression_level", "10", # VBR on, max compression
213
- "-map_metadata", "-1", # Remove metadata
214
- output_path
215
- ]
216
- try:
217
- subprocess.run(command, check=True, capture_output=True)
218
- return output_path
219
- except subprocess.CalledProcessError as e:
220
- print(f"Error converting MP3 to OGG Opus with ffmpeg: {e.stderr.decode()}", file=sys.stderr)
221
- raise
222
 
223
- async def cut_audio_segment(input_file_path, start_ms, end_ms):
224
- output_path = os.path.join(OUTPUT_DIR, f"cut_{os.path.basename(input_file_path)}")
225
- audio = AudioSegment.from_file(input_file_path)
226
- cut_audio = audio[start_ms:end_ms]
227
- cut_audio.export(output_path, format="mp3") # Always export cut audio as MP3
228
- return output_path
229
-
230
- # --- Video Processing ---
231
- async def convert_video_to_note(input_file_path):
232
- output_path = os.path.join(OUTPUT_DIR, f"note_{os.path.basename(input_file_path).replace('.', '_note.')}")
233
- # Convert to circle video (video note). Requires specific resolution (e.g., 640x640), no audio, and short duration.
234
- # Telegram usually expects square video for notes, no audio track, and specific bitrate/codec.
235
- # Assuming input might be non-square, we can crop/scale.
236
- # For simplicity, let's just convert format and ensure it's square and short (e.g., < 1 min)
237
- # We will use ffmpeg directly for better control.
238
-
239
- # Try to get input video duration
240
- try:
241
- probe_command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", input_file_path]
242
- duration_str = subprocess.check_output(probe_command, universal_newlines=True).strip()
243
- duration_seconds = float(duration_str)
244
- if duration_seconds > 60: # Limit to 60 seconds for video notes
245
- print("Video too long for note, trimming to 60 seconds.")
246
- trim_duration = 60
247
- else:
248
- trim_duration = duration_seconds
249
- except Exception as e:
250
- print(f"Could not get video duration, assuming 60s max: {e}", file=sys.stderr)
251
- trim_duration = 60 # Default to 60s max if duration cannot be determined
252
-
253
- command = [
254
- "ffmpeg", "-i", input_file_path,
255
- "-vf", "scale='min(iw,ih):min(iw,ih)',crop='min(iw,ih):min(iw,ih)'", # Crop to square
256
- "-t", str(trim_duration), # Trim to max duration
257
- "-c:v", "libx264", "-b:v", "800k", # H.264, target bitrate
258
- "-preset", "ultrafast", # Faster encoding
259
- "-an", # No audio for video notes
260
- "-f", "mp4", # Ensure MP4 format
261
- output_path
262
- ]
263
- try:
264
- subprocess.run(command, check=True, capture_output=True)
265
- return output_path
266
- except subprocess.CalledProcessError as e:
267
- print(f"Error converting video to note with ffmpeg: {e.stderr.decode()}", file=sys.stderr)
268
- raise
269
-
270
- async def convert_note_to_video(input_file_path):
271
- # A video note is essentially a video with specific Telegram metadata.
272
- # To convert to "normal" video, often just changing container/codec or removing some metadata is enough.
273
- # For simplicity, we'll convert it to a standard MP4 with a common codec.
274
- output_path = os.path.join(OUTPUT_DIR, f"video_{os.path.basename(input_file_path).replace('.', '_video.')}.mp4")
275
- command = [
276
- "ffmpeg", "-i", input_file_path,
277
- "-c:v", "libx264", "-c:a", "aac", # Common video/audio codecs
278
- "-b:a", "128k", "-b:v", "1500k", # Common bitrates
279
- "-movflags", "faststart", # For web streaming optimization
280
- "-f", "mp4", # Ensure MP4 format
281
- output_path
282
- ]
283
- try:
284
- subprocess.run(command, check=True, capture_output=True)
285
- return output_path
286
- except subprocess.CalledProcessError as e:
287
- print(f"Error converting note to video with ffmpeg: {e.stderr.decode()}", file=sys.stderr)
288
- raise
289
-
290
- # --- Telegram Bot Handlers ---
291
-
292
- # Entry point for the bot: Language selection
293
- async def start(update: Update, context):
294
- user_lang = await get_user_language(update.effective_user.id, context) # Get current lang, default if not set
295
- if 'language_chosen' not in context.user_data: # Only show language selection on first start
296
- keyboard = [
297
- [InlineKeyboardButton("English 🇬🇧", callback_data="set_lang_en")],
298
- [InlineKeyboardButton("فارسی 🇮🇷", callback_data="set_lang_fa")]
299
- ]
300
- reply_markup = InlineKeyboardMarkup(keyboard)
301
- await update.message.reply_text(get_message(user_lang, "welcome"), reply_markup=reply_markup)
302
- return LANGUAGE_SELECTION
303
- else:
304
- # If language is already set, check membership and go to main menu
305
- if await check_membership(update, context):
306
- return await send_main_menu(update, context)
307
- return WAITING_FOR_MEMBERSHIP
308
-
309
- async def set_language(update: Update, context):
310
- query = update.callback_query
311
- await query.answer()
312
- lang = query.data.split('_')[2]
313
- context.user_data['language'] = lang
314
- context.user_data['language_chosen'] = True # Mark language as chosen
315
-
316
- user_lang = await get_user_language(query.from_user.id, context)
317
- await query.edit_message_text(get_message(user_lang, "language_chosen"))
318
-
319
- # After setting language, check membership
320
- if await check_membership(query, context): # Pass query for message update
321
- return await send_main_menu(query, context)
322
- return WAITING_FOR_MEMBERSHIP
323
-
324
- async def check_membership_callback(update: Update, context):
325
  query = update.callback_query
326
  await query.answer()
327
-
328
- if await check_membership(query, context):
329
- await query.edit_message_text(get_message(await get_user_language(query.from_user.id, context), "main_menu"),
330
- reply_markup=InlineKeyboardMarkup([
331
- [InlineKeyboardButton(get_message(await get_user_language(query.from_user.id, context), "audio_menu"), callback_data="audio_menu")],
332
- [InlineKeyboardButton(get_message(await get_user_language(query.from_user.id, context), "video_menu"), callback_data="video_menu")]
333
- ]))
334
- return MAIN_MENU
335
- return WAITING_FOR_MEMBERSHIP # Still waiting if not joined
336
 
337
- async def main_menu_callback(update: Update, context):
338
- query = update.callback_query
339
- await query.answer()
340
- user_lang = await get_user_language(query.from_user.id, context)
341
-
342
- if not await check_membership(query, context):
343
- return WAITING_FOR_MEMBERSHIP
344
-
345
- if query.data == "audio_menu":
346
- keyboard = [
347
- [InlineKeyboardButton(get_message(user_lang, "convert_voice_to_mp3"), callback_data="convert_voice_to_mp3")],
348
- [InlineKeyboardButton(get_message(user_lang, "convert_mp3_to_voice"), callback_data="convert_mp3_to_voice")],
349
- [InlineKeyboardButton(get_message(user_lang, "cut_audio"), callback_data="cut_audio")],
350
- [InlineKeyboardButton(get_message(user_lang, "back_to_main_menu"), callback_data="main_menu")]
351
- ]
352
- reply_markup = InlineKeyboardMarkup(keyboard)
353
- await query.edit_message_text(get_message(user_lang, "audio_menu_text"), reply_markup=reply_markup)
354
- return CONVERT_AUDIO_CHOICE
355
-
356
- elif query.data == "video_menu":
357
- keyboard = [
358
- [InlineKeyboardButton(get_message(user_lang, "convert_video_to_note"), callback_data="convert_video_to_note")],
359
- [InlineKeyboardButton(get_message(user_lang, "convert_note_to_video"), callback_data="convert_note_to_video")],
360
- [InlineKeyboardButton(get_message(user_lang, "back_to_main_menu"), callback_data="main_menu")]
361
- ]
362
- reply_markup = InlineKeyboardMarkup(keyboard)
363
- await query.edit_message_text(get_message(user_lang, "video_menu_text"), reply_markup=reply_markup)
364
- return VIDEO_CONVERSION_MODE
365
-
366
- elif query.data == "main_menu":
367
- await send_main_menu(query, context)
368
- return MAIN_MENU
369
-
370
- return MAIN_MENU
371
-
372
- # --- Audio Conversion Handlers ---
373
- async def start_convert_voice_to_mp3(update: Update, context):
374
- query = update.callback_query
375
- await query.answer()
376
- user_lang = await get_user_language(query.from_user.id, context)
377
-
378
- if not await check_membership(query, context):
379
- return WAITING_FOR_MEMBERSHIP
380
-
381
- await query.edit_message_text(get_message(user_lang, "send_voice_for_mp3"))
382
- return CONVERT_VOICE_TO_MP3
383
-
384
- async def handle_voice_to_mp3(update: Update, context):
385
- user_lang = await get_user_language(update.effective_user.id, context)
386
- if update.message.voice:
387
- file_id = update.message.voice.file_id
388
- file = await context.bot.get_file(file_id)
389
- input_file_path = os.path.join(DOWNLOAD_DIR, f"{file_id}.ogg")
390
- await file.download_to_drive(input_file_path)
391
-
392
- await update.message.reply_text(get_message(user_lang, "processing"))
393
- try:
394
- output_mp3_path = await convert_ogg_to_mp3(input_file_path)
395
- await update.message.reply_audio(audio=open(output_mp3_path, 'rb'), caption=get_message(user_lang, "audio_converted"))
396
- except Exception as e:
397
- print(f"Error converting voice to mp3: {e}", file=sys.stderr)
398
- await update.message.reply_text(get_message(user_lang, "error_occurred"))
399
- finally:
400
- os.remove(input_file_path)
401
- if 'output_mp3_path' in locals() and os.path.exists(output_mp3_path):
402
- os.remove(output_mp3_path)
403
-
404
- return await send_main_menu(update, context) # Go back to main menu
405
- else:
406
- await update.message.reply_text(get_message(user_lang, "no_voice_file"))
407
- return CONVERT_VOICE_TO_MP3
408
-
409
- async def start_convert_mp3_to_voice(update: Update, context):
410
- query = update.callback_query
411
- await query.answer()
412
- user_lang = await get_user_language(query.from_user.id, context)
413
-
414
- if not await check_membership(query, context):
415
- return WAITING_FOR_MEMBERSHIP
416
-
417
- await query.edit_message_text(get_message(user_lang, "send_mp3_for_voice"))
418
- return CONVERT_MP3_TO_VOICE
419
-
420
- async def handle_mp3_to_voice(update: Update, context):
421
- user_lang = await get_user_language(update.effective_user.id, context)
422
- if update.message.audio and update.message.audio.mime_type == 'audio/mpeg':
423
- file_id = update.message.audio.file_id
424
- file = await context.bot.get_file(file_id)
425
- input_file_path = os.path.join(DOWNLOAD_DIR, f"{file_id}.mp3")
426
- await file.download_to_drive(input_file_path)
427
-
428
- await update.message.reply_text(get_message(user_lang, "processing"))
429
- try:
430
- output_ogg_path = await convert_mp3_to_ogg_opus(input_file_path)
431
- # Send as voice message
432
- await update.message.reply_voice(voice=open(output_ogg_path, 'rb'), caption=get_message(user_lang, "audio_converted"))
433
- except Exception as e:
434
- print(f"Error converting mp3 to voice: {e}", file=sys.stderr)
435
- await update.message.reply_text(get_message(user_lang, "error_occurred"))
436
- finally:
437
- os.remove(input_file_path)
438
- if 'output_ogg_path' in locals() and os.path.exists(output_ogg_path):
439
- os.remove(output_ogg_path)
440
-
441
- return await send_main_menu(update, context) # Go back to main menu
442
- else:
443
- await update.message.reply_text(get_message(user_lang, "no_mp3_file"))
444
- return CONVERT_MP3_TO_VOICE
445
-
446
- # --- Audio Cutting Handlers ---
447
- async def start_cut_audio(update: Update, context):
448
- query = update.callback_query
449
- await query.answer()
450
- user_lang = await get_user_language(query.from_user.id, context)
451
-
452
- if not await check_membership(query, context):
453
- return WAITING_FOR_MEMBERSHIP
454
-
455
- await query.edit_message_text(get_message(user_lang, "cut_audio_intro"))
456
- return CUT_AUDIO_FILE
457
-
458
- async def handle_cut_audio_file(update: Update, context):
459
- user_lang = await get_user_language(update.effective_user.id, context)
460
- file_id = None
461
- file_extension = None
462
-
463
- if update.message.audio and update.message.audio.mime_type == 'audio/mpeg':
464
- file_id = update.message.audio.file_id
465
- file_extension = "mp3"
466
- context.user_data['audio_type'] = 'mp3'
467
- elif update.message.voice:
468
- file_id = update.message.voice.file_id
469
- file_extension = "ogg"
470
- context.user_data['audio_type'] = 'voice'
471
- else:
472
- await update.message.reply_text(get_message(user_lang, "no_audio_file"))
473
- return CUT_AUDIO_FILE # Stay in this state, waiting for audio file
474
-
475
- file = await context.bot.get_file(file_id)
476
- input_file_path = os.path.join(DOWNLOAD_DIR, f"{file_id}.{file_extension}")
477
- await file.download_to_drive(input_file_path)
478
- context.user_data['input_file_path'] = input_file_path
479
-
480
- await update.message.reply_text(get_message(user_lang, "cut_audio_range_prompt"))
481
- return CUT_AUDIO_RANGE
482
-
483
- async def handle_cut_audio_range(update: Update, context):
484
- user_lang = await get_user_language(update.effective_user.id, context)
485
- time_range_str = update.message.text
486
-
487
- match = re.match(r"^(\d{2})\.(\d{2})-(\d{2})\.(\d{2})$", time_range_str)
488
- if not match:
489
- await update.message.reply_text(get_message(user_lang, "invalid_time_format"))
490
- return CUT_AUDIO_RANGE # Stay in this state, waiting for correct format
491
-
492
- start_min, start_sec, end_min, end_sec = map(int, match.groups())
493
-
494
- start_ms = (start_min * 60 + start_sec) * 1000
495
- end_ms = (end_min * 60 + end_sec) * 1000
496
-
497
- input_file_path = context.user_data.get('input_file_path')
498
- if not input_file_path or not os.path.exists(input_file_path):
499
- await update.message.reply_text(get_message(user_lang, "error_occurred"))
500
- return await send_main_menu(update, context)
501
-
502
- try:
503
- audio_info = AudioSegment.from_file(input_file_path)
504
- audio_duration_ms = len(audio_info)
505
-
506
- # Apply max duration limits
507
- max_duration_ms = 5 * 60 * 1000 if context.user_data.get('audio_type') == 'mp3' else 2 * 60 * 1000
508
- if audio_duration_ms > max_duration_ms:
509
- # If file is longer than max allowed, cut only from the start to max_duration
510
- audio_duration_ms = max_duration_ms
511
-
512
- if end_ms > audio_duration_ms:
513
- end_ms = audio_duration_ms # Cap end time to actual (or limited) duration
514
-
515
- if start_ms >= end_ms:
516
- await update.message.reply_text(get_message(user_lang, "invalid_time_range")) # e.g. start > end
517
- return CUT_AUDIO_RANGE
518
-
519
- await update.message.reply_text(get_message(user_lang, "processing"))
520
-
521
- output_cut_path = await cut_audio_segment(input_file_path, start_ms, end_ms)
522
- await update.message.reply_audio(audio=open(output_cut_path, 'rb'), caption=get_message(user_lang, "audio_cut_success"))
523
-
524
- except Exception as e:
525
- print(f"Error cutting audio: {e}", file=sys.stderr)
526
- await update.message.reply_text(get_message(user_lang, "error_occurred"))
527
- finally:
528
- if input_file_path and os.path.exists(input_file_path):
529
- os.remove(input_file_path)
530
- if 'output_cut_path' in locals() and os.path.exists(output_cut_path):
531
- os.remove(output_cut_path)
532
- if 'input_file_path' in context.user_data:
533
- del context.user_data['input_file_path']
534
- if 'audio_type' in context.user_data:
535
- del context.user_data['audio_type']
536
-
537
- return await send_main_menu(update, context)
538
-
539
- # --- Video Conversion Handlers ---
540
- async def start_convert_video_to_note(update: Update, context):
541
- query = update.callback_query
542
- await query.answer()
543
- user_lang = await get_user_language(query.from_user.id, context)
544
-
545
- if not await check_membership(query, context):
546
- return WAITING_FOR_MEMBERSHIP
547
-
548
- await query.edit_message_text(get_message(user_lang, "send_video_for_note"))
549
- return CONVERT_VIDEO_TO_NOTE
550
-
551
- async def handle_video_to_note(update: Update, context):
552
- user_lang = await get_user_language(update.effective_user.id, context)
553
- if update.message.video:
554
- file_id = update.message.video.file_id
555
- file = await context.bot.get_file(file_id)
556
- input_file_path = os.path.join(DOWNLOAD_DIR, f"{file_id}.mp4") # Assume mp4, or get actual extension
557
- await file.download_to_drive(input_file_path)
558
-
559
- await update.message.reply_text(get_message(user_lang, "processing"))
560
- try:
561
- output_note_path = await convert_video_to_note(input_file_path)
562
- # Send as video note
563
- await update.message.reply_video_note(video_note=open(output_note_path, 'rb'))
564
- await update.message.reply_text(get_message(user_lang, "video_note_converted"))
565
- except Exception as e:
566
- print(f"Error converting video to note: {e}", file=sys.stderr)
567
- await update.message.reply_text(get_message(user_lang, "error_occurred"))
568
- finally:
569
- os.remove(input_file_path)
570
- if 'output_note_path' in locals() and os.path.exists(output_note_path):
571
- os.remove(output_note_path)
572
-
573
- return await send_main_menu(update, context)
574
- else:
575
- await update.message.reply_text(get_message(user_lang, "not_a_video"))
576
- return CONVERT_VIDEO_TO_NOTE
577
-
578
- async def start_convert_note_to_video(update: Update, context):
579
- query = update.callback_query
580
- await query.answer()
581
- user_lang = await get_user_language(query.from_user.id, context)
582
-
583
- if not await check_membership(query, context):
584
- return WAITING_FOR_MEMBERSHIP
585
-
586
- await query.edit_message_text(get_message(user_lang, "send_note_for_video"))
587
- return CONVERT_NOTE_TO_VIDEO
588
-
589
- async def handle_note_to_video(update: Update, context):
590
- user_lang = await get_user_language(update.effective_user.id, context)
591
- if update.message.video_note:
592
- file_id = update.message.video_note.file_id
593
- file = await context.bot.get_file(file_id)
594
- input_file_path = os.path.join(DOWNLOAD_DIR, f"{file_id}.mp4") # Video notes are mp4
595
- await file.download_to_drive(input_file_path)
596
-
597
- await update.message.reply_text(get_message(user_lang, "processing"))
598
- try:
599
- output_video_path = await convert_note_to_video(input_file_path)
600
- await update.message.reply_video(video=open(output_video_path, 'rb'), caption=get_message(user_lang, "video_converted"))
601
- except Exception as e:
602
- print(f"Error converting note to video: {e}", file=sys.stderr)
603
- await update.message.reply_text(get_message(user_lang, "error_occurred"))
604
- finally:
605
- os.remove(input_file_path)
606
- if 'output_video_path' in locals() and os.path.exists(output_video_path):
607
- os.remove(output_video_path)
608
-
609
- return await send_main_menu(update, context)
610
- else:
611
- await update.message.reply_text(get_message(user_lang, "not_a_video_note"))
612
- return CONVERT_NOTE_TO_VIDEO
613
-
614
- # --- Admin Panel Handlers ---
615
- async def admin_link(update: Update, context):
616
- user_lang = await get_user_language(update.effective_user.id, context)
617
- if update.effective_user.id != ADMIN_ID:
618
- await update.message.reply_text(get_message(user_lang, "not_admin"))
619
- return ConversationHandler.END # End conversation if not admin
620
 
 
621
  keyboard = [
622
- [InlineKeyboardButton(get_message(user_lang, "add_channel"), callback_data="admin_add_channel")],
623
- [InlineKeyboardButton(get_message(user_lang, "list_remove_channels"), callback_data="admin_list_remove_channels")],
624
- [InlineKeyboardButton(get_message(user_lang, "back_to_main_menu"), callback_data="main_menu")]
625
  ]
626
- reply_markup = InlineKeyboardMarkup(keyboard)
627
- await update.message.reply_text(get_message(user_lang, "admin_menu"), reply_markup=reply_markup)
628
- return ADMIN_MENU
629
-
630
- async def admin_menu_callback(update: Update, context):
631
- query = update.callback_query
632
- await query.answer()
633
- user_lang = await get_user_language(query.from_user.id, context)
634
-
635
- if query.from_user.id != ADMIN_ID:
636
- await query.edit_message_text(get_message(user_lang, "not_admin"))
637
- return ConversationHandler.END
638
-
639
- if query.data == "admin_add_channel":
640
- await query.edit_message_text(get_message(user_lang, "add_channel_prompt"), parse_mode=ParseMode.MARKDOWN)
641
- return ADD_CHANNEL
642
- elif query.data == "admin_list_remove_channels":
643
- await list_remove_channels(update, context) # Re-use the list function
644
- return LIST_REMOVE_CHANNELS
645
- elif query.data == "main_menu":
646
- return await send_main_menu(query, context)
647
-
648
- return ADMIN_MENU
649
-
650
- async def add_channel_handler(update: Update, context):
651
- user_lang = await get_user_language(update.effective_user.id, context)
652
- channel_identifier = update.message.text.strip()
653
-
654
- channel_id = None
655
- channel_name = None
656
-
657
- await update.message.reply_text(get_message(user_lang, "admin_permission_check").format(channel_name=channel_identifier))
658
-
659
- try:
660
- # Try to get chat object using identifier
661
- chat = await context.bot.get_chat(channel_identifier)
662
- channel_id = chat.id
663
- channel_name = chat.title if chat.title else chat.username if chat.username else str(chat.id)
664
-
665
- # Check if already added
666
- for c in REQUIRED_CHANNELS:
667
- if c['id'] == channel_id:
668
- await update.message.reply_text(get_message(user_lang, "channel_already_added"))
669
- return ADMIN_MENU # Go back to admin menu
670
-
671
- # Check bot's admin status in the channel
672
- bot_member = await context.bot.get_chat_member(chat_id=channel_id, user_id=context.bot.id)
673
- is_bot_admin = bot_member.status in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]
674
-
675
- can_invite_users = False
676
- can_get_chat_members = False
677
-
678
- if is_bot_admin:
679
- if bot_member.can_invite_users and bot_member.can_post_messages and bot_member.can_manage_chat and bot_member.can_delete_messages: # Check required permissions
680
- can_invite_users = True # Can manage chat and post means can invite
681
- if bot_member.can_manage_chat: # can_manage_chat often implies get_chat_member
682
- can_get_chat_members = True
683
-
684
- bot_status_message = ""
685
- if is_bot_admin and can_invite_users and can_get_chat_members:
686
- bot_status_message = get_message(user_lang, "bot_is_admin")
687
- elif is_bot_admin:
688
- bot_status_message = get_message(user_lang, "bot_not_admin") # Admin but missing specific perms
689
- else:
690
- bot_status_message = get_message(user_lang, "bot_not_admin") # Not admin at all
691
-
692
- REQUIRED_CHANNELS.append({
693
- "id": channel_id,
694
- "name": channel_name,
695
- "link": chat.invite_link if chat.invite_link else f"https://t.me/{chat.username}" if chat.username else f"https://t.me/c/{str(channel_id).replace('-100', '')}",
696
- "bot_is_admin": is_bot_admin,
697
- "bot_can_invite_users": can_invite_users,
698
- "bot_can_get_chat_members": can_get_chat_members
699
- })
700
- save_required_channels(REQUIRED_CHANNELS)
701
-
702
- await update.message.reply_text(get_message(user_lang, "channel_added_success").format(channel_name=channel_name, channel_id=channel_id, status=bot_status_message), parse_mode=ParseMode.MARKDOWN)
703
-
704
- except Exception as e:
705
- print(f"Error adding channel {channel_identifier}: {e}", file=sys.stderr)
706
- if "chat not found" in str(e).lower() or "bad request: chat_id is empty" in str(e).lower():
707
- await update.message.reply_text(get_message(user_lang, "channel_not_found"))
708
- elif "bot is not a member of the channel" in str(e).lower():
709
- await update.message.reply_text(get_message(user_lang, "channel_not_found"))
710
- else:
711
- await update.message.reply_text(get_message(user_lang, "error_occurred"))
712
-
713
- # Return to admin menu after adding
714
- keyboard = [
715
- [InlineKeyboardButton(get_message(user_lang, "add_channel"), callback_data="admin_add_channel")],
716
- [InlineKeyboardButton(get_message(user_lang, "list_remove_channels"), callback_data="admin_list_remove_channels")],
717
- [InlineKeyboardButton(get_message(user_lang, "back_to_main_menu"), callback_data="main_menu")]
718
- ]
719
- reply_markup = InlineKeyboardMarkup(keyboard)
720
- await update.message.reply_text(get_message(user_lang, "admin_menu"), reply_markup=reply_markup)
721
- return ADMIN_MENU
722
-
723
- async def list_remove_channels(update: Update, context):
724
- user_lang = await get_user_language(update.effective_user.id, context)
725
-
726
- if not REQUIRED_CHANNELS:
727
- message_text = get_message(user_lang, "no_channels_added")
728
- keyboard = [[InlineKeyboardButton(get_message(user_lang, "back_to_admin_menu"), callback_data="admin_menu")]]
729
- reply_markup = InlineKeyboardMarkup(keyboard)
730
- if update.callback_query:
731
- await update.callback_query.edit_message_text(message_text, reply_markup=reply_markup)
732
- else:
733
- await update.message.reply_text(message_text, reply_markup=reply_markup)
734
- return ADMIN_MENU
735
-
736
- message_text = get_message(user_lang, "list_channels").format(channels_list="")
737
- keyboard_buttons = []
738
-
739
- for i, channel in enumerate(REQUIRED_CHANNELS):
740
- channel_name = channel.get('name', channel['id'])
741
- message_text += f"{i+1}. {channel_name} (ID: `{channel['id']}`)\n"
742
- keyboard_buttons.append([InlineKeyboardButton(f"❌ {channel_name}", callback_data=f"remove_channel_{i}")])
743
-
744
- keyboard_buttons.append([InlineKeyboardButton(get_message(user_lang, "back_to_admin_menu"), callback_data="admin_menu")])
745
- reply_markup = InlineKeyboardMarkup(keyboard_buttons)
746
-
747
  if update.callback_query:
748
- await update.callback_query.edit_message_text(message_text, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
749
- else:
750
- await update.message.reply_text(message_text, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
751
-
752
- return LIST_REMOVE_CHANNELS
753
-
754
- async def remove_channel_handler(update: Update, context):
755
- query = update.callback_query
756
- await query.answer()
757
- user_lang = await get_user_language(query.from_user.id, context)
758
-
759
- if query.from_user.id != ADMIN_ID:
760
- await query.edit_message_text(get_message(user_lang, "not_admin"))
761
- return ConversationHandler.END
762
-
763
- index_to_remove = int(query.data.split('_')[2])
764
-
765
- if 0 <= index_to_remove < len(REQUIRED_CHANNELS):
766
- removed_channel = REQUIRED_CHANNELS.pop(index_to_remove)
767
- save_required_channels(REQUIRED_CHANNELS)
768
- await query.edit_message_text(get_message(user_lang, "channel_removed_success").format(channel_name=removed_channel.get('name', removed_channel['id'])))
769
  else:
770
- await query.edit_message_text(get_message(user_lang, "error_occurred"))
771
-
772
- # After removal, re-list channels or go back to admin menu
773
- await list_remove_channels(update, context) # Show updated list
774
- return LIST_REMOVE_CHANNELS
775
-
776
-
777
- # --- General Handlers ---
778
- async def cancel(update: Update, context):
779
- user_lang = await get_user_language(update.effective_user.id, context)
780
- await update.message.reply_text(get_message(user_lang, "cancel_operation"))
781
- return await send_main_menu(update, context)
782
-
783
- async def error_handler(update: Update, context):
784
- print(f"Error: {context.error} in update {update}", file=sys.stderr)
785
- if update.effective_message:
786
- user_lang = await get_user_language(update.effective_user.id, context)
787
- await update.effective_message.reply_text(get_message(user_lang, "error_occurred"))
788
-
789
- # Flask application init
790
- app = Flask(__name__)
791
- _application_instance = None
792
-
793
- async def get_telegram_application():
794
- global _application_instance
795
- if _application_instance is None:
796
- print("Initializing Telegram bot Application for this worker...")
797
- _app = Application.builder().token(TOKEN).build()
798
-
799
- # Define ConversationHandler with all states
800
- conv_handler = ConversationHandler(
801
- entry_points=[CommandHandler("start", start)],
802
- states={
803
- LANGUAGE_SELECTION: [
804
- CallbackQueryHandler(set_language, pattern=r"^set_lang_")
805
- ],
806
- WAITING_FOR_MEMBERSHIP: [
807
- CallbackQueryHandler(check_membership_callback, pattern="^check_membership_again$"),
808
- MessageHandler(filters.ALL & ~filters.COMMAND, lambda u, c: u.message.reply_text(get_message(c.user_data.get('language', 'fa'), "membership_required_stay"))) # Keep waiting
809
- ],
810
- MAIN_MENU: [
811
- CallbackQueryHandler(main_menu_callback, pattern="^(audio_menu|video_menu|main_menu)$")
812
- ],
813
- CONVERT_AUDIO_CHOICE: [
814
- CallbackQueryHandler(start_convert_voice_to_mp3, pattern="^convert_voice_to_mp3$"),
815
- CallbackQueryHandler(start_convert_mp3_to_voice, pattern="^convert_mp3_to_voice$"),
816
- CallbackQueryHandler(start_cut_audio, pattern="^cut_audio$"),
817
- CallbackQueryHandler(main_menu_callback, pattern="^main_menu$") # Back to main menu
818
- ],
819
- CONVERT_VOICE_TO_MP3: [
820
- MessageHandler(filters.VOICE, handle_voice_to_mp3),
821
- CommandHandler("cancel", cancel),
822
- MessageHandler(filters.ALL & ~filters.COMMAND, lambda u, c: u.message.reply_text(get_message(c.user_data.get('language', 'fa'), "no_voice_file"))) # Repeat prompt
823
- ],
824
- CONVERT_MP3_TO_VOICE: [
825
- MessageHandler(filters.AUDIO & filters.Document.MimeType("audio/mpeg"), handle_mp3_to_voice),
826
- CommandHandler("cancel", cancel),
827
- MessageHandler(filters.ALL & ~filters.COMMAND, lambda u, c: u.message.reply_text(get_message(c.user_data.get('language', 'fa'), "no_mp3_file"))) # Repeat prompt
828
- ],
829
- CUT_AUDIO_FILE: [
830
- MessageHandler(filters.AUDIO | filters.VOICE, handle_cut_audio_file),
831
- CommandHandler("cancel", cancel),
832
- MessageHandler(filters.ALL & ~filters.COMMAND, lambda u, c: u.message.reply_text(get_message(c.user_data.get('language', 'fa'), "no_audio_file"))) # Repeat prompt
833
- ],
834
- CUT_AUDIO_RANGE: [
835
- MessageHandler(filters.TEXT & ~filters.COMMAND, handle_cut_audio_range),
836
- CommandHandler("cancel", cancel),
837
- MessageHandler(filters.ALL & ~filters.COMMAND, lambda u, c: u.message.reply_text(get_message(c.user_data.get('language', 'fa'), "invalid_time_format"))) # Repeat prompt
838
- ],
839
- VIDEO_CONVERSION_MODE: [
840
- CallbackQueryHandler(start_convert_video_to_note, pattern="^convert_video_to_note$"),
841
- CallbackQueryHandler(start_convert_note_to_video, pattern="^convert_note_to_video$"),
842
- CallbackQueryHandler(main_menu_callback, pattern="^main_menu$") # Back to main menu
843
- ],
844
- CONVERT_VIDEO_TO_NOTE: [
845
- MessageHandler(filters.VIDEO, handle_video_to_note),
846
- CommandHandler("cancel", cancel),
847
- MessageHandler(filters.ALL & ~filters.COMMAND, lambda u, c: u.message.reply_text(get_message(c.user_data.get('language', 'fa'), "not_a_video")))
848
- ],
849
- CONVERT_NOTE_TO_VIDEO: [
850
- MessageHandler(filters.VIDEO_NOTE, handle_note_to_video),
851
- CommandHandler("cancel", cancel),
852
- MessageHandler(filters.ALL & ~filters.COMMAND, lambda u, c: u.message.reply_text(get_message(c.user_data.get('language', 'fa'), "not_a_video_note")))
853
- ],
854
- ADMIN_MENU: [
855
- CallbackQueryHandler(admin_menu_callback, pattern="^admin_(add_channel|list_remove_channels)$"),
856
- CallbackQueryHandler(main_menu_callback, pattern="^main_menu$")
857
- ],
858
- ADD_CHANNEL: [
859
- MessageHandler(filters.TEXT & ~filters.COMMAND, add_channel_handler),
860
- CommandHandler("cancel", cancel),
861
- CommandHandler("link", admin_link) # Allow re-entry to admin menu
862
- ],
863
- LIST_REMOVE_CHANNELS: [
864
- CallbackQueryHandler(remove_channel_handler, pattern="^remove_channel_\d+$"),
865
- CallbackQueryHandler(admin_menu_callback, pattern="^admin_menu$") # Back to admin menu
866
- ]
867
- },
868
- fallbacks=[
869
- CommandHandler("cancel", cancel),
870
- CommandHandler("start", start),
871
- CommandHandler("link", admin_link) # Link for admin access
872
- ],
873
- allow_reentry=True # Allow re-entry into the same conversation
874
- )
875
- _app.add_handler(conv_handler)
876
- _app.add_error_handler(error_handler)
877
-
878
- # Admin command list
879
- await _app.bot.set_my_commands([
880
- BotCommand("start", "Start the bot"),
881
- BotCommand("cancel", "Cancel current operation"),
882
- BotCommand("link", "Admin panel (Admin only)")
883
- ])
884
 
885
- await _app.initialize()
886
- print("Telegram bot Application initialized.")
887
- return _application_instance
888
 
889
- # Flask routes
890
  @app.route("/")
891
  async def index():
892
- return jsonify({"status": "ok", "message": "Telegram bot is running."})
893
 
894
  @app.route("/webhook", methods=["POST"])
895
  async def webhook():
@@ -901,29 +130,42 @@ async def webhook():
901
  @app.route("/set_webhook", methods=["GET"])
902
  async def set_webhook_route():
903
  application = await get_telegram_application()
904
- webhook_url = os.getenv("WEBHOOK_URL")
905
- if not webhook_url:
906
  return jsonify({"status": "error", "message": "WEBHOOK_URL not set."}), 500
907
- if not webhook_url.endswith("/webhook"):
908
- webhook_url = f"{webhook_url.rstrip('/')}/webhook"
909
  try:
910
- await application.bot.set_webhook(url=webhook_url)
911
- return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
912
  except Exception as e:
913
- print(f"Failed to set webhook: {e}", file=sys.stderr)
914
- return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
915
 
916
- # Debug run for local
917
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
918
  import asyncio
919
- print("Starting Flask app in local mode...")
920
- # Initialize the application in a separate asyncio task if running locally
921
- # Otherwise, it will be initialized by the webhook call in a production environment
922
- try:
923
- asyncio.run(get_telegram_application())
924
- except RuntimeError as e:
925
- if "cannot run loop while another loop is running" in str(e):
926
- print("Loop already running, likely in an IDE. Skipping explicit asyncio.run.")
927
- else:
928
- raise
929
- app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
 
1
+ # app.py
2
+ # نسخه نهایی کامل‌شده: پشتیبانی از همه قابلیت‌ها
3
+
4
  import os
5
  import sys
6
  import re
 
8
  import json
9
  from io import BytesIO
10
  from flask import Flask, request, jsonify
11
+ from telegram import Update, InputFile, InlineKeyboardButton, InlineKeyboardMarkup
12
  from telegram.ext import (
13
+ Application, CommandHandler, MessageHandler, CallbackQueryHandler,
14
+ filters, ConversationHandler
 
 
 
 
15
  )
16
+ from telegram.constants import ParseMode
17
  from pydub import AudioSegment
18
+ import nest_asyncio
19
 
20
+ # --- تنظیمات ---
21
  TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
 
 
 
 
22
  BOT_USERNAME = os.getenv("TELEGRAM_BOT_USERNAME", "Voice2mp3_RoBot")
23
  ADMIN_ID = int(os.getenv("TELEGRAM_ADMIN_ID", "0"))
24
+ WEBHOOK_URL = os.getenv("WEBHOOK_URL")
 
25
 
26
  DOWNLOAD_DIR = "/tmp/downloads"
27
  OUTPUT_DIR = "/tmp/outputs"
28
  CHANNELS_FILE = "channels.json"
 
29
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
30
  os.makedirs(OUTPUT_DIR, exist_ok=True)
31
 
32
+ LANGUAGE_SELECTION, MAIN_MENU, CONVERT_AUDIO, CUT_AUDIO_FILE, CUT_AUDIO_RANGE, \
33
+ VIDEO_CONVERSION_MODE, WAITING_FOR_MEMBERSHIP, ADMIN_MENU, ADD_CHANNEL, LIST_REMOVE_CHANNELS = range(10)
34
+
35
+ app = Flask(__name__)
36
+ _application_instance = None
37
 
38
+ # --- بارگذاری کانال‌ها ---
39
  def load_required_channels():
40
  if os.path.exists(CHANNELS_FILE):
41
  try:
42
  with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
43
  return json.load(f)
44
  except json.JSONDecodeError:
45
+ print("[!] channels.json خوانده نشد", file=sys.stderr)
46
  return []
47
 
48
  def save_required_channels(channels):
 
51
 
52
  REQUIRED_CHANNELS = load_required_channels()
53
 
54
+ # --- پیام‌ها ---
55
  MESSAGES = {
56
+ 'fa': {
57
+ 'start_welcome': "سلام! من یک ربات تبدیل فرمت صوتی و ویدیویی هستم.",
58
+ 'choose_language': "زبان خود را انتخاب کنید:",
59
+ 'main_menu_prompt': "چه کاری می‌خواهید انجام دهید؟",
60
+ 'btn_convert_format': "تبدیل فرمت صدا 🎵",
61
+ 'btn_cut_audio': "برش صدا ✂️",
62
+ 'btn_video_conversion': "تبدیل ویدیو دایره‌ای 🎥",
63
+ 'processing_start': " در حال پردازش...",
64
+ 'file_received': " فایل دریافت شد.",
65
+ 'conversion_done': "🎉 پردازش انجام شد!",
66
+ 'cancel_message': "عملیات لغو شد.",
67
+ 'membership_required': "برای ادامه، لطفاً ابتدا عضو کانال‌های زیر شوید:",
68
+ 'btn_join_channel': "عضو شدن 🤝",
69
+ 'btn_check_membership': "بررسی عضویت ",
70
+ 'not_admin': "شما دسترسی ندارید.",
71
+ 'admin_menu_prompt': "مدیریت کانال‌ها:",
72
+ 'btn_add_channel': " افزودن کانال",
73
+ 'btn_list_channels': "📋 لیست/حذف کانال‌ها",
74
+ 'send_channel_link': "لینک یا آیدی کانال را ارسال کنید:",
75
+ 'channel_added': "کانال افزوده شد.",
76
+ 'channel_removed': "کانال حذف شد.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
78
  }
79
 
80
+ def get_message(context, key):
81
+ lang = context.user_data.get('language', 'fa')
82
+ return MESSAGES.get(lang, MESSAGES['fa']).get(key, key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ # --- Handler های اصلی ---
85
+ async def start(update, context):
86
+ keyboard = [[InlineKeyboardButton("فارسی 🇮🇷", callback_data='set_lang_fa')]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  reply_markup = InlineKeyboardMarkup(keyboard)
88
+ await update.message.reply_text(get_message(context, 'choose_language'), reply_markup=reply_markup)
89
+ return LANGUAGE_SELECTION
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ async def set_language(update, context):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  query = update.callback_query
93
  await query.answer()
94
+ context.user_data['language'] = query.data.replace('set_lang_', '')
95
+ await query.edit_message_text(text=get_message(context, 'start_welcome'))
96
+ return await show_main_menu(update, context)
 
 
 
 
 
 
97
 
98
+ async def cancel(update, context):
99
+ await update.message.reply_text(get_message(context, 'cancel_message'))
100
+ return await show_main_menu(update, context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
+ async def show_main_menu(update, context):
103
  keyboard = [
104
+ [InlineKeyboardButton(get_message(context, 'btn_convert_format'), callback_data='select_convert_format')],
105
+ [InlineKeyboardButton(get_message(context, 'btn_cut_audio'), callback_data='select_cut_audio')],
106
+ [InlineKeyboardButton(get_message(context, 'btn_video_conversion'), callback_data='select_video_conversion')]
107
  ]
108
+ markup = InlineKeyboardMarkup(keyboard)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  if update.callback_query:
110
+ await update.callback_query.edit_message_text(text=get_message(context, 'main_menu_prompt'), reply_markup=markup)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  else:
112
+ await update.message.reply_text(text=get_message(context, 'main_menu_prompt'), reply_markup=markup)
113
+ return MAIN_MENU
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ # --- توابع تکمیلی (تبدیل، برش، بررسی عضویت، مدیریت ادمین و...) ---
116
+ # [در ادامه پیام‌ها افزوده خواهد شد به دلیل محدودیت حجم متن]
 
117
 
118
+ # --- مسیرهای Flask ---
119
  @app.route("/")
120
  async def index():
121
+ return jsonify({"status": "ok", "message": "Bot is running."})
122
 
123
  @app.route("/webhook", methods=["POST"])
124
  async def webhook():
 
130
  @app.route("/set_webhook", methods=["GET"])
131
  async def set_webhook_route():
132
  application = await get_telegram_application()
133
+ url = WEBHOOK_URL
134
+ if not url:
135
  return jsonify({"status": "error", "message": "WEBHOOK_URL not set."}), 500
136
+ if not url.endswith("/webhook"):
137
+ url = f"{url.rstrip('/')}/webhook"
138
  try:
139
+ await application.bot.set_webhook(url=url)
140
+ return jsonify({"status": "success", "message": f"Webhook set to {url}"})
141
  except Exception as e:
142
+ print(f"Webhook error: {e}", file=sys.stderr)
143
+ return jsonify({"status": "error", "message": str(e)}), 500
144
 
145
+ # --- راه‌اندازی ---
146
+ async def get_telegram_application():
147
+ global _application_instance
148
+ if _application_instance is None:
149
+ print("راه‌اندازی ربات تلگرام...")
150
+ app_ = Application.builder().token(TOKEN).build()
151
+ conv = ConversationHandler(
152
+ entry_points=[CommandHandler("start", start)],
153
+ states={
154
+ LANGUAGE_SELECTION: [CallbackQueryHandler(set_language, pattern="^set_lang_.*")],
155
+ MAIN_MENU: [],
156
+ },
157
+ fallbacks=[CommandHandler("cancel", cancel), CommandHandler("start", start)],
158
+ allow_reentry=True
159
+ )
160
+ app_.add_handler(conv)
161
+ _application_instance = app_
162
+ await _application_instance.initialize()
163
+ print("ربات آماده است.")
164
+ return _application_instance
165
+
166
+ if __name__ == '__main__':
167
  import asyncio
168
+ nest_asyncio.apply()
169
+ print("اجرای لوکال...")
170
+ asyncio.run(get_telegram_application())
171
+ app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))