diff --git a/handlers/buttons.py b/handlers/buttons.py index 836ed719..1f420d6a 100644 --- a/handlers/buttons.py +++ b/handlers/buttons.py @@ -79,6 +79,7 @@ ROUTER = "πŸ“Ά Π ΠΎΡƒΡ‚Π΅Ρ€" MANUAL_INSTRUCTIONS = "πŸ“– Ручная установка" RENEW_KEY = "⏳ ΠŸΡ€ΠΎΠ΄Π»ΠΈΡ‚ΡŒ подписку" RENEW_KEY_NOTIFICATION = "πŸ”„ ΠŸΡ€ΠΎΠ΄Π»ΠΈΡ‚ΡŒ подписку" +CHANGE_TARIFF = "πŸ”„ Π‘ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π°Ρ€ΠΈΡ„" TV_CONTINUE = "β–Ά ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ" TV_INSTRUCTIONS = "πŸ“– Полная инструкция" HWID_BUTTON = "♻️ Π‘Π±Ρ€ΠΎΡΠΈΡ‚ΡŒ привязку" diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index e81c317c..40359674 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -13,6 +13,7 @@ from config import ( DOWNLOAD_MACOS, DOWNLOAD_PC, SUPPORT_CHAT_URL, + WEBHOOK_HOST, ) from database import get_key_details, get_subscription_link from handlers.buttons import ( @@ -110,10 +111,12 @@ async def process_windows_menu(callback_query: CallbackQuery, session: Any): if key_link and "happ://crypt" in key_link: processed_link = urllib.parse.quote(key_link, safe="") + windows_url = f"{WEBHOOK_HOST}/?url={processed_link}" else: processed_link = key_link + windows_url = f"{CONNECT_WINDOWS}{processed_link}" - builder.row(InlineKeyboardButton(text=CONNECT_WINDOWS_BUTTON, url=f"{CONNECT_WINDOWS}{processed_link}")) + builder.row(InlineKeyboardButton(text=CONNECT_WINDOWS_BUTTON, url=windows_url)) builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL)) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}")) @@ -141,10 +144,12 @@ async def process_macos_menu(callback_query: CallbackQuery, session: Any): if key_link and "happ://crypt" in key_link: processed_link = urllib.parse.quote(key_link, safe="") + macos_url = f"{WEBHOOK_HOST}/?url={processed_link}" else: processed_link = key_link + macos_url = f"{CONNECT_MACOS}{processed_link}" - builder.row(InlineKeyboardButton(text=CONNECT_MACOS_BUTTON, url=f"{CONNECT_MACOS}{processed_link}")) + builder.row(InlineKeyboardButton(text=CONNECT_MACOS_BUTTON, url=macos_url)) builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL)) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}")) diff --git a/handlers/keys/key_connect.py b/handlers/keys/key_connect.py index c9015a19..dfbe5b50 100644 --- a/handlers/keys/key_connect.py +++ b/handlers/keys/key_connect.py @@ -12,6 +12,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from config import ( + APP_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, @@ -108,13 +109,17 @@ async def process_callback_connect_phone(callback_query: CallbackQuery, session: ) if key_link and "happ://crypt" in key_link: processed_link = urllib.parse.quote(key_link, safe="") + crypto_url = f"{APP_URL}/?url={processed_link}" + builder.row( + InlineKeyboardButton(text=IMPORT_IOS, url=crypto_url), + InlineKeyboardButton(text=IMPORT_ANDROID, url=crypto_url), + ) else: processed_link = key_link - - builder.row( - InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{processed_link}"), - InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{processed_link}"), - ) + builder.row( + InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{processed_link}"), + InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{processed_link}"), + ) if INSTRUCTIONS_BUTTON: builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}")) @@ -148,10 +153,12 @@ async def process_callback_connect_ios(callback_query: CallbackQuery, session: A if key_link and "happ://crypt" in key_link: processed_link = urllib.parse.quote(key_link, safe="") + ios_url = f"{APP_URL}/?url={processed_link}" else: processed_link = key_link + ios_url = f"{CONNECT_IOS}{processed_link}" - builder.row(InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{processed_link}")) + builder.row(InlineKeyboardButton(text=IMPORT_IOS, url=ios_url)) if INSTRUCTIONS_BUTTON: builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}")) @@ -186,10 +193,12 @@ async def process_callback_connect_android(callback_query: CallbackQuery, sessio if key_link and "happ://crypt" in key_link: processed_link = urllib.parse.quote(key_link, safe="") + android_url = f"{APP_URL}/?url={processed_link}" else: processed_link = key_link + android_url = f"{CONNECT_ANDROID}{processed_link}" - builder.row(InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{processed_link}")) + builder.row(InlineKeyboardButton(text=IMPORT_ANDROID, url=android_url)) if INSTRUCTIONS_BUTTON: builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_device|{email}")) diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index a78f9ddf..a624b4ea 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -15,6 +15,10 @@ from config import ( NOTIFY_INACTIVE_TRAFFIC, NOTIFY_RENEW, NOTIFY_RENEW_EXPIRED, + NOTIFY_24H_ENABLED, + NOTIFY_24H_HOURS, + NOTIFY_10H_ENABLED, + NOTIFY_10H_HOURS, TRIAL_TIME_DISABLE, ) from database import ( @@ -35,10 +39,12 @@ from database import ( ) from handlers.keys.operations import delete_key_from_cluster, renew_key_in_cluster from handlers.notifications.notify_kb import ( + build_change_tariff_kb, build_notification_expired_kb, build_notification_kb, ) from handlers.texts import ( + KEY_CANNOT_RENEW_CURRENT, KEY_DELETED_MSG, KEY_EXPIRED_DELAY_MSG, KEY_EXPIRED_NO_DELAY_MSG, @@ -50,7 +56,7 @@ from hooks.hooks import run_hooks from logger import logger from .hot_leads_notifications import notify_hot_leads -from .notify_utils import send_messages_with_limit, send_notification +from .notify_utils import prepare_key_expiry_data, send_messages_with_limit, send_notification from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic @@ -72,8 +78,6 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker): logger.info("Запуск ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ") current_time = int(datetime.now(moscow_tz).timestamp() * 1000) - threshold_10h = int((datetime.now(moscow_tz) + timedelta(hours=10)).timestamp() * 1000) - threshold_24h = int((datetime.now(moscow_tz) + timedelta(days=1)).timestamp() * 1000) try: keys = await get_all_keys(session=session) @@ -88,15 +92,19 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker): except Exception as e: logger.error(f"Ошибка Π² notify_inactive_trial_users: {e}") - try: - await notify_24h_keys(bot, session, current_time, threshold_24h, keys) - except Exception as e: - logger.error(f"Ошибка Π² notify_24h_keys: {e}") + if NOTIFY_24H_ENABLED: + try: + threshold_24h = int((datetime.now(moscow_tz) + timedelta(hours=NOTIFY_24H_HOURS)).timestamp() * 1000) + await notify_24h_keys(bot, session, current_time, threshold_24h, keys) + except Exception as e: + logger.error(f"Ошибка Π² notify_24h_keys: {e}") - try: - await notify_10h_keys(bot, session, current_time, threshold_10h, keys) - except Exception as e: - logger.error(f"Ошибка Π² notify_10h_keys: {e}") + if NOTIFY_10H_ENABLED: + try: + threshold_10h = int((datetime.now(moscow_tz) + timedelta(hours=NOTIFY_10H_HOURS)).timestamp() * 1000) + await notify_10h_keys(bot, session, current_time, threshold_10h, keys) + except Exception as e: + logger.error(f"Ошибка Π² notify_10h_keys: {e}") try: await handle_expired_keys(bot, session, current_time, keys) @@ -133,13 +141,13 @@ async def notify_24h_keys( threshold_time_24h: int, keys: list, ): - logger.info("Начало ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· 24 часа.") + logger.info(f"Начало ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· {NOTIFY_24H_HOURS} часов.") expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_24h] - logger.info(f"НайдСно {len(expiring_keys)} подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· 24 часа.") + logger.info(f"НайдСно {len(expiring_keys)} подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· {NOTIFY_24H_HOURS} часов.") tg_ids = [getattr(key, "tg_id", key["tg_id"]) for key in expiring_keys] emails = [key.email or "" for key in expiring_keys] - allowed = await check_notifications_bulk(session, "key_24h", 24, tg_ids=tg_ids, emails=emails) + allowed = await check_notifications_bulk(session, "key_24h", NOTIFY_24H_HOURS, tg_ids=tg_ids, emails=emails) allowed_set = {(u["tg_id"], u["email"]) for u in allowed} messages = [] @@ -151,42 +159,19 @@ async def notify_24h_keys( continue notification_id = f"{email}_key_24h" - expiry_timestamp = key.expiry_time - hours_left = int((expiry_timestamp - current_time) / (1000 * 3600)) - hours_left_formatted = ( - f"⏳ ΠžΡΡ‚Π°Π»ΠΎΡΡŒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ: {format_hours(hours_left)}" if hours_left > 0 else "⏳ ПослСдний дСнь подписки!" - ) - expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz) - formatted_expiry_date = expiry_datetime.strftime("%d %B %Y, %H:%M (МБК)") - can_notify = await check_notification_time(session, tg_id, notification_id, hours=24) + can_notify = await check_notification_time(session, tg_id, notification_id, hours=NOTIFY_24H_HOURS) if not can_notify: continue - tariff_name = "β€”" - tariff_details = "" - if getattr(key, "tariff_id", None): - tariff = await get_tariff_by_id(session, key.tariff_id) - if tariff: - tariff_name = tariff.get("name") or "β€”" - traffic_limit = tariff.get("traffic_limit") or 0 - device_limit = tariff.get("device_limit") or 0 - subgroup_title = tariff.get("subgroup_title", "") - traffic_text = "Π±Π΅Π·Π»ΠΈΠΌΠΈΡ‚" if traffic_limit <= 0 else f"{traffic_limit} Π“Π‘" - devices_text = "Π±Π΅Π·Π»ΠΈΠΌΠΈΡ‚" if device_limit <= 0 else str(device_limit) - lines = [] - if subgroup_title: - lines.append(subgroup_title) - lines.append(f"Π’Ρ€Π°Ρ„ΠΈΠΊ: {traffic_text}") - lines.append(f"Устройств: {devices_text}") - tariff_details = "\n" + "\n".join(lines) - + expiry_data = await prepare_key_expiry_data(key, session, current_time) + notification_text = KEY_EXPIRY.format( email=email, - hours_left_formatted=hours_left_formatted, - formatted_expiry_date=formatted_expiry_date, - tariff_name=tariff_name, - tariff_details=tariff_details, + hours_left_formatted=expiry_data["hours_left_formatted"], + formatted_expiry_date=expiry_data["formatted_expiry_date"], + tariff_name=expiry_data["tariff_name"], + tariff_details=expiry_data["tariff_details"], ) if NOTIFY_RENEW: @@ -227,9 +212,9 @@ async def notify_24h_keys( logger.warning( f"НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΎΠ± ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰Π΅ΠΉ подпискС {msg['email']} ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŽ {tg_id}." ) - logger.info(f"ΠžΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ {sent_count} ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎΠ± истСчСнии подписки Ρ‡Π΅Ρ€Π΅Π· 24 часа.") + logger.info(f"ΠžΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ {sent_count} ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎΠ± истСчСнии подписки Ρ‡Π΅Ρ€Π΅Π· {NOTIFY_24H_HOURS} часов.") - logger.info("ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° всСх ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ Π·Π° 24 часа Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π°.") + logger.info(f"ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° всСх ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ Π·Π° {NOTIFY_24H_HOURS} часов Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π°.") await asyncio.sleep(1) @@ -240,13 +225,13 @@ async def notify_10h_keys( threshold_time_10h: int, keys: list, ): - logger.info("Начало ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· 10 часов.") + logger.info(f"Начало ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· {NOTIFY_10H_HOURS} часов.") expiring_keys = [key for key in keys if key.expiry_time and current_time < key.expiry_time <= threshold_time_10h] - logger.info(f"НайдСно {len(expiring_keys)} подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· 10 часов.") + logger.info(f"НайдСно {len(expiring_keys)} подписок, ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰ΠΈΡ… Ρ‡Π΅Ρ€Π΅Π· {NOTIFY_10H_HOURS} часов.") tg_ids = [key.tg_id for key in expiring_keys] emails = [key.email or "" for key in expiring_keys] - allowed = await check_notifications_bulk(session, "key_10h", 10, tg_ids=tg_ids, emails=emails) + allowed = await check_notifications_bulk(session, "key_10h", NOTIFY_10H_HOURS, tg_ids=tg_ids, emails=emails) allowed_set = {(u["tg_id"], u["email"]) for u in allowed} messages = [] @@ -258,42 +243,19 @@ async def notify_10h_keys( continue notification_id = f"{email}_key_10h" - expiry_timestamp = key.expiry_time - hours_left = int((expiry_timestamp - current_time) / (1000 * 3600)) - hours_left_formatted = ( - f"⏳ ΠžΡΡ‚Π°Π»ΠΎΡΡŒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ: {format_hours(hours_left)}" if hours_left > 0 else "⏳ ПослСдний дСнь подписки!" - ) - expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz) - formatted_expiry_date = expiry_datetime.strftime("%d %B %Y, %H:%M (МБК)") - can_notify = await check_notification_time(session, tg_id, notification_id, hours=10) + can_notify = await check_notification_time(session, tg_id, notification_id, hours=NOTIFY_10H_HOURS) if not can_notify: continue - tariff_name = "β€”" - tariff_details = "" - if key.tariff_id: - tariff = await get_tariff_by_id(session, key.tariff_id) - if tariff: - tariff_name = tariff.get("name") or "β€”" - traffic_limit = tariff.get("traffic_limit") or 0 - device_limit = tariff.get("device_limit") or 0 - subgroup_title = tariff.get("subgroup_title", "") - traffic_text = "Π±Π΅Π·Π»ΠΈΠΌΠΈΡ‚" if traffic_limit <= 0 else f"{traffic_limit} Π“Π‘" - devices_text = "Π±Π΅Π·Π»ΠΈΠΌΠΈΡ‚" if device_limit <= 0 else str(device_limit) - lines = [] - if subgroup_title: - lines.append(subgroup_title) - lines.append(f"Π’Ρ€Π°Ρ„ΠΈΠΊ: {traffic_text}") - lines.append(f"Устройств: {devices_text}") - tariff_details = "\n" + "\n".join(lines) - + expiry_data = await prepare_key_expiry_data(key, session, current_time) + notification_text = KEY_EXPIRY.format( email=email, - hours_left_formatted=hours_left_formatted, - formatted_expiry_date=formatted_expiry_date, - tariff_name=tariff_name, - tariff_details=tariff_details, + hours_left_formatted=expiry_data["hours_left_formatted"], + formatted_expiry_date=expiry_data["formatted_expiry_date"], + tariff_name=expiry_data["tariff_name"], + tariff_details=expiry_data["tariff_details"], ) if NOTIFY_RENEW: @@ -334,9 +296,9 @@ async def notify_10h_keys( logger.warning( f"НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΎΠ± ΠΈΡΡ‚Π΅ΠΊΠ°ΡŽΡ‰Π΅ΠΉ подпискС {msg['email']} ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŽ {tg_id}." ) - logger.info(f"ΠžΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ {sent_count} ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎΠ± истСчСнии подписки Ρ‡Π΅Ρ€Π΅Π· 10 часов.") + logger.info(f"ΠžΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ {sent_count} ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ΠΎΠ± истСчСнии подписки Ρ‡Π΅Ρ€Π΅Π· {NOTIFY_10H_HOURS} часов.") - logger.info("ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° всСх ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ Π·Π° 10 часов Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π°.") + logger.info(f"ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° всСх ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ Π·Π° {NOTIFY_10H_HOURS} часов Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π°.") await asyncio.sleep(1) @@ -500,13 +462,7 @@ async def process_auto_renew_or_notify( selected_tariff = None if not tariff_id: - cluster_tariffs = [t for t in tariffs if t["is_active"] and balance >= t["price_rub"]] - if cluster_tariffs: - cluster_tariffs_31 = [t for t in cluster_tariffs if t["duration_days"] <= 31] - if cluster_tariffs_31: - selected_tariff = max(cluster_tariffs_31, key=lambda x: x["duration_days"]) - else: - selected_tariff = None + selected_tariff = None else: if await check_tariff_exists(conn, tariff_id): current_tariff = await get_tariff_by_id(conn, tariff_id) @@ -522,28 +478,57 @@ async def process_auto_renew_or_notify( logger.warning(f"[AUTO_RENEW] Ошибка ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Π³Ρ€ΡƒΠΏΠΏ: {e}") if current_tariff["group_code"] in forbidden_groups: - cluster_tariffs = [t for t in tariffs if t["is_active"] and balance >= t["price_rub"]] - if cluster_tariffs: - cluster_tariffs_31 = [t for t in cluster_tariffs if t["duration_days"] <= 31] - if cluster_tariffs_31: - selected_tariff = max(cluster_tariffs_31, key=lambda x: x["duration_days"]) - else: - selected_tariff = None + selected_tariff = None elif balance >= current_tariff["price_rub"]: selected_tariff = current_tariff else: - cluster_tariffs = [t for t in tariffs if t["is_active"] and balance >= t["price_rub"]] - if cluster_tariffs: - cluster_tariffs_31 = [t for t in cluster_tariffs if t["duration_days"] <= 31] - if cluster_tariffs_31: - selected_tariff = max(cluster_tariffs_31, key=lambda x: x["duration_days"]) - else: - selected_tariff = None + selected_tariff = None if not selected_tariff: - keyboard = build_notification_kb(email) + expiry_data = await prepare_key_expiry_data(key, conn, int(datetime.now(moscow_tz).timestamp() * 1000)) + + use_change_tariff_kb = False + + if tariff_id and await check_tariff_exists(conn, tariff_id): + current_tariff = await get_tariff_by_id(conn, tariff_id) + if current_tariff: + forbidden_groups = ["discounts", "discounts_max", "gifts", "trial"] + try: + hook_results = await run_hooks("renewal_forbidden_groups", chat_id=tg_id, admin=False, session=conn) + for hook_result in hook_results: + additional_groups = hook_result.get("additional_groups", []) + forbidden_groups.extend(additional_groups) + except Exception as e: + logger.warning(f"[AUTO_RENEW] Ошибка ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Π³Ρ€ΡƒΠΏΠΏ: {e}") + + if current_tariff["group_code"] in forbidden_groups: + use_change_tariff_kb = True + + message_text = KEY_CANNOT_RENEW_CURRENT.format( + email=email, + hours_left_formatted=expiry_data["hours_left_formatted"], + formatted_expiry_date=expiry_data["formatted_expiry_date"], + tariff_name=expiry_data["tariff_name"], + tariff_details=expiry_data["tariff_details"], + ) + else: + use_change_tariff_kb = True + + message_text = KEY_CANNOT_RENEW_CURRENT.format( + email=email, + hours_left_formatted=expiry_data["hours_left_formatted"], + formatted_expiry_date=expiry_data["formatted_expiry_date"], + tariff_name=expiry_data["tariff_name"], + tariff_details=expiry_data["tariff_details"], + ) + + if use_change_tariff_kb: + keyboard = build_change_tariff_kb(email) + else: + keyboard = build_notification_kb(email) + await add_notification(conn, tg_id, notification_id) - await send_notification(bot, tg_id, standard_photo, standard_caption, keyboard) + await send_notification(bot, tg_id, standard_photo, message_text, keyboard) return client_id = key.client_id diff --git a/handlers/notifications/notify_kb.py b/handlers/notifications/notify_kb.py index 4e6bf097..20e61e12 100644 --- a/handlers/notifications/notify_kb.py +++ b/handlers/notifications/notify_kb.py @@ -1,6 +1,6 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from handlers.buttons import DISCOUNT_TARIFF, MAIN_MENU, MAX_DISCOUNT_TARIFF, RENEW_KEY_NOTIFICATION +from handlers.buttons import CHANGE_TARIFF, DISCOUNT_TARIFF, MAIN_MENU, MAX_DISCOUNT_TARIFF, RENEW_KEY_NOTIFICATION def build_notification_kb(email: str) -> InlineKeyboardMarkup: @@ -17,6 +17,20 @@ def build_notification_kb(email: str) -> InlineKeyboardMarkup: return builder.as_markup() +def build_change_tariff_kb(email: str) -> InlineKeyboardMarkup: + """ + Π€ΠΎΡ€ΠΌΠΈΡ€ΡƒΠ΅Ρ‚ inline-ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Ρƒ для смСны Ρ‚Π°Ρ€ΠΈΡ„Π°. + Кнопки: "πŸ”„ Π‘ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π°Ρ€ΠΈΡ„" (callback_data содСрТит email) ΠΈ "πŸ‘€ Π›ΠΈΡ‡Π½Ρ‹ΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ‚". + """ + from aiogram.utils.keyboard import InlineKeyboardBuilder + + builder = InlineKeyboardBuilder() + builder.button(text=CHANGE_TARIFF, callback_data=f"renew_key|{email}") + builder.button(text=MAIN_MENU, callback_data="profile") + builder.adjust(1) + return builder.as_markup() + + def build_notification_expired_kb() -> InlineKeyboardMarkup: """ Π€ΠΎΡ€ΠΌΠΈΡ€ΡƒΠ΅Ρ‚ inline-ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Ρƒ для ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ послС удалСния ΠΈΠ»ΠΈ продлСния. diff --git a/handlers/notifications/notify_utils.py b/handlers/notifications/notify_utils.py index 7800903b..c3ba929b 100644 --- a/handlers/notifications/notify_utils.py +++ b/handlers/notifications/notify_utils.py @@ -1,7 +1,9 @@ import asyncio import os +from datetime import datetime import aiofiles +import pytz from aiogram import Bot from aiogram.exceptions import ( @@ -12,7 +14,8 @@ from aiogram.exceptions import ( from aiogram.types import BufferedInputFile, InlineKeyboardMarkup from sqlalchemy.ext.asyncio import AsyncSession -from database import create_blocked_user +from database import create_blocked_user, get_tariff_by_id +from handlers.utils import format_hours from logger import logger @@ -157,3 +160,42 @@ async def _send_text_notification( except Exception as e: logger.error(f"НСизвСстная ошибка ΠΏΡ€ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ΅ сообщСния для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ {tg_id}: {e}") return False + + +async def prepare_key_expiry_data(key, session: AsyncSession, current_time: int) -> dict: + moscow_tz = pytz.timezone("Europe/Moscow") + + expiry_timestamp = key.expiry_time + hours_left = int((expiry_timestamp - current_time) / (1000 * 3600)) + hours_left_formatted = ( + f"⏳ ΠžΡΡ‚Π°Π»ΠΎΡΡŒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ: {format_hours(hours_left)}" if hours_left > 0 else "⏳ ПослСдний дСнь подписки!" + ) + + expiry_datetime = datetime.fromtimestamp(expiry_timestamp / 1000, tz=moscow_tz) + formatted_expiry_date = expiry_datetime.strftime("%d %B %Y, %H:%M (МБК)") + + tariff_name = "β€”" + tariff_details = "" + + if getattr(key, "tariff_id", None): + tariff = await get_tariff_by_id(session, key.tariff_id) + if tariff: + tariff_name = tariff.get("name") or "β€”" + traffic_limit = tariff.get("traffic_limit") or 0 + device_limit = tariff.get("device_limit") or 0 + subgroup_title = tariff.get("subgroup_title", "") + traffic_text = "Π±Π΅Π·Π»ΠΈΠΌΠΈΡ‚" if traffic_limit <= 0 else f"{traffic_limit} Π“Π‘" + devices_text = "Π±Π΅Π·Π»ΠΈΠΌΠΈΡ‚" if device_limit <= 0 else str(device_limit) + lines = [] + if subgroup_title: + lines.append(subgroup_title) + lines.append(f"Π’Ρ€Π°Ρ„ΠΈΠΊ: {traffic_text}") + lines.append(f"Устройств: {devices_text}") + tariff_details = "\n" + "\n".join(lines) + + return { + "hours_left_formatted": hours_left_formatted, + "formatted_expiry_date": formatted_expiry_date, + "tariff_name": tariff_name, + "tariff_details": tariff_details, + } diff --git a/handlers/refferal.py b/handlers/refferal.py index d0fbd51c..4d6797d6 100644 --- a/handlers/refferal.py +++ b/handlers/refferal.py @@ -148,7 +148,7 @@ async def inline_referral_handler(inline_query: InlineQuery, session: AsyncSessi ) ) - await inline_query.answer(results=results, cache_time=86400, is_personal=True) + await inline_query.answer(results=results, cache_time=60, is_personal=True) @router.callback_query(F.data.startswith("show_referral_qr|"))