akdNIKY commited on
Commit
c5344a1
·
verified ·
1 Parent(s): bb66be5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +24 -502
app.py CHANGED
@@ -1,494 +1,32 @@
1
- # -*- coding: utf-8 -*-
 
 
2
  import os
3
- import sys
4
- import re
5
- import subprocess
6
- import json
7
- from io import BytesIO
8
  import asyncio
9
-
10
  from flask import Flask, request, jsonify
 
 
 
11
 
12
- from telegram import Update, InputFile, InlineKeyboardButton, InlineKeyboardMarkup
13
- from telegram.ext import (
14
- Application,
15
- CommandHandler,
16
- MessageHandler,
17
- CallbackQueryHandler,
18
- filters,
19
- ConversationHandler
20
- )
21
- from telegram.constants import ParseMode
22
- from pydub import AudioSegment
23
-
24
- # --- 1. تنظیمات و متغیرهای سراسری ---
25
-
26
- # ⚠️⚠️⚠️ بسیار مهم: این توکن را با توکن واقعی ربات خود که از BotFather دریافت کرده‌اید جایگزین کنید.
27
- TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "8035336072:AAHG9REotvM4u8DgreC7hu8gIAroxhS-N-M")
28
- if "YOUR_TELEGRAM_BOT_TOKEN_HERE" in TOKEN:
29
- print("خطا: لطفا توکن ربات تلگرام خود را در کد جایگزین کنید.", file=sys.stderr)
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
- DOWNLOAD_DIR = "/tmp/downloads"
39
- OUTPUT_DIR = "/tmp/outputs"
40
- CHANNELS_FILE = "/tmp/channels.json"
41
- os.makedirs(DOWNLOAD_DIR, exist_ok=True)
42
- os.makedirs(OUTPUT_DIR, exist_ok=True)
43
-
44
- # --- 2. توابع مدیریت کانال‌ها ---
45
-
46
- def load_required_channels():
47
- if os.path.exists(CHANNELS_FILE):
48
- try:
49
- with open(CHANNELS_FILE, 'r', encoding='utf-8') as f:
50
- return json.load(f)
51
- except json.JSONDecodeError:
52
- print(f"خطا در خواندن فایل JSON کانال‌ها: {CHANNELS_FILE}", file=sys.stderr)
53
- return []
54
- return []
55
-
56
- def save_required_channels(channels):
57
- with open(CHANNELS_FILE, 'w', encoding='utf-8') as f:
58
- json.dump(channels, f, indent=4, ensure_ascii=False)
59
-
60
- REQUIRED_CHANNELS = load_required_channels()
61
-
62
- # --- 3. تعریف حالت‌های مکالمه و پیام‌ها ---
63
- (LANGUAGE_SELECTION, MAIN_MENU, CONVERT_AUDIO, CUT_AUDIO_FILE, CUT_AUDIO_RANGE,
64
- VIDEO_CONVERSION_MODE, WAITING_FOR_MEMBERSHIP, ADMIN_MENU, ADD_CHANNEL,
65
- LIST_REMOVE_CHANNELS) = range(10)
66
-
67
- MESSAGES = {
68
- 'fa': {
69
- 'start_welcome': "سلام! من یک ربات تبدیل فرمت صوتی و ویدیویی هستم.\n\nبرای شروع، از منوی زیر یک قابلیت را انتخاب کنید.",
70
- 'choose_language': "زبان مورد نظر خود را انتخاب کنید:",
71
- 'processing_start': "⏳ در حال شروع پردازش...",
72
- 'file_received': "⬇️ فایل دریافت شد. در حال تبدیل...",
73
- 'conversion_done': "⚙️ تبدیل فرمت انجام شد. در حال ارسال...",
74
- 'mp3_to_voice_reply': "ویس تلگرام شما (تبدیل شده از MP3)",
75
- 'voice_to_mp3_caption': "فایل MP3 شما (تبدیل شده از ویس تلگرام)",
76
- 'error_mp3_to_voice': "❌ خطا در تبدیل MP3 به ویس تلگرام: ",
77
- 'error_voice_to_mp3': "❌ خطا در تبدیل ویس تلگرام به MP3: ",
78
- 'general_error': "متاسفم، مشکلی پیش آمد. لطفاً دوباره تلاش کنید.",
79
- 'main_menu_prompt': "چه کاری می‌خواهید روی فایل خود انجام دهید؟",
80
- 'btn_convert_format': "تغییر فرمت صدا 🎵",
81
- 'btn_cut_audio': "برش قسمتی از صدا ✂️",
82
- 'btn_video_conversion': "تبدیل ویدیو دایره‌ای 🎥",
83
- 'convert_mode_active': "شما در حالت 'تغییر فرمت صدا' هستید. حالا فایل صوتی (ویس یا MP3) خود را برای من ارسال کنید.",
84
- 'cut_mode_active_file': "شما در قسمت 'برش و کات کردن صدا' هستید.\n\nابتدا فایل صوتی (MP3 یا ویس) خود را ارسال کنید.",
85
- 'cut_mode_active_range': "حالا بازه زمانی مورد نظر برای برش را به صورت 'دقیقه.ثانیه-دقیقه.ثانیه' (مثال: 00.21-00.54) ارسال کنید.",
86
- 'invalid_time_format': "فرمت زمان وارد شده صحیح نیست. لطفاً از فرمت 'MM.SS-MM.SS' استفاده کنید. (مثال: 00.21-00.54)",
87
- 'invalid_time_range': "بازه زمانی نامعتبر است ��ا زمان پایان از زمان شروع کمتر است. لطفاً بازه صحیح را وارد کنید.",
88
- 'audio_cut_success': "✅ برش صدا با موفقیت انجام شد. فایل شما آماده است.",
89
- 'no_audio_for_cut': "فایلی برای برش پیدا نشد. لطفاً ابتدا فایل صوتی را ارسال کنید.",
90
- 'cut_processing': "✂️ در حال برش صدا...",
91
- 'returning_to_main_menu': "بازگشت به منوی اصلی...",
92
- 'cancel_message': "عملیات لغو شد. به منوی اصلی بازگشتید.",
93
- 'video_conversion_mode_active': "شما در حالت 'تبدیل ویدیو دایره‌ای' هستید.\n\nیک ویدیو معمولی یا یک ویدیو دایره‌ای (Video Message) برای من ارسال کنید.",
94
- 'file_received_video': "⬇️ فایل ویدیویی دریافت شد. در حال پردازش...",
95
- 'converting_video_note_to_video': "🔄 در حال تبدیل ویدیو دایره‌ای به ویدیو معمولی...",
96
- 'converting_video_to_video_note': "🔄 در حال تبدیل ویدیو معمولی به ویدیو دایره‌ای...",
97
- 'conversion_done_video': "✅ تبدیل ویدیو با موفقیت انجام شد. در حال ارسال...",
98
- 'video_note_to_video_caption': "ویدیو معمولی شما (تبدیل شده از ویدیو دایره‌ای)",
99
- 'video_to_video_note_reply': "ویدیو دایره‌ای شما (تبدیل شده از ویدیو معمولی)",
100
- 'error_video_conversion': "❌ خطا در تبدیل ویدیو: ",
101
- 'invalid_file_type_video': "لطفاً یک فایل ویدیویی یا ویدیو دایره‌ای ارسال کنید.",
102
- 'membership_required': "برای ادامه کار با ربات و استفاده نامحدود، لطفاً ابتدا عضو کانال‌های زیر شوید:",
103
- 'btn_join_channel': "عضو شدن 🤝",
104
- 'btn_check_membership': "بررسی عضویت ✅",
105
- 'membership_success': "✅ عضویت شما تأیید شد! اکنون می‌توانید به صورت نامحدود از ربات استفاده کنید.",
106
- 'membership_failed': "❌ متاسفم، شما هنوز عضو تمام کانال‌های مورد نیاز نیستید. لطفاً ابتدا عضو شوید و سپس دوباره 'بررسی عضویت' را بزنید.",
107
- 'not_admin': "شما اجازه دسترسی به این بخش را ندارید.",
108
- 'admin_menu_prompt': "به پنل مدیریت لینک‌ها خوش آمدید:",
109
- 'btn_add_channel': "افزودن لینک کانال ➕",
110
- 'btn_list_channels': "لیست کانال‌ها و حذف 🗑️",
111
- 'send_channel_link': "لطفاً لینک (مانند @mychannel) یا آیدی عددی کانال را ارسال کنید:",
112
- 'channel_added': "✅ کانال '{channel_id}' با موفقیت اضافه شد.",
113
- 'channel_already_exists': "❗️ این کانال قبلاً اضافه شده است.",
114
- 'no_channels_configured': "هیچ کانالی برای عضویت پیکربندی نشده است.",
115
- 'channel_list_prompt': "لیست کانال‌های فعلی برای عضویت اجباری:",
116
- 'btn_remove_channel': "حذف ❌",
117
- 'channel_removed': "✅ کانال '{channel_id}' با موفقیت حذف شد.",
118
- 'channel_not_found': "❗️ کانال مورد نظر یافت نشد.",
119
- 'invalid_channel_id': "آیدی/لینک کانال نامعتبر است. لطفاً @username یا آیدی عددی (مانند -1001234567890) را ارسال کنید.",
120
- 'bot_not_admin_in_channel': "ربات ادمین کانال '{channel_id}' نیست یا مجوزهای کافی برای بررسی عضویت را ندارد. لطفاً ربات را به عنوان ادمین با مجوز 'بررسی وضعیت اعضا' در کانال اضافه کنید."
121
- },
122
- 'en': {
123
- # English messages go here
124
- }
125
- }
126
-
127
- # --- 4. توابع کمکی ---
128
-
129
- def get_message(context, key, **kwargs):
130
- lang = context.user_data.get('language', 'fa')
131
- message_template = MESSAGES.get(lang, MESSAGES['fa']).get(key, MESSAGES['fa'].get(key, "Message key not found"))
132
- return message_template.format(**kwargs)
133
-
134
- def parse_time_to_ms(time_str):
135
- match = re.match(r'^(\d{2})\.(\d{2})$', time_str)
136
- if not match: raise ValueError("Invalid time format")
137
- minutes, seconds = int(match.group(1)), int(match.group(2))
138
- if seconds >= 60: raise ValueError("Seconds must be between 00 and 59")
139
- return (minutes * 60 + seconds) * 1000
140
-
141
- async def check_user_membership(update: Update, context):
142
- user_id = update.effective_user.id
143
- if not REQUIRED_CHANNELS: return True
144
- for channel_id in REQUIRED_CHANNELS:
145
- try:
146
- chat_member = await context.bot.get_chat_member(chat_id=channel_id, user_id=user_id)
147
- if chat_member.status not in ['member', 'administrator', 'creator']: return False
148
- except Exception as e:
149
- print(f"خطا در بررسی عضویت کانال {channel_id}: {e}", file=sys.stderr)
150
- return False
151
- return True
152
-
153
- async def show_membership_required_message(update: Update, context):
154
- keyboard = []
155
- if not REQUIRED_CHANNELS: return await show_main_menu(update, context)
156
- for channel_id in REQUIRED_CHANNELS:
157
- try:
158
- chat = await context.bot.get_chat(chat_id=channel_id)
159
- url = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
160
- if url: keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_join_channel')} {chat.title or channel_id}", url=url)])
161
- except Exception as e:
162
- print(f"ناتوان در دریافت اطلاعات کانال {channel_id}: {e}", file=sys.stderr)
163
- keyboard.append([InlineKeyboardButton(get_message(context, 'btn_check_membership'), callback_data='check_membership')])
164
- reply_markup = InlineKeyboardMarkup(keyboard)
165
- message_sender = update.callback_query.edit_message_text if update.callback_query else update.effective_message.reply_text
166
- await message_sender(get_message(context, 'membership_required'), reply_markup=reply_markup)
167
- return WAITING_FOR_MEMBERSHIP
168
-
169
- async def process_feature_or_check_membership(update: Update, context, feature_func, *args, **kwargs):
170
- if update.effective_user.id == ADMIN_ID or not REQUIRED_CHANNELS:
171
- return await feature_func(update, context, *args, **kwargs)
172
- is_member = await check_user_membership(update, context)
173
- if is_member:
174
- context.user_data['is_member'] = True
175
- return await feature_func(update, context, *args, **kwargs)
176
- else:
177
- context.user_data['is_member'] = False
178
- return await show_membership_required_message(update, context)
179
-
180
- # --- 5. توابع Handler (مدیریت دستورات و پیام‌ها) ---
181
-
182
- async def start(update: Update, context):
183
- keyboard = [[InlineKeyboardButton("فارسی 🇮🇷", callback_data='set_lang_fa')], [InlineKeyboardButton("English 🇬🇧", callback_data='set_lang_en')]]
184
- reply_markup = InlineKeyboardMarkup(keyboard)
185
- await update.message.reply_text("زبان مورد نظر خود را انتخاب کنید:\nChoose your preferred language:", reply_markup=reply_markup)
186
- return LANGUAGE_SELECTION
187
-
188
- async def set_language(update: Update, context):
189
- query = update.callback_query
190
- await query.answer()
191
- context.user_data['language'] = query.data.replace('set_lang_', '')
192
- await query.edit_message_text(text=get_message(context, 'start_welcome'))
193
- return await show_main_menu(update, context)
194
-
195
- async def show_main_menu(update: Update, context):
196
- keyboard = [
197
- [InlineKeyboardButton(get_message(context, 'btn_convert_format'), callback_data='select_convert_format')],
198
- [InlineKeyboardButton(get_message(context, 'btn_cut_audio'), callback_data='select_cut_audio')],
199
- [InlineKeyboardButton(get_message(context, 'btn_video_conversion'), callback_data='select_video_conversion')]
200
- ]
201
- reply_markup = InlineKeyboardMarkup(keyboard)
202
- message_sender = update.callback_query.edit_message_text if update.callback_query else update.message.reply_text
203
- await message_sender(text=get_message(context, 'main_menu_prompt'), reply_markup=reply_markup)
204
- return MAIN_MENU
205
-
206
- async def change_format_selected(update: Update, context):
207
- query = update.callback_query
208
- await query.answer()
209
- await query.edit_message_text(text=get_message(context, 'convert_mode_active'))
210
- return CONVERT_AUDIO
211
-
212
- async def cut_audio_selected(update: Update, context):
213
- query = update.callback_query
214
- await query.answer()
215
- await query.edit_message_text(text=get_message(context, 'cut_mode_active_file'))
216
- context.user_data.pop('audio_for_cut_path', None)
217
- return CUT_AUDIO_FILE
218
-
219
- async def video_conversion_selected(update: Update, context):
220
- query = update.callback_query
221
- await query.answer()
222
- await query.edit_message_text(text=get_message(context, 'video_conversion_mode_active'))
223
- return VIDEO_CONVERSION_MODE
224
-
225
- async def handle_audio(update: Update, context):
226
- async def _perform_conversion(update, context):
227
- file_id = update.message.audio.file_id
228
- processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
229
- download_path = os.path.join(DOWNLOAD_DIR, f"in_{file_id}.mp3")
230
- output_ogg_path = os.path.join(OUTPUT_DIR, f"out_{file_id}.ogg")
231
- try:
232
- new_file = await context.bot.get_file(file_id)
233
- await new_file.download_to_drive(download_path)
234
- await processing_message.edit_text(get_message(context, 'file_received'))
235
- audio = AudioSegment.from_file(download_path)
236
- audio.export(output_ogg_path, format="ogg", codec="libopus", parameters=["-b:a", "32k"])
237
- await processing_message.edit_text(get_message(context, 'conversion_done'))
238
- with open(output_ogg_path, 'rb') as f:
239
- await update.message.reply_voice(f)
240
- await processing_message.delete()
241
- except Exception as e:
242
- await processing_message.edit_text(get_message(context, 'error_mp3_to_voice') + str(e))
243
- finally:
244
- if os.path.exists(download_path): os.remove(download_path)
245
- if os.path.exists(output_ogg_path): os.remove(output_ogg_path)
246
- return CONVERT_AUDIO
247
- return await process_feature_or_check_membership(update, context, _perform_conversion)
248
-
249
- async def handle_voice(update: Update, context):
250
- async def _perform_conversion(update, context):
251
- file_id = update.message.voice.file_id
252
- processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
253
- download_path = os.path.join(DOWNLOAD_DIR, f"in_{file_id}.ogg")
254
- output_mp3_path = os.path.join(OUTPUT_DIR, f"@{BOT_USERNAME}_{file_id}.mp3")
255
- try:
256
- new_file = await context.bot.get_file(file_id)
257
- await new_file.download_to_drive(download_path)
258
- await processing_message.edit_text(get_message(context, 'file_received'))
259
- audio = AudioSegment.from_file(download_path, format="ogg")
260
- audio.export(output_mp3_path, format="mp3", tags={'album': BOT_USERNAME, 'artist': BOT_USERNAME})
261
- await processing_message.edit_text(get_message(context, 'conversion_done'))
262
- with open(output_mp3_path, 'rb') as f:
263
- await update.message.reply_audio(f, caption=get_message(context, 'voice_to_mp3_caption'))
264
- await processing_message.delete()
265
- except Exception as e:
266
- await processing_message.edit_text(get_message(context, 'error_voice_to_mp3') + str(e))
267
- finally:
268
- if os.path.exists(download_path): os.remove(download_path)
269
- if os.path.exists(output_mp3_path): os.remove(output_mp3_path)
270
- return CONVERT_AUDIO
271
- return await process_feature_or_check_membership(update, context, _perform_conversion)
272
 
273
- async def handle_cut_audio_file(update: Update, context):
274
- async def _perform_file_receive(update, context):
275
- audio_file = update.message.audio or update.message.voice
276
- file_id = audio_file.file_id
277
- ext = 'mp3' if update.message.audio else 'ogg'
278
- download_path = os.path.join(DOWNLOAD_DIR, f"cut_in_{file_id}.{ext}")
279
- try:
280
- new_file = await context.bot.get_file(file_id)
281
- await new_file.download_to_drive(download_path)
282
- context.user_data['audio_for_cut_path'] = download_path
283
- context.user_data['audio_for_cut_type'] = ext
284
- await update.message.reply_text(get_message(context, 'cut_mode_active_range'))
285
- return CUT_AUDIO_RANGE
286
- except Exception as e:
287
- await update.message.reply_text(get_message(context, 'general_error'))
288
- return CUT_AUDIO_FILE
289
- return await process_feature_or_check_membership(update, context, _perform_file_receive)
290
-
291
- async def handle_cut_audio_range(update: Update, context):
292
- async def _perform_cut(update, context):
293
- time_range_str = update.message.text
294
- audio_path = context.user_data.get('audio_for_cut_path')
295
- audio_type = context.user_data.get('audio_for_cut_type')
296
- if not audio_path or not os.path.exists(audio_path):
297
- await update.message.reply_text(get_message(context, 'no_audio_for_cut'))
298
- return CUT_AUDIO_FILE
299
- processing_message = await update.message.reply_text(get_message(context, 'cut_processing'))
300
- output_cut_path = os.path.join(OUTPUT_DIR, f"cut_out_{os.path.basename(audio_path)}.mp3")
301
- try:
302
- start_time_str, end_time_str = time_range_str.split('-')
303
- start_ms = parse_time_to_ms(start_time_str.strip())
304
- end_ms = parse_time_to_ms(end_time_str.strip())
305
- if start_ms >= end_ms:
306
- await processing_message.edit_text(get_message(context, 'invalid_time_range'))
307
- return CUT_AUDIO_RANGE
308
- audio = AudioSegment.from_file(audio_path, format=audio_type)
309
- cut_audio = audio[start_ms:end_ms]
310
- cut_audio.export(output_cut_path, format="mp3")
311
- await processing_message.edit_text(get_message(context, 'audio_cut_success'))
312
- with open(output_cut_path, 'rb') as f:
313
- await update.message.reply_audio(f, caption=f"برش از {start_time_str} تا {end_time_str}")
314
- await processing_message.delete()
315
- return await show_main_menu(update, context)
316
- except ValueError:
317
- await processing_message.edit_text(get_message(context, 'invalid_time_format'))
318
- return CUT_AUDIO_RANGE
319
- except Exception as e:
320
- await processing_message.edit_text(get_message(context, 'general_error'))
321
- return await show_main_menu(update, context)
322
- finally:
323
- if os.path.exists(audio_path): os.remove(audio_path)
324
- if os.path.exists(output_cut_path): os.remove(output_cut_path)
325
- context.user_data.pop('audio_for_cut_path', None)
326
- context.user_data.pop('audio_for_cut_type', None)
327
- return await process_feature_or_check_membership(update, context, _perform_cut)
328
-
329
- async def handle_video_conversion(update: Update, context):
330
- async def _perform_video_conversion(update, context):
331
- is_video_note = bool(update.message.video_note)
332
- file_to_process = update.message.video_note if is_video_note else update.message.video
333
- file_id = file_to_process.file_id
334
- download_path = os.path.join(DOWNLOAD_DIR, f"vid_in_{file_id}.mp4")
335
- output_path = os.path.join(OUTPUT_DIR, f"vid_out_{file_id}.mp4")
336
- processing_message = await update.message.reply_text(get_message(context, 'processing_start'))
337
- try:
338
- new_file = await context.bot.get_file(file_id)
339
- await new_file.download_to_drive(download_path)
340
- await processing_message.edit_text(get_message(context, 'file_received_video'))
341
- if is_video_note:
342
- await processing_message.edit_text(get_message(context, 'converting_video_note_to_video'))
343
- ffmpeg_command = ['ffmpeg', '-y', '-i', download_path, '-c:v', 'libx264', '-crf', '23', '-preset', 'medium', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', output_path]
344
- else:
345
- await processing_message.edit_text(get_message(context, 'converting_video_to_video_note'))
346
- ffmpeg_command = ['ffmpeg', '-y', '-i', download_path, '-vf', 'crop=min(iw\,ih):min(iw\,ih),scale=640:640,setsar=1', '-an', output_path]
347
- subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True)
348
- await processing_message.edit_text(get_message(context, 'conversion_done_video'))
349
- with open(output_path, 'rb') as f:
350
- if is_video_note:
351
- await update.message.reply_video(f, caption=get_message(context, 'video_note_to_video_caption'))
352
- else:
353
- await update.message.reply_video_note(f)
354
- await processing_message.delete()
355
- except subprocess.CalledProcessError as e:
356
- await processing_message.edit_text(get_message(context, 'error_video_conversion') + f"FFmpeg Error: {e.stderr}")
357
- except Exception as e:
358
- await processing_message.edit_text(get_message(context, 'error_video_conversion') + str(e))
359
- finally:
360
- if os.path.exists(download_path): os.remove(download_path)
361
- if os.path.exists(output_path): os.remove(output_path)
362
- return VIDEO_CONVERSION_MODE
363
- return await process_feature_or_check_membership(update, context, _perform_video_conversion)
364
-
365
- async def check_membership_callback(update: Update, context):
366
- query = update.callback_query
367
- await query.answer()
368
- is_member = await check_user_membership(update, context)
369
- if is_member:
370
- context.user_data['is_member'] = True
371
- await query.edit_message_text(get_message(context, 'membership_success'))
372
- return await show_main_menu(update, context)
373
- else:
374
- await query.answer(get_message(context, 'membership_failed'), show_alert=True)
375
- return WAITING_FOR_MEMBERSHIP
376
-
377
- async def admin_link_command(update: Update, context):
378
- if update.effective_user.id != ADMIN_ID:
379
- await update.message.reply_text(get_message(context, 'not_admin'))
380
- return ConversationHandler.END
381
- keyboard = [
382
- [InlineKeyboardButton(get_message(context, 'btn_add_channel'), callback_data='admin_add_channel')],
383
- [InlineKeyboardButton(get_message(context, 'btn_list_channels'), callback_data='admin_list_channels')]
384
- ]
385
- await update.message.reply_text(get_message(context, 'admin_menu_prompt'), reply_markup=InlineKeyboardMarkup(keyboard))
386
- return ADMIN_MENU
387
-
388
- async def admin_add_channel_prompt(update: Update, context):
389
- query = update.callback_query
390
- await query.answer()
391
- await query.edit_message_text(get_message(context, 'send_channel_link'))
392
- return ADD_CHANNEL
393
-
394
- async def admin_handle_add_channel(update: Update, context):
395
- channel_input = update.message.text.strip()
396
- if not (channel_input.startswith('@') or channel_input.startswith('-100')):
397
- await update.message.reply_text(get_message(context, 'invalid_channel_id'))
398
- return ADD_CHANNEL
399
- try:
400
- await context.bot.get_chat(channel_input)
401
- if channel_input not in REQUIRED_CHANNELS:
402
- REQUIRED_CHANNELS.append(channel_input)
403
- save_required_channels(REQUIRED_CHANNELS)
404
- await update.message.reply_text(get_message(context, 'channel_added', channel_id=channel_input))
405
- else:
406
- await update.message.reply_text(get_message(context, 'channel_already_exists'))
407
- except Exception as e:
408
- await update.message.reply_text(get_message(context, 'bot_not_admin_in_channel', channel_id=channel_input) + f"\nError: {e}")
409
- return await admin_link_command(update, context)
410
-
411
- async def admin_list_channels(update: Update, context):
412
- query = update.callback_query
413
- await query.answer()
414
- if not REQUIRED_CHANNELS:
415
- await query.edit_message_text(get_message(context, 'no_channels_configured'))
416
- return ADMIN_MENU
417
- keyboard = []
418
- for channel_id in REQUIRED_CHANNELS:
419
- keyboard.append([InlineKeyboardButton(f"{get_message(context, 'btn_remove_channel')} {channel_id}", callback_data=f'remove_channel_{channel_id}')])
420
- await query.edit_message_text(get_message(context, 'channel_list_prompt'), reply_markup=InlineKeyboardMarkup(keyboard))
421
- return LIST_REMOVE_CHANNELS
422
-
423
- async def admin_handle_remove_channel(update: Update, context):
424
- query = update.callback_query
425
- await query.answer()
426
- channel_id_to_remove = query.data.replace('remove_channel_', '')
427
- if channel_id_to_remove in REQUIRED_CHANNELS:
428
- REQUIRED_CHANNELS.remove(channel_id_to_remove)
429
- save_required_channels(REQUIRED_CHANNELS)
430
- await query.edit_message_text(get_message(context, 'channel_removed', channel_id=channel_id_to_remove))
431
- else:
432
- await query.edit_message_text(get_message(context, 'channel_not_found'))
433
- return await admin_link_command(update, context)
434
-
435
- async def cancel(update: Update, context):
436
- message = update.message or update.callback_query.message
437
- await message.reply_text(get_message(context, 'cancel_message'))
438
- context.user_data.clear()
439
- return await show_main_menu(update, context)
440
-
441
- async def error_handler(update: object, context: object) -> None:
442
- print(f"Update {update} caused error {context.error}", file=sys.stderr)
443
-
444
-
445
- # --- 6. بخش آماده‌سازی و مقداردهی اولیه ربات ---
446
-
447
- def setup_telegram_app():
448
- print("در حال ساختن اپلیکیشن تلگرام...")
449
- application = Application.builder().token(TOKEN).build()
450
 
451
- conv_handler = ConversationHandler(
452
- entry_points=[CommandHandler("start", start), CommandHandler("link", admin_link_command)],
453
- states={
454
- LANGUAGE_SELECTION: [CallbackQueryHandler(set_language, pattern='^set_lang_')],
455
- MAIN_MENU: [
456
- CallbackQueryHandler(change_format_selected, pattern='^select_convert_format$'),
457
- CallbackQueryHandler(cut_audio_selected, pattern='^select_cut_audio$'),
458
- CallbackQueryHandler(video_conversion_selected, pattern='^select_video_conversion$'),
459
- ],
460
- CONVERT_AUDIO: [
461
- MessageHandler(filters.AUDIO & ~filters.COMMAND, handle_audio),
462
- MessageHandler(filters.VOICE & ~filters.COMMAND, handle_voice),
463
- ],
464
- CUT_AUDIO_FILE: [MessageHandler((filters.AUDIO | filters.VOICE) & ~filters.COMMAND, handle_cut_audio_file)],
465
- CUT_AUDIO_RANGE: [MessageHandler(filters.TEXT & ~filters.COMMAND, handle_cut_audio_range)],
466
- VIDEO_CONVERSION_MODE: [MessageHandler((filters.VIDEO | filters.VIDEO_NOTE) & ~filters.COMMAND, handle_video_conversion)],
467
- WAITING_FOR_MEMBERSHIP: [CallbackQueryHandler(check_membership_callback, pattern='^check_membership$')],
468
- ADMIN_MENU: [
469
- CallbackQueryHandler(admin_add_channel_prompt, pattern='^admin_add_channel$'),
470
- CallbackQueryHandler(admin_list_channels, pattern='^admin_list_channels$'),
471
- ],
472
- ADD_CHANNEL: [MessageHandler(filters.TEXT & ~filters.COMMAND, admin_handle_add_channel)],
473
- LIST_REMOVE_CHANNELS: [CallbackQueryHandler(admin_handle_remove_channel, pattern='^remove_channel_')],
474
- },
475
- fallbacks=[CommandHandler("cancel", cancel), CommandHandler("start", start)],
476
- allow_reentry=True,
477
- per_user=True,
478
- per_chat=True
479
- )
480
 
481
- application.add_handler(conv_handler)
482
- application.add_error_handler(error_handler)
483
-
484
- print("اپلیکیشن تلگرام با موفقیت ساخته شد.")
485
- return application
486
 
487
- # --- 7. بخش مربوط به Flask و Webhook (نسخه نهایی) ---
488
-
489
- app = Flask(__name__)
490
- telegram_app = setup_telegram_app()
491
- APP_INITIALIZED = False # پرچم برای پیگیری وضعیت آماده بودن ربات
492
 
493
  @app.route("/")
494
  def index():
@@ -496,45 +34,29 @@ def index():
496
 
497
  @app.route("/webhook", methods=["POST"])
498
  async def webhook():
499
- """این مسیر آپدیت‌ها را از تلگرام دریافت کرده و پردازش می‌کند."""
500
- global APP_INITIALIZED
501
  try:
502
- # **راه‌حل نهایی**: قبل از پردازش، مطمئن می‌شویم که ربات آماده است.
503
- if not APP_INITIALIZED:
504
- print("مقداردهی اولیه ربات در اولین درخواست وبهوک...")
505
- await telegram_app.initialize()
506
- APP_INITIALIZED = True
507
- print("ربات با موفقیت مقداردهی اولیه شد.")
508
-
509
  update = Update.de_json(request.get_json(force=True), telegram_app.bot)
510
  await telegram_app.process_update(update)
511
  return "ok"
512
  except Exception as e:
513
- print(f"Error in webhook: {e}", file=sys.stderr)
514
  return "error", 500
515
 
516
  @app.route("/set_webhook", methods=["GET"])
517
  async def set_webhook_route():
518
- global APP_INITIALIZED
519
  webhook_url = os.getenv("WEBHOOK_URL")
520
  if not webhook_url:
521
  return jsonify({"status": "error", "message": "WEBHOOK_URL environment variable not set."}), 500
522
-
523
  if not webhook_url.endswith("/webhook"):
524
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
525
 
526
  try:
527
- # اطمینان از اینکه ربات قبل از تنظیم وبهوک مقداردهی اولیه شده
528
- if not APP_INITIALIZED:
529
- await telegram_app.initialize()
530
- APP_INITIALIZED = True
531
-
532
  await telegram_app.bot.set_webhook(url=webhook_url, allowed_updates=Update.ALL_TYPES)
533
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
534
  except Exception as e:
535
- print(f"Failed to set webhook: {e}", file=sys.stderr)
536
  return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
537
 
538
  if __name__ == "__main__":
539
- # این بخش فقط برای اجرای محلی (local) با استفاده از وب‌سرور خود Flask است.
540
  app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
 
1
+ # در این نسخه اصلاح‌شده، مقداردهی اولیه ربات به درستی در زمان شروع برنامه انجام می‌شود
2
+ # بنابراین نیازی به ارسال دوباره دستور نیست
3
+
4
  import os
 
 
 
 
 
5
  import asyncio
 
6
  from flask import Flask, request, jsonify
7
+ from telegram import Update
8
+ from telegram.ext import Application
9
+ from main import setup_telegram_app # فرض بر اینکه کل کد بالا در فایل main.py ذخیره شده باشد
10
 
11
+ # --- تنظیمات اولیه ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ app = Flask(__name__)
14
+ telegram_app = setup_telegram_app()
15
+ APP_INITIALIZED = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ # مقداردهی اولیه بلافاصله پس از ساخت اپلیکیشن تلگرام
18
+ async def init_bot():
19
+ global APP_INITIALIZED
20
+ if not APP_INITIALIZED:
21
+ print("در حال مقداردهی اولیه ربات تلگرام...")
22
+ await telegram_app.initialize()
23
+ APP_INITIALIZED = True
24
+ print("ربات با موفقیت مقداردهی اولیه شد.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # اجرای مقداردهی اولیه در زمان شروع اسکریپت
27
+ asyncio.get_event_loop().run_until_complete(init_bot())
 
 
 
28
 
29
+ # --- مسیردهی Flask ---
 
 
 
 
30
 
31
  @app.route("/")
32
  def index():
 
34
 
35
  @app.route("/webhook", methods=["POST"])
36
  async def webhook():
 
 
37
  try:
 
 
 
 
 
 
 
38
  update = Update.de_json(request.get_json(force=True), telegram_app.bot)
39
  await telegram_app.process_update(update)
40
  return "ok"
41
  except Exception as e:
42
+ print(f"Error in webhook: {e}")
43
  return "error", 500
44
 
45
  @app.route("/set_webhook", methods=["GET"])
46
  async def set_webhook_route():
 
47
  webhook_url = os.getenv("WEBHOOK_URL")
48
  if not webhook_url:
49
  return jsonify({"status": "error", "message": "WEBHOOK_URL environment variable not set."}), 500
50
+
51
  if not webhook_url.endswith("/webhook"):
52
  webhook_url = f"{webhook_url.rstrip('/')}/webhook"
53
 
54
  try:
 
 
 
 
 
55
  await telegram_app.bot.set_webhook(url=webhook_url, allowed_updates=Update.ALL_TYPES)
56
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
57
  except Exception as e:
58
+ print(f"Failed to set webhook: {e}")
59
  return jsonify({"status": "error", "message": f"Failed to set webhook: {e}"}), 500
60
 
61
  if __name__ == "__main__":
 
62
  app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))