mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-05-02 02:36:26 +00:00
177
README.md
177
README.md
@@ -50,6 +50,7 @@
|
||||
- 🔒 **Безопасность** - шифрование, валидация, rate limiting
|
||||
- 📈 **Масштабируемость**
|
||||
- 🔧 **Мониторинг** - Prometheus, Grafana, health checks
|
||||
- 🔧 **Режим технических работ** - Ручное включение + Мониторинг системы, который в случае падении панели Remnawave переведет бота в режим технических работ и обратно - отключит его, если панель поднимется.
|
||||
|
||||
---
|
||||
|
||||
@@ -89,50 +90,92 @@ docker compose logs -f bot
|
||||
<summary>🔧 Полная конфигурация .env</summary>
|
||||
|
||||
```env
|
||||
# 🏷️ Основные настройки
|
||||
NODE_ENV=production
|
||||
DEBUG=false
|
||||
LOG_LEVEL=INFO
|
||||
# TELEGRAM BOT CONFIGURATION
|
||||
BOT_TOKEN=
|
||||
ADMIN_IDS=
|
||||
SUPPORT_USERNAME=
|
||||
|
||||
# 🗄️ База данных
|
||||
POSTGRES_DB=bedolaga_bot
|
||||
POSTGRES_USER=bedolaga_user
|
||||
POSTGRES_PASSWORD=secure_password_123
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgresql+asyncpg://bedolaga_user:secure_password_123@postgres:5432/bedolaga_bot
|
||||
# DATABASE CONFIGURATION
|
||||
DATABASE_URL=sqlite+aiosqlite:///./bot.db
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# ⚡ Redis кеш
|
||||
REDIS_PASSWORD=redis_password_123
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://:redis_password_123@redis:6379/0
|
||||
# REMNAWAVE API CONFIGURATION
|
||||
REMNAWAVE_API_URL=
|
||||
REMNAWAVE_API_KEY=
|
||||
|
||||
# 🤖 Telegram Bot
|
||||
BOT_TOKEN=your_bot_token_here
|
||||
ADMIN_IDS=123456789,987654321
|
||||
SUPPORT_USERNAME=@your_support
|
||||
# === NEW: Traffic Selection Mode Settings ===
|
||||
# Режим выбора трафика:
|
||||
# "selectable" - пользователи выбирают пакеты трафика (по умолчанию)
|
||||
# "fixed" - фиксированный лимит трафика для всех подписок, доступно 5/10/25/50/100/250/0 (0 безлимит) гб
|
||||
TRAFFIC_SELECTION_MODE=selectable
|
||||
|
||||
# 🔗 Remnawave API
|
||||
REMNAWAVE_API_URL=https://your-panel.com
|
||||
REMNAWAVE_API_KEY=your_jwt_token_here
|
||||
# Фиксированный лимит трафика в ГБ (используется только в режиме "fixed")
|
||||
# 0 = безлимит
|
||||
# для "fixed" обязательно должы быть проставлены цены на пакеты 5/10/25/50/100/250/0 можно постать 0 руб - будет беслпатно
|
||||
FIXED_TRAFFIC_LIMIT_GB=0
|
||||
|
||||
# 🌐 Webhook настройки
|
||||
WEBHOOK_DOMAIN=your-domain.com
|
||||
WEBHOOK_PORT=8081
|
||||
WEBHOOK_URL=https://your-domain.com
|
||||
WEBHOOK_PATH=/webhook
|
||||
# TRIAL SUBSCRIPTION SETTINGS
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_LIMIT_GB=10
|
||||
TRIAL_DEVICE_LIMIT=2
|
||||
TRIAL_SQUAD_UUID=
|
||||
DEFAULT_TRAFFIC_RESET_STRATEGY=MONTH
|
||||
|
||||
# ⭐ Telegram Stars
|
||||
# SUBSCRIPTION PRICING (в копейках для точности)
|
||||
BASE_SUBSCRIPTION_PRICE=50000
|
||||
|
||||
PRICE_14_DAYS=5000
|
||||
PRICE_30_DAYS=9900
|
||||
PRICE_60_DAYS=18900
|
||||
PRICE_90_DAYS=26900
|
||||
PRICE_180_DAYS=49900
|
||||
PRICE_360_DAYS=89900
|
||||
|
||||
PRICE_TRAFFIC_5GB=2000
|
||||
PRICE_TRAFFIC_10GB=4000
|
||||
PRICE_TRAFFIC_25GB=6000
|
||||
PRICE_TRAFFIC_50GB=10000
|
||||
PRICE_TRAFFIC_100GB=15000
|
||||
PRICE_TRAFFIC_250GB=20000
|
||||
PRICE_TRAFFIC_UNLIMITED=25000
|
||||
|
||||
PRICE_PER_DEVICE=5000
|
||||
|
||||
# REFERRAL SYSTEM SETTINGS
|
||||
REFERRAL_REGISTRATION_REWARD=5000
|
||||
REFERRED_USER_REWARD=10000
|
||||
REFERRAL_COMMISSION_PERCENT=25
|
||||
|
||||
# Режим работы кнопки "Подключиться"
|
||||
# guide - открывает гайд подключения (режим 1)
|
||||
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
|
||||
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
|
||||
CONNECT_BUTTON_MODE=miniapp_subscription
|
||||
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
|
||||
# MINIAPP_CUSTOM_URL=
|
||||
|
||||
# AUTO-PAYMENT SETTINGS
|
||||
AUTOPAY_WARNING_DAYS=3,1
|
||||
|
||||
# MONITORING SETTINGS
|
||||
MONITORING_INTERVAL=60
|
||||
INACTIVE_USER_DELETE_MONTHS=3
|
||||
|
||||
TRIAL_WARNING_HOURS=2
|
||||
ENABLE_NOTIFICATIONS=true
|
||||
NOTIFICATION_RETRY_ATTEMPTS=3
|
||||
MONITORING_LOGS_RETENTION_DAYS=30
|
||||
|
||||
# PAYMENT SYSTEMS
|
||||
TELEGRAM_STARS_ENABLED=true
|
||||
|
||||
# 💳 Tribute платежи
|
||||
TRIBUTE_ENABLED=true
|
||||
TRIBUTE_API_KEY=your_tribute_api_key
|
||||
TRIBUTE_ENABLED=false
|
||||
TRIBUTE_API_KEY=
|
||||
TRIBUTE_WEBHOOK_SECRET=your_webhook_secret
|
||||
TRIBUTE_DONATE_LINK=https://t.me/tribute/app?startapp=XXXX
|
||||
TRIBUTE_WEBHOOK_PATH=/tribute-webhook
|
||||
TRIBUTE_WEBHOOK_PORT=8081
|
||||
TRIBUTE_WEBHOOK_SECRET=your_webhook_secret
|
||||
|
||||
# 💳 YOOKASSA
|
||||
# === НОВЫЕ НАСТРОЙКИ YOOKASSA ===
|
||||
# Включение/выключение YooKassa
|
||||
YOOKASSA_ENABLED=false
|
||||
|
||||
@@ -187,65 +230,23 @@ YOOKASSA_WEBHOOK_PATH=/yookassa-webhook
|
||||
YOOKASSA_WEBHOOK_PORT=8082
|
||||
YOOKASSA_WEBHOOK_SECRET=ваш_секретный_ключ_для_webhook
|
||||
|
||||
# 🚀 Режим работы кнопки "Подключиться"
|
||||
# guide - открывает гайд подключения c настройками и парамтерами из app-config.json (режим 1)
|
||||
# miniapp_subscription - открывает ссылку подписки в мини-приложении (режим 2)
|
||||
# miniapp_custom - открывает заданную ссылку в мини-приложении (режим 3)
|
||||
CONNECT_BUTTON_MODE=miniapp_subscription
|
||||
# URL для режима miniapp_custom (обязателен при CONNECT_BUTTON_MODE=miniapp_custom)
|
||||
# MINIAPP_CUSTOM_URL=
|
||||
WEBHOOK_URL=https://example.com
|
||||
WEBHOOK_PATH=/webhook
|
||||
|
||||
# 🎛️ === NEW: Traffic Selection Mode Settings ===
|
||||
# Режим выбора трафика:
|
||||
# "selectable" - пользователи выбирают пакеты трафика (по умолчанию)
|
||||
# "fixed" - фиксированный лимит трафика для всех подписок(БЕЗ ШАГА ВЫБОРА ПАКЕТА ТРАФИКА ВО ВРЕМЯ ОФОРМЛЕНИЯ ПОДПИСКИ), доступно 5/10/25/50/100/250/0 (0 безлимит) гб
|
||||
# Фиксированный лимит трафика в ГБ (используется только в режиме "fixed")
|
||||
# 0 = безлимит
|
||||
# для "fixed" обязательно должы быть проставлены цены на пакеты 5/10/25/50/100/250/0 можно постать 0 руб - будет беслпатно
|
||||
TRAFFIC_SELECTION_MODE=selectable
|
||||
FIXED_TRAFFIC_LIMIT_GB=0
|
||||
# LOCALIZATION
|
||||
DEFAULT_LANGUAGE=ru
|
||||
AVAILABLE_LANGUAGES=ru
|
||||
|
||||
# 🎁 Триал настройки
|
||||
TRIAL_ENABLED=true
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_LIMIT_GB=10
|
||||
TRIAL_DEVICE_LIMIT=2
|
||||
TRIAL_SQUAD_UUID=your_trial_squad_uuid
|
||||
# LOGGING
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=/tmp/bot.log
|
||||
|
||||
# 💰 Ценообразование (в копейках)
|
||||
BASE_SUBSCRIPTION_PRICE=50000
|
||||
PRICE_14_DAYS=5000
|
||||
PRICE_30_DAYS=9900
|
||||
PRICE_60_DAYS=18900
|
||||
PRICE_90_DAYS=26900
|
||||
PRICE_180_DAYS=49900
|
||||
PRICE_360_DAYS=89900
|
||||
# DEVELOPMENT
|
||||
DEBUG=false
|
||||
|
||||
PRICE_TRAFFIC_5GB=2000
|
||||
PRICE_TRAFFIC_10GB=4000
|
||||
PRICE_TRAFFIC_25GB=6000
|
||||
PRICE_TRAFFIC_50GB=10000
|
||||
PRICE_TRAFFIC_100GB=15000
|
||||
PRICE_TRAFFIC_250GB=20000
|
||||
PRICE_TRAFFIC_UNLIMITED=25000
|
||||
|
||||
PRICE_PER_DEVICE=5000
|
||||
|
||||
# 🤝 Реферальная система
|
||||
REFERRAL_REGISTRATION_REWARD=5000
|
||||
REFERRED_USER_REWARD=2500
|
||||
REFERRAL_COMMISSION_PERCENT=10
|
||||
|
||||
# 🔍 Мониторинг
|
||||
MONITORING_INTERVAL=60
|
||||
ENABLE_NOTIFICATIONS=true
|
||||
AUTOPAY_WARNING_DAYS=3,1
|
||||
MONITORING_LOGS_RETENTION_DAYS=30
|
||||
INACTIVE_USER_DELETE_MONTHS=3
|
||||
|
||||
# 📊 Опционально для мониторинга
|
||||
GRAFANA_USER=admin
|
||||
GRAFANA_PASSWORD=admin123
|
||||
MAINTENANCE_CHECK_INTERVAL=30
|
||||
MAINTENANCE_AUTO_ENABLE=true
|
||||
MAINTENANCE_MESSAGE="Ведутся технические работы"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
45
app/bot.py
45
app/bot.py
@@ -9,6 +9,8 @@ from app.middlewares.auth import AuthMiddleware
|
||||
from app.middlewares.logging import LoggingMiddleware
|
||||
from app.middlewares.throttling import ThrottlingMiddleware
|
||||
from app.middlewares.subscription_checker import SubscriptionStatusMiddleware
|
||||
from app.middlewares.maintenance import MaintenanceMiddleware
|
||||
from app.services.maintenance_service import maintenance_service
|
||||
from app.utils.cache import cache
|
||||
|
||||
from app.handlers import (
|
||||
@@ -20,7 +22,8 @@ from app.handlers.admin import (
|
||||
promocodes as admin_promocodes, messages as admin_messages,
|
||||
monitoring as admin_monitoring, referrals as admin_referrals,
|
||||
rules as admin_rules, remnawave as admin_remnawave,
|
||||
statistics as admin_statistics, servers as admin_servers
|
||||
statistics as admin_statistics, servers as admin_servers,
|
||||
maintenance as admin_maintenance
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,9 +40,9 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
|
||||
try:
|
||||
await cache.connect()
|
||||
logger.info("✅ Кеш инициализирован")
|
||||
logger.info("Кеш инициализирован")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Кеш не инициализирован: {e}")
|
||||
logger.warning(f"Кеш не инициализирован: {e}")
|
||||
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
@@ -53,24 +56,23 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
redis_client = redis.from_url(settings.REDIS_URL)
|
||||
await redis_client.ping()
|
||||
storage = RedisStorage(redis_client)
|
||||
logger.info("✅ Подключено к Redis для FSM storage")
|
||||
logger.info("Подключено к Redis для FSM storage")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ Не удалось подключиться к Redis: {e}")
|
||||
logger.info("🔄 Используется MemoryStorage для FSM")
|
||||
logger.warning(f"Не удалось подключиться к Redis: {e}")
|
||||
logger.info("Используется MemoryStorage для FSM")
|
||||
storage = MemoryStorage()
|
||||
|
||||
dp = Dispatcher(storage=storage)
|
||||
|
||||
dp.message.middleware(LoggingMiddleware())
|
||||
dp.callback_query.middleware(LoggingMiddleware())
|
||||
dp.message.middleware(AuthMiddleware())
|
||||
dp.callback_query.middleware(AuthMiddleware())
|
||||
dp.message.middleware(MaintenanceMiddleware())
|
||||
dp.callback_query.middleware(MaintenanceMiddleware())
|
||||
dp.message.middleware(ThrottlingMiddleware())
|
||||
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)
|
||||
@@ -78,7 +80,6 @@ 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)
|
||||
@@ -90,9 +91,31 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_rules.register_handlers(dp)
|
||||
admin_remnawave.register_handlers(dp)
|
||||
admin_statistics.register_handlers(dp)
|
||||
admin_maintenance.register_handlers(dp)
|
||||
|
||||
common.register_handlers(dp)
|
||||
|
||||
logger.info("✅ Бот успешно настроен")
|
||||
try:
|
||||
await maintenance_service.start_monitoring()
|
||||
logger.info("Мониторинг техработ запущен")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запуска мониторинга техработ: {e}")
|
||||
|
||||
logger.info("Бот успешно настроен")
|
||||
|
||||
return bot, dp
|
||||
|
||||
|
||||
async def shutdown_bot():
|
||||
"""Корректное завершение работы бота"""
|
||||
try:
|
||||
await maintenance_service.stop_monitoring()
|
||||
logger.info("Мониторинг техработ остановлен")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки мониторинга: {e}")
|
||||
|
||||
try:
|
||||
await cache.close()
|
||||
logger.info("Соединения с кешем закрыты")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка закрытия кеша: {e}")
|
||||
|
||||
@@ -65,6 +65,11 @@ class Settings(BaseSettings):
|
||||
|
||||
MONITORING_INTERVAL: int = 60
|
||||
INACTIVE_USER_DELETE_MONTHS: int = 3
|
||||
|
||||
MAINTENANCE_MODE: bool = False
|
||||
MAINTENANCE_CHECK_INTERVAL: int = 30
|
||||
MAINTENANCE_AUTO_ENABLE: bool = True
|
||||
MAINTENANCE_MESSAGE: str = "🔧 Ведутся технические работы. Сервис временно недоступен. Попробуйте позже."
|
||||
|
||||
TELEGRAM_STARS_ENABLED: bool = True
|
||||
|
||||
@@ -199,6 +204,18 @@ class Settings(BaseSettings):
|
||||
elif self.WEBHOOK_URL:
|
||||
return f"{self.WEBHOOK_URL}/payment-success"
|
||||
return "https://t.me/"
|
||||
|
||||
def is_maintenance_mode(self) -> bool:
|
||||
return self.MAINTENANCE_MODE
|
||||
|
||||
def get_maintenance_message(self) -> str:
|
||||
return self.MAINTENANCE_MESSAGE
|
||||
|
||||
def get_maintenance_check_interval(self) -> int:
|
||||
return self.MAINTENANCE_CHECK_INTERVAL
|
||||
|
||||
def is_maintenance_auto_enable(self) -> bool:
|
||||
return self.MAINTENANCE_AUTO_ENABLE
|
||||
|
||||
model_config = {
|
||||
"env_file": ".env",
|
||||
|
||||
235
app/handlers/admin/maintenance.py
Normal file
235
app/handlers/admin/maintenance.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import logging
|
||||
from aiogram import Dispatcher, types, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.models import User
|
||||
from app.services.maintenance_service import maintenance_service
|
||||
from app.keyboards.admin import get_maintenance_keyboard, get_admin_main_keyboard
|
||||
from app.localization.texts import get_texts
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MaintenanceStates(StatesGroup):
|
||||
waiting_for_reason = State()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_maintenance_panel(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
status_info = maintenance_service.get_status_info()
|
||||
|
||||
status_emoji = "🔧" if status_info["is_active"] else "✅"
|
||||
status_text = "Включен" if status_info["is_active"] else "Выключен"
|
||||
|
||||
api_emoji = "✅" if status_info["api_status"] else "❌"
|
||||
api_text = "Доступно" if status_info["api_status"] else "Недоступно"
|
||||
|
||||
monitoring_emoji = "🔄" if status_info["monitoring_active"] else "⏹️"
|
||||
monitoring_text = "Запущен" if status_info["monitoring_active"] else "Остановлен"
|
||||
|
||||
enabled_info = ""
|
||||
if status_info["is_active"] and status_info["enabled_at"]:
|
||||
enabled_time = status_info["enabled_at"].strftime("%d.%m.%Y %H:%M:%S")
|
||||
enabled_info = f"\n📅 <b>Включен:</b> {enabled_time}"
|
||||
if status_info["reason"]:
|
||||
enabled_info += f"\n📝 <b>Причина:</b> {status_info['reason']}"
|
||||
|
||||
last_check_info = ""
|
||||
if status_info["last_check"]:
|
||||
last_check_time = status_info["last_check"].strftime("%H:%M:%S")
|
||||
last_check_info = f"\n🕐 <b>Последняя проверка:</b> {last_check_time}"
|
||||
|
||||
failures_info = ""
|
||||
if status_info["consecutive_failures"] > 0:
|
||||
failures_info = f"\n⚠️ <b>Неудачных проверок подряд:</b> {status_info['consecutive_failures']}"
|
||||
|
||||
message_text = f"""
|
||||
🔧 <b>Режим технических работ</b>
|
||||
|
||||
{status_emoji} <b>Статус:</b> {status_text}
|
||||
{api_emoji} <b>API RemnaWave:</b> {api_text}
|
||||
{monitoring_emoji} <b>Мониторинг:</b> {monitoring_text}
|
||||
⏱️ <b>Интервал проверки:</b> {status_info['check_interval']}с
|
||||
🤖 <b>Автовключение:</b> {'Включено' if status_info['auto_enable_configured'] else 'Отключено'}
|
||||
{enabled_info}
|
||||
{last_check_info}
|
||||
{failures_info}
|
||||
|
||||
ℹ️ <i>В режиме техработ обычные пользователи не могут использовать бота. Администраторы имеют полный доступ.</i>
|
||||
"""
|
||||
|
||||
await callback.message.edit_text(
|
||||
message_text,
|
||||
reply_markup=get_maintenance_keyboard(db_user.language, status_info["is_active"], status_info["monitoring_active"])
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def toggle_maintenance_mode(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext
|
||||
):
|
||||
is_active = maintenance_service.is_maintenance_active()
|
||||
|
||||
if is_active:
|
||||
success = await maintenance_service.disable_maintenance()
|
||||
if success:
|
||||
await callback.answer("Режим техработ выключен", show_alert=True)
|
||||
else:
|
||||
await callback.answer("Ошибка выключения режима техработ", show_alert=True)
|
||||
else:
|
||||
await state.set_state(MaintenanceStates.waiting_for_reason)
|
||||
await callback.message.edit_text(
|
||||
"🔧 <b>Включение режима техработ</b>\n\nВведите причину включения техработ или отправьте /skip для пропуска:",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="❌ Отмена", callback_data="maintenance_panel")]
|
||||
])
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_maintenance_reason(
|
||||
message: types.Message,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext
|
||||
):
|
||||
current_state = await state.get_state()
|
||||
|
||||
if current_state != MaintenanceStates.waiting_for_reason:
|
||||
return
|
||||
|
||||
reason = None
|
||||
if message.text and message.text != "/skip":
|
||||
reason = message.text[:200]
|
||||
|
||||
success = await maintenance_service.enable_maintenance(reason=reason, auto=False)
|
||||
|
||||
if success:
|
||||
response_text = "Режим техработ включен"
|
||||
if reason:
|
||||
response_text += f"\nПричина: {reason}"
|
||||
else:
|
||||
response_text = "Ошибка включения режима техработ"
|
||||
|
||||
await message.answer(response_text)
|
||||
await state.clear()
|
||||
|
||||
status_info = maintenance_service.get_status_info()
|
||||
await message.answer(
|
||||
"Вернуться к панели управления техработами:",
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text="🔧 Панель техработ", callback_data="maintenance_panel")]
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def toggle_monitoring(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
status_info = maintenance_service.get_status_info()
|
||||
|
||||
if status_info["monitoring_active"]:
|
||||
success = await maintenance_service.stop_monitoring()
|
||||
message = "Мониторинг остановлен" if success else "Ошибка остановки мониторинга"
|
||||
else:
|
||||
success = await maintenance_service.start_monitoring()
|
||||
message = "Мониторинг запущен" if success else "Ошибка запуска мониторинга"
|
||||
|
||||
await callback.answer(message, show_alert=True)
|
||||
|
||||
await show_maintenance_panel(callback, db_user, db, None)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def force_api_check(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
await callback.answer("Проверка API...", show_alert=False)
|
||||
|
||||
check_result = await maintenance_service.force_api_check()
|
||||
|
||||
if check_result["success"]:
|
||||
status_text = "доступно" if check_result["api_available"] else "недоступно"
|
||||
message = f"API {status_text}\nВремя ответа: {check_result['response_time']}с"
|
||||
else:
|
||||
message = f"Ошибка проверки: {check_result.get('error', 'Неизвестная ошибка')}"
|
||||
|
||||
await callback.message.answer(message)
|
||||
|
||||
await show_maintenance_panel(callback, db_user, db, None)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def back_to_admin_panel(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.ADMIN_PANEL,
|
||||
reply_markup=get_admin_main_keyboard(db_user.language)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
|
||||
dp.callback_query.register(
|
||||
show_maintenance_panel,
|
||||
F.data == "maintenance_panel"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
toggle_maintenance_mode,
|
||||
F.data == "maintenance_toggle"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
toggle_monitoring,
|
||||
F.data == "maintenance_monitoring"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
force_api_check,
|
||||
F.data == "maintenance_check_api"
|
||||
)
|
||||
|
||||
dp.callback_query.register(
|
||||
back_to_admin_panel,
|
||||
F.data == "admin_panel"
|
||||
)
|
||||
|
||||
dp.message.register(
|
||||
process_maintenance_reason,
|
||||
MaintenanceStates.waiting_for_reason
|
||||
)
|
||||
@@ -25,7 +25,8 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text=texts.ADMIN_REMNAWAVE, callback_data="admin_remnawave")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=texts.ADMIN_STATISTICS, callback_data="admin_statistics")
|
||||
InlineKeyboardButton(text=texts.ADMIN_STATISTICS, callback_data="admin_statistics"),
|
||||
InlineKeyboardButton(text="🔧 Техработы", callback_data="maintenance_panel")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")
|
||||
@@ -575,3 +576,26 @@ def get_admin_pagination_keyboard(
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
def get_maintenance_keyboard(language: str = "ru", is_active: bool = False, monitoring_active: bool = False) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура для управления техработами"""
|
||||
|
||||
if language == "en":
|
||||
toggle_text = "🔴 Disable maintenance" if is_active else "🔧 Enable maintenance"
|
||||
monitoring_text = "⏹️ Stop monitoring" if monitoring_active else "🔄 Start monitoring"
|
||||
check_api_text = "🔍 Check API"
|
||||
back_text = "⬅️ Back to admin"
|
||||
else:
|
||||
toggle_text = "🔴 Выключить техработы" if is_active else "🔧 Включить техработы"
|
||||
monitoring_text = "⏹️ Остановить мониторинг" if monitoring_active else "🔄 Запустить мониторинг"
|
||||
check_api_text = "🔍 Проверить API"
|
||||
back_text = "⬅️ Назад в админку"
|
||||
|
||||
keyboard = [
|
||||
[InlineKeyboardButton(text=toggle_text, callback_data="maintenance_toggle")],
|
||||
[InlineKeyboardButton(text=monitoring_text, callback_data="maintenance_monitoring")],
|
||||
[InlineKeyboardButton(text=check_api_text, callback_data="maintenance_check_api")],
|
||||
[InlineKeyboardButton(text=back_text, callback_data="admin_panel")]
|
||||
]
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
|
||||
@@ -280,6 +280,27 @@ class RussianTexts(Texts):
|
||||
• Поддержка до 3 устройств
|
||||
|
||||
⚡️ Успейте оформить до окончания тестового периода!
|
||||
"""
|
||||
|
||||
MAINTENANCE_MODE_ACTIVE = """
|
||||
🔧 Технические работы!
|
||||
|
||||
Сервис временно недоступен. Ведутся технические работы по улучшению качества обслуживания.
|
||||
|
||||
⏰ Ориентировочное время завершения: неизвестно
|
||||
🔄 Попробуйте позже
|
||||
|
||||
Приносим извинения за временные неудобства.
|
||||
"""
|
||||
|
||||
MAINTENANCE_MODE_API_ERROR = """
|
||||
🔧 Технические работы!
|
||||
|
||||
Сервис временно недоступен из-за проблем с подключением к серверам.
|
||||
|
||||
⏰ Мы работаем над восстановлением. Попробуйте через несколько минут.
|
||||
|
||||
🔄 Последняя проверка: {last_check}
|
||||
"""
|
||||
|
||||
SUBSCRIPTION_EXPIRING_PAID = """
|
||||
|
||||
45
app/middlewares/maintenance.py
Normal file
45
app/middlewares/maintenance.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import logging
|
||||
from typing import Callable, Dict, Any, Awaitable
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Message, CallbackQuery, TelegramObject, User as TgUser
|
||||
|
||||
from app.config import settings
|
||||
from app.services.maintenance_service import maintenance_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MaintenanceMiddleware(BaseMiddleware):
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
|
||||
user: TgUser = None
|
||||
if isinstance(event, (Message, CallbackQuery)):
|
||||
user = event.from_user
|
||||
|
||||
if not user or user.is_bot:
|
||||
return await handler(event, data)
|
||||
|
||||
if not maintenance_service.is_maintenance_active():
|
||||
return await handler(event, data)
|
||||
|
||||
if settings.is_admin(user.id):
|
||||
return await handler(event, data)
|
||||
|
||||
maintenance_message = maintenance_service.get_maintenance_message()
|
||||
|
||||
try:
|
||||
if isinstance(event, Message):
|
||||
await event.answer(maintenance_message, parse_mode="HTML")
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.answer(maintenance_message, show_alert=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки сообщения о техработах пользователю {user.id}: {e}")
|
||||
|
||||
logger.info(f"🔧 Пользователь {user.id} заблокирован во время техработ")
|
||||
return
|
||||
268
app/services/maintenance_service.py
Normal file
268
app/services/maintenance_service.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.config import settings
|
||||
from app.external.remnawave_api import RemnaWaveAPI, test_api_connection
|
||||
from app.utils.cache import cache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MaintenanceStatus:
|
||||
is_active: bool
|
||||
enabled_at: Optional[datetime] = None
|
||||
last_check: Optional[datetime] = None
|
||||
reason: Optional[str] = None
|
||||
auto_enabled: bool = False
|
||||
api_status: bool = True
|
||||
consecutive_failures: int = 0
|
||||
|
||||
|
||||
class MaintenanceService:
|
||||
|
||||
def __init__(self):
|
||||
self._status = MaintenanceStatus(is_active=False)
|
||||
self._check_task: Optional[asyncio.Task] = None
|
||||
self._is_checking = False
|
||||
self._max_consecutive_failures = 3
|
||||
|
||||
@property
|
||||
def status(self) -> MaintenanceStatus:
|
||||
return self._status
|
||||
|
||||
def is_maintenance_active(self) -> bool:
|
||||
return self._status.is_active
|
||||
|
||||
def get_maintenance_message(self) -> str:
|
||||
if self._status.auto_enabled:
|
||||
return f"""
|
||||
🔧 <b>Технические работы</b>
|
||||
|
||||
Сервис временно недоступен из-за проблем с подключением к серверам.
|
||||
|
||||
⏰ Мы работаем над восстановлением. Попробуйте через несколько минут.
|
||||
|
||||
🔄 Последняя проверка: {self._status.last_check.strftime('%H:%M:%S') if self._status.last_check else 'неизвестно'}
|
||||
"""
|
||||
else:
|
||||
return settings.get_maintenance_message()
|
||||
|
||||
async def enable_maintenance(self, reason: Optional[str] = None, auto: bool = False) -> bool:
|
||||
try:
|
||||
if self._status.is_active:
|
||||
logger.warning("Режим техработ уже включен")
|
||||
return True
|
||||
|
||||
self._status.is_active = True
|
||||
self._status.enabled_at = datetime.utcnow()
|
||||
self._status.reason = reason or ("Автоматическое включение" if auto else "Включено администратором")
|
||||
self._status.auto_enabled = auto
|
||||
|
||||
await self._save_status_to_cache()
|
||||
|
||||
logger.warning(f"🔧 Режим техработ ВКЛЮЧЕН. Причина: {self._status.reason}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка включения режима техработ: {e}")
|
||||
return False
|
||||
|
||||
async def disable_maintenance(self) -> bool:
|
||||
try:
|
||||
if not self._status.is_active:
|
||||
logger.info("Режим техработ уже выключен")
|
||||
return True
|
||||
|
||||
self._status.is_active = False
|
||||
self._status.enabled_at = None
|
||||
self._status.reason = None
|
||||
self._status.auto_enabled = False
|
||||
self._status.consecutive_failures = 0
|
||||
|
||||
await self._save_status_to_cache()
|
||||
|
||||
logger.info("✅ Режим техработ ВЫКЛЮЧЕН")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка выключения режима техработ: {e}")
|
||||
return False
|
||||
|
||||
async def start_monitoring(self) -> bool:
|
||||
try:
|
||||
if self._check_task and not self._check_task.done():
|
||||
logger.warning("Мониторинг уже запущен")
|
||||
return True
|
||||
|
||||
await self._load_status_from_cache()
|
||||
|
||||
self._check_task = asyncio.create_task(self._monitoring_loop())
|
||||
logger.info(f"🔄 Запущен мониторинг API RemnaWave (интервал: {settings.get_maintenance_check_interval()}с)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запуска мониторинга: {e}")
|
||||
return False
|
||||
|
||||
async def stop_monitoring(self) -> bool:
|
||||
try:
|
||||
if self._check_task and not self._check_task.done():
|
||||
self._check_task.cancel()
|
||||
try:
|
||||
await self._check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
logger.info("⏹️ Мониторинг API остановлен")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки мониторинга: {e}")
|
||||
return False
|
||||
|
||||
async def check_api_status(self) -> bool:
|
||||
try:
|
||||
if self._is_checking:
|
||||
return self._status.api_status
|
||||
|
||||
self._is_checking = True
|
||||
self._status.last_check = datetime.utcnow()
|
||||
|
||||
api = RemnaWaveAPI(settings.REMNAWAVE_API_URL, settings.REMNAWAVE_API_KEY)
|
||||
|
||||
async with api:
|
||||
is_connected = await test_api_connection(api)
|
||||
|
||||
if is_connected:
|
||||
self._status.api_status = True
|
||||
self._status.consecutive_failures = 0
|
||||
|
||||
if self._status.is_active and self._status.auto_enabled:
|
||||
await self.disable_maintenance()
|
||||
logger.info("✅ API восстановился, режим техработ автоматически отключен")
|
||||
|
||||
return True
|
||||
else:
|
||||
self._status.api_status = False
|
||||
self._status.consecutive_failures += 1
|
||||
|
||||
if (self._status.consecutive_failures >= self._max_consecutive_failures and
|
||||
not self._status.is_active and
|
||||
settings.is_maintenance_auto_enable()):
|
||||
|
||||
await self.enable_maintenance(
|
||||
reason=f"Автоматическое включение после {self._status.consecutive_failures} неудачных проверок API",
|
||||
auto=True
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки API: {e}")
|
||||
self._status.api_status = False
|
||||
self._status.consecutive_failures += 1
|
||||
return False
|
||||
finally:
|
||||
self._is_checking = False
|
||||
await self._save_status_to_cache()
|
||||
|
||||
async def _monitoring_loop(self):
|
||||
while True:
|
||||
try:
|
||||
await self.check_api_status()
|
||||
await asyncio.sleep(settings.get_maintenance_check_interval())
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Мониторинг отменен")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в цикле мониторинга: {e}")
|
||||
await asyncio.sleep(30)
|
||||
|
||||
async def _save_status_to_cache(self):
|
||||
try:
|
||||
status_data = {
|
||||
"is_active": self._status.is_active,
|
||||
"enabled_at": self._status.enabled_at.isoformat() if self._status.enabled_at else None,
|
||||
"reason": self._status.reason,
|
||||
"auto_enabled": self._status.auto_enabled,
|
||||
"consecutive_failures": self._status.consecutive_failures,
|
||||
"last_check": self._status.last_check.isoformat() if self._status.last_check else None
|
||||
}
|
||||
|
||||
await cache.set("maintenance_status", status_data, expire=3600)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сохранения состояния в кеш: {e}")
|
||||
|
||||
async def _load_status_from_cache(self):
|
||||
try:
|
||||
status_data = await cache.get("maintenance_status")
|
||||
if not status_data:
|
||||
return
|
||||
|
||||
self._status.is_active = status_data.get("is_active", False)
|
||||
self._status.reason = status_data.get("reason")
|
||||
self._status.auto_enabled = status_data.get("auto_enabled", False)
|
||||
self._status.consecutive_failures = status_data.get("consecutive_failures", 0)
|
||||
|
||||
if status_data.get("enabled_at"):
|
||||
self._status.enabled_at = datetime.fromisoformat(status_data["enabled_at"])
|
||||
|
||||
if status_data.get("last_check"):
|
||||
self._status.last_check = datetime.fromisoformat(status_data["last_check"])
|
||||
|
||||
logger.info(f"📥 Состояние техработ загружено из кеша: активен={self._status.is_active}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка загрузки состояния из кеша: {e}")
|
||||
|
||||
def get_status_info(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"is_active": self._status.is_active,
|
||||
"enabled_at": self._status.enabled_at,
|
||||
"last_check": self._status.last_check,
|
||||
"reason": self._status.reason,
|
||||
"auto_enabled": self._status.auto_enabled,
|
||||
"api_status": self._status.api_status,
|
||||
"consecutive_failures": self._status.consecutive_failures,
|
||||
"monitoring_active": self._check_task is not None and not self._check_task.done(),
|
||||
"auto_enable_configured": settings.is_maintenance_auto_enable(),
|
||||
"check_interval": settings.get_maintenance_check_interval()
|
||||
}
|
||||
|
||||
async def force_api_check(self) -> Dict[str, Any]:
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
try:
|
||||
api_status = await self.check_api_status()
|
||||
end_time = datetime.utcnow()
|
||||
response_time = (end_time - start_time).total_seconds()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"api_available": api_status,
|
||||
"response_time": round(response_time, 2),
|
||||
"checked_at": end_time,
|
||||
"consecutive_failures": self._status.consecutive_failures
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
end_time = datetime.utcnow()
|
||||
response_time = (end_time - start_time).total_seconds()
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"api_available": False,
|
||||
"error": str(e),
|
||||
"response_time": round(response_time, 2),
|
||||
"checked_at": end_time,
|
||||
"consecutive_failures": self._status.consecutive_failures
|
||||
}
|
||||
|
||||
|
||||
maintenance_service = MaintenanceService()
|
||||
95
main.py
95
main.py
@@ -2,6 +2,7 @@ import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import signal
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(str(Path(__file__).parent))
|
||||
@@ -10,10 +11,21 @@ from app.bot import setup_bot
|
||||
from app.config import settings
|
||||
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.external.webhook_server import WebhookServer
|
||||
from app.database.universal_migration import run_universal_migration
|
||||
|
||||
|
||||
class GracefulExit:
|
||||
|
||||
def __init__(self):
|
||||
self.exit = False
|
||||
|
||||
def exit_gracefully(self, signum, frame):
|
||||
logging.getLogger(__name__).info(f"Получен сигнал {signum}. Корректное завершение работы...")
|
||||
self.exit = True
|
||||
|
||||
|
||||
async def main():
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, settings.LOG_LEVEL),
|
||||
@@ -27,7 +39,14 @@ async def main():
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("🚀 Запуск Bedolaga Remnawave Bot...")
|
||||
|
||||
killer = GracefulExit()
|
||||
signal.signal(signal.SIGINT, killer.exit_gracefully)
|
||||
signal.signal(signal.SIGTERM, killer.exit_gracefully)
|
||||
|
||||
webhook_server = None
|
||||
monitoring_task = None
|
||||
maintenance_task = None
|
||||
polling_task = None
|
||||
|
||||
try:
|
||||
logger.info("📊 Инициализация базы данных...")
|
||||
@@ -66,28 +85,82 @@ async def main():
|
||||
logger.info("🔍 Запуск службы мониторинга...")
|
||||
monitoring_task = asyncio.create_task(monitoring_service.start_monitoring())
|
||||
|
||||
logger.info("🔧 Запуск службы техработ...")
|
||||
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
|
||||
|
||||
logger.info("🔄 Запуск polling...")
|
||||
polling_task = asyncio.create_task(dp.start_polling(bot, skip_updates=True))
|
||||
|
||||
try:
|
||||
await asyncio.gather(
|
||||
dp.start_polling(bot),
|
||||
monitoring_task
|
||||
)
|
||||
while not killer.exit:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if monitoring_task.done():
|
||||
exception = monitoring_task.exception()
|
||||
if exception:
|
||||
logger.error(f"Служба мониторинга завершилась с ошибкой: {exception}")
|
||||
monitoring_task = asyncio.create_task(monitoring_service.start_monitoring())
|
||||
|
||||
if maintenance_task.done():
|
||||
exception = maintenance_task.exception()
|
||||
if exception:
|
||||
logger.error(f"Служба техработ завершилась с ошибкой: {exception}")
|
||||
maintenance_task = asyncio.create_task(maintenance_service.start_monitoring())
|
||||
|
||||
if polling_task.done():
|
||||
exception = polling_task.exception()
|
||||
if exception:
|
||||
logger.error(f"Polling завершился с ошибкой: {exception}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в основном цикле: {e}")
|
||||
monitoring_service.stop_monitoring()
|
||||
if webhook_server:
|
||||
await webhook_server.stop()
|
||||
raise
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Критическая ошибка при запуске: {e}")
|
||||
raise
|
||||
|
||||
finally:
|
||||
logger.info("🛑 Завершение работы бота")
|
||||
monitoring_service.stop_monitoring()
|
||||
logger.info("🛑 Начинается корректное завершение работы...")
|
||||
|
||||
if monitoring_task and not monitoring_task.done():
|
||||
logger.info("⏹️ Остановка службы мониторинга...")
|
||||
monitoring_service.stop_monitoring()
|
||||
monitoring_task.cancel()
|
||||
try:
|
||||
await monitoring_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if maintenance_task and not maintenance_task.done():
|
||||
logger.info("⏹️ Остановка службы техработ...")
|
||||
await maintenance_service.stop_monitoring()
|
||||
maintenance_task.cancel()
|
||||
try:
|
||||
await maintenance_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if polling_task and not polling_task.done():
|
||||
logger.info("⏹️ Остановка polling...")
|
||||
polling_task.cancel()
|
||||
try:
|
||||
await polling_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if webhook_server:
|
||||
logger.info("⏹️ Остановка webhook сервера...")
|
||||
await webhook_server.stop()
|
||||
|
||||
if 'bot' in locals():
|
||||
try:
|
||||
await bot.session.close()
|
||||
logger.info("✅ Сессия бота закрыта")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка закрытия сессии бота: {e}")
|
||||
|
||||
logger.info("✅ Завершение работы бота завершено")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -18,6 +18,9 @@ yookassa==3.0.0
|
||||
# Логирование и мониторинг
|
||||
structlog==23.2.0
|
||||
|
||||
# Планировщик задач для техработ
|
||||
APScheduler==3.10.4
|
||||
|
||||
# Утилиты
|
||||
python-dateutil==2.8.2
|
||||
pytz==2023.4
|
||||
|
||||
Reference in New Issue
Block a user