Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fa69dec3e | |||
| 3d5f3303d1 | |||
| 10883e92b1 | |||
| e2d28333ec | |||
| 6d55d4832f | |||
| 46c563873f | |||
| 10c5d3c408 | |||
| 942080c460 | |||
| 6a0869ca0a | |||
| d9992dd2f2 | |||
| c8c9631518 | |||
| 943f29f227 | |||
| 80a069077c | |||
| e344e55e6a | |||
| a69947b69a | |||
| 926f593f50 | |||
| f13734a4e9 | |||
| 2107166926 | |||
| 60e5601690 | |||
| 0957d9ef88 | |||
| eb481c57ec | |||
| e0b1666f5d | |||
| f2afc088af | |||
| 971d59b2c6 | |||
| 83fe58056f | |||
| 5f107922d0 | |||
| a2ea6b7bf7 | |||
| 1e0e728840 | |||
| cea6ed3f95 | |||
| f15705ed47 | |||
| 0b9c13ae4b | |||
| d0c8ae02f0 | |||
| 4eda7401b9 | |||
| 0c2f3c9399 | |||
| 541ecae48c | |||
| 82a551f1d4 | |||
| fd2253135d | |||
| d0820aa47e | |||
| 09c5800f49 | |||
| 52aa2dc6a8 | |||
| 6418ee7735 | |||
| 25aef89b4f | |||
| 77d17f97d3 | |||
| d5c1ec3aea | |||
| 97a7384381 | |||
| 27b7bfccba | |||
| ab15624a93 | |||
| a9c5d8ed69 | |||
| 234b90796e | |||
| 5f018ba22f | |||
| e11cbbcc18 | |||
| ba4d30d8db | |||
| d11431769c | |||
| d1d1bbb3e4 | |||
| 823f30882a | |||
| d36f389469 | |||
| add686a4d5 | |||
| 59be2d8f02 | |||
| aa3db49f65 |
@@ -1,31 +1,28 @@
|
||||
name: BedolagaBot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Build and Publish Docker Image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Generate security report
|
||||
uses: docker/scout-action@v1
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
command: quickview,compare
|
||||
image: ${{ steps.meta.outputs.tags }}
|
||||
to: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
ignore-unchanged: true
|
||||
only-severities: critical,high
|
||||
write-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,96 +0,0 @@
|
||||
# RemnaWave Bot Docker Management
|
||||
|
||||
.PHONY: help build up down restart logs clean db-backup db-restore
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " build - Build all Docker images"
|
||||
@echo " up - Start all services"
|
||||
@echo " up-min - Start only bot and database (minimal setup)"
|
||||
@echo " up-full - Start all services including nginx and redis"
|
||||
@echo " down - Stop all services"
|
||||
@echo " restart - Restart all services"
|
||||
@echo " logs - Show logs for all services"
|
||||
@echo " logs-bot - Show logs for bot service only"
|
||||
@echo " logs-db - Show logs for database service only"
|
||||
@echo " clean - Remove all containers, networks, and volumes"
|
||||
@echo " db-backup - Create database backup"
|
||||
@echo " db-restore - Restore database from backup"
|
||||
@echo " shell-bot - Open shell in bot container"
|
||||
@echo " shell-db - Open shell in database container"
|
||||
|
||||
# Build all images
|
||||
build:
|
||||
docker compose build
|
||||
|
||||
# Start minimal services (bot + database)
|
||||
up-min: setup-dirs
|
||||
docker compose up -d postgres bot
|
||||
|
||||
# Start all services including optional ones
|
||||
up-full: setup-dirs
|
||||
docker compose --profile with-nginx up -d
|
||||
|
||||
# Start main services (default)
|
||||
up: setup-dirs
|
||||
docker compose up -d postgres redis bot
|
||||
|
||||
# Setup required directories
|
||||
setup-dirs:
|
||||
mkdir -p logs data backups
|
||||
|
||||
# Stop all services
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
# Restart all services
|
||||
restart:
|
||||
docker compose restart
|
||||
|
||||
# Show logs for all services
|
||||
logs:
|
||||
docker compose logs -f
|
||||
|
||||
# Show logs for bot only
|
||||
logs-bot:
|
||||
docker compose logs -f bot
|
||||
|
||||
# Show logs for database only
|
||||
logs-db:
|
||||
docker compose logs -f postgres
|
||||
|
||||
# Clean up everything (DANGEROUS - removes all data)
|
||||
clean:
|
||||
@echo "This will remove all containers, networks, and volumes. Are you sure? [y/N]"
|
||||
@read answer && [ "$$answer" = "y" ] || [ "$$answer" = "Y" ]
|
||||
docker compose down -v --remove-orphans
|
||||
docker system prune -f
|
||||
|
||||
# Database backup
|
||||
db-backup:
|
||||
@mkdir -p backups
|
||||
docker compose exec postgres pg_dump -U remnawave_user remnawave_bot > backups/backup_$(shell date +%Y%m%d_%H%M%S).sql
|
||||
@echo "Backup created in backups/ directory"
|
||||
|
||||
# Database restore (use: make db-restore BACKUP=backup_20231201_120000.sql)
|
||||
db-restore:
|
||||
@if [ -z "$(BACKUP)" ]; then echo "Usage: make db-restore BACKUP=backup_file.sql"; exit 1; fi
|
||||
docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot < backups/$(BACKUP)
|
||||
@echo "Database restored from $(BACKUP)"
|
||||
|
||||
# Open shell in bot container
|
||||
shell-bot:
|
||||
docker compose exec bot /bin/bash
|
||||
|
||||
# Open shell in database container
|
||||
shell-db:
|
||||
docker compose exec postgres psql -U remnawave_user -d remnawave_bot
|
||||
|
||||
# Check services status
|
||||
status:
|
||||
docker compose ps
|
||||
|
||||
# View service resource usage
|
||||
stats:
|
||||
docker stats --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
|
||||
@@ -27,11 +27,11 @@
|
||||
<td width="50%">
|
||||
|
||||
### 👤 **Для пользователей**
|
||||
- 💰 **Управление балансом** - Telegram Stars + P2P через поддержку
|
||||
- 💰 **Управление балансом** - Telegram Stars + Telegram Tribute + P2P через поддержку
|
||||
- 🛒 **Покупка подписок** - различные тарифы с настройкой squad
|
||||
- 📱 **Управление подписками** - просмотр, продление, ссылки
|
||||
- 📱 **Управление подписками** - просмотр, продление, ссылки, настройка автопродления
|
||||
- 🎁 **Промокоды** - денежные бонусы
|
||||
- 👥 **Реферальная программа** - зарабатывай с друзей
|
||||
- 👥 **Реферальная программа** - зарабатывай с друзей и их последующих пополнений
|
||||
- 🎰 **Игра удачи** - ежедневные бонусы
|
||||
- 🆓 **Тестовая подписка** - бесплатная пробная версия
|
||||
- ♾️ **Автопродление** - настраиваемое автообновление
|
||||
@@ -55,87 +55,40 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
# 🚀 Автоматический установщик
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
(ТЕСТИРУЕТСЯ, ДРУЗЬЯ КАК БЫ ВАМ НЕ ХОТЕЛОСЬ, ЧТОБЫ ВСЕ САМО ПОСТАВИЛОСЬ, НО Я ПОКА НЕ МОГУ ЗАСТАВИТЬ СКРИПТ КОРРЕКТНО НАСТРАИВАТЬ ВАМ ВЕБХУК С ОБРАТНЫМ ПРОКСИ, ПОЭТОМУ СОБИРАЕМ РУЧКАМИ ДОКЕР БОТА, ПОДКЛЮЧАЕМ ЕГО К СЕТИ ВАШЕГО NGINX/CADDY ДОКЕРА. ДЕЛАЕМ РУЧКАМИ ОБРАТНО ПРОКСИ НА ХУК /tribute-webhook И КАЙФУЕМ - НЕЙРОНКА КЛАУД В ПОМОЩЬ)
|
||||
|
||||
### ⚡ Автоматическая установка (Ubuntu)
|
||||
## Структура проекта
|
||||
|
||||
Установите бота **одной командой** с интерактивным мастером настройки:
|
||||
После установки в `/opt/bedolaga-bot/`:
|
||||
|
||||
```bash
|
||||
curl -sSL https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh | sudo bash
|
||||
```
|
||||
/opt/bedolaga-bot/
|
||||
├── docker-compose.yml # Конфигурация контейнеров
|
||||
├── .env # Переменные окружения
|
||||
├── logs/ # Логи бота
|
||||
├── data/ # Данные приложения
|
||||
└── backups/ # Резервные копии БД
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>🔧 Что делает установщик</summary>
|
||||
|
||||
- ✅ Обновляет систему Ubuntu
|
||||
- ✅ Устанавливает Docker и зависимости
|
||||
- ✅ Создает структуру проекта в `/opt/bedolaga-bot`
|
||||
- ✅ Интерактивная настройка всех параметров
|
||||
- ✅ Создает systemd службу (опционально)
|
||||
- ✅ Запускает удобное меню управления
|
||||
|
||||
</details>
|
||||
|
||||
### 🎛 Интерактивное меню управления
|
||||
|
||||
После установки вы получаете полнофункциональное меню:
|
||||
|
||||
<img width="500" alt="Интерфейс управления ботом" src="https://github.com/user-attachments/assets/9ab876f5-2758-4c52-93dd-6c9c654a07aa">
|
||||
|
||||
**Доступные действия:**
|
||||
- 🚀 Запуск/остановка/перезапуск бота
|
||||
- 📺 Просмотр логов в реальном времени
|
||||
- 💾 Создание и восстановление резервных копий БД
|
||||
- ✏️ Редактирование конфигурации
|
||||
- 🩺 Диагностика и устранение проблем
|
||||
- 🔄 Автообновление бота
|
||||
|
||||
---
|
||||
|
||||
## 📖 Документация
|
||||
> 💡 **Совет:** После установки добавьте установщик в закладки для быстрого доступа к меню управления!
|
||||
|
||||
### 🎯 Варианты установки
|
||||
## 💡 Полезные команды
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Сценарий</th>
|
||||
<th>Описание</th>
|
||||
<th>Команда</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🏠 <strong>Локальная установка</strong></td>
|
||||
<td>Панель + бот на одном сервере</td>
|
||||
<td><code>./bedolaga.sh</code> → вариант 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🌐 <strong>Удаленная установка</strong></td>
|
||||
<td>Бот отдельно от панели</td>
|
||||
<td><code>./bedolaga.sh</code> → вариант 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>⚡ <strong>Расширенная</strong></td>
|
||||
<td>С Redis и Nginx</td>
|
||||
<td><code>./bedolaga.sh</code> → вариант 3</td>
|
||||
</tr>
|
||||
</table>
|
||||
# Быстрый перезапуск
|
||||
docker compose restart
|
||||
|
||||
### ⚙️ Ключевые настройки
|
||||
# Очистка логов
|
||||
docker compose logs --tail=100
|
||||
|
||||
<details>
|
||||
<summary>📋 Основные параметры</summary>
|
||||
# Проверка использования ресурсов
|
||||
docker stats
|
||||
|
||||
| Переменная | Описание | Пример |
|
||||
|------------|----------|---------|
|
||||
| `BOT_TOKEN` | Токен от [@BotFather](https://t.me/BotFather) | `123456:ABC-DEF...` |
|
||||
| `BOT_USERNAME` | Username бота (без @) | `your_bot` |
|
||||
| `REMNAWAVE_URL` | URL панели Remnawave | `https://panel.com` |
|
||||
| `REMNAWAVE_TOKEN` | API токен панели | `your_api_token` |
|
||||
| `ADMIN_IDS` | ID администраторов | `123456789,987654321` |
|
||||
|
||||
</details>
|
||||
# Резервная копия одной командой
|
||||
docker compose exec postgres pg_dump -U remnawave_user remnawave_bot > backup.sql
|
||||
|
||||
<details>
|
||||
<summary>🎁 Реферальная программа</summary>
|
||||
@@ -256,8 +209,11 @@ curl -sSL https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bed
|
||||
}
|
||||
|
||||
3.3 Рестартим Бота, рестратим Caddy/Nginx
|
||||
4) Указываем ссылку на наш хук в настройках Tribute: https://test.example.com/tribute-webhook
|
||||
|
||||
4) Указываем ссылку на наш хук в настройках Tribute: https://test.example.com/tribute-webhook, создаем API ключ вписываем в .env бота
|
||||
|
||||
5) Делаем тест - если успешно, значит все настроили правильно. Не успешный тест - проверяй доступность хука, где-то ты что-то не так настроил
|
||||
|
||||
6) Тестируем пополнение через бота
|
||||
|
||||
|
||||
@@ -570,14 +526,14 @@ emergency-fix:
|
||||
- ✅ **Правила сервиса** - настройка через админ-панель
|
||||
- ✅ **Автоплатежи** - настраиваемое автопродление подписок
|
||||
- ✅ **Просмотр подписок пользователей** - детальная статистика
|
||||
|
||||
- - ✅ **Автоматическое пополнение лк с помощью доната Tribute**
|
||||
### 🎯 В планах
|
||||
|
||||
| Версия | Функция | Приоритет | Описание |
|
||||
|--------|---------|-----------|----------|
|
||||
| **v1.4.0** | ЮKassa интеграция | 🔴 High | Автоматические платежи |
|
||||
| **v1.4.0** | Веб-панель управления | 🟡 Medium | Полный веб-интерфейс |
|
||||
| **v1.4.0** | Безопасное удаление | 🟡 Medium | Архивирование вместо удаления |
|
||||
| **v1.4.5** | Юкасса интеграция | 🔴 High | Автоматические платежи |
|
||||
| **v1.4.5** | Веб-панель управления | 🟡 Medium | Полный веб-интерфейс |
|
||||
| **v1.4.5** | Безопасное удаление | 🟡 Medium | Архивирование вместо удаления |
|
||||
| **v1.5.0** | Дополнительные платежи | 🟡 Medium | Сбербанк, Tinkoff, Crypto |
|
||||
| **v1.5.0** | Уведомления | 🟡 Medium | Webhook, Email, другие чаты |
|
||||
| **v1.6.0** | Система блокировок | 🟢 Low | Бан/разбан пользователей |
|
||||
@@ -618,7 +574,8 @@ emergency-fix:
|
||||
|
||||
### 📞 Контакты
|
||||
|
||||
- **Telegram:** [@fringg](https://t.me/fringg)
|
||||
- **Telegram:** [@fringg](https://t.me/fringg) - Писать исключительно по делу, я бы конечно был рад всем помочь настроить remnawave, ваши ноды, настроить вам бота, настроить вебхуки, но ребят я один и бота пилю тоже в соло, помощь вам занимает крайне много времени, поэтому если надумаете просить помочь что-то настроить, готовьтесь дарить мне шпагу подарком в тг)(
|
||||
|
||||
- **Issues:** [GitHub Issues](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/issues)
|
||||
- **Discussions:** [GitHub Discussions](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/discussions)
|
||||
|
||||
@@ -639,16 +596,43 @@ emergency-fix:
|
||||
|
||||
<div align="center">
|
||||
|
||||
### 🌟 Понравился проект? Поставьте звезду!
|
||||
## ⭐ История проекта
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
[](https://star-history.com/#Fr1ngg/remnawave-bedolaga-telegram-bot&Date)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
### 💝 Донатеры
|
||||
|
||||
Спасибо всем, кто поддерживает проект!
|
||||
|
||||
| Донатер | Сумма |
|
||||
|---------|-------|
|
||||
| Илья (@ispanec_nn) | $15 |
|
||||
| @Legacyyy777 | 600 руб |
|
||||
|
||||
---
|
||||
|
||||
### Поддержать проект
|
||||
[](https://t.me/tribute/app?startapp=duUO)
|
||||
|
||||
---
|
||||
|
||||
**💝 Создано с любовью для Remnawave сообщества**
|
||||
|
||||
*Автор не является профессиональным разработчиком, но прикладывает все усилия для создания удобного бота для ваших сервисов* 💪
|
||||
|
||||
*The English Readme is currently being written, with revisions for various payment systems and languages. If you have the opportunity, please help me with local optimization and translations. This would greatly help me in writing an open source project that is accessible to different markets/countries.*
|
||||
|
||||
[🔝 Вернуться наверх](#-remnawave-bedolaga-bot)
|
||||
|
||||
</div>
|
||||
|
||||
+255
-52
@@ -5,7 +5,7 @@ from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from database import Database, User, ReferralProgram, ReferralEarning, ServiceRule
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
@@ -75,38 +75,74 @@ async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database
|
||||
try:
|
||||
db_stats = await db.get_stats()
|
||||
|
||||
# Статистика игры в удачу
|
||||
referral_stats = await get_referral_stats(db)
|
||||
|
||||
lucky_stats = await db.get_lucky_game_admin_stats()
|
||||
|
||||
recent_topups = await get_recent_topups(db)
|
||||
recent_lucky_games = await get_recent_lucky_games(db)
|
||||
recent_ref_earnings = await get_recent_referral_earnings(db)
|
||||
|
||||
text = "📊 Расширенная статистика системы\n\n"
|
||||
|
||||
text += "💾 База данных бота:\n"
|
||||
text += f"👥 Пользователей: {db_stats['total_users']}\n"
|
||||
text += f"📋 Подписок: {db_stats['total_subscriptions_non_trial']}\n"
|
||||
text += f"💰 Доходы: {db_stats['total_revenue']} руб.\n\n"
|
||||
text += f"💰 Доходы: {db_stats['total_revenue']:.1f}₽\n\n"
|
||||
|
||||
text += "👥 Реферальная программа:\n"
|
||||
text += f"🎁 Всего выплачено: {referral_stats['total_paid']:.1f}₽\n"
|
||||
text += f"👤 Активных рефереров: {referral_stats['active_referrers']}\n"
|
||||
text += f"🔥 Всего рефералов: {referral_stats['total_referrals']}\n\n"
|
||||
|
||||
text += "🎰 Игра в удачу:\n"
|
||||
if lucky_stats['total_games'] > 0:
|
||||
if lucky_stats and lucky_stats['total_games'] > 0:
|
||||
text += f"🎲 Всего игр: {lucky_stats['total_games']}\n"
|
||||
text += f"🏆 Выигрышей: {lucky_stats['total_wins']} ({lucky_stats['win_rate']:.1f}%)\n"
|
||||
text += f"👥 Уникальных игроков: {lucky_stats['unique_players']}\n"
|
||||
text += f"💎 Выплачено наград: {lucky_stats['total_rewards']:.0f}₽\n"
|
||||
text += f"📈 Средняя награда: {lucky_stats['avg_reward']:.0f}₽\n\n"
|
||||
text += f"💎 Выплачено наград: {lucky_stats['total_rewards']:.1f}₽\n"
|
||||
|
||||
if lucky_stats['games_today'] > 0:
|
||||
text += f"📅 За сегодня:\n"
|
||||
text += f" • Игр: {lucky_stats['games_today']}\n"
|
||||
text += f" • Выигрышей: {lucky_stats['wins_today']} ({lucky_stats['win_rate_today']:.1f}%)\n"
|
||||
text += f" • Выплачено: {lucky_stats['wins_today'] * lucky_stats['avg_reward']:.0f}₽\n\n"
|
||||
|
||||
if lucky_stats['last_game']:
|
||||
last_game_str = format_datetime(
|
||||
datetime.fromisoformat(lucky_stats['last_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
text += f"🕐 Последняя игра: {last_game_str}\n\n"
|
||||
if lucky_stats.get('games_today', 0) > 0:
|
||||
text += f"📅 За сегодня: {lucky_stats['games_today']} игр, {lucky_stats['wins_today']} побед\n"
|
||||
else:
|
||||
text += "🎯 Игр еще не было\n\n"
|
||||
text += "🎯 Игр еще не было\n"
|
||||
text += "\n"
|
||||
|
||||
if recent_topups:
|
||||
text += "💰 Последние 5 пополнений:\n"
|
||||
for topup in recent_topups:
|
||||
username = topup.get('username') or 'N/A'
|
||||
try:
|
||||
date_str = format_datetime(topup['created_at'], user.language)
|
||||
except Exception:
|
||||
date_str = str(topup['created_at'])[:16]
|
||||
text += f"• @{username}: {topup['amount']:.0f}₽ ({date_str})\n"
|
||||
text += "\n"
|
||||
|
||||
if recent_lucky_games:
|
||||
text += "🎰 Последние 5 игр в удачу:\n"
|
||||
for game in recent_lucky_games:
|
||||
username = game.get('username') or 'N/A'
|
||||
result = "🏆" if game.get('is_winner') else "❌"
|
||||
reward = f" +{game['reward_amount']:.0f}₽" if game.get('is_winner') else ""
|
||||
try:
|
||||
date_str = format_datetime(game['played_at'], user.language)
|
||||
except Exception:
|
||||
date_str = str(game['played_at'])[:16]
|
||||
text += f"• {result} @{username}: #{game['chosen_number']}{reward} ({date_str})\n"
|
||||
text += "\n"
|
||||
|
||||
if recent_ref_earnings:
|
||||
text += "🎁 Последние 5 реферальных выплат:\n"
|
||||
for earning in recent_ref_earnings:
|
||||
referrer_name = earning.get('referrer_name') or 'N/A'
|
||||
earning_type = "🎁" if earning.get('earning_type') == 'first_reward' else "💵"
|
||||
try:
|
||||
date_str = format_datetime(earning['created_at'], user.language)
|
||||
except Exception:
|
||||
date_str = str(earning['created_at'])[:16]
|
||||
text += f"• {earning_type} @{referrer_name}: {earning['amount']:.0f}₽ ({date_str})\n"
|
||||
text += "\n"
|
||||
|
||||
if api:
|
||||
try:
|
||||
@@ -120,7 +156,8 @@ async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🎰 Детали игры в удачу", callback_data="lucky_game_admin_details")],
|
||||
[InlineKeyboardButton(text="🖥 Подробная системная статистика", callback_data="admin_system")],
|
||||
[InlineKeyboardButton(text="👥 Реферальная статистика", callback_data="referral_statistics")],
|
||||
[InlineKeyboardButton(text="🖥 Системная статистика", callback_data="admin_system")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stats")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', user.language), callback_data="admin_panel")]
|
||||
])
|
||||
@@ -130,10 +167,151 @@ async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting statistics: {e}")
|
||||
await callback.message.edit_text(
|
||||
t('error_occurred', user.language),
|
||||
"❌ " + t('error_occurred', user.language),
|
||||
reply_markup=back_keyboard("admin_panel", user.language)
|
||||
)
|
||||
|
||||
async def get_referral_stats(db: Database) -> Dict[str, Any]:
|
||||
try:
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select, func, and_
|
||||
from database import ReferralProgram, ReferralEarning
|
||||
|
||||
total_paid = await session.execute(
|
||||
select(func.sum(ReferralEarning.amount))
|
||||
)
|
||||
total_paid = total_paid.scalar() or 0.0
|
||||
|
||||
active_referrers = await session.execute(
|
||||
select(func.count(func.distinct(ReferralEarning.referrer_id)))
|
||||
)
|
||||
active_referrers = active_referrers.scalar() or 0
|
||||
|
||||
total_referrals = await session.execute(
|
||||
select(func.count(ReferralProgram.id)).where(
|
||||
and_(
|
||||
ReferralProgram.referred_id < 900000000,
|
||||
ReferralProgram.referred_id > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
total_referrals = total_referrals.scalar() or 0
|
||||
|
||||
return {
|
||||
'total_paid': total_paid,
|
||||
'active_referrers': active_referrers,
|
||||
'total_referrals': total_referrals
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting referral stats: {e}")
|
||||
return {'total_paid': 0.0, 'active_referrers': 0, 'total_referrals': 0}
|
||||
|
||||
async def get_recent_topups(db: Database) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select, desc
|
||||
from database import Payment, User
|
||||
|
||||
result = await session.execute(
|
||||
select(
|
||||
Payment.amount,
|
||||
Payment.created_at,
|
||||
Payment.payment_type,
|
||||
User.username,
|
||||
User.first_name
|
||||
).select_from(
|
||||
Payment.__table__.join(User.__table__, Payment.user_id == User.telegram_id)
|
||||
).where(
|
||||
and_(
|
||||
Payment.status == 'completed',
|
||||
Payment.payment_type.in_(['topup', 'subscription', 'subscription_extend', 'promocode', 'admin_topup', 'stars'])
|
||||
)
|
||||
).order_by(desc(Payment.created_at)).limit(5)
|
||||
)
|
||||
|
||||
topups = []
|
||||
for row in result.fetchall():
|
||||
topups.append({
|
||||
'amount': row.amount,
|
||||
'created_at': row.created_at,
|
||||
'payment_type': row.payment_type,
|
||||
'username': row.username,
|
||||
'first_name': row.first_name
|
||||
})
|
||||
|
||||
return topups
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recent topups: {e}")
|
||||
return []
|
||||
|
||||
async def get_recent_lucky_games(db: Database) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select, desc
|
||||
from database import LuckyGame, User
|
||||
|
||||
result = await session.execute(
|
||||
select(
|
||||
LuckyGame.chosen_number,
|
||||
LuckyGame.is_winner,
|
||||
LuckyGame.reward_amount,
|
||||
LuckyGame.played_at,
|
||||
User.username,
|
||||
User.first_name
|
||||
).select_from(
|
||||
LuckyGame.__table__.join(User.__table__, LuckyGame.user_id == User.telegram_id)
|
||||
).order_by(desc(LuckyGame.played_at)).limit(5)
|
||||
)
|
||||
|
||||
games = []
|
||||
for row in result.fetchall():
|
||||
games.append({
|
||||
'chosen_number': row.chosen_number,
|
||||
'is_winner': row.is_winner,
|
||||
'reward_amount': row.reward_amount,
|
||||
'played_at': row.played_at,
|
||||
'username': row.username,
|
||||
'first_name': row.first_name
|
||||
})
|
||||
|
||||
return games
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recent lucky games: {e}")
|
||||
return []
|
||||
|
||||
async def get_recent_referral_earnings(db: Database) -> List[Dict[str, Any]]:
|
||||
try:
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select, desc
|
||||
from database import ReferralEarning, User
|
||||
|
||||
result = await session.execute(
|
||||
select(
|
||||
ReferralEarning.amount,
|
||||
ReferralEarning.earning_type,
|
||||
ReferralEarning.created_at,
|
||||
User.username,
|
||||
User.first_name
|
||||
).select_from(
|
||||
ReferralEarning.__table__.join(User.__table__, ReferralEarning.referrer_id == User.telegram_id)
|
||||
).order_by(desc(ReferralEarning.created_at)).limit(5)
|
||||
)
|
||||
|
||||
earnings = []
|
||||
for row in result.fetchall():
|
||||
earnings.append({
|
||||
'amount': row.amount,
|
||||
'earning_type': row.earning_type,
|
||||
'created_at': row.created_at,
|
||||
'referrer_name': row.username,
|
||||
'referrer_first_name': row.first_name
|
||||
})
|
||||
|
||||
return earnings
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting recent referral earnings: {e}")
|
||||
return []
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_subscriptions")
|
||||
async def admin_subscriptions_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
@@ -8330,58 +8508,83 @@ async def lucky_game_admin_details_callback(callback: CallbackQuery, user: User,
|
||||
|
||||
text = "🎰 **Детальная статистика игры в удачу**\n\n"
|
||||
|
||||
if lucky_stats['total_games'] > 0:
|
||||
if lucky_stats and lucky_stats.get('total_games', 0) > 0:
|
||||
text += "📊 **Общая статистика:**\n"
|
||||
text += f"🎲 Всего игр сыграно: {lucky_stats['total_games']}\n"
|
||||
text += f"🏆 Всего выигрышей: {lucky_stats['total_wins']}\n"
|
||||
text += f"📈 Процент побед: {lucky_stats['win_rate']:.2f}%\n"
|
||||
text += f"👥 Уникальных игроков: {lucky_stats['unique_players']}\n"
|
||||
text += f"💎 Общая сумма выплат: {lucky_stats['total_rewards']:.0f}₽\n"
|
||||
text += f"💰 Средняя выплата: {lucky_stats['avg_reward']:.1f}₽\n\n"
|
||||
|
||||
if lucky_stats.get('avg_reward', 0) > 0:
|
||||
text += f"💰 Средняя выплата: {lucky_stats['avg_reward']:.1f}₽\n"
|
||||
text += "\n"
|
||||
|
||||
text += "📅 **За сегодня:**\n"
|
||||
text += f"🎯 Игр: {lucky_stats['games_today']}\n"
|
||||
text += f"🎉 Выигрышей: {lucky_stats['wins_today']}\n"
|
||||
if lucky_stats['games_today'] > 0:
|
||||
text += f"📊 Процент побед: {lucky_stats['win_rate_today']:.1f}%\n"
|
||||
today_payouts = lucky_stats['wins_today'] * lucky_stats['avg_reward']
|
||||
text += f"💸 Выплачено сегодня: {today_payouts:.0f}₽\n"
|
||||
text += f"🎯 Игр: {lucky_stats.get('games_today', 0)}\n"
|
||||
text += f"🎉 Выигрышей: {lucky_stats.get('wins_today', 0)}\n"
|
||||
if lucky_stats.get('games_today', 0) > 0:
|
||||
text += f"📊 Процент побед: {lucky_stats.get('win_rate_today', 0):.1f}%\n"
|
||||
text += "\n"
|
||||
|
||||
if top_players:
|
||||
text += "🏆 **Топ-5 игроков:**\n"
|
||||
for i, player in enumerate(top_players, 1):
|
||||
name = player['first_name'] or player['username']
|
||||
text += f"{i}. {name} (ID: {player['user_id']})\n"
|
||||
text += f" 💰 Выиграл: {player['total_won']:.0f}₽\n"
|
||||
text += f" 🎯 Игр: {player['games_played']} | Побед: {player['wins']} ({player['win_rate']:.1f}%)\n"
|
||||
name = player.get('first_name', 'Unknown')
|
||||
if not name or name == 'Unknown':
|
||||
name = player.get('username', 'N/A')
|
||||
|
||||
if player['last_game']:
|
||||
last_game = format_datetime(
|
||||
datetime.fromisoformat(player['last_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
text += f" 🕐 Последняя игра: {last_game}\n"
|
||||
text += f"{i}. {name}\n"
|
||||
text += f" 💰 Выиграл: {player.get('total_won', 0):.0f}₽\n"
|
||||
text += f" 🎯 Игр: {player.get('games_played', 0)} | "
|
||||
text += f"Побед: {player.get('wins', 0)} ({player.get('win_rate', 0):.1f}%)\n"
|
||||
|
||||
if player.get('last_game'):
|
||||
try:
|
||||
if isinstance(player['last_game'], str):
|
||||
last_game_dt = datetime.fromisoformat(player['last_game']).replace(tzinfo=None)
|
||||
else:
|
||||
last_game_dt = player['last_game']
|
||||
|
||||
last_game = format_datetime(last_game_dt, user.language)
|
||||
text += f" 🕐 Последняя игра: {last_game}\n"
|
||||
except Exception as e:
|
||||
logger.warning(f"Error formatting last_game: {e}")
|
||||
text += f" 🕐 Последняя игра: {str(player['last_game'])[:16]}\n"
|
||||
text += "\n"
|
||||
|
||||
if lucky_stats['first_game'] and lucky_stats['last_game']:
|
||||
first_game = format_datetime(
|
||||
datetime.fromisoformat(lucky_stats['first_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
last_game = format_datetime(
|
||||
datetime.fromisoformat(lucky_stats['last_game']).replace(tzinfo=None),
|
||||
user.language
|
||||
)
|
||||
first_game = lucky_stats.get('first_game')
|
||||
last_game = lucky_stats.get('last_game')
|
||||
|
||||
if first_game and last_game:
|
||||
text += f"🕐 **Временные рамки:**\n"
|
||||
text += f"🥇 Первая игра: {first_game}\n"
|
||||
text += f"🕐 Последняя игра: {last_game}\n\n"
|
||||
try:
|
||||
if isinstance(first_game, str):
|
||||
first_game_dt = datetime.fromisoformat(first_game).replace(tzinfo=None)
|
||||
else:
|
||||
first_game_dt = first_game
|
||||
|
||||
if isinstance(last_game, str):
|
||||
last_game_dt = datetime.fromisoformat(last_game).replace(tzinfo=None)
|
||||
else:
|
||||
last_game_dt = last_game
|
||||
|
||||
first_game_str = format_datetime(first_game_dt, user.language)
|
||||
last_game_str = format_datetime(last_game_dt, user.language)
|
||||
|
||||
text += f"🥇 Первая игра: {first_game_str}\n"
|
||||
text += f"🕐 Последняя игра: {last_game_str}\n\n"
|
||||
except Exception as e:
|
||||
logger.warning(f"Error formatting game times: {e}")
|
||||
text += f"🥇 Первая игра: {str(first_game)[:16]}\n"
|
||||
text += f"🕐 Последняя игра: {str(last_game)[:16]}\n\n"
|
||||
|
||||
else:
|
||||
text += "🎯 В игру в удачу еще никто не играл.\n\n"
|
||||
text += "Игроки смогут играть после активации функции в боте."
|
||||
text += "Игроки смогут играть после активации функции в боте.\n\n"
|
||||
|
||||
text += f"🕕 _Обновлено: {format_datetime(datetime.now(), user.language)}_"
|
||||
current_time = datetime.now()
|
||||
text += f"🕕 _Обновлено: {format_datetime(current_time, user.language)}_"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="lucky_game_admin_details")],
|
||||
|
||||
+1168
-1223
File diff suppressed because it is too large
Load Diff
+143
-94
@@ -662,28 +662,38 @@ class Database:
|
||||
async def get_stats(self) -> dict:
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select, func, and_
|
||||
|
||||
total_users = await session.execute(
|
||||
select(func.count(User.id))
|
||||
)
|
||||
total_users = total_users.scalar()
|
||||
|
||||
|
||||
total_subs_non_trial = await session.execute(
|
||||
select(func.count(UserSubscription.id))
|
||||
.join(Subscription, UserSubscription.subscription_id == Subscription.id)
|
||||
.where(Subscription.is_trial == False)
|
||||
)
|
||||
total_subs_non_trial = total_subs_non_trial.scalar()
|
||||
|
||||
|
||||
total_payments = await session.execute(
|
||||
select(func.sum(Payment.amount)).where(
|
||||
Payment.status == 'completed',
|
||||
Payment.payment_type != 'trial'
|
||||
and_(
|
||||
Payment.status == 'completed',
|
||||
Payment.payment_type.in_([
|
||||
'topup', # Обычные пополнения
|
||||
'subscription', # Покупка подписок
|
||||
'subscription_extend', # Продление подписок
|
||||
'promocode', # Активация промокодов
|
||||
'admin_topup', # Пополнения администратором
|
||||
'stars', # Пополнения через Telegram Stars
|
||||
'autopay' # Автоплатежи
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
total_payments = total_payments.scalar() or 0
|
||||
|
||||
|
||||
return {
|
||||
'total_users': total_users,
|
||||
'total_subscriptions_non_trial': total_subs_non_trial,
|
||||
@@ -2437,98 +2447,137 @@ class Database:
|
||||
}
|
||||
|
||||
async def get_lucky_game_admin_stats(self) -> dict:
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total_games,
|
||||
COUNT(CASE WHEN is_winner = 1 THEN 1 END) as total_wins,
|
||||
COUNT(DISTINCT user_id) as unique_players,
|
||||
SUM(reward_amount) as total_rewards,
|
||||
AVG(reward_amount) as avg_reward,
|
||||
COUNT(CASE WHEN DATE(played_at) = DATE('now') THEN 1 END) as games_today,
|
||||
COUNT(CASE WHEN DATE(played_at) = DATE('now') AND is_winner = 1 THEN 1 END) as wins_today,
|
||||
MAX(played_at) as last_game,
|
||||
MIN(played_at) as first_game
|
||||
FROM lucky_games
|
||||
"""
|
||||
|
||||
async with self.get_connection() as conn:
|
||||
async with conn.execute(query) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
|
||||
if row:
|
||||
stats = {
|
||||
'total_games': row[0] or 0,
|
||||
'total_wins': row[1] or 0,
|
||||
'unique_players': row[2] or 0,
|
||||
'total_rewards': row[3] or 0.0,
|
||||
'avg_reward': row[4] or 0.0,
|
||||
'games_today': row[5] or 0,
|
||||
'wins_today': row[6] or 0,
|
||||
'last_game': row[7],
|
||||
'first_game': row[8]
|
||||
}
|
||||
|
||||
if stats['total_games'] > 0:
|
||||
stats['win_rate'] = (stats['total_wins'] / stats['total_games']) * 100
|
||||
stats['win_rate_today'] = (stats['wins_today'] / stats['games_today']) * 100 if stats['games_today'] > 0 else 0
|
||||
else:
|
||||
stats['win_rate'] = 0
|
||||
stats['win_rate_today'] = 0
|
||||
|
||||
return stats
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, func, and_, text
|
||||
from datetime import date
|
||||
|
||||
total_games = await session.execute(
|
||||
select(func.count(LuckyGame.id))
|
||||
)
|
||||
total_games = total_games.scalar() or 0
|
||||
|
||||
total_wins = await session.execute(
|
||||
select(func.count(LuckyGame.id)).where(LuckyGame.is_winner == True)
|
||||
)
|
||||
total_wins = total_wins.scalar() or 0
|
||||
|
||||
unique_players = await session.execute(
|
||||
select(func.count(func.distinct(LuckyGame.user_id)))
|
||||
)
|
||||
unique_players = unique_players.scalar() or 0
|
||||
|
||||
total_rewards = await session.execute(
|
||||
select(func.sum(LuckyGame.reward_amount))
|
||||
)
|
||||
total_rewards = total_rewards.scalar() or 0.0
|
||||
|
||||
avg_reward = await session.execute(
|
||||
select(func.avg(LuckyGame.reward_amount)).where(LuckyGame.is_winner == True)
|
||||
)
|
||||
avg_reward = avg_reward.scalar() or 0.0
|
||||
|
||||
today = date.today()
|
||||
games_today = await session.execute(
|
||||
select(func.count(LuckyGame.id)).where(
|
||||
func.date(LuckyGame.played_at) == today
|
||||
)
|
||||
)
|
||||
games_today = games_today.scalar() or 0
|
||||
|
||||
return {
|
||||
'total_games': 0, 'total_wins': 0, 'unique_players': 0,
|
||||
'total_rewards': 0.0, 'avg_reward': 0.0, 'games_today': 0,
|
||||
'wins_today': 0, 'win_rate': 0, 'win_rate_today': 0,
|
||||
'last_game': None, 'first_game': None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lucky game admin stats: {e}")
|
||||
return {
|
||||
'total_games': 0, 'total_wins': 0, 'unique_players': 0,
|
||||
'total_rewards': 0.0, 'avg_reward': 0.0, 'games_today': 0,
|
||||
'wins_today': 0, 'win_rate': 0, 'win_rate_today': 0,
|
||||
'last_game': None, 'first_game': None
|
||||
}
|
||||
wins_today = await session.execute(
|
||||
select(func.count(LuckyGame.id)).where(
|
||||
and_(
|
||||
func.date(LuckyGame.played_at) == today,
|
||||
LuckyGame.is_winner == True
|
||||
)
|
||||
)
|
||||
)
|
||||
wins_today = wins_today.scalar() or 0
|
||||
|
||||
first_game = await session.execute(
|
||||
select(LuckyGame.played_at).order_by(LuckyGame.played_at.asc()).limit(1)
|
||||
)
|
||||
first_game_date = first_game.scalar()
|
||||
|
||||
last_game = await session.execute(
|
||||
select(LuckyGame.played_at).order_by(LuckyGame.played_at.desc()).limit(1)
|
||||
)
|
||||
last_game_date = last_game.scalar()
|
||||
|
||||
win_rate = (total_wins / total_games * 100) if total_games > 0 else 0
|
||||
win_rate_today = (wins_today / games_today * 100) if games_today > 0 else 0
|
||||
|
||||
return {
|
||||
'total_games': total_games,
|
||||
'total_wins': total_wins,
|
||||
'unique_players': unique_players,
|
||||
'total_rewards': total_rewards,
|
||||
'avg_reward': avg_reward,
|
||||
'games_today': games_today,
|
||||
'wins_today': wins_today,
|
||||
'win_rate': win_rate,
|
||||
'win_rate_today': win_rate_today,
|
||||
'first_game': first_game_date.isoformat() if first_game_date else None,
|
||||
'last_game': last_game_date.isoformat() if last_game_date else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lucky game admin stats: {e}")
|
||||
return {
|
||||
'total_games': 0,
|
||||
'total_wins': 0,
|
||||
'unique_players': 0,
|
||||
'total_rewards': 0.0,
|
||||
'avg_reward': 0.0,
|
||||
'games_today': 0,
|
||||
'wins_today': 0,
|
||||
'win_rate': 0,
|
||||
'win_rate_today': 0,
|
||||
'first_game': None,
|
||||
'last_game': None
|
||||
}
|
||||
|
||||
async def get_lucky_game_top_players(self, limit: int = 5) -> List[dict]:
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
lg.user_id,
|
||||
u.username,
|
||||
u.first_name,
|
||||
COUNT(*) as games_played,
|
||||
COUNT(CASE WHEN lg.is_winner = 1 THEN 1 END) as wins,
|
||||
SUM(lg.reward_amount) as total_won,
|
||||
MAX(lg.played_at) as last_game
|
||||
FROM lucky_games lg
|
||||
LEFT JOIN users u ON lg.user_id = u.telegram_id
|
||||
GROUP BY lg.user_id
|
||||
ORDER BY total_won DESC, wins DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
|
||||
async with self.get_connection() as conn:
|
||||
async with conn.execute(query, (limit,)) as cursor:
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
'user_id': row[0],
|
||||
'username': row[1] or 'N/A',
|
||||
'first_name': row[2] or 'Unknown',
|
||||
'games_played': row[3],
|
||||
'wins': row[4],
|
||||
'total_won': row[5],
|
||||
'last_game': row[6],
|
||||
'win_rate': (row[4] / row[3]) * 100 if row[3] > 0 else 0
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
async with self.session_factory() as session:
|
||||
from sqlalchemy import select, func, case, desc
|
||||
from database import LuckyGame, User
|
||||
|
||||
query = select(
|
||||
LuckyGame.user_id,
|
||||
User.username,
|
||||
User.first_name,
|
||||
func.count(LuckyGame.id).label('games_played'),
|
||||
func.count(case((LuckyGame.is_winner == True, 1))).label('wins'),
|
||||
func.sum(LuckyGame.reward_amount).label('total_won'),
|
||||
func.max(LuckyGame.played_at).label('last_game')
|
||||
).select_from(
|
||||
LuckyGame.__table__.join(User.__table__, LuckyGame.user_id == User.telegram_id)
|
||||
).group_by(
|
||||
LuckyGame.user_id, User.username, User.first_name
|
||||
).order_by(
|
||||
desc(func.sum(LuckyGame.reward_amount)),
|
||||
desc(func.count(case((LuckyGame.is_winner == True, 1))))
|
||||
).limit(limit)
|
||||
|
||||
result = await session.execute(query)
|
||||
players = []
|
||||
|
||||
for row in result.fetchall():
|
||||
win_rate = (row.wins / row.games_played) * 100 if row.games_played > 0 else 0
|
||||
players.append({
|
||||
'user_id': row.user_id,
|
||||
'username': row.username or 'N/A',
|
||||
'first_name': row.first_name or 'Unknown',
|
||||
'games_played': row.games_played,
|
||||
'wins': row.wins,
|
||||
'total_won': row.total_won or 0.0,
|
||||
'last_game': row.last_game.isoformat() if row.last_game else None,
|
||||
'win_rate': win_rate
|
||||
})
|
||||
|
||||
return players
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting lucky game top players: {e}")
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: remnawave_bot_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: remnawave_bot
|
||||
POSTGRES_USER: remnawave_user
|
||||
POSTGRES_PASSWORD: secure_password_123
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# RemnaWave Bot
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: remnawave_bot
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
# Override database URL to use PostgreSQL container
|
||||
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./data:/app/data
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# Redis (optional, for caching and session storage)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: remnawave_bot_redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass redis_password_123
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# Nginx (optional, for serving static files or reverse proxy)
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: remnawave_bot_nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
- ./static:/usr/share/nginx/html:ro
|
||||
networks:
|
||||
- bot_network
|
||||
depends_on:
|
||||
- bot
|
||||
profiles:
|
||||
- with-nginx
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
bot_network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
@@ -1,100 +0,0 @@
|
||||
"""
|
||||
Экстренное исправление проблемы с отображением подписок
|
||||
Этот патч добавляет недостающие поля в таблицу user_subscriptions
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем корневую директорию в путь
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config import load_config
|
||||
from database import Database
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def check_and_add_column(db, column_name, column_definition):
|
||||
"""Проверяет и добавляет колонку в таблицу"""
|
||||
try:
|
||||
# Отдельная транзакция для проверки
|
||||
async with db.engine.begin() as conn:
|
||||
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
|
||||
logger.info(f"✅ Поле {column_name} уже существует")
|
||||
return True
|
||||
except Exception:
|
||||
# Отдельная транзакция для добавления колонки
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
logger.info(f"➕ Добавляю поле {column_name}...")
|
||||
await conn.execute(text(f"""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN {column_name} {column_definition}
|
||||
"""))
|
||||
logger.info(f"✅ Поле {column_name} добавлено")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
|
||||
return False
|
||||
|
||||
async def emergency_fix():
|
||||
"""Экстренное исправление базы данных"""
|
||||
|
||||
try:
|
||||
# Загружаем конфигурацию
|
||||
config = load_config()
|
||||
|
||||
# Подключаемся к базе данных
|
||||
db = Database(config.DATABASE_URL)
|
||||
|
||||
logger.info("🔧 Выполняю экстренное исправление базы данных...")
|
||||
|
||||
# Проверяем существование таблицы user_subscriptions
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
|
||||
count = result.scalar()
|
||||
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
|
||||
await db.close()
|
||||
return
|
||||
|
||||
# Добавляем поля по одному в отдельных транзакциях
|
||||
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
|
||||
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
|
||||
|
||||
# Финальная проверка в отдельной транзакции
|
||||
if success1 and success2:
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("""
|
||||
SELECT id, auto_pay_enabled, auto_pay_days_before
|
||||
FROM user_subscriptions LIMIT 1
|
||||
"""))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
logger.info("✅ Все поля доступны для чтения")
|
||||
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
|
||||
else:
|
||||
logger.info("✅ Все поля доступны, но таблица пуста")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Поля все еще недоступны: {e}")
|
||||
else:
|
||||
logger.error("❌ Не удалось добавить все необходимые поля")
|
||||
|
||||
await db.close()
|
||||
logger.info("🎉 Экстренное исправление завершено!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Критическая ошибка: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(emergency_fix())
|
||||
+50
-86
@@ -176,14 +176,14 @@ async def language_callback(callback: CallbackQuery, state: FSMContext, db: Data
|
||||
async def show_main_menu(message: Message, lang: str, is_admin: bool = False, user_id: int = None, db: Database = None, config: Config = None):
|
||||
try:
|
||||
show_trial = False
|
||||
show_lucky_game = True
|
||||
show_lucky_game = False
|
||||
|
||||
if config and config.TRIAL_ENABLED and user_id and db:
|
||||
has_used = await db.has_used_trial(user_id)
|
||||
show_trial = not has_used
|
||||
|
||||
if config:
|
||||
show_lucky_game = getattr(config, 'LUCKY_GAME_ENABLED', True)
|
||||
if config and hasattr(config, 'LUCKY_GAME_ENABLED'):
|
||||
show_lucky_game = config.LUCKY_GAME_ENABLED
|
||||
|
||||
await message.answer(
|
||||
t('main_menu', lang),
|
||||
@@ -193,6 +193,22 @@ async def show_main_menu(message: Message, lang: str, is_admin: bool = False, us
|
||||
logger.error(f"Error showing main menu: {e}")
|
||||
await message.answer("❌ Ошибка отображения меню")
|
||||
|
||||
def get_main_menu_keyboard(lang: str, is_admin: bool = False, config: Config = None, db: Database = None, user_id: int = None):
|
||||
show_trial = False
|
||||
show_lucky_game = False
|
||||
|
||||
if config and config.TRIAL_ENABLED and user_id and db:
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
if config and hasattr(config, 'LUCKY_GAME_ENABLED'):
|
||||
show_lucky_game = config.LUCKY_GAME_ENABLED
|
||||
|
||||
return main_menu_keyboard(lang, is_admin, show_trial, show_lucky_game)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "main_menu")
|
||||
async def main_menu_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
@@ -204,6 +220,7 @@ async def main_menu_callback(callback: CallbackQuery, **kwargs):
|
||||
return
|
||||
|
||||
show_trial = False
|
||||
show_lucky_game = False
|
||||
|
||||
if config and config.TRIAL_ENABLED and db:
|
||||
try:
|
||||
@@ -212,9 +229,12 @@ async def main_menu_callback(callback: CallbackQuery, **kwargs):
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking trial availability: {e}")
|
||||
|
||||
if config and hasattr(config, 'LUCKY_GAME_ENABLED'):
|
||||
show_lucky_game = config.LUCKY_GAME_ENABLED
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('main_menu', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial)
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial, show_lucky_game)
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "trial_subscription")
|
||||
@@ -428,19 +448,19 @@ async def topup_balance_callback(callback: CallbackQuery, **kwargs):
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
|
||||
tribute_enabled = config and config.TRIBUTE_ENABLED
|
||||
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
|
||||
|
||||
text = "💰 Выберите способ пополнения баланса:"
|
||||
|
||||
if tribute_enabled:
|
||||
text += "\n\n💳 **Tribute** - карты, СБП, быстрые платежи"
|
||||
text += "\n\n💳 **Tribute** - Оплата банковской картой"
|
||||
if stars_enabled:
|
||||
text += "\n⭐ **Telegram Stars** - быстро и безопасно"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=topup_keyboard(user.language, tribute_enabled),
|
||||
reply_markup=topup_keyboard(user.language, tribute_enabled, stars_enabled),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@@ -1126,6 +1146,16 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
|
||||
|
||||
if result:
|
||||
logger.info(f"Successfully updated RemnaWave user expiry")
|
||||
|
||||
try:
|
||||
traffic_reset = await api.reset_user_traffic(user_uuid)
|
||||
if traffic_reset:
|
||||
logger.info(f"Successfully reset traffic for user {user_uuid}")
|
||||
else:
|
||||
logger.warning(f"Failed to reset traffic for user {user_uuid}")
|
||||
except Exception as traffic_error:
|
||||
logger.error(f"Error resetting traffic for user {user_uuid}: {traffic_error}")
|
||||
|
||||
else:
|
||||
logger.warning(f"Failed to update user in RemnaWave")
|
||||
|
||||
@@ -2027,95 +2057,28 @@ async def topup_tribute_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
if not config or not config.TRIBUTE_ENABLED:
|
||||
await callback.answer("❌ Tribute платежи недоступны")
|
||||
if not user or not config or not config.TRIBUTE_ENABLED:
|
||||
await callback.answer("⚠️ Tribute платежи недоступны")
|
||||
return
|
||||
|
||||
text = (
|
||||
"💳 **Пополнение через Tribute**\n\n"
|
||||
"🔹 **Доступные способы оплаты:**\n"
|
||||
"• 💳 Банковские карты (Visa, MasterCard, МИР)\n"
|
||||
"• 📱 Система быстрых платежей (СБП)\n"
|
||||
"• 🍎 Apple Pay, Google Pay\n\n"
|
||||
"💰 **Выберите сумму пополнения:**"
|
||||
"💰 **Введите любую сумму от 100 до 15000 рублей**\n\n"
|
||||
"⏱️ После оплаты средства поступят на баланс автоматически в течение 1 минуты"
|
||||
"🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!"
|
||||
"Иначе дс не поступят на ваш счет!"
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=tribute_amounts_keyboard(user.language),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("tribute_amount_"))
|
||||
async def tribute_amount_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 Открыть Tribute", url=config.TRIBUTE_DONATE_LINK)],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="topup_balance")]
|
||||
])
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
amount_str = callback.data.split("_")[-1]
|
||||
try:
|
||||
amount = int(amount_str)
|
||||
except ValueError:
|
||||
await callback.answer("❌ Неверная сумма")
|
||||
return
|
||||
|
||||
await create_tribute_payment(callback, user, amount, config)
|
||||
|
||||
@router.callback_query(F.data == "tribute_custom_amount")
|
||||
async def tribute_custom_amount_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"💰 Введите сумму для пополнения (100-15000 рублей):",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_tribute")]
|
||||
])
|
||||
)
|
||||
await state.set_state(BotStates.waiting_tribute_amount)
|
||||
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="Markdown")
|
||||
|
||||
@router.message(StateFilter(BotStates.waiting_tribute_amount))
|
||||
async def handle_tribute_amount(message: Message, state: FSMContext, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
is_valid, amount = is_valid_amount(message.text)
|
||||
|
||||
if not is_valid or amount < 100 or amount > 15000:
|
||||
await message.answer(
|
||||
"❌ Неверная сумма. Введите число от 100 до 15000:",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_tribute")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
class FakeCallback:
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
async def answer(self, text, show_alert=False):
|
||||
await self.message.answer(text)
|
||||
|
||||
fake_callback = FakeCallback(message)
|
||||
await create_tribute_payment(fake_callback, user, int(amount), config)
|
||||
|
||||
async def create_tribute_payment(callback, user, amount: int, config):
|
||||
try:
|
||||
tribute_donate_link = config.TRIBUTE_DONATE_LINK
|
||||
@@ -2126,10 +2089,11 @@ async def create_tribute_payment(callback, user, amount: int, config):
|
||||
f"📋 **Инструкция:**\n"
|
||||
f"1️⃣ Нажмите кнопку «Открыть Tribute»\n"
|
||||
f"2️⃣ Введите сумму: **{amount}₽**\n"
|
||||
f"3️⃣ Выберите способ оплаты (карта/СБП)\n"
|
||||
f"3️⃣ Выберите способ оплаты\n"
|
||||
f"4️⃣ Завершите платеж\n\n"
|
||||
f"⏱️ После оплаты средства поступят на баланс автоматически в течение 1 минуты\n\n"
|
||||
f"💡 Ваш аккаунт привязан автоматически, никаких дополнительных данных вводить не нужно"
|
||||
f"🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!"
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
|
||||
+11
-27
@@ -13,7 +13,7 @@ def language_keyboard() -> InlineKeyboardMarkup:
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False, show_lucky_game: bool = True) -> InlineKeyboardMarkup:
|
||||
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False, show_lucky_game: bool = False) -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(text="💰 " + t('balance', lang), callback_data="balance"),
|
||||
@@ -53,22 +53,26 @@ def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def topup_keyboard(lang: str, tribute_enabled: bool = False) -> InlineKeyboardMarkup:
|
||||
def topup_keyboard(lang: str, tribute_enabled: bool = False, stars_enabled: bool = False) -> InlineKeyboardMarkup:
|
||||
keyboard = []
|
||||
|
||||
if tribute_enabled:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="💳 Tribute (Карта/СБП)" if lang == 'ru' else "💳 Tribute (Card/SBP)",
|
||||
text="💳 Tribute (Карта)" if lang == 'ru' else "💳 Tribute (Card/SBP)",
|
||||
callback_data="topup_tribute"
|
||||
)
|
||||
])
|
||||
|
||||
if stars_enabled:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars" if lang == 'ru' else "⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.extend([
|
||||
[InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars" if lang == 'ru' else "⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text="💬 Связаться с поддержкой" if lang == 'ru' else "💬 Contact Support",
|
||||
callback_data="topup_support"
|
||||
@@ -81,26 +85,6 @@ def topup_keyboard(lang: str, tribute_enabled: bool = False) -> InlineKeyboardMa
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def tribute_amounts_keyboard(lang: str) -> InlineKeyboardMarkup:
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton(text="💯 100₽", callback_data="tribute_amount_100"),
|
||||
InlineKeyboardButton(text="💰 300₽", callback_data="tribute_amount_300")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💎 500₽", callback_data="tribute_amount_500"),
|
||||
InlineKeyboardButton(text="🎯 1000₽", callback_data="tribute_amount_1000")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💸 2000₽", callback_data="tribute_amount_2000"),
|
||||
InlineKeyboardButton(text="🏆 5000₽", callback_data="tribute_amount_5000")
|
||||
],
|
||||
[InlineKeyboardButton(text="✏️ Своя сумма", callback_data="tribute_custom_amount")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="topup_balance")]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
|
||||
def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
@@ -233,6 +233,7 @@ TRANSLATIONS = {
|
||||
'payment_history': 'Payment history',
|
||||
'topup_card': 'Card payment',
|
||||
'topup_support': 'Through support',
|
||||
'your_subscriptions': 'Your Subscriptions',
|
||||
'back': 'Back',
|
||||
'no_subscriptions': '❌ You have no active subscriptions',
|
||||
'subscription_info': '📋 Subscription: {name}\n💰 Price: ${price:.2f}\n⏱ Duration: {days} days\n📊 Traffic: {traffic}\n\n{description}',
|
||||
@@ -269,6 +270,7 @@ TRANSLATIONS = {
|
||||
'total_referrals': 'Total invited',
|
||||
'active_referrals': 'Active referrals',
|
||||
'total_earned': 'Total earned',
|
||||
'language_changed': 'Language chanched ✅',
|
||||
'my_referrals': 'My referrals',
|
||||
'referral_reward_referrer': 'For each invited: $150',
|
||||
'referral_reward_referred': 'Newbie bonus: $150 after topping up $300',
|
||||
|
||||
Reference in New Issue
Block a user