diff --git a/backup.py b/backup.py index 2b142f4b..98b48b55 100644 --- a/backup.py +++ b/backup.py @@ -5,9 +5,13 @@ from pathlib import Path import aiofiles from aiogram.types import BufferedInputFile +from aiogram import Bot from bot import bot -from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT +from config import ( + ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT, + BACKUP_SEND_MODE, BACKUP_CHANNEL_ID, BACKUP_CHANNEL_THREAD_ID, BACKUP_OTHER_BOT_TOKEN, BACKUP_CAPTION +) from logger import logger @@ -139,20 +143,67 @@ async def _send_backup_to_admins(backup_file_path: str) -> None: if not backup_file_path or not os.path.exists(backup_file_path): raise FileNotFoundError(f"Файл бэкапа не найден: {backup_file_path}") + async def send_default(): + for admin_id in ADMIN_ID: + try: + await bot.send_document( + chat_id=admin_id, document=backup_input_file + ) + logger.info(f"Бэкап базы данных отправлен админу: {admin_id}") + except Exception as e: + logger.error(f"Не удалось отправить бэкап админу {admin_id}: {e}") + try: async with aiofiles.open(backup_file_path, "rb") as backup_file: backup_data = await backup_file.read() filename = os.path.basename(backup_file_path) backup_input_file = BufferedInputFile(file=backup_data, filename=filename) - for admin_id in ADMIN_ID: + if BACKUP_SEND_MODE == "default": + await send_default() + + elif BACKUP_SEND_MODE == "channel": + channel_id = BACKUP_CHANNEL_ID.strip() + thread_id = BACKUP_CHANNEL_THREAD_ID.strip() + if not channel_id: + logger.error("BACKUP_CHANNEL_ID не задан для режима 'channel', fallback на default") + await send_default() + return + send_kwargs = dict(chat_id=channel_id, document=backup_input_file) + if thread_id: + send_kwargs["message_thread_id"] = int(thread_id) + if BACKUP_CAPTION: + send_kwargs["caption"] = BACKUP_CAPTION try: - await bot.send_document( - chat_id=admin_id, document=backup_input_file - ) - logger.info(f"Бэкап базы данных отправлен админу: {admin_id}") + await bot.send_document(**send_kwargs) + logger.info(f"Бэкап базы данных отправлен в канал: {channel_id} (топик: {thread_id})") except Exception as e: - logger.error(f"Не удалось отправить бэкап админу {admin_id}: {e}") + logger.error(f"Не удалось отправить бэкап в канал {channel_id}: {e}, fallback на default") + await send_default() + + elif BACKUP_SEND_MODE == "bot": + if not BACKUP_OTHER_BOT_TOKEN: + logger.error("BACKUP_OTHER_BOT_TOKEN не задан для режима 'bot', fallback на default") + await send_default() + return + other_bot = Bot(token=BACKUP_OTHER_BOT_TOKEN) + try: + for admin_id in ADMIN_ID: + try: + send_kwargs = dict(chat_id=admin_id, document=backup_input_file) + if BACKUP_CAPTION: + send_kwargs["caption"] = BACKUP_CAPTION + await other_bot.send_document(**send_kwargs) + logger.info(f"Бэкап базы данных отправлен админу через другого бота: {admin_id}") + except Exception as e: + logger.error(f"Не удалось отправить бэкап админу {admin_id} через другого бота: {e}") + await other_bot.session.close() + except Exception as e: + logger.error(f"Ошибка при отправке через другого бота: {e}, fallback на default") + await send_default() + else: + logger.error(f"Неизвестный BACKUP_SEND_MODE: {BACKUP_SEND_MODE}, fallback на default") + await send_default() except Exception as e: - logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}") + logger.error(f"Ошибка при отправке бэкапа: {e}") raise diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py index 4e3b8234..97d0b895 100644 --- a/handlers/admin/users/keyboard.py +++ b/handlers/admin/users/keyboard.py @@ -99,7 +99,7 @@ def build_user_edit_kb( def build_users_balance_change_kb(tg_id: int) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text=BACK, # todo: fix magic text was set + text=BACK, callback_data=AdminUserEditorCallback( action="users_balance_edit", tg_id=tg_id ).pack(), @@ -112,30 +112,18 @@ async def build_users_balance_kb( ) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - result = await session.execute(select(Tariff)) - tariffs = result.scalars().all() - - unique_prices = set() - for tariff in tariffs: - months = tariff.duration_days // 30 - if months < 1: - continue - price = tariff.price_rub - if price in unique_prices: - continue - unique_prices.add(price) - + for amount in [100, 250, 500, 1000]: builder.row( InlineKeyboardButton( - text=f"+ {price}₽ ({months} мес.)", + text=f"+ {amount}₽", callback_data=AdminUserEditorCallback( - action="users_balance_add", tg_id=tg_id, data=price + action="users_balance_add", tg_id=tg_id, data=amount ).pack(), ), InlineKeyboardButton( - text=f"- {price}₽ ({months} мес.)", + text=f"- {amount}₽", callback_data=AdminUserEditorCallback( - action="users_balance_add", tg_id=tg_id, data=-price + action="users_balance_add", tg_id=tg_id, data=-amount ).pack(), ), ) @@ -172,7 +160,7 @@ async def build_users_balance_kb( def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text=BACK, # todo: fix magic text was set + text=BACK, callback_data=AdminUserEditorCallback( action="users_key_edit", tg_id=tg_id, data=email, edit=True ).pack(), @@ -187,52 +175,39 @@ async def build_users_key_expiry_kb( result = await session.execute(select(Key.server_id, Key.tariff_id).where(Key.email == email)) row = result.first() - if not row or not row[0] or not row[1]: - builder.row( - InlineKeyboardButton( - text="⚠️ Сервер не найден", - callback_data=AdminUserEditorCallback( - action="users_key_edit", tg_id=tg_id, data=email - ).pack(), + server_id, tariff_id = (row if row else (None, None)) + + if tariff_id: + result = await session.execute(select(Tariff.group_code).where(Tariff.id == tariff_id)) + row = result.first() + if row and row[0]: + group_code = row[0] + result = await session.execute( + select(Tariff) + .where(Tariff.group_code == group_code, Tariff.is_active.is_(True)) ) - ) - return builder.as_markup() - - server_id, tariff_id = row - - result = await session.execute(select(Tariff.group_code).where(Tariff.id == tariff_id)) - row = result.first() - if not row or not row[0]: - return builder.as_markup() - group_code = row[0] - - result = await session.execute( - select(Tariff) - .where(Tariff.group_code == group_code, Tariff.is_active.is_(True)) - ) - tariffs = result.scalars().all() - - unique_durations = set() - for tariff in tariffs: - days = tariff.duration_days - if days < 1 or days in unique_durations: - continue - unique_durations.add(days) - label = format_days(days) - builder.row( - InlineKeyboardButton( - text=f"+ {label}", - callback_data=AdminUserKeyEditorCallback( - action="add", tg_id=tg_id, data=email, month=days - ).pack(), - ), - InlineKeyboardButton( - text=f"- {label}", - callback_data=AdminUserKeyEditorCallback( - action="add", tg_id=tg_id, data=email, month=-days - ).pack(), - ), - ) + tariffs = result.scalars().all() + unique_durations = set() + for tariff in tariffs: + days = tariff.duration_days + if days < 1 or days in unique_durations: + continue + unique_durations.add(days) + label = format_days(days) + builder.row( + InlineKeyboardButton( + text=f"+ {label}", + callback_data=AdminUserKeyEditorCallback( + action="add", tg_id=tg_id, data=email, month=days + ).pack(), + ), + InlineKeyboardButton( + text=f"- {label}", + callback_data=AdminUserKeyEditorCallback( + action="add", tg_id=tg_id, data=email, month=-days + ).pack(), + ), + ) builder.row( InlineKeyboardButton( diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py index 17a8831e..8e1fef67 100644 --- a/handlers/keys/key_mode/key_cluster_mode.py +++ b/handlers/keys/key_mode/key_cluster_mode.py @@ -173,7 +173,17 @@ async def key_cluster_mode( expiry_time_local = expiry_time.astimezone(moscow_tz) remaining_time = expiry_time_local - datetime.now(moscow_tz) days = remaining_time.days - key_message_text = key_message_success(final_link, f"⏳ Осталось дней: {days} 📅") + + tariff_info = None + if plan: + tariff_info = await get_tariff_by_id(session, plan) + + key_message_text = key_message_success( + final_link, + tariff_name=tariff_info.get("name", ""), + traffic_limit=tariff_info.get("traffic_limit", 0), + device_limit=tariff_info.get("device_limit", 0) + ) default_media_path = "img/pic.jpg" if safe_to_edit: diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index f145b17b..a18a4d2c 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -545,7 +545,18 @@ async def finalize_key_creation( remaining_time = expiry_time - datetime.now(moscow_tz) days = remaining_time.days link_to_show = public_link or remnawave_link or "Ссылка не найдена" - key_message_text = key_message_success(link_to_show, f"⏳ Осталось дней: {days} 📅") + + tariff_info = None + if tariff_id: + result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) + tariff_info = result.scalar_one_or_none() + + key_message_text = key_message_success( + link_to_show, + tariff_name=tariff_info.name if tariff_info else "", + traffic_limit=tariff_info.traffic_limit if tariff_info and tariff_info.traffic_limit is not None else 0, + device_limit=tariff_info.device_limit if tariff_info and tariff_info.device_limit is not None else 0 + ) await edit_or_send_message( target_message=callback_query.message, diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index 54007af8..9921a3c2 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -11,7 +11,6 @@ from config import ( NOTIFY_DELETE_KEY, NOTIFY_HOT_LEADS, NOTIFY_INACTIVE_TRAFFIC, - NOTIFY_MAXPRICE, NOTIFY_RENEW, NOTIFY_RENEW_EXPIRED, TRIAL_TIME_DISABLE, @@ -45,10 +44,9 @@ from handlers.texts import ( KEY_EXPIRED_NO_DELAY_MSG, KEY_EXPIRY_10H, KEY_EXPIRY_24H, - KEY_RENEWED, - KEY_RENEWED_TEMP_MSG, + get_renewal_message, ) -from handlers.utils import format_hours, format_minutes +from handlers.utils import format_hours, format_minutes, get_russian_month from logger import logger from .hot_leads_notifications import notify_hot_leads @@ -404,7 +402,11 @@ async def handle_expired_keys( notification_id, 1, "notify_expired.jpg", - KEY_RENEWED_TEMP_MSG, + get_renewal_message( + tariff_name=tariff.get("name", ""), + traffic_limit=tariff.get("traffic_limit") if tariff.get("traffic_limit") is not None else 0, + device_limit=tariff.get("device_limit") if tariff.get("device_limit") is not None else 0 + ), ) continue except Exception as e: @@ -598,6 +600,11 @@ async def process_auto_renew_or_notify( new_expiry_time / 1000, tz=moscow_tz ).strftime("%d %B %Y, %H:%M") + formatted_expiry_date = formatted_expiry_date.replace( + datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz).strftime("%B"), + get_russian_month(datetime.fromtimestamp(new_expiry_time / 1000, tz=moscow_tz)) + ) + logger.info( f"Продление подписки {email} на {duration_days} дней для пользователя {tg_id}. Баланс: {balance}, списываем: {renewal_cost}" ) @@ -617,8 +624,11 @@ async def process_auto_renew_or_notify( await add_notification(conn, tg_id, renew_notification_id) await delete_notification(conn, tg_id, notification_id) - renewed_message = KEY_RENEWED.format( - email=email, months=duration_days // 30, expiry_date=formatted_expiry_date + renewed_message = get_renewal_message( + tariff_name=selected_tariff.get("name", ""), + traffic_limit=selected_tariff.get("traffic_limit") if selected_tariff.get("traffic_limit") is not None else 0, + device_limit=selected_tariff.get("device_limit") if selected_tariff.get("device_limit") is not None else 0, + expiry_date=formatted_expiry_date ) keyboard = build_notification_expired_kb()