172 lines
6.1 KiB
Python
172 lines
6.1 KiB
Python
import json
|
|
import re
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
|
from sqlalchemy import and_, distinct, exists, func, not_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from core.constants import PAYMENT_SYSTEMS_EXCLUDED
|
|
from database.models import BlockedUser, Key, ManualBan, Payment, Server, Tariff, User
|
|
from logger import logger
|
|
|
|
|
|
def _not_banned(user_id_col):
|
|
return ~exists().where(BlockedUser.user_id == user_id_col) & ~exists().where(
|
|
ManualBan.user_id == user_id_col,
|
|
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.now(timezone.utc)),
|
|
)
|
|
|
|
|
|
async def get_recipients(session: AsyncSession, send_to: str, cluster_name: str | None = None) -> tuple[list[int], int]:
|
|
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
|
|
|
|
query = None
|
|
|
|
if send_to == "subscribed":
|
|
query = (
|
|
select(distinct(User.tg_id))
|
|
.join(Key, Key.user_id == User.id)
|
|
.where(Key.expiry_time > now_ms)
|
|
.where(User.tg_id.isnot(None))
|
|
.where(_not_banned(User.id))
|
|
)
|
|
|
|
elif send_to == "unsubscribed":
|
|
unsub_base = (
|
|
select(User.id.label("uid"), User.tg_id)
|
|
.outerjoin(Key, User.id == Key.user_id)
|
|
.group_by(User.id, User.tg_id)
|
|
.having(func.count(Key.client_id) == 0)
|
|
.union_all(
|
|
select(User.id.label("uid"), User.tg_id)
|
|
.join(Key, User.id == Key.user_id)
|
|
.group_by(User.id, User.tg_id)
|
|
.having(func.max(Key.expiry_time) <= now_ms)
|
|
)
|
|
).subquery()
|
|
query = (
|
|
select(distinct(unsub_base.c.tg_id))
|
|
.select_from(unsub_base)
|
|
.where(unsub_base.c.tg_id.isnot(None))
|
|
.where(
|
|
~exists().where(BlockedUser.user_id == unsub_base.c.uid),
|
|
~exists().where(
|
|
ManualBan.user_id == unsub_base.c.uid,
|
|
(ManualBan.until.is_(None)) | (ManualBan.until > datetime.now(timezone.utc)),
|
|
),
|
|
)
|
|
)
|
|
|
|
elif send_to == "untrial":
|
|
key_user_ids = select(Key.user_id).distinct()
|
|
query = (
|
|
select(distinct(User.tg_id))
|
|
.where(~User.id.in_(key_user_ids) & User.trial.in_([0, -1]))
|
|
.where(User.tg_id.isnot(None))
|
|
.where(_not_banned(User.id))
|
|
)
|
|
|
|
elif send_to == "cluster":
|
|
query = (
|
|
select(distinct(User.tg_id))
|
|
.join(Key, Key.user_id == User.id)
|
|
.join(Server, Key.server_id == Server.cluster_name)
|
|
.where(Server.cluster_name == cluster_name)
|
|
.where(User.tg_id.isnot(None))
|
|
.where(_not_banned(User.id))
|
|
)
|
|
|
|
elif send_to == "hotleads":
|
|
query = (
|
|
select(distinct(User.tg_id))
|
|
.join(Payment, User.id == Payment.user_id)
|
|
.where(Payment.status == "success")
|
|
.where(Payment.amount > 0)
|
|
.where(Payment.payment_system.notin_(PAYMENT_SYSTEMS_EXCLUDED))
|
|
.where(
|
|
not_(exists(select(1).select_from(Key).where(and_(Key.user_id == User.id, Key.expiry_time > now_ms))))
|
|
)
|
|
.where(User.tg_id.isnot(None))
|
|
.where(_not_banned(User.id))
|
|
)
|
|
|
|
elif send_to == "trial":
|
|
trial_tariff_subquery = select(Tariff.id).where(Tariff.group_code == "trial")
|
|
query = (
|
|
select(distinct(User.tg_id))
|
|
.join(Key, Key.user_id == User.id)
|
|
.where(Key.tariff_id.in_(trial_tariff_subquery))
|
|
.where(User.tg_id.isnot(None))
|
|
.where(_not_banned(User.id))
|
|
)
|
|
|
|
else:
|
|
query = select(distinct(User.tg_id)).where(User.tg_id.isnot(None)).where(_not_banned(User.id))
|
|
|
|
result = await session.execute(query)
|
|
tg_ids = [row[0] for row in result.all()]
|
|
return tg_ids, len(tg_ids)
|
|
|
|
|
|
def strip_html_tags(text: str) -> str:
|
|
text = re.sub(r'<tg-emoji emoji-id="[^"]*">([^<]*)</tg-emoji>', r"\1", text)
|
|
text = re.sub(r"<[^>]+>", "", text)
|
|
text = text.replace("<", "<").replace(">", ">").replace("&", "&")
|
|
return text.strip()
|
|
|
|
|
|
def parse_message_buttons(text: str) -> tuple[str, InlineKeyboardMarkup | None]:
|
|
if "BUTTONS:" not in text:
|
|
return text, None
|
|
|
|
parts = text.split("BUTTONS:", 1)
|
|
clean_text = parts[0].strip()
|
|
buttons_text = parts[1].strip()
|
|
|
|
if not buttons_text:
|
|
return clean_text, None
|
|
|
|
buttons = []
|
|
button_lines = [line.strip() for line in buttons_text.split("\n") if line.strip()]
|
|
|
|
for line in button_lines:
|
|
try:
|
|
cleaned_line = re.sub(r'<tg-emoji emoji-id="[^"]*">([^<]*)</tg-emoji>', r"\1", line)
|
|
button_data = json.loads(cleaned_line)
|
|
|
|
if not isinstance(button_data, dict) or "text" not in button_data:
|
|
logger.warning(f"[Sender] Неверный формат кнопки: {line}")
|
|
continue
|
|
|
|
text_btn = button_data["text"]
|
|
|
|
if "callback" in button_data:
|
|
callback_data = button_data["callback"]
|
|
if len(callback_data) > 64:
|
|
logger.warning(f"[Sender] Callback слишком длинный: {callback_data}")
|
|
continue
|
|
button = InlineKeyboardButton(text=text_btn, callback_data=callback_data)
|
|
elif "url" in button_data:
|
|
url = button_data["url"]
|
|
button = InlineKeyboardButton(text=text_btn, url=url)
|
|
else:
|
|
logger.warning(f"[Sender] Кнопка без действия: {line}")
|
|
continue
|
|
|
|
buttons.append([button])
|
|
|
|
except json.JSONDecodeError as e:
|
|
logger.warning(f"[Sender] Ошибка парсинга JSON кнопки: {line} - {e}")
|
|
continue
|
|
except Exception as e:
|
|
logger.error(f"[Sender] Ошибка создания кнопки: {line} - {e}")
|
|
continue
|
|
|
|
if not buttons:
|
|
return clean_text, None
|
|
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
|
return clean_text, keyboard
|