Files

242 lines
9.8 KiB
Python

from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import pytz
from sqlalchemy.ext.asyncio import async_sessionmaker
from config import EXECUTOR_POOL_SIZE
from database import add_notification, check_notification_time_bulk, check_notifications_bulk
from database.web_notifications import notify_web
from handlers.notifications.context import NotificationContext
from handlers.notifications.keyboards import build_change_tariff_kb, build_notification_expired_kb, build_notification_kb
from handlers.notifications.renewal import RenewalResult, RenewalStatus, try_auto_renew
from handlers.notifications.sender import (
NotificationRateLimiter,
prepare_key_expiry_data,
send_messages_with_limit,
send_notification,
)
from handlers.texts import KEY_CANNOT_RENEW_CURRENT, KEY_EXPIRY, get_renewal_message
from handlers.utils import get_russian_month
from logger import logger
from middlewares.session import wrap_session
from services.tariffs.tariff_display import GB, get_effective_limits_for_key
moscow_tz = pytz.timezone("Europe/Moscow")
async def process_expiring_keys(
ctx: NotificationContext,
keys: list,
min_hours: int,
max_hours: int,
notify_type: str,
photo: str,
notify_renew_enabled: bool,
sessionmaker: async_sessionmaker | None = None,
):
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 = [k for k in keys if k.expiry_time and min_threshold < k.expiry_time <= max_threshold]
if not expiring_keys:
return
logger.info(f"[{notify_type}] Найдено {len(expiring_keys)} истекающих ключей")
tg_ids = [k.tg_id for k in expiring_keys]
emails = [k.email or "" for k in expiring_keys]
allowed = await check_notifications_bulk(ctx.session, notify_type, max_hours, tg_ids=tg_ids, emails=emails)
allowed_set = {(u["tg_id"], u["email"]) for u in allowed}
notify_pairs = [
(k.tg_id, f"{(k.email or '')}_{notify_type}")
for k in expiring_keys
if (k.tg_id, k.email or "") in allowed_set
]
can_notify_set = await check_notification_time_bulk(ctx.session, notify_pairs, max_hours)
renew_candidates = []
simple_notify = []
for key in expiring_keys:
tg_id = key.tg_id
email = key.email or ""
notification_id = f"{email}_{notify_type}"
if (tg_id, email) not in allowed_set:
continue
if (tg_id, notification_id) not in can_notify_set:
continue
if notify_renew_enabled:
renew_candidates.append((key, notification_id))
else:
simple_notify.append((key, notification_id))
if renew_candidates:
await _process_renew_candidates(ctx, renew_candidates, photo, sessionmaker)
if simple_notify:
await _send_simple_warnings(ctx, simple_notify, photo, notify_type)
async def _process_renew_candidates(
ctx: NotificationContext,
candidates: list[tuple],
photo: str,
sessionmaker: async_sessionmaker | None,
):
use_parallel = sessionmaker is not None and EXECUTOR_POOL_SIZE > 1
rate_limiter = NotificationRateLimiter(max_rate=30, window=1.0)
if use_parallel:
semaphore = asyncio.Semaphore(EXECUTOR_POOL_SIZE)
async def do_one(key, notification_id):
async with semaphore:
async with sessionmaker() as session:
session = wrap_session(session, sessionmaker)
one_ctx = NotificationContext(
bot=ctx.bot,
session=session,
current_time=ctx.current_time,
preload_data=ctx.preload_data,
bulk_updates=None,
)
try:
result = await try_auto_renew(one_ctx, key)
return (key, notification_id, result)
except Exception as e:
logger.error(f"Ошибка продления {key.tg_id} ({key.email}): {e}")
return (key, notification_id, RenewalResult(RenewalStatus.NO_TARIFF))
tasks = [do_one(k, nid) for k, nid in candidates]
results = await asyncio.gather(*tasks, return_exceptions=True)
else:
results = []
for key, notification_id in candidates:
try:
result = await try_auto_renew(ctx, key)
results.append((key, notification_id, result))
except Exception as e:
logger.error(f"Ошибка продления {key.tg_id}: {e}")
results.append((key, notification_id, RenewalResult(RenewalStatus.NO_TARIFF)))
for item in results:
if isinstance(item, Exception):
logger.error(f"Ошибка задачи продления: {item}")
continue
key, notification_id, renewal_result = item
tg_id = key.tg_id
email = key.email or ""
await rate_limiter.acquire()
if renewal_result.status == RenewalStatus.SUCCESS:
await _send_renewed(ctx, key, renewal_result.tariff, renewal_result.new_expiry_time)
elif renewal_result.status in (RenewalStatus.FORBIDDEN_TARIFF, RenewalStatus.NO_TARIFF):
await _send_change_tariff(ctx, key, photo)
else:
await _send_expiry_warning(ctx, key, photo)
await add_notification(ctx.session, tg_id, notification_id)
async def _send_simple_warnings(ctx: NotificationContext, items: list[tuple], photo: str, notify_type: str):
messages = []
for key, notification_id in items:
tg_id = key.tg_id
email = key.email or ""
expiry_data = await prepare_key_expiry_data(key, ctx.session, ctx.current_time)
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, getattr(key, "client_id", None))
messages.append({
"tg_id": tg_id,
"text": text,
"photo": photo,
"keyboard": keyboard,
"notification_id": notification_id,
})
try:
await notify_web(ctx.session, tg_id=tg_id, type="key_expiry", template_vars={"email": email}, data={"email": email})
except Exception as e:
logger.warning(f"[Notifications] web-уведомление key_expiry tg_id={tg_id}: {e}")
if messages:
results = await send_messages_with_limit(ctx.bot, messages)
for msg, result in zip(messages, results, strict=False):
await add_notification(ctx.session, msg["tg_id"], msg["notification_id"])
if result:
logger.info(f"Уведомление {notify_type} отправлено {msg['tg_id']}")
async def _send_expiry_warning(ctx: NotificationContext, key, photo: str) -> bool:
expiry_data = await prepare_key_expiry_data(key, ctx.session, ctx.current_time)
text = KEY_EXPIRY.format(
email=key.email or "",
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(key.email or "", getattr(key, "client_id", None))
return await send_notification(ctx.bot, key.tg_id, photo, text, keyboard)
async def _send_change_tariff(ctx: NotificationContext, key, photo: str) -> bool:
expiry_data = await prepare_key_expiry_data(key, ctx.session, ctx.current_time)
text = KEY_CANNOT_RENEW_CURRENT.format(
email=key.email or "",
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(key.email or "", getattr(key, "client_id", None))
return await send_notification(ctx.bot, key.tg_id, photo, text, keyboard)
async def _send_renewed(ctx: NotificationContext, key, tariff: dict, new_expiry_time: int) -> bool:
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)),
)
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()
return await send_notification(ctx.bot, key.tg_id, "notify_expired.jpg", text, keyboard)