diff --git a/.idea/vol_bot.iml b/.idea/vol_bot.iml index de6eff9..5cd92f3 100644 --- a/.idea/vol_bot.iml +++ b/.idea/vol_bot.iml @@ -2,6 +2,7 @@ + diff --git a/handlers/admin_adv.py b/handlers/admin_adv.py new file mode 100644 index 0000000..d695093 --- /dev/null +++ b/handlers/admin_adv.py @@ -0,0 +1,80 @@ +from aiogram import Router, F, Bot +from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import StatesGroup, State +from sqlalchemy import select +from database import async_session +from models import User +from uuid import uuid4 +import os + +router = Router() +admin_ids = list(map(int, os.getenv("ADMINS", "").split(","))) + +# Храним рассылки +adv_memory = {} # {adv_id: {"text": str, "is_active": bool}} +current_adv_id = None + +# Состояния для FSM +class AdvForm(StatesGroup): + short = State() + full = State() + +@router.message(F.text == "/send_adv") +async def start_adv(message: Message, state: FSMContext): + if message.from_user.id not in admin_ids: + return await message.answer("⛔ Доступ запрещён.") + await state.set_state(AdvForm.short) + await message.answer("Введите краткую форму сообщения:") + +@router.message(AdvForm.short) +async def get_short_text(message: Message, state: FSMContext): + await state.update_data(short=message.text) + await state.set_state(AdvForm.full) + await message.answer("Теперь введите полную форму сообщения:") + +@router.message(AdvForm.full) +async def get_full_text_and_send(message: Message, state: FSMContext, bot: Bot): + global current_adv_id, adv_memory + + data = await state.get_data() + short_text = data["short"] + full_text = message.text + + # Генерируем уникальный ID рассылки + adv_id = str(uuid4()) + + # Деактивируем старые рассылки + for adv in adv_memory.values(): + adv["is_active"] = False + + # Сохраняем новую рассылку + adv_memory[adv_id] = { + "text": full_text, + "is_active": True + } + current_adv_id = adv_id + + # Клавиатура + kb = InlineKeyboardMarkup(inline_keyboard=[ + [ + InlineKeyboardButton(text="👍 Интересно", callback_data=f"adv:{adv_id}"), + InlineKeyboardButton(text="👎 Не интересно", callback_data="adv:no") + ] + ]) + + # Рассылка краткой формы + async with async_session() as session: + result = await session.execute(select(User.user_id).where(User.is_subscribed == True)) + users = result.scalars().all() + + sent = 0 + for uid in users: + try: + await bot.send_message(uid, short_text, reply_markup=kb) + sent += 1 + except: + continue + + await message.answer(f"📬 Рассылка завершена. Отправлено: {sent}") + await state.clear() diff --git a/handlers/adv_callbacks.py b/handlers/adv_callbacks.py new file mode 100644 index 0000000..c7c92ea --- /dev/null +++ b/handlers/adv_callbacks.py @@ -0,0 +1,25 @@ +from aiogram import Router, F +from aiogram.types import CallbackQuery +from handlers.admin_adv import adv_memory + +router = Router() + +@router.callback_query(F.data.startswith("adv:")) +async def handle_adv_click(callback: CallbackQuery): + adv_id = callback.data.split(":")[1] + + adv = adv_memory.get(adv_id) + if not adv: + await callback.answer("❌ Сообщение не найдено", show_alert=True) + return + + if not adv["is_active"]: + await callback.answer("⏰ Срок действия сообщения истёк", show_alert=True) + return + + await callback.message.answer(adv["text"]) + await callback.answer("Вот подробности 👇") + +@router.callback_query(F.data == "adv:no") +async def handle_not_interested(callback: CallbackQuery): + await callback.answer("Спасибо за обратную связь!") diff --git a/handlers/user.py b/handlers/user.py index 8907bea..809840c 100644 --- a/handlers/user.py +++ b/handlers/user.py @@ -1,5 +1,7 @@ from aiogram import Router, F +from aiogram.fsm.context import FSMContext from aiogram.types import Message, CallbackQuery +from aiogram.fsm.state import State, StatesGroup from sqlalchemy import select, insert, update from models import User from database import async_session @@ -7,27 +9,51 @@ from keyboards import get_subscription_keyboard router = Router() +# Машина состояний +class Register(StatesGroup): + waiting_for_fullname = State() + waiting_for_study_group = State() + @router.message(F.text == "/start") -async def start_handler(message: Message): - async with async_session() as session: +async def start_handler(message: Message, state: FSMContext): + user_id = message.from_user.id - result = await session.execute(select(User).where(User.user_id == message.from_user.id)) + async with async_session() as session: + result = await session.execute(select(User).where(User.user_id == user_id)) user = result.scalar_one_or_none() - if not user: - await session.execute(insert(User).values(user_id=message.from_user.id)) - await session.commit() - text = "Добро пожаловать! Управляйте подпиской:" - await message.answer(text, reply_markup=get_subscription_keyboard(user.is_subscribed if user else True)) -@router.callback_query(F.data.in_(["subscribe", "unsubscribe"])) -async def toggle_subscription(callback: CallbackQuery): - subscribe = callback.data == "subscribe" + if user: + # Приветствие при повторном запуске + text = f"👋 Привет, {user.full_name or 'друг'}!\n" \ + f"Группа: {user.study_group or 'не указана'}\n" \ + "Используй кнопки ниже, чтобы управлять подпиской:" + await message.answer(text, reply_markup=get_subscription_keyboard(user.is_subscribed)) + else: + # Первый запуск — регистрация + await state.set_state(Register.waiting_for_fullname) + await message.answer("👋 Привет! Пожалуйста, напиши свои ФИО одной строкой:") + +@router.message(Register.waiting_for_fullname) +async def get_fullname(message: Message, state: FSMContext): + await state.update_data(full_name=message.text) + await state.set_state(Register.waiting_for_study_group) + await message.answer("Теперь укажи номер своей группы (например, Б11-111):") + +@router.message(Register.waiting_for_study_group) +async def get_study_group(message: Message, state: FSMContext): + data = await state.get_data() + full_name = data["full_name"] + study_group = message.text + async with async_session() as session: - await session.execute( - update(User) - .where(User.user_id == callback.from_user.id) - .values(is_subscribed=subscribe) - ) + await session.execute(insert(User).values( + user_id=message.from_user.id, + full_name=full_name, + study_group=study_group, + is_subscribed=True + )) await session.commit() - await callback.message.edit_reply_markup(reply_markup=get_subscription_keyboard(subscribe)) - await callback.answer("Вы подписались!" if subscribe else "Вы отписались.") + + await message.answer(f"✅ Регистрация завершена!\nФИО: {full_name}\nГруппа: {study_group}") + await message.answer("Теперь ты можешь управлять подпиской ниже 👇", reply_markup=get_subscription_keyboard(True)) + await state.clear() diff --git a/main.py b/main.py index 6ed69d7..fb032ba 100644 --- a/main.py +++ b/main.py @@ -1,23 +1,33 @@ from aiogram import Bot, Dispatcher from aiogram.fsm.storage.memory import MemoryStorage -from handlers import user, admin -from database import engine, Base + import asyncio import os from dotenv import load_dotenv +from handlers import user, admin, admin_adv, adv_callbacks +from database import engine, Base load_dotenv() + + async def on_startup(): async with engine.begin() as conn: + # await conn.run_sync(Base.metadata.drop_all) # Сброс БД await conn.run_sync(Base.metadata.create_all) async def main(): bot = Bot(token=os.getenv("BOT_TOKEN")) dp = Dispatcher(storage=MemoryStorage()) - dp.include_routers(user.router, admin.router) + dp.include_routers( + user.router, + admin.router, + admin_adv.router, + adv_callbacks.router + ) await on_startup() await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main()) + diff --git a/models.py b/models.py index 5841470..fb4c201 100644 --- a/models.py +++ b/models.py @@ -1,8 +1,10 @@ -from sqlalchemy import Column, BigInteger, Boolean +from sqlalchemy import Column, BigInteger, Boolean, String from database import Base class User(Base): __tablename__ = "users" user_id = Column(BigInteger, primary_key=True) + full_name = Column(String, nullable=False) + study_group = Column(String, nullable=False) is_subscribed = Column(Boolean, default=True)