Merge pull request #120 from streletskiy/feature/user-profile-links
Feature/user profile links
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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):
|
||||
|
||||
+5
-1
@@ -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": "🖥 <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",
|
||||
"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_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}",
|
||||
|
||||
+5
-1
@@ -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": "🖥 <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": "🖥 Системная статистика",
|
||||
"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_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}",
|
||||
|
||||
Reference in New Issue
Block a user