diff --git a/handlers/notifications.py b/handlers/notifications.py index 87286583..367fd0c6 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -9,6 +9,7 @@ from aiogram.exceptions import TelegramForbiddenError from aiogram.types import BufferedInputFile from aiogram.utils.keyboard import InlineKeyboardBuilder from py3xui import AsyncApi +from .utils import format_time_until_deletion from config import ( ADMIN_PASSWORD, @@ -21,6 +22,7 @@ from config import ( RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME, + DELETE_KEYS_DELAY, ) from database import ( add_blocked_user, @@ -402,11 +404,10 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): await asyncio.sleep(1) - async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float): logger.info("Проверка подписок, срок действия которых скоро истекает...") - threshold_time = int((datetime.utcnow() + timedelta(minutes=45)).timestamp() * 1000) + threshold_time = int((datetime.utcnow() + timedelta(seconds=EXPIRED_KEYS_CHECK_INTERVAL * 1.5)).timestamp() * 1000) expiring_keys = await conn.fetch( """ @@ -414,37 +415,43 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: WHERE expiry_time <= $1 AND expiry_time > $2 """, threshold_time, - current_time, + current_time - DELETE_KEYS_DELAY * 1000, ) logger.info(f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает.") - for record in expiring_keys: - try: - await process_key(record, bot, conn) - except Exception as e: - logger.error(f"Ошибка при обработке подписки {record['client_id']}: {e}") - - expired_keys = await conn.fetch( + if DELETE_KEYS_DELAY > 0: + expired_keys_query = """ + SELECT tg_id, client_id, email, server_id FROM keys + WHERE expiry_time <= (CAST($1 AS bigint) - $2 * 1000) """ - SELECT tg_id, client_id, email, server_id FROM keys - WHERE expiry_time <= $1 - """, - current_time, - ) + params = (current_time, DELETE_KEYS_DELAY) + else: + expired_keys_query = """ + SELECT tg_id, client_id, email, server_id FROM keys + WHERE expiry_time <= $1 + """ + params = (current_time,) + + expired_keys = await conn.fetch(expired_keys_query, *params) logger.info(f"Найдено {len(expired_keys)} истёкших ключей.") for record in expired_keys: try: - await delete_key_from_cluster(record["server_id"], record["email"], record["email"]) - await conn.execute( - "DELETE FROM keys WHERE client_id = $1", record["client_id"] + await delete_key_from_cluster( + cluster_id=record["server_id"], + email=record["email"], + client_id=record["client_id"] ) - logger.info(f"Удалён истёкший ключ {record['client_id']} пользователя {record['tg_id']}.") + await conn.execute( + "DELETE FROM keys WHERE client_id = $1", + record["client_id"] + ) + logger.info(f"Ключ {record['client_id']} удалён" + + (f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else "")) except Exception as e: - logger.error(f"Ошибка при удалении истёкшего ключа {record['client_id']}: {e}") - + logger.error(f"Ошибка при удалении ключа {record['client_id']}: {e}") async def process_key(record, bot, conn): tg_id = record["tg_id"] @@ -464,15 +471,18 @@ async def process_key(record, bot, conn): f"Оставшееся время: {time_left}" ) - keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="👤 Личный кабинет", callback_data="profile" - ) - ] - ] - ) + keyboard = InlineKeyboardBuilder() + + if DELETE_KEYS_DELAY > 0: + keyboard.row(types.InlineKeyboardButton( + text="🔄 Продлить", + callback_data=f"renew_key|{email}" + )) + + keyboard.row(types.InlineKeyboardButton( + text="👤 Личный кабинет", + callback_data="profile" + )) image_path = os.path.join("img", "notify_expired.jpg") @@ -505,55 +515,84 @@ async def process_key(record, bot, conn): await bot.send_photo( tg_id, photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"), - caption = KEY_RENEWED.format(email=email), - reply_markup=keyboard, + caption=KEY_RENEWED.format(email=email), + reply_markup=keyboard.as_markup(), ) else: - await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard) + await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard.as_markup()) logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.") except Exception as e: logger.error(f"Не удалось отправить уведомление о продлении клиенту {tg_id}: {e}") else: - message_expired = f"Ваша подписка {email} истекла. Пополните баланс для продления." - try: - if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: - await bot.send_photo( - tg_id, - photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"), - caption=message_expired, - reply_markup=keyboard, - ) + current_time_utc = int(datetime.utcnow().timestamp() * 1000) + + if current_time_utc >= expiry_time: + time_since_expiry = current_time_utc - expiry_time + if time_since_expiry <= EXPIRED_KEYS_CHECK_INTERVAL * 1000: + message_expired = f"Ваша подписка {email} истекла. Пополните баланс для продления." + + if DELETE_KEYS_DELAY > 0: + time_until_deletion = format_time_until_deletion(DELETE_KEYS_DELAY) + if time_until_deletion != "0 минут": + message_expired += f"\n\n⏳ Ключ будет удалён через {time_until_deletion}." + + try: + if os.path.isfile(image_path): + with open(image_path, "rb") as image_file: + await bot.send_photo( + tg_id, + photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"), + caption=message_expired, + reply_markup=keyboard.as_markup(), + ) + else: + await bot.send_message( + tg_id, + text=message_expired, + reply_markup=keyboard.as_markup() + ) + logger.info(f"Уведомление об истечении подписки отправлено пользователю {tg_id}.") + except Exception as e: + logger.error(f"Не удалось отправить уведомление об истечении клиенту {tg_id}: {e}") else: - await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard) - logger.info(f"Уведомление об истечении подписки отправлено пользователю {tg_id}.") - except Exception as e: - logger.error(f"Не удалось отправить уведомление об истечении клиенту {tg_id}: {e}") + logger.info(f"Пропуск {client_id}: просрочено {time_since_expiry // 1000} сек назад.") if AUTO_DELETE_EXPIRED_KEYS: - servers = await get_servers_from_db() + current_time = int(datetime.utcnow().timestamp() * 1000) + + if DELETE_KEYS_DELAY == 0 or current_time >= expiry_time + (DELETE_KEYS_DELAY * 1000): + servers = await get_servers_from_db() + + for cluster_id in servers: + try: + await delete_key_from_cluster(cluster_id, email, client_id) + logger.info(f"Клиент {client_id} удален из кластера {cluster_id}.") + except Exception as e: + logger.error(f"Ошибка при удалении клиента {client_id} из кластера {cluster_id}: {e}") - for cluster_id in servers: try: - await delete_key_from_cluster(cluster_id, email, client_id) - logger.info(f"Клиент {client_id} удален из кластера {cluster_id}.") + await delete_key(client_id) + log_msg = f"Ключ {client_id} удалён из базы данных" + if DELETE_KEYS_DELAY > 0: + log_msg += f" после задержки {DELETE_KEYS_DELAY} сек." + logger.info(log_msg) except Exception as e: - logger.error(f"Ошибка при удалении клиента {client_id} из кластера {cluster_id}: {e}") + logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") + else: + remaining_time = (expiry_time + (DELETE_KEYS_DELAY * 1000) - current_time) // 1000 + logger.info( + f"Ключ {client_id} не удалён. " + f"Осталось времени до удаления: {remaining_time} сек. " + f"(Удаление через {DELETE_KEYS_DELAY} сек после истечения)" + ) - try: - await delete_key(client_id) - logger.info(f"Ключ {client_id} удалён из базы данных.") - except Exception as e: - logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") else: - logger.info(f"Ключ {client_id} НЕ был удалён (AUTO_DELETE_EXPIRED_KEYS=False).") + logger.info(f"Ключ {client_id} не был удалён (AUTO_DELETE_EXPIRED_KEYS=False).") except Exception as e: logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}") - - async def check_online_users(): servers = await get_servers_from_db()