From 8415c68063c25122ddbe5803828791c6f3c5e298 Mon Sep 17 00:00:00 2001 From: gy9vin Date: Sat, 18 Oct 2025 20:56:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=BD=D0=BA=D1=80=D0=B5=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D1=83=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8E=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B5=D0=BF=D0=BE=D1=81=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D1=81=D1=82=D0=B2=D0=B5=D0=BD=D0=BD=D0=BE=20=D0=B8=D0=B7?= =?UTF-8?q?=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/handlers/admin/users.py | 114 ++++++++++++++++++++++++++++++- app/keyboards/admin.py | 7 ++ app/localization/locales/en.json | 8 +++ app/localization/locales/ru.json | 8 +++ app/states.py | 1 + 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py index 3d92231b..a1f44f7c 100644 --- a/app/handlers/admin/users.py +++ b/app/handlers/admin/users.py @@ -2,6 +2,7 @@ import logging from datetime import datetime, timedelta from typing import Optional from aiogram import Dispatcher, types, F +from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession @@ -1644,6 +1645,107 @@ async def start_balance_edit( await callback.answer() +@admin_required +@error_handler +async def start_send_user_message( + callback: types.CallbackQuery, + db_user: User, + state: FSMContext, + db: AsyncSession, +): + user_id = int(callback.data.split('_')[-1]) + + target_user = await get_user_by_id(db, user_id) + if not target_user: + await callback.answer("❌ Пользователь не найден", show_alert=True) + return + + await state.update_data(direct_message_user_id=user_id) + + texts = get_texts(db_user.language) + prompt = ( + texts.t("ADMIN_USER_SEND_MESSAGE_PROMPT", + "✉️ Отправка сообщения пользователю\n\n" + "Введите текст, который бот отправит пользователю." + "\n\nВы можете отменить действие командой /cancel или кнопкой ниже." ) + ) + + await callback.message.edit_text( + prompt, + reply_markup=types.InlineKeyboardMarkup( + inline_keyboard=[ + [types.InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_manage_{user_id}")] + ] + ), + parse_mode="HTML", + ) + + await state.set_state(AdminStates.sending_user_message) + await callback.answer() + + +@admin_required +@error_handler +async def process_send_user_message( + message: types.Message, + db_user: User, + state: FSMContext, + db: AsyncSession, +): + texts = get_texts(db_user.language) + data = await state.get_data() + user_id = data.get("direct_message_user_id") + + if not user_id: + await message.answer(texts.t("ADMIN_USER_SEND_MESSAGE_ERROR_NOT_FOUND", "❌ Пользователь для отправки сообщения не найден")) + await state.clear() + return + + target_user = await get_user_by_id(db, int(user_id)) + if not target_user: + await message.answer(texts.t("ADMIN_USER_SEND_MESSAGE_ERROR_NOT_FOUND", "❌ Пользователь не найден или был удалён")) + await state.clear() + return + + text = (message.text or "").strip() + if not text: + await message.answer(texts.t("ADMIN_USER_SEND_MESSAGE_EMPTY", "❌ Пожалуйста, введите непустое сообщение")) + return + + confirmation_keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[[types.InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_manage_{user_id}")]] + ) + + try: + await message.bot.send_message(target_user.telegram_id, text) + await message.answer( + texts.t("ADMIN_USER_SEND_MESSAGE_SUCCESS", "✅ Сообщение отправлено пользователю"), + reply_markup=confirmation_keyboard, + ) + except TelegramForbiddenError: + await message.answer( + texts.t("ADMIN_USER_SEND_MESSAGE_FORBIDDEN", "⚠️ Пользователь заблокировал бота или не может получить сообщения."), + reply_markup=confirmation_keyboard, + ) + except TelegramBadRequest as err: + logger.error("Ошибка отправки сообщения пользователю %s: %s", target_user.telegram_id, err) + await message.answer( + texts.t("ADMIN_USER_SEND_MESSAGE_BAD_REQUEST", "❌ Telegram отклонил сообщение. Проверьте текст и попробуйте ещё раз."), + reply_markup=confirmation_keyboard, + ) + return + except Exception as err: + logger.error("Неожиданная ошибка отправки сообщения пользователю %s: %s", target_user.telegram_id, err) + await message.answer( + texts.t("ADMIN_USER_SEND_MESSAGE_ERROR", "❌ Не удалось отправить сообщение. Попробуйте позже."), + reply_markup=confirmation_keyboard, + ) + await state.clear() + return + + await state.clear() + + @admin_required @error_handler async def process_balance_edit( @@ -4014,11 +4116,21 @@ def register_handlers(dp: Dispatcher): start_balance_edit, F.data.startswith("admin_user_balance_") ) - + dp.message.register( process_balance_edit, AdminStates.editing_user_balance ) + + dp.callback_query.register( + start_send_user_message, + F.data.startswith("admin_user_send_message_") + ) + + dp.message.register( + process_send_user_message, + AdminStates.sending_user_message + ) dp.callback_query.register( show_inactive_users, diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index a1d9e3a8..75e72bc0 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -762,6 +762,13 @@ def get_user_management_keyboard(user_id: int, user_status: str, language: str = ] ] + keyboard.append([ + InlineKeyboardButton( + text=_t(texts, "ADMIN_USER_SEND_MESSAGE", "✉️ Отправить сообщение"), + callback_data=f"admin_user_send_message_{user_id}" + ) + ]) + if user_status == "active": keyboard.append([ InlineKeyboardButton( diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 62cae4a9..16eb50dc 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -695,6 +695,14 @@ "ADMIN_USER_SUBSCRIPTION_TYPE_TRIAL": "🎁 Trial", "ADMIN_USER_TRAFFIC_USAGE": "{used}/{limit} GB", "ADMIN_USER_TRANSACTIONS": "📋 Transactions", + "ADMIN_USER_SEND_MESSAGE": "✉️ Send message", + "ADMIN_USER_SEND_MESSAGE_PROMPT": "✉️ Send a message to the user\n\nType the text that the bot should send.\n\nYou can cancel with /cancel or the button below.", + "ADMIN_USER_SEND_MESSAGE_SUCCESS": "✅ Message sent to the user", + "ADMIN_USER_SEND_MESSAGE_FORBIDDEN": "⚠️ The user blocked the bot or cannot receive messages.", + "ADMIN_USER_SEND_MESSAGE_BAD_REQUEST": "❌ Telegram rejected the message. Check the text and try again.", + "ADMIN_USER_SEND_MESSAGE_ERROR": "❌ Couldn't send the message. Please try again later.", + "ADMIN_USER_SEND_MESSAGE_ERROR_NOT_FOUND": "❌ User not found", + "ADMIN_USER_SEND_MESSAGE_EMPTY": "❌ Please enter a non-empty message", "ADMIN_USER_UNBLOCK": "✅ Unblock", "ADMIN_USER_USERNAME_NOT_SET": "not set", "ADMIN_WELCOME_DISABLE": "🔴 Disable", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index a57c7c3d..efc3367a 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -695,6 +695,14 @@ "ADMIN_USER_SUBSCRIPTION_TYPE_TRIAL": "🎁 Триал", "ADMIN_USER_TRAFFIC_USAGE": "{used}/{limit} ГБ", "ADMIN_USER_TRANSACTIONS": "📋 Транзакции", + "ADMIN_USER_SEND_MESSAGE": "✉️ Отправить сообщение", + "ADMIN_USER_SEND_MESSAGE_PROMPT": "✉️ Отправка сообщения пользователю\n\nВведите текст, который бот отправит пользователю.\n\nМожно отменить командой /cancel или кнопкой ниже.", + "ADMIN_USER_SEND_MESSAGE_SUCCESS": "✅ Сообщение отправлено пользователю", + "ADMIN_USER_SEND_MESSAGE_FORBIDDEN": "⚠️ Пользователь заблокировал бота или не может получать сообщения.", + "ADMIN_USER_SEND_MESSAGE_BAD_REQUEST": "❌ Telegram отклонил сообщение. Проверьте текст и попробуйте ещё раз.", + "ADMIN_USER_SEND_MESSAGE_ERROR": "❌ Не удалось отправить сообщение. Попробуйте позже.", + "ADMIN_USER_SEND_MESSAGE_ERROR_NOT_FOUND": "❌ Пользователь не найден", + "ADMIN_USER_SEND_MESSAGE_EMPTY": "❌ Пожалуйста, введите непустое сообщение", "ADMIN_USER_UNBLOCK": "✅ Разблокировать", "ADMIN_USER_USERNAME_NOT_SET": "не указан", "ADMIN_WELCOME_DISABLE": "🔴 Отключить", diff --git a/app/states.py b/app/states.py index 67db9213..549e314f 100644 --- a/app/states.py +++ b/app/states.py @@ -36,6 +36,7 @@ class PromoCodeStates(StatesGroup): class AdminStates(StatesGroup): waiting_for_user_search = State() + sending_user_message = State() editing_user_balance = State() extending_subscription = State() adding_traffic = State()