From 8518dcba3bf1f68a675ab3618c896e03a0712677 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Sun, 2 Feb 2025 00:58:07 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=83=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлена работа оповещений 10/24 часа. - Восстановлена и переработана работа истекших подписок. - Переработана работа уведомлений при их удалении. --- handlers/notifications.py | 281 ++++++++++++++++++++------------------ 1 file changed, 150 insertions(+), 131 deletions(-) diff --git a/handlers/notifications.py b/handlers/notifications.py index b051e32a..ce3e7495 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -16,6 +16,7 @@ from config import ( ADMIN_USERNAME, AUTO_DELETE_EXPIRED_KEYS, AUTO_RENEW_KEYS, + SUPPORT_CHAT_URL, DATABASE_URL, DELETE_KEYS_DELAY, DEV_MODE, @@ -34,6 +35,7 @@ from database import ( update_balance, update_key_expiry, ) +from handlers.buttons.profile import ADD_SUB from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED from logger import logger @@ -206,6 +208,7 @@ async def process_24h_record(record, bot, conn): tg_id = record["tg_id"] email = record["email"] expiry_time = record["expiry_time"] + client_id = record["client_id"] moscow_tz = pytz.timezone("Europe/Moscow") @@ -240,7 +243,14 @@ async def process_24h_record(record, bot, conn): await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB) logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.") - await conn.execute("UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", record["client_id"]) + if flag == "notified": + await conn.execute("UPDATE keys SET notified = TRUE WHERE client_id = $1", client_id) + + elif flag == "notified_24h": + await conn.execute("UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", client_id) + + except Exception as e: + logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") image_path = os.path.join("img", "notify_24h.jpg") keyboard = types.InlineKeyboardMarkup( @@ -264,7 +274,7 @@ async def process_24h_record(record, bot, conn): except Exception as e: logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}") else: - await send_renewal_notification(bot, tg_id, email, message_24h, conn, record["client_id"], "notified_24h") + await send_renewal_notification(bot, tg_id, email, message_24h, conn, client_id, "notified_24h") async def send_renewal_notification(bot, tg_id, email, message, conn, client_id, flag): @@ -290,7 +300,7 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id, logger.info(f"Уведомление отправлено пользователю {tg_id}.") - await conn.execute("UPDATE keys SET notified_24h = $1 WHERE client_id = $2", flag, client_id) + await conn.execute("UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", client_id) except Exception as e: logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") @@ -362,177 +372,186 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: logger.info("Проверка подписок, срок действия которых скоро истекает...") threshold_time = int((datetime.utcnow() + timedelta(seconds=EXPIRED_KEYS_CHECK_INTERVAL * 1.5)).timestamp() * 1000) - + expiring_keys = await conn.fetch( """ SELECT tg_id, client_id, expiry_time, email, server_id FROM keys WHERE expiry_time <= $1 AND expiry_time > $2 """, threshold_time, - current_time - DELETE_KEYS_DELAY * 1000, + current_time, ) - logger.info(f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает.") - 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) - """ - 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,) + for record in expiring_keys: + await process_key(record, bot, conn, current_time) + + expired_keys_query = """ + SELECT tg_id, client_id, email, server_id, expiry_time FROM keys + WHERE expiry_time <= $1 + """ + params = (current_time,) expired_keys = await conn.fetch(expired_keys_query, *params) - - logger.info(f"Найдено {len(expired_keys)} истёкших ключей.") + logger.info(f"Найдено {len(expired_keys)} истёкших подписок.") for record in expired_keys: try: - await delete_key_from_cluster( - cluster_id=record["server_id"], email=record["email"], client_id=record["client_id"] - ) - await delete_key(record["client_id"], conn) - logger.info( - f"Ключ {record['client_id']} удалён" - + (f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else "") - ) + balance = await get_balance(record["tg_id"]) + expiry_time_value = record["expiry_time"] + current_time_utc = int(datetime.utcnow().timestamp() * 1000) + time_since_expiry = current_time_utc - expiry_time_value + + if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]: + await process_key(record, bot, conn, current_time, renew=True) + else: + await process_key(record, bot, conn, current_time) + if time_since_expiry >= DELETE_KEYS_DELAY * 1000: + await delete_key_from_cluster( + cluster_id=record["server_id"], + email=record["email"], + client_id=record["client_id"] + ) + await delete_key(record["client_id"], conn) + logger.info(f"Подписка {record['client_id']} удалена") + + message = ( + f"🔔 Уведомление:\n\n" + f"📅 Ваша подписка: {record['email']} была удалена из-за истечения срока действия.\n\n" + f"⏳ Чтобы продолжить использовать наши услуги, пожалуйста, создайте новую подписку.\n\n" + f"💬 Если у вас возникли вопросы, не стесняйтесь обращаться в поддержку!" + ) + + keyboard = InlineKeyboardBuilder() + keyboard.row(types.InlineKeyboardButton(text=ADD_SUB, callback_data="create_key")) + keyboard.row(types.InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL)) + keyboard.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + + image_path = os.path.join("img", "notify_expired.jpg") + + if os.path.isfile(image_path): + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() + await bot.send_photo( + record["tg_id"], + photo=BufferedInputFile(image_data, filename="notify_expired.jpg"), + caption=message, + reply_markup=keyboard.as_markup(), + ) + else: + await bot.send_message( + record["tg_id"], + text=message, + reply_markup=keyboard.as_markup() + ) + + logger.info(f"Уведомление об удалении отправлено пользователю {record['tg_id']}") + + else: + remaining_time = (DELETE_KEYS_DELAY * 1000 - time_since_expiry) // 1000 + logger.info(f"Подписка {record['client_id']} не удалена. Осталось времени до удаления: {remaining_time} сек. (Удаление через {DELETE_KEYS_DELAY} сек после истечения)") + + except TelegramForbiddenError: + logger.warning(f"Бот заблокирован пользователем {record['tg_id']}. Уведомление не отправлено.") 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): +async def process_key(record, bot, conn, current_time, renew=False): tg_id = record["tg_id"] client_id = record["client_id"] email = record["email"] balance = await get_balance(tg_id) - expiry_time = record["expiry_time"] + expiry_time_value = record["expiry_time"] moscow_tz = pytz.timezone("Europe/Moscow") - expiry_date = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz) + expiry_date = datetime.fromtimestamp(expiry_time_value / 1000, tz=moscow_tz) current_date = datetime.now(moscow_tz) - time_left = expiry_date - current_date logger.info( - f"Время истечения ключа: {expiry_time} (МСК: {expiry_date}), " - f"Текущее время (МСК): {current_date}, " - f"Оставшееся время: {time_left}" + f"Время истечения подписки: {expiry_time_value} (МСК: {expiry_date}), " + f"Текущее время (МСК): {current_date}" ) - 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") + current_time_utc = int(datetime.utcnow().timestamp() * 1000) + time_since_expiry = current_time_utc - expiry_time_value try: - if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]: + if not renew: + if current_time_utc >= expiry_time_value: + if time_since_expiry <= DELETE_KEYS_DELAY * 500: + message = ( + f"🔔 Уведомление:\n\n" + f"📅 Ваша подписка: {record['email']} истекла. Пополните баланс для продления.\n\n" + ) + remaining_time = (expiry_time_value + DELETE_KEYS_DELAY * 1000) - current_time_utc + + if remaining_time > 0: + message += f"⏳ Подписка будет удалена через {format_time_until_deletion(remaining_time//1000)}." + + await send_notification(bot, tg_id, message, "notify_expired.jpg", email) + else: + if (expiry_time_value - current_time_utc) <= (EXPIRED_KEYS_CHECK_INTERVAL * 1000): + await send_notification( + bot, + tg_id, + f"Ваша подписка {email} скоро истечет. Пополните баланс для продления.", + "notify_expiring.jpg", + email + ) + + elif renew and AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]: await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn) - new_expiry_time = int((datetime.now(moscow_tz) + timedelta(days=30)).timestamp() * 1000) + await update_key_expiry(client_id, new_expiry_time, conn) - servers = await get_servers(conn) for cluster_id in servers: await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB) - logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.") - - await conn.execute( - """ - UPDATE keys - SET notified = FALSE, notified_24h = FALSE - WHERE client_id = $1 - """, - client_id, - ) - logger.info(f"Флаги notified сброшены для клиента {client_id}.") + logger.info(f"Подписка {tg_id} продлена в кластере {cluster_id}.") try: + image_path = os.path.join("img", "notify_expired.jpg") + caption = KEY_RENEWED.format(email=email) + if os.path.isfile(image_path): - async with aiofiles.open(image_path, "rb") as image_file: - image_data = await image_file.read() + async with aiofiles.open(image_path, "rb") as f: await bot.send_photo( tg_id, - photo=BufferedInputFile(image_data, filename="notify_expired.jpg"), - caption=KEY_RENEWED.format(email=email), - reply_markup=keyboard.as_markup(), + photo=BufferedInputFile(await f.read(), filename="notify_expired.jpg"), + caption=caption, + reply_markup=InlineKeyboardBuilder().as_markup() ) else: - await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard.as_markup()) - logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.") + await bot.send_message(tg_id, text=caption) + + logger.info(f"Уведомление о продлении отправлено {tg_id}") + except Exception as e: - logger.error(f"Не удалось отправить уведомление о продлении клиенту {tg_id}: {e}") - - else: - 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): - async with aiofiles.open(image_path, "rb") as image_file: - image_data = await image_file.read() - await bot.send_photo( - tg_id, - photo=BufferedInputFile(image_data, 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: - logger.info(f"Пропуск {client_id}: просрочено {time_since_expiry // 1000} сек назад.") - - if AUTO_DELETE_EXPIRED_KEYS: - 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(conn) - - 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}") - - try: - 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} из базы данных: {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} сек после истечения)" - ) - - else: - logger.info(f"Ключ {client_id} не был удалён (AUTO_DELETE_EXPIRED_KEYS=False).") + logger.error(f"Ошибка отправки уведомления {tg_id}: {e}") except Exception as e: - logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}") + logger.error(f"Ошибка обработки подписки {tg_id}: {e}") + +async def send_notification(bot, tg_id, message, image_name, email): + 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") + + try: + if os.path.isfile(image_path): + async with aiofiles.open(image_path, "rb") as f: + await bot.send_photo( + tg_id, + photo=BufferedInputFile(await f.read(), filename="notify_expired.jpg"), + caption=message, + reply_markup=keyboard.as_markup() + ) + else: + await bot.send_message(tg_id, text=message, reply_markup=keyboard.as_markup()) + + except TelegramForbiddenError: + logger.warning(f"Пользователь {tg_id} заблокировал бота")