commit c0fe2af364bb06f67888e937c9b5993a810b04f5 Author: ada-dmitry Date: Tue Apr 15 15:35:05 2025 +0300 init project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c66ba3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +.venv \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2fe8f8c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..3dec55b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vol_bot.iml b/.idea/vol_bot.iml new file mode 100644 index 0000000..0c5b3a3 --- /dev/null +++ b/.idea/vol_bot.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff8ab45 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## Бот для Волонтерского Центра НИЯУ МИФИ + +- \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..41701ad --- /dev/null +++ b/database.py @@ -0,0 +1,11 @@ +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker +from sqlalchemy.orm import declarative_base +import os +from dotenv import load_dotenv + +load_dotenv() + +Base = declarative_base() +engine = create_async_engine(os.getenv("DATABASE_URL")) +async_session = async_sessionmaker(engine, expire_on_commit=False) + diff --git a/handlers/admin.py b/handlers/admin.py new file mode 100644 index 0000000..4d60e51 --- /dev/null +++ b/handlers/admin.py @@ -0,0 +1,51 @@ +from aiogram import Router, F, Bot +from aiogram.types import Message +from sqlalchemy import select +from models import User +from database import async_session +import os + +router = Router() +admin_ids = list(map(int, os.getenv("ADMINS").split(","))) + +@router.message(F.text == "/admin") +async def admin_panel(message: Message): + if message.from_user.id not in admin_ids: + return await message.answer("Доступ запрещён.") + await message.answer("Команды:\n/stat — статистика\n/broadcast <текст> — рассылка") + +@router.message(F.text.startswith("/stat")) +async def stat_handler(message: Message): + if message.from_user.id not in admin_ids: + return + async with async_session() as session: + total = await session.scalar(select(User).count()) + subscribed = await session.scalar(select(User).where(User.is_subscribed).count()) + await message.answer(f"Всего пользователей: {total}\nПодписаны: {subscribed}") + +@router.message(F.text.startswith("/broadcast ")) +async def broadcast_handler(message: Message, bot: Bot): + if message.from_user.id not in admin_ids: + await message.answer("Доступ запрещён.") + return + + text = message.text.removeprefix("/broadcast ").strip() + if not text: + await message.answer("Текст рассылки не найден.") + return + + sent = 0 + failed = 0 + + async with async_session() as session: + result = await session.execute(select(User.user_id).where(User.is_subscribed)) + users = result.scalars().all() + + for uid in users: + try: + await bot.send_message(uid, text) + sent += 1 + except Exception: + failed += 1 + + await message.answer(f"📬 Рассылка завершена:\nУспешно: {sent}\nОшибок: {failed}") \ No newline at end of file diff --git a/handlers/user.py b/handlers/user.py new file mode 100644 index 0000000..8907bea --- /dev/null +++ b/handlers/user.py @@ -0,0 +1,33 @@ +from aiogram import Router, F +from aiogram.types import Message, CallbackQuery +from sqlalchemy import select, insert, update +from models import User +from database import async_session +from keyboards import get_subscription_keyboard + +router = Router() + +@router.message(F.text == "/start") +async def start_handler(message: Message): + async with async_session() as session: + + result = await session.execute(select(User).where(User.user_id == message.from_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" + 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.commit() + await callback.message.edit_reply_markup(reply_markup=get_subscription_keyboard(subscribe)) + await callback.answer("Вы подписались!" if subscribe else "Вы отписались.") diff --git a/keyboards.py b/keyboards.py new file mode 100644 index 0000000..ff214e7 --- /dev/null +++ b/keyboards.py @@ -0,0 +1,11 @@ +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton + +def get_subscription_keyboard(subscribed: bool): + if subscribed: + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="❌ Отписаться", callback_data="unsubscribe")] + ]) + else: + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="✅ Подписаться", callback_data="subscribe")] + ]) diff --git a/main.py b/main.py new file mode 100644 index 0000000..6ed69d7 --- /dev/null +++ b/main.py @@ -0,0 +1,23 @@ +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 + +load_dotenv() + +async def on_startup(): + async with engine.begin() as conn: + 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) + await on_startup() + await dp.start_polling(bot) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/models.py b/models.py new file mode 100644 index 0000000..5841470 --- /dev/null +++ b/models.py @@ -0,0 +1,8 @@ +from sqlalchemy import Column, BigInteger, Boolean +from database import Base + +class User(Base): + __tablename__ = "users" + + user_id = Column(BigInteger, primary_key=True) + is_subscribed = Column(Boolean, default=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6483022 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +aiogram~=3.19.0 +SQLAlchemy~=2.0.40 +# dotenv~=0.9.9 +python-dotenv~=1.1.0