Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a7e4ff1c9 | |||
| f0a1e1c338 | |||
| 843af67483 | |||
| 80d251c2d1 | |||
| a1c2907cce | |||
| c1369f113c | |||
| 8ea5bc5330 | |||
| 285dda4795 | |||
| fae423177b | |||
| 51bf43289d | |||
| 3fd87eee9a | |||
| ad14ab5d08 | |||
| aab68767fd | |||
| 84f10229da | |||
| 3a6ce31103 | |||
| da7675a853 | |||
| 09726f8df0 | |||
| 8072ce6954 | |||
| 1e870e49c8 | |||
| eeca712a3e | |||
| 29c3e455e4 | |||
| c23a777783 | |||
| 0439279602 | |||
| 27c81d451b | |||
| 339881f288 | |||
| ef6bfb8ea3 | |||
| 4bd33fbbe2 | |||
| 8a5f097808 | |||
| 239eb9705d | |||
| fee141ea2b | |||
| ec7c121e03 | |||
| 8b047fd18c | |||
| 0c04868bef | |||
| deaf16e6ae | |||
| 389cac3e0a | |||
| c04968818f | |||
| 3a391d79d2 | |||
| d1a8019c31 | |||
| 1c211b0a41 | |||
| 7c1ba6d92b | |||
| c0953eecf3 | |||
| 7f70826023 | |||
| a7aaa6fc88 | |||
| a72b3bf705 | |||
| 71656df0bd | |||
| 7d5cff0aa9 | |||
| 4e27396a09 | |||
| 52a72a11b0 | |||
| c17886489b | |||
| ade2f106ba | |||
| a096b2bc1a | |||
| de65390680 | |||
| bd5f92c121 | |||
| ad53a6bc09 | |||
| f3b2c8056e | |||
| 1e90903d54 | |||
| 8537875537 | |||
| a988e562a0 | |||
| d014a54181 | |||
| 4e5d57bf92 | |||
| 6f8ebbb9f0 | |||
| 131ac32596 | |||
| 56017d023c | |||
| 3afdc6cd31 | |||
| 52a89eac77 | |||
| 0cdd311832 | |||
| 1d5b52ef5b | |||
| 3f37be4509 | |||
| 1636af1585 | |||
| 127409955c | |||
| 423ce5e41b | |||
| 770b7e8358 | |||
| 0daf08ceed | |||
| 1cda2259c6 | |||
| 5879080f33 | |||
| cd3ad8bff9 | |||
| 033a748069 | |||
| de890dbd7f | |||
| 7c5ae09a6e | |||
| e587e3b087 | |||
| d698b3cfe7 | |||
| a01680fa2d | |||
| 531fc0fb22 | |||
| bb8e513598 | |||
| f56679cd9f | |||
| 9885e050a1 |
@@ -0,0 +1,46 @@
|
||||
# Git files
|
||||
.git
|
||||
.gitignore
|
||||
.github/
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# Development files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Docker files (исключаем из копирования в образ)
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
docker-compose*.yml
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
+30
-7
@@ -12,19 +12,26 @@ ADMIN_NOTIFICATIONS_ENABLED=true
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890 # Замени на ID твоего канала (-100) - ПРЕФИКС ЗАКРЫТОГО КАНАЛА! ВСТАВИТЬ СВОЙ ID СРАЗУ ПОСЛЕ (-100) БЕЗ ПРОБЕЛОВ!
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID=123 # Опционально: ID топика
|
||||
|
||||
# ===== DATABASE =====
|
||||
# Для Docker используйте PostgreSQL:
|
||||
DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
|
||||
# Для локального запуска без Docker используйте SQLite: sqlite+aiosqlite:///./bot.db
|
||||
# DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
|
||||
# ===== DATABASE CONFIGURATION =====
|
||||
# Режим базы данных: "auto", "postgresql", "sqlite"
|
||||
DATABASE_MODE=auto
|
||||
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
# Основной URL (можно оставить пустым для автоматического выбора)
|
||||
DATABASE_URL=
|
||||
|
||||
# Пароли для Docker (PostgreSQL/Redis)
|
||||
# PostgreSQL настройки (для Docker и кастомных установок)
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=remnawave_bot
|
||||
POSTGRES_USER=remnawave_user
|
||||
POSTGRES_PASSWORD=secure_password_123
|
||||
|
||||
# SQLite настройки (для локального запуска)
|
||||
SQLITE_PATH=./data/bot.db
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# ===== REMNAWAVE API =====
|
||||
REMNAWAVE_API_URL=https://panel.example.com
|
||||
REMNAWAVE_API_KEY=
|
||||
@@ -169,6 +176,18 @@ PAYMENT_SUBSCRIPTION_DESCRIPTION=Оплата подписки
|
||||
PAYMENT_BALANCE_TEMPLATE={service_name} - {description}
|
||||
PAYMENT_SUBSCRIPTION_TEMPLATE={service_name} - {description}
|
||||
|
||||
# CRYPTOBOT
|
||||
CRYPTOBOT_ENABLED=true
|
||||
CRYPTOBOT_API_TOKEN=123456789:AAzQcZWQqQAbsfgPnOLr4FHC8Doa4L7KryC
|
||||
CRYPTOBOT_WEBHOOK_SECRET=your_webhook_secret_here
|
||||
CRYPTOBOT_BASE_URL=https://pay.crypt.bot
|
||||
CRYPTOBOT_TESTNET=false
|
||||
CRYPTOBOT_WEBHOOK_PATH=/cryptobot-webhook
|
||||
CRYPTOBOT_WEBHOOK_PORT=8083
|
||||
CRYPTOBOT_DEFAULT_ASSET=USDT
|
||||
CRYPTOBOT_ASSETS=USDT,TON,BTC,ETH,LTC,BNB,TRX,USDC
|
||||
CRYPTOBOT_INVOICE_EXPIRES_HOURS=24
|
||||
|
||||
# ===== ИНТЕРФЕЙС И UX =====
|
||||
|
||||
# Режим работы кнопки "Подключиться"
|
||||
@@ -215,3 +234,7 @@ WEBHOOK_PATH=/webhook
|
||||
APP_CONFIG_PATH=app-config.json
|
||||
ENABLE_DEEP_LINKS=true
|
||||
APP_CONFIG_CACHE_TTL=3600
|
||||
|
||||
VERSION_CHECK_ENABLED=true
|
||||
VERSION_CHECK_REPO=fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
VERSION_CHECK_INTERVAL_HOURS=1
|
||||
|
||||
@@ -1,28 +1,111 @@
|
||||
name: BedolagaBot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- main
|
||||
- dev
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get version info
|
||||
id: version
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
# Определяем версию и теги
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🏷️ Собираем релизную версию: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
VERSION="v2.2.5-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:latest,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🚀 Собираем версию из main: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
|
||||
VERSION="v2.2.5-dev-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:dev,fr1ngg/remnawave-bedolaga-telegram-bot:${VERSION}"
|
||||
echo "🧪 Собираем dev версию: $VERSION"
|
||||
else
|
||||
VERSION="v2.2.5-pr-$(git rev-parse --short HEAD)"
|
||||
TAGS="fr1ngg/remnawave-bedolaga-telegram-bot:pr-$(git rev-parse --short HEAD)"
|
||||
echo "🔀 Собираем PR версию: $VERSION"
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
echo "should_push=${{ github.event_name != 'pull_request' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "=== Информация о сборке ==="
|
||||
echo "Версия: $VERSION"
|
||||
echo "Коммит: $(git rev-parse --short HEAD)"
|
||||
echo "Теги: $TAGS"
|
||||
echo "Push: ${{ github.event_name != 'pull_request' }}"
|
||||
echo "==========================="
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
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@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
push: ${{ steps.version.outputs.should_push }}
|
||||
tags: ${{ steps.version.outputs.tags }}
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
BUILD_DATE=${{ steps.version.outputs.build_date }}
|
||||
VCS_REF=${{ steps.version.outputs.short_sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
labels: |
|
||||
org.opencontainers.image.version=${{ steps.version.outputs.version }}
|
||||
org.opencontainers.image.created=${{ steps.version.outputs.build_date }}
|
||||
org.opencontainers.image.revision=${{ steps.version.outputs.short_sha }}
|
||||
org.opencontainers.image.title=Bedolaga RemnaWave Bot
|
||||
org.opencontainers.image.description=Telegram bot for RemnaWave VPN service
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
|
||||
- name: Build Summary
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
echo "## 🚀 Docker Hub Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Параметр | Значение |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|----------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Версия** | \`${{ steps.version.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Коммит** | \`${{ steps.version.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Дата сборки** | \`${{ steps.version.outputs.build_date }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Registry** | Docker Hub |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Ветка** | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Статус** | ✅ Опубликован |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📋 Опубликованные теги:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${{ steps.version.outputs.tags }}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY
|
||||
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🔗 Ссылки:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- [Docker Hub Repository](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- dev
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -24,14 +25,43 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: |
|
||||
network=host
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version info
|
||||
id: version
|
||||
run: |
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "🏷️ Собираем релизную версию: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/main ]]; then
|
||||
VERSION="v2.2.5"
|
||||
echo "🚀 Собираем версию из main: $VERSION"
|
||||
elif [[ $GITHUB_REF == refs/heads/dev ]]; then
|
||||
VERSION="v2.2.5-dev-$(git rev-parse --short HEAD)"
|
||||
echo "🧪 Собираем dev версию: $VERSION"
|
||||
else
|
||||
VERSION="v2.2.5-pr-$(git rev-parse --short HEAD)"
|
||||
echo "🔀 Собираем PR версию: $VERSION"
|
||||
fi
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -45,17 +75,30 @@ jobs:
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
|
||||
type=raw,value=${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.version }}
|
||||
BUILD_DATE=${{ steps.version.outputs.build_date }}
|
||||
VCS_REF=${{ steps.version.outputs.short_sha }}
|
||||
cache-from: |
|
||||
type=gha
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
|
||||
cache-to: |
|
||||
type=gha,mode=max
|
||||
type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
|
||||
build-contexts: |
|
||||
alpine=docker-image://alpine:latest
|
||||
|
||||
- name: Generate security report
|
||||
uses: docker/scout-action@v1
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -67,3 +110,17 @@ jobs:
|
||||
only-severities: critical,high
|
||||
write-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Summary
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
echo "## 🚀 Docker Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Параметр | Значение |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|----------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Версия** | \`${{ steps.version.outputs.version }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Коммит** | \`${{ steps.version.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Дата сборки** | \`${{ steps.version.outputs.build_date }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Registry** | \`${{ env.REGISTRY }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Образ** | \`${{ env.IMAGE_NAME }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Ветка** | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| **Статус** | ✅ Опубликован |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
+42
-9
@@ -1,28 +1,61 @@
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /app
|
||||
FROM python:3.11-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
ARG VERSION="v2.2.5"
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean
|
||||
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
RUN groupadd -g 1000 app && \
|
||||
useradd -u 1000 -g 1000 -m -s /bin/bash app
|
||||
|
||||
COPY . .
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=app:app . .
|
||||
|
||||
RUN mkdir -p logs data && \
|
||||
chown -R app:app /app
|
||||
chown -R app:app /app logs data
|
||||
|
||||
USER app
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONPATH=/app \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
VERSION=${VERSION} \
|
||||
BUILD_DATE=${BUILD_DATE} \
|
||||
VCS_REF=${VCS_REF}
|
||||
|
||||
EXPOSE 8081 8082
|
||||
|
||||
LABEL org.opencontainers.image.title="Bedolaga RemnaWave Bot" \
|
||||
org.opencontainers.image.description="Telegram bot for RemnaWave VPN service" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.created="${BUILD_DATE}" \
|
||||
org.opencontainers.image.revision="${VCS_REF}" \
|
||||
org.opencontainers.image.source="https://github.com/fr1ngg/remnawave-bedolaga-telegram-bot" \
|
||||
org.opencontainers.image.url="https://github.com/fr1ngg/remnawave-bedolaga-telegram-bot" \
|
||||
org.opencontainers.image.vendor="fr1ngg"
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
|
||||
### ⚡ **Полная автоматизация VPN бизнеса**
|
||||
- 🎯 **Готовое решение** - разверни за 5 минут, начни продавать сегодня
|
||||
- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + ЮKassa
|
||||
- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + CryptoBot + ЮKassa + P2P
|
||||
- 🔄 **Автоматизация 99%** - от регистрации до продления подписок
|
||||
- 📊 **Детальная аналитика** - полная картина вашего бизнеса
|
||||
- 💬 **Уведомления в топики** - Уведомление в топик канала об: Активация триала 💎 Покупка подписки 🔄 Конверсия из триала в платную ⏰ Продление подписки 💰 Пополнение баланса
|
||||
- 💬 **Уведомления в топики** об: Активация триала 💎 Покупка подписки 🔄 Конверсия из триала в платную ⏰ Продление подписки 💰 Пополнение баланса 🚧 Включении тех работ ♻️ Появлении новой версии бота
|
||||
|
||||
### 🎛️ **Гибкость конфигурации**
|
||||
- 🌍 **Умный выбор серверов** - автоматический пропуск при одном сервере, мультивыбор при нескольких
|
||||
@@ -309,6 +309,18 @@ MIN_BALANCE_FOR_AUTOPAY_KOPEKS=10000
|
||||
|
||||
# ===== ПЛАТЕЖНЫЕ СИСТЕМЫ =====
|
||||
|
||||
# CRYPTOBOT
|
||||
CRYPTOBOT_ENABLED=true
|
||||
CRYPTOBOT_API_TOKEN=123456789:AAzQcZWQqQAbsfgPnOLr4FHC8Doa4L7KryC
|
||||
CRYPTOBOT_WEBHOOK_SECRET=your_webhook_secret_here
|
||||
CRYPTOBOT_BASE_URL=https://pay.crypt.bot
|
||||
CRYPTOBOT_TESTNET=false
|
||||
CRYPTOBOT_WEBHOOK_PATH=/cryptobot-webhook
|
||||
CRYPTOBOT_WEBHOOK_PORT=8083
|
||||
CRYPTOBOT_DEFAULT_ASSET=USDT
|
||||
CRYPTOBOT_ASSETS=USDT,TON,BTC,ETH,LTC,BNB,TRX,USDC
|
||||
CRYPTOBOT_INVOICE_EXPIRES_HOURS=24
|
||||
|
||||
# Telegram Stars (работает автоматически)
|
||||
TELEGRAM_STARS_ENABLED=true
|
||||
TELEGRAM_STARS_RATE_RUB=1.3
|
||||
@@ -390,6 +402,10 @@ WEBHOOK_PATH=/webhook
|
||||
APP_CONFIG_PATH=app-config.json
|
||||
ENABLE_DEEP_LINKS=true
|
||||
APP_CONFIG_CACHE_TTL=3600
|
||||
|
||||
VERSION_CHECK_ENABLED=true
|
||||
VERSION_CHECK_REPO=fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
VERSION_CHECK_INTERVAL_HOURS=1
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -749,13 +765,14 @@ bedolaga_bot/
|
||||
│ │ ├── 🎫 promocodes.py # Управление промокодами
|
||||
│ │ ├── 🚧 maintenance.py # Тех работы
|
||||
│ │ ├── 📨 messages.py # Рассылки
|
||||
│ │ ├── 📨 user_messages.py # Рандомные сообщения в меню
|
||||
│ │ ├── ⚙️ main.py # Админское меню
|
||||
│ │ ├── 📖 rules.py # Правила
|
||||
│ │ ├── 🙋 referrals.py # Правила
|
||||
│ │ ├── 🌎 servers.py # Сервера
|
||||
│ │ ├── 📱 subscriptions.py # Подписки
|
||||
│ │ ├── 🔍 monitoring.py # Мониторинг
|
||||
│ │ └── 🔗 remnawave.py # Система RemnaWave
|
||||
│ │ └── 🔗 remnawave.py # Система Remnawave
|
||||
│ │
|
||||
│ ├── 🗄️ database/ # База данных
|
||||
│ │ ├── 📊 models.py # Модели SQLAlchemy
|
||||
@@ -768,6 +785,7 @@ bedolaga_bot/
|
||||
│ │ ├── 📜 rules.py # Правила сервиса
|
||||
│ │ ├── 📜 subscription_conversion.py # Правила сервиса
|
||||
│ │ ├── 💳 yookassa.py # YooKassa операции
|
||||
│ │ ├── 💳 cryptobot.py # CryptoBot операции
|
||||
│ │ ├── 🌐 server_squad.py # Серверы и сквады
|
||||
│ │ ├── 🎁 promocode.py # Промокоды
|
||||
│ │ └── 👥 referral.py # Рефералы
|
||||
@@ -781,9 +799,10 @@ bedolaga_bot/
|
||||
│ │ ├── 👥 referral_service.py # Рефералы
|
||||
│ │ ├── 💬 admin_notification_service.py # Уведомления для администраторов в чаты
|
||||
│ │ ├── 🔍 monitoring_service.py # Мониторинг
|
||||
│ │ ├── ♻️ version_service.py # Проверка версий бота
|
||||
│ │ ├── 🎖️ tribute_service.py # Tribute платежи
|
||||
│ │ ├── 💳 yookassa_service.py # YooKassa платежи
|
||||
│ │ └── 🌐 remnawave_service.py # Интеграция с RemnaWave
|
||||
│ │ └── 🌐 remnawave_service.py # Интеграция с Remnawave
|
||||
│ │
|
||||
│ ├── 🛠️ utils/ # Утилиты
|
||||
│ │ ├── 🎨 decorators.py # Декораторы
|
||||
@@ -793,6 +812,7 @@ bedolaga_bot/
|
||||
│ │ ├── 📄 pagination.py # Пагинация
|
||||
│ │ ├── 📄 pricing_utils.py # Цены
|
||||
│ │ ├── 👤 user_utils.py # Утилиты для пользователей
|
||||
│ │ ├── 🫰 currency_converter.py # Курсы для CryptoBota
|
||||
│ │ └── ⚡ cache.py # Кеширование
|
||||
│ │
|
||||
│ ├── 🛡️ middlewares/ # Middleware
|
||||
@@ -812,10 +832,11 @@ bedolaga_bot/
|
||||
│ │ └── 👑 admin.py # Админские клавиатуры
|
||||
│ │
|
||||
│ └── 🔌 external/ # Внешние API
|
||||
│ ├── 🌐 remnawave_api.py # RemnaWave API
|
||||
│ ├── 🌐 remnawave_api.py # Remnawave API
|
||||
│ ├── ⭐ telegram_stars.py # Telegram Stars
|
||||
│ ├── 💳 yookassa_webhook.py # YooKassa webhook
|
||||
│ ├── 🌐 webhook_server.py # Webhook сервер
|
||||
│ ├── 💳 cryptobot.py # CryptoBot Api
|
||||
│ └── 🎖️ tribute.py # Tribute платежи
|
||||
│
|
||||
├── 🔄 migrations/ # Миграции БД
|
||||
@@ -882,6 +903,14 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# CryptoBot webhook endpoint
|
||||
handle /cryptobot-webhook* {
|
||||
reverse_proxy localhost:8081 {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# Для YooKassa
|
||||
location /yookassa-webhook {
|
||||
@@ -942,7 +971,7 @@ your-domain.com {
|
||||
|
||||
## 🛡️ Безопасность
|
||||
|
||||
### 🔐 Защита панели RemnaWave
|
||||
### 🔐 Защита панели Remnawave
|
||||
|
||||
Бот поддерживает интеграцию с системой защиты панели через куки-аутентификацию:
|
||||
|
||||
@@ -1007,22 +1036,22 @@ REMNAWAVE_SECRET_KEY=XXXXXXX:DDDDDDDD
|
||||
|
||||
<tr>
|
||||
<td>🥇</td>
|
||||
<td><strong>Илья (@ispanec_nn)</strong></td>
|
||||
<td>$30</td>
|
||||
<td><strong>@pilot_737800</strong></td>
|
||||
<td>₽4,750</td>
|
||||
<td>За веру в проект с самого начала</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>🥈</td>
|
||||
<td><strong>@Legacyyy777</strong></td>
|
||||
<td>₽2,400</td>
|
||||
<td>₽2,600</td>
|
||||
<td>За ценные предложения по улучшению</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>🥉</td>
|
||||
<td><strong>@pilot_737800</strong></td>
|
||||
<td>₽2,250</td>
|
||||
<td><strong>Илья (@ispanec_nn)</strong></td>
|
||||
<td>$30</td>
|
||||
<td>За активное тестирование и фидбек</td>
|
||||
</tr>
|
||||
|
||||
|
||||
+8
-3
@@ -24,7 +24,8 @@ from app.handlers.admin import (
|
||||
rules as admin_rules, remnawave as admin_remnawave,
|
||||
statistics as admin_statistics, servers as admin_servers,
|
||||
maintenance as admin_maintenance,
|
||||
user_messages as admin_user_messages
|
||||
user_messages as admin_user_messages,
|
||||
updates as admin_updates
|
||||
)
|
||||
from app.handlers.stars_payments import register_stars_handlers
|
||||
|
||||
@@ -68,6 +69,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
storage = MemoryStorage()
|
||||
|
||||
dp = Dispatcher(storage=storage)
|
||||
|
||||
dp.message.middleware(LoggingMiddleware())
|
||||
dp.callback_query.middleware(LoggingMiddleware())
|
||||
dp.message.middleware(AuthMiddleware())
|
||||
@@ -79,6 +81,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
dp.callback_query.middleware(ThrottlingMiddleware())
|
||||
dp.message.middleware(SubscriptionStatusMiddleware())
|
||||
dp.callback_query.middleware(SubscriptionStatusMiddleware())
|
||||
|
||||
start.register_handlers(dp)
|
||||
menu.register_handlers(dp)
|
||||
subscription.register_handlers(dp)
|
||||
@@ -86,6 +89,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
promocode.register_handlers(dp)
|
||||
referral.register_handlers(dp)
|
||||
support.register_handlers(dp)
|
||||
|
||||
admin_main.register_handlers(dp)
|
||||
admin_users.register_handlers(dp)
|
||||
admin_subscriptions.register_handlers(dp)
|
||||
@@ -98,12 +102,13 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_remnawave.register_handlers(dp)
|
||||
admin_statistics.register_handlers(dp)
|
||||
admin_maintenance.register_handlers(dp)
|
||||
admin_user_messages.register_handlers(dp)
|
||||
admin_user_messages.register_handlers(dp)
|
||||
admin_updates.register_handlers(dp)
|
||||
|
||||
common.register_handlers(dp)
|
||||
|
||||
register_stars_handlers(dp)
|
||||
logger.info("🌟 Зарегистрированы обработчики Telegram Stars платежей")
|
||||
logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей")
|
||||
|
||||
try:
|
||||
await maintenance_service.start_monitoring()
|
||||
|
||||
+107
-7
@@ -15,7 +15,18 @@ class Settings(BaseSettings):
|
||||
ADMIN_NOTIFICATIONS_CHAT_ID: Optional[str] = None
|
||||
ADMIN_NOTIFICATIONS_TOPIC_ID: Optional[int] = None
|
||||
|
||||
DATABASE_URL: str
|
||||
DATABASE_URL: str = ""
|
||||
|
||||
POSTGRES_HOST: str = "postgres"
|
||||
POSTGRES_PORT: int = 5432
|
||||
POSTGRES_DB: str = "remnawave_bot"
|
||||
POSTGRES_USER: str = "remnawave_user"
|
||||
POSTGRES_PASSWORD: str = "secure_password_123"
|
||||
|
||||
SQLITE_PATH: str = "./data/bot.db"
|
||||
|
||||
DATABASE_MODE: str = "auto"
|
||||
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
|
||||
REMNAWAVE_API_URL: str
|
||||
@@ -114,6 +125,16 @@ class Settings(BaseSettings):
|
||||
PAYMENT_BALANCE_TEMPLATE: str = "{service_name} - {description}"
|
||||
PAYMENT_SUBSCRIPTION_TEMPLATE: str = "{service_name} - {description}"
|
||||
|
||||
CRYPTOBOT_ENABLED: bool = False
|
||||
CRYPTOBOT_API_TOKEN: Optional[str] = None
|
||||
CRYPTOBOT_WEBHOOK_SECRET: Optional[str] = None
|
||||
CRYPTOBOT_BASE_URL: str = "https://pay.crypt.bot"
|
||||
CRYPTOBOT_TESTNET: bool = False
|
||||
CRYPTOBOT_WEBHOOK_PATH: str = "/cryptobot-webhook"
|
||||
CRYPTOBOT_WEBHOOK_PORT: int = 8083
|
||||
CRYPTOBOT_DEFAULT_ASSET: str = "USDT"
|
||||
CRYPTOBOT_ASSETS: str = "USDT,TON,BTC,ETH"
|
||||
CRYPTOBOT_INVOICE_EXPIRES_HOURS: int = 24
|
||||
|
||||
CONNECT_BUTTON_MODE: str = "guide"
|
||||
MINIAPP_CUSTOM_URL: str = ""
|
||||
@@ -131,6 +152,10 @@ class Settings(BaseSettings):
|
||||
APP_CONFIG_PATH: str = "app-config.json"
|
||||
ENABLE_DEEP_LINKS: bool = True
|
||||
APP_CONFIG_CACHE_TTL: int = 3600
|
||||
|
||||
VERSION_CHECK_ENABLED: bool = True
|
||||
VERSION_CHECK_REPO: str = "fr1ngg/remnawave-bedolaga-telegram-bot"
|
||||
VERSION_CHECK_INTERVAL_HOURS: int = 1
|
||||
|
||||
@field_validator('LOG_FILE', mode='before')
|
||||
@classmethod
|
||||
@@ -139,6 +164,48 @@ class Settings(BaseSettings):
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return str(log_path)
|
||||
|
||||
def get_database_url(self) -> str:
|
||||
if self.DATABASE_URL and self.DATABASE_URL.strip():
|
||||
return self.DATABASE_URL
|
||||
|
||||
mode = self.DATABASE_MODE.lower()
|
||||
|
||||
if mode == "sqlite":
|
||||
return self._get_sqlite_url()
|
||||
elif mode == "postgresql":
|
||||
return self._get_postgresql_url()
|
||||
elif mode == "auto":
|
||||
if os.getenv("DOCKER_ENV") == "true" or os.path.exists("/.dockerenv"):
|
||||
return self._get_postgresql_url()
|
||||
else:
|
||||
return self._get_sqlite_url()
|
||||
else:
|
||||
return self._get_auto_database_url()
|
||||
|
||||
def _get_sqlite_url(self) -> str:
|
||||
sqlite_path = Path(self.SQLITE_PATH)
|
||||
sqlite_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return f"sqlite+aiosqlite:///{sqlite_path.absolute()}"
|
||||
|
||||
def _get_postgresql_url(self) -> str:
|
||||
return (f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}"
|
||||
f"@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}")
|
||||
|
||||
def _get_auto_database_url(self) -> str:
|
||||
if (os.getenv("DOCKER_ENV") == "true" or
|
||||
os.path.exists("/.dockerenv")):
|
||||
return self._get_postgresql_url()
|
||||
else:
|
||||
return self._get_sqlite_url()
|
||||
|
||||
def is_postgresql(self) -> bool:
|
||||
"""Проверяет, используется ли PostgreSQL"""
|
||||
return "postgresql" in self.get_database_url()
|
||||
|
||||
def is_sqlite(self) -> bool:
|
||||
"""Проверяет, используется ли SQLite"""
|
||||
return "sqlite" in self.get_database_url()
|
||||
|
||||
def is_admin(self, user_id: int) -> bool:
|
||||
return user_id in self.get_admin_ids()
|
||||
|
||||
@@ -179,8 +246,8 @@ class Settings(BaseSettings):
|
||||
return ["ru", "en"]
|
||||
|
||||
def format_price(self, price_kopeks: int) -> str:
|
||||
rubles = price_kopeks / 100
|
||||
return f"{rubles:.2f} ₽"
|
||||
rubles = price_kopeks // 100
|
||||
return f"{rubles} ₽"
|
||||
|
||||
def kopeks_to_rubles(self, kopeks: int) -> float:
|
||||
return kopeks / 100
|
||||
@@ -228,6 +295,27 @@ class Settings(BaseSettings):
|
||||
return f"{self.WEBHOOK_URL}/payment-success"
|
||||
return "https://t.me/"
|
||||
|
||||
def is_cryptobot_enabled(self) -> bool:
|
||||
return (self.CRYPTOBOT_ENABLED and
|
||||
self.CRYPTOBOT_API_TOKEN is not None)
|
||||
|
||||
def get_cryptobot_base_url(self) -> str:
|
||||
if self.CRYPTOBOT_TESTNET:
|
||||
return "https://testnet-pay.crypt.bot"
|
||||
return self.CRYPTOBOT_BASE_URL
|
||||
|
||||
def get_cryptobot_assets(self) -> List[str]:
|
||||
try:
|
||||
assets = self.CRYPTOBOT_ASSETS.strip()
|
||||
if not assets:
|
||||
return ["USDT", "TON"]
|
||||
return [asset.strip() for asset in assets.split(',') if asset.strip()]
|
||||
except (ValueError, AttributeError):
|
||||
return ["USDT", "TON"]
|
||||
|
||||
def get_cryptobot_invoice_expires_seconds(self) -> int:
|
||||
return self.CRYPTOBOT_INVOICE_EXPIRES_HOURS * 3600
|
||||
|
||||
def is_maintenance_mode(self) -> bool:
|
||||
return self.MAINTENANCE_MODE
|
||||
|
||||
@@ -251,7 +339,7 @@ class Settings(BaseSettings):
|
||||
period_str = period_str.strip()
|
||||
if period_str:
|
||||
period = int(period_str)
|
||||
if period in PERIOD_PRICES:
|
||||
if hasattr(self, f'PRICE_{period}_DAYS'):
|
||||
periods.append(period)
|
||||
|
||||
return periods if periods else [30, 90, 180]
|
||||
@@ -270,7 +358,7 @@ class Settings(BaseSettings):
|
||||
period_str = period_str.strip()
|
||||
if period_str:
|
||||
period = int(period_str)
|
||||
if period in PERIOD_PRICES:
|
||||
if hasattr(self, f'PRICE_{period}_DAYS'):
|
||||
periods.append(period)
|
||||
|
||||
return periods if periods else [30, 90, 180]
|
||||
@@ -375,6 +463,15 @@ class Settings(BaseSettings):
|
||||
except Exception as e:
|
||||
logger.info(f"ERROR PARSING CONFIG: {e}")
|
||||
return self._get_fallback_traffic_packages()
|
||||
|
||||
def is_version_check_enabled(self) -> bool:
|
||||
return self.VERSION_CHECK_ENABLED
|
||||
|
||||
def get_version_check_repo(self) -> str:
|
||||
return self.VERSION_CHECK_REPO
|
||||
|
||||
def get_version_check_interval(self) -> int:
|
||||
return self.VERSION_CHECK_INTERVAL_HOURS
|
||||
|
||||
def _get_fallback_traffic_packages(self) -> List[Dict]:
|
||||
try:
|
||||
@@ -455,13 +552,13 @@ class Settings(BaseSettings):
|
||||
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
"env_file_encoding": "utf-8"
|
||||
"env_file_encoding": "utf-8",
|
||||
"extra": "ignore"
|
||||
}
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
PERIOD_PRICES = {
|
||||
14: settings.PRICE_14_DAYS,
|
||||
30: settings.PRICE_30_DAYS,
|
||||
@@ -482,3 +579,6 @@ def refresh_traffic_prices():
|
||||
TRAFFIC_PRICES = get_traffic_prices()
|
||||
|
||||
refresh_traffic_prices()
|
||||
|
||||
settings._original_database_url = settings.DATABASE_URL
|
||||
settings.DATABASE_URL = settings.get_database_url()
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.database.models import CryptoBotPayment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_cryptobot_payment(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
invoice_id: str,
|
||||
amount: str,
|
||||
asset: str,
|
||||
status: str = "active",
|
||||
description: Optional[str] = None,
|
||||
payload: Optional[str] = None,
|
||||
bot_invoice_url: Optional[str] = None,
|
||||
mini_app_invoice_url: Optional[str] = None,
|
||||
web_app_invoice_url: Optional[str] = None
|
||||
) -> CryptoBotPayment:
|
||||
|
||||
payment = CryptoBotPayment(
|
||||
user_id=user_id,
|
||||
invoice_id=invoice_id,
|
||||
amount=amount,
|
||||
asset=asset,
|
||||
status=status,
|
||||
description=description,
|
||||
payload=payload,
|
||||
bot_invoice_url=bot_invoice_url,
|
||||
mini_app_invoice_url=mini_app_invoice_url,
|
||||
web_app_invoice_url=web_app_invoice_url
|
||||
)
|
||||
|
||||
db.add(payment)
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(f"Создан CryptoBot платеж: {invoice_id} на {amount} {asset} для пользователя {user_id}")
|
||||
return payment
|
||||
|
||||
|
||||
async def get_cryptobot_payment_by_invoice_id(
|
||||
db: AsyncSession,
|
||||
invoice_id: str
|
||||
) -> Optional[CryptoBotPayment]:
|
||||
|
||||
result = await db.execute(
|
||||
select(CryptoBotPayment)
|
||||
.options(selectinload(CryptoBotPayment.user))
|
||||
.where(CryptoBotPayment.invoice_id == invoice_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_cryptobot_payment_by_id(
|
||||
db: AsyncSession,
|
||||
payment_id: int
|
||||
) -> Optional[CryptoBotPayment]:
|
||||
|
||||
result = await db.execute(
|
||||
select(CryptoBotPayment)
|
||||
.options(selectinload(CryptoBotPayment.user))
|
||||
.where(CryptoBotPayment.id == payment_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def update_cryptobot_payment_status(
|
||||
db: AsyncSession,
|
||||
invoice_id: str,
|
||||
status: str,
|
||||
paid_at: Optional[datetime] = None
|
||||
) -> Optional[CryptoBotPayment]:
|
||||
|
||||
payment = await get_cryptobot_payment_by_invoice_id(db, invoice_id)
|
||||
|
||||
if not payment:
|
||||
return None
|
||||
|
||||
payment.status = status
|
||||
payment.updated_at = datetime.utcnow()
|
||||
|
||||
if status == "paid" and paid_at:
|
||||
payment.paid_at = paid_at
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(f"Обновлен статус CryptoBot платежа {invoice_id}: {status}")
|
||||
return payment
|
||||
|
||||
|
||||
async def link_cryptobot_payment_to_transaction(
|
||||
db: AsyncSession,
|
||||
invoice_id: str,
|
||||
transaction_id: int
|
||||
) -> Optional[CryptoBotPayment]:
|
||||
|
||||
payment = await get_cryptobot_payment_by_invoice_id(db, invoice_id)
|
||||
|
||||
if not payment:
|
||||
return None
|
||||
|
||||
payment.transaction_id = transaction_id
|
||||
payment.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(payment)
|
||||
|
||||
logger.info(f"Связан CryptoBot платеж {invoice_id} с транзакцией {transaction_id}")
|
||||
return payment
|
||||
|
||||
|
||||
async def get_user_cryptobot_payments(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> List[CryptoBotPayment]:
|
||||
|
||||
result = await db.execute(
|
||||
select(CryptoBotPayment)
|
||||
.where(CryptoBotPayment.user_id == user_id)
|
||||
.order_by(CryptoBotPayment.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_pending_cryptobot_payments(
|
||||
db: AsyncSession,
|
||||
older_than_hours: int = 24
|
||||
) -> List[CryptoBotPayment]:
|
||||
|
||||
from datetime import timedelta
|
||||
cutoff_time = datetime.utcnow() - timedelta(hours=older_than_hours)
|
||||
|
||||
result = await db.execute(
|
||||
select(CryptoBotPayment)
|
||||
.options(selectinload(CryptoBotPayment.user))
|
||||
.where(
|
||||
and_(
|
||||
CryptoBotPayment.status == "active",
|
||||
CryptoBotPayment.created_at < cutoff_time
|
||||
)
|
||||
)
|
||||
.order_by(CryptoBotPayment.created_at)
|
||||
)
|
||||
return result.scalars().all()
|
||||
+51
-8
@@ -45,6 +45,7 @@ class PaymentMethod(Enum):
|
||||
TELEGRAM_STARS = "telegram_stars"
|
||||
TRIBUTE = "tribute"
|
||||
YOOKASSA = "yookassa"
|
||||
CRYPTOBOT = "cryptobot"
|
||||
MANUAL = "manual"
|
||||
|
||||
class YooKassaPayment(Base):
|
||||
@@ -95,6 +96,55 @@ class YooKassaPayment(Base):
|
||||
def __repr__(self):
|
||||
return f"<YooKassaPayment(id={self.id}, yookassa_id={self.yookassa_payment_id}, amount={self.amount_rubles}₽, status={self.status})>"
|
||||
|
||||
class CryptoBotPayment(Base):
|
||||
__tablename__ = "cryptobot_payments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
invoice_id = Column(String(255), unique=True, nullable=False, index=True)
|
||||
amount = Column(String(50), nullable=False)
|
||||
asset = Column(String(10), nullable=False)
|
||||
|
||||
status = Column(String(50), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
payload = Column(Text, nullable=True)
|
||||
|
||||
bot_invoice_url = Column(Text, nullable=True)
|
||||
mini_app_invoice_url = Column(Text, nullable=True)
|
||||
web_app_invoice_url = Column(Text, nullable=True)
|
||||
|
||||
paid_at = Column(DateTime, nullable=True)
|
||||
transaction_id = Column(Integer, ForeignKey("transactions.id"), nullable=True)
|
||||
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
user = relationship("User", backref="cryptobot_payments")
|
||||
transaction = relationship("Transaction", backref="cryptobot_payment")
|
||||
|
||||
@property
|
||||
def amount_float(self) -> float:
|
||||
try:
|
||||
return float(self.amount)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def is_paid(self) -> bool:
|
||||
return self.status == "paid"
|
||||
|
||||
@property
|
||||
def is_pending(self) -> bool:
|
||||
return self.status == "active"
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
return self.status == "expired"
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CryptoBotPayment(id={self.id}, invoice_id={self.invoice_id}, amount={self.amount} {self.asset}, status={self.status})>"
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
@@ -585,20 +635,13 @@ class SubscriptionServer(Base):
|
||||
|
||||
class UserMessage(Base):
|
||||
__tablename__ = "user_messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
message_text = Column(Text, nullable=False)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
sort_order = Column(Integer, default=0)
|
||||
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
|
||||
creator = relationship("User", backref="created_messages")
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
+188
-122
@@ -74,11 +74,11 @@ async def check_column_exists(table_name: str, column_name: str) -> bool:
|
||||
logger.error(f"Ошибка проверки существования колонки {column_name}: {e}")
|
||||
return False
|
||||
|
||||
async def create_yookassa_payments_table():
|
||||
|
||||
table_exists = await check_table_exists('yookassa_payments')
|
||||
async def create_cryptobot_payments_table():
|
||||
"""Создание таблицы cryptobot_payments"""
|
||||
table_exists = await check_table_exists('cryptobot_payments')
|
||||
if table_exists:
|
||||
logger.info("Таблица yookassa_payments уже существует")
|
||||
logger.info("Таблица cryptobot_payments уже существует")
|
||||
return True
|
||||
|
||||
try:
|
||||
@@ -87,159 +87,211 @@ async def create_yookassa_payments_table():
|
||||
|
||||
if db_type == 'sqlite':
|
||||
create_sql = """
|
||||
CREATE TABLE yookassa_payments (
|
||||
CREATE TABLE cryptobot_payments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
yookassa_payment_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
amount_kopeks INTEGER NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'RUB' NOT NULL,
|
||||
description TEXT NULL,
|
||||
invoice_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
amount VARCHAR(50) NOT NULL,
|
||||
asset VARCHAR(10) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
is_paid BOOLEAN DEFAULT 0,
|
||||
is_captured BOOLEAN DEFAULT 0,
|
||||
confirmation_url TEXT NULL,
|
||||
metadata_json TEXT NULL,
|
||||
description TEXT NULL,
|
||||
payload TEXT NULL,
|
||||
bot_invoice_url TEXT NULL,
|
||||
mini_app_invoice_url TEXT NULL,
|
||||
web_app_invoice_url TEXT NULL,
|
||||
paid_at DATETIME NULL,
|
||||
transaction_id INTEGER NULL,
|
||||
payment_method_type VARCHAR(50) NULL,
|
||||
refundable BOOLEAN DEFAULT 0,
|
||||
test_mode BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
yookassa_created_at DATETIME NULL,
|
||||
captured_at DATETIME NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_yookassa_payments_user_id ON yookassa_payments(user_id);
|
||||
CREATE INDEX idx_yookassa_payments_yookassa_id ON yookassa_payments(yookassa_payment_id);
|
||||
CREATE INDEX idx_yookassa_payments_status ON yookassa_payments(status);
|
||||
CREATE INDEX idx_cryptobot_payments_user_id ON cryptobot_payments(user_id);
|
||||
CREATE INDEX idx_cryptobot_payments_invoice_id ON cryptobot_payments(invoice_id);
|
||||
CREATE INDEX idx_cryptobot_payments_status ON cryptobot_payments(status);
|
||||
"""
|
||||
|
||||
elif db_type == 'postgresql':
|
||||
create_sql = """
|
||||
CREATE TABLE yookassa_payments (
|
||||
CREATE TABLE cryptobot_payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
yookassa_payment_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
amount_kopeks INTEGER NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'RUB' NOT NULL,
|
||||
description TEXT NULL,
|
||||
invoice_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
amount VARCHAR(50) NOT NULL,
|
||||
asset VARCHAR(10) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
is_paid BOOLEAN DEFAULT FALSE,
|
||||
is_captured BOOLEAN DEFAULT FALSE,
|
||||
confirmation_url TEXT NULL,
|
||||
metadata_json JSONB NULL,
|
||||
description TEXT NULL,
|
||||
payload TEXT NULL,
|
||||
bot_invoice_url TEXT NULL,
|
||||
mini_app_invoice_url TEXT NULL,
|
||||
web_app_invoice_url TEXT NULL,
|
||||
paid_at TIMESTAMP NULL,
|
||||
transaction_id INTEGER NULL,
|
||||
payment_method_type VARCHAR(50) NULL,
|
||||
refundable BOOLEAN DEFAULT FALSE,
|
||||
test_mode BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
yookassa_created_at TIMESTAMP NULL,
|
||||
captured_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_yookassa_payments_user_id ON yookassa_payments(user_id);
|
||||
CREATE INDEX idx_yookassa_payments_yookassa_id ON yookassa_payments(yookassa_payment_id);
|
||||
CREATE INDEX idx_yookassa_payments_status ON yookassa_payments(status);
|
||||
CREATE INDEX idx_cryptobot_payments_user_id ON cryptobot_payments(user_id);
|
||||
CREATE INDEX idx_cryptobot_payments_invoice_id ON cryptobot_payments(invoice_id);
|
||||
CREATE INDEX idx_cryptobot_payments_status ON cryptobot_payments(status);
|
||||
"""
|
||||
|
||||
elif db_type == 'mysql':
|
||||
create_sql = """
|
||||
CREATE TABLE yookassa_payments (
|
||||
CREATE TABLE cryptobot_payments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
yookassa_payment_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
amount_kopeks INT NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'RUB' NOT NULL,
|
||||
description TEXT NULL,
|
||||
invoice_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
amount VARCHAR(50) NOT NULL,
|
||||
asset VARCHAR(10) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
is_paid BOOLEAN DEFAULT FALSE,
|
||||
is_captured BOOLEAN DEFAULT FALSE,
|
||||
confirmation_url TEXT NULL,
|
||||
metadata_json JSON NULL,
|
||||
description TEXT NULL,
|
||||
payload TEXT NULL,
|
||||
bot_invoice_url TEXT NULL,
|
||||
mini_app_invoice_url TEXT NULL,
|
||||
web_app_invoice_url TEXT NULL,
|
||||
paid_at DATETIME NULL,
|
||||
transaction_id INT NULL,
|
||||
payment_method_type VARCHAR(50) NULL,
|
||||
refundable BOOLEAN DEFAULT FALSE,
|
||||
test_mode BOOLEAN DEFAULT FALSE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
yookassa_created_at DATETIME NULL,
|
||||
captured_at DATETIME NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_yookassa_payments_user_id ON yookassa_payments(user_id);
|
||||
CREATE INDEX idx_yookassa_payments_yookassa_id ON yookassa_payments(yookassa_payment_id);
|
||||
CREATE INDEX idx_yookassa_payments_status ON yookassa_payments(status);
|
||||
CREATE INDEX idx_cryptobot_payments_user_id ON cryptobot_payments(user_id);
|
||||
CREATE INDEX idx_cryptobot_payments_invoice_id ON cryptobot_payments(invoice_id);
|
||||
CREATE INDEX idx_cryptobot_payments_status ON cryptobot_payments(status);
|
||||
"""
|
||||
else:
|
||||
logger.error(f"Неподдерживаемый тип БД для создания таблицы: {db_type}")
|
||||
return False
|
||||
|
||||
await conn.execute(text(create_sql))
|
||||
logger.info("Таблица yookassa_payments успешно создана")
|
||||
logger.info("Таблица cryptobot_payments успешно создана")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания таблицы yookassa_payments: {e}")
|
||||
logger.error(f"Ошибка создания таблицы cryptobot_payments: {e}")
|
||||
return False
|
||||
|
||||
async def add_remnawave_v2_columns():
|
||||
|
||||
columns_to_add = {
|
||||
'lifetime_used_traffic_bytes': 'BIGINT DEFAULT 0',
|
||||
'last_remnawave_sync': 'TIMESTAMP NULL',
|
||||
'trojan_password': 'VARCHAR(255) NULL',
|
||||
'vless_uuid': 'VARCHAR(255) NULL',
|
||||
'ss_password': 'VARCHAR(255) NULL'
|
||||
}
|
||||
|
||||
logger.info("=== ПРОВЕРКА КОЛОНОК REMNAWAVE V2.1.5 ===")
|
||||
async def create_user_messages_table():
|
||||
"""Создание таблицы user_messages"""
|
||||
table_exists = await check_table_exists('user_messages')
|
||||
if table_exists:
|
||||
logger.info("Таблица user_messages уже существует")
|
||||
return True
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
columns_added = 0
|
||||
|
||||
for column_name, column_def in columns_to_add.items():
|
||||
exists = await check_column_exists('users', column_name)
|
||||
if db_type == 'sqlite':
|
||||
create_sql = """
|
||||
CREATE TABLE user_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_by INTEGER NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
if not exists:
|
||||
logger.info(f"Добавление колонки {column_name} в таблицу users")
|
||||
|
||||
if db_type == 'sqlite':
|
||||
if column_def.startswith('BIGINT'):
|
||||
column_def = column_def.replace('BIGINT', 'INTEGER')
|
||||
column_def = column_def.replace('TIMESTAMP', 'DATETIME')
|
||||
elif db_type == 'mysql':
|
||||
column_def = column_def.replace('TIMESTAMP', 'DATETIME')
|
||||
|
||||
try:
|
||||
await conn.execute(text(f"ALTER TABLE users ADD COLUMN {column_name} {column_def}"))
|
||||
columns_added += 1
|
||||
logger.info(f"Колонка {column_name} успешно добавлена")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления колонки {column_name}: {e}")
|
||||
continue
|
||||
|
||||
else:
|
||||
logger.debug(f"Колонка {column_name} уже существует")
|
||||
|
||||
if columns_added > 0:
|
||||
logger.info(f"Добавлено {columns_added} новых колонок для RemnaWave v2.1.5")
|
||||
CREATE INDEX idx_user_messages_active ON user_messages(is_active);
|
||||
CREATE INDEX idx_user_messages_sort ON user_messages(sort_order, created_at);
|
||||
"""
|
||||
|
||||
elif db_type == 'postgresql':
|
||||
create_sql = """
|
||||
CREATE TABLE user_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
message_text TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_by INTEGER NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_messages_active ON user_messages(is_active);
|
||||
CREATE INDEX idx_user_messages_sort ON user_messages(sort_order, created_at);
|
||||
"""
|
||||
|
||||
elif db_type == 'mysql':
|
||||
create_sql = """
|
||||
CREATE TABLE user_messages (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
message_text TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INT DEFAULT 0,
|
||||
created_by INT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_messages_active ON user_messages(is_active);
|
||||
CREATE INDEX idx_user_messages_sort ON user_messages(sort_order, created_at);
|
||||
"""
|
||||
else:
|
||||
logger.info("Все колонки RemnaWave v2.1.5 уже существуют")
|
||||
|
||||
return columns_added
|
||||
logger.error(f"Неподдерживаемый тип БД для создания таблицы: {db_type}")
|
||||
return False
|
||||
|
||||
await conn.execute(text(create_sql))
|
||||
logger.info("Таблица user_messages успешно создана")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении колонок RemnaWave v2.1.5: {e}")
|
||||
return 0
|
||||
logger.error(f"Ошибка создания таблицы user_messages: {e}")
|
||||
return False
|
||||
|
||||
async def fix_foreign_keys_for_user_deletion():
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if db_type == 'postgresql':
|
||||
try:
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE user_messages
|
||||
DROP CONSTRAINT IF EXISTS user_messages_created_by_fkey;
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE user_messages
|
||||
ADD CONSTRAINT user_messages_created_by_fkey
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
|
||||
"""))
|
||||
logger.info("Обновлен внешний ключ user_messages.created_by")
|
||||
except Exception as e:
|
||||
logger.warning(f"Ошибка обновления FK user_messages: {e}")
|
||||
|
||||
try:
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE promocodes
|
||||
DROP CONSTRAINT IF EXISTS promocodes_created_by_fkey;
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE promocodes
|
||||
ADD CONSTRAINT promocodes_created_by_fkey
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL;
|
||||
"""))
|
||||
logger.info("Обновлен внешний ключ promocodes.created_by")
|
||||
except Exception as e:
|
||||
logger.warning(f"Ошибка обновления FK promocodes: {e}")
|
||||
|
||||
logger.info("Внешние ключи обновлены для безопасного удаления пользователей")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления внешних ключей: {e}")
|
||||
return False
|
||||
|
||||
async def add_referral_system_columns():
|
||||
logger.info("=== МИГРАЦИЯ РЕФЕРАЛЬНОЙ СИСТЕМЫ ===")
|
||||
@@ -292,7 +344,7 @@ async def add_referral_system_columns():
|
||||
return False
|
||||
|
||||
async def create_subscription_conversions_table():
|
||||
|
||||
"""Создание таблицы subscription_conversions"""
|
||||
table_exists = await check_table_exists('subscription_conversions')
|
||||
if table_exists:
|
||||
logger.info("Таблица subscription_conversions уже существует")
|
||||
@@ -368,7 +420,7 @@ async def create_subscription_conversions_table():
|
||||
return False
|
||||
|
||||
async def fix_subscription_duplicates_universal():
|
||||
|
||||
"""Исправление дублирующихся подписок"""
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
logger.info(f"Обнаружен тип базы данных: {db_type}")
|
||||
@@ -453,19 +505,36 @@ async def run_universal_migration():
|
||||
db_type = await get_database_type()
|
||||
logger.info(f"Тип базы данных: {db_type}")
|
||||
|
||||
await add_remnawave_v2_columns()
|
||||
|
||||
# Миграция реферальной системы
|
||||
referral_migration_success = await add_referral_system_columns()
|
||||
if not referral_migration_success:
|
||||
logger.warning("⚠️ Проблемы с миграцией реферальной системы")
|
||||
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ YOOKASSA ===")
|
||||
yookassa_created = await create_yookassa_payments_table()
|
||||
if yookassa_created:
|
||||
logger.info("✅ Таблица YooKassa payments готова")
|
||||
# Создание таблицы CryptoBot payments
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ CRYPTOBOT ===")
|
||||
cryptobot_created = await create_cryptobot_payments_table()
|
||||
if cryptobot_created:
|
||||
logger.info("✅ Таблица CryptoBot payments готова")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с таблицей YooKassa payments")
|
||||
logger.warning("⚠️ Проблемы с таблицей CryptoBot payments")
|
||||
|
||||
# Создание таблицы user_messages
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ USER_MESSAGES ===")
|
||||
user_messages_created = await create_user_messages_table()
|
||||
if user_messages_created:
|
||||
logger.info("✅ Таблица user_messages готова")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с таблицей user_messages")
|
||||
|
||||
# Обновление внешних ключей
|
||||
logger.info("=== ОБНОВЛЕНИЕ ВНЕШНИХ КЛЮЧЕЙ ===")
|
||||
fk_updated = await fix_foreign_keys_for_user_deletion()
|
||||
if fk_updated:
|
||||
logger.info("✅ Внешние ключи обновлены")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с обновлением внешних ключей")
|
||||
|
||||
# Создание таблицы конверсий подписок
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ КОНВЕРСИЙ ПОДПИСОК ===")
|
||||
conversions_created = await create_subscription_conversions_table()
|
||||
if conversions_created:
|
||||
@@ -473,6 +542,7 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с таблицей subscription_conversions")
|
||||
|
||||
# Проверка и исправление дублирующихся подписок
|
||||
async with engine.begin() as conn:
|
||||
total_subs = await conn.execute(text("SELECT COUNT(*) FROM subscriptions"))
|
||||
unique_users = await conn.execute(text("SELECT COUNT(DISTINCT user_id) FROM subscriptions"))
|
||||
@@ -490,6 +560,7 @@ async def run_universal_migration():
|
||||
|
||||
deleted_count = await fix_subscription_duplicates_universal()
|
||||
|
||||
# Финальная проверка
|
||||
async with engine.begin() as conn:
|
||||
final_check = await conn.execute(text("""
|
||||
SELECT user_id, COUNT(*) as count
|
||||
@@ -506,8 +577,7 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.info("=== МИГРАЦИЯ ЗАВЕРШЕНА УСПЕШНО ===")
|
||||
logger.info("✅ Реферальная система обновлена")
|
||||
logger.info("✅ RemnaWave v2.1.5 колонки добавлены")
|
||||
logger.info("✅ YooKassa таблица готова")
|
||||
logger.info("✅ CryptoBot таблица готова")
|
||||
logger.info("✅ Таблица конверсий подписок создана")
|
||||
logger.info("✅ Дубликаты подписок исправлены")
|
||||
return True
|
||||
@@ -522,25 +592,21 @@ async def check_migration_status():
|
||||
try:
|
||||
status = {
|
||||
"has_made_first_topup_column": False,
|
||||
"yookassa_table": False,
|
||||
"remnawave_v2_columns": False,
|
||||
"cryptobot_table": False,
|
||||
"subscription_duplicates": False,
|
||||
"subscription_conversions_table": False
|
||||
}
|
||||
|
||||
# Проверка колонки реферальной системы
|
||||
status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup')
|
||||
|
||||
status["yookassa_table"] = await check_table_exists('yookassa_payments')
|
||||
# Проверка таблицы CryptoBot
|
||||
status["cryptobot_table"] = await check_table_exists('cryptobot_payments')
|
||||
|
||||
# Проверка таблицы конверсий подписок
|
||||
status["subscription_conversions_table"] = await check_table_exists('subscription_conversions')
|
||||
|
||||
remnawave_columns = ['lifetime_used_traffic_bytes', 'last_remnawave_sync', 'trojan_password', 'vless_uuid', 'ss_password']
|
||||
remnawave_status = []
|
||||
for col in remnawave_columns:
|
||||
exists = await check_column_exists('users', col)
|
||||
remnawave_status.append(exists)
|
||||
status["remnawave_v2_columns"] = all(remnawave_status)
|
||||
|
||||
# Проверка дублирующихся подписок
|
||||
async with engine.begin() as conn:
|
||||
duplicates_check = await conn.execute(text("""
|
||||
SELECT COUNT(*) FROM (
|
||||
@@ -553,11 +619,11 @@ async def check_migration_status():
|
||||
duplicates_count = duplicates_check.fetchone()[0]
|
||||
status["subscription_duplicates"] = (duplicates_count == 0)
|
||||
|
||||
# Вывод результатов
|
||||
check_names = {
|
||||
"has_made_first_topup_column": "Колонка реферальной системы",
|
||||
"yookassa_table": "Таблица YooKassa payments",
|
||||
"cryptobot_table": "Таблица CryptoBot payments",
|
||||
"subscription_conversions_table": "Таблица конверсий подписок",
|
||||
"remnawave_v2_columns": "Колонки RemnaWave v2.1.5",
|
||||
"subscription_duplicates": "Отсутствие дубликатов подписок"
|
||||
}
|
||||
|
||||
|
||||
Vendored
+167
@@ -0,0 +1,167 @@
|
||||
import logging
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import aiohttp
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CryptoBotService:
|
||||
|
||||
def __init__(self):
|
||||
self.api_token = settings.CRYPTOBOT_API_TOKEN
|
||||
self.base_url = settings.get_cryptobot_base_url()
|
||||
self.webhook_secret = settings.CRYPTOBOT_WEBHOOK_SECRET
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
||||
if not self.api_token:
|
||||
logger.error("CryptoBot API token не настроен")
|
||||
return None
|
||||
|
||||
url = f"{self.base_url}/api/{endpoint}"
|
||||
headers = {
|
||||
'Crypto-Pay-API-Token': self.api_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(
|
||||
method,
|
||||
url,
|
||||
headers=headers,
|
||||
json=data if data else None
|
||||
) as response:
|
||||
|
||||
response_data = await response.json()
|
||||
|
||||
if response.status == 200 and response_data.get('ok'):
|
||||
return response_data.get('result')
|
||||
else:
|
||||
logger.error(f"CryptoBot API ошибка: {response_data}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запроса к CryptoBot API: {e}")
|
||||
return None
|
||||
|
||||
async def get_me(self) -> Optional[Dict[str, Any]]:
|
||||
return await self._make_request('GET', 'getMe')
|
||||
|
||||
async def create_invoice(
|
||||
self,
|
||||
amount: str,
|
||||
asset: str = "USDT",
|
||||
description: Optional[str] = None,
|
||||
payload: Optional[str] = None,
|
||||
expires_in: Optional[int] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
||||
data = {
|
||||
'currency_type': 'crypto',
|
||||
'asset': asset,
|
||||
'amount': amount
|
||||
}
|
||||
|
||||
if description:
|
||||
data['description'] = description
|
||||
|
||||
if payload:
|
||||
data['payload'] = payload
|
||||
|
||||
if expires_in:
|
||||
data['expires_in'] = expires_in
|
||||
|
||||
result = await self._make_request('POST', 'createInvoice', data)
|
||||
|
||||
if result:
|
||||
logger.info(f"Создан CryptoBot invoice {result.get('invoice_id')} на {amount} {asset}")
|
||||
|
||||
return result
|
||||
|
||||
async def get_invoices(
|
||||
self,
|
||||
asset: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
offset: int = 0,
|
||||
count: int = 100
|
||||
) -> Optional[list]:
|
||||
|
||||
data = {
|
||||
'offset': offset,
|
||||
'count': count
|
||||
}
|
||||
|
||||
if asset:
|
||||
data['asset'] = asset
|
||||
|
||||
if status:
|
||||
data['status'] = status
|
||||
|
||||
return await self._make_request('GET', 'getInvoices', data)
|
||||
|
||||
async def get_balance(self) -> Optional[list]:
|
||||
return await self._make_request('GET', 'getBalance')
|
||||
|
||||
async def get_exchange_rates(self) -> Optional[list]:
|
||||
return await self._make_request('GET', 'getExchangeRates')
|
||||
|
||||
def verify_webhook_signature(self, body: str, signature: str) -> bool:
|
||||
|
||||
if not self.webhook_secret:
|
||||
logger.warning("CryptoBot webhook secret не настроен")
|
||||
return True
|
||||
|
||||
try:
|
||||
secret_hash = hashlib.sha256(self.webhook_secret.encode()).digest()
|
||||
expected_signature = hmac.new(secret_hash, body.encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
is_valid = hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
if is_valid:
|
||||
logger.info("✅ CryptoBot webhook подпись валидна")
|
||||
else:
|
||||
logger.error("❌ Неверная подпись CryptoBot webhook")
|
||||
|
||||
return is_valid
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки подписи CryptoBot webhook: {e}")
|
||||
return False
|
||||
|
||||
async def process_webhook(self, webhook_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
|
||||
try:
|
||||
update_type = webhook_data.get('update_type')
|
||||
|
||||
if update_type == 'invoice_paid':
|
||||
invoice_data = webhook_data.get('payload', {})
|
||||
|
||||
return {
|
||||
'event_type': 'payment',
|
||||
'payment_id': str(invoice_data.get('invoice_id')),
|
||||
'amount': invoice_data.get('amount'),
|
||||
'asset': invoice_data.get('asset'),
|
||||
'status': 'paid',
|
||||
'user_payload': invoice_data.get('payload'),
|
||||
'paid_at': invoice_data.get('paid_at'),
|
||||
'payment_system': 'cryptobot'
|
||||
}
|
||||
|
||||
logger.warning(f"Неизвестный тип CryptoBot webhook: {update_type}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки CryptoBot webhook: {e}")
|
||||
return None
|
||||
Vendored
+3
-3
@@ -45,8 +45,8 @@ class TelegramStarsService:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽) "
|
||||
f"для {chat_id}, курс: {settings.get_stars_rate():.2f}₽/⭐"
|
||||
f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽) "
|
||||
f"для {chat_id}, курс: {int(settings.get_stars_rate())}₽/⭐"
|
||||
)
|
||||
return invoice_link
|
||||
|
||||
@@ -80,7 +80,7 @@ class TelegramStarsService:
|
||||
|
||||
logger.info(
|
||||
f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд "
|
||||
f"(~{amount_rubles:.2f}₽), курс: {settings.get_stars_rate():.2f}₽/⭐"
|
||||
f"(~{int(amount_rubles)}₽), курс: {int(settings.get_stars_rate())}₽/⭐"
|
||||
)
|
||||
return {
|
||||
"message_id": message.message_id,
|
||||
|
||||
Vendored
+99
-21
@@ -25,12 +25,20 @@ class WebhookServer:
|
||||
self.app = web.Application()
|
||||
|
||||
self.app.router.add_post(settings.TRIBUTE_WEBHOOK_PATH, self._tribute_webhook_handler)
|
||||
|
||||
if settings.is_cryptobot_enabled():
|
||||
self.app.router.add_post(settings.CRYPTOBOT_WEBHOOK_PATH, self._cryptobot_webhook_handler)
|
||||
|
||||
self.app.router.add_get('/health', self._health_check)
|
||||
|
||||
self.app.router.add_options(settings.TRIBUTE_WEBHOOK_PATH, self._options_handler)
|
||||
if settings.is_cryptobot_enabled():
|
||||
self.app.router.add_options(settings.CRYPTOBOT_WEBHOOK_PATH, self._options_handler)
|
||||
|
||||
logger.info(f"Webhook сервер настроен:")
|
||||
logger.info(f" - Tribute webhook: POST {settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
if settings.is_cryptobot_enabled():
|
||||
logger.info(f" - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}")
|
||||
logger.info(f" - Health check: GET /health")
|
||||
|
||||
return self.app
|
||||
@@ -52,11 +60,13 @@ class WebhookServer:
|
||||
|
||||
await self.site.start()
|
||||
|
||||
logger.info(f"✅ Tribute webhook сервер запущен на порту {settings.TRIBUTE_WEBHOOK_PORT}")
|
||||
logger.info(f"🎯 Tribute webhook URL: http://0.0.0.0:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
logger.info(f"Webhook сервер запущен на порту {settings.TRIBUTE_WEBHOOK_PORT}")
|
||||
logger.info(f"Tribute webhook URL: http://0.0.0.0:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
if settings.is_cryptobot_enabled():
|
||||
logger.info(f"CryptoBot webhook URL: http://0.0.0.0:{settings.TRIBUTE_WEBHOOK_PORT}{settings.CRYPTOBOT_WEBHOOK_PATH}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка запуска Tribute webhook сервера: {e}")
|
||||
logger.error(f"Ошибка запуска webhook сервера: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
@@ -64,14 +74,14 @@ class WebhookServer:
|
||||
try:
|
||||
if self.site:
|
||||
await self.site.stop()
|
||||
logger.info("Tribute webhook сайт остановлен")
|
||||
logger.info("Webhook сайт остановлен")
|
||||
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
logger.info("Tribute webhook runner очищен")
|
||||
logger.info("Webhook runner очищен")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки Tribute webhook сервера: {e}")
|
||||
logger.error(f"Ошибка остановки webhook сервера: {e}")
|
||||
|
||||
async def _options_handler(self, request: web.Request) -> web.Response:
|
||||
return web.Response(
|
||||
@@ -79,43 +89,43 @@ class WebhookServer:
|
||||
headers={
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, trbt-signature',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, trbt-signature, Crypto-Pay-API-Signature',
|
||||
}
|
||||
)
|
||||
|
||||
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
|
||||
|
||||
try:
|
||||
logger.info(f"📥 Получен Tribute webhook: {request.method} {request.path}")
|
||||
logger.info(f"📋 Headers: {dict(request.headers)}")
|
||||
logger.info(f"Получен Tribute webhook: {request.method} {request.path}")
|
||||
logger.info(f"Headers: {dict(request.headers)}")
|
||||
|
||||
raw_body = await request.read()
|
||||
|
||||
if not raw_body:
|
||||
logger.warning("⚠️ Получен пустой webhook от Tribute")
|
||||
logger.warning("Получен пустой webhook от Tribute")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "empty_body"},
|
||||
status=400
|
||||
)
|
||||
|
||||
payload = raw_body.decode('utf-8')
|
||||
logger.info(f"📄 Payload: {payload}")
|
||||
logger.info(f"Payload: {payload}")
|
||||
|
||||
try:
|
||||
webhook_data = json.loads(payload)
|
||||
logger.info(f"📊 Распарсенные данные: {webhook_data}")
|
||||
logger.info(f"Распарсенные данные: {webhook_data}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"❌ Ошибка парсинга JSON: {e}")
|
||||
logger.error(f"Ошибка парсинга JSON: {e}")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "invalid_json"},
|
||||
status=400
|
||||
)
|
||||
|
||||
signature = request.headers.get('trbt-signature')
|
||||
logger.info(f"🔐 Signature: {signature}")
|
||||
logger.info(f"Signature: {signature}")
|
||||
|
||||
if not signature:
|
||||
logger.error("❌ Отсутствует заголовок подписи Tribute webhook")
|
||||
logger.error("Отсутствует заголовок подписи Tribute webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "missing_signature"},
|
||||
status=401
|
||||
@@ -125,7 +135,7 @@ class WebhookServer:
|
||||
from app.external.tribute import TributeService as TributeAPI
|
||||
tribute_api = TributeAPI()
|
||||
if not tribute_api.verify_webhook_signature(payload, signature):
|
||||
logger.error("❌ Неверная подпись Tribute webhook")
|
||||
logger.error("Неверная подпись Tribute webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "invalid_signature"},
|
||||
status=401
|
||||
@@ -134,17 +144,83 @@ class WebhookServer:
|
||||
result = await self.tribute_service.process_webhook(payload)
|
||||
|
||||
if result:
|
||||
logger.info(f"✅ Tribute webhook обработан успешно: {result}")
|
||||
logger.info(f"Tribute webhook обработан успешно: {result}")
|
||||
return web.json_response({"status": "ok", "result": result}, status=200)
|
||||
else:
|
||||
logger.error("❌ Ошибка обработки Tribute webhook")
|
||||
logger.error("Ошибка обработки Tribute webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "processing_failed"},
|
||||
status=400
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Критическая ошибка обработки Tribute webhook: {e}", exc_info=True)
|
||||
logger.error(f"Критическая ошибка обработки Tribute webhook: {e}", exc_info=True)
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "internal_error", "message": str(e)},
|
||||
status=500
|
||||
)
|
||||
|
||||
async def _cryptobot_webhook_handler(self, request: web.Request) -> web.Response:
|
||||
|
||||
try:
|
||||
logger.info(f"Получен CryptoBot webhook: {request.method} {request.path}")
|
||||
logger.info(f"Headers: {dict(request.headers)}")
|
||||
|
||||
raw_body = await request.read()
|
||||
|
||||
if not raw_body:
|
||||
logger.warning("Получен пустой CryptoBot webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "empty_body"},
|
||||
status=400
|
||||
)
|
||||
|
||||
payload = raw_body.decode('utf-8')
|
||||
logger.info(f"CryptoBot Payload: {payload}")
|
||||
|
||||
try:
|
||||
webhook_data = json.loads(payload)
|
||||
logger.info(f"CryptoBot данные: {webhook_data}")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Ошибка парсинга CryptoBot JSON: {e}")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "invalid_json"},
|
||||
status=400
|
||||
)
|
||||
|
||||
signature = request.headers.get('Crypto-Pay-API-Signature')
|
||||
logger.info(f"CryptoBot Signature: {signature}")
|
||||
|
||||
if signature and settings.CRYPTOBOT_WEBHOOK_SECRET:
|
||||
from app.external.cryptobot import CryptoBotService
|
||||
cryptobot_service = CryptoBotService()
|
||||
if not cryptobot_service.verify_webhook_signature(payload, signature):
|
||||
logger.error("Неверная подпись CryptoBot webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "invalid_signature"},
|
||||
status=401
|
||||
)
|
||||
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.database.database import AsyncSessionLocal
|
||||
|
||||
payment_service = PaymentService(self.bot)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await payment_service.process_cryptobot_webhook(db, webhook_data)
|
||||
|
||||
if result:
|
||||
logger.info(f"CryptoBot webhook обработан успешно")
|
||||
return web.json_response({"status": "ok"}, status=200)
|
||||
else:
|
||||
logger.error("Ошибка обработки CryptoBot webhook")
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "processing_failed"},
|
||||
status=400
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Критическая ошибка обработки CryptoBot webhook: {e}", exc_info=True)
|
||||
return web.json_response(
|
||||
{"status": "error", "reason": "internal_error", "message": str(e)},
|
||||
status=500
|
||||
@@ -154,8 +230,10 @@ class WebhookServer:
|
||||
|
||||
return web.json_response({
|
||||
"status": "ok",
|
||||
"service": "tribute-webhooks",
|
||||
"service": "payment-webhooks",
|
||||
"tribute_enabled": settings.TRIBUTE_ENABLED,
|
||||
"cryptobot_enabled": settings.is_cryptobot_enabled(),
|
||||
"port": settings.TRIBUTE_WEBHOOK_PORT,
|
||||
"path": settings.TRIBUTE_WEBHOOK_PATH
|
||||
"tribute_path": settings.TRIBUTE_WEBHOOK_PATH,
|
||||
"cryptobot_path": settings.CRYPTOBOT_WEBHOOK_PATH if settings.is_cryptobot_enabled() else None
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ async def show_servers_menu(
|
||||
• С подключениями: {stats['servers_with_connections']}
|
||||
|
||||
💰 <b>Выручка от серверов:</b>
|
||||
• Общая: {stats['total_revenue_rubles']:.2f} ₽
|
||||
• Общая: {int(stats['total_revenue_rubles'])} ₽
|
||||
|
||||
Выберите действие:
|
||||
"""
|
||||
@@ -83,7 +83,7 @@ async def show_servers_list(
|
||||
|
||||
for i, server in enumerate(servers, 1 + (page - 1) * 10):
|
||||
status_emoji = "✅" if server.is_available else "❌"
|
||||
price_text = f"{server.price_rubles:.2f} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
price_text = f"{int(server.price_rubles)} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
|
||||
text += f"{i}. {status_emoji} {server.display_name}\n"
|
||||
text += f" 💰 Цена: {price_text}"
|
||||
@@ -222,7 +222,7 @@ async def show_server_edit_menu(
|
||||
return
|
||||
|
||||
status_emoji = "✅ Доступен" if server.is_available else "❌ Недоступен"
|
||||
price_text = f"{server.price_rubles:.2f} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
price_text = f"{int(server.price_rubles)} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
|
||||
text = f"""
|
||||
🌐 <b>Редактирование сервера</b>
|
||||
@@ -304,7 +304,7 @@ async def toggle_server_availability(
|
||||
server = await get_server_squad_by_id(db, server_id)
|
||||
|
||||
status_emoji = "✅ Доступен" if server.is_available else "❌ Недоступен"
|
||||
price_text = f"{server.price_rubles:.2f} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
price_text = f"{int(server.price_rubles)} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
|
||||
text = f"""
|
||||
🌐 <b>Редактирование сервера</b>
|
||||
@@ -378,7 +378,7 @@ async def start_server_edit_price(
|
||||
await state.set_data({'server_id': server_id})
|
||||
await state.set_state(AdminStates.editing_server_price)
|
||||
|
||||
current_price = f"{server.price_rubles:.2f} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
current_price = f"{int(server.price_rubles)} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"💰 <b>Редактирование цены</b>\n\n"
|
||||
@@ -424,7 +424,7 @@ async def process_server_price_edit(
|
||||
|
||||
await cache.delete("available_countries")
|
||||
|
||||
price_text = f"{price_rubles:.2f} ₽" if price_kopeks > 0 else "Бесплатно"
|
||||
price_text = f"{int(price_rubles)} ₽" if price_kopeks > 0 else "Бесплатно"
|
||||
await message.answer(
|
||||
f"✅ Цена сервера изменена на: <b>{price_text}</b>",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
@@ -613,8 +613,8 @@ async def show_server_detailed_stats(
|
||||
• С активными подключениями: {stats['servers_with_connections']}
|
||||
|
||||
<b>💰 Финансовая статистика:</b>
|
||||
• Общая выручка: {stats['total_revenue_rubles']:.2f} ₽
|
||||
• Средняя цена за сервер: {(stats['total_revenue_rubles'] / max(stats['servers_with_connections'], 1)):.2f} ₽
|
||||
• Общая выручка: {int(stats['total_revenue_rubles'])} ₽
|
||||
• Средняя цена за сервер: {int(stats['total_revenue_rubles'] / max(stats['servers_with_connections'], 1))} ₽
|
||||
|
||||
<b>🔥 Топ серверов по цене:</b>
|
||||
"""
|
||||
@@ -622,7 +622,7 @@ async def show_server_detailed_stats(
|
||||
sorted_servers = sorted(available_servers, key=lambda x: x.price_kopeks, reverse=True)
|
||||
|
||||
for i, server in enumerate(sorted_servers[:5], 1):
|
||||
price_text = f"{server.price_rubles:.2f} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
price_text = f"{int(server.price_rubles)} ₽" if server.price_kopeks > 0 else "Бесплатно"
|
||||
text += f"{i}. {server.display_name} - {price_text}\n"
|
||||
|
||||
if not sorted_servers:
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import logging
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User
|
||||
from app.services.version_service import version_service
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_updates_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔄 Проверить обновления",
|
||||
callback_data="admin_updates_check"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📋 Информация о версии",
|
||||
callback_data="admin_updates_info"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔗 Открыть репозиторий",
|
||||
url=f"https://github.com/{version_service.repo}/releases"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="◀️ Назад",
|
||||
callback_data="admin_panel"
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
def get_version_info_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🔄 Обновить",
|
||||
callback_data="admin_updates_info"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="◀️ К обновлениям",
|
||||
callback_data="admin_updates"
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_updates_menu(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
version_info = await version_service.get_version_info()
|
||||
|
||||
current_version = version_info['current_version']
|
||||
has_updates = version_info['has_updates']
|
||||
total_newer = version_info['total_newer']
|
||||
last_check = version_info['last_check']
|
||||
|
||||
status_icon = "🆕" if has_updates else "✅"
|
||||
status_text = f"Доступно {total_newer} обновлений" if has_updates else "Актуальная версия"
|
||||
|
||||
last_check_text = ""
|
||||
if last_check:
|
||||
last_check_text = f"\n🕐 Последняя проверка: {last_check.strftime('%d.%m.%Y %H:%M')}"
|
||||
|
||||
message = f"""🔄 <b>СИСТЕМА ОБНОВЛЕНИЙ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{current_version}</code>
|
||||
{status_icon} <b>Статус:</b> {status_text}
|
||||
|
||||
🔗 <b>Репозиторий:</b> {version_service.repo}{last_check_text}
|
||||
|
||||
ℹ️ Система автоматически проверяет обновления каждый час и отправляет уведомления о новых версиях."""
|
||||
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=get_updates_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка показа меню обновлений: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки меню обновлений", show_alert=True)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def check_updates(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
await callback.answer("🔄 Проверяю обновления...")
|
||||
|
||||
try:
|
||||
has_updates, newer_releases = await version_service.check_for_updates(force=True)
|
||||
|
||||
if not has_updates:
|
||||
message = f"""✅ <b>ОБНОВЛЕНИЯ НЕ НАЙДЕНЫ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>
|
||||
🎯 <b>Статус:</b> У вас установлена последняя версия
|
||||
|
||||
🔗 <b>Репозиторий:</b> {version_service.repo}"""
|
||||
|
||||
else:
|
||||
updates_list = []
|
||||
for i, release in enumerate(newer_releases[:5]):
|
||||
icon = version_service.format_version_display(release).split()[0]
|
||||
updates_list.append(
|
||||
f"{i+1}. {icon} <code>{release.tag_name}</code> • {release.formatted_date}"
|
||||
)
|
||||
|
||||
updates_text = "\n".join(updates_list)
|
||||
more_text = f"\n\n📋 И еще {len(newer_releases) - 5} обновлений..." if len(newer_releases) > 5 else ""
|
||||
|
||||
message = f"""🆕 <b>НАЙДЕНЫ ОБНОВЛЕНИЯ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>
|
||||
🎯 <b>Доступно обновлений:</b> {len(newer_releases)}
|
||||
|
||||
📋 <b>Последние версии:</b>
|
||||
{updates_text}{more_text}
|
||||
|
||||
🔗 <b>Репозиторий:</b> {version_service.repo}"""
|
||||
|
||||
keyboard = get_updates_keyboard(db_user.language)
|
||||
|
||||
if has_updates:
|
||||
keyboard.inline_keyboard.insert(-2, [
|
||||
InlineKeyboardButton(
|
||||
text="📋 Подробнее о версиях",
|
||||
callback_data="admin_updates_info"
|
||||
)
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
message,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки обновлений: {e}")
|
||||
await callback.message.edit_text(
|
||||
f"❌ <b>ОШИБКА ПРОВЕРКИ ОБНОВЛЕНИЙ</b>\n\n"
|
||||
f"Не удалось связаться с сервером GitHub.\n"
|
||||
f"Попробуйте позже.\n\n"
|
||||
f"📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>",
|
||||
reply_markup=get_updates_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_version_info(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
await callback.answer("📋 Загружаю информацию о версиях...")
|
||||
|
||||
try:
|
||||
version_info = await version_service.get_version_info()
|
||||
|
||||
current_version = version_info['current_version']
|
||||
current_release = version_info['current_release']
|
||||
newer_releases = version_info['newer_releases']
|
||||
has_updates = version_info['has_updates']
|
||||
last_check = version_info['last_check']
|
||||
repo_url = version_info['repo_url']
|
||||
|
||||
current_info = f"📦 <b>ТЕКУЩАЯ ВЕРСИЯ</b>\n\n"
|
||||
|
||||
if current_release:
|
||||
current_info += f"🏷️ <b>Версия:</b> <code>{current_release.tag_name}</code>\n"
|
||||
current_info += f"📅 <b>Дата релиза:</b> {current_release.formatted_date}\n"
|
||||
if current_release.short_description:
|
||||
current_info += f"📝 <b>Описание:</b>\n{current_release.short_description}\n"
|
||||
else:
|
||||
current_info += f"🏷️ <b>Версия:</b> <code>{current_version}</code>\n"
|
||||
current_info += f"ℹ️ <b>Статус:</b> Информация о релизе недоступна\n"
|
||||
|
||||
message_parts = [current_info]
|
||||
|
||||
if has_updates and newer_releases:
|
||||
updates_info = f"\n🆕 <b>ДОСТУПНЫЕ ОБНОВЛЕНИЯ</b>\n\n"
|
||||
|
||||
for i, release in enumerate(newer_releases):
|
||||
icon = "🔥" if i == 0 else "📦"
|
||||
if release.prerelease:
|
||||
icon = "🧪"
|
||||
elif release.is_dev:
|
||||
icon = "🔧"
|
||||
|
||||
updates_info += f"{icon} <b>{release.tag_name}</b>\n"
|
||||
updates_info += f" 📅 {release.formatted_date}\n"
|
||||
if release.short_description:
|
||||
updates_info += f" 📝 {release.short_description}\n"
|
||||
updates_info += "\n"
|
||||
|
||||
message_parts.append(updates_info.rstrip())
|
||||
|
||||
system_info = f"\n🔧 <b>СИСТЕМА ОБНОВЛЕНИЙ</b>\n\n"
|
||||
system_info += f"🔗 <b>Репозиторий:</b> {version_service.repo}\n"
|
||||
system_info += f"⚡ <b>Автопроверка:</b> {'Включена' if version_service.enabled else 'Отключена'}\n"
|
||||
system_info += f"🕐 <b>Интервал:</b> Каждый час\n"
|
||||
|
||||
if last_check:
|
||||
system_info += f"🕐 <b>Последняя проверка:</b> {last_check.strftime('%d.%m.%Y %H:%M')}\n"
|
||||
|
||||
message_parts.append(system_info.rstrip())
|
||||
|
||||
final_message = "\n".join(message_parts)
|
||||
|
||||
if len(final_message) > 4000:
|
||||
final_message = final_message[:3900] + "\n\n... (информация обрезана)"
|
||||
|
||||
await callback.message.edit_text(
|
||||
final_message,
|
||||
reply_markup=get_version_info_keyboard(db_user.language),
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения информации о версиях: {e}")
|
||||
await callback.message.edit_text(
|
||||
f"❌ <b>ОШИБКА ЗАГРУЗКИ</b>\n\n"
|
||||
f"Не удалось получить информацию о версиях.\n\n"
|
||||
f"📦 <b>Текущая версия:</b> <code>{version_service.current_version}</code>",
|
||||
reply_markup=get_version_info_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
show_updates_menu,
|
||||
F.data == "admin_updates"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
check_updates,
|
||||
F.data == "admin_updates_check"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_version_info,
|
||||
F.data == "admin_updates_info"
|
||||
)
|
||||
@@ -636,9 +636,9 @@ async def process_balance_edit(
|
||||
|
||||
description = f"Изменение баланса администратором {db_user.full_name}"
|
||||
if amount_kopeks > 0:
|
||||
description = f"Пополнение администратором: +{amount_rubles} ₽"
|
||||
description = f"Пополнение администратором: +{int(amount_rubles)} ₽"
|
||||
else:
|
||||
description = f"Списание администратором: {amount_rubles} ₽"
|
||||
description = f"Списание администратором: {int(amount_rubles)} ₽"
|
||||
|
||||
success = await user_service.update_user_balance(
|
||||
db, user_id, amount_kopeks, description, db_user.id
|
||||
|
||||
+213
-2
@@ -337,6 +337,10 @@ async def process_topup_amount(
|
||||
from app.database.database import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as db:
|
||||
await process_yookassa_payment_amount(message, db_user, db, amount_kopeks, state)
|
||||
elif payment_method == "cryptobot":
|
||||
from app.database.database import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as db:
|
||||
await process_cryptobot_payment_amount(message, db_user, db, amount_kopeks, state)
|
||||
else:
|
||||
await message.answer("Неизвестный способ оплаты")
|
||||
|
||||
@@ -381,7 +385,7 @@ async def process_stars_payment_amount(
|
||||
f"⭐ <b>Оплата через Telegram Stars</b>\n\n"
|
||||
f"💰 Сумма: {texts.format_price(amount_kopeks)}\n"
|
||||
f"⭐ К оплате: {stars_amount} звезд\n"
|
||||
f"📊 Курс: {settings.get_stars_rate():.2f}₽ за звезду\n\n"
|
||||
f"📊 Курс: {int(settings.get_stars_rate())}₽ за звезду\n\n"
|
||||
f"Нажмите кнопку ниже для оплаты:",
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
@@ -465,7 +469,7 @@ async def process_yookassa_payment_amount(
|
||||
await state.clear()
|
||||
|
||||
logger.info(f"Создан платеж YooKassa для пользователя {db_user.telegram_id}: "
|
||||
f"{amount_kopeks/100}₽, ID: {payment_result['yookassa_payment_id']}")
|
||||
f"{amount_kopeks//100}₽, ID: {payment_result['yookassa_payment_id']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания YooKassa платежа: {e}")
|
||||
@@ -526,6 +530,203 @@ async def check_yookassa_payment_status(
|
||||
logger.error(f"Ошибка проверки статуса платежа: {e}")
|
||||
await callback.answer("❌ Ошибка проверки статуса", show_alert=True)
|
||||
|
||||
@error_handler
|
||||
async def start_cryptobot_payment(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_cryptobot_enabled():
|
||||
await callback.answer("❌ Оплата криптовалютой временно недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
from app.utils.currency_converter import currency_converter
|
||||
try:
|
||||
current_rate = await currency_converter.get_usd_to_rub_rate()
|
||||
rate_text = f"💱 Текущий курс: 1 USD = {current_rate:.2f} ₽"
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось получить курс валют: {e}")
|
||||
current_rate = 95.0
|
||||
rate_text = f"💱 Курс: 1 USD ≈ {current_rate:.0f} ₽"
|
||||
|
||||
available_assets = settings.get_cryptobot_assets()
|
||||
assets_text = ", ".join(available_assets)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"🪙 <b>Пополнение криптовалютой</b>\n\n"
|
||||
f"Введите сумму для пополнения в рублях от 100 до 100,000 ₽:\n\n"
|
||||
f"💰 Доступные активы: {assets_text}\n"
|
||||
f"⚡ Мгновенное зачисление на баланс\n"
|
||||
f"🔒 Безопасная оплата через CryptoBot\n\n"
|
||||
f"{rate_text}\n"
|
||||
f"Сумма будет автоматически конвертирована в USD для оплаты.",
|
||||
reply_markup=get_back_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.set_state(BalanceStates.waiting_for_amount)
|
||||
await state.update_data(payment_method="cryptobot", current_rate=current_rate)
|
||||
await callback.answer()
|
||||
|
||||
@error_handler
|
||||
async def process_cryptobot_payment_amount(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
amount_kopeks: int,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not settings.is_cryptobot_enabled():
|
||||
await message.answer("❌ Оплата криптовалютой временно недоступна")
|
||||
return
|
||||
|
||||
amount_rubles = amount_kopeks / 100
|
||||
|
||||
if amount_rubles < 100:
|
||||
await message.answer("Минимальная сумма пополнения: 100 ₽")
|
||||
return
|
||||
|
||||
if amount_rubles > 100000:
|
||||
await message.answer("Максимальная сумма пополнения: 100,000 ₽")
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем курс из состояния или запрашиваем заново
|
||||
data = await state.get_data()
|
||||
current_rate = data.get('current_rate')
|
||||
|
||||
if not current_rate:
|
||||
from app.utils.currency_converter import currency_converter
|
||||
current_rate = await currency_converter.get_usd_to_rub_rate()
|
||||
|
||||
# Конвертируем рубли в доллары
|
||||
amount_usd = amount_rubles / current_rate
|
||||
|
||||
# Округляем до 2 знаков после запятой
|
||||
amount_usd = round(amount_usd, 2)
|
||||
|
||||
if amount_usd < 1:
|
||||
await message.answer("❌ Минимальная сумма для оплаты в USD: 1.00 USD")
|
||||
return
|
||||
|
||||
if amount_usd > 1000:
|
||||
await message.answer("❌ Максимальная сумма для оплаты в USD: 1,000 USD")
|
||||
return
|
||||
|
||||
payment_service = PaymentService(message.bot)
|
||||
|
||||
payment_result = await payment_service.create_cryptobot_payment(
|
||||
db=db,
|
||||
user_id=db_user.id,
|
||||
amount_usd=amount_usd,
|
||||
asset=settings.CRYPTOBOT_DEFAULT_ASSET,
|
||||
description=f"Пополнение баланса на {amount_rubles:.0f} ₽ ({amount_usd:.2f} USD)",
|
||||
payload=f"balance_{db_user.id}_{amount_kopeks}"
|
||||
)
|
||||
|
||||
if not payment_result:
|
||||
await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
bot_invoice_url = payment_result.get("bot_invoice_url")
|
||||
mini_app_invoice_url = payment_result.get("mini_app_invoice_url")
|
||||
|
||||
payment_url = bot_invoice_url or mini_app_invoice_url
|
||||
|
||||
if not payment_url:
|
||||
await message.answer("❌ Ошибка получения ссылки для оплаты. Обратитесь в поддержку.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🪙 Оплатить", url=payment_url)],
|
||||
[types.InlineKeyboardButton(text="📊 Проверить статус", callback_data=f"check_cryptobot_{payment_result['local_payment_id']}")],
|
||||
[types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")]
|
||||
])
|
||||
|
||||
await message.answer(
|
||||
f"🪙 <b>Оплата криптовалютой</b>\n\n"
|
||||
f"💰 Сумма к зачислению: {amount_rubles:.0f} ₽\n"
|
||||
f"💵 К оплате: {amount_usd:.2f} USD\n"
|
||||
f"🪙 Актив: {payment_result['asset']}\n"
|
||||
f"💱 Курс: 1 USD = {current_rate:.2f} ₽\n"
|
||||
f"🆔 ID платежа: {payment_result['invoice_id'][:8]}...\n\n"
|
||||
f"📱 <b>Инструкция:</b>\n"
|
||||
f"1. Нажмите кнопку 'Оплатить'\n"
|
||||
f"2. Выберите удобный актив\n"
|
||||
f"3. Переведите указанную сумму\n"
|
||||
f"4. Деньги поступят на баланс автоматически\n\n"
|
||||
f"🔒 Оплата проходит через защищенную систему CryptoBot\n"
|
||||
f"⚡ Поддерживаемые активы: USDT, TON, BTC, ETH\n\n"
|
||||
f"❓ Если возникнут проблемы, обратитесь в {settings.SUPPORT_USERNAME}",
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
logger.info(f"Создан CryptoBot платеж для пользователя {db_user.telegram_id}: "
|
||||
f"{amount_rubles:.0f} ₽ ({amount_usd:.2f} USD), ID: {payment_result['invoice_id']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания CryptoBot платежа: {e}")
|
||||
await message.answer("❌ Ошибка создания платежа. Попробуйте позже или обратитесь в поддержку.")
|
||||
await state.clear()
|
||||
|
||||
@error_handler
|
||||
async def check_cryptobot_payment_status(
|
||||
callback: types.CallbackQuery,
|
||||
db: AsyncSession
|
||||
):
|
||||
try:
|
||||
local_payment_id = int(callback.data.split('_')[-1])
|
||||
|
||||
from app.database.crud.cryptobot import get_cryptobot_payment_by_id
|
||||
payment = await get_cryptobot_payment_by_id(db, local_payment_id)
|
||||
|
||||
if not payment:
|
||||
await callback.answer("❌ Платеж не найден", show_alert=True)
|
||||
return
|
||||
|
||||
status_emoji = {
|
||||
"active": "⏳",
|
||||
"paid": "✅",
|
||||
"expired": "❌"
|
||||
}
|
||||
|
||||
status_text = {
|
||||
"active": "Ожидает оплаты",
|
||||
"paid": "Оплачен",
|
||||
"expired": "Истек"
|
||||
}
|
||||
|
||||
emoji = status_emoji.get(payment.status, "❓")
|
||||
status = status_text.get(payment.status, "Неизвестно")
|
||||
|
||||
message_text = (f"🪙 Статус платежа:\n\n"
|
||||
f"🆔 ID: {payment.invoice_id[:8]}...\n"
|
||||
f"💰 Сумма: {payment.amount} {payment.asset}\n"
|
||||
f"📊 Статус: {emoji} {status}\n"
|
||||
f"📅 Создан: {payment.created_at.strftime('%d.%m.%Y %H:%M')}\n")
|
||||
|
||||
if payment.is_paid:
|
||||
message_text += "\n✅ Платеж успешно завершен!\n\nСредства зачислены на баланс."
|
||||
elif payment.is_pending:
|
||||
message_text += "\n⏳ Платеж ожидает оплаты. Нажмите кнопку 'Оплатить' выше."
|
||||
elif payment.is_expired:
|
||||
message_text += f"\n❌ Платеж истек. Обратитесь в {settings.SUPPORT_USERNAME}"
|
||||
|
||||
await callback.answer(message_text, show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки статуса CryptoBot платежа: {e}")
|
||||
await callback.answer("❌ Ошибка проверки статуса", show_alert=True)
|
||||
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
@@ -579,3 +780,13 @@ def register_handlers(dp: Dispatcher):
|
||||
process_topup_amount,
|
||||
BalanceStates.waiting_for_amount
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
start_cryptobot_payment,
|
||||
F.data == "topup_cryptobot"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
check_cryptobot_payment_status,
|
||||
F.data.startswith("check_cryptobot_")
|
||||
)
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import logging
|
||||
from aiogram import Dispatcher, types, F
|
||||
from pathlib import Path
|
||||
|
||||
import qrcode
|
||||
from aiogram import Dispatcher, F, types
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.types import FSInputFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User
|
||||
from app.keyboards.inline import get_referral_keyboard, get_back_keyboard
|
||||
from app.keyboards.inline import get_referral_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
from app.utils.user_utils import get_user_referral_summary, get_detailed_referral_list, get_referral_analytics
|
||||
from app.utils.user_utils import (
|
||||
get_detailed_referral_list,
|
||||
get_referral_analytics,
|
||||
get_user_referral_summary,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -82,11 +91,59 @@ async def show_referral_info(
|
||||
|
||||
referral_text += "📢 Приглашайте друзей и зарабатывайте!"
|
||||
|
||||
await callback.message.edit_text(
|
||||
referral_text,
|
||||
reply_markup=get_referral_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
if callback.message.text:
|
||||
await callback.message.edit_text(
|
||||
referral_text,
|
||||
reply_markup=get_referral_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
else:
|
||||
await callback.message.delete()
|
||||
await callback.message.answer(
|
||||
referral_text,
|
||||
reply_markup=get_referral_keyboard(db_user.language),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def show_referral_qr(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
bot_username = (await callback.bot.get_me()).username
|
||||
referral_link = f"https://t.me/{bot_username}?start={db_user.referral_code}"
|
||||
|
||||
qr_dir = Path("data") / "referral_qr"
|
||||
qr_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_path = qr_dir / f"{db_user.id}.png"
|
||||
if not file_path.exists():
|
||||
img = qrcode.make(referral_link)
|
||||
img.save(file_path)
|
||||
|
||||
photo = FSInputFile(file_path)
|
||||
keyboard = types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="menu_referrals")]]
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.edit_media(
|
||||
types.InputMediaPhoto(
|
||||
media=photo,
|
||||
caption=f"🔗 Ваша реферальная ссылка:\n{referral_link}",
|
||||
),
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.delete()
|
||||
await callback.message.answer_photo(
|
||||
photo,
|
||||
caption=f"🔗 Ваша реферальная ссылка:\n{referral_link}",
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -243,6 +300,11 @@ def register_handlers(dp: Dispatcher):
|
||||
create_invite_message,
|
||||
F.data == "referral_create_invite"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_referral_qr,
|
||||
F.data == "referral_show_qr"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
show_detailed_referral_list,
|
||||
|
||||
@@ -92,7 +92,7 @@ async def handle_successful_payment(
|
||||
await message.answer(
|
||||
f"🎉 <b>Платеж успешно обработан!</b>\n\n"
|
||||
f"⭐ Потрачено звезд: {payment.total_amount}\n"
|
||||
f"💰 Зачислено на баланс: {rubles_amount:.2f} ₽\n"
|
||||
f"💰 Зачислено на баланс: {int(rubles_amount)} ₽\n"
|
||||
f"🆔 ID транзакции: {payment.telegram_payment_charge_id[:8]}...\n\n"
|
||||
f"Спасибо за пополнение! 🚀",
|
||||
parse_mode="HTML"
|
||||
@@ -100,7 +100,7 @@ async def handle_successful_payment(
|
||||
|
||||
logger.info(
|
||||
f"✅ Stars платеж успешно обработан: "
|
||||
f"пользователь {user.id}, {payment.total_amount} звезд → {rubles_amount:.2f}₽"
|
||||
f"пользователь {user.id}, {payment.total_amount} звезд → {int(rubles_amount)}₽"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}")
|
||||
|
||||
@@ -521,10 +521,10 @@ async def complete_registration_from_callback(
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке главного меню: {e}")
|
||||
try:
|
||||
balance_rubles = user.balance_kopeks / 100
|
||||
balance_rubles = user.balance_kopeks // 100
|
||||
await callback.message.answer(
|
||||
f"Добро пожаловать, {user.full_name}!\n"
|
||||
f"Баланс: {balance_rubles:.2f} ₽\n"
|
||||
f"Баланс: {balance_rubles} ₽\n"
|
||||
f"Подписка: Нет активной подписки",
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user.language,
|
||||
@@ -684,10 +684,10 @@ async def complete_registration(
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке главного меню: {e}")
|
||||
try:
|
||||
balance_rubles = user.balance_kopeks / 100
|
||||
balance_rubles = user.balance_kopeks // 100
|
||||
await message.answer(
|
||||
f"Добро пожаловать, {user.full_name}!\n"
|
||||
f"Баланс: {balance_rubles:.2f} ₽\n"
|
||||
f"Баланс: {balance_rubles} ₽\n"
|
||||
f"Подписка: Нет активной подписки",
|
||||
reply_markup=get_main_menu_keyboard(
|
||||
language=user.language,
|
||||
|
||||
@@ -34,7 +34,8 @@ from app.keyboards.inline import (
|
||||
get_manage_countries_keyboard,
|
||||
get_device_selection_keyboard, get_connection_guide_keyboard,
|
||||
get_app_selection_keyboard, get_specific_app_keyboard,
|
||||
get_subscription_settings_keyboard, get_extend_subscription_keyboard_with_prices
|
||||
get_subscription_settings_keyboard, get_extend_subscription_keyboard_with_prices,
|
||||
get_insufficient_balance_keyboard
|
||||
)
|
||||
from app.localization.texts import get_texts
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
@@ -1415,13 +1416,13 @@ async def get_traffic_packages_info() -> str:
|
||||
info_lines.append("\n✅ Активные:")
|
||||
for pkg in enabled_packages:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']/100}₽")
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']//100}₽")
|
||||
|
||||
if disabled_packages:
|
||||
info_lines.append("\n❌ Отключенные:")
|
||||
for pkg in disabled_packages:
|
||||
gb_text = "♾️ Безлимит" if pkg['gb'] == 0 else f"{pkg['gb']} ГБ"
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']/100}₽")
|
||||
info_lines.append(f" • {gb_text}: {pkg['price']//100}₽")
|
||||
|
||||
info_lines.append(f"\n📊 Всего пакетов: {len(packages)}")
|
||||
info_lines.append(f"🟢 Активных: {len(enabled_packages)}")
|
||||
@@ -1796,9 +1797,10 @@ async def confirm_purchase(
|
||||
logger.info(f" ИТОГО: {final_price/100}₽")
|
||||
|
||||
if db_user.balance_kopeks < final_price:
|
||||
missing_kopeks = final_price - db_user.balance_kopeks
|
||||
await callback.message.edit_text(
|
||||
texts.INSUFFICIENT_BALANCE,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
texts.INSUFFICIENT_BALANCE.format(amount=texts.format_price(missing_kopeks)),
|
||||
reply_markup=get_insufficient_balance_keyboard(db_user.language),
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
@@ -1810,9 +1812,10 @@ async def confirm_purchase(
|
||||
)
|
||||
|
||||
if not success:
|
||||
missing_kopeks = final_price - db_user.balance_kopeks
|
||||
await callback.message.edit_text(
|
||||
texts.INSUFFICIENT_BALANCE,
|
||||
reply_markup=get_back_keyboard(db_user.language)
|
||||
texts.INSUFFICIENT_BALANCE.format(amount=texts.format_price(missing_kopeks)),
|
||||
reply_markup=get_insufficient_balance_keyboard(db_user.language),
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
@@ -1918,9 +1921,9 @@ async def confirm_purchase(
|
||||
|
||||
if remnawave_user and hasattr(subscription, 'subscription_url') and subscription.subscription_url:
|
||||
success_text = f"{texts.SUBSCRIPTION_PURCHASED}\n\n"
|
||||
success_text += f"Ваша ссылка для импорта в VPN приложение:\n"
|
||||
success_text += f"🔗 <b>Ваша ссылка для импорта в VPN приложение:</b>\n"
|
||||
success_text += f"<code>{subscription.subscription_url}</code>\n\n"
|
||||
success_text += f"Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве"
|
||||
success_text += f"📱 Нажмите кнопку ниже, чтобы получить инструкцию по настройке VPN на вашем устройстве"
|
||||
|
||||
connect_mode = settings.CONNECT_BUTTON_MODE
|
||||
|
||||
@@ -1928,12 +1931,12 @@ async def confirm_purchase(
|
||||
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Подключиться",
|
||||
text="🔗 Подключиться",
|
||||
web_app=types.WebAppInfo(url=subscription.subscription_url),
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text="Моя подписка", callback_data="menu_subscription")],
|
||||
[InlineKeyboardButton(text="В главное меню", callback_data="back_to_menu")],
|
||||
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")],
|
||||
[InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")],
|
||||
])
|
||||
elif connect_mode == "miniapp_custom":
|
||||
if not settings.MINIAPP_CUSTOM_URL:
|
||||
@@ -1943,18 +1946,18 @@ async def confirm_purchase(
|
||||
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Подключиться",
|
||||
text="🔗 Подключиться",
|
||||
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL),
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text="Моя подписка", callback_data="menu_subscription")],
|
||||
[InlineKeyboardButton(text="В главное меню", callback_data="back_to_menu")],
|
||||
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")],
|
||||
[InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")],
|
||||
])
|
||||
else:
|
||||
connect_keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Подключиться", callback_data="subscription_connect")],
|
||||
[InlineKeyboardButton(text="Моя подписка", callback_data="menu_subscription")],
|
||||
[InlineKeyboardButton(text="В главное меню", callback_data="back_to_menu")],
|
||||
[InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")],
|
||||
[InlineKeyboardButton(text="📱 Моя подписка", callback_data="menu_subscription")],
|
||||
[InlineKeyboardButton(text="⬅️ В главное меню", callback_data="back_to_menu")],
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
|
||||
@@ -29,7 +29,8 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text="🔧 Техработы", callback_data="maintenance_panel")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📢 Сообщения в меню", callback_data="user_messages_panel")
|
||||
InlineKeyboardButton(text="📢 Сообщения в меню", callback_data="user_messages_panel"),
|
||||
InlineKeyboardButton(text="🔄 Обновления", callback_data="admin_updates")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
|
||||
+77
-20
@@ -104,6 +104,19 @@ def get_back_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
])
|
||||
|
||||
|
||||
def get_insufficient_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=texts.GO_TO_BALANCE_TOP_UP,
|
||||
callback_data="balance_topup",
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")],
|
||||
])
|
||||
|
||||
|
||||
def get_subscription_keyboard(
|
||||
language: str = "ru",
|
||||
has_subscription: bool = False,
|
||||
@@ -415,15 +428,7 @@ def get_balance_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> InlineKeyboardMarkup:
|
||||
texts = get_texts(language)
|
||||
keyboard = []
|
||||
|
||||
if settings.TELEGRAM_STARS_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
if settings.is_yookassa_enabled():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
@@ -435,11 +440,27 @@ def get_payment_methods_keyboard(amount_kopeks: int, language: str = "ru") -> In
|
||||
if settings.TRIBUTE_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="💎 Банковская карта (Tribute)",
|
||||
text="💳 Банковская карта (Tribute)",
|
||||
callback_data="topup_tribute"
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
if settings.is_cryptobot_enabled():
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🪙 Криптовалюта (CryptoBot)",
|
||||
callback_data="topup_cryptobot"
|
||||
)
|
||||
])
|
||||
|
||||
if settings.TELEGRAM_STARS_ENABLED:
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="⭐ Telegram Stars",
|
||||
callback_data="topup_stars"
|
||||
)
|
||||
])
|
||||
|
||||
keyboard.append([
|
||||
InlineKeyboardButton(
|
||||
text="🛠️ Через поддержку",
|
||||
@@ -533,6 +554,12 @@ def get_referral_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
callback_data="referral_create_invite"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📱 Показать QR код",
|
||||
callback_data="referral_show_qr"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="👥 Список рефералов",
|
||||
@@ -715,14 +742,14 @@ def get_add_traffic_keyboard(language: str = "ru", subscription_end_date: dateti
|
||||
|
||||
if gb == 0:
|
||||
if language == "ru":
|
||||
text = f"♾️ Безлимитный трафик - {total_price/100:.2f} ₽{period_text}"
|
||||
text = f"♾️ Безлимитный трафик - {total_price//100} ₽{period_text}"
|
||||
else:
|
||||
text = f"♾️ Unlimited traffic - {total_price/100:.2f} ₽{period_text}"
|
||||
text = f"♾️ Unlimited traffic - {total_price//100} ₽{period_text}"
|
||||
else:
|
||||
if language == "ru":
|
||||
text = f"📊 +{gb} ГБ трафика - {total_price/100:.2f} ₽{period_text}"
|
||||
text = f"📊 +{gb} ГБ трафика - {total_price//100} ₽{period_text}"
|
||||
else:
|
||||
text = f"📊 +{gb} GB traffic - {total_price/100:.2f} ₽{period_text}"
|
||||
text = f"📊 +{gb} GB traffic - {total_price//100} ₽{period_text}"
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text=text, callback_data=f"add_traffic_{gb}")
|
||||
@@ -761,9 +788,9 @@ def get_add_devices_keyboard(current_devices: int, language: str = "ru", subscri
|
||||
total_price = price_per_month * months_multiplier
|
||||
|
||||
if language == "ru":
|
||||
text = f"📱 +{count} устройство(а) (итого: {new_total}) - {total_price/100:.2f} ₽{period_text}"
|
||||
text = f"📱 +{count} устройство(а) (итого: {new_total}) - {total_price//100} ₽{period_text}"
|
||||
else:
|
||||
text = f"📱 +{count} device(s) (total: {new_total}) - {total_price/100:.2f} ₽{period_text}"
|
||||
text = f"📱 +{count} device(s) (total: {new_total}) - {total_price//100} ₽{period_text}"
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text=text, callback_data=f"add_devices_{count}")
|
||||
@@ -835,10 +862,10 @@ def get_manage_countries_keyboard(
|
||||
if uuid not in current_subscription_countries and uuid in selected:
|
||||
total_price = price_per_month * months_multiplier
|
||||
if months_multiplier > 1:
|
||||
price_text = f" ({price_per_month/100:.2f}₽/мес × {months_multiplier} = {total_price/100:.2f}₽)"
|
||||
price_text = f" ({price_per_month//100}₽/мес × {months_multiplier} = {total_price//100}₽)"
|
||||
logger.info(f"🔍 Сервер {name}: {price_per_month/100}₽/мес × {months_multiplier} мес = {total_price/100}₽")
|
||||
else:
|
||||
price_text = f" ({total_price/100:.2f}₽)"
|
||||
price_text = f" ({total_price//100}₽)"
|
||||
display_name = f"{icon} {name}{price_text}"
|
||||
else:
|
||||
display_name = f"{icon} {name}"
|
||||
@@ -851,7 +878,7 @@ def get_manage_countries_keyboard(
|
||||
])
|
||||
|
||||
if total_cost > 0:
|
||||
apply_text = f"✅ Применить изменения ({total_cost/100:.2f} ₽)"
|
||||
apply_text = f"✅ Применить изменения ({total_cost//100} ₽)"
|
||||
logger.info(f"🔍 Общая стоимость новых серверов: {total_cost/100}₽")
|
||||
else:
|
||||
apply_text = "✅ Применить изменения"
|
||||
@@ -1046,3 +1073,33 @@ def get_extend_subscription_keyboard_with_prices(language: str, prices: dict) ->
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_cryptobot_payment_keyboard(
|
||||
payment_id: str,
|
||||
local_payment_id: int,
|
||||
amount_usd: float,
|
||||
asset: str,
|
||||
bot_invoice_url: str,
|
||||
language: str = "ru"
|
||||
) -> InlineKeyboardMarkup:
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="🪙 Оплатить",
|
||||
url=bot_invoice_url
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="📊 Проверить статус",
|
||||
callback_data=f"check_cryptobot_{local_payment_id}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="💰 Мой баланс",
|
||||
callback_data="menu_balance"
|
||||
)
|
||||
]
|
||||
])
|
||||
|
||||
@@ -57,7 +57,7 @@ class Texts:
|
||||
|
||||
@staticmethod
|
||||
def format_price(kopeks: int) -> str:
|
||||
return f"{kopeks / 100:.2f} ₽"
|
||||
return f"{int(kopeks / 100)} ₽"
|
||||
|
||||
@staticmethod
|
||||
def format_traffic(gb: float) -> str:
|
||||
@@ -125,7 +125,7 @@ class RussianTexts(Texts):
|
||||
MENU_LANGUAGE = "🌐 Язык"
|
||||
MENU_ADMIN = "⚙️ Админ-панель"
|
||||
BALANCE_BUTTON = "💰 Баланс: {balance}"
|
||||
BALANCE_BUTTON_ZERO = "💰 Баланс: 0.00 ₽"
|
||||
BALANCE_BUTTON_ZERO = "💰 Баланс: 0 ₽"
|
||||
|
||||
SUBSCRIPTION_NONE = "❌ Нет активной подписки"
|
||||
SUBSCRIPTION_TRIAL = "🧪 Тестовая подписка"
|
||||
@@ -204,7 +204,11 @@ class RussianTexts(Texts):
|
||||
Подтвердить покупку?
|
||||
"""
|
||||
|
||||
INSUFFICIENT_BALANCE = "❌ Недостаточно средств на балансе. Пополните баланс и попробуйте снова."
|
||||
INSUFFICIENT_BALANCE = """❌ Недостаточно средств на балансе.
|
||||
|
||||
<b>Пополните баланс на {amount} и попробуйте снова.</b>
|
||||
"""
|
||||
GO_TO_BALANCE_TOP_UP = "💳 Перейти к пополнению баланса"
|
||||
SUBSCRIPTION_PURCHASED = "🎉 Подписка успешно приобретена!"
|
||||
|
||||
BALANCE_INFO = """
|
||||
@@ -443,10 +447,14 @@ To get started, select interface language:
|
||||
CONTINUE = "➡️ Continue"
|
||||
YES = "✅ Yes"
|
||||
NO = "❌ No"
|
||||
|
||||
|
||||
MENU_BALANCE = "💰 Balance"
|
||||
MENU_SUBSCRIPTION = "📱 Subscription"
|
||||
MENU_TRIAL = "🎁 Trial subscription"
|
||||
INSUFFICIENT_BALANCE = """❌ Insufficient balance. " \
|
||||
|
||||
Top up {amount} and try again."""
|
||||
GO_TO_BALANCE_TOP_UP = "💳 Go to balance top up"
|
||||
|
||||
|
||||
LANGUAGES = {
|
||||
|
||||
@@ -130,6 +130,78 @@ class AdminNotificationService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о покупке: {e}")
|
||||
return False
|
||||
|
||||
async def send_version_update_notification(
|
||||
self,
|
||||
current_version: str,
|
||||
latest_version,
|
||||
total_updates: int
|
||||
) -> bool:
|
||||
"""Отправляет уведомление о новых обновлениях"""
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
if latest_version.prerelease:
|
||||
update_type = "🧪 ПРЕДВАРИТЕЛЬНАЯ ВЕРСИЯ"
|
||||
type_icon = "🧪"
|
||||
elif latest_version.is_dev:
|
||||
update_type = "🔧 DEV ВЕРСИЯ"
|
||||
type_icon = "🔧"
|
||||
else:
|
||||
update_type = "📦 НОВАЯ ВЕРСИЯ"
|
||||
type_icon = "📦"
|
||||
|
||||
description = latest_version.short_description
|
||||
if len(description) > 200:
|
||||
description = description[:197] + "..."
|
||||
|
||||
message = f"""{type_icon} <b>{update_type} ДОСТУПНА</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{current_version}</code>
|
||||
🆕 <b>Новая версия:</b> <code>{latest_version.tag_name}</code>
|
||||
📅 <b>Дата релиза:</b> {latest_version.formatted_date}
|
||||
|
||||
📝 <b>Описание:</b>
|
||||
{description}
|
||||
|
||||
🔢 <b>Всего доступно обновлений:</b> {total_updates}
|
||||
🔗 <b>Репозиторий:</b> https://github.com/{getattr(self, 'repo', 'fr1ngg/remnawave-bedolaga-telegram-bot')}
|
||||
|
||||
ℹ️ Для обновления перезапустите контейнер с новым тегом или обновите код из репозитория.
|
||||
|
||||
⚙️ <i>Автоматическая проверка обновлений • {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об обновлении: {e}")
|
||||
return False
|
||||
|
||||
async def send_version_check_error_notification(
|
||||
self,
|
||||
error_message: str,
|
||||
current_version: str
|
||||
) -> bool:
|
||||
if not self._is_enabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
message = f"""⚠️ <b>ОШИБКА ПРОВЕРКИ ОБНОВЛЕНИЙ</b>
|
||||
|
||||
📦 <b>Текущая версия:</b> <code>{current_version}</code>
|
||||
❌ <b>Ошибка:</b> {error_message}
|
||||
|
||||
🔄 Следующая попытка через час.
|
||||
⚙️ Проверьте доступность GitHub API и настройки сети.
|
||||
|
||||
⚙️ <i>Система автоматических обновлений • {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}</i>"""
|
||||
|
||||
return await self._send_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об ошибке проверки версий: {e}")
|
||||
return False
|
||||
|
||||
async def send_balance_topup_notification(
|
||||
self,
|
||||
|
||||
@@ -14,6 +14,8 @@ from app.database.crud.yookassa import create_yookassa_payment, link_yookassa_pa
|
||||
from app.database.crud.transaction import create_transaction
|
||||
from app.database.crud.user import add_user_balance, get_user_by_id
|
||||
from app.database.models import TransactionType, PaymentMethod
|
||||
from app.external.cryptobot import CryptoBotService
|
||||
from app.utils.currency_converter import currency_converter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,6 +26,7 @@ class PaymentService:
|
||||
self.bot = bot
|
||||
self.yookassa_service = YooKassaService() if settings.is_yookassa_enabled() else None
|
||||
self.stars_service = TelegramStarsService(bot) if bot else None
|
||||
self.cryptobot_service = CryptoBotService() if settings.is_cryptobot_enabled() else None
|
||||
|
||||
async def create_stars_invoice(
|
||||
self,
|
||||
@@ -48,7 +51,7 @@ class PaymentService:
|
||||
prices=[LabeledPrice(label="Пополнение", amount=stars_amount)]
|
||||
)
|
||||
|
||||
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{amount_rubles:.2f}₽)")
|
||||
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽)")
|
||||
return invoice_link
|
||||
|
||||
except Exception as e:
|
||||
@@ -90,7 +93,7 @@ class PaymentService:
|
||||
|
||||
logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance} → {user.balance_kopeks} (изменение: +{amount_kopeks})")
|
||||
|
||||
description_for_referral = f"Пополнение Stars: {rubles_amount:.2f}₽ ({stars_amount} ⭐)"
|
||||
description_for_referral = f"Пополнение Stars: {int(rubles_amount)}₽ ({stars_amount} ⭐)"
|
||||
logger.info(f"🔍 Проверка реферальной логики для описания: '{description_for_referral}'")
|
||||
|
||||
if any(word in description_for_referral.lower() for word in ["пополнение", "stars", "yookassa", "topup"]) and not any(word in description_for_referral.lower() for word in ["комиссия", "бонус"]):
|
||||
@@ -125,13 +128,15 @@ class PaymentService:
|
||||
f"Баланс пополнен автоматически!",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {rubles_amount:.2f}₽")
|
||||
logger.info(
|
||||
f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {int(rubles_amount)}₽"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
|
||||
|
||||
logger.info(
|
||||
f"✅ Обработан Stars платеж: пользователь {user_id}, "
|
||||
f"{stars_amount} звезд → {rubles_amount:.2f}₽"
|
||||
f"{stars_amount} звезд → {int(rubles_amount)}₽"
|
||||
)
|
||||
return True
|
||||
else:
|
||||
@@ -313,7 +318,7 @@ class PaymentService:
|
||||
f"Баланс пополнен автоматически!",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {updated_payment.amount_kopeks/100:.2f}₽")
|
||||
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {updated_payment.amount_kopeks//100}₽")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении: {e}")
|
||||
else:
|
||||
@@ -352,7 +357,7 @@ class PaymentService:
|
||||
|
||||
user = await get_user_by_id(db, payment.user_id)
|
||||
if user:
|
||||
await add_user_balance(db, user, payment.amount_kopeks, f"Пополнение YooKassa: {payment.amount_kopeks/100:.2f}₽")
|
||||
await add_user_balance(db, user, payment.amount_kopeks, f"Пополнение YooKassa: {payment.amount_kopeks//100}₽")
|
||||
|
||||
logger.info(f"Успешно обработан платеж YooKassa {payment.yookassa_payment_id}: "
|
||||
f"пользователь {payment.user_id} получил {payment.amount_kopeks/100}₽")
|
||||
@@ -461,3 +466,200 @@ class PaymentService:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки платежа: {e}")
|
||||
return False
|
||||
|
||||
async def create_cryptobot_payment(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
amount_usd: float,
|
||||
asset: str = "USDT",
|
||||
description: str = "Пополнение баланса",
|
||||
payload: Optional[str] = None
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
||||
if not self.cryptobot_service:
|
||||
logger.error("CryptoBot сервис не инициализирован")
|
||||
return None
|
||||
|
||||
try:
|
||||
amount_str = f"{amount_usd:.2f}"
|
||||
|
||||
invoice_data = await self.cryptobot_service.create_invoice(
|
||||
amount=amount_str,
|
||||
asset=asset,
|
||||
description=description,
|
||||
payload=payload or f"balance_topup_{user_id}_{int(amount_usd * 100)}",
|
||||
expires_in=settings.get_cryptobot_invoice_expires_seconds()
|
||||
)
|
||||
|
||||
if not invoice_data:
|
||||
logger.error("Ошибка создания CryptoBot invoice")
|
||||
return None
|
||||
|
||||
from app.database.crud.cryptobot import create_cryptobot_payment
|
||||
|
||||
local_payment = await create_cryptobot_payment(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
invoice_id=str(invoice_data['invoice_id']),
|
||||
amount=amount_str,
|
||||
asset=asset,
|
||||
status="active",
|
||||
description=description,
|
||||
payload=payload,
|
||||
bot_invoice_url=invoice_data.get('bot_invoice_url'),
|
||||
mini_app_invoice_url=invoice_data.get('mini_app_invoice_url'),
|
||||
web_app_invoice_url=invoice_data.get('web_app_invoice_url')
|
||||
)
|
||||
|
||||
logger.info(f"Создан CryptoBot платеж {invoice_data['invoice_id']} на {amount_str} {asset} для пользователя {user_id}")
|
||||
|
||||
return {
|
||||
"local_payment_id": local_payment.id,
|
||||
"invoice_id": str(invoice_data['invoice_id']),
|
||||
"amount": amount_str,
|
||||
"asset": asset,
|
||||
"bot_invoice_url": invoice_data.get('bot_invoice_url'),
|
||||
"mini_app_invoice_url": invoice_data.get('mini_app_invoice_url'),
|
||||
"web_app_invoice_url": invoice_data.get('web_app_invoice_url'),
|
||||
"status": "active",
|
||||
"created_at": local_payment.created_at.isoformat() if local_payment.created_at else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка создания CryptoBot платежа: {e}")
|
||||
return None
|
||||
|
||||
async def process_cryptobot_webhook(self, db: AsyncSession, webhook_data: dict) -> bool:
|
||||
try:
|
||||
from app.database.crud.cryptobot import (
|
||||
get_cryptobot_payment_by_invoice_id,
|
||||
update_cryptobot_payment_status,
|
||||
link_cryptobot_payment_to_transaction
|
||||
)
|
||||
from app.database.crud.transaction import create_transaction
|
||||
from app.database.models import TransactionType, PaymentMethod
|
||||
|
||||
update_type = webhook_data.get("update_type")
|
||||
|
||||
if update_type != "invoice_paid":
|
||||
logger.info(f"Пропуск CryptoBot webhook с типом: {update_type}")
|
||||
return True
|
||||
|
||||
payload = webhook_data.get("payload", {})
|
||||
invoice_id = str(payload.get("invoice_id"))
|
||||
status = "paid"
|
||||
|
||||
if not invoice_id:
|
||||
logger.error("CryptoBot webhook без invoice_id")
|
||||
return False
|
||||
|
||||
payment = await get_cryptobot_payment_by_invoice_id(db, invoice_id)
|
||||
if not payment:
|
||||
logger.error(f"CryptoBot платеж не найден в БД: {invoice_id}")
|
||||
return False
|
||||
|
||||
if payment.status == "paid":
|
||||
logger.info(f"CryptoBot платеж {invoice_id} уже обработан")
|
||||
return True
|
||||
|
||||
paid_at_str = payload.get("paid_at")
|
||||
paid_at = None
|
||||
if paid_at_str:
|
||||
try:
|
||||
paid_at = datetime.fromisoformat(paid_at_str.replace('Z', '+00:00')).replace(tzinfo=None)
|
||||
except:
|
||||
paid_at = datetime.utcnow()
|
||||
else:
|
||||
paid_at = datetime.utcnow()
|
||||
|
||||
updated_payment = await update_cryptobot_payment_status(
|
||||
db, invoice_id, status, paid_at
|
||||
)
|
||||
|
||||
if not updated_payment.transaction_id:
|
||||
# Получаем сумму в USD из платежа
|
||||
amount_usd = updated_payment.amount_float
|
||||
|
||||
# Конвертируем в рубли по текущему курсу с улучшенной обработкой ошибок
|
||||
try:
|
||||
amount_rubles = await currency_converter.usd_to_rub(amount_usd)
|
||||
amount_kopeks = int(amount_rubles * 100)
|
||||
conversion_rate = amount_rubles / amount_usd if amount_usd > 0 else 0
|
||||
logger.info(f"Конвертация USD->RUB: ${amount_usd} -> {amount_rubles}₽ (курс: {conversion_rate:.2f})")
|
||||
except Exception as e:
|
||||
logger.warning(f"Ошибка конвертации валют для платежа {invoice_id}, используем курс 1:1: {e}")
|
||||
amount_rubles = amount_usd
|
||||
amount_kopeks = int(amount_usd * 100)
|
||||
conversion_rate = 1.0
|
||||
|
||||
# Проверяем корректность конвертированной суммы
|
||||
if amount_kopeks <= 0:
|
||||
logger.error(f"Некорректная сумма после конвертации: {amount_kopeks} копеек для платежа {invoice_id}")
|
||||
return False
|
||||
|
||||
transaction = await create_transaction(
|
||||
db,
|
||||
user_id=updated_payment.user_id,
|
||||
type=TransactionType.DEPOSIT,
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=f"Пополнение через CryptoBot ({updated_payment.amount} {updated_payment.asset} → {amount_rubles:.2f}₽)",
|
||||
payment_method=PaymentMethod.CRYPTOBOT,
|
||||
external_id=invoice_id,
|
||||
is_completed=True
|
||||
)
|
||||
|
||||
await link_cryptobot_payment_to_transaction(
|
||||
db, invoice_id, transaction.id
|
||||
)
|
||||
|
||||
user = await get_user_by_id(db, updated_payment.user_id)
|
||||
if user:
|
||||
old_balance = user.balance_kopeks
|
||||
|
||||
user.balance_kopeks += amount_kopeks
|
||||
user.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
try:
|
||||
from app.services.referral_service import process_referral_topup
|
||||
await process_referral_topup(db, user.id, amount_kopeks, self.bot)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки реферального пополнения CryptoBot: {e}")
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
notification_service = AdminNotificationService(self.bot)
|
||||
await notification_service.send_balance_topup_notification(
|
||||
db, user, transaction, old_balance
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении CryptoBot: {e}")
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
f"✅ <b>Пополнение успешно!</b>\n\n"
|
||||
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
|
||||
f"🪙 Платеж: {updated_payment.amount} {updated_payment.asset}\n"
|
||||
f"💱 Курс: 1 USD = {conversion_rate:.2f}₽\n"
|
||||
f"🆔 Транзакция: {invoice_id[:8]}...\n\n"
|
||||
f"Баланс пополнен автоматически!",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {amount_rubles:.2f}₽ ({updated_payment.asset})")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления о пополнении CryptoBot: {e}")
|
||||
else:
|
||||
logger.error(f"Пользователь с ID {updated_payment.user_id} не найден при пополнении баланса")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки CryptoBot webhook: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
@@ -63,7 +63,8 @@ async def process_referral_registration(
|
||||
f"👥 <b>Новый реферал!</b>\n\n"
|
||||
f"По вашей ссылке зарегистрировался пользователь <b>{new_user.full_name}</b>!\n\n"
|
||||
f"💰 Когда он пополнит баланс от {settings.format_price(settings.REFERRAL_MINIMUM_TOPUP_KOPEKS)}, "
|
||||
f"вы получите {settings.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS)}\n\n"
|
||||
f"вы получите минимум {settings.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS)} или "
|
||||
f"{settings.REFERRAL_COMMISSION_PERCENT}% от суммы (что больше).\n\n"
|
||||
f"📈 С каждого последующего пополнения вы будете получать {settings.REFERRAL_COMMISSION_PERCENT}% комиссии."
|
||||
)
|
||||
await send_referral_notification(bot, referrer.telegram_id, inviter_notification)
|
||||
@@ -131,27 +132,30 @@ async def process_referral_topup(
|
||||
)
|
||||
await send_referral_notification(bot, user.telegram_id, bonus_notification)
|
||||
|
||||
if settings.REFERRAL_INVITER_BONUS_KOPEKS > 0:
|
||||
commission_amount = int(topup_amount_kopeks * settings.REFERRAL_COMMISSION_PERCENT / 100)
|
||||
inviter_bonus = max(settings.REFERRAL_INVITER_BONUS_KOPEKS, commission_amount)
|
||||
|
||||
if inviter_bonus > 0:
|
||||
await add_user_balance(
|
||||
db, referrer, settings.REFERRAL_INVITER_BONUS_KOPEKS,
|
||||
db, referrer, inviter_bonus,
|
||||
f"Бонус за первое пополнение реферала {user.full_name}",
|
||||
bot=bot
|
||||
)
|
||||
|
||||
|
||||
await create_referral_earning(
|
||||
db=db,
|
||||
user_id=referrer.id,
|
||||
referral_id=user.id,
|
||||
amount_kopeks=settings.REFERRAL_INVITER_BONUS_KOPEKS,
|
||||
referral_id=user.id,
|
||||
amount_kopeks=inviter_bonus,
|
||||
reason="referral_first_topup"
|
||||
)
|
||||
logger.info(f"💰 Реферер {referrer.telegram_id} получил бонус {settings.REFERRAL_INVITER_BONUS_KOPEKS/100}₽")
|
||||
|
||||
logger.info(f"💰 Реферер {referrer.telegram_id} получил бонус {inviter_bonus/100}₽")
|
||||
|
||||
if bot:
|
||||
inviter_bonus_notification = (
|
||||
f"💰 <b>Реферальная награда!</b>\n\n"
|
||||
f"Ваш реферал <b>{user.full_name}</b> сделал первое пополнение!\n\n"
|
||||
f"🎁 Вы получили награду: {settings.format_price(settings.REFERRAL_INVITER_BONUS_KOPEKS)}\n\n"
|
||||
f"🎁 Вы получили награду: {settings.format_price(inviter_bonus)}\n\n"
|
||||
f"📈 Теперь с каждого его пополнения вы будете получать {settings.REFERRAL_COMMISSION_PERCENT}% комиссии."
|
||||
)
|
||||
await send_referral_notification(bot, referrer.telegram_id, inviter_bonus_notification)
|
||||
|
||||
@@ -221,7 +221,7 @@ class TributeService:
|
||||
|
||||
text = (
|
||||
f"✅ **Платеж успешно получен!**\n\n"
|
||||
f"💰 Сумма: {amount_rubles:.2f} ₽\n"
|
||||
f"💰 Сумма: {int(amount_rubles)} ₽\n"
|
||||
f"💳 Способ оплаты: Tribute\n"
|
||||
f"🎉 Средства зачислены на баланс!\n\n"
|
||||
f"Спасибо за оплату! 🙏"
|
||||
@@ -277,7 +277,7 @@ class TributeService:
|
||||
|
||||
text = (
|
||||
f"🔄 **Возврат средств**\n\n"
|
||||
f"💰 Сумма возврата: {amount_rubles:.2f} ₽\n"
|
||||
f"💰 Сумма возврата: {int(amount_rubles)} ₽\n"
|
||||
f"💳 Способ: Tribute\n\n"
|
||||
f"Средства будут возвращены на вашу карту в течение 3-5 рабочих дней.\n\n"
|
||||
f"Если у вас есть вопросы, обратитесь в поддержку."
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.database.crud.transaction import get_user_transactions_count
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
from app.database.models import (
|
||||
User, UserStatus, Subscription, Transaction, PromoCodeUse,
|
||||
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory
|
||||
ReferralEarning, SubscriptionServer, YooKassaPayment, BroadcastHistory, CryptoBotPayment
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
@@ -246,6 +246,36 @@ class UserService:
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Ошибка деактивации RemnaWave: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import UserMessage
|
||||
from sqlalchemy import update
|
||||
|
||||
result = await db.execute(
|
||||
update(UserMessage)
|
||||
.where(UserMessage.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
if result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {result.rowcount} пользовательских сообщений")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления пользовательских сообщений: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import PromoCode
|
||||
from sqlalchemy import update
|
||||
|
||||
result = await db.execute(
|
||||
update(PromoCode)
|
||||
.where(PromoCode.created_by == user_id)
|
||||
.values(created_by=None)
|
||||
)
|
||||
if result.rowcount > 0:
|
||||
logger.info(f"🔄 Обновлено {result.rowcount} промокодов")
|
||||
await db.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка обновления промокодов: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import YooKassaPayment
|
||||
from sqlalchemy import select
|
||||
@@ -264,6 +294,25 @@ class UserService:
|
||||
logger.info(f"✅ YooKassa платежи удалены")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления YooKassa платежей: {e}")
|
||||
|
||||
try:
|
||||
from app.database.models import CryptoBotPayment
|
||||
from sqlalchemy import select, delete
|
||||
|
||||
cryptobot_result = await db.execute(
|
||||
select(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
|
||||
)
|
||||
cryptobot_payments = cryptobot_result.scalars().all()
|
||||
|
||||
if cryptobot_payments:
|
||||
logger.info(f"🔄 Удаляем {len(cryptobot_payments)} CryptoBot платежей")
|
||||
await db.execute(
|
||||
delete(CryptoBotPayment).where(CryptoBotPayment.user_id == user_id)
|
||||
)
|
||||
await db.flush()
|
||||
logger.info(f"✅ CryptoBot платежи удалены")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка удаления CryptoBot платежей: {e}")
|
||||
|
||||
try:
|
||||
transactions_result = await db.execute(
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import aiohttp
|
||||
from packaging import version
|
||||
import re
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VersionInfo:
|
||||
def __init__(self, tag_name: str, published_at: str, name: str, body: str, prerelease: bool = False):
|
||||
self.tag_name = tag_name
|
||||
self.published_at = datetime.fromisoformat(published_at.replace('Z', '+00:00'))
|
||||
self.name = name or tag_name
|
||||
self.body = body
|
||||
self.prerelease = prerelease
|
||||
self.is_dev = 'dev' in tag_name.lower()
|
||||
|
||||
@property
|
||||
def clean_version(self) -> str:
|
||||
return re.sub(r'^v', '', self.tag_name)
|
||||
|
||||
@property
|
||||
def version_obj(self):
|
||||
try:
|
||||
clean_ver = self.clean_version
|
||||
if 'dev' in clean_ver:
|
||||
base_ver = clean_ver.split('-dev')[0]
|
||||
return version.parse(f"{base_ver}.dev")
|
||||
return version.parse(clean_ver)
|
||||
except Exception:
|
||||
return version.parse("0.0.0")
|
||||
|
||||
@property
|
||||
def formatted_date(self) -> str:
|
||||
return self.published_at.strftime('%d.%m.%Y %H:%M')
|
||||
|
||||
@property
|
||||
def short_description(self) -> str:
|
||||
if not self.body:
|
||||
return "Без описания"
|
||||
|
||||
description = self.body.strip()
|
||||
if len(description) > 350:
|
||||
description = description[:347] + "..."
|
||||
|
||||
return description
|
||||
|
||||
|
||||
class VersionService:
|
||||
def __init__(self, bot=None):
|
||||
self.bot = bot
|
||||
self.repo = getattr(settings, 'VERSION_CHECK_REPO', 'fr1ngg/remnawave-bedolaga-telegram-bot')
|
||||
self.enabled = getattr(settings, 'VERSION_CHECK_ENABLED', True)
|
||||
self.current_version = self._get_current_version()
|
||||
self.cache_ttl = 3600
|
||||
self._cache: Dict = {}
|
||||
self._last_check: Optional[datetime] = None
|
||||
self._notification_service = None
|
||||
|
||||
async def get_latest_stable_version(self) -> str:
|
||||
try:
|
||||
url = f"https://api.github.com/repos/{self.repo}/releases/latest"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
return data['tag_name']
|
||||
except Exception:
|
||||
pass
|
||||
return "UNKNOW"
|
||||
|
||||
def _get_current_version(self) -> str:
|
||||
import os
|
||||
|
||||
current = os.getenv('VERSION', '').strip()
|
||||
|
||||
if current:
|
||||
if '-' in current and current.startswith('v'):
|
||||
base_version = current.split('-')[0]
|
||||
if base_version.count('.') == 2:
|
||||
return base_version
|
||||
return current
|
||||
|
||||
return "UNKNOW"
|
||||
|
||||
def set_notification_service(self, notification_service):
|
||||
self._notification_service = notification_service
|
||||
|
||||
async def check_for_updates(self, force: bool = False) -> Tuple[bool, List[VersionInfo]]:
|
||||
if not self.enabled:
|
||||
return False, []
|
||||
|
||||
try:
|
||||
releases = await self._fetch_releases(force)
|
||||
if not releases:
|
||||
return False, []
|
||||
|
||||
current_ver = self._parse_version(self.current_version)
|
||||
newer_releases = []
|
||||
|
||||
for release in releases:
|
||||
release_ver = release.version_obj
|
||||
if release_ver > current_ver:
|
||||
newer_releases.append(release)
|
||||
|
||||
newer_releases.sort(key=lambda x: x.version_obj, reverse=True)
|
||||
|
||||
has_updates = len(newer_releases) > 0
|
||||
|
||||
if has_updates and not force:
|
||||
await self._send_update_notification(newer_releases)
|
||||
|
||||
return has_updates, newer_releases
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки обновлений: {e}")
|
||||
return False, []
|
||||
|
||||
async def _fetch_releases(self, force: bool = False) -> List[VersionInfo]:
|
||||
if not force and self._cache and self._last_check:
|
||||
if datetime.now() - self._last_check < timedelta(seconds=self.cache_ttl):
|
||||
return self._cache.get('releases', [])
|
||||
|
||||
url = f"https://api.github.com/repos/{self.repo}/releases"
|
||||
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=10)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
releases = []
|
||||
|
||||
for release_data in data[:20]:
|
||||
release = VersionInfo(
|
||||
tag_name=release_data['tag_name'],
|
||||
published_at=release_data['published_at'],
|
||||
name=release_data['name'],
|
||||
body=release_data['body'] or '',
|
||||
prerelease=release_data['prerelease']
|
||||
)
|
||||
releases.append(release)
|
||||
|
||||
self._cache['releases'] = releases
|
||||
self._last_check = datetime.now()
|
||||
|
||||
logger.info(f"Получено {len(releases)} релизов из GitHub")
|
||||
return releases
|
||||
else:
|
||||
logger.warning(f"GitHub API вернул статус {response.status}")
|
||||
return []
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Таймаут при запросе к GitHub API")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запроса к GitHub API: {e}")
|
||||
return []
|
||||
|
||||
def _parse_version(self, version_str: str):
|
||||
try:
|
||||
clean_ver = re.sub(r'^v', '', version_str)
|
||||
if 'dev' in clean_ver:
|
||||
base_ver = clean_ver.split('-dev')[0]
|
||||
return version.parse(f"{base_ver}.dev")
|
||||
if 'unknow' in clean_ver.lower():
|
||||
return version.parse("0.0.0")
|
||||
return version.parse(clean_ver)
|
||||
except Exception:
|
||||
return version.parse("0.0.0")
|
||||
|
||||
async def _send_update_notification(self, newer_releases: List[VersionInfo]):
|
||||
if not self._notification_service or not newer_releases:
|
||||
return
|
||||
|
||||
try:
|
||||
latest_version = newer_releases[0]
|
||||
cache_key = f"notified_{latest_version.tag_name}"
|
||||
|
||||
if self._cache.get(cache_key):
|
||||
return
|
||||
|
||||
await self._notification_service.send_version_update_notification(
|
||||
current_version=self.current_version,
|
||||
latest_version=latest_version,
|
||||
total_updates=len(newer_releases)
|
||||
)
|
||||
|
||||
self._cache[cache_key] = True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки уведомления об обновлении: {e}")
|
||||
|
||||
async def get_version_info(self) -> Dict:
|
||||
try:
|
||||
has_updates, newer_releases = await self.check_for_updates()
|
||||
all_releases = await self._fetch_releases()
|
||||
|
||||
current_release = None
|
||||
current_ver = self._parse_version(self.current_version)
|
||||
|
||||
for release in all_releases:
|
||||
if release.version_obj == current_ver:
|
||||
current_release = release
|
||||
break
|
||||
|
||||
return {
|
||||
'current_version': self.current_version,
|
||||
'current_release': current_release,
|
||||
'has_updates': has_updates,
|
||||
'newer_releases': newer_releases[:5],
|
||||
'total_newer': len(newer_releases),
|
||||
'last_check': self._last_check,
|
||||
'repo_url': f"https://github.com/{self.repo}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения информации о версиях: {e}")
|
||||
return {
|
||||
'current_version': self.current_version,
|
||||
'current_release': None,
|
||||
'has_updates': False,
|
||||
'newer_releases': [],
|
||||
'total_newer': 0,
|
||||
'last_check': None,
|
||||
'repo_url': f"https://github.com/{self.repo}",
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def start_periodic_check(self):
|
||||
if not self.enabled:
|
||||
logger.info("Проверка версий отключена")
|
||||
return
|
||||
|
||||
logger.info(f"Запуск периодической проверки обновлений для {self.repo}")
|
||||
logger.info(f"Текущая версия: {self.current_version}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(3600)
|
||||
await self.check_for_updates()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Остановка проверки обновлений")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в периодической проверке обновлений: {e}")
|
||||
await asyncio.sleep(300)
|
||||
|
||||
def format_version_display(self, version_info: VersionInfo) -> str:
|
||||
status_icon = ""
|
||||
if version_info.prerelease:
|
||||
status_icon = "🧪"
|
||||
elif version_info.is_dev:
|
||||
status_icon = "🔧"
|
||||
else:
|
||||
status_icon = "📦"
|
||||
|
||||
return f"{status_icon} {version_info.tag_name}"
|
||||
|
||||
|
||||
version_service = VersionService()
|
||||
@@ -0,0 +1,121 @@
|
||||
import logging
|
||||
import aiohttp
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CurrencyConverter:
|
||||
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
self._cache_ttl = 3600 # 1 час
|
||||
self._last_update = {}
|
||||
|
||||
async def get_usd_to_rub_rate(self) -> float:
|
||||
"""Получает курс USD/RUB с кешированием"""
|
||||
|
||||
cache_key = "USD_RUB"
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Проверяем кеш
|
||||
if (cache_key in self._cache and
|
||||
cache_key in self._last_update and
|
||||
(now - self._last_update[cache_key]).seconds < self._cache_ttl):
|
||||
return self._cache[cache_key]
|
||||
|
||||
# Получаем новый курс
|
||||
rate = await self._fetch_exchange_rate()
|
||||
|
||||
if rate:
|
||||
self._cache[cache_key] = rate
|
||||
self._last_update[cache_key] = now
|
||||
logger.info(f"Обновлен курс USD/RUB: {rate}")
|
||||
return rate
|
||||
|
||||
# Возвращаем из кеша если API недоступен
|
||||
if cache_key in self._cache:
|
||||
logger.warning("API курсов недоступен, используем кешированный курс")
|
||||
return self._cache[cache_key]
|
||||
|
||||
# Fallback курс
|
||||
logger.warning("Используем fallback курс USD/RUB: 95")
|
||||
return 95.0
|
||||
|
||||
async def _fetch_exchange_rate(self) -> Optional[float]:
|
||||
"""Получает курс с нескольких источников"""
|
||||
|
||||
sources = [
|
||||
self._fetch_from_cbr,
|
||||
self._fetch_from_exchangerate_api,
|
||||
self._fetch_from_fixer
|
||||
]
|
||||
|
||||
for source in sources:
|
||||
try:
|
||||
rate = await source()
|
||||
if rate and 50 < rate < 200: # Разумные границы курса
|
||||
return rate
|
||||
except Exception as e:
|
||||
logger.debug(f"Ошибка получения курса из {source.__name__}: {e}")
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
async def _fetch_from_cbr(self) -> Optional[float]:
|
||||
"""Получает курс с сайта ЦБ РФ"""
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
||||
async with session.get('https://www.cbr-xml-daily.ru/daily_json.js') as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
usd_rate = data['Valute']['USD']['Value']
|
||||
return float(usd_rate)
|
||||
except Exception as e:
|
||||
logger.debug(f"Ошибка получения курса ЦБ: {e}")
|
||||
return None
|
||||
|
||||
async def _fetch_from_exchangerate_api(self) -> Optional[float]:
|
||||
"""Получает курс с exchangerate-api.com"""
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
||||
async with session.get('https://api.exchangerate-api.com/v4/latest/USD') as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
rub_rate = data['rates']['RUB']
|
||||
return float(rub_rate)
|
||||
except Exception as e:
|
||||
logger.debug(f"Ошибка получения курса exchangerate-api: {e}")
|
||||
return None
|
||||
|
||||
async def _fetch_from_fixer(self) -> Optional[float]:
|
||||
"""Получает курс с fixer.io (бесплатный план)"""
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
|
||||
# Используем бесплатный endpoint (EUR base)
|
||||
async with session.get('https://api.fixer.io/latest?access_key=YOUR_API_KEY&symbols=USD,RUB') as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
if data.get('success'):
|
||||
# Конвертируем EUR -> USD -> RUB
|
||||
usd_eur = data['rates']['USD']
|
||||
rub_eur = data['rates']['RUB']
|
||||
usd_rub = rub_eur / usd_eur
|
||||
return float(usd_rub)
|
||||
except Exception as e:
|
||||
logger.debug(f"Ошибка получения курса fixer: {e}")
|
||||
return None
|
||||
|
||||
async def usd_to_rub(self, usd_amount: float) -> float:
|
||||
"""Конвертирует USD в RUB"""
|
||||
rate = await self.get_usd_to_rub_rate()
|
||||
return usd_amount * rate
|
||||
|
||||
async def rub_to_usd(self, rub_amount: float) -> float:
|
||||
"""Конвертирует RUB в USD"""
|
||||
rate = await self.get_usd_to_rub_rate()
|
||||
return rub_amount / rate
|
||||
|
||||
# Глобальный экземпляр
|
||||
currency_converter = CurrencyConverter()
|
||||
@@ -11,8 +11,7 @@ ALLOWED_HTML_TAGS = {
|
||||
'code',
|
||||
'pre',
|
||||
'a',
|
||||
'blockquote',
|
||||
'spoiler', 'tg-spoiler'
|
||||
'blockquote'
|
||||
}
|
||||
|
||||
def validate_email(email: str) -> bool:
|
||||
@@ -203,6 +202,5 @@ def get_html_help_text() -> str:
|
||||
- <code><pre>блок кода</pre></code>
|
||||
- <code><a href="url">ссылка</a></code>
|
||||
- <code><blockquote>цитата</blockquote></code>
|
||||
- <code><spoiler>спойлер</spoiler></code>
|
||||
|
||||
<b>Неподдерживаемые теги:</b> <br>, <p>, <div>, <span> и другие"""
|
||||
<b>Неподдерживаемые теги:</b> <br>, <p>, <div>, <span>, <spoiler> и другие"""
|
||||
|
||||
+28
-7
@@ -7,7 +7,7 @@ services:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-remnawave_bot}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-remnawave_user}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password_123}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
image: redis:7-alpine
|
||||
container_name: remnawave_bot_redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
@@ -35,9 +35,7 @@ services:
|
||||
retries: 3
|
||||
|
||||
bot:
|
||||
build:
|
||||
context: . # директория где лежит Dockerfile
|
||||
dockerfile: Dockerfile
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
container_name: remnawave_bot
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
@@ -48,11 +46,25 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-remnawave_user}:${POSTGRES_PASSWORD:-secure_password_123}@postgres:5432/${POSTGRES_DB:-remnawave_bot}
|
||||
REDIS_URL: redis://redis:6379/0
|
||||
DOCKER_ENV: "true"
|
||||
DATABASE_MODE: "auto"
|
||||
POSTGRES_HOST: "postgres"
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DB: "${POSTGRES_DB:-remnawave_bot}"
|
||||
POSTGRES_USER: "${POSTGRES_USER:-remnawave_user}"
|
||||
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-secure_password_123}"
|
||||
|
||||
REDIS_URL: "redis://redis:6379/0"
|
||||
|
||||
TZ: "Europe/Moscow"
|
||||
volumes:
|
||||
# Логи
|
||||
- ./logs:/app/logs:rw
|
||||
# Данные приложения (для SQLite в случае переключения)
|
||||
- ./data:/app/data:rw
|
||||
# Конфигурация приложения
|
||||
# - ./app-config.json:/app/app-config.json:ro
|
||||
# Timezone
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
@@ -60,10 +72,18 @@ services:
|
||||
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c 'import requests; requests.get(\"http://localhost:8081/health\", timeout=5)' || exit 1"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
bot_network:
|
||||
@@ -71,3 +91,4 @@ networks:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
gateway: 172.20.0.1
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.database.database import init_db
|
||||
from app.services.monitoring_service import monitoring_service
|
||||
from app.services.maintenance_service import maintenance_service
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.services.version_service import version_service
|
||||
from app.external.webhook_server import WebhookServer
|
||||
from app.external.yookassa_webhook import start_yookassa_webhook_server
|
||||
from app.database.universal_migration import run_universal_migration
|
||||
@@ -49,6 +50,7 @@ async def main():
|
||||
yookassa_server_task = None
|
||||
monitoring_task = None
|
||||
maintenance_task = None
|
||||
version_check_task = None
|
||||
polling_task = None
|
||||
|
||||
try:
|
||||
@@ -77,7 +79,15 @@ async def main():
|
||||
bot, dp = await setup_bot()
|
||||
|
||||
monitoring_service.bot = bot
|
||||
maintenance_service.set_bot(bot)
|
||||
maintenance_service.set_bot(bot)
|
||||
|
||||
from app.services.admin_notification_service import AdminNotificationService
|
||||
admin_notification_service = AdminNotificationService(bot)
|
||||
version_service.bot = bot
|
||||
version_service.set_notification_service(admin_notification_service)
|
||||
logger.info(f"🔄 Сервис версий настроен для репозитория: {version_service.repo}")
|
||||
logger.info(f"📦 Текущая версия: {version_service.current_version}")
|
||||
|
||||
logger.info("🔗 Бот подключен к сервисам мониторинга и техработ")
|
||||
|
||||
payment_service = PaymentService(bot)
|
||||
@@ -108,7 +118,13 @@ async def main():
|
||||
logger.info("🔧 Служба техработ уже запущена")
|
||||
maintenance_task = None
|
||||
|
||||
logger.info("🔄 Запуск polling...")
|
||||
if settings.is_version_check_enabled():
|
||||
logger.info("🔄 Запуск сервиса проверки версий...")
|
||||
version_check_task = asyncio.create_task(version_service.start_periodic_check())
|
||||
else:
|
||||
logger.info("ℹ️ Проверка версий отключена")
|
||||
|
||||
logger.info("📄 Запуск polling...")
|
||||
polling_task = asyncio.create_task(dp.start_polling(bot, skip_updates=True))
|
||||
|
||||
logger.info("=" * 50)
|
||||
@@ -117,6 +133,10 @@ async def main():
|
||||
logger.info(f" Tribute: {settings.WEBHOOK_URL}:{settings.TRIBUTE_WEBHOOK_PORT}{settings.TRIBUTE_WEBHOOK_PATH}")
|
||||
if settings.is_yookassa_enabled():
|
||||
logger.info(f" YooKassa: {settings.WEBHOOK_URL}:{settings.YOOKASSA_WEBHOOK_PORT}{settings.YOOKASSA_WEBHOOK_PATH}")
|
||||
logger.info("🔄 Активные фоновые сервисы:")
|
||||
logger.info(f" Мониторинг: {'Включен' if monitoring_task else 'Отключен'}")
|
||||
logger.info(f" Техработы: {'Включен' if maintenance_task else 'Отключен'}")
|
||||
logger.info(f" Проверка версий: {'Включен' if version_check_task else 'Отключен'}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
try:
|
||||
@@ -143,6 +163,14 @@ async def main():
|
||||
if exception:
|
||||
logger.error(f"Служба техработ завершилась с ошибкой: {exception}")
|
||||
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
|
||||
|
||||
if version_check_task and version_check_task.done():
|
||||
exception = version_check_task.exception()
|
||||
if exception:
|
||||
logger.error(f"Сервис проверки версий завершился с ошибкой: {exception}")
|
||||
if settings.is_version_check_enabled():
|
||||
logger.info("🔄 Перезапуск сервиса проверки версий...")
|
||||
version_check_task = asyncio.create_task(version_service.start_periodic_check())
|
||||
|
||||
if polling_task.done():
|
||||
exception = polling_task.exception()
|
||||
@@ -186,6 +214,14 @@ async def main():
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if version_check_task and not version_check_task.done():
|
||||
logger.info("ℹ️ Остановка сервиса проверки версий...")
|
||||
version_check_task.cancel()
|
||||
try:
|
||||
await version_check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if polling_task and not polling_task.done():
|
||||
logger.info("ℹ️ Остановка polling...")
|
||||
polling_task.cancel()
|
||||
|
||||
@@ -25,3 +25,7 @@ APScheduler==3.10.4
|
||||
python-dateutil==2.8.2
|
||||
pytz==2023.4
|
||||
cryptography>=41.0.0
|
||||
qrcode[pil]==7.4.2
|
||||
|
||||
# Для работы с версиями
|
||||
packaging==23.2
|
||||
|
||||
Reference in New Issue
Block a user