akdNIKY commited on
Commit
44815e1
·
verified ·
1 Parent(s): b3f52dc

Upload app.py

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