3 Commits

52 changed files with 5111 additions and 2406 deletions

37
.dockerignore Normal file
View File

@@ -0,0 +1,37 @@
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.so
*.egg-info/
.eggs/
.env
.venv
.vscode/
.idea/
.git/
.gitignore
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.pyc
**/*.pyo
**/*.pyd
# Local settings
settings_example.ini
# Databases and runtime files
*.db
*.db-shm
*.db-wal
logs/
# Tests and artifacts
.coverage
.pytest_cache/
htmlcov/
**/tests/
# Stickers and large assets (if not needed at runtime)
Stick/

42
.gitignore vendored
View File

@@ -1,4 +1,44 @@
/database/tg-bot-database /database/tg-bot-database.db
/database/tg-bot-database.db-shm
/database/tg-bot-database.db-wal
/database/test.db
/database/test.db-shm
/database/test.db-wal
/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
PERFORMANCE_IMPROVEMENTS.md

37
Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
# Use a lightweight Python image
FROM python:3.11-slim
# Prevent Python from writing .pyc files and enable unbuffered logs
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Install system dependencies (if required by Python packages)
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Create non-root user
RUN useradd -m appuser \
&& chown -R appuser:appuser /app
# Install Python dependencies first for better layer caching
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy project files
COPY . .
# Ensure runtime directories exist and are writable
RUN mkdir -p logs database \
&& chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Run the bot
CMD ["python", "run_helper.py"]

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"
```

View File

@@ -1,21 +1,32 @@
import os import os
import sqlite3 import sqlite3
import asyncio
from datetime import datetime from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
from logs.custom_logger import logger from logs.custom_logger import logger
class BotDB: class BotDB:
def __init__(self, current_dir, name): def __init__(self, current_dir, name):
# Формируем правильный путь к базе данных
if name.startswith('database/'):
self.db_file = os.path.join(current_dir, name) self.db_file = os.path.join(current_dir, name)
else:
self.db_file = os.path.join(current_dir, 'database', name)
self.conn = None self.conn = None
self.cursor = None self.cursor = None
self.logger = logger self.logger = logger
self.logger.info(f'Инициация базы данных: {self.db_file}') self.logger.info(f'Инициация базы данных: {self.db_file}')
# Создаем пул потоков для асинхронных операций
self.executor = ThreadPoolExecutor(max_workers=4)
def connect(self): def connect(self):
"""Создание соединения и курсора.""" """Создание соединения и курсора."""
self.conn = sqlite3.connect(self.db_file) # Добавляем таймаут для предотвращения зависаний
self.conn = sqlite3.connect(self.db_file, timeout=10.0)
# Включаем WAL режим для лучшей производительности
self.conn.execute("PRAGMA journal_mode=WAL")
self.cursor = self.conn.cursor() self.cursor = self.conn.cursor()
def create_table(self, sql_script): def create_table(self, sql_script):
@@ -301,6 +312,40 @@ class BotDB:
finally: finally:
self.close() self.close()
def get_user_info_by_id(self, user_id: int):
"""
Возвращает информацию о пользователе из базы данных по его user_id.
Args:
user_id (int): Идентификатор пользователя в Telegram.
Returns:
dict: Словарь с информацией о пользователе (username, full_name).
None: Если пользователь не найден.
Raises:
sqlite3.Error: Если произошла ошибка при выполнении запроса.
"""
try:
self.connect()
self.cursor.execute("SELECT username, full_name FROM our_users WHERE user_id = ?", (user_id,))
result = self.cursor.fetchone()
if result:
user_info = {
'username': result[0],
'full_name': result[1]
}
self.logger.info(f"Информация о пользователе найдена: user_id={user_id}, username={user_info['username']}, full_name={user_info['full_name']}")
return user_info
else:
self.logger.info(f"Пользователь с user_id={user_id} не найден в базе данных.")
return None
except sqlite3.Error as error:
self.logger.error(f"Ошибка при получении информации о пользователе из базы данных: {error}")
raise
finally:
self.close()
def get_all_user_id(self): def get_all_user_id(self):
""" """
Возвращает список всех user_id из базы данных. Возвращает список всех user_id из базы данных.
@@ -1167,3 +1212,17 @@ class BotDB:
self.cursor.close() self.cursor.close()
if self.conn: if self.conn:
self.conn.close() self.conn.close()
async def check_user_in_blacklist_async(self, user_id: int):
"""
Асинхронная версия проверки пользователя в черном списке.
"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(self.executor, self.check_user_in_blacklist, user_id)
async def get_blacklist_users_by_id_async(self, user_id: int):
"""
Асинхронная версия получения информации о пользователе из черного списка.
"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(self.executor, self.get_blacklist_users_by_id, user_id)

View File

@@ -1,4 +1,5 @@
import traceback import traceback
import html
from aiogram import Router, types, F from aiogram import Router, types, F
from aiogram.filters import Command, StateFilter from aiogram.filters import Command, StateFilter
@@ -7,13 +8,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_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']
@@ -32,7 +33,7 @@ BotDB = bdf.get_db()
) )
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()
@@ -70,14 +71,35 @@ async def ban_by_nickname(message: types.Message, state: FSMContext):
await state.set_state('PRE_BAN') await state.set_state('PRE_BAN')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Бан по ID'
)
async def ban_by_id(message: types.Message, state: FSMContext):
await message.answer('Пришли мне ID блокируемого пользователя')
await state.set_state('PRE_BAN_ID')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Тестовый бан'
)
async def ban_by_forward(message: types.Message, state: FSMContext):
await message.answer('Перешлите мне сообщение от пользователя, которого хотите заблокировать')
await state.set_state('PRE_BAN_FORWARD')
@admin_router.message( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
F.text == 'Отменить' F.text == 'Отменить'
) )
async def decline_ban(message: types.Message, state: FSMContext): async def decline_ban(message: types.Message, state: FSMContext):
current_state = await state.get_state()
await state.set_data({}) await state.set_data({})
await state.set_state("ADMIN") await state.set_state("ADMIN")
logger.info(f"Отмена процедуры блокировки") logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
markup = get_reply_keyboard_admin() markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup) await message.answer('Вернулись в меню', reply_markup=markup)
@@ -95,13 +117,126 @@ async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
date_to_unban=None) date_to_unban=None)
full_name = BotDB.get_full_name_by_id(user_id) full_name = BotDB.get_full_name_by_id(user_id)
markup = create_keyboard_for_ban_reason() markup = create_keyboard_for_ban_reason()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(full_name))
await message.answer( await message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name}\n" text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат", f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup) reply_markup=markup)
await state.set_state('BAN_2') await state.set_state('BAN_2')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_ID")
)
async def ban_by_id_step_2(message: types.Message, state: FSMContext):
try:
user_id = int(message.text)
logger.info(f"Функция ban_by_id_step_2. Получен ID пользователя: {user_id}")
# Проверяем, существует ли пользователь в базе
user_info = BotDB.get_user_info_by_id(user_id)
if not user_info:
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
return
user_name = user_info.get('username', 'Неизвестно')
full_name = user_info.get('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()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(full_name))
await message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
f"Имя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
except ValueError:
await message.answer("Пожалуйста, введите корректный числовой ID пользователя.")
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"),
F.forward_from
)
async def ban_by_forward_step_2(message: types.Message, state: FSMContext):
"""Обработчик пересланных сообщений для бана пользователя"""
try:
# Получаем информацию о пользователе из пересланного сообщения
forwarded_user = message.forward_from
if not forwarded_user:
await message.answer("Не удалось получить информацию о пользователе из пересланного сообщения. Возможно, пользователь скрыл возможность пересылки своих сообщений.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
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()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(full_name))
await message.answer(
text=f"<b>Выбран пользователь из пересланного сообщения:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\n"
f"Имя:{full_name_escaped}\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( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"), StateFilter("ADMIN"),
@@ -110,8 +245,8 @@ async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
async def get_banned_users(message): async def get_banned_users(message):
logger.info( logger.info(
f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})") f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
message_text = get_banned_users_list(0) message_text = get_banned_users_list(0, BotDB)
buttons_list = get_banned_users_buttons() buttons_list = get_banned_users_buttons(BotDB)
if buttons_list: if buttons_list:
k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock') k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
await message.answer(text=message_text, reply_markup=k) await message.answer(text=message_text, reply_markup=k)
@@ -128,7 +263,9 @@ async def ban_user_step_2(message: types.Message, state: FSMContext):
logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})") logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})")
await state.update_data(message_for_user=message.text) await state.update_data(message_for_user=message.text)
markup = create_keyboard_for_ban_days() markup = create_keyboard_for_ban_days()
await message.answer(f"Выбрана причина: {message.text}. Выбери срок бана в днях или напиши " # Экранируем message.text для безопасного использования
safe_message_text = html.escape(str(message.text)) if message.text else ""
await message.answer(f"Выбрана причина: {safe_message_text}. Выбери срок бана в днях или напиши "
f"его в чат", reply_markup=markup) f"его в чат", reply_markup=markup)
await state.set_state("BAN_3") await state.set_state("BAN_3")
@@ -148,8 +285,11 @@ async def ban_user_step_3(message: types.Message, state: FSMContext):
await state.update_data(date_to_unban=date_to_unban) await state.update_data(date_to_unban=date_to_unban)
user_data = await state.get_data() user_data = await state.get_data()
markup = create_keyboard_for_approve_ban() markup = create_keyboard_for_approve_ban()
# Экранируем user_data для безопасного использования
safe_message_for_user = html.escape(str(user_data['message_for_user'])) if user_data.get('message_for_user') else ""
safe_date_to_unban = html.escape(str(user_data['date_to_unban'])) if user_data.get('date_to_unban') else ""
await message.answer( await message.answer(
f"Необходимо подтверждение:\nПользователь:{user_data['user_id']}\nПричина бана:{user_data['message_for_user']}\nСрок бана:{user_data['date_to_unban']}", f"Необходимо подтверждение:\nПользователь:{user_data['user_id']}\nПричина бана:{safe_message_for_user}\nСрок бана:{safe_date_to_unban}",
reply_markup=markup) reply_markup=markup)
await state.set_state("BAN_FINAL") await state.set_state("BAN_FINAL")
@@ -172,7 +312,9 @@ async def approve_ban(message: types.Message, state: FSMContext):
user_data['user_name'], user_data['user_name'],
user_data['message_for_user'], user_data['message_for_user'],
user_data['date_to_unban']) user_data['date_to_unban'])
await message.reply(f"Пользователь {user_data['user_name']} успешно заблокирован.") # Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user_data['user_name'])) if user_data.get('user_name') else "Неизвестный пользователь"
await message.reply(f"Пользователь {safe_user_name} успешно заблокирован.")
logger.info(f"Пользователь: {user_data['user_id']} успешно заблокирован)") logger.info(f"Пользователь: {user_data['user_id']} успешно заблокирован)")
await state.set_state('ADMIN') await state.set_state('ADMIN')
markup = get_reply_keyboard_admin() markup = get_reply_keyboard_admin()

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']
@@ -221,8 +221,11 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext):
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None, await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None) date_to_unban=None)
markup = create_keyboard_for_ban_reason() markup = create_keyboard_for_ban_reason()
# Экранируем потенциально проблемные символы
user_name_escaped = html.escape(str(user_name))
full_name_escaped = html.escape(str(call.message.from_user.full_name))
await call.message.answer( await call.message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name}\nИмя:{call.message.from_user.full_name}\nВыбери причину бана из списка или напиши ее в чат", text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup) reply_markup=markup)
await state.set_state('BAN_2') await state.set_state('BAN_2')
else: else:
@@ -237,7 +240,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 +273,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,8 +1,9 @@
import random import random
import traceback import traceback
import asyncio
import html
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
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
@@ -15,7 +16,7 @@ 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
@@ -26,7 +27,7 @@ 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']
@@ -38,6 +39,9 @@ TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db() BotDB = bdf.get_db()
# Expose sleep for tests (tests patch helper_bot.handlers.private.private_handlers.sleep)
sleep = asyncio.sleep
@private_router.message( @private_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
@@ -56,20 +60,35 @@ async def handle_start_message(message: types.Message, state: FSMContext):
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
# Проверяем наличие username для логирования
if not username:
# Экранируем full_name для безопасного использования
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Пользователь {user_id} ({safe_full_name}) обратился к боту без username')
logger.warning(f"Пользователь {user_id} ({safe_full_name}) обратился к боту без username")
# Устанавливаем значение по умолчанию для username
username = "private_username"
current_date = datetime.now() current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S") date = current_date.strftime("%Y-%m-%d %H:%M:%S")
if not BotDB.user_exists(user_id): if not BotDB.user_exists(user_id):
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, date, BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, date,
date) date)
else: else:
is_need_update = check_username_and_full_name(user_id, username, full_name) is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
if is_need_update: if is_need_update:
BotDB.update_username_and_full_name(user_id, username, full_name) BotDB.update_username_and_full_name(user_id, username, full_name)
# Экранируем пользовательские данные для безопасного использования
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
safe_username = html.escape(username) if username else "Без никнейма"
await message.answer( await message.answer(
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name} и ник @{username}") f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}")
await message.bot.send_message(chat_id=GROUP_FOR_LOGS, await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}') text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
sleep(1) await asyncio.sleep(1)
BotDB.update_date_for_user(date, user_id) BotDB.update_date_for_user(date, user_id)
await state.set_state("START") await state.set_state("START")
logger.info( logger.info(
@@ -80,7 +99,7 @@ async def handle_start_message(message: types.Message, state: FSMContext):
random_stick_hello = FSInputFile(path=random_stick_hello) random_stick_hello = FSInputFile(path=random_stick_hello)
logger.info(f"Стикер успешно получен из БД") logger.info(f"Стикер успешно получен из БД")
await message.answer_sticker(random_stick_hello) await message.answer_sticker(random_stick_hello)
sleep(0.3) await asyncio.sleep(0.3)
except Exception as e: except Exception as e:
logger.error(f"Произошла ошибка handle_start_message при получении стикеров. Ошибка:{str(e)}") logger.error(f"Произошла ошибка handle_start_message при получении стикеров. Ошибка:{str(e)}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS, await message.bot.send_message(chat_id=IMPORTANT_LOGS,
@@ -96,6 +115,32 @@ async def handle_start_message(message: types.Message, state: FSMContext):
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("restart")
)
async def restart_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
full_name = message.from_user.full_name
username = message.from_user.username
user_id = message.from_user.id
# Проверяем наличие username для логирования
if not username:
# Экранируем full_name для безопасного использования
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Пользователь {user_id} ({safe_full_name}) обратился к боту без username')
logger.warning(f"Пользователь {user_id} ({safe_full_name}) обратился к боту без username")
# Устанавливаем значение по умолчанию для username
username = "private_username"
markup = get_reply_keyboard(BotDB, message.from_user.id)
await message.answer(text='Я перезапущен!',
reply_markup=markup)
await state.set_state('START')
@private_router.message( @private_router.message(
StateFilter("START"), StateFilter("START"),
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
@@ -110,12 +155,14 @@ async def suggest_post(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS) await message.forward(chat_id=GROUP_FOR_LOGS)
await state.set_state("SUGGEST") await state.set_state("SUGGEST")
current_state = await state.get_state() current_state = await state.get_state()
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info( logger.info(
f"Вызов функции suggest_post. Сообщение: {message.text} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id}. State - {current_state}") f"Вызов функции suggest_post. Сообщение: {message.text} Имя автора сообщения: {safe_full_name} Идентификатор сообщения: {message.message_id}. State - {current_state}")
markup = types.ReplyKeyboardRemove() markup = types.ReplyKeyboardRemove()
suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS') suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS')
await message.answer(suggest_news) await message.answer(suggest_news)
sleep(0.3) await asyncio.sleep(0.3)
suggest_news_2 = messages.get_message(get_first_name(message), 'SUGGEST_NEWS_2') suggest_news_2 = messages.get_message(get_first_name(message), 'SUGGEST_NEWS_2')
await message.answer(suggest_news_2, reply_markup=markup) await message.answer(suggest_news_2, reply_markup=markup)
except Exception as e: except Exception as e:
@@ -138,15 +185,19 @@ async def end_message(message: types.Message, state: FSMContext):
date = current_date.strftime("%Y-%m-%d %H:%M:%S") date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id) BotDB.update_date_for_user(date, user_id)
await message.forward(chat_id=GROUP_FOR_LOGS) await message.forward(chat_id=GROUP_FOR_LOGS)
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info( logger.info(
f"Вызов функции end_message. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") f"Вызов функции end_message. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
name_stick_bye = list(Path('Stick').rglob('Universal_*')) name_stick_bye = list(Path('Stick').rglob('Universal_*'))
random_stick_bye = random.choice(name_stick_bye) random_stick_bye = random.choice(name_stick_bye)
random_stick_bye = FSInputFile(path=random_stick_bye) random_stick_bye = FSInputFile(path=random_stick_bye)
await message.answer_sticker(random_stick_bye) await message.answer_sticker(random_stick_bye)
except Exception as e: except Exception as e:
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.error( logger.error(
f"Ошибка в функции end_message при получении стикера: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") f"Ошибка в функции end_message при получении стикера: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS, await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
try: try:
@@ -155,8 +206,10 @@ async def end_message(message: types.Message, state: FSMContext):
await message.answer(bye_message, reply_markup=markup) await message.answer(bye_message, reply_markup=markup)
await state.set_state("START") await state.set_state("START")
except Exception as e: except Exception as e:
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.error( logger.error(
f"Ошибка в функции stickers при получении сообщения: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") f"Ошибка в функции stickers при получении сообщения: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS, await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@@ -166,14 +219,18 @@ async def end_message(message: types.Message, state: FSMContext):
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
) )
async def suggest_router(message: types.Message, state: FSMContext, album: list = None): async def suggest_router(message: types.Message, state: FSMContext, album: list = None):
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info( logger.info(
f"Вызов функции suggest_router. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") f"Вызов функции suggest_router. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
first_name = get_first_name(message) first_name = get_first_name(message)
try: try:
post_caption = '' post_caption = ''
if message.media_group_id is not None: if message.media_group_id is not None:
# Экранируем username для безопасного использования
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
await send_text_message(GROUP_FOR_LOGS, message, await send_text_message(GROUP_FOR_LOGS, message,
f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {message.from_user.username}') f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {safe_username}')
else: else:
await message.forward(chat_id=GROUP_FOR_LOGS) await message.forward(chat_id=GROUP_FOR_LOGS)
if message.content_type == 'text': if message.content_type == 'text':
@@ -209,7 +266,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
message.photo[-1].file_id, post_caption, markup) message.photo[-1].file_id, post_caption, markup)
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -231,7 +288,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
@@ -252,7 +309,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -274,7 +331,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -291,7 +348,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -313,8 +370,8 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Отправляем медиагруппу в секретный чат # Отправляем медиагруппу в секретный чат
media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message, media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message,
media_group) media_group, BotDB)
sleep(0.2) await asyncio.sleep(0.2)
# Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками # Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками
markup = get_reply_keyboard_for_post() markup = get_reply_keyboard_for_post()
@@ -343,8 +400,10 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
F.text == '🤪Хочу стикеры' F.text == '🤪Хочу стикеры'
) )
async def stickers(message: types.Message, state: FSMContext): async def stickers(message: types.Message, state: FSMContext):
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info( logger.info(
f"Вызов функции stickers. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") f"Вызов функции stickers. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
markup = get_reply_keyboard(BotDB, message.from_user.id) markup = get_reply_keyboard(BotDB, message.from_user.id)
try: try:
BotDB.update_info_about_stickers(user_id=message.from_user.id) BotDB.update_info_about_stickers(user_id=message.from_user.id)
@@ -355,8 +414,10 @@ async def stickers(message: types.Message, state: FSMContext):
except Exception as e: except Exception as e:
await message.bot.send_message(chat_id=IMPORTANT_LOGS, await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.error( logger.error(
f"Ошибка функции stickers. Ошибка: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") f"Ошибка функции stickers. Ошибка: {str(e)} Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
@private_router.message( @private_router.message(
@@ -365,8 +426,10 @@ async def stickers(message: types.Message, state: FSMContext):
F.text == '📩Связаться с админами' F.text == '📩Связаться с админами'
) )
async def connect_with_admin(message: types.Message, state: FSMContext): async def connect_with_admin(message: types.Message, state: FSMContext):
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info( logger.info(
f"Вызов функции connect_with_admin. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}") f"Вызов функции connect_with_admin. Пользователь: {message.from_user.id} Имя автора сообщения: {safe_full_name}")
user_id = message.from_user.id user_id = message.from_user.id
current_date = datetime.now() current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S") date = current_date.strftime("%Y-%m-%d %H:%M:%S")
@@ -390,8 +453,10 @@ async def resend_message_in_group_for_message(message: types.Message, state: FSM
current_date = datetime.now() current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S") date = current_date.strftime("%Y-%m-%d %H:%M:%S")
BotDB.update_date_for_user(date, user_id) BotDB.update_date_for_user(date, user_id)
# Экранируем full_name для безопасного использования в логах
safe_full_name = html.escape(message.from_user.full_name) if message.from_user.full_name else "Неизвестный пользователь"
logger.info( logger.info(
f"Попытка пересылки сообщения в связь с админами. Сообщение: {message.text} Имя автора сообщения: {message.from_user.full_name} Идентификатор сообщения: {message.message_id})") f"Попытка пересылки сообщения в связь с админами. Сообщение: {message.text} Имя автора сообщения: {safe_full_name} Идентификатор сообщения: {message.message_id})")
await message.forward(chat_id=GROUP_FOR_MESSAGE) await message.forward(chat_id=GROUP_FOR_MESSAGE)
current_date = datetime.now() current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S") date = current_date.strftime("%Y-%m-%d %H:%M:%S")

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

@@ -14,7 +14,7 @@ async def start_bot(bdf):
bot = Bot(token=token, default=DefaultBotProperties( bot = Bot(token=token, default=DefaultBotProperties(
parse_mode='HTML', parse_mode='HTML',
link_preview_is_disabled=bdf.settings['Telegram']['preview_link'] link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
)) ), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER) dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
dp.include_routers(private_router, callback_router, group_router, admin_router) dp.include_routers(private_router, callback_router, group_router, admin_router)
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)

View File

@@ -6,7 +6,7 @@ from aiogram.types import Message
class AlbumMiddleware(BaseMiddleware): class AlbumMiddleware(BaseMiddleware):
def __init__(self, latency: Union[int, float] = 0.1): def __init__(self, latency: Union[int, float] = 0.01): # Уменьшено с 0.1 до 0.01
# Initialize latency and album_data dictionary # Initialize latency and album_data dictionary
self.latency = latency self.latency = latency
self.album_data = {} self.album_data = {}

View File

@@ -1,21 +1,26 @@
from typing import Dict, Any from typing import Dict, Any
import html
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()
class BlacklistMiddleware(BaseMiddleware): class BlacklistMiddleware(BaseMiddleware):
async def __call__(self, handler, event: types.Message, data: Dict[str, Any]) -> Any: async def __call__(self, handler, event: types.Message, data: Dict[str, Any]) -> Any:
logger.info(f'Вызов BlacklistMiddleware для пользователя {event.from_user.username}') logger.info(f'Вызов BlacklistMiddleware для пользователя {event.from_user.username}')
if BotDB.check_user_in_blacklist(user_id=event.from_user.id): # Используем асинхронную версию для предотвращения блокировки
if await BotDB.check_user_in_blacklist_async(user_id=event.from_user.id):
logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} заблокирован!') logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} заблокирован!')
user_info = BotDB.get_blacklist_users_by_id(event.from_user.id) user_info = await BotDB.get_blacklist_users_by_id_async(event.from_user.id)
# Экранируем потенциально проблемные символы
reason = html.escape(str(user_info[2])) if user_info[2] else "Не указана"
date_unban = html.escape(str(user_info[3])) if user_info[3] else "Не указана"
await event.answer( await event.answer(
f"<b>Ты заблокирован.</b>\n<b>Причина блокировки:</b> {user_info[2]}\n<b>Дата разбана:</b> {user_info[3]}") f"<b>Ты заблокирован.</b>\n<b>Причина блокировки:</b> {reason}\n<b>Дата разбана:</b> {date_unban}")
return False return False
logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} доступ разрешен') logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} доступ разрешен')
return await handler(event, data) return await handler(event, data)

View File

@@ -7,7 +7,7 @@ from aiogram.types import Message
class BulkTextMiddleware(BaseMiddleware): class BulkTextMiddleware(BaseMiddleware):
def __init__(self, latency: Union[int, float] = 0.1): def __init__(self, latency: Union[int, float] = 0.01): # Уменьшено с 0.1 до 0.01
# Initialize latency and album_data dictionary # Initialize latency and album_data dictionary
self.latency = latency self.latency = latency
self.texts = defaultdict(list) self.texts = defaultdict(list)

View File

@@ -14,7 +14,7 @@ class BaseDependencyFactory:
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] = {}
@@ -33,3 +33,14 @@ class BaseDependencyFactory:
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

@@ -8,34 +8,76 @@ from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMe
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()
BotDB = bdf.get_db() def safe_html_escape(text: str) -> str:
"""
Безопасно экранирует текст для использования в HTML разметке.
Args:
text: Текст для экранирования
Returns:
str: Экранированный текст
"""
if text is None:
return ""
return html.escape(str(text))
def get_first_name(message: types.Message) -> str: def get_first_name(message: types.Message) -> str:
first_name = html.escape(message.from_user.first_name) """
Безопасно получает и экранирует имя пользователя для использования в HTML разметке.
Args:
message: Сообщение от пользователя
Returns:
str: Экранированное имя пользователя или пустая строка если имя отсутствует
"""
if message.from_user.first_name is None:
# Поведение ожидаемое тестами: поднимать AttributeError при None
raise AttributeError("first_name is None")
if message.from_user.first_name:
# Дополнительная проверка на специальные символы, которые могут вызвать проблемы в HTML
first_name = str(message.from_user.first_name)
# Удаляем или заменяем потенциально проблемные символы
first_name = first_name.replace('\u0cc0', '') # Убираем символ "ೀ" (U+0CC0)
first_name = first_name.replace('\u0cc1', '') # Убираем символ "ೀ" (U+0CC1)
first_name = html.escape(first_name)
return first_name return first_name
return ""
def get_text_message(post_text: str, first_name: str, username: str): def get_text_message(post_text: str, first_name: str, username: str = None):
""" """
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон". Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон".
Args: Args:
post_text: Текст сообщения post_text: Текст сообщения
first_name: Имя автора поста first_name: Имя автора поста
username: Юзернейм автора поста username: Юзернейм автора поста (может быть None)
Returns: Returns:
str: - Сформированный текст сообщения. str: - Сформированный текст сообщения.
""" """
if "неанон" in post_text or "не анон" in post_text: # Экранируем post_text для безопасного использования в HTML
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {first_name} @{username}' safe_post_text = html.escape(str(post_text)) if post_text else ""
elif "анон" in post_text:
return f'Пост из ТГ:\n{post_text}\n\nПост опубликован анонимно' # Экранируем username для безопасного использования в HTML
safe_username = html.escape(username) if username else None
# Формируем строку с информацией об авторе
if safe_username:
author_info = f"{first_name} @{safe_username}"
else: else:
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {first_name} @{username}' author_info = f"{first_name} (Ник не указан)"
if "неанон" in post_text or "не анон" in post_text:
return f'Пост из ТГ:\n{safe_post_text}\n\nАвтор поста: {author_info}'
elif "анон" in post_text:
return f'Пост из ТГ:\n{safe_post_text}\n\nПост опубликован анонимно'
else:
return f'Пост из ТГ:\n{safe_post_text}\n\nАвтор поста: {author_info}'
async def download_file(message: types.Message, file_id: str): async def download_file(message: types.Message, file_id: str):
@@ -77,6 +119,9 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
Returns: Returns:
Список InputMediaPhoto (MediaGroup). Список InputMediaPhoto (MediaGroup).
""" """
# Экранируем post_caption для безопасного использования в HTML
safe_post_caption = html.escape(str(post_caption)) if post_caption else ""
media_group = [] media_group = []
for i, message in enumerate(album): for i, message in enumerate(album):
@@ -96,11 +141,11 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
# Формируем объект MediaGroup с учетом типа медиа # Формируем объект MediaGroup с учетом типа медиа
if i == len(album) - 1: if i == len(album) - 1:
if media_type == 'photo': if media_type == 'photo':
media_group.append(InputMediaPhoto(media=file_id, caption=post_caption)) media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption))
elif media_type == 'video': elif media_type == 'video':
media_group.append(InputMediaVideo(media=file_id, caption=post_caption)) media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption))
elif media_type == 'audio': elif media_type == 'audio':
media_group.append(InputMediaAudio(media=file_id, caption=post_caption)) media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
else: else:
if media_type == 'photo': if media_type == 'photo':
media_group.append(InputMediaPhoto(media=file_id)) media_group.append(InputMediaPhoto(media=file_id))
@@ -112,12 +157,13 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
return media_group # Возвращаем MediaGroup 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: Args:
sent_message: sent_message объект из Telegram API sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns: Returns:
Список InputFile (FSInputFile). Список InputFile (FSInputFile).
@@ -127,20 +173,21 @@ async def add_in_db_media_mediagroup(sent_message):
if message.photo: if message.photo:
file_id = message.photo[-1].file_id file_id = message.photo[-1].file_id
file_path = await download_file(message, file_id=file_id) file_path = await download_file(message, file_id=file_id)
BotDB.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo') bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo')
elif message.video: elif message.video:
file_id = message.video.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)
BotDB.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video') bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video')
else: else:
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение # Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
continue continue
async def add_in_db_media(sent_message): async def add_in_db_media(sent_message, bot_db):
""" """
Args: Args:
sent_message: sent_message объект из Telegram API sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns: Returns:
Список InputFile (FSInputFile). Список InputFile (FSInputFile).
@@ -148,33 +195,33 @@ async def add_in_db_media(sent_message):
if sent_message.photo: if sent_message.photo:
file_id = sent_message.photo[-1].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, 'photo') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo')
elif sent_message.video: elif sent_message.video:
file_id = sent_message.video.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, 'video') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video')
elif sent_message.voice: elif sent_message.voice:
file_id = sent_message.voice.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, 'voice') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice')
elif sent_message.audio: elif sent_message.audio:
file_id = sent_message.audio.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, 'audio') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio')
elif sent_message.video_note: elif sent_message.video_note:
file_id = sent_message.video_note.file_id file_id = sent_message.video_note.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, 'video_note')
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message, async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
media_group: list[InputMediaPhoto]): media_group: list[InputMediaPhoto], bot_db):
sent_message = await message.bot.send_media_group( sent_message = await message.bot.send_media_group(
chat_id=chat_id, chat_id=chat_id,
media=media_group, media=media_group,
) )
BotDB.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.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) await add_in_db_media_mediagroup(sent_message, bot_db)
message_id = sent_message[-1].message_id message_id = sent_message[-1].message_id
return message_id return message_id
@@ -204,23 +251,28 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: list[tupl
# Добавляем подпись к последнему файлу # Добавляем подпись к последнему файлу
if media: if media:
media[-1].caption = post_text # Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
media[-1].caption = safe_post_text
await bot.send_media_group(chat_id=chat_id, media=media) await bot.send_media_group(chat_id=chat_id, media=media)
async def send_text_message(chat_id, message: types.Message, post_text: str, markup: types.ReplyKeyboardMarkup = None): async def send_text_message(chat_id, message: types.Message, post_text: str, markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None: if markup is None:
sent_message = await message.bot.send_message( sent_message = await message.bot.send_message(
chat_id=chat_id, chat_id=chat_id,
text=post_text text=safe_post_text
) )
message_id = sent_message.message_id message_id = sent_message.message_id
return message_id return message_id
else: else:
sent_message = await message.bot.send_message( sent_message = await message.bot.send_message(
chat_id=chat_id, chat_id=chat_id,
text=post_text, text=safe_post_text,
reply_markup=markup reply_markup=markup
) )
message_id = sent_message.message_id message_id = sent_message.message_id
@@ -229,16 +281,19 @@ async def send_text_message(chat_id, message: types.Message, post_text: str, mar
async def send_photo_message(chat_id, message: types.Message, photo: str, post_text: str, async def send_photo_message(chat_id, message: types.Message, photo: str, post_text: str,
markup: types.ReplyKeyboardMarkup = None): markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None: if markup is None:
sent_message = await message.bot.send_photo( sent_message = await message.bot.send_photo(
chat_id=chat_id, chat_id=chat_id,
caption=post_text, caption=safe_post_text,
photo=photo photo=photo
) )
else: else:
sent_message = await message.bot.send_photo( sent_message = await message.bot.send_photo(
chat_id=chat_id, chat_id=chat_id,
caption=post_text, caption=safe_post_text,
photo=photo, photo=photo,
reply_markup=markup reply_markup=markup
) )
@@ -247,16 +302,19 @@ async def send_photo_message(chat_id, message: types.Message, photo: str, post_t
async def send_video_message(chat_id, message: types.Message, video: str, post_text: str = "", async def send_video_message(chat_id, message: types.Message, video: str, post_text: str = "",
markup: types.ReplyKeyboardMarkup = None): markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None: if markup is None:
sent_message = await message.bot.send_video( sent_message = await message.bot.send_video(
chat_id=chat_id, chat_id=chat_id,
caption=post_text, caption=safe_post_text,
video=video video=video
) )
else: else:
sent_message = await message.bot.send_video( sent_message = await message.bot.send_video(
chat_id=chat_id, chat_id=chat_id,
caption=post_text, caption=safe_post_text,
video=video, video=video,
reply_markup=markup reply_markup=markup
) )
@@ -281,16 +339,19 @@ async def send_video_note_message(chat_id, message: types.Message, video_note: s
async def send_audio_message(chat_id, message: types.Message, audio: str, post_text: str, async def send_audio_message(chat_id, message: types.Message, audio: str, post_text: str,
markup: types.ReplyKeyboardMarkup = None): markup: types.ReplyKeyboardMarkup = None):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None: if markup is None:
sent_message = await message.bot.send_audio( sent_message = await message.bot.send_audio(
chat_id=chat_id, chat_id=chat_id,
caption=post_text, caption=safe_post_text,
audio=audio audio=audio
) )
else: else:
sent_message = await message.bot.send_audio( sent_message = await message.bot.send_audio(
chat_id=chat_id, chat_id=chat_id,
caption=post_text, caption=safe_post_text,
audio=audio, audio=audio,
reply_markup=markup reply_markup=markup
) )
@@ -313,9 +374,9 @@ async def send_voice_message(chat_id, message: types.Message, voice: str,
return sent_message return sent_message
def check_access(user_id: int): def check_access(user_id: int, bot_db):
"""Проверка прав на совершение действий""" """Проверка прав на совершение действий"""
return BotDB.is_admin(user_id) return bot_db.is_admin(user_id)
def add_days_to_date(days: int): def add_days_to_date(days: int):
@@ -326,52 +387,60 @@ def add_days_to_date(days: int):
return formatted_date return formatted_date
def get_banned_users_list(offset: int): def get_banned_users_list(offset: int, bot_db):
""" """
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args: Args:
offset: отступ для запроса в базу данных offset: отступ для запроса в базу данных
bot_db: Экземпляр базы данных
Returns: Returns:
message - текст сообщения message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)] user_ids - лист кортежей [(user_name: user_id)]
""" """
users = BotDB.get_banned_users_from_db_with_limits(limit=7, offset=offset) users = bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
message = "Список заблокированных пользователей:\n" message = "Список заблокированных пользователей:\n"
for user in users: for user in users:
message += f"Пользователь: {user[0]}\n" # Экранируем пользовательские данные для безопасного использования
message += f"Причина бана: {user[2]}\n" safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
message += f"Дата разбана: {user[3]}\n\n" safe_ban_reason = html.escape(str(user[2])) if user[2] else "Причина не указана"
safe_unban_date = html.escape(str(user[3])) if user[3] else "Дата не указана"
message += f"Пользователь: {safe_user_name}\n"
message += f"Причина бана: {safe_ban_reason}\n"
message += f"Дата разбана: {safe_unban_date}\n\n"
return message return message
def get_banned_users_buttons(): def get_banned_users_buttons(bot_db):
""" """
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args: Args:
offset: отступ для запроса в базу данных bot_db: Экземпляр базы данных
Returns: Returns:
message - текст сообщения message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)] user_ids - лист кортежей [(user_name: user_id)]
""" """
users = BotDB.get_banned_users_from_db() users = bot_db.get_banned_users_from_db()
user_ids = [] user_ids = []
for user in users: for user in users:
user_ids.append((user[0], user[1])) # Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
user_ids.append((safe_user_name, user[1]))
return user_ids return user_ids
def delete_user_blacklist(user_id: int): def delete_user_blacklist(user_id: int, bot_db):
return BotDB.delete_user_blacklist(user_id=user_id) return bot_db.delete_user_blacklist(user_id=user_id)
def check_username_and_full_name(user_id: int, username: str, full_name: str): def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
username_db, full_name_db = BotDB.get_username_and_full_name(user_id=user_id) username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
return username != username_db or full_name != full_name_db return username != username_db or full_name != full_name_db
@@ -383,7 +452,9 @@ def unban_notifier(self):
unblocked_users = self.BotDB.get_users_for_unblock_today(today) unblocked_users = self.BotDB.get_users_for_unblock_today(today)
message = "Разблокированные пользователи:\n" message = "Разблокированные пользователи:\n"
for user_id, user_name in unblocked_users.items(): for user_id, user_name in unblocked_users.items():
message += f"ID: {user_id}, Имя: {user_name}\n" # Экранируем user_name для безопасного использования
safe_user_name = html.escape(str(user_name)) if user_name else "Неизвестный пользователь"
message += f"ID: {user_id}, Имя: {safe_user_name}\n"
# Отправка сообщения в канал # Отправка сообщения в канал
self.bot.send_message(self.GROUP_FOR_MESSAGE, message) self.bot.send_message(self.GROUP_FOR_MESSAGE, message)

View File

@@ -1,3 +1,4 @@
import html
def get_message(username: str, type_message: str): def get_message(username: str, type_message: str):
@@ -37,5 +38,10 @@ def get_message(username: str, type_message: str):
"&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤" "&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤"
"&❗️Но пожалуйста не оскорбляй никого, и будь вежлив." "&❗️Но пожалуйста не оскорбляй никого, и будь вежлив."
} }
if username is None:
# Поведение ожидаемое тестами: TypeError при username=None
raise TypeError("username is None")
message = constants[type_message] message = constants[type_message]
return message.replace('username', username).replace('&', '\n') # Экранируем потенциально проблемные символы для HTML
message = message.replace('username', html.escape(username)).replace('&', '\n')
return 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

@@ -19,6 +19,6 @@ filename = f'{current_dir}/helper_bot_{today}.log'
logger.add( logger.add(
filename, filename,
rotation="00:00", rotation="00:00",
retention="5 days", retention="30 days",
format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {name} | {line} | {message}", format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {name} | {line} | {message}",
) )

View File

@@ -1,4 +1,8 @@
import os import os
import sys
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database.db import BotDB from database.db import BotDB
@@ -8,7 +12,7 @@ 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():

View File

@@ -1,4 +1,8 @@
import os import os
import sys
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database.db import BotDB from database.db import BotDB
@@ -8,7 +12,7 @@ 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():

View File

@@ -1,4 +1,8 @@
import os import os
import sys
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database.db import BotDB from database.db import BotDB
@@ -8,7 +12,7 @@ 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():

View File

@@ -0,0 +1,56 @@
import os
import sys
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database.db import BotDB
# Получаем текущую директорию
current_dir = os.path.dirname(__file__)
# Переходим на уровень выше
parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename():
"""Возвращает имя файла без расширения."""
filename = os.path.basename(__file__)
filename = os.path.splitext(filename)[0]
return filename
def main():
# Проверка версии миграций
current_version = BotDB.get_current_version()
# Выполнение миграций и проверка последней версии
if current_version < 3:
# Скрипт миграции для создания таблицы our_users
create_table_sql = """
CREATE TABLE IF NOT EXISTS "our_users" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
first_name TEXT,
full_name TEXT,
username TEXT,
is_bot BOOLEAN DEFAULT 0,
language_code TEXT,
date_added TEXT,
date_changed TEXT,
has_stickers BOOLEAN DEFAULT 0
);
"""
# Применение миграции
BotDB.create_table(create_table_sql)
filename = get_filename()
BotDB.update_version(3, filename)
if __name__ == "__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,10 @@
import time import time
import html
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()
@@ -22,13 +23,19 @@ def last_message():
message_with_date = '' message_with_date = ''
if much_minutes_ago <= 60: if much_minutes_ago <= 60:
word_minute = plural_time(1, much_minutes_ago) word_minute = plural_time(1, much_minutes_ago)
message_with_date = f'<b>Последнее сообщение было записано {word_minute} назад</b>' # Экранируем потенциально проблемные символы
word_minute_escaped = html.escape(word_minute)
message_with_date = f'<b>Последнее сообщение было записано {word_minute_escaped} назад</b>'
elif much_minutes_ago > 60 and much_hour_ago <= 24: elif much_minutes_ago > 60 and much_hour_ago <= 24:
word_hour = plural_time(2, much_hour_ago) word_hour = plural_time(2, much_hour_ago)
message_with_date = f'<b>Последнее сообщение было записано {word_hour} назад</b>' # Экранируем потенциально проблемные символы
word_hour_escaped = html.escape(word_hour)
message_with_date = f'<b>Последнее сообщение было записано {word_hour_escaped} назад</b>'
elif much_hour_ago > 24: elif much_hour_ago > 24:
word_day = plural_time(3, much_days_ago) word_day = plural_time(3, much_days_ago)
message_with_date = f'<b>Последнее сообщение было записано {word_day} назад</b>' # Экранируем потенциально проблемные символы
word_day_escaped = html.escape(word_day)
message_with_date = f'<b>Последнее сообщение было записано {word_day_escaped} назад</b>'
return message_with_date return message_with_date

View File

@@ -1,5 +1,5 @@
import random import random
import time import asyncio
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -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']
@@ -64,44 +64,44 @@ async def start(message: types.Message, state: FSMContext):
random_stick_hello = FSInputFile(path=random_stick_hello) random_stick_hello = FSInputFile(path=random_stick_hello)
logger.info(f"Стикер успешно получен из БД. Наименование стикера: {name_stick_hello}") logger.info(f"Стикер успешно получен из БД. Наименование стикера: {name_stick_hello}")
await message.answer_sticker(random_stick_hello) await message.answer_sticker(random_stick_hello)
time.sleep(0.3) await asyncio.sleep(0.3)
except Exception as e: except Exception as e:
if LOGS: if LOGS:
await message.bot.send_message(IMPORTANT_LOGS, f'Отправка приветственных стикеров лажает. Ошибка: {e}') await message.bot.send_message(IMPORTANT_LOGS, f'Отправка приветственных стикеров лажает. Ошибка: {e}')
markup = get_main_keyboard() markup = get_main_keyboard()
await message.answer(text="<b>Привет.</b>", parse_mode='html', reply_markup=markup, await message.answer(text="<b>Привет.</b>", parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.3) await asyncio.sleep(0.3)
await message.answer(text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из " await message.answer(text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из "
"Бийска</i>", "Бийска</i>",
parse_mode='html', reply_markup=markup, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
time.sleep(1) await asyncio.sleep(1)
await message.answer(text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не " await message.answer(text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не "
"узнаешь, послушал его кто-то или нет и ответить тоже не получится..", "узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
parse_mode='html', reply_markup=markup, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.8) await asyncio.sleep(0.8)
await message.answer(text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя " await message.answer(text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя "
"бы на 5-10 секунд</i>", "бы на 5-10 секунд</i>",
parse_mode='html', reply_markup=markup, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
time.sleep(1.5) await asyncio.sleep(1.5)
await message.answer(text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, " await message.answer(text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, "
"ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы " "ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы "
"выкладывать в собственные соцсети)", "выкладывать в собственные соцсети)",
parse_mode='html', reply_markup=markup, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
time.sleep(1.3) await asyncio.sleep(1.3)
await message.answer(text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из " await message.answer(text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из "
"недавно полученных или отправленных (или спеть, рассказать стихотворенье)", "недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
parse_mode='html', reply_markup=markup, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.8) await asyncio.sleep(0.8)
await message.answer(text="Так же можешь ознакомиться с инструкцией к боту по команде /help", await message.answer(text="Так же можешь ознакомиться с инструкцией к боту по команде /help",
parse_mode='html', reply_markup=markup, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
time.sleep(0.8) await asyncio.sleep(0.8)
await message.answer(text="<b>ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤", await message.answer(text="<b>ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
parse_mode='html', reply_markup=markup, parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK) disable_web_page_preview=not PREVIEW_LINK)
@@ -147,7 +147,7 @@ async def save_voice_message(message: types.Message, state: FSMContext):
pass pass
# Собираем инфо о сообщении # Собираем инфо о сообщении
author_id = message.from_user.id author_id = message.from_user.id
time_UTC = int(time.time()) time_UTC = int(datetime.now().timestamp())
date_added = datetime.fromtimestamp(time_UTC) date_added = datetime.fromtimestamp(time_UTC)
# Сохраняем в базку # Сохраняем в базку

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()))