diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..65e013c --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +BOT_TOKEN= +ADMINS= +DATABASE_URL= + diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 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/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index f8ae353..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/postgres - $ProjectFileDir$ - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/postgres - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 562afa4..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 3dec55b..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vol_bot.iml b/.idea/vol_bot.iml deleted file mode 100644 index 5cd92f3..0000000 --- a/.idea/vol_bot.iml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index ff8ab45..4898564 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,93 @@ -## Бот для Волонтерского Центра НИЯУ МИФИ +# Volunteer Center Bot (Волонтерский Центр НИЯУ МИФИ Бот) -- \ No newline at end of file +## Описание + +Этот проект представляет собой Telegram-бота, разработанного для Волонтерского Центра НИЯУ МИФИ. Бот предоставляет функционал для регистрации пользователей, управления рассылками и взаимодействия с администраторами. + +## Основной функционал: + +1. **Регистрация пользователей:** + * Позволяет новым пользователям зарегистрироваться в системе бота. + * Данные пользователей сохраняются в базе данных. + +2. **Подписка/Отписка от рассылки:** + * Пользователи могут подписываться на рассылку сообщений от администраторов. + * Пользователи могут отписываться от рассылки в любое время. + +3. **Рассылка сообщений:** + * Администраторы могут отправлять сообщения пользователям, подписанным на рассылку. + * Поддерживаются два формата сообщений: короткий и длинный. + +4. **Статистика рассылки:** + * Предоставляет администраторам информацию об объеме рассылки (например, количество получателей). + +## Технологии: + +* **Python:** Основной язык программирования. +* **aiogram:** Библиотека для создания Telegram-ботов. +* **SQLAlchemy:** ORM для работы с базой данных. +* **PostgreSQL:** Используется в качестве базы данных. +* **dotenv:** Для управления переменными окружения. +* **FSM (Finite State Machine):** Для управления состояниями бота. +* **MemoryStorage:** Хранилище состояний в оперативной памяти. + +## Структура проекта: + +* **`main.py`:** Основной файл, запускающий бота. +* **`handlers/`:** Директория, содержащая обработчики событий (хендлеры) для различных типов пользователей (обычные пользователи, администраторы) и callback-запросов. + * `user.py`: Обработчики для обычных пользователей. + * `admin.py`: Обработчики для администраторов. + * `admin_adv.py`: Обработчики для рассылки сообщений администраторами. + * `adv_callbacks.py`: Обработчики для callback-запросов, связанных с рассылкой. +* **`database/`:** Директория, содержащая код для работы с базой данных. + * `__init__.py`: Инициализация модуля. +* **`.env`:** Файл с переменными окружения (например, токен бота). +* **`README.md`:** Этот файл. + +## Установка и запуск: + +1. **Клонируйте репозиторий:** + ```bash + git clone https://github.com/ada-dmitry/vol_bot + cd vol_bot + ``` + +2. **Создайте виртуальное окружение (рекомендуется):** + ```bash + python3 -m venv venv + source venv/bin/activate # Linux/macOS + venv\Scripts\activate # Windows + ``` + +3. **Установите зависимости:** + ```bash + pip install -r requirements.txt + ``` + (Если у вас нет файла `requirements.txt`, то установите зависимости вручную: `pip install aiogram python-dotenv sqlalchemy`) + +4. **Создайте файл `.env`:** + * Скопируйте содержимое файла `.env.example` в новый файл `.env`. + * Замените `BOT_TOKEN` на токен вашего бота, полученный от BotFather. + +5. **Запустите бота:** + ```bash + python main.py + ``` + +## Разработка: + +* Для добавления нового функционала создавайте новые обработчики в соответствующих файлах директории `handlers/`. +* Для работы с базой данных используйте SQLAlchemy. +* Для управления состояниями бота используйте FSM. + +## Планы по развитию: + +* Добавление новых команд и функций. +* Улучшение интерфейса взаимодействия с пользователем. +* Расширение возможностей администраторов. +* Добавление логирования. +* Добавление тестов. + +## Контакты +Другие проекты: [GitHub](https://github.com/ada-dmitry) +По вопросам и предложениям обращайтесь: [TG - Дмитрий](https://t.me/ada_dmitry) diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..a082cdf Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index d6aff86..57063a4 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/bot.log b/bot.log new file mode 100644 index 0000000..073cda3 --- /dev/null +++ b/bot.log @@ -0,0 +1,51 @@ +2025-05-02 20:48:11,017 - INFO - __main__ - Запуск работы бота. +2025-05-02 20:48:11,070 - INFO - aiogram.dispatcher - Start polling +2025-05-02 20:48:11,331 - INFO - aiogram.dispatcher - Run polling for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 20:48:38,874 - WARNING - aiogram.dispatcher - Received SIGINT signal +2025-05-02 20:48:38,875 - INFO - aiogram.dispatcher - Polling stopped for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 20:48:38,875 - INFO - aiogram.dispatcher - Polling stopped +2025-05-02 20:52:05,968 - INFO - __main__ - Запуск работы бота. +2025-05-02 20:52:06,013 - INFO - aiogram.dispatcher - Start polling +2025-05-02 20:52:06,277 - INFO - aiogram.dispatcher - Run polling for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 20:52:13,305 - WARNING - aiogram.dispatcher - Received SIGINT signal +2025-05-02 20:52:13,306 - INFO - aiogram.dispatcher - Polling stopped for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 20:52:13,306 - INFO - aiogram.dispatcher - Polling stopped +2025-05-02 20:52:17,747 - INFO - __main__ - Запуск работы бота. +2025-05-02 20:52:17,780 - INFO - aiogram.dispatcher - Start polling +2025-05-02 20:52:18,048 - INFO - aiogram.dispatcher - Run polling for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 20:52:32,713 - INFO - aiogram.event - Update id=328315775 is handled. Duration 226 ms by bot id=7827792861 +2025-05-02 20:52:44,275 - INFO - aiogram.event - Update id=328315776 is not handled. Duration 1 ms by bot id=7827792861 +2025-05-02 20:53:04,829 - INFO - aiogram.event - Update id=328315777 is not handled. Duration 2 ms by bot id=7827792861 +2025-05-02 20:53:17,894 - WARNING - aiogram.dispatcher - Received SIGINT signal +2025-05-02 20:53:17,895 - INFO - aiogram.dispatcher - Polling stopped for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 20:53:17,896 - INFO - aiogram.dispatcher - Polling stopped +2025-05-02 20:55:10,923 - INFO - __main__ - Запуск работы бота. +2025-05-02 20:55:10,971 - INFO - aiogram.dispatcher - Start polling +2025-05-02 20:55:11,246 - INFO - aiogram.dispatcher - Run polling for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 20:55:19,000 - INFO - aiogram.event - Update id=328315778 is not handled. Duration 1 ms by bot id=7827792861 +2025-05-02 20:55:20,367 - INFO - main - User 451218808 started the bot +2025-05-02 20:55:20,838 - INFO - aiogram.event - Update id=328315779 is handled. Duration 472 ms by bot id=7827792861 +2025-05-02 21:00:28,136 - INFO - aiogram.event - Update id=328315780 is not handled. Duration 1 ms by bot id=7827792861 +2025-05-02 21:00:28,840 - INFO - main - User 451218808 started the bot +2025-05-02 21:00:28,963 - INFO - aiogram.event - Update id=328315781 is handled. Duration 123 ms by bot id=7827792861 +2025-05-02 21:00:31,620 - INFO - aiogram.event - Update id=328315782 is not handled. Duration 2 ms by bot id=7827792861 +2025-05-02 21:00:33,258 - INFO - aiogram.event - Update id=328315783 is not handled. Duration 1 ms by bot id=7827792861 +2025-05-02 21:00:33,973 - INFO - aiogram.event - Update id=328315784 is not handled. Duration 1 ms by bot id=7827792861 +2025-05-02 21:00:34,039 - INFO - aiogram.event - Update id=328315785 is not handled. Duration 1 ms by bot id=7827792861 +2025-05-02 21:00:34,178 - INFO - aiogram.event - Update id=328315786 is not handled. Duration 2 ms by bot id=7827792861 +2025-05-02 21:00:34,389 - INFO - aiogram.event - Update id=328315787 is not handled. Duration 1 ms by bot id=7827792861 +2025-05-02 21:08:22,430 - WARNING - aiogram.dispatcher - Received SIGINT signal +2025-05-02 21:08:22,431 - INFO - aiogram.dispatcher - Polling stopped for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 21:08:22,431 - INFO - aiogram.dispatcher - Polling stopped +2025-05-02 21:16:06,283 - INFO - __main__ - Запуск работы бота. +2025-05-02 21:16:06,343 - INFO - aiogram.dispatcher - Start polling +2025-05-02 21:16:06,584 - INFO - aiogram.dispatcher - Run polling for bot @volunteer_mephi_bot id=7827792861 - 'VC_Bot' +2025-05-02 21:16:23,053 - INFO - handlers.user - User 451218808 started the bot +2025-05-02 21:16:23,296 - INFO - aiogram.event - Update id=328315793 is handled. Duration 243 ms by bot id=7827792861 +2025-05-02 21:16:24,462 - INFO - aiogram.event - Update id=328315794 is handled. Duration 220 ms by bot id=7827792861 +2025-05-02 21:16:25,683 - INFO - aiogram.event - Update id=328315795 is handled. Duration 234 ms by bot id=7827792861 +2025-05-02 21:16:27,117 - INFO - aiogram.event - Update id=328315796 is handled. Duration 212 ms by bot id=7827792861 +2025-05-02 21:16:28,052 - INFO - aiogram.event - Update id=328315797 is handled. Duration 381 ms by bot id=7827792861 +2025-05-02 21:16:30,962 - INFO - aiogram.event - Update id=328315798 is handled. Duration 200 ms by bot id=7827792861 +2025-05-03 09:36:21,305 - INFO - __main__ - Запуск работы бота. +2025-05-03 09:37:58,037 - INFO - __main__ - Запуск работы бота. diff --git a/handlers/__pycache__/admin.cpython-313.pyc b/handlers/__pycache__/admin.cpython-313.pyc index 6c7decf..745c3af 100644 Binary files a/handlers/__pycache__/admin.cpython-313.pyc and b/handlers/__pycache__/admin.cpython-313.pyc differ diff --git a/handlers/__pycache__/admin_adv.cpython-313.pyc b/handlers/__pycache__/admin_adv.cpython-313.pyc new file mode 100644 index 0000000..7be8a80 Binary files /dev/null and b/handlers/__pycache__/admin_adv.cpython-313.pyc differ diff --git a/handlers/__pycache__/adv_callbacks.cpython-313.pyc b/handlers/__pycache__/adv_callbacks.cpython-313.pyc new file mode 100644 index 0000000..7da5577 Binary files /dev/null and b/handlers/__pycache__/adv_callbacks.cpython-313.pyc differ diff --git a/handlers/__pycache__/user.cpython-313.pyc b/handlers/__pycache__/user.cpython-313.pyc index f44555b..a64f805 100644 Binary files a/handlers/__pycache__/user.cpython-313.pyc and b/handlers/__pycache__/user.cpython-313.pyc differ diff --git a/handlers/admin.py b/handlers/admin.py index 4d60e51..4720299 100644 --- a/handlers/admin.py +++ b/handlers/admin.py @@ -5,24 +5,30 @@ 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()) + 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: @@ -48,4 +54,4 @@ async def broadcast_handler(message: Message, bot: Bot): except Exception: failed += 1 - await message.answer(f"📬 Рассылка завершена:\nУспешно: {sent}\nОшибок: {failed}") \ No newline at end of file + await message.answer(f"📬 Рассылка завершена:\nУспешно: {sent}\nОшибок: {failed}") diff --git a/handlers/admin_adv.py b/handlers/admin_adv.py index d695093..23f12c1 100644 --- a/handlers/admin_adv.py +++ b/handlers/admin_adv.py @@ -11,11 +11,10 @@ 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() @@ -44,18 +43,15 @@ async def get_full_text_and_send(message: Message, state: FSMContext, bot: Bot): # Генерируем уникальный 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}"), diff --git a/handlers/user.py b/handlers/user.py index 809840c..eac8f4e 100644 --- a/handlers/user.py +++ b/handlers/user.py @@ -3,35 +3,47 @@ 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 +import logging + from models import User from database import async_session from keyboards import get_subscription_keyboard +logger = logging.getLogger(__name__) 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, state: FSMContext): user_id = message.from_user.id - + logger.info(f"User {message.from_user.id} started the bot") 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 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)) + 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("👋 Привет! Пожалуйста, напиши свои ФИО одной строкой:") + await message.answer( + "👋 Привет! Пожалуйста, напиши свои ФИО одной строкой:" + ) + @router.message(Register.waiting_for_fullname) async def get_fullname(message: Message, state: FSMContext): @@ -39,6 +51,7 @@ async def get_fullname(message: Message, state: FSMContext): 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() @@ -46,14 +59,51 @@ async def get_study_group(message: Message, state: FSMContext): study_group = message.text async with async_session() as session: - 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.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 message.answer(f"✅ Регистрация завершена!\nФИО: {full_name}\nГруппа: {study_group}") - await message.answer("Теперь ты можешь управлять подпиской ниже 👇", reply_markup=get_subscription_keyboard(True)) + await message.answer( + f"✅ Регистрация завершена!\nФИО: {full_name}\nГруппа: {study_group}" + ) + await message.answer( + "Теперь ты можешь управлять подпиской ниже 👇", + reply_markup=get_subscription_keyboard(True), + ) await state.clear() + + +@router.callback_query(F.data == "subscribe") +async def subscribe(callback: CallbackQuery): + async with async_session() as session: + await session.execute( + update(User) + .where(User.user_id == callback.from_user.id) + .values(is_subscribed=True) + ) + await session.commit() + await callback.message.edit_reply_markup( + reply_markup=get_subscription_keyboard(True) + ) + await callback.answer("✅ Вы подписались на рассылку!") + + +@router.callback_query(F.data == "unsubscribe") +async def unsubscribe(callback: CallbackQuery): + async with async_session() as session: + await session.execute( + update(User) + .where(User.user_id == callback.from_user.id) + .values(is_subscribed=False) + ) + await session.commit() + await callback.message.edit_reply_markup( + reply_markup=get_subscription_keyboard(False) + ) + await callback.answer("❌ Вы отписались от рассылки.") diff --git a/main.py b/main.py index fb032ba..1f61f1a 100644 --- a/main.py +++ b/main.py @@ -4,30 +4,47 @@ from aiogram.fsm.storage.memory import MemoryStorage import asyncio import os from dotenv import load_dotenv +import logging + from handlers import user, admin, admin_adv, adv_callbacks from database import engine, Base + load_dotenv() +logging.basicConfig( + level=logging.INFO, # Уровень логирования (DEBUG, INFO, WARNING, ERROR, CRITICAL) + format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", + handlers=[ + logging.FileHandler("bot.log"), # Логирование в файл bot.log + # logging.StreamHandler(), # Логирование в консоль + ], +) +logger = logging.getLogger(__name__) async def on_startup(): + """ + Основная процедура для запуска работы с БД чреез SQLAlchemy. + """ 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")) + """ + Функция запуска бота, подключение к TG_API, создание диспетчера, подключение роутеров и запуск бота в режиме пуллинг. + """ + bot = Bot(token=os.getenv("BOT_TOKEN")) # type: ignore dp = Dispatcher(storage=MemoryStorage()) + logger.info("Запуск работы бота.") dp.include_routers( - user.router, - admin.router, - admin_adv.router, - adv_callbacks.router + user.router, admin.router, admin_adv.router, adv_callbacks.router ) await on_startup() - await dp.start_polling(bot) + await dp.start_polling(bot) # Бот в пуллинг режиме - "почтальон для API" + if __name__ == "__main__": asyncio.run(main()) - diff --git a/requirements.txt b/requirements.txt index c702118..8a97097 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ aiogram~=3.20.0 -dotenv~=0.9.9 python-dotenv~=1.1.0 sqlalchemy~=2.0.40 asyncpg~=0.30.0