16 Commits

Author SHA1 Message Date
8bb098764d Refactor module imports to ensure project root is included in sys.path for both run_helper and voice_bot. Enhance database user insertion with backward compatibility for emoji column. Update user info handling to check username and full name more robustly. Improve emoji handling with fallback options for environments lacking the emoji package. 2025-08-26 18:56:47 +03:00
70d6ad9a6e Merge remote-tracking branch 'origin/voice-1' into merge-voice-1 2025-08-26 18:27:20 +03:00
ANDREY KATYKHIN
94ece4e307 Merge pull request #7 from KerradKerridi/dev-4
Dev 4
2025-08-26 17:41:36 +03:00
61dd85035d Update log retention period to 30 days in custom logger and add comment on log cleanup mechanism. 2025-08-26 17:23:20 +03:00
fee22f8ad4 Enhance database handling and improve HTML safety across the bot. Added async methods for blacklist checks, updated connection settings for SQLite, and implemented HTML escaping for user inputs and messages to prevent potential issues. Adjusted middleware latency and refactored various handlers for better performance and reliability. 2025-08-26 16:51:28 +03:00
7b6abe2a0e WIP: Temporary commit for branch move 2025-08-26 02:14:11 +03:00
Andrey
0c29609e4a hot fix 2024-11-17 22:43:41 +03:00
Andrey
cb0f94c718 some fix 2024-11-17 22:10:45 +03:00
b2c27040aa some fix with emoji function 2024-11-17 22:09:37 +03:00
Andrey
47c5b2f083 hotfix 2024-11-17 01:20:51 +03:00
Andrey
e0e0a6de51 add new func in voice bot 2024-11-17 00:50:55 +03:00
502c07a2c9 some fix 2024-11-16 18:45:05 +03:00
Andrey
ee9eafa09f some fix 2024-11-14 00:24:37 +03:00
ANDREY KATYKHIN
b8b92434ff Merge pull request #5 from KerradKerridi/dev-3
refactor voice_bot
2024-11-14 00:06:13 +03:00
ANDREY KATYKHIN
bc454fce8c Merge pull request #4 from KerradKerridi/dev-3
fix with html tags
2024-10-31 22:00:09 +03:00
ANDREY KATYKHIN
198b522976 Merge pull request #3 from KerradKerridi/dev-3
Dev-3 Добавил новые функции MediaGroup, разные типы файлов, ответы пользователю
2024-07-21 23:24:44 +05:00
57 changed files with 4483 additions and 1369 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):
self.db_file = os.path.join(current_dir, name) # Формируем правильный путь к базе данных
if name.startswith('database/'):
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):
@@ -100,27 +111,8 @@ class BotDB:
finally: finally:
self.close() self.close()
# TODO: Deprecated. Остался только в voice боте, удалить и оттуда
def get_error_message_from_db(self, id: int):
"""
@deprecated
Функция для запроса к базе данных и получения сообщений ошибки. В аргументы передаются:
id - идентификатор ошибки
"""
# Подключаемся к базе
try:
self.connect()
self.cursor.execute(f"SELECT * FROM error_messages WHERE id=?", (id,))
response_from_database = str(self.cursor.fetchone()[1])
return response_from_database
except sqlite3.Error as error:
self.logger.error(f"Ошибка при получении сообщения об ошибка voice_bot: {error}")
finally:
self.close()
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str, username: str, is_bot: bool, def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str, username: str, is_bot: bool,
language_code: str, date_added: str, language_code: str, emoji: str, date_added: str, date_changed: str):
date_changed: str):
""" """
Добавляет нового пользователя в базу данных. Добавляет нового пользователя в базу данных.
@@ -131,6 +123,7 @@ class BotDB:
username (str): Username пользователя в Telegram. username (str): Username пользователя в Telegram.
is_bot (bool): Флаг, указывающий, является ли пользователь ботом. is_bot (bool): Флаг, указывающий, является ли пользователь ботом.
language_code (str): Код языка пользователя. language_code (str): Код языка пользователя.
emoji (str): Эмодзи закрепленная за пользователем
date_added (str): Дата добавления пользователя в базу. date_added (str): Дата добавления пользователя в базу.
date_changed (str): Дата последнего изменения данных пользователя. date_changed (str): Дата последнего изменения данных пользователя.
@@ -141,12 +134,26 @@ class BotDB:
self.logger.info(f"Попытка добавить пользователя в базу данных: user_id={user_id}, first_name={first_name}") self.logger.info(f"Попытка добавить пользователя в базу данных: user_id={user_id}, first_name={first_name}")
try: try:
self.connect() self.connect()
self.cursor.execute("INSERT INTO 'our_users' ('user_id', 'first_name', 'full_name', 'username', 'is_bot', " try:
"'language_code', 'date_added', 'date_changed') VALUES (?, ?, ?, ?, ?, ?, ?, ?)", # Новая схема с колонкой emoji
(user_id, first_name, full_name, self.cursor.execute(
username, is_bot, language_code, date_added, date_changed)) "INSERT INTO 'our_users' ('user_id', 'first_name', 'full_name', 'username', 'is_bot', "
"'language_code', 'emoji', 'date_added', 'date_changed') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(user_id, first_name, full_name, username, is_bot, language_code, emoji, date_added, date_changed)
)
except sqlite3.OperationalError as e:
# Обратная совместимость: старая схема без колонки emoji
if 'no column named emoji' in str(e):
self.cursor.execute(
"INSERT INTO 'our_users' ('user_id', 'first_name', 'full_name', 'username', 'is_bot', "
"'language_code', 'date_added', 'date_changed') VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(user_id, first_name, full_name, username, is_bot, language_code, date_added, date_changed)
)
else:
raise
self.conn.commit() self.conn.commit()
self.logger.info(f"Новый пользователь добавлен в базу: user_id={user_id}, first_name={first_name}") self.logger.info(
f"Новый пользователь добавлен в базу: user_id={user_id}, first_name={first_name}, emoji={emoji}")
return None return None
except sqlite3.Error as error: except sqlite3.Error as error:
self.logger.error(f"Ошибка при добавлении пользователя в базу: {error}. " self.logger.error(f"Ошибка при добавлении пользователя в базу: {error}. "
@@ -301,6 +308,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 из базы данных.
@@ -675,15 +716,15 @@ class BotDB:
def update_date_for_user(self, date: str, user_id: int): def update_date_for_user(self, date: str, user_id: int):
""" """
#TODO: Не возвращается ошибка sqlite3. Error. Тест не перехватывает. Возвращается no such table: our_users #TODO: Не возвращается ошибка sqlite3. Error. Тест не перехватывает. Возвращается no such table: our_users
Обновляет дату последнего изменения данных пользователя в базе. Обновляет дату последнего изменения данных пользователя в базе
Args: Args:
date (str): Новая дата изменения. date (str): Новая дата изменения
user_id (int): Идентификатор пользователя в Telegram. user_id (int): Идентификатор пользователя в Telegram
Returns: Returns:
None: Если обновление прошло успешно. None: Если обновление прошло успешно
sqlite3. Error: Если произошла ошибка при выполнении запроса. sqlite3. Error: Если произошла ошибка при выполнении запроса
""" """
self.logger.info(f"Запуск функции update_date_for_user: user_id={user_id}, date={date}") self.logger.info(f"Запуск функции update_date_for_user: user_id={user_id}, date={date}")
try: try:
@@ -699,6 +740,107 @@ class BotDB:
finally: finally:
self.close() self.close()
def check_emoji(self, emoji: str):
"""
Проверяет, есть ли уже такой emoji в таблице.
Args:
emoji: emoji для проверки.
Returns:
True, если эмодзи уже есть, иначе False.
Raises:
None: В случае ошибки возвращается None
"""
self.logger.info(f"Запуск функции check_emoji: emoji={emoji}")
try:
self.connect()
self.cursor.execute("SELECT 1 FROM our_users WHERE emoji = ?", (emoji,))
result = self.cursor.fetchone()
return bool(result)
except sqlite3.Error as error:
self.logger.error(f"Ошибка проверки эмодзи в базе: {error}")
return None
finally:
self.close()
def update_emoji_for_user(self, user_id: int, emoji: str):
"""
Обновляет эмодзи для пользователя в базе если его ранее не было установлено
Args:
user_id (int): Идентификатор пользователя в Telegram
emoji (str): Эмодзи пользователя
Returns:
None: Если обновление прошло успешно
sqlite3. Error: Если произошла ошибка при выполнении запроса
"""
self.logger.info(f"Запуск функции update_emoji_for_user: user_id={user_id}, emoji={emoji}")
try:
self.connect()
self.cursor.execute("UPDATE our_users SET emoji = ? WHERE user_id = ?",
(emoji, user_id,))
self.conn.commit()
self.logger.info(f"Эмоджи обновлен для пользователя: user_id={user_id}")
except sqlite3.Error as error:
self.logger.error(f"Ошибка обновления эмодзи для пользователя: {error}")
return error
finally:
self.close()
def check_emoji_for_user(self, user_id: int):
"""
Проверяет, есть ли уже у пользователя назначенный emoji.
Args:
user_id: user_id пользователя.
Returns:
True, если эмодзи такого нет, иначе False.
Raises:
error: В случае ошибки возвращается error
"""
self.logger.info(f"Запуск функции check_emoji_for_user: user_id={user_id}")
try:
self.connect()
self.cursor.execute("SELECT emoji FROM our_users WHERE user_id = ?", (user_id,))
pre_result = self.cursor.fetchone()
# Возвращаем "Смайл не определен", если pre_result или pre_result[0] is None
result = pre_result[0] if pre_result else None
return str(result) if result is not None else "Смайл еще не определен"
except sqlite3.Error as error:
self.logger.error(f"Ошибка проверки эмодзи в базе: {error}")
return error
finally:
self.close()
def refresh_listen_audio(self, user_id: int):
"""
Очищает всю информацию о прослушанных аудио пользователем
Args:
user_id: user_id пользователя.
Returns:
None - если все очищено успешно
Raises:
error: В случае ошибки возвращается error
"""
self.logger.info(f"Запуск функции check_emoji_for_user: user_id={user_id}")
try:
self.connect()
self.cursor.execute("DELETE FROM listen_audio_users WHERE user_id = ?", (user_id,))
return None
except sqlite3.Error as error:
self.logger.error(f"Ошибка проверки эмодзи в базе: {error}")
return error
finally:
self.close()
def is_admin(self, user_id: int): def is_admin(self, user_id: int):
""" """
Проверяет, является ли пользователь администратором. Проверяет, является ли пользователь администратором.
@@ -719,7 +861,7 @@ class BotDB:
result = self.cursor.fetchone() result = self.cursor.fetchone()
return bool(result) return bool(result)
except sqlite3.Error as error: except sqlite3.Error as error:
self.logger.error(f"Ошибка добавления сообщения в базу данных: {error}") self.logger.error(f"Ошибка проверки прав пользователя админа: {error}")
return None return None
finally: finally:
self.close() self.close()
@@ -963,6 +1105,62 @@ class BotDB:
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка в функции get_author_id_by_helper_message_id {str(e)}") self.logger.error(f"Ошибка в функции get_author_id_by_helper_message_id {str(e)}")
def get_user_id_by_message_id_for_voice_bot(self, message_id: int):
self.logger.info(f"Запуск функции get_user_id_by_message_id_for_voice_bot, идентификатор поста "
f"{message_id}")
try:
self.connect()
result = self.cursor.execute("SELECT user_id "
"FROM audio_moderate WHERE message_id = ?",
(message_id,))
user_id = result.fetchone()[0]
self.logger.info(f"Функция get_user_id_by_message_id_for_voice_bot получила author_id {user_id}")
return user_id
except Exception as e:
self.logger.error(f"Ошибка в функции get_user_id_by_message_id_for_voice_bot {str(e)}")
def set_user_id_and_message_id_for_voice_bot(self, message_id: int, user_id: int):
self.logger.info(f"Запуск функции set_user_id_and_message_id_for_voice_bot, идентификатор поста "
f"{message_id}, user_id {user_id}")
try:
self.connect()
result = self.cursor.execute(
"INSERT INTO audio_moderate (message_id, user_id)"
"VALUES (?, ?)", (message_id, user_id))
self.conn.commit()
self.logger.info(f"Функция set_user_id_and_message_id_for_voice_bot отработала успешно")
return True
except Exception as e:
self.logger.error(f"Ошибка в функции set_user_id_and_message_id_for_voice_bot {str(e)}")
def get_user_id_by_file_name(self, file_name: str):
self.logger.info(f"Запуск функции get_user_id_by_file_name, идентификатор файла "
f"{file_name}")
try:
self.connect()
result = self.cursor.execute("SELECT author_id "
"FROM audio_message_reference WHERE file_name = ?",
(file_name,))
user_id = result.fetchone()[0]
self.logger.info(f"Функция get_user_id_by_file_name получила user_id {user_id}")
return user_id
except Exception as e:
self.logger.error(f"Ошибка в функции get_user_id_by_file_name {str(e)}")
def get_date_by_file_name(self, file_name: str):
self.logger.info(f"Запуск функции get_date_by_file_name, идентификатор файла "
f"{file_name}")
try:
self.connect()
result = self.cursor.execute("SELECT date_added "
"FROM audio_message_reference WHERE file_name = ?",
(file_name,))
date_added = result.fetchone()[0]
self.logger.info(f"Функция get_date_by_file_name получила date_added {date_added}")
return date_added
except Exception as e:
self.logger.error(f"Ошибка в функции get_date_by_file_name {str(e)}")
def add_post_content_in_db(self, post_id: int, message_id: int, content_name: str, type_content: str): def add_post_content_in_db(self, post_id: int, message_id: int, content_name: str, type_content: str):
self.logger.info( self.logger.info(
f"Запуск функции add_post_content_in_db: post_id={post_id}, message_id={message_id}, " f"Запуск функции add_post_content_in_db: post_id={post_id}, message_id={message_id}, "
@@ -1033,7 +1231,7 @@ class BotDB:
f"date_added = {date_added}") f"date_added = {date_added}")
return None return None
except sqlite3.Error as error: except sqlite3.Error as error:
print(error) self.logger.error(f"Ошибка при добавлении войса в базу: {error}")
raise raise
finally: finally:
self.close() self.close()
@@ -1073,6 +1271,24 @@ class BotDB:
finally: finally:
self.close() self.close()
def delete_listen_count_for_user(self, user_id):
"""Удаляет данные о прослушанных пользователем аудио"""
self.logger.info(
f"Запуск функции delete_listen_count_for_user. user_id={user_id}")
try:
self.connect()
self.cursor.execute("DELETE FROM `listen_audio_users` WHERE `user_id` = ?",
(user_id,))
self.conn.commit()
self.logger.info(
f"Функция delete_listen_count_for_user успешно отработала")
return None
except sqlite3.Error as error:
self.logger.error(f"Ошибка удаления записей прослушивания по пользователю: {error}")
raise
finally:
self.close()
def get_id_for_audio_record(self, user_id): def get_id_for_audio_record(self, user_id):
"""Получает ID аудио сообщения пользователя""" """Получает ID аудио сообщения пользователя"""
self.logger.info( self.logger.info(
@@ -1167,3 +1383,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,10 +16,11 @@ 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, send_video_message, \
send_video_message, send_video_note_message, send_audio_message, send_voice_message, add_in_db_media send_video_note_message, send_audio_message, send_voice_message, add_in_db_media, \
check_user_emoji, check_username_and_full_name
from logs.custom_logger import logger from logs.custom_logger import logger
private_router = Router() private_router = Router()
@@ -26,7 +28,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 +40,38 @@ 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(
ChatTypeFilter(chat_type=["private"]),
Command("emoji")
)
async def handle_emoji_message(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
user_emoji = check_user_emoji(message)
await state.set_state("START")
if user_emoji is not None:
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("restart")
)
async def handle_restart_message(message: types.Message, state: FSMContext):
try:
markup = get_reply_keyboard(BotDB, message.from_user.id)
await message.forward(chat_id=GROUP_FOR_LOGS)
await state.set_state("START")
await update_user_info('love', message)
check_user_emoji(message)
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
except Exception as e:
logger.error(f"Произошла ошибка handle_restart_message. Ошибка:{str(e)}")
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
text=f"Произошла ошибка handle_restart_message: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message( @private_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
@@ -56,20 +90,36 @@ 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 +130,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 +146,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 +186,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 +216,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 +237,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 +250,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 +297,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 +319,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 +340,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 +362,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 +379,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 +401,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 +431,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 +445,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 +457,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 +484,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

@@ -1,41 +1,102 @@
import html import html
import os import os
import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep
try:
import emoji as _emoji_lib
except Exception:
_emoji_lib = None
from aiogram import types from aiogram import types
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, 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()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
if _emoji_lib is not None:
emoji_list = list(_emoji_lib.EMOJI_DATA.keys())
else:
# Fallback minimal emoji set for environments without the 'emoji' package (e.g., CI tests)
emoji_list = [
"🙂", "😀", "😉", "😎", "🤖", "🦄", "🐱", "🐶", "🍀", "🔥",
"🌟", "🎉", "💡", "🚀", "🌈"
]
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) """
return 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 ""
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 +138,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 +160,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 +176,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 +192,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 +214,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 +270,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 +300,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 +321,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 +358,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 +393,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 +406,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 +471,58 @@ 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)
async def update_user_info(source: str, message: types.Message):
# Собираем данные
full_name = message.from_user.full_name
username = message.from_user.username
first_name = get_first_name(message)
is_bot = message.from_user.is_bot
language_code = message.from_user.language_code
user_id = message.from_user.id
current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
# Выбираем эмодзю, пробегаемся циклом и смотрим что в базе такого еще не было
user_emoji = get_random_emoji()
if not BotDB.user_exists(user_id):
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, user_emoji, date,
date)
else:
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
if is_need_update:
BotDB.update_username_and_full_name(user_id, username, full_name)
if source != 'voice':
await message.answer(
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
sleep(1)
BotDB.update_date_for_user(date, user_id)
def check_user_emoji(message: types.Message):
user_id = message.from_user.id
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
user_emoji = get_random_emoji()
BotDB.update_emoji_for_user(user_id=user_id, emoji=user_emoji)
return user_emoji
def get_random_emoji():
attempts = 0
while attempts < 100:
user_emoji = random.choice(emoji_list)
if not BotDB.check_emoji(user_emoji):
return user_emoji
attempts += 1
logger.error("Не удалось найти уникальный эмодзи после нескольких попыток.")
return "Эмоджи не определен"

View File

@@ -1,3 +1,4 @@
import html
def get_message(username: str, type_message: str): def get_message(username: str, type_message: str):
@@ -25,7 +26,7 @@ def get_message(username: str, type_message: str):
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️", "&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
"DEL_MESSAGE": "username, напиши свое обращение или предложение✍" "DEL_MESSAGE": "username, напиши свое обращение или предложение✍"
"&Мы рассмотрим и ответим тебе в ближайшее время☺❤", "&Мы рассмотрим и ответим тебе в ближайшее время☺❤",
"BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /start" "BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /restart"
"&&И тебе пока!👋🏼❤️", "&&И тебе пока!👋🏼❤️",
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.", "USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
"QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉", "QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉",
@@ -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

@@ -20,3 +20,4 @@ attrs~=23.2.0
typing_extensions~=4.12.2 typing_extensions~=4.12.2
aiohttp~=3.9.5 aiohttp~=3.9.5
aiogram~=3.10.0 aiogram~=3.10.0
emoji~=2.14.0

View File

@@ -1,7 +1,14 @@
import asyncio import asyncio
import os
import sys
# Ensure project root is on sys.path for module resolution
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
if CURRENT_DIR not in sys.path:
sys.path.insert(0, CURRENT_DIR)
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()
@@ -252,16 +252,17 @@ def test_add_new_user_in_db(bot):
username = "@petr_ivanov" username = "@petr_ivanov"
is_bot = False is_bot = False
language_code = "ru" language_code = "ru"
emoji = '🦀'
date_added = "2024-07-09" date_added = "2024-07-09"
date_changed = "2024-07-09" date_changed = "2024-07-09"
# Вызываем функцию add_new_user_in_db # Вызываем функцию add_new_user_in_db
bot.add_new_user_in_db( bot.add_new_user_in_db(
user_id, first_name, full_name, username, is_bot, language_code, date_added, date_changed user_id, first_name, full_name, username, is_bot, language_code, emoji, date_added, date_changed
) )
# Проверяем наличие записи в базе данных # Проверяем наличие записи в базе данных
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()
@@ -285,7 +286,7 @@ def test_add_new_user_in_db_duplicate_user_id(bot, setup_db):
# Попытка добавить пользователя с тем же user_id # Попытка добавить пользователя с тем же user_id
with pytest.raises(sqlite3.IntegrityError): with pytest.raises(sqlite3.IntegrityError):
bot.add_new_user_in_db( bot.add_new_user_in_db(
user_id, "Марина", "Марина Альфредовна", "marina", False, "bg", "2024-07-09", "2024-07-09" user_id, "Марина", "Марина Альфредовна", "marina", False, "bg", "🦀", "2024-07-09", "2024-07-09"
) )
@@ -297,16 +298,17 @@ def test_add_new_user_in_db_empty_first_name(bot):
username = "@boris" username = "@boris"
is_bot = False is_bot = False
language_code = "fr" language_code = "fr"
emoji = "🦀"
date_added = "2024-07-09" date_added = "2024-07-09"
date_changed = "2024-07-09" date_changed = "2024-07-09"
# Вызываем функцию add_new_user_in_db # Вызываем функцию add_new_user_in_db
bot.add_new_user_in_db( bot.add_new_user_in_db(
user_id, first_name, full_name, username, is_bot, language_code, date_added, date_changed user_id, first_name, full_name, username, is_bot, language_code, emoji, date_added, date_changed
) )
# Проверяем наличие записи в базе данных # Проверяем наличие записи в базе данных
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 +390,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 +483,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 +497,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 +514,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 +621,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 +660,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 +693,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 +743,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 +770,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 +803,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 +820,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

@@ -0,0 +1,68 @@
import time
from datetime import datetime
from pathlib import Path
from aiogram import Router, F
from aiogram.types import CallbackQuery
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory
callback_router = Router()
bdf = BaseDependencyFactory()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db()
@callback_router.callback_query(
F.data == "save"
)
async def save_voice_message(call: CallbackQuery):
file_name = ''
file_id = 1
user_id = BotDB.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
# Проверяем что запись о файле есть в базе данных
is_having_audio_from_user = BotDB.get_last_user_audio_record(user_id=user_id)
if is_having_audio_from_user is False:
# Если нет, то генерируем имя файла
file_name = f'message_from_{user_id}_number_{file_id}'
else:
# Иначе берем последнюю запись из БД, добавляем к ней 1, и создаем новую запись
file_name = BotDB.get_path_for_audio_record(user_id=user_id)
file_id = BotDB.get_id_for_audio_record(user_id) + 1
path = Path(f'voice_users/{file_name}.ogg')
if path.exists():
file_name = f'message_from_{user_id}_number_{file_id}'
else:
pass
# Собираем инфо о сообщении
time_UTC = int(time.time())
date_added = datetime.fromtimestamp(time_UTC)
# Сохраняем в базку
BotDB.add_audio_record(file_name, user_id, date_added, 0, file_id)
file_info = await call.message.bot.get_file(file_id=call.message.voice.file_id)
downloaded_file = await call.message.bot.download_file(file_path=file_info.file_path)
with open(f'voice_users/{file_name}.ogg', 'wb') as new_file:
new_file.write(downloaded_file.read())
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
await call.answer(text='Сохранено!', cache_time=3)
@callback_router.callback_query(
F.data == "delete"
)
async def delete_voice_message(call: CallbackQuery):
# Получаем из базы автора + отправляем сообщение + удаляем сообщение из предложки
await call.bot.delete_message(chat_id=GROUP_FOR_POST, message_id=call.message.message_id)
await call.answer(text='Удалено!', cache_time=3)

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,22 +9,65 @@ 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.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import update_user_info, check_user_emoji, send_voice_message
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, get_reply_keyboard_for_voice
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']
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs'] IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']
PREVIEW_LINK = bdf.settings['Telegram']['preview_link'] PREVIEW_LINK = bdf.settings['Telegram']['preview_link']
LOGS = bdf.settings['Settings']['logs'] LOGS = bdf.settings['Settings']['logs']
TEST = bdf.settings['Settings']['test'] TEST = bdf.settings['Settings']['test']
BotDB = bdf.get_db() BotDB = bdf.get_db()
voice_router.message.middleware(BlacklistMiddleware())
@voice_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)
await update_user_info('voice', message)
check_user_emoji(message)
markup = get_main_keyboard()
await message.answer(text='Я перезапущен!',
reply_markup=markup)
await state.set_state('START')
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("emoji")
)
async def handle_emoji_message(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
user_emoji = check_user_emoji(message)
await state.set_state("START")
if user_emoji is not None:
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("help")
)
async def help_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
await update_user_info('voice', message)
await message.answer(
text='Скорее всего ответы на твои вопросы есть здесь, ознакомься: https://telegra.ph/Instrukciya-k-botu-Golosa-Bijsk-10-11-2'
'\nЕсли это не поможет, пиши в личку: @Kerrad1', disable_web_page_preview=not PREVIEW_LINK)
await state.set_state('START')
@voice_router.message( @voice_router.message(
@@ -34,55 +77,78 @@ BotDB = bdf.get_db()
async def start(message: types.Message, state: FSMContext): async def start(message: types.Message, state: FSMContext):
await state.set_state("START") await state.set_state("START")
await message.forward(chat_id=GROUP_FOR_LOGS) await message.forward(chat_id=GROUP_FOR_LOGS)
await update_user_info('voice', message)
user_emoji = check_user_emoji(message)
try: try:
name_stick_hello = list(Path('Stick').rglob('Hello_*')) name_stick_hello = list(Path('Stick').rglob('Hello_*'))
random_stick_hello = random.choice(name_stick_hello) random_stick_hello = random.choice(name_stick_hello)
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=f"Любые войсы будут помечены эмоджи. <b>Твой эмоджи - </b>{user_emoji}"
f"Таким эмоджи будут помечены твои сообщения для других "
f"Но другие люди не узнают кто за каким эмоджи скрывается:)",
parse_mode='html', reply_markup=markup,
disable_web_page_preview=not PREVIEW_LINK)
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)
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("refresh")
)
async def refresh_listen_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
await update_user_info('voice', message)
markup = get_main_keyboard()
BotDB.delete_listen_count_for_user(message.from_user.id)
await message.answer(
text='Прослушивания очищены. Можешь начать слушать заново🤗', disable_web_page_preview=not PREVIEW_LINK,
markup=markup)
await state.set_state('START')
@voice_router.message( @voice_router.message(
StateFilter("START"), StateFilter("START"),
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
@@ -92,53 +158,33 @@ async def standup_write(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS) await message.forward(chat_id=GROUP_FOR_LOGS)
markup = types.ReplyKeyboardRemove() markup = types.ReplyKeyboardRemove()
await message.answer(text='Хорошо, теперь пришли мне свое голосовое сообщение', reply_markup=markup) await message.answer(text='Хорошо, теперь пришли мне свое голосовое сообщение', reply_markup=markup)
message_with_date = last_message() try:
await message.answer(text=message_with_date, parse_mode="html") message_with_date = last_message()
await message.answer(text=message_with_date, parse_mode="html")
except Exception as e:
logger.error(f'Не удалось получить дату последнего сообщения - {e}')
await state.set_state('STANDUP_WRITE') await state.set_state('STANDUP_WRITE')
@voice_router.message( @voice_router.message(
StateFilter("STANDUP_WRITE"), StateFilter("STANDUP_WRITE"),
ChatTypeFilter(chat_type=["private"]) ChatTypeFilter(chat_type=["private"]),
) )
async def save_voice_message(message: types.Message, state: FSMContext): async def suggest_voice(message: types.Message, state: FSMContext):
logger.info(
f"Вызов функции suggest_voice. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}")
await message.forward(chat_id=GROUP_FOR_LOGS)
markup = get_main_keyboard() markup = get_main_keyboard()
if message.content_type == 'voice': if message.content_type == 'voice':
await message.forward(chat_id=GROUP_FOR_LOGS) markup_for_voice = get_reply_keyboard_for_voice()
file_name = '' # Отправляем аудио в приватный канал
file_id = 1 sent_message = await send_voice_message(GROUP_FOR_POST, message,
# Проверяем что запись о файле есть в базе данных message.voice.file_id, markup_for_voice)
is_having_audio_from_user = BotDB.get_last_user_audio_record(user_id=message.from_user.id)
if is_having_audio_from_user is False:
# Если нет, то генерируем имя файла
file_name = f'message_from_{message.from_user.id}_number_{file_id}'
else:
# Иначе берем последнюю запись из БД, добавляем к ней 1, и создаем новую запись
file_name = BotDB.get_path_for_audio_record(user_id=message.from_user.id)
file_id = BotDB.get_id_for_audio_record(message.from_user.id) + 1
path = Path(f'voice_users/{file_name}.ogg')
if path.exists():
file_name = f'message_from_{message.from_user.id}_number_{file_id}'
else:
pass
# Собираем инфо о сообщении
author_id = message.from_user.id
time_UTC = int(time.time())
date_added = datetime.fromtimestamp(time_UTC)
# Сохраняем в базку # Сохраняем в базу инфо о посте
BotDB.add_audio_record(file_name, author_id, date_added, 0, file_id) BotDB.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id)
# Сохраняем файл на сервер # Отправляем юзеру ответ и возвращаем его в меню
# file_info = message.bot.get_file(file_id=message.voice.file_id)
# downloaded_file = message.bot.download_file(file_path=file_info.file_path)
# with open(f'voice_users/{file_name}.ogg', 'wb') as new_file:
# new_file.write(downloaded_file)
file_info = await message.bot.get_file(file_id=message.voice.file_id)
downloaded_file = await message.bot.download_file(file_path=file_info.file_path)
with open(f'voice_users/{file_name}.ogg', 'wb') as new_file:
new_file.write(downloaded_file.read())
await message.answer(text='Окей, сохранил!👌', reply_markup=markup) await message.answer(text='Окей, сохранил!👌', reply_markup=markup)
await state.set_state('START') await state.set_state('START')
else: else:
@@ -153,48 +199,38 @@ async def save_voice_message(message: types.Message, state: FSMContext):
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
F.text == '🎧Послушать' F.text == '🎧Послушать'
) )
async def standup_listen_audio(message: types.Message, state: FSMContext): async def standup_listen_audio(message: types.Message):
check_audio = BotDB.check_listen_audio(user_id=message.from_user.id) check_audio = BotDB.check_listen_audio(user_id=message.from_user.id)
list_audio = list(check_audio) list_audio = list(check_audio)
markup = get_main_keyboard() markup = get_main_keyboard()
await message.forward(chat_id=GROUP_FOR_LOGS)
if not list_audio: if not list_audio:
await message.answer(text='Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится', await message.answer(text='Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится',
reply_markup=markup) reply_markup=markup)
message_with_date = last_message() try:
message.send_message(chat_id=message.chat.id, text=message_with_date, parse_mode="html") message_with_date = last_message()
await message.answer(text=message_with_date, parse_mode="html")
except Exception as e:
logger.error(f'Не удалось получить последнюю дату {e}')
else: else:
# Получаем ссылку на аудио сообщение пользователя
number_element = random.randint(0, len(list_audio) - 1) number_element = random.randint(0, len(list_audio) - 1)
audio_for_user = check_audio[number_element] audio_for_user = check_audio[number_element]
# Получаем автора записи + эмодзи по нему
user_id = BotDB.get_user_id_by_file_name(audio_for_user)
date_added = BotDB.get_date_by_file_name(audio_for_user)
user_emoji = BotDB.check_emoji_for_user(user_id)
path = Path(f'voice_users/{audio_for_user}.ogg') path = Path(f'voice_users/{audio_for_user}.ogg')
# voice = open(path, 'rb')
voice = FSInputFile(path) voice = FSInputFile(path)
# Маркируем сообщение как прослушанное # Маркируем сообщение как прослушанное
BotDB.mark_listened_audio(audio_for_user, user_id=message.from_user.id) BotDB.mark_listened_audio(audio_for_user, user_id=message.from_user.id)
await message.bot.send_voice(message.chat.id, voice=voice, reply_markup=markup)
await message.forward(chat_id=GROUP_FOR_LOGS)
await state.set_state('START')
# Формируем подпись
@voice_router.message( if user_emoji:
ChatTypeFilter(chat_type=["private"]), caption = f'{user_emoji}\nДата записи: {date_added}'
Command("restart") else:
) caption = f'Дата записи: {date_added}'
async def restart_function(message: types.Message, state: FSMContext): await message.bot.send_voice(chat_id=message.chat.id, voice=voice, caption=caption, reply_markup=markup)
await message.forward(chat_id=GROUP_FOR_LOGS) await message.answer(text=f'Осталось непрослушанных: <b>{len(check_audio) - 1}</b>', reply_markup=markup)
markup = get_main_keyboard()
await message.answer(text='Я перезапущен!',
reply_markup=markup)
await state.set_state('START')
@voice_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("help")
)
async def help_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
await message.answer(
text='Скорее всего ответы на твои вопросы есть здесь, ознакомься: https://telegra.ph/Instrukciya-k-botu-Golosa-Bijsk-10-11-2'
'\nЕсли это не поможет, пиши в тг: @Kerrad1', disable_web_page_preview=not PREVIEW_LINK)
await state.set_state('START')

View File

@@ -1,5 +1,5 @@
from aiogram import types from aiogram import types
from aiogram.utils.keyboard import ReplyKeyboardBuilder from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
def get_main_keyboard(): def get_main_keyboard():
@@ -8,3 +8,15 @@ def get_main_keyboard():
builder.add(types.KeyboardButton(text="🎧Послушать")) builder.add(types.KeyboardButton(text="🎧Послушать"))
markup = builder.as_markup(resize_keyboard=True) markup = builder.as_markup(resize_keyboard=True)
return markup return markup
def get_reply_keyboard_for_voice():
builder = InlineKeyboardBuilder()
builder.row(types.InlineKeyboardButton(
text="Сохранить", callback_data="save")
)
builder.row(types.InlineKeyboardButton(
text="Удалить", callback_data="delete")
)
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
return markup

View File

@@ -1,9 +1,19 @@
import os
import sys
# Ensure project root is on sys.path for module resolution when running voice bot directly
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(CURRENT_DIR)
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties from aiogram.client.default import DefaultBotProperties
from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.fsm.strategy import FSMStrategy from aiogram.fsm.strategy import FSMStrategy
from voice_bot.voice_handler.voice_handler import voice_router from voice_bot.handlers.callback_handler import callback_router
from voice_bot.handlers.voice_handler import voice_router
async def start_bot(bdf): async def start_bot(bdf):
@@ -13,6 +23,6 @@ async def start_bot(bdf):
link_preview_is_disabled=bdf.settings['Telegram']['preview_link'] link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
)) ))
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER) dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
dp.include_routers(voice_router) dp.include_routers(voice_router, callback_router)
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot, skip_updates=True) await dp.start_polling(bot, skip_updates=True)

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()
@@ -11,6 +12,9 @@ BotDB = bdf.get_db()
def last_message(): def last_message():
# функция с отображением сообщения "Последнее сообщение было записано" # функция с отображением сообщения "Последнее сообщение было записано"
date_from_db = BotDB.last_date_audio() date_from_db = BotDB.last_date_audio()
if date_from_db is None:
return None
parse_date = datetime.strptime(date_from_db, "%Y-%m-%d %H:%M:%S") parse_date = datetime.strptime(date_from_db, "%Y-%m-%d %H:%M:%S")
last_voice_time_timestamp = time.mktime(parse_date.timetuple()) last_voice_time_timestamp = time.mktime(parse_date.timetuple())
time_now_timestamp = time.time() time_now_timestamp = time.time()
@@ -22,13 +26,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,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()))