feat: add broadcast preview with confirmation / improve referral bonuses (percent and fixed RUB) / unify personal message formatting

This commit is contained in:
Capybara-z
2025-06-19 00:11:35 +03:00
parent d532f1211c
commit 0abb83aae1
3 changed files with 165 additions and 38 deletions
+65 -30
View File
@@ -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-форматирование — <b>жирный</b>, <i>курсив</i> и другие стили через редактор Telegram.\n\n"
"Вы можете отправить:\n"
"• Только <b>текст</b>\n"
"• Только <b>картинку</b>\n"
"• <b>Текст + картинку</b>"
),
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"Максимум: <b>{max_len}</b> символов, сейчас: <b>{len(text_message)}</b>.",
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"📤 <b>Рассылка начата!</b>\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"📤 <b>Рассылка завершена!</b>\n\n"
f"👥 <b>Количество получателей:</b> {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()