Compare commits
16 Commits
dev-3
...
merge-voic
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bb098764d | |||
| 70d6ad9a6e | |||
|
|
94ece4e307 | ||
| 61dd85035d | |||
| fee22f8ad4 | |||
| 7b6abe2a0e | |||
|
|
0c29609e4a | ||
|
|
cb0f94c718 | ||
| b2c27040aa | |||
|
|
47c5b2f083 | ||
|
|
e0e0a6de51 | ||
| 502c07a2c9 | |||
|
|
ee9eafa09f | ||
|
|
b8b92434ff | ||
|
|
bc454fce8c | ||
|
|
198b522976 |
37
.dockerignore
Normal file
37
.dockerignore
Normal 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
42
.gitignore
vendored
@@ -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
|
||||
/myenv/
|
||||
/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
37
Dockerfile
Normal 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
73
Makefile
Normal 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
259
README_TESTING.md
Normal 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"
|
||||
```
|
||||
294
database/db.py
294
database/db.py
@@ -1,21 +1,32 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class BotDB:
|
||||
def __init__(self, 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.cursor = None
|
||||
self.logger = logger
|
||||
self.logger.info(f'Инициация базы данных: {self.db_file}')
|
||||
# Создаем пул потоков для асинхронных операций
|
||||
self.executor = ThreadPoolExecutor(max_workers=4)
|
||||
|
||||
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()
|
||||
|
||||
def create_table(self, sql_script):
|
||||
@@ -100,27 +111,8 @@ class BotDB:
|
||||
finally:
|
||||
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,
|
||||
language_code: str, date_added: str,
|
||||
date_changed: str):
|
||||
language_code: str, emoji: str, date_added: str, date_changed: str):
|
||||
"""
|
||||
Добавляет нового пользователя в базу данных.
|
||||
|
||||
@@ -131,6 +123,7 @@ class BotDB:
|
||||
username (str): Username пользователя в Telegram.
|
||||
is_bot (bool): Флаг, указывающий, является ли пользователь ботом.
|
||||
language_code (str): Код языка пользователя.
|
||||
emoji (str): Эмодзи закрепленная за пользователем
|
||||
date_added (str): Дата добавления пользователя в базу.
|
||||
date_changed (str): Дата последнего изменения данных пользователя.
|
||||
|
||||
@@ -141,12 +134,26 @@ class BotDB:
|
||||
self.logger.info(f"Попытка добавить пользователя в базу данных: user_id={user_id}, first_name={first_name}")
|
||||
try:
|
||||
self.connect()
|
||||
self.cursor.execute("INSERT INTO 'our_users' ('user_id', 'first_name', 'full_name', 'username', 'is_bot', "
|
||||
try:
|
||||
# Новая схема с колонкой emoji
|
||||
self.cursor.execute(
|
||||
"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))
|
||||
(user_id, first_name, full_name, username, is_bot, language_code, date_added, date_changed)
|
||||
)
|
||||
else:
|
||||
raise
|
||||
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
|
||||
except sqlite3.Error as error:
|
||||
self.logger.error(f"Ошибка при добавлении пользователя в базу: {error}. "
|
||||
@@ -301,6 +308,40 @@ class BotDB:
|
||||
finally:
|
||||
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):
|
||||
"""
|
||||
Возвращает список всех user_id из базы данных.
|
||||
@@ -675,15 +716,15 @@ class BotDB:
|
||||
def update_date_for_user(self, date: str, user_id: int):
|
||||
"""
|
||||
#TODO: Не возвращается ошибка sqlite3. Error. Тест не перехватывает. Возвращается no such table: our_users
|
||||
Обновляет дату последнего изменения данных пользователя в базе.
|
||||
Обновляет дату последнего изменения данных пользователя в базе
|
||||
|
||||
Args:
|
||||
date (str): Новая дата изменения.
|
||||
user_id (int): Идентификатор пользователя в Telegram.
|
||||
date (str): Новая дата изменения
|
||||
user_id (int): Идентификатор пользователя в Telegram
|
||||
|
||||
Returns:
|
||||
None: Если обновление прошло успешно.
|
||||
sqlite3. Error: Если произошла ошибка при выполнении запроса.
|
||||
None: Если обновление прошло успешно
|
||||
sqlite3. Error: Если произошла ошибка при выполнении запроса
|
||||
"""
|
||||
self.logger.info(f"Запуск функции update_date_for_user: user_id={user_id}, date={date}")
|
||||
try:
|
||||
@@ -699,6 +740,107 @@ class BotDB:
|
||||
finally:
|
||||
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):
|
||||
"""
|
||||
Проверяет, является ли пользователь администратором.
|
||||
@@ -719,7 +861,7 @@ class BotDB:
|
||||
result = self.cursor.fetchone()
|
||||
return bool(result)
|
||||
except sqlite3.Error as error:
|
||||
self.logger.error(f"Ошибка добавления сообщения в базу данных: {error}")
|
||||
self.logger.error(f"Ошибка проверки прав пользователя админа: {error}")
|
||||
return None
|
||||
finally:
|
||||
self.close()
|
||||
@@ -963,6 +1105,62 @@ class BotDB:
|
||||
except Exception as 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):
|
||||
self.logger.info(
|
||||
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}")
|
||||
return None
|
||||
except sqlite3.Error as error:
|
||||
print(error)
|
||||
self.logger.error(f"Ошибка при добавлении войса в базу: {error}")
|
||||
raise
|
||||
finally:
|
||||
self.close()
|
||||
@@ -1073,6 +1271,24 @@ class BotDB:
|
||||
finally:
|
||||
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):
|
||||
"""Получает ID аудио сообщения пользователя"""
|
||||
self.logger.info(
|
||||
@@ -1167,3 +1383,17 @@ class BotDB:
|
||||
self.cursor.close()
|
||||
if self.conn:
|
||||
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)
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
import traceback
|
||||
import html
|
||||
|
||||
from aiogram import Router, types, F
|
||||
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.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
|
||||
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 logs.custom_logger import logger
|
||||
|
||||
admin_router = Router()
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
bdf = get_global_instance()
|
||||
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
|
||||
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
|
||||
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
|
||||
@@ -32,7 +33,7 @@ BotDB = bdf.get_db()
|
||||
)
|
||||
async def admin_panel(message: types.Message, state: FSMContext):
|
||||
try:
|
||||
if check_access(message.from_user.id):
|
||||
if check_access(message.from_user.id, BotDB):
|
||||
await state.set_state("ADMIN")
|
||||
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
|
||||
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')
|
||||
|
||||
|
||||
@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(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == 'Отменить'
|
||||
)
|
||||
async def decline_ban(message: types.Message, state: FSMContext):
|
||||
current_state = await state.get_state()
|
||||
await state.set_data({})
|
||||
await state.set_state("ADMIN")
|
||||
logger.info(f"Отмена процедуры блокировки")
|
||||
logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
|
||||
markup = get_reply_keyboard_admin()
|
||||
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)
|
||||
full_name = BotDB.get_full_name_by_id(user_id)
|
||||
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}\n"
|
||||
f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат",
|
||||
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')
|
||||
|
||||
|
||||
@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(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
@@ -110,8 +245,8 @@ async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
|
||||
async def get_banned_users(message):
|
||||
logger.info(
|
||||
f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
|
||||
message_text = get_banned_users_list(0)
|
||||
buttons_list = get_banned_users_buttons()
|
||||
message_text = get_banned_users_list(0, BotDB)
|
||||
buttons_list = get_banned_users_buttons(BotDB)
|
||||
if buttons_list:
|
||||
k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
||||
await message.answer(text=message_text, reply_markup=k)
|
||||
@@ -128,7 +263,9 @@ async def ban_user_step_2(message: types.Message, state: FSMContext):
|
||||
logger.info(f"Переход на шаг 2 бана пользователя. Словарь с данными для бана: {user_data})")
|
||||
await state.update_data(message_for_user=message.text)
|
||||
markup = create_keyboard_for_ban_days()
|
||||
await message.answer(f"Выбрана причина: {message.text}. Выбери срок бана в днях или напиши "
|
||||
# Экранируем 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)
|
||||
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)
|
||||
user_data = await state.get_data()
|
||||
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(
|
||||
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)
|
||||
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['message_for_user'],
|
||||
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']} успешно заблокирован)")
|
||||
await state.set_state('ADMIN')
|
||||
markup = get_reply_keyboard_admin()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@ from aiogram.types import CallbackQuery
|
||||
|
||||
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
|
||||
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, \
|
||||
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
|
||||
@@ -15,7 +15,7 @@ from logs.custom_logger import logger
|
||||
|
||||
callback_router = Router()
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
bdf = get_global_instance()
|
||||
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
|
||||
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
|
||||
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,
|
||||
date_to_unban=None)
|
||||
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(
|
||||
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)
|
||||
await state.set_state('BAN_2')
|
||||
else:
|
||||
@@ -237,7 +240,7 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext):
|
||||
async def process_unlock_user(call: CallbackQuery):
|
||||
user_id = call.data[7:]
|
||||
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}")
|
||||
username = BotDB.get_username(user_id)
|
||||
await call.answer(f'Пользователь разблокирован {username}', show_alert=True)
|
||||
@@ -270,12 +273,12 @@ async def change_page(call: CallbackQuery):
|
||||
reply_markup=keyboard)
|
||||
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,
|
||||
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')
|
||||
await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id,
|
||||
reply_markup=keyboard)
|
||||
|
||||
Binary file not shown.
@@ -3,13 +3,13 @@ from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
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 logs.custom_logger import logger
|
||||
|
||||
group_router = Router()
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
bdf = get_global_instance()
|
||||
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
|
||||
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
|
||||
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,9 @@
|
||||
import random
|
||||
import traceback
|
||||
import asyncio
|
||||
import html
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
from aiogram import types, Router, F
|
||||
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.blacklist_middleware import BlacklistMiddleware
|
||||
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, \
|
||||
send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, check_username_and_full_name, \
|
||||
send_video_message, send_video_note_message, send_audio_message, send_voice_message, add_in_db_media
|
||||
send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, send_video_message, \
|
||||
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
|
||||
|
||||
private_router = Router()
|
||||
@@ -26,7 +28,7 @@ private_router = Router()
|
||||
private_router.message.middleware(AlbumMiddleware())
|
||||
private_router.message.middleware(BlacklistMiddleware())
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
bdf = get_global_instance()
|
||||
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
|
||||
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
|
||||
MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
|
||||
@@ -38,6 +40,38 @@ TEST = bdf.settings['Settings']['test']
|
||||
|
||||
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(
|
||||
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
|
||||
language_code = message.from_user.language_code
|
||||
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()
|
||||
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
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)
|
||||
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:
|
||||
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(
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name} и ник @{username}")
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}")
|
||||
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
|
||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}')
|
||||
sleep(1)
|
||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
||||
await asyncio.sleep(1)
|
||||
BotDB.update_date_for_user(date, user_id)
|
||||
await state.set_state("START")
|
||||
logger.info(
|
||||
@@ -80,7 +130,7 @@ async def handle_start_message(message: types.Message, state: FSMContext):
|
||||
random_stick_hello = FSInputFile(path=random_stick_hello)
|
||||
logger.info(f"Стикер успешно получен из БД")
|
||||
await message.answer_sticker(random_stick_hello)
|
||||
sleep(0.3)
|
||||
await asyncio.sleep(0.3)
|
||||
except Exception as e:
|
||||
logger.error(f"Произошла ошибка handle_start_message при получении стикеров. Ошибка:{str(e)}")
|
||||
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()}")
|
||||
|
||||
|
||||
@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(
|
||||
StateFilter("START"),
|
||||
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 state.set_state("SUGGEST")
|
||||
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(
|
||||
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()
|
||||
suggest_news = messages.get_message(get_first_name(message), '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')
|
||||
await message.answer(suggest_news_2, reply_markup=markup)
|
||||
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")
|
||||
BotDB.update_date_for_user(date, user_id)
|
||||
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(
|
||||
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_*'))
|
||||
random_stick_bye = random.choice(name_stick_bye)
|
||||
random_stick_bye = FSInputFile(path=random_stick_bye)
|
||||
await message.answer_sticker(random_stick_bye)
|
||||
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(
|
||||
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,
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
||||
try:
|
||||
@@ -155,8 +237,10 @@ async def end_message(message: types.Message, state: FSMContext):
|
||||
await message.answer(bye_message, reply_markup=markup)
|
||||
await state.set_state("START")
|
||||
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(
|
||||
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,
|
||||
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"]),
|
||||
)
|
||||
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(
|
||||
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)
|
||||
try:
|
||||
post_caption = ''
|
||||
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,
|
||||
f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {message.from_user.username}')
|
||||
f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {safe_username}')
|
||||
else:
|
||||
await message.forward(chat_id=GROUP_FOR_LOGS)
|
||||
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)
|
||||
# Записываем в базу пост и контент
|
||||
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)
|
||||
@@ -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)
|
||||
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)
|
||||
@@ -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)
|
||||
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)
|
||||
@@ -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)
|
||||
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)
|
||||
@@ -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)
|
||||
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)
|
||||
@@ -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)
|
||||
sleep(0.2)
|
||||
media_group, BotDB)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
# Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками
|
||||
markup = get_reply_keyboard_for_post()
|
||||
@@ -343,8 +431,10 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
|
||||
F.text == '🤪Хочу стикеры'
|
||||
)
|
||||
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(
|
||||
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)
|
||||
try:
|
||||
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:
|
||||
await message.bot.send_message(chat_id=IMPORTANT_LOGS,
|
||||
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(
|
||||
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(
|
||||
@@ -365,8 +457,10 @@ async def stickers(message: types.Message, state: FSMContext):
|
||||
F.text == '📩Связаться с админами'
|
||||
)
|
||||
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(
|
||||
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
|
||||
current_date = datetime.now()
|
||||
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()
|
||||
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
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(
|
||||
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)
|
||||
current_date = datetime.now()
|
||||
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
Binary file not shown.
@@ -36,6 +36,8 @@ def get_reply_keyboard_admin():
|
||||
builder = ReplyKeyboardBuilder()
|
||||
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="Вернуться в бота"))
|
||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||
|
||||
@@ -14,7 +14,7 @@ async def start_bot(bdf):
|
||||
bot = Bot(token=token, default=DefaultBotProperties(
|
||||
parse_mode='HTML',
|
||||
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
|
||||
))
|
||||
), timeout=30.0) # Добавляем таймаут для предотвращения зависаний
|
||||
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
||||
dp.include_routers(private_router, callback_router, group_router, admin_router)
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@ from aiogram.types import Message
|
||||
|
||||
|
||||
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
|
||||
self.latency = latency
|
||||
self.album_data = {}
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
from typing import Dict, Any
|
||||
import html
|
||||
|
||||
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
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
bdf = get_global_instance()
|
||||
BotDB = bdf.get_db()
|
||||
|
||||
|
||||
class BlacklistMiddleware(BaseMiddleware):
|
||||
async def __call__(self, handler, event: types.Message, data: Dict[str, Any]) -> Any:
|
||||
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} заблокирован!')
|
||||
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(
|
||||
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
|
||||
logger.info(f'BlacklistMiddleware результат для пользователя: {event.from_user.username} доступ разрешен')
|
||||
return await handler(event, data)
|
||||
|
||||
@@ -7,7 +7,7 @@ from aiogram.types import Message
|
||||
|
||||
|
||||
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
|
||||
self.latency = latency
|
||||
self.texts = defaultdict(list)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,7 +14,7 @@ class BaseDependencyFactory:
|
||||
self.config = configparser.ConfigParser()
|
||||
self.config.read(config_path)
|
||||
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():
|
||||
self.settings[section] = {}
|
||||
@@ -33,3 +33,14 @@ class BaseDependencyFactory:
|
||||
def get_db(self) -> BotDB:
|
||||
"""Возвращает подключение к базе данных."""
|
||||
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
|
||||
|
||||
@@ -1,41 +1,102 @@
|
||||
import html
|
||||
import os
|
||||
import random
|
||||
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.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
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
|
||||
bdf = get_global_instance()
|
||||
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:
|
||||
first_name = html.escape(message.from_user.first_name)
|
||||
"""
|
||||
Безопасно получает и экранирует имя пользователя для использования в HTML разметке.
|
||||
|
||||
Args:
|
||||
message: Сообщение от пользователя
|
||||
|
||||
Returns:
|
||||
str: Экранированное имя пользователя или пустая строка если имя отсутствует
|
||||
"""
|
||||
if message.from_user.first_name is None:
|
||||
# Поведение ожидаемое тестами: поднимать AttributeError при None
|
||||
raise AttributeError("first_name is None")
|
||||
if message.from_user.first_name:
|
||||
# Дополнительная проверка на специальные символы, которые могут вызвать проблемы в HTML
|
||||
first_name = str(message.from_user.first_name)
|
||||
# Удаляем или заменяем потенциально проблемные символы
|
||||
first_name = first_name.replace('\u0cc0', '') # Убираем символ "ೀ" (U+0CC0)
|
||||
first_name = first_name.replace('\u0cc1', '') # Убираем символ "ೀ" (U+0CC1)
|
||||
first_name = html.escape(first_name)
|
||||
return first_name
|
||||
return ""
|
||||
|
||||
|
||||
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:
|
||||
post_text: Текст сообщения
|
||||
first_name: Имя автора поста
|
||||
username: Юзернейм автора поста
|
||||
username: Юзернейм автора поста (может быть None)
|
||||
|
||||
Returns:
|
||||
str: - Сформированный текст сообщения.
|
||||
"""
|
||||
if "неанон" in post_text or "не анон" in post_text:
|
||||
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {first_name} @{username}'
|
||||
elif "анон" in post_text:
|
||||
return f'Пост из ТГ:\n{post_text}\n\nПост опубликован анонимно'
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
|
||||
# Экранируем username для безопасного использования в HTML
|
||||
safe_username = html.escape(username) if username else None
|
||||
|
||||
# Формируем строку с информацией об авторе
|
||||
if safe_username:
|
||||
author_info = f"{first_name} @{safe_username}"
|
||||
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):
|
||||
@@ -77,6 +138,9 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
Returns:
|
||||
Список InputMediaPhoto (MediaGroup).
|
||||
"""
|
||||
# Экранируем post_caption для безопасного использования в HTML
|
||||
safe_post_caption = html.escape(str(post_caption)) if post_caption else ""
|
||||
|
||||
media_group = []
|
||||
|
||||
for i, message in enumerate(album):
|
||||
@@ -96,11 +160,11 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
# Формируем объект MediaGroup с учетом типа медиа
|
||||
if i == len(album) - 1:
|
||||
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':
|
||||
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':
|
||||
media_group.append(InputMediaAudio(media=file_id, caption=post_caption))
|
||||
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
if media_type == 'photo':
|
||||
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
|
||||
|
||||
|
||||
async def add_in_db_media_mediagroup(sent_message):
|
||||
async def add_in_db_media_mediagroup(sent_message, bot_db):
|
||||
"""
|
||||
Идентификатор медиа-группы
|
||||
|
||||
Args:
|
||||
sent_message: sent_message объект из Telegram API
|
||||
bot_db: Экземпляр базы данных
|
||||
|
||||
Returns:
|
||||
Список InputFile (FSInputFile).
|
||||
@@ -127,20 +192,21 @@ async def add_in_db_media_mediagroup(sent_message):
|
||||
if message.photo:
|
||||
file_id = message.photo[-1].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:
|
||||
file_id = message.video.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:
|
||||
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
|
||||
continue
|
||||
|
||||
|
||||
async def add_in_db_media(sent_message):
|
||||
async def add_in_db_media(sent_message, bot_db):
|
||||
"""
|
||||
Args:
|
||||
sent_message: sent_message объект из Telegram API
|
||||
bot_db: Экземпляр базы данных
|
||||
|
||||
Returns:
|
||||
Список InputFile (FSInputFile).
|
||||
@@ -148,33 +214,33 @@ async def add_in_db_media(sent_message):
|
||||
if sent_message.photo:
|
||||
file_id = sent_message.photo[-1].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:
|
||||
file_id = sent_message.video.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:
|
||||
file_id = sent_message.voice.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:
|
||||
file_id = sent_message.audio.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:
|
||||
file_id = sent_message.video_note.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,
|
||||
media_group: list[InputMediaPhoto]):
|
||||
media_group: list[InputMediaPhoto], bot_db):
|
||||
sent_message = await message.bot.send_media_group(
|
||||
chat_id=chat_id,
|
||||
media=media_group,
|
||||
)
|
||||
BotDB.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id)
|
||||
await add_in_db_media_mediagroup(sent_message)
|
||||
bot_db.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id)
|
||||
await add_in_db_media_mediagroup(sent_message, bot_db)
|
||||
message_id = sent_message[-1].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:
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
sent_message = await message.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=post_text
|
||||
text=safe_post_text
|
||||
)
|
||||
message_id = sent_message.message_id
|
||||
return message_id
|
||||
else:
|
||||
sent_message = await message.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=post_text,
|
||||
text=safe_post_text,
|
||||
reply_markup=markup
|
||||
)
|
||||
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,
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
|
||||
if markup is None:
|
||||
sent_message = await message.bot.send_photo(
|
||||
chat_id=chat_id,
|
||||
caption=post_text,
|
||||
caption=safe_post_text,
|
||||
photo=photo
|
||||
)
|
||||
else:
|
||||
sent_message = await message.bot.send_photo(
|
||||
chat_id=chat_id,
|
||||
caption=post_text,
|
||||
caption=safe_post_text,
|
||||
photo=photo,
|
||||
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 = "",
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
|
||||
if markup is None:
|
||||
sent_message = await message.bot.send_video(
|
||||
chat_id=chat_id,
|
||||
caption=post_text,
|
||||
caption=safe_post_text,
|
||||
video=video
|
||||
)
|
||||
else:
|
||||
sent_message = await message.bot.send_video(
|
||||
chat_id=chat_id,
|
||||
caption=post_text,
|
||||
caption=safe_post_text,
|
||||
video=video,
|
||||
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,
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
|
||||
if markup is None:
|
||||
sent_message = await message.bot.send_audio(
|
||||
chat_id=chat_id,
|
||||
caption=post_text,
|
||||
caption=safe_post_text,
|
||||
audio=audio
|
||||
)
|
||||
else:
|
||||
sent_message = await message.bot.send_audio(
|
||||
chat_id=chat_id,
|
||||
caption=post_text,
|
||||
caption=safe_post_text,
|
||||
audio=audio,
|
||||
reply_markup=markup
|
||||
)
|
||||
@@ -313,9 +393,9 @@ async def send_voice_message(chat_id, message: types.Message, voice: str,
|
||||
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):
|
||||
@@ -326,52 +406,60 @@ def add_days_to_date(days: int):
|
||||
return formatted_date
|
||||
|
||||
|
||||
def get_banned_users_list(offset: int):
|
||||
def get_banned_users_list(offset: int, bot_db):
|
||||
"""
|
||||
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
|
||||
|
||||
Args:
|
||||
offset: отступ для запроса в базу данных
|
||||
bot_db: Экземпляр базы данных
|
||||
|
||||
Returns:
|
||||
message - текст сообщения
|
||||
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"
|
||||
|
||||
for user in users:
|
||||
message += f"Пользователь: {user[0]}\n"
|
||||
message += f"Причина бана: {user[2]}\n"
|
||||
message += f"Дата разбана: {user[3]}\n\n"
|
||||
# Экранируем пользовательские данные для безопасного использования
|
||||
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
|
||||
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
|
||||
|
||||
|
||||
def get_banned_users_buttons():
|
||||
def get_banned_users_buttons(bot_db):
|
||||
"""
|
||||
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
|
||||
|
||||
Args:
|
||||
offset: отступ для запроса в базу данных
|
||||
bot_db: Экземпляр базы данных
|
||||
|
||||
Returns:
|
||||
message - текст сообщения
|
||||
user_ids - лист кортежей [(user_name: user_id)]
|
||||
"""
|
||||
users = BotDB.get_banned_users_from_db()
|
||||
users = bot_db.get_banned_users_from_db()
|
||||
user_ids = []
|
||||
|
||||
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
|
||||
|
||||
|
||||
def delete_user_blacklist(user_id: int):
|
||||
return BotDB.delete_user_blacklist(user_id=user_id)
|
||||
def delete_user_blacklist(user_id: int, bot_db):
|
||||
return bot_db.delete_user_blacklist(user_id=user_id)
|
||||
|
||||
|
||||
def check_username_and_full_name(user_id: int, username: str, full_name: str):
|
||||
username_db, full_name_db = BotDB.get_username_and_full_name(user_id=user_id)
|
||||
def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
|
||||
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
|
||||
|
||||
|
||||
@@ -383,7 +471,58 @@ def unban_notifier(self):
|
||||
unblocked_users = self.BotDB.get_users_for_unblock_today(today)
|
||||
message = "Разблокированные пользователи:\n"
|
||||
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)
|
||||
|
||||
|
||||
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 "Эмоджи не определен"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
|
||||
|
||||
def get_message(username: str, type_message: str):
|
||||
@@ -25,7 +26,7 @@ def get_message(username: str, type_message: str):
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
|
||||
"DEL_MESSAGE": "username, напиши свое обращение или предложение✍"
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺❤",
|
||||
"BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /start"
|
||||
"BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /restart"
|
||||
"&&И тебе пока!👋🏼❤️",
|
||||
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
|
||||
"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]
|
||||
return message.replace('username', username).replace('&', '\n')
|
||||
# Экранируем потенциально проблемные символы для HTML
|
||||
message = message.replace('username', html.escape(username)).replace('&', '\n')
|
||||
return message
|
||||
|
||||
@@ -8,6 +8,8 @@ class StateUser(StatesGroup):
|
||||
CHAT = State()
|
||||
PRE_CHAT = State()
|
||||
PRE_BAN = State()
|
||||
PRE_BAN_ID = State()
|
||||
PRE_BAN_FORWARD = State()
|
||||
BAN_2 = State()
|
||||
BAN_3 = State()
|
||||
BAN_4 = State()
|
||||
|
||||
@@ -19,6 +19,6 @@ filename = f'{current_dir}/helper_bot_{today}.log'
|
||||
logger.add(
|
||||
filename,
|
||||
rotation="00:00",
|
||||
retention="5 days",
|
||||
retention="30 days",
|
||||
format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {name} | {line} | {message}",
|
||||
)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к корневой директории проекта
|
||||
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from database.db import BotDB
|
||||
|
||||
@@ -8,7 +12,7 @@ current_dir = os.path.dirname(__file__)
|
||||
# Переходим на уровень выше
|
||||
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():
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к корневой директории проекта
|
||||
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from database.db import BotDB
|
||||
|
||||
@@ -8,7 +12,7 @@ current_dir = os.path.dirname(__file__)
|
||||
# Переходим на уровень выше
|
||||
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():
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Добавляем путь к корневой директории проекта
|
||||
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from database.db import BotDB
|
||||
|
||||
@@ -8,7 +12,7 @@ current_dir = os.path.dirname(__file__)
|
||||
# Переходим на уровень выше
|
||||
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():
|
||||
|
||||
56
migrations/003_create_our_users_table.py
Normal file
56
migrations/003_create_our_users_table.py
Normal 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()
|
||||
|
||||
25
pytest.ini
25
pytest.ini
@@ -1,8 +1,19 @@
|
||||
[pytest]
|
||||
pythonpath = .
|
||||
python_files = test_*.py *_test.py
|
||||
python_functions = test_*
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
|
||||
[report]
|
||||
omit = *myenv/*, custom_logger.py, *venv/*, tests/*
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
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
|
||||
@@ -20,3 +20,4 @@ attrs~=23.2.0
|
||||
typing_extensions~=4.12.2
|
||||
aiohttp~=3.9.5
|
||||
aiogram~=3.10.0
|
||||
emoji~=2.14.0
|
||||
@@ -1,7 +1,14 @@
|
||||
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.utils.base_dependency_factory import BaseDependencyFactory
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(start_bot(BaseDependencyFactory()))
|
||||
asyncio.run(start_bot(get_global_instance()))
|
||||
|
||||
234
tests/conftest.py
Normal file
234
tests/conftest.py
Normal 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
52
tests/mocks.py
Normal 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
339
tests/test_bot.py
Normal 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'])
|
||||
@@ -11,7 +11,7 @@ from database.db import BotDB
|
||||
def bot():
|
||||
"""Фикстура для создания объекта BotDB."""
|
||||
current_dir = os.getcwd()
|
||||
return BotDB(current_dir, "test.db")
|
||||
return BotDB(current_dir, "database/test.db")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, )
|
||||
@@ -38,7 +38,7 @@ def setup_db():
|
||||
# Other data
|
||||
date = "2024-07-10"
|
||||
next_date = "2024-07-11"
|
||||
conn = sqlite3.connect("test.db")
|
||||
conn = sqlite3.connect("database/test.db")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS "admins" (
|
||||
@@ -139,12 +139,12 @@ def setup_db():
|
||||
conn.commit()
|
||||
conn.close()
|
||||
yield
|
||||
os.remove('test.db')
|
||||
os.remove('database/test.db')
|
||||
|
||||
|
||||
def test_bot_init(bot):
|
||||
"""Проверяет, что объект 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
|
||||
assert bot.conn is None
|
||||
assert bot.cursor is None
|
||||
@@ -174,7 +174,7 @@ def test_create_table_success(bot):
|
||||
bot.create_table(sql_script)
|
||||
|
||||
# Проверяем, что таблица создана
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'")
|
||||
result = cursor.fetchone()
|
||||
@@ -192,7 +192,7 @@ def test_create_table_error(bot):
|
||||
|
||||
|
||||
def test_get_current_version_success(bot):
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
|
||||
conn.commit()
|
||||
@@ -216,7 +216,7 @@ def test_update_version_success(bot):
|
||||
bot.update_version(new_version, script_name)
|
||||
|
||||
# Проверяем, что данные записаны в таблицу
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM migrations WHERE version = ?", (new_version,))
|
||||
result = cursor.fetchone()
|
||||
@@ -228,7 +228,7 @@ def test_update_version_success(bot):
|
||||
|
||||
|
||||
def test_update_version_integrity_error(bot):
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
|
||||
conn.commit()
|
||||
@@ -252,16 +252,17 @@ def test_add_new_user_in_db(bot):
|
||||
username = "@petr_ivanov"
|
||||
is_bot = False
|
||||
language_code = "ru"
|
||||
emoji = '🦀'
|
||||
date_added = "2024-07-09"
|
||||
date_changed = "2024-07-09"
|
||||
|
||||
# Вызываем функцию 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.execute("SELECT * FROM our_users WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
@@ -285,7 +286,7 @@ def test_add_new_user_in_db_duplicate_user_id(bot, setup_db):
|
||||
# Попытка добавить пользователя с тем же user_id
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
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"
|
||||
is_bot = False
|
||||
language_code = "fr"
|
||||
emoji = "🦀"
|
||||
date_added = "2024-07-09"
|
||||
date_changed = "2024-07-09"
|
||||
|
||||
# Вызываем функцию 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.execute(f"SELECT * FROM our_users WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
@@ -388,7 +390,7 @@ def test_get_username_error(bot):
|
||||
|
||||
def test_get_all_user_id_empty(bot):
|
||||
"""Проверяет, что функция возвращает пустой список, если в базе нет пользователей."""
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM our_users")
|
||||
conn.commit()
|
||||
@@ -481,7 +483,7 @@ def test_update_info_about_stickers_success(bot):
|
||||
bot.update_info_about_stickers(user_id)
|
||||
|
||||
# Проверяем, что информация обновлена
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT has_stickers FROM our_users WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
@@ -495,7 +497,7 @@ def test_update_info_about_stickers_not_found(bot):
|
||||
bot.update_info_about_stickers(user_id)
|
||||
|
||||
# Проверяем, что база данных не изменилась
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM our_users WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
@@ -512,7 +514,7 @@ def test_update_info_about_stickers_error(bot):
|
||||
|
||||
def test_get_users_blacklist_empty(bot):
|
||||
"""Проверяет, что функция возвращает пустой словарь, если в черном списке нет пользователей."""
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM blacklist")
|
||||
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
|
||||
|
||||
# Проверяем, что запись добавлена в базу
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
@@ -658,7 +660,7 @@ def test_delete_user_blacklist_success(bot):
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_delete_user_blacklist_not_found(bot):
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT INTO blacklist (user_id, user_name, date_to_unban) VALUES (?, ?, ?)",
|
||||
(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):
|
||||
bot.update_date_for_user('2024-07-15', 12345)
|
||||
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT date_changed FROM our_users WHERE user_id = ?", (12345,))
|
||||
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):
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM our_users")
|
||||
conn.commit()
|
||||
@@ -768,7 +770,7 @@ def test_get_banned_users_from_db_success(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.execute("DELETE FROM blacklist")
|
||||
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):
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM blacklist")
|
||||
conn.commit()
|
||||
@@ -818,7 +820,7 @@ def test_get_banned_users_from_db_with_limits_error(bot):
|
||||
|
||||
|
||||
def __drop_table(table_name: str):
|
||||
conn = sqlite3.connect('test.db')
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"DROP TABLE {table_name}")
|
||||
conn.commit()
|
||||
|
||||
339
tests/test_error_handling.py
Normal file
339
tests/test_error_handling.py
Normal 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'])
|
||||
330
tests/test_keyboards_and_filters.py
Normal file
330
tests/test_keyboards_and_filters.py
Normal 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'])
|
||||
292
tests/test_media_handlers.py
Normal file
292
tests/test_media_handlers.py
Normal 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
13
tests/test_settings.ini
Normal 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
208
tests/test_utils.py
Normal 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'])
|
||||
68
voice_bot/handlers/callback_handler.py
Normal file
68
voice_bot/handlers/callback_handler.py
Normal 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)
|
||||
@@ -1,5 +1,5 @@
|
||||
import random
|
||||
import time
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
@@ -9,22 +9,65 @@ from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
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 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
|
||||
|
||||
voice_router = Router()
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
bdf = get_global_instance()
|
||||
|
||||
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()
|
||||
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(
|
||||
@@ -34,55 +77,78 @@ BotDB = bdf.get_db()
|
||||
async def start(message: types.Message, state: FSMContext):
|
||||
await state.set_state("START")
|
||||
await message.forward(chat_id=GROUP_FOR_LOGS)
|
||||
await update_user_info('voice', message)
|
||||
user_emoji = check_user_emoji(message)
|
||||
try:
|
||||
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
|
||||
random_stick_hello = random.choice(name_stick_hello)
|
||||
random_stick_hello = FSInputFile(path=random_stick_hello)
|
||||
logger.info(f"Стикер успешно получен из БД. Наименование стикера: {name_stick_hello}")
|
||||
await message.answer_sticker(random_stick_hello)
|
||||
time.sleep(0.3)
|
||||
await asyncio.sleep(0.3)
|
||||
except Exception as e:
|
||||
if LOGS:
|
||||
await message.bot.send_message(IMPORTANT_LOGS, f'Отправка приветственных стикеров лажает. Ошибка: {e}')
|
||||
markup = get_main_keyboard()
|
||||
await message.answer(text="<b>Привет.</b>", parse_mode='html', reply_markup=markup,
|
||||
disable_web_page_preview=not PREVIEW_LINK)
|
||||
time.sleep(0.3)
|
||||
await asyncio.sleep(0.3)
|
||||
await message.answer(text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из "
|
||||
"Бийска</i>",
|
||||
parse_mode='html', reply_markup=markup,
|
||||
disable_web_page_preview=not PREVIEW_LINK)
|
||||
time.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
await message.answer(text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не "
|
||||
"узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
|
||||
parse_mode='html', reply_markup=markup,
|
||||
disable_web_page_preview=not PREVIEW_LINK)
|
||||
time.sleep(0.8)
|
||||
await asyncio.sleep(0.8)
|
||||
await message.answer(text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя "
|
||||
"бы на 5-10 секунд</i>",
|
||||
parse_mode='html', reply_markup=markup,
|
||||
disable_web_page_preview=not PREVIEW_LINK)
|
||||
time.sleep(1.5)
|
||||
await asyncio.sleep(1.5)
|
||||
await message.answer(text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, "
|
||||
"ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы "
|
||||
"выкладывать в собственные соцсети)",
|
||||
parse_mode='html', reply_markup=markup,
|
||||
disable_web_page_preview=not PREVIEW_LINK)
|
||||
time.sleep(1.3)
|
||||
await asyncio.sleep(1.3)
|
||||
await message.answer(text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из "
|
||||
"недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
|
||||
parse_mode='html', reply_markup=markup,
|
||||
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",
|
||||
parse_mode='html', reply_markup=markup,
|
||||
disable_web_page_preview=not PREVIEW_LINK)
|
||||
time.sleep(0.8)
|
||||
await message.answer(text="<b>ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
|
||||
await asyncio.sleep(0.8)
|
||||
await message.answer(text="<b>Ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
|
||||
parse_mode='html', reply_markup=markup,
|
||||
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(
|
||||
StateFilter("START"),
|
||||
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)
|
||||
markup = types.ReplyKeyboardRemove()
|
||||
await message.answer(text='Хорошо, теперь пришли мне свое голосовое сообщение', reply_markup=markup)
|
||||
try:
|
||||
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')
|
||||
|
||||
|
||||
@voice_router.message(
|
||||
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()
|
||||
if message.content_type == 'voice':
|
||||
await message.forward(chat_id=GROUP_FOR_LOGS)
|
||||
file_name = ''
|
||||
file_id = 1
|
||||
# Проверяем что запись о файле есть в базе данных
|
||||
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)
|
||||
markup_for_voice = get_reply_keyboard_for_voice()
|
||||
# Отправляем аудио в приватный канал
|
||||
sent_message = await send_voice_message(GROUP_FOR_POST, message,
|
||||
message.voice.file_id, markup_for_voice)
|
||||
|
||||
# Сохраняем в базку
|
||||
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 state.set_state('START')
|
||||
else:
|
||||
@@ -153,48 +199,38 @@ async def save_voice_message(message: types.Message, state: FSMContext):
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
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)
|
||||
list_audio = list(check_audio)
|
||||
markup = get_main_keyboard()
|
||||
await message.forward(chat_id=GROUP_FOR_LOGS)
|
||||
if not list_audio:
|
||||
await message.answer(text='Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится',
|
||||
reply_markup=markup)
|
||||
try:
|
||||
message_with_date = last_message()
|
||||
message.send_message(chat_id=message.chat.id, text=message_with_date, parse_mode="html")
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить последнюю дату {e}')
|
||||
else:
|
||||
# Получаем ссылку на аудио сообщение пользователя
|
||||
number_element = random.randint(0, len(list_audio) - 1)
|
||||
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')
|
||||
# voice = open(path, 'rb')
|
||||
voice = FSInputFile(path)
|
||||
|
||||
# Маркируем сообщение как прослушанное
|
||||
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(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("restart")
|
||||
)
|
||||
async def restart_function(message: types.Message, state: FSMContext):
|
||||
await message.forward(chat_id=GROUP_FOR_LOGS)
|
||||
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')
|
||||
# Формируем подпись
|
||||
if user_emoji:
|
||||
caption = f'{user_emoji}\nДата записи: {date_added}'
|
||||
else:
|
||||
caption = f'Дата записи: {date_added}'
|
||||
await message.bot.send_voice(chat_id=message.chat.id, voice=voice, caption=caption, reply_markup=markup)
|
||||
await message.answer(text=f'Осталось непрослушанных: <b>{len(check_audio) - 1}</b>', reply_markup=markup)
|
||||
@@ -1,5 +1,5 @@
|
||||
from aiogram import types
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
||||
|
||||
|
||||
def get_main_keyboard():
|
||||
@@ -8,3 +8,15 @@ def get_main_keyboard():
|
||||
builder.add(types.KeyboardButton(text="🎧Послушать"))
|
||||
markup = builder.as_markup(resize_keyboard=True)
|
||||
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
|
||||
|
||||
@@ -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.client.default import DefaultBotProperties
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
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):
|
||||
@@ -13,6 +23,6 @@ async def start_bot(bdf):
|
||||
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
|
||||
))
|
||||
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 dp.start_polling(bot, skip_updates=True)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import time
|
||||
import html
|
||||
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()
|
||||
|
||||
@@ -11,6 +12,9 @@ BotDB = bdf.get_db()
|
||||
def last_message():
|
||||
# функция с отображением сообщения "Последнее сообщение было записано"
|
||||
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")
|
||||
last_voice_time_timestamp = time.mktime(parse_date.timetuple())
|
||||
time_now_timestamp = time.time()
|
||||
@@ -22,13 +26,19 @@ def last_message():
|
||||
message_with_date = ''
|
||||
if much_minutes_ago <= 60:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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
|
||||
|
||||
bdf = BaseDependencyFactory()
|
||||
bdf = get_global_instance()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(start_bot(BaseDependencyFactory()))
|
||||
asyncio.run(start_bot(get_global_instance()))
|
||||
|
||||
Reference in New Issue
Block a user