akdNIKY commited on
Commit
0f9dcf0
·
verified ·
1 Parent(s): 56a8909

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -568
app.py CHANGED
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
  import os
3
  import sys
4
  import re
@@ -20,632 +19,111 @@ from telegram.ext import (
20
  from telegram.constants import ParseMode
21
  from pydub import AudioSegment
22
 
23
- # --- 1. تنظیمات و متغیرهای سراسری ---
24
-
25
- # ⚠️⚠️⚠️ بسیار مهم: این توکن را با توکن واقعی ربات خود که از BotFather دریافت کرده‌اید جایگزین کنید.
26
- # می‌توانید توکن را به عنوان یک متغیر محیطی (Environment Variable) به نام TELEGRAM_BOT_TOKEN نیز تعریف کنید.
27
- TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8035336072:AAHG9REotvM4u8DgreC7hu8gIAroxhS-N-M")
28
- if "YOUR_TELEGRAM_BOT_TOKEN_HERE" in TOKEN:
29
- print("خطا: لطفا توکن ربات تلگرام خود را در کد جایگزین کنید.", file=sys.stderr)
30
  sys.exit(1)
31
 
32
- # نام کاربری ربات و آیدی ادمین
33
  BOT_USERNAME = os.getenv("TELEGRAM_BOT_USERNAME", "Voice2mp3_RoBot")
34
- # ⚠️⚠️⚠️ این را به آیدی عددی تلگرام خودتان تغییر دهید.
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:
52
  return json.load(f)
53
  except json.JSONDecodeError:
54
- print(f"خطا در خواندن فایل JSON کانال‌ها: {CHANNELS_FILE}", file=sys.stderr)
55
- return []
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برای شروع، از منوی زیر یک قابلیت را انتخاب کنید.",
77
- 'choose_language': "زبان مورد نظر خود را انتخاب کنید:",
78
- 'processing_start': "⏳ در حال شروع پردازش...",
79
- 'file_received': "⬇️ فایل دریافت شد. در حال تبدیل...",
80
- 'conversion_done': "⚙️ تبدیل فرمت انجام شد. در حال ارسال...",
81
- 'mp3_to_voice_reply': "ویس تلگرام شما (تبدیل شده از MP3)",
82
- 'voice_to_mp3_caption': "فایل MP3 شما (تبدیل شده از ویس تلگرام)",
83
- 'error_mp3_to_voice': "❌ خطا در تبدیل MP3 به ویس تلگرام: ",
84
- 'error_voice_to_mp3': "❌ خطا در تبدیل ویس تلگرام به MP3: ",
85
- 'general_error': "متاسفم، مشکلی پیش آمد. لطفاً دوباره تلاش کنید.",
86
- 'main_menu_prompt': "چه کاری می‌خواهید روی فایل خود انجام دهید؟",
87
- 'btn_convert_format': "تغییر فرمت صدا 🎵",
88
- 'btn_cut_audio': "برش قسمتی از صدا ✂️",
89
- 'btn_video_conversion': "تبدیل ویدیو دایره‌ای 🎥",
90
- 'convert_mode_active': "شما در حالت 'تغییر فرمت صدا' هستید. حالا فایل صوتی (ویس یا MP3) خود را برای من ارسال کنید.",
91
- 'cut_mode_active_file': "شما در قسمت 'برش و کات کردن صدا' هستید.\n\nابتدا فایل صوتی (MP3 یا ویس) خود را ارسال کنید.",
92
- 'cut_mode_active_range': "حالا بازه زمانی مورد نظر برای برش را به صورت 'دقیقه.ثانیه-دقیقه.ثانیه' (مثال: 00.21-00.54) ارسال کنید.",
93
- 'invalid_time_format': "فرمت زمان وارد شده صحیح نیست. لطفاً از فرمت 'MM.SS-MM.SS' استفاده کنید. (مثال: 00.21-00.54)",
94
- 'invalid_time_range': "بازه زمانی نامعتبر است یا زمان پایان از زمان شروع کمتر است. لطفاً بازه صحیح را وارد کنید.",
95
- 'audio_cut_success': "✅ برش صدا با موفقیت انجام شد. فایل شما آماده است.",
96
- 'no_audio_for_cut': "فایلی برای برش پیدا نشد. لطفاً ابتدا فایل صوتی را ارسال کنید.",
97
- 'cut_processing': "✂️ در حال برش صدا...",
98
- 'returning_to_main_menu': "بازگشت به منوی اصلی...",
99
- 'cancel_message': "عملیات لغو شد. به منوی اصلی بازگشتید.",
100
- 'video_conversion_mode_active': "شما در حالت 'تبدیل ویدیو دایره‌ای' هستید.\n\nیک ویدیو معمولی یا یک ویدیو دایره‌ای (Video Message) برای من ارسال کنید.",
101
- 'file_received_video': "⬇️ فایل ویدیویی دریافت شد. در حال پردازش...",
102
- 'converting_video_note_to_video': "🔄 در حال تبدیل ویدیو دایره‌ای به ویدیو معمولی...",
103
- 'converting_video_to_video_note': "🔄 در حال تبدیل ویدیو معمولی به ویدیو دایره‌ای...",
104
- 'conversion_done_video': "✅ تبدیل ویدیو با موفقیت انجام شد. در حال ارسال...",
105
- 'video_note_to_video_caption': "ویدیو معمولی شما (تبدیل شده از ویدیو دایره‌ای)",
106
- 'video_to_video_note_reply': "ویدیو دایره‌ای شما (تبدیل شده از ویدیو معمولی)",
107
- 'error_video_conversion': "❌ خطا در تبدیل ویدیو: ",
108
- 'invalid_file_type_video': "لطفاً یک فایل ویدیویی یا ویدیو دایره‌ای ارسال کنید.",
109
- 'membership_required': "برای ادامه کار با ربات و استفاده نامحدود، لطفاً ابتدا عضو کانال‌های زیر شوید:",
110
- 'btn_join_channel': "عضو شدن 🤝",
111
- 'btn_check_membership': "بررسی عضویت ✅",
112
- 'membership_success': "✅ عضویت شما تأیید شد! اکنون می‌توانید به صورت نامحدود از ربات استفاده کنید.",
113
- 'membership_failed': "❌ متاسفم، شما هنوز عضو تمام کانال‌های مورد نیاز نیستید. لطفاً ابتدا عضو شوید و سپس دوباره 'بررسی عضویت' را بزنید.",
114
- 'not_admin': "شما اجازه دسترسی به این بخش را ندارید.",
115
- 'admin_menu_prompt': "به پنل مدیریت لینک‌ها خوش آمدید:",
116
- 'btn_add_channel': "افزودن لینک کانال ➕",
117
- 'btn_list_channels': "لیست کانال‌ها و حذف 🗑️",
118
- 'send_channel_link': "لطفاً لینک (مانند @mychannel) یا آیدی عددی کانال را ارسال کنید:",
119
- 'channel_added': "✅ کانال '{channel_id}' با موفقیت اضافه شد.",
120
- 'channel_already_exists': "❗️ این کانال قبلاً اضافه شده است.",
121
- 'no_channels_configured': "هیچ کانالی برای عضویت پیکربندی نشده است.",
122
- 'channel_list_prompt': "لیست کانال‌های فعلی برای عضویت اجباری:",
123
- 'btn_remove_channel': "حذف ❌",
124
- 'channel_removed': "✅ کانال '{channel_id}' با موفقیت حذف شد.",
125
- 'channel_not_found': "❗️ کانال مورد نظر یافت نشد.",
126
- 'invalid_channel_id': "آیدی/لینک کانال نامعتبر است. لطفاً @username یا آیدی عددی (مانند -1001234567890) را ارسال کنید.",
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
250
- return await feature_func(update, context, *args, **kwargs)
251
- else:
252
- context.user_data['is_member'] = False
253
- return await show_membership_required_message(update, context)
254
-
255
- # --- 5. تمام توابع Handler (مدیریت دستورات و پیام‌ها) ---
256
-
257
- async def start(update: Update, context):
258
- """دستور /start را مدیریت کرده و درخواست انتخاب زبان می‌کند."""
259
- keyboard = [
260
- [InlineKeyboardButton("فارسی 🇮🇷", callback_data='set_lang_fa')],
261
- [InlineKeyboardButton("English 🇬🇧", callback_data='set_lang_en')]
262
- ]
263
- reply_markup = InlineKeyboardMarkup(keyboard)
264
- await update.message.reply_text(
265
- "زبان مورد نظر خود را انتخاب کنید:\nChoose your preferred language:",
266
- reply_markup=reply_markup
267
- )
268
- return LANGUAGE_SELECTION
269
-
270
- async def set_language(update: Update, context):
271
- """زبان کاربر را تنظیم کرده و به منوی اصلی می‌رود."""
272
- query = update.callback_query
273
- await query.answer()
274
- context.user_data['language'] = query.data.replace('set_lang_', '')
275
- await query.edit_message_text(text=get_message(context, 'start_welcome'))
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)
628
- return "error", 500
629
 
630
  @app.route("/set_webhook", methods=["GET"])
631
  async def set_webhook_route():
632
- """یک مسیر برای تنظیم وبهوک به صورت خودکار."""
633
  webhook_url = os.getenv("WEBHOOK_URL")
634
  if not webhook_url:
635
- return jsonify({"status": "error", "message": "WEBHOOK_URL environment variable not set."}), 500
636
 
637
  if not webhook_url.endswith("/webhook"):
638
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
639
 
640
  try:
641
- application = await get_telegram_application()
642
  await application.bot.set_webhook(url=webhook_url)
643
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
644
  except Exception as e:
645
  print(f"Failed to set webhook: {e}", file=sys.stderr)
646
  return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
647
 
 
648
  if __name__ == "__main__":
649
- # این بخش برای اجرای محلی (local) سرور Flask است
650
- # در محیط‌های production مانند Hugging Face، یک وب سرور مانند Gunicorn این فایل را اجرا می‌کند.
 
651
  app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import sys
3
  import re
 
19
  from telegram.constants import ParseMode
20
  from pydub import AudioSegment
21
 
22
+ # Environment variables
23
+ TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
24
+ if not TOKEN:
25
+ print("Error: TELEGRAM_BOT_TOKEN environment variable not set.", file=sys.stderr)
 
 
 
26
  sys.exit(1)
27
 
 
28
  BOT_USERNAME = os.getenv("TELEGRAM_BOT_USERNAME", "Voice2mp3_RoBot")
29
+ ADMIN_ID = int(os.getenv("TELEGRAM_ADMIN_ID", "0"))
30
+ if ADMIN_ID == 0:
31
+ print("Warning: TELEGRAM_ADMIN_ID not set or 0.", file=sys.stderr)
32
 
 
 
33
  DOWNLOAD_DIR = "/tmp/downloads"
34
  OUTPUT_DIR = "/tmp/outputs"
35
+ CHANNELS_FILE = "channels.json"
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, CUT_AUDIO_FILE, CUT_AUDIO_RANGE, \
41
+ VIDEO_CONVERSION_MODE, WAITING_FOR_MEMBERSHIP, ADMIN_MENU, ADD_CHANNEL, LIST_REMOVE_CHANNELS = range(10)
42
+
43
+ # Load and save channels
44
 
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}.", file=sys.stderr)
 
52
  return []
53
 
54
  def save_required_channels(channels):
 
55
  with open(CHANNELS_FILE, 'w', encoding='utf-8') as f:
56
  json.dump(channels, f, indent=4, ensure_ascii=False)
57
 
58
  REQUIRED_CHANNELS = load_required_channels()
59
 
60
+ # Telegram application init
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  app = Flask(__name__)
62
  _application_instance = None
63
 
64
  async def get_telegram_application():
 
65
  global _application_instance
66
  if _application_instance is None:
67
  print("Initializing Telegram bot Application for this worker...")
68
+ _app = Application.builder().token(TOKEN).build()
69
 
70
  conv_handler = ConversationHandler(
71
+ entry_points=[CommandHandler("start", start)],
72
+ states={},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  fallbacks=[CommandHandler("cancel", cancel), CommandHandler("start", start)],
74
  allow_reentry=True
75
  )
76
 
77
+ _app.add_handler(conv_handler)
78
+ _app.add_error_handler(error_handler)
79
+ await _app.initialize() # <-- Fix: proper initialization
80
+ _application_instance = _app
81
  print("Telegram bot Application initialized.")
82
  return _application_instance
83
 
84
+ # Flask routes
85
+
86
  @app.route("/")
87
+ async def index():
88
+ return jsonify({"status": "ok", "message": "Telegram bot is running."})
 
89
 
90
  @app.route("/webhook", methods=["POST"])
91
  async def webhook():
92
+ application = await get_telegram_application()
93
+ update = Update.de_json(request.get_json(force=True), application.bot)
94
+ await application.process_update(update)
95
+ return "ok"
 
 
 
 
 
96
 
97
  @app.route("/set_webhook", methods=["GET"])
98
  async def set_webhook_route():
99
+ application = await get_telegram_application()
100
  webhook_url = os.getenv("WEBHOOK_URL")
101
  if not webhook_url:
102
+ return jsonify({"status": "error", "message": "WEBHOOK_URL not set."}), 500
103
 
104
  if not webhook_url.endswith("/webhook"):
105
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
106
 
107
  try:
 
108
  await application.bot.set_webhook(url=webhook_url)
109
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
110
  except Exception as e:
111
  print(f"Failed to set webhook: {e}", file=sys.stderr)
112
  return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
113
 
114
+ # Debug run for local
115
  if __name__ == "__main__":
116
+ import asyncio
117
+ print("Starting Flask app in local mode...")
118
+ asyncio.run(get_telegram_application())
119
  app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
120
+
121
+ # Placeholder handlers (implement as needed)
122
+ async def start(update, context):
123
+ await update.message.reply_text("Bot started!")
124
+
125
+ async def cancel(update, context):
126
+ await update.message.reply_text("Operation cancelled.")
127
+
128
+ async def error_handler(update, context):
129
+ print(f"Error: {context.error}", file=sys.stderr)