- - " . htmlspecialchars($error) . ""; ?>
373 | - " . $msg . ""; ?>
- - " . htmlspecialchars($error) . ""; ?>
├── logo.jpg ├── wizardpanel.png ├── LICENSE ├── src ├── includes │ ├── db.php │ ├── config.php │ └── functions.php ├── cron.php ├── verify_payment.php ├── api │ ├── marzban_api.php │ ├── marzneshin_api.php │ └── sanaei_api.php └── install.php └── README.md /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webwizards-team/wizardpanel/HEAD/logo.jpg -------------------------------------------------------------------------------- /wizardpanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webwizards-team/wizardpanel/HEAD/wizardpanel.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 webwizards-team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/includes/db.php: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 14 | 15 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 16 | 17 | PDO::ATTR_EMULATE_PREPARES => false, 18 | ]; 19 | 20 | try { 21 | $this->pdo = new PDO($dsn, DB_USER, DB_PASS, $options); 22 | } catch (\PDOException $e) { 23 | error_log("Database Connection Error: " . $e->getMessage()); 24 | 25 | die('Database connection failed. Please check server logs.'); 26 | } 27 | } 28 | 29 | public static function getInstance() { 30 | if (self::$instance == null) { 31 | self::$instance = new Database(); 32 | } 33 | return self::$instance; 34 | } 35 | 36 | public function getConnection() { 37 | return $this->pdo; 38 | } 39 | } 40 | 41 | function pdo() { 42 | return Database::getInstance()->getConnection(); 43 | } -------------------------------------------------------------------------------- /src/includes/config.php: -------------------------------------------------------------------------------- 1 | Expiration warnings are disabled. Skipping.\n"; 26 | return; 27 | } 28 | 29 | echo "-> Checking for expiration warnings...\n"; 30 | 31 | $services = pdo() 32 | ->query("SELECT id, owner_chat_id, marzban_username, server_id FROM services WHERE warning_sent = 0 AND expire_timestamp > " . time()) 33 | ->fetchAll(PDO::FETCH_ASSOC); 34 | if (empty($services)) { 35 | echo " - No services to check for expiration.\n"; 36 | return; 37 | } 38 | 39 | $threshold_time = time() + $days * 86400; 40 | $threshold_gb_bytes = $gb * 1024 * 1024 * 1024; 41 | $sent_count = 0; 42 | 43 | foreach ($services as $service) { 44 | if (empty($service['server_id'])) { 45 | echo " - Service ID {$service['id']} has no server_id. Skipping.\n"; 46 | continue; 47 | } 48 | 49 | $user_info = getMarzbanUser($service['marzban_username'], $service['server_id']); 50 | if (!$user_info || isset($user_info['detail'])) { 51 | echo " - Could not fetch Marzban info for user {$service['marzban_username']} on server {$service['server_id']}. Skipping.\n"; 52 | continue; 53 | } 54 | 55 | $expire_ts = $user_info['expire'] ?? 0; 56 | $data_limit = $user_info['data_limit'] ?? 0; 57 | $used_traffic = $user_info['used_traffic'] ?? 0; 58 | $data_remaining = $data_limit - $used_traffic; 59 | 60 | $warn = false; 61 | $reason = ""; 62 | if ($expire_ts > 0 && $expire_ts < $threshold_time) { 63 | $warn = true; 64 | $reason = " (Reason: Time limit)"; 65 | } 66 | if ($data_limit > 0 && $data_remaining < $threshold_gb_bytes) { 67 | $warn = true; 68 | $reason .= " (Reason: Data limit)"; 69 | } 70 | 71 | if ($warn) { 72 | sendMessage($service['owner_chat_id'], $message); 73 | 74 | pdo() 75 | ->prepare("UPDATE services SET warning_sent = 1 WHERE id = ?") 76 | ->execute([$service['id']]); 77 | echo " - Warning sent to user {$service['owner_chat_id']} for service {$service['marzban_username']}" . trim($reason) . "\n"; 78 | $sent_count++; 79 | usleep(200000); 80 | } 81 | } 82 | echo " - Total expiration warnings sent: {$sent_count}\n"; 83 | } 84 | 85 | // --- تابع بررسی کاربران غیرفعال --- 86 | function checkInactiveUsers() 87 | { 88 | $settings = getSettings(); 89 | $status = $settings['notification_inactive_status'] ?? 'off'; 90 | $days = (int) ($settings['notification_inactive_days'] ?? 30); 91 | $message = $settings['notification_inactive_message'] ?? ''; 92 | 93 | if ($status === 'off' || empty($message)) { 94 | echo "-> Inactivity reminders are disabled. Skipping.\n"; 95 | return; 96 | } 97 | 98 | echo "-> Checking for inactive users...\n"; 99 | $inactive_date = date('Y-m-d H:i:s', strtotime("-{$days} days")); 100 | 101 | $stmt = pdo()->prepare("SELECT chat_id FROM users WHERE status = 'active' AND last_seen_at IS NOT NULL AND last_seen_at < ? AND reminder_sent = 0"); 102 | $stmt->execute([$inactive_date]); 103 | $users = $stmt->fetchAll(PDO::FETCH_COLUMN); 104 | 105 | if (empty($users)) { 106 | echo " - No inactive users found.\n"; 107 | return; 108 | } 109 | 110 | $sent_count = 0; 111 | foreach ($users as $chat_id) { 112 | sendMessage($chat_id, $message); 113 | pdo() 114 | ->prepare("UPDATE users SET reminder_sent = 1 WHERE chat_id = ?") 115 | ->execute([$chat_id]); 116 | echo " - Inactivity reminder sent to user {$chat_id}\n"; 117 | $sent_count++; 118 | usleep(200000); 119 | } 120 | echo " - Total inactivity reminders sent: {$sent_count}\n"; 121 | } 122 | 123 | // --- اجرای توابع --- 124 | try { 125 | checkExpirationWarnings(); 126 | checkInactiveUsers(); 127 | } catch (Exception $e) { 128 | echo "An error occurred: " . $e->getMessage() . "\n"; 129 | } 130 | 131 | echo "Cron job finished at " . date('Y-m-d H:i:s') . "\n"; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ربات تلگرام ویزارد پنل (Wizard Panel) - مدیریت فروش اکانت مرزبان 2 | 3 | ویزارد پنل (Wizard Panel) یک سورس ربات تلگرام پیشرفته و قدرتمند برای مدیریت و فروش خودکار اکانتهای پنل مرزبان (Marzban) است. این ربات که با PHP و پایگاه داده MySQL توسعه یافته، به شما اجازه میدهد تا به راحتی چندین سرور مرزبان را مدیریت کرده و فرآیند فروش کانفیگهای V2Ray را به طور کامل اتوماتیک کنید. 4 | 5 | هدف اصلی ویزارد پنل، فراهم کردن یک ابزار حرفهای، امن و با قابلیت شخصیسازی بالا برای مدیران سرورهای VPN است تا بتوانند کسبوکار خود را بدون دغدغه و به سادگی مدیریت کنند. 6 | 7 |  8 | 9 | ✨ ویژگیهای کلیدی ویزارد پنل (Wizard Panel) 10 | 11 | 🚀 مدیریت چند سرور مرزبان: به راحتی چندین سرور مرزبان را اضافه، مدیریت و حذف کنید. هنگام ساخت پلن، انتخاب کنید که روی کدام سرور ساخته شود. 12 | 13 | 🔐 پنل مدیریت قدرتمند و با دسترسی طبقهبندی شده: 14 | 15 | مدیریت کامل سرورها، دستهبندیها و پلنهای فروش. 16 | 17 | قابلیت ویرایش کامل اطلاعات پلنها (نام، قیمت، حجم، مدت، سرور و...). 18 | 19 | مدیریت کاربران (افزایش/کاهش موجودی، ارسال پیام، مسدود/آزاد کردن). 20 | 21 | عملیات همگانی روی تمام سرویسها (افزایش حجم یا زمان انقضا). 22 | 23 | مشاهده آمار دقیق کاربران و درآمد (روزانه، هفتگی، ماهانه). 24 | 25 | مدیریت روشهای پرداخت و درگاهها. 26 | 27 | سیستم مدیریت ادمینها با قابلیت تعریف دسترسیهای مختلف (فقط برای ادمین اصلی). 28 | 29 | 🛒 فرآیند خرید کاملاً خودکار برای کاربر: 30 | 31 | مشاهده دستهبندیها و پلنهای هر سرور. 32 | 33 | شارژ حساب از طریق کارت به کارت و تایید توسط ادمین. 34 | 35 | خرید آنی سرویس و دریافت کانفیگ بلافاصله پس از خرید. 36 | 37 | مشاهده و مدیریت سرویسهای فعال. 38 | 39 | دریافت کانفیگ تست رایگان (با قابلیت محدودسازی توسط ادمین). 40 | 41 | سیستم تیکتینگ برای پشتیبانی. 42 | 43 | 🔔 سیستم اعلانهای هوشمند (Cron Job): 44 | 45 | ارسال هشدار خودکار به کاربران قبل از اتمام حجم یا زمان سرویس. 46 | 47 | ارسال پیام یادآوری به کاربران غیرفعال. 48 | 49 | 🔗 عضویت اجباری در کانال (Force Join): 50 | 51 | قابلیت فعال/غیرفعالسازی و تنظیم کانال تلگرام. 52 | 53 | دکمه هوشمند "✅ عضو شدم" برای تجربه کاربری بهتر. 54 | 55 | ✅ سیستم احراز هویت کاربران: 56 | 57 | امکان فعالسازی تایید هویت کاربران جدید (از طریق شماره تماس یا دکمه شیشهای) قبل از استفاده از ربات. 58 | 59 | 🎁 ابزارهای بازاریابی: 60 | 61 | سیستم مدیریت کدهای تخفیف (درصدی و مبلغی). 62 | 63 | قابلیت تعریف هدیه خوشآمدگویی (شارژ اولیه) برای کاربران جدید. 64 | 65 | ⚙️ نصب و ارتقا آسان: 66 | 67 | دارای اسکریپت نصب خودکار (install.php) که جداول دیتابیس را ساخته و وبهوک را تنظیم میکند. 68 | 69 | 🔧 راهنمای نصب و راهاندازی 70 | 71 | نصب ویزارد پنل بسیار ساده است و در چند مرحله انجام میشود: 72 | 73 | دانلود سورس: ابتدا سورس کامل پروژه را از این مخزن گیتهاب دانلود کنید. 74 | 75 | آپلود در هاست: فایلهای دانلود شده را در هاست خود (در یک پوشه مانند bot یا روت اصلی) آپلود کنید. 76 | 77 | ساخت دیتابیس: یک پایگاه داده (Database) جدید از نوع MySQL یا MariaDB در هاست خود ایجاد کنید. اطلاعات دیتابیس (نام، نام کاربری و رمز عبور) را یادداشت کنید. 78 | 79 | ساخت ربات در تلگرام: 80 | 81 | به ربات @BotFather در تلگرام بروید. 82 | 83 | یک ربات جدید بسازید و توکن (Token) آن را کپی کنید. 84 | 85 | شناسه عددی اکانت تلگرام خودتان را که میخواهید ادمین اصلی باشد، از رباتهایی مانند @userinfobot دریافت کنید. 86 | 87 | اجرای اسکریپت نصب: 88 | 89 | فایل install.php را در مرورگر خود باز کنید. (مثال: https://yourdomain.com/bot/install.php) 90 | 91 | فرم نصب نمایش داده میشود. اطلاعات زیر را با دقت وارد کنید: 92 | 93 | توکن ربات: توکنی که از BotFather گرفتید. 94 | 95 | آیدی عددی ادمین اصلی: شناسه عددی اکانت تلگرام خودتان. 96 | 97 | اطلاعات دیتابیس: اطلاعاتی که در مرحله ۳ ایجاد کردید. 98 | 99 | روی دکمه "نصب و راهاندازی" کلیک کنید. اسکریپت به صورت خودکار جداول را ساخته، فایل config.php را آپدیت کرده و وبهوک (Webhook) را برای ربات شما تنظیم میکند. 100 | 101 | ⚠️ مرحله امنیتی بسیار مهم: 102 | 103 | پس از مشاهده پیام موفقیتآمیز، بلافاصله فایل install.php را از روی هاست خود حذف کنید. باقی ماندن این فایل یک حفره امنیتی بزرگ محسوب میشود. 104 | 105 | شروع کار با ربات: به ربات خود در تلگرام رفته و دستور /start را ارسال کنید. منوی مدیریت باید برای شما نمایش داده شود. 106 | 107 | cron.php - تنظیم کرون جاب (Cron Job) 108 | 109 | برای اینکه سیستم اعلانهای خودکار (هشدار انقضا و یادآوری به کاربران غیرفعال) کار کند، باید یک کرون جاب در هاست خود تنظیم کنید. 110 | 111 | وارد کنترل پنل هاست خود (cPanel, DirectAdmin, etc.) شوید و به بخش Cron Jobs بروید. 112 | 113 | یک کرون جاب جدید با تنظیمات زیر ایجاد کنید (توصیه میشود هر ۵ دقیقه یکبار اجرا شود): 114 | 115 | code 116 | Bash 117 | download 118 | content_copy 119 | expand_less 120 | 121 | */5 * * * * /usr/bin/php /home/your_username/public_html/botsql/cron.php 122 | 123 | توجه: مسیر /usr/bin/php و /home/your_username/... ممکن است در هاست شما متفاوت باشد. مسیر صحیح را از پشتیبانی هاست خود سوال کنید. 124 | 125 | 🔑 کلمات کلیدی برای جستجو 126 | 127 | سورس ربات تلگرام ویزارد پنل، Wizard Panel, ربات پنل مرزبان, سورس ربات فروش v2ray, ربات فروش کانفیگ تلگرام, ربات مرزبان چند سروره, سورس ربات PHP, ربات فروش VPN, ربات مدیریت مرزبان, Marzban panel bot, Marzban multi server bot, Telegram VPN sales bot source. 128 | 129 | 🤝 مشارکت در پروژه 130 | 131 | از مشارکت شما در توسعه و بهبود ویزارد پنل استقبال میکنیم. میتوانید از طریق Pull Request تغییرات خود را ارسال کنید یا با ثبت Issue مشکلات و پیشنهادات خود را با ما در میان بگذارید. 132 | 133 | 📜 مجوز (License) 134 | 135 | این پروژه تحت مجوز MIT منتشر شده است. 136 | -------------------------------------------------------------------------------- /src/verify_payment.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT * FROM transactions WHERE authority = ? AND status = 'pending'"); 18 | $stmt->execute([$authority]); 19 | $transaction = $stmt->fetch(); 20 | 21 | if (!$transaction) { 22 | die("تراکنش یافت نشد یا قبلاً پردازش شده است."); 23 | } 24 | 25 | $settings = getSettings(); 26 | $merchant_id = $settings['zarinpal_merchant_id'] ?? ''; 27 | $amount = (int)$transaction['amount']; // مبلغ به تومان 28 | 29 | if ($status == 'OK') { 30 | // تراکنش موفق بود 31 | $data = [ 32 | "merchant_id" => $merchant_id, 33 | "amount" => $amount * 10, // تبدیل تومان به ریال برای وریفای 34 | "authority" => $authority, 35 | ]; 36 | $jsonData = json_encode($data); 37 | 38 | $ch = curl_init('https://api.zarinpal.com/pg/v4/payment/verify.json'); 39 | curl_setopt($ch, CURLOPT_USERAGENT, 'ZarinPal Rest Api v4'); 40 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); 41 | curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); 42 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 43 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Content-Length: ' . strlen($jsonData)]); 44 | 45 | $result = curl_exec($ch); 46 | curl_close($ch); 47 | $result = json_decode($result, true); 48 | 49 | if (empty($result['errors'])) { 50 | $code = $result['data']['code']; 51 | if ($code == 100 || $code == 101) { // 100: موفق, 101: قبلا وریفای شده 52 | $ref_id = $result['data']['ref_id']; 53 | 54 | // آپدیت وضعیت تراکنش 55 | $stmt = pdo()->prepare("UPDATE transactions SET status = 'completed', ref_id = ?, verified_at = NOW() WHERE id = ?"); 56 | $stmt->execute([$ref_id, $transaction['id']]); 57 | 58 | $metadata = json_decode($transaction['metadata'], true); 59 | 60 | // --- تشخیص هدف پرداخت --- 61 | if (isset($metadata['purpose']) && $metadata['purpose'] === 'complete_purchase') { 62 | 63 | $plan_id = $metadata['plan_id']; 64 | $user_id = $metadata['user_id']; 65 | $discount_code = $metadata['discount_code'] ?? null; 66 | 67 | $plan = getPlanById($plan_id); 68 | $final_price = (float)$plan['price']; 69 | $discount_applied = false; 70 | $discount_object = null; 71 | 72 | if ($discount_code) { 73 | $stmt_discount = pdo()->prepare("SELECT * FROM discount_codes WHERE code = ?"); 74 | $stmt_discount->execute([$discount_code]); 75 | $discount_object = $stmt_discount->fetch(); 76 | if ($discount_object) { 77 | if ($discount_object['type'] == 'percent') { 78 | $final_price = $plan['price'] - ($plan['price'] * $discount_object['value']) / 100; 79 | } else { 80 | $final_price = $plan['price'] - $discount_object['value']; 81 | } 82 | $final_price = max(0, $final_price); 83 | $discount_applied = true; 84 | } 85 | } 86 | 87 | // شارژ موقت حساب کاربر برای کسر هزینه 88 | updateUserBalance($user_id, $transaction['amount'], 'add'); 89 | 90 | 91 | // نام دلخواه از متادیتا 92 | $custom_name = $metadata['custom_name'] ?? 'سرویس'; 93 | $purchase_result = completePurchase($user_id, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied); 94 | 95 | if ($purchase_result['success']) { 96 | sendPhoto($user_id, $purchase_result['qr_code_url'], $purchase_result['caption'], $purchase_result['keyboard']); 97 | sendMessage(ADMIN_CHAT_ID, $purchase_result['admin_notification']); 98 | echo "
سرویس شما با موفقیت ایجاد شد. لطفاً به ربات تلگرام بازگردید.
"; 99 | } else { 100 | sendMessage($user_id, "❌ پرداخت شما موفق بود اما در ایجاد سرویس خطایی رخ داد. مبلغ پرداخت شده به موجودی شما اضافه شد. لطفاً با پشتیبانی تماس بگیرید."); 101 | echo "پرداخت موفق بود اما سرویس ایجاد نشد. مبلغ به حساب شما اضافه شد.
"; 102 | } 103 | 104 | } else { 105 | // پرداخت برای شارژ عادی حساب 106 | updateUserBalance($transaction['user_id'], $transaction['amount'], 'add'); 107 | $new_balance_data = getUserData($transaction['user_id']); 108 | 109 | $message = "✅ پرداخت شما به مبلغ " . number_format($transaction['amount']) . " تومان با موفقیت انجام و حساب شما شارژ شد.\n\n" . 110 | "▫️ شماره پیگیری: `{$ref_id}`\n" . 111 | "💰 موجودی جدید: " . number_format($new_balance_data['balance']) . " تومان"; 112 | sendMessage($transaction['user_id'], $message); 113 | 114 | echo "تراکنش شما با موفقیت انجام شد و حساب شما شارژ گردید. شماره پیگیری: {$ref_id}. لطفاً به ربات تلگرام بازگردید.
"; 115 | } 116 | 117 | } else { 118 | // آپدیت وضعیت تراکنش به ناموفق 119 | $stmt = pdo()->prepare("UPDATE transactions SET status = 'failed' WHERE id = ?"); 120 | $stmt->execute([$transaction['id']]); 121 | $error_message = "خطا در وریفای تراکنش. کد خطا: " . $code; 122 | sendMessage($transaction['user_id'], "❌ تراکنش شما ناموفق بود. " . $error_message); 123 | echo "{$error_message}
"; 124 | } 125 | } else { 126 | // خطایی در ارتباط با زرینپال رخ داده 127 | $error_message = "خطا در ارتباط با درگاه پرداخت."; 128 | sendMessage($transaction['user_id'], "❌ " . $error_message); 129 | echo "{$error_message}
"; 130 | } 131 | 132 | } else { 133 | // کاربر تراکنش را لغو کرده 134 | $stmt = pdo()->prepare("UPDATE transactions SET status = 'cancelled' WHERE id = ?"); 135 | $stmt->execute([$transaction['id']]); 136 | sendMessage($transaction['user_id'], "❌ شما تراکنش را لغو کردید."); 137 | echo "شما عملیات پرداخت را لغو کردید. لطفاً به ربات بازگردید.
"; 138 | } -------------------------------------------------------------------------------- /src/api/marzban_api.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT * FROM servers WHERE id = ?"); 5 | $stmt->execute([$server_id]); 6 | $server_info = $stmt->fetch(); 7 | 8 | if (!$server_info) { 9 | error_log("Marzban server with ID {$server_id} not found."); 10 | return ['error' => 'Marzban server is not configured.']; 11 | } 12 | 13 | $url = rtrim($server_info['url'], '/') . $endpoint; 14 | 15 | $headers = ['Content-Type: application/json', 'Accept: application/json']; 16 | if ($accessToken) { 17 | $headers[] = 'Authorization: Bearer ' . $accessToken; 18 | } 19 | 20 | $ch = curl_init(); 21 | curl_setopt_array($ch, [ 22 | CURLOPT_URL => $url, 23 | CURLOPT_RETURNTRANSFER => true, 24 | CURLOPT_HTTPHEADER => $headers, 25 | CURLOPT_TIMEOUT => 15, 26 | CURLOPT_SSL_VERIFYPEER => false, 27 | CURLOPT_SSL_VERIFYHOST => false, 28 | ]); 29 | 30 | switch ($method) { 31 | case 'POST': 32 | curl_setopt($ch, CURLOPT_POST, true); 33 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 34 | break; 35 | case 'PUT': 36 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); 37 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 38 | break; 39 | case 'DELETE': 40 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); 41 | break; 42 | } 43 | 44 | $response = curl_exec($ch); 45 | if (curl_errno($ch)) { 46 | error_log("Marzban API cURL error for server {$server_id}: " . curl_error($ch)); 47 | curl_close($ch); 48 | return false; 49 | } 50 | curl_close($ch); 51 | 52 | return json_decode($response, true); 53 | } 54 | 55 | function getMarzbanToken($server_id) { 56 | $stmt = pdo()->prepare("SELECT * FROM servers WHERE id = ?"); 57 | $stmt->execute([$server_id]); 58 | $server_info = $stmt->fetch(); 59 | 60 | if (!$server_info) { 61 | error_log("Marzban credentials are not configured for server ID {$server_id}."); 62 | return false; 63 | } 64 | 65 | $cache_key = 'marzban_token_' . $server_id; 66 | $current_time = time(); 67 | 68 | $stmt_cache = pdo()->prepare("SELECT cache_value FROM cache WHERE cache_key = ? AND expire_at > ?"); 69 | $stmt_cache->execute([$cache_key, $current_time]); 70 | $cached_token = $stmt_cache->fetchColumn(); 71 | if ($cached_token) { 72 | return $cached_token; 73 | } 74 | 75 | $url = rtrim($server_info['url'], '/') . '/api/admin/token'; 76 | $postData = http_build_query([ 77 | 'username' => $server_info['username'], 78 | 'password' => $server_info['password'], 79 | ]); 80 | 81 | $headers = ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json']; 82 | 83 | $ch = curl_init(); 84 | curl_setopt_array($ch, [ 85 | CURLOPT_URL => $url, 86 | CURLOPT_RETURNTRANSFER => true, 87 | CURLOPT_HTTPHEADER => $headers, 88 | CURLOPT_POST => true, 89 | CURLOPT_POSTFIELDS => $postData, 90 | CURLOPT_TIMEOUT => 10, 91 | CURLOPT_SSL_VERIFYPEER => false, 92 | CURLOPT_SSL_VERIFYHOST => false, 93 | ]); 94 | 95 | $response_body = curl_exec($ch); 96 | 97 | if (curl_errno($ch)) { 98 | error_log("Marzban Token cURL error for server {$server_id}: " . curl_error($ch)); 99 | curl_close($ch); 100 | return false; 101 | } 102 | curl_close($ch); 103 | 104 | $response = json_decode($response_body, true); 105 | 106 | if (isset($response['access_token'])) { 107 | $new_token = $response['access_token']; 108 | $expire_time = $current_time + 3500; 109 | 110 | $stmt_insert_cache = pdo()->prepare( 111 | "INSERT INTO cache (cache_key, cache_value, expire_at) VALUES (?, ?, ?) 112 | ON DUPLICATE KEY UPDATE cache_value = VALUES(cache_value), expire_at = VALUES(expire_at)" 113 | ); 114 | $stmt_insert_cache->execute([$cache_key, $new_token, $expire_time]); 115 | 116 | return $new_token; 117 | } 118 | 119 | error_log("Failed to get Marzban access token for server {$server_id}. Response: " . $response_body); 120 | return false; 121 | } 122 | 123 | function createMarzbanUser($plan, $chat_id, $plan_id) { 124 | $server_id = $plan['server_id']; 125 | $accessToken = getMarzbanToken($server_id); 126 | if (!$accessToken) { 127 | return false; 128 | } 129 | 130 | $stmt_server_protocols = pdo()->prepare("SELECT marzban_protocols FROM servers WHERE id = ?"); 131 | $stmt_server_protocols->execute([$server_id]); 132 | $protocols_json = $stmt_server_protocols->fetchColumn(); 133 | 134 | $proxies = new stdClass(); 135 | 136 | if ($protocols_json) { 137 | $protocol_list = json_decode($protocols_json, true); 138 | if (is_array($protocol_list) && !empty($protocol_list)) { 139 | foreach ($protocol_list as $protocol) { 140 | $proxies->{$protocol} = new stdClass(); 141 | } 142 | } 143 | } 144 | 145 | if (empty((array)$proxies)) { 146 | $proxies->vless = new stdClass(); 147 | } 148 | 149 | $username = $plan['full_username']; 150 | 151 | $userData = [ 152 | 'username' => $username, 153 | 'proxies' => $proxies, 154 | 'inbounds' => new stdClass(), 155 | 'expire' => time() + $plan['duration_days'] * 86400, 156 | 'data_limit' => $plan['volume_gb'] * 1024 * 1024 * 1024, 157 | 'data_limit_reset_strategy' => 'no_reset', 158 | ]; 159 | 160 | $response = marzbanApiRequest('/api/user', $server_id, 'POST', $userData, $accessToken); 161 | 162 | if (isset($response['username'])) { 163 | pdo() 164 | ->prepare("UPDATE services SET warning_sent = 0 WHERE marzban_username = ? AND server_id = ?") 165 | ->execute([$response['username'], $server_id]); 166 | 167 | 168 | $stmt_server = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); 169 | $stmt_server->execute([$server_id]); 170 | $server_info = $stmt_server->fetch(); 171 | 172 | $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); 173 | $sub_path = parse_url($response['subscription_url'], PHP_URL_PATH); 174 | $final_sub_url = $base_sub_url . $sub_path; 175 | 176 | 177 | $response['subscription_url'] = $final_sub_url; 178 | return $response; 179 | } 180 | 181 | error_log("Failed to create Marzban user for chat_id {$chat_id} on server {$server_id}. Response: " . json_encode($response)); 182 | return false; 183 | } 184 | 185 | function getMarzbanUser($username, $server_id) { 186 | $accessToken = getMarzbanToken($server_id); 187 | if (!$accessToken) { 188 | return false; 189 | } 190 | 191 | return marzbanApiRequest("/api/user/{$username}", $server_id, 'GET', [], $accessToken); 192 | } 193 | 194 | function modifyMarzbanUser($username, $server_id, $data) { 195 | $accessToken = getMarzbanToken($server_id); 196 | if (!$accessToken) { 197 | return false; 198 | } 199 | 200 | return marzbanApiRequest("/api/user/{$username}", $server_id, 'PUT', $data, $accessToken); 201 | } 202 | 203 | function deleteMarzbanUser($username, $server_id) { 204 | $accessToken = getMarzbanToken($server_id); 205 | if (!$accessToken) { 206 | return false; 207 | } 208 | 209 | return marzbanApiRequest("/api/user/{$username}", $server_id, 'DELETE', [], $accessToken); 210 | } -------------------------------------------------------------------------------- /src/api/marzneshin_api.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT url FROM servers WHERE id = ?"); 7 | $stmt->execute([$server_id]); 8 | $server_url = $stmt->fetchColumn(); 9 | if (!$server_url) return ['error' => 'Server not configured.']; 10 | 11 | $accessTokenResult = getMarzneshinToken($server_id); 12 | if (is_array($accessTokenResult) && isset($accessTokenResult['error'])) { 13 | return ['error' => 'Token Error: ' . $accessTokenResult['error']]; 14 | } 15 | $accessToken = $accessTokenResult; 16 | 17 | $url = rtrim($server_url, '/') . $endpoint; 18 | $headers = ['Content-Type: application/json', 'Accept: application/json', 'Authorization: Bearer ' . $accessToken]; 19 | 20 | $ch = curl_init(); 21 | curl_setopt_array($ch, [ 22 | CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, 23 | CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, 24 | ]); 25 | 26 | switch (strtoupper($method)) { 27 | case 'POST': 28 | curl_setopt($ch, CURLOPT_POST, true); 29 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 30 | break; 31 | case 'PUT': 32 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); 33 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 34 | break; 35 | case 'DELETE': 36 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); 37 | break; 38 | } 39 | $response_body = curl_exec($ch); 40 | curl_close($ch); 41 | return json_decode($response_body, true); 42 | } 43 | 44 | function marzneshinPublicApiRequest($endpoint, $server_id) { 45 | $stmt = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); 46 | $stmt->execute([$server_id]); 47 | $server_info = $stmt->fetch(); 48 | if (!$server_info) return false; 49 | 50 | $base_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); 51 | $url = $base_url . $endpoint; 52 | 53 | $ch = curl_init(); 54 | curl_setopt_array($ch, [ 55 | CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, 56 | CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, 57 | ]); 58 | $response = curl_exec($ch); 59 | curl_close($ch); 60 | return $response; 61 | } 62 | 63 | function getMarzneshinToken($server_id) { 64 | $cache_key = 'marzneshin_token_' . $server_id; 65 | $stmt_cache = pdo()->prepare("SELECT cache_value FROM cache WHERE cache_key = ? AND expire_at > ?"); 66 | $stmt_cache->execute([$cache_key, time()]); 67 | if ($cached_token = $stmt_cache->fetchColumn()) return $cached_token; 68 | 69 | $stmt = pdo()->prepare("SELECT url, username, password FROM servers WHERE id = ?"); 70 | $stmt->execute([$server_id]); 71 | $server_info = $stmt->fetch(); 72 | if (!$server_info) return ['error' => "Server info not found for server ID: {$server_id}"]; 73 | 74 | $url = rtrim($server_info['url'], '/') . '/api/admins/token'; 75 | $postData = http_build_query(['username' => $server_info['username'], 'password' => $server_info['password']]); 76 | 77 | $ch = curl_init(); 78 | curl_setopt_array($ch, [ 79 | CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, 80 | CURLOPT_POSTFIELDS => $postData, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => false, 81 | CURLOPT_SSL_VERIFYHOST => false, 82 | CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'], 83 | ]); 84 | 85 | $response_body = curl_exec($ch); 86 | $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 87 | curl_close($ch); 88 | 89 | $response = json_decode($response_body, true); 90 | if (isset($response['access_token'])) { 91 | $new_token = $response['access_token']; 92 | $expire_time = time() + 3500; 93 | $stmt_insert_cache = pdo()->prepare("INSERT INTO cache (cache_key, cache_value, expire_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE cache_value = VALUES(cache_value), expire_at = VALUES(expire_at)"); 94 | $stmt_insert_cache->execute([$cache_key, $new_token, $expire_time]); 95 | return $new_token; 96 | } 97 | 98 | $error_detail = $response['detail'] ?? $response_body; 99 | return ['error' => "HTTP {$http_code} - " . (is_string($error_detail) ? $error_detail : json_encode($error_detail))]; 100 | } 101 | 102 | function getMarzneshinServices($server_id) { 103 | $response = marzneshinApiRequest('/api/services', $server_id, 'GET'); 104 | return $response['items'] ?? []; 105 | } 106 | 107 | function createMarzneshinUser($plan, $chat_id, $plan_id) { 108 | $server_id = $plan['server_id']; 109 | $service_id = $plan['marzneshin_service_id']; 110 | $username = $plan['full_username']; 111 | 112 | $stmt = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); 113 | $stmt->execute([$server_id]); 114 | $server_info = $stmt->fetch(); 115 | if (!$server_info) return false; 116 | 117 | $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); 118 | $userData = [ 119 | 'username' => $username, 120 | 'data_limit' => $plan['volume_gb'] * 1024 * 1024 * 1024, 121 | 'expire_date' => date('c', time() + $plan['duration_days'] * 86400), 122 | 'service_ids' => [(int)$service_id], 123 | 'expire_strategy' => 'fixed_date' 124 | ]; 125 | 126 | $response = marzneshinApiRequest('/api/users', $server_id, 'POST', $userData); 127 | 128 | if (isset($response['username'])) { 129 | // استخراج صحیح یزورنیم و پسورد 130 | $new_username = $response['username']; 131 | $key = $response['key']; 132 | 133 | // لینک اشتراک و کانفیگ تکی با پارامتر میسازیم 134 | $subscription_path = "/sub/{$new_username}/{$key}/"; 135 | $links_path = $subscription_path . 'links'; 136 | 137 | $full_subscription_url = $base_sub_url . $subscription_path; 138 | 139 | $links = []; 140 | $links_response_raw = marzneshinPublicApiRequest($links_path, $server_id); 141 | if (is_string($links_response_raw) && !str_contains(strtolower($links_response_raw), 'error')) { 142 | $links = explode("\n", trim($links_response_raw)); 143 | } 144 | 145 | return [ 146 | 'username' => $new_username, 147 | 'subscription_url' => $full_subscription_url, 148 | 'links' => array_filter($links), 149 | ]; 150 | } 151 | 152 | error_log("[Marzneshin Create User Failed] Payload: " . json_encode($userData) . " | Response: " . json_encode($response)); 153 | return false; 154 | } 155 | 156 | // --- تابع دریافت اطلاعات کاربر --- 157 | function getMarzneshinUser($username, $server_id) { 158 | 159 | $user_response = marzneshinApiRequest("/api/users/{$username}", $server_id, 'GET'); 160 | 161 | if (isset($user_response['username'])) { 162 | $links = []; 163 | 164 | 165 | if (isset($user_response['key'])) { 166 | $key = $user_response['key']; 167 | 168 | $links_endpoint = "/sub/{$username}/{$key}/links"; 169 | 170 | $links_response_raw = marzneshinPublicApiRequest($links_endpoint, $server_id); 171 | 172 | if (is_string($links_response_raw) && !str_contains(strtolower($links_response_raw), 'error')) { 173 | $links = explode("\n", trim($links_response_raw)); 174 | } 175 | } 176 | 177 | return [ 178 | 'status' => $user_response['is_active'] ? 'active' : 'disabled', 179 | 'expire' => $user_response['expire_date'] ? strtotime($user_response['expire_date']) : 0, 180 | 'used_traffic' => $user_response['used_traffic'], 181 | 'data_limit' => $user_response['data_limit'], 182 | 'links' => array_filter($links), 183 | ]; 184 | } 185 | 186 | error_log("[Marzneshin Get User Failed] Username: {$username} | Response: " . json_encode($user_response)); 187 | return false; 188 | } 189 | 190 | function modifyMarzneshinUser($username, $server_id, $data) { 191 | $marzneshinData = []; 192 | if (isset($data['data_limit'])) { 193 | $marzneshinData['data_limit'] = $data['data_limit']; 194 | } 195 | if (isset($data['expire'])) { 196 | $marzneshinData['expire_date'] = date('c', $data['expire']); 197 | } 198 | 199 | $response = marzneshinApiRequest("/api/users/{$username}", $server_id, 'PUT', $marzneshinData); 200 | return $response && isset($response['username']); 201 | } 202 | 203 | function deleteMarzneshinUser($username, $server_id) { 204 | $response = marzneshinApiRequest("/api/users/{$username}", $server_id, 'DELETE'); 205 | return is_null($response) || (isset($response['detail']) && str_contains($response['detail'], 'not found')); 206 | } -------------------------------------------------------------------------------- /src/api/sanaei_api.php: -------------------------------------------------------------------------------- 1 | prepare("SELECT cache_value FROM cache WHERE cache_key = ? AND expire_at > ?"); 8 | $stmt_cache->execute([$cache_key, time()]); 9 | if ($cached_cookie = $stmt_cache->fetchColumn()) { 10 | return $cached_cookie; 11 | } 12 | 13 | $stmt = pdo()->prepare("SELECT url, username, password FROM servers WHERE id = ?"); 14 | $stmt->execute([$server_id]); 15 | $server_info = $stmt->fetch(); 16 | if (!$server_info) return false; 17 | 18 | $url = rtrim($server_info['url'], '/') . '/login'; 19 | $postData = ['username' => $server_info['username'], 'password' => $server_info['password']]; 20 | 21 | $ch = curl_init(); 22 | curl_setopt_array($ch, [ 23 | CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, 24 | CURLOPT_POSTFIELDS => http_build_query($postData), CURLOPT_HEADER => true, 25 | CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, 26 | ]); 27 | 28 | $response = curl_exec($ch); 29 | curl_close($ch); 30 | 31 | preg_match('/^Set-Cookie:\s*([^;]*)/mi', $response, $matches); 32 | if (isset($matches[1])) { 33 | $cookie = $matches[1]; 34 | $expire_time = time() + 3500; 35 | $stmt_insert_cache = pdo()->prepare("INSERT INTO cache (cache_key, cache_value, expire_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE cache_value = VALUES(cache_value), expire_at = VALUES(expire_at)"); 36 | $stmt_insert_cache->execute([$cache_key, $cookie, $expire_time]); 37 | return $cookie; 38 | } 39 | return false; 40 | } 41 | 42 | function sanaeiApiRequest($endpoint, $server_id, $method = 'GET', $data = []) { 43 | $stmt = pdo()->prepare("SELECT url FROM servers WHERE id = ?"); 44 | $stmt->execute([$server_id]); 45 | $server_url = $stmt->fetchColumn(); 46 | if (!$server_url) return ['success' => false, 'msg' => 'Sanaei server is not configured.']; 47 | 48 | $cookie = getSanaeiCookie($server_id); 49 | if (!$cookie) return ['success' => false, 'msg' => 'Login failed']; 50 | 51 | $url = rtrim($server_url, '/') . $endpoint; 52 | $headers = ['Cookie: ' . $cookie, 'Accept: application/json']; 53 | if ($method === 'POST') $headers[] = 'Content-Type: application/json'; 54 | 55 | $ch = curl_init(); 56 | curl_setopt_array($ch, [ 57 | CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, 58 | CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, 59 | ]); 60 | 61 | if ($method === 'POST') { 62 | curl_setopt($ch, CURLOPT_POST, true); 63 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 64 | } 65 | $response = curl_exec($ch); 66 | curl_close($ch); 67 | return json_decode($response, true); 68 | } 69 | 70 | 71 | function getSanaeiInbounds($server_id) { 72 | $response = sanaeiApiRequest('/panel/api/inbounds/list', $server_id); 73 | return ($response['success'] && isset($response['obj'])) ? $response['obj'] : []; 74 | } 75 | 76 | function _findSanaeiClientInAllInbounds($email_username, $server_id) { 77 | $inbounds = getSanaeiInbounds($server_id); 78 | if (empty($inbounds)) return false; 79 | 80 | foreach ($inbounds as $inbound_summary) { 81 | $inbound_id = $inbound_summary['id']; 82 | $response = sanaeiApiRequest("/panel/api/inbounds/get/{$inbound_id}", $server_id); 83 | 84 | if ($response && $response['success'] && isset($response['obj']['settings'])) { 85 | $settings = json_decode($response['obj']['settings'], true); 86 | if (isset($settings['clients'])) { 87 | foreach ($settings['clients'] as $client) { 88 | if (isset($client['email']) && $client['email'] === $email_username) { 89 | return ['client' => $client, 'inbound_id' => $inbound_id]; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | return false; 96 | } 97 | 98 | function createSanaeiUser($plan, $chat_id, $plan_id) { 99 | $server_id = $plan['server_id']; 100 | $inbound_id = $plan['inbound_id']; 101 | 102 | $stmt_server = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); 103 | $stmt_server->execute([$server_id]); 104 | $server_info = $stmt_server->fetch(); 105 | if(!$server_info) return false; 106 | 107 | $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); 108 | $uuid = generateUUID(); 109 | $email = $plan['full_username']; 110 | $subId = generateUUID(16); 111 | $expire_time = ($plan['duration_days'] > 0) ? (time() + $plan['duration_days'] * 86400) * 1000 : 0; 112 | $total_bytes = ($plan['volume_gb'] > 0) ? $plan['volume_gb'] * 1024 * 1024 * 1024 : 0; 113 | $client_settings = [ "id" => $uuid, "email" => $email, "totalGB" => $total_bytes, "expiryTime" => $expire_time, "enable" => true, "tgId" => (string)$chat_id, "subId" => $subId ]; 114 | $data = ['id' => (int)$inbound_id, 'settings' => json_encode(['clients' => [$client_settings]])]; 115 | $response = sanaeiApiRequest('/panel/api/inbounds/addClient', $server_id, 'POST', $data); 116 | 117 | if (isset($response['success']) && $response['success']) { 118 | $sub_link = $base_sub_url . '/sub/' . $subId; 119 | // اصلاحیه: پاس دادن server_id به تابع کمکی 120 | $links = fetchAndParseSubscriptionUrl($sub_link, $server_id); 121 | 122 | return ['username' => $email, 'subscription_url' => $sub_link, 'links' => $links]; 123 | } 124 | 125 | error_log("Failed to create Sanaei user. Response: " . json_encode($response)); 126 | return false; 127 | } 128 | 129 | function getSanaeiUser($username, $server_id) { 130 | $traffic_response = sanaeiApiRequest("/panel/api/inbounds/getClientTraffics/{$username}", $server_id); 131 | if (!$traffic_response || !$traffic_response['success'] || !isset($traffic_response['obj'])) { 132 | error_log("Could not fetch user traffic for {$username}."); 133 | return false; 134 | } 135 | $client_traffic_data = $traffic_response['obj']; 136 | 137 | $stmt_service = pdo()->prepare("SELECT sub_url FROM services WHERE marzban_username = ? AND server_id = ?"); 138 | $stmt_service->execute([$username, $server_id]); 139 | $sub_url = $stmt_service->fetchColumn(); 140 | 141 | // اصلاحیه: پاس دادن server_id به تابع کمکی 142 | $links = fetchAndParseSubscriptionUrl($sub_url, $server_id); 143 | 144 | return [ 145 | 'status' => ($client_traffic_data['enable'] && ($client_traffic_data['expiryTime'] == 0 || $client_traffic_data['expiryTime'] > time() * 1000)) ? 'active' : 'disabled', 146 | 'expire' => $client_traffic_data['expiryTime'] > 0 ? floor($client_traffic_data['expiryTime'] / 1000) : 0, 147 | 'used_traffic' => $client_traffic_data['up'] + $client_traffic_data['down'], 148 | 'data_limit' => $client_traffic_data['totalGB'] ?? 0, 149 | 'links' => $links, 150 | ]; 151 | } 152 | 153 | function modifySanaeiUser($username, $server_id, $data) { 154 | $foundClientData = _findSanaeiClientInAllInbounds($username, $server_id); 155 | if (!$foundClientData) return false; 156 | 157 | $inbound_id = $foundClientData['inbound_id']; 158 | $uuid = $foundClientData['client']['id']; 159 | 160 | $traffic_response = sanaeiApiRequest("/panel/api/inbounds/getClientTraffics/{$username}", $server_id); 161 | if (!$traffic_response || !$traffic_response['success'] || !isset($traffic_response['obj'])) return false; 162 | $currentClientData = $traffic_response['obj']; 163 | 164 | $update_payload = [ 165 | 'id' => (int)$inbound_id, 166 | 'settings' => json_encode(['clients' => [[ 167 | 'id' => $uuid, 'email' => $username, 'enable' => true, 168 | 'totalGB' => $data['data_limit'] ?? ($currentClientData['totalGB'] ?? 0), 169 | 'expiryTime' => isset($data['expire']) ? $data['expire'] * 1000 : ($currentClientData['expiryTime'] ?? 0), 170 | ]]]) 171 | ]; 172 | 173 | $response = sanaeiApiRequest("/panel/api/inbounds/updateClient/{$uuid}", $server_id, 'POST', $update_payload); 174 | return $response && $response['success']; 175 | } 176 | 177 | function deleteSanaeiUser($username, $server_id) { 178 | $foundClientData = _findSanaeiClientInAllInbounds($username, $server_id); 179 | if (!$foundClientData) return true; 180 | 181 | $inbound_id = $foundClientData['inbound_id']; 182 | $uuid = $foundClientData['client']['id']; 183 | 184 | $response = sanaeiApiRequest("/panel/api/inbounds/{$inbound_id}/delClient/{$uuid}", $server_id, 'POST'); 185 | return $response && $response['success']; 186 | } 187 | 188 | function generateUUID($length = 36) { 189 | if ($length === 36) { 190 | return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 191 | mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), 192 | mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, 193 | mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) 194 | ); 195 | } else { 196 | $characters = '0123456789abcdefghijklmnopqrstuvwxyz'; 197 | $randomString = ''; 198 | for ($i = 0; $i < $length; $i++) { 199 | $randomString .= $characters[rand(0, strlen($characters) - 1)]; 200 | } 201 | return $randomString; 202 | } 203 | } -------------------------------------------------------------------------------- /src/install.php: -------------------------------------------------------------------------------- 1 | prepare("SHOW COLUMNS FROM `$tableName` LIKE ?"); 76 | $stmt->execute([$columnName]); 77 | return $stmt->rowCount() > 0; 78 | } catch (PDOException $e) { 79 | return false; 80 | } 81 | } 82 | 83 | function runDbUpgrades(PDO $pdo): array { 84 | $messages = []; 85 | 86 | if (columnExists($pdo, 'users', 'state') && !columnExists($pdo, 'users', 'user_state')) { 87 | $pdo->exec("ALTER TABLE `users` CHANGE `state` `user_state` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'main_menu';"); 88 | $messages[] = "✅ ستون `state` در جدول `users` به `user_state` تغییر نام یافت."; 89 | } 90 | 91 | // --- ارتقا برای پشتیبانی از چند پنل --- 92 | if (!columnExists($pdo, 'servers', 'type')) { 93 | $pdo->exec("ALTER TABLE `servers` ADD `type` VARCHAR(20) NOT NULL DEFAULT 'marzban' AFTER `password`;"); 94 | $messages[] = "✅ ستون `type` برای پشتیبانی از چند نوع پنل به جدول `servers` اضافه شد."; 95 | } 96 | if (!columnExists($pdo, 'plans', 'inbound_id')) { 97 | $pdo->exec("ALTER TABLE `plans` ADD `inbound_id` INT NULL DEFAULT NULL AFTER `category_id`;"); 98 | $messages[] = "✅ ستون `inbound_id` برای پنل سنایی به جدول `plans` اضافه شد."; 99 | } 100 | if (!columnExists($pdo, 'plans', 'marzneshin_service_id')) { 101 | $pdo->exec("ALTER TABLE `plans` ADD `marzneshin_service_id` INT NULL DEFAULT NULL AFTER `inbound_id`;"); 102 | $messages[] = "✅ ستون `marzneshin_service_id` برای پنل مرزنشین به جدول `plans` اضافه شد."; 103 | } 104 | if (!columnExists($pdo, 'services', 'sanaei_inbound_id')) { 105 | $pdo->exec("ALTER TABLE `services` ADD `sanaei_inbound_id` INT NULL DEFAULT NULL AFTER `volume_gb`;"); 106 | $messages[] = "✅ ستون `sanaei_inbound_id` برای پنل سنایی به جدول `services` اضافه شد."; 107 | } 108 | if (!columnExists($pdo, 'services', 'sanaei_uuid')) { 109 | $pdo->exec("ALTER TABLE `services` ADD `sanaei_uuid` VARCHAR(255) NULL DEFAULT NULL AFTER `sanaei_inbound_id`;"); 110 | $messages[] = "✅ ستون `sanaei_uuid` برای پنل سنایی به جدول `services` اضافه شد."; 111 | } 112 | 113 | // --- ارتقاهای مربوط به اعلانها و ردیابی کاربران --- 114 | if (!columnExists($pdo, 'users', 'last_seen_at')) { 115 | $pdo->exec("ALTER TABLE `users` ADD `last_seen_at` TIMESTAMP NULL DEFAULT NULL AFTER `status`;"); 116 | $messages[] = "✅ ستون `last_seen_at` برای ردیابی آخرین فعالیت کاربران اضافه شد."; 117 | } 118 | if (!columnExists($pdo, 'users', 'reminder_sent')) { 119 | $pdo->exec("ALTER TABLE `users` ADD `reminder_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `last_seen_at`;"); 120 | $messages[] = "✅ ستون `reminder_sent` برای ارسال یادآور عدم فعالیت اضافه شد."; 121 | } 122 | if (!columnExists($pdo, 'services', 'warning_sent')) { 123 | $pdo->exec("ALTER TABLE `services` ADD `warning_sent` TINYINT(1) NOT NULL DEFAULT 0 AFTER `volume_gb`;"); 124 | $messages[] = "✅ ستون `warning_sent` برای ارسال هشدار انقضا به جدول `services` اضافه شد."; 125 | } 126 | if (!columnExists($pdo, 'users', 'test_config_count')) { 127 | $pdo->exec("ALTER TABLE `users` ADD `test_config_count` INT NOT NULL DEFAULT 0 AFTER `status`;"); 128 | $messages[] = "✅ ستون `test_config_count` برای کانفیگ تست به جدول `users` اضافه شد."; 129 | } 130 | if (!columnExists($pdo, 'plans', 'is_test_plan')) { 131 | $pdo->exec("ALTER TABLE `plans` ADD `is_test_plan` TINYINT(1) NOT NULL DEFAULT 0 AFTER `show_conf_links`;"); 132 | $messages[] = "✅ ستون `is_test_plan` برای کانفیگ تست به جدول `plans` اضافه شد."; 133 | } 134 | if (!columnExists($pdo, 'plans', 'purchase_limit')) { 135 | $pdo->exec("ALTER TABLE `plans` ADD `purchase_limit` INT NOT NULL DEFAULT 0 AFTER `is_test_plan`;"); 136 | $messages[] = "✅ ستون `purchase_limit` برای محدودیت خرید پلنها اضافه شد."; 137 | } 138 | if (!columnExists($pdo, 'plans', 'purchase_count')) { 139 | $pdo->exec("ALTER TABLE `plans` ADD `purchase_count` INT NOT NULL DEFAULT 0 AFTER `purchase_limit`;"); 140 | $messages[] = "✅ ستون `purchase_count` برای شمارش خرید پلنها اضافه شد."; 141 | } 142 | if (!columnExists($pdo, 'users', 'is_verified')) { 143 | $pdo->exec("ALTER TABLE `users` ADD `is_verified` TINYINT(1) NOT NULL DEFAULT 0 AFTER `test_config_count`;"); 144 | $messages[] = "✅ ستون `is_verified` برای وضعیت احراز هویت کاربران اضافه شد."; 145 | } 146 | if (!columnExists($pdo, 'users', 'phone_number')) { 147 | $pdo->exec("ALTER TABLE `users` ADD `phone_number` VARCHAR(20) NULL DEFAULT NULL AFTER `is_verified`;"); 148 | $messages[] = "✅ ستون `phone_number` برای ذخیره شماره تلفن کاربران اضافه شد."; 149 | } 150 | if (!columnExists($pdo, 'admins', 'is_super_admin')) { 151 | $pdo->exec("ALTER TABLE `admins` ADD `is_super_admin` TINYINT(1) NOT NULL DEFAULT 0;"); 152 | $messages[] = "✅ ستون `is_super_admin` برای مدیریت ادمین اصلی اضافه شد."; 153 | } 154 | if (!columnExists($pdo, 'users', 'inline_keyboard')) { 155 | $pdo->exec("ALTER TABLE `users` ADD `inline_keyboard` TINYINT(1) NOT NULL DEFAULT 0;"); 156 | $messages[] = "✅ ستون `inline_keyboard` برای مدیریت نوع کیبورد کاربران اضافه شد."; 157 | } 158 | if (!columnExists($pdo, 'servers', 'sub_host')) { 159 | $pdo->exec("ALTER TABLE `servers` ADD `sub_host` VARCHAR(255) NULL DEFAULT NULL AFTER `url`;"); 160 | $messages[] = "✅ ستون `sub_host` برای لینک اشتراک سفارشی به جدول `servers` اضافه شد."; 161 | } 162 | if (!columnExists($pdo, 'servers', 'marzban_protocols')) { 163 | $pdo->exec("ALTER TABLE `servers` ADD `marzban_protocols` VARCHAR(255) NULL DEFAULT NULL AFTER `sub_host`;"); 164 | $messages[] = "✅ ستون `marzban_protocols` برای تنظیم پروتکلهای مرزبان اضافه شد."; 165 | } 166 | if (!columnExists($pdo, 'services', 'custom_name')) { 167 | $pdo->exec("ALTER TABLE `services` ADD `custom_name` VARCHAR(255) NULL DEFAULT NULL AFTER `marzban_username`;"); 168 | $messages[] = "✅ ستون `custom_name` برای نام دلخواه سرویس به جدول `services` اضافه شد."; 169 | } 170 | 171 | return $messages; 172 | } 173 | 174 | // --- مدیریت منطق مراحل --- 175 | if ($step === 2) { 176 | if (empty($bot_token)) $errors[] = 'توکن ربات الزامی است.'; 177 | if (empty($admin_id) || !is_numeric($admin_id)) $errors[] = 'آیدی عددی ادمین الزامی و باید عدد باشد.'; 178 | if (!empty($errors)) $step = 1; 179 | } 180 | elseif ($step === 3) { 181 | $db_host = trim($_POST['db_host'] ?? 'localhost'); 182 | $db_name = trim($_POST['db_name'] ?? ''); 183 | $db_user = trim($_POST['db_user'] ?? ''); 184 | $db_pass = trim($_POST['db_pass'] ?? ''); 185 | 186 | if (empty($db_name)) $errors[] = 'نام دیتابیس الزامی است.'; 187 | if (empty($db_user)) $errors[] = 'نام کاربری دیتابیس الزامی است.'; 188 | 189 | if (empty($errors)) { 190 | if (!is_dir(__DIR__ . '/includes')) @mkdir(__DIR__ . '/includes', 0755, true); 191 | if (!file_exists($configFile)) @file_put_contents($configFile, "setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 200 | $pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"); 201 | $pdo->exec("USE `$db_name`"); 202 | $successMessages[] = "✅ اتصال به دیتابیس برقرار و دیتابیس `{$db_name}` بررسی/ایجاد شد."; 203 | 204 | $pdo->exec(getDbBaseSchemaSQL()); 205 | $successMessages[] = "✅ ساختار پایه جداول با موفقیت ایجاد/بررسی شد."; 206 | 207 | $secretToken = generateRandomString(64); 208 | $config_content = 'exec("INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES 227 | ('bot_status', 'on'), ('sales_status', 'on'), ('join_channel_status', 'off'), ('join_channel_id', '@'), 228 | ('welcome_gift_balance', '0'), ('inline_keyboard', 'on'), ('verification_method', 'off'), 229 | ('verification_iran_only', 'off'), ('test_config_usage_limit', '1'), ('notification_expire_status', 'off'), 230 | ('notification_expire_days', '3'), ('notification_expire_gb', '1'), ('notification_inactive_status', 'off'), 231 | ('notification_inactive_days', '30'), 232 | ('renewal_status', 'off'), ('renewal_price_per_day', '1000'), ('renewal_price_per_gb', '2000'), ('payment_gateway_status', 'off'), ('zarinpal_merchant_id', '');"); 233 | $successMessages[] = "✅ تنظیمات پیشفرض با موفقیت افزوده شد."; 234 | 235 | $apiUrl = "https://api.telegram.org/bot$bot_token/setWebhook?secret_token=$secretToken&url=" . urlencode($botFileUrl); 236 | $response = @file_get_contents($apiUrl); 237 | $response_data = json_decode($response, true); 238 | 239 | if (!$response || !$response_data['ok']) { 240 | $errors[] = 'خطا در ثبت وبهوک: ' . ($response_data['description'] ?? 'پاسخ نامعتبر از تلگرام. از صحت توکن مطمئن شوید.'); 241 | } else { 242 | $successMessages[] = "✅ وبهوک با موفقیت در تلگرام ثبت شد."; 243 | $successMessages[] = "🎉 نصب/ارتقا با موفقیت به پایان رسید!"; 244 | } 245 | 246 | } catch (PDOException $e) { 247 | $errors[] = "خطا در اتصال به دیتابیس یا اجرای کوئریها: " . $e->getMessage(); 248 | } 249 | } 250 | } 251 | ?> 252 | 253 | 254 | 255 | 256 | 257 |
373 | {$code['code']}\n" . "▫️ نوع تخفیف: {$value_text} {$type_text}\n" . "▫️ میزان استفاده: {$usage_text}";
549 |
550 | $keyboard = ['inline_keyboard' => [[['text' => "🗑 حذف", 'callback_data' => "delete_discount_{$code_id}"], ['text' => $status_action_text, 'callback_data' => "toggle_discount_{$code_id}"]]]];
551 |
552 | sendMessage($chat_id, $info_message, $keyboard);
553 | }
554 | }
555 |
556 | function generateCategoryList($chat_id) {
557 | $categories = getCategories();
558 | if (empty($categories)) {
559 | sendMessage($chat_id, "هیچ دستهبندیای یافت نشد.");
560 | return;
561 | }
562 |
563 | sendMessage($chat_id, "🗂 لیست دستهبندیها:\nبرای مدیریت هر مورد، از دکمههای زیر آن استفاده کنید.");
564 |
565 | foreach ($categories as $category) {
566 | $status_icon = $category['status'] == 'active' ? '✅' : '❌';
567 | $status_action = $category['status'] == 'active' ? 'غیرفعال کردن' : 'فعال کردن';
568 |
569 | $message_text = "{$status_icon} {$category['name']}";
570 |
571 | $keyboard = ['inline_keyboard' => [[['text' => "🗑 حذف", 'callback_data' => "delete_cat_{$category['id']}"], ['text' => $status_action, 'callback_data' => "toggle_cat_{$category['id']}"]]]];
572 |
573 | sendMessage($chat_id, $message_text, $keyboard);
574 | }
575 | }
576 |
577 | function generatePlanList($chat_id) {
578 | $plans = pdo()
579 | ->query("SELECT p.*, s.name as server_name, s.type as server_type FROM plans p LEFT JOIN servers s ON p.server_id = s.id ORDER BY p.is_test_plan DESC, p.id ASC")
580 | ->fetchAll(PDO::FETCH_ASSOC);
581 | $categories_raw = getCategories();
582 | $categories = array_column($categories_raw, 'name', 'id');
583 |
584 | if (empty($plans)) {
585 | sendMessage($chat_id, "هیچ پلنی یافت نشد.");
586 | return;
587 | }
588 | sendMessage($chat_id, "📝 لیست پلنها:\nبرای مدیریت، روی دکمههای زیر هر مورد کلیک کنید.");
589 |
590 | foreach ($plans as $plan) {
591 | $plan_id = $plan['id'];
592 | $cat_name = $categories[$plan['category_id']] ?? 'نامشخص';
593 | $server_name = $plan['server_name'] ?? 'سرور حذف شده';
594 | $status_icon = $plan['status'] == 'active' ? '✅' : '❌';
595 | $status_action = $plan['status'] == 'active' ? 'غیرفعال کردن' : 'فعال کردن';
596 |
597 | $plan_info = "";
598 | if ($plan['is_test_plan']) {
599 | $plan_info .= "🧪 (پلن تست) {$plan['name']}\n";
600 | }
601 | else {
602 | $plan_info .= "{$status_icon} {$plan['name']}\n";
603 | }
604 |
605 | $plan_info .= "▫️ سرور: {$server_name}\n";
606 |
607 | if ($plan['server_type'] === 'sanaei' && !empty($plan['inbound_id'])) {
608 | $plan_info .= "▫️ اینباند: {$plan['inbound_id']}\n";
609 | } elseif ($plan['server_type'] === 'marzneshin' && !empty($plan['marzneshin_service_id'])) {
610 | $plan_info .= "▫️ سرویس: {$plan['marzneshin_service_id']}\n";
611 | }
612 |
613 | $plan_info .= "▫️ دستهبندی: {$cat_name}\n" . "▫️ قیمت: " . number_format($plan['price']) . " تومان\n" . "▫️ حجم: {$plan['volume_gb']} گیگابایت | " . "مدت: {$plan['duration_days']} روز\n";
614 |
615 | if ($plan['purchase_limit'] > 0) {
616 | $plan_info .= "📈 تعداد خرید: {$plan['purchase_count']} / {$plan['purchase_limit']}\n";
617 | }
618 |
619 | $keyboard_buttons = [];
620 | // --- open_plan_editor ---
621 | $keyboard_buttons[] = [['text' => "🗑 حذف", 'callback_data' => "delete_plan_{$plan_id}"], ['text' => $status_action, 'callback_data' => "toggle_plan_{$plan_id}"], ['text' => "✏️ ویرایش", 'callback_data' => "open_plan_editor_{$plan_id}"]];
622 |
623 | if ($plan['is_test_plan']) {
624 | $keyboard_buttons[] = [['text' => '↔️ تبدیل به پلن عادی', 'callback_data' => "make_plan_normal_{$plan_id}"]];
625 | }
626 | else {
627 | $keyboard_buttons[] = [['text' => '🧪 تنظیم به عنوان پلن تست', 'callback_data' => "set_as_test_plan_{$plan_id}"]];
628 | }
629 |
630 | if ($plan['purchase_limit'] > 0) {
631 | $keyboard_buttons[] = [['text' => '🔄 ریست کردن تعداد خرید', 'callback_data' => "reset_plan_count_{$plan_id}"]];
632 | }
633 |
634 | sendMessage($chat_id, $plan_info, ['inline_keyboard' => $keyboard_buttons]);
635 | }
636 | }
637 |
638 | function showServersForCategory($chat_id, $category_id) {
639 | $category_stmt = pdo()->prepare("SELECT name FROM categories WHERE id = ?");
640 | $category_stmt->execute([$category_id]);
641 | $category_name = $category_stmt->fetchColumn();
642 | if (!$category_name) {
643 | sendMessage($chat_id, "خطا: دستهبندی یافت نشد.");
644 | return;
645 | }
646 |
647 | // کوئری برای پیدا کردن سرورهای فعال که در این دستهبندی پلن فعال دارند
648 | $stmt = pdo()->prepare("
649 | SELECT DISTINCT s.id, s.name
650 | FROM servers s
651 | JOIN plans p ON s.id = p.server_id
652 | WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active'
653 | ");
654 | $stmt->execute([$category_id]);
655 | $servers = $stmt->fetchAll(PDO::FETCH_ASSOC);
656 |
657 | if (empty($servers)) {
658 | sendMessage($chat_id, "متاسفانه در حال حاضر هیچ سروری در این دستهبندی پلن فعال ندارد.");
659 | return;
660 | }
661 |
662 | $message = "🛍️ دستهبندی «{$category_name}»\n\nلطفاً سرور (لوکیشن) مورد نظر خود را انتخاب کنید:";
663 | $keyboard_buttons = [];
664 | foreach ($servers as $server) {
665 |
666 | $keyboard_buttons[] = [['text' => "🖥 {$server['name']}", 'callback_data' => "show_plans_cat_{$category_id}_srv_{$server['id']}"]];
667 | }
668 | $keyboard_buttons[] = [['text' => '◀️ بازگشت به دستهبندیها', 'callback_data' => 'back_to_categories']];
669 | sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]);
670 | }
671 |
672 | function showAdminManagementMenu($chat_id) {
673 | $admins = getAdmins();
674 | $message = "👨💼 مدیریت ادمینها\n\nدر این بخش میتوانید ادمینهای ربات و دسترسیهای آنها را مدیریت کنید. (حداکثر ۱۰ ادمین)";
675 | $keyboard_buttons = [];
676 |
677 | if (count($admins) < 10) {
678 | $keyboard_buttons[] = [['text' => '➕ افزودن ادمین جدید', 'callback_data' => 'add_admin']];
679 | }
680 |
681 | foreach ($admins as $admin_id => $admin_data) {
682 | if ($admin_id == ADMIN_CHAT_ID) {
683 | continue;
684 | }
685 | $admin_name = htmlspecialchars($admin_data['first_name'] ?? "ادمین $admin_id");
686 | $keyboard_buttons[] = [['text' => "👤 {$admin_name}", 'callback_data' => "edit_admin_permissions_{$admin_id}"]];
687 | }
688 |
689 | $keyboard_buttons[] = [['text' => '◀️ بازگشت به پنل مدیریت', 'callback_data' => 'back_to_admin_panel']];
690 | sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]);
691 | }
692 |
693 | function showPermissionEditor($chat_id, $message_id, $target_admin_id) {
694 | $admins = getAdmins();
695 | $target_admin = $admins[$target_admin_id] ?? null;
696 | if (!$target_admin) {
697 | editMessageText($chat_id, $message_id, "❌ خطا: ادمین مورد نظر یافت نشد.");
698 | return;
699 | }
700 |
701 | $admin_name = htmlspecialchars($target_admin['first_name'] ?? "ادمین $target_admin_id");
702 | $message = "ویرایش دسترسیهای: {$admin_name}\n\nبا کلیک روی هر دکمه، دسترسی آن را فعال یا غیرفعال کنید.";
703 |
704 | $permission_map = getPermissionMap();
705 | $current_permissions = $target_admin['permissions'] ?? [];
706 | $keyboard_buttons = [];
707 | $row = [];
708 |
709 | foreach ($permission_map as $key => $name) {
710 | $has_perm = in_array($key, $current_permissions);
711 | $icon = $has_perm ? '✅' : '❌';
712 | $row[] = ['text' => "{$icon} {$name}", 'callback_data' => "toggle_perm_{$target_admin_id}_{$key}"];
713 | if (count($row) == 2) {
714 | $keyboard_buttons[] = $row;
715 | $row = [];
716 | }
717 | }
718 | if (!empty($row)) {
719 | $keyboard_buttons[] = $row;
720 | }
721 |
722 | $keyboard_buttons[] = [['text' => '🗑 حذف این ادمین', 'callback_data' => "delete_admin_confirm_{$target_admin_id}"]];
723 | $keyboard_buttons[] = [['text' => '◀️ بازگشت به لیست ادمینها', 'callback_data' => 'back_to_admin_list']];
724 |
725 | editMessageText($chat_id, $message_id, $message, ['inline_keyboard' => $keyboard_buttons]);
726 | }
727 |
728 | function handleMainMenu($chat_id, $first_name, $is_start_command = false) {
729 |
730 | $isAnAdmin = isUserAdmin($chat_id);
731 | $user_data = getUserData($chat_id, $first_name);
732 | $admin_view_mode = $user_data['state_data']['admin_view'] ?? 'user';
733 |
734 | if ($is_start_command) {
735 | $message = "سلام $first_name عزیز!\nبه ربات فروش کانفیگ خوش آمدید. 🌹";
736 | }
737 | else {
738 | $message = "به منوی اصلی بازگشتید. لطفا گزینه مورد نظر را انتخاب کنید.";
739 | }
740 |
741 | $keyboard_buttons = [[['text' => '🛒 خرید سرویس']], [['text' => '💳 شارژ حساب'], ['text' => '👤 حساب کاربری']], [['text' => '🔧 سرویسهای من'], ['text' => '📨 پشتیبانی']]];
742 |
743 | $test_plan = getTestPlan();
744 | if ($test_plan) {
745 | array_splice($keyboard_buttons, 1, 0, [[['text' => '🧪 دریافت کانفیگ تست']]]);
746 | }
747 |
748 | $stmt = pdo()->query("SELECT COUNT(*) FROM guides WHERE status = 'active'");
749 | if ($stmt->fetchColumn() > 0) {
750 | $keyboard_buttons[] = [['text' => '📚 راهنما']];
751 | }
752 |
753 | if ($isAnAdmin) {
754 | if ($admin_view_mode === 'admin') {
755 | if ($is_start_command) {
756 | $message = "ادمین عزیز، به پنل مدیریت خوش آمدید.";
757 | }
758 | else {
759 | $message = "به پنل مدیریت بازگشتید.";
760 | }
761 | $admin_keyboard = [];
762 | $rows = array_fill(0, 7, []);
763 | if (hasPermission($chat_id, 'manage_categories')) {
764 | $rows[0][] = ['text' => '🗂 مدیریت دستهبندیها'];
765 | }
766 | if (hasPermission($chat_id, 'manage_plans')) {
767 | $rows[0][] = ['text' => '📝 مدیریت پلنها'];
768 | }
769 | if (hasPermission($chat_id, 'manage_users')) {
770 | $rows[1][] = ['text' => '👥 مدیریت کاربران'];
771 | }
772 | if (hasPermission($chat_id, 'broadcast')) {
773 | $rows[1][] = ['text' => '📣 ارسال همگانی'];
774 | }
775 | if (hasPermission($chat_id, 'view_stats')) {
776 | $rows[2][] = ['text' => '📊 آمار کلی'];
777 | $rows[2][] = ['text' => '💰 آمار درآمد'];
778 | }
779 | if (hasPermission($chat_id, 'manage_payment')) {
780 | $rows[3][] = ['text' => '💳 مدیریت پرداخت'];
781 | $rows[3][] = ['text' => '💳 مدیریت درگاه پرداخت'];
782 | }
783 | if (hasPermission($chat_id, 'manage_marzban')) {
784 | $rows[4][] = ['text' => '🌐 مدیریت سرورها'];
785 | }
786 | if (hasPermission($chat_id, 'manage_settings')) {
787 | $rows[5][] = ['text' => '⚙️ تنظیمات کلی ربات'];
788 | }
789 | if (hasPermission($chat_id, 'manage_guides')) {
790 | $rows[5][] = ['text' => '📚 مدیریت راهنما'];
791 | }
792 | if (hasPermission($chat_id, 'manage_notifications')) {
793 | $rows[5][] = ['text' => '📢 مدیریت اعلانها'];
794 | }
795 | if (hasPermission($chat_id, 'manage_test_config')) {
796 | $rows[6][] = ['text' => '🧪 مدیریت کانفیگ تست'];
797 | }
798 | if ($chat_id == ADMIN_CHAT_ID) {
799 | $rows[6][] = ['text' => '👨💼 مدیریت ادمینها'];
800 | }
801 | if (hasPermission($chat_id, 'manage_verification')) {
802 | $rows[7][] = ['text' => '🔐 مدیریت احراز هویت'];
803 | }
804 | $rows[7][] = ['text' => '🎁 مدیریت کد تخفیف'];
805 | $rows[8][] = ['text' => '🔄 مدیریت تمدید'];
806 | foreach ($rows as $row) {
807 | if (!empty($row)) {
808 | $admin_keyboard[] = $row;
809 | }
810 | }
811 | $admin_keyboard[] = [['text' => '↩️ بازگشت به منوی کاربری']];
812 | $keyboard_buttons = $admin_keyboard;
813 | }
814 | else {
815 | $keyboard_buttons[] = [['text' => '👑 ورود به پنل مدیریت']];
816 | }
817 | }
818 |
819 | $keyboard = ['keyboard' => $keyboard_buttons, 'resize_keyboard' => true];
820 |
821 | $stmt = pdo()->prepare("SELECT inline_keyboard FROM users WHERE chat_id = ?");
822 | $stmt->execute([$chat_id]);
823 | $inline_keyboard = $stmt->fetch()['inline_keyboard'];
824 | if (USER_INLINE_KEYBOARD && ($inline_keyboard != 1 || $is_start_command)) {
825 | $stmt = pdo()->prepare("UPDATE users SET inline_keyboard = '1' WHERE chat_id = ?");
826 | $stmt->execute([$chat_id]);
827 |
828 | $delMsgId = json_decode(apiRequest('sendMessage', [
829 | 'chat_id' => $chat_id,
830 | 'text' => '🏠',
831 | 'reply_markup' => json_encode(['remove_keyboard' => true])
832 | ]), true)['result']['message_id'];
833 | }
834 | elseif (!USER_INLINE_KEYBOARD && $inline_keyboard == 1) {
835 | $stmt = pdo()->prepare("UPDATE users SET inline_keyboard = '0' WHERE chat_id = ?");
836 | $stmt->execute([$chat_id]);
837 | }
838 |
839 | sendMessage($chat_id, $message, $keyboard, true);
840 |
841 | if (isset($delMsgId)) {
842 | apiRequest('deleteMessage', [
843 | 'chat_id' => $chat_id,
844 | 'message_id' => $delMsgId
845 | ]);
846 | }
847 |
848 | }
849 |
850 | function showVerificationManagementMenu($chat_id) {
851 | $settings = getSettings();
852 | $current_method = $settings['verification_method'];
853 | $iran_only_icon = $settings['verification_iran_only'] == 'on' ? '🇮🇷' : '🌎';
854 |
855 | $method_text = 'غیرفعال';
856 | if ($current_method == 'phone') {
857 | $method_text = 'شماره تلفن';
858 | }
859 | elseif ($current_method == 'button') {
860 | $method_text = 'دکمه شیشهای';
861 | }
862 |
863 | $message = "🔐 مدیریت احراز هویت کاربران\n\n" . "در این بخش میتوانید روش تایید هویت کاربران قبل از استفاده از ربات را مشخص کنید.\n\n" . "▫️ روش فعلی: " . $method_text . "";
864 |
865 | $keyboard = [
866 | 'inline_keyboard' => [
867 | [
868 | ['text' => ($current_method == 'off' ? '✅' : '') . ' غیرفعال', 'callback_data' => 'set_verification_off'],
869 | ['text' => ($current_method == 'phone' ? '✅' : '') . ' 📞 شماره تلفن', 'callback_data' => 'set_verification_phone'],
870 | ['text' => ($current_method == 'button' ? '✅' : '') . ' 🔘 دکمه شیشهای', 'callback_data' => 'set_verification_button'],
871 | ],
872 | [],
873 | [['text' => '◀️ بازگشت به پنل مدیریت', 'callback_data' => 'back_to_admin_panel']],
874 | ],
875 | ];
876 |
877 | if ($current_method == 'phone') {
878 | $keyboard['inline_keyboard'][1][] = ['text' => $iran_only_icon . " محدودیت شماره (ایران/همه)", 'callback_data' => 'toggle_verification_iran_only'];
879 | }
880 |
881 | global $update;
882 | $message_id = $update['callback_query']['message']['message_id'] ?? null;
883 | if ($message_id) {
884 | editMessageText($chat_id, $message_id, $message, $keyboard);
885 | }
886 | else {
887 | sendMessage($chat_id, $message, $keyboard);
888 | }
889 | }
890 |
891 | // =====================================================================
892 | // --- توابع انتزاعی برای مدیریت پنلها ---
893 | // =====================================================================
894 |
895 | function getPanelUser($username, $server_id) {
896 | $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?");
897 | $stmt->execute([$server_id]);
898 | $type = $stmt->fetchColumn();
899 |
900 | switch ($type) {
901 | case 'marzban':
902 | return getMarzbanUser($username, $server_id);
903 | case 'sanaei':
904 | return getSanaeiUser($username, $server_id);
905 | case 'marzneshin':
906 | return getMarzneshinUser($username, $server_id);
907 | default:
908 | return false;
909 | }
910 | }
911 |
912 | function createPanelUser($plan, $chat_id, $plan_id) {
913 | $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?");
914 | $stmt->execute([$plan['server_id']]);
915 | $type = $stmt->fetchColumn();
916 |
917 | switch ($type) {
918 | case 'marzban':
919 | return createMarzbanUser($plan, $chat_id, $plan_id);
920 | case 'sanaei':
921 | return createSanaeiUser($plan, $chat_id, $plan_id);
922 | case 'marzneshin':
923 | return createMarzneshinUser($plan, $chat_id, $plan_id);
924 | default:
925 | return false;
926 | }
927 | }
928 |
929 | function deletePanelUser($username, $server_id) {
930 | $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?");
931 | $stmt->execute([$server_id]);
932 | $type = $stmt->fetchColumn();
933 |
934 | switch ($type) {
935 | case 'marzban':
936 | return deleteMarzbanUser($username, $server_id);
937 | case 'sanaei':
938 | return deleteSanaeiUser($username, $server_id);
939 | case 'marzneshin':
940 | return deleteMarzneshinUser($username, $server_id);
941 | default:
942 | return false;
943 | }
944 | }
945 |
946 | function modifyPanelUser($username, $server_id, $data) {
947 | $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?");
948 | $stmt->execute([$server_id]);
949 | $type = $stmt->fetchColumn();
950 |
951 | switch ($type) {
952 | case 'marzban':
953 | return modifyMarzbanUser($username, $server_id, $data);
954 | case 'sanaei':
955 | return modifySanaeiUser($username, $server_id, $data);
956 | case 'marzneshin':
957 | return modifyMarzneshinUser($username, $server_id, $data);
958 | default:
959 | return false;
960 | }
961 | }
962 |
963 | function showPlanEditor($chat_id, $message_id, $plan_id, $prompt = null)
964 | {
965 | $plan = getPlanById($plan_id);
966 | if (!$plan) {
967 | editMessageText($chat_id, $message_id, "❌ خطا: پلن مورد نظر یافت نشد.");
968 | return;
969 | }
970 |
971 | $status_icon = $plan['status'] == 'active' ? '✅' : '❌';
972 | $message_text = " ویرایش پلن: {$plan['name']} {$status_icon}\n";
973 | $message_text .= "➖➖➖➖➖➖➖➖➖➖\n";
974 | $message_text .= "▫️ نام: {$plan['name']}\n";
975 | $message_text .= "▫️ قیمت: " . number_format($plan['price']) . " تومان\n";
976 | $message_text .= "▫️ حجم: {$plan['volume_gb']} گیگابایت\n";
977 | $message_text .= "▫️ مدت: {$plan['duration_days']} روز\n";
978 | $message_text .= "▫️ محدودیت خرید: " . ($plan['purchase_limit'] == 0 ? 'نامحدود' : $plan['purchase_limit']) . "\n";
979 | $message_text .= "➖➖➖➖➖➖➖➖➖➖";
980 |
981 | if ($prompt) {
982 | $message_text .= "\n\n" . $prompt . "";
983 | }
984 |
985 | $keyboard = [
986 | 'inline_keyboard' => [
987 | [['text' => '✏️ نام', 'callback_data' => "edit_plan_field_{$plan_id}_name"], ['text' => '💰 قیمت', 'callback_data' => "edit_plan_field_{$plan_id}_price"]],
988 | [['text' => '📊 حجم', 'callback_data' => "edit_plan_field_{$plan_id}_volume_gb"], ['text' => '⏰ مدت', 'callback_data' => "edit_plan_field_{$plan_id}_duration_days"]],
989 | [['text' => '📈 محدودیت خرید', 'callback_data' => "edit_plan_field_{$plan_id}_purchase_limit"]],
990 | [['text' => '◀️ بازگشت به لیست پلنها', 'callback_data' => "back_to_plan_list"]],
991 | ],
992 | ];
993 |
994 | editMessageText($chat_id, $message_id, $message_text, $keyboard);
995 | }
996 |
997 | function fetchAndParseSubscriptionUrl($sub_url, $server_id) {
998 | if (empty($sub_url)) {
999 | return [];
1000 | }
1001 |
1002 | $stmt = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?");
1003 | $stmt->execute([$server_id]);
1004 | $server_info = $stmt->fetch();
1005 | if (!$server_info) return [];
1006 |
1007 | $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/');
1008 |
1009 | $stmt_type = pdo()->prepare("SELECT type FROM servers WHERE id = ?");
1010 | $stmt_type->execute([$server_id]);
1011 | $server_type = $stmt_type->fetchColumn();
1012 |
1013 | $sub_path = '';
1014 |
1015 | if ($server_type === 'marzban' || $server_type === 'sanaei') {
1016 | $sub_path_raw = strstr($sub_url, '/sub/');
1017 | if ($sub_path_raw !== false) {
1018 | $sub_path = $sub_path_raw;
1019 | }
1020 | }
1021 |
1022 |
1023 | if (empty($sub_path)) {
1024 | $sub_path = parse_url($sub_url, PHP_URL_PATH);
1025 | }
1026 |
1027 | $full_correct_url = $base_sub_url . $sub_path;
1028 |
1029 | $ch = curl_init();
1030 | curl_setopt_array($ch, [
1031 | CURLOPT_URL => $full_correct_url,
1032 | CURLOPT_RETURNTRANSFER => true,
1033 | CURLOPT_TIMEOUT => 10,
1034 | CURLOPT_SSL_VERIFYPEER => false,
1035 | CURLOPT_SSL_VERIFYHOST => false,
1036 | CURLOPT_FOLLOWLOCATION => true,
1037 | ]);
1038 |
1039 | $response_body = curl_exec($ch);
1040 | $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
1041 | curl_close($ch);
1042 |
1043 | if ($http_code !== 200) {
1044 | error_log("Failed to fetch subscription URL {$full_correct_url}. HTTP Code: {$http_code}");
1045 | return [];
1046 | }
1047 |
1048 | $decoded_links = base64_decode($response_body);
1049 | if ($decoded_links === false) {
1050 | $decoded_links = $response_body;
1051 | }
1052 |
1053 | $links_array = preg_split("/\r\n|\n|\r/", trim($decoded_links));
1054 |
1055 | return array_filter($links_array);
1056 | }
1057 |
1058 | function showPlansForCategoryAndServer($chat_id, $category_id, $server_id) {
1059 | // دریافت نام دسته بندی و سرور برای نمایش در پیام
1060 | $category_name = pdo()->prepare("SELECT name FROM categories WHERE id = ?")->execute([$category_id]) ? pdo()->lastInsertId() : 'نامشخص';
1061 | $server_name = pdo()->prepare("SELECT name FROM servers WHERE id = ?")->execute([$server_id]) ? pdo()->lastInsertId() : 'نامشخص';
1062 |
1063 |
1064 | $stmt = pdo()->prepare("SELECT * FROM plans WHERE category_id = ? AND server_id = ? AND status = 'active' AND is_test_plan = 0");
1065 | $stmt->execute([$category_id, $server_id]);
1066 | $active_plans = $stmt->fetchAll(PDO::FETCH_ASSOC);
1067 |
1068 | if (empty($active_plans)) {
1069 | sendMessage($chat_id, "متاسفانه پلن فعالی برای این سرور یافت نشد.");
1070 | return;
1071 | }
1072 |
1073 | $user_balance = getUserData($chat_id)['balance'] ?? 0;
1074 | $message = "🛍️ پلنهای سرور «{$server_name}»\nموجودی شما: " . number_format($user_balance) . " تومان\n\nلطفا پلن مورد نظر خود را انتخاب کنید:";
1075 | $keyboard_buttons = [];
1076 | foreach ($active_plans as $plan) {
1077 | $button_text = "{$plan['name']} | {$plan['volume_gb']}GB | " . number_format($plan['price']) . " تومان";
1078 | $keyboard_buttons[] = [['text' => $button_text, 'callback_data' => "buy_plan_{$plan['id']}"]];
1079 | }
1080 | // فرمت callback جدید برای کد تخفیف: apply_discount_code_{cat_ID}_{srv_ID}
1081 | $keyboard_buttons[] = [['text' => '🎁 اعمال کد تخفیف', 'callback_data' => "apply_discount_code_{$category_id}_{$server_id}"]];
1082 | // دکمه بازگشت به لیست سرورها برای همان دسته بندی
1083 | $keyboard_buttons[] = [['text' => '◀️ بازگشت به انتخاب سرور', 'callback_data' => 'cat_' . $category_id]];
1084 | sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]);
1085 | }
1086 |
1087 | function applyRenewal($chat_id, $username, $days_to_add, $gb_to_add) {
1088 | $stmt = pdo()->prepare("SELECT server_id FROM services WHERE owner_chat_id = ? AND marzban_username = ?");
1089 | $stmt->execute([$chat_id, $username]);
1090 | $server_id = $stmt->fetchColumn();
1091 |
1092 | if (!$server_id) {
1093 | return ['success' => false, 'message' => 'سرویس در دیتابیس ربات یافت نشد.'];
1094 | }
1095 |
1096 | $current_user_data = getPanelUser($username, $server_id);
1097 | if (!$current_user_data || isset($current_user_data['detail'])) {
1098 | return ['success' => false, 'message' => 'اطلاعات سرویس از پنل دریافت نشد.'];
1099 | }
1100 |
1101 | $update_data = [];
1102 |
1103 | // محاسبه زمان جدید
1104 | if ($days_to_add > 0) {
1105 | $seconds_to_add = $days_to_add * 86400;
1106 | $current_expire = $current_user_data['expire'] ?? 0;
1107 | // اگر سرویس منقضی شده، از زمان حال حساب کن
1108 | $new_expire = ($current_expire > 0 && $current_expire > time()) ? $current_expire + $seconds_to_add : time() + $seconds_to_add;
1109 | $update_data['expire'] = $new_expire;
1110 | }
1111 |
1112 | // محاسبه حجم جدید
1113 | if ($gb_to_add > 0) {
1114 | $bytes_to_add = $gb_to_add * 1024 * 1024 * 1024;
1115 | $current_limit = $current_user_data['data_limit'] ?? 0;
1116 | if ($current_limit > 0) { // فقط به سرویسهای حجمدار، حجم اضافه کن
1117 | $new_limit = $current_limit + $bytes_to_add;
1118 | $update_data['data_limit'] = $new_limit;
1119 | }
1120 | }
1121 |
1122 | if (empty($update_data)) {
1123 | return ['success' => false, 'message' => 'هیچ تغییری برای اعمال وجود نداشت.'];
1124 | }
1125 |
1126 | $result = modifyPanelUser($username, $server_id, $update_data);
1127 |
1128 | // بروزرسانی دیتابیس محلی
1129 | if ($result && !isset($result['detail'])) {
1130 | if(isset($update_data['expire'])){
1131 | pdo()->prepare("UPDATE services SET expire_timestamp = ? WHERE marzban_username = ? AND server_id = ?")->execute([$update_data['expire'], $username, $server_id]);
1132 | }
1133 | if(isset($update_data['data_limit'])){
1134 | $new_volume_gb = ($update_data['data_limit'] / (1024*1024*1024));
1135 | pdo()->prepare("UPDATE services SET volume_gb = ? WHERE marzban_username = ? AND server_id = ?")->execute([$new_volume_gb, $username, $server_id]);
1136 | }
1137 | return ['success' => true];
1138 | }
1139 |
1140 | return ['success' => false, 'message' => 'خطا در ارتباط با پنل برای اعمال تغییرات.'];
1141 | }
1142 |
1143 | function showRenewalManagementMenu($chat_id, $message_id = null) {
1144 | $settings = getSettings();
1145 | $status_icon = ($settings['renewal_status'] ?? 'off') == 'on' ? '✅' : '❌';
1146 | $message = "🔄 مدیریت تمدید سرویس\n\n" .
1147 | "▫️ وضعیت کلی: " . ($status_icon == '✅' ? 'فعال' : 'غیرفعال') . "\n" .
1148 | "▫️ هزینه هر روز تمدید: " . number_format($settings['renewal_price_per_day'] ?? 1000) . " تومان\n" .
1149 | "▫️ هزینه هر گیگابایت تمدید: " . number_format($settings['renewal_price_per_gb'] ?? 2000) . " تومان";
1150 |
1151 | $keyboard = [
1152 | 'inline_keyboard' => [
1153 | [['text' => $status_icon . ' فعال/غیرفعال کردن', 'callback_data' => 'toggle_renewal_status']],
1154 | [['text' => '💰 تنظیم قیمت روز', 'callback_data' => 'set_renewal_price_day']],
1155 | [['text' => '📊 تنظیم قیمت حجم', 'callback_data' => 'set_renewal_price_gb']],
1156 | [['text' => '◀️ بازگشت به پنل', 'callback_data' => 'back_to_admin_panel']],
1157 | ]
1158 | ];
1159 |
1160 | if ($message_id) {
1161 | editMessageText($chat_id, $message_id, $message, $keyboard);
1162 | } else {
1163 | sendMessage($chat_id, $message, $keyboard);
1164 | }
1165 | }
1166 |
1167 | function showMarzbanProtocolEditor($chat_id, $message_id, $server_id) {
1168 | $stmt_server = pdo()->prepare("SELECT name, marzban_protocols FROM servers WHERE id = ?");
1169 | $stmt_server->execute([$server_id]);
1170 | $server = $stmt_server->fetch();
1171 |
1172 | if (!$server) {
1173 | editMessageText($chat_id, $message_id, "❌ سرور یافت نشد.");
1174 | return;
1175 | }
1176 |
1177 | $all_protocols = ['vless', 'vmess', 'trojan', 'shadowsocks'];
1178 |
1179 | $enabled_protocols = $server['marzban_protocols'] ? json_decode($server['marzban_protocols'], true) : ['vless'];
1180 | if (!is_array($enabled_protocols)) $enabled_protocols = ['vless'];
1181 |
1182 | $message = "⚙️ تنظیم پروتکلهای سرور: {$server['name']}\n\n";
1183 | $message .= "پروتکلهایی را که میخواهید برای کاربران جدید در این سرور ایجاد شوند، انتخاب کنید.";
1184 |
1185 | $keyboard_buttons = [];
1186 | $row = [];
1187 | foreach ($all_protocols as $protocol) {
1188 | $icon = in_array($protocol, $enabled_protocols) ? '✅' : '❌';
1189 | $row[] = ['text' => "{$icon} " . ucfirst($protocol), 'callback_data' => "toggle_protocol_{$server_id}_{$protocol}"];
1190 | if (count($row) == 2) {
1191 | $keyboard_buttons[] = $row;
1192 | $row = [];
1193 | }
1194 | }
1195 | if (!empty($row)) {
1196 | $keyboard_buttons[] = $row;
1197 | }
1198 |
1199 | $keyboard_buttons[] = [['text' => '◀️ بازگشت به سرور', 'callback_data' => "view_server_{$server_id}"]];
1200 |
1201 | editMessageText($chat_id, $message_id, $message, ['inline_keyboard' => $keyboard_buttons]);
1202 | }
1203 |
1204 | function createZarinpalLink($chat_id, $amount, $description, $metadata = []) {
1205 | $settings = getSettings();
1206 | $merchant_id = $settings['zarinpal_merchant_id'];
1207 | $script_url = 'https://' . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['PHP_SELF']), '/') . '/verify_payment.php';
1208 |
1209 | $data = [
1210 | "merchant_id" => $merchant_id,
1211 | "amount" => $amount * 10, // تبدیل تومان به ریال
1212 | "callback_url" => $script_url,
1213 | "description" => $description,
1214 | "metadata" => $metadata
1215 | ];
1216 | $jsonData = json_encode($data);
1217 |
1218 | $ch = curl_init('https://api.zarinpal.com/pg/v4/payment/request.json');
1219 | curl_setopt($ch, CURLOPT_USERAGENT, 'ZarinPal Rest Api v4');
1220 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
1221 | curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
1222 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1223 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Content-Length: ' . strlen($jsonData)]);
1224 |
1225 | $result = curl_exec($ch);
1226 | curl_close($ch);
1227 | $result = json_decode($result, true);
1228 |
1229 | if (empty($result['errors'])) {
1230 | $authority = $result['data']['authority'];
1231 |
1232 | // ثبت تراکنش در دیتابیس
1233 | $stmt = pdo()->prepare("INSERT INTO transactions (user_id, amount, authority, description, metadata) VALUES (?, ?, ?, ?, ?)");
1234 | $stmt->execute([$chat_id, $amount, $authority, $description, json_encode($metadata)]);
1235 |
1236 | $payment_url = 'https://www.zarinpal.com/pg/StartPay/' . $authority;
1237 | return ['success' => true, 'url' => $payment_url];
1238 | } else {
1239 | $error_code = $result['errors']['code'];
1240 | return ['success' => false, 'error' => "❌ خطا در اتصال به درگاه پرداخت. کد خطا: {$error_code}"];
1241 | }
1242 | }
1243 |
1244 | function completePurchase($user_id, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied) {
1245 | $plan = getPlanById($plan_id);
1246 | $user_data = getUserData($user_id);
1247 | $first_name = $user_data['first_name'];
1248 |
1249 | // ساخت نام کاربری کامل و یکتا برای پنل
1250 | $plan['full_username'] = preg_replace('/[^a-zA-Z0-9_.]/', '', $custom_name) . '_user' . $user_id . '_' . time();
1251 |
1252 |
1253 | $panel_user_data = createPanelUser($plan, $user_id, $plan_id);
1254 | if ($panel_user_data && isset($panel_user_data['username'])) {
1255 | if ($plan['is_test_plan'] == 1) {
1256 | pdo()->prepare("UPDATE users SET test_config_count = test_config_count + 1 WHERE chat_id = ?")->execute([$user_id]);
1257 | } else {
1258 | updateUserBalance($user_id, $final_price, 'deduct');
1259 | }
1260 |
1261 | if ($plan['purchase_limit'] > 0) {
1262 | pdo()->prepare("UPDATE plans SET purchase_count = purchase_count + 1 WHERE id = ?")->execute([$plan_id]);
1263 | }
1264 |
1265 | if ($discount_applied && $discount_object) {
1266 | pdo()->prepare("UPDATE discount_codes SET usage_count = usage_count + 1 WHERE id = ?")->execute([$discount_object['id']]);
1267 | }
1268 |
1269 | $expire_timestamp = $panel_user_data['expire'] ?? (isset($panel_user_data['expire_date']) ? strtotime($panel_user_data['expire_date']) : (time() + $plan['duration_days'] * 86400));
1270 |
1271 | saveUserService($user_id, [
1272 | 'server_id' => $plan['server_id'],
1273 | 'username' => $panel_user_data['username'],
1274 | 'custom_name' => $custom_name,
1275 | 'plan_id' => $plan_id,
1276 | 'sub_url' => $panel_user_data['subscription_url'],
1277 | 'expire_timestamp' => $expire_timestamp,
1278 | 'volume_gb' => $plan['volume_gb'],
1279 | ]);
1280 |
1281 | $new_balance = $user_data['balance'] - $final_price;
1282 | $sub_link = $panel_user_data['subscription_url'];
1283 | $qr_code_url = generateQrCodeUrl($sub_link);
1284 |
1285 | $caption = "✅ خرید شما با موفقیت انجام شد.\n";
1286 | if ($discount_applied) {
1287 | $caption .= "🏷 قیمت اصلی: " . number_format($plan['price']) . " تومان\n";
1288 | $caption .= "💰 قیمت با تخفیف: " . number_format($final_price) . " تومان\n";
1289 | }
1290 | $caption .= "\n▫️ نام سرویس: " . htmlspecialchars($custom_name) . "\n\n";
1291 |
1292 | if ($plan['show_sub_link']) {
1293 | $caption .= "🔗 لینک اشتراک (Subscription):\n" . htmlspecialchars($sub_link) . "\n\n";
1294 | }
1295 |
1296 | $caption .= "💰 موجودی جدید شما: " . number_format($new_balance) . " تومان";
1297 |
1298 | $chat_info_response = apiRequest('getChat', ['chat_id' => $user_id]);
1299 | $chat_info = json_decode($chat_info_response, true);
1300 |
1301 | $profile_link_html = "👤 کاربر: " . htmlspecialchars($first_name) . " ($user_id)\n";
1302 |
1303 | $admin_notification = "✅ خرید جدید\n\n";
1304 | $admin_notification .= $profile_link_html;
1305 | $admin_notification .= "🛍️ پلن: {$plan['name']}\n";
1306 | $admin_notification .= "💬 نام سرویس: " . htmlspecialchars($custom_name) . "\n";
1307 |
1308 | if ($discount_applied) {
1309 | $admin_notification .= "💵 قیمت اصلی: " . number_format($plan['price']) . " تومان\n";
1310 | $admin_notification .= "🏷 کد تخفیف: {$discount_code}\n";
1311 | $admin_notification .= "💳 مبلغ پرداخت شده: " . number_format($final_price) . " تومان";
1312 | } else {
1313 | $admin_notification .= "💳 مبلغ پرداخت شده: " . number_format($final_price) . " تومان";
1314 | }
1315 |
1316 | $keyboard_buttons = [];
1317 | if ($plan['show_conf_links'] && !empty($panel_user_data['links'])) {
1318 | $keyboard_buttons[] = [['text' => '📋 دریافت کانفیگها', 'callback_data' => "get_configs_{$panel_user_data['username']}"]];
1319 | }
1320 |
1321 | return [
1322 | 'success' => true,
1323 | 'caption' => $caption,
1324 | 'qr_code_url' => $qr_code_url,
1325 | 'keyboard' => ['inline_keyboard' => $keyboard_buttons],
1326 | 'admin_notification' => $admin_notification,
1327 | ];
1328 | }
1329 |
1330 | return [
1331 | 'success' => false,
1332 | 'error_message' => "❌ متاسفانه در ایجاد سرویس شما مشکلی پیش آمد. لطفا با پشتیبانی تماس بگیرید. مبلغی از حساب شما کسر نشده است."
1333 | ];
1334 | }
--------------------------------------------------------------------------------