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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +593 -52
app.py CHANGED
@@ -1,14 +1,13 @@
1
- # app.py - نسخه نهایی با مقداردهی اولیه ربات قبل از دریافت اولین درخواست
2
-
3
  import os
4
  import sys
5
  import re
6
  import subprocess
7
  import json
8
  from io import BytesIO
9
- import asyncio
10
 
11
  from flask import Flask, request, jsonify
 
12
  from telegram import Update, InputFile, InlineKeyboardButton, InlineKeyboardMarkup
13
  from telegram.ext import (
14
  Application,
@@ -21,66 +20,608 @@ from telegram.ext import (
21
  from telegram.constants import ParseMode
22
  from pydub import AudioSegment
23
 
24
- # ---------------------- تنظیمات و مقداردهی اولیه ----------------------
25
 
26
- # تنظیمات اولیه محیطی
27
- TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
28
- ADMIN_ID = int(os.getenv("TELEGRAM_ADMIN_ID", "123456789"))
29
- BOT_USERNAME = os.getenv("TELEGRAM_BOT_USERNAME", "MyBot")
30
- WEBHOOK_URL = os.getenv("WEBHOOK_URL")
 
31
 
 
 
 
 
 
 
 
32
  DOWNLOAD_DIR = "/tmp/downloads"
33
  OUTPUT_DIR = "/tmp/outputs"
34
  CHANNELS_FILE = "/tmp/channels.json"
35
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
36
  os.makedirs(OUTPUT_DIR, exist_ok=True)
37
 
38
- # ---------------------- پیام‌ها و زبان ----------------------
39
- MESSAGES = { 'fa': { ... }, 'en': { ... } } # پیام‌ها را وارد نکن چون گفتی نمی‌خوای
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- # ---------------------- توابع و کلاس‌های ربات (همانند قبل) ----------------------
42
- # تمام کدهایی که قبلاً داشتی اینجا وارد می‌شوند، بدون تغییر
43
- # (مانند ConversationHandler، stateها، متدهای handle_...)
44
- # فقط خط setup_telegram_app را از پایین به بالا منتقل می‌کنیم
 
 
 
 
45
 
46
- def setup_telegram_app():
47
- print("در حال ساخت اپلیکیشن تلگرام...")
48
- application = Application.builder().token(TOKEN).build()
49
-
50
- # همان تعریف handler ها و conversation که داشتی...
51
- # application.add_handler(...)
52
 
53
- application.add_error_handler(error_handler)
54
- print("اپلیکیشن تلگرام با موفقیت ساخته شد.")
55
- return application
56
 
57
- # ---------------------- Flask و Webhook ----------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  app = Flask(__name__)
60
- telegram_app = setup_telegram_app()
61
- APP_INITIALIZED = False
 
 
 
 
 
 
62
 
63
- # مقداردهی اولیه همزمان با شروع
64
- async def init_bot():
65
- global APP_INITIALIZED
66
- if not APP_INITIALIZED:
67
- print("⏳ در حال مقداردهی اولیه ربات...")
68
- await telegram_app.initialize()
69
- APP_INITIALIZED = True
70
- print("✅ مقداردهی ربات انجام شد.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- # اجرای مقداردهی اولیه قبل از اولین درخواست
73
- asyncio.get_event_loop().run_until_complete(init_bot())
 
 
 
 
74
 
75
  @app.route("/")
76
  def index():
77
- return "ربات تلگرام آماده است."
 
78
 
79
  @app.route("/webhook", methods=["POST"])
80
  async def webhook():
 
81
  try:
82
- update = Update.de_json(request.get_json(force=True), telegram_app.bot)
83
- await telegram_app.process_update(update)
 
84
  return "ok"
85
  except Exception as e:
86
  print(f"Error in webhook: {e}", file=sys.stderr)
@@ -88,23 +629,23 @@ async def webhook():
88
 
89
  @app.route("/set_webhook", methods=["GET"])
90
  async def set_webhook_route():
91
- global APP_INITIALIZED
92
- if not WEBHOOK_URL:
93
- return jsonify({"status": "error", "message": "WEBHOOK_URL not set"}), 500
 
94
 
95
- if not WEBHOOK_URL.endswith("/webhook"):
96
- webhook_url = f"{WEBHOOK_URL.rstrip('/')}/webhook"
97
 
98
  try:
99
- if not APP_INITIALIZED:
100
- await telegram_app.initialize()
101
- APP_INITIALIZED = True
102
-
103
- await telegram_app.bot.set_webhook(url=webhook_url, allowed_updates=Update.ALL_TYPES)
104
  return jsonify({"status": "success", "message": f"Webhook set to {webhook_url}"})
105
  except Exception as e:
106
  print(f"Failed to set webhook: {e}", file=sys.stderr)
107
- return jsonify({"status": "error", "message": str(e)}), 500
108
 
109
  if __name__ == "__main__":
110
- app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860)))
 
 
 
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
 
9
  from flask import Flask, request, jsonify
10
+
11
  from telegram import Update, InputFile, InlineKeyboardButton, InlineKeyboardMarkup
12
  from telegram.ext import (
13
  Application,
 
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)
 
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)))