diff --git a/README.md b/README.md index 2a4b294..2c02c2b 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,75 @@ -ربات تلگرام ویزارد پنل (Wizard Panel) - مدیریت فروش اکانت مرزبان +# WizardPanel Pro (Enhanced Web Edition) 🚀 -ویزارد پنل (Wizard Panel) یک سورس ربات تلگرام پیشرفته و قدرتمند برای مدیریت و فروش خودکار اکانت‌های پنل مرزبان (Marzban) است. این ربات که با PHP و پایگاه داده MySQL توسعه یافته، به شما اجازه می‌دهد تا به راحتی چندین سرور مرزبان را مدیریت کرده و فرآیند فروش کانفیگ‌های V2Ray را به طور کامل اتوماتیک کنید. +> **نسخه پیشرفته و توسعه‌یافته WizardPanel با پنل مدیریت وب و مینی‌اپ تلگرام** -هدف اصلی ویزارد پنل، فراهم کردن یک ابزار حرفه‌ای، امن و با قابلیت شخصی‌سازی بالا برای مدیران سرورهای VPN است تا بتوانند کسب‌وکار خود را بدون دغدغه و به سادگی مدیریت کنند. +این پروژه یک فورک اساسی و توسعه‌یافته از [WizardPanel](https://github.com/webwizards-team/wizardpanel) است. در حالی که نسخه اصلی صرفاً یک ربات تلگرام CLI بود، این نسخه با افزودن **رابط کاربری تحت وب (Web Interface)**، قدرت مدیریت و کاربری را به سطح کاملاً جدیدی ارتقا داده است. -![Wizard Panel](https://raw.githubusercontent.com/webwizards-team/wizardpanel/e67cc64f01ef7808c9c65850f91606ea56451cfb/wizardpanel.png) +--- -✨ ویژگی‌های کلیدی ویزارد پنل (Wizard Panel) +## ✨ ویژگی‌های جدید و متمایز (New Features) -🚀 مدیریت چند سرور مرزبان: به راحتی چندین سرور مرزبان را اضافه، مدیریت و حذف کنید. هنگام ساخت پلن، انتخاب کنید که روی کدام سرور ساخته شود. +تغییر اصلی در این نسخه، اضافه شدن پوشه `web/` است که شامل دو بخش مجزا و قدرتمند می‌باشد: -🔐 پنل مدیریت قدرتمند و با دسترسی طبقه‌بندی شده: +### 1. 🌐 پنل مدیریت تحت وب (Web Admin Panel) +دیگر نیازی نیست همه کارها را با دستورات تلگرام انجام دهید! اکنون یک پنل مدیریت کامل در اختیار دارید: +- **مدیریت داشبورد:** مشاهده آمار فروش، کاربران آنلاین و وضعیت سرورها به صورت گرافیکی. +- **مدیریت کاربران:** مشاهده لیست کامل کاربران، موجودی، سرویس‌ها و ویرایش اطلاعات آنها. +- **مدیریت سرورها:** افزودن و ویرایش سرورهای Marzban/Sanaei/Marzneshin از طریق وب. +- **مدیریت پلن‌ها و دسته‌بندی‌ها:** تعریف قیمت، حجم و مدت زمان سرویس‌ها با رابط کاربری آسان. +- **مدیریت مالی:** مشاهده و تایید/رد درخواست‌های افزایش موجودی و کارت‌به‌کارت. +- **سیستم تیکتینگ:** پاسخگویی به تیکت‌های پشتیبانی کاربران از محیط وب. +- **ارسال همگانی:** ارسال پیام به همه کاربران با سرعت بالا. -مدیریت کامل سرورها، دسته‌بندی‌ها و پلن‌های فروش. +### 2. 📱 مینی‌اپ تلگرام (User Mini App) +تجربه کاربری مدرن برای مشتریان شما با استفاده از Telegram Web App: +- **خرید و تمدید آسان:** رابط کاربری گرافیکی برای انتخاب سرور و پلن. +- **کیف پول پیشرفته:** امکان افزایش موجودی، آپلود رسید کارت‌به‌کارت و مشاهده تراکنش‌ها. +- **مشاهده سرویس‌ها:** نمایش وضعیت سرویس، حجم باقی‌مانده و تاریخ انقضا با گرافیک زیبا. +- **پشتیبانی:** ارسال تیکت و دریافت پاسخ در محیط چت‌مانند. +- **احراز هویت امن:** لاگین خودکار و ایمن با استفاده از داده‌های تلگرام. -قابلیت ویرایش کامل اطلاعات پلن‌ها (نام، قیمت، حجم، مدت، سرور و...). -مدیریت کاربران (افزایش/کاهش موجودی، ارسال پیام، مسدود/آزاد کردن). +--- -عملیات همگانی روی تمام سرویس‌ها (افزایش حجم یا زمان انقضا). +## 📂 ساختار فایل‌ها (File Structure) -مشاهده آمار دقیق کاربران و درآمد (روزانه، هفتگی، ماهانه). +``` +src/ +├── api/ # ارتباط با پنل‌های مرزبان و ... +├── includes/ # توابع و تنظیمات اصلی (هسته ربات) +├── web/ # <--- بخش جدید اضافه شده +│ ├── assets/ # فایل‌های CSS, JS, Images +│ ├── pages/ # صفحات پنل ادمین (Users, Plans, Servers...) +│ ├── user/ # صفحات مینی‌اپ کاربر (Wallet, Shop, Renew...) +│ ├── login.php # صفحه ورود ادمین +│ └── index.php # داشبورد اصلی +├── bot.php # هسته ربات تلگرام +└── install.php # اسکریپت نصب خودکار +``` -مدیریت روش‌های پرداخت و درگاه‌ها. +--- -سیستم مدیریت ادمین‌ها با قابلیت تعریف دسترسی‌های مختلف (فقط برای ادمین اصلی). +## 🛠 نصب و راه‌اندازی (Installation) -🛒 فرآیند خرید کاملاً خودکار برای کاربر: +1. **آپلود فایل‌ها:** کل محتویات پوشه `src` را در هاست خود آپلود کنید. +2. **نصب:** فایل `install.php` را در مرورگر اجرا کنید تا دیتابیس ساخته شود. +3. **تنظیم وبهوک:** وبهوک ربات را به فایل `bot.php` ست کنید. +4. **تنظیم وب‌اپ:** در BotFather، آدرس `https://your-domain.com/web/user/` را به عنوان Menu Button URL تنظیم کنید. -مشاهده دسته‌بندی‌ها و پلن‌های هر سرور. +# -شارژ حساب از طریق کارت به کارت و تایید توسط ادمین. +--- -خرید آنی سرویس و دریافت کانفیگ بلافاصله پس از خرید. +## 📸 اسکرین‌شات‌ها -مشاهده و مدیریت سرویس‌های فعال. +*پنل ادمین* +![توضیح عکس](web-admin.png) -دریافت کانفیگ تست رایگان (با قابلیت محدودسازی توسط ادمین). -سیستم تیکتینگ برای پشتیبانی. +*پنل مینی اپ* +![توضیح عکس](mini-app.jpg) -🔔 سیستم اعلان‌های هوشمند (Cron Job): +--- -ارسال هشدار خودکار به کاربران قبل از اتمام حجم یا زمان سرویس. - -ارسال پیام یادآوری به کاربران غیرفعال. - -🔗 عضویت اجباری در کانال (Force Join): - -قابلیت فعال/غیرفعال‌سازی و تنظیم کانال تلگرام. - -دکمه هوشمند "✅ عضو شدم" برای تجربه کاربری بهتر. - -✅ سیستم احراز هویت کاربران: - -امکان فعال‌سازی تایید هویت کاربران جدید (از طریق شماره تماس یا دکمه شیشه‌ای) قبل از استفاده از ربات. - -🎁 ابزارهای بازاریابی: - -سیستم مدیریت کدهای تخفیف (درصدی و مبلغی). - -قابلیت تعریف هدیه خوش‌آمدگویی (شارژ اولیه) برای کاربران جدید. - -⚙️ نصب و ارتقا آسان: - -دارای اسکریپت نصب خودکار (install.php) که جداول دیتابیس را ساخته و وبهوک را تنظیم می‌کند. - -🔧 راهنمای نصب و راه‌اندازی - -نصب ویزارد پنل بسیار ساده است و در چند مرحله انجام می‌شود: - -دانلود سورس: ابتدا سورس کامل پروژه را از این مخزن گیت‌هاب دانلود کنید. - -آپلود در هاست: فایل‌های دانلود شده را در هاست خود (در یک پوشه مانند bot یا روت اصلی) آپلود کنید. - -ساخت دیتابیس: یک پایگاه داده (Database) جدید از نوع MySQL یا MariaDB در هاست خود ایجاد کنید. اطلاعات دیتابیس (نام، نام کاربری و رمز عبور) را یادداشت کنید. - -ساخت ربات در تلگرام: - -به ربات @BotFather در تلگرام بروید. - -یک ربات جدید بسازید و توکن (Token) آن را کپی کنید. - -شناسه عددی اکانت تلگرام خودتان را که می‌خواهید ادمین اصلی باشد، از ربات‌هایی مانند @userinfobot دریافت کنید. - -اجرای اسکریپت نصب: - -فایل install.php را در مرورگر خود باز کنید. (مثال: https://yourdomain.com/bot/install.php) - -فرم نصب نمایش داده می‌شود. اطلاعات زیر را با دقت وارد کنید: - -توکن ربات: توکنی که از BotFather گرفتید. - -آیدی عددی ادمین اصلی: شناسه عددی اکانت تلگرام خودتان. - -اطلاعات دیتابیس: اطلاعاتی که در مرحله ۳ ایجاد کردید. - -روی دکمه "نصب و راه‌اندازی" کلیک کنید. اسکریپت به صورت خودکار جداول را ساخته، فایل config.php را آپدیت کرده و وبهوک (Webhook) را برای ربات شما تنظیم می‌کند. - -⚠️ مرحله امنیتی بسیار مهم: - -پس از مشاهده پیام موفقیت‌آمیز، بلافاصله فایل install.php را از روی هاست خود حذف کنید. باقی ماندن این فایل یک حفره امنیتی بزرگ محسوب می‌شود. - -شروع کار با ربات: به ربات خود در تلگرام رفته و دستور /start را ارسال کنید. منوی مدیریت باید برای شما نمایش داده شود. - -cron.php - تنظیم کرون جاب (Cron Job) - -برای اینکه سیستم اعلان‌های خودکار (هشدار انقضا و یادآوری به کاربران غیرفعال) کار کند، باید یک کرون جاب در هاست خود تنظیم کنید. - -وارد کنترل پنل هاست خود (cPanel, DirectAdmin, etc.) شوید و به بخش Cron Jobs بروید. - -یک کرون جاب جدید با تنظیمات زیر ایجاد کنید (توصیه می‌شود هر ۵ دقیقه یکبار اجرا شود): - -code -Bash -download -content_copy -expand_less - -*/5 * * * * /usr/bin/php /home/your_username/public_html/botsql/cron.php - -توجه: مسیر /usr/bin/php و /home/your_username/... ممکن است در هاست شما متفاوت باشد. مسیر صحیح را از پشتیبانی هاست خود سوال کنید. - -🔑 کلمات کلیدی برای جستجو - -سورس ربات تلگرام ویزارد پنل، Wizard Panel, ربات پنل مرزبان, سورس ربات فروش v2ray, ربات فروش کانفیگ تلگرام, ربات مرزبان چند سروره, سورس ربات PHP, ربات فروش VPN, ربات مدیریت مرزبان, Marzban panel bot, Marzban multi server bot, Telegram VPN sales bot source. - -🤝 مشارکت در پروژه - -از مشارکت شما در توسعه و بهبود ویزارد پنل استقبال می‌کنیم. می‌توانید از طریق Pull Request تغییرات خود را ارسال کنید یا با ثبت Issue مشکلات و پیشنهادات خود را با ما در میان بگذارید. - -📜 مجوز (License) - -این پروژه تحت مجوز MIT منتشر شده است. +## ❤️ تقدیر و تشکر +این پروژه بر پایه هسته قدرتمند [WizardPanel](https://github.com/webwizards-team/wizardpanel) بنا شده و با هدف ارائه تجربه کاربری بهتر توسعه یافته است. diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..f1e0d04 --- /dev/null +++ b/install.sh @@ -0,0 +1,871 @@ +#!/bin/bash + +#═══════════════════════════════════════════════════════════════════════════════ +# WIZARD PANEL INSTALLER v0.1.4 +# Automated Installation Script for Linux +#═══════════════════════════════════════════════════════════════════════════════ + +# ═══════════════════════════════════════════════════════════════════════════════ +# COLOR DEFINITIONS +# ═══════════════════════════════════════════════════════════════════════════════ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +WHITE='\033[1;37m' +BOLD='\033[1m' +NC='\033[0m' + +# ═══════════════════════════════════════════════════════════════════════════════ +# GLOBAL VARIABLES +# ═══════════════════════════════════════════════════════════════════════════════ +PROJECT_URL="https://github.com/poryajp/wizardpanel/archive/refs/tags/0.1.4.zip" +WP_DIR="/root/ols/wp" +DB_DIR="/root/ols/db" +OLS_DIR="/root/ols" +COMPOSE_CMD="" +PKG_MANAGER="" +OS="" + +# ═══════════════════════════════════════════════════════════════════════════════ +# PRINT FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════════ +print_banner() { + clear + echo -e "${CYAN}" + echo "╔═══════════════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ ██╗ ██╗██╗███████╗ █████╗ ██████╗ ██████╗ ║" + echo "║ ██║ ██║██║╚══███╔╝██╔══██╗██╔══██╗██╔══██╗ ║" + echo "║ ██║ █╗ ██║██║ ███╔╝ ███████║██████╔╝██║ ██║ ║" + echo "║ ██║███╗██║██║ ███╔╝ ██╔══██║██╔══██╗██║ ██║ ║" + echo "║ ╚███╔███╔╝██║███████╗██║ ██║██║ ██║██████╔╝ ║" + echo "║ ╚══╝╚══╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ║" + echo "║ ║" + echo "║ 🧙 PANEL INSTALLER v0.1.4 🧙 ║" + echo "║ ║" + echo "╚═══════════════════════════════════════════════════════════════════════╝" + echo -e "${NC}" + echo "" +} + +print_step() { + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE}[▶]${NC} ${WHITE}${BOLD}$1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +print_info() { + echo -e "${CYAN} ℹ${NC} ${WHITE}$1${NC}" +} + +print_success() { + echo -e "${GREEN} ✓${NC} ${WHITE}$1${NC}" +} + +print_error() { + echo -e "${RED} ✗${NC} ${WHITE}$1${NC}" +} + +print_warning() { + echo -e "${YELLOW} ⚠${NC} ${WHITE}$1${NC}" +} + +print_separator() { + echo -e "${PURPLE}───────────────────────────────────────────────────────────────────────${NC}" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# ERROR HANDLING +# ═══════════════════════════════════════════════════════════════════════════════ +error_exit() { + print_error "$1" + exit 1 +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# CHECK ROOT +# ═══════════════════════════════════════════════════════════════════════════════ +check_root() { + print_step "Checking Root Privileges" + if [ "$(id -u)" != "0" ]; then + print_error "This script must be run as root!" + print_info "Please run: sudo bash $0" + exit 1 + fi + print_success "Running as root user" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# OS DETECTION (از اسکریپت مرجع) +# ═══════════════════════════════════════════════════════════════════════════════ +detect_os() { + print_step "Detecting Operating System" + + if [ -f /etc/lsb-release ]; then + OS=$(lsb_release -si) + elif [ -f /etc/os-release ]; then + OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') + elif [ -f /etc/redhat-release ]; then + OS=$(cat /etc/redhat-release | awk '{print $1}') + elif [ -f /etc/arch-release ]; then + OS="Arch" + else + print_warning "Could not detect OS, will try generic methods" + OS="Unknown" + fi + + print_success "Detected: $OS" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# PACKAGE MANAGER DETECTION AND UPDATE (از اسکریپت مرجع) +# ═══════════════════════════════════════════════════════════════════════════════ +detect_and_update_package_manager() { + print_info "Detecting and updating package manager..." + + if [[ "$OS" == *"Ubuntu"* ]] || [[ "$OS" == *"Debian"* ]]; then + PKG_MANAGER="apt-get" + $PKG_MANAGER update -qq >/dev/null 2>&1 || true + elif [[ "$OS" == *"CentOS"* ]] || [[ "$OS" == *"AlmaLinux"* ]] || [[ "$OS" == *"Rocky"* ]]; then + PKG_MANAGER="yum" + $PKG_MANAGER update -y -q >/dev/null 2>&1 || true + $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1 || true + elif [[ "$OS" == *"Fedora"* ]]; then + PKG_MANAGER="dnf" + $PKG_MANAGER update -q -y >/dev/null 2>&1 || true + elif [[ "$OS" == *"Arch"* ]]; then + PKG_MANAGER="pacman" + $PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 || true + elif [[ "$OS" == *"openSUSE"* ]]; then + PKG_MANAGER="zypper" + $PKG_MANAGER refresh --quiet >/dev/null 2>&1 || true + else + # Try to detect package manager + if command -v apt-get >/dev/null 2>&1; then + PKG_MANAGER="apt-get" + $PKG_MANAGER update -qq >/dev/null 2>&1 || true + elif command -v yum >/dev/null 2>&1; then + PKG_MANAGER="yum" + elif command -v dnf >/dev/null 2>&1; then + PKG_MANAGER="dnf" + elif command -v pacman >/dev/null 2>&1; then + PKG_MANAGER="pacman" + else + print_warning "Could not detect package manager" + fi + fi + + print_success "Package manager: $PKG_MANAGER" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# INSTALL PACKAGE (از اسکریپت مرجع) +# ═══════════════════════════════════════════════════════════════════════════════ +install_package() { + local PACKAGE=$1 + print_info "Installing $PACKAGE..." + + if [ -z "$PKG_MANAGER" ]; then + detect_and_update_package_manager + fi + + case $PKG_MANAGER in + apt-get) + $PKG_MANAGER -y -qq install "$PACKAGE" >/dev/null 2>&1 || $PKG_MANAGER -y install "$PACKAGE" + ;; + yum) + $PKG_MANAGER install -y -q "$PACKAGE" >/dev/null 2>&1 || $PKG_MANAGER install -y "$PACKAGE" + ;; + dnf) + $PKG_MANAGER install -y -q "$PACKAGE" >/dev/null 2>&1 || $PKG_MANAGER install -y "$PACKAGE" + ;; + pacman) + $PKG_MANAGER -S --noconfirm --quiet "$PACKAGE" >/dev/null 2>&1 || $PKG_MANAGER -S --noconfirm "$PACKAGE" + ;; + zypper) + $PKG_MANAGER --quiet install -y "$PACKAGE" >/dev/null 2>&1 || $PKG_MANAGER install -y "$PACKAGE" + ;; + *) + print_warning "Unknown package manager, trying common methods..." + apt-get install -y "$PACKAGE" 2>/dev/null || yum install -y "$PACKAGE" 2>/dev/null || dnf install -y "$PACKAGE" 2>/dev/null + ;; + esac +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# INSTALL DEPENDENCIES +# ═══════════════════════════════════════════════════════════════════════════════ +install_dependencies() { + print_step "Installing Required Dependencies" + + detect_and_update_package_manager + + # Install curl if not exists + if ! command -v curl >/dev/null 2>&1; then + install_package curl + fi + print_success "curl is installed" + + # Install wget if not exists + if ! command -v wget >/dev/null 2>&1; then + install_package wget + fi + print_success "wget is installed" + + # Install unzip if not exists + if ! command -v unzip >/dev/null 2>&1; then + install_package unzip + fi + print_success "unzip is installed" + + print_success "All dependencies installed successfully" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# INSTALL DOCKER (از اسکریپت مرجع - روش رسمی) +# ═══════════════════════════════════════════════════════════════════════════════ +install_docker() { + print_step "Installing Docker Engine" + + # Check if Docker is already installed and working + if command -v docker >/dev/null 2>&1; then + if docker info >/dev/null 2>&1; then + DOCKER_VERSION=$(docker --version | awk '{print $3}' | tr -d ',') + print_success "Docker is already installed and running (version: ${DOCKER_VERSION})" + return 0 + else + print_warning "Docker is installed but not running. Attempting to start..." + systemctl start docker 2>/dev/null || service docker start 2>/dev/null || true + sleep 3 + if docker info >/dev/null 2>&1; then + print_success "Docker started successfully" + return 0 + fi + print_warning "Could not start Docker. Will reinstall..." + fi + fi + + print_info "Installing Docker using official script..." + + # Install Docker using official script (این روش همیشه کار میکند) + curl -fsSL https://get.docker.com | sh + + # Start Docker service + print_info "Starting Docker service..." + if command -v systemctl >/dev/null 2>&1; then + systemctl start docker 2>/dev/null || true + systemctl enable docker 2>/dev/null || true + elif command -v service >/dev/null 2>&1; then + service docker start 2>/dev/null || true + fi + + # Wait for Docker to be ready + local attempts=0 + local max_attempts=30 + print_info "Waiting for Docker to be ready..." + + while [ $attempts -lt $max_attempts ]; do + if docker info >/dev/null 2>&1; then + break + fi + sleep 1 + attempts=$((attempts + 1)) + done + + # Verify installation + if docker info >/dev/null 2>&1; then + DOCKER_VERSION=$(docker --version | awk '{print $3}' | tr -d ',') + print_success "Docker installed successfully (version: ${DOCKER_VERSION})" + else + error_exit "Docker installation failed! Please install Docker manually and re-run this script." + fi +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# DETECT DOCKER COMPOSE (از اسکریپت مرجع) +# ═══════════════════════════════════════════════════════════════════════════════ +detect_compose() { + print_step "Detecting Docker Compose" + + # Check if docker compose command exists + if docker compose version >/dev/null 2>&1; then + COMPOSE_CMD='docker compose' + local version=$(docker compose version --short 2>/dev/null || docker compose version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + print_success "Docker Compose found: $COMPOSE_CMD (version: $version)" + elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD='docker-compose' + local version=$(docker-compose --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + print_success "Docker Compose found: $COMPOSE_CMD (version: $version)" + else + print_warning "Docker Compose not found. Installing..." + install_docker_compose + fi +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# INSTALL DOCKER COMPOSE +# ═══════════════════════════════════════════════════════════════════════════════ +install_docker_compose() { + print_info "Installing Docker Compose..." + + # Try installing plugin first + if [ -n "$PKG_MANAGER" ]; then + case $PKG_MANAGER in + apt-get) + $PKG_MANAGER install -y docker-compose-plugin >/dev/null 2>&1 || true + ;; + yum|dnf) + $PKG_MANAGER install -y docker-compose-plugin >/dev/null 2>&1 || true + ;; + esac + fi + + # Check if plugin installed + if docker compose version >/dev/null 2>&1; then + COMPOSE_CMD='docker compose' + print_success "Docker Compose plugin installed" + return 0 + fi + + # Install standalone docker-compose + print_info "Installing Docker Compose standalone..." + + local COMPOSE_VERSION="v2.24.0" + local ARCH=$(uname -m) + + case $ARCH in + x86_64) ARCH="x86_64" ;; + aarch64|arm64) ARCH="aarch64" ;; + armv7l) ARCH="armv7" ;; + *) ARCH="x86_64" ;; + esac + + curl -SL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-${ARCH}" \ + -o /usr/local/bin/docker-compose 2>/dev/null + + chmod +x /usr/local/bin/docker-compose + + if command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD='docker-compose' + print_success "Docker Compose standalone installed" + else + error_exit "Failed to install Docker Compose!" + fi +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# DIRECTORY CREATION +# ═══════════════════════════════════════════════════════════════════════════════ +create_directories() { + print_step "Creating Project Directories" + + # Check if directory exists and not empty + if [ -d "$WP_DIR" ] && [ "$(ls -A $WP_DIR 2>/dev/null)" ]; then + print_warning "Directory $WP_DIR already exists and is not empty" + echo -ne "${YELLOW} ⚠${NC} Do you want to remove it and start fresh? (y/N): " + read -r REPLY + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + rm -rf "$WP_DIR" + print_info "Removed existing directory" + else + print_info "Keeping existing directory" + fi + fi + + mkdir -p "$WP_DIR" + print_success "Created: $WP_DIR" + + mkdir -p "$DB_DIR" + print_success "Created: $DB_DIR" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# PROJECT DOWNLOAD +# ═══════════════════════════════════════════════════════════════════════════════ +download_project() { + print_step "Downloading Wizard Panel" + + cd "$WP_DIR" || error_exit "Cannot change to directory $WP_DIR" + + print_info "Downloading from GitHub..." + + # Remove old zip if exists + rm -f wizardpanel.zip 2>/dev/null || true + + # Download + if wget --no-verbose --show-progress "$PROJECT_URL" -O wizardpanel.zip 2>&1; then + print_success "Download completed" + else + # Try with curl as fallback + print_warning "wget failed, trying curl..." + if curl -L -o wizardpanel.zip "$PROJECT_URL"; then + print_success "Download completed with curl" + else + error_exit "Failed to download project!" + fi + fi + + print_info "Extracting files..." + + if ! unzip -q -o wizardpanel.zip; then + error_exit "Failed to extract archive!" + fi + + print_success "Extraction completed" + + # Find the extracted directory + print_info "Moving source files..." + + EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "wizardpanel*" | head -1) + + if [ -z "$EXTRACTED_DIR" ]; then + error_exit "Could not find extracted directory!" + fi + + print_info "Found directory: $EXTRACTED_DIR" + + # Check if src directory exists + if [ ! -d "$EXTRACTED_DIR/src" ]; then + error_exit "Source directory not found in archive!" + fi + + # Move contents from src to wp directory + if [ -d "$EXTRACTED_DIR/src" ]; then + # Copy all files including hidden ones + cp -rf "$EXTRACTED_DIR"/src/* . 2>/dev/null || true + cp -rf "$EXTRACTED_DIR"/src/.[!.]* . 2>/dev/null || true + fi + + # Clean up + rm -rf "$EXTRACTED_DIR" + rm -f wizardpanel.zip + + # Verify + local file_count=$(find . -maxdepth 1 -type f 2>/dev/null | wc -l) + + if [ "$file_count" -eq 0 ]; then + error_exit "No files found after extraction!" + fi + + print_success "Source files moved successfully ($file_count files)" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# GENERATE RANDOM PASSWORD/STRING +# ═══════════════════════════════════════════════════════════════════════════════ +generate_random_password() { + local length=${1:-16} + # Generate a random alphanumeric password + tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length" +} + +generate_random_username() { + local prefix=${1:-"user"} + # Generate username with prefix and 6 random characters + echo "${prefix}$(tr -dc 'a-z0-9' < /dev/urandom | head -c 6)" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# USER INPUT +# ═══════════════════════════════════════════════════════════════════════════════ +get_user_inputs() { + print_step "Configuration Settings" + + echo "" + echo -e "${PURPLE}╔═══════════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${PURPLE}║ 📝 ENTER YOUR CONFIGURATION ║${NC}" + echo -e "${PURPLE}║ (Leave empty to auto-generate secure values) ║${NC}" + echo -e "${PURPLE}╚═══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" + + # MySQL Root Password + echo -e "${CYAN}[1/6]${NC} ${WHITE}Enter MySQL Root Password (leave empty to auto-generate):${NC}" + read -s MYSQL_ROOT_PASSWORD + echo + if [ -z "$MYSQL_ROOT_PASSWORD" ]; then + MYSQL_ROOT_PASSWORD=$(generate_random_password 16) + print_success "MySQL root password auto-generated" + else + print_success "MySQL root password set" + fi + + print_separator + + # MySQL User + echo -e "${CYAN}[2/6]${NC} ${WHITE}Enter MySQL Username (leave empty to auto-generate):${NC}" + read MYSQL_USER + if [ -z "$MYSQL_USER" ]; then + MYSQL_USER=$(generate_random_username "dbuser") + print_success "MySQL username auto-generated: $MYSQL_USER" + else + print_success "MySQL username: $MYSQL_USER" + fi + + print_separator + + # MySQL Password + echo -e "${CYAN}[3/6]${NC} ${WHITE}Enter MySQL User Password (leave empty to auto-generate):${NC}" + read -s MYSQL_PASSWORD + echo + if [ -z "$MYSQL_PASSWORD" ]; then + MYSQL_PASSWORD=$(generate_random_password 16) + print_success "MySQL user password auto-generated" + else + print_success "MySQL user password set" + fi + + print_separator + + # WordPress Port + echo -e "${CYAN}[4/6]${NC} ${WHITE}Enter WP/Panel Port (default: 80):${NC}" + read WP_PORT + WP_PORT=${WP_PORT:-80} + + # Validate port number + if ! [[ "$WP_PORT" =~ ^[0-9]+$ ]] || [ "$WP_PORT" -lt 1 ] || [ "$WP_PORT" -gt 65535 ]; then + print_warning "Invalid port number. Using default: 80" + WP_PORT=80 + fi + print_success "WP port: $WP_PORT" + + print_separator + + # OLS Admin User + echo -e "${CYAN}[5/6]${NC} ${WHITE}Enter OpenLiteSpeed Admin Username (leave empty to auto-generate):${NC}" + read LSWS_ADMIN_USER + if [ -z "$LSWS_ADMIN_USER" ]; then + LSWS_ADMIN_USER=$(generate_random_username "admin") + print_success "OLS admin username auto-generated: $LSWS_ADMIN_USER" + else + print_success "OLS admin username: $LSWS_ADMIN_USER" + fi + + print_separator + + # OLS Admin Password + echo -e "${CYAN}[6/6]${NC} ${WHITE}Enter OpenLiteSpeed Admin Password (leave empty to auto-generate):${NC}" + read -s LSWS_ADMIN_PASS + echo + if [ -z "$LSWS_ADMIN_PASS" ]; then + LSWS_ADMIN_PASS=$(generate_random_password 16) + print_success "OLS admin password auto-generated" + else + print_success "OLS admin password set" + fi + + echo "" + print_success "All configuration settings saved!" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# DOCKER COMPOSE FILE CREATION +# ═══════════════════════════════════════════════════════════════════════════════ +create_docker_compose() { + print_step "Creating Docker Compose Configuration" + + cat > "$OLS_DIR/docker-compose.yml" << EOF +version: '3.8' + +services: + db: + image: mysql:8.0 + container_name: ols-db + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" + MYSQL_DATABASE: db + MYSQL_USER: "${MYSQL_USER}" + MYSQL_PASSWORD: "${MYSQL_PASSWORD}" + volumes: + - ${DB_DIR}:/var/lib/mysql + networks: + - ols-network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + wordpress: + image: litespeedtech/openlitespeed:latest + container_name: ols-wp + restart: unless-stopped + depends_on: + db: + condition: service_healthy + ports: + - "${WP_PORT}:80" + - "7080:7080" + environment: + TZ: Asia/Tehran + LSWS_ADMIN_USER: "${LSWS_ADMIN_USER}" + LSWS_ADMIN_PASS: "${LSWS_ADMIN_PASS}" + volumes: + - ${WP_DIR}:/var/www/vhosts/localhost/html + networks: + - ols-network + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: ols-phpmyadmin + restart: unless-stopped + depends_on: + - db + ports: + - "8081:80" + environment: + PMA_HOST: db + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" + networks: + - ols-network + +networks: + ols-network: + driver: bridge +EOF + + print_success "docker-compose.yml created at $OLS_DIR/" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# START CONTAINERS +# ═══════════════════════════════════════════════════════════════════════════════ +start_containers() { + print_step "Starting Docker Containers" + + cd "$OLS_DIR" || error_exit "Cannot change to directory $OLS_DIR" + + print_info "Pulling Docker images (this may take a few minutes)..." + + # Pull images + $COMPOSE_CMD -f "$OLS_DIR/docker-compose.yml" pull || print_warning "Some images might not have been pulled" + + print_success "Images pulled" + + print_info "Starting containers..." + + # Start containers + $COMPOSE_CMD -f "$OLS_DIR/docker-compose.yml" up -d || error_exit "Failed to start containers" + + # Wait for containers to start + print_info "Waiting for containers to initialize..." + + local max_attempts=60 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + printf "\r${CYAN} ⏳${NC} Waiting for containers... (%d/%d)" "$attempt" "$max_attempts" + + # Check if all containers are running + local running=$(docker ps --filter "name=ols-" --filter "status=running" --format "{{.Names}}" 2>/dev/null | wc -l) + + if [ "$running" -ge 3 ]; then + echo "" + print_success "All containers are running!" + break + fi + + sleep 2 + attempt=$((attempt + 1)) + done + + if [ $attempt -gt $max_attempts ]; then + echo "" + print_warning "Some containers might still be starting..." + fi + + # Show container status + echo "" + print_info "Container Status:" + docker ps --filter "name=ols-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || docker ps --filter "name=ols-" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# SET PERMISSIONS +# ═══════════════════════════════════════════════════════════════════════════════ +set_permissions() { + print_step "Setting Directory Permissions" + + # Create required directories + RECEIPTS_DIR="/root/ols/wp/web/user/uploads/receipts" + + # Wait for container to create structure + sleep 5 + + # Create directory if it doesn't exist + mkdir -p "$RECEIPTS_DIR" + print_success "Created: $RECEIPTS_DIR" + + # Set permissions + chmod 0777 "$RECEIPTS_DIR" + chmod 666 ols/wp/includes/config.php + print_success "Set permissions 0777 on receipts directory" + + # Set proper permissions on wp directory + chmod -R 755 "$WP_DIR" 2>/dev/null || true + print_info "Set permissions 755 on WP directory" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# GET SERVER IP +# ═══════════════════════════════════════════════════════════════════════════════ +get_server_ip() { + SERVER_IP="" + + # Try IPv4 first + SERVER_IP=$(curl -s -4 --connect-timeout 3 --max-time 5 ifconfig.io 2>/dev/null | tr -d '[:space:]') + + # Try IPv6 if IPv4 failed + if [ -z "$SERVER_IP" ]; then + SERVER_IP=$(curl -s -6 --connect-timeout 3 --max-time 5 ifconfig.io 2>/dev/null | tr -d '[:space:]') + fi + + # Try other services + if [ -z "$SERVER_IP" ]; then + for service in "icanhazip.com" "ipinfo.io/ip" "api.ipify.org" "checkip.amazonaws.com"; do + SERVER_IP=$(curl -s --connect-timeout 3 --max-time 5 "$service" 2>/dev/null | tr -d '[:space:]') + if [ -n "$SERVER_IP" ]; then + break + fi + done + fi + + # Fallback to local IP + if [ -z "$SERVER_IP" ]; then + SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}') + fi + + if [ -z "$SERVER_IP" ]; then + SERVER_IP="YOUR_SERVER_IP" + fi +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# DISPLAY FINAL INFORMATION +# ═══════════════════════════════════════════════════════════════════════════════ +display_info() { + get_server_ip + + echo "" + echo "" + echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ ║${NC}" + echo -e "${GREEN}║ 🎉🎉🎉 INSTALLATION COMPLETED SUCCESSFULLY! 🎉🎉🎉 ║${NC}" + echo -e "${GREEN}║ ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo "" + + # Configuration Summary + echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${CYAN}║ 📋 YOUR CONFIGURATION SUMMARY ║${NC}" + echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e " ${YELLOW} DATABASE CONFIGURATION${NC}" + echo -e " ${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e " ${WHITE}MySQL Host:${NC} ${GREEN}db${NC}" + echo -e " ${WHITE}MySQL Database:${NC} ${GREEN}db${NC}" + echo -e " ${WHITE}MySQL Port:${NC} ${GREEN}3306${NC}" + echo -e " ${WHITE}MySQL Root Password:${NC} ${GREEN}${MYSQL_ROOT_PASSWORD}${NC}" + echo -e " ${WHITE}MySQL Username:${NC} ${GREEN}${MYSQL_USER}${NC}" + echo -e " ${WHITE}MySQL User Password:${NC} ${GREEN}${MYSQL_PASSWORD}${NC}" + echo "" + echo -e " ${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e " ${PURPLE} OPENLITESPEED CONFIGURATION${NC}" + echo -e " ${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e " ${WHITE}Admin Username:${NC} ${GREEN}${LSWS_ADMIN_USER}${NC}" + echo -e " ${WHITE}Admin Password:${NC} ${GREEN}${LSWS_ADMIN_PASS}${NC}" + echo -e " ${WHITE}Admin Panel Port:${NC} ${GREEN}7080${NC}" + echo "" + echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e " ${BLUE} PORT CONFIGURATION${NC}" + echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e " ${WHITE}Wizard Panel Port:${NC} ${GREEN}${WP_PORT}${NC}" + echo -e " ${WHITE}OLS Admin Port:${NC} ${GREEN}7080${NC}" + echo -e " ${WHITE}phpMyAdmin Port:${NC} ${GREEN}8081${NC}" + echo "" + echo "" + + # Access Links + echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ 🔗 ACCESS LINKS ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${BOLD}${YELLOW}★ Wizard Panel Installation Page:${NC}" + echo -e " ${WHITE}http://${SERVER_IP}:${WP_PORT}/install.php${NC}" + echo "" + echo -e " ${BOLD}${PURPLE}★ OpenLiteSpeed Admin Panel:${NC}" + echo -e " ${WHITE}https://${SERVER_IP}:7080${NC}" + echo "" + echo -e " ${BOLD}${BLUE}★ phpMyAdmin:${NC}" + echo -e " ${WHITE}http://${SERVER_IP}:8081${NC}" + echo "" + echo "" + + # Important Note + echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${YELLOW}║ ⚠️ IMPORTANT NOTES ║${NC}" + echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${WHITE}1.${NC} Instead of using the server IP address, you can use your" + echo -e " domain that has been configured with ${BOLD}Cloudflare Zero Trust${NC}" + echo -e " service on port ${WHITE}${WP_PORT}${NC}" + echo "" + echo -e " ${CYAN}Example: https://yourdomain.com/install.php${NC}" + echo "" + echo -e " ${WHITE}2.${NC} During installation, use ${BOLD}${GREEN}'db'${NC} as the MySQL host" + echo -e " ${RED}(NOT localhost or 127.0.0.1)${NC}" + echo "" + echo -e " ${WHITE}3.${NC} Make sure ports ${WHITE}${WP_PORT}${NC}, ${WHITE}7080${NC}, and ${WHITE}8081${NC} are open in your firewall" + echo "" + echo "" + + # Quick Copy Section + echo -e "${PURPLE}╔═══════════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${PURPLE}║ 📋 QUICK COPY - DATABASE INFO ║${NC}" + echo -e "${PURPLE}╚═══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " Host: ${GREEN}db${NC}" + echo -e " Database: ${GREEN}db${NC}" + echo -e " Username: ${GREEN}${MYSQL_USER}${NC}" + echo -e " Password: ${GREEN}${MYSQL_PASSWORD}${NC}" + echo "" + echo "" + + echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ 🧙 Thank you for using Wizard Panel! Happy hosting! 🧙 ║${NC}" + echo -e "${GREEN}║ GitHub: https://github.com/poryajp/wizardpanel ║${NC}" + echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# MAIN FUNCTION +# ═══════════════════════════════════════════════════════════════════════════════ +main() { + print_banner + + check_root + detect_os + install_dependencies + install_docker + detect_compose + create_directories + download_project + get_user_inputs + create_docker_compose + start_containers + set_permissions + display_info +} + +# ═══════════════════════════════════════════════════════════════════════════════ +# RUN MAIN +# ═══════════════════════════════════════════════════════════════════════════════ +main "$@" diff --git a/mini-app.jpg b/mini-app.jpg new file mode 100644 index 0000000..c8b4047 Binary files /dev/null and b/mini-app.jpg differ diff --git a/src/api/marzban_api.php b/src/api/marzban_api.php index 101c579..20e85f4 100644 --- a/src/api/marzban_api.php +++ b/src/api/marzban_api.php @@ -1,6 +1,7 @@ prepare("SELECT * FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_info = $stmt->fetch(); @@ -52,7 +53,8 @@ function marzbanApiRequest($endpoint, $server_id, $method = 'GET', $data = [], $ return json_decode($response, true); } -function getMarzbanToken($server_id) { +function getMarzbanToken($server_id) +{ $stmt = pdo()->prepare("SELECT * FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_info = $stmt->fetch(); @@ -120,7 +122,8 @@ function getMarzbanToken($server_id) { return false; } -function createMarzbanUser($plan, $chat_id, $plan_id) { +function createMarzbanUser($plan, $chat_id, $plan_id) +{ $server_id = $plan['server_id']; $accessToken = getMarzbanToken($server_id); if (!$accessToken) { @@ -130,9 +133,9 @@ function createMarzbanUser($plan, $chat_id, $plan_id) { $stmt_server_protocols = pdo()->prepare("SELECT marzban_protocols FROM servers WHERE id = ?"); $stmt_server_protocols->execute([$server_id]); $protocols_json = $stmt_server_protocols->fetchColumn(); - - $proxies = new stdClass(); - + + $proxies = new stdClass(); + if ($protocols_json) { $protocol_list = json_decode($protocols_json, true); if (is_array($protocol_list) && !empty($protocol_list)) { @@ -141,16 +144,16 @@ function createMarzbanUser($plan, $chat_id, $plan_id) { } } } - - if (empty((array)$proxies)) { - $proxies->vless = new stdClass(); + + if (empty((array) $proxies)) { + $proxies->vless = new stdClass(); } - + $username = $plan['full_username']; $userData = [ 'username' => $username, - 'proxies' => $proxies, + 'proxies' => $proxies, 'inbounds' => new stdClass(), 'expire' => time() + $plan['duration_days'] * 86400, 'data_limit' => $plan['volume_gb'] * 1024 * 1024 * 1024, @@ -164,17 +167,17 @@ function createMarzbanUser($plan, $chat_id, $plan_id) { ->prepare("UPDATE services SET warning_sent = 0 WHERE marzban_username = ? AND server_id = ?") ->execute([$response['username'], $server_id]); - + $stmt_server = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); $stmt_server->execute([$server_id]); $server_info = $stmt_server->fetch(); - + $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); $sub_path = parse_url($response['subscription_url'], PHP_URL_PATH); $final_sub_url = $base_sub_url . $sub_path; - - - $response['subscription_url'] = $final_sub_url; + + + $response['subscription_url'] = $final_sub_url; return $response; } @@ -182,7 +185,8 @@ function createMarzbanUser($plan, $chat_id, $plan_id) { return false; } -function getMarzbanUser($username, $server_id) { +function getMarzbanUser($username, $server_id) +{ $accessToken = getMarzbanToken($server_id); if (!$accessToken) { return false; @@ -191,7 +195,8 @@ function getMarzbanUser($username, $server_id) { return marzbanApiRequest("/api/user/{$username}", $server_id, 'GET', [], $accessToken); } -function modifyMarzbanUser($username, $server_id, $data) { +function modifyMarzbanUser($username, $server_id, $data) +{ $accessToken = getMarzbanToken($server_id); if (!$accessToken) { return false; @@ -200,11 +205,22 @@ function modifyMarzbanUser($username, $server_id, $data) { return marzbanApiRequest("/api/user/{$username}", $server_id, 'PUT', $data, $accessToken); } -function deleteMarzbanUser($username, $server_id) { +function deleteMarzbanUser($username, $server_id) +{ $accessToken = getMarzbanToken($server_id); if (!$accessToken) { return false; } return marzbanApiRequest("/api/user/{$username}", $server_id, 'DELETE', [], $accessToken); +} + +function resetMarzbanUserUsage($username, $server_id) +{ + $accessToken = getMarzbanToken($server_id); + if (!$accessToken) { + return false; + } + + return marzbanApiRequest("/api/user/{$username}/reset", $server_id, 'POST', [], $accessToken); } \ No newline at end of file diff --git a/src/api/marzneshin_api.php b/src/api/marzneshin_api.php index a281ffb..d565e87 100644 --- a/src/api/marzneshin_api.php +++ b/src/api/marzneshin_api.php @@ -1,26 +1,32 @@ prepare("SELECT url FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_url = $stmt->fetchColumn(); - if (!$server_url) return ['error' => 'Server not configured.']; + if (!$server_url) + return ['error' => 'Server not configured.']; $accessTokenResult = getMarzneshinToken($server_id); if (is_array($accessTokenResult) && isset($accessTokenResult['error'])) { return ['error' => 'Token Error: ' . $accessTokenResult['error']]; } $accessToken = $accessTokenResult; - + $url = rtrim($server_url, '/') . $endpoint; $headers = ['Content-Type: application/json', 'Accept: application/json', 'Authorization: Bearer ' . $accessToken]; $ch = curl_init(); curl_setopt_array($ch, [ - CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, - CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, ]); switch (strtoupper($method)) { @@ -41,51 +47,63 @@ function marzneshinApiRequest($endpoint, $server_id, $method = 'GET', $data = [] return json_decode($response_body, true); } -function marzneshinPublicApiRequest($endpoint, $server_id) { +function marzneshinPublicApiRequest($endpoint, $server_id) +{ $stmt = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_info = $stmt->fetch(); - if (!$server_info) return false; - + if (!$server_info) + return false; + $base_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); $url = $base_url . $endpoint; $ch = curl_init(); curl_setopt_array($ch, [ - CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, - CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, ]); $response = curl_exec($ch); curl_close($ch); return $response; } -function getMarzneshinToken($server_id) { +function getMarzneshinToken($server_id) +{ $cache_key = 'marzneshin_token_' . $server_id; $stmt_cache = pdo()->prepare("SELECT cache_value FROM cache WHERE cache_key = ? AND expire_at > ?"); $stmt_cache->execute([$cache_key, time()]); - if ($cached_token = $stmt_cache->fetchColumn()) return $cached_token; + if ($cached_token = $stmt_cache->fetchColumn()) + return $cached_token; $stmt = pdo()->prepare("SELECT url, username, password FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_info = $stmt->fetch(); - if (!$server_info) return ['error' => "Server info not found for server ID: {$server_id}"]; - + if (!$server_info) + return ['error' => "Server info not found for server ID: {$server_id}"]; + $url = rtrim($server_info['url'], '/') . '/api/admins/token'; $postData = http_build_query(['username' => $server_info['username'], 'password' => $server_info['password']]); - + $ch = curl_init(); curl_setopt_array($ch, [ - CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $postData, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_TIMEOUT => 20, + CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded', 'Accept: application/json'], ]); - + $response_body = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); - + $response = json_decode($response_body, true); if (isset($response['access_token'])) { $new_token = $response['access_token']; @@ -94,37 +112,40 @@ function getMarzneshinToken($server_id) { $stmt_insert_cache->execute([$cache_key, $new_token, $expire_time]); return $new_token; } - + $error_detail = $response['detail'] ?? $response_body; return ['error' => "HTTP {$http_code} - " . (is_string($error_detail) ? $error_detail : json_encode($error_detail))]; } -function getMarzneshinServices($server_id) { +function getMarzneshinServices($server_id) +{ $response = marzneshinApiRequest('/api/services', $server_id, 'GET'); return $response['items'] ?? []; } -function createMarzneshinUser($plan, $chat_id, $plan_id) { +function createMarzneshinUser($plan, $chat_id, $plan_id) +{ $server_id = $plan['server_id']; $service_id = $plan['marzneshin_service_id']; $username = $plan['full_username']; - + $stmt = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_info = $stmt->fetch(); - if (!$server_info) return false; + if (!$server_info) + return false; $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); $userData = [ 'username' => $username, 'data_limit' => $plan['volume_gb'] * 1024 * 1024 * 1024, 'expire_date' => date('c', time() + $plan['duration_days'] * 86400), - 'service_ids' => [(int)$service_id], + 'service_ids' => [(int) $service_id], 'expire_strategy' => 'fixed_date' ]; $response = marzneshinApiRequest('/api/users', $server_id, 'POST', $userData); - + if (isset($response['username'])) { // استخراج صحیح یزورنیم و پسورد $new_username = $response['username']; @@ -135,7 +156,7 @@ function createMarzneshinUser($plan, $chat_id, $plan_id) { $links_path = $subscription_path . 'links'; $full_subscription_url = $base_sub_url . $subscription_path; - + $links = []; $links_response_raw = marzneshinPublicApiRequest($links_path, $server_id); if (is_string($links_response_raw) && !str_contains(strtolower($links_response_raw), 'error')) { @@ -148,32 +169,33 @@ function createMarzneshinUser($plan, $chat_id, $plan_id) { 'links' => array_filter($links), ]; } - + error_log("[Marzneshin Create User Failed] Payload: " . json_encode($userData) . " | Response: " . json_encode($response)); return false; } // --- تابع دریافت اطلاعات کاربر --- -function getMarzneshinUser($username, $server_id) { +function getMarzneshinUser($username, $server_id) +{ $user_response = marzneshinApiRequest("/api/users/{$username}", $server_id, 'GET'); if (isset($user_response['username'])) { $links = []; - + if (isset($user_response['key'])) { - $key = $user_response['key']; + $key = $user_response['key']; + + $links_endpoint = "/sub/{$username}/{$key}/links"; - $links_endpoint = "/sub/{$username}/{$key}/links"; - - $links_response_raw = marzneshinPublicApiRequest($links_endpoint, $server_id); - - if (is_string($links_response_raw) && !str_contains(strtolower($links_response_raw), 'error')) { + $links_response_raw = marzneshinPublicApiRequest($links_endpoint, $server_id); + + if (is_string($links_response_raw) && !str_contains(strtolower($links_response_raw), 'error')) { $links = explode("\n", trim($links_response_raw)); - } + } } - + return [ 'status' => $user_response['is_active'] ? 'active' : 'disabled', 'expire' => $user_response['expire_date'] ? strtotime($user_response['expire_date']) : 0, @@ -187,7 +209,8 @@ function getMarzneshinUser($username, $server_id) { return false; } -function modifyMarzneshinUser($username, $server_id, $data) { +function modifyMarzneshinUser($username, $server_id, $data) +{ $marzneshinData = []; if (isset($data['data_limit'])) { $marzneshinData['data_limit'] = $data['data_limit']; @@ -200,7 +223,15 @@ function modifyMarzneshinUser($username, $server_id, $data) { return $response && isset($response['username']); } -function deleteMarzneshinUser($username, $server_id) { +function deleteMarzneshinUser($username, $server_id) +{ $response = marzneshinApiRequest("/api/users/{$username}", $server_id, 'DELETE'); return is_null($response) || (isset($response['detail']) && str_contains($response['detail'], 'not found')); +} + +function resetMarzneshinUserUsage($username, $server_id) +{ + // مرزنشین همانند مرزبان از endpoint reset پشتیبانی می‌کند + $response = marzneshinApiRequest("/api/users/{$username}/reset", $server_id, 'POST'); + return $response && isset($response['username']); } \ No newline at end of file diff --git a/src/api/sanaei_api.php b/src/api/sanaei_api.php index fa6825c..caf1bef 100644 --- a/src/api/sanaei_api.php +++ b/src/api/sanaei_api.php @@ -2,7 +2,8 @@ // --- توابع پایه --- -function getSanaeiCookie($server_id) { +function getSanaeiCookie($server_id) +{ $cache_key = 'sanaei_cookie_' . $server_id; $stmt_cache = pdo()->prepare("SELECT cache_value FROM cache WHERE cache_key = ? AND expire_at > ?"); $stmt_cache->execute([$cache_key, time()]); @@ -13,16 +14,22 @@ function getSanaeiCookie($server_id) { $stmt = pdo()->prepare("SELECT url, username, password FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_info = $stmt->fetch(); - if (!$server_info) return false; + if (!$server_info) + return false; $url = rtrim($server_info['url'], '/') . '/login'; $postData = ['username' => $server_info['username'], 'password' => $server_info['password']]; $ch = curl_init(); curl_setopt_array($ch, [ - CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query($postData), CURLOPT_HEADER => true, - CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($postData), + CURLOPT_HEADER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, ]); $response = curl_exec($ch); @@ -39,23 +46,31 @@ function getSanaeiCookie($server_id) { return false; } -function sanaeiApiRequest($endpoint, $server_id, $method = 'GET', $data = []) { +function sanaeiApiRequest($endpoint, $server_id, $method = 'GET', $data = []) +{ $stmt = pdo()->prepare("SELECT url FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_url = $stmt->fetchColumn(); - if (!$server_url) return ['success' => false, 'msg' => 'Sanaei server is not configured.']; + if (!$server_url) + return ['success' => false, 'msg' => 'Sanaei server is not configured.']; $cookie = getSanaeiCookie($server_id); - if (!$cookie) return ['success' => false, 'msg' => 'Login failed']; + if (!$cookie) + return ['success' => false, 'msg' => 'Login failed']; $url = rtrim($server_url, '/') . $endpoint; $headers = ['Cookie: ' . $cookie, 'Accept: application/json']; - if ($method === 'POST') $headers[] = 'Content-Type: application/json'; + if ($method === 'POST') + $headers[] = 'Content-Type: application/json'; $ch = curl_init(); curl_setopt_array($ch, [ - CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, - CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_TIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, ]); if ($method === 'POST') { @@ -68,19 +83,22 @@ function sanaeiApiRequest($endpoint, $server_id, $method = 'GET', $data = []) { } -function getSanaeiInbounds($server_id) { +function getSanaeiInbounds($server_id) +{ $response = sanaeiApiRequest('/panel/api/inbounds/list', $server_id); return ($response['success'] && isset($response['obj'])) ? $response['obj'] : []; } -function _findSanaeiClientInAllInbounds($email_username, $server_id) { +function _findSanaeiClientInAllInbounds($email_username, $server_id) +{ $inbounds = getSanaeiInbounds($server_id); - if (empty($inbounds)) return false; + if (empty($inbounds)) + return false; foreach ($inbounds as $inbound_summary) { $inbound_id = $inbound_summary['id']; $response = sanaeiApiRequest("/panel/api/inbounds/get/{$inbound_id}", $server_id); - + if ($response && $response['success'] && isset($response['obj']['settings'])) { $settings = json_decode($response['obj']['settings'], true); if (isset($settings['clients'])) { @@ -95,14 +113,16 @@ function _findSanaeiClientInAllInbounds($email_username, $server_id) { return false; } -function createSanaeiUser($plan, $chat_id, $plan_id) { +function createSanaeiUser($plan, $chat_id, $plan_id) +{ $server_id = $plan['server_id']; $inbound_id = $plan['inbound_id']; - + $stmt_server = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); $stmt_server->execute([$server_id]); $server_info = $stmt_server->fetch(); - if(!$server_info) return false; + if (!$server_info) + return false; $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); $uuid = generateUUID(); @@ -110,37 +130,38 @@ function createSanaeiUser($plan, $chat_id, $plan_id) { $subId = generateUUID(16); $expire_time = ($plan['duration_days'] > 0) ? (time() + $plan['duration_days'] * 86400) * 1000 : 0; $total_bytes = ($plan['volume_gb'] > 0) ? $plan['volume_gb'] * 1024 * 1024 * 1024 : 0; - $client_settings = [ "id" => $uuid, "email" => $email, "totalGB" => $total_bytes, "expiryTime" => $expire_time, "enable" => true, "tgId" => (string)$chat_id, "subId" => $subId ]; - $data = ['id' => (int)$inbound_id, 'settings' => json_encode(['clients' => [$client_settings]])]; + $client_settings = ["id" => $uuid, "email" => $email, "totalGB" => $total_bytes, "expiryTime" => $expire_time, "enable" => true, "tgId" => (string) $chat_id, "subId" => $subId]; + $data = ['id' => (int) $inbound_id, 'settings' => json_encode(['clients' => [$client_settings]])]; $response = sanaeiApiRequest('/panel/api/inbounds/addClient', $server_id, 'POST', $data); if (isset($response['success']) && $response['success']) { $sub_link = $base_sub_url . '/sub/' . $subId; // اصلاحیه: پاس دادن server_id به تابع کمکی $links = fetchAndParseSubscriptionUrl($sub_link, $server_id); - + return ['username' => $email, 'subscription_url' => $sub_link, 'links' => $links]; } - + error_log("Failed to create Sanaei user. Response: " . json_encode($response)); return false; } -function getSanaeiUser($username, $server_id) { +function getSanaeiUser($username, $server_id) +{ $traffic_response = sanaeiApiRequest("/panel/api/inbounds/getClientTraffics/{$username}", $server_id); if (!$traffic_response || !$traffic_response['success'] || !isset($traffic_response['obj'])) { error_log("Could not fetch user traffic for {$username}."); return false; } $client_traffic_data = $traffic_response['obj']; - + $stmt_service = pdo()->prepare("SELECT sub_url FROM services WHERE marzban_username = ? AND server_id = ?"); $stmt_service->execute([$username, $server_id]); $sub_url = $stmt_service->fetchColumn(); // اصلاحیه: پاس دادن server_id به تابع کمکی $links = fetchAndParseSubscriptionUrl($sub_url, $server_id); - + return [ 'status' => ($client_traffic_data['enable'] && ($client_traffic_data['expiryTime'] == 0 || $client_traffic_data['expiryTime'] > time() * 1000)) ? 'active' : 'disabled', 'expire' => $client_traffic_data['expiryTime'] > 0 ? floor($client_traffic_data['expiryTime'] / 1000) : 0, @@ -150,33 +171,44 @@ function getSanaeiUser($username, $server_id) { ]; } -function modifySanaeiUser($username, $server_id, $data) { +function modifySanaeiUser($username, $server_id, $data) +{ $foundClientData = _findSanaeiClientInAllInbounds($username, $server_id); - if (!$foundClientData) return false; + if (!$foundClientData) + return false; $inbound_id = $foundClientData['inbound_id']; $uuid = $foundClientData['client']['id']; - + $traffic_response = sanaeiApiRequest("/panel/api/inbounds/getClientTraffics/{$username}", $server_id); - if (!$traffic_response || !$traffic_response['success'] || !isset($traffic_response['obj'])) return false; + if (!$traffic_response || !$traffic_response['success'] || !isset($traffic_response['obj'])) + return false; $currentClientData = $traffic_response['obj']; $update_payload = [ - 'id' => (int)$inbound_id, - 'settings' => json_encode(['clients' => [[ - 'id' => $uuid, 'email' => $username, 'enable' => true, - 'totalGB' => $data['data_limit'] ?? ($currentClientData['totalGB'] ?? 0), - 'expiryTime' => isset($data['expire']) ? $data['expire'] * 1000 : ($currentClientData['expiryTime'] ?? 0), - ]]]) + 'id' => (int) $inbound_id, + 'settings' => json_encode([ + 'clients' => [ + [ + 'id' => $uuid, + 'email' => $username, + 'enable' => true, + 'totalGB' => $data['data_limit'] ?? ($currentClientData['totalGB'] ?? 0), + 'expiryTime' => isset($data['expire']) ? $data['expire'] * 1000 : ($currentClientData['expiryTime'] ?? 0), + ] + ] + ]) ]; - + $response = sanaeiApiRequest("/panel/api/inbounds/updateClient/{$uuid}", $server_id, 'POST', $update_payload); return $response && $response['success']; } -function deleteSanaeiUser($username, $server_id) { +function deleteSanaeiUser($username, $server_id) +{ $foundClientData = _findSanaeiClientInAllInbounds($username, $server_id); - if (!$foundClientData) return true; + if (!$foundClientData) + return true; $inbound_id = $foundClientData['inbound_id']; $uuid = $foundClientData['client']['id']; @@ -185,12 +217,19 @@ function deleteSanaeiUser($username, $server_id) { return $response && $response['success']; } -function generateUUID($length = 36) { +function generateUUID($length = 36) +{ if ($length === 36) { - return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), - mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) ); } else { $characters = '0123456789abcdefghijklmnopqrstuvwxyz'; @@ -200,4 +239,13 @@ function generateUUID($length = 36) { } return $randomString; } +} + +function resetSanaeiUserUsage($username, $server_id) +{ + // در پنل Sanaei/3x-ui، یک endpoint مستقیم برای reset usage وجود ندارد + // ولی با تنظیم مجدد totalGB از طریق modifySanaeiUser، usage به طور خودکار ریست می‌شود + // بنابراین این تابع صرفاً برای سازگاری با interface وجود دارد + // عملیات ریست در تابع modifySanaeiUser انجام شده است + return true; } \ No newline at end of file diff --git a/src/bot.php b/src/bot.php index 8937b8c..4bff691 100644 --- a/src/bot.php +++ b/src/bot.php @@ -2,8 +2,7 @@ if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); -} -elseif (function_exists('litespeed_finish_request')) { +} elseif (function_exists('litespeed_finish_request')) { litespeed_finish_request(); } @@ -39,8 +38,7 @@ if (isset($update['callback_query'])) { $chat_id = $update['callback_query']['message']['chat']['id']; $first_name = $update['callback_query']['from']['first_name']; -} -elseif (isset($update['message']['chat']['id'])) { +} elseif (isset($update['message']['chat']['id'])) { $chat_id = $update['message']['chat']['id']; $first_name = $update['message']['from']['first_name']; } @@ -90,8 +88,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); deleteMessage($chat_id, $message_id); handleMainMenu($chat_id, $first_name, true); - } - else { + } else { apiRequest('answerCallbackQuery', [ 'callback_query_id' => $callback_id, 'text' => '❌ شما هنوز در کانال عضو نشده‌اید!', @@ -122,67 +119,66 @@ ]); die; } - + // --- مدیریت پرداخت مستقیم برای خرید پلن --- if (strpos($data, 'charge_for_plan_') === 0) { - $parts = explode('_', $data); - $amount_to_charge = (int)$parts[3]; - $plan_id_to_buy = (int)$parts[4]; - $discount_code_to_use = (isset($parts[5]) && !empty($parts[5])) ? $parts[5] : null; - $custom_name_encoded = $parts[6] ?? ''; - $custom_name = base64_decode($custom_name_encoded); - - $description = "تکمیل خرید پلن #{$plan_id_to_buy}"; - $metadata = [ - "purpose" => "complete_purchase", - "plan_id" => $plan_id_to_buy, - "user_id" => $chat_id, - "custom_name" => $custom_name // ذخیره نام دلخواه - ]; - if ($discount_code_to_use) { - $metadata["discount_code"] = $discount_code_to_use; - } + $parts = explode('_', $data); + $amount_to_charge = (int) $parts[3]; + $plan_id_to_buy = (int) $parts[4]; + $discount_code_to_use = (isset($parts[5]) && !empty($parts[5])) ? $parts[5] : null; + $custom_name_encoded = $parts[6] ?? ''; + $custom_name = base64_decode($custom_name_encoded); + + $description = "تکمیل خرید پلن #{$plan_id_to_buy}"; + $metadata = [ + "purpose" => "complete_purchase", + "plan_id" => $plan_id_to_buy, + "user_id" => $chat_id, + "custom_name" => $custom_name // ذخیره نام دلخواه + ]; + if ($discount_code_to_use) { + $metadata["discount_code"] = $discount_code_to_use; + } - $zarinpal_result = createZarinpalLink($chat_id, $amount_to_charge, $description, $metadata); - if ($zarinpal_result['success']) { - $message = "⏳ در حال انتقال به درگاه پرداخت... لطفا صبر کنید."; - $keyboard = ['inline_keyboard' => [[['text' => '🚀 ورود به صفحه پرداخت', 'url' => $zarinpal_result['url']]]]]; - editMessageText($chat_id, $message_id, $message, $keyboard); - } else { - editMessageText($chat_id, $message_id, $zarinpal_result['error']); - } - die; -} - elseif (strpos($data, 'manual_pay_for_plan_') === 0) { - $parts = explode('_', $data); - $amount_to_charge = (int)$parts[4]; - $plan_id_to_buy = (int)$parts[5]; - $discount_code_to_use = (isset($parts[6]) && !empty($parts[6])) ? $parts[6] : null; - $custom_name_encoded = $parts[7] ?? ''; - $custom_name = base64_decode($custom_name_encoded); - - $state_data = [ - 'charge_amount' => $amount_to_charge, - 'purpose' => 'complete_purchase', - 'plan_id' => $plan_id_to_buy, - 'custom_name' => $custom_name, // ذخیره نام دلخواه در state - ]; - if ($discount_code_to_use) { - $state_data['discount_code'] = $discount_code_to_use; - } + $zarinpal_result = createZarinpalLink($chat_id, $amount_to_charge, $description, $metadata); + if ($zarinpal_result['success']) { + $message = "⏳ در حال انتقال به درگاه پرداخت... لطفا صبر کنید."; + $keyboard = ['inline_keyboard' => [[['text' => '🚀 ورود به صفحه پرداخت', 'url' => $zarinpal_result['url']]]]]; + editMessageText($chat_id, $message_id, $message, $keyboard); + } else { + editMessageText($chat_id, $message_id, $zarinpal_result['error']); + } + die; + } elseif (strpos($data, 'manual_pay_for_plan_') === 0) { + $parts = explode('_', $data); + $amount_to_charge = (int) $parts[4]; + $plan_id_to_buy = (int) $parts[5]; + $discount_code_to_use = (isset($parts[6]) && !empty($parts[6])) ? $parts[6] : null; + $custom_name_encoded = $parts[7] ?? ''; + $custom_name = base64_decode($custom_name_encoded); + + $state_data = [ + 'charge_amount' => $amount_to_charge, + 'purpose' => 'complete_purchase', + 'plan_id' => $plan_id_to_buy, + 'custom_name' => $custom_name, // ذخیره نام دلخواه در state + ]; + if ($discount_code_to_use) { + $state_data['discount_code'] = $discount_code_to_use; + } - updateUserData($chat_id, 'awaiting_payment_screenshot', $state_data); + updateUserData($chat_id, 'awaiting_payment_screenshot', $state_data); - $settings = getSettings(); - $payment_method = $settings['payment_method']; - $card_number_display = ($payment_method['copy_enabled'] ?? false) ? "{$payment_method['card_number']}" : $payment_method['card_number']; - $message = "برای تکمیل خرید به مبلغ " . number_format($amount_to_charge) . " تومان، لطفا مبلغ را به اطلاعات زیر واریز نمایید:\n\n" . - "💳 شماره کارت:\n" . $card_number_display . "\n" . - "👤 صاحب حساب: {$payment_method['card_holder']}\n\n" . - "پس از واریز، لطفا از رسید پرداخت خود اسکرین‌شات گرفته و در همینجا ارسال کنید. پس از تایید، سرویس شما به صورت خودکار ایجاد خواهد شد."; - editMessageText($chat_id, $message_id, $message); - die; -} + $settings = getSettings(); + $payment_method = $settings['payment_method']; + $card_number_display = ($payment_method['copy_enabled'] ?? false) ? "{$payment_method['card_number']}" : $payment_method['card_number']; + $message = "برای تکمیل خرید به مبلغ " . number_format($amount_to_charge) . " تومان، لطفا مبلغ را به اطلاعات زیر واریز نمایید:\n\n" . + "💳 شماره کارت:\n" . $card_number_display . "\n" . + "👤 صاحب حساب: {$payment_method['card_holder']}\n\n" . + "پس از واریز، لطفا از رسید پرداخت خود اسکرین‌شات گرفته و در همینجا ارسال کنید. پس از تایید، سرویس شما به صورت خودکار ایجاد خواهد شد."; + editMessageText($chat_id, $message_id, $message); + die; + } // --- دکمه‌های مخصوص ادمین‌ها --- if ($isAnAdmin) { @@ -193,14 +189,13 @@ sendMessage($chat_id, "لطفا مبلغی که می‌خواهید به موجودی کاربر $target_id اضافه کنید را به تومان وارد کنید:", $cancelKeyboard); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; - } - elseif (strpos($data, 'show_user_services_') === 0 && hasPermission($chat_id, 'manage_users')) { + } elseif (strpos($data, 'show_user_services_') === 0 && hasPermission($chat_id, 'manage_users')) { $target_id = str_replace('show_user_services_', '', $data); $services = getUserServices($target_id); - + $target_user_info = getUserData($target_id); $target_user_name = htmlspecialchars($target_user_info['first_name'] ?? "کاربر $target_id"); - + if (empty($services)) { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => "کاربر {$target_user_name} هیچ سرویسی ندارد.", 'show_alert' => true]); } else { @@ -213,28 +208,25 @@ $message_text .= "▫️ نام کاربری پنل: {$service['marzban_username']}\n"; $message_text .= "▫️ تاریخ انقضا: {$expire_date}\n---\n"; } - + // پیام را در یک پیام جدید ارسال می‌کنیم تا منوی مدیریت اصلی حفظ شود sendMessage($chat_id, $message_text); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); } die; - } - elseif (strpos($data, 'deduct_balance_') === 0 && hasPermission($chat_id, 'manage_users')) { + } elseif (strpos($data, 'deduct_balance_') === 0 && hasPermission($chat_id, 'manage_users')) { $target_id = str_replace('deduct_balance_', '', $data); updateUserData($chat_id, 'admin_awaiting_amount_for_deduct_balance', ['target_user_id' => $target_id, 'admin_view' => 'admin']); sendMessage($chat_id, "لطفا مبلغی که می‌خواهید از موجودی کاربر $target_id کسر کنید را به تومان وارد کنید:", $cancelKeyboard); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; - } - elseif (strpos($data, 'message_user_') === 0 && hasPermission($chat_id, 'manage_users')) { + } elseif (strpos($data, 'message_user_') === 0 && hasPermission($chat_id, 'manage_users')) { $target_id = str_replace('message_user_', '', $data); updateUserData($chat_id, 'admin_awaiting_message_for_user', ['target_user_id' => $target_id, 'admin_view' => 'admin']); sendMessage($chat_id, "پیام خود را برای ارسال به کاربر $target_id وارد کنید:", $cancelKeyboard); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; - } - elseif (strpos($data, 'ban_user_') === 0 && hasPermission($chat_id, 'manage_users')) { + } elseif (strpos($data, 'ban_user_') === 0 && hasPermission($chat_id, 'manage_users')) { $target_id = str_replace('ban_user_', '', $data); if ($target_id == ADMIN_CHAT_ID) { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ شما نمی‌توانید خودتان را مسدود کنید!', 'show_alert' => true]); @@ -245,16 +237,14 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'کاربر مسدود شد']); } die; - } - elseif (strpos($data, 'unban_user_') === 0 && hasPermission($chat_id, 'manage_users')) { + } elseif (strpos($data, 'unban_user_') === 0 && hasPermission($chat_id, 'manage_users')) { $target_id = str_replace('unban_user_', '', $data); setUserStatus($target_id, 'active'); sendMessage($target_id, "✅ شما توسط ادمین از حالت مسدودیت خارج شدید."); editMessageText($chat_id, $message_id, $update['callback_query']['message']['text'] . "\n\n---\n✅ کاربر با موفقیت آزاد شد."); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'کاربر آزاد شد']); die; - } - elseif ($data === 'search_another_user' && hasPermission($chat_id, 'manage_users')) { + } elseif ($data === 'search_another_user' && hasPermission($chat_id, 'manage_users')) { deleteMessage($chat_id, $message_id); updateUserData($chat_id, 'admin_awaiting_user_search', ['admin_view' => 'admin']); sendMessage($chat_id, "لطفاً شناسه عددی (Chat ID) کاربر بعدی را وارد کنید:", $cancelKeyboard); @@ -262,7 +252,7 @@ die; } // end - + if (strpos($data, 'delete_cat_') === 0 && hasPermission($chat_id, 'manage_categories')) { $cat_id = str_replace('delete_cat_', '', $data); pdo() @@ -271,82 +261,75 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ حذف شد']); deleteMessage($chat_id, $message_id); generateCategoryList($chat_id); - } - elseif (strpos($data, 'charge_zarinpal_') === 0) { - $amount = (int)str_replace('charge_zarinpal_', '', $data); - $settings = getSettings(); - $merchant_id = $settings['zarinpal_merchant_id']; - - $script_url = 'https://' . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['PHP_SELF']), '/') . '/verify_payment.php'; - - $data = [ - "merchant_id" => $merchant_id, - "amount" => $amount * 10, // تبدیل تومان به ریال - "callback_url" => $script_url, - "description" => "شارژ حساب کاربری - " . $chat_id, - "metadata" => ["order_id" => "user_{$chat_id}_" . time()] - ]; - $jsonData = json_encode($data); - - $ch = curl_init('https://api.zarinpal.com/pg/v4/payment/request.json'); - curl_setopt($ch, CURLOPT_USERAGENT, 'ZarinPal Rest Api v4'); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); - curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Content-Length: ' . strlen($jsonData)]); - - $result = curl_exec($ch); - curl_close($ch); - $result = json_decode($result, true); - - if (empty($result['errors'])) { - $authority = $result['data']['authority']; - - // ثبت تراکنش در دیتابیس - $stmt = pdo()->prepare("INSERT INTO transactions (user_id, amount, authority, description) VALUES (?, ?, ?, ?)"); - $stmt->execute([$chat_id, $amount, $authority, "شارژ حساب"]); - - $payment_url = 'https://www.zarinpal.com/pg/StartPay/' . $authority; - - $message = "⏳ در حال انتقال به درگاه پرداخت... لطفا صبر کنید."; - $keyboard = ['inline_keyboard' => [[['text' => '🚀 ورود به صفحه پرداخت', 'url' => $payment_url]]]]; - editMessageText($chat_id, $message_id, $message, $keyboard); - - } else { - $error_code = $result['errors']['code']; - editMessageText($chat_id, $message_id, "❌ خطا در اتصال به درگاه پرداخت. کد خطا: {$error_code}"); - } - } - elseif ($data === 'toggle_gateway_status') { + } elseif (strpos($data, 'charge_zarinpal_') === 0) { + $amount = (int) str_replace('charge_zarinpal_', '', $data); + $settings = getSettings(); + $merchant_id = $settings['zarinpal_merchant_id']; + + $script_url = 'https://' . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['PHP_SELF']), '/') . '/verify_payment.php'; + + $data = [ + "merchant_id" => $merchant_id, + "amount" => $amount * 10, // تبدیل تومان به ریال + "callback_url" => $script_url, + "description" => "شارژ حساب کاربری - " . $chat_id, + "metadata" => ["order_id" => "user_{$chat_id}_" . time()] + ]; + $jsonData = json_encode($data); + + $ch = curl_init('https://api.zarinpal.com/pg/v4/payment/request.json'); + curl_setopt($ch, CURLOPT_USERAGENT, 'ZarinPal Rest Api v4'); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Content-Length: ' . strlen($jsonData)]); + + $result = curl_exec($ch); + curl_close($ch); + $result = json_decode($result, true); + + if (empty($result['errors'])) { + $authority = $result['data']['authority']; + + // ثبت تراکنش در دیتابیس + $stmt = pdo()->prepare("INSERT INTO transactions (user_id, amount, authority, description) VALUES (?, ?, ?, ?)"); + $stmt->execute([$chat_id, $amount, $authority, "شارژ حساب"]); + + $payment_url = 'https://www.zarinpal.com/pg/StartPay/' . $authority; + + $message = "⏳ در حال انتقال به درگاه پرداخت... لطفا صبر کنید."; + $keyboard = ['inline_keyboard' => [[['text' => '🚀 ورود به صفحه پرداخت', 'url' => $payment_url]]]]; + editMessageText($chat_id, $message_id, $message, $keyboard); + + } else { + $error_code = $result['errors']['code']; + editMessageText($chat_id, $message_id, "❌ خطا در اتصال به درگاه پرداخت. کد خطا: {$error_code}"); + } + } elseif ($data === 'toggle_gateway_status') { $settings = getSettings(); $settings['payment_gateway_status'] = ($settings['payment_gateway_status'] ?? 'off') == 'on' ? 'off' : 'on'; saveSettings($settings); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت تغییر کرد.']); - - } - elseif ($data === 'set_zarinpal_merchant_id') { + + } elseif ($data === 'set_zarinpal_merchant_id') { updateUserData($chat_id, 'admin_awaiting_merchant_id'); editMessageText($chat_id, $message_id, "لطفا مرچنت کد ۳۶ کاراکتری زرین‌پال خود را وارد کنید:"); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif ($data === 'toggle_renewal_status') { - $settings = getSettings(); - $settings['renewal_status'] = ($settings['renewal_status'] ?? 'off') == 'on' ? 'off' : 'on'; - saveSettings($settings); - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت تغییر کرد.']); - showRenewalManagementMenu($chat_id, $message_id); -} - elseif ($data === 'set_renewal_price_day') { + } elseif ($data === 'toggle_renewal_status') { + $settings = getSettings(); + $settings['renewal_status'] = ($settings['renewal_status'] ?? 'off') == 'on' ? 'off' : 'on'; + saveSettings($settings); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت تغییر کرد.']); + showRenewalManagementMenu($chat_id, $message_id); + } elseif ($data === 'set_renewal_price_day') { updateUserData($chat_id, 'admin_awaiting_renewal_price_day'); editMessageText($chat_id, $message_id, "لطفا هزینه تمدید به ازای هر **روز** را به تومان وارد کنید (فقط عدد):"); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif ($data === 'set_renewal_price_gb') { + } elseif ($data === 'set_renewal_price_gb') { updateUserData($chat_id, 'admin_awaiting_renewal_price_gb'); editMessageText($chat_id, $message_id, "لطفا هزینه تمدید به ازای هر **گیگابایت** را به تومان وارد کنید (فقط عدد):"); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'toggle_cat_') === 0 && hasPermission($chat_id, 'manage_categories')) { + } elseif (strpos($data, 'toggle_cat_') === 0 && hasPermission($chat_id, 'manage_categories')) { $cat_id = str_replace('toggle_cat_', '', $data); pdo() ->prepare("UPDATE categories SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?") @@ -354,16 +337,14 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت تغییر کرد']); deleteMessage($chat_id, $message_id); generateCategoryList($chat_id); - } - elseif (strpos($data, 'delete_plan_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'delete_plan_') === 0 && hasPermission($chat_id, 'manage_plans')) { $plan_id = str_replace('delete_plan_', '', $data); pdo() ->prepare("DELETE FROM plans WHERE id = ?") ->execute([$plan_id]); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ پلن حذف شد']); deleteMessage($chat_id, $message_id); - } - elseif (strpos($data, 'toggle_plan_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'toggle_plan_') === 0 && hasPermission($chat_id, 'manage_plans')) { $plan_id = str_replace('toggle_plan_', '', $data); pdo() ->prepare("UPDATE plans SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?") @@ -371,23 +352,20 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت تغییر کرد']); deleteMessage($chat_id, $message_id); generatePlanList($chat_id); - } - elseif ($data === 'back_to_plan_list' && hasPermission($chat_id, 'manage_plans')) { + } elseif ($data === 'back_to_plan_list' && hasPermission($chat_id, 'manage_plans')) { updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); deleteMessage($chat_id, $message_id); generatePlanList($chat_id); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'open_plan_editor_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'open_plan_editor_') === 0 && hasPermission($chat_id, 'manage_plans')) { $plan_id = str_replace('open_plan_editor_', '', $data); showPlanEditor($chat_id, $message_id, $plan_id); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'edit_plan_field_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'edit_plan_field_') === 0 && hasPermission($chat_id, 'manage_plans')) { preg_match('/edit_plan_field_(\d+)_(\w+)/', $data, $matches); $plan_id = $matches[1]; $field = $matches[2]; - + $field_map = [ 'name' => ['prompt' => '👇 لطفا نام جدید پلن را وارد کنید:', 'column' => 'name', 'validation' => 'text'], 'price' => ['prompt' => '👇 لطفا قیمت جدید را به تومان وارد کنید (فقط عدد):', 'column' => 'price', 'validation' => 'numeric'], @@ -401,18 +379,16 @@ $state_data = [ 'editing_plan_id' => $plan_id, 'editing_field_info' => $field_info, - 'editor_message_id' => $message_id + 'editor_message_id' => $message_id ]; updateUserData($chat_id, 'admin_awaiting_plan_edit_input', $state_data); showPlanEditor($chat_id, $message_id, $plan_id, $field_info['prompt']); } apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'back_to_plan_view_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'back_to_plan_view_') === 0 && hasPermission($chat_id, 'manage_plans')) { deleteMessage($chat_id, $message_id); generatePlanList($chat_id); - } - elseif (strpos($data, 'edit_plan_field_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'edit_plan_field_') === 0 && hasPermission($chat_id, 'manage_plans')) { preg_match('/edit_plan_field_(\d+)_(\w+)/', $data, $matches); $plan_id = $matches[1]; $field = $matches[2]; @@ -470,11 +446,10 @@ if ($field !== 'category' && $field !== 'server') { deleteMessage($chat_id, $message_id); } - + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; - } - elseif (strpos($data, 'set_plan_category_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'set_plan_category_') === 0 && hasPermission($chat_id, 'manage_plans')) { preg_match('/set_plan_category_(\d+)_(\d+)/', $data, $matches); $plan_id = $matches[1]; $category_id = $matches[2]; @@ -484,8 +459,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ دسته‌بندی پلن با موفقیت تغییر کرد.']); deleteMessage($chat_id, $message_id); generatePlanList($chat_id); - } - elseif (strpos($data, 'set_plan_server_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'set_plan_server_') === 0 && hasPermission($chat_id, 'manage_plans')) { preg_match('/set_plan_server_(\d+)_(\d+)/', $data, $matches); $plan_id = $matches[1]; $server_id = $matches[2]; @@ -495,8 +469,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ سرور پلن با موفقیت تغییر کرد.']); deleteMessage($chat_id, $message_id); generatePlanList($chat_id); - } - elseif (strpos($data, 'p_cat_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'p_cat_') === 0 && hasPermission($chat_id, 'manage_plans')) { $category_id = str_replace('p_cat_', '', $data); $servers = pdo() ->query("SELECT id, name FROM servers WHERE status = 'active'") @@ -511,12 +484,11 @@ $keyboard_buttons[] = [['text' => $server['name'], 'callback_data' => "p_server_{$server['id']}_cat_{$category_id}"]]; } editMessageText($chat_id, $message_id, "این پلن روی کدام سرور ساخته شود؟", ['inline_keyboard' => $keyboard_buttons]); - } - elseif (strpos($data, 'p_server_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'p_server_') === 0 && hasPermission($chat_id, 'manage_plans')) { preg_match('/p_server_(\d+)_cat_(\d+)/', $data, $matches); $server_id = $matches[1]; $category_id = $matches[2]; - + $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_type = $stmt->fetchColumn(); @@ -535,7 +507,7 @@ editMessageText($chat_id, $message_id, "این پلن به کدام اینباند اضافه شود؟", ['inline_keyboard' => $keyboard_buttons]); } elseif ($server_type === 'marzneshin') { $services = getMarzneshinServices($server_id); - if (empty($services)) { + if (empty($services)) { editMessageText($chat_id, $message_id, "❌ هیچ سرویسی روی این سرور مرزنشین یافت نشد. لطفا ابتدا یک سرویس در پنل خود بسازید."); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; @@ -554,8 +526,7 @@ sendMessage($chat_id, "1/6 - لطفا نام پلن را وارد کنید:", $cancelKeyboard); deleteMessage($chat_id, $message_id); } - } - elseif (strpos($data, 'p_inbound_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'p_inbound_') === 0 && hasPermission($chat_id, 'manage_plans')) { preg_match('/p_inbound_(\d+)_server_(\d+)_cat_(\d+)/', $data, $matches); $inbound_id = $matches[1]; $server_id = $matches[2]; @@ -569,8 +540,7 @@ updateUserData($chat_id, 'awaiting_plan_name', $state_data); sendMessage($chat_id, "1/6 - لطفا نام پلن را وارد کنید:", $cancelKeyboard); deleteMessage($chat_id, $message_id); - } - elseif (strpos($data, 'p_service_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'p_service_') === 0 && hasPermission($chat_id, 'manage_plans')) { preg_match('/p_service_(\d+)_server_(\d+)_cat_(\d+)/', $data, $matches); $service_id = $matches[1]; $server_id = $matches[2]; @@ -584,8 +554,7 @@ updateUserData($chat_id, 'awaiting_plan_name', $state_data); sendMessage($chat_id, "1/6 - لطفا نام پلن را وارد کنید:", $cancelKeyboard); deleteMessage($chat_id, $message_id); - } - elseif (strpos($data, 'copy_toggle_') === 0 && hasPermission($chat_id, 'manage_payment')) { + } elseif (strpos($data, 'copy_toggle_') === 0 && hasPermission($chat_id, 'manage_payment')) { $toggle = str_replace('copy_toggle_', '', $data) === 'yes'; $settings = getSettings(); $settings['payment_method'] = ['card_number' => $user_data['state_data']['temp_card_number'], 'card_holder' => $user_data['state_data']['temp_card_holder'], 'copy_enabled' => $toggle]; @@ -594,8 +563,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ تنظیمات ذخیره شد']); editMessageText($chat_id, $message_id, "✅ تنظیمات روش پرداخت با موفقیت ذخیره شد."); handleMainMenu($chat_id, $first_name); - } - elseif (strpos($data, 'approve_') === 0 || strpos($data, 'reject_') === 0) { + } elseif (strpos($data, 'approve_') === 0 || strpos($data, 'reject_') === 0) { list($action, $request_id) = explode('_', $data); $stmt = pdo()->prepare("SELECT * FROM payment_requests WHERE id = ?"); @@ -632,9 +600,9 @@ // این پرداخت برای تکمیل خرید یک پلن $plan_id = $metadata['plan_id']; $discount_code = $metadata['discount_code'] ?? null; - + $plan = getPlanById($plan_id); - $final_price = (float)$plan['price']; + $final_price = (float) $plan['price']; $discount_applied = false; $discount_object = null; @@ -643,7 +611,7 @@ $stmt_discount->execute([$discount_code]); $discount_object = $stmt_discount->fetch(); if ($discount_object) { - if ($discount_object['type'] == 'percent') { + if ($discount_object['type'] == 'percent') { $final_price = $plan['price'] - ($plan['price'] * $discount_object['value']) / 100; } else { $final_price = $plan['price'] - $discount_object['value']; @@ -652,19 +620,19 @@ $discount_applied = true; } } - + // شارژ موقت حساب کاربر با مبلغ پرداختی updateUserBalance($user_id_to_charge, $amount_to_charge, 'add'); - $custom_name = $metadata['custom_name'] ?? 'سرویس'; -$purchase_result = completePurchase($user_id_to_charge, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied); + $custom_name = $metadata['custom_name'] ?? 'سرویس'; + $purchase_result = completePurchase($user_id_to_charge, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied); if ($purchase_result['success']) { sendPhoto($user_id_to_charge, $purchase_result['qr_code_url'], $purchase_result['caption']); sendMessage(ADMIN_CHAT_ID, $purchase_result['admin_notification']); sendMessage($user_id_to_charge, "✅ پرداخت شما تایید و سرویس با موفقیت ایجاد شد."); } else { - sendMessage($user_id_to_charge, "❌ پرداخت شما تایید شد اما در ایجاد سرویس خطایی رخ داد. مبلغ پرداخت شده به موجودی شما اضافه شد. لطفاً با پشتیبانی تماس بگیرید."); + sendMessage($user_id_to_charge, "❌ پرداخت شما تایید شد اما در ایجاد سرویس خطایی رخ داد. مبلغ پرداخت شده به موجودی شما اضافه شد. لطفاً با پشتیبانی تماس بگیرید."); } updateUserData($user_id_to_charge, 'main_menu'); @@ -678,8 +646,7 @@ editMessageCaption($chat_id, $message_id, $update['callback_query']['message']['caption'] . "\n\n✅ توسط شما تایید شد.", null); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ درخواست تایید شد']); - } - elseif ($action == 'reject') { + } elseif ($action == 'reject') { pdo()->prepare("UPDATE payment_requests SET status = 'rejected', processed_by_admin_id = ?, processed_at = NOW() WHERE id = ?")->execute([$admin_who_processed, $request_id]); sendMessage($user_id_to_charge, "❌ درخواست شارژ حساب شما به مبلغ " . number_format($amount_to_charge) . " تومان توسط ادمین رد شد."); @@ -687,8 +654,7 @@ editMessageCaption($chat_id, $message_id, $update['callback_query']['message']['caption'] . "\n\n❌ توسط شما رد شد.", null); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ درخواست رد شد']); } - } - elseif ($data === 'manage_servers' && hasPermission($chat_id, 'manage_marzban')) { + } elseif ($data === 'manage_servers' && hasPermission($chat_id, 'manage_marzban')) { $servers = pdo() ->query("SELECT id, name FROM servers") ->fetchAll(PDO::FETCH_ASSOC); @@ -699,117 +665,112 @@ $keyboard_buttons[] = [['text' => '◀️ بازگشت به پنل', 'callback_data' => 'back_to_admin_panel']]; editMessageText($chat_id, $message_id, "🌐 مدیریت سرورها\n\nسرور مورد نظر را برای مشاهده یا حذف انتخاب کنید، یا یک سرور جدید اضافه کنید:", ['inline_keyboard' => $keyboard_buttons]); - } - elseif ($data === 'add_server_select_type' && hasPermission($chat_id, 'manage_marzban')) { - $keyboard = ['inline_keyboard' => [ - [['text' => '🔵 مرزبان (Marzban)', 'callback_data' => 'add_server_type_marzban']], - [['text' => '🟠 سنایی (3x-ui)', 'callback_data' => 'add_server_type_sanaei']], - [['text' => '🟢 مرزنشین (Marzneshin)', 'callback_data' => 'add_server_type_marzneshin']], - [['text' => '◀️ بازگشت', 'callback_data' => 'manage_servers']], - ]]; + } elseif ($data === 'add_server_select_type' && hasPermission($chat_id, 'manage_marzban')) { + $keyboard = [ + 'inline_keyboard' => [ + [['text' => '🔵 مرزبان (Marzban)', 'callback_data' => 'add_server_type_marzban']], + [['text' => '🟠 سنایی (3x-ui)', 'callback_data' => 'add_server_type_sanaei']], + [['text' => '🟢 مرزنشین (Marzneshin)', 'callback_data' => 'add_server_type_marzneshin']], + [['text' => '◀️ بازگشت', 'callback_data' => 'manage_servers']], + ] + ]; editMessageText($chat_id, $message_id, "لطفا نوع پنل سرور را انتخاب کنید:", $keyboard); - } - elseif (strpos($data, 'edit_protocols_') === 0 && hasPermission($chat_id, 'manage_marzban')) { + } elseif (strpos($data, 'edit_protocols_') === 0 && hasPermission($chat_id, 'manage_marzban')) { $server_id = str_replace('edit_protocols_', '', $data); showMarzbanProtocolEditor($chat_id, $message_id, $server_id); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'toggle_protocol_') === 0 && hasPermission($chat_id, 'manage_marzban')) { + } elseif (strpos($data, 'toggle_protocol_') === 0 && hasPermission($chat_id, 'manage_marzban')) { preg_match('/toggle_protocol_(\d+)_(\w+)/', $data, $matches); $server_id = $matches[1]; $protocol = $matches[2]; - + $stmt_get = pdo()->prepare("SELECT marzban_protocols FROM servers WHERE id = ?"); $stmt_get->execute([$server_id]); $protocols_json = $stmt_get->fetchColumn(); - + $current_protocols = $protocols_json ? json_decode($protocols_json, true) : []; - if (!is_array($current_protocols)) $current_protocols = []; + if (!is_array($current_protocols)) + $current_protocols = []; if (in_array($protocol, $current_protocols)) { $current_protocols = array_diff($current_protocols, [$protocol]); } else { $current_protocols[] = $protocol; } - + $new_protocols_json = json_encode(array_values($current_protocols)); $stmt_update = pdo()->prepare("UPDATE servers SET marzban_protocols = ? WHERE id = ?"); $stmt_update->execute([$new_protocols_json, $server_id]); - + showMarzbanProtocolEditor($chat_id, $message_id, $server_id); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'add_server_type_') === 0 && hasPermission($chat_id, 'manage_marzban')) { + } elseif (strpos($data, 'add_server_type_') === 0 && hasPermission($chat_id, 'manage_marzban')) { deleteMessage($chat_id, $message_id); $type = str_replace('add_server_type_', '', $data); updateUserData($chat_id, 'admin_awaiting_server_name', ['selected_server_type' => $type]); sendMessage($chat_id, "مرحله ۱/۴: یک نام دلخواه برای شناسایی سرور وارد کنید (مثال: آلمان-هتزنر):", $cancelKeyboard); - } - elseif (strpos($data, 'view_server_') === 0 && hasPermission($chat_id, 'manage_marzban')) { + } elseif (strpos($data, 'view_server_') === 0 && hasPermission($chat_id, 'manage_marzban')) { $server_id = str_replace('view_server_', '', $data); $stmt = pdo()->prepare("SELECT * FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server = $stmt->fetch(); if ($server) { $panel_type_text = ucfirst($server['type']); - if ($server['type'] === 'sanaei') $panel_type_text = 'سنایی (3x-ui)'; - if ($server['type'] === 'marzneshin') $panel_type_text = 'مرزنشین'; - + if ($server['type'] === 'sanaei') + $panel_type_text = 'سنایی (3x-ui)'; + if ($server['type'] === 'marzneshin') + $panel_type_text = 'مرزنشین'; + $msg = "مشخصات سرور: {$server['name']}\n\n"; $msg .= "▫️ نوع پنل: {$panel_type_text}\n"; $msg .= "▫️ آدرس مدیریت پنل: {$server['url']}\n"; - + $keyboard_buttons = []; - - + + if ($server['type'] === 'sanaei' || $server['type'] === 'marzban') { $sub_host_text = !empty($server['sub_host']) ? "{$server['sub_host']}" : "پیش‌فرض (مانند آدرس پنل)"; $msg .= "▫️ آدرس لینک اشتراک: {$sub_host_text}\n"; $keyboard_buttons[] = [['text' => '🔗 ویرایش آدرس ساب', 'callback_data' => "edit_sub_host_{$server_id}"]]; } - + if ($server['type'] === 'marzban') { $keyboard_buttons[] = [['text' => '⚙️ تنظیم پروتکل‌ها', 'callback_data' => "edit_protocols_{$server_id}"]]; } - + $msg .= "▫️ نام کاربری: {$server['username']}\n"; $keyboard_buttons[] = [['text' => '🗑 حذف این سرور', 'callback_data' => "delete_server_{$server_id}"]]; $keyboard_buttons[] = [['text' => '◀️ بازگشت به لیست سرورها', 'callback_data' => 'manage_servers']]; - + $keyboard = ['inline_keyboard' => $keyboard_buttons]; editMessageText($chat_id, $message_id, $msg, $keyboard); } - } - elseif (strpos($data, 'edit_sub_host_') === 0 && hasPermission($chat_id, 'manage_marzban')) { + } elseif (strpos($data, 'edit_sub_host_') === 0 && hasPermission($chat_id, 'manage_marzban')) { $server_id = str_replace('edit_sub_host_', '', $data); updateUserData($chat_id, 'admin_awaiting_sub_host', ['editing_server_id' => $server_id]); $prompt = "لطفا آدرس کامل و عمومی که برای لینک اشتراک استفاده می‌شود را وارد کنید.\nاین آدرس باید شامل http/https و پورت صحیح باشد (مثال: http://your.domain.com:2096).\n\n💡 برای بازگشت به حالت پیش‌فرض (استفاده از همان آدرس پنل)، کلمه `reset` را ارسال کنید."; editMessageText($chat_id, $message_id, $prompt); - } - elseif (strpos($data, 'delete_server_') === 0 && hasPermission($chat_id, 'manage_marzban')) { + } elseif (strpos($data, 'delete_server_') === 0 && hasPermission($chat_id, 'manage_marzban')) { $server_id = str_replace('delete_server_', '', $data); $stmt_check = pdo()->prepare("SELECT COUNT(*) FROM plans WHERE server_id = ?"); $stmt_check->execute([$server_id]); if ($stmt_check->fetchColumn() > 0) { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ نمی‌توانید این سرور را حذف کنید زیرا یک یا چند پلن به آن متصل هستند.', 'show_alert' => true]); - } - else { + } else { $stmt = pdo()->prepare("DELETE FROM servers WHERE id = ?"); $stmt->execute([$server_id]); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ سرور با موفقیت حذف شد.']); - $data = 'manage_servers'; + $data = 'manage_servers'; } - } - elseif (strpos($data, 'plan_set_sub_') === 0) { + } elseif (strpos($data, 'plan_set_sub_') === 0) { $show_sub = str_replace('plan_set_sub_', '', $data) === 'yes'; $state_data = $user_data['state_data']; $state_data['temp_plan_data']['show_sub_link'] = $show_sub; updateUserData($chat_id, 'awaiting_plan_conf_link_setting', $state_data); $keyboard = ['inline_keyboard' => [[['text' => '✅ بله', 'callback_data' => 'plan_set_conf_yes'], ['text' => '❌ خیر', 'callback_data' => 'plan_set_conf_no']]]]; editMessageText($chat_id, $message_id, "سوال ۲/۲: آیا لینک‌های تکی کانفیگ‌ها به کاربر نمایش داده شود؟\n(پیشنهادی: بله)", $keyboard); - } - elseif (strpos($data, 'plan_set_conf_') === 0) { + } elseif (strpos($data, 'plan_set_conf_') === 0) { $show_conf = str_replace('plan_set_conf_', '', $data) === 'yes'; $final_plan_data = $user_data['state_data']['temp_plan_data'] ?? null; if ($final_plan_data) { @@ -834,14 +795,12 @@ editMessageText($chat_id, $message_id, "✅ پلن جدید با تمام تنظیمات با موفقیت ذخیره شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); handleMainMenu($chat_id, $first_name); - } - else { + } else { editMessageText($chat_id, $message_id, "❌ خطا در ذخیره‌سازی پلن. لطفا مجددا تلاش کنید."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); handleMainMenu($chat_id, $first_name); } - } - elseif (strpos($data, 'discount_type_') === 0) { + } elseif (strpos($data, 'discount_type_') === 0) { $type = str_replace('discount_type_', '', $data); $state_data = $user_data['state_data']; $state_data['new_discount_type'] = $type; @@ -849,16 +808,14 @@ $unit = $type == 'percent' ? 'درصد' : 'تومان'; editMessageText($chat_id, $message_id, "3/4 - لطفاً مقدار تخفیف را به $unit وارد کنید (فقط عدد):"); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'delete_discount_') === 0) { + } elseif (strpos($data, 'delete_discount_') === 0) { $code_id = str_replace('delete_discount_', '', $data); pdo() ->prepare("DELETE FROM discount_codes WHERE id = ?") ->execute([$code_id]); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ کد تخفیف حذف شد.']); deleteMessage($chat_id, $message_id); - } - elseif (strpos($data, 'toggle_discount_') === 0) { + } elseif (strpos($data, 'toggle_discount_') === 0) { $code_id = str_replace('toggle_discount_', '', $data); pdo() ->prepare("UPDATE discount_codes SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?") @@ -866,8 +823,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت کد تخفیف تغییر کرد.']); deleteMessage($chat_id, $message_id); generateDiscountCodeList($chat_id); - } - elseif (strpos($data, 'delete_guide_') === 0 && hasPermission($chat_id, 'manage_guides')) { + } elseif (strpos($data, 'delete_guide_') === 0 && hasPermission($chat_id, 'manage_guides')) { $guide_id = str_replace('delete_guide_', '', $data); pdo() ->prepare("DELETE FROM guides WHERE id = ?") @@ -875,8 +831,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ راهنما حذف شد.']); deleteMessage($chat_id, $message_id); generateGuideList($chat_id); - } - elseif (strpos($data, 'toggle_guide_') === 0 && hasPermission($chat_id, 'manage_guides')) { + } elseif (strpos($data, 'toggle_guide_') === 0 && hasPermission($chat_id, 'manage_guides')) { $guide_id = str_replace('toggle_guide_', '', $data); pdo() ->prepare("UPDATE guides SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?") @@ -884,8 +839,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت راهنما تغییر کرد.']); deleteMessage($chat_id, $message_id); generateGuideList($chat_id); - } - elseif (strpos($data, 'reset_plan_count_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'reset_plan_count_') === 0 && hasPermission($chat_id, 'manage_plans')) { $plan_id = str_replace('reset_plan_count_', '', $data); pdo() ->prepare("UPDATE plans SET purchase_count = 0 WHERE id = ?") @@ -904,8 +858,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ این پلن به عنوان پلن تست تنظیم شد.']); deleteMessage($chat_id, $message_id); generatePlanList($chat_id); - } - elseif (strpos($data, 'make_plan_normal_') === 0 && hasPermission($chat_id, 'manage_plans')) { + } elseif (strpos($data, 'make_plan_normal_') === 0 && hasPermission($chat_id, 'manage_plans')) { $plan_id = str_replace('make_plan_normal_', '', $data); pdo() ->prepare("UPDATE plans SET is_test_plan = 0 WHERE id = ?") @@ -917,8 +870,7 @@ if (strpos($data, 'admin_notifications_soon') === 0) { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'این بخش به زودی فعال خواهد شد.', 'show_alert' => true]); - } - elseif (($data == 'user_notifications_menu' || $data == 'config_expire_warning' || $data == 'config_inactive_reminder') && hasPermission($chat_id, 'manage_notifications')) { + } elseif (($data == 'user_notifications_menu' || $data == 'config_expire_warning' || $data == 'config_inactive_reminder') && hasPermission($chat_id, 'manage_notifications')) { $settings = getSettings(); $expire_status_icon = ($settings['notification_expire_status'] ?? 'off') == 'on' ? '✅' : '❌'; $inactive_status_icon = ($settings['notification_inactive_status'] ?? 'off') == 'on' ? '✅' : '❌'; @@ -941,8 +893,7 @@ ], ]; editMessageText($chat_id, $message_id, $message, $keyboard); - } - elseif ($data == 'config_expire_warning') { + } elseif ($data == 'config_expire_warning') { $message = "⚙️ تنظیمات هشدار انقضا\n\nاین پیام زمانی برای کاربر ارسال می‌شود که حجم یا زمان سرویس او رو به اتمام باشد.\n\n" . "▫️وضعیت: " . @@ -959,8 +910,7 @@ ], ]; editMessageText($chat_id, $message_id, $message, $keyboard); - } - elseif ($data == 'config_inactive_reminder') { + } elseif ($data == 'config_inactive_reminder') { $message = "⚙️ تنظیمات یادآور عدم فعالیت\n\nاین پیام زمانی برای کاربر ارسال می‌شود که برای مدت طولانی از ربات استفاده نکرده باشد.\n\n" . "▫️وضعیت: " . @@ -977,22 +927,19 @@ ]; editMessageText($chat_id, $message_id, $message, $keyboard); } - } - elseif (strpos($data, 'toggle_expire_notification') === 0 && hasPermission($chat_id, 'manage_notifications')) { + } elseif (strpos($data, 'toggle_expire_notification') === 0 && hasPermission($chat_id, 'manage_notifications')) { $settings = getSettings(); $settings['notification_expire_status'] = ($settings['notification_expire_status'] ?? 'off') == 'on' ? 'off' : 'on'; saveSettings($settings); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت تغییر کرد.']); $data = 'config_expire_warning'; - } - elseif (strpos($data, 'toggle_inactive_notification') === 0 && hasPermission($chat_id, 'manage_notifications')) { + } elseif (strpos($data, 'toggle_inactive_notification') === 0 && hasPermission($chat_id, 'manage_notifications')) { $settings = getSettings(); $settings['notification_inactive_status'] = ($settings['notification_inactive_status'] ?? 'off') == 'on' ? 'off' : 'on'; saveSettings($settings); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ وضعیت تغییر کرد.']); $data = 'config_inactive_reminder'; - } - elseif (in_array($data, ['set_expire_days', 'set_expire_gb', 'edit_expire_message', 'set_inactive_days', 'edit_inactive_message']) && hasPermission($chat_id, 'manage_notifications')) { + } elseif (in_array($data, ['set_expire_days', 'set_expire_gb', 'edit_expire_message', 'set_inactive_days', 'edit_inactive_message']) && hasPermission($chat_id, 'manage_notifications')) { deleteMessage($chat_id, $message_id); switch ($data) { case 'set_expire_days': @@ -1031,8 +978,7 @@ } $keyboard_buttons[] = [['text' => '◀️ بازگشت به پنل', 'callback_data' => 'back_to_admin_panel']]; editMessageText($chat_id, $message_id, "🌐 مدیریت سرورها\n\nسرور مورد نظر را برای مشاهده یا حذف انتخاب کنید، یا یک سرور جدید اضافه کنید:", ['inline_keyboard' => $keyboard_buttons]); - } - else { + } else { $menu_to_refresh = strpos($data, 'inactive') !== false || strpos($user_state, 'inactive') !== false ? 'config_inactive_reminder' : 'config_expire_warning'; $message_id = sendMessage($chat_id, "درحال بارگذاری مجدد منو...")['result']['message_id']; $data = $menu_to_refresh; @@ -1062,19 +1008,16 @@ $admins = getAdmins(); if (count($admins) >= 9) { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ حداکثر تعداد ادمین‌ها (۱۰) ثبت شده است.', 'show_alert' => true]); - } - else { + } else { updateUserData($chat_id, 'admin_awaiting_new_admin_id'); editMessageText($chat_id, $message_id, "لطفا شناسه عددی (Chat ID) کاربر مورد نظر را برای افزودن به لیست ادمین‌ها وارد کنید:"); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); } - } - elseif (strpos($data, 'edit_admin_permissions_') === 0) { + } elseif (strpos($data, 'edit_admin_permissions_') === 0) { $target_admin_id = str_replace('edit_admin_permissions_', '', $data); showPermissionEditor($chat_id, $message_id, $target_admin_id); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'toggle_perm_') === 0) { + } elseif (strpos($data, 'toggle_perm_') === 0) { $payload = substr($data, strlen('toggle_perm_')); $parts = explode('_', $payload, 2); if (count($parts) === 2) { @@ -1085,8 +1028,7 @@ $current_permissions = $admins[$target_admin_id]['permissions'] ?? []; if (($key = array_search($permission_key, $current_permissions)) !== false) { unset($current_permissions[$key]); - } - else { + } else { $current_permissions[] = $permission_key; } updateAdminPermissions($target_admin_id, array_values($current_permissions)); @@ -1094,14 +1036,12 @@ } } apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'delete_admin_confirm_') === 0) { + } elseif (strpos($data, 'delete_admin_confirm_') === 0) { $target_admin_id = str_replace('delete_admin_confirm_', '', $data); $keyboard = ['inline_keyboard' => [[['text' => '✅ بله، حذف کن', 'callback_data' => "delete_admin_do_{$target_admin_id}"]], [['text' => '❌ انصراف', 'callback_data' => "edit_admin_permissions_{$target_admin_id}"]]]]; editMessageText($chat_id, $message_id, "⚠️ آیا از حذف این ادمین مطمئن هستید؟", $keyboard); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'delete_admin_do_') === 0) { + } elseif (strpos($data, 'delete_admin_do_') === 0) { $target_admin_id = str_replace('delete_admin_do_', '', $data); $result = removeAdmin($target_admin_id); if ($result) { @@ -1118,12 +1058,10 @@ } $keyboard_buttons[] = [['text' => '◀️ بازگشت به پنل مدیریت', 'callback_data' => 'back_to_admin_panel']]; editMessageText($chat_id, $message_id, $message, ['inline_keyboard' => $keyboard_buttons]); - } - else { + } else { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ خطا در حذف ادمین.', 'show_alert' => true]); } - } - elseif ($data == 'back_to_admin_list') { + } elseif ($data == 'back_to_admin_list') { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); $admins = getAdmins(); $message = "👨‍💼 مدیریت ادمین‌ها\n\nدر این بخش می‌توانید ادمین‌های ربات و دسترسی‌های آن‌ها را مدیریت کنید. (حداکثر ۱۰ ادمین)"; @@ -1137,8 +1075,7 @@ } $keyboard_buttons[] = [['text' => '◀️ بازگشت به پنل مدیریت', 'callback_data' => 'back_to_admin_panel']]; editMessageText($chat_id, $message_id, $message, ['inline_keyboard' => $keyboard_buttons]); - } - elseif ($data == 'back_to_admin_panel') { + } elseif ($data == 'back_to_admin_panel') { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); deleteMessage($chat_id, $message_id); handleMainMenu($chat_id, $first_name); @@ -1158,51 +1095,96 @@ $ticket_status = $stmt->fetchColumn(); if (!$ticket_status || $ticket_status == 'closed') { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'این تیکت بسته شده است.', 'show_alert' => true]); - } - else { + } else { if ($isAnAdmin) { updateUserData($chat_id, 'admin_replying_to_ticket', ['replying_to_ticket' => $ticket_id]); sendMessage($chat_id, "لطفا پاسخ خود را برای تیکت $ticket_id وارد کنید:", $cancelKeyboard); - } - else { + } else { updateUserData($chat_id, 'user_replying_to_ticket', ['replying_to_ticket' => $ticket_id]); sendMessage($chat_id, "لطفا پاسخ خود را برای تیکت $ticket_id وارد کنید:", $cancelKeyboard); } } - } - elseif (strpos($data, 'approve_renewal_') === 0 || strpos($data, 'reject_renewal_') === 0) { - list($action, $type, $request_id) = explode('_', $data); + } elseif (strpos($data, 'approve_charge_') === 0 || strpos($data, 'reject_charge_') === 0) { + // Handle payment request approval/rejection + if ($isAnAdmin && !hasPermission($chat_id, 'manage_payment')) { + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'شما دسترسی لازم برای مدیریت پرداخت‌ها را ندارید.', 'show_alert' => true]); + die; + } - $stmt = pdo()->prepare("SELECT * FROM renewal_requests WHERE id = ?"); - $stmt->execute([$request_id]); - $request = $stmt->fetch(); + list($action, $charge_word, $request_id) = explode('_', $data); - if (!$request || $request['status'] !== 'pending') { - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'این درخواست قبلا پردازش شده است.', 'show_alert' => true]); - die; - } - - $admin_who_processed = $update['callback_query']['from']['id']; + $stmt = pdo()->prepare("SELECT * FROM payment_requests WHERE id = ?"); + $stmt->execute([$request_id]); + $request = $stmt->fetch(); - if ($action == 'approve') { - $result = applyRenewal($request['user_id'], $request['service_username'], $request['days_to_add'], $request['gb_to_add']); - if ($result['success']) { - pdo()->prepare("UPDATE renewal_requests SET status = 'approved', processed_by_admin_id = ?, processed_at = NOW() WHERE id = ?")->execute([$admin_who_processed, $request_id]); - sendMessage($request['user_id'], "✅ درخواست تمدید شما برای سرویس `{$request['service_username']}` تایید و با موفقیت اعمال شد."); - editMessageCaption($chat_id, $message_id, $update['callback_query']['message']['caption'] . "\n\n✅ توسط شما تایید شد.", null); - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ تمدید تایید شد.']); - } else { - sendMessage($chat_id, "❌ خطا در اعمال تمدید: " . $result['message']); - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'خطا در اعمال تمدید.', 'show_alert' => true]); - } - } elseif ($action == 'reject') { - pdo()->prepare("UPDATE renewal_requests SET status = 'rejected', processed_by_admin_id = ?, processed_at = NOW() WHERE id = ?")->execute([$admin_who_processed, $request_id]); - sendMessage($request['user_id'], "❌ درخواست تمدید شما برای سرویس `{$request['service_username']}` توسط ادمین رد شد."); - editMessageCaption($chat_id, $message_id, $update['callback_query']['message']['caption'] . "\n\n❌ توسط شما رد شد.", null); - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ درخواست رد شد.']); + if (!$request || $request['status'] !== 'pending') { + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'این درخواست قبلا پردازش شده است.', 'show_alert' => true]); + die; + } + + $admin_who_processed = $update['callback_query']['from']['id']; + + if ($action == 'approve') { + // Update user balance + $stmt_balance = pdo()->prepare("UPDATE users SET balance = balance + ? WHERE chat_id = ?"); + $stmt_balance->execute([$request['amount'], $request['user_id']]); + + // Update request status + pdo()->prepare("UPDATE payment_requests SET status = 'approved', processed_by_admin_id = ?, processed_at = NOW() WHERE id = ?")->execute([$admin_who_processed, $request_id]); + + // Notify user + sendMessage($request['user_id'], "✅ درخواست شارژ کیف پول شما به مبلغ " . number_format($request['amount']) . " تومان تایید شد و به حساب شما اضافه گردید."); + + // Update admin message + $new_caption = $update['callback_query']['message']['caption'] . "\n\n✅ توسط شما تایید و واریز شد."; + editMessageCaption($chat_id, $message_id, $new_caption, null); + + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ شارژ تایید و واریز شد.']); + } elseif ($action == 'reject') { + // Update request status + pdo()->prepare("UPDATE payment_requests SET status = 'rejected', processed_by_admin_id = ?, processed_at = NOW() WHERE id = ?")->execute([$admin_who_processed, $request_id]); + + // Notify user + sendMessage($request['user_id'], "❌ درخواست شارژ کیف پول شما به مبلغ " . number_format($request['amount']) . " تومان توسط ادمین رد شد. لطفاً با پشتیبانی تماس بگیرید."); + + // Update admin message + $new_caption = $update['callback_query']['message']['caption'] . "\n\n❌ توسط شما رد شد."; + editMessageCaption($chat_id, $message_id, $new_caption, null); + + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ درخواست رد شد.']); + } + } elseif (strpos($data, 'approve_renewal_') === 0 || strpos($data, 'reject_renewal_') === 0) { + list($action, $type, $request_id) = explode('_', $data); + + $stmt = pdo()->prepare("SELECT * FROM renewal_requests WHERE id = ?"); + $stmt->execute([$request_id]); + $request = $stmt->fetch(); + + if (!$request || $request['status'] !== 'pending') { + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'این درخواست قبلا پردازش شده است.', 'show_alert' => true]); + die; + } + + $admin_who_processed = $update['callback_query']['from']['id']; + + if ($action == 'approve') { + $result = applyRenewal($request['user_id'], $request['service_username'], $request['days_to_add'], $request['gb_to_add']); + if ($result['success']) { + pdo()->prepare("UPDATE renewal_requests SET status = 'approved', processed_by_admin_id = ?, processed_at = NOW() WHERE id = ?")->execute([$admin_who_processed, $request_id]); + sendMessage($request['user_id'], "✅ درخواست تمدید شما برای سرویس `{$request['service_username']}` تایید و با موفقیت اعمال شد."); + editMessageCaption($chat_id, $message_id, $update['callback_query']['message']['caption'] . "\n\n✅ توسط شما تایید شد.", null); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '✅ تمدید تایید شد.']); + } else { + sendMessage($chat_id, "❌ خطا در اعمال تمدید: " . $result['message']); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'خطا در اعمال تمدید.', 'show_alert' => true]); } + } elseif ($action == 'reject') { + pdo()->prepare("UPDATE renewal_requests SET status = 'rejected', processed_by_admin_id = ?, processed_at = NOW() WHERE id = ?")->execute([$admin_who_processed, $request_id]); + sendMessage($request['user_id'], "❌ درخواست تمدید شما برای سرویس `{$request['service_username']}` توسط ادمین رد شد."); + editMessageCaption($chat_id, $message_id, $update['callback_query']['message']['caption'] . "\n\n❌ توسط شما رد شد.", null); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ درخواست رد شد.']); } - elseif (strpos($data, 'close_ticket_') === 0) { + } elseif (strpos($data, 'close_ticket_') === 0) { if ($isAnAdmin && !hasPermission($chat_id, 'view_tickets')) { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'شما دسترسی لازم برای بستن تیکت‌ها را ندارید.', 'show_alert' => true]); die; @@ -1225,8 +1207,7 @@ } editMessageText($chat_id, $message_id, $update['callback_query']['message']['text'] . "\n\n-- ➖ این تیکت بسته شد ➖ --", null); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'تیکت با موفقیت بسته شد.']); - } - else { + } else { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => 'خطا: تیکت یافت نشد.', 'show_alert' => true]); } } @@ -1234,7 +1215,7 @@ // --- دکمه‌های عمومی کاربران --- elseif (strpos($data, 'get_configs_') === 0) { $username = str_replace('get_configs_', '', $data); - + $stmt_service = pdo()->prepare("SELECT server_id FROM services WHERE owner_chat_id = ? AND marzban_username = ?"); $stmt_service->execute([$chat_id, $username]); $server_id = $stmt_service->fetchColumn(); @@ -1245,7 +1226,7 @@ } $panel_user = getPanelUser($username, $server_id); - + if ($panel_user && !empty($panel_user['links'])) { // --- ارسال مستقیم همه کانفیگ‌ها --- $all_links_text = implode("\n\n", $panel_user['links']); @@ -1256,8 +1237,7 @@ apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ هیچ لینک کانفیگی برای این سرویس یافت نشد.', 'show_alert' => true]); } die; - } - elseif (strpos($data, 'show_guide_') === 0) { + } elseif (strpos($data, 'show_guide_') === 0) { $guide_id = str_replace('show_guide_', '', $data); $stmt = pdo()->prepare("SELECT * FROM guides WHERE id = ? AND status = 'active'"); $stmt->execute([$guide_id]); @@ -1270,17 +1250,14 @@ } if ($guide['content_type'] === 'photo' && !empty($guide['photo_id'])) { sendPhoto($chat_id, $guide['photo_id'], $guide['message_text'], $keyboard); - } - else { + } else { sendMessage($chat_id, $guide['message_text'], $keyboard); } - } - else { + } else { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ این راهنما یافت نشد یا غیرفعال شده است.', 'show_alert' => true]); } - } - elseif (strpos($data, 'charge_manual_') === 0) { - $amount = (int)str_replace('charge_manual_', '', $data); + } elseif (strpos($data, 'charge_manual_') === 0) { + $amount = (int) str_replace('charge_manual_', '', $data); $settings = getSettings(); $payment_method = $settings['payment_method'] ?? []; $card_number = $payment_method['card_number'] ?? ''; @@ -1288,32 +1265,46 @@ $copy_enabled = $payment_method['copy_enabled'] ?? false; if (empty($card_number)) { - editMessageText($chat_id, $message_id, "❌ روش پرداخت دستی توسط ادمین تنظیم نشده است."); - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - die; + editMessageText($chat_id, $message_id, "❌ روش پرداخت دستی توسط ادمین تنظیم نشده است."); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); + die; } $card_number_display = $copy_enabled ? "{$card_number}" : $card_number; $message = "برای شارژ حساب به مبلغ " . number_format($amount) . " تومان، لطفا مبلغ را به اطلاعات زیر واریز نمایید:\n\n" . - "💳 شماره کارت:\n" . $card_number_display . "\n" . - "👤 صاحب حساب: {$card_holder}\n\n" . - "پس از واریز، لطفا از رسید پرداخت خود اسکرین‌شات گرفته و در همینجا ارسال کنید."; + "💳 شماره کارت:\n" . $card_number_display . "\n" . + "👤 صاحب حساب: {$card_holder}\n\n" . + "پس از واریز، لطفا از رسید پرداخت خود اسکرین‌شات گرفته و در همینجا ارسال کنید."; editMessageText($chat_id, $message_id, $message); updateUserData($chat_id, 'awaiting_payment_screenshot', ['charge_amount' => $amount]); - } - elseif (strpos($data, 'cat_') === 0) { + } elseif (strpos($data, 'cat_') === 0) { $categoryId = str_replace('cat_', '', $data); - showServersForCategory($chat_id, $categoryId); + + // Check if only one server exists for this category + $stmt = pdo()->prepare(" + SELECT DISTINCT s.id + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt->execute([$categoryId]); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (count($servers) === 1) { + // Auto-skip to plan selection + showPlansForCategoryAndServer($chat_id, $categoryId, $servers[0]['id']); + } else { + // Show server selection + showServersForCategory($chat_id, $categoryId); + } deleteMessage($chat_id, $message_id); - } - elseif (strpos($data, 'show_plans_cat_') === 0) { + } elseif (strpos($data, 'show_plans_cat_') === 0) { preg_match('/show_plans_cat_(\d+)_srv_(\d+)/', $data, $matches); $category_id = $matches[1]; $server_id = $matches[2]; showPlansForCategoryAndServer($chat_id, $category_id, $server_id); deleteMessage($chat_id, $message_id); - } - elseif (strpos($data, 'apply_discount_code_') === 0) { + } elseif (strpos($data, 'apply_discount_code_') === 0) { $parts = explode('_', $data); $category_id = $parts[3]; $server_id = $parts[4]; // server_id اضافه شد @@ -1323,220 +1314,265 @@ ]); editMessageText($chat_id, $message_id, "🎁 لطفاً کد تخفیف خود را وارد کنید:"); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'buy_plan_') === 0) { - $parts = explode('_', $data); - $plan_id = $parts[2]; - $discount_code = null; - if (isset($parts[5]) && $parts[3] == 'with' && $parts[4] == 'code') { - $discount_code = strtoupper($parts[5]); - } - - $plan = getPlanById($plan_id); - if (!$plan) { - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ خطا: پلن یافت نشد.']); - die; - } + } elseif (strpos($data, 'buy_plan_') === 0) { + $parts = explode('_', $data); + $plan_id = $parts[2]; + $discount_code = null; + if (isset($parts[5]) && $parts[3] == 'with' && $parts[4] == 'code') { + $discount_code = strtoupper($parts[5]); + } - if ($plan['purchase_limit'] > 0 && $plan['purchase_count'] >= $plan['purchase_limit']) { - apiRequest('answerCallbackQuery', [ - 'callback_query_id' => $callback_id, - 'text' => '❌ متاسفانه ظرفیت خرید این پلن به اتمام رسیده است.', - 'show_alert' => true, - ]); + $plan = getPlanById($plan_id); + if (!$plan) { + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ خطا: پلن یافت نشد.']); + die; + } + + if ($plan['purchase_limit'] > 0 && $plan['purchase_count'] >= $plan['purchase_limit']) { + apiRequest('answerCallbackQuery', [ + 'callback_query_id' => $callback_id, + 'text' => '❌ متاسفانه ظرفیت خرید این پلن به اتمام رسیده است.', + 'show_alert' => true, + ]); + die; + } + + // به جای خرید مستقیم، وضعیت را برای دریافت نام تنظیم می‌کنیم + $state_data = [ + 'purchasing_plan_id' => $plan_id, + 'discount_code' => $discount_code + ]; + updateUserData($chat_id, 'awaiting_service_name', $state_data); + + $message = "✅ پلن انتخاب شد.\n\nلطفاً یک نام دلخواه برای این سرویس وارد کنید (مثلاً: سرویس شخصی). این نام در لیست سرویس‌های شما نمایش داده خواهد شد."; + + + deleteMessage($chat_id, $message_id); + sendMessage($chat_id, $message, $cancelKeyboard); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; - } + // Note: confirm_renewal_payment handler حذف شد - این بخش به سیستم جدید تمدید بر اساس پلن منتقل شده است + } elseif ($data == 'back_to_categories') { + deleteMessage($chat_id, $message_id); + $categories = getCategories(true); + $keyboard_buttons = []; + foreach ($categories as $category) { + $keyboard_buttons[] = [['text' => '🛍 ' . $category['name'], 'callback_data' => 'cat_' . $category['id']]]; + } + sendMessage($chat_id, "لطفا یکی از دسته‌بندی‌های زیر را انتخاب کنید:", ['inline_keyboard' => $keyboard_buttons]); + } elseif (strpos($data, 'service_details_') === 0) { + $username = str_replace('service_details_', '', $data); + if (isset($update['callback_query']['message']['photo'])) { + editMessageCaption($chat_id, $message_id, "⏳ در حال دریافت اطلاعات به‌روز سرویس، لطفا صبر کنید..."); + } else { + editMessageText($chat_id, $message_id, "⏳ در حال دریافت اطلاعات به‌روز سرویس، لطفا صبر کنید..."); + } - // به جای خرید مستقیم، وضعیت را برای دریافت نام تنظیم می‌کنیم - $state_data = [ - 'purchasing_plan_id' => $plan_id, - 'discount_code' => $discount_code - ]; - updateUserData($chat_id, 'awaiting_service_name', $state_data); - - $message = "✅ پلن انتخاب شد.\n\nلطفاً یک نام دلخواه برای این سرویس وارد کنید (مثلاً: سرویس شخصی). این نام در لیست سرویس‌های شما نمایش داده خواهد شد."; - - - deleteMessage($chat_id, $message_id); - sendMessage($chat_id, $message, $cancelKeyboard); - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - die; -} - elseif ($data === 'confirm_renewal_payment') { - $state_data = $user_data['state_data']; - $total_cost = $state_data['renewal_total_cost']; - - if ($user_data['balance'] >= $total_cost) { - // پرداخت از موجودی - editMessageText($chat_id, $message_id, "⏳ در حال تمدید سرویس با استفاده از موجودی شما..."); - updateUserBalance($chat_id, $total_cost, 'deduct'); - - $result = applyRenewal($chat_id, $state_data['renewal_username'], $state_data['renewal_days'], $state_data['renewal_gb']); - - if ($result['success']) { - $new_balance = number_format($user_data['balance'] - $total_cost); - $success_msg = "✅ سرویس شما با موفقیت تمدید شد.\n\n" . - "💰 مبلغ " . number_format($total_cost) . " تومان از حساب شما کسر گردید.\n" . - "موجودی جدید: {$new_balance} تومان."; - editMessageText($chat_id, $message_id, $success_msg); + $stmt_local = pdo()->prepare("SELECT s.*, p.name as plan_name, p.show_sub_link, p.show_conf_links FROM services s JOIN plans p ON s.plan_id = p.id WHERE s.owner_chat_id = ? AND s.marzban_username = ?"); + $stmt_local->execute([$chat_id, $username]); + $local_service = $stmt_local->fetch(); + + if ($local_service) { + $stmt_server = pdo()->prepare("SELECT * FROM servers WHERE id = ?"); + $stmt_server->execute([$local_service['server_id']]); + $server_info = $stmt_server->fetch(); + + $dynamic_sub_url = $local_service['sub_url']; + if ($server_info) { + $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); + $sub_path = strstr($local_service['sub_url'], '/sub/'); + if ($sub_path === false) { + $sub_path = parse_url($local_service['sub_url'], PHP_URL_PATH); + } + $dynamic_sub_url = $base_sub_url . $sub_path; + } + + $panel_user = getPanelUser($username, $local_service['server_id']); + + if ($panel_user && !isset($panel_user['detail'])) { + $qr_code_url = generateQrCodeUrl($dynamic_sub_url); + + $total_gb_from_db = $local_service['volume_gb']; + $used_bytes_from_panel = $panel_user['used_traffic']; + + $total_text = ($total_gb_from_db > 0) ? "{$total_gb_from_db} گیگابایت" : 'نامحدود'; + $used_text = formatBytes($used_bytes_from_panel); + + $remaining_text = 'نامحدود'; + if ($total_gb_from_db > 0) { + $total_bytes_from_db = $total_gb_from_db * 1024 * 1024 * 1024; + $remaining_bytes = $total_bytes_from_db - $used_bytes_from_panel; + $remaining_text = formatBytes(max(0, $remaining_bytes)); + } + + $expire_date = $panel_user['expire'] ? date('Y-m-d', $panel_user['expire']) : 'نامحدود'; + $status_text = ($panel_user['status'] === 'active' && ($panel_user['expire'] == 0 || $panel_user['expire'] > time())) ? 'فعال' : 'غیرفعال'; + + $caption = + "مشخصات سرویس: {$local_service['plan_name']}\n" . + "➖➖➖➖➖➖➖➖➖➖\n" . + "▫️ وضعیت: {$status_text}\n" . + "🗓 تاریخ انقضا: {$expire_date}\n\n" . + "📊 حجم کل: " . $total_text . "\n" . + "📈 حجم مصرفی: " . $used_text . "\n" . + "📉 حجم باقی‌مانده: " . $remaining_text . "\n" . + "➖➖➖➖➖➖➖➖➖➖\n"; + + if ($local_service['show_sub_link']) { + $caption .= "\n🔗 لینک اشتراک (Subscription):\n" . htmlspecialchars($dynamic_sub_url) . "\n"; + } else { + $caption .= "\n🔗 لینک اشتراک برای این پلن نمایش داده نمی‌شود.\n"; + } + + + $keyboard_buttons = [ + [['text' => '♻️ تمدید سرویس', 'callback_data' => "renew_service_{$username}"]], + ]; + + if ($local_service['show_conf_links'] && !empty($panel_user['links'])) { + $keyboard_buttons[0][] = ['text' => '📋 دریافت کانفیگ‌ها', 'callback_data' => "get_configs_{$username}"]; + } + + $keyboard_buttons[] = [['text' => '🗑 حذف سرویس', 'callback_data' => "delete_service_confirm_{$username}"]]; + $keyboard_buttons[] = [['text' => '◀️ بازگشت به لیست', 'callback_data' => 'back_to_services']]; + + + $keyboard = ['inline_keyboard' => $keyboard_buttons]; + + deleteMessage($chat_id, $message_id); + sendPhoto($chat_id, $qr_code_url, trim($caption), $keyboard); } else { - editMessageText($chat_id, $message_id, "❌ خطایی در تمدید سرویس رخ داد: " . $result['message']); - - updateUserBalance($chat_id, $total_cost, 'add'); + editMessageText($chat_id, $message_id, "❌ خطایی در دریافت اطلاعات سرویس از سرور رخ داد یا سرویس یافت نشد. ممکن است توسط ادمین حذف شده باشد."); } - updateUserData($chat_id, 'main_menu'); + } else { + editMessageText($chat_id, $message_id, "❌ سرویس در دیتابیس ربات یافت نشد."); + } + } elseif (strpos($data, 'renew_service_') === 0) { + $settings = getSettings(); + if (($settings['renewal_status'] ?? 'off') !== 'on') { + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ قابلیت تمدید سرویس در حال حاضر غیرفعال است.', 'show_alert' => true]); + die; + } + + $username = str_replace('renew_service_', '', $data); - } else { - - $stmt = pdo()->prepare( - "INSERT INTO renewal_requests (user_id, service_username, days_to_add, gb_to_add, total_cost) VALUES (?, ?, ?, ?, ?)" - ); - $stmt->execute([$chat_id, $state_data['renewal_username'], $state_data['renewal_days'], $state_data['renewal_gb'], $total_cost]); - $request_id = pdo()->lastInsertId(); - - $state_data['renewal_request_id'] = $request_id; - updateUserData($chat_id, 'awaiting_renewal_screenshot', $state_data); + // ذخیره username سرویس که قرار است تمدید شود + updateUserData($chat_id, 'renewal_selecting_category', [ + 'renewal_username' => $username, + 'is_renewal' => true + ]); - - $settings = getSettings(); - $payment_method = $settings['payment_method'] ?? []; - if (empty($payment_method['card_number'])) { - editMessageText($chat_id, $message_id, "موجودی شما کافی نیست و روش پرداخت کارت به کارت نیز توسط ادمین تنظیم نشده است. لطفا ابتدا حساب خود را شارژ کنید."); - } else { - $card_number = $payment_method['card_number'] ?? ''; - $card_holder = $payment_method['card_holder'] ?? ''; - $copy_enabled = $payment_method['copy_enabled'] ?? false; - $card_number_display = $copy_enabled ? "{$card_number}" : $card_number; - $message = "موجودی شما کافی نیست. لطفا مبلغ " . number_format($total_cost) . " تومان را به اطلاعات زیر واریز کرده و سپس اسکرین‌شات رسید را ارسال کنید:\n\n" . - "💳 شماره کارت:\n" . $card_number_display . "\n" . - "👤 صاحب حساب: {$card_holder}"; - editMessageText($chat_id, $message_id, $message); - } - } - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif ($data == 'back_to_categories') { + // نمایش دسته‌بندی‌ها deleteMessage($chat_id, $message_id); $categories = getCategories(true); $keyboard_buttons = []; foreach ($categories as $category) { - $keyboard_buttons[] = [['text' => '🛍 ' . $category['name'], 'callback_data' => 'cat_' . $category['id']]]; + $keyboard_buttons[] = [['text' => '🛍 ' . $category['name'], 'callback_data' => 'renewal_cat_' . $category['id']]]; } - sendMessage($chat_id, "لطفا یکی از دسته‌بندی‌های زیر را انتخاب کنید:", ['inline_keyboard' => $keyboard_buttons]); - } - elseif (strpos($data, 'service_details_') === 0) { - $username = str_replace('service_details_', '', $data); - if (isset($update['callback_query']['message']['photo'])) { - editMessageCaption($chat_id, $message_id, "⏳ در حال دریافت اطلاعات به‌روز سرویس، لطفا صبر کنید..."); - } else { - editMessageText($chat_id, $message_id, "⏳ در حال دریافت اطلاعات به‌روز سرویس، لطفا صبر کنید..."); - } + $keyboard_buttons[] = [['text' => '◀️ بازگشت', 'callback_data' => "service_details_{$username}"]]; + sendMessage($chat_id, "🔄 تمدید سرویس\n\nبرای تمدید سرویس، لطفاً دسته‌بندی مورد نظر را انتخاب کنید:", ['inline_keyboard' => $keyboard_buttons]); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); + } elseif (strpos($data, 'renewal_cat_') === 0) { + // handler انتخاب دسته‌بندی برای تمدید + $categoryId = str_replace('renewal_cat_', '', $data); + $state_data = $user_data['state_data']; + $state_data['renewal_category_id'] = $categoryId; + + // Check if only one server exists for this category + $stmt = pdo()->prepare(" + SELECT DISTINCT s.id + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt->execute([$categoryId]); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (count($servers) === 1) { + // Auto-skip to plan selection + $server_id = $servers[0]['id']; + $state_data['renewal_server_id'] = $server_id; + updateUserData($chat_id, 'renewal_selecting_plan', $state_data); + showPlansForCategoryAndServerRenewal($chat_id, $categoryId, $server_id, $state_data['renewal_username']); + } else { + updateUserData($chat_id, 'renewal_selecting_server', $state_data); + showServersForCategoryRenewal($chat_id, $categoryId, $state_data['renewal_username']); + } + deleteMessage($chat_id, $message_id); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); + } elseif (strpos($data, 'renewal_show_plans_') === 0) { + // handler انتخاب سرور برای تمدید + preg_match('/renewal_show_plans_cat_(\d+)_srv_(\d+)/', $data, $matches); + $category_id = $matches[1]; + $server_id = $matches[2]; + $state_data = $user_data['state_data']; + $state_data['renewal_server_id'] = $server_id; + updateUserData($chat_id, 'renewal_selecting_plan', $state_data); - $stmt_local = pdo()->prepare("SELECT s.*, p.name as plan_name, p.show_sub_link, p.show_conf_links FROM services s JOIN plans p ON s.plan_id = p.id WHERE s.owner_chat_id = ? AND s.marzban_username = ?"); - $stmt_local->execute([$chat_id, $username]); - $local_service = $stmt_local->fetch(); + showPlansForCategoryAndServerRenewal($chat_id, $category_id, $server_id, $state_data['renewal_username']); + deleteMessage($chat_id, $message_id); + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); + } elseif (strpos($data, 'renewal_buy_plan_') === 0) { + // handler انتخاب پلن برای تمدید + $parts = explode('_', $data); + $plan_id = $parts[3]; - if ($local_service) { - $stmt_server = pdo()->prepare("SELECT * FROM servers WHERE id = ?"); - $stmt_server->execute([$local_service['server_id']]); - $server_info = $stmt_server->fetch(); + $state_data = $user_data['state_data']; + $username_to_renew = $state_data['renewal_username']; - $dynamic_sub_url = $local_service['sub_url']; - if ($server_info) { - $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); - $sub_path = strstr($local_service['sub_url'], '/sub/'); - if ($sub_path === false) { - $sub_path = parse_url($local_service['sub_url'], PHP_URL_PATH); - } - $dynamic_sub_url = $base_sub_url . $sub_path; - } + $plan = getPlanById($plan_id); + if (!$plan) { + apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ خطا: پلن یافت نشد.', 'show_alert' => true]); + die; + } - $panel_user = getPanelUser($username, $local_service['server_id']); - - if ($panel_user && !isset($panel_user['detail'])) { - $qr_code_url = generateQrCodeUrl($dynamic_sub_url); - - $total_gb_from_db = $local_service['volume_gb']; - $used_bytes_from_panel = $panel_user['used_traffic']; - - $total_text = ($total_gb_from_db > 0) ? "{$total_gb_from_db} گیگابایت" : 'نامحدود'; - $used_text = formatBytes($used_bytes_from_panel); - - $remaining_text = 'نامحدود'; - if ($total_gb_from_db > 0) { - $total_bytes_from_db = $total_gb_from_db * 1024 * 1024 * 1024; - $remaining_bytes = $total_bytes_from_db - $used_bytes_from_panel; - $remaining_text = formatBytes(max(0, $remaining_bytes)); - } + $final_price = (float) $plan['price']; + $user_balance = $user_data['balance']; - $expire_date = $panel_user['expire'] ? date('Y-m-d', $panel_user['expire']) : 'نامحدود'; - $status_text = ($panel_user['status'] === 'active' && ($panel_user['expire'] == 0 || $panel_user['expire'] > time())) ? 'فعال' : 'غیرفعال'; - - $caption = - "مشخصات سرویس: {$local_service['plan_name']}\n" . - "➖➖➖➖➖➖➖➖➖➖\n" . - "▫️ وضعیت: {$status_text}\n" . - "🗓 تاریخ انقضا: {$expire_date}\n\n" . - "📊 حجم کل: " . $total_text . "\n" . - "📈 حجم مصرفی: " . $used_text . "\n" . - "📉 حجم باقی‌مانده: " . $remaining_text . "\n" . - "➖➖➖➖➖➖➖➖➖➖\n"; - - if ($local_service['show_sub_link']) { - $caption .= "\n🔗 لینک اشتراک (Subscription):\n" . htmlspecialchars($dynamic_sub_url) . "\n"; - } else { - $caption .= "\n🔗 لینک اشتراک برای این پلن نمایش داده نمی‌شود.\n"; - } + if ($user_balance >= $final_price) { + editMessageText($chat_id, $message_id, "⏳ در حال تمدید سرویس شما..."); - - $keyboard_buttons = [ - [['text' => '♻️ تمدید سرویس', 'callback_data' => "renew_service_{$username}"]], - ]; + $renewal_result = applyPlanRenewal($chat_id, $username_to_renew, $plan_id, $final_price); - if ($local_service['show_conf_links'] && !empty($panel_user['links'])) { - $keyboard_buttons[0][] = ['text' => '📋 دریافت کانفیگ‌ها', 'callback_data' => "get_configs_{$username}"]; - } - - $keyboard_buttons[] = [['text' => '🗑 حذف سرویس', 'callback_data' => "delete_service_confirm_{$username}"]]; - $keyboard_buttons[] = [['text' => '◀️ بازگشت به لیست', 'callback_data' => 'back_to_services']]; - + if ($renewal_result['success']) { + editMessageText($chat_id, $message_id, $renewal_result['message']); + } else { + editMessageText($chat_id, $message_id, "❌ " . $renewal_result['message']); + } - $keyboard = ['inline_keyboard' => $keyboard_buttons]; + updateUserData($chat_id, 'main_menu'); + handleMainMenu($chat_id, $first_name); + } else { + // موجودی کافی نیست، نیاز به پرداخت + $needed_amount = $final_price - $user_balance; + $settings = getSettings(); - deleteMessage($chat_id, $message_id); - sendPhoto($chat_id, $qr_code_url, trim($caption), $keyboard); - } else { - editMessageText($chat_id, $message_id, "❌ خطایی در دریافت اطلاعات سرویس از سرور رخ داد یا سرویس یافت نشد. ممکن است توسط ادمین حذف شده باشد."); - } - } else { - editMessageText($chat_id, $message_id, "❌ سرویس در دیتابیس ربات یافت نشد."); - } + $keyboard_buttons = []; + if (($settings['payment_gateway_status'] ?? 'off') == 'on' && !empty($settings['zarinpal_merchant_id'])) { + $keyboard_buttons[] = [['text' => '🌐 پرداخت آنلاین (زرین‌پال)', 'callback_data' => "charge_for_renewal_{$needed_amount}_{$plan_id}_{$username_to_renew}"]]; + } + if (!empty($settings['payment_method']['card_number'])) { + $keyboard_buttons[] = [['text' => '💳 پرداخت کارت به کارت', 'callback_data' => "manual_pay_for_renewal_{$needed_amount}_{$plan_id}_{$username_to_renew}"]]; + } + + if (empty($keyboard_buttons)) { + editMessageText($chat_id, $message_id, "❌ موجودی شما کافی نیست و هیچ روش پرداختی توسط ادمین فعال نشده است."); + } else { + $message = "⚠️ موجودی شما کافی نیست!\n\n" . + "▫️ قیمت پلن: " . number_format($final_price) . " تومان\n" . + "▫️ موجودی شما: " . number_format($user_balance) . " تومان\n" . + "💰 مبلغ مورد نیاز: " . number_format($needed_amount) . " تومان\n\n" . + "لطفاً روش پرداخت برای تکمیل تمدید را انتخاب کنید:"; + editMessageText($chat_id, $message_id, $message, ['inline_keyboard' => $keyboard_buttons]); } - elseif (strpos($data, 'renew_service_') === 0) { - $settings = getSettings(); - if (($settings['renewal_status'] ?? 'off') !== 'on') { - apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id, 'text' => '❌ قابلیت تمدید سرویس در حال حاضر غیرفعال است.', 'show_alert' => true]); - die; } - $username = str_replace('renew_service_', '', $data); - updateUserData($chat_id, 'user_awaiting_renewal_days', ['renewal_username' => $username]); - - $price_day = number_format($settings['renewal_price_per_day'] ?? 1000); - $message = "تمدید سرویس\n\n" . - "۱. چند **روز** به اعتبار سرویس شما اضافه شود؟\n\n" . - "▫️ هزینه هر روز: {$price_day} تومان\n" . - "💡 برای رد شدن و عدم تمدید زمان، عدد `0` را وارد کنید."; - - editMessageCaption($chat_id, $message_id, $message, null); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); - } - elseif (strpos($data, 'delete_service_confirm_') === 0) { + } elseif (strpos($data, 'delete_service_confirm_') === 0) { $username = str_replace('delete_service_confirm_', '', $data); $keyboard = ['inline_keyboard' => [[['text' => '✅ بله، حذف کن', 'callback_data' => "delete_service_do_{$username}"], ['text' => '❌ خیر، لغو', 'callback_data' => "service_details_{$username}"]]]]; editMessageCaption($chat_id, $message_id, "⚠️ آیا از حذف این سرویس مطمئن هستید؟\nاین عمل غیرقابل بازگشت است و تمام اطلاعات سرویس پاک خواهد شد.", $keyboard); - } - elseif (strpos($data, 'delete_service_do_') === 0) { + } elseif (strpos($data, 'delete_service_do_') === 0) { $username = str_replace('delete_service_do_', '', $data); editMessageCaption($chat_id, $message_id, "⏳ در حال حذف سرویس..."); @@ -1549,23 +1585,19 @@ deleteUserService($chat_id, $username, $server_id); if ($result_panel) { editMessageCaption($chat_id, $message_id, "✅ سرویس شما با موفقیت حذف شد."); - } - else { + } else { editMessageCaption($chat_id, $message_id, "⚠️ سرویس از لیست شما حذف شد، اما ممکن است در حذف از پنل اصلی مشکلی رخ داده باشد. لطفا به پشتیبانی اطلاع دهید."); error_log("Failed to delete panel user {$username} on server {$server_id}. Response: " . json_encode($result_panel)); } - } - else { + } else { editMessageCaption($chat_id, $message_id, "❌ خطایی در یافتن اطلاعات سرور برای این سرویس رخ داد."); } - } - elseif ($data == 'back_to_services') { + } elseif ($data == 'back_to_services') { deleteMessage($chat_id, $message_id); $services = getUserServices($chat_id); if (empty($services)) { sendMessage($chat_id, "شما هیچ سرویس فعالی ندارید."); - } - else { + } else { $keyboard_buttons = []; $now = time(); foreach ($services as $service) { @@ -1582,8 +1614,7 @@ handleMainMenu($chat_id, $first_name, true); apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; - } - elseif ($apiRequest) { + } elseif ($apiRequest) { apiRequest('answerCallbackQuery', ['callback_query_id' => $callback_id]); die; } @@ -1605,8 +1636,7 @@ $keyboard = ['keyboard' => [[['text' => '🔒 اشتراک‌گذاری شماره تلفن', 'request_contact' => true]]], 'resize_keyboard' => true, 'one_time_keyboard' => true]; sendMessage($chat_id, $message, $keyboard); die; - } - elseif ($verification_method === 'button') { + } elseif ($verification_method === 'button') { $message = "سلام! برای اطمینان از اینکه شما یک کاربر واقعی هستید، لطفاً روی دکمه زیر کلیک کنید."; $keyboard = ['inline_keyboard' => [[['text' => '✅ تایید می‌کنم', 'callback_data' => 'verify_by_button']]]]; sendMessage($chat_id, $message, $keyboard); @@ -1621,18 +1651,18 @@ $amount = $state_data['charge_amount']; $user_id = $update['message']['from']['id']; $photo_id = $update['message']['photo'][count($update['message']['photo']) - 1]['file_id']; - + // --- آماده‌سازی metadata --- $metadata_to_save = null; if (isset($state_data['purpose']) && $state_data['purpose'] === 'complete_purchase') { $metadata_to_save = json_encode([ - 'purpose' => 'complete_purchase', - 'plan_id' => $state_data['plan_id'], - 'discount_code' => $state_data['discount_code'] ?? null, - 'custom_name' => $state_data['custom_name'] ?? 'سرویس' // اضافه کردن نام دلخواه -]); -} - + 'purpose' => 'complete_purchase', + 'plan_id' => $state_data['plan_id'], + 'discount_code' => $state_data['discount_code'] ?? null, + 'custom_name' => $state_data['custom_name'] ?? 'سرویس' // اضافه کردن نام دلخواه + ]); + } + $stmt = pdo()->prepare("INSERT INTO payment_requests (user_id, amount, photo_file_id, metadata) VALUES (?, ?, ?, ?)"); $stmt->execute([$user_id, $amount, $photo_id, $metadata_to_save]); @@ -1680,8 +1710,7 @@ $stmt->execute([$phone_number, $chat_id]); sendMessage($chat_id, "✅ احراز هویت شما با موفقیت انجام شد. از همراهی شما سپاسگزاریم!"); handleMainMenu($chat_id, $first_name); - } - else { + } else { $message = "❌ متاسفانه شماره ارسالی شما مورد تایید نیست. این ربات فقط برای شماره‌های ایران (+98) فعال است."; $keyboard = ['keyboard' => [[['text' => '🔒 اشتراک‌گذاری شماره تلفن', 'request_contact' => true]]], 'resize_keyboard' => true, 'one_time_keyboard' => true]; sendMessage($chat_id, $message, $keyboard); @@ -1707,8 +1736,7 @@ if ($isAnAdmin && (strpos($user_state, 'admin_') === 0 || $admin_view_mode === 'admin')) { updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); handleMainMenu($chat_id, $first_name, false); - } - else { + } else { updateUserData($chat_id, 'main_menu', ['admin_view' => 'user']); handleMainMenu($chat_id, $first_name, false); } @@ -1739,95 +1767,97 @@ if ($user_state !== 'main_menu') { switch ($user_state) { - + case 'awaiting_service_name': - $custom_name = trim($text); - if (empty($custom_name) || mb_strlen($custom_name) > 50) { - sendMessage($chat_id, "❌ نام وارد شده نامعتبر است. لطفاً یک نام کوتاه‌تر (حداکثر 50 کاراکتر) وارد کنید.", $cancelKeyboard); - break; - } + $custom_name = trim($text); + if (empty($custom_name) || mb_strlen($custom_name) > 50) { + sendMessage($chat_id, "❌ نام وارد شده نامعتبر است. لطفاً یک نام کوتاه‌تر (حداکثر 50 کاراکتر) وارد کنید.", $cancelKeyboard); + break; + } - $state_data = $user_data['state_data']; - $plan_id = $state_data['purchasing_plan_id']; - $discount_code = $state_data['discount_code'] ?? null; - - $plan = getPlanById($plan_id); - if (!$plan) { - sendMessage($chat_id, "❌ خطایی رخ داد. پلن یافت نشد."); - updateUserData($chat_id, 'main_menu'); - break; - } + $state_data = $user_data['state_data']; + $plan_id = $state_data['purchasing_plan_id']; + $discount_code = $state_data['discount_code'] ?? null; - // --- کپی کردن منطق بررسی موجودی و قیمت نهایی از کد قبلی --- - $final_price = (float)$plan['price']; - $discount_applied = false; - $discount_object = null; - if ($discount_code) { - $stmt = pdo()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND usage_count < max_usage"); - $stmt->execute([$discount_code]); - $discount = $stmt->fetch(); - if ($discount) { - if ($discount['type'] == 'percent') { - $final_price = $plan['price'] - ($plan['price'] * $discount['value']) / 100; - } else { - $final_price = $plan['price'] - $discount['value']; - } - $final_price = max(0, $final_price); - $discount_applied = true; - $discount_object = $discount; - } - } - - $user_balance = $user_data['balance']; - - if ($user_balance >= $final_price) { - sendMessage($chat_id, "⏳ نام سرویس تایید شد. لطفاً صبر کنید... در حال ایجاد سرویس شما هستیم."); - $purchase_result = completePurchase($chat_id, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied); - - if ($purchase_result['success']) { - sendPhoto($chat_id, $purchase_result['qr_code_url'], $purchase_result['caption'], $purchase_result['keyboard']); - sendMessage(ADMIN_CHAT_ID, $purchase_result['admin_notification']); - } else { - sendMessage($chat_id, $purchase_result['error_message']); - sendMessage(ADMIN_CHAT_ID, "⚠️ خطای ساخت سرویس\n\nکاربر با شناسه $chat_id قصد خرید پلن '{$plan['name']}' را داشت اما ارتباط با پنل ناموفق بود."); - } - updateUserData($chat_id, 'main_menu'); - handleMainMenu($chat_id, $first_name); + $plan = getPlanById($plan_id); + if (!$plan) { + sendMessage($chat_id, "❌ خطایی رخ داد. پلن یافت نشد."); + updateUserData($chat_id, 'main_menu'); + break; + } - } else { - // کاربر موجودی کافی ندارد، فاکتور صادر شود - $needed_amount = $final_price - $user_balance; - $settings = getSettings(); - - $encoded_name = base64_encode($custom_name); - - $keyboard_buttons = []; - if (($settings['payment_gateway_status'] ?? 'off') == 'on' && !empty($settings['zarinpal_merchant_id'])) { - $callback_data_online = "charge_for_plan_{$needed_amount}_{$plan_id}"; - if ($discount_code) $callback_data_online .= "_{$discount_code}"; - $callback_data_online .= "_{$encoded_name}"; // اضافه کردن نام به انتها - $keyboard_buttons[] = [['text' => '🌐 پرداخت آنلاین (زرین‌پال)', 'callback_data' => $callback_data_online]]; - } - if (!empty($settings['payment_method']['card_number'])) { - $callback_data_manual = "manual_pay_for_plan_{$needed_amount}_{$plan_id}"; - if ($discount_code) $callback_data_manual .= "_{$discount_code}"; - $callback_data_manual .= "_{$encoded_name}"; // اضافه کردن نام به انتها - $keyboard_buttons[] = [['text' => '💳 پرداخت کارت به کارت', 'callback_data' => $callback_data_manual]]; - } + // --- کپی کردن منطق بررسی موجودی و قیمت نهایی از کد قبلی --- + $final_price = (float) $plan['price']; + $discount_applied = false; + $discount_object = null; + if ($discount_code) { + $stmt = pdo()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND usage_count < max_usage"); + $stmt->execute([$discount_code]); + $discount = $stmt->fetch(); + if ($discount) { + if ($discount['type'] == 'percent') { + $final_price = $plan['price'] - ($plan['price'] * $discount['value']) / 100; + } else { + $final_price = $plan['price'] - $discount['value']; + } + $final_price = max(0, $final_price); + $discount_applied = true; + $discount_object = $discount; + } + } + + $user_balance = $user_data['balance']; + + if ($user_balance >= $final_price) { + sendMessage($chat_id, "⏳ نام سرویس تایید شد. لطفاً صبر کنید... در حال ایجاد سرویس شما هستیم."); + $purchase_result = completePurchase($chat_id, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied); + + if ($purchase_result['success']) { + sendPhoto($chat_id, $purchase_result['qr_code_url'], $purchase_result['caption'], $purchase_result['keyboard']); + sendMessage(ADMIN_CHAT_ID, $purchase_result['admin_notification']); + } else { + sendMessage($chat_id, $purchase_result['error_message']); + sendMessage(ADMIN_CHAT_ID, "⚠️ خطای ساخت سرویس\n\nکاربر با شناسه $chat_id قصد خرید پلن '{$plan['name']}' را داشت اما ارتباط با پنل ناموفق بود."); + } + updateUserData($chat_id, 'main_menu'); + handleMainMenu($chat_id, $first_name); + + } else { + // کاربر موجودی کافی ندارد، فاکتور صادر شود + $needed_amount = $final_price - $user_balance; + $settings = getSettings(); + + $encoded_name = base64_encode($custom_name); + + $keyboard_buttons = []; + if (($settings['payment_gateway_status'] ?? 'off') == 'on' && !empty($settings['zarinpal_merchant_id'])) { + $callback_data_online = "charge_for_plan_{$needed_amount}_{$plan_id}"; + if ($discount_code) + $callback_data_online .= "_{$discount_code}"; + $callback_data_online .= "_{$encoded_name}"; // اضافه کردن نام به انتها + $keyboard_buttons[] = [['text' => '🌐 پرداخت آنلاین (زرین‌پال)', 'callback_data' => $callback_data_online]]; + } + if (!empty($settings['payment_method']['card_number'])) { + $callback_data_manual = "manual_pay_for_plan_{$needed_amount}_{$plan_id}"; + if ($discount_code) + $callback_data_manual .= "_{$discount_code}"; + $callback_data_manual .= "_{$encoded_name}"; // اضافه کردن نام به انتها + $keyboard_buttons[] = [['text' => '💳 پرداخت کارت به کارت', 'callback_data' => $callback_data_manual]]; + } + + if (empty($keyboard_buttons)) { + sendMessage($chat_id, "❌ موجودی شما کافی نیست و هیچ روش پرداختی توسط ادمین فعال نشده است."); + } else { + $message = "⚠️ موجودی شما کافی نیست!\n\n" . + "▫️ قیمت پلن: " . number_format($final_price) . " تومان\n" . + "▫️ موجودی شما: " . number_format($user_balance) . " تومان\n" . + "💰 مبلغ مورد نیاز: " . number_format($needed_amount) . " تومان\n\n" . + "لطفاً روش پرداخت برای تکمیل خرید را انتخاب کنید:"; + sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); + } + } + break; - if (empty($keyboard_buttons)) { - sendMessage($chat_id, "❌ موجودی شما کافی نیست و هیچ روش پرداختی توسط ادمین فعال نشده است."); - } else { - $message = "⚠️ موجودی شما کافی نیست!\n\n" . - "▫️ قیمت پلن: " . number_format($final_price) . " تومان\n" . - "▫️ موجودی شما: " . number_format($user_balance) . " تومان\n" . - "💰 مبلغ مورد نیاز: " . number_format($needed_amount) . " تومان\n\n" . - "لطفاً روش پرداخت برای تکمیل خرید را انتخاب کنید:"; - sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); - } - } - break; - case 'admin_awaiting_user_search': if ($isAnAdmin && hasPermission($chat_id, 'manage_users')) { if (!is_numeric($text)) { @@ -1839,10 +1869,10 @@ sendMessage($chat_id, "❌ کاربری با این شناسه یافت نشد. لطفاً شناسه را بررسی کرده و مجدداً تلاش کنید.", $cancelKeyboard); break; } - + $chat_info_response = apiRequest('getChat', ['chat_id' => $target_user['chat_id']]); $chat_info = json_decode($chat_info_response, true); - + $profile_link_html = ''; if ($chat_info['ok'] && !empty($chat_info['result']['username'])) { $username = $chat_info['result']['username']; @@ -1850,84 +1880,86 @@ } else { $profile_link_html = "👤 حساب کاربری: مشاهده پروفایل (بدون یوزرنیم)\n"; } - + // نمایش اطلاعات و دکمه‌های مدیریتی $balance = $target_user['balance'] ?? 0; $status_text = ($target_user['status'] ?? 'active') === 'active' ? 'فعال ✅' : 'مسدود 🚫'; $message = "اطلاعات کاربر: " . htmlspecialchars($target_user['first_name']) . "\n\n" . - "▫️ شناسه: {$target_user['chat_id']}\n" . - $profile_link_html . - "💰 موجودی: " . number_format($balance) . " تومان\n" . - "▫️ وضعیت: {$status_text}\n\n" . - "لطفاً عملیات مورد نظر را انتخاب کنید:"; + "▫️ شناسه: {$target_user['chat_id']}\n" . + $profile_link_html . + "💰 موجودی: " . number_format($balance) . " تومان\n" . + "▫️ وضعیت: {$status_text}\n\n" . + "لطفاً عملیات مورد نظر را انتخاب کنید:"; $status_button_text = ($target_user['status'] ?? 'active') === 'active' ? '🚫 مسدود کردن' : '✅ آزاد کردن'; $status_callback = ($target_user['status'] ?? 'active') === 'active' ? "ban_user_{$target_user['chat_id']}" : "unban_user_{$target_user['chat_id']}"; - $keyboard = ['inline_keyboard' => [ - [ - ['text' => '➕ افزایش موجودی', 'callback_data' => "add_balance_{$target_user['chat_id']}"], - ['text' => '➖ کاهش موجودی', 'callback_data' => "deduct_balance_{$target_user['chat_id']}"] - ], - [ - ['text' => '✉️ ارسال پیام', 'callback_data' => "message_user_{$target_user['chat_id']}"], - ['text' => '🔧 سرویس‌های کاربر', 'callback_data' => "show_user_services_{$target_user['chat_id']}"] - ], - [ - ['text' => $status_button_text, 'callback_data' => $status_callback] - ], - [ - ['text' => '🔎 جستجوی کاربر دیگر', 'callback_data' => 'search_another_user'] + $keyboard = [ + 'inline_keyboard' => [ + [ + ['text' => '➕ افزایش موجودی', 'callback_data' => "add_balance_{$target_user['chat_id']}"], + ['text' => '➖ کاهش موجودی', 'callback_data' => "deduct_balance_{$target_user['chat_id']}"] + ], + [ + ['text' => '✉️ ارسال پیام', 'callback_data' => "message_user_{$target_user['chat_id']}"], + ['text' => '🔧 سرویس‌های کاربر', 'callback_data' => "show_user_services_{$target_user['chat_id']}"] + ], + [ + ['text' => $status_button_text, 'callback_data' => $status_callback] + ], + [ + ['text' => '🔎 جستجوی کاربر دیگر', 'callback_data' => 'search_another_user'] + ] ] - ]]; + ]; sendMessage($chat_id, $message, $keyboard); // وضعیت را به حالت انتظار برای جستجوی بعدی برمی‌گردانیم تا ادمین بتواند پشت سر هم جستجو کند updateUserData($chat_id, 'admin_awaiting_user_search', ['admin_view' => 'admin']); } break; - + case 'admin_awaiting_renewal_price_day': - if ($isAnAdmin && is_numeric($text) && $text >= 0) { - $settings = getSettings(); - $settings['renewal_price_per_day'] = (int)$text; - saveSettings($settings); - sendMessage($chat_id, "✅ قیمت با موفقیت تنظیم شد."); - updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); - showRenewalManagementMenu($chat_id); - } else { - sendMessage($chat_id, "❌ لطفا فقط عدد وارد کنید."); - } - break; - - case 'admin_awaiting_merchant_id': + if ($isAnAdmin && is_numeric($text) && $text >= 0) { + $settings = getSettings(); + $settings['renewal_price_per_day'] = (int) $text; + saveSettings($settings); + sendMessage($chat_id, "✅ قیمت با موفقیت تنظیم شد."); + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); + showRenewalManagementMenu($chat_id); + } else { + sendMessage($chat_id, "❌ لطفا فقط عدد وارد کنید."); + } + break; + + case 'admin_awaiting_merchant_id': if ($isAnAdmin && strlen($text) === 36) { $settings = getSettings(); $settings['zarinpal_merchant_id'] = $text; saveSettings($settings); sendMessage($chat_id, "✅ مرچنت کد با موفقیت ذخیره شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); - + } else { sendMessage($chat_id, "❌ مرچنت کد نامعتبر است. باید دقیقا ۳۶ کاراکتر باشد."); } break; - + case 'admin_awaiting_renewal_price_gb': - if ($isAnAdmin && is_numeric($text) && $text >= 0) { - $settings = getSettings(); - $settings['renewal_price_per_gb'] = (int)$text; - saveSettings($settings); - sendMessage($chat_id, "✅ قیمت با موفقیت تنظیم شد."); - updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); - showRenewalManagementMenu($chat_id); - } else { - sendMessage($chat_id, "❌ لطفا فقط عدد وارد کنید."); - } - break; - + if ($isAnAdmin && is_numeric($text) && $text >= 0) { + $settings = getSettings(); + $settings['renewal_price_per_gb'] = (int) $text; + saveSettings($settings); + sendMessage($chat_id, "✅ قیمت با موفقیت تنظیم شد."); + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); + showRenewalManagementMenu($chat_id); + } else { + sendMessage($chat_id, "❌ لطفا فقط عدد وارد کنید."); + } + break; + case 'admin_awaiting_category_name': if (!hasPermission($chat_id, 'manage_categories')) { break; @@ -1958,7 +1990,7 @@ break; } $state_data = $user_data['state_data']; - $state_data['new_plan_price'] = (int)$text; + $state_data['new_plan_price'] = (int) $text; updateUserData($chat_id, 'awaiting_plan_volume', $state_data); sendMessage($chat_id, "3/6 - لطفا حجم پلن را به گیگابایت (GB) وارد کنید (فقط عدد):", $cancelKeyboard); break; @@ -1972,7 +2004,7 @@ break; } $state_data = $user_data['state_data']; - $state_data['new_plan_volume'] = (int)$text; + $state_data['new_plan_volume'] = (int) $text; updateUserData($chat_id, 'awaiting_plan_duration', $state_data); sendMessage($chat_id, "4/6 - لطفا مدت زمان پلن را به روز وارد کنید (فقط عدد):", $cancelKeyboard); break; @@ -1986,7 +2018,7 @@ break; } $state_data = $user_data['state_data']; - $state_data['new_plan_duration'] = (int)$text; + $state_data['new_plan_duration'] = (int) $text; updateUserData($chat_id, 'awaiting_plan_description', $state_data); $keyboard = ['keyboard' => [[['text' => 'رد شدن'], ['text' => '◀️ بازگشت به منوی اصلی']]], 'resize_keyboard' => true]; sendMessage($chat_id, "5/6 - در صورت تمایل، توضیحات مختصری برای پلن وارد کنید (اختیاری):", $keyboard); @@ -2006,11 +2038,11 @@ sendMessage($chat_id, "6/6 - تعداد مجاز خرید برای این پلن را وارد کنید (فقط عدد).\n\nبرای فروش نامحدود، عدد `0` را وارد کنید.", $keyboard); break; - case 'awaiting_plan_purchase_limit': + case 'awaiting_plan_purchase_limit': if (!hasPermission($chat_id, 'manage_plans')) { break; } - if (!is_numeric($text) || (int)$text < 0) { + if (!is_numeric($text) || (int) $text < 0) { sendMessage($chat_id, "❌ لطفا فقط یک عدد صحیح (مثبت یا صفر) وارد کنید.", $cancelKeyboard); break; } @@ -2026,7 +2058,7 @@ 'volume_gb' => $state_data['new_plan_volume'], 'duration_days' => $state_data['new_plan_duration'], 'description' => $state_data['new_plan_description'], - 'purchase_limit' => (int)$text, + 'purchase_limit' => (int) $text, ]; updateUserData($chat_id, 'awaiting_plan_sub_link_setting', ['temp_plan_data' => $new_plan_data]); @@ -2034,15 +2066,16 @@ $keyboard = ['inline_keyboard' => [[['text' => '✅ بله', 'callback_data' => 'plan_set_sub_yes'], ['text' => '❌ خیر', 'callback_data' => 'plan_set_sub_no']]]]; sendMessage($chat_id, "سوال ۱/۲: آیا لینک اشتراک (Subscription) به کاربر نمایش داده شود؟\n(پیشنهادی: بله)", $keyboard); break; - - case 'admin_awaiting_sub_host': - if (!hasPermission($chat_id, 'manage_marzban')) break; + + case 'admin_awaiting_sub_host': + if (!hasPermission($chat_id, 'manage_marzban')) + break; $state_data = $user_data['state_data']; $server_id = $state_data['editing_server_id']; $new_sub_host = null; $message_text = ""; - + if (strtolower($text) === 'reset') { $new_sub_host = null; $message_text = "✅ آدرس لینک اشتراک با موفقیت به حالت پیش‌فرض بازنشانی شد."; @@ -2056,10 +2089,10 @@ $stmt = pdo()->prepare("UPDATE servers SET sub_host = ? WHERE id = ?"); $stmt->execute([$new_sub_host, $server_id]); - + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); sendMessage($chat_id, $message_text); - + $servers = pdo()->query("SELECT id, name FROM servers")->fetchAll(PDO::FETCH_ASSOC); $keyboard_buttons = [[['text' => '➕ افزودن سرور جدید', 'callback_data' => 'add_server_select_type']]]; foreach ($servers as $server) { @@ -2068,7 +2101,7 @@ $keyboard_buttons[] = [['text' => '◀️ بازگشت به پنل', 'callback_data' => 'back_to_admin_panel']]; sendMessage($chat_id, "🌐 لیست سرورها به‌روز شد.", ['inline_keyboard' => $keyboard_buttons]); break; - + case 'admin_awaiting_card_number': if (!hasPermission($chat_id, 'manage_payment')) { break; @@ -2155,15 +2188,16 @@ } else { $error_message = "⚠️ هشدار: ربات نتوانست به سرور جدید متصل شود. لطفا اطلاعات وارد شده را بررسی کرده و در صورت نیاز سرور را حذف و مجدداً اضافه کنید."; if ($connection_error) { - $error_message .= "\n\nجزئیات خطا:\n" . htmlspecialchars($connection_error) . ""; + $error_message .= "\n\nجزئیات خطا:\n" . htmlspecialchars($connection_error) . ""; } sendMessage($chat_id, $error_message); } handleMainMenu($chat_id, $first_name); break; - - case 'admin_awaiting_plan_edit_input': - if (!hasPermission($chat_id, 'manage_plans')) break; + + case 'admin_awaiting_plan_edit_input': + if (!hasPermission($chat_id, 'manage_plans')) + break; $state_data = $user_data['state_data']; $plan_id = $state_data['editing_plan_id']; @@ -2173,7 +2207,7 @@ $validation = $field_info['validation']; $value = $text; $user_message_id = $update['message']['message_id']; - + $is_valid = false; if ($validation === 'text' && !empty($value)) { $is_valid = true; @@ -2186,25 +2220,25 @@ deleteMessage($chat_id, $user_message_id); break; } - + $stmt = pdo()->prepare("UPDATE plans SET `{$column}` = ? WHERE id = ?"); $stmt->execute([$value, $plan_id]); - + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); showPlanEditor($chat_id, $editor_message_id, $plan_id, "✅ مقدار با موفقیت به‌روز شد."); deleteMessage($chat_id, $user_message_id); break; - case 'awaiting_charge_amount': + case 'awaiting_charge_amount': if (!is_numeric($text) || $text <= 0) { sendMessage($chat_id, "❌ لطفا یک مبلغ معتبر (عدد مثبت) به تومان وارد کنید.", $cancelKeyboard); break; } - $amount = (int)$text; + $amount = (int) $text; $settings = getSettings(); - + $keyboard_buttons = []; - + if (!empty($settings['payment_method']['card_number'])) { $keyboard_buttons[] = [['text' => '💳 پرداخت کارت به کارت', 'callback_data' => "charge_manual_{$amount}"]]; } @@ -2212,7 +2246,7 @@ if (($settings['payment_gateway_status'] ?? 'off') == 'on' && !empty($settings['zarinpal_merchant_id'])) { $keyboard_buttons[] = [['text' => '🌐 پرداخت آنلاین (زرین‌پال)', 'callback_data' => "charge_zarinpal_{$amount}"]]; } - + if (empty($keyboard_buttons)) { sendMessage($chat_id, "متاسفانه هیچ روش پرداختی توسط ادمین فعال نشده است."); updateUserData($chat_id, 'main_menu'); @@ -2298,8 +2332,7 @@ $user_keyboard = ['inline_keyboard' => [[['text' => '💬 پاسخ مجدد', 'callback_data' => "reply_ticket_{$ticket_id}"], ['text' => '✖️ بستن تیکت', 'callback_data' => "close_ticket_{$ticket_id}"]]]]; sendMessage($target_user_id, $user_message, $user_keyboard); sendMessage($chat_id, "✅ پاسخ شما برای کاربر ارسال شد."); - } - else { + } else { sendMessage($chat_id, "❌ خطایی در ارسال پاسخ رخ داد. تیکت یا کاربر یافت نشد."); } updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); @@ -2328,7 +2361,7 @@ } $state_data = $user_data['state_data']; $target_id = $state_data['target_user_id']; - updateUserBalance($target_id, (int)$text, 'add'); + updateUserBalance($target_id, (int) $text, 'add'); $new_balance_data = getUserData($target_id, ''); sendMessage($chat_id, "✅ مبلغ " . number_format($text) . " تومان با موفقیت به موجودی کاربر $target_id اضافه شد."); sendMessage($target_id, "✅ مبلغ " . number_format($text) . " تومان توسط ادمین به موجودی شما اضافه شد.\nموجودی جدید: " . number_format($new_balance_data['balance']) . " تومان."); @@ -2359,11 +2392,11 @@ $state_data = $user_data['state_data']; $target_id = $state_data['target_user_id']; $target_user_data = getUserData($target_id, ''); - if ($target_user_data['balance'] < (int)$text) { + if ($target_user_data['balance'] < (int) $text) { sendMessage($chat_id, "❌ موجودی کاربر برای کسر این مبلغ کافی نیست.\nموجودی فعلی: " . number_format($target_user_data['balance']) . " تومان", $cancelKeyboard); break; } - updateUserBalance($target_id, (int)$text, 'deduct'); + updateUserBalance($target_id, (int) $text, 'deduct'); $new_balance_data = getUserData($target_id, ''); sendMessage($chat_id, "✅ مبلغ " . number_format($text) . " تومان با موفقیت از موجودی کاربر $target_id کسر شد."); sendMessage($target_id, "❗️ مبلغ " . number_format($text) . " تومان توسط ادمین از موجودی شما کسر شد.\nموجودی جدید: " . number_format($new_balance_data['balance']) . " تومان."); @@ -2394,8 +2427,7 @@ $decoded_result = json_decode($result, true); if ($decoded_result && $decoded_result['ok']) { sendMessage($chat_id, "✅ پیام شما با موفقیت به کاربر $target_id ارسال شد."); - } - else { + } else { sendMessage($chat_id, "❌ ارسال پیام به کاربر $target_id ناموفق بود. ممکن است کاربر ربات را بلاک کرده باشد."); } updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); @@ -2481,7 +2513,7 @@ break; } $settings = getSettings(); - $settings['welcome_gift_balance'] = (int)$text; + $settings['welcome_gift_balance'] = (int) $text; saveSettings($settings); sendMessage($chat_id, "✅ هدیه عضویت برای کاربران جدید روی " . number_format($text) . " تومان تنظیم شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); @@ -2497,7 +2529,7 @@ break; } sendMessage($chat_id, "⏳ عملیات افزودن حجم به تمام سرویس‌ها شروع شد. این فرآیند ممکن است کمی طول بکشد..."); - $data_to_add_gb = (float)$text; + $data_to_add_gb = (float) $text; $bytes_to_add = $data_to_add_gb * 1024 * 1024 * 1024; $all_services = pdo() ->query("SELECT marzban_username, server_id FROM services") @@ -2520,13 +2552,11 @@ $result = modifyPanelUser($username, $server_id, ['data_limit' => $new_limit]); if ($result && !isset($result['detail'])) { $success_count++; - } - else { + } else { $fail_count++; } } - } - else { + } else { $fail_count++; } usleep(100000); @@ -2545,7 +2575,7 @@ break; } sendMessage($chat_id, "⏳ عملیات افزودن زمان به تمام سرویس‌ها شروع شد. این فرآیند ممکن است کمی طول بکشد..."); - $days_to_add = (int)$text; + $days_to_add = (int) $text; $seconds_to_add = $days_to_add * 86400; $all_services = pdo() ->query("SELECT marzban_username, server_id FROM services") @@ -2568,13 +2598,11 @@ $result = modifyPanelUser($username, $server_id, ['expire' => $new_expire]); if ($result && !isset($result['detail'])) { $success_count++; - } - else { + } else { $fail_count++; } } - } - else { + } else { $fail_count++; } usleep(100000); @@ -2592,7 +2620,7 @@ sendMessage($chat_id, "❌ شناسه وارد شده نامعتبر است. لطفا فقط عدد وارد کنید.", $cancelKeyboard); break; } - $target_id = (int)$text; + $target_id = (int) $text; if ($target_id == ADMIN_CHAT_ID) { sendMessage($chat_id, "❌ شما نمی‌توانید خودتان را به عنوان ادمین اضافه کنید.", $cancelKeyboard); break; @@ -2613,8 +2641,7 @@ $target_first_name = "کاربر {$target_id}"; if ($chat_info['ok'] && isset($chat_info['result']['first_name'])) { $target_first_name = $chat_info['result']['first_name']; - } - else { + } else { sendMessage($chat_id, "⚠️ نتوانستم نام کاربر را از تلگرام دریافت کنم. با نام پیش‌فرض ثبت شد."); } addAdmin($target_id, $target_first_name); @@ -2636,7 +2663,7 @@ break; } $state_data = $user_data['state_data']; - $state_data['new_discount_value'] = (int)$text; + $state_data['new_discount_value'] = (int) $text; updateUserData($chat_id, 'admin_awaiting_discount_usage', $state_data); sendMessage($chat_id, "4/4 - حداکثر تعداد استفاده از این کد را وارد کنید (فقط عدد):", $cancelKeyboard); break; @@ -2648,30 +2675,30 @@ } $discount_data = $user_data['state_data']; $stmt = pdo()->prepare("INSERT INTO discount_codes (code, type, value, max_usage) VALUES (?, ?, ?, ?)"); - $stmt->execute([$discount_data['new_discount_code'], $discount_data['new_discount_type'], $discount_data['new_discount_value'], (int)$text]); + $stmt->execute([$discount_data['new_discount_code'], $discount_data['new_discount_type'], $discount_data['new_discount_value'], (int) $text]); sendMessage($chat_id, "✅ کد تخفیف `{$discount_data['new_discount_code']}` با موفقیت ایجاد شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); $current_first_name = $update['message']['from']['first_name']; handleMainMenu($chat_id, $current_first_name); break; - case 'user_awaiting_discount_code': + case 'user_awaiting_discount_code': $code = strtoupper(trim($text)); $category_id = $user_data['state_data']['target_category_id']; $server_id = $user_data['state_data']['target_server_id']; - + $stmt = pdo()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND usage_count < max_usage"); $stmt->execute([$code]); $discount = $stmt->fetch(); if (!$discount) { sendMessage($chat_id, "❌ کد تخفیف وارد شده نامعتبر یا منقضی شده است."); - - showPlansForCategoryAndServer($chat_id, $category_id, $server_id); + + showPlansForCategoryAndServer($chat_id, $category_id, $server_id); updateUserData($chat_id, 'main_menu'); break; } - + $plan_stmt = pdo()->prepare("SELECT * FROM plans WHERE category_id = ? AND server_id = ? AND status = 'active' AND is_test_plan = 0"); $plan_stmt->execute([$category_id, $server_id]); $active_plans = $plan_stmt->fetchAll(PDO::FETCH_ASSOC); @@ -2685,15 +2712,14 @@ $discounted_price = 0; if ($discount['type'] == 'percent') { $discounted_price = $original_price - ($original_price * $discount['value']) / 100; - } - else { + } else { $discounted_price = $original_price - $discount['value']; } $discounted_price = max(0, $discounted_price); $button_text = "{$plan['name']} | " . number_format($original_price) . " ⬅️ " . number_format($discounted_price) . " تومان"; $keyboard_buttons[] = [['text' => $button_text, 'callback_data' => "buy_plan_{$plan['id']}_with_code_{$code}"]]; } - + $keyboard_buttons[] = [['text' => '◀️ بازگشت', 'callback_data' => "show_plans_cat_{$category_id}_srv_{$server_id}"]]; sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); updateUserData($chat_id, 'main_menu'); @@ -2707,7 +2733,7 @@ sendMessage($chat_id, "❌ لطفا یک مبلغ معتبر (عدد مثبت) به تومان وارد کنید.", $cancelKeyboard); break; } - $amount_to_add = (int)$text; + $amount_to_add = (int) $text; sendMessage($chat_id, "⏳ عملیات افزایش موجودی همگانی شروع شد..."); $updated_users_count = increaseAllUsersBalance($amount_to_add); sendMessage($chat_id, "✅ عملیات با موفقیت انجام شد.\nمبلغ " . number_format($amount_to_add) . " تومان به موجودی {$updated_users_count} کاربر فعال اضافه گردید."); @@ -2732,8 +2758,7 @@ $state_data['new_guide_content_type'] = 'photo'; $state_data['new_guide_photo_id'] = $update['message']['photo'][count($update['message']['photo']) - 1]['file_id']; $state_data['new_guide_message_text'] = $update['message']['caption'] ?? ''; - } - else { + } else { $state_data['new_guide_content_type'] = 'text'; $state_data['new_guide_photo_id'] = null; $state_data['new_guide_message_text'] = $text; @@ -2754,7 +2779,7 @@ break; } $settings = getSettings(); - $settings['test_config_usage_limit'] = (int)$text; + $settings['test_config_usage_limit'] = (int) $text; saveSettings($settings); sendMessage($chat_id, "✅ تعداد مجاز برای هر کاربر روی {$text} بار تنظیم شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); @@ -2799,7 +2824,7 @@ break; } $settings = getSettings(); - $settings['notification_expire_days'] = (int)$text; + $settings['notification_expire_days'] = (int) $text; saveSettings($settings); sendMessage($chat_id, "✅ با موفقیت روی {$text} روز تنظیم شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); @@ -2815,7 +2840,7 @@ break; } $settings = getSettings(); - $settings['notification_expire_gb'] = (int)$text; + $settings['notification_expire_gb'] = (int) $text; saveSettings($settings); sendMessage($chat_id, "✅ با موفقیت روی {$text} گیگابایت تنظیم شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); @@ -2843,7 +2868,7 @@ break; } $settings = getSettings(); - $settings['notification_inactive_days'] = (int)$text; + $settings['notification_inactive_days'] = (int) $text; saveSettings($settings); sendMessage($chat_id, "✅ با موفقیت روی {$text} روز تنظیم شد."); updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); @@ -2861,96 +2886,71 @@ updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); $data = 'config_inactive_reminder'; break; - - case 'user_awaiting_renewal_days': - if (!is_numeric($text) || $text < 0) { - sendMessage($chat_id, "❌ لطفا فقط یک عدد صحیح (مثبت یا صفر) وارد کنید."); + + // Note: حذف state handlers قدیمی تمدید: user_awaiting_renewal_days, user_awaiting_renewal_gb و awaiting_renewal_screenshot + // سیستم جدید از طریق انتخاب پلن عمل می‌کند + + case 'admin_awaiting_charge_amount': + if (!hasPermission($chat_id, 'manage_payment')) { + break; + } + if (!is_numeric($text) || $text <= 0) { + sendMessage($chat_id, "❌ لطفا یک مبلغ معتبر (عدد مثبت) به تومان وارد کنید.", $cancelKeyboard); break; } + $amount = (int) $text; $state_data = $user_data['state_data']; - $state_data['renewal_days'] = (int)$text; - updateUserData($chat_id, 'user_awaiting_renewal_gb', $state_data); + $user_to_charge_id = $state_data['user_id']; - $settings = getSettings(); - $price_gb = number_format($settings['renewal_price_per_gb'] ?? 2000); - $message = "تمدید سرویس\n\n" . - "۲. چند **گیگابایت** به حجم سرویس شما اضافه شود؟\n\n" . - "▫️ هزینه هر گیگ: {$price_gb} تومان\n" . - "💡 برای رد شدن و عدم تمدید حجم، عدد `0` را وارد کنید."; - sendMessage($chat_id, $message); + $user_to_charge = getUser($user_to_charge_id); + if (!$user_to_charge) { + sendMessage($chat_id, "❌ کاربری با این شناسه یافت نشد."); + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); + handleMainMenu($chat_id, $first_name); + break; + } + + $new_balance = $user_to_charge['balance'] + $amount; + updateUserBalance($user_to_charge_id, $new_balance); + + sendMessage($chat_id, "✅ مبلغ " . number_format($amount) . " تومان به موجودی کاربر " . htmlspecialchars($user_to_charge['first_name']) . " ({$user_to_charge_id}) اضافه شد.\nموجودی جدید: " . number_format($new_balance) . " تومان."); + sendMessage($user_to_charge_id, "✅ مبلغ " . number_format($amount) . " تومان به موجودی حساب شما اضافه شد.\nموجودی جدید: " . number_format($new_balance) . " تومان."); + + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); + handleMainMenu($chat_id, $first_name); break; - case 'user_awaiting_renewal_gb': - if (!is_numeric($text) || $text < 0) { - sendMessage($chat_id, "❌ لطفا فقط یک عدد صحیح (مثبت یا صفر) وارد کنید."); + case 'admin_awaiting_deduct_amount': + if (!hasPermission($chat_id, 'manage_payment')) { + break; + } + if (!is_numeric($text) || $text <= 0) { + sendMessage($chat_id, "❌ لطفا یک مبلغ معتبر (عدد مثبت) به تومان وارد کنید.", $cancelKeyboard); break; } + $amount = (int) $text; $state_data = $user_data['state_data']; - $days_to_add = $state_data['renewal_days']; - $gb_to_add = (int)$text; - - if ($days_to_add == 0 && $gb_to_add == 0) { - sendMessage($chat_id, "شما هیچ مقداری برای تمدید وارد نکردید. عملیات لغو شد."); - updateUserData($chat_id, 'main_menu'); + $user_to_deduct_id = $state_data['user_id']; + + $user_to_deduct = getUser($user_to_deduct_id); + if (!$user_to_deduct) { + sendMessage($chat_id, "❌ کاربری با این شناسه یافت نشد."); + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); handleMainMenu($chat_id, $first_name); break; } - - $settings = getSettings(); - $cost_days = $days_to_add * (int)($settings['renewal_price_per_day'] ?? 1000); - $cost_gb = $gb_to_add * (int)($settings['renewal_price_per_gb'] ?? 2000); - $total_cost = $cost_days + $cost_gb; - - $state_data['renewal_gb'] = $gb_to_add; - $state_data['renewal_total_cost'] = $total_cost; - updateUserData($chat_id, 'user_confirming_renewal', $state_data); - - $summary = "خلاصه درخواست تمدید شما:\n\n" . - "▫️ افزایش زمان: {$days_to_add} روز\n" . - "▫️ افزایش حجم: {$gb_to_add} گیگابایت\n\n" . - "💰 هزینه کل: " . number_format($total_cost) . " تومان\n\n" . - "موجودی فعلی شما: " . number_format($user_data['balance']) . " تومان\n\n" . - "آیا تایید می‌کنید؟"; - - $keyboard = ['inline_keyboard' => [[['text' => '✅ بله، پرداخت کن', 'callback_data' => 'confirm_renewal_payment']]]]; - sendMessage($chat_id, $summary, $keyboard); - break; - - case 'awaiting_renewal_screenshot': - if (isset($update['message']['photo'])) { - $state_data = $user_data['state_data']; - $photo_id = $update['message']['photo'][count($update['message']['photo']) - 1]['file_id']; - - $stmt = pdo()->prepare("UPDATE renewal_requests SET photo_file_id = ? WHERE id = ?"); - $stmt->execute([$photo_id, $state_data['renewal_request_id']]); - - - $request_id = $state_data['renewal_request_id']; - $caption = "درخواست تمدید سرویس جدید\n\n" . - "👤 کاربر: " . htmlspecialchars($first_name) . " ({$chat_id})\n" . - "▫️ سرویس: {$state_data['renewal_username']}\n" . - "⏰ تمدید زمان: {$state_data['renewal_days']} روز\n" . - "📊 تمدید حجم: {$state_data['renewal_gb']} گیگ\n" . - "💰 هزینه: " . number_format($state_data['renewal_total_cost']) . " تومان\n" . - "▫️ شماره درخواست: #R-{$request_id}"; - - $keyboard = ['inline_keyboard' => [[ - ['text' => '✅ تایید تمدید', 'callback_data' => "approve_renewal_{$request_id}"], - ['text' => '❌ رد تمدید', 'callback_data' => "reject_renewal_{$request_id}"] - ]]]; - - $all_admins = getAdmins(); - $all_admins[ADMIN_CHAT_ID] = []; - foreach (array_keys($all_admins) as $admin_id) { - if (hasPermission($admin_id, 'manage_payment')) { - sendPhoto($admin_id, $photo_id, $caption, $keyboard); - } - } - sendMessage($chat_id, "✅ رسید شما برای ادمین ارسال شد. پس از بررسی، سرویس شما تمدید خواهد شد."); - updateUserData($chat_id, 'main_menu'); - handleMainMenu($chat_id, $first_name); + $new_balance = $user_to_deduct['balance'] - $amount; + if ($new_balance < 0) { + $new_balance = 0; // Ensure balance doesn't go negative } + updateUserBalance($user_to_deduct_id, $new_balance); + + sendMessage($chat_id, "✅ مبلغ " . number_format($amount) . " تومان از موجودی کاربر " . htmlspecialchars($user_to_deduct['first_name']) . " ({$user_to_deduct_id}) کسر شد.\nموجودی جدید: " . number_format($new_balance) . " تومان."); + sendMessage($user_to_deduct_id, "❌ مبلغ " . number_format($amount) . " تومان از موجودی حساب شما کسر شد.\nموجودی جدید: " . number_format($new_balance) . " تومان."); + + updateUserData($chat_id, 'main_menu', ['admin_view' => 'admin']); + handleMainMenu($chat_id, $first_name); break; } die; @@ -2965,8 +2965,7 @@ $categories = getCategories(true); if (empty($categories)) { sendMessage($chat_id, "متاسفانه در حال حاضر هیچ سرویسی برای فروش موجود نیست."); - } - else { + } else { $keyboard_buttons = []; foreach ($categories as $category) { $keyboard_buttons[] = [['text' => '🛍 ' . $category['name'], 'callback_data' => 'cat_' . $category['id']]]; @@ -3120,27 +3119,8 @@ sendMessage($chat_id, "لطفاً شناسه عددی (Chat ID) کاربر مورد نظر را برای جستجو وارد کنید:", $cancelKeyboard); } break; - - case '💰 افزایش موجودی همگانی': - if ($isAnAdmin && hasPermission($chat_id, 'manage_users')) { - updateUserData($chat_id, 'admin_awaiting_bulk_balance_amount', ['admin_view' => 'admin']); - sendMessage($chat_id, "لطفا مبلغی که می‌خواهید به موجودی تمام کاربران فعال اضافه شود را به تومان وارد کنید:", $cancelKeyboard); - } - break; - case '➕ افزودن حجم همگانی': - if ($isAnAdmin && hasPermission($chat_id, 'manage_users')) { - updateUserData($chat_id, 'admin_awaiting_bulk_data_amount', ['admin_view' => 'admin']); - sendMessage($chat_id, "لطفا مقدار حجمی که می‌خواهید به تمام سرویس‌ها اضافه شود را به گیگابایت (GB) وارد کنید:", $cancelKeyboard); - } - break; - case '➕ افزودن زمان همگانی': - if ($isAnAdmin && hasPermission($chat_id, 'manage_users')) { - updateUserData($chat_id, 'admin_awaiting_bulk_time_amount', ['admin_view' => 'admin']); - sendMessage($chat_id, "لطفا تعداد روزی که می‌خواهید به تمام سرویس‌ها اضافه شود را وارد کنید:", $cancelKeyboard); - } - break; case '📣 ارسال همگانی': if ($isAnAdmin && hasPermission($chat_id, 'broadcast')) { @@ -3257,16 +3237,16 @@ sendMessage($chat_id, "مرحله ۱/۳: شماره کارت ۱۶ رقمی را وارد کنید:", $cancelKeyboard); } break; - + case '💳 مدیریت درگاه پرداخت': if ($isAnAdmin) { $settings = getSettings(); $status_icon = ($settings['payment_gateway_status'] ?? 'off') == 'on' ? '✅' : '❌'; $merchant_id = $settings['zarinpal_merchant_id'] ?? 'تنظیم نشده'; - + $message = "💳 مدیریت درگاه پرداخت زرین‌پال\n\n" . - "▫️ وضعیت کلی: " . ($status_icon == '✅' ? 'فعال' : 'غیرفعال') . "\n" . - "▫️ مرچنت کد: {$merchant_id}"; + "▫️ وضعیت کلی: " . ($status_icon == '✅' ? 'فعال' : 'غیرفعال') . "\n" . + "▫️ مرچنت کد: {$merchant_id}"; $keyboard = [ 'inline_keyboard' => [ @@ -3278,7 +3258,7 @@ sendMessage($chat_id, $message, $keyboard); } break; - + case '📊 آمار کلی': if ($isAnAdmin && hasPermission($chat_id, 'view_stats')) { $total_users = pdo() @@ -3340,12 +3320,12 @@ sendMessage($chat_id, "🎁 بخش مدیریت کدهای تخفیف:", $keyboard); } break; - + case '🔄 مدیریت تمدید': - if ($isAnAdmin) { + if ($isAnAdmin) { showRenewalManagementMenu($chat_id); } - break; + break; case '➕ افزودن کد تخفیف': if ($isAnAdmin) { @@ -3384,8 +3364,7 @@ foreach ($services as $service) { if ($service['expire_timestamp'] < $now) { $expired_services_count++; - } - else { + } else { $active_services_count++; } } @@ -3409,8 +3388,7 @@ $services = getUserServices($chat_id); if (empty($services)) { sendMessage($chat_id, "شما هیچ سرویس فعالی ندارید."); - } - else { + } else { $keyboard_buttons = []; $now = time(); foreach ($services as $service) { @@ -3440,7 +3418,7 @@ } $settings = getSettings(); - $usage_limit = (int)($settings['test_config_usage_limit'] ?? 1); + $usage_limit = (int) ($settings['test_config_usage_limit'] ?? 1); if ($user_data['test_config_count'] >= $usage_limit) { sendMessage($chat_id, "❌ شما قبلا از حداکثر تعداد کانفیگ تست خود استفاده کرده‌اید."); @@ -3498,6 +3476,17 @@ } break; + case '📱 پنل کاربری': + $web_app_url = BASE_URL . '/web/user/index.php'; + $message = "برای ورود به پنل کاربری روی دکمه زیر کلیک کنید:"; + $keyboard = [ + 'inline_keyboard' => [ + [['text' => '📱 ورود به پنل کاربری', 'web_app' => ['url' => $web_app_url]]] + ] + ]; + sendMessage($chat_id, $message, $keyboard); + break; + default: if ($user_state === 'main_menu' && !$apiRequest) { sendMessage($chat_id, "دستور شما را متوجه نشدم. لطفا از دکمه‌های موجود استفاده کنید."); diff --git a/src/cron.php b/src/cron.php index 7c24b2a..e6fbb19 100644 --- a/src/cron.php +++ b/src/cron.php @@ -8,7 +8,7 @@ require_once __DIR__ . '/includes/config.php'; require_once __DIR__ . '/includes/db.php'; require_once __DIR__ . '/includes/functions.php'; -require_once __DIR__ . '/includes/marzban_api.php'; +require_once __DIR__ . '/api/marzban_api.php'; echo "Cron job started at " . date('Y-m-d H:i:s') . "\n"; @@ -128,4 +128,4 @@ function checkInactiveUsers() echo "An error occurred: " . $e->getMessage() . "\n"; } -echo "Cron job finished at " . date('Y-m-d H:i:s') . "\n"; \ No newline at end of file +echo "Cron job finished at " . date('Y-m-d H:i:s') . "\n"; diff --git a/src/includes/config.php b/src/includes/config.php index 759d3c0..78d42d0 100644 --- a/src/includes/config.php +++ b/src/includes/config.php @@ -10,6 +10,9 @@ define('SECRET_TOKEN', 'SECRET'); +// آدرس دامنه برای وب‌اپ (بدون اسلش آخر) +define('BASE_URL', 'https://your-domain.com'); + // --------------------------------------------------------------------- // --- مسیرهای پروژه --- // --------------------------------------------------------------------- @@ -21,9 +24,9 @@ // --- تنظیمات اتصال به پایگاه داده --- // --------------------------------------------------------------------- -define('DB_HOST', 'localhost'); -define('DB_NAME', 'NAME'); -define('DB_USER', 'USER'); +define('DB_HOST', 'localhost'); +define('DB_NAME', 'NAME'); +define('DB_USER', 'USER'); define('DB_PASS', 'PASSWORD'); // --------------------------------------------------------------------- diff --git a/src/includes/functions.php b/src/includes/functions.php index 2390fdb..bc82ad6 100644 --- a/src/includes/functions.php +++ b/src/includes/functions.php @@ -10,7 +10,8 @@ // ===================================================================== -function handleKeyboard($keyboard, $handleMainMenu = false) { +function handleKeyboard($keyboard, $handleMainMenu = false) +{ if (USER_INLINE_KEYBOARD) { if (is_null($keyboard)) { @@ -24,8 +25,7 @@ function handleKeyboard($keyboard, $handleMainMenu = false) { ] ] ]; - } - else { + } else { if (isset($keyboard['keyboard'])) { $keyboard = convertToInlineKeyboard($keyboard); } @@ -42,13 +42,13 @@ function handleKeyboard($keyboard, $handleMainMenu = false) { if (is_null($keyboard)) { return null; - } - else { + } else { return json_encode($keyboard); } } -function convertToInlineKeyboard($keyboard) { +function convertToInlineKeyboard($keyboard) +{ $inlineKeyboard = []; if (isset($keyboard['keyboard'])) { @@ -66,15 +66,15 @@ function convertToInlineKeyboard($keyboard) { $inlineKeyboard[] = $inlineRow; } } - } - else { + } else { return null; } return ['inline_keyboard' => $inlineKeyboard]; } -function array_str_contains(array $array, string|array $needle): bool { +function array_str_contains(array $array, string|array $needle): bool +{ if (is_array($needle)) { foreach ($needle as $n) { if (array_str_contains($array, $n)) { @@ -89,15 +89,15 @@ function array_str_contains(array $array, string|array $needle): bool { if (array_str_contains($item, $needle)) { return true; } - } - elseif (is_string($item) && stripos($item, $needle) !== false) { + } elseif (is_string($item) && stripos($item, $needle) !== false) { return true; } } return false; } -function sendMessage($chat_id, $text, $keyboard = null, $handleMainMenu = false) { +function sendMessage($chat_id, $text, $keyboard = null, $handleMainMenu = false) +{ $params = ['chat_id' => $chat_id, 'text' => $text, 'reply_markup' => handleKeyboard($keyboard, $handleMainMenu), 'parse_mode' => 'HTML']; global $update, $oneTimeEdit; @@ -111,60 +111,81 @@ function sendMessage($chat_id, $text, $keyboard = null, $handleMainMenu = false) return apiRequest('sendMessage', $params); } return $result; - } - else { + } else { return apiRequest('sendMessage', $params); } } -function forwardMessage($to_chat_id, $from_chat_id, $message_id) { +function forwardMessage($to_chat_id, $from_chat_id, $message_id) +{ $params = ['chat_id' => $to_chat_id, 'from_chat_id' => $from_chat_id, 'message_id' => $message_id]; return apiRequest('forwardMessage', $params); } -function sendPhoto($chat_id, $photo, $caption, $keyboard = null) { - $params = ['chat_id' => $chat_id, 'photo' => $photo, 'caption' => $caption, 'reply_markup' => handleKeyboard($keyboard), 'parse_mode' => 'HTML']; +function sendPhoto($chat_id, $photo, $caption, $keyboard = null) +{ + $params = ['chat_id' => $chat_id, 'caption' => $caption, 'reply_markup' => handleKeyboard($keyboard), 'parse_mode' => 'HTML']; + if (file_exists($photo)) { + $params['photo'] = new CURLFile($photo); + } else { + $params['photo'] = $photo; + } return apiRequest('sendPhoto', $params); } -function editMessageText($chat_id, $message_id, $text, $keyboard = null) { +function editMessageText($chat_id, $message_id, $text, $keyboard = null) +{ $params = ['chat_id' => $chat_id, 'message_id' => $message_id, 'text' => $text, 'reply_markup' => handleKeyboard($keyboard), 'parse_mode' => 'HTML']; global $oneTimeEdit; if (USER_INLINE_KEYBOARD && $oneTimeEdit) { $oneTimeEdit = false; return apiRequest('editMessageText', $params); - } - else { - + } else { + unset($params['message_id']); return apiRequest('sendMessage', $params); } } -function editMessageCaption($chat_id, $message_id, $caption, $keyboard = null) { +function editMessageCaption($chat_id, $message_id, $caption, $keyboard = null) +{ $params = ['chat_id' => $chat_id, 'message_id' => $message_id, 'caption' => $caption, 'reply_markup' => handleKeyboard($keyboard), 'parse_mode' => 'HTML']; return apiRequest('editMessageCaption', $params); } -function deleteMessage($chat_id, $message_id) { +function deleteMessage($chat_id, $message_id) +{ global $update, $oneTimeEdit; - if (USER_INLINE_KEYBOARD && !$oneTimeEdit && isset($update['callback_query']['message']['message_id']) && $update['callback_query']['message']['message_id'] == $message_id) return false; + if (USER_INLINE_KEYBOARD && !$oneTimeEdit && isset($update['callback_query']['message']['message_id']) && $update['callback_query']['message']['message_id'] == $message_id) + return false; $params = ['chat_id' => $chat_id, 'message_id' => $message_id]; return apiRequest('deleteMessage', $params); } -function apiRequest($method, $params = []) { +function apiRequest($method, $params = []) +{ global $apiRequest; $apiRequest = true; $url = 'https://api.telegram.org/bot' . BOT_TOKEN . '/' . $method; $ch = curl_init(); + + $hasFile = false; + foreach ($params as $key => $value) { + if ($value instanceof CURLFile) { + $hasFile = true; + break; + } + } + + $postFields = $hasFile ? $params : http_build_query($params); + curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query($params), + CURLOPT_POSTFIELDS => $postFields, CURLOPT_RETURNTRANSFER => true, ]); $response = curl_exec($ch); @@ -180,7 +201,8 @@ function apiRequest($method, $params = []) { // ===================================================================== // --- مدیریت کاربران --- -function getUserData($chat_id, $first_name = 'کاربر') { +function getUserData($chat_id, $first_name = 'کاربر') +{ pdo() ->prepare("UPDATE users SET last_seen_at = CURRENT_TIMESTAMP, reminder_sent = 0 WHERE chat_id = ?") ->execute([$chat_id]); @@ -191,7 +213,7 @@ function getUserData($chat_id, $first_name = 'کاربر') { if (!$user) { $settings = getSettings(); - $welcome_gift = (int)($settings['welcome_gift_balance'] ?? 0); + $welcome_gift = (int) ($settings['welcome_gift_balance'] ?? 0); $stmt = pdo()->prepare("INSERT INTO users (chat_id, first_name, balance, user_state) VALUES (?, ?, ?, 'main_menu')"); $stmt->execute([$chat_id, $first_name, $welcome_gift]); @@ -211,47 +233,166 @@ function getUserData($chat_id, $first_name = 'کاربر') { return $user; } -function updateUserData($chat_id, $state, $data = []) { +function updateUserData($chat_id, $state, $data = []) +{ $state_data_json = json_encode($data, JSON_UNESCAPED_UNICODE); $stmt = pdo()->prepare("UPDATE users SET user_state = ?, state_data = ? WHERE chat_id = ?"); $stmt->execute([$state, $state_data_json, $chat_id]); } -function updateUserBalance($chat_id, $amount, $operation = 'add') { +function updateUserBalance($chat_id, $amount, $operation = 'add') +{ if ($operation == 'add') { $stmt = pdo()->prepare("UPDATE users SET balance = balance + ? WHERE chat_id = ?"); - } - else { + } else { $stmt = pdo()->prepare("UPDATE users SET balance = balance - ? WHERE chat_id = ?"); } $stmt->execute([$amount, $chat_id]); } -function setUserStatus($chat_id, $status) { +function setUserStatus($chat_id, $status) +{ $stmt = pdo()->prepare("UPDATE users SET status = ? WHERE chat_id = ?"); $stmt->execute([$status, $chat_id]); } -function getAllUsers() { +function getAllUsers() +{ return pdo() ->query("SELECT chat_id FROM users WHERE status = 'active'") ->fetchAll(PDO::FETCH_COLUMN); } -function increaseAllUsersBalance($amount) { +function increaseAllUsersBalance($amount) +{ $stmt = pdo()->prepare("UPDATE users SET balance = balance + ? WHERE status = 'active'"); $stmt->execute([$amount]); return $stmt->rowCount(); } -function resetAllUsersTestCount() { +function resetAllUsersTestCount() +{ $stmt = pdo()->prepare("UPDATE users SET test_config_count = 0"); $stmt->execute(); return $stmt->rowCount(); } +/** + * Add volume (GB) to all active services - Updates both database AND panel servers + * Returns array with success and fail counts + */ +function addVolumeToAllServices($volume_gb) +{ + $bytes_to_add = $volume_gb * 1024 * 1024 * 1024; + + $all_services = pdo() + ->query("SELECT marzban_username, server_id FROM services") + ->fetchAll(PDO::FETCH_ASSOC); + + $success_count = 0; + $fail_count = 0; + + foreach ($all_services as $service) { + $username = $service['marzban_username']; + $server_id = $service['server_id']; + + if (!$server_id) { + $fail_count++; + continue; + } + + // Get current user data from panel + $current_user_data = getPanelUser($username, $server_id); + + if ($current_user_data && !isset($current_user_data['detail'])) { + $current_limit = $current_user_data['data_limit']; + + if ($current_limit > 0) { + $new_limit = $current_limit + $bytes_to_add; + + // Update on panel server via API + $result = modifyPanelUser($username, $server_id, ['data_limit' => $new_limit]); + + if ($result && !isset($result['detail'])) { + $success_count++; + } else { + $fail_count++; + } + } else { + // User has unlimited data + $fail_count++; + } + } else { + $fail_count++; + } + + // Small delay to avoid overwhelming the API + usleep(100000); // 0.1 second + } + + return ['success' => $success_count, 'fail' => $fail_count]; +} + +/** + * Add time (days) to all active services - Updates both database AND panel servers + * Returns array with success and fail counts + */ +function addTimeToAllServices($days) +{ + $seconds_to_add = $days * 86400; // 86400 seconds in a day + + $all_services = pdo() + ->query("SELECT marzban_username, server_id FROM services") + ->fetchAll(PDO::FETCH_ASSOC); + + $success_count = 0; + $fail_count = 0; + + foreach ($all_services as $service) { + $username = $service['marzban_username']; + $server_id = $service['server_id']; + + if (!$server_id) { + $fail_count++; + continue; + } + + // Get current user data from panel + $current_user_data = getPanelUser($username, $server_id); + + if ($current_user_data && !isset($current_user_data['detail'])) { + $current_expire = $current_user_data['expire'] ?? 0; + + if ($current_expire > 0) { + // If already expired, start from now. Otherwise add to current expiry + $new_expire = $current_expire < time() ? time() + $seconds_to_add : $current_expire + $seconds_to_add; + + // Update on panel server via API + $result = modifyPanelUser($username, $server_id, ['expire' => $new_expire]); + + if ($result && !isset($result['detail'])) { + $success_count++; + } else { + $fail_count++; + } + } else { + // User has unlimited time + $fail_count++; + } + } else { + $fail_count++; + } + + // Small delay to avoid overwhelming the API + usleep(100000); // 0.1 second + } + + return ['success' => $success_count, 'fail' => $fail_count]; +} + // --- مدیریت ادمین‌ها --- -function getAdmins() { +function getAdmins() +{ $stmt = pdo()->prepare("SELECT * FROM admins WHERE is_super_admin = 0"); $stmt->execute(); $admins_from_db = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -265,22 +406,26 @@ function getAdmins() { return $admins; } -function addAdmin($chat_id, $first_name) { +function addAdmin($chat_id, $first_name, $permissions = []) +{ $stmt = pdo()->prepare("INSERT INTO admins (chat_id, first_name, permissions, is_super_admin) VALUES (?, ?, ?, ?)"); - return $stmt->execute([$chat_id, $first_name, json_encode([]), 0]); + return $stmt->execute([$chat_id, $first_name, json_encode($permissions), 0]); } -function removeAdmin($chat_id) { +function removeAdmin($chat_id) +{ $stmt = pdo()->prepare("DELETE FROM admins WHERE chat_id = ? AND is_super_admin = 0"); return $stmt->execute([$chat_id]); } -function updateAdminPermissions($chat_id, $permissions) { +function updateAdminPermissions($chat_id, $permissions) +{ $stmt = pdo()->prepare("UPDATE admins SET permissions = ? WHERE chat_id = ?"); return $stmt->execute([json_encode($permissions), $chat_id]); } -function isUserAdmin($chat_id) { +function isUserAdmin($chat_id) +{ if ($chat_id == ADMIN_CHAT_ID) { return true; } @@ -289,7 +434,8 @@ function isUserAdmin($chat_id) { return $stmt->fetchColumn() > 0; } -function hasPermission($chat_id, $permission) { +function hasPermission($chat_id, $permission) +{ if ($chat_id == ADMIN_CHAT_ID) { return true; } @@ -306,7 +452,8 @@ function hasPermission($chat_id, $permission) { } // --- مدیریت تنظیمات --- -function getSettings() { +function getSettings() +{ $stmt = pdo()->query("SELECT * FROM settings"); $settings_from_db = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); @@ -342,7 +489,8 @@ function getSettings() { return $settings_from_db; } -function saveSettings($settings) { +function saveSettings($settings) +{ foreach ($settings as $key => $value) { if (is_array($value)) { $value = json_encode($value, JSON_UNESCAPED_UNICODE); @@ -353,7 +501,8 @@ function saveSettings($settings) { } // --- مدیریت دسته‌بندی‌ها، پلن‌ها و سرویس‌ها --- -function getCategories($only_active = false) { +function getCategories($only_active = false) +{ $sql = "SELECT * FROM categories"; if ($only_active) { $sql .= " WHERE status = 'active'"; @@ -363,31 +512,36 @@ function getCategories($only_active = false) { ->fetchAll(PDO::FETCH_ASSOC); } -function getPlans() { +function getPlans() +{ return pdo() ->query("SELECT * FROM plans WHERE is_test_plan = 0") ->fetchAll(PDO::FETCH_ASSOC); } -function getPlansForCategory($category_id) { +function getPlansForCategory($category_id) +{ $stmt = pdo()->prepare("SELECT * FROM plans WHERE category_id = ? AND status = 'active' AND is_test_plan = 0"); $stmt->execute([$category_id]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } -function getPlanById($plan_id) { +function getPlanById($plan_id) +{ $stmt = pdo()->prepare("SELECT * FROM plans WHERE id = ?"); $stmt->execute([$plan_id]); return $stmt->fetch(PDO::FETCH_ASSOC); } -function getTestPlan() { +function getTestPlan() +{ return pdo() ->query("SELECT * FROM plans WHERE is_test_plan = 1 AND status = 'active' LIMIT 1") ->fetch(PDO::FETCH_ASSOC); } -function getUserServices($chat_id) { +function getUserServices($chat_id) +{ $stmt = pdo()->prepare(" SELECT s.*, p.name as plan_name FROM services s @@ -399,12 +553,14 @@ function getUserServices($chat_id) { return $stmt->fetchAll(PDO::FETCH_ASSOC); } -function saveUserService($chat_id, $serviceData) { +function saveUserService($chat_id, $serviceData) +{ $stmt = pdo()->prepare("INSERT INTO services (owner_chat_id, server_id, marzban_username, custom_name, plan_id, sub_url, expire_timestamp, volume_gb) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([$chat_id, $serviceData['server_id'], $serviceData['username'], $serviceData['custom_name'], $serviceData['plan_id'], $serviceData['sub_url'], $serviceData['expire_timestamp'], $serviceData['volume_gb']]); } -function deleteUserService($chat_id, $username, $server_id) { +function deleteUserService($chat_id, $username, $server_id) +{ $stmt = pdo()->prepare("DELETE FROM services WHERE owner_chat_id = ? AND marzban_username = ? AND server_id = ?"); return $stmt->execute([$chat_id, $username, $server_id]); } @@ -413,7 +569,8 @@ function deleteUserService($chat_id, $username, $server_id) { // --- توابع کمکی و عمومی --- // ===================================================================== -function getPermissionMap() { +function getPermissionMap() +{ return [ 'manage_categories' => '🗂 مدیریت دسته‌بندی‌ها', 'manage_plans' => '📝 مدیریت پلن‌ها', @@ -431,7 +588,8 @@ function getPermissionMap() { ]; } -function checkJoinStatus($user_id) { +function checkJoinStatus($user_id) +{ $settings = getSettings(); $channel_id = $settings['join_channel_id']; if ($settings['join_channel_status'] !== 'on' || empty($channel_id)) { @@ -445,35 +603,45 @@ function checkJoinStatus($user_id) { return false; } -function generateQrCodeUrl($text) { +function generateQrCodeUrl($text) +{ return 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=' . urlencode($text); } -function formatBytes($bytes, $precision = 2) { +function formatBytes($bytes, $precision = 2) +{ if ($bytes <= 0) { return "0 GB"; } return round(floatval($bytes) / pow(1024, 3), $precision) . ' GB'; } -function calculateIncomeStats() { +function calculateIncomeStats() +{ $stats = [ - 'today' => - pdo() - ->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE DATE(s.purchase_date) = CURDATE()") - ->fetchColumn() ?? 0, - 'week' => - pdo() - ->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE s.purchase_date >= CURDATE() - INTERVAL 7 DAY") - ->fetchColumn() ?? 0, - 'month' => - pdo() - ->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE MONTH(s.purchase_date) = MONTH(CURDATE()) AND YEAR(s.purchase_date) = YEAR(CURDATE())") - ->fetchColumn() ?? 0, - 'year' => - pdo() - ->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE YEAR(s.purchase_date) = YEAR(CURDATE())") - ->fetchColumn() ?? 0, + 'today' => ( + pdo()->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE DATE(s.purchase_date) = CURDATE()")->fetchColumn() ?? 0 + ) + ( + pdo()->query("SELECT SUM(amount) FROM renewals WHERE DATE(renewal_date) = CURDATE()")->fetchColumn() ?? 0 + ), + + 'week' => ( + pdo()->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE s.purchase_date >= CURDATE() - INTERVAL 7 DAY")->fetchColumn() ?? 0 + ) + ( + pdo()->query("SELECT SUM(amount) FROM renewals WHERE renewal_date >= CURDATE() - INTERVAL 7 DAY")->fetchColumn() ?? 0 + ), + + 'month' => ( + pdo()->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE MONTH(s.purchase_date) = MONTH(CURDATE()) AND YEAR(s.purchase_date) = YEAR(CURDATE())")->fetchColumn() ?? 0 + ) + ( + pdo()->query("SELECT SUM(amount) FROM renewals WHERE MONTH(renewal_date) = MONTH(CURDATE()) AND YEAR(renewal_date) = YEAR(CURDATE())")->fetchColumn() ?? 0 + ), + + 'year' => ( + pdo()->query("SELECT SUM(p.price) FROM services s JOIN plans p ON s.plan_id = p.id WHERE YEAR(s.purchase_date) = YEAR(CURDATE())")->fetchColumn() ?? 0 + ) + ( + pdo()->query("SELECT SUM(amount) FROM renewals WHERE YEAR(renewal_date) = YEAR(CURDATE())")->fetchColumn() ?? 0 + ), ]; return $stats; } @@ -482,7 +650,8 @@ function calculateIncomeStats() { // --- توابع نمایش منوها --- // ===================================================================== -function generateGuideList($chat_id) { +function generateGuideList($chat_id) +{ $stmt = pdo()->query("SELECT id, button_name, status FROM guides ORDER BY id DESC"); $guides = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -506,7 +675,8 @@ function generateGuideList($chat_id) { } } -function showGuideSelectionMenu($chat_id) { +function showGuideSelectionMenu($chat_id) +{ $stmt = pdo()->query("SELECT id, button_name FROM guides WHERE status = 'active' ORDER BY id ASC"); $guides = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -524,7 +694,8 @@ function showGuideSelectionMenu($chat_id) { sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); } -function generateDiscountCodeList($chat_id) { +function generateDiscountCodeList($chat_id) +{ $stmt = pdo()->query("SELECT * FROM discount_codes ORDER BY id DESC"); $codes = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -553,7 +724,8 @@ function generateDiscountCodeList($chat_id) { } } -function generateCategoryList($chat_id) { +function generateCategoryList($chat_id) +{ $categories = getCategories(); if (empty($categories)) { sendMessage($chat_id, "هیچ دسته‌بندی‌ای یافت نشد."); @@ -574,7 +746,8 @@ function generateCategoryList($chat_id) { } } -function generatePlanList($chat_id) { +function generatePlanList($chat_id) +{ $plans = pdo() ->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") ->fetchAll(PDO::FETCH_ASSOC); @@ -597,19 +770,18 @@ function generatePlanList($chat_id) { $plan_info = ""; if ($plan['is_test_plan']) { $plan_info .= "🧪 (پلن تست) {$plan['name']}\n"; - } - else { + } else { $plan_info .= "{$status_icon} {$plan['name']}\n"; } $plan_info .= "▫️ سرور: {$server_name}\n"; - + if ($plan['server_type'] === 'sanaei' && !empty($plan['inbound_id'])) { $plan_info .= "▫️ اینباند: {$plan['inbound_id']}\n"; } elseif ($plan['server_type'] === 'marzneshin' && !empty($plan['marzneshin_service_id'])) { $plan_info .= "▫️ سرویس: {$plan['marzneshin_service_id']}\n"; } - + $plan_info .= "▫️ دسته‌بندی: {$cat_name}\n" . "▫️ قیمت: " . number_format($plan['price']) . " تومان\n" . "▫️ حجم: {$plan['volume_gb']} گیگابایت | " . "مدت: {$plan['duration_days']} روز\n"; if ($plan['purchase_limit'] > 0) { @@ -622,8 +794,7 @@ function generatePlanList($chat_id) { if ($plan['is_test_plan']) { $keyboard_buttons[] = [['text' => '↔️ تبدیل به پلن عادی', 'callback_data' => "make_plan_normal_{$plan_id}"]]; - } - else { + } else { $keyboard_buttons[] = [['text' => '🧪 تنظیم به عنوان پلن تست', 'callback_data' => "set_as_test_plan_{$plan_id}"]]; } @@ -635,7 +806,8 @@ function generatePlanList($chat_id) { } } -function showServersForCategory($chat_id, $category_id) { +function showServersForCategory($chat_id, $category_id) +{ $category_stmt = pdo()->prepare("SELECT name FROM categories WHERE id = ?"); $category_stmt->execute([$category_id]); $category_name = $category_stmt->fetchColumn(); @@ -662,14 +834,15 @@ function showServersForCategory($chat_id, $category_id) { $message = "🛍️ دسته‌بندی «{$category_name}»\n\nلطفاً سرور (لوکیشن) مورد نظر خود را انتخاب کنید:"; $keyboard_buttons = []; foreach ($servers as $server) { - + $keyboard_buttons[] = [['text' => "🖥 {$server['name']}", 'callback_data' => "show_plans_cat_{$category_id}_srv_{$server['id']}"]]; } $keyboard_buttons[] = [['text' => '◀️ بازگشت به دسته‌بندی‌ها', 'callback_data' => 'back_to_categories']]; sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); } -function showAdminManagementMenu($chat_id) { +function showAdminManagementMenu($chat_id) +{ $admins = getAdmins(); $message = "👨‍💼 مدیریت ادمین‌ها\n\nدر این بخش می‌توانید ادمین‌های ربات و دسترسی‌های آن‌ها را مدیریت کنید. (حداکثر ۱۰ ادمین)"; $keyboard_buttons = []; @@ -690,7 +863,8 @@ function showAdminManagementMenu($chat_id) { sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); } -function showPermissionEditor($chat_id, $message_id, $target_admin_id) { +function showPermissionEditor($chat_id, $message_id, $target_admin_id) +{ $admins = getAdmins(); $target_admin = $admins[$target_admin_id] ?? null; if (!$target_admin) { @@ -725,7 +899,8 @@ function showPermissionEditor($chat_id, $message_id, $target_admin_id) { editMessageText($chat_id, $message_id, $message, ['inline_keyboard' => $keyboard_buttons]); } -function handleMainMenu($chat_id, $first_name, $is_start_command = false) { +function handleMainMenu($chat_id, $first_name, $is_start_command = false) +{ $isAnAdmin = isUserAdmin($chat_id); $user_data = getUserData($chat_id, $first_name); @@ -733,8 +908,7 @@ function handleMainMenu($chat_id, $first_name, $is_start_command = false) { if ($is_start_command) { $message = "سلام $first_name عزیز!\nبه ربات فروش کانفیگ خوش آمدید. 🌹"; - } - else { + } else { $message = "به منوی اصلی بازگشتید. لطفا گزینه مورد نظر را انتخاب کنید."; } @@ -750,12 +924,14 @@ function handleMainMenu($chat_id, $first_name, $is_start_command = false) { $keyboard_buttons[] = [['text' => '📚 راهنما']]; } + // Add User Panel Button + $keyboard_buttons[] = [['text' => '📱 پنل کاربری']]; + if ($isAnAdmin) { if ($admin_view_mode === 'admin') { if ($is_start_command) { $message = "ادمین عزیز، به پنل مدیریت خوش آمدید."; - } - else { + } else { $message = "به پنل مدیریت بازگشتید."; } $admin_keyboard = []; @@ -778,7 +954,7 @@ function handleMainMenu($chat_id, $first_name, $is_start_command = false) { } if (hasPermission($chat_id, 'manage_payment')) { $rows[3][] = ['text' => '💳 مدیریت پرداخت']; - $rows[3][] = ['text' => '💳 مدیریت درگاه پرداخت']; + $rows[3][] = ['text' => '💳 مدیریت درگاه پرداخت']; } if (hasPermission($chat_id, 'manage_marzban')) { $rows[4][] = ['text' => '🌐 مدیریت سرورها']; @@ -810,8 +986,7 @@ function handleMainMenu($chat_id, $first_name, $is_start_command = false) { } $admin_keyboard[] = [['text' => '↩️ بازگشت به منوی کاربری']]; $keyboard_buttons = $admin_keyboard; - } - else { + } else { $keyboard_buttons[] = [['text' => '👑 ورود به پنل مدیریت']]; } } @@ -830,8 +1005,7 @@ function handleMainMenu($chat_id, $first_name, $is_start_command = false) { 'text' => '🏠', 'reply_markup' => json_encode(['remove_keyboard' => true]) ]), true)['result']['message_id']; - } - elseif (!USER_INLINE_KEYBOARD && $inline_keyboard == 1) { + } elseif (!USER_INLINE_KEYBOARD && $inline_keyboard == 1) { $stmt = pdo()->prepare("UPDATE users SET inline_keyboard = '0' WHERE chat_id = ?"); $stmt->execute([$chat_id]); } @@ -847,7 +1021,8 @@ function handleMainMenu($chat_id, $first_name, $is_start_command = false) { } -function showVerificationManagementMenu($chat_id) { +function showVerificationManagementMenu($chat_id) +{ $settings = getSettings(); $current_method = $settings['verification_method']; $iran_only_icon = $settings['verification_iran_only'] == 'on' ? '🇮🇷' : '🌎'; @@ -855,8 +1030,7 @@ function showVerificationManagementMenu($chat_id) { $method_text = 'غیرفعال'; if ($current_method == 'phone') { $method_text = 'شماره تلفن'; - } - elseif ($current_method == 'button') { + } elseif ($current_method == 'button') { $method_text = 'دکمه شیشه‌ای'; } @@ -882,8 +1056,7 @@ function showVerificationManagementMenu($chat_id) { $message_id = $update['callback_query']['message']['message_id'] ?? null; if ($message_id) { editMessageText($chat_id, $message_id, $message, $keyboard); - } - else { + } else { sendMessage($chat_id, $message, $keyboard); } } @@ -892,7 +1065,8 @@ function showVerificationManagementMenu($chat_id) { // --- توابع انتزاعی برای مدیریت پنل‌ها --- // ===================================================================== -function getPanelUser($username, $server_id) { +function getPanelUser($username, $server_id) +{ $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $type = $stmt->fetchColumn(); @@ -909,7 +1083,8 @@ function getPanelUser($username, $server_id) { } } -function createPanelUser($plan, $chat_id, $plan_id) { +function createPanelUser($plan, $chat_id, $plan_id) +{ $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?"); $stmt->execute([$plan['server_id']]); $type = $stmt->fetchColumn(); @@ -926,7 +1101,8 @@ function createPanelUser($plan, $chat_id, $plan_id) { } } -function deletePanelUser($username, $server_id) { +function deletePanelUser($username, $server_id) +{ $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $type = $stmt->fetchColumn(); @@ -943,7 +1119,8 @@ function deletePanelUser($username, $server_id) { } } -function modifyPanelUser($username, $server_id, $data) { +function modifyPanelUser($username, $server_id, $data) +{ $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $type = $stmt->fetchColumn(); @@ -960,6 +1137,24 @@ function modifyPanelUser($username, $server_id, $data) { } } +function resetPanelUserUsage($username, $server_id) +{ + $stmt = pdo()->prepare("SELECT type FROM servers WHERE id = ?"); + $stmt->execute([$server_id]); + $type = $stmt->fetchColumn(); + + switch ($type) { + case 'marzban': + return resetMarzbanUserUsage($username, $server_id); + case 'sanaei': + return resetSanaeiUserUsage($username, $server_id); + case 'marzneshin': + return resetMarzneshinUserUsage($username, $server_id); + default: + return false; + } +} + function showPlanEditor($chat_id, $message_id, $plan_id, $prompt = null) { $plan = getPlanById($plan_id); @@ -994,32 +1189,34 @@ function showPlanEditor($chat_id, $message_id, $plan_id, $prompt = null) editMessageText($chat_id, $message_id, $message_text, $keyboard); } -function fetchAndParseSubscriptionUrl($sub_url, $server_id) { +function fetchAndParseSubscriptionUrl($sub_url, $server_id) +{ if (empty($sub_url)) { return []; } - + $stmt = pdo()->prepare("SELECT url, sub_host FROM servers WHERE id = ?"); $stmt->execute([$server_id]); $server_info = $stmt->fetch(); - if (!$server_info) return []; - - $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); - + if (!$server_info) + return []; + + $base_sub_url = !empty($server_info['sub_host']) ? rtrim($server_info['sub_host'], '/') : rtrim($server_info['url'], '/'); + $stmt_type = pdo()->prepare("SELECT type FROM servers WHERE id = ?"); $stmt_type->execute([$server_id]); $server_type = $stmt_type->fetchColumn(); $sub_path = ''; - + if ($server_type === 'marzban' || $server_type === 'sanaei') { $sub_path_raw = strstr($sub_url, '/sub/'); if ($sub_path_raw !== false) { $sub_path = $sub_path_raw; } } - - + + if (empty($sub_path)) { $sub_path = parse_url($sub_url, PHP_URL_PATH); } @@ -1049,13 +1246,14 @@ function fetchAndParseSubscriptionUrl($sub_url, $server_id) { if ($decoded_links === false) { $decoded_links = $response_body; } - + $links_array = preg_split("/\r\n|\n|\r/", trim($decoded_links)); - + return array_filter($links_array); } -function showPlansForCategoryAndServer($chat_id, $category_id, $server_id) { +function showPlansForCategoryAndServer($chat_id, $category_id, $server_id) +{ // دریافت نام دسته بندی و سرور برای نمایش در پیام $category_name = pdo()->prepare("SELECT name FROM categories WHERE id = ?")->execute([$category_id]) ? pdo()->lastInsertId() : 'نامشخص'; $server_name = pdo()->prepare("SELECT name FROM servers WHERE id = ?")->execute([$server_id]) ? pdo()->lastInsertId() : 'نامشخص'; @@ -1074,17 +1272,200 @@ function showPlansForCategoryAndServer($chat_id, $category_id, $server_id) { $message = "🛍️ پلن‌های سرور «{$server_name}»\nموجودی شما: " . number_format($user_balance) . " تومان\n\nلطفا پلن مورد نظر خود را انتخاب کنید:"; $keyboard_buttons = []; foreach ($active_plans as $plan) { - $button_text = "{$plan['name']} | {$plan['volume_gb']}GB | " . number_format($plan['price']) . " تومان"; + $button_text = "{$plan['name']} | " . number_format($plan['price']) . " تومان | {$plan['volume_gb']} GB"; $keyboard_buttons[] = [['text' => $button_text, 'callback_data' => "buy_plan_{$plan['id']}"]]; } // فرمت callback جدید برای کد تخفیف: apply_discount_code_{cat_ID}_{srv_ID} $keyboard_buttons[] = [['text' => '🎁 اعمال کد تخفیف', 'callback_data' => "apply_discount_code_{$category_id}_{$server_id}"]]; // دکمه بازگشت به لیست سرورها برای همان دسته بندی - $keyboard_buttons[] = [['text' => '◀️ بازگشت به انتخاب سرور', 'callback_data' => 'cat_' . $category_id]]; + // Check if only one server exists to adjust back button + $stmt_count = pdo()->prepare(" + SELECT COUNT(DISTINCT s.id) + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt_count->execute([$category_id]); + $server_count = $stmt_count->fetchColumn(); + + if ($server_count == 1) { + $keyboard_buttons[] = [['text' => '◀️ بازگشت به دسته‌بندی‌ها', 'callback_data' => 'back_to_categories']]; + } else { + $keyboard_buttons[] = [['text' => '◀️ بازگشت به انتخاب سرور', 'callback_data' => 'cat_' . $category_id]]; + } + sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); +} + +// ===================================================================== +// --- توابع جدید تمدید سرویس بر اساس پلن --- +// ===================================================================== + +function applyPlanRenewal($chat_id, $username, $plan_id, $final_price) +{ + $plan = getPlanById($plan_id); + if (!$plan) { + return ['success' => false, 'message' => '❌ پلن یافت نشد.']; + } + + // دریافت اطلاعات سرویس از دیتابیس + $stmt = pdo()->prepare("SELECT server_id FROM services WHERE owner_chat_id = ? AND marzban_username = ?"); + $stmt->execute([$chat_id, $username]); + $server_id = $stmt->fetchColumn(); + + if (!$server_id) { + return ['success' => false, 'message' => 'سرویس در دیتابیس ربات یافت نشد.']; + } + + // دریافت اطلاعات فعلی از پنل + $current_user_data = getPanelUser($username, $server_id); + if (!$current_user_data || isset($current_user_data['detail'])) { + return ['success' => false, 'message' => 'اطلاعات سرویس از پنل دریافت نشد.']; + } + + $update_data = []; + + // محاسبه زمان جدید: اگر سرویس فعال است، به زمان فعلی اضافه شود + $days_to_add = $plan['duration_days']; + $seconds_to_add = $days_to_add * 86400; + $current_expire = $current_user_data['expire'] ?? 0; + + // اگر سرویس منقضی نشده و زمان دارد، به آن اضافه کن + if ($current_expire > 0 && $current_expire > time()) { + $new_expire = $current_expire + $seconds_to_add; + } else { + // سرویس منقضی شده، از همین الان شروع کن + $new_expire = time() + $seconds_to_add; + } + $update_data['expire'] = $new_expire; + + // حجم جدید: حجم پلن جایگزین می‌شود + $new_volume_bytes = $plan['volume_gb'] * 1024 * 1024 * 1024; + $update_data['data_limit'] = $new_volume_bytes; + + // اعمال تغییرات در پنل (زمان و حجم) + $result = modifyPanelUser($username, $server_id, $update_data); + + if ($result && !isset($result['detail'])) { + // ریست کردن حجم مصرفی از طریق endpoint مخصوص + $reset_result = resetPanelUserUsage($username, $server_id); + + // بروزرسانی دیتابیس محلی + pdo()->prepare("UPDATE services SET expire_timestamp = ?, volume_gb = ? WHERE marzban_username = ? AND server_id = ?") + ->execute([$new_expire, $plan['volume_gb'], $username, $server_id]); + + // ثبت تمدید در جدول renewals برای محاسبه درآمد (commented out - optional) + // $stmt_renewal = pdo()->prepare("INSERT INTO renewals (user_id, service_username, plan_id, amount, renewal_date) VALUES (?, ?, ?, ?, NOW())"); + // $stmt_renewal->execute([$chat_id, $username, $plan_id, $final_price]); + + // کسر موجودی + updateUserBalance($chat_id, $final_price, 'deduct'); + + $user_data = getUserData($chat_id); + $new_balance = $user_data['balance']; + + $success_msg = "✅ سرویس شما با موفقیت تمدید شد.\n\n" . + "📦 پلن: {$plan['name']}\n" . + "⏰ زمان اعتبار: {$days_to_add} روز\n" . + "📊 حجم جدید: {$plan['volume_gb']} گیگابایت\n\n" . + "💰 مبلغ " . number_format($final_price) . " تومان از حساب شما کسر گردید.\n" . + "موجودی جدید: " . number_format($new_balance) . " تومان."; + + // نوتیفیکیشن برای ادمین + $admin_notification = "✅ تمدید سرویس\n\n" . + "👤 کاربر: $chat_id\n" . + "🔧 سرویس: $username\n" . + "📦 پلن: {$plan['name']}\n" . + "💳 مبلغ: " . number_format($final_price) . " تومان"; + + sendMessage(ADMIN_CHAT_ID, $admin_notification); + + return ['success' => true, 'message' => $success_msg]; + } + + return ['success' => false, 'message' => 'خطا در ارتباط با پنل برای اعمال تغییرات.']; +} + +function showServersForCategoryRenewal($chat_id, $category_id, $renewal_username) +{ + // مشابه showServersForCategory اما با callback_data متفاوت + $category_stmt = pdo()->prepare("SELECT name FROM categories WHERE id = ?"); + $category_stmt->execute([$category_id]); + $category_name = $category_stmt->fetchColumn(); + + if (!$category_name) { + sendMessage($chat_id, "خطا: دسته‌بندی یافت نشد."); + return; + } + + $stmt = pdo()->prepare(" + SELECT DISTINCT s.id, s.name + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt->execute([$category_id]); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($servers)) { + sendMessage($chat_id, "متاسفانه در حال حاضر هیچ سروری در این دسته‌بندی پلن فعال ندارد."); + return; + } + + $message = "🔄 تمدید سرویس - دسته‌بندی «{$category_name}»\n\nلطفاً سرور (لوکیشن) مورد نظر خود را انتخاب کنید:"; + $keyboard_buttons = []; + foreach ($servers as $server) { + $keyboard_buttons[] = [['text' => "🖥 {$server['name']}", 'callback_data' => "renewal_show_plans_cat_{$category_id}_srv_{$server['id']}"]]; + } + $keyboard_buttons[] = [['text' => '◀️ بازگشت', 'callback_data' => "service_details_{$renewal_username}"]]; sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); } -function applyRenewal($chat_id, $username, $days_to_add, $gb_to_add) { +function showPlansForCategoryAndServerRenewal($chat_id, $category_id, $server_id, $renewal_username) +{ + // مشابه showPlansForCategoryAndServer اما با callback_data متفاوت + $stmt = pdo()->prepare("SELECT * FROM plans WHERE category_id = ? AND server_id = ? AND status = 'active' AND is_test_plan = 0"); + $stmt->execute([$category_id, $server_id]); + $plans = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($plans)) { + sendMessage($chat_id, "هیچ پلن فعالی در این سرور و دسته‌بندی یافت نشد."); + return; + } + + $user_balance = getUserData($chat_id)['balance'] ?? 0; + $message = "🔄 تمدید سرویس - انتخاب پلن\n\nموجودی شما: " . number_format($user_balance) . " تومان\n\nلطفاً پلن مورد نظر خود را انتخاب کنید:"; + $keyboard_buttons = []; + + foreach ($plans as $plan) { + $price_formatted = number_format($plan['price']); + $button_text = "📦 {$plan['name']} - {$price_formatted} تومان"; + $keyboard_buttons[] = [['text' => $button_text, 'callback_data' => "renewal_buy_plan_{$plan['id']}"]]; + } + + // Check if only one server exists to adjust back button + $stmt_count = pdo()->prepare(" + SELECT COUNT(DISTINCT s.id) + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt_count->execute([$category_id]); + $server_count = $stmt_count->fetchColumn(); + + if ($server_count == 1) { + $keyboard_buttons[] = [['text' => '◀️ بازگشت', 'callback_data' => "renew_service_{$renewal_username}"]]; + } else { + $keyboard_buttons[] = [['text' => '◀️ بازگشت', 'callback_data' => "renewal_cat_{$category_id}"]]; + } + sendMessage($chat_id, $message, ['inline_keyboard' => $keyboard_buttons]); +} + +// ===================================================================== + + + +function applyRenewal($chat_id, $username, $days_to_add, $gb_to_add) +{ $stmt = pdo()->prepare("SELECT server_id FROM services WHERE owner_chat_id = ? AND marzban_username = ?"); $stmt->execute([$chat_id, $username]); $server_id = $stmt->fetchColumn(); @@ -1120,19 +1501,19 @@ function applyRenewal($chat_id, $username, $days_to_add, $gb_to_add) { } if (empty($update_data)) { - return ['success' => false, 'message' => 'هیچ تغییری برای اعمال وجود نداشت.']; + return ['success' => false, 'message' => 'هیچ تغییری برای اعمال وجود نداشت.']; } $result = modifyPanelUser($username, $server_id, $update_data); - + // بروزرسانی دیتابیس محلی if ($result && !isset($result['detail'])) { - if(isset($update_data['expire'])){ - pdo()->prepare("UPDATE services SET expire_timestamp = ? WHERE marzban_username = ? AND server_id = ?")->execute([$update_data['expire'], $username, $server_id]); + if (isset($update_data['expire'])) { + pdo()->prepare("UPDATE services SET expire_timestamp = ? WHERE marzban_username = ? AND server_id = ?")->execute([$update_data['expire'], $username, $server_id]); } - if(isset($update_data['data_limit'])){ - $new_volume_gb = ($update_data['data_limit'] / (1024*1024*1024)); - pdo()->prepare("UPDATE services SET volume_gb = ? WHERE marzban_username = ? AND server_id = ?")->execute([$new_volume_gb, $username, $server_id]); + if (isset($update_data['data_limit'])) { + $new_volume_gb = ($update_data['data_limit'] / (1024 * 1024 * 1024)); + pdo()->prepare("UPDATE services SET volume_gb = ? WHERE marzban_username = ? AND server_id = ?")->execute([$new_volume_gb, $username, $server_id]); } return ['success' => true]; } @@ -1140,19 +1521,20 @@ function applyRenewal($chat_id, $username, $days_to_add, $gb_to_add) { return ['success' => false, 'message' => 'خطا در ارتباط با پنل برای اعمال تغییرات.']; } -function showRenewalManagementMenu($chat_id, $message_id = null) { +function showRenewalManagementMenu($chat_id, $message_id = null) +{ $settings = getSettings(); $status_icon = ($settings['renewal_status'] ?? 'off') == 'on' ? '✅' : '❌'; + $status_text = $status_icon == '✅' ? 'فعال' : 'غیرفعال'; + $message = "🔄 مدیریت تمدید سرویس\n\n" . - "▫️ وضعیت کلی: " . ($status_icon == '✅' ? 'فعال' : 'غیرفعال') . "\n" . - "▫️ هزینه هر روز تمدید: " . number_format($settings['renewal_price_per_day'] ?? 1000) . " تومان\n" . - "▫️ هزینه هر گیگابایت تمدید: " . number_format($settings['renewal_price_per_gb'] ?? 2000) . " تومان"; + "▫️ وضعیت کلی: " . $status_text . "\n\n" . + "📌 توجه: تمدید سرویس بر اساس انتخاب پلن انجام می‌شود.\n" . + "کاربران برای تمدید سرویس خود، یک پلن را انتخاب می‌کنند و قیمت آن پلن برای تمدید محاسبه می‌شود."; $keyboard = [ 'inline_keyboard' => [ [['text' => $status_icon . ' فعال/غیرفعال کردن', 'callback_data' => 'toggle_renewal_status']], - [['text' => '💰 تنظیم قیمت روز', 'callback_data' => 'set_renewal_price_day']], - [['text' => '📊 تنظیم قیمت حجم', 'callback_data' => 'set_renewal_price_gb']], [['text' => '◀️ بازگشت به پنل', 'callback_data' => 'back_to_admin_panel']], ] ]; @@ -1164,7 +1546,8 @@ function showRenewalManagementMenu($chat_id, $message_id = null) { } } -function showMarzbanProtocolEditor($chat_id, $message_id, $server_id) { +function showMarzbanProtocolEditor($chat_id, $message_id, $server_id) +{ $stmt_server = pdo()->prepare("SELECT name, marzban_protocols FROM servers WHERE id = ?"); $stmt_server->execute([$server_id]); $server = $stmt_server->fetch(); @@ -1175,13 +1558,14 @@ function showMarzbanProtocolEditor($chat_id, $message_id, $server_id) { } $all_protocols = ['vless', 'vmess', 'trojan', 'shadowsocks']; - - $enabled_protocols = $server['marzban_protocols'] ? json_decode($server['marzban_protocols'], true) : ['vless']; - if (!is_array($enabled_protocols)) $enabled_protocols = ['vless']; - + + $enabled_protocols = $server['marzban_protocols'] ? json_decode($server['marzban_protocols'], true) : ['vless']; + if (!is_array($enabled_protocols)) + $enabled_protocols = ['vless']; + $message = "⚙️ تنظیم پروتکل‌های سرور: {$server['name']}\n\n"; $message .= "پروتکل‌هایی را که می‌خواهید برای کاربران جدید در این سرور ایجاد شوند، انتخاب کنید."; - + $keyboard_buttons = []; $row = []; foreach ($all_protocols as $protocol) { @@ -1195,17 +1579,18 @@ function showMarzbanProtocolEditor($chat_id, $message_id, $server_id) { if (!empty($row)) { $keyboard_buttons[] = $row; } - + $keyboard_buttons[] = [['text' => '◀️ بازگشت به سرور', 'callback_data' => "view_server_{$server_id}"]]; - + editMessageText($chat_id, $message_id, $message, ['inline_keyboard' => $keyboard_buttons]); } -function createZarinpalLink($chat_id, $amount, $description, $metadata = []) { +function createZarinpalLink($chat_id, $amount, $description, $metadata = []) +{ $settings = getSettings(); $merchant_id = $settings['zarinpal_merchant_id']; $script_url = 'https://' . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['PHP_SELF']), '/') . '/verify_payment.php'; - + $data = [ "merchant_id" => $merchant_id, "amount" => $amount * 10, // تبدیل تومان به ریال @@ -1221,18 +1606,18 @@ function createZarinpalLink($chat_id, $amount, $description, $metadata = []) { curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Content-Length: ' . strlen($jsonData)]); - + $result = curl_exec($ch); curl_close($ch); $result = json_decode($result, true); - + if (empty($result['errors'])) { $authority = $result['data']['authority']; - + // ثبت تراکنش در دیتابیس $stmt = pdo()->prepare("INSERT INTO transactions (user_id, amount, authority, description, metadata) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([$chat_id, $amount, $authority, $description, json_encode($metadata)]); - + $payment_url = 'https://www.zarinpal.com/pg/StartPay/' . $authority; return ['success' => true, 'url' => $payment_url]; } else { @@ -1241,13 +1626,14 @@ function createZarinpalLink($chat_id, $amount, $description, $metadata = []) { } } -function completePurchase($user_id, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied) { +function completePurchase($user_id, $plan_id, $custom_name, $final_price, $discount_code, $discount_object, $discount_applied) +{ $plan = getPlanById($plan_id); $user_data = getUserData($user_id); $first_name = $user_data['first_name']; // ساخت نام کاربری کامل و یکتا برای پنل - $plan['full_username'] = preg_replace('/[^a-zA-Z0-9_.]/', '', $custom_name) . '_user' . $user_id . '_' . time(); + $plan['full_username'] = $user_id . '_' . rand(10, 99); $panel_user_data = createPanelUser($plan, $user_id, $plan_id); @@ -1265,9 +1651,9 @@ function completePurchase($user_id, $plan_id, $custom_name, $final_price, $disco if ($discount_applied && $discount_object) { pdo()->prepare("UPDATE discount_codes SET usage_count = usage_count + 1 WHERE id = ?")->execute([$discount_object['id']]); } - + $expire_timestamp = $panel_user_data['expire'] ?? (isset($panel_user_data['expire_date']) ? strtotime($panel_user_data['expire_date']) : (time() + $plan['duration_days'] * 86400)); - + saveUserService($user_id, [ 'server_id' => $plan['server_id'], 'username' => $panel_user_data['username'], @@ -1277,7 +1663,7 @@ function completePurchase($user_id, $plan_id, $custom_name, $final_price, $disco 'expire_timestamp' => $expire_timestamp, 'volume_gb' => $plan['volume_gb'], ]); - + $new_balance = $user_data['balance'] - $final_price; $sub_link = $panel_user_data['subscription_url']; $qr_code_url = generateQrCodeUrl($sub_link); @@ -1292,12 +1678,12 @@ function completePurchase($user_id, $plan_id, $custom_name, $final_price, $disco if ($plan['show_sub_link']) { $caption .= "🔗 لینک اشتراک (Subscription):\n" . htmlspecialchars($sub_link) . "\n\n"; } - + $caption .= "💰 موجودی جدید شما: " . number_format($new_balance) . " تومان"; $chat_info_response = apiRequest('getChat', ['chat_id' => $user_id]); $chat_info = json_decode($chat_info_response, true); - + $profile_link_html = "👤 کاربر: " . htmlspecialchars($first_name) . " ($user_id)\n"; $admin_notification = "✅ خرید جدید\n\n"; @@ -1312,7 +1698,7 @@ function completePurchase($user_id, $plan_id, $custom_name, $final_price, $disco } else { $admin_notification .= "💳 مبلغ پرداخت شده: " . number_format($final_price) . " تومان"; } - + $keyboard_buttons = []; if ($plan['show_conf_links'] && !empty($panel_user_data['links'])) { $keyboard_buttons[] = [['text' => '📋 دریافت کانفیگ‌ها', 'callback_data' => "get_configs_{$panel_user_data['username']}"]]; @@ -1326,9 +1712,22 @@ function completePurchase($user_id, $plan_id, $custom_name, $final_price, $disco 'admin_notification' => $admin_notification, ]; } - + return [ 'success' => false, 'error_message' => "❌ متاسفانه در ایجاد سرویس شما مشکلی پیش آمد. لطفا با پشتیبانی تماس بگیرید. مبلغی از حساب شما کسر نشده است." ]; +} + +function getServers() +{ + $stmt = pdo()->query("SELECT * FROM servers ORDER BY id DESC"); + return $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +function getServerById($id) +{ + $stmt = pdo()->prepare("SELECT * FROM servers WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); } \ No newline at end of file diff --git a/src/install.php b/src/install.php index ea38861..116633f 100644 --- a/src/install.php +++ b/src/install.php @@ -14,20 +14,26 @@ // --- متغیرهای اولیه --- $configFile = __DIR__ . '/includes/config.php'; $botFileUrl = 'https://' . $_SERVER['HTTP_HOST'] . rtrim(dirname($_SERVER['PHP_SELF']), '/') . '/bot.php'; +$defaultDomainUrl = 'https://' . $_SERVER['HTTP_HOST']; -$step = isset($_POST['step']) ? (int)$_POST['step'] : 1; +$step = isset($_POST['step']) ? (int) $_POST['step'] : 1; $errors = []; $successMessages = []; // --- داده‌های فرم --- $bot_token = trim($_POST['bot_token'] ?? ''); $admin_id = trim($_POST['admin_id'] ?? ''); +$domain_url = trim($_POST['domain_url'] ?? ''); +$web_username = trim($_POST['web_username'] ?? ''); +$web_password = trim($_POST['web_password'] ?? ''); -function generateRandomString(int $length = 32): string { +function generateRandomString(int $length = 32): string +{ return bin2hex(random_bytes($length / 2)); } -function getDbBaseSchemaSQL(): string { +function getDbBaseSchemaSQL(): string +{ return " CREATE TABLE IF NOT EXISTS `users` ( `chat_id` BIGINT NOT NULL, `first_name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `user_state` VARCHAR(255) DEFAULT 'main_menu', `state_data` TEXT, `status` VARCHAR(20) NOT NULL DEFAULT 'active', `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`chat_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `admins` ( `chat_id` BIGINT NOT NULL PRIMARY KEY, `first_name` VARCHAR(255), `permissions` TEXT, `is_super_admin` TINYINT(1) NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -70,7 +76,8 @@ function getDbBaseSchemaSQL(): string { "; } -function columnExists(PDO $pdo, string $tableName, string $columnName): bool { +function columnExists(PDO $pdo, string $tableName, string $columnName): bool +{ try { $stmt = $pdo->prepare("SHOW COLUMNS FROM `$tableName` LIKE ?"); $stmt->execute([$columnName]); @@ -80,7 +87,8 @@ function columnExists(PDO $pdo, string $tableName, string $columnName): bool { } } -function runDbUpgrades(PDO $pdo): array { +function runDbUpgrades(PDO $pdo): array +{ $messages = []; if (columnExists($pdo, 'users', 'state') && !columnExists($pdo, 'users', 'user_state')) { @@ -109,7 +117,7 @@ function runDbUpgrades(PDO $pdo): array { $pdo->exec("ALTER TABLE `services` ADD `sanaei_uuid` VARCHAR(255) NULL DEFAULT NULL AFTER `sanaei_inbound_id`;"); $messages[] = "✅ ستون `sanaei_uuid` برای پنل سنایی به جدول `services` اضافه شد."; } - + // --- ارتقاهای مربوط به اعلان‌ها و ردیابی کاربران --- if (!columnExists($pdo, 'users', 'last_seen_at')) { $pdo->exec("ALTER TABLE `users` ADD `last_seen_at` TIMESTAMP NULL DEFAULT NULL AFTER `status`;"); @@ -173,24 +181,51 @@ function runDbUpgrades(PDO $pdo): array { // --- مدیریت منطق مراحل --- if ($step === 2) { - if (empty($bot_token)) $errors[] = 'توکن ربات الزامی است.'; - if (empty($admin_id) || !is_numeric($admin_id)) $errors[] = 'آیدی عددی ادمین الزامی و باید عدد باشد.'; - if (!empty($errors)) $step = 1; -} -elseif ($step === 3) { + if (empty($bot_token)) + $errors[] = 'توکن ربات الزامی است.'; + if (empty($admin_id) || !is_numeric($admin_id)) + $errors[] = 'آیدی عددی ادمین الزامی و باید عدد باشد.'; + // اگر آدرس دامنه خالی بود، از آدرس جاری استفاده شود + if (empty($domain_url)) + $domain_url = $defaultDomainUrl; + + // حذف اسلش آخر از آدرس دامنه در صورت وجود + $domain_url = rtrim($domain_url, '/'); + + // بررسی فرمت URL + if (!empty($domain_url) && !filter_var($domain_url, FILTER_VALIDATE_URL)) { + $errors[] = 'فرمت آدرس دامنه صحیح نیست. باید با http:// یا https:// شروع شود.'; + } + + // اگر username/password خالی بود، مقادیر پیش‌فرض تولید شود + if (empty($web_username)) { + $web_username = 'admin'; + } + if (empty($web_password)) { + $web_password = generateRandomString(16); + } + + if (!empty($errors)) + $step = 1; +} elseif ($step === 3) { $db_host = trim($_POST['db_host'] ?? 'localhost'); $db_name = trim($_POST['db_name'] ?? ''); $db_user = trim($_POST['db_user'] ?? ''); $db_pass = trim($_POST['db_pass'] ?? ''); - if (empty($db_name)) $errors[] = 'نام دیتابیس الزامی است.'; - if (empty($db_user)) $errors[] = 'نام کاربری دیتابیس الزامی است.'; - + if (empty($db_name)) + $errors[] = 'نام دیتابیس الزامی است.'; + if (empty($db_user)) + $errors[] = 'نام کاربری دیتابیس الزامی است.'; + if (empty($errors)) { - if (!is_dir(__DIR__ . '/includes')) @mkdir(__DIR__ . '/includes', 0755, true); - if (!file_exists($configFile)) @file_put_contents($configFile, "exec(getDbBaseSchemaSQL()); $successMessages[] = "✅ ساختار پایه جداول با موفقیت ایجاد/بررسی شد."; - + $secretToken = generateRandomString(64); + $web_password_hash = password_hash($web_password, PASSWORD_BCRYPT); + $config_content = '{$web_username} | Password={$web_password}"; + $upgradeMessages = runDbUpgrades($pdo); if (!empty($upgradeMessages)) { $successMessages = array_merge($successMessages, $upgradeMessages); @@ -231,11 +273,11 @@ function runDbUpgrades(PDO $pdo): array { ('notification_inactive_days', '30'), ('renewal_status', 'off'), ('renewal_price_per_day', '1000'), ('renewal_price_per_gb', '2000'), ('payment_gateway_status', 'off'), ('zarinpal_merchant_id', '');"); $successMessages[] = "✅ تنظیمات پیش‌فرض با موفقیت افزوده شد."; - + $apiUrl = "https://api.telegram.org/bot$bot_token/setWebhook?secret_token=$secretToken&url=" . urlencode($botFileUrl); $response = @file_get_contents($apiUrl); $response_data = json_decode($response, true); - + if (!$response || !$response_data['ok']) { $errors[] = 'خطا در ثبت وبهوک: ' . ($response_data['description'] ?? 'پاسخ نامعتبر از تلگرام. از صحت توکن مطمئن شوید.'); } else { @@ -251,6 +293,7 @@ function runDbUpgrades(PDO $pdo): array { ?> + @@ -258,198 +301,448 @@ function runDbUpgrades(PDO $pdo): array { + -
-
-

نصب و راه‌اندازی ربات تلگرام

-
- -
- -
-
- +
+

نصب و راه‌اندازی ربات تلگرام

+
+ +
+ +
+
+ -
- -
-
۱
-
اطلاعات ربات
-
-
-
۲
-
دیتابیس
-
-
-
۳
-
پایان نصب
-
-
+ ?> +
- -
- خطا! -
    - " . htmlspecialchars($error) . ""; ?>
+
+
۱
+
اطلاعات ربات
+
+
+
۲
+
دیتابیس
+
+
+
۳
+
پایان نصب
+
- - -
- آدرس وبهوک شما: - -
-
-
مرحله ۱: اطلاعات ربات تلگرام
-
- -
- - -

مثال: 123456789:ABCdefGHIjklMnOpQRstUvWxYz

-
-
- - -

مثال: 123456789

-
- -
-
- -
-
مرحله ۲: تنظیمات پایگاه داده
-
- - - -
- - -
-
- - -
-
- - -
-
- - -
- -
-
- -
- -
- نصب با موفقیت به پایان رسید! -
    " . $msg . ""; ?>
-
-
- مهم: این فایل جهت افزایش امنیت تا چند ثانیه دیگر به صورت خودکار حذف خواهد شد. -
- -
- نصب با خطا مواجه شد! -
    - " . htmlspecialchars($error) . ""; ?>
-
- -
- + +
+ خطا! +
    - " . htmlspecialchars($error) . ""; ?>
+
+ + + +
+ آدرس وبهوک شما: + +
+
+
مرحله ۱: اطلاعات ربات تلگرام
+
+ +
+ + +

مثال: 123456789:ABCdefGHIjklMnOpQRstUvWxYz

+
+
+ + +

مثال: 123456789

+
+
+ + +

مثال: https://yourdomain.com (بدون اسلش آخر) - در صورت خالی بودن، آدرس + فعلی استفاده می‌شود

+
+ +
+ 🔐 اطلاعات ورود به پنل تحت وب (اختیاری) +

در صورت خالی گذاشتن، + نام کاربری "admin" و رمز عبور تصادفی تولید می‌شود.

+
+ +
+ + +

پیش‌فرض: admin

+
+
+ + +

حداقل 8 کاراکتر - در صورت خالی بودن، رمز تصادفی تولید می‌شود

+
+ +
+
+ +
+
مرحله ۲: تنظیمات پایگاه داده
+
+ + + + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ نصب با موفقیت به پایان رسید! +
    " . $msg . ""; ?>
+
+
+ مهم: این فایل جهت افزایش امنیت تا چند ثانیه دیگر به صورت خودکار حذف خواهد + شد. +
+ +
+ نصب با خطا مواجه شد! +
    - " . htmlspecialchars($error) . ""; ?>
+
+ +
+ +
-
- - - - + }) + .catch(function (error) { console.error('خطا در حذف خودکار فایل:', error); }); + }, 5000); // 5-second delay + + - \ No newline at end of file + + diff --git a/src/web/README.md b/src/web/README.md new file mode 100644 index 0000000..cb35ddf --- /dev/null +++ b/src/web/README.md @@ -0,0 +1,50 @@ +# پنل تحت وب v2ray - راهنمای استفاده + +## نصب + +1. فایل `install.php` را در مرورگر باز کنید +2. در مرحله اول، اطلاعات زیر را وارد کنید: + - توکن ربات تلگرام + - آیدی عددی ادمین + - **(اختیاری)** نام کاربری و رمز عبور پنل وب +3. در مرحله دوم، اطلاعات دیتابیس را وارد کنید +4. بعد از نصب موفق، اطلاعات ورود به پنل وب نمایش داده می‌شود + +## ورود به پنل + +1. به آدرس `http://your-domain/web/` بروید +2. با نام کاربری و رمز عبور وارد شوید +3. به داشبورد هدایت می‌شوید + +## قابلیت‌ها + +### ✅ فعال شده: +- 🔐 سیستم احراز هویت امن +- 📊 داشبورد با آمار کلی +- 🗂 مدیریت دسته‌بندی‌ها (افزودن، حذف، فعال/غیرفعال) +- 👥 مدیریت کاربران (جستجو، افزایش/کاهش موجودی، مسدود/آزاد، مشاهده سرویس‌ها) +- 📈 آمار و گزارشات (آمار کاربران، سرویس‌ها، درآمد) +- ⚙️ تنظیمات (وضعیت ربات، کانال اجباری، هدیه خوش‌آمد، درگاه پرداخت) +- 🎨 طراحی مدرن و زیبا +- 📱 Responsive برای موبایل + +### 🔜 در حال توسعه: +- 📝 مدیریت پلن‌ها +- 🌐 مدیریت سرورها +- 👨‍💼 مدیریت ادمین‌ها +- 🎁 مدیریت کدهای تخفیف +- 📚 مدیریت راهنما +- 💳 مدیریت پرداخت‌های دستی +- 📣 ارسال همگانی + +## ویژگی‌های امنیتی + +- ✅ Hash کردن رمز عبور با bcrypt +- ✅ Session security +- ✅ محافظت از تمام صفحات با requireLogin +- ✅ Sanitization ورودی‌ها +- ✅ استفاده از Prepared Statements + +## پشتیبانی + +برای گزارش مشکلات یا پیشنهادات، با توسعه‌دهنده تماس بگیرید. diff --git a/src/web/assets/css/style.css b/src/web/assets/css/style.css new file mode 100644 index 0000000..7c95c49 --- /dev/null +++ b/src/web/assets/css/style.css @@ -0,0 +1,692 @@ +:root { + --bg-main: #0a0e1a; + --bg-container: #1e293b; + --bg-card: #2d3748; + --bg-input: #111827; + --primary: #8b5cf6; + --primary-hover: #7c3aed; + --success: #10b981; + --danger: #ef4444; + --warning: #f59e0b; + --info: #3b82f6; + --blue: #3b82f6; + --green: #10b981; + --purple: #8b5cf6; + --orange: #f97316; + --teal: #14b8a6; + --red: #ef4444; + --text-light: #f8fafc; + --text-muted: #94a3b8; + --border-color: rgba(148, 163, 184, 0.2); + --shadow-color: rgba(0, 0, 0, 0.5); + --sidebar-width: 280px; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: Vazirmatn, sans-serif; +} + +body { + background-color: var(--bg-main); + background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"%3E%3Cg fill-rule="evenodd"%3E%3Cg fill="%231e293b" fill-opacity="0.2"%3E%3Cpath d="M0 38.59l2.83-2.83 1.41 1.41L1.41 40H0v-1.41zM0 1.4l2.83 2.83 1.41-1.41L1.41 0H0v1.41zM38.59 40l-2.83-2.83 1.41-1.41L40 38.59V40h-1.41zM40 1.41l-2.83 2.83-1.41-1.41L38.59 0H40v1.41zM20 18.6l2.83-2.83 1.41 1.41L21.41 20l2.83 2.83-1.41 1.41L20 21.41l-2.83 2.83-1.41-1.41L18.59 20l-2.83-2.83 1.41-1.41L20 18.59z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E'); + min-height: 100vh; + color: var(--text-light); +} + +/* Layout */ +.layout { + display: flex; + min-height: 100vh; +} + +.main-content { + flex: 1; + margin-right: var(--sidebar-width); + transition: margin-right 0.3s; + width: 100%; + min-width: 0; + /* Important for flex child */ +} + +/* Sidebar */ +.sidebar { + position: fixed; + right: 0; + top: 0; + width: var(--sidebar-width); + height: 100vh; + background: var(--bg-container); + border-left: 1px solid var(--border-color); + display: flex; + flex-direction: column; + z-index: 1000; + transition: transform 0.3s; + overflow-y: auto; +} + +.sidebar-header { + padding: 30px 20px; + border-bottom: 1px solid var(--border-color); +} + +.sidebar-header h2 { + font-size: 1.5rem; + margin-bottom: 5px; +} + +.sidebar-header .username { + font-size: 0.9rem; + color: var(--text-muted); +} + +.sidebar-nav { + flex: 1; + padding: 20px 0; + overflow-y: auto; +} + +.sidebar-nav a { + display: flex; + align-items: center; + padding: 12px 20px; + color: var(--text-muted); + text-decoration: none; + transition: all 0.3s; + border-right: 3px solid transparent; +} + +.sidebar-nav a i { + width: 24px; + margin-left: 12px; +} + +.sidebar-nav a:hover { + background-color: rgba(139, 92, 246, 0.1); + color: var(--text-light); +} + +.sidebar-nav a.active { + background-color: rgba(139, 92, 246, 0.15); + color: var(--primary); + border-right-color: var(--primary); +} + +.sidebar-footer { + padding: 20px; + border-top: 1px solid var(--border-color); +} + +.logout-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 12px; + background: linear-gradient(135deg, var(--danger), #dc2626); + color: white; + text-decoration: none; + border-radius: 8px; + transition: transform 0.2s; + font-weight: 500; +} + +.logout-btn:hover { + transform: translateY(-2px); +} + +.logout-btn i { + margin-left: 8px; +} + +/* Topbar */ +.topbar { + display: flex; + align-items: center; + padding: 20px 30px; + background: var(--bg-container); + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: 100; +} + +.topbar h1 { + font-size: 1.5rem; + font-weight: 600; + flex: 1; +} + +.menu-toggle { + display: none; + background: transparent; + border: none; + color: var(--text-light); + font-size: 1.5rem; + cursor: pointer; + margin-left: 15px; + padding: 8px; +} + +.menu-toggle:hover { + background-color: rgba(139, 92, 246, 0.1); + border-radius: 8px; +} + +/* Content Area */ +.content-area { + padding: 30px; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: var(--bg-container); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; + display: flex; + align-items: center; + gap: 20px; + transition: transform 0.2s, box-shadow 0.2s; +} + +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.stat-icon { + width: 60px; + height: 60px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + flex-shrink: 0; +} + +.stat-icon.blue { + background: rgba(59, 130, 246, 0.2); + color: var(--blue); +} + +.stat-icon.green { + background: rgba(16, 185, 129, 0.2); + color: var(--green); +} + +.stat-icon.purple { + background: rgba(139, 92, 246, 0.2); + color: var(--purple); +} + +.stat-icon.orange { + background: rgba(249, 115, 22, 0.2); + color: var(--orange); +} + +.stat-icon.teal { + background: rgba(20, 184, 166, 0.2); + color: var(--teal); +} + +.stat-icon.red { + background: rgba(239, 68, 68, 0.2); + color: var(--red); +} + +.stat-info { + flex: 1; + min-width: 0; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + margin-bottom: 5px; +} + +.stat-label { + color: var(--text-muted); + font-size: 0.9rem; +} + +.stat-sub { + color: var(--success); + font-size: 0.85rem; + margin-top: 5px; +} + +/* Cards */ +.card { + background: var(--bg-container); + border: 1px solid var(--border-color); + border-radius: 12px; + overflow: hidden; + margin-bottom: 20px; +} + +.card-header { + padding: 20px; + border-bottom: 1px solid var(--border-color); +} + +.card-header h3 { + font-size: 1.25rem; + font-weight: 600; + word-break: break-word; +} + +.card-header h3 i { + margin-left: 10px; +} + +.card-body { + padding: 20px; +} + +/* Tables */ +.data-table { + width: 100%; + border-collapse: collapse; + min-width: 600px; +} + +.data-table thead { + background: rgba(139, 92, 246, 0.1); +} + +.data-table th { + padding: 12px; + text-align: right; + font-weight: 600; + color: var(--text-light); + white-space: nowrap; +} + +.data-table tbody tr { + border-bottom: 1px solid var(--border-color); + transition: background-color 0.2s; +} + +.data-table tbody tr:hover { + background-color: rgba(255, 255, 255, 0.02); +} + +.data-table td { + padding: 12px; + text-align: right; +} + +/* Forms */ +.form-group { + margin-bottom: 20px; +} + +label { + display: block; + margin-bottom: 8px; + color: var(--text-muted); + font-weight: 500; +} + +input[type="text"], +input[type="password"], +input[type="number"], +input[type="email"], +select, +textarea { + width: 100%; + padding: 12px; + background: var(--bg-input); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-light); + font-size: 1rem; + transition: border-color 0.3s, box-shadow 0.3s; +} + +input:focus, +select:focus, +textarea:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.3); + outline: none; +} + +/* Buttons */ +.btn { + padding: 12px 24px; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + text-decoration: none; + display: inline-block; + text-align: center; +} + +.btn:hover { + transform: translateY(-2px); +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + color: white; +} + +.btn-success { + background: linear-gradient(135deg, var(--success), #059669); + color: white; +} + +.btn-danger { + background: linear-gradient(135deg, var(--danger), #dc2626); + color: white; +} + +/* Alerts */ +.alert { + padding: 15px; + border-radius: 8px; + margin-bottom: 20px; + border-right-width: 4px; + border-right-style: solid; +} + +.alert-success { + background-color: rgba(16, 185, 129, 0.1); + border-right-color: var(--success); + color: #a7f3d0; +} + +.alert-danger { + background-color: rgba(239, 68, 68, 0.1); + border-right-color: var(--danger); + color: #fca5a5; +} + +.alert-warning { + background-color: rgba(245, 158, 11, 0.1); + border-right-color: var(--warning); + color: #fcd34d; +} + +.alert-info { + background-color: rgba(59, 130, 246, 0.1); + border-right-color: var(--info); + color: #93c5fd; +} + +/* Utilities */ +.text-muted { + color: var(--text-muted); +} + +.text-center { + text-align: center; +} + +.mb-20 { + margin-bottom: 20px; +} + +.mt-20 { + margin-top: 20px; +} + +/* Mobile Overlay */ +.sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; + opacity: 0; + transition: opacity 0.3s; +} + +.sidebar-overlay.active { + opacity: 1; +} + +/* ==================== Responsive Design ==================== */ + +/* Tablets (768px - 1024px) */ +@media (max-width: 1024px) { + .content-area { + padding: 20px; + } + + .topbar { + padding: 15px 20px; + } + + .stats-grid { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + } + + .stat-value { + font-size: 1.5rem; + } +} + +/* Mobile Devices (up to 768px) */ +@media (max-width: 768px) { + :root { + --sidebar-width: 280px; + } + + /* Hide sidebar by default on mobile */ + .sidebar { + transform: translateX(100%); + box-shadow: -5px 0 15px rgba(0, 0, 0, 0.3); + } + + .sidebar.active { + transform: translateX(0); + } + + /* Show overlay when sidebar is active */ + .sidebar-overlay { + display: block; + } + + /* Remove sidebar margin from main content */ + .main-content { + margin-right: 0; + } + + /* Show menu toggle button */ + .menu-toggle { + display: block; + } + + /* Adjust topbar */ + .topbar { + padding: 12px 15px; + } + + .topbar h1 { + font-size: 1.2rem; + } + + /* Content area padding */ + .content-area { + padding: 15px; + } + + /* Stats grid - single column */ + .stats-grid { + grid-template-columns: 1fr; + gap: 12px; + } + + .stat-card { + padding: 16px; + gap: 12px; + } + + .stat-icon { + width: 50px; + height: 50px; + font-size: 1.2rem; + } + + .stat-value { + font-size: 1.5rem; + } + + .stat-label { + font-size: 0.85rem; + } + + /* Cards */ + .card-header { + padding: 15px; + } + + .card-header h3 { + font-size: 1.1rem; + } + + .card-body { + padding: 15px; + } + + /* Tables - enable horizontal scroll */ + .card-body { + overflow-x: auto; + } + + .data-table { + font-size: 0.85rem; + } + + .data-table th, + .data-table td { + padding: 8px; + white-space: nowrap; + } + + /* Forms */ + input[type="text"], + input[type="password"], + input[type="number"], + input[type="email"], + select, + textarea { + font-size: 16px; + /* Prevents zoom on iOS */ + } + + /* Buttons */ + .btn { + padding: 10px 16px; + font-size: 0.9rem; + width: 100%; + margin-bottom: 8px; + } + + .btn:last-child { + margin-bottom: 0; + } + + /* Sidebar adjustments */ + .sidebar-header { + padding: 20px 15px; + } + + .sidebar-header h2 { + font-size: 1.3rem; + } + + .sidebar-nav a { + padding: 10px 15px; + font-size: 0.95rem; + } +} + +/* Small Mobile Devices (up to 480px) */ +@media (max-width: 480px) { + .topbar h1 { + font-size: 1rem; + } + + .content-area { + padding: 10px; + } + + .stat-card { + flex-direction: column; + text-align: center; + } + + .stat-value { + font-size: 1.8rem; + } + + .card-header h3 { + font-size: 1rem; + } + + .data-table { + font-size: 0.75rem; + } + + .data-table th, + .data-table td { + padding: 6px 4px; + } + + /* Make buttons stack vertically */ + .btn { + display: block; + width: 100%; + } + + /* Adjust sidebar width for very small screens */ + :root { + --sidebar-width: 90%; + } +} + +/* Landscape orientation on mobile */ +@media (max-width: 768px) and (orientation: landscape) { + .sidebar { + width: 60%; + } + + .content-area { + padding: 15px; + } +} + +/* Print styles */ +@media print { + + .sidebar, + .topbar, + .menu-toggle, + .btn { + display: none; + } + + .main-content { + margin: 0; + } +} \ No newline at end of file diff --git a/src/web/assets/js/main.js b/src/web/assets/js/main.js new file mode 100644 index 0000000..4c96a54 --- /dev/null +++ b/src/web/assets/js/main.js @@ -0,0 +1,158 @@ +// Main JavaScript for Web Panel +document.addEventListener('DOMContentLoaded', function () { + // Menu Toggle for Mobile + const menuToggle = document.getElementById('menuToggle'); + const sidebar = document.getElementById('sidebar'); + + // Create overlay for mobile + let overlay = document.querySelector('.sidebar-overlay'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.className = 'sidebar-overlay'; + document.body.appendChild(overlay); + } + + if (menuToggle && sidebar) { + // Toggle sidebar on menu button click + menuToggle.addEventListener('click', function (e) { + e.stopPropagation(); + sidebar.classList.toggle('active'); + overlay.classList.toggle('active'); + + // Prevent body scroll when sidebar is open on mobile + if (sidebar.classList.contains('active')) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + }); + + // Close sidebar when clicking overlay + overlay.addEventListener('click', function () { + sidebar.classList.remove('active'); + overlay.classList.remove('active'); + document.body.style.overflow = ''; + }); + + // Close sidebar when clicking outside on mobile + document.addEventListener('click', function (e) { + if (window.innerWidth <= 768) { + if (!sidebar.contains(e.target) && !menuToggle.contains(e.target)) { + sidebar.classList.remove('active'); + overlay.classList.remove('active'); + document.body.style.overflow = ''; + } + } + }); + + // Close sidebar when clicking a link on mobile + const sidebarLinks = sidebar.querySelectorAll('a'); + sidebarLinks.forEach(link => { + link.addEventListener('click', function () { + if (window.innerWidth <= 768) { + sidebar.classList.remove('active'); + overlay.classList.remove('active'); + document.body.style.overflow = ''; + } + }); + }); + + // Handle window resize + window.addEventListener('resize', function () { + if (window.innerWidth > 768) { + sidebar.classList.remove('active'); + overlay.classList.remove('active'); + document.body.style.overflow = ''; + } + }); + } + + // Auto-hide alerts after 5 seconds + const alerts = document.querySelectorAll('.alert'); + alerts.forEach(alert => { + alert.style.transition = 'opacity 0.3s'; + setTimeout(() => { + alert.style.opacity = '0'; + setTimeout(() => { + alert.remove(); + }, 300); + }, 5000); + }); + + // Add touch-friendly enhancements for mobile + if ('ontouchstart' in window) { + document.body.classList.add('touch-device'); + + // Make table rows tappable + const tableRows = document.querySelectorAll('.data-table tbody tr'); + tableRows.forEach(row => { + row.style.cursor = 'pointer'; + }); + } + + // Prevent double-tap zoom on buttons + const buttons = document.querySelectorAll('.btn'); + buttons.forEach(button => { + button.addEventListener('touchend', function (e) { + e.preventDefault(); + button.click(); + }, { passive: false }); + }); +}); + +/** + * Show toast notification + */ +function showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.className = `alert alert-${type}`; + toast.style.position = 'fixed'; + toast.style.top = '20px'; + toast.style.left = '50%'; + toast.style.transform = 'translateX(-50%)'; + toast.style.zIndex = '9999'; + toast.style.minWidth = '300px'; + toast.style.maxWidth = '90%'; + toast.style.transition = 'opacity 0.3s'; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + setTimeout(() => { + toast.remove(); + }, 300); + }, 3000); +} + +/** + * Confirm dialog + */ +function confirmAction(message) { + return confirm(message); +} + +/** + * Format number with thousands separator + */ +function formatNumber(num) { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + +/** + * Detect if device is mobile + */ +function isMobile() { + return window.innerWidth <= 768; +} + +/** + * Smooth scroll to element + */ +function scrollToElement(elementId) { + const element = document.getElementById(elementId); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } +} diff --git a/src/web/dashboard.php b/src/web/dashboard.php new file mode 100644 index 0000000..7561075 --- /dev/null +++ b/src/web/dashboard.php @@ -0,0 +1,184 @@ +query("SELECT COUNT(*) FROM users"); + $stats['total_users'] = $stmt->fetchColumn(); + + // Active users + $stmt = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'active'"); + $stats['active_users'] = $stmt->fetchColumn(); + + // Total services + $stmt = pdo()->query("SELECT COUNT(*) FROM services"); + $stats['total_services'] = $stmt->fetchColumn(); + + // Today income + $stmt = pdo()->prepare("SELECT SUM(amount) FROM payment_requests WHERE status = 'approved' AND DATE(processed_at) = CURDATE()"); + $stmt->execute(); + $stats['today_income'] = $stmt->fetchColumn() ?: 0; + + // Month income + $stmt = pdo()->prepare("SELECT SUM(amount) FROM payment_requests WHERE status = 'approved' AND MONTH(processed_at) = MONTH(CURDATE()) AND YEAR(processed_at) = YEAR(CURDATE())"); + $stmt->execute(); + $stats['month_income'] = $stmt->fetchColumn() ?: 0; + + // Total servers + $stmt = pdo()->query("SELECT COUNT(*) FROM servers WHERE status = 'active'"); + $stats['total_servers'] = $stmt->fetchColumn(); + + // Pending payments + try { + $stmt = pdo()->query("SELECT COUNT(*) FROM payment_requests WHERE status = 'pending'"); + $stats['pending_payments'] = $stmt->fetchColumn(); + } catch (Exception $e) { + $stats['pending_payments'] = 0; + } + + // Recent services (last 5) + $stmt = pdo()->query(" + SELECT s.*, p.name as plan_name, u.first_name + FROM services s + JOIN plans p ON s.plan_id = p.id + JOIN users u ON s.owner_chat_id = u.chat_id + ORDER BY s.id DESC + LIMIT 5 + "); + $recent_services = $stmt->fetchAll(PDO::FETCH_ASSOC); + +} catch (Exception $e) { + die("Error loading dashboard: " . $e->getMessage()); +} + +renderHeader('داشبورد'); +?> + +
+ + +
+ + +
+ +
+
+
+ +
+
+
+
کل کاربران
+
فعال
+
+
+ +
+
+ +
+
+
+
کل سرویس‌ها
+
+
+ +
+
+ +
+
+
+
درآمد امروز (تومان)
+
+
+ +
+
+ +
+
+
+
درآمد ماه (تومان)
+
+
+ +
+
+ +
+
+
+
سرورهای فعال
+
+
+ +
+
+ +
+
+
+
پرداخت‌های در انتظار
+
+
+
+ + +
+
+

آخرین سرویس‌ها

+
+
+ +

هیچ سرویسی یافت نشد.

+ + + + + + + + + + + + + + + + + + + + +
کاربرپلننام سرویستاریخ خرید
+
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/web/includes/auth.php b/src/web/includes/auth.php new file mode 100644 index 0000000..14e21b4 --- /dev/null +++ b/src/web/includes/auth.php @@ -0,0 +1,53 @@ + + + + + + + + <?php echo htmlspecialchars($pageTitle); ?> - پنل مدیریت + + + + + + + + + + + + + + + +
+ +

+
+ ' . htmlspecialchars($message) . '
'; +} + +/** + * Show error message + */ +function showError($message) +{ + echo '
' . htmlspecialchars($message) . '
'; +} + +/** + * Sanitize input + */ +function sanitizeInput($data) +{ + return htmlspecialchars(strip_tags(trim($data))); +} \ No newline at end of file diff --git a/src/web/index.php b/src/web/index.php new file mode 100644 index 0000000..66a4ffc --- /dev/null +++ b/src/web/index.php @@ -0,0 +1,207 @@ + + + + + + + + ورود به پنل مدیریت + + + + + +
+ + + +
+ + +
+
+ + +
+ +
+ + +
+ + +
+
+ + + \ No newline at end of file diff --git a/src/web/pages/admins.php b/src/web/pages/admins.php new file mode 100644 index 0000000..00998d9 --- /dev/null +++ b/src/web/pages/admins.php @@ -0,0 +1,280 @@ + + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+
+ +
+
+
+
کل ادمین‌ها
+
شامل سوپر ادمین
+
+
+ +
+
+ +
+
+
+
ادمین‌های عادی
+
+
+ +
+
+ +
+
+
+
انواع دسترسی
+
+
+
+ + +
+
+

افزودن ادمین جدید

+
+
+
+
+
+ + + شناسه کاربر تلگرام +
+ +
+ + +
+
+ +
+ +
+ $label): ?> + + +
+ + همه دسترسی‌های مورد نیاز را انتخاب کنید + +
+ + +
+
+
+ + +
+
+

لیست ادمین‌ها ( نفر)

+
+
+ +

+ + هیچ ادمینی یافت نشد. از فرم بالا برای افزودن ادمین جدید استفاده کنید. +

+ + $admin): ?> +
+
+
+
+

+ + +

+

+ Chat ID: +

+
+ + حذف + +
+ + +
+ + دسترسی‌ها + + ( مورد) + + + +
+ + +
+ $label): ?> + + +
+ + +
+
+ + +
+ دسترسی‌های فعلی: +
+ + هیچ دسترسی ندارد + + + + + + + +
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/web/pages/broadcast.php b/src/web/pages/broadcast.php new file mode 100644 index 0000000..041ed63 --- /dev/null +++ b/src/web/pages/broadcast.php @@ -0,0 +1,480 @@ + false, 'error' => 'BOT_TOKEN تعریف نشده است. لطفاً فایل config.php را بررسی کنید.']); + exit; + } + + if (BOT_TOKEN === 'TOKEN' || empty(BOT_TOKEN)) { + echo json_encode(['success' => false, 'error' => 'BOT_TOKEN در فایل config.php به درستی تنظیم نشده است.']); + exit; + } + + $offset = (int) ($_POST['offset'] ?? 0); + $batch_size = 50; // Process 50 users at a time + $message_text = $_POST['message_text'] ?? ''; + $target_group = $_POST['target_group'] ?? 'all'; + $photo_id = sanitizeInput($_POST['photo_id'] ?? ''); + + if (empty($message_text)) { + echo json_encode(['success' => false, 'error' => 'متن پیام خالی است']); + exit; + } + + // Get target users based on selection + switch ($target_group) { + case 'all': + $stmt = pdo()->query("SELECT chat_id FROM users WHERE status = 'active' LIMIT $offset, $batch_size"); + $count_stmt = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'active'"); + break; + case 'with_service': + $stmt = pdo()->query(" + SELECT DISTINCT u.chat_id + FROM users u + JOIN services s ON u.chat_id = s.owner_chat_id + WHERE u.status = 'active' + LIMIT $offset, $batch_size + "); + $count_stmt = pdo()->query("SELECT COUNT(DISTINCT u.chat_id) FROM users u JOIN services s ON u.chat_id = s.owner_chat_id WHERE u.status = 'active'"); + break; + case 'no_service': + $stmt = pdo()->query(" + SELECT chat_id + FROM users + WHERE status = 'active' + AND chat_id NOT IN (SELECT DISTINCT owner_chat_id FROM services) + LIMIT $offset, $batch_size + "); + $count_stmt = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'active' AND chat_id NOT IN (SELECT DISTINCT owner_chat_id FROM services)"); + break; + case 'active_service': + $now = time(); + $stmt = pdo()->prepare(" + SELECT DISTINCT u.chat_id + FROM users u + JOIN services s ON u.chat_id = s.owner_chat_id + WHERE u.status = 'active' + AND s.expire_timestamp > ? + LIMIT $offset, $batch_size + "); + $stmt->execute([$now]); + $count_stmt = pdo()->prepare("SELECT COUNT(DISTINCT u.chat_id) FROM users u JOIN services s ON u.chat_id = s.owner_chat_id WHERE u.status = 'active' AND s.expire_timestamp > ?"); + $count_stmt->execute([$now]); + break; + default: + $stmt = pdo()->query("SELECT chat_id FROM users WHERE status = 'active' LIMIT $offset, $batch_size"); + $count_stmt = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'active'"); + } + + $users = $stmt->fetchAll(PDO::FETCH_COLUMN); + $total_users = $count_stmt->fetchColumn(); + + $sent = 0; + $failed = 0; + $error_details = []; + + // Send messages to this batch + foreach ($users as $chat_id) { + try { + if (!empty($photo_id)) { + // Send photo with caption using cURL for better error handling + $url = "https://api.telegram.org/bot" . BOT_TOKEN . "/sendPhoto"; + $data = [ + 'chat_id' => $chat_id, + 'photo' => $photo_id, + 'caption' => $message_text, + 'parse_mode' => 'HTML' + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $result = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($result && $http_code == 200) { + $response = json_decode($result, true); + if ($response && isset($response['ok']) && $response['ok']) { + $sent++; + } else { + $failed++; + $error_details[] = "Chat $chat_id: " . ($response['description'] ?? 'Unknown error'); + } + } else { + $failed++; + $error_details[] = "Chat $chat_id: HTTP $http_code"; + } + } else { + // Send text message + $response = sendMessage($chat_id, $message_text); + $decoded = json_decode($response, true); + if ($decoded && isset($decoded['ok']) && $decoded['ok']) { + $sent++; + } else { + $failed++; + $error_details[] = "Chat $chat_id: " . ($decoded['description'] ?? 'sendMessage failed'); + } + } + + // Small delay to avoid rate limiting + usleep(30000); // 30ms delay + } catch (Exception $e) { + $failed++; + $error_details[] = "Chat $chat_id: Exception - " . $e->getMessage(); + } + } + + $response_data = [ + 'success' => true, + 'sent' => $sent, + 'failed' => $failed, + 'total' => $total_users, + 'processed' => $offset + count($users), + 'has_more' => ($offset + count($users)) < $total_users + ]; + + // Include error details in debug mode (first batch only) + if ($offset == 0 && !empty($error_details)) { + $response_data['debug_errors'] = array_slice($error_details, 0, 5); // First 5 errors + } + + echo json_encode($response_data); + exit; +} + +// Get statistics for display +$stats = []; +$stats['all'] = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'active'")->fetchColumn(); +$stats['with_service'] = pdo()->query("SELECT COUNT(DISTINCT owner_chat_id) FROM services s JOIN users u ON s.owner_chat_id = u.chat_id WHERE u.status = 'active'")->fetchColumn(); +$stats['no_service'] = $stats['all'] - $stats['with_service']; +$now = time(); +$stmt = pdo()->prepare("SELECT COUNT(DISTINCT s.owner_chat_id) FROM services s JOIN users u ON s.owner_chat_id = u.chat_id WHERE u.status = 'active' AND s.expire_timestamp > ?"); +$stmt->execute([$now]); +$stats['active_service'] = $stmt->fetchColumn(); + +renderHeader('پیام همگانی'); +?> + +
+ + +
+ + +
+ + + + +
+
+
+ +
+
+
+
همه کاربران فعال
+
+
+ +
+
+ +
+
+
+
با سرویس
+
+
+ +
+
+ +
+
+
+
بدون سرویس
+
+
+ +
+
+ +
+
+
+
سرویس فعال
+
+
+
+ + +
+
+

ارسال پیام جدید

+
+
+
+ ⚠️ هشدار: پیام به تمام کاربران گروه انتخابی ارسال خواهد شد. این عمل قابل برگشت + نیست! +
+ +
+
+ + +
+ 📊 تعداد گیرندگان: نفر +
+
+ +
+ + + از HTML برای فرمت‌بندی استفاده کنید +
+ +
+ + + برای ارسال پیام با تصویر، یک عکس به ربات ارسال کنید + و Photo ID آن را اینجا وارد کنید +
+ +
+ + +
+ ارسال به صورت خودکار در پس‌زمینه انجام می‌شود +
+
+
+
+
+ + +
+
+

پیش‌نمایش

+
+
+
+

متن پیام خود را بنویسید تا اینجا نمایش + داده شود...

+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/web/pages/categories.php b/src/web/pages/categories.php new file mode 100644 index 0000000..d6b8d8e --- /dev/null +++ b/src/web/pages/categories.php @@ -0,0 +1,147 @@ +prepare("INSERT INTO categories (name, status) VALUES (?, 'active')"); + if ($stmt->execute([$name])) { + $success = 'دسته‌بندی با موفقیت اضافه شد.'; + } else { + $error = 'خطا در افزودن دسته‌بندی.'; + } + } + } +} + +// Handle delete +if (isset($_GET['delete'])) { + $id = (int) $_GET['delete']; + $stmt = pdo()->prepare("DELETE FROM categories WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'دسته‌بندی حذف شد.'; + } +} + +// Handle toggle status +if (isset($_GET['toggle'])) { + $id = (int) $_GET['toggle']; + $stmt = pdo()->prepare("UPDATE categories SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'وضعیت دسته‌بندی تغییر کرد.'; + } +} + +// Get all categories +$categories = getCategories(); + +renderHeader('مدیریت دسته‌بندی‌ها'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+

افزودن دسته‌بندی جدید

+
+
+
+
+ + +
+ +
+
+
+ + +
+
+

لیست دسته‌بندی‌ها

+
+
+ +

هیچ دسته‌بندی‌ای یافت نشد.

+ + + + + + + + + + + + + + + + + + + + +
شناسهناموضعیتعملیات
+ + ✅ فعال + + ❌ غیرفعال + + + + + + + حذف + +
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/web/pages/discount.php b/src/web/pages/discount.php new file mode 100644 index 0000000..2fd8d79 --- /dev/null +++ b/src/web/pages/discount.php @@ -0,0 +1,260 @@ + 0 && $max_usage > 0) { + // Check if code already exists + $stmt = pdo()->prepare("SELECT COUNT(*) FROM discount_codes WHERE code = ?"); + $stmt->execute([$code]); + if ($stmt->fetchColumn() > 0) { + $error = 'این کد تخفیف قبلاً ثبت شده است.'; + } else { + $stmt = pdo()->prepare("INSERT INTO discount_codes (code, type, value, max_usage, usage_count, status) VALUES (?, ?, ?, ?, 0, 'active')"); + if ($stmt->execute([$code, $type, $value, $max_usage])) { + $success = 'کد تخفیف با موفقیت اضافه شد.'; + } else { + $error = 'خطا در افزودن کد تخفیف.'; + } + } + } else { + $error = 'لطفاً تمام فیلدها را به درستی پر کنید.'; + } +} + +// Handle edit discount +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_discount'])) { + $id = (int) $_POST['discount_id']; + $code = strtoupper(sanitizeInput($_POST['code'])); + $type = sanitizeInput($_POST['type']); + $value = (float) $_POST['value']; + $max_usage = (int) $_POST['max_usage']; + + $stmt = pdo()->prepare("UPDATE discount_codes SET code=?, type=?, value=?, max_usage=? WHERE id=?"); + if ($stmt->execute([$code, $type, $value, $max_usage, $id])) { + $success = 'کد تخفیف بهروزرسانی شد.'; + } else { + $error = 'خطا در بهروزرسانی کد تخفیف.'; + } +} + +// Handle delete +if (isset($_GET['delete'])) { + $id = (int) $_GET['delete']; + $stmt = pdo()->prepare("DELETE FROM discount_codes WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'کد تخفیف حذف شد.'; + } +} + +// Handle toggle +if (isset($_GET['toggle'])) { + $id = (int) $_GET['toggle']; + $stmt = pdo()->prepare("UPDATE discount_codes SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'وضعیت کد تخفیف تغییر کرد.'; + } +} + +// Get discount for edit +$edit_discount = null; +if (isset($_GET['edit'])) { + $edit_id = (int) $_GET['edit']; + $stmt = pdo()->prepare("SELECT * FROM discount_codes WHERE id = ?"); + $stmt->execute([$edit_id]); + $edit_discount = $stmt->fetch(PDO::FETCH_ASSOC); +} + +// Get all discount codes +$stmt = pdo()->query("SELECT * FROM discount_codes ORDER BY id DESC"); +$codes = $stmt->fetchAll(PDO::FETCH_ASSOC); + +renderHeader('مدیریت کدهای تخفیف'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+

+

+
+
+
+ + + + +
+
+ + + فقط حروف انگلیسی و اعداد +
+ +
+ + +
+ +
+ + + درصد (1-100) یا تومان +
+ +
+ + +
+
+ + + + + + لغو + + +
+
+
+ + +
+
+

لیست کدهای تخفیف ()

+
+
+ +

هیچ کد تخفیفی یافت نشد.

+ + + + + + + + + + + + + + + + + + + + + + + + +
کدنوعمقداراستفادهوضعیتعملیات
+ + + + + + + + + + + + / + + + + ✅ فعال + + ❌ غیرفعال + + + + + + + + + + + +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/web/pages/guides.php b/src/web/pages/guides.php new file mode 100644 index 0000000..dc19932 --- /dev/null +++ b/src/web/pages/guides.php @@ -0,0 +1,239 @@ +prepare("INSERT INTO guides (button_name, content_type, message_text, photo_id, status) VALUES (?, ?, ?, ?, 'active')"); + if ($stmt->execute([$button_name, $content_type, $message_text, $photo_id])) { + $success = 'راهنما با موفقیت اضافه شد.'; + } else { + $error = 'خطا در افزودن راهنما.'; + } + } else { + $error = 'لطفاً نام دکمه را وارد کنید.'; + } +} + +// Handle edit guide +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_guide'])) { + $id = (int) $_POST['guide_id']; + $button_name = sanitizeInput($_POST['button_name']); + $content_type = sanitizeInput($_POST['content_type']); + $message_text = $_POST['message_text'] ?? ''; + $photo_id = sanitizeInput($_POST['photo_id'] ?? ''); + + $stmt = pdo()->prepare("UPDATE guides SET button_name=?, content_type=?, message_text=?, photo_id=? WHERE id=?"); + if ($stmt->execute([$button_name, $content_type, $message_text, $photo_id, $id])) { + $success = 'راهنما بهروزرسانی شد.'; + } else { + $error = 'خطا در بهروزرسانی راهنما.'; + } +} + +// Handle delete +if (isset($_GET['delete'])) { + $id = (int) $_GET['delete']; + $stmt = pdo()->prepare("DELETE FROM guides WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'راهنما حذف شد.'; + } +} + +// Handle toggle +if (isset($_GET['toggle'])) { + $id = (int) $_GET['toggle']; + $stmt = pdo()->prepare("UPDATE guides SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'وضعیت راهنما تغییر کرد.'; + } +} + +// Get guide for edit +$edit_guide = null; +if (isset($_GET['edit'])) { + $edit_id = (int) $_GET['edit']; + $stmt = pdo()->prepare("SELECT * FROM guides WHERE id = ?"); + $stmt->execute([$edit_id]); + $edit_guide = $stmt->fetch(PDO::FETCH_ASSOC); +} + +// Get all guides +$stmt = pdo()->query("SELECT * FROM guides ORDER BY id DESC"); +$guides = $stmt->fetchAll(PDO::FETCH_ASSOC); + +renderHeader('مدیریت راهنما'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+

+

+
+
+
+ + + + +
+
+ + +
+ +
+ + +
+
+ +
+ + + از Markdown برای فرمت‌بندی استفاده کنید +
+ +
+ + + برای دریافت: یک تصویر به ربات ارسال کنید +
+ + + + + + لغو + + +
+
+
+ + +
+
+

لیست راهنماها ()

+
+
+ +

هیچ راهنمایی یافت نشد.

+ + + + + + + + + + + + + + + + + + + + + + +
شناسهعنوان دکمهنوع محتواوضعیتعملیات
+ + + + ✅ فعال + + ❌ غیرفعال + + + + + + + + + + + +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/web/pages/payments.php b/src/web/pages/payments.php new file mode 100644 index 0000000..d3dd364 --- /dev/null +++ b/src/web/pages/payments.php @@ -0,0 +1,256 @@ +prepare("SELECT * FROM payment_requests WHERE id = ? AND status = 'pending'"); + $stmt->execute([$payment_id]); + $payment = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($payment) { + // Update user balance + updateUserBalance($payment['user_id'], $payment['amount'], 'add'); + + // Update payment status + $stmt = pdo()->prepare("UPDATE payment_requests SET status='approved', processed_at=NOW(), processed_by_admin_id=? WHERE id=?"); + $stmt->execute([ADMIN_CHAT_ID, $payment_id]); + + // Send notification to user via bot + sendMessage($payment['user_id'], "✅ درخواست شارژ حساب شما به مبلغ " . number_format($payment['amount']) . " تومان تایید شد.\n\n💰 موجودی جدید: " . number_format(getUserBalance($payment['user_id'])) . " تومان"); + + $success = 'درخواست تایید شد و موجودی کاربر افزایش یافت.'; + } else { + $error = 'درخواست یافت نشد یا قبلاً پردازش شده است.'; + } +} + +// Handle reject payment +if (isset($_GET['reject'])) { + $payment_id = (int) $_GET['reject']; + $stmt = pdo()->prepare("SELECT * FROM payment_requests WHERE id = ? AND status = 'pending'"); + $stmt->execute([$payment_id]); + $payment = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($payment) { + $stmt = pdo()->prepare("UPDATE payment_requests SET status='rejected', processed_at=NOW(), processed_by_admin_id=? WHERE id=?"); + $stmt->execute([ADMIN_CHAT_ID, $payment_id]); + + // Send notification to user via bot + sendMessage($payment['user_id'], "❌ درخواست شارژ حساب شما به مبلغ " . number_format($payment['amount']) . " تومان رد شد.\n\nلطفاً با پشتیبانی تماس بگیرید."); + + $success = 'درخواست رد شد.'; + } else { + $error = 'درخواست یافت نشد یا قبلاً پردازش شده است.'; + } +} + +// Get pending payment requests +$stmt = pdo()->query(" + SELECT pr.*, u.first_name + FROM payment_requests pr + JOIN users u ON pr.user_id = u.chat_id + WHERE pr.status = 'pending' + ORDER BY pr.created_at DESC +"); +$pending_payments = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Get processed payment requests (last 50) +$stmt = pdo()->query(" + SELECT pr.*, u.first_name + FROM payment_requests pr + JOIN users u ON pr.user_id = u.chat_id + WHERE pr.status != 'pending' + ORDER BY pr.processed_at DESC + LIMIT 50 +"); +$processed_payments = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Get renewal requests +$stmt = pdo()->query(" + SELECT rr.*, u.first_name + FROM renewal_requests rr + JOIN users u ON rr.user_id = u.chat_id + WHERE rr.status = 'pending' + ORDER BY rr.created_at DESC +"); +$renewal_requests = $stmt->fetchAll(PDO::FETCH_ASSOC); + +renderHeader('مدیریت پرداخت‌ها و تمدیدها'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+

درخواست‌های شارژ حساب در انتظار + ()

+
+
+ +

هیچ درخواستی در انتظار نیست.

+ +
+ + + + + + + + + + + + + + + + + + + +
شناسهکاربرمبلغتاریخرسیدعملیات
+ + + رد + +
+
+ +
+
+ + + +
+
+

درخواست‌های تمدید در انتظار + ()

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
کاربرسرویسروز/حجممبلغتاریخرسید
روز / + GB + تومان + + + مشاهده + + + - + +
+
+
+ ℹ️ توجه: برای تایید/رد درخواست‌های تمدید، از ربات تلگرام استفاده کنید. +
+
+
+ + + +
+
+

تاریخچه پرداخت‌ها (50 مورد اخیر)

+
+
+ +

هیچ درخواست پردازش شده‌ای وجود ندارد.

+ +
+ + + + + + + + + + + + + + + + + + + + + +
کاربرمبلغتاریخ ثبتتاریخ پردازشوضعیت
تومان + + + ✅ تایید شده + + ❌ رد شده + +
+
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/web/pages/plans.php b/src/web/pages/plans.php new file mode 100644 index 0000000..c710a47 --- /dev/null +++ b/src/web/pages/plans.php @@ -0,0 +1,299 @@ + 0 && $category_id > 0) { + $stmt = pdo()->prepare("INSERT INTO plans (server_id, category_id, name, price, volume_gb, duration_days, description, show_sub_link, show_conf_links, is_test_plan, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')"); + if ($stmt->execute([$server_id, $category_id, $name, $price, $volume_gb, $duration_days, $description, $show_sub_link, $show_conf_links, $is_test_plan])) { + $success = 'پلن با موفقیت اضافه شد.'; + } else { + $error = 'خطا در افزودن پلن.'; + } + } else { + $error = 'لطفاً تمام فیلدهای الزامی را پر کنید.'; + } +} + +// Handle edit plan +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_plan'])) { + $id = (int) $_POST['plan_id']; + $server_id = (int) $_POST['server_id']; + $category_id = (int) $_POST['category_id']; + $name = sanitizeInput($_POST['name']); + $price = (float) $_POST['price']; + $volume_gb = (int) $_POST['volume_gb']; + $duration_days = (int) $_POST['duration_days']; + $description = sanitizeInput($_POST['description']); + $show_sub_link = isset($_POST['show_sub_link']) ? 1 : 0; + $show_conf_links = isset($_POST['show_conf_links']) ? 1 : 0; + + $stmt = pdo()->prepare("UPDATE plans SET server_id=?, category_id=?, name=?, price=?, volume_gb=?, duration_days=?, description=?, show_sub_link=?, show_conf_links=? WHERE id=?"); + if ($stmt->execute([$server_id, $category_id, $name, $price, $volume_gb, $duration_days, $description, $show_sub_link, $show_conf_links, $id])) { + $success = 'پلن بهروزرسانی شد.'; + } else { + $error = 'خطا در بهروزرسانی پلن.'; + } +} + +// Handle delete +if (isset($_GET['delete'])) { + $id = (int) $_GET['delete']; + $stmt = pdo()->prepare("DELETE FROM plans WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'پلن حذف شد.'; + } +} + +// Handle toggle status +if (isset($_GET['toggle'])) { + $id = (int) $_GET['toggle']; + $stmt = pdo()->prepare("UPDATE plans SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'وضعیت پلن تغییر کرد.'; + } +} + +// Get plan for edit +$edit_plan = null; +if (isset($_GET['edit'])) { + $edit_id = (int) $_GET['edit']; + $stmt = pdo()->prepare("SELECT * FROM plans WHERE id = ?"); + $stmt->execute([$edit_id]); + $edit_plan = $stmt->fetch(PDO::FETCH_ASSOC); +} + +// Get servers and categories for form +$servers = getServers(); +$categories = getCategories(); + +// Get all plans with server info +$stmt = pdo()->query(" + SELECT p.*, s.name as server_name, s.type as server_type, c.name as category_name + FROM plans p + LEFT JOIN servers s ON p.server_id = s.id + LEFT JOIN categories c ON p.category_id = c.id + ORDER BY p.is_test_plan DESC, p.id ASC +"); +$plans = $stmt->fetchAll(PDO::FETCH_ASSOC); + +renderHeader('مدیریت پلن‌ها'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+

+
+
+
+ + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + + + + + + +
+ + + + + + لغو + + +
+
+
+ + +
+
+

لیست پلن‌ها ()

+
+
+ +

هیچ پلنی یافت نشد.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
شناسهنامسروردسته‌بندیقیمتحجم/مدتوضعیتعملیات
+ + 🧪 + + + تومانGB / روز + + + ✅ فعال + + ❌ غیرفعال + + + + + + + + + + + +
+
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/web/pages/servers.php b/src/web/pages/servers.php new file mode 100644 index 0000000..3ecda27 --- /dev/null +++ b/src/web/pages/servers.php @@ -0,0 +1,369 @@ +prepare("INSERT INTO servers (name, url, sub_host, marzban_protocols, username, password, type, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'active')"); + if ($stmt->execute([$name, $url, $sub_host, $protocols_json, $username, $password, $type])) { + $success = 'سرور با موفقیت اضافه شد.'; + } else { + $error = 'خطا در افزودن سرور.'; + } + } else { + $error = 'لطفاً تمام فیلدها را پر کنید.'; + } +} + +// Handle edit server +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_server'])) { + $id = (int) $_POST['server_id']; + $name = sanitizeInput($_POST['name']); + $url = sanitizeInput($_POST['url']); + $username = sanitizeInput($_POST['username']); + $password = $_POST['password']; + $type = sanitizeInput($_POST['type']); + $sub_host = sanitizeInput($_POST['sub_host'] ?? ''); + + // Handle protocols for marzban + $protocols_json = null; + if ($type === 'marzban') { + if (isset($_POST['protocols']) && !empty($_POST['protocols'])) { + $protocols = array_map('sanitizeInput', $_POST['protocols']); + $protocols_json = json_encode(array_values($protocols)); + } else { + // Default to VLESS if no protocols selected + $protocols_json = json_encode(['vless']); + } + } + + // Only update password if provided + if (!empty($password)) { + $stmt = pdo()->prepare("UPDATE servers SET name=?, url=?, sub_host=?, marzban_protocols=?, username=?, password=?, type=? WHERE id=?"); + $success = $stmt->execute([$name, $url, $sub_host, $protocols_json, $username, $password, $type, $id]); + } else { + $stmt = pdo()->prepare("UPDATE servers SET name=?, url=?, sub_host=?, marzban_protocols=?, username=?, type=? WHERE id=?"); + $success = $stmt->execute([$name, $url, $sub_host, $protocols_json, $username, $type, $id]); + } + + if ($success) { + $success = 'سرور بهروزرسانی شد.'; + } else { + $error = 'خطا در بهروزرسانی سرور.'; + } +} + +// Handle delete +if (isset($_GET['delete'])) { + $id = (int) $_GET['delete']; + $stmt = pdo()->prepare("DELETE FROM servers WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'سرور حذف شد.'; + } +} + +// Handle toggle status +if (isset($_GET['toggle'])) { + $id = (int) $_GET['toggle']; + $stmt = pdo()->prepare("UPDATE servers SET status = IF(status = 'active', 'inactive', 'active') WHERE id = ?"); + if ($stmt->execute([$id])) { + $success = 'وضعیت سرور تغییر کرد.'; + } +} + +// Get server for edit +$edit_server = null; +if (isset($_GET['edit'])) { + $edit_id = (int) $_GET['edit']; + $stmt = pdo()->prepare("SELECT * FROM servers WHERE id = ?"); + $stmt->execute([$edit_id]); + $edit_server = $stmt->fetch(PDO::FETCH_ASSOC); +} + +// Get all servers +$stmt = pdo()->query("SELECT * FROM servers ORDER BY id DESC"); +$servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + +renderHeader('مدیریت سرورها'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+

+

+
+
+
+ + + + +
+
+ + +
+ +
+ + +
+
+ +
+ + + بدون / در انتها +
+ + + + + + + + + +
+
+ + +
+ +
+ + > +
+
+ + + + + + لغو + + +
+
+
+ + +
+
+

لیست سرورها ()

+
+
+ +

هیچ سروری یافت نشد.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
شناسهنامنوعآدرسپروتکل‌ها / ویژگی‌هاوضعیتعملیات
+ '🔷 Marzban', + 'sanaei' => '🔶 Sanaei', + 'marzneshin' => '🔵 Marzneshin' + ]; + echo $type_labels[$server['type']] ?? $server['type']; + ?> + + + + + '; + foreach ($protocols as $protocol) { + echo '' . htmlspecialchars($protocol) . ''; + } + echo ''; + } else { + echo 'همه'; + } + ?> + + 🔗 Custom Sub + + + + + + ✅ فعال + + ❌ غیرفعال + + + + + + + + + + + +
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/web/pages/settings.php b/src/web/pages/settings.php new file mode 100644 index 0000000..eb32c8a --- /dev/null +++ b/src/web/pages/settings.php @@ -0,0 +1,333 @@ + sanitizeInput($_POST['card_number'] ?? ''), + 'card_holder' => sanitizeInput($_POST['card_owner_name'] ?? ''), + 'copy_enabled' => true + ]; + + // Verification + $settings_to_update['verification_method'] = $_POST['verification_method'] ?? 'off'; + $settings_to_update['verification_iran_only'] = $_POST['verification_iran_only'] ?? 'off'; + + // Test config + $settings_to_update['test_config_usage_limit'] = (int) ($_POST['test_config_usage_limit'] ?? 1); + + // Notifications + $settings_to_update['notification_expire_status'] = $_POST['notification_expire_status'] ?? 'off'; + $settings_to_update['notification_expire_days'] = (int) ($_POST['notification_expire_days'] ?? 3); + $settings_to_update['notification_expire_gb'] = (int) ($_POST['notification_expire_gb'] ?? 1); + $settings_to_update['notification_inactive_status'] = $_POST['notification_inactive_status'] ?? 'off'; + $settings_to_update['notification_inactive_days'] = (int) ($_POST['notification_inactive_days'] ?? 30); + + // Renewal + $settings_to_update['renewal_status'] = $_POST['renewal_status'] ?? 'off'; + + saveSettings($settings_to_update); + $success = 'تنظیمات با موفقیت ذخیره شد.'; +} + +// Get current settings +$settings = getSettings(); + +// Extract card info from payment_method +$card_number = $settings['payment_method']['card_number'] ?? ''; +$card_owner_name = $settings['payment_method']['card_holder'] ?? ''; + +renderHeader('تنظیمات'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + +
+ +
+
+

تنظیمات ربات

+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+
+

کانال اجباری

+
+
+
+
+ + +
+ +
+ + +
+
+
+
+ + +
+
+

اطلاعات کارت بانکی

+
+
+
+
+ + + برای پرداخت دستی +
+ +
+ + +
+
+
+
+ + +
+
+

هدیه خوش‌آمدگویی

+
+
+
+ + +
+
+
+ + +
+
+

درگاه پرداخت

+
+
+
+
+ + +
+ +
+ + +
+
+
+
+ + +
+
+

احراز هویت

+
+
+
+
+ + +
+ +
+ + +
+
+
+
+ + +
+
+

کانفیگ تست

+
+
+
+ + +
+
+ +

با کلیک روی دکمه زیر، شمارنده دریافت کانفیگ تست برای تمام کاربران به صفر بازنشانی می‌شود.

+ + ریست کردن دریافت‌های تمام کاربران + +
+
+
+ + +
+
+

اعلان‌ها

+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ + +
+ +
+ + +
+
+
+
+ + +
+
+

تمدید سرویس

+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + \ No newline at end of file diff --git a/src/web/pages/stats.php b/src/web/pages/stats.php new file mode 100644 index 0000000..58beee4 --- /dev/null +++ b/src/web/pages/stats.php @@ -0,0 +1,220 @@ +query("SELECT COUNT(*) FROM users"); +$stats['total_users'] = $stmt->fetchColumn(); + +$stmt = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'active'"); +$stats['active_users'] = $stmt->fetchColumn(); + +$stmt = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'banned'"); +$stats['banned_users'] = $stmt->fetchColumn(); + +// Services stats +$stmt = pdo()->query("SELECT COUNT(*) FROM services"); +$stats['total_services'] = $stmt->fetchColumn(); + +$now = time(); +$stmt = pdo()->prepare("SELECT COUNT(*) FROM services WHERE expire_timestamp > ?"); +$stmt->execute([$now]); +$stats['active_services'] = $stmt->fetchColumn(); + +$stmt = pdo()->prepare("SELECT COUNT(*) FROM services WHERE expire_timestamp <= ?"); +$stmt->execute([$now]); +$stats['expired_services'] = $stmt->fetchColumn(); + +// Income stats +// Calculate income stats (using payment_requests for stability) +$income_stats = [ + 'today' => 0, + 'week' => 0, + 'month' => 0, + 'year' => 0 +]; + +try { + // Today + $stmt = pdo()->prepare("SELECT SUM(amount) FROM payment_requests WHERE status = 'approved' AND DATE(processed_at) = CURDATE()"); + $stmt->execute(); + $income_stats['today'] = $stmt->fetchColumn() ?: 0; + + // Week + $stmt = pdo()->prepare("SELECT SUM(amount) FROM payment_requests WHERE status = 'approved' AND YEARWEEK(processed_at, 1) = YEARWEEK(CURDATE(), 1)"); + $stmt->execute(); + $income_stats['week'] = $stmt->fetchColumn() ?: 0; + + // Month + $stmt = pdo()->prepare("SELECT SUM(amount) FROM payment_requests WHERE status = 'approved' AND MONTH(processed_at) = MONTH(CURDATE()) AND YEAR(processed_at) = YEAR(CURDATE())"); + $stmt->execute(); + $income_stats['month'] = $stmt->fetchColumn() ?: 0; + + // Year + $stmt = pdo()->prepare("SELECT SUM(amount) FROM payment_requests WHERE status = 'approved' AND YEAR(processed_at) = YEAR(CURDATE())"); + $stmt->execute(); + $income_stats['year'] = $stmt->fetchColumn() ?: 0; +} catch (Exception $e) { + // Keep defaults +} + +renderHeader('آمار و گزارشات'); +?> + +
+ + +
+ + +
+ +
+
+

آمار کاربران

+
+
+
+
+
+ +
+
+
+
کل کاربران
+
+
+ +
+
+ +
+
+
+
کاربران فعال
+
+
+ +
+
+ +
+
+
+
کاربران مسدود
+
+
+
+
+
+ + +
+
+

آمار سرویس‌ها

+
+
+
+
+
+ +
+
+
+
کل سرویس‌ها
+
+
+ +
+
+ +
+
+
+
سرویس‌های فعال
+
+
+ +
+
+ +
+
+
+
سرویس‌های منقضی شده
+
+
+
+
+
+ + +
+
+

آمار درآمد

+
+
+
+
+
+ +
+
+
+
درآمد امروز (تومان)
+
+
+ +
+
+ +
+
+
+
درآمد هفته (تومان)
+
+
+ +
+
+ +
+
+
+
درآمد ماه (تومان)
+
+
+ +
+
+ +
+
+
+
درآمد سال (تومان)
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/web/pages/users.php b/src/web/pages/users.php new file mode 100644 index 0000000..f8b7efd --- /dev/null +++ b/src/web/pages/users.php @@ -0,0 +1,405 @@ +query("SELECT COUNT(*) FROM users")->fetchColumn(); +$stats['active'] = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'active'")->fetchColumn(); +$stats['banned'] = pdo()->query("SELECT COUNT(*) FROM users WHERE status = 'banned'")->fetchColumn(); + +// Pagination for user list +$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1; +$per_page = 20; +$offset = ($page - 1) * $per_page; + +// Get total users count +$total_users = $stats['total']; +$total_pages = ceil($total_users / $per_page); + +// Get all users with pagination +$stmt = pdo()->prepare(" + SELECT u.*, + COUNT(DISTINCT s.id) as service_count + FROM users u + LEFT JOIN services s ON u.chat_id = s.owner_chat_id + GROUP BY u.chat_id + ORDER BY u.created_at DESC + LIMIT ? OFFSET ? +"); +$stmt->execute([$per_page, $offset]); +$all_users = $stmt->fetchAll(PDO::FETCH_ASSOC); + +// Handle search +if (isset($_GET['search'])) { + $chat_id = $_GET['chat_id'] ?? ''; + + if (!empty($chat_id) && is_numeric($chat_id)) { + $stmt = pdo()->prepare("SELECT * FROM users WHERE chat_id = ?"); + $stmt->execute([$chat_id]); + $user_info = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$user_info) { + $error = 'کاربر یافت نشد.'; + } + } +} + +// Handle add balance +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_balance'])) { + $chat_id = (int) $_POST['chat_id']; + $amount = (int) $_POST['amount']; + + if ($amount != 0) { + updateUserBalance($chat_id, abs($amount), $amount > 0 ? 'add' : 'subtract'); + $success = 'موجودی کاربر به‌روزرسانی شد.'; + + // Refresh user info if viewing a user + if (isset($_GET['search']) && isset($_GET['chat_id'])) { + $stmt = pdo()->prepare("SELECT * FROM users WHERE chat_id = ?"); + $stmt->execute([$_GET['chat_id']]); + $user_info = $stmt->fetch(PDO::FETCH_ASSOC); + } + } +} + +// Handle bulk operations +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_add_volume'])) { + $volume_gb = (int) $_POST['volume_gb']; + if ($volume_gb > 0) { + require_once __DIR__ . '/../../includes/functions.php'; + $result = addVolumeToAllServices($volume_gb); + $success = "✅ حجم همگانی اضافه شد. موفق: {$result['success']} | ناموفق: {$result['fail']}"; + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_add_time'])) { + $days = (int) $_POST['days']; + if ($days > 0) { + require_once __DIR__ . '/../../includes/functions.php'; + $result = addTimeToAllServices($days); + $success = "✅ زمان همگانی اضافه شد. موفق: {$result['success']} | ناموفق: {$result['fail']}"; + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bulk_add_balance'])) { + $amount = (int) $_POST['amount']; + if ($amount > 0) { + $stmt = pdo()->query("SELECT chat_id FROM users WHERE status = 'active'"); + $users = $stmt->fetchAll(PDO::FETCH_ASSOC); + + $count = 0; + foreach ($users as $user) { + updateUserBalance($user['chat_id'], $amount, 'add'); + $count++; + } + + $success = "✅ موجودی $count کاربر فعال افزایش یافت."; + } +} + +if (isset($_GET['ban'])) { + $chat_id = (int) $_GET['ban']; + setUserStatus($chat_id, 'banned'); + $success = 'کاربر مسدود شد.'; + header('Location: ?'); + exit; +} + +if (isset($_GET['unban'])) { + $chat_id = (int) $_GET['unban']; + setUserStatus($chat_id, 'active'); + $success = 'کاربر آزاد شد.'; + header('Location: ?'); + exit; +} + +renderHeader('مدیریت کاربران'); +?> + +
+ + +
+ + +
+ +
+ + + +
+ + + +
+
+
+ +
+
+
+
کل کاربران
+
+
+ +
+
+ +
+
+
+
کاربران فعال
+
+
+ +
+
+ +
+
+
+
کاربران مسدود
+
+
+
+ + +
+
+

عملیات همگانی

+
+
+
+ +
+ + + + حجم مشخص شده به تمام سرویس‌ها اضافه می‌شود +
+ + +
+ + + + روزهای مشخص شده به تاریخ انقضای تمام سرویس‌ها اضافه + می‌شود +
+ + +
+ + + + مبلغ مشخص شده به موجودی تمام کاربران فعال اضافه + می‌شود +
+
+
+
+ + +
+
+

جستجوی کاربر

+
+
+
+
+ + +
+ +
+
+
+ + + +
+
+

اطلاعات کاربر

+
+
+

نام:

+

شناسه:

+

موجودی: تومان

+

وضعیت: + + ✅ فعال + + ❌ مسدود + +

+

تاریخ ثبت‌نام:

+ +
+ + +
+ +
+ +
+ +
+ + + + مسدود کردن + + + + آزاد کردن + + + + + +

سرویس‌های کاربر ()

+ +

کاربر هیچ سرویسی ندارد.

+ + + + + + + + + + + + + + + + + + + + +
پلننام سرویستاریخ خریدانقضا
+ +
+ +
+
+ + + +
+
+

لیست تمام کاربران ( نفر) +

+
+
+ +

هیچ کاربری یافت نشد.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
نامChat IDموجودیتعداد سرویسوضعیتتاریخ عضویتعملیات
تومان + + ✅ فعال + + ❌ مسدود + + + + مشاهده + +
+
+ + + 1): ?> +
+ + + + + + + + + + + +
+ + +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/web/user/AUTH-FLOW-CHANGES.md b/src/web/user/AUTH-FLOW-CHANGES.md new file mode 100644 index 0000000..4eba09a --- /dev/null +++ b/src/web/user/AUTH-FLOW-CHANGES.md @@ -0,0 +1,209 @@ +# تغییرات ایجاد شده در فلوی احراز هویت + +## مشکل قبلی +کد قبلی در `index.php` به این صورت بود: + +```php +require_once __DIR__ . '/session.php'; +// ... handle POST request ... +requireUserLogin(); // ← این خط فوراً صفحه را می‌بست! +``` + +**نتیجه:** اگر کاربر لاگین نبود، PHP فوراً با پیام "Access Denied" صفحه را می‌بست و کد JavaScript هرگز اجرا نمی‌شد! + +--- + +## راه‌حل جدید + +### تغییرات در index.php + +#### 1. حذف `requireUserLogin()` +به جای die کردن، حالا چک می‌کنیم که آیا کاربر لاگین است یا خیر: + +```php +$isLoggedIn = isUserLoggedIn(); + +if ($isLoggedIn) { + $user = getCurrentUser(); + $services = getUserServices($user['chat_id']); + // ... محاسبه آمار +} else { + // مقادیر پیش‌فرض + $user = ['first_name' => 'کاربر', 'chat_id' => 0, 'balance' => 0]; + $services = []; + $total_services = 0; + $active_services = 0; + $expired_services = 0; + $recent_services = []; +} +``` + +#### 2. نمایش Loading Screen +اگر کاربر لاگین نیست، صفحه loading نمایش می‌دهد: + +```html +
+
+

در حال احراز هویت...

+
+``` + +#### 3. مخفی کردن محتوا +محتوای اصلی تا زمان لاگین مخفی است: + +```html +
+ +
+``` + +#### 4. احراز هویت خودکار با JavaScript +JavaScript بعد از لود صفحه، تلاش می‌کند کاربر را احراز هویت کند: + +```javascript +const isLoggedIn = ; + +if (!isLoggedIn) { + if (tg.initData) { + // ارسال درخواست برای احراز هویت + fetch('index.php', { + method: 'POST', + body: 'initData=' + encodeURIComponent(tg.initData) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + window.location.reload(); // رفرش صفحه بعد از موفقیت + } else { + // نمایش خطا + showError('خطا در احراز هویت'); + } + }); + } else { + // نمایش پیام: باید از تلگرام باز شود + showError('لطفاً از داخل ربات تلگرام وارد شوید'); + } +} +``` + +--- + +## مزایای روش جدید + +✅ **صفحه لود می‌شود**: دیگر "Access Denied" نمایش داده نمی‌شود +✅ **Loading زیبا**: کاربر یک صفحه loading حرفه‌ای می‌بیند +✅ **خطاهای دقیق**: اگر مشکلی باشد، دقیقاً می‌گوید چیست +✅ **تست آسان**: می‌توانید مشکل احراز هویت را راحت‌تر debug کنید + +--- + +## فلوی کامل احراز هویت + +``` +1. کاربر از تلگرام لینک را باز می‌کند + ↓ +2. PHP چک می‌کند: آیا session دارد؟ + ├─ بله → محتوا نمایش داده می‌شود + └─ خیر → loading نمایش داده می‌شود + ↓ +3. JavaScript اجرا می‌شود + ↓ +4. initData از تلگرام دریافت می‌شود + ↓ +5. POST request به index.php ارسال می‌شود + ↓ +6. PHP در auth.php، initData را validate می‌کند + ├─ معتبر → session ذخیره + return success + └─ نامعتبر → return error + ↓ +7. JavaScript نتیجه را دریافت می‌کند + ├─ موفق → صفحه reload می‌شود + └─ ناموفق → پیام خطا نمایش داده می‌شود +``` + +--- + +## نکات مهم برای عیب‌یابی + +### اگر هنوز "خطا در احراز هویت" می‌دهد: + +#### 1. بررسی Console مرورگر +- F12 را بزنید +- در tab **Console** ببینید چه خطایی هست +- باید log های زیر را ببینید: + ``` + Attempting authentication... + Authentication successful, reloading... + ``` + +#### 2. بررسی Network Tab +- F12 → Network +- فیلتر "XHR" را انتخاب کنید +- ببینید POST request به index.php چه response می‌دهد + - 200 + `{"success": true}` → موفق + - 401 + `{"success": false, "error": "..."}` → validation شکست خورد + +#### 3. بررسی BOT_TOKEN +```bash +# در سرور +cat /path/to/includes/config.php | grep BOT_TOKEN +``` + +باید توکن واقعی ربات را نشان دهد، نه 'TOKEN' + +#### 4. بررسی تنظیمات BotFather +- به [@BotFather](https://t.me/BotFather) بروید +- `/mybots` → انتخاب ربات → Bot Settings → Menu Button +- مطمئن شوید URL دقیقاً با دامنه شما مطابقت دارد + +--- + +## صفحات دیگر + +**توجه:** صفحات زیر هنوز از روش قدیمی استفاده می‌کنند: +- `services.php` +- `shop.php` +- `wallet.php` +- `support.php` +- `guides.php` +- `account.php` +- `renew.php` + +**گزینه 1:** می‌توانید از `requireUserLogin()` استفاده کنید چون فقط index.php باید از طریق تلگرام باز شود + +**گزینه 2:** اگر می‌خواهید همه صفحات به صورت مستقل کار کنند، باید همین منطق را در همه صفحات پیاده کنید + +--- + +## تست کنید + +بعد از آپلود فایل جدید: + +1. Session را پاک کنید: + ```javascript + // در Console مرورگر + sessionStorage.clear(); + localStorage.clear(); + ``` + +2. صفحه را از تلگرام باز کنید + +3. باید یکی از این‌ها را ببینید: + - ✅ Loading → بعد از 1-2 ثانیه رفرش و نمایش محتوا + - ❌ Loading → پیام خطا (که حداقل نشان می‌دهد مشکل دقیقاً چیست) + +4. Console مرورگر را بررسی کنید برای جزئیات + +--- + +## نتیجه‌گیری + +با این تغییرات: +- **مشکل "Access Denied" حل شد** چون صفحه دیگر فوراً نمی‌میرد +- **تجربه کاربری بهتر** با loading screen +- **Debug آسان‌تر** با پیام‌های دقیق + +اگر هنوز مشکل دارید، لطفاً: +1. Screenshot از Console را بفرستید +2. Response از Network tab را بفرستید +3. بگویید دقیقاً چه پیامی نمایش می‌دهد diff --git a/src/web/user/OPENLITESPEED-GUIDE.md b/src/web/user/OPENLITESPEED-GUIDE.md new file mode 100644 index 0000000..47d697c --- /dev/null +++ b/src/web/user/OPENLITESPEED-GUIDE.md @@ -0,0 +1,321 @@ +# راهنمای حل مشکل 404 در OpenLiteSpeed + +## مشکل فعلی +خطای 404 هنگام دسترسی به فایل‌های پروژه در OpenLiteSpeed + +--- + +## راه‌حل‌های گام‌به‌گام + +### مرحله 1️⃣: تست دسترسی به فایل‌ها + +ابتدا بررسی کنید که آیا فایل‌ها اصلاً در دسترس هستند: + +**الف) تست فایل HTML ساده:** +``` +https://your-domain.com/web/user/test-simple.html +``` + +- ✅ اگر باز شد → فایل‌ها آپلود شده‌اند و مسیر درست است +- ❌ اگر 404 داد → مشکل در مسیر یا آپلود فایل‌ها + +**ب) تست فایل PHP:** +``` +https://your-domain.com/web/user/test-php.php +``` + +- ✅ اگر باز شد → PHP کار می‌کند +- ❌ اگر 404 داد → مشکل در تنظیمات OpenLiteSpeed یا مسیر + +--- + +### مرحله 2️⃣: بررسی ساختار پوشه‌ها + +ساختار صحیح باید این‌طور باشد: + +``` +/home/username/public_html/ (یا هر document root دیگر) +│ +├── src/ +│ ├── includes/ +│ │ ├── config.php +│ │ ├── db.php +│ │ └── functions.php +│ │ +│ ├── web/ +│ │ ├── .htaccess +│ │ ├── index.php +│ │ ├── dashboard.php +│ │ │ +│ │ ├── user/ +│ │ │ ├── .htaccess +│ │ │ ├── index.php +│ │ │ ├── test-simple.html +│ │ │ ├── test-php.php +│ │ │ └── ... (سایر فایل‌ها) +│ │ │ +│ │ └── pages/ +│ │ └── ... +│ │ +│ └── data/ +│ └── ... +``` + +**در سرور این دستور را اجرا کنید:** +```bash +ls -la /path/to/your/public_html/src/web/user/ +``` + +باید فایل‌های `test-simple.html` و `test-php.php` را ببینید. + +--- + +### مرحله 3️⃣: بررسی و تنظیم مجوزها (Permissions) + +**مشکل رایج در OpenLiteSpeed**: مجوزهای نادرست فایل‌ها + +#### تنظیم مجوزهای صحیح: + +```bash +# رفتن به پوشه اصلی پروژه +cd /path/to/your/public_html/src + +# تنظیم مجوزها برای فایل‌ها (644) +find . -type f -exec chmod 644 {} \; + +# تنظیم مجوزها برای پوشه‌ها (755) +find . -type d -exec chmod 755 {} \; + +# تنظیم مجوز پوشه data برای نوشتن +chmod -R 775 data/ +``` + +#### بررسی Owner (مالک فایل‌ها): + +```bash +# بررسی کنید که مالک فایل‌ها کیست +ls -la /path/to/your/public_html/src/web/user/ + +# باید مالک nobody:nogroup یا username:username باشد +# اگر نبود، تغییر دهید: +chown -R nobody:nogroup /path/to/your/public_html/src/ +# یا +chown -R username:username /path/to/your/public_html/src/ +``` + +--- + +### مرحله 4️⃣: تنظیمات OpenLiteSpeed + +#### الف) بررسی Virtual Host Configuration + +1. وارد پنل OpenLiteSpeed شوید: + ``` + https://your-server-ip:7080 + ``` + +2. به **Virtual Hosts** → **[your-domain]** بروید + +3. بررسی کنید که: + - **Document Root** درست تنظیم شده (مثلاً `/home/username/public_html/src/web`) + - **Enable Scripts/ExtApps** روی `Yes` باشد + +#### ب) فعال‌سازی Rewrite در OpenLiteSpeed + +OpenLiteSpeed به طور پیش‌فرض از `.htaccess` پشتیبانی می‌کند، اما باید فعال باشد: + +1. در پنل OpenLiteSpeed: + - **Virtual Hosts** → **[your-domain]** → **Rewrite** + +2. مطمئن شوید که: + - **Enable Rewrite**: `Yes` + - **Auto Load from .htaccess**: `Yes` + +#### ج) راه‌اندازی مجدد OpenLiteSpeed + +بعد از هر تغییر، OpenLiteSpeed را restart کنید: + +```bash +# روش 1 +systemctl restart lsws + +# روش 2 +/usr/local/lsws/bin/lswsctrl restart + +# روش 3 (Graceful Restart - بدون قطع سرویس) +/usr/local/lsws/bin/lswsctrl graceful +``` + +--- + +### مرحله 5️⃣: بررسی فایل .htaccess + +فایل `.htaccess` در OpenLiteSpeed ممکن است نیاز به تنظیمات خاصی داشته باشد. + +#### بررسی/ایجاد `.htaccess` در پوشه `web/`: + +```apache +# Protect web panel files + + Order Allow,Deny + Allow from all + + +# Prevent directory listing +Options -Indexes + +# Default document +DirectoryIndex index.php index.html + +# Security headers + + Header set X-Content-Type-Options "nosniff" + Header set X-Frame-Options "SAMEORIGIN" + Header set X-XSS-Protection "1; mode=block" + + +# PHP settings (OpenLiteSpeed compatible) + + php_value max_execution_time 300 + php_value memory_limit 256M + php_value upload_max_filesize 10M + php_value post_max_size 10M + +``` + +#### بررسی/ایجاد `.htaccess` در پوشه `web/user/`: + +```apache +# Disable caching for user panel + + Header set Cache-Control "no-cache, no-store, must-revalidate" + Header set Pragma "no-cache" + Header set Expires 0 + + +# Set default charset +AddDefaultCharset UTF-8 + +# Default document +DirectoryIndex index.php + +# PHP settings + + php_value max_execution_time 300 + php_value memory_limit 256M + +``` + +--- + +### مرحله 6️⃣: بررسی Log ها + +اگر هنوز مشکل دارید، log ها را بررسی کنید: + +```bash +# Error log سرور +tail -f /usr/local/lsws/logs/error.log + +# Access log +tail -f /usr/local/lsws/logs/access.log + +# Virtual Host error log (اگر جداگانه تنظیم کرده‌اید) +tail -f /path/to/your/vhost/logs/error.log +``` + +--- + +### مرحله 7️⃣: تنظیمات PHP در OpenLiteSpeed + +اگر PHP کار نمی‌کند: + +#### الف) بررسی PHP Handler + +1. در پنل OpenLiteSpeed: + - **Server Configuration** → **External App** + +2. مطمئن شوید که `lsphp` به درستی تنظیم شده + +#### ب) بررسی Script Handler + +1. **Server Configuration** → **Script Handler** +2. مطمئن شوید که suffixes شامل `.php` است + +--- + +## چک‌لیست سریع برای عیب‌یابی 404 + +- [ ] فایل‌ها در مسیر صحیح آپلود شده‌اند +- [ ] مجوزها درست است (644 برای فایل‌ها، 755 برای پوشه‌ها) +- [ ] Owner فایل‌ها درست است (nobody:nogroup یا user:user) +- [ ] Document Root در Virtual Host درست تنظیم شده +- [ ] Rewrite در Virtual Host فعال است +- [ ] Auto Load from .htaccess روشن است +- [ ] OpenLiteSpeed راه‌اندازی مجدد شده +- [ ] فایل `.htaccess` موجود و صحیح است +- [ ] PHP Handler درست کار می‌کند +- [ ] SSL/HTTPS فعال است (برای WebApp تلگرام ضروری) + +--- + +## تست نهایی + +بعد از انجام تنظیمات بالا، به ترتیب این URL ها را تست کنید: + +1. `https://your-domain.com/web/user/test-simple.html` + - باید صفحه سبز رنگ نمایش دهد + +2. `https://your-domain.com/web/user/test-php.php` + - باید اطلاعات سرور و PHP را نمایش دهد + +3. `https://your-domain.com/web/user/index.php` + - باید پنل کاربری یا صفحه لاگین را نمایش دهد + +--- + +## نکات مهم برای OpenLiteSpeed + +### 1. تفاوت با Apache +- OpenLiteSpeed از `.htaccess` پشتیبانی می‌کند، اما باید فعال شود +- برخی دستورات Apache در OLS کار نمی‌کنند (مثلاً `php_value` در برخی موارد) + +### 2. Cache +OpenLiteSpeed دارای cache قوی است که ممکن است مشکل ایجاد کند: + +```bash +# Flush cache +/usr/local/lsws/bin/lswsctrl flush +``` + +### 3. Session در OpenLiteSpeed +مطمئن شوید که پوشه session قابل نوشتن است: + +```bash +# بررسی مسیر session +php -i | grep session.save_path + +# تنظیم مجوز +chmod 1733 /path/to/session/dir +``` + +--- + +## کمک بیشتر + +اگر هنوز مشکل حل نشد: + +1. خروجی این دستورات را بفرستید: +```bash +ls -la /path/to/web/user/ +cat /usr/local/lsws/logs/error.log | tail -n 50 +php -v +``` + +2. اسکرین‌شات از: + - تنظیمات Virtual Host در پنل OLS + - خطای دقیق 404 + +3. بگویید که از کدام روش این URL را باز می‌کنید: + - مستقیماً از مرورگر + - از WebApp تلگرام + - از curl یا ابزار دیگر diff --git a/src/web/user/README-QUICK-FIX.md b/src/web/user/README-QUICK-FIX.md new file mode 100644 index 0000000..f2c9517 --- /dev/null +++ b/src/web/user/README-QUICK-FIX.md @@ -0,0 +1,98 @@ +# راهنمای سریع رفع خطای 404 در OpenLiteSpeed + +## فایل‌های کمکی ایجاد شده + +### 1. فایل‌های تست +- `test-simple.html` - تست فایل HTML ساده +- `test-php.php` - تست PHP و نمایش تنظیمات سرور +- `test-telegram-auth.php` - تست احراز هویت تلگرام + +### 2. راهنماها +- `OPENLITESPEED-GUIDE.md` - راهنمای جامع OpenLiteSpeed +- `TROUBLESHOOTING-ACCESS-DENIED.md` - راهنمای رفع خطای Access Denied + +### 3. اسکریپت نصب +- `setup-openlitespeed.sh` - اسکریپت تنظیم خودکار مجوزها + +--- + +## راه‌حل سریع (5 دقیقه) + +### قدم 1: آپلود فایل‌ها +همه فایل‌های پروژه را در سرور آپلود کنید. + +### قدم 2: تنظیم مجوزها +در سرور این دستورات را اجرا کنید: + +```bash +cd /path/to/your/project/src + +# اجرای اسکریپت تنظیم خودکار +chmod +x setup-openlitespeed.sh +./setup-openlitespeed.sh +``` + +یا به صورت دستی: + +```bash +# تنظیم مجوزها +find . -type f -exec chmod 644 {} \; +find . -type d -exec chmod 755 {} \; +chmod -R 775 data/ + +# تنظیم owner +chown -R nobody:nogroup . + +# راه‌اندازی مجدد +systemctl restart lsws +``` + +### قدم 3: تست +به ترتیب این لینک‌ها را باز کنید: + +1. `https://your-domain.com/web/user/test-simple.html` +2. `https://your-domain.com/web/user/test-php.php` +3. `https://your-domain.com/web/user/index.php` + +--- + +## چک‌لیست + +- [ ] فایل‌ها آپلود شدند +- [ ] مجوزها تنظیم شد (644 برای files، 755 برای folders) +- [ ] Owner درست است (nobody:nogroup) +- [ ] OpenLiteSpeed راه‌اندازی مجدد شد +- [ ] فایل config.php وجود دارد و BOT_TOKEN و BASE_URL تنظیم شده +- [ ] SSL/HTTPS فعال است +- [ ] دامنه در BotFather تنظیم شده + +--- + +## اگر هنوز 404 می‌ده + +1. Log ها را بررسی کنید: +```bash +tail -f /usr/local/lsws/logs/error.log +``` + +2. بررسی کنید که Virtual Host درست تنظیم شده: + - پنل OLS: `https://server-ip:7080` + - Virtual Hosts → Document Root را بررسی کنید + +3. Rewrite را فعال کنید: + - Virtual Hosts → Rewrite + - Enable Rewrite: Yes + - Auto Load from .htaccess: Yes + +--- + +## پشتیبانی + +برای کمک بیشتر، خروجی این دستورات را بفرستید: + +```bash +ls -la /path/to/web/user/ +cat /usr/local/lsws/logs/error.log | tail -n 50 +php -v +cat /path/to/includes/config.php | grep -E "BOT_TOKEN|BASE_URL" +``` diff --git a/src/web/user/TROUBLESHOOTING-ACCESS-DENIED.md b/src/web/user/TROUBLESHOOTING-ACCESS-DENIED.md new file mode 100644 index 0000000..397cc7e --- /dev/null +++ b/src/web/user/TROUBLESHOOTING-ACCESS-DENIED.md @@ -0,0 +1,154 @@ +# راهنمای رفع مشکل "Access Denied. Please open from Telegram" + +## مشکل +پس از انتقال پروژه به سرور اصلی با دامنه جدید، پیام خطای زیر نمایش داده می‌شود: +``` +Access Denied. Please open from Telegram. +``` + +## علت‌های احتمالی + +### 1. عدم تنظیم دامنه در BotFather (احتمال بالا ⭐⭐⭐⭐⭐) +تلگرام برای امنیت، فقط اجازه می‌دهد Web App از دامنه‌های مشخص شده باز شود. + +**راه حل:** +1. در تلگرام به [@BotFather](https://t.me/BotFather) پیام دهید +2. دستور `/mybots` را ارسال کنید +3. ربات خود را انتخاب کنید +4. گزینه **Bot Settings** را انتخاب کنید +5. گزینه **Menu Button** را انتخاب کنید +6. گزینه **Edit Menu Button URL** را انتخاب کنید +7. URL جدید خود را وارد کنید (مثال: `https://your-domain.com/web/user/`) + +**نکته مهم:** URL باید با `https://` شروع شود و دامنه باید دارای گواهی SSL معتبر باشد. + +--- + +### 2. مشکل با تنظیمات BASE_URL در config (احتمال متوسط ⭐⭐⭐) +اگر BASE_URL در فایل کانفیگ به درستی تنظیم نشده باشد،ممکن است مشکلاتی پیش بیاید. + +**بررسی:** +- فایل: `src/includes/config.php` +- خط 14: بررسی کنید که `BASE_URL` دقیقاً با دامنه واقعی شما مطابقت داشته باشد +- **بدون اسلش در انتها**: ❌ `https://domain.com/` | ✅ `https://domain.com` + +**مثال صحیح:** +```php +define('BASE_URL', 'https://your-domain.com'); +``` + +--- + +### 3. مشکل Session در PHP (احتمال متوسط ⭐⭐⭐) +ممکن است تنظیمات session در سرور جدید درست کار نکند. + +**راه حل:** +1. بررسی کنید که پوشه `session` قابل نوشتن باشد +2. اضافه کردن تنظیمات زیر به `.htaccess`: + +```apache +# Session settings +php_value session.cookie_secure 1 +php_value session.cookie_httponly 1 +php_value session.cookie_samesite Lax +``` + +--- + +### 4. مشکل HTTPS/SSL (احتمال بالا ⭐⭐⭐⭐) +تلگرام فقط از دامنه‌های با HTTPS معتبر پشتیبانی می‌کند. + +**بررسی:** +- آیا دامنه شما گواهی SSL معتبر دارد؟ +- آیا وقتی به `https://your-domain.com` می‌روید، قفل سبز نمایش می‌دهد؟ +- آیا certificate از یک CA معتبر صادر شده (مثل Let's Encrypt)؟ + +**راه حل:** +اگر SSL ندارید، از Let's Encrypt استفاده کنید (رایگان): +```bash +# برای cPanel +# از SSL/TLS Manager استفاده کنید و Let's Encrypt را فعال کنید +``` + +--- + +### 5. عدم همخوانی BOT_TOKEN (احتمال پایین ⭐⭐) +اگر BOT_TOKEN در سرور جدید اشتباه باشد، احراز هویت شکست می‌خورد. + +**بررسی:** +- فایل: `src/includes/config.php` +- خط 7: بررسی کنید که `BOT_TOKEN` دقیقاً با توکن ربات شما مطابقت دارد + +--- + +## گام‌های عیب‌یابی (به ترتیب اولویت) + +### مرحله 1: تست صفحه دیباگ +1. فایل `test-telegram-auth.php` را در سرور آپلود کنید (در پوشه `web/user/`) +2. از طریق ربات تلگرام، به آدرس زیر بروید: + ``` + https://your-domain.com/web/user/test-telegram-auth.php + ``` +3. اطلاعات نمایش داده شده را بررسی کنید: + - آیا `initData` خالی است؟ → مشکل با تنظیمات BotFather + - آیا `BOT_TOKEN` تنظیم شده؟ + - آیا HTTPS فعال است؟ + +### مرحله 2: بررسی تنظیمات BotFather +1. به BotFather بروید و Menu Button URL را بررسی کنید +2. مطمئن شوید که URL دقیقاً با دامنه سرور شما مطابقت دارد +3. URL باید به صفحه `index.php` در پوشه `web/user/` اشاره کند + +### مرحله 3: بررسی فایل config.php +```php +// باید این‌طور باشد: +define('BOT_TOKEN', 'اینجا_توکن_واقعی_ربات_شما'); +define('BASE_URL', 'https://your-actual-domain.com'); // بدون / در انتها +``` + +### مرحله 4: بررسی SSL +از سایت [SSL Labs](https://www.ssllabs.com/ssltest/) وضعیت SSL دامنه خود را بررسی کنید. + +--- + +## راه‌حل سریع (Quick Fix) + +اگر می‌خواهید سریع تست کنید که آیا مشکل از BotFather است یا خیر: + +1. در فایل `session.php` (خط 25)، موقتاً کد زیر را کامنت کنید: +```php +// die('Access Denied. Please open from Telegram.'); +``` + +2. به جای آن، این کد را اضافه کنید: +```php +// Temporary: Show detailed error +echo "
";
+echo "Session Data: ";
+print_r($_SESSION);
+echo "\n\nServer Data: ";
+echo "HTTPS: " . (isset($_SERVER['HTTPS']) ? 'Yes' : 'No') . "\n";
+echo "Host: " . $_SERVER['HTTP_HOST'] . "\n";
+echo "
"; +die(); +``` + +3. صفحه را از تلگرام باز کنید و ببینید چه خروجی می‌دهد + +--- + +## نکات امنیتی + +⚠️ **هشدار**: بعد از حل مشکل: +1. فایل‌های تست (`test-telegram-auth.php`) را حذف کنید +2. کدهای دیباگ موقت را پاک کنید +3. مطمئن شوید که `BOT_TOKEN` و `SECRET_TOKEN` در `config.php` قوی هستند + +--- + +## تماس برای کمک + +اگر هنوز مشکل حل نشد: +1. نتیجه صفحه `test-telegram-auth.php` را بفرستید +2. بگویید که آیا دامنه SSL دارد یا خیر +3. بگویید که آیا Menu Button URL در BotFather را آپدیت کرده‌اید یا خیر diff --git a/src/web/user/account.php b/src/web/user/account.php new file mode 100644 index 0000000..a45a952 --- /dev/null +++ b/src/web/user/account.php @@ -0,0 +1,191 @@ + + + + + + + + حساب کاربری + + + + + + + + +
+
+ + +
+ + +
+
+
+ +
+

+
+ + شناسه: +
+
+
+ + +
+
+
اطلاعات مالی
+
+
+
+
+
موجودی کیف پول +
+
+ تومان +
+
+ + شارژ + +
+
+
+ + +
+
+
آمار سرویس‌ها
+
+
+
+
+
+ +
+
کل
+
+
+
+ +
+
فعال
+
+
+
+ +
+
منقضی
+
+
+
+
+ + + +
+ + +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + + + \ No newline at end of file diff --git a/src/web/user/assets/css/style.css b/src/web/user/assets/css/style.css new file mode 100644 index 0000000..ce05323 --- /dev/null +++ b/src/web/user/assets/css/style.css @@ -0,0 +1,323 @@ +:root { + --primary-color: #3b82f6; + --primary-dark: #2563eb; + --bg-color: #f3f4f6; + --bg-secondary: #e5e7eb; + --card-bg: #ffffff; + --text-color: #1f2937; + --text-muted: #6b7280; + --border-color: #e5e7eb; + --success-color: #10b981; + --danger-color: #ef4444; + --warning-color: #f59e0b; + --radius: 12px; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +/* Dark Mode - triggered by .dark-mode class or data-theme attribute */ +.dark-mode, +[data-theme="dark"], +body.dark-theme { + --bg-color: #0f172a; + --bg-secondary: #1e293b; + --card-bg: #1e293b; + --text-color: #f1f5f9; + --text-muted: #94a3b8; + --border-color: #334155; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); +} + +/* Smooth theme transition */ +body, +.card, +.bottom-nav, +.btn, +input, +textarea { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; +} + +/* Theme Toggle Button */ +.theme-toggle { + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid var(--border-color); + background: transparent; + color: var(--text-color); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + transition: all 0.3s ease; +} + +.theme-toggle:hover { + transform: scale(1.05); + background: var(--bg-secondary); +} + +.theme-toggle:active { + transform: scale(0.95); +} + +.theme-toggle .fa-sun { + display: none; + color: #fbbf24; +} + +.theme-toggle .fa-moon { + display: inline-block; + color: #6366f1; +} + +.dark-mode .theme-toggle, +body.dark-theme .theme-toggle { + border-color: var(--border-color); + background: transparent; +} + +.dark-mode .theme-toggle .fa-sun, +body.dark-theme .theme-toggle .fa-sun { + display: inline-block; + color: #ffffff; +} + +.dark-mode .theme-toggle .fa-moon, +body.dark-theme .theme-toggle .fa-moon { + display: none; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background-color: var(--bg-color); + color: var(--text-color); + direction: rtl; + line-height: 1.6; + padding-bottom: 80px; + /* Space for bottom nav */ +} + +.container { + max-width: 600px; + margin: 0 auto; + padding: 16px; +} + +/* Header */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.user-profile { + display: flex; + align-items: center; + gap: 12px; +} + +.avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--primary-color); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + font-weight: bold; +} + +.user-info h2 { + font-size: 1.1rem; + margin-bottom: 2px; +} + +.user-info p { + font-size: 0.85rem; + color: var(--text-muted); +} + +/* Cards */ +.card { + background: var(--card-bg); + border-radius: var(--radius); + padding: 16px; + margin-bottom: 16px; + box-shadow: var(--shadow); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.card-title { + font-size: 1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + margin-bottom: 24px; +} + +.stat-item { + background: var(--card-bg); + padding: 16px; + border-radius: var(--radius); + text-align: center; + box-shadow: var(--shadow); +} + +.stat-value { + font-size: 1.25rem; + font-weight: bold; + color: var(--primary-color); + margin-bottom: 4px; +} + +.stat-label { + font-size: 0.85rem; + color: var(--text-muted); +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 20px; + border-radius: var(--radius); + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + border: none; + outline: none; + text-decoration: none; + width: 100%; + gap: 8px; +} + +.btn-primary { + background: var(--primary-color); + color: white; +} + +.btn-primary:active { + background: var(--primary-dark); + transform: scale(0.98); +} + +.btn-outline { + background: transparent; + border: 1px solid var(--border-color); + color: var(--text-color); +} + +/* Bottom Navigation */ +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: var(--card-bg); + display: flex; + justify-content: space-around; + padding: 12px; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05); + z-index: 100; +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + text-decoration: none; + color: var(--text-muted); + font-size: 0.75rem; + gap: 4px; +} + +.nav-item i { + font-size: 1.25rem; +} + +.nav-item.active { + color: var(--primary-color); +} + +/* Loading */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--bg-color); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Utility */ +.text-success { + color: var(--success-color); +} + +.text-danger { + color: var(--danger-color); +} + +.text-warning { + color: var(--warning-color); +} + +.mb-2 { + margin-bottom: 8px; +} + +.mb-4 { + margin-bottom: 16px; +} + +.w-100 { + width: 100%; +} \ No newline at end of file diff --git a/src/web/user/assets/js/app.js b/src/web/user/assets/js/app.js new file mode 100644 index 0000000..e9eb906 --- /dev/null +++ b/src/web/user/assets/js/app.js @@ -0,0 +1,43 @@ +// Helper Functions for Telegram Web App Panel + +function showLoading() { + const loading = document.getElementById('loading'); + if (loading) { + loading.style.display = 'flex'; + } +} + +function hideLoading() { + const loading = document.getElementById('loading'); + if (loading) { + loading.style.display = 'none'; + } +} + +function formatPrice(price) { + return new Intl.NumberFormat('fa-IR').format(price) + ' تومان'; +} + +function formatNumber(num) { + return new Intl.NumberFormat('fa-IR').format(num); +} + +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; +} + +// Jalali Date Helper (Simple implementation) +function jdate(format, timestamp) { + // This is a simplified version + // For production, use a proper Jalali date library + const date = new Date(timestamp * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}/${month}/${day}`; +} diff --git a/src/web/user/assets/js/theme.js b/src/web/user/assets/js/theme.js new file mode 100644 index 0000000..84e13d9 --- /dev/null +++ b/src/web/user/assets/js/theme.js @@ -0,0 +1,136 @@ +/** + * Theme Manager for Dark/Light Mode + * Auto-detects system preference and allows manual toggle + * Persists user preference in localStorage + */ + +(function () { + 'use strict'; + + const THEME_KEY = 'user-theme-preference'; + const DARK_CLASS = 'dark-mode'; + + /** + * Get the current theme preference + * Priority: localStorage > Telegram WebApp > System preference + */ + function getThemePreference() { + // 1. Check localStorage first (user's manual choice) + const savedTheme = localStorage.getItem(THEME_KEY); + if (savedTheme) { + return savedTheme; + } + + // 2. Check Telegram WebApp color scheme + if (window.Telegram?.WebApp?.colorScheme) { + return window.Telegram.WebApp.colorScheme; + } + + // 3. Check system preference + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark'; + } + + return 'light'; + } + + /** + * Apply theme to the document + */ + function applyTheme(theme) { + if (theme === 'dark') { + document.body.classList.add(DARK_CLASS); + document.documentElement.setAttribute('data-theme', 'dark'); + } else { + document.body.classList.remove(DARK_CLASS); + document.documentElement.removeAttribute('data-theme'); + } + + // Update all toggle buttons + updateToggleButtons(theme); + } + + /** + * Toggle between dark and light mode + */ + function toggleTheme() { + const currentTheme = document.body.classList.contains(DARK_CLASS) ? 'dark' : 'light'; + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + + // Save preference + localStorage.setItem(THEME_KEY, newTheme); + + // Apply theme + applyTheme(newTheme); + + return newTheme; + } + + /** + * Update toggle button icons + */ + function updateToggleButtons(theme) { + const toggleButtons = document.querySelectorAll('.theme-toggle'); + toggleButtons.forEach(btn => { + const sunIcon = btn.querySelector('.fa-sun'); + const moonIcon = btn.querySelector('.fa-moon'); + + if (sunIcon && moonIcon) { + if (theme === 'dark') { + sunIcon.style.display = 'inline-block'; + moonIcon.style.display = 'none'; + } else { + sunIcon.style.display = 'none'; + moonIcon.style.display = 'inline-block'; + } + } + }); + } + + /** + * Create toggle button HTML + */ + function createToggleButton() { + const button = document.createElement('button'); + button.className = 'theme-toggle'; + button.setAttribute('aria-label', 'تغییر تم'); + button.setAttribute('title', 'تغییر تم روشن/تاریک'); + button.innerHTML = ''; + button.addEventListener('click', toggleTheme); + return button; + } + + /** + * Initialize theme system + */ + function initTheme() { + // Apply initial theme + const theme = getThemePreference(); + applyTheme(theme); + + // Listen for system theme changes + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + // Only auto-switch if user hasn't set a preference + if (!localStorage.getItem(THEME_KEY)) { + applyTheme(e.matches ? 'dark' : 'light'); + } + }); + } + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initTheme); + } else { + initTheme(); + } + + // Expose functions globally + window.ThemeManager = { + toggle: toggleTheme, + apply: applyTheme, + getPreference: getThemePreference, + createButton: createToggleButton + }; +})(); diff --git a/src/web/user/auth.php b/src/web/user/auth.php new file mode 100644 index 0000000..8d5e364 --- /dev/null +++ b/src/web/user/auth.php @@ -0,0 +1,52 @@ + $value) { + $data_check_string[] = $key . '=' . $value; + } + $data_check_string = implode("\n", $data_check_string); + + $secret_key = hash_hmac('sha256', BOT_TOKEN, "WebAppData", true); + $calculated_hash = bin2hex(hash_hmac('sha256', $data_check_string, $secret_key, true)); + + if (strcmp($hash, $calculated_hash) !== 0) { + return false; + } + + // Check auth_date for freshness (e.g., within 24 hours) + if (isset($data['auth_date'])) { + if (time() - $data['auth_date'] > 86400) { + return false; + } + } + + return json_decode($data['user'], true); +} diff --git a/src/web/user/debug.html b/src/web/user/debug.html new file mode 100644 index 0000000..00832f7 --- /dev/null +++ b/src/web/user/debug.html @@ -0,0 +1,150 @@ + + + + + + + Debug Panel + + + + + +
+

🔍 Telegram Web App Debug

+
+
+ +
+

📊 Test Results

+
+
+ + + + + \ No newline at end of file diff --git a/src/web/user/guides.php b/src/web/user/guides.php new file mode 100644 index 0000000..d9bc4aa --- /dev/null +++ b/src/web/user/guides.php @@ -0,0 +1,118 @@ +query("SELECT * FROM guides WHERE status = 'active' ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC); +?> + + + + + + + راهنماها + + + + + + + + +
+
+ + +
+ + +
+
+ +

هیچ راهنمایی موجود نیست.

+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+ + + + + +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + + + \ No newline at end of file diff --git a/src/web/user/index.php b/src/web/user/index.php new file mode 100644 index 0000000..9d73134 --- /dev/null +++ b/src/web/user/index.php @@ -0,0 +1,309 @@ + true]); + exit; + } else { + http_response_code(401); + echo json_encode(['success' => false, 'error' => 'Invalid authentication']); + exit; + } +} + +// Check if user is logged in +$isLoggedIn = isUserLoggedIn(); + +// If logged in, get user data +if ($isLoggedIn) { + $user = getCurrentUser(); + $services = getUserServices($user['chat_id']); + + // Calculate stats + $total_services = count($services); + $active_services = 0; + $expired_services = 0; + $now = time(); + + foreach ($services as $service) { + if ($service['expire_timestamp'] < $now) { + $expired_services++; + } else { + $active_services++; + } + } + + // Get recent services (last 3) + $recent_services = array_slice($services, 0, 3); +} else { + // Set defaults for non-logged users (will be handled by JS) + $user = ['first_name' => 'کاربر', 'chat_id' => 0, 'balance' => 0]; + $services = []; + $total_services = 0; + $active_services = 0; + $expired_services = 0; + $recent_services = []; +} +?> + + + + + + + پنل کاربری + + + + + + + + + +
+
+

در حال احراز هویت...

+
+ +
+ +
+ + +
+ + +
+
+
اطلاعات کاربری
+
+
+
+ + نام: +
+
+ + شناسه: +
+
+ + موجودی: + + تومان + +
+
+
+ + +
+
+
آمار سرویس‌ها
+
+
+
+
+ +
+
+ کل سرویس‌ها +
+
+
+
+ +
+
+ فعال +
+
+
+
+ +
+
+ منقضی +
+
+
+
+ + + + + + +
+
+
سرویس‌های اخیر
+ + مشاهده همه + +
+
+ +
+
+
+ +
+
+ + +
+
+
+ + انقضا: +
+
+ +
+
+ +
+
+ +

شما هنوز سرویسی خریداری نکرده‌اید.

+ + خرید اولین سرویس + +
+
+ +
+ + +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + + + \ No newline at end of file diff --git a/src/web/user/renew.php b/src/web/user/renew.php new file mode 100644 index 0000000..9f2b72a --- /dev/null +++ b/src/web/user/renew.php @@ -0,0 +1,319 @@ +خطا

قابلیت تمدید سرویس در حال حاضر غیرفعال است.

بازگشت
'); +} + +$username = $_GET['username'] ?? ''; +if (empty($username)) { + header('Location: services.php'); + exit(); +} + +// Validate service ownership +$stmt = pdo()->prepare("SELECT * FROM services WHERE marzban_username = ? AND owner_chat_id = ?"); +$stmt->execute([$username, $chat_id]); +$service = $stmt->fetch(); + +if (!$service) { + die('خطا

سرویس یافت نشد.

بازگشت
'); +} + +$step = 1; +if (isset($_GET['plan_id'])) + $step = 4; +elseif (isset($_GET['server_id'])) + $step = 3; +elseif (isset($_GET['category_id'])) + $step = 2; + +// Auto-skip server selection if only one server exists +if ($step === 2) { + $category_id = $_GET['category_id']; + $stmt = pdo()->prepare(" + SELECT DISTINCT s.id + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt->execute([$category_id]); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (count($servers) === 1) { + $step = 3; + $_GET['server_id'] = $servers[0]['id']; + } +} + +$error = ''; +$success = ''; + +// Handle Renewal Action +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_renewal'])) { + $plan_id = $_POST['plan_id']; + $plan = getPlanById($plan_id); + + if ($plan) { + $final_price = (float) $plan['price']; + $user_balance = $user['balance']; + + if ($user_balance >= $final_price) { + try { + // Apply renewal + $result = applyPlanRenewal($chat_id, $username, $plan_id, $final_price); + if ($result['success']) { + // Redirect to services.php with success message + header('Location: services.php?success_msg=' . urlencode($result['message'])); + exit(); + } else { + $error = $result['message']; + } + } catch (Throwable $e) { + $error = 'خطای سیستمی: ' . $e->getMessage(); + error_log($e); + } + } else { + $error = 'موجودی حساب شما کافی نیست.'; + } + } else { + $error = 'پلن انتخاب شده نامعتبر است.'; + } +} + +?> + + + + + + + تمدید سرویس + + + + + + + + +
+
+ + +
+ + + + + پلن یافت نشد.
'; + } else { + $final_price = $plan['price']; + $can_afford = $user['balance'] >= $final_price; + ?> +
+
+
تایید تمدید
+
+
+
+
+ سرویس: + +
+
+ پلن جدید: + +
+
+ قیمت: + تومان +
+
+ موجودی شما: + + تومان + +
+
+ + +
+ + + +
+ + +
+ +
+ موجودی حساب شما کافی نیست. لطفاً ابتدا کیف پول خود را شارژ کنید. +
+ مبلغ مورد نیاز: تومان +
+ + شارژ کیف پول + + +
+
+ + + + prepare("SELECT * FROM plans WHERE category_id = ? AND server_id = ? AND status = 'active' AND is_test_plan = 0"); + $stmt->execute([$category_id, $server_id]); + $plans = $stmt->fetchAll(PDO::FETCH_ASSOC); + ?> +
+
+
انتخاب پلن
+
+ +
+ prepare("SELECT COUNT(DISTINCT s.id) FROM servers s JOIN plans p ON s.id = p.server_id WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' AND s.id IS NOT NULL"); + $stmt_count->execute([$category_id]); + $server_count = $stmt_count->fetchColumn(); + + $back_link = ($server_count == 1) ? "?username=" . urlencode($username) : "?username=" . urlencode($username) . "&category_id=" . urlencode($category_id); + $back_text = ($server_count == 1) ? 'بازگشت به دسته‌بندی‌ها' : 'بازگشت به سرورها'; + ?> + + + +
+ +
+ +

هیچ پلنی یافت نشد.

+ + + +
+ + تومان +
+
+ + +
+
+ + + prepare(" + SELECT DISTINCT s.id, s.name + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt->execute([$category_id]); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + ?> +
+
+
انتخاب سرور (لوکیشن)
+
+
+ +

هیچ سروری یافت نشد.

+ + + + + + + +
+
+ + + +
+
+
انتخاب دسته‌بندی
+
+
+ +

هیچ دسته‌بندی یافت نشد.

+ + + + + + + +
+
+ + + + + + + + + \ No newline at end of file diff --git a/src/web/user/services.php b/src/web/user/services.php new file mode 100644 index 0000000..bd8cce5 --- /dev/null +++ b/src/web/user/services.php @@ -0,0 +1,265 @@ + + + + + + + + سرویس‌های من + + + + + + + + +
+
+ + +
+ + +
+
+ +

شما هیچ سرویسی ندارید.

+ + خرید سرویس + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+ + پلن: +
+
+ + تاریخ انقضا: + 0): ?> + + ( روز مانده) + + +
+
+ + حجم: GB +
+
+
+ + +
+ + + + + تمدید + +
+ +
+ + لینک اشتراک در دسترس نیست +
+ +
+ + +
+ + + + + +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + + + + \ No newline at end of file diff --git a/src/web/user/session.php b/src/web/user/session.php new file mode 100644 index 0000000..119660f --- /dev/null +++ b/src/web/user/session.php @@ -0,0 +1,92 @@ + 'Unauthorized']); + exit; + } + + // Otherwise, we might need to show a login page or error + // But for Web App, we usually handle auth via JS on the index page + // So we just exit or redirect to an error page + die('Access Denied. Please open from Telegram.'); + } +} + +// Get current user data +function getCurrentUser() +{ + if (isUserLoggedIn()) { + return getUserData($_SESSION['user_id'], $_SESSION['first_name'] ?? 'کاربر'); + } + return null; +} + +// Login user with new session +function loginUser($userId, $firstName) +{ + // If there's an existing session with a different user, destroy it + if (isset($_SESSION['user_id']) && $_SESSION['user_id'] != $userId) { + // Clear all session data + $_SESSION = array(); + + // Destroy the session cookie + if (isset($_COOKIE[session_name()])) { + setcookie(session_name(), '', time() - 3600, '/'); + } + + // Destroy the session + session_destroy(); + + // Start a new session + session_start(); + } + + // Regenerate session ID to prevent session fixation + session_regenerate_id(true); + + // Set session data + $_SESSION['user_id'] = $userId; + $_SESSION['first_name'] = $firstName; + $_SESSION['session_created'] = time(); + $_SESSION['last_activity'] = time(); +} + +// Logout user +function logoutUser() +{ + // Clear all session data + $_SESSION = array(); + + // Destroy the session cookie + if (isset($_COOKIE[session_name()])) { + setcookie(session_name(), '', time() - 3600, '/'); + } + + // Destroy the session + session_destroy(); +} diff --git a/src/web/user/shop.php b/src/web/user/shop.php new file mode 100644 index 0000000..1b991af --- /dev/null +++ b/src/web/user/shop.php @@ -0,0 +1,589 @@ + false, 'message' => 'نام سرویس نمی‌تواند خالی باشد']); + exit; + } + + $plan = getPlanById($plan_id); + if (!$plan || $plan['status'] !== 'active') { + echo json_encode(['success' => false, 'message' => 'پلن یافت نشد یا غیرفعال است']); + exit; + } + + if ($plan['purchase_limit'] > 0 && $plan['purchase_count'] >= $plan['purchase_limit']) { + echo json_encode(['success' => false, 'message' => 'ظرفیت خرید این پلن تکمیل شده است']); + exit; + } + + // Check balance + if ($user['balance'] < $plan['price']) { + echo json_encode([ + 'success' => false, + 'need_charge' => true, + 'message' => 'موجودی کافی نیست', + 'required' => $plan['price'], + 'current' => $user['balance'] + ]); + exit; + } + + // Complete purchase + $result = completePurchase($user['chat_id'], $plan_id, $custom_name, $plan['price'], null, null, false); + + if ($result['success']) { + echo json_encode(['success' => true, 'message' => 'خرید با موفقیت انجام شد']); + } else { + echo json_encode(['success' => false, 'message' => $result['error_message'] ?? 'خطا در انجام خرید']); + } + exit; + } elseif ($_POST['action'] === 'check_test_config') { + // Check if user can get test config + $testPlan = getTestPlan(); + + if (!$testPlan) { + echo json_encode(['success' => false, 'message' => '❌ دریافت کانفیگ تست در حال حاضر توسط مدیر غیرفعال شده است.']); + exit; + } + + if ($user['test_config_count'] >= $usage_limit) { + echo json_encode(['success' => false, 'message' => '❌ شما قبلا از حداکثر تعداد کانفیگ تست خود استفاده کرده‌اید.']); + exit; + } + + // Return test plan details + echo json_encode([ + 'success' => true, + 'plan' => [ + 'id' => $testPlan['id'], + 'name' => $testPlan['name'], + 'volume_gb' => $testPlan['volume_gb'], + 'duration_days' => $testPlan['duration_days'] + ] + ]); + exit; + } elseif ($_POST['action'] === 'get_test_config') { + $custom_name = trim($_POST['custom_name'] ?? ''); + + if (empty($custom_name)) { + echo json_encode(['success' => false, 'message' => 'نام سرویس نمی‌تواند خالی باشد']); + exit; + } + + if ($user['test_config_count'] >= $usage_limit) { + echo json_encode(['success' => false, 'message' => 'شما قبلاً از حداکثر تعداد کانفیگ تست استفاده کرده‌اید.']); + exit; + } + + $testPlan = getTestPlan(); + if (!$testPlan) { + echo json_encode(['success' => false, 'message' => 'در حال حاضر پلن تست فعال وجود ندارد.']); + exit; + } + + // Create test service + $result = completePurchase($user['chat_id'], $testPlan['id'], $custom_name, 0, null, null, false); + + if ($result['success']) { + echo json_encode(['success' => true, 'message' => '✅ خرید شما با موفقیت انجام شد.']); + } else { + echo json_encode(['success' => false, 'message' => $result['error_message'] ?? 'خطا در ایجاد کانفیگ تست']); + } + exit; + } +} + +// Get URL parameters +$selected_cat_id = isset($_GET['cat_id']) ? (int) $_GET['cat_id'] : null; +$selected_server_id = isset($_GET['server_id']) ? (int) $_GET['server_id'] : null; + +// Auto-select server if only one exists +if ($selected_cat_id && !$selected_server_id) { + $stmt = pdo()->prepare(" + SELECT DISTINCT s.id + FROM servers s + JOIN plans p ON s.id = p.server_id + WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active' + "); + $stmt->execute([$selected_cat_id]); + $active_servers_in_cat = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (count($active_servers_in_cat) === 1) { + $selected_server_id = $active_servers_in_cat[0]['id']; + } +} +?> + + + + + + + فروشگاه + + + + + + + + + + +
+
+ +
+ +
+ + تومان + +
+
+
+ + +
+
+ +

🧪 کانفیگ تست رایگان

+

برای دریافت این کانفیگ رایگان، روی دکمه + زیر کلیک کنید.

+ +
+
+ + + + +
+ prepare("SELECT COUNT(DISTINCT s.id) FROM servers s JOIN plans p ON s.id = p.server_id WHERE p.category_id = ? AND p.status = 'active' AND s.status = 'active'"); + $stmt_count->execute([$selected_cat_id]); + $server_count = $stmt_count->fetchColumn(); + + $back_link = ($server_count == 1) ? 'shop.php' : 'shop.php?cat_id=' . $selected_cat_id; + $back_text = ($server_count == 1) ? 'بازگشت به دسته‌بندی‌ها' : 'بازگشت به سرورها'; + ?> + + + +
+ + + + +
+
+
سرور انتخاب شده:
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +

هیچ پلنی برای این سرور موجود نیست.

+
+ + +
+
+
+
+ تومان +
+
+
+
+ حجم: گیگابایت +
+
+ مدت: روز +
+ +
+ +
+ +
+ +
+ + + + + +
+ + بازگشت به دسته‌بندی‌ها + +
+ + + + +
+ +

هیچ سروری در حال حاضر فعال نیست.

+
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + + + + +
+ +

هیچ دسته‌بندی فعالی وجود ندارد.

+
+ +
+ + + + + + + + + +
+ + +
+ + + + + + + + + + + +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + + + \ No newline at end of file diff --git a/src/web/user/simple-test.html b/src/web/user/simple-test.html new file mode 100644 index 0000000..ea9a9a5 --- /dev/null +++ b/src/web/user/simple-test.html @@ -0,0 +1,72 @@ + + + + + + + تست ساده + + + + +

🔍 تست اتصال

+
+ + + + + + \ No newline at end of file diff --git a/src/web/user/support.php b/src/web/user/support.php new file mode 100644 index 0000000..b539346 --- /dev/null +++ b/src/web/user/support.php @@ -0,0 +1,274 @@ + false, 'message' => 'موضوع و متن پیام نمی‌تواند خالی باشد']); + exit; + } + + // Send to admin via bot + require_once __DIR__ . '/../../includes/functions.php'; + + $ticket_message = "📨 تیکت پشتیبانی جدید\n\n" . + "👤 کاربر: " . htmlspecialchars($user['first_name']) . "\n" . + "🆔 شناسه: {$user['chat_id']}\n" . + "📋 موضوع: " . htmlspecialchars($subject) . "\n\n" . + "💬 پیام: \n" . htmlspecialchars($message); + + // Get all admins + $admins = getAdmins(); + $admins[ADMIN_CHAT_ID] = []; + + // Insert into tickets table BEFORE sending + $ticket_id = uniqid('ticket_', true); + $stmt = pdo()->prepare("INSERT INTO tickets (id, user_id, user_name, subject, status, created_at) VALUES (?, ?, ?, ?, 'open', NOW())"); + $stmt->execute([$ticket_id, $user['chat_id'], $user['first_name'], $subject]); + + $stmt_conv = pdo()->prepare("INSERT INTO ticket_conversations (ticket_id, sender, message_text, sent_at) VALUES (?, 'user', ?, NOW())"); + $stmt_conv->execute([$ticket_id, $message]); + + $keyboard = [ + 'inline_keyboard' => [ + [ + ['text' => '💬 پاسخ', 'callback_data' => "reply_ticket_{$ticket_id}"] + ] + ] + ]; + + $sent = false; + foreach (array_keys($admins) as $admin_id) { + if (hasPermission($admin_id, 'manage_users')) { + $result = sendMessage($admin_id, $ticket_message, $keyboard); + if ($result) { + $sent = true; + } + } + } + + if ($sent) { + echo json_encode(['success' => true, 'message' => 'پیام شما برای پشتیبانی ارسال شد']); + } else { + echo json_encode(['success' => false, 'message' => 'خطا در ارسال پیام. لطفاً با آیدی تلگرام تماس بگیرید.']); + } + exit; +} +?> + + + + + + + پشتیبانی + + + + + + + + + + +
+
+ + +
+ + +
+
+ +

پشتیبانی ۲۴ ساعته

+ + + + @ + + +
+
+ + +
+
+
ارسال تیکت پشتیبانی
+
+
+
+
+ + +
+ +
+ + +
+ + +
+
+
+ + +
+
+
سوالات متداول
+
+
+
+
+ + چگونه سرویس خریداری کنم؟ +
+
+ از بخش فروشگاه، دسته‌بندی، سرور و پلن مورد نظر را انتخاب کنید و پس از پرداخت، سرویس برای شما + ساخته می‌شود. +
+
+ +
+
+ + چگونه کیف پول را شارژ کنم؟ +
+
+ از بخش کیف پول می‌توانید با درگاه آنلاین یا کارت به کارت، حساب خود را شارژ کنید. +
+
+ +
+
+ + لینک اشتراک چیست؟ +
+
+ لینک اشتراک (Subscription) را در برنامه‌های V2Ray خود وارد کنید تا به سرویس متصل شوید. +
+
+
+
+
+ + +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + + + \ No newline at end of file diff --git a/src/web/user/test-config.php b/src/web/user/test-config.php new file mode 100644 index 0000000..2dcf75d --- /dev/null +++ b/src/web/user/test-config.php @@ -0,0 +1,201 @@ + false, 'message' => 'کانفیگ تست غیرفعال است']); + exit; + } + + if ($user['test_config_count'] >= $usage_limit) { + echo json_encode(['success' => false, 'message' => 'شما قبلاً از حداکثر تعداد کانفیگ تست استفاده کرده‌اید']); + exit; + } + + // Create test service + $result = completePurchase($user['chat_id'], $test_plan['id'], 'تست رایگان', 0, null, null, false); + + if ($result['success']) { + echo json_encode(['success' => true, 'message' => 'کانفیگ تست با موفقیت ساخته شد']); + } else { + echo json_encode(['success' => false, 'message' => $result['error_message'] ?? 'خطا در ساخت کانفیگ']); + } + exit; +} +?> + + + + + + + کانفیگ تست + + + + + + + + + +
+
+ +
+ + +
+
+ +

دریافت کانفیگ تست در حال حاضر غیرفعال است.

+
+
+ = $usage_limit): ?> +
+
+ +

شما قبلاً از کانفیگ تست استفاده کرده‌اید

+

+ هر کاربر فقط بار می‌تواند کانفیگ تست دریافت کند. +

+ + + خرید سرویس + +
+
+ +
+
+ +

کانفیگ تست رایگان

+

برای آزمایش سرعت و کیفیت سرویس

+
+
+ +
+
+
مشخصات کانفیگ تست
+
+
+
+
+ نام پلن + +
+
+ حجم + GB +
+
+ مدت اعتبار + روز +
+
+ +
+ + توجه: هر کاربر فقط بار می‌تواند کانفیگ تست دریافت کند. +
+ + +
+
+ +
+ + +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + + + \ No newline at end of file diff --git a/src/web/user/test-php.php b/src/web/user/test-php.php new file mode 100644 index 0000000..5663420 --- /dev/null +++ b/src/web/user/test-php.php @@ -0,0 +1,227 @@ + + + + + + + + تست PHP - OpenLiteSpeed + + + + +
+

✅ عالی! PHP کار می‌کند

+ +
+

🎉 PHP با موفقیت اجرا شد!

+

نسخه PHP:

+

سرور:

+
+ +

اطلاعات سرور

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
پارامترمقدار
Document Root
Script Filename
Request URI
HTTP Host
HTTPS +
Server Software
+ +

بررسی مسیرها

+
+

مسیر فعلی:

+

فایل فعلی:

+
+ + +
+

فایل Config

+

مسیر:

+

وضعیت:

+ +

قابل خواندن: +

+ +
+ + +
+

Session

+

وضعیت: ✅ فعال

+

Session ID:

+

Save Path:

+
+ +

تنظیمات PHP

+ + + + + + + + + + + + + + + + + + + + + +
تنظیممقدار
max_execution_time ثانیه
memory_limit
upload_max_filesize
post_max_size
+ +

مرحله بعدی

+
+

اگر این صفحه را می‌بینید، یعنی PHP به درستی کار می‌کند. حالا باید صفحه اصلی را تست کنید:

+ رفتن به صفحه اصلی + تست احراز هویت تلگرام +
+ +
+

📋 دستورات مفید برای OpenLiteSpeed

+

اگر هنوز مشکل دارید، این دستورات را در سرور اجرا کنید:

+
+# بررسی مجوزها
+ls -la 
+
+# تنظیم مجوزهای صحیح
+find  -type f -exec chmod 644 {} \;
+find  -type d -exec chmod 755 {} \;
+
+# راه‌اندازی مجدد OpenLiteSpeed
+systemctl restart lsws
+# یا
+/usr/local/lsws/bin/lswsctrl restart
+            
+
+
+ + + \ No newline at end of file diff --git a/src/web/user/test-simple.html b/src/web/user/test-simple.html new file mode 100644 index 0000000..5aa2bdb --- /dev/null +++ b/src/web/user/test-simple.html @@ -0,0 +1,79 @@ + + + + + + + تست ساده - آیا فایل‌ها در دسترس هستند؟ + + + + +
+
+

عالی! فایل‌ها در دسترس هستند

+

+ اگر این پیام را می‌بینید، یعنی فایل‌های HTML شما در سرور به درستی کار می‌کنند. +

+ +
+

مرحله بعدی: تست PHP

+

حالا باید فایل‌های PHP را تست کنید. لینک زیر را کلیک کنید:

+ + تست PHP + +
+ +
+

اطلاعات مسیر

+

فایل جاری: test-simple.html

+

مسیر انتظاری: /web/user/test-simple.html

+

+
+
+ + + + + \ No newline at end of file diff --git a/src/web/user/test-telegram-auth.php b/src/web/user/test-telegram-auth.php new file mode 100644 index 0000000..672fb7f --- /dev/null +++ b/src/web/user/test-telegram-auth.php @@ -0,0 +1,163 @@ + + + + + + + + Telegram Web App Auth Test + + + + + +
+

تنظیمات Config

+

BOT_TOKEN: + 10 ? 'تنظیم شده (' . strlen(BOT_TOKEN) . ' کاراکتر)' : 'خیلی کوتاه!') : 'تنظیم نشده!'; ?> +

+

BASE_URL: + تنظیم نشده!'; ?>

+
+ +
+

اطلاعات Session

+

Session Active: + بله' : 'خیر'; ?>

+

User Logged In: + بله (ID: ' . $_SESSION['user_id'] . ')' : 'خیر'; ?> +

+

Session Data:

+
+
+ +
+

اطلاعات Telegram WebApp

+

WebApp Loaded: در حال بررسی...

+

initData:

+
در حال بارگذاری...
+

initDataUnsafe:

+
در حال بارگذاری...
+
+ +
+

تست احراز هویت

+ +
+
+ +
+

اطلاعات سرور

+

Current URL:

+

HTTPS: + بله' : 'خیر'; ?> +

+

User Agent:

+
+ + + + + \ No newline at end of file diff --git a/src/web/user/test.php b/src/web/user/test.php new file mode 100644 index 0000000..f3e6e7c --- /dev/null +++ b/src/web/user/test.php @@ -0,0 +1,28 @@ +"; +require_once __DIR__ . '/../../includes/config.php'; +echo "✅ Config loaded. BOT_TOKEN exists: " . (defined('BOT_TOKEN') ? 'YES' : 'NO') . "
"; +echo "✅ BASE_URL: " . (defined('BASE_URL') ? BASE_URL : 'NOT DEFINED') . "

"; + +echo "2. Testing db.php...
"; +require_once __DIR__ . '/../../includes/db.php'; +echo "✅ Database connection loaded

"; + +echo "3. Testing functions.php...
"; +require_once __DIR__ . '/../../includes/functions.php'; +echo "✅ Functions loaded

"; + +echo "4. Testing auth.php...
"; +require_once __DIR__ . '/auth.php'; +echo "✅ Auth loaded

"; + +echo "5. Testing session.php...
"; +require_once __DIR__ . '/session.php'; +echo "✅ Session loaded

"; + +echo "All systems operational!
"; +echo "You can now delete this file (test.php)."; diff --git a/src/web/user/wallet.php b/src/web/user/wallet.php new file mode 100644 index 0000000..25f7b7c --- /dev/null +++ b/src/web/user/wallet.php @@ -0,0 +1,498 @@ + false, 'message' => 'مبلغ باید حداقل ۱۰۰۰ تومان باشد']); + exit; + } + // Validate file + $file = $_FILES['receipt']; + if ($file['error'] !== UPLOAD_ERR_OK) { + $error_messages = [ + UPLOAD_ERR_INI_SIZE => 'حجم فایل بیشتر از حد مجاز است', + UPLOAD_ERR_FORM_SIZE => 'حجم فایل بیشتر از حد مجاز فرم است', + UPLOAD_ERR_PARTIAL => 'فایل به صورت ناقص آپلود شده', + UPLOAD_ERR_NO_FILE => 'هیچ فایلی انتخاب نشده', + UPLOAD_ERR_NO_TMP_DIR => 'پوشه موقت وجود ندارد', + UPLOAD_ERR_CANT_WRITE => 'خطا در نوشتن فایل', + UPLOAD_ERR_EXTENSION => 'آپلود توسط افزونه متوقف شده' + ]; + $error = $error_messages[$file['error']] ?? 'خطای نامشخص'; + echo json_encode(['success' => false, 'message' => 'خطا در آپلود فایل: ' . $error]); + exit; + } + $allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; + if (!in_array($file['type'], $allowed_types)) { + echo json_encode(['success' => false, 'message' => 'فقط فایل‌های JPG, PNG و WebP مجاز هستند']); + exit; + } + if ($file['size'] > 5 * 1024 * 1024) { // 5MB max + echo json_encode(['success' => false, 'message' => 'حجم فایل نباید بیشتر از ۵ مگابایت باشد']); + exit; + } + // Save photo - مسیر صحیح بر اساس ساختار جدید + $upload_dir = __DIR__ . '/uploads/receipts/'; + // Create directory if it doesn't exist + if (!is_dir($upload_dir)) { + if (!mkdir($upload_dir, 0777, true)) { + echo json_encode(['success' => false, 'message' => 'خطا در ایجاد پوشه آپلود: ' . $upload_dir]); + exit; + } + chmod($upload_dir, 0777); + } + // Generate unique filename + $file_extension = pathinfo($file['name'], PATHINFO_EXTENSION); + $filename = uniqid('receipt_') . '_' . time() . '.' . ($file_extension ?: 'jpg'); + $filepath = $upload_dir . $filename; + if (move_uploaded_file($file['tmp_name'], $filepath)) { + // Make file readable + chmod($filepath, 0644); + + // Get all admins + $admins = getAdmins() ?: []; + $admins[ADMIN_CHAT_ID] = ['permissions' => ['manage_payment']]; // Add main admin + + // Insert into payment_requests table BEFORE sending to Telegram + $stmt = pdo()->prepare("INSERT INTO payment_requests (user_id, amount, photo_file_id, status, created_at) VALUES (?, ?, ?, 'pending', NOW())"); + $stmt->execute([$user['chat_id'], $amount, $filename]); + $request_id = pdo()->lastInsertId(); + + try { + // Load Telegram functions - مسیر صحیح + require_once __DIR__ . '/../../includes/functions.php'; + $caption = "💳 درخواست شارژ کیف پول\n" . + "👤 کاربر: " . htmlspecialchars($user['first_name'] ?? 'ناشناس') . "\n" . + "🆔 شناسه: {$user['chat_id']}\n" . + "💰 مبلغ: " . number_format($amount) . " تومان"; + // Get all admins + + $keyboard = [ + 'inline_keyboard' => [ + [ + ['text' => '✅ تایید و شارژ', 'callback_data' => "approve_{$request_id}"], + ['text' => '❌ رد کردن', 'callback_data' => "reject_{$request_id}"] + ] + ] + ]; + $sent = false; + $errors = []; + foreach (array_keys($admins) as $admin_id) { + // Check if admin has permission + $has_permission = false; + if (isset($admins[$admin_id]['permissions']) && is_array($admins[$admin_id]['permissions'])) { + $has_permission = in_array('manage_payment', $admins[$admin_id]['permissions']); + } elseif (function_exists('hasPermission')) { + $has_permission = hasPermission($admin_id, 'manage_payment'); + } + if ($has_permission) { + try { + $result = sendPhoto($admin_id, $filepath, $caption, $keyboard); + if ($result) { + $sent = true; + } else { + $errors[] = "عدم موفقیت در ارسال به ادمین {$admin_id}"; + } + } catch (Exception $e) { + $errors[] = "خطا در ارسال به ادمین {$admin_id}: " . $e->getMessage(); + } + } + } + if ($sent) { + echo json_encode(['success' => true, 'message' => 'رسید شما برای ادمین ارسال شد. پس از بررسی، کیف پول شما شارژ خواهد شد.']); + } else { + $error_message = !empty($errors) ? implode("\n", $errors) : 'خطا در ارسال به ادمین‌ها'; + echo json_encode(['success' => false, 'message' => 'رسید شما آپلود شد اما در ارسال به ادمین‌ها مشکلی پیش آمد. لطفاً با پشتیبانی تماس بگیرید.']); + } + } catch (Exception $e) { + // Log error to server log + error_log('خطای پردازش پس از آپلود: ' . $e->getMessage()); + // For security, don't expose full error details to user + echo json_encode([ + 'success' => true, + 'message' => 'رسید شما با موفقیت آپلود شد. پس از بررسی دستی توسط ادمین، کیف پول شما شارژ خواهد شد.' + ]); + } + } else { + echo json_encode(['success' => false, 'message' => 'خطا در ذخیره فایل. لطفاً دوباره تلاش کنید.']); + } + exit; +} +?> + + + + + + + کیف پول + + + + + + + + + + +
+
+ + +
+
+
+
موجودی کیف پول
+
+ +
+
تومان
+
+
+
+
+
مبلغ شارژ
+
+
+ +
+ + حداقل مبلغ شارژ: ۱,۰۰۰ تومان +
+
+
+
+
+
روش پرداخت
+
+
+ + + + + +
+
+
+ +
+ + + خانه + + + + سرویس‌ها + + + + فروشگاه + + + + کیف پول + + + + پشتیبانی + +
+ + + + \ No newline at end of file diff --git a/web-admin.png b/web-admin.png new file mode 100644 index 0000000..7c99618 Binary files /dev/null and b/web-admin.png differ