From 207e7751cdc3726111044d2d213ed10935c1cf9b Mon Sep 17 00:00:00 2001 From: Bogdan Strielecki Date: Tue, 28 Oct 2025 15:13:19 +0300 Subject: [PATCH 1/2] Add paged list of all users to admin panel --- bot/handlers/admin/common.py | 16 +++- bot/handlers/admin/user_management.py | 114 ++++++++++++++++++++++-- bot/keyboards/inline/admin_keyboards.py | 68 +++++++++++++- db/dal/user_dal.py | 20 +++++ locales/en.json | 5 +- locales/ru.json | 5 +- 6 files changed, 215 insertions(+), 13 deletions(-) diff --git a/bot/handlers/admin/common.py b/bot/handlers/admin/common.py index 996cd83..a3f5e62 100644 --- a/bot/handlers/admin/common.py +++ b/bot/handlers/admin/common.py @@ -98,8 +98,22 @@ async def admin_panel_actions_callback_handler( await admin_user_mgmnt_handlers.unban_user_prompt_handler( callback, state, i18n_data, settings, session) elif action == "users_management": + # This is deprecated, kept for compatibility from . import user_management as admin_user_management_handlers - await admin_user_management_handlers.user_management_menu_handler( + await admin_user_management_handlers.user_search_prompt_handler( + callback, state, i18n_data, settings, session) + elif action == "users_list" and len(action_parts) > 2: + # Route to users list handler with page number + from . import user_management as admin_user_management_handlers + try: + page = int(action_parts[2]) + await admin_user_management_handlers.users_list_handler( + callback, i18n_data, settings, session, page) + except (IndexError, ValueError): + await callback.answer("Invalid page number", show_alert=True) + elif action == "users_search_prompt": + from . import user_management as admin_user_management_handlers + await admin_user_management_handlers.user_search_prompt_handler( callback, state, i18n_data, settings, session) elif action == "view_banned": diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py index db0e602..b970156 100644 --- a/bot/handlers/admin/user_management.py +++ b/bot/handlers/admin/user_management.py @@ -29,14 +29,57 @@ router = Router(name="admin_user_management_router") USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$") -async def user_management_menu_handler(callback: types.CallbackQuery, - state: FSMContext, i18n_data: dict, - settings: Settings, session: AsyncSession): - """Display user management menu""" +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 management.", show_alert=True) + 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", + default="👥 Список пользователей\n\nСтраница {current}/{total} ({total_users} пользователей)", + 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) @@ -911,9 +954,9 @@ async def process_ban_user_handler(message: types.Message, state: FSMContext, @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): + 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") @@ -978,3 +1021,58 @@ async def process_unban_user_handler(message: types.Message, state: FSMContext, )) 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) + 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) + + # Format user card + try: + from bot.services.referral_service import ReferralService + referral_service = ReferralService(settings, subscription_service, bot, i18n) + user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang, referral_service) + + await callback.message.edit_text( + user_card_text, + reply_markup=keyboard.as_markup(), + 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) diff --git a/bot/keyboards/inline/admin_keyboards.py b/bot/keyboards/inline/admin_keyboards.py index 37dfbf8..91d4379 100644 --- a/bot/keyboards/inline/admin_keyboards.py +++ b/bot/keyboards/inline/admin_keyboards.py @@ -59,13 +59,15 @@ def get_user_management_keyboard(i18n_instance, lang: str) -> InlineKeyboardMark builder = InlineKeyboardBuilder() builder.button(text=_(key="admin_users_management_button"), - callback_data="admin_action:users_management") + callback_data="admin_action:users_list:0") + builder.button(text=_(key="admin_users_search_button"), + callback_data="admin_action:users_search_prompt") builder.button(text=_(key="admin_ban_management_section"), callback_data="admin_section:ban_management") builder.button(text=_(key="back_to_admin_panel_button"), callback_data="admin_action:main") - builder.adjust(2, 1) + builder.adjust(2, 1, 1) return builder.as_markup() @@ -305,6 +307,68 @@ def get_banned_users_keyboard(banned_users: List[User], current_page: int, return builder.as_markup() +def get_users_list_keyboard(users: List[User], current_page: int, + total_users: int, i18n_instance, lang: str, + page_size: int = 15) -> InlineKeyboardMarkup: + """Generate keyboard for paginated user list""" + _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs) + builder = InlineKeyboardBuilder() + + # Add user buttons + for user in users: + user_display_parts = [] + if user.username: + user_display_parts.append(f"@{user.username}") + user_display_parts.append(f"ID: {user.user_id}") + if user.first_name: + user_display_parts.append(f"- {user.first_name}") + + button_text = " ".join(user_display_parts) + builder.row( + InlineKeyboardButton( + text=button_text, + callback_data=f"admin_user_card_from_list:{user.user_id}:{current_page}" + ) + ) + + # Pagination buttons + if total_users > page_size: + total_pages = math.ceil(total_users / page_size) + pagination_buttons = [] + if current_page > 0: + pagination_buttons.append( + InlineKeyboardButton( + text=_("prev_page_button"), + callback_data=f"admin_action:users_list:{current_page - 1}" + ) + ) + pagination_buttons.append( + InlineKeyboardButton( + text=f"{current_page + 1}/{total_pages}", + callback_data="stub_page_display" + ) + ) + if current_page < total_pages - 1: + pagination_buttons.append( + InlineKeyboardButton( + text=_("next_page_button"), + callback_data=f"admin_action:users_list:{current_page + 1}" + ) + ) + if pagination_buttons: + builder.row(*pagination_buttons) + + # Back button + builder.row( + InlineKeyboardButton( + text=_("back_to_user_management_button"), + callback_data="admin_section:user_management" + ) + ) + + return builder.as_markup() + + def get_user_card_keyboard(user_id: int, is_banned: bool, i18n_instance, diff --git a/db/dal/user_dal.py b/db/dal/user_dal.py index 423945f..b0b7b42 100644 --- a/db/dal/user_dal.py +++ b/db/dal/user_dal.py @@ -227,3 +227,23 @@ async def get_user_ids_without_active_subscription(session: AsyncSession) -> Lis ) result = await session.execute(stmt) return result.scalars().all() + + +async def get_all_users_paginated(session: AsyncSession, page: int = 0, page_size: int = 15) -> List[User]: + """Get all users with pagination, ordered by registration date descending.""" + offset = page * page_size + stmt = ( + select(User) + .order_by(User.registration_date.desc()) + .offset(offset) + .limit(page_size) + ) + result = await session.execute(stmt) + return result.scalars().all() + + +async def count_all_users(session: AsyncSession) -> int: + """Count total number of users in the database.""" + stmt = select(func.count(User.user_id)) + result = await session.execute(stmt) + return result.scalar() or 0 \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index cc2437e..9fc48f6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -101,7 +101,10 @@ "admin_promo_marketing_section": "🎁 Promos", "admin_system_functions_section": "⚙️ System", "admin_ban_management_section": "🚫 Bans", - "admin_users_management_button": "👤 Management", + "admin_users_search_button": "🔍 Search", + "admin_users_management_button": "👥 User List", + "admin_users_list_header": "👥 User List\n\nPage {current}/{total} ({total_users} users)", + "admin_user_back_to_list_button": "⬅️ Back to List", "back_to_user_management_button": "⬅️ To Users", "back_to_admin_panel_button": "⬅️ To Admin", "admin_stats_header": "📊 Bot Statistics", diff --git a/locales/ru.json b/locales/ru.json index ad106ae..b2c3b2d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -101,7 +101,10 @@ "admin_promo_marketing_section": "🎁 Промокоды", "admin_system_functions_section": "⚙️ Система", "admin_ban_management_section": "🚫 Блокировки", - "admin_users_management_button": "👤 Управление", + "admin_users_search_button": "🔍 Поиск", + "admin_users_management_button": "👥 Список пользователей", + "admin_users_list_header": "👥 Список пользователей\n\nСтраница {current}/{total} ({total_users} пользователей)", + "admin_user_back_to_list_button": "⬅️ К списку", "back_to_user_management_button": "⬅️ К пользователям", "back_to_admin_panel_button": "⬅️ В админку", "admin_stats_header": "📊 Статистика Бота", From 3b0b88e9a0a30e45053b9da2d739d9f151293f2a Mon Sep 17 00:00:00 2001 From: Bogdan Strielecki Date: Thu, 30 Oct 2025 17:55:08 +0300 Subject: [PATCH 2/2] Add admin button for delete user from bot and panel --- bot/handlers/admin/user_management.py | 223 +++++++++++++++++++++++++- bot/services/panel_api_service.py | 31 ++++ bot/states/admin_states.py | 1 + db/dal/user_dal.py | 84 ++++++++-- locales/en.json | 10 ++ locales/ru.json | 10 ++ 6 files changed, 340 insertions(+), 19 deletions(-) diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py index b970156..e0ea2d1 100644 --- a/bot/handlers/admin/user_management.py +++ b/bot/handlers/admin/user_management.py @@ -138,8 +138,14 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb text=_(key="admin_user_refresh_button", default="🔄 Обновить"), callback_data=f"user_action:refresh:{user_id}" ) + + # Row 4: Destructive action + builder.button( + text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"), + callback_data=f"user_action:delete_user:{user_id}" + ) - # Row 4: Back button + # Row 5: Navigation builder.button( text=_(key="admin_user_search_new_button", default="🔍 Найти другого"), callback_data="admin_action:users_management" @@ -149,7 +155,7 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb callback_data="admin_action:main" ) - builder.adjust(2, 2, 2, 2) + builder.adjust(2, 2, 2, 1, 2) return builder @@ -367,6 +373,10 @@ async def user_action_handler(callback: types.CallbackQuery, state: FSMContext, 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 + ) else: await callback.answer(_("admin_unknown_action"), show_alert=True) @@ -592,8 +602,217 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User, 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", + default="❌ У вас нет прав для удаления пользователей.", + ), + 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", + default=( + "⚠️ Вы хотите полностью удалить пользователя {user_id}.\n\n" + "Отправьте точный Telegram ID этого пользователя, чтобы подтвердить удаление.\n" + "Любой другой ответ отменит операцию." + ), + 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", + default="❌ У вас нет прав для удаления пользователей.", + ) + ) + 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", + default="⚠️ Нет активной операции удаления. Начните заново.", + ) + ) + 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", + default="Операция удаления отменена по запросу.", + ) + ) + await state.clear() + return + + if confirmation_input != str(target_user_id): + await message.answer( + _( + "admin_user_delete_mismatch", + default="⚠️ ID не совпадает. Удаление отменено.", + ) + ) + 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", + default="ℹ️ Пользователь уже удален.", + ) + ) + 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", + default=( + "❌ Не удалось удалить пользователя на панели. " + "Операция прервана." + ), + ) + ) + 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", + default="ℹ️ Пользователь уже удален.", + ) + ) + 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", + default="✅ Пользователь {user_id} удален из бота и панели.", + 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", + default="❌ Не удалось завершить удаление пользователя. Попробуйте позже.", + ) + ) + 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, diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py index 60586d0..6acb08d 100644 --- a/bot/services/panel_api_service.py +++ b/bot/services/panel_api_service.py @@ -455,6 +455,37 @@ class PanelApiService: ) return False + async def delete_user_from_panel(self, + user_uuid: str, + log_response: bool = True) -> bool: + """Delete a user from the panel. Treat not-found as already deleted.""" + endpoint = f"/users/{user_uuid}" + response_data = await self._request( + "DELETE", endpoint, log_full_response=log_response + ) + + if not response_data: + logging.error( + f"Panel API delete_user_from_panel returned no data for user {user_uuid}." + ) + return False + + if response_data.get("error"): + details = response_data.get("details") or {} + error_code = details.get("errorCode") or response_data.get("errorCode") + if error_code in {"A062", "A040"}: + logging.info( + f"Panel user {user_uuid} already absent (errorCode {error_code}). Treating as deleted." + ) + return True + logging.error( + f"Failed to delete user {user_uuid} on panel. Response: {response_data}" + ) + return False + + logging.info(f"Panel user {user_uuid} deleted successfully.") + return True + async def get_subscription_link( self, short_uuid_or_sub_uuid: str, diff --git a/bot/states/admin_states.py b/bot/states/admin_states.py index 6db5421..68b0c4a 100644 --- a/bot/states/admin_states.py +++ b/bot/states/admin_states.py @@ -28,6 +28,7 @@ class AdminStates(StatesGroup): waiting_for_user_search = State() waiting_for_subscription_days_to_add = State() waiting_for_direct_message_to_user = State() + waiting_for_user_delete_confirmation = State() # Ads campaigns waiting_for_ad_source = State() diff --git a/db/dal/user_dal.py b/db/dal/user_dal.py index b0b7b42..4676a81 100644 --- a/db/dal/user_dal.py +++ b/db/dal/user_dal.py @@ -3,11 +3,20 @@ from typing import Optional, List, Dict, Any, Tuple from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy.orm import selectinload -from sqlalchemy import update, delete, func, and_ +from sqlalchemy import update, delete, func, and_, or_ from datetime import datetime, timezone from sqlalchemy.dialects.postgresql import insert as pg_insert -from ..models import User, Subscription +from ..models import ( + User, + Subscription, + Payment, + PromoCodeActivation, + MessageLog, + UserBilling, + UserPaymentMethod, + AdAttribution, +) async def get_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]: @@ -102,6 +111,29 @@ async def get_banned_users(session: AsyncSession) -> List[User]: return result.scalars().all() +async def get_all_users_paginated( + session: AsyncSession, *, page: int = 0, page_size: int = 15 +) -> List[User]: + """Return a slice of users ordered by newest registration first.""" + safe_page = max(page, 0) + safe_page_size = max(page_size, 1) + + stmt = ( + select(User) + .order_by(User.registration_date.desc()) + .offset(safe_page * safe_page_size) + .limit(safe_page_size) + ) + result = await session.execute(stmt) + return result.scalars().all() + + +async def count_all_users(session: AsyncSession) -> int: + """Count total number of users.""" + result = await session.execute(select(func.count(User.user_id))) + return result.scalar_one() + + async def get_all_active_user_ids_for_broadcast(session: AsyncSession) -> List[int]: stmt = select(User.user_id).where(User.is_banned == False) result = await session.execute(stmt) @@ -229,21 +261,39 @@ async def get_user_ids_without_active_subscription(session: AsyncSession) -> Lis return result.scalars().all() -async def get_all_users_paginated(session: AsyncSession, page: int = 0, page_size: int = 15) -> List[User]: - """Get all users with pagination, ordered by registration date descending.""" - offset = page * page_size - stmt = ( - select(User) - .order_by(User.registration_date.desc()) - .offset(offset) - .limit(page_size) +async def delete_user_and_relations(session: AsyncSession, user_id: int) -> bool: + """Completely remove a user and all dependent records from the database. + + This helper ensures we do not leave dangling foreign keys or orphaned data. + """ + user = await get_user_by_id(session, user_id) + if not user: + return False + + # Ensure referral pointers do not block deletion + await session.execute( + update(User).where(User.referred_by_id == user_id).values(referred_by_id=None) ) - result = await session.execute(stmt) - return result.scalars().all() + # Clean up dependent tables that do not cascade automatically + await session.execute( + delete(MessageLog).where( + or_(MessageLog.user_id == user_id, MessageLog.target_user_id == user_id) + ) + ) + await session.execute(delete(Payment).where(Payment.user_id == user_id)) + await session.execute( + delete(Subscription).where(Subscription.user_id == user_id) + ) + await session.execute( + delete(PromoCodeActivation).where(PromoCodeActivation.user_id == user_id) + ) + await session.execute( + delete(UserPaymentMethod).where(UserPaymentMethod.user_id == user_id) + ) + await session.execute(delete(UserBilling).where(UserBilling.user_id == user_id)) + await session.execute(delete(AdAttribution).where(AdAttribution.user_id == user_id)) -async def count_all_users(session: AsyncSession) -> int: - """Count total number of users in the database.""" - stmt = select(func.count(User.user_id)) - result = await session.execute(stmt) - return result.scalar() or 0 \ No newline at end of file + await session.delete(user) + await session.flush() + return True diff --git a/locales/en.json b/locales/en.json index 9fc48f6..b3ffec6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -247,6 +247,16 @@ "admin_user_send_message_button": "✉️ Send Message", "admin_user_view_logs_button": "📜 User Actions", "admin_user_refresh_button": "🔄 Refresh", + "admin_user_delete_button": "❌ Delete User", + "admin_user_delete_not_allowed": "❌ You are not permitted to delete users.", + "admin_user_delete_confirmation_prompt": "⚠️ You are about to delete user {user_id} completely.\n\nSend the exact Telegram ID of this user to confirm.\nAny other reply will cancel the operation.", + "admin_user_delete_state_missing": "⚠️ No active delete operation. Start again.", + "admin_user_delete_cancelled": "Deletion cancelled.", + "admin_user_delete_mismatch": "⚠️ ID mismatch. Deletion aborted.", + "admin_user_delete_already_removed": "ℹ️ The user is already removed.", + "admin_user_delete_panel_error": "❌ Failed to remove the user from the panel. Operation aborted.", + "admin_user_delete_success": "✅ User {user_id} was removed from the bot and the panel.", + "admin_user_delete_error": "❌ Unable to delete the user. Please try again later.", "admin_user_search_new_button": "🔍 Find Another", "admin_user_view_all_logs_button": "📋 All Actions", "admin_user_back_to_card_button": "🔙 Back to Card", diff --git a/locales/ru.json b/locales/ru.json index b2c3b2d..a9e97d9 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -247,6 +247,16 @@ "admin_user_send_message_button": "✉️ Сообщение", "admin_user_view_logs_button": "📜 Логи", "admin_user_refresh_button": "🔄 Обновить", + "admin_user_delete_button": "❌ Удалить пользователя", + "admin_user_delete_not_allowed": "❌ У вас нет прав для удаления пользователей.", + "admin_user_delete_confirmation_prompt": "⚠️ Вы хотите полностью удалить пользователя {user_id}.\n\nОтправьте точный Telegram ID этого пользователя, чтобы подтвердить удаление.\nЛюбой другой ответ отменит операцию.", + "admin_user_delete_state_missing": "⚠️ Нет активной операции удаления. Начните заново.", + "admin_user_delete_cancelled": "Удаление отменено.", + "admin_user_delete_mismatch": "⚠️ ID не совпадает. Удаление отменено.", + "admin_user_delete_already_removed": "ℹ️ Пользователь уже удалён.", + "admin_user_delete_panel_error": "❌ Не удалось удалить пользователя на панели. Операция прервана.", + "admin_user_delete_success": "✅ Пользователь {user_id} удалён из бота и панели.", + "admin_user_delete_error": "❌ Не удалось удалить пользователя. Попробуйте позже.", "admin_user_search_new_button": "🔍 Новый поиск", "admin_user_view_all_logs_button": "📋 Все логи", "admin_user_back_to_card_button": "🔙 К карточке",