1986 lines
69 KiB
Python
1986 lines
69 KiB
Python
import logging
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
|
|
from aiogram import Bot, F, Router, types
|
|
from aiogram.exceptions import TelegramBadRequest
|
|
from aiogram.fsm.context import FSMContext
|
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
|
from aiogram.utils.markdown import hcode
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard
|
|
from bot.middlewares.i18n import JsonI18n
|
|
from bot.services.panel_api_service import PanelApiService
|
|
from bot.services.referral_service import ReferralService
|
|
from bot.services.subscription_service import SubscriptionService
|
|
from bot.states.admin_states import AdminStates
|
|
from bot.utils import get_message_content, send_direct_message
|
|
from bot.utils.telegram_markup import (
|
|
is_profile_link_error,
|
|
remove_profile_link_buttons,
|
|
)
|
|
from bot.utils.text_sanitizer import (
|
|
sanitize_display_name,
|
|
sanitize_username,
|
|
username_for_display,
|
|
)
|
|
from config.settings import Settings
|
|
from db.dal import message_log_dal, subscription_dal, user_dal
|
|
from db.models import User
|
|
|
|
router = Router(name="admin_user_management_router")
|
|
USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
|
|
EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
|
|
|
|
async def _resolve_bot_username(bot: Optional[Bot]) -> Optional[str]:
|
|
"""Best-effort resolution of the running bot's @username (cached by aiogram)."""
|
|
if bot is None:
|
|
return None
|
|
try:
|
|
me = await bot.me()
|
|
return getattr(me, "username", None)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _format_traffic_period(strategy: Optional[str], get_text: Callable[..., str]) -> Optional[str]:
|
|
if not strategy:
|
|
return None
|
|
strategy_upper = str(strategy).upper()
|
|
key_map = {
|
|
"MONTH": "traffic_period_month",
|
|
"WEEK": "traffic_period_week",
|
|
"DAY": "traffic_period_day",
|
|
"NO_RESET": "traffic_period_no_reset",
|
|
}
|
|
label_key = key_map.get(strategy_upper)
|
|
return get_text(label_key) if label_key else strategy_upper
|
|
|
|
|
|
def _format_used_with_period(
|
|
get_text: Callable[..., str], used_display: str, period_label: Optional[str]
|
|
) -> str:
|
|
if not period_label:
|
|
return used_display
|
|
return get_text(
|
|
"traffic_used_with_period", traffic_used=used_display, traffic_period=period_label
|
|
)
|
|
|
|
|
|
async def _find_user_by_admin_input(
|
|
session: AsyncSession,
|
|
input_text: str,
|
|
) -> Optional[User]:
|
|
if input_text.isdigit() or (input_text.startswith("-") and input_text[1:].isdigit()):
|
|
try:
|
|
return await user_dal.get_user_by_id(session, int(input_text))
|
|
except ValueError:
|
|
return None
|
|
if EMAIL_REGEX.match(input_text):
|
|
return await user_dal.get_user_by_email(session, input_text)
|
|
if input_text.startswith("@") and USERNAME_REGEX.match(input_text[1:]):
|
|
return await user_dal.get_user_by_username(session, input_text[1:])
|
|
if USERNAME_REGEX.match(input_text):
|
|
return await user_dal.get_user_by_username(session, input_text)
|
|
return None
|
|
|
|
|
|
async def users_list_handler(
|
|
callback: types.CallbackQuery,
|
|
i18n_data: dict,
|
|
settings: Settings,
|
|
session: AsyncSession,
|
|
page: int = 0,
|
|
):
|
|
"""Display paginated list of all users"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n or not callback.message:
|
|
await callback.answer("Error preparing user list.", show_alert=True)
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
try:
|
|
# Get paginated users
|
|
from bot.keyboards.inline.admin_keyboards import get_users_list_keyboard
|
|
from db.dal import user_dal
|
|
|
|
users = await user_dal.get_all_users_paginated(session, page=page, page_size=15)
|
|
total_users = await user_dal.count_all_users(session)
|
|
total_pages = max(1, (total_users + 14) // 15)
|
|
|
|
# Format message
|
|
header_text = _(
|
|
"admin_users_list_header", current=page + 1, total=total_pages, total_users=total_users
|
|
)
|
|
|
|
keyboard = get_users_list_keyboard(
|
|
users, page, total_users, i18n, current_lang, page_size=15
|
|
)
|
|
|
|
await callback.message.edit_text(header_text, reply_markup=keyboard, parse_mode="HTML")
|
|
await callback.answer()
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error displaying user list: {e}")
|
|
await callback.answer("Ошибка отображения списка пользователей", show_alert=True)
|
|
|
|
|
|
async def user_search_prompt_handler(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
i18n_data: dict,
|
|
settings: Settings,
|
|
session: AsyncSession,
|
|
):
|
|
"""Display search prompt for user management"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n or not callback.message:
|
|
await callback.answer("Error preparing search.", show_alert=True)
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
prompt_text = _("admin_user_management_prompt")
|
|
|
|
try:
|
|
await callback.message.edit_text(
|
|
prompt_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
|
)
|
|
except Exception as e:
|
|
logging.warning(f"Could not edit message for user management: {e}. Sending new.")
|
|
await callback.message.answer(
|
|
prompt_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
|
)
|
|
|
|
await callback.answer()
|
|
await state.set_state(AdminStates.waiting_for_user_search)
|
|
|
|
|
|
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()
|
|
|
|
# Row 1: Trial and Subscription actions
|
|
builder.button(
|
|
text=_(key="admin_user_reset_trial_button"),
|
|
callback_data=f"user_action:reset_trial:{user_id}",
|
|
)
|
|
builder.button(
|
|
text=_(key="admin_user_add_subscription_button"),
|
|
callback_data=f"user_action:add_subscription:{user_id}",
|
|
)
|
|
|
|
# Row 2: Block/Unblock and Message
|
|
builder.button(
|
|
text=_(key="admin_user_toggle_ban_button"),
|
|
callback_data=f"user_action:toggle_ban:{user_id}",
|
|
)
|
|
builder.button(
|
|
text=_(key="admin_user_send_message_button"),
|
|
callback_data=f"user_action:send_message:{user_id}",
|
|
)
|
|
|
|
# Row 3: View actions
|
|
builder.button(
|
|
text=_(key="admin_user_view_logs_button"), callback_data=f"user_action:view_logs:{user_id}"
|
|
)
|
|
builder.button(
|
|
text=_(key="admin_user_refresh_button"), callback_data=f"user_action:refresh:{user_id}"
|
|
)
|
|
|
|
# Row 3b: Premium override + traffic grant
|
|
builder.button(
|
|
text=_(key="admin_user_premium_override_button"),
|
|
callback_data=f"user_action:premium_override:{user_id}",
|
|
)
|
|
builder.button(
|
|
text=_(key="admin_user_traffic_grant_button"),
|
|
callback_data=f"user_action:traffic_grant:{user_id}",
|
|
)
|
|
|
|
# Row 4: Quick links — only for users with a real Telegram profile
|
|
# (synthetic email-only users have a negative user_id with no tg profile).
|
|
has_self_link = user_id > 0
|
|
has_referrer_link = bool(referrer_id) and referrer_id > 0
|
|
if has_self_link:
|
|
builder.button(text=_(key="user_card_open_profile_button"), url=f"tg://user?id={user_id}")
|
|
if has_referrer_link:
|
|
builder.button(
|
|
text=_(key="user_card_open_referrer_profile_button"), url=f"tg://user?id={referrer_id}"
|
|
)
|
|
|
|
# Row 5: Destructive action
|
|
builder.button(
|
|
text=_(key="admin_user_delete_button"), callback_data=f"user_action:delete_user:{user_id}"
|
|
)
|
|
|
|
# Row 6: Navigation
|
|
builder.button(
|
|
text=_(key="admin_user_search_new_button"), callback_data="admin_action:users_management"
|
|
)
|
|
builder.button(text=_(key="back_to_admin_panel_button"), callback_data="admin_action:main")
|
|
|
|
quick_links_count = (1 if has_self_link else 0) + (1 if has_referrer_link else 0)
|
|
if quick_links_count == 0:
|
|
builder.adjust(2, 2, 2, 2, 1, 2)
|
|
else:
|
|
builder.adjust(2, 2, 2, 2, quick_links_count, 1, 2)
|
|
return builder
|
|
|
|
|
|
async def _send_with_profile_link_fallback(
|
|
sender: Callable[..., Awaitable[Any]],
|
|
*,
|
|
text: str,
|
|
markup: Optional[types.InlineKeyboardMarkup],
|
|
user_id: int,
|
|
parse_mode: Optional[str] = "HTML",
|
|
) -> None:
|
|
"""Send text with markup and fallback if Telegram rejects tg://user buttons."""
|
|
send_kwargs: Dict[str, Any] = {"text": text, "reply_markup": markup}
|
|
if parse_mode is not None:
|
|
send_kwargs["parse_mode"] = parse_mode
|
|
|
|
try:
|
|
await sender(**send_kwargs)
|
|
except TelegramBadRequest as exc:
|
|
if not is_profile_link_error(exc):
|
|
raise
|
|
|
|
logging.warning(
|
|
"Telegram rejected profile buttons for user %s: %s. Retrying without tg:// links.",
|
|
user_id,
|
|
getattr(exc, "message", "") or str(exc),
|
|
)
|
|
fallback_markup = remove_profile_link_buttons(markup)
|
|
send_kwargs["reply_markup"] = fallback_markup
|
|
await sender(**send_kwargs)
|
|
|
|
|
|
async def format_user_card(
|
|
user: User,
|
|
session: AsyncSession,
|
|
subscription_service: SubscriptionService,
|
|
i18n_instance,
|
|
lang: str,
|
|
referral_service: Optional[ReferralService] = None,
|
|
*,
|
|
settings: Optional[Settings] = None,
|
|
bot_username: Optional[str] = None,
|
|
) -> str:
|
|
"""Format user information as a detailed card"""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
# Basic user info
|
|
card_parts = []
|
|
card_parts.append(f"👤 <b>{_('admin_user_card_title')}</b>\n")
|
|
|
|
# User details
|
|
na_value = _("admin_user_na_value")
|
|
safe_first_name = sanitize_display_name(user.first_name) if user.first_name else None
|
|
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
|
|
)
|
|
|
|
card_parts.append(f"{_('admin_user_id_label')} {hcode(str(user.user_id))}")
|
|
card_parts.append(f"{_('admin_user_name_label')} {hcode(user_name)}")
|
|
card_parts.append(f"{_('admin_user_username_label')} {hcode(username_display)}")
|
|
if user.email:
|
|
card_parts.append(f"{_('admin_user_email_label')} {hcode(user.email)}")
|
|
if user.telegram_id and int(user.telegram_id) != int(user.user_id):
|
|
card_parts.append(f"{_('admin_user_telegram_id_label')} {hcode(str(user.telegram_id))}")
|
|
card_parts.append(f"{_('admin_user_language_label')} {hcode(user.language_code or na_value)}")
|
|
card_parts.append(f"{_('admin_user_registration_label')} {hcode(registration_date)}")
|
|
|
|
# Ban status
|
|
ban_status = _("admin_user_status_banned") if user.is_banned else _("admin_user_status_active")
|
|
card_parts.append(f"{_('admin_user_status_label')} {ban_status}")
|
|
|
|
# Referral info
|
|
if user.referred_by_id:
|
|
card_parts.append(f"{_('admin_user_referral_label')} {hcode(str(user.referred_by_id))}")
|
|
|
|
# Panel info
|
|
if user.panel_user_uuid:
|
|
card_parts.append(
|
|
f"{_('admin_user_panel_uuid_label')} {hcode(user.panel_user_uuid[:8] + '...' if len(user.panel_user_uuid) > 8 else user.panel_user_uuid)}"
|
|
)
|
|
|
|
card_parts.append("") # Empty line
|
|
|
|
# Subscription info
|
|
try:
|
|
subscription_details = await subscription_service.get_active_subscription_details(
|
|
session, user.user_id
|
|
)
|
|
if subscription_details:
|
|
card_parts.append(f"💳 <b>{_('admin_user_subscription_info')}</b>")
|
|
|
|
end_date = subscription_details.get("end_date")
|
|
if end_date:
|
|
end_date_str = (
|
|
end_date.strftime("%Y-%m-%d %H:%M")
|
|
if isinstance(end_date, datetime)
|
|
else str(end_date)
|
|
)
|
|
card_parts.append(
|
|
f"{_('admin_user_subscription_active_until')} {hcode(end_date_str)}"
|
|
)
|
|
|
|
status = subscription_details.get("status_from_panel", "UNKNOWN")
|
|
card_parts.append(f"{_('admin_user_panel_status_label')} {hcode(status)}")
|
|
|
|
traffic_limit = subscription_details.get("traffic_limit_bytes")
|
|
traffic_used = subscription_details.get("traffic_used_bytes")
|
|
traffic_strategy = subscription_details.get("traffic_limit_strategy")
|
|
period_label = _format_traffic_period(traffic_strategy, _)
|
|
if traffic_used is not None or traffic_limit is not None:
|
|
used_display = _("traffic_na")
|
|
if traffic_used is not None:
|
|
traffic_used_gb = traffic_used / (1024**3)
|
|
used_display = f"{traffic_used_gb:.2f}GB"
|
|
used_display = _format_used_with_period(_, used_display, period_label)
|
|
|
|
if traffic_limit:
|
|
traffic_limit_gb = traffic_limit / (1024**3)
|
|
limit_display = f"{traffic_limit_gb:.2f}GB"
|
|
else:
|
|
limit_display = _("traffic_unlimited")
|
|
|
|
card_parts.append(
|
|
f"{_('admin_user_traffic_label')} {hcode(f'{used_display} / {limit_display}')}"
|
|
)
|
|
|
|
premium_unlimited = bool(subscription_details.get("premium_unlimited_override"))
|
|
premium_bonus_bytes = int(subscription_details.get("premium_bonus_bytes") or 0)
|
|
if premium_unlimited:
|
|
card_parts.append(
|
|
f"{_('admin_user_premium_override_label')} {hcode(_('admin_user_premium_override_unlimited'))}"
|
|
)
|
|
elif premium_bonus_bytes > 0:
|
|
bonus_gb = premium_bonus_bytes / (1024**3)
|
|
card_parts.append(
|
|
f"{_('admin_user_premium_override_label')} {hcode(_('admin_user_premium_override_bonus_value', gb=f'{bonus_gb:.2f}'))}"
|
|
)
|
|
else:
|
|
card_parts.append(
|
|
f"{_('admin_user_subscription_label')} {hcode(_('admin_user_subscription_none'))}"
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Error getting subscription details for user {user.user_id}: {e}")
|
|
card_parts.append(
|
|
f"{_('admin_user_subscription_label')} {hcode(_('admin_user_subscription_error'))}"
|
|
)
|
|
|
|
# Statistics
|
|
try:
|
|
# Count user logs
|
|
logs_count = await message_log_dal.count_user_message_logs(session, user.user_id)
|
|
card_parts.append(f"{_('admin_user_actions_count_label')} {hcode(str(logs_count))}")
|
|
|
|
# Check if user had any subscriptions
|
|
had_subscriptions = await subscription_service.has_had_any_subscription(
|
|
session, user.user_id
|
|
)
|
|
trial_status = (
|
|
_("admin_user_trial_used") if had_subscriptions else _("admin_user_trial_not_used")
|
|
)
|
|
card_parts.append(f"{_('admin_user_trial_label')} {hcode(trial_status)}")
|
|
|
|
# Financial analytics (admin-only)
|
|
try:
|
|
from db.dal import payment_dal
|
|
|
|
# Total amount paid by this user
|
|
total_paid = await payment_dal.get_user_total_paid(session, user.user_id)
|
|
card_parts.append(
|
|
f"{_('admin_user_total_paid_label')} {hcode(f'{total_paid:.2f} RUB')}"
|
|
)
|
|
|
|
# Total revenue from referrals
|
|
referral_revenue = await payment_dal.get_referral_revenue(session, user.user_id)
|
|
card_parts.append(
|
|
f"{_('admin_user_referral_revenue_label')} {hcode(f'{referral_revenue:.2f} RUB')}"
|
|
)
|
|
except Exception as e_fin:
|
|
logging.error(
|
|
f"Failed to build financial analytics for admin card {user.user_id}: {e_fin}"
|
|
)
|
|
|
|
# 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')} {hcode(str(invited_count))}"
|
|
)
|
|
card_parts.append(
|
|
f"{_('admin_user_ref_purchased_label')} {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:
|
|
logging.error(f"Error getting user statistics for {user.user_id}: {e}")
|
|
|
|
# Links section: subscription page + both referral links.
|
|
link_lines: list[str] = []
|
|
|
|
# Subscription URL — the user's panel-issued config link.
|
|
if user.panel_user_uuid:
|
|
try:
|
|
panel_data = await subscription_service.panel_service.get_user_by_uuid(
|
|
user.panel_user_uuid
|
|
)
|
|
sub_url = panel_data.get("subscriptionUrl") if panel_data else None
|
|
if sub_url:
|
|
link_lines.append(f"{_('admin_user_subscription_url_label')} {sub_url}")
|
|
except Exception as exc_sub:
|
|
logging.warning(
|
|
"Failed to fetch subscriptionUrl for user %s: %s", user.user_id, exc_sub
|
|
)
|
|
|
|
# Referral links — bot deep-link + webapp deep-link.
|
|
if referral_service is not None and bot_username:
|
|
try:
|
|
bot_ref_link = await referral_service.generate_referral_link(
|
|
session, bot_username, user.user_id
|
|
)
|
|
if bot_ref_link:
|
|
link_lines.append(f"{_('admin_user_ref_bot_link_label')} {bot_ref_link}")
|
|
except Exception as exc_bot_ref:
|
|
logging.warning(
|
|
"Failed to build bot referral link for %s: %s", user.user_id, exc_bot_ref
|
|
)
|
|
|
|
if settings is not None:
|
|
webapp_base = getattr(settings, "SUBSCRIPTION_MINI_APP_URL", None)
|
|
if webapp_base:
|
|
try:
|
|
code = await user_dal.ensure_referral_code(session, user)
|
|
if code:
|
|
from urllib.parse import parse_qsl, urlsplit, urlunsplit
|
|
|
|
parts = urlsplit(webapp_base)
|
|
query = dict(parse_qsl(parts.query, keep_blank_values=True))
|
|
query["ref"] = f"u{code}"
|
|
webapp_ref_link = urlunsplit(
|
|
(
|
|
parts.scheme,
|
|
parts.netloc,
|
|
parts.path,
|
|
"&".join(f"{k}={v}" for k, v in query.items()),
|
|
parts.fragment,
|
|
)
|
|
)
|
|
link_lines.append(f"{_('admin_user_ref_webapp_link_label')} {webapp_ref_link}")
|
|
except Exception as exc_web_ref:
|
|
logging.warning(
|
|
"Failed to build webapp referral link for %s: %s", user.user_id, exc_web_ref
|
|
)
|
|
|
|
if link_lines:
|
|
card_parts.append("")
|
|
card_parts.append(_("admin_user_links_section_title"))
|
|
card_parts.extend(link_lines)
|
|
|
|
return "\n".join(card_parts)
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_user_search, F.text)
|
|
async def process_user_search_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Process user search input and display user card"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
input_text = message.text.strip() if message.text else ""
|
|
user_model = await _find_user_by_admin_input(session, input_text)
|
|
|
|
if not user_model:
|
|
await message.answer(_("admin_user_not_found", input=hcode(input_text)))
|
|
return
|
|
|
|
# Store user ID in state for further operations
|
|
await state.update_data(target_user_id=user_model.user_id)
|
|
await state.clear()
|
|
|
|
# Format and send user card
|
|
try:
|
|
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
|
|
bot_username = await _resolve_bot_username(message.bot)
|
|
user_card_text = await format_user_card(
|
|
user_model,
|
|
session,
|
|
subscription_service,
|
|
i18n,
|
|
current_lang,
|
|
referral_service,
|
|
settings=settings,
|
|
bot_username=bot_username,
|
|
)
|
|
keyboard = get_user_card_keyboard(
|
|
user_model.user_id, i18n, current_lang, user_model.referred_by_id
|
|
)
|
|
|
|
await _send_with_profile_link_fallback(
|
|
message.answer,
|
|
text=user_card_text,
|
|
markup=keyboard.as_markup(),
|
|
user_id=user_model.user_id,
|
|
parse_mode="HTML",
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Error displaying user card for {user_model.user_id}: {e}")
|
|
await message.answer(_("admin_user_card_error"))
|
|
|
|
|
|
@router.callback_query(F.data.startswith("user_action:"))
|
|
async def user_action_handler(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
bot: Bot,
|
|
subscription_service: SubscriptionService,
|
|
panel_service: PanelApiService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Handle user management actions"""
|
|
try:
|
|
parts = callback.data.split(":")
|
|
action = parts[1]
|
|
user_id = int(parts[2])
|
|
except (IndexError, ValueError):
|
|
await callback.answer("Invalid action format.", show_alert=True)
|
|
return
|
|
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await callback.answer("Language service error.", show_alert=True)
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
# Get user from database
|
|
user = await user_dal.get_user_by_id(session, user_id)
|
|
if not user:
|
|
await callback.answer(_("admin_user_not_found_action"), show_alert=True)
|
|
return
|
|
|
|
if action == "reset_trial":
|
|
await handle_reset_trial(callback, user, subscription_service, session, i18n, current_lang)
|
|
elif action == "add_subscription":
|
|
await handle_add_subscription_prompt(callback, state, user, i18n, current_lang)
|
|
elif action == "toggle_ban":
|
|
await handle_toggle_ban(callback, user, panel_service, session, i18n, current_lang)
|
|
elif action == "send_message":
|
|
await handle_send_message_prompt(callback, state, user, i18n, current_lang)
|
|
elif action == "view_logs":
|
|
await handle_view_user_logs(callback, user, session, settings, i18n, current_lang)
|
|
elif action == "refresh":
|
|
await handle_refresh_user_card(
|
|
callback, user, subscription_service, session, i18n, current_lang
|
|
)
|
|
elif action == "delete_user":
|
|
await handle_delete_user_prompt(
|
|
callback, state, user, settings, i18n, current_lang, session
|
|
)
|
|
elif action == "premium_override":
|
|
await handle_premium_override_menu(
|
|
callback, state, user, subscription_service, session, i18n, current_lang
|
|
)
|
|
elif action == "premium_override_set_unlimited":
|
|
await handle_premium_override_apply(
|
|
callback,
|
|
user,
|
|
subscription_service,
|
|
session,
|
|
i18n,
|
|
current_lang,
|
|
unlimited=True,
|
|
bonus_bytes=0,
|
|
)
|
|
elif action == "premium_override_clear":
|
|
await handle_premium_override_apply(
|
|
callback,
|
|
user,
|
|
subscription_service,
|
|
session,
|
|
i18n,
|
|
current_lang,
|
|
unlimited=False,
|
|
bonus_bytes=0,
|
|
)
|
|
elif action == "premium_override_set_bonus":
|
|
await handle_premium_override_bonus_prompt(callback, state, user, i18n, current_lang)
|
|
elif action == "traffic_grant":
|
|
await handle_traffic_grant_menu(callback, user, i18n, current_lang)
|
|
elif action == "traffic_grant_regular":
|
|
await handle_traffic_grant_prompt(callback, state, user, "regular", i18n, current_lang)
|
|
elif action == "traffic_grant_premium":
|
|
await handle_traffic_grant_prompt(callback, state, user, "premium", i18n, current_lang)
|
|
else:
|
|
await callback.answer(_("admin_unknown_action"), show_alert=True)
|
|
|
|
|
|
async def handle_premium_override_menu(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
user: User,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
i18n_instance,
|
|
lang: str,
|
|
) -> None:
|
|
"""Show premium override sub-menu with current state and quick toggles."""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
active_sub = await subscription_dal.get_active_subscription_by_user_id(session, user.user_id)
|
|
if not active_sub:
|
|
await callback.answer(_("admin_premium_override_no_subscription"), show_alert=True)
|
|
return
|
|
|
|
unlimited = bool(getattr(active_sub, "premium_unlimited_override", False))
|
|
bonus_bytes = int(getattr(active_sub, "premium_bonus_bytes", 0) or 0)
|
|
bonus_gb = bonus_bytes / (1024**3) if bonus_bytes else 0
|
|
|
|
if unlimited:
|
|
current_text = _("admin_premium_override_state_unlimited")
|
|
elif bonus_bytes > 0:
|
|
current_text = _("admin_premium_override_state_bonus", gb=f"{bonus_gb:.2f}")
|
|
else:
|
|
current_text = _("admin_premium_override_state_none")
|
|
|
|
text = "\n".join(
|
|
[
|
|
f"<b>{_('admin_premium_override_title')}</b>",
|
|
"",
|
|
_("admin_premium_override_hint"),
|
|
"",
|
|
_("admin_premium_override_current", current=current_text),
|
|
]
|
|
)
|
|
|
|
builder = InlineKeyboardBuilder()
|
|
builder.button(
|
|
text=_("admin_premium_override_btn_unlimited"),
|
|
callback_data=f"user_action:premium_override_set_unlimited:{user.user_id}",
|
|
)
|
|
builder.button(
|
|
text=_("admin_premium_override_btn_bonus"),
|
|
callback_data=f"user_action:premium_override_set_bonus:{user.user_id}",
|
|
)
|
|
builder.button(
|
|
text=_("admin_premium_override_btn_clear"),
|
|
callback_data=f"user_action:premium_override_clear:{user.user_id}",
|
|
)
|
|
builder.button(
|
|
text=_("admin_user_back_to_card_button"),
|
|
callback_data=f"user_action:refresh:{user.user_id}",
|
|
)
|
|
builder.adjust(1, 1, 1, 1)
|
|
|
|
try:
|
|
await callback.message.edit_text(text, reply_markup=builder.as_markup(), parse_mode="HTML")
|
|
except Exception:
|
|
await callback.message.answer(text, reply_markup=builder.as_markup(), parse_mode="HTML")
|
|
await state.update_data(target_user_id=user.user_id)
|
|
await callback.answer()
|
|
|
|
|
|
async def handle_premium_override_apply(
|
|
callback: types.CallbackQuery,
|
|
user: User,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
i18n_instance,
|
|
lang: str,
|
|
*,
|
|
unlimited: bool,
|
|
bonus_bytes: int,
|
|
) -> None:
|
|
"""Persist a premium override (unlimited or explicit bonus) on the active subscription."""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
try:
|
|
active_sub = await subscription_dal.get_active_subscription_by_user_id(
|
|
session, user.user_id
|
|
)
|
|
if not active_sub:
|
|
await callback.answer(_("admin_premium_override_no_subscription"), show_alert=True)
|
|
return
|
|
|
|
active_sub.premium_unlimited_override = bool(unlimited)
|
|
active_sub.premium_bonus_bytes = max(0, int(bonus_bytes or 0))
|
|
if unlimited:
|
|
active_sub.premium_is_limited = False
|
|
|
|
await message_log_dal.create_message_log_no_commit(
|
|
session,
|
|
{
|
|
"user_id": callback.from_user.id if callback.from_user else user.user_id,
|
|
"event_type": "admin:premium_override",
|
|
"content": (f"unlimited={bool(unlimited)} bonus_bytes={int(bonus_bytes or 0)}"),
|
|
"is_admin_event": True,
|
|
"target_user_id": user.user_id,
|
|
"timestamp": datetime.now(timezone.utc),
|
|
},
|
|
)
|
|
await session.commit()
|
|
|
|
await subscription_service.sync_premium_squad_access_to_panel(session, user.user_id)
|
|
await session.commit()
|
|
|
|
await callback.answer(_("admin_premium_override_saved"), show_alert=False)
|
|
await handle_refresh_user_card(
|
|
callback, user, subscription_service, session, i18n_instance, lang
|
|
)
|
|
except Exception as exc:
|
|
logging.error(
|
|
"Failed to apply premium override for user %s: %s", user.user_id, exc, exc_info=True
|
|
)
|
|
await session.rollback()
|
|
await callback.answer(_("admin_premium_override_save_error"), show_alert=True)
|
|
|
|
|
|
async def handle_premium_override_bonus_prompt(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
user: User,
|
|
i18n_instance,
|
|
lang: str,
|
|
) -> None:
|
|
"""Ask admin for the bonus GB to grant."""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
await state.update_data(target_user_id=user.user_id)
|
|
await state.set_state(AdminStates.waiting_for_premium_override_bonus_gb)
|
|
prompt = _("admin_premium_override_bonus_prompt", user_id=user.user_id)
|
|
try:
|
|
await callback.message.edit_text(prompt)
|
|
except Exception:
|
|
await callback.message.answer(prompt)
|
|
await callback.answer()
|
|
|
|
|
|
async def handle_traffic_grant_menu(
|
|
callback: types.CallbackQuery,
|
|
user: User,
|
|
i18n_instance,
|
|
lang: str,
|
|
) -> None:
|
|
"""Show traffic-grant menu (regular vs premium)."""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
text = "\n".join(
|
|
[
|
|
f"<b>{_('admin_traffic_grant_title')}</b>",
|
|
"",
|
|
_("admin_traffic_grant_hint"),
|
|
]
|
|
)
|
|
builder = InlineKeyboardBuilder()
|
|
builder.button(
|
|
text=_("admin_traffic_grant_btn_regular"),
|
|
callback_data=f"user_action:traffic_grant_regular:{user.user_id}",
|
|
)
|
|
builder.button(
|
|
text=_("admin_traffic_grant_btn_premium"),
|
|
callback_data=f"user_action:traffic_grant_premium:{user.user_id}",
|
|
)
|
|
builder.button(
|
|
text=_("admin_user_back_to_card_button"),
|
|
callback_data=f"user_action:refresh:{user.user_id}",
|
|
)
|
|
builder.adjust(1, 1, 1)
|
|
|
|
try:
|
|
await callback.message.edit_text(text, reply_markup=builder.as_markup(), parse_mode="HTML")
|
|
except Exception:
|
|
await callback.message.answer(text, reply_markup=builder.as_markup(), parse_mode="HTML")
|
|
await callback.answer()
|
|
|
|
|
|
async def handle_traffic_grant_prompt(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
user: User,
|
|
kind: str,
|
|
i18n_instance,
|
|
lang: str,
|
|
) -> None:
|
|
"""Ask the admin for the amount of GB to grant (regular or premium)."""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
kind_normalized = "premium" if kind == "premium" else "regular"
|
|
await state.update_data(target_user_id=user.user_id, traffic_grant_kind=kind_normalized)
|
|
await state.set_state(AdminStates.waiting_for_traffic_grant_gb)
|
|
prompt_key = (
|
|
"admin_traffic_grant_prompt_premium"
|
|
if kind_normalized == "premium"
|
|
else "admin_traffic_grant_prompt_regular"
|
|
)
|
|
prompt = _(prompt_key, user_id=user.user_id)
|
|
try:
|
|
await callback.message.edit_text(prompt)
|
|
except Exception:
|
|
await callback.message.answer(prompt)
|
|
await callback.answer()
|
|
|
|
|
|
# `process_traffic_grant_gb_handler` is declared near the other FSM-bound
|
|
# handlers at the bottom of the module, so the router decorator can attach to
|
|
# the same `router` instance the premium override flow uses.
|
|
|
|
|
|
async def handle_reset_trial(
|
|
callback: types.CallbackQuery,
|
|
user: User,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
i18n_instance,
|
|
lang: str,
|
|
):
|
|
"""Reset user's trial eligibility"""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
try:
|
|
# Delete all user subscriptions to reset trial eligibility
|
|
await subscription_dal.delete_all_user_subscriptions(session, user.user_id)
|
|
await session.commit()
|
|
|
|
await callback.answer(_("admin_user_trial_reset_success"), show_alert=True)
|
|
|
|
# Refresh user card
|
|
await handle_refresh_user_card(
|
|
callback, user, subscription_service, session, i18n_instance, lang
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error resetting trial for user {user.user_id}: {e}")
|
|
await session.rollback()
|
|
await callback.answer(_("admin_user_trial_reset_error"), show_alert=True)
|
|
|
|
|
|
async def handle_add_subscription_prompt(
|
|
callback: types.CallbackQuery, state: FSMContext, user: User, i18n_instance, lang: str
|
|
):
|
|
"""Prompt admin to enter subscription days to add"""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
await state.update_data(target_user_id=user.user_id)
|
|
await state.set_state(AdminStates.waiting_for_subscription_days_to_add)
|
|
|
|
prompt_text = _("admin_user_add_subscription_prompt", user_id=user.user_id)
|
|
|
|
try:
|
|
await callback.message.edit_text(prompt_text)
|
|
except Exception:
|
|
await callback.message.answer(prompt_text)
|
|
|
|
await callback.answer()
|
|
|
|
|
|
async def handle_toggle_ban(
|
|
callback: types.CallbackQuery,
|
|
user: User,
|
|
panel_service: PanelApiService,
|
|
session: AsyncSession,
|
|
i18n_instance,
|
|
lang: str,
|
|
):
|
|
"""Toggle user ban status"""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
try:
|
|
new_ban_status = not user.is_banned
|
|
|
|
# Update in database
|
|
await user_dal.update_user(session, user.user_id, {"is_banned": new_ban_status})
|
|
|
|
# Update on panel if user has panel UUID
|
|
if user.panel_user_uuid:
|
|
panel_status = "DISABLED" if new_ban_status else "ACTIVE"
|
|
await panel_service.update_user_status_on_panel(
|
|
user.panel_user_uuid, not new_ban_status
|
|
)
|
|
|
|
await session.commit()
|
|
|
|
status_text = (
|
|
_("admin_user_ban_action_banned")
|
|
if new_ban_status
|
|
else _("admin_user_ban_action_unbanned")
|
|
)
|
|
await callback.answer(
|
|
_("admin_user_ban_toggle_success", status=status_text), show_alert=True
|
|
)
|
|
|
|
# Refresh user card with updated ban status
|
|
user.is_banned = new_ban_status # Update local object
|
|
from bot.services.panel_api_service import PanelApiService
|
|
from config.settings import Settings
|
|
|
|
settings = Settings()
|
|
async with PanelApiService(settings) as panel_service:
|
|
subscription_service = SubscriptionService(settings, panel_service)
|
|
await handle_refresh_user_card(
|
|
callback, user, subscription_service, session, i18n_instance, lang
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error toggling ban for user {user.user_id}: {e}")
|
|
await session.rollback()
|
|
await callback.answer(_("admin_user_ban_toggle_error"), show_alert=True)
|
|
|
|
|
|
async def handle_send_message_prompt(
|
|
callback: types.CallbackQuery, state: FSMContext, user: User, i18n_instance, lang: str
|
|
):
|
|
"""Prompt admin to enter message to send to user"""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
await state.update_data(target_user_id=user.user_id)
|
|
await state.set_state(AdminStates.waiting_for_direct_message_to_user)
|
|
|
|
prompt_text = _("admin_user_send_message_prompt", user_id=user.user_id)
|
|
|
|
try:
|
|
await callback.message.edit_text(prompt_text)
|
|
except Exception:
|
|
await callback.message.answer(prompt_text)
|
|
|
|
await callback.answer()
|
|
|
|
|
|
async def handle_view_user_logs(
|
|
callback: types.CallbackQuery,
|
|
user: User,
|
|
session: AsyncSession,
|
|
settings: Settings,
|
|
i18n_instance,
|
|
lang: str,
|
|
):
|
|
"""Show recent user logs"""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
try:
|
|
# Get recent logs for user
|
|
logs = await message_log_dal.get_user_message_logs(
|
|
session, user.user_id, limit=10, offset=0
|
|
)
|
|
|
|
if not logs:
|
|
await callback.answer(_("admin_user_no_logs"), show_alert=True)
|
|
return
|
|
|
|
logs_text_parts = [f"{_('admin_user_recent_actions_title', user_id=user.user_id)}\n"]
|
|
|
|
for log in logs:
|
|
timestamp = log.timestamp.strftime("%Y-%m-%d %H:%M") if log.timestamp else "N/A"
|
|
event_type = log.event_type or "N/A"
|
|
content_preview = (log.content or "")[:50] + (
|
|
"..." if len(log.content or "") > 50 else ""
|
|
)
|
|
|
|
logs_text_parts.append(
|
|
f"🕐 {hcode(timestamp)} - {hcode(event_type)}\n {content_preview}"
|
|
)
|
|
|
|
logs_text = "\n\n".join(logs_text_parts)
|
|
|
|
# Create inline keyboard for full logs
|
|
builder = InlineKeyboardBuilder()
|
|
builder.button(
|
|
text=_(key="admin_user_view_all_logs_button"),
|
|
callback_data=f"admin_logs:view_user:{user.user_id}:0",
|
|
)
|
|
builder.button(
|
|
text=_(key="admin_user_back_to_card_button"),
|
|
callback_data=f"user_action:refresh:{user.user_id}",
|
|
)
|
|
builder.adjust(1)
|
|
|
|
try:
|
|
await callback.message.edit_text(
|
|
logs_text, reply_markup=builder.as_markup(), parse_mode="HTML"
|
|
)
|
|
except Exception:
|
|
await callback.message.answer(
|
|
logs_text, reply_markup=builder.as_markup(), parse_mode="HTML"
|
|
)
|
|
|
|
await callback.answer()
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error viewing logs for user {user.user_id}: {e}")
|
|
await callback.answer(_("admin_user_logs_error"), show_alert=True)
|
|
|
|
|
|
async def handle_refresh_user_card(
|
|
callback: types.CallbackQuery,
|
|
user: User,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
i18n_instance,
|
|
lang: str,
|
|
):
|
|
"""Refresh user card with latest information"""
|
|
try:
|
|
# Reload user from database
|
|
fresh_user = await user_dal.get_user_by_id(session, user.user_id)
|
|
if not fresh_user:
|
|
await callback.answer("User not found", show_alert=True)
|
|
return
|
|
|
|
from config.settings import Settings as _Settings
|
|
|
|
_settings = _Settings()
|
|
referral_service = ReferralService(
|
|
_settings, subscription_service, callback.message.bot, i18n_instance
|
|
)
|
|
bot_username = await _resolve_bot_username(callback.message.bot)
|
|
user_card_text = await format_user_card(
|
|
fresh_user,
|
|
session,
|
|
subscription_service,
|
|
i18n_instance,
|
|
lang,
|
|
referral_service,
|
|
settings=_settings,
|
|
bot_username=bot_username,
|
|
)
|
|
keyboard = get_user_card_keyboard(
|
|
fresh_user.user_id, i18n_instance, lang, fresh_user.referred_by_id
|
|
)
|
|
markup = keyboard.as_markup()
|
|
|
|
try:
|
|
await _send_with_profile_link_fallback(
|
|
callback.message.edit_text,
|
|
text=user_card_text,
|
|
markup=markup,
|
|
user_id=fresh_user.user_id,
|
|
parse_mode="HTML",
|
|
)
|
|
except Exception:
|
|
await _send_with_profile_link_fallback(
|
|
callback.message.answer,
|
|
text=user_card_text,
|
|
markup=markup,
|
|
user_id=fresh_user.user_id,
|
|
parse_mode="HTML",
|
|
)
|
|
|
|
await callback.answer()
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error refreshing user card for {user.user_id}: {e}")
|
|
await callback.answer("Error refreshing user card", show_alert=True)
|
|
|
|
|
|
# Destructive deletion flow
|
|
async def handle_delete_user_prompt(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
user: User,
|
|
settings: Settings,
|
|
i18n_instance,
|
|
lang: str,
|
|
session: AsyncSession,
|
|
):
|
|
"""Trigger confirmation workflow for destructive deletion."""
|
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
|
|
|
admin = callback.from_user
|
|
admin_id = admin.id if admin else None
|
|
if not admin_id or admin_id not in settings.ADMIN_IDS:
|
|
logging.warning(f"Unauthorized delete attempt by user {admin_id} targeting {user.user_id}.")
|
|
await callback.answer(
|
|
_(
|
|
"admin_user_delete_not_allowed",
|
|
),
|
|
show_alert=True,
|
|
)
|
|
return
|
|
|
|
await state.update_data(
|
|
target_user_id=user.user_id,
|
|
delete_initiator_id=admin_id,
|
|
)
|
|
await state.set_state(AdminStates.waiting_for_user_delete_confirmation)
|
|
|
|
prompt_text = _(
|
|
"admin_user_delete_confirmation_prompt",
|
|
user_id=hcode(str(user.user_id)),
|
|
)
|
|
|
|
try:
|
|
await callback.message.answer(prompt_text, parse_mode="HTML")
|
|
except Exception as e:
|
|
logging.error(f"Failed to send delete confirmation prompt for user {user.user_id}: {e}")
|
|
await callback.message.reply(prompt_text, parse_mode="HTML")
|
|
|
|
await callback.answer()
|
|
|
|
|
|
async def _log_admin_user_deletion(
|
|
session: AsyncSession,
|
|
admin_id: int,
|
|
admin_user: Optional[types.User],
|
|
target_user_id: int,
|
|
) -> None:
|
|
"""Store audit log for successful deletion."""
|
|
try:
|
|
await message_log_dal.create_message_log_no_commit(
|
|
session,
|
|
{
|
|
"user_id": admin_id,
|
|
"telegram_username": admin_user.username if admin_user else None,
|
|
"telegram_first_name": admin_user.first_name if admin_user else None,
|
|
"event_type": "admin:user_deleted",
|
|
"content": f"Admin {admin_id} deleted user {target_user_id}",
|
|
"raw_update_preview": None,
|
|
"is_admin_event": True,
|
|
"target_user_id": target_user_id,
|
|
"timestamp": datetime.now(timezone.utc),
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Failed to log deletion audit for admin {admin_id} -> user {target_user_id}: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
|
|
# Message handlers for state-based inputs
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_user_delete_confirmation, F.text)
|
|
async def process_delete_user_confirmation_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
panel_service: PanelApiService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Confirm and execute destructive user deletion."""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
await state.clear()
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
admin = message.from_user
|
|
admin_id = admin.id if admin else None
|
|
if not admin_id or admin_id not in settings.ADMIN_IDS:
|
|
logging.warning(f"Unauthorized delete confirmation attempt by user {admin_id}.")
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_not_allowed",
|
|
)
|
|
)
|
|
await state.clear()
|
|
return
|
|
|
|
data = await state.get_data()
|
|
target_user_id = data.get("target_user_id")
|
|
if not target_user_id:
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_state_missing",
|
|
)
|
|
)
|
|
await state.clear()
|
|
return
|
|
|
|
confirmation_input = message.text.strip() if message.text else ""
|
|
if confirmation_input.lower() in {"/cancel", "cancel", "отмена"}:
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_cancelled",
|
|
)
|
|
)
|
|
await state.clear()
|
|
return
|
|
|
|
if confirmation_input != str(target_user_id):
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_mismatch",
|
|
)
|
|
)
|
|
await state.clear()
|
|
return
|
|
|
|
user_model = await user_dal.get_user_by_id(session, target_user_id)
|
|
if not user_model:
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_already_removed",
|
|
)
|
|
)
|
|
await state.clear()
|
|
return
|
|
|
|
try:
|
|
if user_model.panel_user_uuid:
|
|
panel_deleted = await panel_service.delete_user_from_panel(user_model.panel_user_uuid)
|
|
if not panel_deleted:
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_panel_error",
|
|
)
|
|
)
|
|
await session.rollback()
|
|
await state.clear()
|
|
return
|
|
|
|
deleted = await user_dal.delete_user_and_relations(session, target_user_id)
|
|
if not deleted:
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_already_removed",
|
|
)
|
|
)
|
|
await state.clear()
|
|
return
|
|
|
|
await _log_admin_user_deletion(session, admin_id, admin, target_user_id)
|
|
await session.commit()
|
|
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_success",
|
|
user_id=hcode(str(target_user_id)),
|
|
),
|
|
parse_mode="HTML",
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Error deleting user {target_user_id}: {e}", exc_info=True)
|
|
await session.rollback()
|
|
await message.answer(
|
|
_(
|
|
"admin_user_delete_error",
|
|
)
|
|
)
|
|
finally:
|
|
await state.clear()
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_subscription_days_to_add, F.text)
|
|
async def process_subscription_days_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Process subscription days input"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
data = await state.get_data()
|
|
target_user_id = data.get("target_user_id")
|
|
if not target_user_id:
|
|
await message.answer("Error: target user not found in state")
|
|
await state.clear()
|
|
return
|
|
|
|
try:
|
|
days_to_add = int(message.text.strip())
|
|
if days_to_add <= 0 or days_to_add > 3650: # Max 10 years
|
|
raise ValueError("Invalid days count")
|
|
except ValueError:
|
|
await message.answer(_("admin_user_invalid_days"))
|
|
return
|
|
|
|
try:
|
|
# Extend subscription
|
|
result = await subscription_service.extend_active_subscription_days(
|
|
session, target_user_id, days_to_add, "admin_manual_extension"
|
|
)
|
|
|
|
if result:
|
|
await session.commit()
|
|
await message.answer(
|
|
_("admin_user_subscription_added_success", days=days_to_add, user_id=target_user_id)
|
|
)
|
|
|
|
# Show updated user card
|
|
user = await user_dal.get_user_by_id(session, target_user_id)
|
|
if user:
|
|
referral_service = ReferralService(
|
|
settings, subscription_service, message.bot, i18n
|
|
)
|
|
bot_username = await _resolve_bot_username(message.bot)
|
|
user_card_text = await format_user_card(
|
|
user,
|
|
session,
|
|
subscription_service,
|
|
i18n,
|
|
current_lang,
|
|
referral_service,
|
|
settings=settings,
|
|
bot_username=bot_username,
|
|
)
|
|
keyboard = get_user_card_keyboard(
|
|
user.user_id, i18n, current_lang, user.referred_by_id
|
|
)
|
|
|
|
await _send_with_profile_link_fallback(
|
|
message.answer,
|
|
text=user_card_text,
|
|
markup=keyboard.as_markup(),
|
|
user_id=user.user_id,
|
|
parse_mode="HTML",
|
|
)
|
|
else:
|
|
await session.rollback()
|
|
await message.answer(_("admin_user_subscription_added_error"))
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error adding subscription days for user {target_user_id}: {e}")
|
|
await session.rollback()
|
|
await message.answer(_("admin_user_subscription_added_error"))
|
|
|
|
await state.clear()
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_direct_message_to_user)
|
|
async def process_direct_message_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
bot: Bot,
|
|
session: AsyncSession,
|
|
):
|
|
"""Process direct message to user"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
data = await state.get_data()
|
|
target_user_id = data.get("target_user_id")
|
|
if not target_user_id:
|
|
await message.answer("Error: target user not found in state")
|
|
await state.clear()
|
|
return
|
|
|
|
# Determine content similar to broadcast
|
|
text = (message.text or message.caption or "").strip()
|
|
if len(text) > 4000:
|
|
await message.answer(_("admin_user_message_too_long"))
|
|
return
|
|
|
|
try:
|
|
# Get target user
|
|
target_user = await user_dal.get_user_by_id(session, target_user_id)
|
|
if not target_user:
|
|
await message.answer("Target user not found")
|
|
await state.clear()
|
|
return
|
|
|
|
# Prepare admin signature and get content
|
|
admin_signature = _("admin_direct_message_signature")
|
|
|
|
content = get_message_content(message)
|
|
|
|
if not content.text and not content.file_id:
|
|
await message.answer(_("admin_direct_empty_message"))
|
|
return
|
|
|
|
caption_with_signature = (content.text + admin_signature) if content.text else None
|
|
|
|
# Send to target user using our fancy match/case function
|
|
try:
|
|
await send_direct_message(
|
|
bot,
|
|
target_user_id,
|
|
content,
|
|
extra_text=admin_signature,
|
|
parse_mode="HTML",
|
|
disable_web_page_preview=True,
|
|
)
|
|
except TelegramBadRequest as e:
|
|
await message.answer(
|
|
_(
|
|
"admin_broadcast_invalid_html",
|
|
error=str(e),
|
|
)
|
|
)
|
|
return
|
|
|
|
# Confirm to admin
|
|
await message.answer(_("admin_user_message_sent_success", user_id=target_user_id))
|
|
|
|
# Show user card again
|
|
from bot.services.panel_api_service import PanelApiService
|
|
|
|
async with PanelApiService(settings) as panel_service:
|
|
subscription_service = SubscriptionService(settings, panel_service)
|
|
referral_service = ReferralService(settings, subscription_service, bot, i18n)
|
|
bot_username = await _resolve_bot_username(bot)
|
|
user_card_text = await format_user_card(
|
|
target_user,
|
|
session,
|
|
subscription_service,
|
|
i18n,
|
|
current_lang,
|
|
referral_service,
|
|
settings=settings,
|
|
bot_username=bot_username,
|
|
)
|
|
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",
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error sending direct message to user {target_user_id}: {e}")
|
|
await message.answer(_("admin_user_message_sent_error"))
|
|
|
|
await state.clear()
|
|
|
|
|
|
async def ban_user_prompt_handler(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
i18n_data: dict,
|
|
settings: Settings,
|
|
session: AsyncSession,
|
|
):
|
|
"""Prompt admin to enter user ID or username to ban"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n or not callback.message:
|
|
await callback.answer("Error preparing ban prompt.", show_alert=True)
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
prompt_text = _("admin_ban_user_prompt")
|
|
|
|
try:
|
|
await callback.message.edit_text(
|
|
prompt_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
|
)
|
|
except Exception as e:
|
|
logging.warning(f"Could not edit message for ban prompt: {e}. Sending new.")
|
|
await callback.message.answer(
|
|
prompt_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
|
)
|
|
|
|
await callback.answer()
|
|
await state.set_state(AdminStates.waiting_for_user_id_to_ban)
|
|
|
|
|
|
async def unban_user_prompt_handler(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
i18n_data: dict,
|
|
settings: Settings,
|
|
session: AsyncSession,
|
|
):
|
|
"""Prompt admin to enter user ID or username to unban"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n or not callback.message:
|
|
await callback.answer("Error preparing unban prompt.", show_alert=True)
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
prompt_text = _("admin_unban_user_prompt")
|
|
|
|
try:
|
|
await callback.message.edit_text(
|
|
prompt_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
|
)
|
|
except Exception as e:
|
|
logging.warning(f"Could not edit message for unban prompt: {e}. Sending new.")
|
|
await callback.message.answer(
|
|
prompt_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
|
)
|
|
|
|
await callback.answer()
|
|
await state.set_state(AdminStates.waiting_for_user_id_to_unban)
|
|
|
|
|
|
async def view_banned_users_handler(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
i18n_data: dict,
|
|
settings: Settings,
|
|
session: AsyncSession,
|
|
):
|
|
"""Display list of banned users"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n or not callback.message:
|
|
await callback.answer("Error preparing banned users list.", show_alert=True)
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
try:
|
|
# Get banned users
|
|
banned_users = await user_dal.get_banned_users(session)
|
|
|
|
if not banned_users:
|
|
message_text = _("admin_banned_users_empty")
|
|
else:
|
|
user_list = []
|
|
for user in banned_users:
|
|
display_name = user.first_name or "Unknown"
|
|
if user.username:
|
|
display_name = f"@{user.username}"
|
|
user_list.append(f"• {display_name} (ID: {user.user_id})")
|
|
|
|
message_text = _(
|
|
"admin_banned_users_list", count=len(banned_users), users="\n".join(user_list)
|
|
)
|
|
|
|
await callback.message.edit_text(
|
|
message_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error displaying banned users: {e}")
|
|
await callback.answer("Error loading banned users", show_alert=True)
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_user_id_to_ban, F.text)
|
|
async def process_ban_user_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
panel_service: PanelApiService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Process user ban input"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
input_text = message.text.strip() if message.text else ""
|
|
user_model = await _find_user_by_admin_input(session, input_text)
|
|
|
|
if not user_model:
|
|
await message.answer(_("admin_user_not_found", input=hcode(input_text)))
|
|
return
|
|
|
|
try:
|
|
# Check if user is already banned
|
|
if user_model.is_banned:
|
|
await message.answer(_("admin_user_already_banned"))
|
|
await state.clear()
|
|
return
|
|
|
|
# Ban the user
|
|
await user_dal.update_user(session, user_model.user_id, {"is_banned": True})
|
|
|
|
# Update on panel if user has panel UUID
|
|
if user_model.panel_user_uuid:
|
|
await panel_service.update_user_status_on_panel(user_model.panel_user_uuid, False)
|
|
|
|
await session.commit()
|
|
|
|
await message.answer(_("admin_user_ban_success", input=hcode(input_text)))
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error banning user {user_model.user_id}: {e}")
|
|
await session.rollback()
|
|
await message.answer(_("admin_user_ban_error"))
|
|
|
|
await state.clear()
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_user_id_to_unban, F.text)
|
|
async def process_unban_user_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
panel_service: PanelApiService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Process user unban input"""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
input_text = message.text.strip() if message.text else ""
|
|
user_model = await _find_user_by_admin_input(session, input_text)
|
|
|
|
if not user_model:
|
|
await message.answer(_("admin_user_not_found", input=hcode(input_text)))
|
|
return
|
|
|
|
try:
|
|
# Check if user is not banned
|
|
if not user_model.is_banned:
|
|
await message.answer(_("admin_user_not_banned"))
|
|
await state.clear()
|
|
return
|
|
|
|
# Unban the user
|
|
await user_dal.update_user(session, user_model.user_id, {"is_banned": False})
|
|
|
|
# Update on panel if user has panel UUID
|
|
if user_model.panel_user_uuid:
|
|
await panel_service.update_user_status_on_panel(user_model.panel_user_uuid, True)
|
|
|
|
await session.commit()
|
|
|
|
await message.answer(_("admin_user_unban_success", input=hcode(input_text)))
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error unbanning user {user_model.user_id}: {e}")
|
|
await session.rollback()
|
|
await message.answer(_("admin_user_unban_error"))
|
|
|
|
await state.clear()
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_premium_override_bonus_gb, F.text)
|
|
async def process_premium_override_bonus_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Read bonus GB and apply premium override (non-unlimited path)."""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
data = await state.get_data()
|
|
target_user_id = data.get("target_user_id")
|
|
if not target_user_id:
|
|
await message.answer(_("admin_premium_override_state_missing"))
|
|
await state.clear()
|
|
return
|
|
|
|
raw = (message.text or "").strip().replace(",", ".")
|
|
try:
|
|
gb = float(raw)
|
|
if gb < 0 or gb > 1_000_000:
|
|
raise ValueError("out_of_range")
|
|
except (TypeError, ValueError):
|
|
await message.answer(_("admin_premium_override_invalid_gb"))
|
|
return
|
|
|
|
bonus_bytes = int(round(gb * (1024**3)))
|
|
target_user = await user_dal.get_user_by_id(session, target_user_id)
|
|
if not target_user:
|
|
await message.answer(_("admin_user_not_found_action"))
|
|
await state.clear()
|
|
return
|
|
|
|
try:
|
|
active_sub = await subscription_dal.get_active_subscription_by_user_id(
|
|
session, target_user_id
|
|
)
|
|
if not active_sub:
|
|
await message.answer(_("admin_premium_override_no_subscription"))
|
|
await state.clear()
|
|
return
|
|
|
|
active_sub.premium_unlimited_override = False
|
|
active_sub.premium_bonus_bytes = max(0, bonus_bytes)
|
|
|
|
await message_log_dal.create_message_log_no_commit(
|
|
session,
|
|
{
|
|
"user_id": message.from_user.id if message.from_user else target_user_id,
|
|
"event_type": "admin:premium_override",
|
|
"content": f"unlimited=False bonus_bytes={int(bonus_bytes)}",
|
|
"is_admin_event": True,
|
|
"target_user_id": target_user_id,
|
|
"timestamp": datetime.now(timezone.utc),
|
|
},
|
|
)
|
|
await session.commit()
|
|
|
|
await subscription_service.sync_premium_squad_access_to_panel(session, target_user_id)
|
|
await session.commit()
|
|
|
|
await message.answer(
|
|
_("admin_premium_override_bonus_set", gb=f"{gb:.2f}", user_id=target_user_id)
|
|
)
|
|
|
|
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
|
|
bot_username = await _resolve_bot_username(message.bot)
|
|
user_card_text = await format_user_card(
|
|
target_user,
|
|
session,
|
|
subscription_service,
|
|
i18n,
|
|
current_lang,
|
|
referral_service,
|
|
settings=settings,
|
|
bot_username=bot_username,
|
|
)
|
|
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",
|
|
)
|
|
except Exception as exc:
|
|
logging.error(
|
|
"Error setting premium override bonus for user %s: %s",
|
|
target_user_id,
|
|
exc,
|
|
exc_info=True,
|
|
)
|
|
await session.rollback()
|
|
await message.answer(_("admin_premium_override_save_error"))
|
|
finally:
|
|
await state.clear()
|
|
|
|
|
|
@router.message(AdminStates.waiting_for_traffic_grant_gb, F.text)
|
|
async def process_traffic_grant_gb_handler(
|
|
message: types.Message,
|
|
state: FSMContext,
|
|
settings: Settings,
|
|
i18n_data: dict,
|
|
subscription_service: SubscriptionService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Read GB amount and apply admin grant of regular or premium traffic."""
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await message.reply("Language service error.")
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
data = await state.get_data()
|
|
target_user_id = data.get("target_user_id")
|
|
kind = (data.get("traffic_grant_kind") or "regular").lower()
|
|
if not target_user_id:
|
|
await message.answer(_("admin_traffic_grant_no_user"))
|
|
await state.clear()
|
|
return
|
|
|
|
raw = (message.text or "").strip().replace(",", ".")
|
|
try:
|
|
gb_value = float(raw)
|
|
if gb_value <= 0 or gb_value > 1_000_000:
|
|
raise ValueError("out_of_range")
|
|
except (TypeError, ValueError):
|
|
await message.answer(_("admin_traffic_grant_invalid_gb"))
|
|
return
|
|
|
|
target_user = await user_dal.get_user_by_id(session, target_user_id)
|
|
if not target_user:
|
|
await message.answer(_("admin_user_not_found_action"))
|
|
await state.clear()
|
|
return
|
|
|
|
try:
|
|
if kind == "premium":
|
|
result = await subscription_service.admin_grant_premium_topup(
|
|
session, target_user_id, gb_value
|
|
)
|
|
else:
|
|
result = await subscription_service.admin_grant_topup(session, target_user_id, gb_value)
|
|
if not result:
|
|
await session.rollback()
|
|
await message.answer(_("admin_traffic_grant_failed"))
|
|
await state.clear()
|
|
return
|
|
await message_log_dal.create_message_log_no_commit(
|
|
session,
|
|
{
|
|
"user_id": message.from_user.id if message.from_user else target_user_id,
|
|
"event_type": "admin:traffic_grant",
|
|
"content": f"kind={kind} gb={gb_value:g}",
|
|
"is_admin_event": True,
|
|
"target_user_id": target_user_id,
|
|
"timestamp": datetime.now(timezone.utc),
|
|
},
|
|
)
|
|
await session.commit()
|
|
|
|
gb_text = f"{gb_value:g}"
|
|
success_key = (
|
|
"admin_traffic_grant_premium_done"
|
|
if kind == "premium"
|
|
else "admin_traffic_grant_regular_done"
|
|
)
|
|
await message.answer(_(success_key, gb=gb_text, user_id=target_user_id))
|
|
|
|
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
|
|
bot_username = await _resolve_bot_username(message.bot)
|
|
user_card_text = await format_user_card(
|
|
target_user,
|
|
session,
|
|
subscription_service,
|
|
i18n,
|
|
current_lang,
|
|
referral_service,
|
|
settings=settings,
|
|
bot_username=bot_username,
|
|
)
|
|
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",
|
|
)
|
|
except Exception as exc:
|
|
logging.error(
|
|
"Error granting traffic for user %s (kind=%s, gb=%s): %s",
|
|
target_user_id,
|
|
kind,
|
|
gb_value,
|
|
exc,
|
|
exc_info=True,
|
|
)
|
|
await session.rollback()
|
|
await message.answer(_("admin_traffic_grant_failed"))
|
|
finally:
|
|
await state.clear()
|
|
|
|
|
|
@router.callback_query(F.data.startswith("admin_user_card_from_list:"))
|
|
async def user_card_from_list_handler(
|
|
callback: types.CallbackQuery,
|
|
state: FSMContext,
|
|
i18n_data: dict,
|
|
settings: Settings,
|
|
bot: Bot,
|
|
subscription_service: SubscriptionService,
|
|
panel_service: PanelApiService,
|
|
session: AsyncSession,
|
|
):
|
|
"""Display user card when clicked from user list"""
|
|
try:
|
|
parts = callback.data.split(":")
|
|
user_id = int(parts[1])
|
|
page = int(parts[2])
|
|
except (IndexError, ValueError):
|
|
await callback.answer("Invalid user data", show_alert=True)
|
|
return
|
|
|
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
if not i18n:
|
|
await callback.answer("Language service error", show_alert=True)
|
|
return
|
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
|
|
|
# Get user from database
|
|
user = await user_dal.get_user_by_id(session, user_id)
|
|
if not user:
|
|
await callback.answer("User not found", show_alert=True)
|
|
return
|
|
|
|
# Create keyboard with back to list button
|
|
keyboard = get_user_card_keyboard(user_id, i18n, current_lang, user.referred_by_id)
|
|
keyboard.button(
|
|
text=_("admin_user_back_to_list_button"), callback_data=f"admin_action:users_list:{page}"
|
|
)
|
|
quick_links_width = 2 if user.referred_by_id else 1
|
|
keyboard.adjust(2, 2, 2, 2, quick_links_width, 1, 2, 1)
|
|
|
|
# Format user card
|
|
try:
|
|
from bot.services.referral_service import ReferralService
|
|
|
|
referral_service = ReferralService(settings, subscription_service, bot, i18n)
|
|
bot_username = await _resolve_bot_username(bot)
|
|
user_card_text = await format_user_card(
|
|
user,
|
|
session,
|
|
subscription_service,
|
|
i18n,
|
|
current_lang,
|
|
referral_service,
|
|
settings=settings,
|
|
bot_username=bot_username,
|
|
)
|
|
markup = keyboard.as_markup()
|
|
|
|
await _send_with_profile_link_fallback(
|
|
callback.message.edit_text,
|
|
text=user_card_text,
|
|
markup=markup,
|
|
user_id=user.user_id,
|
|
parse_mode="HTML",
|
|
)
|
|
await callback.answer()
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error displaying user card: {e}")
|
|
await callback.answer("Error displaying user card", show_alert=True)
|