Compare commits

...
6 Commits
11 changed files with 402 additions and 65 deletions
+40 -8
View File
@@ -15,9 +15,15 @@ from bot.states.admin_states import AdminStates
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
from bot.services.referral_service import ReferralService
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.utils import get_message_content, send_direct_message from bot.utils import get_message_content, send_direct_message
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
from bot.utils.text_sanitizer import (
sanitize_display_name,
sanitize_username,
username_for_display,
)
router = Router(name="admin_user_management_router") router = Router(name="admin_user_management_router")
USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$") USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
@@ -106,7 +112,8 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb
async def format_user_card(user: User, session: AsyncSession, async def format_user_card(user: User, session: AsyncSession,
subscription_service: SubscriptionService, subscription_service: SubscriptionService,
i18n_instance, lang: str) -> str: i18n_instance, lang: str,
referral_service: Optional[ReferralService] = None) -> str:
"""Format user information as a detailed card""" """Format user information as a detailed card"""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
@@ -116,8 +123,16 @@ async def format_user_card(user: User, session: AsyncSession,
# User details # User details
na_value = _("admin_user_na_value", default="N/A") na_value = _("admin_user_na_value", default="N/A")
user_name = user.first_name or na_value safe_first_name = sanitize_display_name(user.first_name) if user.first_name else None
username_display = f"@{user.username}" if user.username else na_value user_name = safe_first_name or na_value
if user.username:
sanitized_username = sanitize_username(user.username)
if sanitized_username:
username_display = f"@{sanitized_username}"
else:
username_display = username_for_display(user.username, with_at=False)
else:
username_display = na_value
registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else na_value registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else na_value
card_parts.append(f"{_('admin_user_id_label', default='🆔 <b>ID:</b>')} {hcode(str(user.user_id))}") card_parts.append(f"{_('admin_user_id_label', default='🆔 <b>ID:</b>')} {hcode(str(user.user_id))}")
@@ -176,6 +191,17 @@ async def format_user_card(user: User, session: AsyncSession,
had_subscriptions = await subscription_service.has_had_any_subscription(session, user.user_id) had_subscriptions = await subscription_service.has_had_any_subscription(session, user.user_id)
trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал") trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал")
card_parts.append(f"{_('admin_user_trial_label', default='🏡 <b>Триал:</b>')} {hcode(trial_status)}") card_parts.append(f"{_('admin_user_trial_label', default='🏡 <b>Триал:</b>')} {hcode(trial_status)}")
# Referral stats
if referral_service is not None:
try:
stats = await referral_service.get_referral_stats(session, user.user_id)
invited_count = stats.get('invited_count', 0)
purchased_count = stats.get('purchased_count', 0)
card_parts.append(f"{_('admin_user_invited_friends_label', default='👥 <b>Приглашено друзей:</b>')} {hcode(str(invited_count))}")
card_parts.append(f"{_('admin_user_ref_purchased_label', default='💳 <b>Купили подписку:</b>')} {hcode(str(purchased_count))}")
except Exception as e_rs:
logging.error(f"Failed to build referral stats for admin card {user.user_id}: {e_rs}")
except Exception as e: except Exception as e:
logging.error(f"Error getting user statistics for {user.user_id}: {e}") logging.error(f"Error getting user statistics for {user.user_id}: {e}")
@@ -224,7 +250,8 @@ async def process_user_search_handler(message: types.Message, state: FSMContext,
# Format and send user card # Format and send user card
try: try:
user_card_text = await format_user_card(user_model, session, subscription_service, i18n, current_lang) referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
user_card_text = await format_user_card(user_model, session, subscription_service, i18n, current_lang, referral_service)
keyboard = get_user_card_keyboard(user_model.user_id, i18n, current_lang) keyboard = get_user_card_keyboard(user_model.user_id, i18n, current_lang)
await message.answer( await message.answer(
@@ -482,7 +509,10 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User,
await callback.answer("User not found", show_alert=True) await callback.answer("User not found", show_alert=True)
return return
user_card_text = await format_user_card(fresh_user, session, subscription_service, i18n_instance, lang) from config.settings import Settings as _Settings
_settings = _Settings()
referral_service = ReferralService(_settings, subscription_service, callback.message.bot, i18n_instance)
user_card_text = await format_user_card(fresh_user, session, subscription_service, i18n_instance, lang, referral_service)
keyboard = get_user_card_keyboard(fresh_user.user_id, i18n_instance, lang) keyboard = get_user_card_keyboard(fresh_user.user_id, i18n_instance, lang)
try: try:
@@ -556,7 +586,8 @@ async def process_subscription_days_handler(message: types.Message, state: FSMCo
# Show updated user card # Show updated user card
user = await user_dal.get_user_by_id(session, target_user_id) user = await user_dal.get_user_by_id(session, target_user_id)
if user: if user:
user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang) referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang, referral_service)
keyboard = get_user_card_keyboard(user.user_id, i18n, current_lang) keyboard = get_user_card_keyboard(user.user_id, i18n, current_lang)
await message.answer( await message.answer(
@@ -664,7 +695,8 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
async with PanelApiService(settings) as panel_service: async with PanelApiService(settings) as panel_service:
subscription_service = SubscriptionService(settings, panel_service) subscription_service = SubscriptionService(settings, panel_service)
user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang) referral_service = ReferralService(settings, subscription_service, bot, i18n)
user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang, referral_service)
keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang) keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang)
await message.answer( await message.answer(
@@ -931,4 +963,4 @@ async def process_unban_user_handler(message: types.Message, state: FSMContext,
default="❌ Ошибка разблокировки пользователя" default="❌ Ошибка разблокировки пользователя"
)) ))
await state.clear() await state.clear()
+7 -4
View File
@@ -22,6 +22,7 @@ from bot.middlewares.i18n import JsonI18n
from config.settings import Settings from config.settings import Settings
from bot.services.notification_service import NotificationService from bot.services.notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
payment_processing_lock = asyncio.Lock() payment_processing_lock = asyncio.Lock()
@@ -241,10 +242,12 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
if db_user and db_user.referred_by_id: if db_user and db_user.referred_by_id:
inviter = await user_dal.get_user_by_id( inviter = await user_dal.get_user_by_id(
session, db_user.referred_by_id) session, db_user.referred_by_id)
if inviter and inviter.first_name: if inviter:
inviter_name_display = inviter.first_name safe_name = sanitize_display_name(inviter.first_name) if inviter.first_name else None
elif inviter and inviter.username: if safe_name:
inviter_name_display = f"@{inviter.username}" inviter_name_display = safe_name
elif inviter.username:
inviter_name_display = username_for_display(inviter.username, with_at=False)
details_message = _( details_message = _(
"payment_successful_with_referral_bonus_full", "payment_successful_with_referral_bonus_full",
+16 -11
View File
@@ -17,6 +17,7 @@ from bot.services.referral_service import ReferralService
from bot.services.promo_code_service import PromoCodeService from bot.services.promo_code_service import PromoCodeService
from config.settings import Settings from config.settings import Settings
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.utils.text_sanitizer import sanitize_username, sanitize_display_name
router = Router(name="user_start_router") router = Router(name="user_start_router")
@@ -152,13 +153,17 @@ async def start_command_handler(message: types.Message,
ad_start_param = ad_param_match.group(1) ad_start_param = ad_param_match.group(1)
logging.info(f"User {user_id} started with ad start param: {ad_start_param}") logging.info(f"User {user_id} started with ad start param: {ad_start_param}")
sanitized_username = sanitize_username(user.username)
sanitized_first_name = sanitize_display_name(user.first_name)
sanitized_last_name = sanitize_display_name(user.last_name)
db_user = await user_dal.get_user_by_id(session, user_id) db_user = await user_dal.get_user_by_id(session, user_id)
if not db_user: if not db_user:
user_data_to_create = { user_data_to_create = {
"user_id": user_id, "user_id": user_id,
"username": user.username, "username": sanitized_username,
"first_name": user.first_name, "first_name": sanitized_first_name,
"last_name": user.last_name, "last_name": sanitized_last_name,
"language_code": current_lang, "language_code": current_lang,
"referred_by_id": referred_by_user_id, "referred_by_id": referred_by_user_id,
"registration_date": datetime.now(timezone.utc) "registration_date": datetime.now(timezone.utc)
@@ -177,8 +182,8 @@ async def start_command_handler(message: types.Message,
notification_service = NotificationService(message.bot, settings, i18n) notification_service = NotificationService(message.bot, settings, i18n)
await notification_service.notify_new_user_registration( await notification_service.notify_new_user_registration(
user_id=user_id, user_id=user_id,
username=user.username, username=sanitized_username,
first_name=user.first_name, first_name=sanitized_first_name,
referred_by_id=referred_by_user_id referred_by_id=referred_by_user_id
) )
except Exception as e: except Exception as e:
@@ -203,12 +208,12 @@ async def start_command_handler(message: types.Message,
is_active_now = False is_active_now = False
if not is_active_now: if not is_active_now:
update_payload["referred_by_id"] = referred_by_user_id update_payload["referred_by_id"] = referred_by_user_id
if user.username != db_user.username: if sanitized_username != db_user.username:
update_payload["username"] = user.username update_payload["username"] = sanitized_username
if user.first_name != db_user.first_name: if sanitized_first_name != db_user.first_name:
update_payload["first_name"] = user.first_name update_payload["first_name"] = sanitized_first_name
if user.last_name != db_user.last_name: if sanitized_last_name != db_user.last_name:
update_payload["last_name"] = user.last_name update_payload["last_name"] = sanitized_last_name
if update_payload: if update_payload:
try: try:
+15 -11
View File
@@ -6,6 +6,7 @@ from aiogram.types import Update, User as TgUser
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from db.dal import user_dal from db.dal import user_dal
from bot.utils.text_sanitizer import sanitize_username, sanitize_display_name, username_for_display
class ProfileSyncMiddleware(BaseMiddleware): class ProfileSyncMiddleware(BaseMiddleware):
@@ -24,12 +25,16 @@ class ProfileSyncMiddleware(BaseMiddleware):
db_user = await user_dal.get_user_by_id(session, tg_user.id) db_user = await user_dal.get_user_by_id(session, tg_user.id)
if db_user: if db_user:
update_payload: Dict[str, Any] = {} update_payload: Dict[str, Any] = {}
if db_user.username != tg_user.username: sanitized_username = sanitize_username(tg_user.username)
update_payload["username"] = tg_user.username sanitized_first_name = sanitize_display_name(tg_user.first_name)
if db_user.first_name != tg_user.first_name: sanitized_last_name = sanitize_display_name(tg_user.last_name)
update_payload["first_name"] = tg_user.first_name
if db_user.last_name != tg_user.last_name: if db_user.username != sanitized_username:
update_payload["last_name"] = tg_user.last_name update_payload["username"] = sanitized_username
if db_user.first_name != sanitized_first_name:
update_payload["first_name"] = sanitized_first_name
if db_user.last_name != sanitized_last_name:
update_payload["last_name"] = sanitized_last_name
if update_payload: if update_payload:
await user_dal.update_user(session, tg_user.id, update_payload) await user_dal.update_user(session, tg_user.id, update_payload)
@@ -42,10 +47,10 @@ class ProfileSyncMiddleware(BaseMiddleware):
panel_service = data.get("panel_service") panel_service = data.get("panel_service")
if panel_service and db_user.panel_user_uuid: if panel_service and db_user.panel_user_uuid:
description_text = "\n".join([ description_text = "\n".join([
tg_user.username or "", username_for_display(tg_user.username, with_at=False) if sanitized_username is not None else "",
tg_user.first_name or "", sanitized_first_name or "",
tg_user.last_name or "", sanitized_last_name or "",
]) ]).strip()
await panel_service.update_user_details_on_panel( await panel_service.update_user_details_on_panel(
db_user.panel_user_uuid, db_user.panel_user_uuid,
{"description": description_text}, {"description": description_text},
@@ -62,4 +67,3 @@ class ProfileSyncMiddleware(BaseMiddleware):
return await handler(event, data) return await handler(event, data)
+7 -4
View File
@@ -16,6 +16,7 @@ from bot.services.referral_service import ReferralService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from bot.services.notification_service import NotificationService from bot.services.notification_service import NotificationService
from db.dal import payment_dal, user_dal from db.dal import payment_dal, user_dal
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
class CryptoPayService: class CryptoPayService:
@@ -189,10 +190,12 @@ class CryptoPayService:
inviter_name_display = _("friend_placeholder") inviter_name_display = _("friend_placeholder")
if db_user and db_user.referred_by_id: if db_user and db_user.referred_by_id:
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id) inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
if inviter and inviter.first_name: if inviter:
inviter_name_display = inviter.first_name safe_name = sanitize_display_name(inviter.first_name) if inviter.first_name else None
elif inviter and inviter.username: if safe_name:
inviter_name_display = f"@{inviter.username}" inviter_name_display = safe_name
elif inviter.username:
inviter_name_display = username_for_display(inviter.username, with_at=False)
text = _("payment_successful_with_referral_bonus_full", text = _("payment_successful_with_referral_bonus_full",
months=months, months=months,
base_end_date=activation["end_date"].strftime('%Y-%m-%d'), base_end_date=activation["end_date"].strftime('%Y-%m-%d'),
+38 -16
View File
@@ -10,6 +10,10 @@ from config.settings import Settings
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.utils.message_queue import get_queue_manager from bot.utils.message_queue import get_queue_manager
from bot.utils.text_sanitizer import (
display_name_or_fallback,
username_for_display,
)
class NotificationService: class NotificationService:
@@ -19,6 +23,17 @@ class NotificationService:
self.bot = bot self.bot = bot
self.settings = settings self.settings = settings
self.i18n = i18n self.i18n = i18n
@staticmethod
def _format_user_display(
user_id: int,
username: Optional[str] = None,
first_name: Optional[str] = None,
) -> str:
base_display = display_name_or_fallback(first_name, f"ID {user_id}")
if username:
base_display = f"{base_display} ({username_for_display(username)})"
return base_display
async def _send_to_log_channel(self, message: str, thread_id: Optional[int] = None): async def _send_to_log_channel(self, message: str, thread_id: Optional[int] = None):
"""Send message to configured log channel/group using message queue""" """Send message to configured log channel/group using message queue"""
@@ -101,9 +116,11 @@ class NotificationService:
admin_lang = self.settings.DEFAULT_LANGUAGE admin_lang = self.settings.DEFAULT_LANGUAGE
_ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k
user_display = first_name or f"ID {user_id}" user_display = self._format_user_display(
if username: user_id=user_id,
user_display += f" (@{username})" username=username,
first_name=first_name,
)
referral_text = "" referral_text = ""
if referred_by_id: if referred_by_id:
@@ -134,9 +151,10 @@ class NotificationService:
admin_lang = self.settings.DEFAULT_LANGUAGE admin_lang = self.settings.DEFAULT_LANGUAGE
_ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k
user_display = f"ID {user_id}" user_display = self._format_user_display(
if username: user_id=user_id,
user_display += f" (@{username})" username=username,
)
provider_emoji = { provider_emoji = {
"yookassa": "💳", "yookassa": "💳",
@@ -174,9 +192,10 @@ class NotificationService:
admin_lang = self.settings.DEFAULT_LANGUAGE admin_lang = self.settings.DEFAULT_LANGUAGE
_ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k
user_display = f"ID {user_id}" user_display = self._format_user_display(
if username: user_id=user_id,
user_display += f" (@{username})" username=username,
)
message = _( message = _(
"log_promo_activation", "log_promo_activation",
@@ -203,9 +222,10 @@ class NotificationService:
admin_lang = self.settings.DEFAULT_LANGUAGE admin_lang = self.settings.DEFAULT_LANGUAGE
_ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(admin_lang, k, **kw) if self.i18n else k
user_display = f"ID {user_id}" user_display = self._format_user_display(
if username: user_id=user_id,
user_display += f" (@{username})" username=username,
)
message = _( message = _(
"log_trial_activation", "log_trial_activation",
@@ -268,9 +288,11 @@ class NotificationService:
_ = lambda k, **kw: self.i18n.gettext( _ = lambda k, **kw: self.i18n.gettext(
admin_lang, k, **kw) if self.i18n else k admin_lang, k, **kw) if self.i18n else k
user_display = first_name or f"ID {user_id}" user_display = self._format_user_display(
if username: user_id=user_id,
user_display += f" (@{username})" username=username,
first_name=first_name,
)
message = _( message = _(
"log_suspicious_promo", "log_suspicious_promo",
@@ -295,4 +317,4 @@ class NotificationService:
if to_admins: if to_admins:
await self._send_to_admins(message) await self._send_to_admins(message)
# Removed legacy helper functions that duplicated NotificationService API # Removed legacy helper functions that duplicated NotificationService API
+7 -5
View File
@@ -12,6 +12,7 @@ from .referral_service import ReferralService
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from .notification_service import NotificationService from .notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
class StarsService: class StarsService:
@@ -124,10 +125,12 @@ class StarsService:
db_user = await user_dal.get_user_by_id(session, message.from_user.id) db_user = await user_dal.get_user_by_id(session, message.from_user.id)
if db_user and db_user.referred_by_id: if db_user and db_user.referred_by_id:
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id) inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
if inviter and inviter.first_name: if inviter:
inviter_name_display = inviter.first_name safe_name = sanitize_display_name(inviter.first_name) if inviter.first_name else None
elif inviter and inviter.username: if safe_name:
inviter_name_display = f"@{inviter.username}" inviter_name_display = safe_name
elif inviter.username:
inviter_name_display = username_for_display(inviter.username, with_at=False)
success_msg = _( success_msg = _(
"payment_successful_with_referral_bonus_full", "payment_successful_with_referral_bonus_full",
months=months, months=months,
@@ -173,4 +176,3 @@ class StarsService:
) )
except Exception as e: except Exception as e:
logging.error(f"Failed to send stars payment notification: {e}") logging.error(f"Failed to send stars payment notification: {e}")
+40 -6
View File
@@ -16,6 +16,7 @@ from bot.services.referral_service import ReferralService
from .notification_service import NotificationService from .notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from db.dal import payment_dal, user_dal, subscription_dal from db.dal import payment_dal, user_dal, subscription_dal
from bot.utils.text_sanitizer import sanitize_display_name, username_for_display
def convert_period_to_months(period: Optional[str]) -> int: def convert_period_to_months(period: Optional[str]) -> int:
@@ -184,10 +185,12 @@ class TributeService:
inviter_name_display = _('friend_placeholder') inviter_name_display = _('friend_placeholder')
if db_user and db_user.referred_by_id: if db_user and db_user.referred_by_id:
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id) inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
if inviter and inviter.first_name: if inviter:
inviter_name_display = inviter.first_name safe_name = sanitize_display_name(inviter.first_name) if inviter.first_name else None
elif inviter and inviter.username: if safe_name:
inviter_name_display = f"@{inviter.username}" inviter_name_display = safe_name
elif inviter.username:
inviter_name_display = username_for_display(inviter.username, with_at=False)
success_msg = _( success_msg = _(
"payment_successful_with_referral_bonus_full", "payment_successful_with_referral_bonus_full",
months=months, months=months,
@@ -250,8 +253,39 @@ class TributeService:
from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup
try: try:
# Set all user's subscriptions to expire in 1 day (grace period) grace_days = 1
await subscription_dal.set_user_subscriptions_cancelled_with_grace(session, user_id, grace_days=1) grace_end = datetime.now(timezone.utc) + timedelta(days=grace_days)
active_subscriptions = await subscription_dal.get_active_subscriptions_for_user(session, user_id)
panel_users_updated: set[str] = set()
for sub in active_subscriptions:
updated_sub = await subscription_dal.update_subscription(
session,
sub.subscription_id,
{
"end_date": grace_end,
"status_from_panel": "CANCELLED",
"skip_notifications": True,
},
)
panel_uuid = updated_sub.panel_user_uuid if updated_sub else None
if panel_uuid and panel_uuid not in panel_users_updated:
panel_users_updated.add(panel_uuid)
panel_payload = {
"expireAt": grace_end.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
}
try:
await self.panel_service.update_user_details_on_panel(
panel_uuid,
panel_payload,
log_response=False,
)
except Exception as panel_err:
logging.error(
f"Failed to update panel expiry for user {user_id} (panel_uuid {panel_uuid}) during Tribute cancellation: {panel_err}")
await session.commit() await session.commit()
# Send notification about cancellation if enabled # Send notification about cancellation if enabled
+226
View File
@@ -0,0 +1,226 @@
import re
import unicodedata
from typing import Optional
_OBFUSCATION_CHARS = " .\\-/\\\\•﹒٫_․·∙‧ꞏ‒–—﹘﹣⁻−"
_URL_PATTERNS = [
re.compile(r"(?i)https?://\S+"),
re.compile(r"(?i)www\.\S+"),
re.compile(r"(?i)tg://\S+"),
re.compile(r"(?i)telegram\.me\S*"),
re.compile(r"(?i)t\.me/\+\S*"),
re.compile(r"(?i)joinchat\S*"),
]
_OBFUSCATED_DOMAIN_PATTERNS = [
re.compile(
r"(?i)[tт][\s{}\u2022]*[\.{}\u2022]*[\s{}\u2022]*[mм][eе]".format(
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
)
),
re.compile(
r"(?i)[tт][{}\s]*[eе][{}\s]*[lłl1i|][{}\s]*[eе]"
r"[{}\s]*[gɢgqг][{}\s]*[rр][{}\s]*[aа]"
r"[{}\s]*(?:[mм]|rn)".format(
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
re.escape(_OBFUSCATION_CHARS),
)
),
re.compile(r"(?i)t\.me\S*"),
]
_ENGLISH_SERVICE_PATTERNS = [
re.compile(r"(?i)telegram"),
re.compile(r"(?i)teleqram"),
re.compile(r"(?i)teiegram"),
re.compile(r"(?i)teieqram"),
re.compile(r"(?i)telegrarn"),
re.compile(r"(?i)service"),
re.compile(r"(?i)notif(?:ication)?"),
re.compile(r"(?i)system"),
re.compile(r"(?i)security"),
re.compile(r"(?i)safety"),
re.compile(r"(?i)support"),
re.compile(r"(?i)moderation"),
re.compile(r"(?i)review"),
re.compile(r"(?i)compliance"),
re.compile(r"(?i)abuse"),
re.compile(r"(?i)spam"),
re.compile(r"(?i)report"),
]
_RUSSIAN_SERVICE_PATTERNS = [
re.compile(r"(?i)телеграм\w*"),
re.compile(r"(?i)служебн\w*"),
re.compile(r"(?i)уведомлен\w*"),
re.compile(r"(?i)поддержк\w*"),
re.compile(r"(?i)безопасн\w*"),
re.compile(r"(?i)модерац\w*"),
re.compile(r"(?i)жалоб\w*"),
re.compile(r"(?i)абуз\w*"),
]
_PRE_LOWER_TRANSLATION = str.maketrans(
{
"I": "l",
"İ": "l",
"Q": "g",
"": " ",
}
)
_POST_LOWER_TRANSLATION = str.maketrans(
{
"а": "a",
"б": "b",
"в": "v",
"г": "g",
"д": "d",
"е": "e",
"ё": "e",
"ж": "zh",
"з": "z",
"и": "i",
"і": "i",
"й": "i",
"к": "k",
"л": "l",
"м": "m",
"н": "n",
"о": "o",
"п": "p",
"р": "r",
"с": "s",
"т": "t",
"у": "u",
"ф": "f",
"х": "h",
"ц": "c",
"ч": "ch",
"ш": "sh",
"щ": "sh",
"ъ": "",
"ы": "y",
"ь": "",
"э": "e",
"ю": "yu",
"я": "ya",
"_": "_",
}
)
_NORMALIZED_BANNED_TOKENS = {
"tme",
"telegram",
"teleqram",
"teiegram",
"teieqram",
"telegrarn",
"joinchat",
"http",
"https",
"www",
"tg",
"service",
"notification",
"system",
"security",
"safety",
"support",
"moderation",
"review",
"compliance",
"abuse",
"spam",
"report",
}
_USERNAME_PLACEHOLDER = "клиент"
def _normalize_for_detection(value: str) -> str:
if not value:
return ""
normalized = unicodedata.normalize("NFKD", value)
normalized = normalized.translate(_PRE_LOWER_TRANSLATION)
normalized = normalized.lower()
normalized = "".join(
ch for ch in normalized if unicodedata.category(ch) != "Mn"
)
normalized = normalized.translate(_POST_LOWER_TRANSLATION)
normalized = normalized.replace("rn", "m")
pattern = rf"[{re.escape(_OBFUSCATION_CHARS)}\s]+"
normalized = re.sub(pattern, "", normalized)
normalized = re.sub(r"[^a-z0-9]+", "", normalized)
return normalized
def _remove_patterns(value: str) -> str:
updated = value
for pattern in (
_URL_PATTERNS
+ _OBFUSCATED_DOMAIN_PATTERNS
+ _ENGLISH_SERVICE_PATTERNS
+ _RUSSIAN_SERVICE_PATTERNS
):
updated = pattern.sub(" ", updated)
return updated
def _finalize(value: str) -> Optional[str]:
compacted = re.sub(r"\s+", " ", value)
compacted = compacted.strip(" \t\r\n-_.,/\\")
compacted = compacted.strip()
if not compacted:
return None
normalized = _normalize_for_detection(compacted)
if any(token in normalized for token in _NORMALIZED_BANNED_TOKENS):
return None
return compacted
def sanitize_display_name(value: Optional[str]) -> Optional[str]:
if value is None:
return None
clean = value.replace("@", " ")
clean = _remove_patterns(clean)
return _finalize(clean)
def sanitize_username(value: Optional[str]) -> Optional[str]:
if value is None:
return None
clean = value.strip()
clean = clean.lstrip("@")
clean = _remove_patterns(clean)
return _finalize(clean)
def username_for_display(username: Optional[str], with_at: bool = False) -> str:
sanitized = sanitize_username(username)
if not sanitized:
return _USERNAME_PLACEHOLDER
return f"@{sanitized}" if with_at else sanitized
def display_name_or_fallback(
first_name: Optional[str],
fallback: Optional[str] = None,
) -> str:
sanitized = sanitize_display_name(first_name)
if sanitized:
return sanitized
if fallback is not None:
return fallback
return _USERNAME_PLACEHOLDER
+3
View File
@@ -228,6 +228,7 @@
"admin_user_search_new_button": "🔍 Find Another", "admin_user_search_new_button": "🔍 Find Another",
"admin_user_view_all_logs_button": "📋 All Actions", "admin_user_view_all_logs_button": "📋 All Actions",
"admin_user_back_to_card_button": "🔙 Back to Card", "admin_user_back_to_card_button": "🔙 Back to Card",
"admin_user_recent_actions_title": "📜 Recent actions of user {user_id}:",
"admin_user_not_found": "❌ User not found: {input}", "admin_user_not_found": "❌ User not found: {input}",
"admin_user_not_found_action": "User not found", "admin_user_not_found_action": "User not found",
"admin_user_card_error": "❌ Error displaying user card", "admin_user_card_error": "❌ Error displaying user card",
@@ -316,6 +317,8 @@
"admin_user_na_value": "N/A", "admin_user_na_value": "N/A",
"admin_user_subscription_none": "No active subscription", "admin_user_subscription_none": "No active subscription",
"admin_user_actions_count_label": "📜 <b>Total actions:</b>", "admin_user_actions_count_label": "📜 <b>Total actions:</b>",
"admin_user_invited_friends_label": "👥 <b>Friends invited:</b>",
"admin_user_ref_purchased_label": "💳 <b>Purchased subscription:</b>",
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>", "admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
"admin_user_subscription_error": "Loading error", "admin_user_subscription_error": "Loading error",
"admin_promo_management_button": "🎟 Promo Management", "admin_promo_management_button": "🎟 Promo Management",
+3
View File
@@ -227,6 +227,7 @@
"admin_user_search_new_button": "🔍 Новый поиск", "admin_user_search_new_button": "🔍 Новый поиск",
"admin_user_view_all_logs_button": "📋 Все логи", "admin_user_view_all_logs_button": "📋 Все логи",
"admin_user_back_to_card_button": "🔙 К карточке", "admin_user_back_to_card_button": "🔙 К карточке",
"admin_user_recent_actions_title": "📜 Последние действия пользователя {user_id}:",
"admin_user_not_found": "❌ Пользователь не найден: {input}", "admin_user_not_found": "❌ Пользователь не найден: {input}",
"admin_user_not_found_action": "Пользователь не найден", "admin_user_not_found_action": "Пользователь не найден",
"admin_user_card_error": "❌ Ошибка отображения карточки пользователя", "admin_user_card_error": "❌ Ошибка отображения карточки пользователя",
@@ -315,6 +316,8 @@
"admin_user_na_value": "N/A", "admin_user_na_value": "N/A",
"admin_user_subscription_none": "Нет активной подписки", "admin_user_subscription_none": "Нет активной подписки",
"admin_user_actions_count_label": "📜 <b>Всего действий:</b>", "admin_user_actions_count_label": "📜 <b>Всего действий:</b>",
"admin_user_invited_friends_label": "👥 <b>Приглашено друзей:</b>",
"admin_user_ref_purchased_label": "💳 <b>Купили подписку:</b>",
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>", "admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
"admin_user_subscription_error": "Ошибка загрузки", "admin_user_subscription_error": "Ошибка загрузки",
"admin_promo_management_button": "🎟 Управление промокодами", "admin_promo_management_button": "🎟 Управление промокодами",