From 5791f2e58cba416228610b9336bdd6b4b8be7ca9 Mon Sep 17 00:00:00 2001 From: Bogdan Strielecki Date: Mon, 3 Nov 2025 16:00:50 +0300 Subject: [PATCH] Add user profile links User profile buttons quickly opens user's telegram profile Add user profile link button to log messages Add user profile link button to user card in admin panel Add referrer profile link button to log messages Add referrer profile link button to user card in admin panel --- bot/handlers/admin/user_management.py | 61 +++++++++++++++++---- bot/keyboards/inline/admin_keyboards.py | 7 +++ bot/services/notification_service.py | 71 +++++++++++++++++++++---- locales/en.json | 6 ++- locales/ru.json | 6 ++- 5 files changed, 129 insertions(+), 22 deletions(-) diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py index e0ea2d1..956dffb 100644 --- a/bot/handlers/admin/user_management.py +++ b/bot/handlers/admin/user_management.py @@ -104,7 +104,8 @@ async def user_search_prompt_handler(callback: types.CallbackQuery, await state.set_state(AdminStates.waiting_for_user_search) -def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyboardBuilder: +def get_user_card_keyboard(user_id: int, i18n_instance, lang: str, + referrer_id: Optional[int] = None) -> InlineKeyboardBuilder: """Generate keyboard for user management actions""" _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) builder = InlineKeyboardBuilder() @@ -139,13 +140,26 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb callback_data=f"user_action:refresh:{user_id}" ) - # Row 4: Destructive action + # Row 4: Quick links + builder.button( + text=_(key="user_card_open_profile_button", + default="👤 Открыть профиль"), + url=f"tg://user?id={user_id}" + ) + if referrer_id: + builder.button( + text=_(key="user_card_open_referrer_profile_button", + default="👤 Открыть профиль пригласившего"), + url=f"tg://user?id={referrer_id}" + ) + + # Row 5: Destructive action builder.button( text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"), callback_data=f"user_action:delete_user:{user_id}" ) - # Row 5: Navigation + # Row 6: Navigation builder.button( text=_(key="admin_user_search_new_button", default="🔍 Найти другого"), callback_data="admin_action:users_management" @@ -155,7 +169,8 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb callback_data="admin_action:main" ) - builder.adjust(2, 2, 2, 1, 2) + quick_links_width = 2 if referrer_id else 1 + builder.adjust(2, 2, 2, quick_links_width, 1, 2) return builder @@ -315,7 +330,12 @@ async def process_user_search_handler(message: types.Message, state: FSMContext, try: 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, + user_model.referred_by_id + ) await message.answer( user_card_text, @@ -580,7 +600,12 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User, _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, + fresh_user.referred_by_id + ) try: await callback.message.edit_text( @@ -864,7 +889,12 @@ async def process_subscription_days_handler(message: types.Message, state: FSMCo if user: 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, + user.referred_by_id + ) await message.answer( user_card_text, @@ -973,7 +1003,12 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte subscription_service = SubscriptionService(settings, panel_service) 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, + target_user.referred_by_id + ) await message.answer( user_card_text, @@ -1272,12 +1307,18 @@ async def user_card_from_list_handler(callback: types.CallbackQuery, return # Create keyboard with back to list button - keyboard = get_user_card_keyboard(user_id, i18n, current_lang) + keyboard = get_user_card_keyboard( + user_id, + i18n, + current_lang, + user.referred_by_id + ) keyboard.button( text=_("admin_user_back_to_list_button", default="⬅️ К списку"), callback_data=f"admin_action:users_list:{page}" ) - keyboard.adjust(2, 2, 2, 2, 1) + quick_links_width = 2 if user.referred_by_id else 1 + keyboard.adjust(2, 2, 2, quick_links_width, 1, 2, 1) # Format user card try: diff --git a/bot/keyboards/inline/admin_keyboards.py b/bot/keyboards/inline/admin_keyboards.py index 91d4379..c27a3bf 100644 --- a/bot/keyboards/inline/admin_keyboards.py +++ b/bot/keyboards/inline/admin_keyboards.py @@ -384,6 +384,13 @@ def get_user_card_keyboard(user_id: int, builder.button( text=_(key="user_card_ban_button"), callback_data=f"admin_ban_confirm:{user_id}:{banned_list_page}") + builder.button( + text=_( + key="user_card_open_profile_button", + default="👤 Open profile" + ), + url=f"tg://user?id={user_id}" + ) builder.button( text=_(key="user_card_back_to_banned_list_button"), callback_data=f"admin_action:view_banned:{banned_list_page}") diff --git a/bot/services/notification_service.py b/bot/services/notification_service.py index 24f1a39..8a1e6c3 100644 --- a/bot/services/notification_service.py +++ b/bot/services/notification_service.py @@ -1,10 +1,11 @@ import logging import asyncio from aiogram import Bot +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.text_decorations import html_decoration as hd from aiogram.exceptions import TelegramRetryAfter from datetime import datetime, timezone -from typing import Optional, Union, Dict, Any +from typing import Optional, Union, Dict, Any, Callable from config.settings import Settings from sqlalchemy.orm import sessionmaker @@ -34,8 +35,45 @@ class NotificationService: if username: base_display = f"{base_display} ({username_for_display(username)})" return base_display + + @staticmethod + def _build_profile_keyboard( + translate: Callable[..., str], + user_id: int, + referrer_id: Optional[int] = None, + ) -> InlineKeyboardMarkup: + """Create inline keyboard with links to user (and referrer) profiles.""" + buttons = [ + [ + InlineKeyboardButton( + text=translate( + "log_open_profile_link", + default="👤 Открыть профиль", + ), + url=f"tg://user?id={user_id}", + ) + ] + ] + + if referrer_id: + buttons.append([ + InlineKeyboardButton( + text=translate( + "log_open_referrer_profile_button", + default="👤 Открыть профиль пригласившего", + ), + url=f"tg://user?id={referrer_id}", + ) + ]) + + return InlineKeyboardMarkup(inline_keyboard=buttons) - 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, + reply_markup: Optional[InlineKeyboardMarkup] = None, + ): """Send message to configured log channel/group using message queue""" if not self.settings.LOG_CHAT_ID: return @@ -49,6 +87,7 @@ class NotificationService: text=message, parse_mode="HTML", disable_web_page_preview=True, + reply_markup=reply_markup, message_thread_id=thread_id or self.settings.LOG_THREAD_ID ) except Exception as e: @@ -64,6 +103,8 @@ class NotificationService: "parse_mode": "HTML", "disable_web_page_preview": True } + if reply_markup: + kwargs["reply_markup"] = reply_markup # Add thread ID for supergroups if specified if final_thread_id: @@ -124,7 +165,12 @@ class NotificationService: referral_text = "" if referred_by_id: - referral_text = _("log_referral_suffix", default=" (реферал от {referrer_id})", referrer_id=referred_by_id) + referrer_link = hd.link(str(referred_by_id), f"tg://user?id={referred_by_id}") + referral_text = _( + "log_referral_suffix", + default=" (реферал от {referrer_link})", + referrer_link=referrer_link, + ) message = _( "log_new_user_registration", @@ -137,9 +183,10 @@ class NotificationService: referral_text=referral_text, timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S") ) - + # Send to log channel - await self._send_to_log_channel(message) + profile_keyboard = self._build_profile_keyboard(_, user_id, referred_by_id) + await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_payment_received(self, user_id: int, amount: float, currency: str, months: int, payment_provider: str, @@ -182,7 +229,8 @@ class NotificationService: ) # Send to log channel - await self._send_to_log_channel(message) + profile_keyboard = self._build_profile_keyboard(_, user_id) + await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_promo_activation(self, user_id: int, promo_code: str, bonus_days: int, username: Optional[str] = None): @@ -212,7 +260,8 @@ class NotificationService: ) # Send to log channel - await self._send_to_log_channel(message) + profile_keyboard = self._build_profile_keyboard(_, user_id) + await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_trial_activation(self, user_id: int, end_date: datetime, username: Optional[str] = None): @@ -240,7 +289,8 @@ class NotificationService: ) # Send to log channel - await self._send_to_log_channel(message) + profile_keyboard = self._build_profile_keyboard(_, user_id) + await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def notify_panel_sync(self, status: str, details: str, users_processed: int, subs_synced: int, @@ -275,7 +325,7 @@ class NotificationService: details=details ) - # Send to log channel + # Send to log channel await self._send_to_log_channel(message) async def notify_suspicious_promo_attempt( @@ -308,7 +358,8 @@ class NotificationService: timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z")) # Send to log channel - await self._send_to_log_channel(message) + profile_keyboard = self._build_profile_keyboard(_, user_id) + await self._send_to_log_channel(message, reply_markup=profile_keyboard) async def send_custom_notification(self, message: str, to_admins: bool = False, to_log_channel: bool = True, thread_id: Optional[int] = None): diff --git a/locales/en.json b/locales/en.json index b3ffec6..e2d6354 100644 --- a/locales/en.json +++ b/locales/en.json @@ -210,6 +210,8 @@ "admin_user_card_title": "User Card", "user_card_ban_button": "🚫 Ban", "user_card_unban_button": "✅ Unban", + "user_card_open_profile_button": "👤 Open profile", + "user_card_open_referrer_profile_button": "👤 Referrer profile", "user_card_back_to_banned_list_button": "⬅️ Back to Ban List", "admin_logs_menu_title": "Logs Menu:", "admin_view_all_logs_button": "📜 All Message Logs", @@ -291,7 +293,9 @@ "inline_admin_financial_stats_title": "💰 Financial Statistics", "inline_system_stats_message": "🖥 Panel Statistics\n\n🟢 Online: {online}\n📊 Active: {active}\n🔴 Disabled: {disabled}\n⏰ Expired: {expired}\n⚠️ Limited: {limited}\n👥 Total users: {total}\n💾 RAM Usage: {memory:.1f}%\n📊 Week traffic: {week_traffic}\n📊 Month traffic: {month_traffic}\n🔗 Active nodes: {active_nodes}/{total_nodes}", "inline_admin_system_stats_title": "🖥 System Statistics", - "log_referral_suffix": " (referral from {referrer_id})", + "log_referral_suffix": " (referral from {referrer_link})", + "log_open_profile_link": "👤 Open profile", + "log_open_referrer_profile_button": "👤 Referrer profile", "log_new_user_registration": "👤 New User\n\n🆔 ID: {user_id}\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}", "log_payment_received": "{provider_emoji} Payment Received\n\n👤 User: {user_display}\n💰 Amount: {amount} {currency}\n📅 Period: {months} mo.\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}", "log_promo_activation": "🎁 Promo Code Activated\n\n👤 User: {user_display}\n🏷 Code: {promo_code}\n🎯 Bonus: +{bonus_days}d\n🕐 Time: {timestamp}", diff --git a/locales/ru.json b/locales/ru.json index a9e97d9..ba55685 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -220,6 +220,8 @@ "admin_user_card_title": "Карточка пользователя", "user_card_ban_button": "🚫 Заблокировать", "user_card_unban_button": "✅ Разблокировать", + "user_card_open_profile_button": "👤 Открыть профиль", + "user_card_open_referrer_profile_button": "👤 Профиль пригласившего", "user_card_back_to_banned_list_button": "⬅️ К списку забаненных", "admin_logs_menu_title": "Меню логов:", "admin_view_all_logs_button": "📜 Все логи сообщений", @@ -291,7 +293,9 @@ "inline_admin_financial_stats_title": "💰 Финансовая статистика", "inline_system_stats_message": "🖥 Статистика панели\n\n🟢 Онлайн: {online}\n📊 Активных: {active}\n🔴 Отключенных: {disabled}\n⏰ Истекшие: {expired}\n⚠️ Ограниченные: {limited}\n👥 Всего пользователей: {total}\n💾 Использование RAM: {memory:.1f}%\n📊 Трафик за неделю: {week_traffic}\n📊 Трафик за месяц: {month_traffic}\n🔗 Активных нод: {active_nodes}/{total_nodes}", "inline_admin_system_stats_title": "🖥 Системная статистика", - "log_referral_suffix": " (реферал от {referrer_id})", + "log_referral_suffix": " (реферал от {referrer_link})", + "log_open_profile_link": "👤 Открыть профиль", + "log_open_referrer_profile_button": "👤 Профиль пригласившего", "log_new_user_registration": "👤 Новый пользователь\n\n🆔 ID: {user_id}\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}", "log_payment_received": "{provider_emoji} Получен платеж\n\n👤 Пользователь: {user_display}\n💰 Сумма: {amount} {currency}\n📅 Период: {months} мес.\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}", "log_promo_activation": "🎁 Активирован промокод\n\n👤 Пользователь: {user_display}\n🏷 Код: {promo_code}\n🎯 Бонус: +{bonus_days} дн.\n🕐 Время: {timestamp}",