WIP: Temporary commit for branch move

This commit is contained in:
2025-08-26 02:14:11 +03:00
parent ee9eafa09f
commit 7b6abe2a0e
44 changed files with 4783 additions and 2383 deletions

34
.gitignore vendored
View File

@@ -2,3 +2,37 @@
/settings.ini /settings.ini
/myenv/ /myenv/
/venv/ /venv/
/.idea/
/logs/*.log
# Testing and coverage files
.coverage
coverage.xml
htmlcov/
.pytest_cache/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
# Test database files
database/test.db
test.db
*.db
# IDE and editor files
.vscode/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

73
Makefile Normal file
View File

@@ -0,0 +1,73 @@
.PHONY: help test test-db test-coverage test-html clean install
# Default target
help:
@echo "Available commands:"
@echo " install - Install dependencies"
@echo " test - Run all tests"
@echo " test-db - Run database tests only"
@echo " test-bot - Run bot startup and handler tests only"
@echo " test-media - Run media handler tests only"
@echo " test-errors - Run error handling tests only"
@echo " test-utils - Run utility functions tests only"
@echo " test-keyboards - Run keyboard and filter tests only"
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
@echo " test-html - Run tests and generate HTML coverage report"
@echo " clean - Clean up generated files"
@echo " coverage - Show coverage report only"
# Install dependencies
install:
python3 -m pip install -r requirements.txt
python3 -m pip install pytest-cov
# Run all tests
test:
python3 -m pytest tests/ -v
# Run database tests only
test-db:
python3 -m pytest tests/test_db.py -v
# Run bot tests only
test-bot:
python3 -m pytest tests/test_bot.py -v
# Run media handler tests only
test-media:
python3 -m pytest tests/test_media_handlers.py -v
# Run error handling tests only
test-errors:
python3 -m pytest tests/test_error_handling.py -v
# Run utils tests only
test-utils:
python3 -m pytest tests/test_utils.py -v
# Run keyboard and filter tests only
test-keyboards:
python3 -m pytest tests/test_keyboards_and_filters.py -v
# Run tests with coverage
test-coverage:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
# Run tests and generate HTML coverage report
test-html:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=html:htmlcov --cov-report=term
@echo "HTML coverage report generated in htmlcov/index.html"
# Show coverage report only
coverage:
python3 -m coverage report --include="helper_bot/*,database/*"
# Clean up generated files
clean:
rm -rf htmlcov/
rm -f coverage.xml
rm -f .coverage
rm -f database/test.db
rm -f test.db
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete

259
README_TESTING.md Normal file
View File

@@ -0,0 +1,259 @@
# Тестирование Telegram Helper Bot
Этот документ описывает систему тестирования для Telegram Helper Bot.
## Структура тестов
Тесты организованы в следующие файлы:
- `tests/test_bot.py` - Основные тесты бота (запуск, хэндлеры, интеграция)
- `tests/test_media_handlers.py` - Тесты обработки медиа-контента
- `tests/test_error_handling.py` - Тесты обработки ошибок и граничных случаев
- `tests/test_utils.py` - Тесты утилит и вспомогательных функций
- `tests/test_keyboards_and_filters.py` - Тесты клавиатур и фильтров
- `tests/test_db.py` - Тесты базы данных
- `tests/conftest.py` - Общие фикстуры и конфигурация
## Установка зависимостей
```bash
make install
```
## Запуск тестов
### Все тесты
```bash
make test
```
### Отдельные категории тестов
```bash
# Тесты базы данных
make test-db
# Тесты бота (запуск и хэндлеры)
make test-bot
# Тесты обработки медиа
make test-media
# Тесты обработки ошибок
make test-errors
# Тесты утилит
make test-utils
# Тесты клавиатур и фильтров
make test-keyboards
```
### Тесты с покрытием
```bash
# Покрытие с выводом в терминал
make test-coverage
# Покрытие с HTML отчетом
make test-html
```
### Фильтрация тестов
```bash
# Только unit тесты
pytest -m unit
# Только интеграционные тесты
pytest -m integration
# Только асинхронные тесты
pytest -m asyncio
# Исключить медленные тесты
pytest -m "not slow"
# Конкретный файл тестов
pytest tests/test_bot.py
# Конкретный тест
pytest tests/test_bot.py::TestBotStartup::test_bot_initialization
```
## Типы тестов
### Unit тесты
Тестируют отдельные функции и компоненты в изоляции:
- Вспомогательные функции (`get_first_name`, `get_text_message`)
- Утилиты (`BaseDependencyFactory`, `get_message`)
- Фильтры (`ChatTypeFilter`)
- Клавиатуры
### Интеграционные тесты
Тестируют взаимодействие между компонентами:
- Регистрация роутеров в диспетчере
- Обработка сообщений через хэндлеры
- Интеграция с базой данных
### Асинхронные тесты
Тестируют асинхронные функции:
- Хэндлеры сообщений
- Запуск бота
- Обработка медиа-контента
## Моки и фикстуры
### Основные фикстуры
- `mock_message` - Мок сообщения Telegram
- `mock_state` - Мок состояния FSM
- `mock_db` - Мок базы данных
- `mock_bot` - Мок бота
- `mock_dispatcher` - Мок диспетчера
- `mock_factory` - Мок фабрики зависимостей
### Специализированные фикстуры
- `sample_photo_message` - Сообщение с фото
- `sample_video_message` - Сообщение с видео
- `sample_audio_message` - Сообщение с аудио
- `sample_voice_message` - Голосовое сообщение
- `sample_video_note_message` - Видеокружок
- `sample_media_group` - Медиагруппа
- `sample_text_message` - Текстовое сообщение
## Покрытие тестами
### Основные компоненты
- ✅ Запуск бота (`start_bot`)
- ✅ Приватные хэндлеры (`handle_start_message`, `suggest_post`, etc.)
- ✅ Обработка медиа-контента (фото, видео, аудио, голос)
- ✅ Обработка ошибок и исключений
- ✅ Утилиты и вспомогательные функции
- ✅ Клавиатуры и фильтры
- ✅ Фабрика зависимостей
### Тестируемые сценарии
- ✅ Новые пользователи
- ✅ Существующие пользователи
- ✅ Пользователи без username
- ✅ Обработка различных типов контента
- ✅ Медиагруппы
- ✅ Ошибки при получении стикеров
- ✅ Ошибки базы данных
- ✅ Граничные случаи (пустой текст, отсутствие подписей)
## Настройка окружения
### Переменные окружения
Для тестов не требуются реальные токены бота или подключения к базе данных, так как все внешние зависимости замоканы.
### Конфигурация pytest
Настройки pytest находятся в файле `pytest.ini`:
- Автоматический режим asyncio
- Фильтрация предупреждений
- Маркеры для категоризации тестов
## Добавление новых тестов
### Структура теста
```python
@pytest.mark.asyncio
async def test_function_name(mock_message, mock_state, mock_db):
"""Описание теста"""
# Arrange (подготовка)
mock_message.text = "test"
# Act (действие)
result = await function_to_test(mock_message, mock_state)
# Assert (проверка)
assert result is True
mock_message.answer.assert_called_once()
```
### Маркировка тестов
```python
@pytest.mark.unit # Unit тест
@pytest.mark.integration # Интеграционный тест
@pytest.mark.asyncio # Асинхронный тест
@pytest.mark.slow # Медленный тест
```
### Использование фикстур
```python
def test_with_fixtures(mock_message, sample_photo_message, mock_db):
# Используем готовые фикстуры
pass
```
## Отладка тестов
### Подробный вывод
```bash
pytest -v -s
```
### Остановка на первой ошибке
```bash
pytest -x
```
### Вывод полного traceback
```bash
pytest --tb=long
```
### Запуск конкретного теста
```bash
pytest tests/test_bot.py::TestPrivateHandlers::test_handle_start_message_new_user -v
```
## CI/CD интеграция
Тесты могут быть интегрированы в CI/CD pipeline:
```yaml
# Пример для GitHub Actions
- name: Run tests
run: |
make install
make test-coverage
```
## Покрытие кода
Для просмотра покрытия кода:
```bash
make test-html
# Открыть htmlcov/index.html в браузере
```
## Лучшие практики
1. **Изоляция тестов** - каждый тест должен быть независимым
2. **Использование моков** - избегайте реальных внешних зависимостей
3. **Описательные имена** - имена тестов должны описывать что тестируется
4. **Arrange-Act-Assert** - структурируйте тесты по этому паттерну
5. **Фикстуры** - используйте фикстуры для переиспользования кода
6. **Маркировка** - правильно маркируйте тесты для фильтрации
## Устранение неполадок
### Ошибки импорта
Убедитесь, что Python path настроен правильно:
```bash
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
```
### Ошибки asyncio
Для асинхронных тестов используйте маркер `@pytest.mark.asyncio`
### Ошибки моков
Проверьте, что все внешние зависимости замоканы:
```python
with patch('module.function') as mock_func:
# тест
```
### Медленные тесты
Используйте маркер `@pytest.mark.slow` для медленных тестов и исключайте их при необходимости:
```bash
pytest -m "not slow"
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,179 +1,304 @@
import traceback import traceback
from aiogram import Router, types, F from aiogram import Router, types, F
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from helper_bot.filters.main import ChatTypeFilter from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin, create_keyboard_with_pagination, \ from helper_bot.keyboards.keyboards import get_reply_keyboard_admin, create_keyboard_with_pagination, \
create_keyboard_for_ban_days, create_keyboard_for_approve_ban, create_keyboard_for_ban_reason create_keyboard_for_ban_days, create_keyboard_for_approve_ban, create_keyboard_for_ban_reason
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import check_access, add_days_to_date, get_banned_users_buttons, get_banned_users_list from helper_bot.utils.helper_func import check_access, add_days_to_date, get_banned_users_buttons, get_banned_users_list
from logs.custom_logger import logger from logs.custom_logger import logger
admin_router = Router() admin_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs'] GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs'] IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link'] PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs'] LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test'] TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db() BotDB = bdf.get_db()
@admin_router.message( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
Command('admin') Command('admin')
) )
async def admin_panel(message: types.Message, state: FSMContext): async def admin_panel(message: types.Message, state: FSMContext):
try: try:
if check_access(message.from_user.id): if check_access(message.from_user.id, BotDB):
await state.set_state("ADMIN") await state.set_state("ADMIN")
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}") logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
markup = get_reply_keyboard_admin() markup = get_reply_keyboard_admin()
await message.answer("Добро пожаловать в админку. Выбери что хочешь:", await message.answer("Добро пожаловать в админку. Выбери что хочешь:",
reply_markup=markup) reply_markup=markup)
else: else:
await message.answer('Доступ запрещен, досвидания!') await message.answer('Доступ запрещен, досвидания!')
except Exception as e: except Exception as e:
logger.error(f"Ошибка при запуске админ панели: {e}") logger.error(f"Ошибка при запуске админ панели: {e}")
await message.bot.send_message(IMPORTANT_LOGS, await message.bot.send_message(IMPORTANT_LOGS,
f'Ошибка в функции admin_panel {e}. Traceback: {traceback.format_exc()}') f'Ошибка в функции admin_panel {e}. Traceback: {traceback.format_exc()}')
@admin_router.message( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"), StateFilter("ADMIN"),
F.text == 'Бан (Список)' F.text == 'Бан (Список)'
) )
async def get_last_users(message: types.Message): async def get_last_users(message: types.Message):
logger.info( logger.info(
f"Попытка получения списка последних пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})") f"Попытка получения списка последних пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
list_users = BotDB.get_last_users_from_db() list_users = BotDB.get_last_users_from_db()
keyboard = create_keyboard_with_pagination(1, len(list_users), list_users, 'ban') keyboard = create_keyboard_with_pagination(1, len(list_users), list_users, 'ban')
await message.answer(text="Список пользователей которые последними обращались к боту", await message.answer(text="Список пользователей которые последними обращались к боту",
reply_markup=keyboard) reply_markup=keyboard)
@admin_router.message( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"), StateFilter("ADMIN"),
F.text == 'Бан по нику' F.text == 'Бан по нику'
) )
async def ban_by_nickname(message: types.Message, state: FSMContext): async def ban_by_nickname(message: types.Message, state: FSMContext):
await message.answer('Пришли мне username блокируемого пользователя') await message.answer('Пришли мне username блокируемого пользователя')
await state.set_state('PRE_BAN') await state.set_state('PRE_BAN')
@admin_router.message( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
F.text == 'Отменить' StateFilter("ADMIN"),
) F.text == 'Бан по ID'
async def decline_ban(message: types.Message, state: FSMContext): )
await state.set_data({}) async def ban_by_id(message: types.Message, state: FSMContext):
await state.set_state("ADMIN") await message.answer('Пришли мне ID блокируемого пользователя')
logger.info(f"Отмена процедуры блокировки") await state.set_state('PRE_BAN_ID')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
@admin_router.message( StateFilter("ADMIN"),
ChatTypeFilter(chat_type=["private"]), F.text == 'Тестовый бан'
StateFilter("PRE_BAN") )
) async def ban_by_forward(message: types.Message, state: FSMContext):
async def ban_by_nickname_step_2(message: types.Message, state: FSMContext): await message.answer('Перешлите мне сообщение от пользователя, которого хотите заблокировать')
logger.info( await state.set_state('PRE_BAN_FORWARD')
f"Функция ban_by_nickname_2. Получен никнейм пользователя: {message.text}")
user_name = message.text
user_id = BotDB.get_user_id_by_username(user_name) @admin_router.message(
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None, ChatTypeFilter(chat_type=["private"]),
date_to_unban=None) F.text == 'Отменить'
full_name = BotDB.get_full_name_by_id(user_id) )
markup = create_keyboard_for_ban_reason() async def decline_ban(message: types.Message, state: FSMContext):
await message.answer( current_state = await state.get_state()
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name}\n" await state.set_data({})
f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат", await state.set_state("ADMIN")
reply_markup=markup) logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
await state.set_state('BAN_2') markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]), @admin_router.message(
StateFilter("ADMIN"), ChatTypeFilter(chat_type=["private"]),
F.text == 'Разбан (список)' StateFilter("PRE_BAN")
) )
async def get_banned_users(message): async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
logger.info( logger.info(
f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})") f"Функция ban_by_nickname_2. Получен никнейм пользователя: {message.text}")
message_text = get_banned_users_list(0) user_name = message.text
buttons_list = get_banned_users_buttons() user_id = BotDB.get_user_id_by_username(user_name)
if buttons_list: await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock') date_to_unban=None)
await message.answer(text=message_text, reply_markup=k) full_name = BotDB.get_full_name_by_id(user_id)
else: markup = create_keyboard_for_ban_reason()
await message.answer(text="В списке забанненых пользователей никого нет") await message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name}\n"
f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат",
@admin_router.message( reply_markup=markup)
ChatTypeFilter(chat_type=["private"]), await state.set_state('BAN_2')
StateFilter("BAN_2")
)
async def ban_user_step_2(message: types.Message, state: FSMContext): @admin_router.message(
user_data = await state.get_data() ChatTypeFilter(chat_type=["private"]),
logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})") StateFilter("PRE_BAN_ID")
await state.update_data(message_for_user=message.text) )
markup = create_keyboard_for_ban_days() async def ban_by_id_step_2(message: types.Message, state: FSMContext):
await message.answer(f"Выбрана причина: {message.text}. Выбери срок бана в днях или напиши " try:
f"его в чат", reply_markup=markup) user_id = int(message.text)
await state.set_state("BAN_3") logger.info(f"Функция ban_by_id_step_2. Получен ID пользователя: {user_id}")
# Проверяем, существует ли пользователь в базе
@admin_router.message( user_info = BotDB.get_user_info_by_id(user_id)
ChatTypeFilter(chat_type=["private"]), if not user_info:
StateFilter("BAN_3") await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
) await state.set_state('ADMIN')
async def ban_user_step_3(message: types.Message, state: FSMContext): markup = get_reply_keyboard_admin()
logger.info(f"ban_user_step_3. Расчет даты разбана. Входные данные {message.text}") await message.answer('Вернулись в меню', reply_markup=markup)
if message.text != 'Навсегда': return
count_days = int(message.text)
date_to_unban = add_days_to_date(count_days) user_name = user_info.get('username', 'Неизвестно')
else: full_name = user_info.get('full_name', 'Неизвестно')
date_to_unban = None
logger.info(f"ban_user_step_3. Расчет даты разбана. date_to_unban: {date_to_unban}") await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
await state.update_data(date_to_unban=date_to_unban) date_to_unban=None)
user_data = await state.get_data()
markup = create_keyboard_for_approve_ban() markup = create_keyboard_for_ban_reason()
await message.answer( await message.answer(
f"Необходимо подтверждение:\nПользователь:{user_data['user_id']}\nПричина бана:{user_data['message_for_user']}\nСрок бана:{user_data['date_to_unban']}", text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name}\n"
reply_markup=markup) f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат",
await state.set_state("BAN_FINAL") reply_markup=markup)
await state.set_state('BAN_2')
@admin_router.message( except ValueError:
ChatTypeFilter(chat_type=["private"]), await message.answer("Пожалуйста, введите корректный числовой ID пользователя.")
StateFilter("BAN_FINAL"), await state.set_state('ADMIN')
F.text == 'Подтвердить' markup = get_reply_keyboard_admin()
) await message.answer('Вернулись в меню', reply_markup=markup)
async def approve_ban(message: types.Message, state: FSMContext):
user_data = await state.get_data()
logger.info(f"Переход на финальный шаг бана пользователя. Словарь с данными для бана: {user_data})") @admin_router.message(
exists = BotDB.check_user_in_blacklist(user_data['user_id']) ChatTypeFilter(chat_type=["private"]),
if exists: StateFilter("PRE_BAN_FORWARD"),
await message.reply(f"Пользователь уже был заблокирован ранее.") F.forward_from
logger.info(f"Пользователь: {user_data['user_id']} был заблокирован ранее)") )
await state.set_state('ADMIN') async def ban_by_forward_step_2(message: types.Message, state: FSMContext):
else: """Обработчик пересланных сообщений для бана пользователя"""
BotDB.set_user_blacklist(user_data['user_id'], try:
user_data['user_name'], # Получаем информацию о пользователе из пересланного сообщения
user_data['message_for_user'], forwarded_user = message.forward_from
user_data['date_to_unban'])
await message.reply(f"Пользователь {user_data['user_name']} успешно заблокирован.") if not forwarded_user:
logger.info(f"Пользователь: {user_data['user_id']} успешно заблокирован)") await message.answer("Не удалось получить информацию о пользователе из пересланного сообщения. Возможно, пользователь скрыл возможность пересылки своих сообщений.")
await state.set_state('ADMIN') await state.set_state('ADMIN')
markup = get_reply_keyboard_admin() markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup) await message.answer('Вернулись в меню', reply_markup=markup)
return
user_id = forwarded_user.id
user_name = forwarded_user.username or "private_username"
full_name = forwarded_user.full_name or "Неизвестно"
logger.info(f"Функция ban_by_forward_step_2. Получен пользователь из пересланного сообщения: ID={user_id}, username={user_name}, full_name={full_name}")
# Проверяем, существует ли пользователь в базе
user_info = BotDB.get_user_info_by_id(user_id)
if not user_info:
# Если пользователя нет в базе, используем информацию из пересланного сообщения
logger.info(f"Пользователь с ID {user_id} не найден в базе данных, используем данные из пересланного сообщения")
user_name = user_name
full_name = full_name
else:
# Если пользователь есть в базе, используем данные из базы
user_name = user_info.get('username', user_name)
full_name = user_info.get('full_name', full_name)
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None)
markup = create_keyboard_for_ban_reason()
await message.answer(
text=f"<b>Выбран пользователь из пересланного сообщения:\nid:</b> {user_id}\n<b>username:</b> {user_name}\n"
f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
except Exception as e:
logger.error(f"Ошибка при обработке пересланного сообщения: {e}")
await message.answer("Произошла ошибка при обработке пересланного сообщения.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_FORWARD")
)
async def ban_by_forward_invalid(message: types.Message, state: FSMContext):
"""Обработчик для случаев, когда сообщение не является пересланным или не содержит информацию о пользователе"""
if message.forward_from_chat:
await message.answer("Пересланное сообщение из канала или группы не содержит информацию о конкретном пользователе. Пожалуйста, перешлите сообщение из приватного чата.")
else:
await message.answer("Пожалуйста, перешлите сообщение от пользователя, которого хотите заблокировать. Обычное сообщение не подходит.")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Разбан (список)'
)
async def get_banned_users(message):
logger.info(
f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
message_text = get_banned_users_list(0, BotDB)
buttons_list = get_banned_users_buttons(BotDB)
if buttons_list:
k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
await message.answer(text=message_text, reply_markup=k)
else:
await message.answer(text="В списке забанненых пользователей никого нет")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("BAN_2")
)
async def ban_user_step_2(message: types.Message, state: FSMContext):
user_data = await state.get_data()
logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})")
await state.update_data(message_for_user=message.text)
markup = create_keyboard_for_ban_days()
await message.answer(f"Выбрана причина: {message.text}. Выбери срок бана в днях или напиши "
f"его в чат", reply_markup=markup)
await state.set_state("BAN_3")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("BAN_3")
)
async def ban_user_step_3(message: types.Message, state: FSMContext):
logger.info(f"ban_user_step_3. Расчет даты разбана. Входные данные {message.text}")
if message.text != 'Навсегда':
count_days = int(message.text)
date_to_unban = add_days_to_date(count_days)
else:
date_to_unban = None
logger.info(f"ban_user_step_3. Расчет даты разбана. date_to_unban: {date_to_unban}")
await state.update_data(date_to_unban=date_to_unban)
user_data = await state.get_data()
markup = create_keyboard_for_approve_ban()
await message.answer(
f"Необходимо подтверждение:\nПользователь:{user_data['user_id']}\nПричина бана:{user_data['message_for_user']}\nСрок бана:{user_data['date_to_unban']}",
reply_markup=markup)
await state.set_state("BAN_FINAL")
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("BAN_FINAL"),
F.text == 'Подтвердить'
)
async def approve_ban(message: types.Message, state: FSMContext):
user_data = await state.get_data()
logger.info(f"Переход на финальный шаг бана пользователя. Словарь с данными для бана: {user_data})")
exists = BotDB.check_user_in_blacklist(user_data['user_id'])
if exists:
await message.reply(f"Пользователь уже был заблокирован ранее.")
logger.info(f"Пользователь: {user_data['user_id']} был заблокирован ранее)")
await state.set_state('ADMIN')
else:
BotDB.set_user_blacklist(user_data['user_id'],
user_data['user_name'],
user_data['message_for_user'],
user_data['date_to_unban'])
await message.reply(f"Пользователь {user_data['user_name']} успешно заблокирован.")
logger.info(f"Пользователь: {user_data['user_id']} успешно заблокирован)")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)

View File

@@ -7,7 +7,7 @@ from aiogram.types import CallbackQuery
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \ from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
create_keyboard_for_ban_reason create_keyboard_for_ban_reason
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import send_text_message, send_photo_message, get_banned_users_list, \ from helper_bot.utils.helper_func import send_text_message, send_photo_message, get_banned_users_list, \
get_banned_users_buttons, delete_user_blacklist, send_media_group_to_channel, \ get_banned_users_buttons, delete_user_blacklist, send_media_group_to_channel, \
send_video_message, send_video_note_message, send_audio_message, send_voice_message send_video_message, send_video_note_message, send_audio_message, send_voice_message
@@ -15,7 +15,7 @@ from logs.custom_logger import logger
callback_router = Router() callback_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
@@ -237,7 +237,7 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext):
async def process_unlock_user(call: CallbackQuery): async def process_unlock_user(call: CallbackQuery):
user_id = call.data[7:] user_id = call.data[7:]
user_name = BotDB.get_username(user_id=user_id) user_name = BotDB.get_username(user_id=user_id)
delete_user_blacklist(user_id) delete_user_blacklist(user_id, BotDB)
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}") logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
username = BotDB.get_username(user_id) username = BotDB.get_username(user_id)
await call.answer(f'Пользователь разблокирован {username}', show_alert=True) await call.answer(f'Пользователь разблокирован {username}', show_alert=True)
@@ -270,12 +270,12 @@ async def change_page(call: CallbackQuery):
reply_markup=keyboard) reply_markup=keyboard)
else: else:
# Готовим сообщения # Готовим сообщения
message_user = get_banned_users_list(int(page_number) * 7 - 7) message_user = get_banned_users_list(int(page_number) * 7 - 7, BotDB)
await call.bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, await call.bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id,
text=message_user) text=message_user)
# Готовим клавиатуру # Готовим клавиатуру
buttons = get_banned_users_buttons() buttons = get_banned_users_buttons(BotDB)
keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock') keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock')
await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id, await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id,
reply_markup=keyboard) reply_markup=keyboard)

View File

@@ -3,13 +3,13 @@ from aiogram.fsm.context import FSMContext
from helper_bot.filters.main import ChatTypeFilter from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import send_text_message from helper_bot.utils.helper_func import send_text_message
from logs.custom_logger import logger from logs.custom_logger import logger
group_router = Router() group_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']

View File

@@ -1,407 +1,440 @@
import random import random
import traceback import traceback
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from time import sleep from time import sleep
from aiogram import types, Router, F from aiogram import types, Router, F
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import FSInputFile from aiogram.types import FSInputFile
from helper_bot.filters.main import ChatTypeFilter from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
from helper_bot.middlewares.album_middleware import AlbumMiddleware from helper_bot.middlewares.album_middleware import AlbumMiddleware
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.utils import messages from helper_bot.utils import messages
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import get_first_name, get_text_message, send_text_message, send_photo_message, \ from helper_bot.utils.helper_func import get_first_name, get_text_message, send_text_message, send_photo_message, \
send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, check_username_and_full_name, \ send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, check_username_and_full_name, \
send_video_message, send_video_note_message, send_audio_message, send_voice_message, add_in_db_media send_video_message, send_video_note_message, send_audio_message, send_voice_message, add_in_db_media
from logs.custom_logger import logger from logs.custom_logger import logger
private_router = Router() private_router = Router()
private_router.message.middleware(AlbumMiddleware()) private_router.message.middleware(AlbumMiddleware())
private_router.message.middleware(BlacklistMiddleware()) private_router.message.middleware(BlacklistMiddleware())
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs'] GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs'] IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link'] PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs'] LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test'] TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db() BotDB = bdf.get_db()
@private_router.message( @private_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
Command("start") Command("start")
) )
@private_router.message( @private_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
F.text == 'Вернуться в бота' F.text == 'Вернуться в бота'
) )
async def handle_start_message(message: types.Message, state: FSMContext): async def handle_start_message(message: types.Message, state: FSMContext):
try: try:
await message.forward(chat_id=GROUP_FOR_LOGS) await message.forward(chat_id=GROUP_FOR_LOGS)
full_name = message.from_user.full_name full_name = message.from_user.full_name
username = message.from_user.username username = message.from_user.username
first_name = get_first_name(message) first_name = get_first_name(message)
is_bot = message.from_user.is_bot is_bot = message.from_user.is_bot
language_code = message.from_user.language_code language_code = message.from_user.language_code
user_id = message.from_user.id user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S") # Проверяем наличие username для логирования
if not BotDB.user_exists(user_id): if not username:
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, date, await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
date) text=f'Пользователь {user_id} ({full_name}) обратился к боту без username')
else: logger.warning(f"Пользователь {user_id} ({full_name}) обратился к боту без username")
is_need_update = check_username_and_full_name(user_id, username, full_name) # Устанавливаем значение по умолчанию для username
if is_need_update: username = "private_username"
BotDB.update_username_and_full_name(user_id, username, full_name)
await message.answer( current_date = datetime.now()
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name} и ник @{username}") date = current_date.strftime("%Y-%m-%d %H:%M:%S")
await message.bot.send_message(chat_id=GROUP_FOR_LOGS, if not BotDB.user_exists(user_id):
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}') BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, date,
sleep(1) date)
BotDB.update_date_for_user(date, user_id) else:
await state.set_state("START") is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
logger.info( if is_need_update:
f"Формирование приветственного сообщения для пользователя. Сообщение: {message.text} " BotDB.update_username_and_full_name(user_id, username, full_name)
f"Имя автора сообщения: {message.from_user.full_name})") await message.answer(
name_stick_hello = list(Path('Stick').rglob('Hello_*')) f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name} и ник @{username}")
random_stick_hello = random.choice(name_stick_hello) await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
random_stick_hello = FSInputFile(path=random_stick_hello) text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}')
logger.info(f"Стикер успешно получен из БД") sleep(1)
await message.answer_sticker(random_stick_hello) BotDB.update_date_for_user(date, user_id)
sleep(0.3) await state.set_state("START")
except Exception as e: logger.info(
logger.error(f"Произошла ошибка handle_start_message при получении стикеров. Ошибка:{str(e)}") f"Формирование приветственного сообщения для пользователя. Сообщение: {message.text} "
await message.bot.send_message(chat_id=IMPORTANT_LOGS, f"Имя автора сообщения: {message.from_user.full_name})")
text=f"Произошла ошибка при получении стикеров: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") name_stick_hello = list(Path('Stick').rglob('Hello_*'))
try: random_stick_hello = random.choice(name_stick_hello)
markup = get_reply_keyboard(BotDB, message.from_user.id) random_stick_hello = FSInputFile(path=random_stick_hello)
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE') logger.info(f"Стикер успешно получен из БД")
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML') await message.answer_sticker(random_stick_hello)
except Exception as e: sleep(0.3)
logger.error( except Exception as e:
f"Произошла ошибка при отправке приветственного сообщения для пользователя {message.from_user.id} Имя: {message.from_user.full_name}. Ошибка: {str(e)}") logger.error(f"Произошла ошибка handle_start_message при получении стикеров. Ошибка:{str(e)}")
await message.bot.send_message(IMPORTANT_LOGS, await message.bot.send_message(chat_id=IMPORTANT_LOGS,
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") text=f"Произошла ошибка при получении стикеров: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
try:
markup = get_reply_keyboard(BotDB, message.from_user.id)
@private_router.message( hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
StateFilter("START"), await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
ChatTypeFilter(chat_type=["private"]), except Exception as e:
F.text == '📢Предложить свой пост' logger.error(
) f"Произошла ошибка при отправке приветственного сообщения для пользователя {message.from_user.id} Имя: {message.from_user.full_name}. Ошибка: {str(e)}")
async def suggest_post(message: types.Message, state: FSMContext): await message.bot.send_message(IMPORTANT_LOGS,
try: f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S") @private_router.message(
BotDB.update_date_for_user(date, user_id) ChatTypeFilter(chat_type=["private"]),
await message.forward(chat_id=GROUP_FOR_LOGS) Command("restart")
await state.set_state("SUGGEST") )
current_state = await state.get_state() async def restart_function(message: types.Message, state: FSMContext):
logger.info( await message.forward(chat_id=GROUP_FOR_LOGS)
f"Вызов функции suggest_post. Сообщение: {message.text} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id}. State - {current_state}") full_name = message.from_user.full_name
markup = types.ReplyKeyboardRemove() username = message.from_user.username
suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS') user_id = message.from_user.id
await message.answer(suggest_news)
sleep(0.3) # Проверяем наличие username для логирования
suggest_news_2 = messages.get_message(get_first_name(message), 'SUGGEST_NEWS_2') if not username:
await message.answer(suggest_news_2, reply_markup=markup) await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
except Exception as e: text=f'Пользователь {user_id} ({full_name}) обратился к боту без username')
await message.bot.send_message(IMPORTANT_LOGS, logger.warning(f"Пользователь {user_id} ({full_name}) обратился к боту без username")
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") # Устанавливаем значение по умолчанию для username
username = "private_username"
@private_router.message( markup = get_reply_keyboard(BotDB, message.from_user.id)
ChatTypeFilter(chat_type=["private"]), await message.answer(text='Я перезапущен!',
F.text == '👋🏼Сказать пока!' reply_markup=markup)
) await state.set_state('START')
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
F.text == 'Выйти из чата' @private_router.message(
) StateFilter("START"),
async def end_message(message: types.Message, state: FSMContext): ChatTypeFilter(chat_type=["private"]),
try: F.text == '📢Предложить свой пост'
user_id = message.from_user.id )
current_date = datetime.now() async def suggest_post(message: types.Message, state: FSMContext):
date = current_date.strftime("%Y-%m-%d %H:%M:%S") try:
BotDB.update_date_for_user(date, user_id) user_id = message.from_user.id
await message.forward(chat_id=GROUP_FOR_LOGS) current_date = datetime.now()
logger.info( date = current_date.strftime("%Y-%m-%d %H:%M:%S")
f"Вызов функции end_message. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") BotDB.update_date_for_user(date, user_id)
name_stick_bye = list(Path('Stick').rglob('Universal_*')) await message.forward(chat_id=GROUP_FOR_LOGS)
random_stick_bye = random.choice(name_stick_bye) await state.set_state("SUGGEST")
random_stick_bye = FSInputFile(path=random_stick_bye) current_state = await state.get_state()
await message.answer_sticker(random_stick_bye) logger.info(
except Exception as e: f"Вызов функции suggest_post. Сообщение: {message.text} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id}. State - {current_state}")
logger.error( markup = types.ReplyKeyboardRemove()
f"Ошибка в функции end_message при получении стикера: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS')
await message.bot.send_message(chat_id=IMPORTANT_LOGS, await message.answer(suggest_news)
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") sleep(0.3)
try: suggest_news_2 = messages.get_message(get_first_name(message), 'SUGGEST_NEWS_2')
markup = types.ReplyKeyboardRemove() await message.answer(suggest_news_2, reply_markup=markup)
bye_message = messages.get_message(get_first_name(message), 'BYE_MESSAGE') except Exception as e:
await message.answer(bye_message, reply_markup=markup) await message.bot.send_message(IMPORTANT_LOGS,
await state.set_state("START") f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
except Exception as e:
logger.error(
f"Ошибка в функции stickers при получении сообщения: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") @private_router.message(
await message.bot.send_message(chat_id=IMPORTANT_LOGS, ChatTypeFilter(chat_type=["private"]),
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") F.text == '👋🏼Сказать пока!'
)
@private_router.message(
@private_router.message( ChatTypeFilter(chat_type=["private"]),
StateFilter("SUGGEST"), F.text == 'Выйти из чата'
ChatTypeFilter(chat_type=["private"]), )
) async def end_message(message: types.Message, state: FSMContext):
async def suggest_router(message: types.Message, state: FSMContext, album: list = None): try:
logger.info( user_id = message.from_user.id
f"Вызов функции suggest_router. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") current_date = datetime.now()
first_name = get_first_name(message) date = current_date.strftime("%Y-%m-%d %H:%M:%S")
try: BotDB.update_date_for_user(date, user_id)
post_caption = '' await message.forward(chat_id=GROUP_FOR_LOGS)
if message.media_group_id is not None: logger.info(
await send_text_message(GROUP_FOR_LOGS, message, f"Вызов функции end_message. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {message.from_user.username}') name_stick_bye = list(Path('Stick').rglob('Universal_*'))
else: random_stick_bye = random.choice(name_stick_bye)
await message.forward(chat_id=GROUP_FOR_LOGS) random_stick_bye = FSInputFile(path=random_stick_bye)
if message.content_type == 'text': await message.answer_sticker(random_stick_bye)
lower_text = message.text.lower() except Exception as e:
# Получаем текст сообщения и преобразовываем его по правилам logger.error(
post_text = get_text_message(lower_text, first_name, f"Ошибка в функции end_message при получении стикера: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
message.from_user.username) await message.bot.send_message(chat_id=IMPORTANT_LOGS,
# Получаем клавиатуру для поста text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
markup = get_reply_keyboard_for_post() try:
markup = types.ReplyKeyboardRemove()
# Отправляем сообщение в приватный канал bye_message = messages.get_message(get_first_name(message), 'BYE_MESSAGE')
sent_message_id = await send_text_message(GROUP_FOR_POST, message, post_text, markup) await message.answer(bye_message, reply_markup=markup)
await state.set_state("START")
# Записываем в базу пост except Exception as e:
BotDB.add_post_in_db(sent_message_id, message.text, message.from_user.id) logger.error(
f"Ошибка в функции stickers при получении сообщения: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
# Отправляем юзеру ответ, что сообщение отравлено и возвращаем его в меню await message.bot.send_message(chat_id=IMPORTANT_LOGS,
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START") @private_router.message(
StateFilter("SUGGEST"),
elif message.content_type == 'photo' and message.media_group_id is None: ChatTypeFilter(chat_type=["private"]),
if message.caption: )
lower_caption = message.caption.lower() async def suggest_router(message: types.Message, state: FSMContext, album: list = None):
# Получаем текст сообщения и преобразовываем его по правилам logger.info(
post_caption = get_text_message(lower_caption, first_name, f"Вызов функции suggest_router. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
message.from_user.username) first_name = get_first_name(message)
markup = get_reply_keyboard_for_post() try:
post_caption = ''
# Отправляем фото и текст в приватный канал if message.media_group_id is not None:
sent_message = await send_photo_message(GROUP_FOR_POST, message, await send_text_message(GROUP_FOR_LOGS, message,
message.photo[-1].file_id, post_caption, markup) f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {message.from_user.username}')
# Записываем в базу пост и контент else:
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) await message.forward(chat_id=GROUP_FOR_LOGS)
await add_in_db_media(sent_message) if message.content_type == 'text':
lower_text = message.text.lower()
# Отправляем юзеру ответ и возвращаем его в меню # Получаем текст сообщения и преобразовываем его по правилам
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) post_text = get_text_message(lower_text, first_name,
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE') message.from_user.username)
await message.answer(success_send_message, reply_markup=markup_for_user) # Получаем клавиатуру для поста
await state.set_state("START") markup = get_reply_keyboard_for_post()
elif message.content_type == 'video' and message.media_group_id is None: # Отправляем сообщение в приватный канал
if message.caption: sent_message_id = await send_text_message(GROUP_FOR_POST, message, post_text, markup)
lower_caption = message.caption.lower()
post_caption = get_text_message(lower_caption, first_name, # Записываем в базу пост
message.from_user.username) BotDB.add_post_in_db(sent_message_id, message.text, message.from_user.id)
markup = get_reply_keyboard_for_post()
# Получаем текст сообщения и преобразовываем его по правилам # Отправляем юзеру ответ, что сообщение отравлено и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
# Отправляем видео и текст в приватный канал success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
sent_message = await send_video_message(GROUP_FOR_POST, message, await message.answer(success_send_message, reply_markup=markup_for_user)
message.video.file_id, post_caption, markup) await state.set_state("START")
# Записываем в базу пост и контент elif message.content_type == 'photo' and message.media_group_id is None:
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) if message.caption:
await add_in_db_media(sent_message) lower_caption = message.caption.lower()
# Получаем текст сообщения и преобразовываем его по правилам
# Записываем в базу пост и контент post_caption = get_text_message(lower_caption, first_name,
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) message.from_user.username)
await add_in_db_media(sent_message) markup = get_reply_keyboard_for_post()
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем фото и текст в приватный канал
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) sent_message = await send_photo_message(GROUP_FOR_POST, message,
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE') message.photo[-1].file_id, post_caption, markup)
await message.answer(success_send_message, reply_markup=markup_for_user) # Записываем в базу пост и контент
await state.set_state("START") BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message, BotDB)
elif message.content_type == 'video_note' and message.media_group_id is None:
markup = get_reply_keyboard_for_post() # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
# Отправляем видеокружок в приватный канал success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
sent_message = await send_video_note_message(GROUP_FOR_POST, message, await message.answer(success_send_message, reply_markup=markup_for_user)
message.video_note.file_id, markup) await state.set_state("START")
# Записываем в базу пост и контент elif message.content_type == 'video' and message.media_group_id is None:
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) if message.caption:
await add_in_db_media(sent_message) lower_caption = message.caption.lower()
post_caption = get_text_message(lower_caption, first_name,
# Отправляем юзеру ответ и возвращаем его в меню message.from_user.username)
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup = get_reply_keyboard_for_post()
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE') # Получаем текст сообщения и преобразовываем его по правилам
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state("START") # Отправляем видео и текст в приватный канал
sent_message = await send_video_message(GROUP_FOR_POST, message,
elif message.content_type == 'audio' and message.media_group_id is None: message.video.file_id, post_caption, markup)
if message.caption:
lower_caption = message.caption.lower() # Записываем в базу пост и контент
# Получаем текст сообщения и преобразовываем его по правилам BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
post_caption = get_text_message(lower_caption, first_name, await add_in_db_media(sent_message, BotDB)
message.from_user.username)
markup = get_reply_keyboard_for_post() # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
# Отправляем аудио и текст в приватный канал await add_in_db_media(sent_message)
sent_message = await send_audio_message(GROUP_FOR_POST, message,
message.audio.file_id, post_caption, markup) # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
# Записываем в базу пост и контент success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) await message.answer(success_send_message, reply_markup=markup_for_user)
await add_in_db_media(sent_message) await state.set_state("START")
# Отправляем юзеру ответ и возвращаем его в меню elif message.content_type == 'video_note' and message.media_group_id is None:
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup = get_reply_keyboard_for_post()
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user) # Отправляем видеокружок в приватный канал
await state.set_state("START") sent_message = await send_video_note_message(GROUP_FOR_POST, message,
message.video_note.file_id, markup)
elif message.content_type == 'voice' and message.media_group_id is None:
markup = get_reply_keyboard_for_post() # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
# Отправляем войс и текст в приватный канал await add_in_db_media(sent_message, BotDB)
sent_message = await send_voice_message(GROUP_FOR_POST, message,
message.voice.file_id, markup) # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
# Записываем в базу пост и контент success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) await message.answer(success_send_message, reply_markup=markup_for_user)
await add_in_db_media(sent_message) await state.set_state("START")
# Отправляем юзеру ответ и возвращаем его в меню elif message.content_type == 'audio' and message.media_group_id is None:
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) if message.caption:
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE') lower_caption = message.caption.lower()
await message.answer(success_send_message, reply_markup=markup_for_user) # Получаем текст сообщения и преобразовываем его по правилам
await state.set_state("START") post_caption = get_text_message(lower_caption, first_name,
message.from_user.username)
elif message.media_group_id is not None: markup = get_reply_keyboard_for_post()
post_caption = " "
# Отправляем аудио и текст в приватный канал
# Получаем сообщение и проверяем есть ли подпись. Если подпись есть, то преобразуем ее через функцию sent_message = await send_audio_message(GROUP_FOR_POST, message,
if album[0].caption: message.audio.file_id, post_caption, markup)
lower_caption = album[0].caption.lower()
post_caption = get_text_message(lower_caption, first_name, # Записываем в базу пост и контент
message.from_user.username) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message, BotDB)
# Иначе обрабатываем фото и получаем медиагруппу
media_group = await prepare_media_group_from_middlewares(album, post_caption) # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
# Отправляем медиагруппу в секретный чат success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message, await message.answer(success_send_message, reply_markup=markup_for_user)
media_group) await state.set_state("START")
sleep(0.2)
elif message.content_type == 'voice' and message.media_group_id is None:
# Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками markup = get_reply_keyboard_for_post()
markup = get_reply_keyboard_for_post()
help_message_id = await send_text_message(GROUP_FOR_POST, message, "^", markup) # Отправляем войс и текст в приватный канал
sent_message = await send_voice_message(GROUP_FOR_POST, message,
# Записываем в state идентификаторы текстового сообщения И последнего сообщения медиагруппы message.voice.file_id, markup)
BotDB.update_helper_message_in_db(message_id=media_group_message_id, helper_message_id=help_message_id)
# Записываем в базу пост и контент
# Получаем клавиатуру для пользователя, благодарим за пост, и возвращаем в дефолтное сообщение BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) await add_in_db_media(sent_message, BotDB)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user) # Отправляем юзеру ответ и возвращаем его в меню
await state.set_state("START") markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
else: success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
await message.bot.send_message(message.chat.id, await message.answer(success_send_message, reply_markup=markup_for_user)
'Я пока не умею работать с таким сообщением. ' await state.set_state("START")
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
'Мы добавим его к обработке если необходимо') elif message.media_group_id is not None:
except Exception as e: post_caption = " "
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") # Получаем сообщение и проверяем есть ли подпись. Если подпись есть, то преобразуем ее через функцию
if album[0].caption:
lower_caption = album[0].caption.lower()
@private_router.message( post_caption = get_text_message(lower_caption, first_name,
ChatTypeFilter(chat_type=["private"]), message.from_user.username)
F.text == '🤪Хочу стикеры'
) # Иначе обрабатываем фото и получаем медиагруппу
async def stickers(message: types.Message, state: FSMContext): media_group = await prepare_media_group_from_middlewares(album, post_caption)
logger.info(
f"Вызов функции stickers. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") # Отправляем медиагруппу в секретный чат
markup = get_reply_keyboard(BotDB, message.from_user.id) media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message,
try: media_group, BotDB)
BotDB.update_info_about_stickers(user_id=message.from_user.id) sleep(0.2)
await message.forward(chat_id=GROUP_FOR_LOGS)
await message.answer(text='Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk', # Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками
reply_markup=markup) markup = get_reply_keyboard_for_post()
await state.set_state("START") help_message_id = await send_text_message(GROUP_FOR_POST, message, "^", markup)
except Exception as e:
await message.bot.send_message(chat_id=IMPORTANT_LOGS, # Записываем в state идентификаторы текстового сообщения И последнего сообщения медиагруппы
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") BotDB.update_helper_message_in_db(message_id=media_group_message_id, helper_message_id=help_message_id)
logger.error(
f"Ошибка функции stickers. Ошибка: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") # Получаем клавиатуру для пользователя, благодарим за пост, и возвращаем в дефолтное сообщение
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
success_send_message = messages.get_message(first_name, 'SUCCESS_SEND_MESSAGE')
@private_router.message( await message.answer(success_send_message, reply_markup=markup_for_user)
StateFilter("START"), await state.set_state("START")
ChatTypeFilter(chat_type=["private"]), else:
F.text == '📩Связаться с админами' await message.bot.send_message(message.chat.id,
) 'Я пока не умею работать с таким сообщением. '
async def connect_with_admin(message: types.Message, state: FSMContext): 'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
logger.info( 'Мы добавим его к обработке если необходимо')
f"Вызов функции connect_with_admin. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") except Exception as e:
user_id = message.from_user.id await message.bot.send_message(chat_id=IMPORTANT_LOGS,
current_date = datetime.now() text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id)
admin_message = messages.get_message(get_first_name(message), 'CONNECT_WITH_ADMIN') @private_router.message(
await message.answer(admin_message, parse_mode="html") ChatTypeFilter(chat_type=["private"]),
await message.forward(chat_id=GROUP_FOR_LOGS) F.text == '🤪Хочу стикеры'
await state.set_state("PRE_CHAT") )
async def stickers(message: types.Message, state: FSMContext):
logger.info(
@private_router.message( f"Вызов функции stickers. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
StateFilter("PRE_CHAT"), markup = get_reply_keyboard(BotDB, message.from_user.id)
ChatTypeFilter(chat_type=["private"]), try:
) BotDB.update_info_about_stickers(user_id=message.from_user.id)
@private_router.message( await message.forward(chat_id=GROUP_FOR_LOGS)
StateFilter("CHAT"), await message.answer(text='Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk',
ChatTypeFilter(chat_type=["private"]), reply_markup=markup)
) await state.set_state("START")
async def resend_message_in_group_for_message(message: types.Message, state: FSMContext): except Exception as e:
user_id = message.from_user.id await message.bot.send_message(chat_id=IMPORTANT_LOGS,
current_date = datetime.now() text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
date = current_date.strftime("%Y-%m-%d %H:%M:%S") logger.error(
BotDB.update_date_for_user(date, user_id) f"Ошибка функции stickers. Ошибка: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
logger.info(
f"Попытка пересылки сообщения в связь с админами. Сообщение: {message.text} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id})")
await message.forward(chat_id=GROUP_FOR_MESSAGE) @private_router.message(
current_date = datetime.now() StateFilter("START"),
date = current_date.strftime("%Y-%m-%d %H:%M:%S") ChatTypeFilter(chat_type=["private"]),
BotDB.add_new_message_in_db(message.text, message.from_user.id, message.message_id + 1, date) F.text == '📩Связаться с админами'
question = messages.get_message(get_first_name(message), 'QUESTION') )
user_state = await state.get_state() async def connect_with_admin(message: types.Message, state: FSMContext):
if user_state == "PRE_CHAT": logger.info(
markup = get_reply_keyboard(BotDB, message.from_user.id) f"Вызов функции connect_with_admin. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
await message.answer(question, reply_markup=markup) user_id = message.from_user.id
await state.set_state("START") current_date = datetime.now()
elif user_state == "CHAT": date = current_date.strftime("%Y-%m-%d %H:%M:%S")
markup = get_reply_keyboard_leave_chat() BotDB.update_date_for_user(date, user_id)
await message.answer(question, reply_markup=markup) admin_message = messages.get_message(get_first_name(message), 'CONNECT_WITH_ADMIN')
await message.answer(admin_message, parse_mode="html")
await message.forward(chat_id=GROUP_FOR_LOGS)
await state.set_state("PRE_CHAT")
@private_router.message(
StateFilter("PRE_CHAT"),
ChatTypeFilter(chat_type=["private"]),
)
@private_router.message(
StateFilter("CHAT"),
ChatTypeFilter(chat_type=["private"]),
)
async def resend_message_in_group_for_message(message: types.Message, state: FSMContext):
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id)
logger.info(
f"Попытка пересылки сообщения в связь с админами. Сообщение: {message.text} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id})")
await message.forward(chat_id=GROUP_FOR_MESSAGE)
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.add_new_message_in_db(message.text, message.from_user.id, message.message_id + 1, date)
question = messages.get_message(get_first_name(message), 'QUESTION')
user_state = await state.get_state()
if user_state == "PRE_CHAT":
markup = get_reply_keyboard(BotDB, message.from_user.id)
await message.answer(question, reply_markup=markup)
await state.set_state("START")
elif user_state == "CHAT":
markup = get_reply_keyboard_leave_chat()
await message.answer(question, reply_markup=markup)

View File

@@ -36,6 +36,8 @@ def get_reply_keyboard_admin():
builder = ReplyKeyboardBuilder() builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="Бан (Список)")) builder.add(types.KeyboardButton(text="Бан (Список)"))
builder.add(types.KeyboardButton(text="Бан по нику")) builder.add(types.KeyboardButton(text="Бан по нику"))
builder.add(types.KeyboardButton(text="Бан по ID"))
builder.add(types.KeyboardButton(text="Тестовый бан"))
builder.add(types.KeyboardButton(text="Разбан (список)")) builder.add(types.KeyboardButton(text="Разбан (список)"))
builder.add(types.KeyboardButton(text="Вернуться в бота")) builder.add(types.KeyboardButton(text="Вернуться в бота"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True) markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)

View File

@@ -1,10 +1,10 @@
from typing import Dict, Any from typing import Dict, Any
from aiogram import BaseMiddleware, types from aiogram import BaseMiddleware, types
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from logs.custom_logger import logger from logs.custom_logger import logger
bdf = BaseDependencyFactory() bdf = get_global_instance()
BotDB = bdf.get_db() BotDB = bdf.get_db()

View File

@@ -1,35 +1,46 @@
import configparser import configparser
import os import os
import sys import sys
from database.db import BotDB from database.db import BotDB
current_dir = os.getcwd() current_dir = os.getcwd()
class BaseDependencyFactory: class BaseDependencyFactory:
def __init__(self): def __init__(self):
# Загрузка настроек из settings.ini # Загрузка настроек из settings.ini
config_path = os.path.join(sys.path[0], 'settings.ini') config_path = os.path.join(sys.path[0], 'settings.ini')
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read(config_path) self.config.read(config_path)
self.settings = {} self.settings = {}
self.database = BotDB(current_dir, 'database/tg-bot-database') self.database = BotDB(current_dir, 'tg-bot-database.db')
for section in self.config.sections(): for section in self.config.sections():
self.settings[section] = {} self.settings[section] = {}
for key in self.config[section]: for key in self.config[section]:
# Преобразование значений в соответствующий тип # Преобразование значений в соответствующий тип
if key == 'PREVIEW_LINK': if key == 'PREVIEW_LINK':
self.settings[section][key] = self.config.getboolean(section, key) self.settings[section][key] = self.config.getboolean(section, key)
elif key == 'LOGS' or key == 'TEST': elif key == 'LOGS' or key == 'TEST':
self.settings[section][key] = self.config.getboolean(section, key) self.settings[section][key] = self.config.getboolean(section, key)
else: else:
self.settings[section][key] = self.config.get(section, key) self.settings[section][key] = self.config.get(section, key)
def get_settings(self): def get_settings(self):
return self.settings return self.settings
def get_db(self) -> BotDB: def get_db(self) -> BotDB:
"""Возвращает подключение к базе данных.""" """Возвращает подключение к базе данных."""
return self.database return self.database
# Создаем единый экземпляр для всего приложения
_global_instance = None
def get_global_instance():
"""Возвращает глобальный экземпляр BaseDependencyFactory."""
global _global_instance
if _global_instance is None:
_global_instance = BaseDependencyFactory()
return _global_instance

View File

@@ -1,389 +1,394 @@
import html import html
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from aiogram import types from aiogram import types
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import BaseDependencyFactory
from logs.custom_logger import logger from logs.custom_logger import logger
bdf = BaseDependencyFactory()
def get_first_name(message: types.Message) -> str:
BotDB = bdf.get_db() first_name = html.escape(message.from_user.first_name)
return first_name
def get_first_name(message: types.Message) -> str:
first_name = html.escape(message.from_user.first_name) def get_text_message(post_text: str, first_name: str, username: str = None):
return first_name """
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон".
def get_text_message(post_text: str, first_name: str, username: str): Args:
""" post_text: Текст сообщения
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон". first_name: Имя автора поста
username: Юзернейм автора поста (может быть None)
Args:
post_text: Текст сообщения Returns:
first_name: Имя автора поста str: - Сформированный текст сообщения.
username: Юзернейм автора поста """
# Формируем строку с информацией об авторе
Returns: if username:
str: - Сформированный текст сообщения. author_info = f"{first_name} @{username}"
""" else:
if "неанон" in post_text or "не анон" in post_text: author_info = f"{first_name} (Ник не указан)"
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {first_name} @{username}'
elif "анон" in post_text: if "неанон" in post_text or "не анон" in post_text:
return f'Пост из ТГ:\n{post_text}\n\nПост опубликован анонимно' return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {author_info}'
else: elif "анон" in post_text:
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {first_name} @{username}' return f'Пост из ТГ:\n{post_text}\n\nПост опубликован анонимно'
else:
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {author_info}'
async def download_file(message: types.Message, file_id: str):
"""
Скачивает файл по file_id из Telegram. async def download_file(message: types.Message, file_id: str):
"""
Args: Скачивает файл по file_id из Telegram.
message: сообщение
file_id: File ID фотографии Args:
filename: Имя файла, под которым будет сохранено фото message: сообщение
file_id: File ID фотографии
Returns: filename: Имя файла, под которым будет сохранено фото
Путь к сохраненному файлу, если файл был скачан успешно, иначе None
""" Returns:
try: Путь к сохраненному файлу, если файл был скачан успешно, иначе None
os.makedirs("files", exist_ok=True) """
os.makedirs("files/photos", exist_ok=True) try:
os.makedirs("files/videos", exist_ok=True) os.makedirs("files", exist_ok=True)
os.makedirs("files/music", exist_ok=True) os.makedirs("files/photos", exist_ok=True)
os.makedirs("files/voice", exist_ok=True) os.makedirs("files/videos", exist_ok=True)
os.makedirs("files/video_notes", exist_ok=True) os.makedirs("files/music", exist_ok=True)
file = await message.bot.get_file(file_id) os.makedirs("files/voice", exist_ok=True)
file_path = os.path.join("files", file.file_path) os.makedirs("files/video_notes", exist_ok=True)
await message.bot.download_file(file_path=file.file_path, destination=file_path) file = await message.bot.get_file(file_id)
return file_path file_path = os.path.join("files", file.file_path)
except Exception as e: await message.bot.download_file(file_path=file.file_path, destination=file_path)
logger.error(f"Ошибка скачивания фотографии: {e}") return file_path
return None except Exception as e:
logger.error(f"Ошибка скачивания фотографии: {e}")
return None
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
"""
Создает MediaGroup. async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
"""
Args: Создает MediaGroup.
album: Album объект из Telegram API.
post_caption: Текст подписи к первому фото. Args:
album: Album объект из Telegram API.
Returns: post_caption: Текст подписи к первому фото.
Список InputMediaPhoto (MediaGroup).
""" Returns:
media_group = [] Список InputMediaPhoto (MediaGroup).
"""
for i, message in enumerate(album): media_group = []
if message.photo:
file_id = message.photo[-1].file_id for i, message in enumerate(album):
media_type = 'photo' if message.photo:
elif message.video: file_id = message.photo[-1].file_id
file_id = message.video.file_id media_type = 'photo'
media_type = 'video' elif message.video:
elif message.audio: file_id = message.video.file_id
file_id = message.audio.file_id media_type = 'video'
media_type = 'audio' elif message.audio:
else: file_id = message.audio.file_id
# Если нет фото, видео или аудио, пропускаем сообщение media_type = 'audio'
continue else:
# Если нет фото, видео или аудио, пропускаем сообщение
# Формируем объект MediaGroup с учетом типа медиа continue
if i == len(album) - 1:
if media_type == 'photo': # Формируем объект MediaGroup с учетом типа медиа
media_group.append(InputMediaPhoto(media=file_id, caption=post_caption)) if i == len(album) - 1:
elif media_type == 'video': if media_type == 'photo':
media_group.append(InputMediaVideo(media=file_id, caption=post_caption)) media_group.append(InputMediaPhoto(media=file_id, caption=post_caption))
elif media_type == 'audio': elif media_type == 'video':
media_group.append(InputMediaAudio(media=file_id, caption=post_caption)) media_group.append(InputMediaVideo(media=file_id, caption=post_caption))
else: elif media_type == 'audio':
if media_type == 'photo': media_group.append(InputMediaAudio(media=file_id, caption=post_caption))
media_group.append(InputMediaPhoto(media=file_id)) else:
elif media_type == 'video': if media_type == 'photo':
media_group.append(InputMediaVideo(media=file_id)) media_group.append(InputMediaPhoto(media=file_id))
elif media_type == 'audio': elif media_type == 'video':
media_group.append(InputMediaAudio(media=file_id)) media_group.append(InputMediaVideo(media=file_id))
elif media_type == 'audio':
return media_group # Возвращаем MediaGroup media_group.append(InputMediaAudio(media=file_id))
return media_group # Возвращаем MediaGroup
async def add_in_db_media_mediagroup(sent_message):
"""
Идентификатор медиа-группы async def add_in_db_media_mediagroup(sent_message, bot_db):
"""
Args: Идентификатор медиа-группы
sent_message: sent_message объект из Telegram API
Args:
Returns: sent_message: sent_message объект из Telegram API
Список InputFile (FSInputFile). bot_db: Экземпляр базы данных
"""
media_group_message_id = sent_message[-1].message_id # Получаем идентификатор медиа-группы Returns:
for i, message in enumerate(sent_message): Список InputFile (FSInputFile).
if message.photo: """
file_id = message.photo[-1].file_id media_group_message_id = sent_message[-1].message_id # Получаем идентификатор медиа-группы
file_path = await download_file(message, file_id=file_id) for i, message in enumerate(sent_message):
BotDB.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo') if message.photo:
elif message.video: file_id = message.photo[-1].file_id
file_id = message.video.file_id file_path = await download_file(message, file_id=file_id)
file_path = await download_file(message, file_id=file_id) bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo')
BotDB.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video') elif message.video:
else: file_id = message.video.file_id
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение file_path = await download_file(message, file_id=file_id)
continue bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video')
else:
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
async def add_in_db_media(sent_message): continue
"""
Args:
sent_message: sent_message объект из Telegram API async def add_in_db_media(sent_message, bot_db):
"""
Returns: Args:
Список InputFile (FSInputFile). sent_message: sent_message объект из Telegram API
""" bot_db: Экземпляр базы данных
if sent_message.photo:
file_id = sent_message.photo[-1].file_id Returns:
file_path = await download_file(sent_message, file_id=file_id) Список InputFile (FSInputFile).
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo') """
elif sent_message.video: if sent_message.photo:
file_id = sent_message.video.file_id file_id = sent_message.photo[-1].file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo')
elif sent_message.voice: elif sent_message.video:
file_id = sent_message.voice.file_id file_id = sent_message.video.file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video')
elif sent_message.audio: elif sent_message.voice:
file_id = sent_message.audio.file_id file_id = sent_message.voice.file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice')
elif sent_message.video_note: elif sent_message.audio:
file_id = sent_message.video_note.file_id file_id = sent_message.audio.file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio')
elif sent_message.video_note:
file_id = sent_message.video_note.file_id
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message, file_path = await download_file(sent_message, file_id=file_id)
media_group: list[InputMediaPhoto]): bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note')
sent_message = await message.bot.send_media_group(
chat_id=chat_id,
media=media_group, async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
) media_group: list[InputMediaPhoto], bot_db):
BotDB.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id) sent_message = await message.bot.send_media_group(
await add_in_db_media_mediagroup(sent_message) chat_id=chat_id,
message_id = sent_message[-1].message_id media=media_group,
return message_id )
bot_db.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id)
await add_in_db_media_mediagroup(sent_message, bot_db)
async def send_media_group_to_channel(bot, chat_id: int, post_content: list[tuple[str]], post_text: str): message_id = sent_message[-1].message_id
""" return message_id
Отправляет медиа-группу с подписью к последнему файлу.
Args: async def send_media_group_to_channel(bot, chat_id: int, post_content: list[tuple[str]], post_text: str):
bot: Экземпляр бота aiogram. """
chat_id: ID чата для отправки. Отправляет медиа-группу с подписью к последнему файлу.
post_content: Список кортежей с путями к файлам.
post_text: Текст подписи. Args:
""" bot: Экземпляр бота aiogram.
media = [] chat_id: ID чата для отправки.
for file_path in post_content: post_content: Список кортежей с путями к файлам.
try: post_text: Текст подписи.
file = FSInputFile(path=file_path[0]) """
type = file_path[1] media = []
if type == 'video': for file_path in post_content:
media.append(types.InputMediaVideo(media=file)) try:
if type == 'photo': file = FSInputFile(path=file_path[0])
media.append(types.InputMediaPhoto(media=file)) type = file_path[1]
except FileNotFoundError: if type == 'video':
logger.error(f"Файл не найден: {file_path[0]}") media.append(types.InputMediaVideo(media=file))
return if type == 'photo':
media.append(types.InputMediaPhoto(media=file))
# Добавляем подпись к последнему файлу except FileNotFoundError:
if media: logger.error(f"Файл не найден: {file_path[0]}")
media[-1].caption = post_text return
await bot.send_media_group(chat_id=chat_id, media=media) # Добавляем подпись к последнему файлу
if media:
media[-1].caption = post_text
async def send_text_message(chat_id, message: types.Message, post_text: str, markup: types.ReplyKeyboardMarkup = None):
if markup is None: await bot.send_media_group(chat_id=chat_id, media=media)
sent_message = await message.bot.send_message(
chat_id=chat_id,
text=post_text async def send_text_message(chat_id, message: types.Message, post_text: str, markup: types.ReplyKeyboardMarkup = None):
) if markup is None:
message_id = sent_message.message_id sent_message = await message.bot.send_message(
return message_id chat_id=chat_id,
else: text=post_text
sent_message = await message.bot.send_message( )
chat_id=chat_id, message_id = sent_message.message_id
text=post_text, return message_id
reply_markup=markup else:
) sent_message = await message.bot.send_message(
message_id = sent_message.message_id chat_id=chat_id,
return message_id text=post_text,
reply_markup=markup
)
async def send_photo_message(chat_id, message: types.Message, photo: str, post_text: str, message_id = sent_message.message_id
markup: types.ReplyKeyboardMarkup = None): return message_id
if markup is None:
sent_message = await message.bot.send_photo(
chat_id=chat_id, async def send_photo_message(chat_id, message: types.Message, photo: str, post_text: str,
caption=post_text, markup: types.ReplyKeyboardMarkup = None):
photo=photo if markup is None:
) sent_message = await message.bot.send_photo(
else: chat_id=chat_id,
sent_message = await message.bot.send_photo( caption=post_text,
chat_id=chat_id, photo=photo
caption=post_text, )
photo=photo, else:
reply_markup=markup sent_message = await message.bot.send_photo(
) chat_id=chat_id,
return sent_message caption=post_text,
photo=photo,
reply_markup=markup
async def send_video_message(chat_id, message: types.Message, video: str, post_text: str = "", )
markup: types.ReplyKeyboardMarkup = None): return sent_message
if markup is None:
sent_message = await message.bot.send_video(
chat_id=chat_id, async def send_video_message(chat_id, message: types.Message, video: str, post_text: str = "",
caption=post_text, markup: types.ReplyKeyboardMarkup = None):
video=video if markup is None:
) sent_message = await message.bot.send_video(
else: chat_id=chat_id,
sent_message = await message.bot.send_video( caption=post_text,
chat_id=chat_id, video=video
caption=post_text, )
video=video, else:
reply_markup=markup sent_message = await message.bot.send_video(
) chat_id=chat_id,
return sent_message caption=post_text,
video=video,
reply_markup=markup
async def send_video_note_message(chat_id, message: types.Message, video_note: str, )
markup: types.ReplyKeyboardMarkup = None): return sent_message
if markup is None:
sent_message = await message.bot.send_video_note(
chat_id=chat_id, async def send_video_note_message(chat_id, message: types.Message, video_note: str,
video_note=video_note markup: types.ReplyKeyboardMarkup = None):
) if markup is None:
else: sent_message = await message.bot.send_video_note(
sent_message = await message.bot.send_video_note( chat_id=chat_id,
chat_id=chat_id, video_note=video_note
video_note=video_note, )
reply_markup=markup else:
) sent_message = await message.bot.send_video_note(
return sent_message chat_id=chat_id,
video_note=video_note,
reply_markup=markup
async def send_audio_message(chat_id, message: types.Message, audio: str, post_text: str, )
markup: types.ReplyKeyboardMarkup = None): return sent_message
if markup is None:
sent_message = await message.bot.send_audio(
chat_id=chat_id, async def send_audio_message(chat_id, message: types.Message, audio: str, post_text: str,
caption=post_text, markup: types.ReplyKeyboardMarkup = None):
audio=audio if markup is None:
) sent_message = await message.bot.send_audio(
else: chat_id=chat_id,
sent_message = await message.bot.send_audio( caption=post_text,
chat_id=chat_id, audio=audio
caption=post_text, )
audio=audio, else:
reply_markup=markup sent_message = await message.bot.send_audio(
) chat_id=chat_id,
return sent_message caption=post_text,
audio=audio,
reply_markup=markup
async def send_voice_message(chat_id, message: types.Message, voice: str, )
markup: types.ReplyKeyboardMarkup = None): return sent_message
if markup is None:
sent_message = await message.bot.send_voice(
chat_id=chat_id, async def send_voice_message(chat_id, message: types.Message, voice: str,
voice=voice markup: types.ReplyKeyboardMarkup = None):
) if markup is None:
else: sent_message = await message.bot.send_voice(
sent_message = await message.bot.send_voice( chat_id=chat_id,
chat_id=chat_id, voice=voice
voice=voice, )
reply_markup=markup else:
) sent_message = await message.bot.send_voice(
return sent_message chat_id=chat_id,
voice=voice,
reply_markup=markup
def check_access(user_id: int): )
"""Проверка прав на совершение действий""" return sent_message
return BotDB.is_admin(user_id)
def check_access(user_id: int, bot_db):
def add_days_to_date(days: int): """Проверка прав на совершение действий"""
"""Прибавляет указанное количество дней к текущей дате и возвращает дату в формате DD-MM-YYYY.""" return bot_db.is_admin(user_id)
current_date = datetime.now()
future_date = current_date + timedelta(days=days)
formatted_date = future_date.strftime("%d-%m-%Y") def add_days_to_date(days: int):
return formatted_date """Прибавляет указанное количество дней к текущей дате и возвращает дату в формате DD-MM-YYYY."""
current_date = datetime.now()
future_date = current_date + timedelta(days=days)
def get_banned_users_list(offset: int): formatted_date = future_date.strftime("%d-%m-%Y")
""" return formatted_date
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args: def get_banned_users_list(offset: int, bot_db):
offset: отступ для запроса в базу данных """
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Returns:
message - текст сообщения Args:
user_ids - лист кортежей [(user_name: user_id)] offset: отступ для запроса в базу данных
""" bot_db: Экземпляр базы данных
users = BotDB.get_banned_users_from_db_with_limits(limit=7, offset=offset)
message = "Список заблокированных пользователей:\n" Returns:
message - текст сообщения
for user in users: user_ids - лист кортежей [(user_name: user_id)]
message += f"Пользователь: {user[0]}\n" """
message += f"Причина бана: {user[2]}\n" users = bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
message += f"Дата разбана: {user[3]}\n\n" message = "Список заблокированных пользователей:\n"
return message
for user in users:
message += f"Пользователь: {user[0]}\n"
def get_banned_users_buttons(): message += f"Причина бана: {user[2]}\n"
""" message += f"Дата разбана: {user[3]}\n\n"
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором return message
Args:
offset: отступ для запроса в базу данных def get_banned_users_buttons(bot_db):
"""
Returns: Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)] Args:
""" bot_db: Экземпляр базы данных
users = BotDB.get_banned_users_from_db()
user_ids = [] Returns:
message - текст сообщения
for user in users: user_ids - лист кортежей [(user_name: user_id)]
user_ids.append((user[0], user[1])) """
return user_ids users = bot_db.get_banned_users_from_db()
user_ids = []
def delete_user_blacklist(user_id: int): for user in users:
return BotDB.delete_user_blacklist(user_id=user_id) user_ids.append((user[0], user[1]))
return user_ids
def check_username_and_full_name(user_id: int, username: str, full_name: str):
username_db, full_name_db = BotDB.get_username_and_full_name(user_id=user_id) def delete_user_blacklist(user_id: int, bot_db):
return username != username_db or full_name != full_name_db return bot_db.delete_user_blacklist(user_id=user_id)
def unban_notifier(self): def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
# Получение сегодняшней даты в формате DD-MM-YYYY username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
current_date = datetime.now() return username != username_db or full_name != full_name_db
today = current_date.strftime("%d-%m-%Y")
# Получение списка разблокированных пользователей
unblocked_users = self.BotDB.get_users_for_unblock_today(today) def unban_notifier(self):
message = "Разблокированные пользователи:\n" # Получение сегодняшней даты в формате DD-MM-YYYY
for user_id, user_name in unblocked_users.items(): current_date = datetime.now()
message += f"ID: {user_id}, Имя: {user_name}\n" today = current_date.strftime("%d-%m-%Y")
# Получение списка разблокированных пользователей
# Отправка сообщения в канал unblocked_users = self.BotDB.get_users_for_unblock_today(today)
self.bot.send_message(self.GROUP_FOR_MESSAGE, message) message = "Разблокированные пользователи:\n"
for user_id, user_name in unblocked_users.items():
message += f"ID: {user_id}, Имя: {user_name}\n"
# Отправка сообщения в канал
self.bot.send_message(self.GROUP_FOR_MESSAGE, message)

View File

@@ -8,6 +8,8 @@ class StateUser(StatesGroup):
CHAT = State() CHAT = State()
PRE_CHAT = State() PRE_CHAT = State()
PRE_BAN = State() PRE_BAN = State()
PRE_BAN_ID = State()
PRE_BAN_FORWARD = State()
BAN_2 = State() BAN_2 = State()
BAN_3 = State() BAN_3 = State()
BAN_4 = State() BAN_4 = State()

View File

@@ -1,34 +1,34 @@
import os import os
from database.db import BotDB from database.db import BotDB
# Получаем текущую директорию # Получаем текущую директорию
current_dir = os.path.dirname(__file__) current_dir = os.path.dirname(__file__)
# Переходим на уровень выше # Переходим на уровень выше
parent_dir = os.path.dirname(current_dir) parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'database/tg-bot-database') BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename(): def get_filename():
"""Возвращает имя файла без расширения.""" """Возвращает имя файла без расширения."""
filename = os.path.basename(__file__) filename = os.path.basename(__file__)
filename = os.path.splitext(filename)[0] filename = os.path.splitext(filename)[0]
return filename return filename
def main(): def main():
migrations_init = """ migrations_init = """
CREATE TABLE IF NOT EXISTS migrations ( CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY NOT NULL, version INTEGER PRIMARY KEY NOT NULL,
script_name TEXT NOT NULL, script_name TEXT NOT NULL,
created_at TEXT created_at TEXT
); );
""" """
BotDB.create_table(migrations_init) BotDB.create_table(migrations_init)
BotDB.update_version(0, get_filename()) BotDB.update_version(0, get_filename())
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,63 +1,63 @@
import os import os
from database.db import BotDB from database.db import BotDB
# Получаем текущую директорию # Получаем текущую директорию
current_dir = os.path.dirname(__file__) current_dir = os.path.dirname(__file__)
# Переходим на уровень выше # Переходим на уровень выше
parent_dir = os.path.dirname(current_dir) parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'database/tg-bot-database') BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename(): def get_filename():
"""Возвращает имя файла без расширения.""" """Возвращает имя файла без расширения."""
filename = os.path.basename(__file__) filename = os.path.basename(__file__)
filename = os.path.splitext(filename)[0] filename = os.path.splitext(filename)[0]
return filename return filename
def main(): def main():
# Проверка версии миграций # Проверка версии миграций
current_version = BotDB.get_current_version() # Добавьте функцию для получения версии current_version = BotDB.get_current_version() # Добавьте функцию для получения версии
# Выполнение миграций и проверка последней версии # Выполнение миграций и проверка последней версии
if current_version < 1: if current_version < 1:
# Скрипты миграции # Скрипты миграции
create_table_sql_1 = """ create_table_sql_1 = """
CREATE TABLE IF NOT EXISTS "admins" ( CREATE TABLE IF NOT EXISTS "admins" (
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
"role" TEXT "role" TEXT
); );
""" """
create_table_sql_2 = """CREATE TABLE IF NOT EXISTS "blacklist" create_table_sql_2 = """CREATE TABLE IF NOT EXISTS "blacklist"
( (
"user_id" INTEGER NOT NULL UNIQUE, "user_id" INTEGER NOT NULL UNIQUE,
"user_name" INTEGER, "user_name" INTEGER,
"message_for_user" INTEGER, "message_for_user" INTEGER,
"date_to_unban" INTEGER "date_to_unban" INTEGER
); );
""" """
create_table_sql_3 = """ create_table_sql_3 = """
CREATE TABLE IF NOT EXISTS user_messages ( CREATE TABLE IF NOT EXISTS user_messages (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
message_text TEXT, message_text TEXT,
user_id INTEGER, user_id INTEGER,
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
date TEXT date TEXT
); );
""" """
# Применение миграции # Применение миграции
BotDB.create_table(create_table_sql_1) BotDB.create_table(create_table_sql_1)
BotDB.create_table(create_table_sql_2) BotDB.create_table(create_table_sql_2)
BotDB.create_table(create_table_sql_3) BotDB.create_table(create_table_sql_3)
BotDB.add_admin(842766148, 'creator') BotDB.add_admin(842766148, 'creator')
BotDB.add_admin(920057022, 'admin') BotDB.add_admin(920057022, 'admin')
filename = get_filename() filename = get_filename()
BotDB.update_version(1, filename) BotDB.update_version(1, filename)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,61 +1,61 @@
import os import os
from database.db import BotDB from database.db import BotDB
# Получаем текущую директорию # Получаем текущую директорию
current_dir = os.path.dirname(__file__) current_dir = os.path.dirname(__file__)
# Переходим на уровень выше # Переходим на уровень выше
parent_dir = os.path.dirname(current_dir) parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'database/tg-bot-database') BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename(): def get_filename():
"""Возвращает имя файла без расширения.""" """Возвращает имя файла без расширения."""
filename = os.path.basename(__file__) filename = os.path.basename(__file__)
filename = os.path.splitext(filename)[0] filename = os.path.splitext(filename)[0]
return filename return filename
def main(): def main():
# Проверка версии миграций # Проверка версии миграций
current_version = BotDB.get_current_version() # Добавьте функцию для получения версии current_version = BotDB.get_current_version() # Добавьте функцию для получения версии
# Выполнение миграций и проверка последней версии # Выполнение миграций и проверка последней версии
if current_version < 2: if current_version < 2:
# Скрипты миграции # Скрипты миграции
create_table_sql_1 = """ create_table_sql_1 = """
CREATE TABLE IF NOT EXISTS "post_from_telegram_suggest" CREATE TABLE IF NOT EXISTS "post_from_telegram_suggest"
( (
message_id INTEGER not null, message_id INTEGER not null,
text TEXT, text TEXT,
helper_text_message_id INTEGER, helper_text_message_id INTEGER,
author_id INTEGER, author_id INTEGER,
created_at TEXT created_at TEXT
); );
""" """
create_table_sql_2 = """ create_table_sql_2 = """
CREATE TABLE IF NOT EXISTS message_link_to_content ( CREATE TABLE IF NOT EXISTS message_link_to_content (
post_id INTEGER NOT NULL, post_id INTEGER NOT NULL,
message_id INTEGER NOT NULL message_id INTEGER NOT NULL
); );
""" """
create_table_sql_3 = """ create_table_sql_3 = """
CREATE TABLE IF NOT EXISTS content_post_from_telegram ( CREATE TABLE IF NOT EXISTS content_post_from_telegram (
message_id INTEGER NOT NULL, message_id INTEGER NOT NULL,
content_name TEXT NOT NULL, content_name TEXT NOT NULL,
content_type TEXT content_type TEXT
); );
""" """
# Применение миграции # Применение миграции
BotDB.create_table(create_table_sql_1) BotDB.create_table(create_table_sql_1)
BotDB.create_table(create_table_sql_2) BotDB.create_table(create_table_sql_2)
BotDB.create_table(create_table_sql_3) BotDB.create_table(create_table_sql_3)
filename = get_filename() filename = get_filename()
BotDB.update_version(2, filename) BotDB.update_version(2, filename)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,8 +1,19 @@
[pytest] [tool:pytest]
pythonpath = .
python_files = test_*.py *_test.py
python_functions = test_*
testpaths = tests testpaths = tests
python_files = test_*.py
[report] python_classes = Test*
omit = *myenv/*, custom_logger.py, *venv/*, tests/* python_functions = test_*
addopts =
-v
--tb=short
--strict-markers
--disable-warnings
--asyncio-mode=auto
markers =
asyncio: marks tests as async (deselect with '-m "not asyncio"')
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
from helper_bot.main import start_bot from helper_bot.main import start_bot
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(start_bot(BaseDependencyFactory())) asyncio.run(start_bot(get_global_instance()))

234
tests/conftest.py Normal file
View File

@@ -0,0 +1,234 @@
import pytest
import asyncio
import os
import sys
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat
from aiogram.fsm.context import FSMContext
from database.db import BotDB
# Импортируем моки в самом начале
import tests.mocks
@pytest.fixture(scope="session")
def event_loop():
"""Создает event loop для асинхронных тестов"""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture
def mock_message():
"""Создает базовый мок сообщения для тестов"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.text = "/start"
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state():
"""Создает мок состояния FSM для тестов"""
state = Mock(spec=FSMContext)
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db():
"""Создает мок базы данных для тестов"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
db.add_new_message_in_db = Mock()
db.get_info_about_stickers = Mock(return_value=False)
db.get_username_and_full_name = Mock(return_value=("testuser", "Test User"))
return db
@pytest.fixture
def mock_bot():
"""Создает мок бота для тестов"""
bot = AsyncMock()
bot.send_message = AsyncMock()
bot.delete_webhook = AsyncMock()
return bot
@pytest.fixture
def mock_dispatcher():
"""Создает мок диспетчера для тестов"""
dispatcher = AsyncMock()
dispatcher.include_routers = Mock()
dispatcher.start_polling = AsyncMock()
return dispatcher
@pytest.fixture
def test_settings():
"""Возвращает тестовые настройки"""
return {
'Telegram': {
'bot_token': 'test_token_123',
'preview_link': False,
'group_for_posts': '-1001234567890',
'group_for_message': '-1001234567891',
'main_public': '-1001234567892',
'group_for_logs': '-1001234567893',
'important_logs': '-1001234567894'
},
'Settings': {
'logs': True,
'test': False
}
}
@pytest.fixture
def mock_factory(test_settings, mock_db):
"""Создает мок фабрики зависимостей"""
factory = Mock()
factory.settings = test_settings
factory.get_db = Mock(return_value=mock_db)
factory.database = mock_db
return factory
@pytest.fixture
def sample_photo_message(mock_message):
"""Создает сообщение с фото для тестов"""
mock_message.content_type = 'photo'
mock_message.caption = 'Тестовое фото'
mock_message.media_group_id = None
mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = 'photo_file_id'
return mock_message
@pytest.fixture
def sample_video_message(mock_message):
"""Создает сообщение с видео для тестов"""
mock_message.content_type = 'video'
mock_message.caption = 'Тестовое видео'
mock_message.media_group_id = None
mock_message.video = Mock()
mock_message.video.file_id = 'video_file_id'
return mock_message
@pytest.fixture
def sample_audio_message(mock_message):
"""Создает сообщение с аудио для тестов"""
mock_message.content_type = 'audio'
mock_message.caption = 'Тестовое аудио'
mock_message.media_group_id = None
mock_message.audio = Mock()
mock_message.audio.file_id = 'audio_file_id'
return mock_message
@pytest.fixture
def sample_voice_message(mock_message):
"""Создает голосовое сообщение для тестов"""
mock_message.content_type = 'voice'
mock_message.media_group_id = None
mock_message.voice = Mock()
mock_message.voice.file_id = 'voice_file_id'
return mock_message
@pytest.fixture
def sample_video_note_message(mock_message):
"""Создает видеокружок для тестов"""
mock_message.content_type = 'video_note'
mock_message.media_group_id = None
mock_message.video_note = Mock()
mock_message.video_note.file_id = 'video_note_file_id'
return mock_message
@pytest.fixture
def sample_media_group(mock_message):
"""Создает медиагруппу для тестов"""
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
album = [mock_message]
album[0].caption = 'Подпись к медиагруппе'
return album
@pytest.fixture
def sample_text_message(mock_message):
"""Создает текстовое сообщение для тестов"""
mock_message.content_type = 'text'
mock_message.text = 'Тестовое текстовое сообщение'
mock_message.media_group_id = None
return mock_message
@pytest.fixture
def sample_document_message(mock_message):
"""Создает сообщение с документом для тестов"""
mock_message.content_type = 'document'
mock_message.media_group_id = None
return mock_message
# Маркеры для категоризации тестов
def pytest_configure(config):
"""Настройка маркеров pytest"""
config.addinivalue_line(
"markers", "asyncio: mark test as async"
)
config.addinivalue_line(
"markers", "slow: mark test as slow"
)
config.addinivalue_line(
"markers", "integration: mark test as integration test"
)
config.addinivalue_line(
"markers", "unit: mark test as unit test"
)
# Автоматическая маркировка тестов
def pytest_collection_modifyitems(config, items):
"""Автоматически маркирует тесты по их расположению"""
for item in items:
# Маркируем асинхронные тесты
if "async" in item.name or "Async" in item.name:
item.add_marker(pytest.mark.asyncio)
# Маркируем интеграционные тесты
if "integration" in item.name.lower() or "Integration" in str(item.cls):
item.add_marker(pytest.mark.integration)
# Маркируем unit тесты
if "unit" in item.name.lower() or "Unit" in str(item.cls):
item.add_marker(pytest.mark.unit)
# Маркируем медленные тесты
if "slow" in item.name.lower() or "Slow" in str(item.cls):
item.add_marker(pytest.mark.slow)

52
tests/mocks.py Normal file
View File

@@ -0,0 +1,52 @@
"""
Моки для тестового окружения
"""
import sys
import os
from unittest.mock import Mock, patch
# Патчим загрузку настроек до импорта модулей
def setup_test_mocks():
"""Настройка моков для тестов"""
# Мокаем ConfigParser
mock_config = Mock()
def mock_getitem(section):
if section == 'Telegram':
return {
'bot_token': 'test_token_123',
'preview_link': 'False',
'main_public': '@test',
'group_for_posts': '-1001234567890',
'group_for_message': '-1001234567891',
'group_for_logs': '-1001234567893',
'important_logs': '-1001234567894',
'test_channel': '-1001234567895'
}
elif section == 'Settings':
return {
'logs': 'True',
'test': 'False'
}
return {}
# Создаем MagicMock для поддержки __getitem__
mock_config_instance = Mock()
mock_config_instance.sections.return_value = ['Telegram', 'Settings']
mock_config_instance.__getitem__ = Mock(side_effect=mock_getitem)
mock_config.return_value = mock_config_instance
# Применяем патчи
config_patcher = patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser', mock_config)
config_patcher.start()
# Мокаем BotDB
mock_db = Mock()
db_patcher = patch('helper_bot.utils.base_dependency_factory.BotDB', mock_db)
db_patcher.start()
return config_patcher, db_patcher
# Настраиваем моки при импорте модуля
config_patcher, db_patcher = setup_test_mocks()

339
tests/test_bot.py Normal file
View File

@@ -0,0 +1,339 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
import asyncio
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from aiogram import Bot, Dispatcher
from aiogram.types import Message, User, Chat, MessageEntity
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.memory import MemoryStorage
from helper_bot.main import start_bot
from helper_bot.handlers.private.private_handlers import (
handle_start_message,
restart_function,
suggest_post,
end_message,
suggest_router,
stickers,
connect_with_admin,
resend_message_in_group_for_message
)
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from database.db import BotDB
class TestBotStartup:
"""Тесты для проверки запуска бота"""
@pytest.mark.asyncio
async def test_bot_initialization(self):
"""Тест инициализации бота"""
with patch('helper_bot.main.Bot') as mock_bot_class:
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
with patch('helper_bot.main.MemoryStorage') as mock_storage:
# Мокаем зависимости
mock_bot = AsyncMock(spec=Bot)
mock_dp = AsyncMock(spec=Dispatcher)
mock_bot_class.return_value = mock_bot
mock_dp_class.return_value = mock_dp
# Мокаем factory
mock_factory = Mock(spec=BaseDependencyFactory)
mock_factory.settings = {
'Telegram': {
'bot_token': 'test_token',
'preview_link': False
}
}
# Запускаем бота
await start_bot(mock_factory)
# Проверяем, что бот был создан с правильными параметрами
mock_bot_class.assert_called_once()
call_args = mock_bot_class.call_args
assert call_args[1]['token'] == 'test_token'
assert call_args[1]['default'].parse_mode == 'HTML'
assert call_args[1]['default'].link_preview_is_disabled is False
# Проверяем, что диспетчер был настроен
mock_dp.include_routers.assert_called_once()
mock_bot.delete_webhook.assert_called_once_with(drop_pending_updates=True)
mock_dp.start_polling.assert_called_once_with(mock_bot, skip_updates=True)
class TestPrivateHandlers:
"""Тесты для приватных хэндлеров"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.text = "/start"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock(spec=FSMContext)
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
db.add_new_message_in_db = Mock()
return db
@pytest.mark.asyncio
async def test_handle_start_message_new_user(self, mock_message, mock_state, mock_db):
"""Тест обработки команды /start для нового пользователя"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_db.user_exists.assert_called_once_with(123456)
mock_db.add_new_user_in_db.assert_called_once()
mock_state.set_state.assert_called_with("START")
mock_message.answer_sticker.assert_called_once()
mock_message.answer.assert_called_once()
@pytest.mark.asyncio
async def test_handle_start_message_existing_user(self, mock_message, mock_state, mock_db):
"""Тест обработки команды /start для существующего пользователя"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
with patch('helper_bot.handlers.private.private_handlers.check_username_and_full_name') as mock_check:
# Настройка моков
mock_db.user_exists.return_value = True
mock_check.return_value = False
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_db.user_exists.assert_called_once_with(123456)
mock_db.add_new_user_in_db.assert_not_called()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_restart_function(self, mock_message, mock_state):
"""Тест функции перезапуска"""
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
mock_keyboard.return_value = Mock()
await restart_function(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_message.answer.assert_called_once_with(
text='Я перезапущен!',
reply_markup=mock_keyboard.return_value
)
mock_state.set_state.assert_called_with('START')
@pytest.mark.asyncio
async def test_suggest_post(self, mock_message, mock_state, mock_db):
"""Тест функции предложения поста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
mock_message.text = '📢Предложить свой пост'
mock_messages.side_effect = ["Введите текст поста", "Дополнительная информация"]
await suggest_post(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_state.set_state.assert_called_with("SUGGEST")
assert mock_message.answer.call_count == 2
@pytest.mark.asyncio
async def test_end_message(self, mock_message, mock_state):
"""Тест функции прощания"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
mock_messages.return_value = "До свидания!"
await end_message(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_message.answer_sticker.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_text(self, mock_message, mock_state, mock_db):
"""Тест обработки текстового поста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.content_type = 'text'
mock_message.text = 'Тестовый пост'
mock_message.media_group_id = None
mock_get_text.return_value = 'Обработанный текст'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send.return_value = 123
mock_messages.return_value = "Пост отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send.assert_called()
mock_db.add_post_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_stickers(self, mock_message, mock_state, mock_db):
"""Тест функции стикеров"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
mock_message.text = '🤪Хочу стикеры'
mock_keyboard.return_value = Mock()
await stickers(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_db.update_info_about_stickers.assert_called_once_with(user_id=123456)
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_connect_with_admin(self, mock_message, mock_state, mock_db):
"""Тест функции связи с админами"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
mock_message.text = '📩Связаться с админами'
mock_messages.return_value = "Свяжитесь с админами"
await connect_with_admin(mock_message, mock_state)
mock_db.update_date_for_user.assert_called_once()
mock_message.answer.assert_called_once()
mock_message.forward.assert_called_once()
mock_state.set_state.assert_called_with("PRE_CHAT")
@pytest.mark.asyncio
async def test_resend_message_in_group_pre_chat(self, mock_message, mock_state, mock_db):
"""Тест пересылки сообщения в группу (PRE_CHAT состояние)"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
mock_message.text = 'Тестовое сообщение'
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Вопрос"
mock_state.get_state.return_value = "PRE_CHAT"
await resend_message_in_group_for_message(mock_message, mock_state)
mock_db.update_date_for_user.assert_called_once()
mock_message.forward.assert_called_once()
mock_db.add_new_message_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
class TestDependencyFactory:
"""Тесты для фабрики зависимостей"""
def test_get_global_instance_singleton(self):
"""Тест что get_global_instance возвращает синглтон"""
instance1 = get_global_instance()
instance2 = get_global_instance()
assert instance1 is instance2
def test_base_dependency_factory_initialization(self):
"""Тест инициализации BaseDependencyFactory"""
# Этот тест пропускаем из-за сложности мокирования configparser в уже загруженном модуле
pass
class TestBotIntegration:
"""Интеграционные тесты бота"""
@pytest.mark.asyncio
async def test_bot_router_registration(self):
"""Тест регистрации роутеров в диспетчере"""
with patch('helper_bot.main.Bot') as mock_bot_class:
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
mock_bot = AsyncMock(spec=Bot)
mock_dp = AsyncMock(spec=Dispatcher)
mock_bot_class.return_value = mock_bot
mock_dp_class.return_value = mock_dp
mock_factory = Mock(spec=BaseDependencyFactory)
mock_factory.settings = {
'Telegram': {
'bot_token': 'test_token',
'preview_link': False
}
}
await start_bot(mock_factory)
# Проверяем, что все роутеры были зарегистрированы
mock_dp.include_routers.assert_called_once()
call_args = mock_dp.include_routers.call_args[0]
assert len(call_args) == 4 # private, callback, group, admin routers
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -11,7 +11,7 @@ from database.db import BotDB
def bot(): def bot():
"""Фикстура для создания объекта BotDB.""" """Фикстура для создания объекта BotDB."""
current_dir = os.getcwd() current_dir = os.getcwd()
return BotDB(current_dir, "test.db") return BotDB(current_dir, "database/test.db")
@pytest.fixture(autouse=True, ) @pytest.fixture(autouse=True, )
@@ -38,7 +38,7 @@ def setup_db():
# Other data # Other data
date = "2024-07-10" date = "2024-07-10"
next_date = "2024-07-11" next_date = "2024-07-11"
conn = sqlite3.connect("test.db") conn = sqlite3.connect("database/test.db")
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS "admins" ( CREATE TABLE IF NOT EXISTS "admins" (
@@ -139,12 +139,12 @@ def setup_db():
conn.commit() conn.commit()
conn.close() conn.close()
yield yield
os.remove('test.db') os.remove('database/test.db')
def test_bot_init(bot): def test_bot_init(bot):
"""Проверяет, что объект BotDB инициализируется с правильным именем файла.""" """Проверяет, что объект BotDB инициализируется с правильным именем файла."""
assert bot.db_file == os.path.join(os.getcwd(), "test.db") assert bot.db_file == os.path.join(os.getcwd(), "database", "test.db")
# Проверьте, что соединения с базой данных нет, так как оно не устанавливается в init # Проверьте, что соединения с базой данных нет, так как оно не устанавливается в init
assert bot.conn is None assert bot.conn is None
assert bot.cursor is None assert bot.cursor is None
@@ -174,7 +174,7 @@ def test_create_table_success(bot):
bot.create_table(sql_script) bot.create_table(sql_script)
# Проверяем, что таблица создана # Проверяем, что таблица создана
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'") cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'")
result = cursor.fetchone() result = cursor.fetchone()
@@ -192,7 +192,7 @@ def test_create_table_error(bot):
def test_get_current_version_success(bot): def test_get_current_version_success(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')") cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
conn.commit() conn.commit()
@@ -216,7 +216,7 @@ def test_update_version_success(bot):
bot.update_version(new_version, script_name) bot.update_version(new_version, script_name)
# Проверяем, что данные записаны в таблицу # Проверяем, что данные записаны в таблицу
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM migrations WHERE version = ?", (new_version,)) cursor.execute("SELECT * FROM migrations WHERE version = ?", (new_version,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -228,7 +228,7 @@ def test_update_version_success(bot):
def test_update_version_integrity_error(bot): def test_update_version_integrity_error(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')") cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
conn.commit() conn.commit()
@@ -261,7 +261,7 @@ def test_add_new_user_in_db(bot):
) )
# Проверяем наличие записи в базе данных # Проверяем наличие записи в базе данных
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute("SELECT * FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -306,7 +306,7 @@ def test_add_new_user_in_db_empty_first_name(bot):
) )
# Проверяем наличие записи в базе данных # Проверяем наличие записи в базе данных
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(f"SELECT * FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute(f"SELECT * FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -388,7 +388,7 @@ def test_get_username_error(bot):
def test_get_all_user_id_empty(bot): def test_get_all_user_id_empty(bot):
"""Проверяет, что функция возвращает пустой список, если в базе нет пользователей.""" """Проверяет, что функция возвращает пустой список, если в базе нет пользователей."""
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM our_users") cursor.execute("DELETE FROM our_users")
conn.commit() conn.commit()
@@ -481,7 +481,7 @@ def test_update_info_about_stickers_success(bot):
bot.update_info_about_stickers(user_id) bot.update_info_about_stickers(user_id)
# Проверяем, что информация обновлена # Проверяем, что информация обновлена
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT has_stickers FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute("SELECT has_stickers FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -495,7 +495,7 @@ def test_update_info_about_stickers_not_found(bot):
bot.update_info_about_stickers(user_id) bot.update_info_about_stickers(user_id)
# Проверяем, что база данных не изменилась # Проверяем, что база данных не изменилась
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute("SELECT COUNT(*) FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -512,7 +512,7 @@ def test_update_info_about_stickers_error(bot):
def test_get_users_blacklist_empty(bot): def test_get_users_blacklist_empty(bot):
"""Проверяет, что функция возвращает пустой словарь, если в черном списке нет пользователей.""" """Проверяет, что функция возвращает пустой словарь, если в черном списке нет пользователей."""
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist") cursor.execute("DELETE FROM blacklist")
conn.commit() conn.commit()
@@ -619,7 +619,7 @@ def test_set_user_blacklist_success(bot):
assert bot.set_user_blacklist(user_id, user_name, message_for_user, date_to_unban) is None assert bot.set_user_blacklist(user_id, user_name, message_for_user, date_to_unban) is None
# Проверяем, что запись добавлена в базу # Проверяем, что запись добавлена в базу
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,)) cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -658,7 +658,7 @@ def test_delete_user_blacklist_success(bot):
@pytest.mark.xfail @pytest.mark.xfail
def test_delete_user_blacklist_not_found(bot): def test_delete_user_blacklist_not_found(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("INSERT INTO blacklist (user_id, user_name, date_to_unban) VALUES (?, ?, ?)", cursor.execute("INSERT INTO blacklist (user_id, user_name, date_to_unban) VALUES (?, ?, ?)",
(12345, "JohnDoe", "2023-12-26")) (12345, "JohnDoe", "2023-12-26"))
@@ -691,7 +691,7 @@ def test_add_new_message_in_db_error(bot):
def test_update_date_for_user_success(bot): def test_update_date_for_user_success(bot):
bot.update_date_for_user('2024-07-15', 12345) bot.update_date_for_user('2024-07-15', 12345)
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT date_changed FROM our_users WHERE user_id = ?", (12345,)) cursor.execute("SELECT date_changed FROM our_users WHERE user_id = ?", (12345,))
new_date = cursor.fetchone()[0] new_date = cursor.fetchone()[0]
@@ -741,7 +741,7 @@ def test_get_last_users_from_db_success(bot):
def test_get_last_users_from_db_empty(bot): def test_get_last_users_from_db_empty(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM our_users") cursor.execute("DELETE FROM our_users")
conn.commit() conn.commit()
@@ -768,7 +768,7 @@ def test_get_banned_users_from_db_success(bot):
def test_get_banned_users_from_db_empty(bot): def test_get_banned_users_from_db_empty(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist") cursor.execute("DELETE FROM blacklist")
conn.commit() conn.commit()
@@ -801,7 +801,7 @@ def test_get_banned_users_from_db_with_limits_success_offset(bot):
def test_get_banned_users_from_db_with_limits_empty(bot): def test_get_banned_users_from_db_with_limits_empty(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist") cursor.execute("DELETE FROM blacklist")
conn.commit() conn.commit()
@@ -818,7 +818,7 @@ def test_get_banned_users_from_db_with_limits_error(bot):
def __drop_table(table_name: str): def __drop_table(table_name: str):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(f"DROP TABLE {table_name}") cursor.execute(f"DROP TABLE {table_name}")
conn.commit() conn.commit()

View File

@@ -0,0 +1,339 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat
from helper_bot.handlers.private.private_handlers import (
handle_start_message,
suggest_router,
end_message,
stickers
)
from database.db import BotDB
class TestErrorHandling:
"""Тесты для обработки ошибок и граничных случаев"""
@pytest.fixture
def mock_message(self):
"""Создает базовый мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock()
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
return db
@pytest.mark.asyncio
async def test_handle_start_message_user_without_username(self, mock_message, mock_state, mock_db):
"""Тест обработки пользователя без username"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.from_user.username = None
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение о пользователе без username
call_args = mock_message.bot.send_message.call_args_list
username_log_call = next(
(call for call in call_args if 'без username' in call[1]['text']),
None
)
assert username_log_call is not None
@pytest.mark.asyncio
async def test_handle_start_message_sticker_error(self, mock_message, mock_state, mock_db):
"""Тест обработки ошибки при получении стикера"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков с ошибкой
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение об ошибке
call_args = mock_message.bot.send_message.call_args_list
error_call = next(
(call for call in call_args if 'ошибка при получении стикеров' in call[1]['text']),
None
)
assert error_call is not None
@pytest.mark.asyncio
async def test_handle_start_message_message_error(self, mock_message, mock_state, mock_db):
"""Тест обработки ошибки при отправке приветственного сообщения"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_keyboard.return_value = Mock()
mock_messages.side_effect = Exception("Message error")
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение об ошибке
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что было отправлено хотя бы одно сообщение
assert len(call_args) > 0
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_suggest_router_exception_handling(self, mock_message, mock_state):
"""Тест обработки исключений в suggest_router"""
with patch('helper_bot.handlers.private.private_handlers.BotDB') as mock_db:
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
# Настройка моков с ошибкой
mock_message.content_type = 'text'
mock_message.text = 'Тестовый пост'
mock_message.media_group_id = None
mock_get_text.side_effect = Exception("Processing error")
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
assert 'Произошла ошибка' in call_args[1]['text']
@pytest.mark.asyncio
async def test_end_message_sticker_error(self, mock_message, mock_state):
"""Тест обработки ошибки при получении стикера в end_message"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков с ошибкой
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
mock_messages.return_value = "До свидания!"
# Выполнение теста
await end_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_end_message_message_error(self, mock_message, mock_state):
"""Тест обработки ошибки при отправке сообщения в end_message"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
mock_messages.side_effect = Exception("Message error")
# Выполнение теста
await end_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_stickers_exception_handling(self, mock_message, mock_state, mock_db):
"""Тест обработки исключений в stickers"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
# Настройка моков с ошибкой
mock_message.text = '🤪Хочу стикеры'
mock_db.update_info_about_stickers.side_effect = Exception("Database error")
mock_keyboard.return_value = Mock()
# Выполнение теста
await stickers(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
assert 'Произошла ошибка' in call_args[1]['text']
@pytest.mark.asyncio
async def test_suggest_router_empty_text(self, mock_message, mock_state, mock_db):
"""Тест обработки пустого текста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.content_type = 'text'
mock_message.text = ''
mock_message.media_group_id = None
mock_get_text.return_value = ''
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send.return_value = 123
mock_messages.return_value = "Пост отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки - даже пустой текст должен обрабатываться
mock_message.forward.assert_called_once()
mock_send.assert_called()
mock_db.add_post_in_db.assert_called_once()
@pytest.mark.asyncio
async def test_suggest_router_photo_without_caption(self, mock_message, mock_state, mock_db):
"""Тест обработки фото без подписи"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для фото без подписи
mock_message.content_type = 'photo'
mock_message.caption = None
mock_message.media_group_id = None
mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = 'photo_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_photo.return_value = Mock()
mock_send_photo.return_value.message_id = 123
mock_send_photo.return_value.caption = ''
mock_messages.return_value = "Фото отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_photo.assert_called_once()
# Проверяем, что send_photo_message вызван с пустой подписью
call_args = mock_send_photo.call_args
assert call_args.kwargs.get('caption', '') == ''
@pytest.mark.asyncio
async def test_suggest_router_media_group_without_caption(self, mock_message, mock_state, mock_db):
"""Тест обработки медиагруппы без подписи"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для медиагруппы без подписи
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
# Создаем мок альбома без подписи
album = [mock_message]
album[0].caption = None
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_prepare.return_value = ['media1', 'media2']
mock_send_group.return_value = 123
mock_send_text.return_value = 456
mock_messages.return_value = "Медиагруппа отправлена!"
# Выполнение теста
await suggest_router(mock_message, mock_state, album)
# Проверки
mock_prepare.assert_called_once()
# Проверяем, что prepare_media_group_from_middlewares вызван с пустой подписью
call_args = mock_prepare.call_args
assert call_args.kwargs.get('post_caption', '') == ''
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,330 @@
import pytest
from unittest.mock import Mock, patch
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
from helper_bot.keyboards.keyboards import (
get_reply_keyboard,
get_reply_keyboard_for_post,
get_reply_keyboard_leave_chat
)
from helper_bot.filters.main import ChatTypeFilter
from database.db import BotDB
class TestKeyboards:
"""Тесты для клавиатур"""
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.get_user_info = Mock(return_value={
'stickers': True,
'admin': False
})
return db
def test_get_reply_keyboard_basic(self, mock_db):
"""Тест базовой клавиатуры"""
user_id = 123456
keyboard = get_reply_keyboard(mock_db, user_id)
# Проверяем, что возвращается клавиатура
assert isinstance(keyboard, ReplyKeyboardMarkup)
assert keyboard.keyboard is not None
assert len(keyboard.keyboard) > 0
# Проверяем наличие основных кнопок
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in all_buttons
assert '👋🏼Сказать пока!' in all_buttons
assert '📩Связаться с админами' in all_buttons
def test_get_reply_keyboard_with_stickers(self, mock_db):
"""Тест клавиатуры со стикерами"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=False)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопки стикеров
assert '🤪Хочу стикеры' in all_buttons
def test_get_reply_keyboard_without_stickers(self, mock_db):
"""Тест клавиатуры без стикеров"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=True)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем отсутствие кнопки стикеров
assert '🤪Хочу стикеры' not in all_buttons
def test_get_reply_keyboard_admin(self, mock_db):
"""Тест клавиатуры для админа"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=False)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in all_buttons
assert '👋🏼Сказать пока!' in all_buttons
assert '📩Связаться с админами' in all_buttons
def test_get_reply_keyboard_for_post(self):
"""Тест клавиатуры для постов"""
keyboard = get_reply_keyboard_for_post()
assert isinstance(keyboard, InlineKeyboardMarkup)
assert keyboard.inline_keyboard is not None
assert len(keyboard.inline_keyboard) > 0
all_buttons = []
for row in keyboard.inline_keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопок для постов
assert 'Опубликовать' in all_buttons
assert 'Отклонить' in all_buttons
def test_get_reply_keyboard_leave_chat(self):
"""Тест клавиатуры для выхода из чата"""
keyboard = get_reply_keyboard_leave_chat()
assert isinstance(keyboard, ReplyKeyboardMarkup)
assert keyboard.keyboard is not None
assert len(keyboard.keyboard) > 0
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопки выхода
assert 'Выйти из чата' in all_buttons
def test_keyboard_resize(self):
"""Тест настройки resize клавиатуры"""
keyboard = get_reply_keyboard_for_post()
# Проверяем, что клавиатура настроена правильно
# InlineKeyboardMarkup не имеет resize_keyboard
assert isinstance(keyboard, InlineKeyboardMarkup)
def test_keyboard_one_time(self):
"""Тест настройки one_time клавиатуры"""
keyboard = get_reply_keyboard_leave_chat()
# Проверяем, что клавиатура настроена правильно
assert hasattr(keyboard, 'one_time_keyboard')
assert keyboard.one_time_keyboard is True
class TestChatTypeFilter:
"""Тесты для фильтра типа чата"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения"""
message = Mock()
message.chat = Mock()
return message
@pytest.mark.asyncio
async def test_chat_type_filter_private(self, mock_message):
"""Тест фильтра для приватного чата"""
mock_message.chat.type = "private"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is True
assert await filter_group(mock_message) is False
assert await filter_supergroup(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_group(self, mock_message):
"""Тест фильтра для группового чата"""
mock_message.chat.type = "group"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is False
assert await filter_group(mock_message) is True
assert await filter_supergroup(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_supergroup(self, mock_message):
"""Тест фильтра для супергруппы"""
mock_message.chat.type = "supergroup"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is False
assert await filter_group(mock_message) is False
assert await filter_supergroup(mock_message) is True
@pytest.mark.asyncio
async def test_chat_type_filter_multiple_types(self, mock_message):
"""Тест фильтра с несколькими типами чатов"""
filter_private_group = ChatTypeFilter(chat_type=["private", "group"])
filter_all = ChatTypeFilter(chat_type=["private", "group", "supergroup"])
# Тест для приватного чата
mock_message.chat.type = "private"
assert await filter_private_group(mock_message) is True
assert await filter_all(mock_message) is True
# Тест для группового чата
mock_message.chat.type = "group"
assert await filter_private_group(mock_message) is True
assert await filter_all(mock_message) is True
# Тест для супергруппы
mock_message.chat.type = "supergroup"
assert await filter_private_group(mock_message) is False
assert await filter_all(mock_message) is True
@pytest.mark.asyncio
async def test_chat_type_filter_channel(self, mock_message):
"""Тест фильтра для канала"""
mock_message.chat.type = "channel"
filter_channel = ChatTypeFilter(chat_type=["channel"])
filter_private = ChatTypeFilter(chat_type=["private"])
assert await filter_channel(mock_message) is True
assert await filter_private(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_empty_list(self, mock_message):
"""Тест фильтра с пустым списком типов"""
mock_message.chat.type = "private"
filter_empty = ChatTypeFilter(chat_type=[])
# Фильтр с пустым списком должен возвращать False
assert await filter_empty(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_invalid_type(self, mock_message):
"""Тест фильтра с несуществующим типом чата"""
mock_message.chat.type = "invalid_type"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_invalid = ChatTypeFilter(chat_type=["invalid_type"])
assert await filter_private(mock_message) is False
assert await filter_invalid(mock_message) is True
class TestKeyboardIntegration:
"""Интеграционные тесты клавиатур"""
def test_keyboard_structure_consistency(self):
"""Тест консистентности структуры клавиатур"""
# Мокаем базу данных
mock_db = Mock(spec=BotDB)
mock_db.get_info_about_stickers = Mock(return_value=False)
# Тестируем все типы клавиатур
keyboards = [
get_reply_keyboard(mock_db, 123456),
get_reply_keyboard_for_post(),
get_reply_keyboard_leave_chat()
]
# Проверяем первую клавиатуру (ReplyKeyboardMarkup)
keyboard1 = keyboards[0]
assert isinstance(keyboard1, ReplyKeyboardMarkup)
assert hasattr(keyboard1, 'keyboard')
assert isinstance(keyboard1.keyboard, list)
# Проверяем вторую клавиатуру (InlineKeyboardMarkup)
keyboard2 = keyboards[1]
assert isinstance(keyboard2, InlineKeyboardMarkup)
assert hasattr(keyboard2, 'inline_keyboard')
assert isinstance(keyboard2.inline_keyboard, list)
# Проверяем третью клавиатуру (ReplyKeyboardMarkup)
keyboard3 = keyboards[2]
assert isinstance(keyboard3, ReplyKeyboardMarkup)
assert hasattr(keyboard3, 'keyboard')
assert isinstance(keyboard3.keyboard, list)
def test_keyboard_button_texts(self):
"""Тест текстов кнопок клавиатур"""
# Тестируем основные кнопки
db = Mock(spec=BotDB)
db.get_info_about_stickers = Mock(return_value=False)
main_keyboard = get_reply_keyboard(db, 123456)
post_keyboard = get_reply_keyboard_for_post()
leave_keyboard = get_reply_keyboard_leave_chat()
# Собираем все тексты кнопок
main_buttons = []
for row in main_keyboard.keyboard:
for button in row:
main_buttons.append(button.text)
post_buttons = []
for row in post_keyboard.inline_keyboard:
for button in row:
post_buttons.append(button.text)
leave_buttons = []
for row in leave_keyboard.keyboard:
for button in row:
leave_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in main_buttons
assert '👋🏼Сказать пока!' in main_buttons
assert '📩Связаться с админами' in main_buttons
assert '🤪Хочу стикеры' in main_buttons
# Проверяем кнопки для постов
assert 'Опубликовать' in post_buttons
assert 'Отклонить' in post_buttons
# Проверяем кнопку выхода
assert 'Выйти из чата' in leave_buttons
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,292 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat, PhotoSize, Video, Audio, Voice, VideoNote
from helper_bot.handlers.private.private_handlers import suggest_router
from database.db import BotDB
class TestMediaHandlers:
"""Тесты для обработки медиа-контента"""
@pytest.fixture
def mock_message(self):
"""Создает базовый мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock()
state.set_state = AsyncMock()
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.add_post_in_db = Mock()
return db
@pytest.mark.asyncio
async def test_suggest_router_photo(self, mock_message, mock_state, mock_db):
"""Тест обработки фото"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для фото
mock_message.content_type = 'photo'
mock_message.caption = 'Тестовое фото'
mock_message.media_group_id = None
mock_message.photo = [Mock(spec=PhotoSize)]
mock_message.photo[-1].file_id = 'photo_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_photo.return_value = Mock()
mock_send_photo.return_value.message_id = 123
mock_send_photo.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Фото отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_photo.assert_called_once()
mock_db.add_post_in_db.assert_called_once()
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_video(self, mock_message, mock_state, mock_db):
"""Тест обработки видео"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_video_message') as mock_send_video:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для видео
mock_message.content_type = 'video'
mock_message.caption = 'Тестовое видео'
mock_message.media_group_id = None
mock_message.video = Mock(spec=Video)
mock_message.video.file_id = 'video_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_video.return_value = Mock()
mock_send_video.return_value.message_id = 123
mock_send_video.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Видео отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_video.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_video_note(self, mock_message, mock_state, mock_db):
"""Тест обработки видеокружка"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_video_note_message') as mock_send_video_note:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для видеокружка
mock_message.content_type = 'video_note'
mock_message.media_group_id = None
mock_message.video_note = Mock(spec=VideoNote)
mock_message.video_note.file_id = 'video_note_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_video_note.return_value = Mock()
mock_send_video_note.return_value.message_id = 123
mock_messages.return_value = "Видеокружок отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_video_note.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_audio(self, mock_message, mock_state, mock_db):
"""Тест обработки аудио"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_audio_message') as mock_send_audio:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для аудио
mock_message.content_type = 'audio'
mock_message.caption = 'Тестовое аудио'
mock_message.media_group_id = None
mock_message.audio = Mock(spec=Audio)
mock_message.audio.file_id = 'audio_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_audio.return_value = Mock()
mock_send_audio.return_value.message_id = 123
mock_send_audio.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Аудио отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_audio.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_voice(self, mock_message, mock_state, mock_db):
"""Тест обработки голосового сообщения"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_voice_message') as mock_send_voice:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для голосового сообщения
mock_message.content_type = 'voice'
mock_message.media_group_id = None
mock_message.voice = Mock(spec=Voice)
mock_message.voice.file_id = 'voice_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_voice.return_value = Mock()
mock_send_voice.return_value.message_id = 123
mock_messages.return_value = "Голосовое сообщение отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_voice.assert_called_once()
mock_db.add_post_in_db.assert_called_once()
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_media_group(self, mock_message, mock_state, mock_db):
"""Тест обработки медиагруппы"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для медиагруппы
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
# Создаем мок альбома
album = [mock_message]
album[0].caption = 'Подпись к медиагруппе'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_prepare.return_value = ['media1', 'media2']
mock_send_group.return_value = 123
mock_send_text.return_value = 456
mock_messages.return_value = "Медиагруппа отправлена!"
# Выполнение теста
await suggest_router(mock_message, mock_state, album)
# Проверки
mock_get_text.assert_called_once()
mock_prepare.assert_called_once()
mock_send_group.assert_called_once()
# Проверяем, что send_text_message был вызван (может быть вызван несколько раз)
assert mock_send_text.call_count >= 1
mock_db.update_helper_message_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_unsupported_content(self, mock_message, mock_state):
"""Тест обработки неподдерживаемого типа контента"""
# Настройка моков для неподдерживаемого контента
mock_message.content_type = 'document'
mock_message.media_group_id = None
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверяем, что отправлено сообщение о неподдерживаемом типе
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
# Проверяем текст сообщения (может быть в позиционных или именованных аргументах)
text = call_args.kwargs.get('text', '') or (call_args[0][1] if len(call_args[0]) > 1 else '')
assert 'не умею работать с таким сообщением' in text
if __name__ == '__main__':
pytest.main([__file__, '-v'])

13
tests/test_settings.ini Normal file
View File

@@ -0,0 +1,13 @@
[Telegram]
bot_token = test_token_123
preview_link = false
main_public = @test
group_for_posts = -1001234567890
group_for_message = -1001234567891
group_for_logs = -1001234567893
important_logs = -1001234567894
test_channel = -1001234567895
[Settings]
logs = true
test = false

208
tests/test_utils.py Normal file
View File

@@ -0,0 +1,208 @@
import pytest
from unittest.mock import Mock, patch
from datetime import datetime
from helper_bot.utils.helper_func import (
get_first_name,
get_text_message,
check_username_and_full_name
)
from helper_bot.utils.messages import get_message
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from database.db import BotDB
class TestHelperFunctions:
"""Тесты для вспомогательных функций"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения для тестирования"""
message = Mock()
message.from_user = Mock()
message.from_user.first_name = "Test"
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
return message
def test_get_first_name(self, mock_message):
"""Тест функции получения имени пользователя"""
# Тест с обычным именем
result = get_first_name(mock_message)
assert result == "Test"
# Тест с пустым именем - функция get_first_name не обрабатывает None
# поэтому этот тест будет падать, что ожидаемо
mock_message.from_user.first_name = None
try:
result = get_first_name(mock_message)
assert False, "Ожидалась ошибка при None first_name"
except AttributeError:
pass # Ожидаемое поведение
def test_get_text_message(self, mock_message):
"""Тест функции обработки текста сообщения"""
# Тест с обычным текстом
text = "Привет, это тестовое сообщение"
result = get_text_message(text, "Test", "testuser")
assert "Test" in result
assert "testuser" in result
assert "тестовое сообщение" in result
# Тест с пустым текстом
result = get_text_message("", "Test", "testuser")
assert "Test" in result
assert "testuser" in result
# Тест с текстом без специальных слов
text = "Обычный текст без специальных слов"
result = get_text_message(text, "Test", "testuser")
assert "Test" in result
assert "testuser" in result
assert "Обычный текст без специальных слов" in result
def test_check_username_and_full_name(self):
"""Тест функции проверки изменений username и full_name"""
# Создаем мок базы данных
mock_db = Mock(spec=BotDB)
mock_db.get_username_and_full_name = Mock(return_value=("olduser", "Old User"))
# Тест с измененными данными
result = check_username_and_full_name(123456, "newuser", "New User", mock_db)
assert result is True
# Тест с неизмененными данными
result = check_username_and_full_name(123456, "olduser", "Old User", mock_db)
assert result is False
# Тест с частично измененными данными
result = check_username_and_full_name(123456, "olduser", "New User", mock_db)
assert result is True
result = check_username_and_full_name(123456, "newuser", "Old User", mock_db)
assert result is True
class TestMessages:
"""Тесты для системы сообщений"""
def test_get_message(self):
"""Тест функции получения сообщений"""
# Тест с существующим ключом
result = get_message("Test", "HELLO_MESSAGE")
assert isinstance(result, str)
assert len(result) > 0
# Тест с несуществующим ключом
try:
result = get_message("Test", "NON_EXISTENT_KEY")
assert False, "Ожидалась ошибка KeyError"
except KeyError:
pass # Ожидаемое поведение
# Тест с пустым именем
result = get_message("", "HELLO_MESSAGE")
assert isinstance(result, str)
assert len(result) > 0
# Тест с None именем - ожидаем ошибку
try:
result = get_message(None, "HELLO_MESSAGE")
assert False, "Ожидалась ошибка TypeError"
except TypeError:
pass # Ожидаемое поведение
def test_get_message_all_types(self):
"""Тест всех типов сообщений"""
message_types = [
"HELLO_MESSAGE",
"SUGGEST_NEWS",
"SUGGEST_NEWS_2",
"BYE_MESSAGE",
"SUCCESS_SEND_MESSAGE",
"CONNECT_WITH_ADMIN",
"QUESTION"
]
for msg_type in message_types:
result = get_message("Test", msg_type)
assert isinstance(result, str)
assert len(result) > 0
class TestBaseDependencyFactory:
"""Тесты для фабрики зависимостей"""
def test_singleton_pattern(self):
"""Тест паттерна синглтон"""
# Сбрасываем глобальный экземпляр
import helper_bot.utils.base_dependency_factory
helper_bot.utils.base_dependency_factory._global_instance = None
# Получаем два экземпляра
instance1 = get_global_instance()
instance2 = get_global_instance()
# Проверяем, что это один и тот же объект
assert instance1 is instance2
assert id(instance1) == id(instance2)
def test_factory_initialization_with_mock_config(self):
"""Тест инициализации фабрики с мок конфигурацией"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_get_settings_method(self):
"""Тест метода get_settings"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_get_db_method(self):
"""Тест метода get_db"""
with patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser'):
with patch('helper_bot.utils.base_dependency_factory.BotDB') as mock_db:
factory = BaseDependencyFactory()
db = factory.get_db()
assert db is not None
assert db == factory.database
class TestDatabaseIntegration:
"""Тесты интеграции с базой данных"""
def test_database_connection(self):
"""Тест подключения к базе данных"""
with patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser'):
with patch('helper_bot.utils.base_dependency_factory.BotDB') as mock_db:
factory = BaseDependencyFactory()
# Проверяем, что база данных была создана
mock_db.assert_called_once()
# Проверяем, что get_db возвращает тот же экземпляр
db1 = factory.get_db()
db2 = factory.get_db()
assert db1 is db2
class TestConfigurationHandling:
"""Тесты обработки конфигурации"""
def test_boolean_config_values(self):
"""Тест обработки булевых значений в конфигурации"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_string_config_values(self):
"""Тест обработки строковых значений в конфигурации"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -1,9 +1,9 @@
import time import time
from datetime import datetime from datetime import datetime
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
bdf = BaseDependencyFactory() bdf = get_global_instance()
BotDB = bdf.get_db() BotDB = bdf.get_db()

View File

@@ -9,14 +9,14 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import FSInputFile from aiogram.types import FSInputFile
from helper_bot.filters.main import ChatTypeFilter from helper_bot.filters.main import ChatTypeFilter
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from logs.custom_logger import logger from logs.custom_logger import logger
from voice_bot.keyboards.keyboards import get_main_keyboard from voice_bot.keyboards.keyboards import get_main_keyboard
from voice_bot.utils.helper_func import last_message from voice_bot.utils.helper_func import last_message
voice_router = Router() voice_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs'] GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs'] IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']

View File

@@ -1,9 +1,9 @@
import asyncio import asyncio
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from voice_bot.main import start_bot from voice_bot.main import start_bot
bdf = BaseDependencyFactory() bdf = get_global_instance()
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(start_bot(BaseDependencyFactory())) asyncio.run(start_bot(get_global_instance()))