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()