diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index e6f297fa..2dbffde0 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -3,7 +3,7 @@ from datetime import datetime from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, Message +from aiogram.types import CallbackQuery, Message, InlineKeyboardMarkup, InlineKeyboardButton from sqlalchemy import distinct, func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -19,6 +19,7 @@ router = Router() class AdminSender(StatesGroup): waiting_for_message = State() + preview = State() @router.callback_query( @@ -40,7 +41,14 @@ async def handle_sender_callback_text( callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext ): await callback_query.message.edit_text( - text="✍️ Введите текст сообщения для рассылки:", + text=( + "✍️ Введите текст сообщения для рассылки\n\n" + "Поддерживается только Telegram-форматирование — жирный, курсив и другие стили через редактор Telegram.\n\n" + "Вы можете отправить:\n" + "• Только текст\n" + "• Только картинку\n" + "• Текст + картинку" + ), reply_markup=build_admin_back_kb("sender"), ) await state.update_data(type=callback_data.type, cluster_name=callback_data.data) @@ -62,31 +70,51 @@ async def handle_sender_callback(callback_query: CallbackQuery, session: AsyncSe @router.message(AdminSender.waiting_for_message, IsAdminFilter()) -async def handle_message_input(message: Message, state: FSMContext, session): - text_message = message.html_text if message.text else None +async def handle_message_input(message: Message, state: FSMContext): + text_message = message.html_text or message.text or message.caption or "" photo = message.photo[-1].file_id if message.photo else None - photo_url = ( - message.caption - if message.photo and message.caption and message.caption.startswith("http") - else None - ) - if not text_message and message.caption: - text_message = message.caption - - if not text_message and not photo and not photo_url: - await message.answer("⚠ Ошибка! Отправьте текст или изображение для рассылки.") + max_len = 1024 if photo else 4096 + if len(text_message) > max_len: + await message.answer( + f"⚠️ Сообщение слишком длинное.\n" + f"Максимум: {max_len} символов, сейчас: {len(text_message)}.", + reply_markup=build_admin_back_kb("sender"), + ) + await state.clear() return - state_data = await state.get_data() - send_to = state_data.get("type", "all") + await state.update_data(text=text_message, photo=photo) + await state.set_state(AdminSender.preview) + + if photo: + await message.answer_photo(photo=photo, caption=text_message, parse_mode="HTML") + else: + await message.answer(text=text_message, parse_mode="HTML") + + await message.answer( + "👀 Это предпросмотр рассылки.\nОтправить?", + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [ + InlineKeyboardButton(text="📤 Отправить", callback_data="send_message"), + InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_message"), + ] + ]), + ) + + +@router.callback_query(F.data == "send_message", IsAdminFilter()) +async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession): + data = await state.get_data() + text_message = data.get("text") + photo = data.get("photo") + send_to = data.get("type", "all") + cluster_name = data.get("cluster_name") now_ms = int(datetime.utcnow().timestamp() * 1000) query = None - if send_to == "subscribed": query = select(distinct(User.tg_id)).join(Key).where(Key.expiry_time > now_ms) - elif send_to == "unsubscribed": subquery = ( select(User.tg_id) @@ -101,20 +129,16 @@ async def handle_message_input(message: Message, state: FSMContext, session): ) ) query = select(distinct(subquery.c.tg_id)) - elif send_to == "untrial": subquery = select(Key.tg_id) query = select(distinct(User.tg_id)).where(~User.tg_id.in_(subquery)) - elif send_to == "cluster": - cluster_name = state_data.get("cluster_name") query = ( select(distinct(User.tg_id)) .join(Key, User.tg_id == Key.tg_id) .join(Server, Key.server_id == Server.cluster_name) .where(Server.cluster_name == cluster_name) ) - elif send_to == "hotleads": subquery = select(Key.tg_id) query = ( @@ -123,7 +147,6 @@ async def handle_message_input(message: Message, state: FSMContext, session): .where(Payment.status == "success") .where(~User.tg_id.in_(subquery)) ) - else: query = select(distinct(User.tg_id)) @@ -133,27 +156,30 @@ async def handle_message_input(message: Message, state: FSMContext, session): total_users = len(tg_ids) success_count = 0 - await message.answer( + await callback_query.message.edit_text( f"📤 Рассылка начата!\n👥 Количество получателей: {total_users}" ) for tg_id in tg_ids: try: - if photo or photo_url: - await message.bot.send_photo( + if photo: + await callback_query.bot.send_photo( chat_id=tg_id, - photo=photo or photo_url, + photo=photo, caption=text_message, + parse_mode="HTML", ) else: - await message.bot.send_message( - chat_id=tg_id, text=text_message, parse_mode="HTML" + await callback_query.bot.send_message( + chat_id=tg_id, + text=text_message, + parse_mode="HTML", ) success_count += 1 except Exception as e: logger.error(f"❌ Ошибка отправки пользователю {tg_id}: {e}") - await message.answer( + await callback_query.message.answer( text=( f"📤 Рассылка завершена!\n\n" f"👥 Количество получателей: {total_users}\n" @@ -163,3 +189,12 @@ async def handle_message_input(message: Message, state: FSMContext, session): reply_markup=build_admin_back_kb("sender"), ) await state.clear() + + +@router.callback_query(F.data == "cancel_message", IsAdminFilter()) +async def handle_send_cancel(callback_query: CallbackQuery, state: FSMContext): + await callback_query.message.edit_text( + "🚫 Рассылка отменена.", + reply_markup=build_admin_back_kb("sender"), + ) + await state.clear() \ No newline at end of file diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 29c75197..4788dec4 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -8,7 +8,7 @@ from aiogram import F, Router, types from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, InlineKeyboardButton, Message +from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy import delete, func, or_, select, update from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -77,6 +77,7 @@ class UserEditorState(StatesGroup): waiting_for_balance = State() waiting_for_expiry_time = State() waiting_for_message_text = State() + preview_message = State() selecting_cluster = State() selecting_duration = State() selecting_country = State() @@ -315,7 +316,14 @@ async def handle_send_message( tg_id = callback_data.tg_id await callback_query.message.edit_text( - text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", + text=( + "✉️ Введите текст сообщения, которое вы хотите отправить пользователю:\n\n" + "Поддерживается только Telegram-форматирование — жирный, курсив и другие стили через редактор Telegram.\n\n" + "Вы можете отправить:\n" + "• Только текст\n" + "• Только картинку\n" + "• Текст + картинку" + ), reply_markup=build_editor_kb(tg_id), ) @@ -327,18 +335,76 @@ async def handle_send_message( async def handle_message_text_input(message: Message, state: FSMContext): data = await state.get_data() tg_id = data.get("tg_id") + text_message = message.html_text or message.text or message.caption or "" + photo = message.photo[-1].file_id if message.photo else None - try: - await message.bot.send_message(chat_id=tg_id, text=message.text) + max_len = 1024 if photo else 4096 + if len(text_message) > max_len: await message.answer( + f"⚠️ Сообщение слишком длинное.\n" + f"Максимум: {max_len} символов, сейчас: {len(text_message)}.", + reply_markup=build_editor_kb(tg_id), + ) + await state.clear() + return + + await state.update_data(text=text_message, photo=photo) + await state.set_state(UserEditorState.preview_message) + + if photo: + await message.answer_photo(photo=photo, caption=text_message, parse_mode="HTML") + else: + await message.answer(text=text_message, parse_mode="HTML") + + await message.answer( + "👀 Это предпросмотр сообщения. Отправить?", + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [ + InlineKeyboardButton(text="📤 Отправить", callback_data="send_user_message"), + InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_user_message"), + ] + ]), + ) + + +@router.callback_query(F.data == "send_user_message", IsAdminFilter(), UserEditorState.preview_message) +async def handle_send_user_message(callback_query: CallbackQuery, state: FSMContext): + data = await state.get_data() + tg_id = data.get("tg_id") + text_message = data.get("text") + photo = data.get("photo") + try: + if photo: + await callback_query.bot.send_photo( + chat_id=tg_id, + photo=photo, + caption=text_message, + parse_mode="HTML", + ) + else: + await callback_query.bot.send_message( + chat_id=tg_id, + text=text_message, + parse_mode="HTML", + ) + await callback_query.message.edit_text( text="✅ Сообщение успешно отправлено.", reply_markup=build_editor_kb(tg_id) ) except Exception as e: - await message.answer( + await callback_query.message.edit_text( text=f"❌ Не удалось отправить сообщение: {e}", reply_markup=build_editor_kb(tg_id), ) + await state.clear() + +@router.callback_query(F.data == "cancel_user_message", IsAdminFilter(), UserEditorState.preview_message) +async def handle_cancel_user_message(callback_query: CallbackQuery, state: FSMContext): + data = await state.get_data() + tg_id = data.get("tg_id") + await callback_query.message.edit_text( + text="🚫 Отправка сообщения отменена.", reply_markup=build_editor_kb(tg_id) + ) await state.clear() diff --git a/handlers/refferal.py b/handlers/refferal.py index 50bfac50..35d6a010 100644 --- a/handlers/refferal.py +++ b/handlers/refferal.py @@ -18,7 +18,7 @@ from sqlalchemy import desc, func, select from sqlalchemy.ext.asyncio import AsyncSession from bot import bot -from config import ADMIN_ID, INLINE_MODE, TOP_REFERRAL_BUTTON, TRIAL_CONFIG, USERNAME_BOT +from config import ADMIN_ID, INLINE_MODE, TOP_REFERRAL_BUTTON, TRIAL_CONFIG, USERNAME_BOT, REFERRAL_BONUS_PERCENTAGES from database import ( add_referral, add_user, @@ -34,10 +34,11 @@ from handlers.texts import ( REFERRAL_OFFERS, REFERRAL_SUCCESS_MSG, TOP_REFERRALS_TEXT, + INVITE_MESSAGE_TEMPLATE, ) from logger import logger -from .texts import get_referral_link, invite_message_send +from .texts import get_referral_link from .utils import edit_or_send_message, format_days router = Router() @@ -57,7 +58,32 @@ async def invite_handler( referral_link = get_referral_link(chat_id) referral_stats = await get_referral_stats(session, chat_id) - invite_message = invite_message_send(referral_link, referral_stats) + + bonuses_lines = [] + for level, value in REFERRAL_BONUS_PERCENTAGES.items(): + if isinstance(value, float): + bonuses_lines.append(f"{level} уровень: 🌟 {int(value * 100)}% бонуса") + else: + bonuses_lines.append(f"{level} уровень: 💸 {int(value)}₽ бонуса") + bonuses_block = "\n".join(bonuses_lines) + + details_lines = [] + for level, stats in referral_stats['referrals_by_level'].items(): + bonus_value = REFERRAL_BONUS_PERCENTAGES.get(level) + if isinstance(bonus_value, float): + bonus_str = f"{int(bonus_value * 100)}%" + else: + bonus_str = f"{int(bonus_value)}₽" + details_lines.append(f"🔹 Уровень {level}: {stats['total']} - {bonus_str}") + details_block = "\n".join(details_lines) + + invite_message = INVITE_MESSAGE_TEMPLATE.format( + referral_link=referral_link, + bonuses_block=bonuses_block, + total_referrals=referral_stats['total_referrals'], + details_block=details_block, + total_referral_bonus=referral_stats['total_referral_bonus'], + ) image_path = os.path.join("img", "pic_invite.jpg") builder = InlineKeyboardBuilder()