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