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
This commit is contained in:
Bogdan Strielecki
2025-11-03 16:00:50 +03:00
parent c0d70030c3
commit 5791f2e58c
5 changed files with 129 additions and 22 deletions
+51 -10
View File
@@ -104,7 +104,8 @@ async def user_search_prompt_handler(callback: types.CallbackQuery,
await state.set_state(AdminStates.waiting_for_user_search) 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""" """Generate keyboard for user management actions"""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder() 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}" 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( builder.button(
text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"), text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"),
callback_data=f"user_action:delete_user:{user_id}" callback_data=f"user_action:delete_user:{user_id}"
) )
# Row 5: Navigation # Row 6: Navigation
builder.button( builder.button(
text=_(key="admin_user_search_new_button", default="🔍 Найти другого"), text=_(key="admin_user_search_new_button", default="🔍 Найти другого"),
callback_data="admin_action:users_management" 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" 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 return builder
@@ -315,7 +330,12 @@ async def process_user_search_handler(message: types.Message, state: FSMContext,
try: try:
referral_service = ReferralService(settings, subscription_service, message.bot, i18n) 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) 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( await message.answer(
user_card_text, user_card_text,
@@ -580,7 +600,12 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User,
_settings = _Settings() _settings = _Settings()
referral_service = ReferralService(_settings, subscription_service, callback.message.bot, i18n_instance) 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) 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: try:
await callback.message.edit_text( await callback.message.edit_text(
@@ -864,7 +889,12 @@ async def process_subscription_days_handler(message: types.Message, state: FSMCo
if user: if user:
referral_service = ReferralService(settings, subscription_service, message.bot, i18n) 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) 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( await message.answer(
user_card_text, user_card_text,
@@ -973,7 +1003,12 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
subscription_service = SubscriptionService(settings, panel_service) subscription_service = SubscriptionService(settings, panel_service)
referral_service = ReferralService(settings, subscription_service, bot, i18n) 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) 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( await message.answer(
user_card_text, user_card_text,
@@ -1272,12 +1307,18 @@ async def user_card_from_list_handler(callback: types.CallbackQuery,
return return
# Create keyboard with back to list button # 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( keyboard.button(
text=_("admin_user_back_to_list_button", default="⬅️ К списку"), text=_("admin_user_back_to_list_button", default="⬅️ К списку"),
callback_data=f"admin_action:users_list:{page}" 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 # Format user card
try: try:
+7
View File
@@ -384,6 +384,13 @@ def get_user_card_keyboard(user_id: int,
builder.button( builder.button(
text=_(key="user_card_ban_button"), text=_(key="user_card_ban_button"),
callback_data=f"admin_ban_confirm:{user_id}:{banned_list_page}") 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( builder.button(
text=_(key="user_card_back_to_banned_list_button"), text=_(key="user_card_back_to_banned_list_button"),
callback_data=f"admin_action:view_banned:{banned_list_page}") callback_data=f"admin_action:view_banned:{banned_list_page}")
+61 -10
View File
@@ -1,10 +1,11 @@
import logging import logging
import asyncio import asyncio
from aiogram import Bot from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.text_decorations import html_decoration as hd from aiogram.utils.text_decorations import html_decoration as hd
from aiogram.exceptions import TelegramRetryAfter from aiogram.exceptions import TelegramRetryAfter
from datetime import datetime, timezone 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 config.settings import Settings
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -34,8 +35,45 @@ class NotificationService:
if username: if username:
base_display = f"{base_display} ({username_for_display(username)})" base_display = f"{base_display} ({username_for_display(username)})"
return base_display 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""" """Send message to configured log channel/group using message queue"""
if not self.settings.LOG_CHAT_ID: if not self.settings.LOG_CHAT_ID:
return return
@@ -49,6 +87,7 @@ class NotificationService:
text=message, text=message,
parse_mode="HTML", parse_mode="HTML",
disable_web_page_preview=True, disable_web_page_preview=True,
reply_markup=reply_markup,
message_thread_id=thread_id or self.settings.LOG_THREAD_ID message_thread_id=thread_id or self.settings.LOG_THREAD_ID
) )
except Exception as e: except Exception as e:
@@ -64,6 +103,8 @@ class NotificationService:
"parse_mode": "HTML", "parse_mode": "HTML",
"disable_web_page_preview": True "disable_web_page_preview": True
} }
if reply_markup:
kwargs["reply_markup"] = reply_markup
# Add thread ID for supergroups if specified # Add thread ID for supergroups if specified
if final_thread_id: if final_thread_id:
@@ -124,7 +165,12 @@ class NotificationService:
referral_text = "" referral_text = ""
if referred_by_id: 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 = _( message = _(
"log_new_user_registration", "log_new_user_registration",
@@ -137,9 +183,10 @@ class NotificationService:
referral_text=referral_text, referral_text=referral_text,
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S") timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
) )
# Send to log channel # 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, async def notify_payment_received(self, user_id: int, amount: float, currency: str,
months: int, payment_provider: str, months: int, payment_provider: str,
@@ -182,7 +229,8 @@ class NotificationService:
) )
# Send to log channel # 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, async def notify_promo_activation(self, user_id: int, promo_code: str, bonus_days: int,
username: Optional[str] = None): username: Optional[str] = None):
@@ -212,7 +260,8 @@ class NotificationService:
) )
# Send to log channel # 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, async def notify_trial_activation(self, user_id: int, end_date: datetime,
username: Optional[str] = None): username: Optional[str] = None):
@@ -240,7 +289,8 @@ class NotificationService:
) )
# Send to log channel # 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, async def notify_panel_sync(self, status: str, details: str,
users_processed: int, subs_synced: int, users_processed: int, subs_synced: int,
@@ -275,7 +325,7 @@ class NotificationService:
details=details details=details
) )
# Send to log channel # Send to log channel
await self._send_to_log_channel(message) await self._send_to_log_channel(message)
async def notify_suspicious_promo_attempt( 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")) timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"))
# Send to log channel # 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, async def send_custom_notification(self, message: str, to_admins: bool = False,
to_log_channel: bool = True, thread_id: Optional[int] = None): to_log_channel: bool = True, thread_id: Optional[int] = None):
+5 -1
View File
@@ -210,6 +210,8 @@
"admin_user_card_title": "User Card", "admin_user_card_title": "User Card",
"user_card_ban_button": "🚫 Ban", "user_card_ban_button": "🚫 Ban",
"user_card_unban_button": "✅ Unban", "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", "user_card_back_to_banned_list_button": "⬅️ Back to Ban List",
"admin_logs_menu_title": "Logs Menu:", "admin_logs_menu_title": "Logs Menu:",
"admin_view_all_logs_button": "📜 All Message Logs", "admin_view_all_logs_button": "📜 All Message Logs",
@@ -291,7 +293,9 @@
"inline_admin_financial_stats_title": "💰 Financial Statistics", "inline_admin_financial_stats_title": "💰 Financial Statistics",
"inline_system_stats_message": "🖥 <b>Panel Statistics</b>\n\n🟢 Online: <b>{online}</b>\n📊 Active: <b>{active}</b>\n🔴 Disabled: <b>{disabled}</b>\n⏰ Expired: <b>{expired}</b>\n⚠️ Limited: <b>{limited}</b>\n👥 Total users: <b>{total}</b>\n💾 RAM Usage: <b>{memory:.1f}%</b>\n📊 Week traffic: <b>{week_traffic}</b>\n📊 Month traffic: <b>{month_traffic}</b>\n🔗 Active nodes: <b>{active_nodes}/{total_nodes}</b>", "inline_system_stats_message": "🖥 <b>Panel Statistics</b>\n\n🟢 Online: <b>{online}</b>\n📊 Active: <b>{active}</b>\n🔴 Disabled: <b>{disabled}</b>\n⏰ Expired: <b>{expired}</b>\n⚠️ Limited: <b>{limited}</b>\n👥 Total users: <b>{total}</b>\n💾 RAM Usage: <b>{memory:.1f}%</b>\n📊 Week traffic: <b>{week_traffic}</b>\n📊 Month traffic: <b>{month_traffic}</b>\n🔗 Active nodes: <b>{active_nodes}/{total_nodes}</b>",
"inline_admin_system_stats_title": "🖥 System Statistics", "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": "👤 <b>New User</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}", "log_new_user_registration": "👤 <b>New User</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}",
"log_payment_received": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n📅 Period: <b>{months} mo.</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}", "log_payment_received": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n📅 Period: <b>{months} mo.</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}",
"log_promo_activation": "🎁 <b>Promo Code Activated</b>\n\n👤 User: {user_display}\n🏷 Code: <code>{promo_code}</code>\n🎯 Bonus: <b>+{bonus_days}d</b>\n🕐 Time: {timestamp}", "log_promo_activation": "🎁 <b>Promo Code Activated</b>\n\n👤 User: {user_display}\n🏷 Code: <code>{promo_code}</code>\n🎯 Bonus: <b>+{bonus_days}d</b>\n🕐 Time: {timestamp}",
+5 -1
View File
@@ -220,6 +220,8 @@
"admin_user_card_title": "Карточка пользователя", "admin_user_card_title": "Карточка пользователя",
"user_card_ban_button": "🚫 Заблокировать", "user_card_ban_button": "🚫 Заблокировать",
"user_card_unban_button": "✅ Разблокировать", "user_card_unban_button": "✅ Разблокировать",
"user_card_open_profile_button": "👤 Открыть профиль",
"user_card_open_referrer_profile_button": "👤 Профиль пригласившего",
"user_card_back_to_banned_list_button": "⬅️ К списку забаненных", "user_card_back_to_banned_list_button": "⬅️ К списку забаненных",
"admin_logs_menu_title": "Меню логов:", "admin_logs_menu_title": "Меню логов:",
"admin_view_all_logs_button": "📜 Все логи сообщений", "admin_view_all_logs_button": "📜 Все логи сообщений",
@@ -291,7 +293,9 @@
"inline_admin_financial_stats_title": "💰 Финансовая статистика", "inline_admin_financial_stats_title": "💰 Финансовая статистика",
"inline_system_stats_message": "🖥 <b>Статистика панели</b>\n\n🟢 Онлайн: <b>{online}</b>\n📊 Активных: <b>{active}</b>\n🔴 Отключенных: <b>{disabled}</b>\n⏰ Истекшие: <b>{expired}</b>\n⚠️ Ограниченные: <b>{limited}</b>\n👥 Всего пользователей: <b>{total}</b>\n💾 Использование RAM: <b>{memory:.1f}%</b>\n📊 Трафик за неделю: <b>{week_traffic}</b>\n📊 Трафик за месяц: <b>{month_traffic}</b>\n🔗 Активных нод: <b>{active_nodes}/{total_nodes}</b>", "inline_system_stats_message": "🖥 <b>Статистика панели</b>\n\n🟢 Онлайн: <b>{online}</b>\n📊 Активных: <b>{active}</b>\n🔴 Отключенных: <b>{disabled}</b>\n⏰ Истекшие: <b>{expired}</b>\n⚠️ Ограниченные: <b>{limited}</b>\n👥 Всего пользователей: <b>{total}</b>\n💾 Использование RAM: <b>{memory:.1f}%</b>\n📊 Трафик за неделю: <b>{week_traffic}</b>\n📊 Трафик за месяц: <b>{month_traffic}</b>\n🔗 Активных нод: <b>{active_nodes}/{total_nodes}</b>",
"inline_admin_system_stats_title": "🖥 Системная статистика", "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": "👤 <b>Новый пользователь</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}", "log_new_user_registration": "👤 <b>Новый пользователь</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}",
"log_payment_received": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n📅 Период: <b>{months} мес.</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}", "log_payment_received": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n📅 Период: <b>{months} мес.</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}",
"log_promo_activation": "🎁 <b>Активирован промокод</b>\n\n👤 Пользователь: {user_display}\n🏷 Код: <code>{promo_code}</code>\n🎯 Бонус: <b>+{bonus_days} дн.</b>\n🕐 Время: {timestamp}", "log_promo_activation": "🎁 <b>Активирован промокод</b>\n\n👤 Пользователь: {user_display}\n🏷 Код: <code>{promo_code}</code>\n🎯 Бонус: <b>+{bonus_days} дн.</b>\n🕐 Время: {timestamp}",