Compare commits

...

156 Commits

Author SHA1 Message Date
Egor 83af139d01 Update README.md 2025-08-19 17:48:07 +03:00
Egor e76e297d22 Update keyboards.py 2025-08-17 05:32:45 +03:00
Egor b317368df2 Update admin_handlers.py 2025-08-17 05:32:30 +03:00
Egor 572170b1cc Update database.py 2025-08-17 05:32:14 +03:00
Egor 1adeb53a0d Update handlers.py 2025-08-17 05:31:58 +03:00
Egor 3e5811ec11 Update handlers.py 2025-08-17 05:25:04 +03:00
Egor e26a76c8cf Update handlers.py 2025-08-17 05:20:31 +03:00
Egor 1d5755e415 Update keyboards.py 2025-08-17 05:16:43 +03:00
Egor 32beec7515 Update database.py 2025-08-17 05:16:31 +03:00
Egor feb2250f37 Update admin_handlers.py 2025-08-17 05:16:06 +03:00
Egor da36db39c5 Update handlers.py 2025-08-17 05:15:50 +03:00
Egor fd0fa71f85 Update subscription_monitor.py 2025-08-17 04:41:07 +03:00
Egor 04250b38bb Update README.md 2025-08-17 00:47:11 +03:00
Egor deb6978572 Update README.md 2025-08-16 17:10:40 +03:00
Egor d2a26ead1e Update .env.example 2025-08-16 13:59:00 +03:00
Egor 9e40f406ee Update config.py 2025-08-16 13:57:26 +03:00
Egor 2cdfaf9356 Update subscription_monitor.py 2025-08-16 13:56:54 +03:00
Egor 66af399bc0 Update .env.example 2025-08-16 13:13:10 +03:00
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
Egor c101cb3d3f Update README.md 2025-08-14 05:29:41 +03:00
Egor 63912af43b Update README.md 2025-08-14 05:26:12 +03:00
Egor d19263dd79 Update .env.example 2025-08-14 05:08:03 +03:00
Egor f8c9c59a03 Update database.py 2025-08-14 04:45:02 +03:00
Egor 5595f7a954 Update admin_handlers.py 2025-08-14 04:44:33 +03:00
Egor bac4527945 Update autopay_service.py 2025-08-14 04:44:09 +03:00
Egor 51da8bcb72 Update remnawave_api.py 2025-08-14 04:41:38 +03:00
Egor 8aa6e7e245 Update subscription_monitor.py 2025-08-14 04:40:29 +03:00
Egor 5029b35d14 Update subscription_monitor.py 2025-08-14 04:39:37 +03:00
Egor 2412027d9b Update handlers.py 2025-08-14 04:39:17 +03:00
Egor ddff5219e5 Create webhook_server.py 2025-08-14 04:38:58 +03:00
Egor 346bc18b58 Create tribute_service.py 2025-08-14 04:38:12 +03:00
Egor c256ae3664 Update keyboards.py 2025-08-14 04:37:33 +03:00
Egor 2b47358a9e Update config.py 2025-08-14 04:36:58 +03:00
Egor e0b041ce44 Update main.py 2025-08-14 04:36:41 +03:00
Egor 3f64dee927 Update database.py 2025-08-13 08:02:35 +03:00
Egor 67c5ebeabd Update admin_handlers.py 2025-08-13 08:02:18 +03:00
Egor 84bc8e96cc Update database.py 2025-08-13 07:58:18 +03:00
Egor 2d30cec7c6 Update lucky_game.py 2025-08-13 07:55:50 +03:00
Egor 5f5f66178b Update lucky_game.py 2025-08-13 07:50:17 +03:00
Egor e9fadec0ac Update database.py 2025-08-13 07:49:40 +03:00
Egor 10008e51ff Update admin_handlers.py 2025-08-13 07:39:20 +03:00
Egor 3098610d17 Update database.py 2025-08-13 07:31:29 +03:00
Egor 7a2566d320 Update admin_handlers.py 2025-08-13 07:31:09 +03:00
Egor 01ba8ca53e Update subscription_monitor.py 2025-08-13 07:09:12 +03:00
Egor 2a0a2dccff Update README.md 2025-08-13 06:55:22 +03:00
Egor 66a6f32d4c Update .env.example 2025-08-13 06:51:35 +03:00
Egor 02b3f61953 Update translations.py 2025-08-13 06:37:10 +03:00
Egor dc08526255 Update config.py 2025-08-13 06:36:52 +03:00
Egor 2ca339610b Update subscription_monitor.py 2025-08-13 06:36:32 +03:00
Egor ffe65937c1 Update README.md 2025-08-12 19:16:27 +03:00
Egor 4b6f03eef5 Update README.md 2025-08-12 19:15:02 +03:00
Egor d2f530a7fe Update bedolaga.sh 2025-08-12 19:10:37 +03:00
Egor 44ea4b6b13 Update .env.example 2025-08-12 18:55:22 +03:00
Egor ebc8421a70 Update README.md 2025-08-12 18:52:20 +03:00
Egor 526f142dea Update remnawave_api.py 2025-08-12 18:12:34 +03:00
Egor ca431d36e6 Add files via upload 2025-08-12 03:49:13 +03:00
Egor 68a81bdd89 Delete screens/уведомления/Снимок экрана 2025-08-12 в 01.45.14.png 2025-08-12 03:48:59 +03:00
Fr1ngg 68a3d713ef Update README.md 2025-08-12 03:19:42 +03:00
Fr1ngg 50d3d08e38 Update README.md 2025-08-12 03:14:42 +03:00
Fr1ngg 9a2fec4d8f Update README.md 2025-08-12 03:10:34 +03:00
Fr1ngg e68b1d2092 Add files via upload 2025-08-12 02:48:15 +03:00
Fr1ngg 6dfedef81e Update README.md 2025-08-12 02:36:41 +03:00
Fr1ngg 317e39c823 Update keyboards.py 2025-08-12 02:34:19 +03:00
Fr1ngg 0072a3cb71 Update translations.py 2025-08-12 02:34:03 +03:00
Fr1ngg 8d021c1530 Update database.py 2025-08-12 02:33:44 +03:00
Fr1ngg 23606dc2d4 Update admin_handlers.py 2025-08-12 02:33:23 +03:00
Fr1ngg 0838c4196d Add files via upload 2025-08-12 01:47:52 +03:00
Fr1ngg 65605a0de2 Create 1 2025-08-12 01:47:38 +03:00
Fr1ngg a20ac80281 Delete screens/уведомления 2025-08-12 01:47:25 +03:00
Fr1ngg 02e83fb0d0 Create уведомления 2025-08-12 01:47:13 +03:00
Fr1ngg eddad0db6e Add files via upload 2025-08-12 01:46:00 +03:00
Fr1ngg ffad0afb61 Delete screens/Balance/Снимок экрана 2025-08-09 в 07.54.52.png 2025-08-12 01:44:40 +03:00
Fr1ngg 83eed8e1cd Add files via upload 2025-08-12 01:44:17 +03:00
Fr1ngg 0c7c8abade Add files via upload 2025-08-12 01:42:18 +03:00
Fr1ngg a064890ed8 Delete screens/adminpanel/Снимок экрана 2025-08-09 в 08.09.05.png 2025-08-12 01:41:33 +03:00
Fr1ngg f310f4a35e Delete screens/sub/Снимок экрана 2025-08-09 в 08.05.47.png 2025-08-12 01:41:14 +03:00
Fr1ngg 0268e09882 Delete screens/Главное меню/Mainmenu.png 2025-08-12 01:40:23 +03:00
Fr1ngg 449814d0df Add files via upload 2025-08-12 01:40:09 +03:00
Fr1ngg a3d3577103 Update README.md 2025-08-12 01:37:43 +03:00
Fr1ngg a3086fdd2c Update README.md 2025-08-12 01:34:57 +03:00
Fr1ngg daafc6a41e Update README.md 2025-08-12 01:31:42 +03:00
Fr1ngg c27ee116f7 Update README.md 2025-08-12 01:28:56 +03:00
Fr1ngg 5167cde443 Update bedolaga.sh 2025-08-12 01:25:30 +03:00
Fr1ngg 34542d3a9c Update bedolaga.sh 2025-08-12 01:09:56 +03:00
Fr1ngg e73d1df4cc Update logo2.svg 2025-08-12 01:05:39 +03:00
Fr1ngg f9a95c3d48 Update README.md 2025-08-12 01:04:53 +03:00
Fr1ngg 2dd2b5fdbf Update README.md 2025-08-12 01:04:37 +03:00
Fr1ngg 896f1db9d4 Update README.md 2025-08-11 23:09:23 +03:00
Fr1ngg a1bd2d775f Create emergency_fix.py 2025-08-11 22:54:05 +03:00
Fr1ngg 8cbe23f9ed Update database.py 2025-08-11 22:31:17 +03:00
Fr1ngg edc891c93d Update README.md 2025-08-11 22:06:49 +03:00
Fr1ngg 2d706a4be0 Update keyboards.py 2025-08-11 22:01:38 +03:00
Fr1ngg 32cb81ef34 Update handlers.py 2025-08-11 22:01:09 +03:00
Fr1ngg 88aa3c4a3d Update database.py
autopay mod
2025-08-11 22:00:43 +03:00
Fr1ngg e813d3ccb7 Create autopay_service.py 2025-08-11 22:00:19 +03:00
Fr1ngg a24d7a4e24 Update main.py 2025-08-11 21:59:51 +03:00
Fr1ngg 39987b9717 Update admin_handlers.py
autopay mod
2025-08-11 21:59:31 +03:00
Fr1ngg c77a1ea4f8 Update README.md 2025-08-11 17:57:35 +03:00
38 changed files with 5692 additions and 1840 deletions
+17 -2
View File
@@ -10,7 +10,6 @@ REFERRAL_PERCENTAGE=0.2
# RemnaWave API Configuration
REMNAWAVE_URL=https://example.com
REMNAWAVE_MODE=remote
REMNAWAVE_TOKEN=
# Admin Configuration (comma-separated list of Telegram user IDs)
@@ -24,14 +23,23 @@ TRIAL_DURATION_DAYS=3 # Дней триала
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
TRIAL_PRICE=0.0 # Оставить 0!
TRIAL_NOTIFICATION_ENABLED=true # Уведомление об истекшей триальной подписке
TRIAL_NOTIFICATION_HOURS_AFTER=1 # Через сколько отсылать сообщение
TRIAL_NOTIFICATION_HOURS_WINDOW=23 # Через сколько выслать повторно
# Monitor Service Settings (дополнительные настройки)
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
MONITOR_ENABLED=true
MONITOR_CHECK_INTERVAL=3600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
MONITOR_DAILY_CHECK_HOUR=10 # Ежедневный чек в 10 утра
MONITOR_WARNING_DAYS=2 # За сколько начинать до окончания подписки слать уведомления
DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после истечения удалять триал подписки
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
# Предупреждения о скором истечении триала (ДО окончания)
TRIAL_WARNING_ENABLED=true
TRIAL_WARNING_HOURS_BEFORE=2 # Предупреждения за сколько до окончания триала выслать
TRIAL_WARNING_HOURS_WINDOW=6 # Окно для Предупреждения
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
@@ -59,3 +67,10 @@ STARS_500_RATE=550
STARS_750_RATE=800
# 1000 звёзд
STARS_1000_RATE=1000
TRIBUTE_ENABLED=true # true/false включить/выключить пополнение баланса с помощью доната на Tribute
TRIBUTE_API_KEY= # API ключ из настроек Tribute
TRIBUTE_WEBHOOK_PORT=8081
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
TRIBUTE_DONATE_LINK=https://t.me/tribute/app?startapp=XXXXXXXX # укажите ссылку на донат в Tribute
@@ -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}}"
+486 -556
View File
File diff suppressed because it is too large Load Diff
+1308 -31
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -261,7 +261,7 @@
text-anchor="middle"
fill="url(#mainGradient)"
filter="url(#glow)">
RemnaWave
Remnawave
</text>
<text x="200" y="345"

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+284
View File
@@ -0,0 +1,284 @@
import asyncio
import logging
from datetime import datetime, timedelta
from typing import List, Optional
from database import Database, UserSubscription, User, Subscription
from remnawave_api import RemnaWaveAPI
from referral_utils import process_referral_rewards
logger = logging.getLogger(__name__)
class AutoPayService:
def __init__(self, db: Database, api: Optional[RemnaWaveAPI] = None, bot=None):
self.db = db
self.api = api
self.bot = bot
self.is_running = False
self.check_task = None
async def start(self):
if self.is_running:
logger.warning("AutoPay service is already running")
return
self.is_running = True
logger.info("🔄 Starting AutoPay service...")
self.check_task = asyncio.create_task(self._periodic_check())
async def stop(self):
if not self.is_running:
return
self.is_running = False
logger.info("⏹ Stopping AutoPay service...")
if self.check_task:
self.check_task.cancel()
try:
await self.check_task
except asyncio.CancelledError:
pass
async def _periodic_check(self):
while self.is_running:
try:
await self.process_autopayments()
# Проверяем каждые 30 минут
await asyncio.sleep(1800)
except asyncio.CancelledError:
logger.info("AutoPay periodic check cancelled")
break
except Exception as e:
logger.error(f"Error in AutoPay periodic check: {e}")
await asyncio.sleep(300)
async def process_autopayments(self) -> dict:
logger.info("🔄 Processing autopayments...")
try:
subscriptions_to_pay = await self.db.get_subscriptions_for_autopay()
if not subscriptions_to_pay:
logger.info("No subscriptions ready for autopay")
return {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': []
}
logger.info(f"Found {len(subscriptions_to_pay)} subscriptions for autopay")
stats = {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': []
}
for user_sub in subscriptions_to_pay:
try:
result = await self._process_single_autopayment(user_sub)
stats['processed'] += 1
if result['success']:
stats['successful'] += 1
logger.info(f"✅ Autopay successful for user {user_sub.user_id}, subscription {user_sub.id}")
elif result['reason'] == 'insufficient_balance':
stats['insufficient_balance'] += 1
logger.info(f"💳 Insufficient balance for user {user_sub.user_id}, subscription {user_sub.id}")
else:
stats['failed'] += 1
stats['errors'].append(f"User {user_sub.user_id}: {result['reason']}")
logger.warning(f"❌ Autopay failed for user {user_sub.user_id}: {result['reason']}")
except Exception as e:
stats['processed'] += 1
stats['failed'] += 1
error_msg = f"User {user_sub.user_id}: {str(e)}"
stats['errors'].append(error_msg)
logger.error(f"Error processing autopay for user {user_sub.user_id}: {e}")
logger.info(f"📊 Autopay processing complete: {stats['successful']} successful, "
f"{stats['failed']} failed, {stats['insufficient_balance']} insufficient balance")
return stats
except Exception as e:
logger.error(f"Error in process_autopayments: {e}")
return {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': [str(e)]
}
async def _process_single_autopayment(self, user_sub: UserSubscription) -> dict:
try:
user = await self.db.get_user_by_telegram_id(user_sub.user_id)
if not user:
return {'success': False, 'reason': 'User not found'}
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
return {'success': False, 'reason': 'Subscription plan not found'}
if subscription.is_trial:
logger.info(f"Skipping autopay for trial subscription: user {user_sub.user_id}")
return {'success': False, 'reason': 'Trial subscriptions are not eligible for autopay'}
if not user_sub.is_active or not user_sub.auto_pay_enabled:
return {'success': False, 'reason': 'Subscription inactive or autopay disabled'}
if user.balance < subscription.price:
await self._notify_insufficient_balance(user, subscription, user_sub)
return {'success': False, 'reason': 'insufficient_balance'}
return await self._execute_autopayment(user, subscription, user_sub)
except Exception as e:
logger.error(f"Error in _process_single_autopayment: {e}")
return {'success': False, 'reason': str(e)}
async def _execute_autopayment(self, user: User, subscription: Subscription, user_sub: UserSubscription) -> dict:
try:
user.balance -= subscription.price
await self.db.update_user(user)
now = datetime.utcnow()
if user_sub.expires_at > now:
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
else:
new_expiry = now + timedelta(days=subscription.duration_days)
user_sub.expires_at = new_expiry
user_sub.is_active = True
await self.db.update_user_subscription(user_sub)
if self.api and user_sub.short_uuid:
try:
remna_user_details = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if remna_user_details:
user_uuid = remna_user_details.get('uuid')
if user_uuid:
expiry_str = new_expiry.isoformat() + 'Z'
update_data = {
'enable': True,
'expireAt': expiry_str
}
await self.api.update_user(user_uuid, update_data)
logger.info(f"Updated RemnaWave expiry for user {user_sub.user_id}")
except Exception as e:
logger.warning(f"Failed to update RemnaWave expiry: {e}")
payment = await self.db.create_payment(
user_id=user_sub.user_id,
amount=-subscription.price,
payment_type='autopay',
description=f'Автоплатеж: {subscription.name}',
status='completed'
)
if self.bot:
try:
await process_referral_rewards(
user_sub.user_id,
subscription.price,
payment.id,
self.db,
self.bot,
payment_type='autopay'
)
except Exception as e:
logger.warning(f"Failed to process referral rewards for autopay: {e}")
await self._notify_successful_autopay(user, subscription, user_sub, new_expiry)
return {'success': True, 'reason': 'Payment processed successfully'}
except Exception as e:
logger.error(f"Error executing autopayment: {e}")
try:
user.balance += subscription.price
await self.db.update_user(user)
except:
pass
return {'success': False, 'reason': str(e)}
async def _notify_successful_autopay(self, user: User, subscription: Subscription,
user_sub: UserSubscription, new_expiry: datetime):
if not self.bot:
return
try:
from utils import format_datetime
text = f"✅ Автоматическое продление подписки\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Списано: {subscription.price} руб.\n"
text += f"📅 Продлено до: {format_datetime(new_expiry, user.language)}\n"
text += f"💳 Остаток на балансе: {user.balance} руб.\n\n"
text += f"🔄 Следующее продление произойдет автоматически за {user_sub.auto_pay_days_before} дн. до истечения.\n\n"
text += f"ℹ️ Для отключения автоплатежа перейдите в 'Мои подписки' → выберите подписку"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
[InlineKeyboardButton(text="💰 Баланс", callback_data="balance")]
])
await self.bot.send_message(
user.telegram_id,
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Failed to send autopay success notification: {e}")
async def _notify_insufficient_balance(self, user: User, subscription: Subscription, user_sub: UserSubscription):
if not self.bot:
return
try:
from utils import format_datetime
needed = subscription.price - user.balance
days_left = (user_sub.expires_at - datetime.utcnow()).days
text = f"⚠️ Не удалось автоматически продлить подписку\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Нужно для продления: {subscription.price} руб.\n"
text += f"💳 Ваш баланс: {user.balance} руб.\n"
text += f"💸 Недостает: {needed} руб.\n\n"
text += f"📅 Подписка истекает: {format_datetime(user_sub.expires_at, user.language)}\n"
text += f"⏰ Осталось дней: {days_left}\n\n"
text += f"💡 Пополните баланс для автоматического продления"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")]
])
await self.bot.send_message(
user.telegram_id,
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Failed to send insufficient balance notification: {e}")
async def get_service_status(self) -> dict:
return {
'is_running': self.is_running,
'check_interval': 1800,
'has_api': self.api is not None,
'has_bot': self.bot is not None
}
+1189 -775
View File
File diff suppressed because it is too large Load Diff
+25 -1
View File
@@ -28,6 +28,18 @@ class Config:
DELETE_EXPIRED_TRIAL_DAYS: int = 1
DELETE_EXPIRED_REGULAR_DAYS: int = 7
AUTO_DELETE_ENABLED: bool = False
TRIAL_NOTIFICATION_ENABLED: bool = True
TRIAL_NOTIFICATION_HOURS_AFTER: int = 2
TRIAL_NOTIFICATION_HOURS_WINDOW: int = 22
TRIAL_WARNING_ENABLED: bool = True
TRIAL_WARNING_HOURS_BEFORE: int = 2
TRIAL_WARNING_HOURS_WINDOW: int = 6
TRIBUTE_ENABLED: bool = False
TRIBUTE_API_KEY: str = ""
TRIBUTE_WEBHOOK_PORT: int = 8081
TRIBUTE_WEBHOOK_PATH: str = "/tribute-webhook"
TRIBUTE_DONATE_URL: str = ""
TRIBUTE_DONATE_LINK: str = ""
LUCKY_GAME_ENABLED: bool = True
LUCKY_GAME_REWARD: float = 50.0
@@ -112,12 +124,24 @@ def load_config() -> Config:
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
TRIAL_NOTIFICATION_ENABLED=get_bool('TRIAL_NOTIFICATION_ENABLED', True),
TRIAL_NOTIFICATION_HOURS_AFTER=get_int('TRIAL_NOTIFICATION_HOURS_AFTER', 2),
TRIAL_NOTIFICATION_HOURS_WINDOW=get_int('TRIAL_NOTIFICATION_HOURS_WINDOW', 22),
TRIAL_WARNING_ENABLED=get_bool('TRIAL_WARNING_ENABLED', True),
TRIAL_WARNING_HOURS_BEFORE=get_int('TRIAL_WARNING_HOURS_BEFORE', 2),
TRIAL_WARNING_HOURS_WINDOW=get_int('TRIAL_WARNING_HOURS_WINDOW', 6),
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
STARS_ENABLED=get_bool('STARS_ENABLED', True),
STARS_RATES=parse_stars_rates()
STARS_RATES=parse_stars_rates(),
TRIBUTE_ENABLED=get_bool('TRIBUTE_ENABLED', False),
TRIBUTE_API_KEY=os.getenv('TRIBUTE_API_KEY', ''),
TRIBUTE_WEBHOOK_PORT=get_int('TRIBUTE_WEBHOOK_PORT', 8081),
TRIBUTE_WEBHOOK_PATH=os.getenv('TRIBUTE_WEBHOOK_PATH', '/tribute-webhook'),
TRIBUTE_DONATE_URL=os.getenv('TRIBUTE_DONATE_URL', ''),
TRIBUTE_DONATE_LINK=os.getenv('TRIBUTE_DONATE_LINK', '')
)
def debug_environment():
+974 -207
View File
File diff suppressed because it is too large Load Diff
-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
+377 -9
View File
@@ -66,6 +66,7 @@ class BotStates(StatesGroup):
waiting_rule_edit_title = State()
waiting_rule_edit_content = State()
waiting_rule_edit_order = State()
waiting_tribute_amount = State()
router = Router()
@@ -175,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),
@@ -192,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')
@@ -203,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:
@@ -211,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")
@@ -427,17 +448,19 @@ async def topup_balance_callback(callback: CallbackQuery, **kwargs):
await callback.answer("❌ Ошибка пользователя")
return
tribute_enabled = config and config.TRIBUTE_ENABLED
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
text = t('topup_balance', user.language)
text = "💰 Выберите способ пополнения баланса:"
if tribute_enabled:
text += "\n\n💳 **Tribute** - Оплата банковской картой"
if stars_enabled:
text += "\n\n⭐ **Новинка!** Теперь можно пополнять баланс через Telegram Stars!"
text += "\n💎 Быстро, безопасно, без комиссий!"
text += "\n⭐ **Telegram Stars** - быстро и безопасно"
await callback.message.edit_text(
text,
reply_markup=topup_keyboard(user.language),
reply_markup=topup_keyboard(user.language, tribute_enabled, stars_enabled),
parse_mode='Markdown'
)
@@ -939,6 +962,20 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
if not is_trial and not is_imported and subscription.price > 0:
text += f"\n\n🔄 **Автоплатеж:**\n"
if user_sub.auto_pay_enabled:
text += f"✅ Включен (за {user_sub.auto_pay_days_before} дн. до истечения)\n"
text += f"💰 Стоимость продления: {subscription.price} руб.\n"
text += f"💳 Ваш баланс: {user.balance} руб."
if user.balance < subscription.price:
needed = subscription.price - user.balance
text += f"\n⚠️ Недостаточно средств! Нужно еще {needed} руб."
else:
text += f"❌ Отключен\n"
text += f"💡 Включите для автоматического продления"
if user_sub.short_uuid and api:
try:
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
@@ -958,7 +995,14 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
await callback.message.edit_text(
text,
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend, is_imported),
reply_markup=user_subscription_detail_keyboard(
user_sub_id,
user.language,
show_extend,
is_imported,
is_trial,
user_sub.auto_pay_enabled
),
parse_mode='HTML',
disable_web_page_preview=True
)
@@ -1102,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")
@@ -1774,3 +1828,317 @@ async def cancel_promocode_callback(callback: CallbackQuery, state: FSMContext,
t('main_menu', user.language),
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial, show_lucky_game)
)
@router.callback_query(F.data.startswith("toggle_autopay_"))
async def toggle_autopay_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
user_sub_id = int(callback.data.split("_")[2])
user_subs = await db.get_user_subscriptions(user.telegram_id)
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
if not user_sub:
await callback.answer("❌ Подписка не найдена")
return
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
await callback.answer("❌ План подписки не найден")
return
if subscription.is_trial:
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
return
if subscription.is_imported or subscription.price == 0:
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
return
new_state = not user_sub.auto_pay_enabled
success = await db.toggle_autopay(user_sub_id, new_state)
if success:
status_text = "включен" if new_state else "отключен"
await callback.answer(f"✅ Автоплатеж {status_text}")
updated_subs = await db.get_user_subscriptions(user.telegram_id)
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
if updated_sub:
await autopay_settings_callback(callback, db, user=user)
else:
await callback.answer("❌ Ошибка изменения настроек")
except Exception as e:
logger.error(f"Error toggling autopay: {e}")
await callback.answer("❌ Ошибка операции")
@router.callback_query(F.data == "autopay_help")
async def autopay_help_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
text = "🔄 **Автоплатеж - как это работает?**\n\n"
text += "**🎯 Что такое автоплатеж:**\n"
text += "Автоматическое продление подписки за несколько дней до истечения срока действия.\n\n"
text += "**⚙️ Как настроить:**\n"
text += "1. Перейдите в 'Мои подписки'\n"
text += "2. Выберите нужную подписку\n"
text += "3. Нажмите 'Настроить автоплатеж'\n"
text += "4. Включите автоплатеж и выберите за сколько дней до истечения продлевать\n\n"
text += "**💰 Требования:**\n"
text += "• На балансе должно быть достаточно средств\n"
text += "• Подписка должна быть активной\n"
text += "• Автоплатеж работает только с обычными подписками\n\n"
text += "**📅 Варианты продления:**\n"
text += "• За 1 день - для тех, кто следит за балансом\n"
text += "• За 3 дня - рекомендуемый вариант\n"
text += "• За 5 дней - для подстраховки\n"
text += "• За 7 дней - максимальный запас времени\n\n"
text += "**🔔 Уведомления:**\n"
text += "Вы получите уведомление при успешном продлении или если недостаточно средств.\n\n"
text += "**❓ Проблемы:**\n"
text += "Если автоплатеж не сработал, проверьте баланс и обратитесь в поддержку."
await callback.message.edit_text(
text,
reply_markup=autopay_help_keyboard(user.language),
parse_mode='Markdown'
)
@router.callback_query(F.data == "autopay_insufficient_balance_help")
async def autopay_insufficient_balance_help_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
text = "⚠️ **Недостаточно средств для автоплатежа**\n\n"
text += f"💳 **Ваш текущий баланс:** {user.balance}\n\n"
text += "**🔄 Что происходит:**\n"
text += "Система пыталась автоматически продлить вашу подписку, но на балансе недостаточно средств.\n\n"
text += "**✅ Что нужно сделать:**\n"
text += "1. Пополните баланс на нужную сумму\n"
text += "2. Автоплатеж попробует снова при следующей проверке\n"
text += "3. Или продлите подписку вручную\n\n"
text += "**⏰ Важно:**\n"
text += "Пополните баланс до истечения срока подписки, иначе доступ будет приостановлен."
await callback.message.edit_text(
text,
reply_markup=autopay_help_keyboard(user.language),
parse_mode='Markdown'
)
@router.callback_query(F.data.startswith("autopay_settings_"))
async def autopay_settings_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
user_sub_id = int(callback.data.split("_")[2])
user_subs = await db.get_user_subscriptions(user.telegram_id)
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
if not user_sub:
await callback.answer("❌ Подписка не найдена")
return
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
await callback.answer("❌ План подписки не найден")
return
if subscription.is_trial:
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
return
if subscription.is_imported or subscription.price == 0:
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
return
from datetime import datetime
now = datetime.utcnow()
days_left = (user_sub.expires_at - now).days
text = f"⚙️ Настройки автоплатежа\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Цена продления: {subscription.price} руб.\n"
text += f"⏱ Длительность: {subscription.duration_days} дн.\n"
text += f"📅 Истекает через: {days_left} дн.\n\n"
status_emoji = "" if user_sub.auto_pay_enabled else ""
status_text = "Включен" if user_sub.auto_pay_enabled else "Отключен"
text += f"🔄 Автоплатеж: {status_emoji} {status_text}\n"
if user_sub.auto_pay_enabled:
text += f"📅 Продлять за: {user_sub.auto_pay_days_before} дн. до истечения\n\n"
if user.balance >= subscription.price:
text += f"✅ На балансе достаточно средств ({user.balance}₽)\n"
text += f"💡 Подписка будет автоматически продлена"
else:
needed = subscription.price - user.balance
text += f"⚠️ Недостаточно средств на балансе!\n"
text += f"💳 Ваш баланс: {user.balance}\n"
text += f"💸 Нужно еще: {needed}\n"
text += f"💡 Пополните баланс для автоматического продления"
else:
text += f"\n💡 Включите автоплатеж для автоматического продления подписки\n"
text += f"🎯 Рекомендуем включить за 3 дня до истечения"
await callback.message.edit_text(
text,
reply_markup=autopay_settings_keyboard(user_sub_id, user_sub, user.language)
)
except Exception as e:
logger.error(f"Error showing autopay settings: {e}")
await callback.answer("❌ Ошибка загрузки настроек")
@router.callback_query(F.data.startswith("autopay_days_"))
async def autopay_days_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
parts = callback.data.split("_")
user_sub_id = int(parts[2])
days = int(parts[3])
user_subs = await db.get_user_subscriptions(user.telegram_id)
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
if not user_sub:
await callback.answer("❌ Подписка не найдена")
return
success = await db.set_autopay_days(user_sub_id, days)
if success:
await callback.answer(f"✅ Установлено: продлять за {days} дн.")
updated_subs = await db.get_user_subscriptions(user.telegram_id)
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
if updated_sub:
await autopay_settings_callback(callback, db, user=user)
else:
await callback.answer("❌ Ошибка обновления настроек")
except Exception as e:
logger.error(f"Error setting autopay days: {e}")
await callback.answer("❌ Ошибка операции")
@router.callback_query(F.data == "topup_tribute")
async def topup_tribute_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user or not config or not config.TRIBUTE_ENABLED:
await callback.answer("⚠️ Tribute платежи недоступны")
return
text = (
"💳 **Пополнение через Tribute**\n\n"
"🔹 **Доступные способы оплаты:**\n"
"• 💳 Банковские карты (Visa, MasterCard, МИР)\n"
"💰 **Введите любую сумму от 100 до 15000 рублей**\n\n"
"⏱️ После оплаты средства поступят на баланс автоматически в течение 1 минуты"
"🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!"
"Иначе дс не поступят на ваш счет!"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Открыть Tribute", url=config.TRIBUTE_DONATE_LINK)],
[InlineKeyboardButton(text="🔙 Назад", callback_data="topup_balance")]
])
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="Markdown")
@router.message(StateFilter(BotStates.waiting_tribute_amount))
async def create_tribute_payment(callback, user, amount: int, config):
try:
tribute_donate_link = config.TRIBUTE_DONATE_LINK
text = (
f"💳 **Пополнение через Tribute**\n\n"
f"💰 Сумма: **{amount}₽**\n\n"
f"📋 **Инструкция:**\n"
f"1️⃣ Нажмите кнопку «Открыть Tribute»\n"
f"2️⃣ Введите сумму: **{amount}₽**\n"
f"3️⃣ Выберите способ оплаты\n"
f"4️⃣ Завершите платеж\n\n"
f"⏱️ После оплаты средства поступят на баланс автоматически в течение 1 минуты\n\n"
f"💡 Ваш аккаунт привязан автоматически, никаких дополнительных данных вводить не нужно"
f"🚨 НЕ ОТПРАВЛЯТЬ ПЛАТЕЖ АНОНИМНО!"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Открыть Tribute", url=tribute_donate_link)],
[InlineKeyboardButton(text="🔄 Проверить платеж", callback_data=f"check_tribute_{amount}")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="topup_balance")]
])
await callback.message.edit_text(text, reply_markup=keyboard, parse_mode="Markdown")
except Exception as e:
logger.error(f"Error creating Tribute payment: {e}")
await callback.answer("❌ Ошибка создания платежа")
@router.callback_query(F.data.startswith("check_tribute_"))
async def check_tribute_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
payments = await db.get_user_payments(user.telegram_id)
recent_tribute_payments = [
p for p in payments
if p.payment_type == 'tribute' and p.status == 'completed'
and (datetime.utcnow() - p.created_at).total_seconds() < 1800
]
if recent_tribute_payments:
await callback.answer("✅ Платеж найден! Средства зачислены на баланс", show_alert=True)
await callback.message.edit_text(
f"✅ **Платеж успешно обработан!**\n\n"
f"💰 Зачислено: {recent_tribute_payments[0].amount}\n"
f"💳 Текущий баланс: {user.balance}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Мой баланс", callback_data="balance")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
]),
parse_mode="Markdown"
)
else:
await callback.answer("⏳ Платеж еще не обработан. Попробуйте через минуту.", show_alert=True)
except Exception as e:
logger.error(f"Error checking Tribute payment: {e}")
await callback.answer("❌ Ошибка проверки платежа")
+247 -21
View File
@@ -2,6 +2,7 @@ from database import Subscription
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from typing import List, Optional, Dict
from translations import t
from datetime import datetime
def language_keyboard() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -12,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"),
@@ -52,13 +53,38 @@ def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
])
return keyboard
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
# [InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
[InlineKeyboardButton(text="👨‍💼 " + t('topup_support', lang), callback_data="topup_support")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
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)",
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="💬 Связаться с поддержкой" if lang == 'ru' else "💬 Contact Support",
callback_data="topup_support"
)],
[InlineKeyboardButton(
text="🔙 Назад" if lang == 'ru' else "🔙 Back",
callback_data="balance"
)]
])
return keyboard
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
@@ -110,22 +136,52 @@ def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru'
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False, is_imported: bool = False) -> InlineKeyboardMarkup:
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru',
show_extend: bool = False, is_imported: bool = False,
is_trial: bool = False, autopay_enabled: bool = False) -> InlineKeyboardMarkup:
buttons = []
if is_imported:
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
elif is_trial:
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🛒 Купить полную подписку", callback_data="buy_subscription")])
else:
if show_extend:
buttons.append([InlineKeyboardButton(text="" + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
if autopay_enabled:
autopay_text = "🔄✅ Настроить автоплатеж"
else:
autopay_text = "🔄❌ Настроить автоплатеж"
buttons.append([InlineKeyboardButton(text=autopay_text, callback_data=f"autopay_settings_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_confirmation_keyboard(subscription_id: int, action: str, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_autopay_{action}_{subscription_id}"),
InlineKeyboardButton(text="❌ Нет", callback_data=f"autopay_settings_{subscription_id}")
]
])
return keyboard
def autopay_help_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура помощи по автоплатежам"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
return keyboard
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
@@ -160,14 +216,17 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
],
[
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals")
],
[
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"), # НОВАЯ КНОПКА
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"),
InlineKeyboardButton(text="🔄 Автоплатежи", callback_data="admin_autopay")
],
[
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system"),
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")
],
[
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
@@ -185,11 +244,84 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_user_subscriptions_filters_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура фильтров для подписок пользователей"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🟢 Активные", callback_data="filter_subs_active"),
InlineKeyboardButton(text="🔴 Истекшие", callback_data="filter_subs_expired")
],
[
InlineKeyboardButton(text="🔄✅ С автоплатежом", callback_data="filter_subs_autopay"),
InlineKeyboardButton(text="⏰ Истекают скоро", callback_data="filter_subs_expiring")
],
[
InlineKeyboardButton(text="🆓 Триальные", callback_data="filter_subs_trial"),
InlineKeyboardButton(text="📦 Импортированные", callback_data="filter_subs_imported")
],
[
InlineKeyboardButton(text="📋 Все подписки", callback_data="admin_user_subscriptions_all"),
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")
]
])
return keyboard
def admin_user_subscription_detail_keyboard(subscription_id: int, user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура для детального просмотра подписки пользователя"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_user_sub_{subscription_id}"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_sub_{subscription_id}")
],
[
InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_detail_{user_id}"),
InlineKeyboardButton(text="📋 К списку подписок", callback_data="admin_user_subscriptions_all")
],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")]
])
return keyboard
def user_subscriptions_pagination_keyboard(current_page: int, total_pages: int,
filter_type: str = "all", lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"user_subs_page_{current_page - 1}_{filter_type}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"user_subs_page_{current_page + 1}_{filter_type}"))
buttons.append(nav_row)
buttons.append([
InlineKeyboardButton(text="🔍 Фильтры", callback_data="admin_user_subscriptions_filters"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_subs_{filter_type}")
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
@@ -606,14 +738,6 @@ def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="topup_stars")],
[InlineKeyboardButton(text="👨‍💼 " + t('topup_support', lang), callback_data="topup_support")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
])
return keyboard
def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
@@ -638,7 +762,6 @@ def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> Inl
))
buttons.append(row)
# Добавляем кнопку назад
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
@@ -719,3 +842,106 @@ def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> Inline
]
])
return keyboard
def autopay_settings_keyboard(user_sub_id: int, user_sub, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if user_sub.auto_pay_enabled:
toggle_text = "❌ Отключить автоплатеж"
toggle_callback = f"toggle_autopay_{user_sub_id}"
else:
toggle_text = "✅ Включить автоплатеж"
toggle_callback = f"toggle_autopay_{user_sub_id}"
buttons.append([InlineKeyboardButton(text=toggle_text, callback_data=toggle_callback)])
if user_sub.auto_pay_enabled:
buttons.append([InlineKeyboardButton(text="📅 Настроить дни до продления", callback_data="noop")])
days_row = []
for days in [1, 3, 5, 7]:
emoji = "🔹" if user_sub.auto_pay_days_before == days else ""
days_row.append(InlineKeyboardButton(
text=f"{emoji} {days}д",
callback_data=f"autopay_days_{user_sub_id}_{days}"
))
buttons.append(days_row)
buttons.append([InlineKeyboardButton(text="🔙 К подписке", callback_data=f"view_sub_{user_sub_id}")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_status_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return keyboard
def autopay_subscriptions_keyboard(subscriptions_data: List[Dict], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
critical_subs = expired + due_soon
for sub_data in critical_subs[:8]:
username = sub_data['username'] if sub_data['username'] != 'N/A' else f"ID:{sub_data['user_id']}"
days = sub_data['expires_in_days']
if days <= 0:
status_emoji = ""
status_text = f"Истекла"
elif days <= sub_data['auto_pay_days_before']:
status_emoji = "⚠️"
status_text = f"Через {days}д"
else:
status_emoji = ""
status_text = f"Через {days}д"
button_text = f"{status_emoji} @{username} ({status_text})"
buttons.append([
InlineKeyboardButton(
text=button_text,
callback_data=f"autopay_user_detail_{sub_data['user_id']}"
)
])
if len(subscriptions_data) > 8:
buttons.append([
InlineKeyboardButton(
text=f"... и еще {len(subscriptions_data) - 8}",
callback_data="noop"
)
])
buttons.extend([
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_user_detail_keyboard(user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Добавить баланс", callback_data=f"admin_add_balance_to_{user_id}")],
[InlineKeyboardButton(text="📋 Управление подписками", callback_data=f"admin_user_subscriptions_{user_id}")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data=f"autopay_user_detail_{user_id}")],
[InlineKeyboardButton(text="🔙 К списку", callback_data="autopay_subscriptions_list")]
])
return keyboard
def autopay_statistics_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⚠️ Недостаточно средств", callback_data="autopay_insufficient_balance_users")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_statistics")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return keyboard
+86 -4
View File
@@ -8,6 +8,7 @@ from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from lucky_game import lucky_game_router
from stars_handlers import stars_router
from autopay_service import AutoPayService
print("🚀 Запуск бота...")
print(f"📍 Рабочая директория: {os.getcwd()}")
@@ -46,6 +47,40 @@ class BotApplication:
self.bot = None
self.dp = None
self.monitor_service = None
self.autopay_service = None
self.webhook_server = None
async def _init_autopay_service(self):
try:
logger.info("🔧 Initializing autopay service...")
if not self.bot:
logger.error("❌ Bot instance is None, cannot initialize autopay")
return
if not self.db:
logger.error("❌ Database instance is None, cannot initialize autopay")
return
self.autopay_service = AutoPayService(self.db, self.api, self.bot)
self.dp.workflow_data["autopay_service"] = self.autopay_service
logger.info("✅ Autopay service added to workflow_data")
logger.info("🚀 Starting autopay service...")
await self.autopay_service.start()
status = await self.autopay_service.get_service_status()
if status['is_running']:
logger.info("✅ Autopay service started successfully")
logger.info(f"📊 Autopay status: interval=30min")
else:
logger.warning("⚠️ Autopay service created but not running")
except Exception as e:
logger.error(f"❌ Failed to initialize autopay service: {e}", exc_info=True)
logger.warning("⚠️ Continuing without autopay service")
self.autopay_service = None
async def initialize(self):
@@ -76,6 +111,7 @@ class BotApplication:
logger.info(f"Bot Username: {self.config.BOT_USERNAME}")
self.db = Database(self.config.DATABASE_URL)
await self._init_database()
self.api = RemnaWaveAPI(
@@ -95,8 +131,20 @@ class BotApplication:
await self._test_bot_token()
self._setup_dispatcher()
await self._init_webhook_server()
if self.config.TRIBUTE_ENABLED:
logger.info("✅ Tribute платежи включены")
if not self.config.TRIBUTE_API_KEY:
logger.warning("⚠️ TRIBUTE_API_KEY не установлен!")
if not self.config.TRIBUTE_DONATE_URL:
logger.warning("⚠️ TRIBUTE_DONATE_URL не установлен!")
else:
logger.info("❌ Tribute платежи отключены")
await self._init_monitor_service()
await self._init_autopay_service()
if self.config.STARS_ENABLED:
logger.info("✅ Telegram Stars пополнение включено")
@@ -115,18 +163,37 @@ class BotApplication:
self.config.STARS_ENABLED = False
else:
logger.info("❌ Telegram Stars пополнение отключено")
async def _init_webhook_server(self):
"""Инициализация webhook сервера для Tribute"""
try:
logger.info("🔧 Initializing webhook server...")
from webhook_server import WebhookServer
self.webhook_server = WebhookServer(self.bot, self.db, self.config)
logger.info("🚀 Starting webhook server...")
await self.webhook_server.start()
logger.info("✅ Webhook server started successfully")
except Exception as e:
logger.error(f"❌ Failed to initialize webhook server: {e}", exc_info=True)
logger.warning("⚠️ Continuing without webhook server")
self.webhook_server = None
async def _init_database(self):
max_retries = 3
for attempt in range(max_retries):
try:
logger.info(f"🗄️ Database initialization attempt {attempt + 1}/{max_retries}")
await self.db.init_db()
logger.info("Database initialized successfully")
logger.info("Database initialized successfully with all migrations")
break
except Exception as e:
logger.error(f"Database initialization attempt {attempt + 1} failed: {e}")
logger.error(f"Database initialization attempt {attempt + 1} failed: {e}")
if attempt == max_retries - 1:
logger.error("Failed to initialize database after all retries")
logger.error("💥 Failed to initialize database after all retries")
raise
await asyncio.sleep(2)
@@ -162,7 +229,8 @@ class BotApplication:
"config": self.config,
"api": self.api,
"db": self.db,
"monitor_service": None
"monitor_service": None,
"autopay_service": None
})
self.dp.message.middleware(LoggingMiddleware())
@@ -249,6 +317,20 @@ class BotApplication:
async def shutdown(self):
logger.info("Shutting down bot...")
if self.webhook_server:
try:
await self.webhook_server.stop()
logger.info("Webhook server stopped")
except Exception as e:
logger.error(f"Error stopping webhook server: {e}")
if self.autopay_service:
try:
await self.autopay_service.stop()
logger.info("Autopay service stopped")
except Exception as e:
logger.error(f"Error stopping autopay service: {e}")
if self.monitor_service:
try:
+3 -1
View File
@@ -19,7 +19,9 @@ class RemnaWaveAPI:
headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
'Accept': 'application/json',
'X-Forwarded-Proto': 'https', # ← ДОБАВЬ ЭТО
'X-Forwarded-For': '127.0.0.1' # ← И ЭТО
}
timeout = aiohttp.ClientTimeout(total=30)
self.session = aiohttp.ClientSession(
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+1
View File
@@ -0,0 +1 @@
Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

+292 -22
View File
@@ -86,26 +86,34 @@ class SubscriptionMonitorService:
logger.info("Subscription monitor service stopped")
async def _monitor_loop(self):
logger.info("🔄 Starting monitor loop")
logger.info("🔥 Starting monitor loop")
logger.info("⏰ Initial check in 10 seconds...")
await asyncio.sleep(10)
while self.is_running:
try:
logger.info("🔍 Running periodic subscription check...")
trial_warnings = await self._check_trial_warnings()
warnings_sent = await self._check_expiring_subscriptions()
if warnings_sent > 0:
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent")
trial_notifications = await self._check_expired_trial_subscriptions()
total_notifications = trial_warnings + warnings_sent + trial_notifications
if total_notifications > 0:
logger.info(f"✅ Monitor check completed: {trial_warnings} trial warnings, "
f"{warnings_sent} expiry warnings, {trial_notifications} trial notifications")
else:
logger.info("✅ Monitor check completed: no warnings needed")
logger.info("✅ Monitor check completed: no notifications needed")
self.last_check_time = datetime.now()
logger.debug(f"😴 Sleeping for {self.config.MONITOR_CHECK_INTERVAL} seconds...")
await asyncio.sleep(self.config.MONITOR_CHECK_INTERVAL)
except asyncio.CancelledError:
logger.info("Monitor loop cancelled")
break
@@ -147,7 +155,111 @@ class SubscriptionMonitorService:
break
except Exception as e:
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
await asyncio.sleep(3600)
await asyncio.sleep(3600)
async def _check_expired_trial_subscriptions(self) -> int:
try:
logger.info("🆓 Checking for expired trial subscriptions...")
notifications_sent = 0
now_utc = datetime.utcnow()
all_users = await self.db.get_all_users()
for user in all_users:
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
try:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
continue
if not subscription.is_trial:
continue
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_since_expiry = -time_diff.total_seconds() / 3600
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
logger.info(f"🆓 Sending trial expiry notification to user {user.telegram_id}: "
f"trial '{subscription.name}' expired {hours_since_expiry:.1f} hours ago")
try:
await self._send_trial_expiry_notification(user, subscription)
notifications_sent += 1
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id}")
except Exception as notification_error:
logger.error(f"❌ Failed to send trial notification to user {user.telegram_id}: {notification_error}")
except Exception as sub_error:
logger.error(f"❌ Error checking trial subscription {user_sub.id}: {sub_error}")
except Exception as user_error:
logger.error(f"❌ Error checking trial subscriptions for user {user.telegram_id}: {user_error}")
if notifications_sent > 0:
logger.info(f"🆓 Trial expiry check completed: {notifications_sent} notifications sent")
return notifications_sent
except Exception as e:
logger.error(f"❌ Critical error in check_expired_trial_subscriptions: {e}", exc_info=True)
return 0
async def _send_trial_expiry_notification(self, user, subscription):
try:
if not self.bot:
logger.error("❌ Bot instance is None, cannot send trial notification")
return
from translations import t
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
try:
available_subscriptions = await self.db.get_all_subscriptions(exclude_trial=True, exclude_imported=True)
min_price = None
if available_subscriptions:
active_subs = [sub for sub in available_subscriptions if sub.is_active and sub.price > 0]
if active_subs:
min_price = min(sub.price for sub in active_subs)
except Exception as e:
logger.error(f"Error getting subscription prices: {e}")
min_price = None
message = t('trial_subscription_expired', user.language, name=subscription.name)
if min_price:
buy_button_text = f"🛒 Купить от {min_price:.0f}"
else:
buy_button_text = "🛒 Купить подписку"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=buy_button_text,
callback_data="buy_subscription"
)],
[InlineKeyboardButton(
text="📋 Мои подписки",
callback_data="my_subscriptions"
)]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard)
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id} for subscription '{subscription.name}'")
except Exception as e:
logger.error(f"❌ Error sending trial expiry notification to user {user.telegram_id}: {e}", exc_info=True)
raise
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
try:
@@ -388,11 +500,11 @@ class SubscriptionMonitorService:
f"threshold={self.config.MONITOR_WARNING_DAYS}")
if subscription.is_trial:
logger.debug(f" Skipping trial subscription '{subscription.name}'")
logger.debug(f" Skipping trial subscription '{subscription.name}'")
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
logger.debug(f" Skipping imported subscription '{subscription.name}'")
logger.debug(f" Skipping imported subscription '{subscription.name}'")
continue
should_warn = (
@@ -517,9 +629,13 @@ class SubscriptionMonitorService:
warnings_sent = await self._check_expiring_subscriptions()
logger.info(f"📢 Sent {warnings_sent} expiry warnings")
logger.info("🔄 Deactivating expired subscriptions...")
logger.info("🆓 Checking expired trial subscriptions...")
trial_notifications = await self._check_expired_trial_subscriptions()
logger.info(f"🆓 Sent {trial_notifications} trial expiry notifications")
logger.info("🔥 Deactivating expired subscriptions...")
deactivated_count = await self.deactivate_expired_subscriptions()
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
logger.info(f"🔥 Deactivated {deactivated_count} expired subscriptions")
deleted_trials = 0
deleted_regular = 0
@@ -538,7 +654,7 @@ class SubscriptionMonitorService:
await self._send_final_expiry_notifications()
logger.info("📩 Final notifications sent")
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}, "
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Trial notifications: {trial_notifications}, Deactivated: {deactivated_count}, "
f"Deleted trials: {deleted_trials}, Deleted regular: {deleted_regular}")
return deactivated_count
@@ -590,7 +706,7 @@ class SubscriptionMonitorService:
user_data = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if user_data and user_data.get('uuid'):
await self.api.update_user(user_data['uuid'], {'status': 'EXPIRED'})
logger.debug(f"🔄 Also deactivated user {user_data['uuid']} in RemnaWave")
logger.debug(f"🔥 Also deactivated user {user_data['uuid']} in RemnaWave")
except Exception as api_error:
logger.warning(f"⚠️ Could not deactivate user in RemnaWave: {api_error}")
else:
@@ -704,11 +820,30 @@ class SubscriptionMonitorService:
days_left = int(hours_left / 24)
if subscription.is_trial:
results.append({
'success': True,
'message': f'Trial subscription "{subscription.name}" skipped (no warnings for trials)',
'error': None
})
hours_since_expiry = -hours_left
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
test_message = f"🧪 [ТЕСТОВОЕ УВЕДОМЛЕНИЕ]\n\n🆓 Ваша триальная подписка '{subscription.name}' истекла! Купите новый тариф чтобы продолжить использование VPN."
if self.bot:
try:
await self.bot.send_message(user_id, test_message)
results.append({
'success': True,
'message': f'✅ Sent test trial expiry notification for "{subscription.name}" (expired {hours_since_expiry:.1f} hours ago)',
'error': None
})
except Exception as send_error:
results.append({
'success': False,
'message': f'❌ Failed to send test trial notification for "{subscription.name}"',
'error': str(send_error)
})
else:
results.append({
'success': True,
'message': f'Trial subscription "{subscription.name}" - no notification needed (expired {hours_since_expiry:.1f} hours ago)',
'error': None
})
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
@@ -802,6 +937,141 @@ class SubscriptionMonitorService:
}
}
async def _check_trial_warnings(self) -> int:
try:
if not getattr(self.config, 'TRIAL_WARNING_ENABLED', True):
return 0
logger.info("⚠️ Checking for trials that will expire soon...")
warnings_sent = 0
now_utc = datetime.utcnow()
hours_before = getattr(self.config, 'TRIAL_WARNING_HOURS_BEFORE', 2)
hours_window = getattr(self.config, 'TRIAL_WARNING_HOURS_WINDOW', 6)
logger.info(f"🔧 Trial warning settings: hours_before={hours_before}, hours_window={hours_window}")
all_users = await self.db.get_all_users()
for user in all_users:
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
try:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or not subscription.is_trial:
continue
if not user_sub.is_active:
continue
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_until_expiry = time_diff.total_seconds() / 3600
logger.debug(f"🔍 Trial warning check for '{subscription.name}' (user {user.telegram_id}):")
logger.debug(f" Expires at: {expires_at_utc}")
logger.debug(f" Current time: {now_utc}")
logger.debug(f" Hours until expiry: {hours_until_expiry:.2f}")
logger.debug(f" Should warn: {hours_before - hours_window/2 <= hours_until_expiry <= hours_before + hours_window/2}")
warning_start = hours_before - hours_window / 2
warning_end = hours_before + hours_window / 2
if warning_start <= hours_until_expiry <= warning_end:
logger.info(f"⚠️ Sending trial warning to user {user.telegram_id}: "
f"trial '{subscription.name}' expires in {hours_until_expiry:.1f} hours")
try:
await self._send_trial_warning(user, subscription, hours_until_expiry)
warnings_sent += 1
logger.info(f"✅ Trial warning sent to user {user.telegram_id}")
except Exception as notification_error:
logger.error(f"❌ Failed to send trial warning: {notification_error}")
except Exception as sub_error:
logger.error(f"❌ Error checking trial warning for subscription {user_sub.id}: {sub_error}")
except Exception as user_error:
logger.error(f"❌ Error checking trial warnings for user {user.telegram_id}: {user_error}")
if warnings_sent > 0:
logger.info(f"⚠️ Trial warnings completed: {warnings_sent} warnings sent")
return warnings_sent
except Exception as e:
logger.error(f"❌ Critical error in trial warnings: {e}", exc_info=True)
return 0
async def _send_trial_warning(self, user, subscription, hours_left: float):
try:
if not self.bot:
logger.error("❌ Bot instance is None, cannot send trial warning")
return
from translations import t
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
try:
available_subscriptions = await self.db.get_all_subscriptions(exclude_trial=True, exclude_imported=True)
min_price = None
if available_subscriptions:
active_subs = [sub for sub in available_subscriptions if sub.is_active and sub.price > 0]
if active_subs:
min_price = min(sub.price for sub in active_subs)
except Exception as e:
logger.error(f"Error getting subscription prices: {e}")
min_price = None
hours_int = int(hours_left)
if hours_int <= 1:
time_text = "менее часа"
elif hours_int == 1:
time_text = "около часа"
else:
time_text = f"около {hours_int} часов"
message = (
f"⚠️ **Ваша триальная подписка скоро истечет!**\n\n"
f"📋 Подписка: **{subscription.name}**\n"
f"⏰ Осталось: **{time_text}**\n\n"
f"🛒 Приобретите полную подписку, чтобы продолжить пользоваться VPN без перерывов!"
)
if min_price:
buy_button_text = f"🛒 Купить от {min_price:.0f}"
else:
buy_button_text = "🛒 Купить подписку"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=buy_button_text,
callback_data="buy_subscription"
)],
[InlineKeyboardButton(
text="📋 Мои подписки",
callback_data="my_subscriptions"
)]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard, parse_mode='Markdown')
logger.info(f"✅ Trial warning sent to user {user.telegram_id} for subscription '{subscription.name}' ({hours_left:.1f} hours left)")
except Exception as e:
logger.error(f"❌ Error sending trial warning to user {user.telegram_id}: {e}", exc_info=True)
raise
async def create_subscription_monitor(bot, db: Database, config, api: RemnaWaveAPI = None) -> SubscriptionMonitorService:
logger.info("🏭 Creating subscription monitor service...")
service = SubscriptionMonitorService(bot, db, config, api)
+94 -2
View File
@@ -97,6 +97,48 @@ TRANSLATIONS = {
'manage_balance': 'Управление балансом',
'manage_promocodes': 'Управление промокодами',
'statistics': 'Статистика',
'admin_user_subscriptions': 'Подписки пользователей',
'all_user_subscriptions': 'Все подписки пользователей',
'user_subscription_filters': 'Фильтры подписок',
'active_subscriptions': 'Активные подписки',
'expired_subscriptions': 'Истекшие подписки',
'expiring_subscriptions': 'Истекающие подписки',
'autopay_subscriptions': 'Подписки с автоплатежом',
'trial_subscriptions': 'Триальные подписки',
'imported_subscriptions': 'Импортированные подписки',
'subscription_details': 'Детали подписки',
'user_info': 'Информация о пользователе',
'subscription_info': 'Информация о подписке',
'subscription_status': 'Статус подписки',
'subscription_dates': 'Временные рамки',
'autopay_settings': 'Настройки автоплатежа',
'subscription_type': 'Тип подписки',
'edit_subscription': 'Редактировать подписку',
'toggle_status': 'Изменить статус',
'edit_expiry': 'Изменить срок',
'edit_autopay': 'Настроить автоплатеж',
'subscription_activated': 'Подписка активирована',
'subscription_deactivated': 'Подписка деактивирована',
'insufficient_funds': 'Недостаточно средств',
'funds_needed': 'Нужно еще',
'created_date': 'Дата создания',
'expires_date': 'Дата истечения',
'updated_date': 'Дата обновления',
'days_left': 'Дней осталось',
'autopay_enabled': 'Автоплатеж включен',
'autopay_disabled': 'Автоплатеж отключен',
'renew_before_days': 'Продлять за дней',
'view_details': 'Просмотр деталей',
'back_to_list': 'К списку',
'refresh_data': 'Обновить данные',
'no_subscriptions_found': 'Подписки не найдены',
'subscription_not_found': 'Подписка не найдена',
'database_unavailable': 'База данных недоступна',
'error_loading_subscriptions': 'Ошибка загрузки подписок',
'error_loading_details': 'Ошибка загрузки деталей',
'pagination_error': 'Ошибка навигации',
'edit_error': 'Ошибка редактирования',
'status_toggle_error': 'Ошибка изменения статуса',
'not_admin': '❌ У вас нет прав администратора',
'create_subscription': 'Создать подписку',
'enter_sub_name': 'Введите название подписки:',
@@ -152,7 +194,10 @@ TRANSLATIONS = {
'lucky_game_games_played': 'Игр сыграно: {count}',
'lucky_game_wins': 'Выигрышей: {count}',
'lucky_game_total_won': 'Всего выиграно: {amount}',
'lucky_game_win_rate': 'Процент побед: {rate}%'
'lucky_game_win_rate': 'Процент побед: {rate}%',
'trial_subscription_expired': '🆓 Ваша триальная подписка "{name}" истекла!\n\n'
'💡 Чтобы продолжить использование VPN, купите полный тариф.\n\n'
'✨ Доступны различные планы подписки с выгодными ценами!'
},
'en': {
@@ -188,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}',
@@ -204,6 +250,9 @@ TRANSLATIONS = {
'subscription_expires_day_after_tomorrow': '⏰ Your subscription \'{name}\' expires in {days} days!\n\nWe recommend renewing it in advance in \'My Subscriptions\'.\n\n💰 Check your balance - you might want to top it up.',
'subscription_expires_in_days': '⏳ Your subscription \'{name}\' expires in {days} days!\n\nYou can renew it in \'My Subscriptions\'.\n\n💡 Early renewal extends from current expiration date.',
'extend_subscription_btn': '🔄 Extend Subscription',
'trial_subscription_expired': '🆓 Your trial subscription "{name}" has expired!\n\n'
'💡 To continue using VPN, please purchase a full plan.\n\n'
'✨ Various subscription plans with great prices are available!',
'my_subscriptions_btn': '📋 My Subscriptions',
'buy_new_subscription_btn': '🛒 Buy New Subscription',
'restore_subscription_btn': '🔄 Restore Subscription',
@@ -221,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',
@@ -294,7 +344,49 @@ TRANSLATIONS = {
'lucky_game_games_played': 'Games played: {count}',
'lucky_game_wins': 'Wins: {count}',
'lucky_game_total_won': 'Total won: ${amount}',
'lucky_game_win_rate': 'Win rate: {rate}%'
'lucky_game_win_rate': 'Win rate: {rate}%',
'admin_user_subscriptions': 'User Subscriptions',
'all_user_subscriptions': 'All User Subscriptions',
'user_subscription_filters': 'Subscription Filters',
'active_subscriptions': 'Active Subscriptions',
'expired_subscriptions': 'Expired Subscriptions',
'expiring_subscriptions': 'Expiring Subscriptions',
'autopay_subscriptions': 'Autopay Subscriptions',
'trial_subscriptions': 'Trial Subscriptions',
'imported_subscriptions': 'Imported Subscriptions',
'subscription_details': 'Subscription Details',
'user_info': 'User Information',
'subscription_info': 'Subscription Information',
'subscription_status': 'Subscription Status',
'subscription_dates': 'Time Frames',
'autopay_settings': 'Autopay Settings',
'subscription_type': 'Subscription Type',
'edit_subscription': 'Edit Subscription',
'toggle_status': 'Toggle Status',
'edit_expiry': 'Edit Expiry',
'edit_autopay': 'Configure Autopay',
'subscription_activated': 'Subscription activated',
'subscription_deactivated': 'Subscription deactivated',
'insufficient_funds': 'Insufficient funds',
'funds_needed': 'Need more',
'created_date': 'Created Date',
'expires_date': 'Expiry Date',
'updated_date': 'Updated Date',
'days_left': 'Days Left',
'autopay_enabled': 'Autopay Enabled',
'autopay_disabled': 'Autopay Disabled',
'renew_before_days': 'Renew Before Days',
'view_details': 'View Details',
'back_to_list': 'Back to List',
'refresh_data': 'Refresh Data',
'no_subscriptions_found': 'No subscriptions found',
'subscription_not_found': 'Subscription not found',
'database_unavailable': 'Database unavailable',
'error_loading_subscriptions': 'Error loading subscriptions',
'error_loading_details': 'Error loading details',
'pagination_error': 'Navigation error',
'edit_error': 'Edit error',
'status_toggle_error': 'Status toggle error'
}
}
+165
View File
@@ -0,0 +1,165 @@
import logging
import hmac
import hashlib
import json
from typing import Optional
from aiohttp import web
from aiogram import Bot
from sqlalchemy.orm import sessionmaker
from config import Config
from database import Database
logger = logging.getLogger(__name__)
def convert_period_to_months(period: Optional[str]) -> int:
"""Map Tribute subscription period strings to months."""
if not period:
return 1
mapping = {
"monthly": 1,
"quarterly": 3,
"3-month": 3,
"3months": 3,
"3-months": 3,
"q": 3,
"halfyearly": 6,
"yearly": 12,
"annual": 12,
"y": 12,
}
return mapping.get(period.lower(), 1)
class TributeService:
def __init__(
self,
bot: Bot,
config: Config,
db: Database,
):
self.bot = bot
self.config = config
self.db = db
async def handle_webhook(self, raw_body: bytes, signature_header: Optional[str]) -> web.Response:
def ok(data: Optional[dict] = None) -> web.Response:
payload = {"status": "ok"}
if data:
payload.update(data)
return web.json_response(payload, status=200)
def ignored(reason: str) -> web.Response:
return web.json_response({"status": "ignored", "reason": reason}, status=200)
def bad_request(reason: str) -> web.Response:
return web.json_response({"status": "error", "reason": reason}, status=400)
if hasattr(self.config, 'TRIBUTE_API_KEY') and self.config.TRIBUTE_API_KEY:
if not signature_header:
return web.json_response({"status": "error", "reason": "no_signature"}, status=403)
expected_sig = hmac.new(self.config.TRIBUTE_API_KEY.encode(), raw_body,
hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected_sig, signature_header):
return web.json_response({"status": "error", "reason": "invalid_signature"}, status=403)
try:
payload = json.loads(raw_body.decode())
except Exception:
return bad_request("invalid_json")
logging.info(
"Tribute webhook data: %s",
json.dumps(payload, ensure_ascii=False),
)
event_name = payload.get("name")
data = payload.get("payload", {})
user_id = data.get("telegram_user_id")
if not user_id:
return ignored("missing_telegram_user_id")
amount_value = data.get("amount", 0)
currency = data.get("currency", "RUB").upper()
amount_float = round(amount_value / 100.0, 2)
if event_name == "new_donation":
await self._handle_new_donation(user_id, amount_float, currency, data)
elif event_name == "cancelled_subscription":
await self._handle_cancellation(user_id)
return ok({"event": event_name or "unknown"})
async def _handle_new_donation(self, user_id: int, amount: float, currency: str, data: dict):
try:
if not user_id:
logger.warning(f"No telegram_user_id in webhook data")
return
async with self.db.session_factory() as session:
payment = await self.db.create_payment(
user_id=int(user_id),
amount=amount,
payment_type='tribute',
description=f'Пополнение через Tribute: {amount} {currency}',
status='completed'
)
await self.db.add_balance(int(user_id), amount)
try:
success_msg = (
f"✅ **Платеж через Tribute получен!**\n\n"
f"💰 Сумма: {amount} {currency}\n"
f"🎉 Средства зачислены на баланс!\n\n"
f"💳 Ваш текущий баланс можно посмотреть в главном меню."
)
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Мой баланс", callback_data="balance")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
await self.bot.send_message(
int(user_id),
success_msg,
reply_markup=keyboard,
parse_mode="Markdown"
)
except Exception as e:
logger.error(f"Failed to send Tribute payment success message to user {user_id}: {e}")
await session.commit()
logger.info(f"Successfully processed Tribute donation: {amount} {currency} for user {user_id}")
except Exception as e:
logger.error(f"Error handling tribute donation: {e}")
async def _handle_cancellation(self, user_id: int):
try:
cancellation_msg = (
"🚨 Ваш платеж Tribute был отменен.\n\n"
"Если это произошло по ошибке, обратитесь в поддержку."
)
await self.bot.send_message(
int(user_id),
cancellation_msg,
parse_mode="HTML"
)
logger.info(f"Tribute subscription cancelled for user {user_id}")
except Exception as e:
logger.error(f"Error handling tribute cancellation for user {user_id}: {e}")
async def tribute_webhook_route(request: web.Request):
tribute_service: TributeService = request.app['tribute_service']
raw_body = await request.read()
signature_header = request.headers.get('trbt-signature')
return await tribute_service.handle_webhook(raw_body, signature_header)
+70
View File
@@ -0,0 +1,70 @@
import logging
from aiohttp import web, ClientSession
from aiogram import Bot
from database import Database
from config import Config
from tribute_service import TributeService, tribute_webhook_route
logger = logging.getLogger(__name__)
class WebhookServer:
def __init__(self, bot: Bot, db: Database, config: Config):
self.bot = bot
self.db = db
self.config = config
self.app = None
self.runner = None
self.site = None
async def create_app(self):
self.app = web.Application()
tribute_service = TributeService(self.bot, self.config, self.db)
self.app['tribute_service'] = tribute_service
self.app.router.add_post(self.config.TRIBUTE_WEBHOOK_PATH, tribute_webhook_route)
async def health_check(request):
return web.json_response({"status": "ok", "service": "tribute-webhooks"})
self.app.router.add_get('/health', health_check)
logger.info(f"Webhook server configured with route: {self.config.TRIBUTE_WEBHOOK_PATH}")
return self.app
async def start(self):
try:
if not self.app:
await self.create_app()
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(
self.runner,
host='0.0.0.0',
port=self.config.TRIBUTE_WEBHOOK_PORT
)
await self.site.start()
logger.info(f"✅ Webhook server started on port {self.config.TRIBUTE_WEBHOOK_PORT}")
logger.info(f"🎯 Tribute webhook URL: http://your-server:{self.config.TRIBUTE_WEBHOOK_PORT}{self.config.TRIBUTE_WEBHOOK_PATH}")
except Exception as e:
logger.error(f"❌ Failed to start webhook server: {e}")
raise
async def stop(self):
try:
if self.site:
await self.site.stop()
logger.info("Webhook site stopped")
if self.runner:
await self.runner.cleanup()
logger.info("Webhook runner cleaned up")
except Exception as e:
logger.error(f"Error stopping webhook server: {e}")