Добавление логгирования

This commit is contained in:
ada-dmitry
2025-05-03 09:42:50 +03:00
parent e1063b669f
commit ec2c0c7dfc
21 changed files with 244 additions and 99 deletions
+4
View File
@@ -0,0 +1,4 @@
BOT_TOKEN=
ADMINS=
DATABASE_URL=
-8
View File
@@ -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
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgres@localhost" uuid="64a7c47f-1c6d-4f9b-bb6d-5b6fe66fba41">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="vc_database" uuid="bdaa7723-af4b-4080-b3fe-20d0cf595d3c">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (vol_bot)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (vc_bot)" project-jdk-type="Python SDK" />
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/vol_bot.iml" filepath="$PROJECT_DIR$/.idea/vol_bot.iml" />
</modules>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (vc_bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings">
<option name="keepMatchingSpecifier" value="false" />
</component>
</module>
+92 -2
View File
@@ -1,3 +1,93 @@
## Бот для Волонтерского Центра НИЯУ МИФИ # Volunteer Center Bot (Волонтерский Центр НИЯУ МИФИ Бот)
- ## Описание
Этот проект представляет собой 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)
Binary file not shown.
Binary file not shown.
+51
View File
@@ -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__ - Запуск работы бота.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7 -1
View File
@@ -5,24 +5,30 @@ from models import User
from database import async_session from database import async_session
import os import os
router = Router() router = Router()
admin_ids = list(map(int, os.getenv("ADMINS").split(","))) admin_ids = list(map(int, os.getenv("ADMINS").split(",")))
@router.message(F.text == "/admin") @router.message(F.text == "/admin")
async def admin_panel(message: Message): async def admin_panel(message: Message):
if message.from_user.id not in admin_ids: if message.from_user.id not in admin_ids:
return await message.answer("Доступ запрещён.") return await message.answer("Доступ запрещён.")
await message.answer("Команды:\n/stat — статистика\n/broadcast <текст> — рассылка") await message.answer("Команды:\n/stat — статистика\n/broadcast <текст> — рассылка")
@router.message(F.text.startswith("/stat")) @router.message(F.text.startswith("/stat"))
async def stat_handler(message: Message): async def stat_handler(message: Message):
if message.from_user.id not in admin_ids: if message.from_user.id not in admin_ids:
return return
async with async_session() as session: async with async_session() as session:
total = await session.scalar(select(User).count()) 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}") await message.answer(f"Всего пользователей: {total}\nПодписаны: {subscribed}")
@router.message(F.text.startswith("/broadcast ")) @router.message(F.text.startswith("/broadcast "))
async def broadcast_handler(message: Message, bot: Bot): async def broadcast_handler(message: Message, bot: Bot):
if message.from_user.id not in admin_ids: if message.from_user.id not in admin_ids:
+1 -5
View File
@@ -11,11 +11,10 @@ import os
router = Router() router = Router()
admin_ids = list(map(int, os.getenv("ADMINS", "").split(","))) admin_ids = list(map(int, os.getenv("ADMINS", "").split(",")))
# Храним рассылки
adv_memory = {} # {adv_id: {"text": str, "is_active": bool}} adv_memory = {} # {adv_id: {"text": str, "is_active": bool}}
current_adv_id = None current_adv_id = None
# Состояния для FSM
class AdvForm(StatesGroup): class AdvForm(StatesGroup):
short = State() short = State()
full = State() full = State()
@@ -44,18 +43,15 @@ async def get_full_text_and_send(message: Message, state: FSMContext, bot: Bot):
# Генерируем уникальный ID рассылки # Генерируем уникальный ID рассылки
adv_id = str(uuid4()) adv_id = str(uuid4())
# Деактивируем старые рассылки
for adv in adv_memory.values(): for adv in adv_memory.values():
adv["is_active"] = False adv["is_active"] = False
# Сохраняем новую рассылку
adv_memory[adv_id] = { adv_memory[adv_id] = {
"text": full_text, "text": full_text,
"is_active": True "is_active": True
} }
current_adv_id = adv_id current_adv_id = adv_id
# Клавиатура
kb = InlineKeyboardMarkup(inline_keyboard=[ kb = InlineKeyboardMarkup(inline_keyboard=[
[ [
InlineKeyboardButton(text="👍 Интересно", callback_data=f"adv:{adv_id}"), InlineKeyboardButton(text="👍 Интересно", callback_data=f"adv:{adv_id}"),
+64 -14
View File
@@ -3,35 +3,47 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
from sqlalchemy import select, insert, update from sqlalchemy import select, insert, update
import logging
from models import User from models import User
from database import async_session from database import async_session
from keyboards import get_subscription_keyboard from keyboards import get_subscription_keyboard
logger = logging.getLogger(__name__)
router = Router() router = Router()
# Машина состояний # Машина состояний
class Register(StatesGroup): class Register(StatesGroup):
waiting_for_fullname = State() waiting_for_fullname = State()
waiting_for_study_group = State() waiting_for_study_group = State()
@router.message(F.text == "/start") @router.message(F.text == "/start")
async def start_handler(message: Message, state: FSMContext): async def start_handler(message: Message, state: FSMContext):
user_id = message.from_user.id user_id = message.from_user.id
logger.info(f"User {message.from_user.id} started the bot")
async with async_session() as session: async with async_session() as session:
result = await session.execute(select(User).where(User.user_id == user_id)) result = await session.execute(select(User).where(User.user_id == user_id))
user = result.scalar_one_or_none() user = result.scalar_one_or_none()
if user: if user:
# Приветствие при повторном запуске # Приветствие при повторном запуске
text = f"👋 Привет, {user.full_name or 'друг'}!\n" \ text = (
f"Группа: {user.study_group or 'не указана'}\n" \ f"👋 Привет, {user.full_name or 'друг'}!\n"
"Используй кнопки ниже, чтобы управлять подпиской:" f"Группа: {user.study_group or 'не указана'}\n"
await message.answer(text, reply_markup=get_subscription_keyboard(user.is_subscribed)) "Используй кнопки ниже, чтобы управлять подпиской:"
)
await message.answer(
text, reply_markup=get_subscription_keyboard(user.is_subscribed)
)
else: else:
# Первый запуск — регистрация # Первый запуск — регистрация
await state.set_state(Register.waiting_for_fullname) await state.set_state(Register.waiting_for_fullname)
await message.answer("👋 Привет! Пожалуйста, напиши свои ФИО одной строкой:") await message.answer(
"👋 Привет! Пожалуйста, напиши свои ФИО одной строкой:"
)
@router.message(Register.waiting_for_fullname) @router.message(Register.waiting_for_fullname)
async def get_fullname(message: Message, state: FSMContext): 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 state.set_state(Register.waiting_for_study_group)
await message.answer("Теперь укажи номер своей группы (например, Б11-111):") await message.answer("Теперь укажи номер своей группы (например, Б11-111):")
@router.message(Register.waiting_for_study_group) @router.message(Register.waiting_for_study_group)
async def get_study_group(message: Message, state: FSMContext): async def get_study_group(message: Message, state: FSMContext):
data = await state.get_data() data = await state.get_data()
@@ -46,14 +59,51 @@ async def get_study_group(message: Message, state: FSMContext):
study_group = message.text study_group = message.text
async with async_session() as session: async with async_session() as session:
await session.execute(insert(User).values( await session.execute(
user_id=message.from_user.id, insert(User).values(
full_name=full_name, user_id=message.from_user.id,
study_group=study_group, full_name=full_name,
is_subscribed=True study_group=study_group,
)) is_subscribed=True,
)
)
await session.commit() await session.commit()
await message.answer(f"✅ Регистрация завершена!\nФИО: {full_name}\nГруппа: {study_group}") await message.answer(
await message.answer("Теперь ты можешь управлять подпиской ниже 👇", reply_markup=get_subscription_keyboard(True)) f"✅ Регистрация завершена!\nФИО: {full_name}\nГруппа: {study_group}"
)
await message.answer(
"Теперь ты можешь управлять подпиской ниже 👇",
reply_markup=get_subscription_keyboard(True),
)
await state.clear() 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("❌ Вы отписались от рассылки.")
+24 -7
View File
@@ -4,30 +4,47 @@ from aiogram.fsm.storage.memory import MemoryStorage
import asyncio import asyncio
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
import logging
from handlers import user, admin, admin_adv, adv_callbacks from handlers import user, admin, admin_adv, adv_callbacks
from database import engine, Base from database import engine, Base
load_dotenv() 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(): async def on_startup():
"""
Основная процедура для запуска работы с БД чреез SQLAlchemy.
"""
async with engine.begin() as conn: async with engine.begin() as conn:
# await conn.run_sync(Base.metadata.drop_all) # Сброс БД # await conn.run_sync(Base.metadata.drop_all) # Сброс БД
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
async def main(): 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()) dp = Dispatcher(storage=MemoryStorage())
logger.info("Запуск работы бота.")
dp.include_routers( dp.include_routers(
user.router, user.router, admin.router, admin_adv.router, adv_callbacks.router
admin.router,
admin_adv.router,
adv_callbacks.router
) )
await on_startup() await on_startup()
await dp.start_polling(bot) await dp.start_polling(bot) # Бот в пуллинг режиме - "почтальон для API"
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
-1
View File
@@ -1,5 +1,4 @@
aiogram~=3.20.0 aiogram~=3.20.0
dotenv~=0.9.9
python-dotenv~=1.1.0 python-dotenv~=1.1.0
sqlalchemy~=2.0.40 sqlalchemy~=2.0.40
asyncpg~=0.30.0 asyncpg~=0.30.0