import asyncio from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, Optional import pytz from aiogram import Bot, Router from sqlalchemy import select, text, update from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from config import ( NOTIFICATION_TIME, NOTIFY_10H_ENABLED, NOTIFY_10H_HOURS, NOTIFY_24H_ENABLED, NOTIFY_24H_HOURS, NOTIFY_DELETE_DELAY, NOTIFY_DELETE_KEY, NOTIFY_HOT_LEADS, NOTIFY_INACTIVE_TRAFFIC, NOTIFY_RENEW, NOTIFY_RENEW_EXPIRED, TRIAL_TIME_DISABLE, ) from core.bootstrap import MODES_CONFIG, NOTIFICATIONS_CONFIG from database import ( add_notification, check_notification_time, check_notifications_bulk, delete_key, delete_notification, get_all_keys, get_balance, get_last_notification_time, update_balance, update_key_expiry, update_key_tariff, ) from database.models import Key, Tariff, User from database.tariffs import ( check_tariff_exists, get_tariff_by_id, get_tariffs_for_cluster, ) 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.tariffs.tariff_display import GB, get_effective_limits_for_key, resolve_price_to_charge from handlers.texts import ( KEY_CANNOT_RENEW_CURRENT, KEY_DELETED_MSG, KEY_EXPIRED_DELAY_MSG, KEY_EXPIRED_NO_DELAY_MSG, KEY_EXPIRY, get_renewal_message, ) from handlers.utils import format_hours, format_minutes, get_russian_month from hooks.hooks import run_hooks from logger import logger from .hot_leads_notifications import notify_hot_leads 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 router = Router() moscow_tz = pytz.timezone("Europe/Moscow") notification_lock = asyncio.Lock() @dataclass class NotificationContext: bot: Bot session: AsyncSession current_time: int preload_data: Optional[dict] = None bulk_updates: Optional[dict] = None def get_balance(self, tg_id: int) -> float: if self.preload_data and tg_id in self.preload_data.get("balances_cache", {}): return self.preload_data["balances_cache"][tg_id] return 0.0 def get_tariff(self, tariff_id: int) -> Optional[dict]: if self.preload_data and tariff_id in self.preload_data.get("tariffs_cache", {}): return self.preload_data["tariffs_cache"][tariff_id] return None async def preload_notification_data(session: AsyncSession) -> dict[str, Any]: stmt = ( select( Key, Tariff, User.balance.label("user_balance"), ) .outerjoin(Tariff, Key.tariff_id == Tariff.id) .outerjoin(User, Key.tg_id == User.tg_id) .where(Key.is_frozen.is_(False)) ) result = await session.execute(stmt) rows = result.all() keys_data = {} tariffs_cache = {} balances_cache = {} for row in rows: key = row[0] tariff = row[1] balance = row[2] or 0.0 key_dict = { "key": key, "tariff": dict(tariff.__dict__) if tariff else None, "balance": float(balance), } keys_data[key.client_id] = key_dict if tariff and tariff.id not in tariffs_cache: tariffs_cache[tariff.id] = dict(tariff.__dict__) balances_cache[key.tg_id] = float(balance) return { "keys_data": keys_data, "tariffs_cache": tariffs_cache, "balances_cache": balances_cache, } async def execute_bulk_updates(session: AsyncSession, bulk_updates: dict[str, Any]) -> None: try: if bulk_updates["balance_changes"]: for tg_id, balance_change in bulk_updates["balance_changes"].items(): await session.execute( text("UPDATE users SET balance = balance + :change WHERE tg_id = :tg_id"), {"change": balance_change, "tg_id": tg_id}, ) logger.info(f"Bulk: обновлено {len(bulk_updates['balance_changes'])} балансов") if bulk_updates["key_expiry_updates"]: for client_id, new_expiry in bulk_updates["key_expiry_updates"]: await session.execute(update(Key).where(Key.client_id == client_id).values(expiry_time=new_expiry)) logger.info(f"Bulk: обновлено {len(bulk_updates['key_expiry_updates'])} сроков действия ключей") if bulk_updates["key_tariff_updates"]: for client_id, new_tariff_id in bulk_updates["key_tariff_updates"]: await session.execute(update(Key).where(Key.client_id == client_id).values(tariff_id=new_tariff_id)) logger.info(f"Bulk: обновлено {len(bulk_updates['key_tariff_updates'])} тарифов ключей") for tg_id, notification_type in bulk_updates["notifications_to_add"]: await add_notification(session, tg_id, notification_type) for tg_id, notification_type in bulk_updates["notifications_to_delete"]: await delete_notification(session, tg_id, notification_type) if bulk_updates["notifications_to_add"] or bulk_updates["notifications_to_delete"]: logger.info( f"Bulk: обработано {len(bulk_updates['notifications_to_add'])} добавлений " f"и {len(bulk_updates['notifications_to_delete'])} удалений уведомлений" ) await session.commit() except Exception as error: logger.error(f"Ошибка в bulk-обновлениях: {error}") await session.rollback() raise async def send_expiry_warning(ctx: NotificationContext, key, hours_left: int, photo: str) -> bool: tg_id = key.tg_id email = key.email or "" expiry_data = await prepare_key_expiry_data(key, ctx.session, ctx.current_time) message_text = KEY_EXPIRY.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"], ) keyboard = build_notification_kb(email) return await send_notification(ctx.bot, tg_id, photo, message_text, keyboard) async def send_cannot_renew(ctx: NotificationContext, key, photo: str) -> bool: tg_id = key.tg_id email = key.email or "" expiry_data = await prepare_key_expiry_data(key, ctx.session, ctx.current_time) 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"], ) keyboard = build_change_tariff_kb(email) return await send_notification(ctx.bot, tg_id, photo, message_text, keyboard) async def send_expired_notification(ctx: NotificationContext, key, delay_minutes: int) -> bool: tg_id = key.tg_id email = key.email or "" if delay_minutes > 0: hours = delay_minutes // 60 minutes = delay_minutes % 60 if hours > 0 and minutes > 0: time_formatted = f"{format_hours(hours)} и {format_minutes(minutes)}" elif hours > 0: time_formatted = format_hours(hours) else: time_formatted = format_minutes(minutes) message_text = KEY_EXPIRED_DELAY_MSG.format(email=email, time_formatted=time_formatted) else: message_text = KEY_EXPIRED_NO_DELAY_MSG.format(email=email) keyboard = build_notification_kb(email) return await send_notification(ctx.bot, tg_id, "notify_expired.jpg", message_text, keyboard) async def send_deleted_notification(ctx: NotificationContext, key) -> bool: tg_id = key.tg_id email = key.email or "" message_text = KEY_DELETED_MSG.format(email=email) keyboard = build_notification_expired_kb() return await send_notification(ctx.bot, tg_id, "notify_expired.jpg", message_text, keyboard) async def send_renewed_notification(ctx: NotificationContext, key, tariff: dict, new_expiry_time: int) -> bool: tg_id = key.tg_id email = key.email or "" selected_device_limit = getattr(key, "selected_device_limit", None) selected_traffic_limit = getattr(key, "selected_traffic_limit", None) selected_traffic_gb = int(selected_traffic_limit) if selected_traffic_limit is not None else None device_limit_effective, traffic_limit_bytes_effective = await get_effective_limits_for_key( session=ctx.session, tariff_id=int(tariff["id"]), selected_device_limit=int(selected_device_limit) if selected_device_limit is not None else None, selected_traffic_gb=selected_traffic_gb, ) traffic_limit_gb = int(traffic_limit_bytes_effective / GB) if traffic_limit_bytes_effective else 0 formatted_expiry_date = datetime.fromtimestamp(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)), ) message_text = get_renewal_message( tariff_name=tariff["name"], traffic_limit=traffic_limit_gb, device_limit=device_limit_effective, expiry_date=formatted_expiry_date, subgroup_title=tariff.get("subgroup_title", ""), ) keyboard = build_notification_expired_kb() result = await send_notification(ctx.bot, tg_id, "notify_expired.jpg", message_text, keyboard) if result: logger.info(f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}.") else: logger.warning(f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}.") return result async def try_auto_renew(ctx: NotificationContext, key) -> tuple[bool, Optional[dict], Optional[int]]: tg_id = key.tg_id email = key.email or "" renew_notification_id = f"{email}_renew" can_renew = await check_notification_time(ctx.session, tg_id, renew_notification_id, hours=24) if not can_renew: logger.debug(f"⏳ Подписка {email} уже продлевалась в течение последних 24 часов.") return False, None, None if ctx.preload_data and tg_id in ctx.preload_data.get("balances_cache", {}): balance = ctx.preload_data["balances_cache"][tg_id] else: balance = await get_balance(ctx.session, tg_id) server_id = key.server_id tariff_id = key.tariff_id tariffs = await get_tariffs_for_cluster(ctx.session, server_id) if not tariffs: logger.warning(f"⛔ Нет доступных тарифов для продления подписки {email}") return False, None, None current_tariff = None if tariff_id: current_tariff = ctx.get_tariff(tariff_id) if not current_tariff and await check_tariff_exists(ctx.session, tariff_id): current_tariff = await get_tariff_by_id(ctx.session, tariff_id) if not current_tariff: return False, None, None forbidden_groups = ["discounts", "discounts_max", "gifts", "trial"] try: hook_results = await run_hooks("renewal_forbidden_groups", chat_id=tg_id, admin=False, session=ctx.session) for hook_result in hook_results: additional_groups = hook_result.get("additional_groups", []) forbidden_groups.extend(additional_groups) except Exception as error: logger.warning(f"[AUTO_RENEW] Ошибка при получении дополнительных групп: {error}") if current_tariff["group_code"] in forbidden_groups: return False, None, None renewal_cost = await resolve_price_to_charge( ctx.session, { "tariff_id": current_tariff.get("id"), "selected_device_limit": getattr(key, "selected_device_limit", None), "selected_traffic_limit": getattr(key, "selected_traffic_limit", None), "selected_price_rub": getattr(key, "selected_price_rub", None), }, ) if renewal_cost is None or balance < renewal_cost: return False, None, None client_id = key.client_id current_expiry = key.expiry_time duration_days = current_tariff["duration_days"] selected_device_limit = getattr(key, "selected_device_limit", None) selected_traffic_limit = getattr(key, "selected_traffic_limit", None) selected_traffic_gb = int(selected_traffic_limit) if selected_traffic_limit is not None else None device_limit_effective, traffic_limit_bytes_effective = await get_effective_limits_for_key( session=ctx.session, tariff_id=int(current_tariff["id"]), selected_device_limit=int(selected_device_limit) if selected_device_limit is not None else None, selected_traffic_gb=selected_traffic_gb, ) traffic_limit_gb = int(traffic_limit_bytes_effective / GB) if traffic_limit_bytes_effective else 0 new_expiry_time = ( current_expiry if current_expiry > datetime.utcnow().timestamp() * 1000 else datetime.utcnow().timestamp() * 1000 ) + duration_days * 24 * 60 * 60 * 1000 logger.info( f"Продление подписки {email} на {duration_days} дней для пользователя {tg_id}. " f"Баланс: {balance}, списываем: {renewal_cost}" ) key_subgroup = current_tariff.get("subgroup_title") await renew_key_in_cluster( cluster_id=server_id, email=email, client_id=client_id, new_expiry_time=int(new_expiry_time), total_gb=traffic_limit_gb, hwid_device_limit=device_limit_effective, session=ctx.session, target_subgroup=key_subgroup, old_subgroup=key_subgroup, plan=current_tariff["id"], ) if ctx.bulk_updates is not None: if tg_id in ctx.bulk_updates["balance_changes"]: ctx.bulk_updates["balance_changes"][tg_id] -= renewal_cost else: ctx.bulk_updates["balance_changes"][tg_id] = -renewal_cost ctx.bulk_updates["key_expiry_updates"].append((client_id, int(new_expiry_time))) ctx.bulk_updates["key_tariff_updates"].append((client_id, current_tariff["id"])) ctx.bulk_updates["notifications_to_add"].append((tg_id, renew_notification_id)) else: await update_balance(ctx.session, tg_id, -renewal_cost) await update_key_expiry(ctx.session, client_id, int(new_expiry_time)) await update_key_tariff(ctx.session, client_id, current_tariff["id"]) await add_notification(ctx.session, tg_id, renew_notification_id) return True, current_tariff, int(new_expiry_time) async def notify_expiring_keys( ctx: NotificationContext, keys: list, min_hours: int, max_hours: int, notify_type: str, photo: str, notify_renew_enabled: bool, ): if min_hours > 0: logger.info(f"Начало проверки подписок, истекающих через {min_hours}-{max_hours} часов.") else: logger.info(f"Начало проверки подписок, истекающих через {max_hours} часов.") min_threshold = int((datetime.now(moscow_tz) + timedelta(hours=min_hours)).timestamp() * 1000) max_threshold = int((datetime.now(moscow_tz) + timedelta(hours=max_hours)).timestamp() * 1000) expiring_keys = [key for key in keys if key.expiry_time and min_threshold < key.expiry_time <= max_threshold] if min_hours > 0: logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через {min_hours}-{max_hours} часов.") else: logger.info(f"Найдено {len(expiring_keys)} подписок, истекающих через {max_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(ctx.session, notify_type, max_hours, tg_ids=tg_ids, emails=emails) allowed_set = {(user["tg_id"], user["email"]) for user in allowed} messages = [] for key in expiring_keys: tg_id = key.tg_id email = key.email or "" if (tg_id, email) not in allowed_set: continue notification_id = f"{email}_{notify_type}" can_notify = await check_notification_time(ctx.session, tg_id, notification_id, hours=max_hours) if not can_notify: continue if notify_renew_enabled: try: renewed, tariff, new_expiry = await try_auto_renew(ctx, key) if renewed and tariff and new_expiry: await send_renewed_notification(ctx, key, tariff, new_expiry) await add_notification(ctx.session, tg_id, notification_id) else: await send_cannot_renew(ctx, key, photo) await add_notification(ctx.session, tg_id, notification_id) except Exception as error: logger.error(f"Ошибка авто-продления/уведомления для пользователя {tg_id}: {error}") else: expiry_data = await prepare_key_expiry_data(key, ctx.session, ctx.current_time) notification_text = KEY_EXPIRY.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"], ) keyboard = build_notification_kb(email) messages.append({ "tg_id": tg_id, "text": notification_text, "photo": photo, "keyboard": keyboard, "notification_id": notification_id, "email": email, }) if messages: results = await send_messages_with_limit(ctx.bot, messages, session=ctx.session) sent_count = 0 for msg, result in zip(messages, results, strict=False): await add_notification(ctx.session, msg["tg_id"], msg["notification_id"]) if result: sent_count += 1 logger.info( f"Отправлено уведомление об истекающей подписке {msg['email']} пользователю {msg['tg_id']}." ) logger.info(f"Отправлено {sent_count} уведомлений типа {notify_type}.") logger.info(f"Обработка уведомлений {notify_type} завершена.") await asyncio.sleep(1) async def handle_expired_keys(ctx: NotificationContext, keys: list): logger.info("Начало обработки истекших ключей.") expired_keys = [key for key in keys if key.expiry_time and key.expiry_time < ctx.current_time] logger.info(f"Найдено {len(expired_keys)} истекших ключей.") tg_ids = [key.tg_id for key in expired_keys] emails = [key.email or "" for key in expired_keys] users = await check_notifications_bulk(ctx.session, "key_expired", 0, tg_ids=tg_ids, emails=emails) users_set = {(user["tg_id"], user["email"]) for user in users} notify_renew_expired_enabled = bool(NOTIFICATIONS_CONFIG.get("RENEW_EXPIRED_ENABLED", NOTIFY_RENEW_EXPIRED)) notify_delete_key_enabled = bool(NOTIFICATIONS_CONFIG.get("DELETE_KEY_ENABLED", NOTIFY_DELETE_KEY)) delete_key_delay_minutes = int(NOTIFICATIONS_CONFIG.get("DELETE_KEY_DELAY_MINUTES", NOTIFY_DELETE_DELAY)) for key in expired_keys: tg_id = key.tg_id email = key.email or "" client_id = key.client_id server_id = key.server_id notification_id = f"{email}_key_expired" last_notification_time = await get_last_notification_time(ctx.session, tg_id, notification_id) if notify_renew_expired_enabled: try: renewed, tariff, new_expiry = await try_auto_renew(ctx, key) if renewed and tariff and new_expiry: await send_renewed_notification(ctx, key, tariff, new_expiry) if ctx.bulk_updates: ctx.bulk_updates["notifications_to_delete"].append((tg_id, notification_id)) else: await delete_notification(ctx.session, tg_id, notification_id) continue except Exception as error: logger.error(f"Ошибка авто-продления для пользователя {tg_id}: {error}") continue if notify_delete_key_enabled: should_delete = False if delete_key_delay_minutes == 0: should_delete = True elif last_notification_time is not None: minutes_passed = (ctx.current_time - last_notification_time) / (1000 * 60) should_delete = minutes_passed >= delete_key_delay_minutes logger.info(f"Прошло минут={minutes_passed:.2f} DELETE_KEY_DELAY_MINUTES={delete_key_delay_minutes}") if should_delete: try: await delete_key_from_cluster(server_id, email, client_id, ctx.session) await delete_key(ctx.session, client_id) logger.info(f"🗑 Ключ {client_id} для пользователя {tg_id} успешно удалён.") await send_deleted_notification(ctx, key) except Exception as error: logger.error(f"Ошибка удаления ключа {client_id} для пользователя {tg_id}: {error}") continue if last_notification_time is None and (tg_id, email) in users_set: await send_expired_notification(ctx, key, delete_key_delay_minutes) await add_notification(ctx.session, tg_id, notification_id) logger.info("Обработка истекших ключей завершена.") await asyncio.sleep(1) async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker): while True: notification_interval = int(NOTIFICATIONS_CONFIG.get("BASE_NOTIFICATION_MINUTE", NOTIFICATION_TIME)) if notification_lock.locked(): logger.warning("Уведомления уже выполняются. Пропуск...") await asyncio.sleep(notification_interval) continue async with notification_lock: try: async with sessionmaker() as session: logger.info("Запуск обработки уведомлений") current_time = int(datetime.now(moscow_tz).timestamp() * 1000) start_time = datetime.now() try: preload_data = await preload_notification_data(session) keys_data = preload_data["keys_data"] keys = [data["key"] for data in keys_data.values()] preload_time = (datetime.now() - start_time).total_seconds() logger.info( f"Предзагружено данных: {len(keys)} ключей, " f"{len(preload_data['tariffs_cache'])} тарифов за {preload_time:.2f}s" ) bulk_updates = { "balance_changes": {}, "key_expiry_updates": [], "key_tariff_updates": [], "notifications_to_add": [], "notifications_to_delete": [], } except Exception as error: logger.error(f"Ошибка при предварительной загрузке данных: {error}") try: keys = await get_all_keys(session=session) keys = [k for k in keys if not k.is_frozen] preload_data = None bulk_updates = None preload_time = (datetime.now() - start_time).total_seconds() logger.info(f"Fallback: получено {len(keys)} ключей за {preload_time:.2f}s") except Exception as fallback_error: logger.error(f"Ошибка fallback получения ключей: {fallback_error}") keys = [] preload_data = None bulk_updates = None ctx = NotificationContext( bot=bot, session=session, current_time=current_time, preload_data=preload_data, bulk_updates=bulk_updates, ) trial_time_disable = bool(MODES_CONFIG.get("TRIAL_TIME_DISABLED", TRIAL_TIME_DISABLE)) if not trial_time_disable: try: await notify_inactive_trial_users(bot, session) except Exception as error: logger.error(f"Ошибка в notify_inactive_trial_users: {error}") notify_24_enabled = bool(NOTIFICATIONS_CONFIG.get("EXPIRY_24H_ENABLED", NOTIFY_24H_ENABLED)) notify_24_hours = int(NOTIFICATIONS_CONFIG.get("EXPIRY_24H_BEFORE_HOURS", NOTIFY_24H_HOURS)) notify_10_enabled = bool(NOTIFICATIONS_CONFIG.get("EXPIRY_10H_ENABLED", NOTIFY_10H_ENABLED)) notify_10_hours = int(NOTIFICATIONS_CONFIG.get("EXPIRY_10H_BEFORE_HOURS", NOTIFY_10H_HOURS)) notify_renew_enabled = bool(NOTIFICATIONS_CONFIG.get("RENEW_ENABLED", NOTIFY_RENEW)) inactive_traffic_enabled = bool( NOTIFICATIONS_CONFIG.get("INACTIVE_TRAFFIC_ENABLED", NOTIFY_INACTIVE_TRAFFIC) ) notify_hot_leads_enabled = bool(NOTIFICATIONS_CONFIG.get("HOT_LEADS_ENABLED", NOTIFY_HOT_LEADS)) if notify_24_enabled: try: await notify_expiring_keys( ctx, keys, min_hours=notify_10_hours if notify_10_enabled else 0, max_hours=notify_24_hours, notify_type="key_24h", photo="notify_24h.jpg", notify_renew_enabled=notify_renew_enabled, ) except Exception as error: logger.error(f"Ошибка в notify_expiring_keys (24h): {error}") if notify_10_enabled: try: await notify_expiring_keys( ctx, keys, min_hours=0, max_hours=notify_10_hours, notify_type="key_10h", photo="notify_10h.jpg", notify_renew_enabled=notify_renew_enabled, ) except Exception as error: logger.error(f"Ошибка в notify_expiring_keys (10h): {error}") try: await handle_expired_keys(ctx, keys) except Exception as error: logger.error(f"Ошибка в handle_expired_keys: {error}") if inactive_traffic_enabled: try: await notify_users_no_traffic(bot, session, current_time, keys) except Exception as error: logger.error(f"Ошибка в notify_users_no_traffic: {error}") try: await run_hooks("periodic_notifications", bot=bot, session=session, keys=keys) except Exception as error: logger.error(f"Ошибка в хуках periodic_notifications: {error}") if notify_hot_leads_enabled: try: await notify_hot_leads(bot, session) except Exception as error: logger.error(f"Ошибка в notify_hot_leads: {error}") if bulk_updates: bulk_start = datetime.now() await execute_bulk_updates(session, bulk_updates) bulk_time = (datetime.now() - bulk_start).total_seconds() total_renewals = len(bulk_updates["balance_changes"]) total_key_updates = len(bulk_updates["key_expiry_updates"]) + len( bulk_updates["key_tariff_updates"] ) total_notification_updates = len(bulk_updates["notifications_to_add"]) + len( bulk_updates["notifications_to_delete"] ) logger.info( f"Bulk-операции выполнены за {bulk_time:.2f}s. Обработано: {total_renewals} продлений, {total_key_updates} ключей, {total_notification_updates} уведомлений" ) total_time = (datetime.now() - start_time).total_seconds() logger.info(f"Уведомления завершены за {total_time:.2f}s") except Exception as error: logger.error(f"Ошибка в periodic_notifications: {error}") await asyncio.sleep(notification_interval)