Compare commits

...

59 Commits

Author SHA1 Message Date
Egor 5fa69dec3e Update README.md 2025-08-16 09:01:18 +03:00
Egor 3d5f3303d1 Update README.md 2025-08-16 08:58:16 +03:00
Egor 10883e92b1 Update handlers.py 2025-08-16 07:13:07 +03:00
Egor e2d28333ec Update handlers.py 2025-08-16 07:08:45 +03:00
Egor 6d55d4832f Update remnawave_api.py 2025-08-16 06:58:48 +03:00
Egor 46c563873f Update handlers.py 2025-08-16 06:58:29 +03:00
Egor 10c5d3c408 Update remnawave_api.py 2025-08-16 06:53:39 +03:00
Egor 942080c460 Update handlers.py 2025-08-16 06:53:15 +03:00
Egor 6a0869ca0a Update remnawave_api.py 2025-08-16 06:31:15 +03:00
Egor d9992dd2f2 Update remnawave_api.py 2025-08-16 06:30:01 +03:00
Egor c8c9631518 Update remnawave_api.py 2025-08-16 06:29:35 +03:00
Egor 943f29f227 Update handlers.py 2025-08-16 06:13:03 +03:00
Egor 80a069077c Update handlers.py 2025-08-16 06:09:01 +03:00
Egor e344e55e6a Create docker-hub.yml 2025-08-16 02:27:32 +03:00
Egor a69947b69a Delete .github/workflows/docker-build.yml 2025-08-16 02:27:12 +03:00
Egor 926f593f50 Create docker-registry.yml 2025-08-16 02:26:57 +03:00
Egor f13734a4e9 Update translations.py 2025-08-16 02:19:36 +03:00
Egor 2107166926 Update translations.py 2025-08-16 02:18:16 +03:00
Egor 60e5601690 Update README.md 2025-08-16 02:13:48 +03:00
Egor 0957d9ef88 Update bedolaga.sh 2025-08-16 02:10:19 +03:00
Egor eb481c57ec Update keyboards.py 2025-08-16 00:15:29 +03:00
Egor e0b1666f5d Update handlers.py 2025-08-16 00:14:37 +03:00
Egor f2afc088af Update README.md 2025-08-15 22:41:43 +03:00
Egor 971d59b2c6 Update README.md 2025-08-15 22:39:18 +03:00
Egor 83fe58056f Update README.md 2025-08-15 22:38:26 +03:00
Egor 5f107922d0 Update README.md 2025-08-15 22:26:56 +03:00
Egor a2ea6b7bf7 Update README.md 2025-08-15 22:20:23 +03:00
Egor 1e0e728840 Update README.md 2025-08-15 22:19:51 +03:00
Egor cea6ed3f95 Update README.md 2025-08-15 08:54:16 +03:00
Egor f15705ed47 Update README.md 2025-08-15 08:52:34 +03:00
Egor 0b9c13ae4b Update README.md 2025-08-15 08:49:35 +03:00
Egor d0c8ae02f0 Update README.md 2025-08-15 08:47:50 +03:00
Egor 4eda7401b9 Update README.md 2025-08-15 08:44:07 +03:00
Egor 0c2f3c9399 Update README.md 2025-08-15 08:43:01 +03:00
Egor 541ecae48c Update bedolaga.sh 2025-08-15 08:37:48 +03:00
Egor 82a551f1d4 Update README.md 2025-08-15 07:29:29 +03:00
Egor fd2253135d Update README.md 2025-08-15 07:27:51 +03:00
Egor d0820aa47e Update README.md 2025-08-15 07:27:24 +03:00
Egor 09c5800f49 Delete emergency_fix.py 2025-08-15 06:41:25 +03:00
Egor 52aa2dc6a8 Delete Makefile 2025-08-15 06:40:25 +03:00
Egor 6418ee7735 Delete docker-compose.yml 2025-08-15 06:39:39 +03:00
Egor 25aef89b4f Update README.md 2025-08-15 05:35:18 +03:00
Egor 77d17f97d3 Update admin_handlers.py 2025-08-15 04:56:43 +03:00
Egor d5c1ec3aea Update database.py 2025-08-15 04:54:53 +03:00
Egor 97a7384381 Update database.py 2025-08-15 04:49:48 +03:00
Egor 27b7bfccba Update admin_handlers.py 2025-08-15 04:49:18 +03:00
Egor ab15624a93 Update keyboards.py 2025-08-15 04:19:58 +03:00
Egor a9c5d8ed69 Update handlers.py 2025-08-15 04:19:40 +03:00
Egor 234b90796e Update handlers.py 2025-08-15 04:02:38 +03:00
Egor 5f018ba22f Update keyboards.py 2025-08-15 04:00:47 +03:00
Egor e11cbbcc18 Update handlers.py 2025-08-15 03:58:51 +03:00
Egor ba4d30d8db Update handlers.py 2025-08-15 03:46:22 +03:00
Egor d11431769c Update keyboards.py 2025-08-15 03:36:12 +03:00
Egor d1d1bbb3e4 Update handlers.py 2025-08-15 03:35:42 +03:00
Egor 823f30882a Update handlers.py 2025-08-15 03:33:09 +03:00
Egor d36f389469 Merge pull request #1 from maximovnikolay/feature/disable_stars_payment
disable stars payment button by config

Спасибо за помощь и вклад! Очень ценю!
2025-08-15 03:25:01 +03:00
Maximov Nikolay add686a4d5 disable stars payment button by config 2025-08-14 23:49:23 +03:00
Egor 59be2d8f02 Update README.md 2025-08-14 05:47:01 +03:00
Egor aa3db49f65 Update README.md 2025-08-14 05:39:19 +03:00
12 changed files with 1764 additions and 1868 deletions
@@ -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
+66
View File
@@ -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 }}
-96
View File
@@ -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}}"
+62 -78
View File
@@ -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">
![Stars](https://img.shields.io/github/stars/Fr1ngg/remnawave-bedolaga-telegram-bot?style=for-the-badge&logo=github&color=yellow)
![Forks](https://img.shields.io/github/forks/Fr1ngg/remnawave-bedolaga-telegram-bot?style=for-the-badge&logo=github&color=blue)
![Issues](https://img.shields.io/github/issues/Fr1ngg/remnawave-bedolaga-telegram-bot?style=for-the-badge&logo=github&color=red)
![Last Commit](https://img.shields.io/github/last-commit/Fr1ngg/remnawave-bedolaga-telegram-bot?style=for-the-badge&logo=github)
[![Star History Chart](https://api.star-history.com/svg?repos=Fr1ngg/remnawave-bedolaga-telegram-bot&type=Date)](https://star-history.com/#Fr1ngg/remnawave-bedolaga-telegram-bot&Date)
</div>
---
### 💝 Донатеры
Спасибо всем, кто поддерживает проект!
| Донатер | Сумма |
|---------|-------|
| Илья (@ispanec_nn) | $15 |
| @Legacyyy777 | 600 руб |
---
### Поддержать проект
[![Donate](https://img.shields.io/badge/Donate-Telegram-blue?style=for-the-badge)](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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+143 -94
View File
@@ -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}")
-102
View File
@@ -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
-100
View File
@@ -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
View File
@@ -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
View File
@@ -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 = []
+2
View File
@@ -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',