Merge pull request #2490 from BEDOLAGA-DEV/dev

Dev
This commit is contained in:
Egor
2026-02-02 01:07:29 +03:00
committed by GitHub
2 changed files with 154 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
"""
Сервис стартового уведомления бота.
Отправляет красивое сообщение с информацией о системе при запуске бота.
"""
import logging
@@ -8,12 +9,12 @@ import os
from datetime import datetime
from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import func, select
from app.config import settings
from app.database.database import AsyncSessionLocal
from app.database.models import User, UserStatus
from app.database.models import Subscription, SubscriptionStatus, Ticket, TicketStatus, User, UserStatus
from app.external.remnawave_api import RemnaWaveAPI, test_api_connection
from app.utils.timezone import format_local_datetime
@@ -61,9 +62,44 @@ class StartupNotificationService:
logger.error(f'Ошибка получения суммы балансов: {e}')
return 0
async def _get_open_tickets_count(self) -> int:
"""Получает количество открытых тикетов."""
try:
async with AsyncSessionLocal() as db:
result = await db.execute(select(func.count(Ticket.id)).where(Ticket.status == TicketStatus.OPEN.value))
return result.scalar() or 0
except Exception as e:
logger.error(f'Ошибка получения количества открытых тикетов: {e}')
return 0
async def _get_paid_subscriptions_count(self) -> int:
"""Получает количество платных подписок (не триальных, активных)."""
try:
async with AsyncSessionLocal() as db:
result = await db.execute(
select(func.count(Subscription.id)).where(
Subscription.is_trial == False,
Subscription.status == SubscriptionStatus.ACTIVE.value,
)
)
return result.scalar() or 0
except Exception as e:
logger.error(f'Ошибка получения количества платных подписок: {e}')
return 0
async def _get_trial_subscriptions_count(self) -> int:
"""Получает количество триальных подписок."""
try:
async with AsyncSessionLocal() as db:
result = await db.execute(select(func.count(Subscription.id)).where(Subscription.is_trial == True))
return result.scalar() or 0
except Exception as e:
logger.error(f'Ошибка получения количества триальных подписок: {e}')
return 0
async def _check_remnawave_connection(self) -> tuple[bool, str]:
"""
Проверяет соединение с панелью RemnaWave.
Проверяет соединение с панелью Remnawave.
Returns:
Tuple[bool, str]: (is_connected, status_message)
@@ -99,7 +135,7 @@ class StartupNotificationService:
return False, 'Недоступна'
except Exception as e:
logger.error(f'Ошибка проверки соединения с RemnaWave: {e}')
logger.error(f'Ошибка проверки соединения с Remnawave: {e}')
return False, 'Ошибка подключения'
def _format_balance(self, kopeks: int) -> str:
@@ -126,9 +162,12 @@ class StartupNotificationService:
version = self._get_version()
users_count = await self._get_users_count()
total_balance_kopeks = await self._get_total_balance()
open_tickets_count = await self._get_open_tickets_count()
paid_subscriptions_count = await self._get_paid_subscriptions_count()
trial_subscriptions_count = await self._get_trial_subscriptions_count()
remnawave_connected, remnawave_status = await self._check_remnawave_connection()
# Иконка статуса RemnaWave
# Иконка статуса Remnawave
remnawave_icon = '🟢' if remnawave_connected else '🔴'
# Формируем системную информацию для blockquote
@@ -136,7 +175,10 @@ class StartupNotificationService:
f'Версия: {version}',
f'Пользователей: {users_count:,}'.replace(',', ' '),
f'Сумма балансов: {self._format_balance(total_balance_kopeks)}',
f'{remnawave_icon} RemnaWave: {remnawave_status}',
f'Платных подписок: {paid_subscriptions_count:,}'.replace(',', ' '),
f'Триальных подписок: {trial_subscriptions_count:,}'.replace(',', ' '),
f'Открытых тикетов: {open_tickets_count:,}'.replace(',', ' '),
f'{remnawave_icon} Remnawave: {remnawave_status}',
]
system_info = '\n'.join(system_info_lines)
@@ -144,6 +186,7 @@ class StartupNotificationService:
message = (
f'<b>Remnawave Bedolaga Bot</b>\n\n'
f'✅ Бот успешно запущен\n\n'
f'<blockquote expandable>{system_info}</blockquote>\n\n'
f'<i>{timestamp}</i>'
)
@@ -203,3 +246,76 @@ async def send_bot_startup_notification(bot: Bot) -> bool:
"""
service = StartupNotificationService(bot)
return await service.send_startup_notification()
async def send_crash_notification(bot: Bot, error: Exception, traceback_str: str) -> bool:
"""
Отправляет уведомление о падении бота с лог-файлом.
Args:
bot: Экземпляр бота aiogram
error: Исключение, вызвавшее падение
traceback_str: Строка с полным traceback
Returns:
bool: True если уведомление отправлено успешно
"""
chat_id = getattr(settings, 'ADMIN_NOTIFICATIONS_CHAT_ID', None)
topic_id = getattr(settings, 'ADMIN_NOTIFICATIONS_TOPIC_ID', None)
enabled = getattr(settings, 'ADMIN_NOTIFICATIONS_ENABLED', False)
if not enabled or not chat_id:
logger.debug('Уведомление о падении отключено или chat_id не задан')
return False
try:
timestamp = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')
error_type = type(error).__name__
error_message = str(error)
# Формируем содержимое лог-файла
log_content = (
f'CRASH REPORT\n'
f'{"=" * 50}\n\n'
f'Timestamp: {timestamp}\n'
f'Error Type: {error_type}\n'
f'Error Message: {error_message}\n\n'
f'{"=" * 50}\n'
f'TRACEBACK\n'
f'{"=" * 50}\n\n'
f'{traceback_str}\n'
)
# Создаем файл для отправки
file_name = f'crash_report_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.txt'
file = BufferedInputFile(
file=log_content.encode('utf-8'),
filename=file_name,
)
# Текст сообщения
message_text = (
f'<b>Remnawave Bedolaga Bot</b>\n\n'
f'❌ Бот упал с ошибкой\n\n'
f'<b>Тип:</b> <code>{error_type}</code>\n'
f'<b>Сообщение:</b> <code>{error_message[:200]}</code>\n\n'
f'<i>{timestamp}</i>'
)
message_kwargs: dict = {
'chat_id': chat_id,
'document': file,
'caption': message_text,
'parse_mode': 'HTML',
}
if topic_id:
message_kwargs['message_thread_id'] = topic_id
await bot.send_document(**message_kwargs)
logger.info(f'Уведомление о падении отправлено в чат {chat_id}')
return True
except Exception as e:
logger.error(f'Ошибка отправки уведомления о падении: {e}')
return False

32
main.py
View File

@@ -915,6 +915,30 @@ async def main():
logger.info('✅ Завершение работы бота завершено')
async def _send_crash_notification_on_error(error: Exception) -> None:
"""Отправляет уведомление о падении бота в админский чат."""
import traceback
from app.config import settings
if not getattr(settings, 'BOT_TOKEN', None):
return
try:
from aiogram import Bot
from app.services.startup_notification_service import send_crash_notification
bot = Bot(token=settings.BOT_TOKEN)
try:
traceback_str = traceback.format_exc()
await send_crash_notification(bot, error, traceback_str)
finally:
await bot.session.close()
except Exception as notify_error:
print(f'⚠️ Не удалось отправить уведомление о падении: {notify_error}')
if __name__ == '__main__':
try:
asyncio.run(main())
@@ -922,4 +946,12 @@ if __name__ == '__main__':
print('\n🛑 Бот остановлен пользователем')
except Exception as e:
print(f'❌ Критическая ошибка: {e}')
import traceback
traceback.print_exc()
# Пытаемся отправить уведомление о падении
try:
asyncio.run(_send_crash_notification_on_error(e))
except Exception:
pass
sys.exit(1)