diff --git a/bot/handlers/admin/statistics.py b/bot/handlers/admin/statistics.py index 5210dbd..3a8a71a 100644 --- a/bot/handlers/admin/statistics.py +++ b/bot/handlers/admin/statistics.py @@ -13,17 +13,27 @@ from bot.services.panel_api_service import PanelApiService from bot.keyboards.inline.admin_keyboards import ( get_back_to_admin_panel_keyboard, - get_user_ratings_keyboard, + get_back_to_user_management_keyboard, ) from bot.middlewares.i18n import JsonI18n router = Router(name="admin_statistics_router") -def _format_rating_user_label(user_row: Dict[str, object]) -> str: +def _format_rating_user_label(user_row: Dict[str, object], + bot_username: Optional[str] = None) -> str: user_id = int(user_row.get("user_id", 0) or 0) username = user_row.get("username") first_name = user_row.get("first_name") + user_id_text = str(user_id) + user_id_html = html.escape(user_id_text) + + if bot_username: + safe_bot_username = html.escape(bot_username) + user_id_html = ( + f'' + f"{user_id_html}" + ) parts: List[str] = [] if username: @@ -32,25 +42,13 @@ def _format_rating_user_label(user_row: Dict[str, object]) -> str: parts.append(html.escape(str(first_name))) if not parts: - parts.append(f"ID {user_id}") + parts.append(f"ID {user_id_html}") else: - parts.append(f"(ID {user_id})") + parts.append(f"(ID {user_id_html})") return " ".join(parts) -def _format_rating_user_short_label(user_row: Dict[str, object]) -> str: - username = user_row.get("username") - first_name = user_row.get("first_name") - user_id = int(user_row.get("user_id", 0) or 0) - - if username: - return f"@{html.escape(str(username))}" - if first_name: - return html.escape(str(first_name)) - return f"ID {user_id}" - - async def show_statistics_handler(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, session: AsyncSession): @@ -308,6 +306,14 @@ async def show_user_ratings_handler( await callback.answer() top_limit = 10 + bot_username: Optional[str] = None + try: + me = await callback.bot.get_me() + bot_username = me.username + except Exception as e_get_me: + logging.warning("Failed to resolve bot username for ratings links: %s", + e_get_me) + traffic_top = await user_dal.get_top_users_by_traffic_used(session, limit=top_limit) invited_top = await user_dal.get_top_users_by_referrals_count(session, limit=top_limit) revenue_top = await user_dal.get_top_users_by_referral_revenue(session, limit=top_limit) @@ -325,7 +331,7 @@ async def show_user_ratings_handler( _( "admin_user_ratings_traffic_item", rank=idx, - user=_format_rating_user_label(row), + user=_format_rating_user_label(row, bot_username), traffic_gb=f"{traffic_gb:.2f}", ) ) @@ -339,7 +345,7 @@ async def show_user_ratings_handler( _( "admin_user_ratings_invited_item", rank=idx, - user=_format_rating_user_label(row), + user=_format_rating_user_label(row, bot_username), invited_count=int(row.get("invited_count") or 0), ) ) @@ -353,29 +359,15 @@ async def show_user_ratings_handler( _( "admin_user_ratings_revenue_item", rank=idx, - user=_format_rating_user_label(row), + user=_format_rating_user_label(row, bot_username), revenue=f"{float(row.get('referral_revenue') or 0):.2f}", ) ) else: text_parts.append(_("admin_user_ratings_empty")) - unique_users: Dict[int, Dict[str, object]] = {} - for row in traffic_top + invited_top + revenue_top: - user_id = int(row.get("user_id", 0) or 0) - if user_id <= 0 or user_id in unique_users: - continue - unique_users[user_id] = { - "user_id": user_id, - "label": _format_rating_user_short_label(row), - } - await callback.message.edit_text( "\n".join(text_parts), - reply_markup=get_user_ratings_keyboard( - list(unique_users.values()), - current_lang, - i18n, - ), + reply_markup=get_back_to_user_management_keyboard(current_lang, i18n), parse_mode="HTML", ) diff --git a/bot/handlers/user/start.py b/bot/handlers/user/start.py index 54ec2a8..03aab17 100644 --- a/bot/handlers/user/start.py +++ b/bot/handlers/user/start.py @@ -305,7 +305,8 @@ async def ensure_required_channel_subscription( @router.message(CommandStart()) @router.message(CommandStart(magic=F.args.regexp(r"^ref_((?:[uU][A-Za-z0-9]{9})|(?:[A-Za-z0-9]{9})|\d+)$").as_("ref_match"))) @router.message(CommandStart(magic=F.args.regexp(r"^promo_(\w+)$").as_("promo_match"))) -@router.message(CommandStart(magic=F.args.regexp(r"^(?!ref_|promo_)([A-Za-z0-9_\-]{2,64})$").as_("ad_param_match"))) +@router.message(CommandStart(magic=F.args.regexp(r"^admin_user_(\d+)$").as_("admin_user_match"))) +@router.message(CommandStart(magic=F.args.regexp(r"^(?!ref_|promo_|admin_user_)([A-Za-z0-9_\-]{2,64})$").as_("ad_param_match"))) async def start_command_handler(message: types.Message, state: FSMContext, settings: Settings, @@ -314,7 +315,8 @@ async def start_command_handler(message: types.Message, session: AsyncSession, ref_match: Optional[re.Match] = None, promo_match: Optional[re.Match] = None, - ad_param_match: Optional[re.Match] = None): + ad_param_match: Optional[re.Match] = None, + admin_user_match: Optional[re.Match] = None): await state.clear() current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") @@ -324,6 +326,58 @@ async def start_command_handler(message: types.Message, user = message.from_user user_id = user.id + if admin_user_match and user_id in settings.ADMIN_IDS: + target_user_id = int(admin_user_match.group(1)) + target_user = await user_dal.get_user_by_id(session, target_user_id) + if not target_user: + await message.answer( + _("admin_user_not_found", input=hd.quote(str(target_user_id))) + ) + return + + try: + from bot.handlers.admin.user_management import ( + format_user_card, + get_user_card_keyboard, + _send_with_profile_link_fallback, + ) + + referral_service = ReferralService( + settings, subscription_service, message.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, + target_user.referred_by_id, + ) + + await _send_with_profile_link_fallback( + message.answer, + text=user_card_text, + markup=keyboard.as_markup(), + user_id=target_user.user_id, + parse_mode="HTML", + ) + return + except Exception as e_admin_card: + logging.error( + "Failed to open admin user card via deep-link for %s: %s", + target_user_id, + e_admin_card, + exc_info=True, + ) + await message.answer(_("admin_user_card_error")) + return + referred_by_user_id: Optional[int] = None promo_code_to_apply: Optional[str] = None ad_start_param: Optional[str] = None diff --git a/bot/keyboards/inline/admin_keyboards.py b/bot/keyboards/inline/admin_keyboards.py index 78f3512..66ad5ea 100644 --- a/bot/keyboards/inline/admin_keyboards.py +++ b/bot/keyboards/inline/admin_keyboards.py @@ -480,34 +480,3 @@ def get_back_to_user_management_keyboard(lang: str, builder.button(text=_(key="back_to_user_management_button"), callback_data="admin_section:user_management") return builder.as_markup() - - -def get_user_ratings_keyboard(rating_users: List[dict], - lang: str, - i18n_instance) -> InlineKeyboardMarkup: - _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) - builder = InlineKeyboardBuilder() - - for entry in rating_users: - user_id = int(entry.get("user_id", 0) or 0) - if user_id <= 0: - continue - - label = str(entry.get("label", "")).strip() - if label: - button_text = _("admin_user_ratings_open_user_button", - user_id=user_id, - user_label=label) - else: - button_text = _("admin_user_ratings_open_user_button_simple", - user_id=user_id) - - builder.button( - text=button_text, - callback_data=f"admin_user_card_from_list:{user_id}:0", - ) - - builder.button(text=_(key="back_to_user_management_button"), - callback_data="admin_section:user_management") - builder.adjust(1) - return builder.as_markup() diff --git a/locales/en.json b/locales/en.json index 029318c..f022b37 100644 --- a/locales/en.json +++ b/locales/en.json @@ -142,8 +142,6 @@ "admin_user_ratings_invited_item": "{rank}. {user} — {invited_count}", "admin_user_ratings_revenue_item": "{rank}. {user} — {revenue} RUB", "admin_user_ratings_empty": "No data yet.", - "admin_user_ratings_open_user_button": "👤 ID {user_id} ({user_label})", - "admin_user_ratings_open_user_button_simple": "👤 ID {user_id}", "admin_payments_header": "💰 All Payments", "admin_no_payments_found": "No payments found.", "admin_export_payments_csv": "📊 Export CSV", diff --git a/locales/ru.json b/locales/ru.json index c69fee0..96bd1c7 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -142,8 +142,6 @@ "admin_user_ratings_invited_item": "{rank}. {user} — {invited_count}", "admin_user_ratings_revenue_item": "{rank}. {user} — {revenue} RUB", "admin_user_ratings_empty": "Пока нет данных.", - "admin_user_ratings_open_user_button": "👤 ID {user_id} ({user_label})", - "admin_user_ratings_open_user_button_simple": "👤 ID {user_id}", "admin_payments_header": "💰 Все платежи", "admin_no_payments_found": "Платежи не найдены.", "admin_export_payments_csv": "📊 Экспорт CSV",