Dev 8 #10
@@ -72,8 +72,6 @@ docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# Development files
|
||||
Makefile
|
||||
start_docker.sh
|
||||
*.sh
|
||||
|
||||
# Stickers and media
|
||||
@@ -94,4 +92,4 @@ Stick/
|
||||
|
||||
# Monitoring configs (will be mounted)
|
||||
prometheus.yml
|
||||
grafana/
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -89,3 +89,7 @@ env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Other files
|
||||
voice_users/
|
||||
files/
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -28,6 +28,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
# Install runtime dependencies only
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
||||
curl \
|
||||
sqlite3 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean
|
||||
|
||||
@@ -37,28 +39,38 @@ RUN groupadd -g 1001 deploy && useradd -u 1001 -g deploy deploy
|
||||
# Copy virtual environment from builder
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
RUN chown -R deploy:deploy /opt/venv
|
||||
RUN chown -R 1001:1001 /opt/venv
|
||||
|
||||
# Create app directory and set permissions
|
||||
WORKDIR /app
|
||||
RUN mkdir -p /app/database /app/logs && \
|
||||
chown -R deploy:deploy /app
|
||||
RUN mkdir -p /app/database /app/logs /app/voice_users && \
|
||||
chown -R 1001:1001 /app
|
||||
|
||||
# Copy application code
|
||||
COPY --chown=deploy:deploy . .
|
||||
COPY --chown=1001:1001 . .
|
||||
|
||||
# Initialize SQLite database with schema
|
||||
RUN sqlite3 /app/database/tg-bot-database.db < /app/database/schema.sql && \
|
||||
chown 1001:1001 /app/database/tg-bot-database.db && \
|
||||
chmod 644 /app/database/tg-bot-database.db
|
||||
|
||||
# Switch to non-root user
|
||||
USER deploy
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
# Health check with better timeout handling
|
||||
HEALTHCHECK --interval=30s --timeout=15s --start-period=10s --retries=5 \
|
||||
CMD curl -f --connect-timeout 5 --max-time 10 http://localhost:8080/health || exit 1
|
||||
|
||||
# Expose metrics port
|
||||
EXPOSE 8000
|
||||
EXPOSE 8080
|
||||
|
||||
# Graceful shutdown
|
||||
# Graceful shutdown with longer timeout
|
||||
STOPSIGNAL SIGTERM
|
||||
|
||||
# Run application
|
||||
CMD ["python", "run_helper.py"]
|
||||
# Set environment variables for better network stability
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONHASHSEED=random
|
||||
|
||||
# Run application with proper signal handling
|
||||
CMD ["python", "-u", "run_helper.py"]
|
||||
|
||||
121
Makefile
121
Makefile
@@ -1,121 +0,0 @@
|
||||
.PHONY: help build up down logs clean restart status deploy migrate backup
|
||||
|
||||
help: ## Показать справку
|
||||
@echo "🐍 Telegram Bot - Доступные команды (Production Ready):"
|
||||
@echo ""
|
||||
@echo "🔧 Основные команды:"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
@echo "📊 Мониторинг:"
|
||||
@echo " Prometheus: http://localhost:9090"
|
||||
@echo " Grafana: http://localhost:3000 (admin/admin)"
|
||||
@echo " Bot Health: http://localhost:8000/health"
|
||||
|
||||
build: ## Собрать все контейнеры
|
||||
docker-compose build
|
||||
|
||||
up: ## Запустить все сервисы
|
||||
docker-compose up -d
|
||||
|
||||
down: ## Остановить все сервисы
|
||||
docker-compose down
|
||||
|
||||
logs: ## Показать логи всех сервисов
|
||||
docker-compose logs -f
|
||||
|
||||
logs-bot: ## Показать логи бота
|
||||
docker-compose logs -f telegram-bot
|
||||
|
||||
logs-prometheus: ## Показать логи Prometheus
|
||||
docker-compose logs -f prometheus
|
||||
|
||||
logs-grafana: ## Показать логи Grafana
|
||||
docker-compose logs -f grafana
|
||||
|
||||
restart: ## Перезапустить все сервисы
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
|
||||
restart-bot: ## Перезапустить только бота
|
||||
docker-compose restart telegram-bot
|
||||
|
||||
restart-prometheus: ## Перезапустить только Prometheus
|
||||
docker-compose restart prometheus
|
||||
|
||||
restart-grafana: ## Перезапустить только Grafana
|
||||
docker-compose restart grafana
|
||||
|
||||
status: ## Показать статус контейнеров
|
||||
docker-compose ps
|
||||
|
||||
health: ## Проверить здоровье сервисов
|
||||
@echo "🏥 Checking service health..."
|
||||
@curl -f http://localhost:8000/health || echo "❌ Bot health check failed"
|
||||
@curl -f http://localhost:9090/-/healthy || echo "❌ Prometheus health check failed"
|
||||
@curl -f http://localhost:3000/api/health || echo "❌ Grafana health check failed"
|
||||
|
||||
check-python: ## Проверить версию Python в контейнере
|
||||
@echo "🐍 Проверяю версию Python в контейнере..."
|
||||
@docker exec telegram-bot python --version || echo "Контейнер не запущен"
|
||||
|
||||
deploy: ## Полный деплой на продакшен
|
||||
@echo "🚀 Starting production deployment..."
|
||||
@chmod +x scripts/deploy.sh
|
||||
@./scripts/deploy.sh
|
||||
|
||||
migrate: ## Миграция с systemctl + cron на Docker
|
||||
@echo "🔄 Starting migration from systemctl to Docker..."
|
||||
@chmod +x scripts/migrate_from_systemctl.sh
|
||||
@sudo ./scripts/migrate_from_systemctl.sh
|
||||
|
||||
backup: ## Создать backup данных
|
||||
@echo "💾 Creating backup..."
|
||||
@mkdir -p backups
|
||||
@tar -czf "backups/backup-$(date +%Y%m%d-%H%M%S).tar.gz" database/ logs/ .env
|
||||
@echo "✅ Backup created in backups/"
|
||||
|
||||
restore: ## Восстановить из backup (указать файл: make restore FILE=backup.tar.gz)
|
||||
@echo "🔄 Restoring from backup..."
|
||||
@if [ -z "$(FILE)" ]; then echo "❌ Please specify backup file: make restore FILE=backup.tar.gz"; exit 1; fi
|
||||
@tar -xzf "backups/$(FILE)" -C .
|
||||
@echo "✅ Backup restored"
|
||||
|
||||
update: ## Обновить бота (pull latest code and redeploy)
|
||||
@echo "📥 Pulling latest changes..."
|
||||
@git pull origin main
|
||||
@echo "🔨 Rebuilding and restarting..."
|
||||
@make restart
|
||||
|
||||
clean: ## Очистить все контейнеры и образы
|
||||
docker-compose down -v --rmi all
|
||||
docker system prune -f
|
||||
|
||||
security-scan: ## Сканировать образы на уязвимости
|
||||
@echo "🔍 Scanning Docker images for vulnerabilities..."
|
||||
@docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v $(PWD):/workspace \
|
||||
--workdir /workspace \
|
||||
anchore/grype:latest \
|
||||
telegram-helper-bot_telegram-bot:latest || echo "⚠️ Grype not available, skipping scan"
|
||||
|
||||
monitoring: ## Открыть мониторинг в браузере
|
||||
@echo "📊 Opening monitoring dashboards..."
|
||||
@open http://localhost:3000 || xdg-open http://localhost:3000 || echo "Please open manually: http://localhost:3000"
|
||||
|
||||
start: build up ## Собрать и запустить все сервисы
|
||||
@echo "🐍 Telegram Bot запущен!"
|
||||
@echo "📊 Prometheus: http://localhost:9090"
|
||||
@echo "📈 Grafana: http://localhost:3000 (admin/admin)"
|
||||
@echo "🤖 Bot Health: http://localhost:8000/health"
|
||||
@echo "📝 Логи: make logs"
|
||||
|
||||
stop: down ## Остановить все сервисы
|
||||
@echo "🛑 Все сервисы остановлены"
|
||||
|
||||
test: ## Запустить все тесты
|
||||
@echo "🧪 Запускаю все тесты..."
|
||||
@docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest"
|
||||
|
||||
test-coverage: ## Запустить все тесты с покрытием
|
||||
@echo "🧪 Запускаю все тесты с покрытием..."
|
||||
@docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest --cov=helper_bot --cov-report=term-missing"
|
||||
@@ -1,259 +0,0 @@
|
||||
# Тестирование 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"
|
||||
```
|
||||
26
database/__init__.py
Normal file
26
database/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Пакет для работы с базой данных.
|
||||
|
||||
Содержит:
|
||||
- models: модели данных
|
||||
- base: базовый класс для работы с БД
|
||||
- repositories: репозитории для разных сущностей
|
||||
- repository_factory: фабрика репозиториев
|
||||
- async_db: основной класс AsyncBotDB
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
User, BlacklistUser, UserMessage, TelegramPost, PostContent,
|
||||
MessageContentLink, Admin, Migration, AudioMessage, AudioListenRecord, AudioModerate
|
||||
)
|
||||
from .repository_factory import RepositoryFactory
|
||||
from .base import DatabaseConnection
|
||||
from .async_db import AsyncBotDB
|
||||
|
||||
# Для обратной совместимости экспортируем старый интерфейс
|
||||
__all__ = [
|
||||
'User', 'BlacklistUser', 'UserMessage', 'TelegramPost', 'PostContent',
|
||||
'MessageContentLink', 'Admin', 'Migration', 'AudioMessage', 'AudioListenRecord', 'AudioModerate',
|
||||
'RepositoryFactory', 'DatabaseConnection', 'AsyncBotDB'
|
||||
]
|
||||
|
||||
1167
database/async_db.py
1167
database/async_db.py
File diff suppressed because it is too large
Load Diff
114
database/base.py
Normal file
114
database/base.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import aiosqlite
|
||||
from typing import Optional
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class DatabaseConnection:
|
||||
"""Базовый класс для работы с базой данных."""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = os.path.abspath(db_path)
|
||||
self.logger = logger
|
||||
self.logger.info(f'Инициация базы данных: {self.db_path}')
|
||||
|
||||
async def _get_connection(self):
|
||||
"""Получение асинхронного соединения с базой данных."""
|
||||
try:
|
||||
conn = await aiosqlite.connect(self.db_path)
|
||||
# Включаем поддержку внешних ключей
|
||||
await conn.execute("PRAGMA foreign_keys = ON")
|
||||
# Включаем WAL режим для лучшей производительности
|
||||
await conn.execute("PRAGMA journal_mode = WAL")
|
||||
await conn.execute("PRAGMA synchronous = NORMAL")
|
||||
await conn.execute("PRAGMA cache_size = 10000")
|
||||
await conn.execute("PRAGMA temp_store = MEMORY")
|
||||
return conn
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при получении соединения: {e}")
|
||||
raise
|
||||
|
||||
async def _execute_query(self, query: str, params: tuple = ()):
|
||||
"""Выполнение запроса с автоматическим закрытием соединения."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
result = await conn.execute(query, params)
|
||||
await conn.commit()
|
||||
return result
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при выполнении запроса: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def _execute_query_with_result(self, query: str, params: tuple = ()):
|
||||
"""Выполнение запроса с результатом и автоматическим закрытием соединения."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
result = await conn.execute(query, params)
|
||||
# Получаем все результаты сразу, чтобы можно было закрыть соединение
|
||||
rows = await result.fetchall()
|
||||
return rows
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при выполнении запроса: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def _execute_transaction(self, queries: list):
|
||||
"""Выполнение транзакции с несколькими запросами."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
for query, params in queries:
|
||||
await conn.execute(query, params)
|
||||
await conn.commit()
|
||||
except Exception as e:
|
||||
if conn:
|
||||
await conn.rollback()
|
||||
self.logger.error(f"Ошибка при выполнении транзакции: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def check_database_integrity(self):
|
||||
"""Проверяет целостность базы данных и очищает WAL файлы."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
result = await conn.execute("PRAGMA integrity_check")
|
||||
integrity_result = await result.fetchone()
|
||||
|
||||
if integrity_result and integrity_result[0] == "ok":
|
||||
self.logger.info("Проверка целостности базы данных прошла успешно")
|
||||
await conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
self.logger.info("WAL файлы очищены")
|
||||
else:
|
||||
self.logger.warning(f"Проблемы с целостностью базы данных: {integrity_result}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при проверке целостности базы данных: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def cleanup_wal_files(self):
|
||||
"""Очищает WAL файлы и переключает на DELETE режим для предотвращения проблем с I/O."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
await conn.execute("PRAGMA journal_mode=DELETE")
|
||||
await conn.execute("PRAGMA journal_mode=WAL")
|
||||
self.logger.info("WAL файлы очищены и режим восстановлен")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при очистке WAL файлов: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
1495
database/db.py
1495
database/db.py
File diff suppressed because it is too large
Load Diff
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Скрипт для диагностики и исправления проблем с базой данных Telegram бота.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
def check_database_file(db_path):
|
||||
"""Проверяет состояние файла базы данных."""
|
||||
print(f"Проверка файла: {db_path}")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Файл базы данных не найден: {db_path}")
|
||||
return False
|
||||
|
||||
# Проверяем права доступа
|
||||
if not os.access(db_path, os.R_OK | os.W_OK):
|
||||
print(f"❌ Нет прав доступа к файлу: {db_path}")
|
||||
return False
|
||||
|
||||
# Проверяем размер файла
|
||||
file_size = os.path.getsize(db_path)
|
||||
print(f"✅ Размер файла: {file_size} байт")
|
||||
|
||||
return True
|
||||
|
||||
def check_wal_files(db_path):
|
||||
"""Проверяет WAL файлы."""
|
||||
db_dir = os.path.dirname(db_path)
|
||||
db_name = os.path.basename(db_path)
|
||||
base_name = os.path.splitext(db_name)[0]
|
||||
|
||||
wal_file = os.path.join(db_dir, f"{base_name}.db-wal")
|
||||
shm_file = os.path.join(db_dir, f"{base_name}.db-shm")
|
||||
|
||||
print(f"\nПроверка WAL файлов:")
|
||||
|
||||
if os.path.exists(wal_file):
|
||||
wal_size = os.path.getsize(wal_file)
|
||||
print(f"✅ WAL файл найден: {wal_file} ({wal_size} байт)")
|
||||
else:
|
||||
print(f"ℹ️ WAL файл не найден: {wal_file}")
|
||||
|
||||
if os.path.exists(shm_file):
|
||||
shm_size = os.path.getsize(shm_file)
|
||||
print(f"✅ SHM файл найден: {shm_file} ({shm_size} байт)")
|
||||
else:
|
||||
print(f"ℹ️ SHM файл не найден: {shm_file}")
|
||||
|
||||
return wal_file, shm_file
|
||||
|
||||
def test_database_connection(db_path):
|
||||
"""Тестирует подключение к базе данных."""
|
||||
print(f"\nТестирование подключения к базе данных...")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path, timeout=10.0)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверяем версию SQLite
|
||||
cursor.execute("SELECT sqlite_version()")
|
||||
version = cursor.fetchone()[0]
|
||||
print(f"✅ SQLite версия: {version}")
|
||||
|
||||
# Проверяем таблицы
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = cursor.fetchall()
|
||||
print(f"✅ Найдено таблиц: {len(tables)}")
|
||||
|
||||
# Проверяем целостность
|
||||
cursor.execute("PRAGMA integrity_check")
|
||||
integrity = cursor.fetchone()[0]
|
||||
if integrity == "ok":
|
||||
print("✅ Целостность базы данных: OK")
|
||||
else:
|
||||
print(f"⚠️ Проблемы с целостностью: {integrity}")
|
||||
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ Ошибка SQLite: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Неожиданная ошибка: {e}")
|
||||
return False
|
||||
|
||||
def cleanup_wal_files(db_path):
|
||||
"""Очищает WAL файлы."""
|
||||
print(f"\nОчистка WAL файлов...")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path, timeout=10.0)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Переключаем на DELETE режим для очистки WAL
|
||||
cursor.execute("PRAGMA journal_mode=DELETE")
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
|
||||
# Принудительно создаем checkpoint
|
||||
cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
|
||||
conn.close()
|
||||
print("✅ WAL файлы очищены")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при очистке WAL файлов: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Основная функция."""
|
||||
print("🔧 Диагностика базы данных Telegram бота")
|
||||
print("=" * 50)
|
||||
|
||||
# Определяем путь к базе данных
|
||||
current_dir = os.getcwd()
|
||||
db_path = os.path.join(current_dir, 'database', 'tg-bot-database.db')
|
||||
|
||||
print(f"Текущая директория: {current_dir}")
|
||||
print(f"Путь к базе данных: {db_path}")
|
||||
|
||||
# Проверяем файл базы данных
|
||||
if not check_database_file(db_path):
|
||||
print("\n❌ Файл базы данных недоступен. Проверьте права доступа и существование файла.")
|
||||
return
|
||||
|
||||
# Проверяем WAL файлы
|
||||
wal_file, shm_file = check_wal_files(db_path)
|
||||
|
||||
# Тестируем подключение
|
||||
if not test_database_connection(db_path):
|
||||
print("\n❌ Не удалось подключиться к базе данных.")
|
||||
return
|
||||
|
||||
# Очищаем WAL файлы
|
||||
if cleanup_wal_files(db_path):
|
||||
print("\n✅ База данных проверена и исправлена.")
|
||||
else:
|
||||
print("\n⚠️ База данных проверена, но не удалось очистить WAL файлы.")
|
||||
|
||||
print("\n📋 Рекомендации:")
|
||||
print("1. Убедитесь, что у процесса есть права на запись в директорию database/")
|
||||
print("2. Проверьте свободное место на диске")
|
||||
print("3. Если проблемы продолжаются, попробуйте перезапустить бота")
|
||||
print("4. В крайнем случае, создайте резервную копию и пересоздайте базу данных")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
103
database/models.py
Normal file
103
database/models.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""Модель пользователя."""
|
||||
user_id: int
|
||||
first_name: str
|
||||
full_name: str
|
||||
username: Optional[str] = None
|
||||
is_bot: bool = False
|
||||
language_code: str = "ru"
|
||||
emoji: str = "😊"
|
||||
has_stickers: bool = False
|
||||
date_added: Optional[str] = None
|
||||
date_changed: Optional[str] = None
|
||||
voice_bot_welcome_received: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlacklistUser:
|
||||
"""Модель пользователя в черном списке."""
|
||||
user_id: int
|
||||
message_for_user: Optional[str] = None
|
||||
date_to_unban: Optional[int] = None
|
||||
created_at: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserMessage:
|
||||
"""Модель сообщения пользователя."""
|
||||
message_text: str
|
||||
user_id: int
|
||||
telegram_message_id: int
|
||||
date: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TelegramPost:
|
||||
"""Модель поста из Telegram."""
|
||||
message_id: int
|
||||
text: str
|
||||
author_id: int
|
||||
helper_text_message_id: Optional[int] = None
|
||||
created_at: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PostContent:
|
||||
"""Модель контента поста."""
|
||||
message_id: int
|
||||
content_name: str
|
||||
content_type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageContentLink:
|
||||
"""Модель связи сообщения с контентом."""
|
||||
post_id: int
|
||||
message_id: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Admin:
|
||||
"""Модель администратора."""
|
||||
user_id: int
|
||||
role: str = "admin"
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Migration:
|
||||
"""Модель миграции."""
|
||||
version: int
|
||||
script_name: str
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioMessage:
|
||||
"""Модель аудио сообщения."""
|
||||
file_name: str
|
||||
author_id: int
|
||||
date_added: str
|
||||
file_id: str
|
||||
listen_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioListenRecord:
|
||||
"""Модель записи прослушивания аудио."""
|
||||
file_name: str
|
||||
user_id: int
|
||||
is_listen: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioModerate:
|
||||
"""Модель для voice bot."""
|
||||
message_id: int
|
||||
user_id: int
|
||||
23
database/repositories/__init__.py
Normal file
23
database/repositories/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Пакет репозиториев для работы с базой данных.
|
||||
|
||||
Содержит репозитории для разных сущностей:
|
||||
- user_repository: работа с пользователями
|
||||
- blacklist_repository: работа с черным списком
|
||||
- message_repository: работа с сообщениями
|
||||
- post_repository: работа с постами
|
||||
- admin_repository: работа с администраторами
|
||||
- audio_repository: работа с аудио
|
||||
"""
|
||||
|
||||
from .user_repository import UserRepository
|
||||
from .blacklist_repository import BlacklistRepository
|
||||
from .message_repository import MessageRepository
|
||||
from .post_repository import PostRepository
|
||||
from .admin_repository import AdminRepository
|
||||
from .audio_repository import AudioRepository
|
||||
|
||||
__all__ = [
|
||||
'UserRepository', 'BlacklistRepository', 'MessageRepository', 'PostRepository',
|
||||
'AdminRepository', 'AudioRepository'
|
||||
]
|
||||
74
database/repositories/admin_repository.py
Normal file
74
database/repositories/admin_repository.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from typing import Optional
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import Admin
|
||||
|
||||
|
||||
class AdminRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с администраторами."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы администраторов."""
|
||||
# Включаем поддержку внешних ключей
|
||||
await self._execute_query("PRAGMA foreign_keys = ON")
|
||||
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
role TEXT DEFAULT 'admin',
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица администраторов создана")
|
||||
|
||||
async def add_admin(self, admin: Admin) -> None:
|
||||
"""Добавление администратора."""
|
||||
query = "INSERT INTO admins (user_id, role) VALUES (?, ?)"
|
||||
params = (admin.user_id, admin.role)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Администратор добавлен: user_id={admin.user_id}, role={admin.role}")
|
||||
|
||||
async def remove_admin(self, user_id: int) -> None:
|
||||
"""Удаление администратора."""
|
||||
query = "DELETE FROM admins WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Администратор удален: user_id={user_id}")
|
||||
|
||||
async def is_admin(self, user_id: int) -> bool:
|
||||
"""Проверка, является ли пользователь администратором."""
|
||||
query = "SELECT 1 FROM admins WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return bool(row)
|
||||
|
||||
async def get_admin(self, user_id: int) -> Optional[Admin]:
|
||||
"""Получение информации об администраторе."""
|
||||
query = "SELECT user_id, role, created_at FROM admins WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return Admin(
|
||||
user_id=row[0],
|
||||
role=row[1],
|
||||
created_at=row[2] if len(row) > 2 else None
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_all_admins(self) -> list[Admin]:
|
||||
"""Получение всех администраторов."""
|
||||
query = "SELECT user_id, role, created_at FROM admins ORDER BY created_at DESC"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
|
||||
admins = []
|
||||
for row in rows:
|
||||
admin = Admin(
|
||||
user_id=row[0],
|
||||
role=row[1],
|
||||
created_at=row[2] if len(row) > 2 else None
|
||||
)
|
||||
admins.append(admin)
|
||||
|
||||
return admins
|
||||
216
database/repositories/audio_repository.py
Normal file
216
database/repositories/audio_repository.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from typing import Optional, List
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import AudioMessage, AudioListenRecord, AudioModerate
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AudioRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с аудио сообщениями."""
|
||||
|
||||
async def enable_foreign_keys(self):
|
||||
"""Включает поддержку внешних ключей."""
|
||||
await self._execute_query("PRAGMA foreign_keys = ON;")
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблиц для аудио."""
|
||||
# Таблица аудио сообщений
|
||||
audio_query = '''
|
||||
CREATE TABLE IF NOT EXISTS audio_message_reference (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
file_name TEXT NOT NULL UNIQUE,
|
||||
author_id INTEGER NOT NULL,
|
||||
date_added INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(audio_query)
|
||||
|
||||
# Таблица прослушивания аудио
|
||||
listen_query = '''
|
||||
CREATE TABLE IF NOT EXISTS user_audio_listens (
|
||||
file_name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (file_name, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(listen_query)
|
||||
|
||||
# Таблица для voice bot
|
||||
voice_query = '''
|
||||
CREATE TABLE IF NOT EXISTS audio_moderate (
|
||||
user_id INTEGER NOT NULL,
|
||||
message_id INTEGER,
|
||||
PRIMARY KEY (user_id, message_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(voice_query)
|
||||
|
||||
self.logger.info("Таблицы для аудио созданы")
|
||||
|
||||
async def add_audio_record(self, audio: AudioMessage) -> None:
|
||||
"""Добавляет информацию о войсе пользователя."""
|
||||
query = """
|
||||
INSERT INTO audio_message_reference (file_name, author_id, date_added)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
# Преобразуем datetime в UNIX timestamp если нужно
|
||||
if isinstance(audio.date_added, str):
|
||||
date_timestamp = int(datetime.fromisoformat(audio.date_added).timestamp())
|
||||
elif isinstance(audio.date_added, datetime):
|
||||
date_timestamp = int(audio.date_added.timestamp())
|
||||
else:
|
||||
date_timestamp = audio.date_added
|
||||
|
||||
params = (audio.file_name, audio.author_id, date_timestamp)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Аудио добавлено: file_name={audio.file_name}, author_id={audio.author_id}")
|
||||
|
||||
async def add_audio_record_simple(self, file_name: str, user_id: int, date_added) -> None:
|
||||
"""Добавляет информацию о войсе пользователя (упрощенная версия)."""
|
||||
query = """
|
||||
INSERT INTO audio_message_reference (file_name, author_id, date_added)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
# Преобразуем datetime в UNIX timestamp если нужно
|
||||
if isinstance(date_added, str):
|
||||
date_timestamp = int(datetime.fromisoformat(date_added).timestamp())
|
||||
elif isinstance(date_added, datetime):
|
||||
date_timestamp = int(date_added.timestamp())
|
||||
else:
|
||||
date_timestamp = date_added
|
||||
|
||||
params = (file_name, user_id, date_timestamp)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Аудио добавлено: file_name={file_name}, user_id={user_id}")
|
||||
|
||||
async def get_last_date_audio(self) -> Optional[int]:
|
||||
"""Получает дату последнего войса."""
|
||||
query = "SELECT date_added FROM audio_message_reference ORDER BY date_added DESC LIMIT 1"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
self.logger.info(f"Последняя дата аудио: {row[0]}")
|
||||
return row[0]
|
||||
return None
|
||||
|
||||
async def get_user_audio_records_count(self, user_id: int) -> int:
|
||||
"""Получает количество записей пользователя."""
|
||||
query = "SELECT COUNT(*) FROM audio_message_reference WHERE author_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else 0
|
||||
|
||||
async def get_path_for_audio_record(self, user_id: int) -> Optional[str]:
|
||||
"""Получает название последнего файла пользователя."""
|
||||
query = """
|
||||
SELECT file_name FROM audio_message_reference
|
||||
WHERE author_id = ? ORDER BY date_added DESC LIMIT 1
|
||||
"""
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else None
|
||||
|
||||
async def check_listen_audio(self, user_id: int) -> List[str]:
|
||||
"""Проверяет непрослушанные аудио для пользователя."""
|
||||
query = """
|
||||
SELECT l.file_name
|
||||
FROM audio_message_reference a
|
||||
LEFT JOIN user_audio_listens l ON l.file_name = a.file_name
|
||||
WHERE l.user_id = ? AND l.file_name IS NOT NULL
|
||||
"""
|
||||
listened_files = await self._execute_query_with_result(query, (user_id,))
|
||||
|
||||
# Получаем все аудио, кроме созданных пользователем
|
||||
all_audio_query = 'SELECT file_name FROM audio_message_reference WHERE author_id <> ?'
|
||||
all_files = await self._execute_query_with_result(all_audio_query, (user_id,))
|
||||
|
||||
# Находим непрослушанные
|
||||
listened_set = {row[0] for row in listened_files}
|
||||
all_set = {row[0] for row in all_files}
|
||||
new_files = list(all_set - listened_set)
|
||||
|
||||
self.logger.info(f"Найдено {len(new_files)} непрослушанных аудио для пользователя {user_id}")
|
||||
return new_files
|
||||
|
||||
async def mark_listened_audio(self, file_name: str, user_id: int) -> None:
|
||||
"""Отмечает аудио прослушанным для пользователя."""
|
||||
query = "INSERT OR IGNORE INTO user_audio_listens (file_name, user_id) VALUES (?, ?)"
|
||||
params = (file_name, user_id)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Аудио {file_name} отмечено как прослушанное для пользователя {user_id}")
|
||||
|
||||
async def get_user_id_by_file_name(self, file_name: str) -> Optional[int]:
|
||||
"""Получает user_id пользователя по имени файла."""
|
||||
query = "SELECT author_id FROM audio_message_reference WHERE file_name = ?"
|
||||
rows = await self._execute_query_with_result(query, (file_name,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
user_id = row[0]
|
||||
self.logger.info(f"Получен user_id {user_id} для файла {file_name}")
|
||||
return user_id
|
||||
return None
|
||||
|
||||
async def get_date_by_file_name(self, file_name: str) -> Optional[str]:
|
||||
"""Получает дату добавления файла."""
|
||||
query = "SELECT date_added FROM audio_message_reference WHERE file_name = ?"
|
||||
rows = await self._execute_query_with_result(query, (file_name,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
date_added = row[0]
|
||||
# Преобразуем UNIX timestamp в читаемую дату
|
||||
readable_date = datetime.fromtimestamp(date_added).strftime('%d.%m.%Y %H:%M')
|
||||
self.logger.info(f"Получена дата {readable_date} для файла {file_name}")
|
||||
return readable_date
|
||||
return None
|
||||
|
||||
async def refresh_listen_audio(self, user_id: int) -> None:
|
||||
"""Очищает всю информацию о прослушанных аудио пользователем."""
|
||||
query = "DELETE FROM user_audio_listens WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Очищены записи прослушивания для пользователя {user_id}")
|
||||
|
||||
async def delete_listen_count_for_user(self, user_id: int) -> None:
|
||||
"""Удаляет данные о прослушанных пользователем аудио."""
|
||||
query = "DELETE FROM user_audio_listens WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Удалены записи прослушивания для пользователя {user_id}")
|
||||
|
||||
# Методы для voice bot
|
||||
async def set_user_id_and_message_id_for_voice_bot(self, message_id: int, user_id: int) -> bool:
|
||||
"""Устанавливает связь между message_id и user_id для voice bot."""
|
||||
try:
|
||||
query = "INSERT OR IGNORE INTO audio_moderate (user_id, message_id) VALUES (?, ?)"
|
||||
params = (user_id, message_id)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Связь установлена: message_id={message_id}, user_id={user_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка установки связи: {e}")
|
||||
return False
|
||||
|
||||
async def get_user_id_by_message_id_for_voice_bot(self, message_id: int) -> Optional[int]:
|
||||
"""Получает user_id пользователя по message_id для voice bot."""
|
||||
query = "SELECT user_id FROM audio_moderate WHERE message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
user_id = row[0]
|
||||
self.logger.info(f"Получен user_id {user_id} для message_id {message_id}")
|
||||
return user_id
|
||||
return None
|
||||
|
||||
async def delete_audio_moderate_record(self, message_id: int) -> None:
|
||||
"""Удаляет запись из таблицы audio_moderate по message_id."""
|
||||
query = "DELETE FROM audio_moderate WHERE message_id = ?"
|
||||
await self._execute_query(query, (message_id,))
|
||||
self.logger.info(f"Удалена запись из audio_moderate для message_id {message_id}")
|
||||
116
database/repositories/blacklist_repository.py
Normal file
116
database/repositories/blacklist_repository.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from typing import Optional, List, Dict
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import BlacklistUser
|
||||
|
||||
|
||||
class BlacklistRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с черным списком."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы черного списка."""
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS blacklist (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
message_for_user TEXT,
|
||||
date_to_unban INTEGER,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица черного списка создана")
|
||||
|
||||
async def add_user(self, blacklist_user: BlacklistUser) -> None:
|
||||
"""Добавляет пользователя в черный список."""
|
||||
query = """
|
||||
INSERT INTO blacklist (user_id, message_for_user, date_to_unban)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
params = (blacklist_user.user_id, blacklist_user.message_for_user, blacklist_user.date_to_unban)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Пользователь добавлен в черный список: user_id={blacklist_user.user_id}")
|
||||
|
||||
async def remove_user(self, user_id: int) -> bool:
|
||||
"""Удаляет пользователя из черного списка."""
|
||||
try:
|
||||
query = "DELETE FROM blacklist WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Пользователь с идентификатором {user_id} успешно удален из черного списка.")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка удаления пользователя с идентификатором {user_id} "
|
||||
f"из таблицы blacklist. Ошибка: {str(e)}")
|
||||
return False
|
||||
|
||||
async def user_exists(self, user_id: int) -> bool:
|
||||
"""Проверяет, существует ли запись с данным user_id в blacklist."""
|
||||
query = "SELECT 1 FROM blacklist WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
self.logger.info(f"Существует ли пользователь: user_id={user_id} Итог: {rows}")
|
||||
return bool(rows)
|
||||
|
||||
async def get_user(self, user_id: int) -> Optional[BlacklistUser]:
|
||||
"""Возвращает информацию о пользователе в черном списке по user_id."""
|
||||
query = "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return BlacklistUser(
|
||||
user_id=row[0],
|
||||
message_for_user=row[1],
|
||||
date_to_unban=row[2],
|
||||
created_at=row[3]
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_all_users(self, offset: int = 0, limit: int = 10) -> List[BlacklistUser]:
|
||||
"""Возвращает список пользователей в черном списке."""
|
||||
query = "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist LIMIT ?, ?"
|
||||
rows = await self._execute_query_with_result(query, (offset, limit))
|
||||
|
||||
users = []
|
||||
for row in rows:
|
||||
users.append(BlacklistUser(
|
||||
user_id=row[0],
|
||||
message_for_user=row[1],
|
||||
date_to_unban=row[2],
|
||||
created_at=row[3]
|
||||
))
|
||||
|
||||
self.logger.info(f"Получен список пользователей в черном списке (offset={offset}, limit={limit}): {len(users)}")
|
||||
return users
|
||||
|
||||
async def get_all_users_no_limit(self) -> List[BlacklistUser]:
|
||||
"""Возвращает список всех пользователей в черном списке без лимитов."""
|
||||
query = "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
|
||||
users = []
|
||||
for row in rows:
|
||||
users.append(BlacklistUser(
|
||||
user_id=row[0],
|
||||
message_for_user=row[1],
|
||||
date_to_unban=row[2],
|
||||
created_at=row[3]
|
||||
))
|
||||
|
||||
self.logger.info(f"Получен список всех пользователей в черном списке: {len(users)}")
|
||||
return users
|
||||
|
||||
async def get_users_for_unblock_today(self, current_timestamp: int) -> Dict[int, int]:
|
||||
"""Возвращает список пользователей, у которых истек срок блокировки."""
|
||||
query = "SELECT user_id FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?"
|
||||
rows = await self._execute_query_with_result(query, (current_timestamp,))
|
||||
|
||||
users = {user_id: user_id for user_id, in rows}
|
||||
self.logger.info(f"Получен список пользователей для разблокировки: {users}")
|
||||
return users
|
||||
|
||||
async def get_count(self) -> int:
|
||||
"""Получение количества пользователей в черном списке."""
|
||||
query = "SELECT COUNT(*) FROM blacklist"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else 0
|
||||
44
database/repositories/message_repository.py
Normal file
44
database/repositories/message_repository.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import UserMessage
|
||||
|
||||
|
||||
class MessageRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с сообщениями пользователей."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы сообщений пользователей."""
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT,
|
||||
user_id INTEGER,
|
||||
telegram_message_id INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица сообщений пользователей создана")
|
||||
|
||||
async def add_message(self, message: UserMessage) -> None:
|
||||
"""Добавление сообщения пользователя."""
|
||||
if message.date is None:
|
||||
message.date = int(datetime.now().timestamp())
|
||||
|
||||
query = """
|
||||
INSERT INTO user_messages (message_text, user_id, telegram_message_id, date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
params = (message.message_text, message.user_id, message.telegram_message_id, message.date)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Новое сообщение добавлено: telegram_message_id={message.telegram_message_id}")
|
||||
|
||||
async def get_user_by_message_id(self, message_id: int) -> Optional[int]:
|
||||
"""Получение пользователя по message_id."""
|
||||
query = "SELECT user_id FROM user_messages WHERE telegram_message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (message_id,))
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else None
|
||||
150
database/repositories/post_repository.py
Normal file
150
database/repositories/post_repository.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Tuple
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import TelegramPost, PostContent, MessageContentLink
|
||||
|
||||
|
||||
class PostRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с постами из Telegram."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблиц для постов."""
|
||||
# Таблица постов из Telegram
|
||||
post_query = '''
|
||||
CREATE TABLE IF NOT EXISTS post_from_telegram_suggest (
|
||||
message_id INTEGER NOT NULL PRIMARY KEY,
|
||||
text TEXT,
|
||||
helper_text_message_id INTEGER,
|
||||
author_id INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(post_query)
|
||||
|
||||
# Таблица контента постов
|
||||
content_query = '''
|
||||
CREATE TABLE IF NOT EXISTS content_post_from_telegram (
|
||||
message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
content_type TEXT,
|
||||
PRIMARY KEY (message_id, content_name),
|
||||
FOREIGN KEY (message_id) REFERENCES post_from_telegram_suggest (message_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(content_query)
|
||||
|
||||
# Таблица связи сообщений с контентом
|
||||
link_query = '''
|
||||
CREATE TABLE IF NOT EXISTS message_link_to_content (
|
||||
post_id INTEGER NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, message_id),
|
||||
FOREIGN KEY (post_id) REFERENCES post_from_telegram_suggest (message_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(link_query)
|
||||
|
||||
self.logger.info("Таблицы для постов созданы")
|
||||
|
||||
async def add_post(self, post: TelegramPost) -> None:
|
||||
"""Добавление поста."""
|
||||
if not post.created_at:
|
||||
post.created_at = int(datetime.now().timestamp())
|
||||
|
||||
query = """
|
||||
INSERT INTO post_from_telegram_suggest (message_id, text, author_id, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
params = (post.message_id, post.text, post.author_id, post.created_at)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Пост добавлен: message_id={post.message_id}")
|
||||
|
||||
async def update_helper_message(self, message_id: int, helper_message_id: int) -> None:
|
||||
"""Обновление helper сообщения."""
|
||||
query = "UPDATE post_from_telegram_suggest SET helper_text_message_id = ? WHERE message_id = ?"
|
||||
await self._execute_query(query, (helper_message_id, message_id))
|
||||
|
||||
async def add_post_content(self, post_id: int, message_id: int, content_name: str, content_type: str) -> bool:
|
||||
"""Добавление контента поста."""
|
||||
try:
|
||||
# Сначала добавляем связь
|
||||
link_query = "INSERT OR IGNORE INTO message_link_to_content (post_id, message_id) VALUES (?, ?)"
|
||||
await self._execute_query(link_query, (post_id, message_id))
|
||||
|
||||
# Затем добавляем контент
|
||||
content_query = """
|
||||
INSERT OR IGNORE INTO content_post_from_telegram (message_id, content_name, content_type)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
await self._execute_query(content_query, (message_id, content_name, content_type))
|
||||
|
||||
self.logger.info(f"Контент поста добавлен: post_id={post_id}, message_id={message_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при добавлении контента поста: {e}")
|
||||
return False
|
||||
|
||||
async def get_post_content_by_helper_id(self, helper_message_id: int) -> List[Tuple[str, str]]:
|
||||
"""Получает контент поста по helper_text_message_id."""
|
||||
query = """
|
||||
SELECT cpft.content_name, cpft.content_type
|
||||
FROM post_from_telegram_suggest pft
|
||||
JOIN message_link_to_content mltc ON pft.message_id = mltc.post_id
|
||||
JOIN content_post_from_telegram cpft ON cpft.message_id = mltc.message_id
|
||||
WHERE pft.helper_text_message_id = ?
|
||||
"""
|
||||
post_content = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
|
||||
self.logger.info(f"Получен контент поста: {len(post_content)} элементов")
|
||||
return post_content
|
||||
|
||||
async def get_post_text_by_helper_id(self, helper_message_id: int) -> Optional[str]:
|
||||
"""Получает текст поста по helper_text_message_id."""
|
||||
query = "SELECT text FROM post_from_telegram_suggest WHERE helper_text_message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
self.logger.info(f"Получен текст поста для helper_message_id={helper_message_id}")
|
||||
return row[0]
|
||||
return None
|
||||
|
||||
async def get_post_ids_by_helper_id(self, helper_message_id: int) -> List[int]:
|
||||
"""Получает ID сообщений по helper_text_message_id."""
|
||||
query = """
|
||||
SELECT mltc.message_id
|
||||
FROM post_from_telegram_suggest pft
|
||||
JOIN message_link_to_content mltc ON pft.message_id = mltc.post_id
|
||||
WHERE pft.helper_text_message_id = ?
|
||||
"""
|
||||
rows = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
|
||||
post_ids = [row[0] for row in rows]
|
||||
self.logger.info(f"Получены ID сообщений: {len(post_ids)} элементов")
|
||||
return post_ids
|
||||
|
||||
async def get_author_id_by_message_id(self, message_id: int) -> Optional[int]:
|
||||
"""Получает ID автора по message_id."""
|
||||
query = "SELECT author_id FROM post_from_telegram_suggest WHERE message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
author_id = row[0]
|
||||
self.logger.info(f"Получен author_id: {author_id} для message_id={message_id}")
|
||||
return author_id
|
||||
return None
|
||||
|
||||
async def get_author_id_by_helper_message_id(self, helper_message_id: int) -> Optional[int]:
|
||||
"""Получает ID автора по helper_text_message_id."""
|
||||
query = "SELECT author_id FROM post_from_telegram_suggest WHERE helper_text_message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
author_id = row[0]
|
||||
self.logger.info(f"Получен author_id: {author_id} для helper_message_id={helper_message_id}")
|
||||
return author_id
|
||||
return None
|
||||
258
database/repositories/user_repository.py
Normal file
258
database/repositories/user_repository.py
Normal file
@@ -0,0 +1,258 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import User
|
||||
|
||||
|
||||
class UserRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с пользователями."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы пользователей."""
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
full_name TEXT,
|
||||
username TEXT,
|
||||
is_bot BOOLEAN DEFAULT 0,
|
||||
language_code TEXT,
|
||||
has_stickers BOOLEAN DEFAULT 0 NOT NULL,
|
||||
emoji TEXT,
|
||||
date_added INTEGER NOT NULL,
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица пользователей создана")
|
||||
|
||||
async def user_exists(self, user_id: int) -> bool:
|
||||
"""Проверяет, существует ли пользователь в базе данных."""
|
||||
query = "SELECT user_id FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
self.logger.info(f"Проверка существования пользователя: user_id={user_id}, результат={rows}")
|
||||
return bool(len(rows))
|
||||
|
||||
async def add_user(self, user: User) -> None:
|
||||
"""Добавление нового пользователя."""
|
||||
if not user.date_added:
|
||||
user.date_added = int(datetime.now().timestamp())
|
||||
if not user.date_changed:
|
||||
user.date_changed = int(datetime.now().timestamp())
|
||||
|
||||
query = """
|
||||
INSERT INTO our_users (user_id, first_name, full_name, username, is_bot,
|
||||
language_code, emoji, has_stickers, date_added, date_changed, voice_bot_welcome_received)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
params = (user.user_id, user.first_name, user.full_name, user.username,
|
||||
user.is_bot, user.language_code, user.emoji, user.has_stickers,
|
||||
user.date_added, user.date_changed, user.voice_bot_welcome_received)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Новый пользователь добавлен: {user.user_id}")
|
||||
|
||||
async def get_user_info(self, user_id: int) -> Optional[User]:
|
||||
"""Получение информации о пользователе."""
|
||||
query = "SELECT username, full_name, has_stickers, emoji FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return User(
|
||||
user_id=user_id,
|
||||
first_name="", # Не получаем из этого запроса
|
||||
full_name=row[1],
|
||||
username=row[0],
|
||||
has_stickers=bool(row[2]) if row[2] is not None else False,
|
||||
emoji=row[3]
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Получение пользователя по ID."""
|
||||
query = "SELECT * FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return User(
|
||||
user_id=row[0],
|
||||
first_name=row[1],
|
||||
full_name=row[2],
|
||||
username=row[3],
|
||||
is_bot=bool(row[4]),
|
||||
language_code=row[5],
|
||||
has_stickers=bool(row[6]),
|
||||
emoji=row[7],
|
||||
date_added=row[8],
|
||||
date_changed=row[9],
|
||||
voice_bot_welcome_received=bool(row[10]) if len(row) > 10 else False
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_username(self, user_id: int) -> Optional[str]:
|
||||
"""Возвращает username пользователя."""
|
||||
query = "SELECT username FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
username = row[0]
|
||||
self.logger.info(f"Username пользователя найден: user_id={user_id}, username={username}")
|
||||
return username
|
||||
return None
|
||||
|
||||
async def get_user_id_by_username(self, username: str) -> Optional[int]:
|
||||
"""Возвращает user_id пользователя по username."""
|
||||
query = "SELECT user_id FROM our_users WHERE username = ?"
|
||||
rows = await self._execute_query_with_result(query, (username,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
user_id = row[0]
|
||||
self.logger.info(f"User_id пользователя найден: username={username}, user_id={user_id}")
|
||||
return user_id
|
||||
return None
|
||||
|
||||
async def get_full_name_by_id(self, user_id: int) -> Optional[str]:
|
||||
"""Возвращает full_name пользователя."""
|
||||
query = "SELECT full_name FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
full_name = row[0]
|
||||
self.logger.info(f"Full_name пользователя найден: user_id={user_id}, full_name={full_name}")
|
||||
return full_name
|
||||
return None
|
||||
|
||||
async def get_user_first_name(self, user_id: int) -> Optional[str]:
|
||||
"""Возвращает first_name пользователя."""
|
||||
query = "SELECT first_name FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
first_name = row[0]
|
||||
self.logger.info(f"First_name пользователя найден: user_id={user_id}, first_name={first_name}")
|
||||
return first_name
|
||||
return None
|
||||
|
||||
async def get_all_user_ids(self) -> List[int]:
|
||||
"""Возвращает список всех user_id."""
|
||||
query = "SELECT user_id FROM our_users"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
user_ids = [row[0] for row in rows]
|
||||
self.logger.info(f"Получен список всех user_id: {user_ids}")
|
||||
return user_ids
|
||||
|
||||
async def get_last_users(self, limit: int = 30) -> List[tuple]:
|
||||
"""Получение последних пользователей."""
|
||||
query = "SELECT full_name, user_id FROM our_users ORDER BY date_changed DESC LIMIT ?"
|
||||
rows = await self._execute_query_with_result(query, (limit,))
|
||||
return rows
|
||||
|
||||
async def update_user_date(self, user_id: int) -> None:
|
||||
"""Обновление даты последнего изменения пользователя."""
|
||||
date_changed = int(datetime.now().timestamp())
|
||||
query = "UPDATE our_users SET date_changed = ? WHERE user_id = ?"
|
||||
await self._execute_query(query, (date_changed, user_id))
|
||||
|
||||
async def update_user_info(self, user_id: int, username: str = None, full_name: str = None) -> None:
|
||||
"""Обновление информации о пользователе."""
|
||||
if username and full_name:
|
||||
query = "UPDATE our_users SET username = ?, full_name = ? WHERE user_id = ?"
|
||||
params = (username, full_name, user_id)
|
||||
elif username:
|
||||
query = "UPDATE our_users SET username = ? WHERE user_id = ?"
|
||||
params = (username, user_id)
|
||||
elif full_name:
|
||||
query = "UPDATE our_users SET full_name = ? WHERE user_id = ?"
|
||||
params = (full_name, user_id)
|
||||
else:
|
||||
return
|
||||
|
||||
await self._execute_query(query, params)
|
||||
|
||||
async def update_user_emoji(self, user_id: int, emoji: str) -> None:
|
||||
"""Обновление эмодзи пользователя."""
|
||||
query = "UPDATE our_users SET emoji = ? WHERE user_id = ?"
|
||||
await self._execute_query(query, (emoji, user_id))
|
||||
|
||||
async def update_stickers_info(self, user_id: int) -> None:
|
||||
"""Обновление информации о стикерах."""
|
||||
query = "UPDATE our_users SET has_stickers = 1 WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
|
||||
async def get_stickers_info(self, user_id: int) -> bool:
|
||||
"""Получение информации о стикерах."""
|
||||
query = "SELECT has_stickers FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return bool(row[0]) if row and row[0] is not None else False
|
||||
|
||||
async def check_emoji_exists(self, emoji: str) -> bool:
|
||||
"""Проверка существования эмодзи."""
|
||||
query = "SELECT 1 FROM our_users WHERE emoji = ?"
|
||||
rows = await self._execute_query_with_result(query, (emoji,))
|
||||
row = rows[0] if rows else None
|
||||
return bool(row)
|
||||
|
||||
async def get_user_emoji(self, user_id: int) -> str:
|
||||
"""
|
||||
Получает эмодзи пользователя.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя.
|
||||
|
||||
Returns:
|
||||
str: Эмодзи пользователя или "Смайл еще не определен" если не установлен.
|
||||
"""
|
||||
query = "SELECT emoji FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row and row[0]:
|
||||
emoji = row[0]
|
||||
self.logger.info(f"Эмодзи пользователя найден: user_id={user_id}, emoji={emoji}")
|
||||
return str(emoji)
|
||||
else:
|
||||
self.logger.info(f"Эмодзи пользователя не найден: user_id={user_id}")
|
||||
return "Смайл еще не определен"
|
||||
|
||||
async def check_emoji_for_user(self, user_id: int) -> str:
|
||||
"""
|
||||
Проверяет, есть ли уже у пользователя назначенный emoji.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя.
|
||||
|
||||
Returns:
|
||||
str: Эмодзи пользователя или "Смайл еще не определен" если не установлен.
|
||||
"""
|
||||
return await self.get_user_emoji(user_id)
|
||||
|
||||
async def check_voice_bot_welcome_received(self, user_id: int) -> bool:
|
||||
"""Проверяет, получал ли пользователь приветственное сообщение от voice_bot."""
|
||||
query = "SELECT voice_bot_welcome_received FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
welcome_received = bool(row[0])
|
||||
self.logger.info(f"Пользователь {user_id} получал приветствие: {welcome_received}")
|
||||
return welcome_received
|
||||
return False
|
||||
|
||||
async def mark_voice_bot_welcome_received(self, user_id: int) -> bool:
|
||||
"""Отмечает, что пользователь получил приветственное сообщение от voice_bot."""
|
||||
try:
|
||||
query = "UPDATE our_users SET voice_bot_welcome_received = 1 WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Пользователь {user_id} отмечен как получивший приветствие")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при отметке получения приветствия: {e}")
|
||||
return False
|
||||
79
database/repository_factory.py
Normal file
79
database/repository_factory.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from typing import Optional
|
||||
from database.repositories.user_repository import UserRepository
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.repositories.post_repository import PostRepository
|
||||
from database.repositories.admin_repository import AdminRepository
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
|
||||
|
||||
class RepositoryFactory:
|
||||
"""Фабрика для создания репозиториев."""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self._user_repo: Optional[UserRepository] = None
|
||||
self._blacklist_repo: Optional[BlacklistRepository] = None
|
||||
self._message_repo: Optional[MessageRepository] = None
|
||||
self._post_repo: Optional[PostRepository] = None
|
||||
self._admin_repo: Optional[AdminRepository] = None
|
||||
self._audio_repo: Optional[AudioRepository] = None
|
||||
|
||||
@property
|
||||
def users(self) -> UserRepository:
|
||||
"""Возвращает репозиторий пользователей."""
|
||||
if self._user_repo is None:
|
||||
self._user_repo = UserRepository(self.db_path)
|
||||
return self._user_repo
|
||||
|
||||
@property
|
||||
def blacklist(self) -> BlacklistRepository:
|
||||
"""Возвращает репозиторий черного списка."""
|
||||
if self._blacklist_repo is None:
|
||||
self._blacklist_repo = BlacklistRepository(self.db_path)
|
||||
return self._blacklist_repo
|
||||
|
||||
@property
|
||||
def messages(self) -> MessageRepository:
|
||||
"""Возвращает репозиторий сообщений."""
|
||||
if self._message_repo is None:
|
||||
self._message_repo = MessageRepository(self.db_path)
|
||||
return self._message_repo
|
||||
|
||||
@property
|
||||
def posts(self) -> PostRepository:
|
||||
"""Возвращает репозиторий постов."""
|
||||
if self._post_repo is None:
|
||||
self._post_repo = PostRepository(self.db_path)
|
||||
return self._post_repo
|
||||
|
||||
@property
|
||||
def admins(self) -> AdminRepository:
|
||||
"""Возвращает репозиторий администраторов."""
|
||||
if self._admin_repo is None:
|
||||
self._admin_repo = AdminRepository(self.db_path)
|
||||
return self._admin_repo
|
||||
|
||||
@property
|
||||
def audio(self) -> AudioRepository:
|
||||
"""Возвращает репозиторий аудио."""
|
||||
if self._audio_repo is None:
|
||||
self._audio_repo = AudioRepository(self.db_path)
|
||||
return self._audio_repo
|
||||
|
||||
async def create_all_tables(self):
|
||||
"""Создает все таблицы в базе данных."""
|
||||
await self.users.create_tables()
|
||||
await self.blacklist.create_tables()
|
||||
await self.messages.create_tables()
|
||||
await self.posts.create_tables()
|
||||
await self.admins.create_tables()
|
||||
await self.audio.create_tables()
|
||||
|
||||
async def check_database_integrity(self):
|
||||
"""Проверяет целостность базы данных."""
|
||||
await self.users.check_database_integrity()
|
||||
|
||||
async def cleanup_wal_files(self):
|
||||
"""Очищает WAL файлы."""
|
||||
await self.users.cleanup_wal_files()
|
||||
113
database/schema.sql
Normal file
113
database/schema.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
-- Telegram Helper Bot Database Schema
|
||||
-- Compatible with Docker container deployment
|
||||
|
||||
-- IMPORTANT: Enable foreign key support after each database connection
|
||||
-- PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Note: sqlite_sequence table is automatically created by SQLite for AUTOINCREMENT fields
|
||||
-- No need to create it manually
|
||||
|
||||
-- Users who have listened to audio messages
|
||||
CREATE TABLE IF NOT EXISTS user_audio_listens (
|
||||
file_name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (file_name, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Reference table for audio messages
|
||||
CREATE TABLE IF NOT EXISTS audio_message_reference (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
file_name TEXT NOT NULL UNIQUE,
|
||||
author_id INTEGER NOT NULL,
|
||||
date_added INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Bot administrators
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
role TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- User blacklist for banned users
|
||||
CREATE TABLE IF NOT EXISTS blacklist (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
message_for_user TEXT,
|
||||
date_to_unban INTEGER,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- User message history
|
||||
CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT,
|
||||
user_id INTEGER,
|
||||
telegram_message_id INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Suggested posts from Telegram
|
||||
CREATE TABLE IF NOT EXISTS post_from_telegram_suggest (
|
||||
message_id INTEGER NOT NULL PRIMARY KEY,
|
||||
text TEXT,
|
||||
helper_text_message_id INTEGER,
|
||||
author_id INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Links between posts and content
|
||||
CREATE TABLE IF NOT EXISTS message_link_to_content (
|
||||
post_id INTEGER NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, message_id),
|
||||
FOREIGN KEY (post_id) REFERENCES post_from_telegram_suggest(message_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Content associated with Telegram posts
|
||||
CREATE TABLE IF NOT EXISTS content_post_from_telegram (
|
||||
message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
content_type TEXT,
|
||||
PRIMARY KEY (message_id, content_name),
|
||||
FOREIGN KEY (message_id) REFERENCES post_from_telegram_suggest(message_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Bot users information (user_id is now PRIMARY KEY)
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
full_name TEXT,
|
||||
username TEXT,
|
||||
is_bot BOOLEAN DEFAULT 0,
|
||||
language_code TEXT,
|
||||
has_stickers BOOLEAN DEFAULT 0 NOT NULL,
|
||||
emoji TEXT,
|
||||
date_added INTEGER NOT NULL,
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
-- Audio moderation tracking
|
||||
CREATE TABLE IF NOT EXISTS audio_moderate (
|
||||
user_id INTEGER NOT NULL,
|
||||
message_id INTEGER,
|
||||
PRIMARY KEY (user_id, message_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
-- Optimized index for user_audio_listens - only user_id for "show all audio listened by user X"
|
||||
CREATE INDEX IF NOT EXISTS idx_user_audio_listens_user_id ON user_audio_listens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_message_reference_author_id ON audio_message_reference(author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_messages_user_id ON user_messages(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_author_id ON post_from_telegram_suggest(author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_blacklist_date_to_unban ON blacklist(date_to_unban);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_messages_date ON user_messages(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_message_reference_date ON audio_message_reference(date_added);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_date ON post_from_telegram_suggest(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_our_users_date_changed ON our_users(date_changed);
|
||||
@@ -1,130 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
telegram-bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.bot
|
||||
container_name: telegram-bot
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "8000"
|
||||
environment:
|
||||
- PYTHONPATH=/app
|
||||
- DOCKER_CONTAINER=true
|
||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
- LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-30}
|
||||
- METRICS_HOST=${METRICS_HOST:-0.0.0.0}
|
||||
- METRICS_PORT=${METRICS_PORT:-8000}
|
||||
# Telegram settings
|
||||
- TELEGRAM_BOT_TOKEN=${BOT_TOKEN}
|
||||
- TELEGRAM_LISTEN_BOT_TOKEN=${LISTEN_BOT_TOKEN}
|
||||
- TELEGRAM_TEST_BOT_TOKEN=${TEST_BOT_TOKEN}
|
||||
- TELEGRAM_PREVIEW_LINK=${PREVIEW_LINK:-false}
|
||||
- TELEGRAM_MAIN_PUBLIC=${MAIN_PUBLIC}
|
||||
- TELEGRAM_GROUP_FOR_POSTS=${GROUP_FOR_POSTS}
|
||||
- TELEGRAM_GROUP_FOR_MESSAGE=${GROUP_FOR_MESSAGE}
|
||||
- TELEGRAM_GROUP_FOR_LOGS=${GROUP_FOR_LOGS}
|
||||
- TELEGRAM_IMPORTANT_LOGS=${IMPORTANT_LOGS}
|
||||
- TELEGRAM_ARCHIVE=${ARCHIVE}
|
||||
- TELEGRAM_TEST_GROUP=${TEST_GROUP}
|
||||
# Bot settings
|
||||
- SETTINGS_LOGS=${LOGS:-false}
|
||||
- SETTINGS_TEST=${TEST:-false}
|
||||
# Database
|
||||
- DATABASE_PATH=${DATABASE_PATH:-database/tg-bot-database.db}
|
||||
volumes:
|
||||
- ./database:/app/database:rw
|
||||
- ./logs:/app/logs:rw
|
||||
- ./.env:/app/.env:ro
|
||||
networks:
|
||||
- bot-internal
|
||||
depends_on:
|
||||
- prometheus
|
||||
- grafana
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '0.5'
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
expose:
|
||||
- "9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
- '--storage.tsdb.retention.time=200h'
|
||||
- '--web.enable-lifecycle'
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- bot-internal
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9090/-/healthy"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
ports:
|
||||
- "3000:3000" # Grafana доступна извне
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_SERVER_ROOT_URL=http://localhost:3000
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
|
||||
- ./grafana/datasources:/etc/grafana/provisioning/datasources:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- bot-internal
|
||||
depends_on:
|
||||
- prometheus
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
volumes:
|
||||
prometheus_data:
|
||||
driver: local
|
||||
grafana_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
bot-internal:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
@@ -20,9 +20,9 @@ TEST=false
|
||||
# Database
|
||||
DATABASE_PATH=database/tg-bot-database.db
|
||||
|
||||
# Monitoring
|
||||
# Monitoring (Centralized Prometheus)
|
||||
METRICS_HOST=0.0.0.0
|
||||
METRICS_PORT=8000
|
||||
METRICS_PORT=8080
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Telegram Bot Dashboards'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 10
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
@@ -1 +0,0 @@
|
||||
from . import server_monitor
|
||||
|
||||
@@ -25,6 +25,13 @@ from helper_bot.handlers.admin.utils import (
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors
|
||||
)
|
||||
|
||||
# Создаем роутер с middleware для проверки доступа
|
||||
admin_router = Router()
|
||||
admin_router.message.middleware(AdminAccessMiddleware())
|
||||
@@ -38,9 +45,12 @@ admin_router.message.middleware(AdminAccessMiddleware())
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command('admin')
|
||||
)
|
||||
@track_time("admin_panel", "admin_handlers")
|
||||
@track_errors("admin_handlers", "admin_panel")
|
||||
async def admin_panel(
|
||||
message: types.Message,
|
||||
state: FSMContext
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
"""Главное меню администратора"""
|
||||
try:
|
||||
@@ -52,11 +62,38 @@ async def admin_panel(
|
||||
await handle_admin_error(message, e, state, "admin_panel")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ХЕНДЛЕР ОТМЕНЫ
|
||||
# ============================================================================
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"),
|
||||
F.text == 'Отменить'
|
||||
)
|
||||
@track_time("cancel_ban_process", "admin_handlers")
|
||||
@track_errors("admin_handlers", "cancel_ban_process")
|
||||
async def cancel_ban_process(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
"""Отмена процесса блокировки"""
|
||||
try:
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
|
||||
await return_to_admin_menu(message, state)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "cancel_ban_process")
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
F.text == 'Бан (Список)'
|
||||
)
|
||||
@track_time("get_last_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_last_users")
|
||||
async def get_last_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
@@ -66,11 +103,11 @@ async def get_last_users(
|
||||
try:
|
||||
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
|
||||
admin_service = AdminService(bot_db)
|
||||
users = admin_service.get_last_users()
|
||||
users = await admin_service.get_last_users()
|
||||
|
||||
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
|
||||
users_data = [
|
||||
(user.full_name, user.username) # (full_name, username) - формат кортежей
|
||||
(user.full_name, user.user_id)
|
||||
for user in users
|
||||
]
|
||||
|
||||
@@ -88,6 +125,8 @@ async def get_last_users(
|
||||
StateFilter("ADMIN"),
|
||||
F.text == 'Разбан (список)'
|
||||
)
|
||||
@track_time("get_banned_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_banned_users")
|
||||
async def get_banned_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
@@ -97,7 +136,7 @@ async def get_banned_users(
|
||||
try:
|
||||
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
|
||||
admin_service = AdminService(bot_db)
|
||||
message_text, buttons_list = admin_service.get_banned_users_for_display(0)
|
||||
message_text, buttons_list = await admin_service.get_banned_users_for_display(0)
|
||||
|
||||
if buttons_list:
|
||||
keyboard = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
||||
@@ -117,9 +156,12 @@ async def get_banned_users(
|
||||
StateFilter("ADMIN"),
|
||||
F.text.in_(['Бан по нику', 'Бан по ID'])
|
||||
)
|
||||
@track_time("start_ban_process", "admin_handlers")
|
||||
@track_errors("admin_handlers", "start_ban_process")
|
||||
async def start_ban_process(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
"""Начало процесса блокировки пользователя"""
|
||||
try:
|
||||
@@ -137,38 +179,50 @@ async def start_ban_process(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_TARGET")
|
||||
)
|
||||
@track_time("process_ban_target", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_target")
|
||||
async def process_ban_target(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Обработка введенного username/ID для блокировки"""
|
||||
logger.info(f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
ban_type = user_data.get('ban_type')
|
||||
admin_service = AdminService(bot_db)
|
||||
|
||||
logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}")
|
||||
|
||||
# Определяем пользователя
|
||||
if ban_type == "username":
|
||||
user = admin_service.get_user_by_username(message.text)
|
||||
logger.info(f"process_ban_target: Поиск пользователя по username: {message.text}")
|
||||
user = await admin_service.get_user_by_username(message.text)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с username '{message.text}' не найден")
|
||||
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
else: # ban_type == "id"
|
||||
try:
|
||||
user_id = admin_service.validate_user_input(message.text)
|
||||
user = admin_service.get_user_by_id(user_id)
|
||||
logger.info(f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}")
|
||||
user_id = await admin_service.validate_user_input(message.text)
|
||||
user = await admin_service.get_user_by_id(user_id)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных")
|
||||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
except InvalidInputError as e:
|
||||
logger.error(f"process_ban_target: Ошибка валидации ID: {e}")
|
||||
await message.answer(str(e))
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
|
||||
logger.info(f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}")
|
||||
|
||||
# Сохраняем данные пользователя
|
||||
await state.update_data(
|
||||
target_user_id=user.user_id,
|
||||
@@ -179,13 +233,17 @@ async def process_ban_target(
|
||||
# Показываем информацию о пользователе и запрашиваем причину
|
||||
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||||
markup = create_keyboard_for_ban_reason()
|
||||
logger.info(f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}")
|
||||
|
||||
await message.answer(
|
||||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||
reply_markup=markup
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DETAILS')
|
||||
logger.info("process_ban_target: Состояние изменено на AWAIT_BAN_DETAILS")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"process_ban_target: Неожиданная ошибка: {e}", exc_info=True)
|
||||
await handle_admin_error(message, e, state, "process_ban_target")
|
||||
|
||||
|
||||
@@ -193,21 +251,41 @@ async def process_ban_target(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_DETAILS")
|
||||
)
|
||||
@track_time("process_ban_reason", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_reason")
|
||||
async def process_ban_reason(
|
||||
message: types.Message,
|
||||
state: FSMContext
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
"""Обработка причины блокировки"""
|
||||
logger.info(f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
|
||||
try:
|
||||
# Проверяем текущее состояние
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"process_ban_reason: Текущее состояние: {current_state}")
|
||||
|
||||
# Проверяем данные состояния
|
||||
state_data = await state.get_data()
|
||||
logger.info(f"process_ban_reason: Данные состояния: {state_data}")
|
||||
|
||||
logger.info(f"process_ban_reason: Обновление данных состояния с причиной: {message.text}")
|
||||
await state.update_data(ban_reason=message.text)
|
||||
|
||||
markup = create_keyboard_for_ban_days()
|
||||
safe_reason = escape_html(message.text)
|
||||
logger.info(f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}")
|
||||
|
||||
await message.answer(
|
||||
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
||||
reply_markup=markup
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DURATION')
|
||||
logger.info("process_ban_reason: Состояние изменено на AWAIT_BAN_DURATION")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"process_ban_reason: Неожиданная ошибка: {e}", exc_info=True)
|
||||
await handle_admin_error(message, e, state, "process_ban_reason")
|
||||
|
||||
|
||||
@@ -215,9 +293,12 @@ async def process_ban_reason(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_DURATION")
|
||||
)
|
||||
@track_time("process_ban_duration", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_duration")
|
||||
async def process_ban_duration(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
"""Обработка срока блокировки"""
|
||||
try:
|
||||
@@ -257,10 +338,13 @@ async def process_ban_duration(
|
||||
StateFilter("BAN_CONFIRMATION"),
|
||||
F.text == 'Подтвердить'
|
||||
)
|
||||
@track_time("confirm_ban", "admin_handlers")
|
||||
@track_errors("admin_handlers", "confirm_ban")
|
||||
async def confirm_ban(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
bot_db: MagicData("bot_db"),
|
||||
**kwargs
|
||||
):
|
||||
"""Подтверждение блокировки пользователя"""
|
||||
try:
|
||||
@@ -269,7 +353,7 @@ async def confirm_ban(
|
||||
|
||||
|
||||
# Выполняем блокировку
|
||||
admin_service.ban_user(
|
||||
await admin_service.ban_user(
|
||||
user_id=user_data['target_user_id'],
|
||||
username=user_data['target_username'],
|
||||
reason=user_data['ban_reason'],
|
||||
@@ -285,66 +369,3 @@ async def confirm_ban(
|
||||
await return_to_admin_menu(message, state)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "confirm_ban")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ХЕНДЛЕРЫ ОТМЕНЫ И НАВИГАЦИИ
|
||||
# ============================================================================
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"),
|
||||
F.text == 'Отменить'
|
||||
)
|
||||
async def cancel_ban_process(
|
||||
message: types.Message,
|
||||
state: FSMContext
|
||||
):
|
||||
"""Отмена процесса блокировки"""
|
||||
try:
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
|
||||
await return_to_admin_menu(message, state)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "cancel_ban_process")
|
||||
|
||||
|
||||
@admin_router.message(Command("test_metrics"))
|
||||
async def test_metrics_handler(
|
||||
message: types.Message,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Тестовый хендлер для проверки метрик"""
|
||||
from helper_bot.utils.metrics import metrics
|
||||
|
||||
try:
|
||||
# Принудительно записываем тестовые метрики
|
||||
metrics.record_command("test_metrics", "admin_handler", "admin", "success")
|
||||
metrics.record_message("text", "private", "admin_handler")
|
||||
metrics.record_error("TestError", "admin_handler", "test_metrics_handler")
|
||||
|
||||
# Проверяем активных пользователей
|
||||
if hasattr(bot_db, 'connect') and hasattr(bot_db, 'cursor'):
|
||||
active_users_query = """
|
||||
SELECT COUNT(DISTINCT user_id) as active_users
|
||||
FROM our_users
|
||||
WHERE date_changed > datetime('now', '-1 day')
|
||||
"""
|
||||
try:
|
||||
bot_db.connect()
|
||||
bot_db.cursor.execute(active_users_query)
|
||||
result = bot_db.cursor.fetchone()
|
||||
active_users = result[0] if result else 0
|
||||
finally:
|
||||
bot_db.close()
|
||||
else:
|
||||
active_users = "N/A"
|
||||
|
||||
await message.answer(
|
||||
f"✅ Тестовые метрики записаны\n"
|
||||
f"📊 Активных пользователей: {active_users}\n"
|
||||
f"🔧 Проверьте Grafana дашборд"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка тестирования метрик: {e}")
|
||||
|
||||
29
helper_bot/handlers/admin/constants.py
Normal file
29
helper_bot/handlers/admin/constants.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Constants for admin handlers"""
|
||||
|
||||
from typing import Final, Dict
|
||||
|
||||
# Admin button texts
|
||||
ADMIN_BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||
"BAN_LIST": "Бан (Список)",
|
||||
"BAN_BY_USERNAME": "Бан по нику",
|
||||
"BAN_BY_ID": "Бан по ID",
|
||||
"UNBAN_LIST": "Разбан (список)",
|
||||
"RETURN_TO_BOT": "Вернуться в бота",
|
||||
"CANCEL": "Отменить"
|
||||
}
|
||||
|
||||
# Admin button to command mapping for metrics
|
||||
ADMIN_BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"Бан (Список)": "admin_ban_list",
|
||||
"Бан по нику": "admin_ban_by_username",
|
||||
"Бан по ID": "admin_ban_by_id",
|
||||
"Разбан (список)": "admin_unban_list",
|
||||
"Вернуться в бота": "admin_return_to_bot",
|
||||
"Отменить": "admin_cancel"
|
||||
}
|
||||
|
||||
# Admin commands
|
||||
ADMIN_COMMANDS: Final[Dict[str, str]] = {
|
||||
"ADMIN": "admin",
|
||||
"TEST_METRICS": "test_metrics"
|
||||
}
|
||||
@@ -17,6 +17,9 @@ class AdminAccessMiddleware(BaseMiddleware):
|
||||
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||
if hasattr(event, 'from_user'):
|
||||
user_id = event.from_user.id
|
||||
username = getattr(event.from_user, 'username', 'Unknown')
|
||||
|
||||
logger.info(f"AdminAccessMiddleware: проверка доступа для пользователя {username} (ID: {user_id})")
|
||||
|
||||
# Получаем bot_db из data (внедренного DependenciesMiddleware)
|
||||
bot_db = data.get('bot_db')
|
||||
@@ -25,7 +28,11 @@ class AdminAccessMiddleware(BaseMiddleware):
|
||||
bdf = get_global_instance()
|
||||
bot_db = bdf.get_db()
|
||||
|
||||
if not check_access(user_id, bot_db):
|
||||
is_admin_result = await check_access(user_id, bot_db)
|
||||
logger.info(f"AdminAccessMiddleware: результат проверки для {username}: {is_admin_result}")
|
||||
|
||||
if not is_admin_result:
|
||||
logger.warning(f"AdminAccessMiddleware: доступ запрещен для пользователя {username} (ID: {user_id})")
|
||||
if hasattr(event, 'answer'):
|
||||
await event.answer('Доступ запрещен!')
|
||||
return
|
||||
|
||||
@@ -5,6 +5,14 @@ from helper_bot.utils.helper_func import add_days_to_date, get_banned_users_butt
|
||||
from helper_bot.handlers.admin.exceptions import UserAlreadyBannedError, InvalidInputError
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
|
||||
class User:
|
||||
"""Модель пользователя"""
|
||||
@@ -29,10 +37,12 @@ class AdminService:
|
||||
def __init__(self, bot_db):
|
||||
self.bot_db = bot_db
|
||||
|
||||
def get_last_users(self) -> List[User]:
|
||||
@track_time("get_last_users", "admin_service")
|
||||
@track_errors("admin_service", "get_last_users")
|
||||
async def get_last_users(self) -> List[User]:
|
||||
"""Получить список последних пользователей"""
|
||||
try:
|
||||
users_data = self.bot_db.get_last_users_from_db()
|
||||
users_data = await self.bot_db.get_last_users(30)
|
||||
return [
|
||||
User(
|
||||
user_id=user[1],
|
||||
@@ -45,31 +55,41 @@ class AdminService:
|
||||
logger.error(f"Ошибка при получении списка последних пользователей: {e}")
|
||||
raise
|
||||
|
||||
def get_banned_users(self) -> List[BannedUser]:
|
||||
@track_time("get_banned_users", "admin_service")
|
||||
@track_errors("admin_service", "get_banned_users")
|
||||
async def get_banned_users(self) -> List[BannedUser]:
|
||||
"""Получить список заблокированных пользователей"""
|
||||
try:
|
||||
banned_users_data = self.bot_db.get_banned_users_from_db()
|
||||
return [
|
||||
BannedUser(
|
||||
user_id=user[1], # user_id
|
||||
username=user[0], # user_name
|
||||
reason=user[2], # message_for_user
|
||||
unban_date=user[3] # date_to_unban
|
||||
)
|
||||
for user in banned_users_data
|
||||
]
|
||||
banned_users_data = await self.bot_db.get_banned_users_from_db()
|
||||
banned_users = []
|
||||
for user_data in banned_users_data:
|
||||
user_id, reason, unban_date = user_data
|
||||
# Получаем username и full_name из таблицы users
|
||||
username = await self.bot_db.get_username(user_id)
|
||||
full_name = await self.bot_db.get_full_name_by_id(user_id)
|
||||
user_name = username or full_name or f"User_{user_id}"
|
||||
|
||||
banned_users.append(BannedUser(
|
||||
user_id=user_id,
|
||||
username=user_name,
|
||||
reason=reason,
|
||||
unban_date=unban_date
|
||||
))
|
||||
return banned_users
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
|
||||
raise
|
||||
|
||||
def get_user_by_username(self, username: str) -> Optional[User]:
|
||||
@track_time("get_user_by_username", "admin_service")
|
||||
@track_errors("admin_service", "get_user_by_username")
|
||||
async def get_user_by_username(self, username: str) -> Optional[User]:
|
||||
"""Получить пользователя по username"""
|
||||
try:
|
||||
user_id = self.bot_db.get_user_id_by_username(username)
|
||||
user_id = await self.bot_db.get_user_id_by_username(username)
|
||||
if not user_id:
|
||||
return None
|
||||
|
||||
full_name = self.bot_db.get_full_name_by_id(user_id)
|
||||
full_name = await self.bot_db.get_full_name_by_id(user_id)
|
||||
return User(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
@@ -79,27 +99,31 @@ class AdminService:
|
||||
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
|
||||
raise
|
||||
|
||||
def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
@track_time("get_user_by_id", "admin_service")
|
||||
@track_errors("admin_service", "get_user_by_id")
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Получить пользователя по ID"""
|
||||
try:
|
||||
user_info = self.bot_db.get_user_info_by_id(user_id)
|
||||
user_info = await self.bot_db.get_user_by_id(user_id)
|
||||
if not user_info:
|
||||
return None
|
||||
|
||||
return User(
|
||||
user_id=user_id,
|
||||
username=user_info.get('username', 'Неизвестно'),
|
||||
full_name=user_info.get('full_name', 'Неизвестно')
|
||||
username=user_info.username or 'Неизвестно',
|
||||
full_name=user_info.full_name or 'Неизвестно'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
|
||||
raise
|
||||
|
||||
def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int]) -> None:
|
||||
@track_time("ban_user", "admin_service")
|
||||
@track_errors("admin_service", "ban_user")
|
||||
async def ban_user(self, user_id: int, username: str, reason: str, ban_days: Optional[int]) -> None:
|
||||
"""Заблокировать пользователя"""
|
||||
try:
|
||||
# Проверяем, не заблокирован ли уже пользователь
|
||||
if self.bot_db.check_user_in_blacklist(user_id):
|
||||
if await self.bot_db.check_user_in_blacklist(user_id):
|
||||
raise UserAlreadyBannedError(f"Пользователь {user_id} уже заблокирован")
|
||||
|
||||
# Рассчитываем дату разблокировки
|
||||
@@ -107,8 +131,8 @@ class AdminService:
|
||||
if ban_days is not None:
|
||||
date_to_unban = add_days_to_date(ban_days)
|
||||
|
||||
# Сохраняем в БД
|
||||
self.bot_db.set_user_blacklist(user_id, username, reason, date_to_unban)
|
||||
# Сохраняем в БД (username больше не передается, так как не используется в новой схеме)
|
||||
await self.bot_db.set_user_blacklist(user_id, None, reason, date_to_unban)
|
||||
|
||||
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
|
||||
|
||||
@@ -116,16 +140,20 @@ class AdminService:
|
||||
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
|
||||
raise
|
||||
|
||||
def unban_user(self, user_id: int) -> None:
|
||||
@track_time("unban_user", "admin_service")
|
||||
@track_errors("admin_service", "unban_user")
|
||||
async def unban_user(self, user_id: int) -> None:
|
||||
"""Разблокировать пользователя"""
|
||||
try:
|
||||
self.bot_db.delete_user_blacklist(user_id)
|
||||
await self.bot_db.delete_user_blacklist(user_id)
|
||||
logger.info(f"Пользователь {user_id} разблокирован")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}")
|
||||
raise
|
||||
|
||||
def validate_user_input(self, input_text: str) -> int:
|
||||
@track_time("validate_user_input", "admin_service")
|
||||
@track_errors("admin_service", "validate_user_input")
|
||||
async def validate_user_input(self, input_text: str) -> int:
|
||||
"""Валидация введенного ID пользователя"""
|
||||
try:
|
||||
user_id = int(input_text.strip())
|
||||
@@ -135,11 +163,14 @@ class AdminService:
|
||||
except ValueError:
|
||||
raise InvalidInputError("ID пользователя должен быть числом")
|
||||
|
||||
def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]:
|
||||
@track_time("get_banned_users_for_display", "admin_service")
|
||||
@track_errors("admin_service", "get_banned_users_for_display")
|
||||
async def get_banned_users_for_display(self, page: int = 0) -> tuple[str, list]:
|
||||
"""Получить данные заблокированных пользователей для отображения"""
|
||||
try:
|
||||
message_text = get_banned_users_list(page, self.bot_db)
|
||||
buttons_list = get_banned_users_buttons(self.bot_db)
|
||||
message_text = await get_banned_users_list(page, self.bot_db)
|
||||
|
||||
buttons_list = await get_banned_users_buttons(self.bot_db)
|
||||
return message_text, buttons_list
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении данных заблокированных пользователей: {e}")
|
||||
|
||||
@@ -16,14 +16,18 @@ def escape_html(text: str) -> str:
|
||||
async def return_to_admin_menu(message: types.Message, state: FSMContext,
|
||||
additional_message: Optional[str] = None) -> None:
|
||||
"""Универсальная функция для возврата в админ-меню"""
|
||||
logger.info(f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}")
|
||||
|
||||
await state.set_data({})
|
||||
await state.set_state("ADMIN")
|
||||
markup = get_reply_keyboard_admin()
|
||||
|
||||
if additional_message:
|
||||
logger.info(f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}")
|
||||
await message.answer(additional_message)
|
||||
|
||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
||||
logger.info(f"return_to_admin_menu: Пользователь {message.from_user.id} успешно возвращен в админ-меню")
|
||||
|
||||
|
||||
async def handle_admin_error(message: types.Message, error: Exception,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import html
|
||||
import traceback
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import CallbackQuery
|
||||
from aiogram import F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.filters import MagicData
|
||||
|
||||
from helper_bot.handlers.voice.constants import CALLBACK_SAVE, CALLBACK_DELETE
|
||||
from helper_bot.handlers.voice.services import AudioFileService
|
||||
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
|
||||
create_keyboard_for_ban_reason
|
||||
from helper_bot.utils.helper_func import get_banned_users_list, get_banned_users_buttons
|
||||
@@ -21,10 +24,20 @@ from .constants import (
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
callback_router = Router()
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_PUBLISH)
|
||||
@track_time("post_for_group", "callback_handlers")
|
||||
@track_errors("callback_handlers", "post_for_group")
|
||||
async def post_for_group(
|
||||
call: CallbackQuery,
|
||||
settings: MagicData("settings")
|
||||
@@ -56,6 +69,8 @@ async def post_for_group(
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_DECLINE)
|
||||
@track_time("decline_post_for_group", "callback_handlers")
|
||||
@track_errors("callback_handlers", "decline_post_for_group")
|
||||
async def decline_post_for_group(
|
||||
call: CallbackQuery,
|
||||
settings: MagicData("settings")
|
||||
@@ -86,7 +101,9 @@ async def decline_post_for_group(
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_BAN)
|
||||
async def ban_user_from_post(call: CallbackQuery):
|
||||
@track_time("ban_user_from_post", "callback_handlers")
|
||||
@track_errors("callback_handlers", "ban_user_from_post")
|
||||
async def ban_user_from_post(call: CallbackQuery, **kwargs):
|
||||
ban_service = get_ban_service()
|
||||
# TODO: переделать на MagicData
|
||||
try:
|
||||
@@ -106,21 +123,31 @@ async def ban_user_from_post(call: CallbackQuery):
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_BAN))
|
||||
async def process_ban_user(call: CallbackQuery, state: FSMContext):
|
||||
@track_time("process_ban_user", "callback_handlers")
|
||||
@track_errors("callback_handlers", "process_ban_user")
|
||||
async def process_ban_user(call: CallbackQuery, state: FSMContext, **kwargs):
|
||||
ban_service = get_ban_service()
|
||||
# TODO: переделать на MagicData
|
||||
user_id = call.data[4:]
|
||||
logger.info(f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
|
||||
|
||||
# Проверяем, что user_id является валидным числом
|
||||
try:
|
||||
user_name = await ban_service.ban_user(user_id, "")
|
||||
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None, date_to_unban=None)
|
||||
user_id_int = int(user_id)
|
||||
except ValueError:
|
||||
logger.error(f"Некорректный user_id в callback: {user_id}")
|
||||
await call.answer(text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3)
|
||||
return
|
||||
|
||||
try:
|
||||
user_name = await ban_service.ban_user(str(user_id_int), "")
|
||||
await state.update_data(user_id=user_id_int, 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_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
|
||||
text=f"<b>Выбран пользователь:\nid:</b> {user_id_int}\n<b>username:</b> {user_name_escaped}\nИмя:{full_name_escaped}\nВыбери причину бана из списка или напиши ее в чат",
|
||||
reply_markup=markup
|
||||
)
|
||||
await state.set_state('BAN_2')
|
||||
@@ -131,13 +158,23 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext):
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_UNLOCK))
|
||||
async def process_unlock_user(call: CallbackQuery):
|
||||
@track_time("process_unlock_user", "callback_handlers")
|
||||
@track_errors("callback_handlers", "process_unlock_user")
|
||||
async def process_unlock_user(call: CallbackQuery, **kwargs):
|
||||
ban_service = get_ban_service()
|
||||
# TODO: переделать на MagicData
|
||||
user_id = call.data[7:]
|
||||
|
||||
# Проверяем, что user_id является валидным числом
|
||||
try:
|
||||
username = await ban_service.unlock_user(user_id)
|
||||
user_id_int = int(user_id)
|
||||
except ValueError:
|
||||
logger.error(f"Некорректный user_id в callback: {user_id}")
|
||||
await call.answer(text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3)
|
||||
return
|
||||
|
||||
try:
|
||||
username = await ban_service.unlock_user(str(user_id_int))
|
||||
await call.answer(f'{MESSAGE_USER_UNLOCKED} {username}', show_alert=True)
|
||||
except UserNotFoundError:
|
||||
await call.answer(text='Пользователь не найден в базе', show_alert=True, cache_time=3)
|
||||
@@ -147,7 +184,9 @@ async def process_unlock_user(call: CallbackQuery):
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_RETURN)
|
||||
async def return_to_main_menu(call: CallbackQuery):
|
||||
@track_time("return_to_main_menu", "callback_handlers")
|
||||
@track_errors("callback_handlers", "return_to_main_menu")
|
||||
async def return_to_main_menu(call: CallbackQuery, **kwargs):
|
||||
await call.message.delete()
|
||||
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
|
||||
markup = get_reply_keyboard_admin()
|
||||
@@ -155,16 +194,25 @@ async def return_to_main_menu(call: CallbackQuery):
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_PAGE))
|
||||
@track_time("change_page", "callback_handlers")
|
||||
@track_errors("callback_handlers", "change_page")
|
||||
async def change_page(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db")
|
||||
bot_db: MagicData("bot_db"),
|
||||
**kwargs
|
||||
):
|
||||
try:
|
||||
page_number = int(call.data[5:])
|
||||
except ValueError:
|
||||
logger.error(f"Некорректный номер страницы в callback: {call.data}")
|
||||
await call.answer(text="Ошибка: некорректный номер страницы", show_alert=True, cache_time=3)
|
||||
return
|
||||
|
||||
logger.info(f"Переход на страницу {page_number}")
|
||||
|
||||
if call.message.text == 'Список пользователей которые последними обращались к боту':
|
||||
list_users = bot_db.get_last_users_from_db()
|
||||
keyboard = create_keyboard_with_pagination(int(page_number), len(list_users), list_users, 'ban')
|
||||
list_users = await bot_db.get_last_users(30)
|
||||
keyboard = create_keyboard_with_pagination(page_number, len(list_users), list_users, 'ban')
|
||||
await call.bot.edit_message_reply_markup(
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
@@ -179,9 +227,83 @@ async def change_page(
|
||||
)
|
||||
|
||||
buttons = get_banned_users_buttons(bot_db)
|
||||
keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock')
|
||||
keyboard = create_keyboard_with_pagination(page_number, 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
|
||||
)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_SAVE)
|
||||
@track_time("save_voice_message", "callback_handlers")
|
||||
@track_errors("callback_handlers", "save_voice_message")
|
||||
async def save_voice_message(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
**kwargs
|
||||
):
|
||||
try:
|
||||
# Создаем сервис для работы с аудио файлами
|
||||
audio_service = AudioFileService(bot_db)
|
||||
|
||||
# Получаем ID пользователя из базы
|
||||
user_id = await bot_db.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
|
||||
|
||||
# Генерируем имя файла
|
||||
file_name = await audio_service.generate_file_name(user_id)
|
||||
|
||||
# Собираем инфо о сообщении
|
||||
time_UTC = int(time.time())
|
||||
date_added = datetime.fromtimestamp(time_UTC)
|
||||
|
||||
# Получаем file_id из voice сообщения
|
||||
file_id = call.message.voice.file_id if call.message.voice else ""
|
||||
|
||||
# Сохраняем в базу данных
|
||||
await audio_service.save_audio_file(file_name, user_id, date_added, file_id)
|
||||
|
||||
# Скачиваем и сохраняем файл
|
||||
await audio_service.download_and_save_audio(call.bot, call.message, file_name)
|
||||
|
||||
# Удаляем сообщение из предложки
|
||||
await call.bot.delete_message(
|
||||
chat_id=settings['Telegram']['group_for_posts'],
|
||||
message_id=call.message.message_id
|
||||
)
|
||||
|
||||
# Удаляем запись из таблицы audio_moderate
|
||||
await bot_db.delete_audio_moderate_record(call.message.message_id)
|
||||
|
||||
await call.answer(text='Сохранено!', cache_time=3)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении голосового сообщения: {e}")
|
||||
await call.answer(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_DELETE)
|
||||
@track_time("delete_voice_message", "callback_handlers")
|
||||
@track_errors("callback_handlers", "delete_voice_message")
|
||||
async def delete_voice_message(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
**kwargs
|
||||
):
|
||||
try:
|
||||
# Удаляем сообщение из предложки
|
||||
await call.bot.delete_message(
|
||||
chat_id=settings['Telegram']['group_for_posts'],
|
||||
message_id=call.message.message_id
|
||||
)
|
||||
|
||||
# Удаляем запись из таблицы audio_moderate
|
||||
await bot_db.delete_audio_moderate_record(call.message.message_id)
|
||||
|
||||
await call.answer(text='Удалено!', cache_time=3)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении голосового сообщения: {e}")
|
||||
await call.answer(text='Ошибка при удалении!', cache_time=3)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Final, Dict
|
||||
|
||||
# Callback data constants
|
||||
CALLBACK_PUBLISH = "publish"
|
||||
CALLBACK_DECLINE = "decline"
|
||||
@@ -27,3 +29,13 @@ MESSAGE_USER_BANNED_SPAM = "Ты заблокирован за спам. Дат
|
||||
|
||||
# Error messages
|
||||
ERROR_BOT_BLOCKED = "Forbidden: bot was blocked by the user"
|
||||
|
||||
# Callback to command mapping for metrics
|
||||
CALLBACK_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"publish": "publish",
|
||||
"decline": "decline",
|
||||
"ban": "ban",
|
||||
"unlock": "unlock",
|
||||
"return": "return",
|
||||
"page": "page"
|
||||
}
|
||||
|
||||
@@ -10,24 +10,16 @@ from .services import PostPublishService, BanService
|
||||
def get_post_publish_service() -> PostPublishService:
|
||||
"""Фабрика для PostPublishService"""
|
||||
bdf = get_global_instance()
|
||||
bot = Bot(
|
||||
token=bdf.settings['Telegram']['bot_token'],
|
||||
default=DefaultBotProperties(parse_mode='HTML'),
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
db = bdf.get_db()
|
||||
settings = bdf.settings
|
||||
return PostPublishService(bot, db, settings)
|
||||
return PostPublishService(None, db, settings)
|
||||
|
||||
|
||||
def get_ban_service() -> BanService:
|
||||
"""Фабрика для BanService"""
|
||||
bdf = get_global_instance()
|
||||
bot = Bot(
|
||||
token=bdf.settings['Telegram']['bot_token'],
|
||||
default=DefaultBotProperties(parse_mode='HTML'),
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
db = bdf.get_db()
|
||||
settings = bdf.settings
|
||||
return BanService(bot, db, settings)
|
||||
return BanService(None, db, settings)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import html
|
||||
from datetime import datetime, timedelta
|
||||
import html
|
||||
from typing import Dict, Any
|
||||
|
||||
from aiogram import Bot
|
||||
@@ -23,9 +23,18 @@ from .constants import (
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
|
||||
class PostPublishService:
|
||||
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
|
||||
# bot может быть None - в этом случае используем бота из контекста сообщения
|
||||
self.bot = bot
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
@@ -33,11 +42,24 @@ class PostPublishService:
|
||||
self.main_public = settings['Telegram']['main_public']
|
||||
self.important_logs = settings['Telegram']['important_logs']
|
||||
|
||||
def _get_bot(self, message) -> Bot:
|
||||
"""Получает бота из контекста сообщения или использует переданного"""
|
||||
if self.bot:
|
||||
return self.bot
|
||||
return message.bot
|
||||
|
||||
@track_time("publish_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "publish_post")
|
||||
async def publish_post(self, call: CallbackQuery) -> None:
|
||||
"""Основной метод публикации поста"""
|
||||
# Проверяем, является ли сообщение частью медиагруппы
|
||||
if call.message.media_group_id:
|
||||
await self._publish_media_group(call)
|
||||
return
|
||||
|
||||
content_type = call.message.content_type
|
||||
|
||||
if content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP:
|
||||
if content_type == CONTENT_TYPE_TEXT:
|
||||
await self._publish_text_post(call)
|
||||
elif content_type == CONTENT_TYPE_PHOTO:
|
||||
await self._publish_photo_post(call)
|
||||
@@ -49,129 +71,234 @@ class PostPublishService:
|
||||
await self._publish_audio_post(call)
|
||||
elif content_type == CONTENT_TYPE_VOICE:
|
||||
await self._publish_voice_post(call)
|
||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
await self._publish_media_group(call)
|
||||
else:
|
||||
raise PublishError(f"Неподдерживаемый тип контента: {content_type}")
|
||||
|
||||
@track_time("_publish_text_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_text_post")
|
||||
async def _publish_text_post(self, call: CallbackQuery) -> None:
|
||||
"""Публикация текстового поста"""
|
||||
text_post = html.escape(str(call.message.text))
|
||||
author_id = self._get_author_id(call.message.message_id)
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
await send_text_message(self.main_public, call.message, text_post)
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Текст сообщения опубликован в канале {self.main_public}.')
|
||||
|
||||
@track_time("_publish_photo_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_photo_post")
|
||||
async def _publish_photo_post(self, call: CallbackQuery) -> None:
|
||||
"""Публикация поста с фото"""
|
||||
text_post_with_photo = html.escape(str(call.message.caption))
|
||||
author_id = self._get_author_id(call.message.message_id)
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
await send_photo_message(self.main_public, call.message, call.message.photo[-1].file_id, text_post_with_photo)
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с фото опубликован в канале {self.main_public}.')
|
||||
|
||||
@track_time("_publish_video_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_video_post")
|
||||
async def _publish_video_post(self, call: CallbackQuery) -> None:
|
||||
"""Публикация поста с видео"""
|
||||
text_post_with_photo = html.escape(str(call.message.caption))
|
||||
author_id = self._get_author_id(call.message.message_id)
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
await send_video_message(self.main_public, call.message, call.message.video.file_id, text_post_with_photo)
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с видео опубликован в канале {self.main_public}.')
|
||||
|
||||
@track_time("_publish_video_note_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_video_note_post")
|
||||
async def _publish_video_note_post(self, call: CallbackQuery) -> None:
|
||||
"""Публикация поста с кружком"""
|
||||
author_id = self._get_author_id(call.message.message_id)
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
await send_video_note_message(self.main_public, call.message, call.message.video_note.file_id)
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с кружком опубликован в канале {self.main_public}.')
|
||||
|
||||
@track_time("_publish_audio_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_audio_post")
|
||||
async def _publish_audio_post(self, call: CallbackQuery) -> None:
|
||||
"""Публикация поста с аудио"""
|
||||
text_post_with_photo = html.escape(str(call.message.caption))
|
||||
author_id = self._get_author_id(call.message.message_id)
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
await send_audio_message(self.main_public, call.message, call.message.audio.file_id, text_post_with_photo)
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с аудио опубликован в канале {self.main_public}.')
|
||||
|
||||
@track_time("_publish_voice_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_voice_post")
|
||||
async def _publish_voice_post(self, call: CallbackQuery) -> None:
|
||||
"""Публикация поста с войсом"""
|
||||
author_id = self._get_author_id(call.message.message_id)
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
await send_voice_message(self.main_public, call.message, call.message.voice.file_id)
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с войсом опубликован в канале {self.main_public}.')
|
||||
|
||||
@track_time("_publish_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_media_group")
|
||||
async def _publish_media_group(self, call: CallbackQuery) -> None:
|
||||
"""Публикация медиагруппы"""
|
||||
post_content = self.db.get_post_content_from_telegram_by_last_id(call.message.message_id)
|
||||
pre_text = self.db.get_post_text_from_telegram_by_last_id(call.message.message_id)
|
||||
post_text = html.escape(str(pre_text))
|
||||
author_id = self._get_author_id_for_media_group(call.message.message_id)
|
||||
logger.info(f"Начинаю публикацию медиагруппы. Helper message ID: {call.message.message_id}")
|
||||
try:
|
||||
# call.message.message_id - это ID helper сообщения
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
await send_media_group_to_channel(bot=self.bot, chat_id=self.main_public, post_content=post_content, post_text=post_text)
|
||||
# Получаем контент медиагруппы по helper_message_id
|
||||
logger.debug(f"Получаю контент медиагруппы для helper_message_id: {helper_message_id}")
|
||||
post_content = await self.db.get_post_content_by_helper_id(helper_message_id)
|
||||
if not post_content:
|
||||
logger.error(f"Контент медиагруппы не найден в базе данных для helper_message_id: {helper_message_id}")
|
||||
raise PublishError("Контент медиагруппы не найден в базе данных")
|
||||
|
||||
# Получаем текст поста по helper_message_id
|
||||
logger.debug(f"Получаю текст поста для helper_message_id: {helper_message_id}")
|
||||
pre_text = await self.db.get_post_text_by_helper_id(helper_message_id)
|
||||
post_text = html.escape(str(pre_text)) if pre_text else ""
|
||||
logger.debug(f"Текст поста получен: {'пустой' if not post_text else f'длина: {len(post_text)} символов'}")
|
||||
|
||||
# Получаем ID автора по helper_message_id
|
||||
logger.debug(f"Получаю ID автора для helper_message_id: {helper_message_id}")
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(helper_message_id)
|
||||
if not author_id:
|
||||
logger.error(f"Автор не найден для медиагруппы {helper_message_id}")
|
||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {helper_message_id}")
|
||||
logger.debug(f"ID автора получен: {author_id}")
|
||||
|
||||
# Отправляем медиагруппу в канал
|
||||
logger.info(f"Отправляю медиагруппу в канал {self.main_public}")
|
||||
await send_media_group_to_channel(
|
||||
bot=self._get_bot(call.message),
|
||||
chat_id=self.main_public,
|
||||
post_content=post_content,
|
||||
post_text=post_text
|
||||
)
|
||||
|
||||
logger.debug(f"Удаляю медиагруппу и уведомляю автора {author_id}")
|
||||
await self._delete_media_group_and_notify_author(call, author_id)
|
||||
logger.info(f'Медиагруппа опубликована в канале {self.main_public}.')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при публикации медиагруппы: {e}")
|
||||
raise PublishError(f"Не удалось опубликовать медиагруппу: {str(e)}")
|
||||
|
||||
@track_time("decline_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "decline_post")
|
||||
async def decline_post(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение поста"""
|
||||
logger.info(f"Начинаю отклонение поста. Message ID: {call.message.message_id}, Content type: {call.message.content_type}")
|
||||
# Проверяем, является ли сообщение частью медиагруппы (осознанный костыль, т.к. сообщение к которому прикреплен коллбек без медиагруппы)
|
||||
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
logger.debug("Сообщение является частью медиагруппы, вызываю _decline_media_group")
|
||||
await self._decline_media_group(call)
|
||||
return
|
||||
|
||||
content_type = call.message.content_type
|
||||
|
||||
if (content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP) or \
|
||||
content_type in [CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||
if content_type in [CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||
logger.debug(f"Отклоняю одиночный пост типа: {content_type}")
|
||||
await self._decline_single_post(call)
|
||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
await self._decline_media_group(call)
|
||||
else:
|
||||
logger.error(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
|
||||
@track_time("_decline_single_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_decline_single_post")
|
||||
async def _decline_single_post(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение одиночного поста"""
|
||||
author_id = self._get_author_id(call.message.message_id)
|
||||
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
logger.debug(f"Отклоняю одиночный пост. Message ID: {call.message.message_id}")
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
logger.debug(f"ID автора получен: {author_id}")
|
||||
|
||||
logger.debug(f"Удаляю сообщение из группы {self.group_for_posts}")
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
try:
|
||||
logger.debug(f"Отправляю уведомление об отклонении автору {author_id}")
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||
raise
|
||||
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
||||
|
||||
@track_time("_decline_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_decline_media_group")
|
||||
async def _decline_media_group(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение медиагруппы"""
|
||||
post_ids = self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||
message_ids = [row[0] for row in post_ids]
|
||||
message_ids.append(call.message.message_id)
|
||||
logger.debug(f"Отклоняю медиагруппу. Helper message ID: {call.message.message_id}")
|
||||
|
||||
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||
message_ids = post_ids.copy()
|
||||
message_ids.append(call.message.message_id)
|
||||
logger.debug(f"Получены ID сообщений для удаления: {message_ids}")
|
||||
|
||||
author_id = await self._get_author_id_for_media_group(call.message.message_id)
|
||||
logger.debug(f"ID автора медиагруппы получен: {author_id}")
|
||||
|
||||
logger.debug(f"Удаляю {len(message_ids)} сообщений из группы {self.group_for_posts}")
|
||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||
|
||||
author_id = self._get_author_id_for_media_group(call.message.message_id)
|
||||
await self.bot.delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||
try:
|
||||
logger.debug(f"Отправляю уведомление об отклонении автору медиагруппы {author_id}")
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"Ошибка при отправке уведомления автору медиагруппы {author_id}: {e}")
|
||||
raise
|
||||
|
||||
def _get_author_id(self, message_id: int) -> int:
|
||||
@track_time("_get_author_id", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_get_author_id")
|
||||
async def _get_author_id(self, message_id: int) -> int:
|
||||
"""Получение ID автора по ID сообщения"""
|
||||
author_id = self.db.get_author_id_by_message_id(message_id)
|
||||
author_id = await self.db.get_author_id_by_message_id(message_id)
|
||||
if not author_id:
|
||||
raise PostNotFoundError(f"Автор не найден для сообщения {message_id}")
|
||||
return author_id
|
||||
|
||||
def _get_author_id_for_media_group(self, message_id: int) -> int:
|
||||
@track_time("_get_author_id_for_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_get_author_id_for_media_group")
|
||||
async def _get_author_id_for_media_group(self, message_id: int) -> int:
|
||||
"""Получение ID автора для медиагруппы"""
|
||||
author_id = self.db.get_author_id_by_helper_message_id(message_id)
|
||||
# Сначала пытаемся найти автора по helper_message_id
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(message_id)
|
||||
if author_id:
|
||||
return author_id
|
||||
|
||||
# Если не найден, ищем по основному message_id медиагруппы
|
||||
# Для этого нужно найти связанные сообщения медиагруппы
|
||||
try:
|
||||
# Получаем все ID сообщений медиагруппы
|
||||
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(message_id)
|
||||
if post_ids:
|
||||
# Берем первый ID (основное сообщение медиагруппы)
|
||||
main_message_id = post_ids[0]
|
||||
author_id = await self.db.get_author_id_by_message_id(main_message_id)
|
||||
if author_id:
|
||||
return author_id
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось найти автора через связанные сообщения: {e}")
|
||||
|
||||
# Если все способы не сработали, ищем напрямую
|
||||
author_id = await self.db.get_author_id_by_message_id(message_id)
|
||||
if not author_id:
|
||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
|
||||
return author_id
|
||||
|
||||
@track_time("_delete_post_and_notify_author", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_delete_post_and_notify_author")
|
||||
async def _delete_post_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
|
||||
"""Удаление поста и уведомление автора"""
|
||||
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
except Exception as e:
|
||||
@@ -179,12 +306,15 @@ class PostPublishService:
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
raise
|
||||
|
||||
@track_time("_delete_media_group_and_notify_author", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_delete_media_group_and_notify_author")
|
||||
async def _delete_media_group_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
|
||||
"""Удаление медиагруппы и уведомление автора"""
|
||||
post_ids = self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||
message_ids = [row[0] for row in post_ids]
|
||||
message_ids.append(call.message.message_id)
|
||||
await self.bot.delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||
|
||||
#message_ids = post_ids.copy()
|
||||
post_ids.append(call.message.message_id)
|
||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=post_ids)
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
except Exception as e:
|
||||
@@ -201,26 +331,33 @@ class BanService:
|
||||
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||
self.important_logs = settings['Telegram']['important_logs']
|
||||
|
||||
def _get_bot(self, message) -> Bot:
|
||||
"""Получает бота из контекста сообщения или использует переданного"""
|
||||
if self.bot:
|
||||
return self.bot
|
||||
return message.bot
|
||||
|
||||
@track_time("ban_user_from_post", "ban_service")
|
||||
@track_errors("ban_service", "ban_user_from_post")
|
||||
async def ban_user_from_post(self, call: CallbackQuery) -> None:
|
||||
"""Бан пользователя за спам"""
|
||||
author_id = self.db.get_author_id_by_message_id(call.message.message_id)
|
||||
author_id = await self.db.get_author_id_by_message_id(call.message.message_id)
|
||||
if not author_id:
|
||||
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")
|
||||
|
||||
user_name = self.db.get_username(user_id=author_id)
|
||||
current_date = datetime.now()
|
||||
date_to_unban = current_date + timedelta(days=7)
|
||||
date_to_unban = int((current_date + timedelta(days=7)).timestamp())
|
||||
|
||||
self.db.set_user_blacklist(
|
||||
await self.db.set_user_blacklist(
|
||||
user_id=author_id,
|
||||
user_name=user_name,
|
||||
user_name=None,
|
||||
message_for_user="Спам",
|
||||
date_to_unban=date_to_unban
|
||||
)
|
||||
|
||||
await self.bot.delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
date_str = date_to_unban.strftime("%d.%m.%Y %H:%M")
|
||||
date_str = (current_date + timedelta(days=7)).strftime("%d.%m.%Y %H:%M")
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_USER_BANNED_SPAM.format(date=date_str))
|
||||
except Exception as e:
|
||||
@@ -230,20 +367,24 @@ class BanService:
|
||||
|
||||
logger.info(f"Пользователь {author_id} заблокирован за спам до {date_str}")
|
||||
|
||||
@track_time("ban_user", "ban_service")
|
||||
@track_errors("ban_service", "ban_user")
|
||||
async def ban_user(self, user_id: str, user_name: str) -> str:
|
||||
"""Бан пользователя по ID"""
|
||||
user_name = self.db.get_username(user_id=user_id)
|
||||
user_name = await self.db.get_username(int(user_id))
|
||||
if not user_name:
|
||||
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
||||
|
||||
return user_name
|
||||
|
||||
@track_time("unlock_user", "ban_service")
|
||||
@track_errors("ban_service", "unlock_user")
|
||||
async def unlock_user(self, user_id: str) -> str:
|
||||
"""Разблокировка пользователя"""
|
||||
user_name = self.db.get_username(user_id=user_id)
|
||||
user_name = await self.db.get_username(int(user_id))
|
||||
if not user_name:
|
||||
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
||||
|
||||
delete_user_blacklist(user_id, self.db)
|
||||
await delete_user_blacklist(int(user_id), self.db)
|
||||
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
|
||||
return user_name
|
||||
|
||||
@@ -5,6 +5,7 @@ from aiogram import Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
# Local imports - filters
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
|
||||
# Local imports - modular components
|
||||
@@ -26,7 +27,7 @@ from helper_bot.utils.metrics import (
|
||||
class GroupHandlers:
|
||||
"""Main handler class for group messages"""
|
||||
|
||||
def __init__(self, db, keyboard_markup: types.ReplyKeyboardMarkup):
|
||||
def __init__(self, db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMarkup):
|
||||
self.db = db
|
||||
self.keyboard_markup = keyboard_markup
|
||||
self.admin_reply_service = AdminReplyService(db)
|
||||
@@ -45,7 +46,9 @@ class GroupHandlers:
|
||||
)
|
||||
|
||||
@error_handler
|
||||
async def handle_message(self, message: types.Message, state: FSMContext):
|
||||
@track_errors("group_handlers", "handle_message")
|
||||
@track_time("handle_message", "group_handlers")
|
||||
async def handle_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle admin reply to user through group chat"""
|
||||
|
||||
logger.info(
|
||||
@@ -67,7 +70,7 @@ class GroupHandlers:
|
||||
|
||||
try:
|
||||
# Get user ID for reply
|
||||
chat_id = self.admin_reply_service.get_user_id_for_reply(message_id)
|
||||
chat_id = await self.admin_reply_service.get_user_id_for_reply(message_id)
|
||||
|
||||
# Send reply to user
|
||||
await self.admin_reply_service.send_reply_to_user(
|
||||
@@ -86,7 +89,7 @@ class GroupHandlers:
|
||||
|
||||
|
||||
# Factory function to create handlers with dependencies
|
||||
def create_group_handlers(db, keyboard_markup: types.ReplyKeyboardMarkup) -> GroupHandlers:
|
||||
def create_group_handlers(db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMarkup) -> GroupHandlers:
|
||||
"""Create group handlers instance with dependencies"""
|
||||
return GroupHandlers(db, keyboard_markup)
|
||||
|
||||
@@ -103,6 +106,7 @@ def init_legacy_router():
|
||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
||||
|
||||
bdf = get_global_instance()
|
||||
#TODO: поменять архитектуру и подключить правильный BotDB
|
||||
db = bdf.get_db()
|
||||
keyboard_markup = get_reply_keyboard_leave_chat()
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ from helper_bot.utils.metrics import (
|
||||
|
||||
class DatabaseProtocol(Protocol):
|
||||
"""Protocol for database operations"""
|
||||
def get_user_by_message_id(self, message_id: int) -> Optional[int]: ...
|
||||
async def get_user_by_message_id(self, message_id: int) -> Optional[int]: ...
|
||||
async def add_message(self, message_text: str, user_id: int, message_id: int, date: int = None): ...
|
||||
|
||||
|
||||
class AdminReplyService:
|
||||
@@ -31,7 +32,9 @@ class AdminReplyService:
|
||||
def __init__(self, db: DatabaseProtocol) -> None:
|
||||
self.db = db
|
||||
|
||||
def get_user_id_for_reply(self, message_id: int) -> int:
|
||||
@track_time("get_user_id_for_reply", "admin_reply_service")
|
||||
@track_errors("admin_reply_service", "get_user_id_for_reply")
|
||||
async def get_user_id_for_reply(self, message_id: int) -> int:
|
||||
"""
|
||||
Get user ID for reply by message ID.
|
||||
|
||||
@@ -44,11 +47,13 @@ class AdminReplyService:
|
||||
Raises:
|
||||
UserNotFoundError: If user is not found in database
|
||||
"""
|
||||
user_id = self.db.get_user_by_message_id(message_id)
|
||||
user_id = await self.db.get_user_by_message_id(message_id)
|
||||
if user_id is None:
|
||||
raise UserNotFoundError(f"User not found for message_id: {message_id}")
|
||||
return user_id
|
||||
|
||||
@track_time("send_reply_to_user", "admin_reply_service")
|
||||
@track_errors("admin_reply_service", "send_reply_to_user")
|
||||
async def send_reply_to_user(
|
||||
self,
|
||||
chat_id: int,
|
||||
|
||||
@@ -17,7 +17,19 @@ BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||
"LEAVE_CHAT": "Выйти из чата",
|
||||
"RETURN_TO_BOT": "Вернуться в бота",
|
||||
"WANT_STICKERS": "🤪Хочу стикеры",
|
||||
"CONNECT_ADMIN": "📩Связаться с админами"
|
||||
"CONNECT_ADMIN": "📩Связаться с админами",
|
||||
"VOICE_BOT": "🎤Голосовой бот"
|
||||
}
|
||||
|
||||
# Button to command mapping for metrics
|
||||
BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"📢Предложить свой пост": "suggest_post",
|
||||
"👋🏼Сказать пока!": "say_goodbye",
|
||||
"Выйти из чата": "leave_chat",
|
||||
"Вернуться в бота": "return_to_bot",
|
||||
"🤪Хочу стикеры": "want_stickers",
|
||||
"📩Связаться с админами": "connect_admin",
|
||||
"🎤Голосовой бот": "voice_bot"
|
||||
}
|
||||
|
||||
# Error messages
|
||||
|
||||
@@ -10,6 +10,7 @@ from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
# Local imports - filters and middlewares
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.middlewares.album_middleware import AlbumMiddleware
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
@@ -28,7 +29,8 @@ from helper_bot.utils.helper_func import (
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
# Local imports - modular components
|
||||
@@ -43,7 +45,7 @@ sleep = asyncio.sleep
|
||||
class PrivateHandlers:
|
||||
"""Main handler class for private messages"""
|
||||
|
||||
def __init__(self, db, settings: BotSettings):
|
||||
def __init__(self, db: AsyncBotDB, settings: BotSettings):
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.user_service = UserService(db, settings)
|
||||
@@ -73,31 +75,38 @@ class PrivateHandlers:
|
||||
self.router.message.register(self.stickers, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["WANT_STICKERS"])
|
||||
self.router.message.register(self.connect_with_admin, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["CONNECT_ADMIN"])
|
||||
|
||||
|
||||
# State handlers
|
||||
self.router.message.register(self.suggest_router, StateFilter(FSM_STATES["SUGGEST"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["PRE_CHAT"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["CHAT"]), ChatTypeFilter(chat_type=["private"]))
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_emoji_message")
|
||||
@track_time("handle_emoji_message", "private_handlers")
|
||||
async def handle_emoji_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle emoji command"""
|
||||
await self.user_service.log_user_message(message)
|
||||
user_emoji = check_user_emoji(message)
|
||||
user_emoji = await check_user_emoji(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
if user_emoji is not None:
|
||||
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_restart_message")
|
||||
@track_time("handle_restart_message", "private_handlers")
|
||||
async def handle_restart_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle restart command"""
|
||||
markup = get_reply_keyboard(self.db, message.from_user.id)
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
await update_user_info('love', message)
|
||||
check_user_emoji(message)
|
||||
await check_user_emoji(message)
|
||||
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_start_message")
|
||||
@track_time("handle_start_message", "private_handlers")
|
||||
async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle start command and return to bot button with metrics tracking"""
|
||||
# User service operations with metrics
|
||||
@@ -109,11 +118,13 @@ class PrivateHandlers:
|
||||
await self.sticker_service.send_random_hello_sticker(message)
|
||||
|
||||
# Send welcome message with metrics
|
||||
markup = get_reply_keyboard(self.db, message.from_user.id)
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
|
||||
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "suggest_post")
|
||||
@track_time("suggest_post", "private_handlers")
|
||||
async def suggest_post(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle suggest post button"""
|
||||
# User service operations with metrics
|
||||
@@ -126,6 +137,8 @@ class PrivateHandlers:
|
||||
await message.answer(suggest_news, reply_markup=markup)
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "end_message")
|
||||
@track_time("end_message", "private_handlers")
|
||||
async def end_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle goodbye button"""
|
||||
# User service operations with metrics
|
||||
@@ -142,6 +155,8 @@ class PrivateHandlers:
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "suggest_router")
|
||||
@track_time("suggest_router", "private_handlers")
|
||||
async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs):
|
||||
"""Handle post submission in suggest state"""
|
||||
# Post service operations with metrics
|
||||
@@ -150,17 +165,19 @@ class PrivateHandlers:
|
||||
await self.post_service.process_post(message, album)
|
||||
|
||||
# Send success message and return to start state
|
||||
markup_for_user = get_reply_keyboard(self.db, message.from_user.id)
|
||||
markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
|
||||
await message.answer(success_send_message, reply_markup=markup_for_user)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "stickers")
|
||||
@track_time("stickers", "private_handlers")
|
||||
async def stickers(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle stickers request"""
|
||||
# User service operations with metrics
|
||||
markup = get_reply_keyboard(self.db, message.from_user.id)
|
||||
self.db.update_info_about_stickers(user_id=message.from_user.id)
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await self.db.update_stickers_info(message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
await message.answer(
|
||||
text=ERROR_MESSAGES["STICKERS_LINK"],
|
||||
@@ -169,6 +186,8 @@ class PrivateHandlers:
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "connect_with_admin")
|
||||
@track_time("connect_with_admin", "private_handlers")
|
||||
async def connect_with_admin(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle connect with admin button"""
|
||||
# User service operations with metrics
|
||||
@@ -179,6 +198,8 @@ class PrivateHandlers:
|
||||
await state.set_state(FSM_STATES["PRE_CHAT"])
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "resend_message_in_group_for_message")
|
||||
@track_time("resend_message_in_group_for_message", "private_handlers")
|
||||
async def resend_message_in_group_for_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle messages in admin chat states"""
|
||||
# User service operations with metrics
|
||||
@@ -186,14 +207,14 @@ class PrivateHandlers:
|
||||
await message.forward(chat_id=self.settings.group_for_message)
|
||||
|
||||
current_date = datetime.now()
|
||||
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.db.add_new_message_in_db(message.text, message.from_user.id, message.message_id + 1, date)
|
||||
date = int(current_date.timestamp())
|
||||
await self.db.add_message(message.text, message.from_user.id, message.message_id + 1, date)
|
||||
|
||||
question = messages.get_message(get_first_name(message), 'QUESTION')
|
||||
user_state = await state.get_state()
|
||||
|
||||
if user_state == FSM_STATES["PRE_CHAT"]:
|
||||
markup = get_reply_keyboard(self.db, message.from_user.id)
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await message.answer(question, reply_markup=markup)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
elif user_state == FSM_STATES["CHAT"]:
|
||||
@@ -202,7 +223,7 @@ class PrivateHandlers:
|
||||
|
||||
|
||||
# Factory function to create handlers with dependencies
|
||||
def create_private_handlers(db, settings: BotSettings) -> PrivateHandlers:
|
||||
def create_private_handlers(db: AsyncBotDB, settings: BotSettings) -> PrivateHandlers:
|
||||
"""Create private handlers instance with dependencies"""
|
||||
return PrivateHandlers(db, settings)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from dataclasses import dataclass
|
||||
# Third-party imports
|
||||
from aiogram import types
|
||||
from aiogram.types import FSInputFile
|
||||
from database.models import TelegramPost, User
|
||||
|
||||
# Local imports - utilities
|
||||
from helper_bot.utils.helper_func import (
|
||||
@@ -41,16 +42,14 @@ from helper_bot.utils.metrics import (
|
||||
|
||||
class DatabaseProtocol(Protocol):
|
||||
"""Protocol for database operations"""
|
||||
def user_exists(self, user_id: int) -> bool: ...
|
||||
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str,
|
||||
username: str, is_bot: bool, language_code: str,
|
||||
emoji: str, created_date: str, updated_date: str) -> None: ...
|
||||
def update_username_and_full_name(self, user_id: int, username: str, full_name: str) -> None: ...
|
||||
def update_date_for_user(self, date: str, user_id: int) -> None: ...
|
||||
def add_post_in_db(self, message_id: int, text: str, user_id: int) -> None: ...
|
||||
def update_info_about_stickers(self, user_id: int) -> None: ...
|
||||
def add_new_message_in_db(self, text: str, user_id: int, message_id: int, date: str) -> None: ...
|
||||
def update_helper_message_in_db(self, message_id: int, helper_message_id: int) -> None: ...
|
||||
async def user_exists(self, user_id: int) -> bool: ...
|
||||
async def add_user(self, user: User) -> None: ...
|
||||
async def update_user_info(self, user_id: int, username: str = None, full_name: str = None) -> None: ...
|
||||
async def update_user_date(self, user_id: int) -> None: ...
|
||||
async def add_post(self, post: TelegramPost) -> None: ...
|
||||
async def update_stickers_info(self, user_id: int) -> None: ...
|
||||
async def add_message(self, message_text: str, user_id: int, message_id: int, date: int = None) -> None: ...
|
||||
async def update_helper_message(self, message_id: int, helper_message_id: int) -> None: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -75,11 +74,9 @@ class UserService:
|
||||
|
||||
@track_time("update_user_activity", "user_service")
|
||||
@track_errors("user_service", "update_user_activity")
|
||||
@db_query_time("update_user_activity", "users", "update")
|
||||
async def update_user_activity(self, user_id: int) -> None:
|
||||
"""Update user's last activity timestamp with metrics tracking"""
|
||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.db.update_date_for_user(current_date, user_id)
|
||||
await self.db.update_user_date(user_id)
|
||||
|
||||
@track_time("ensure_user_exists", "user_service")
|
||||
@track_errors("user_service", "ensure_user_exists")
|
||||
@@ -92,19 +89,28 @@ class UserService:
|
||||
is_bot = message.from_user.is_bot
|
||||
language_code = message.from_user.language_code
|
||||
|
||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if not self.db.user_exists(user_id):
|
||||
# Record database operation
|
||||
self.db.add_new_user_in_db(
|
||||
user_id, first_name, full_name, username, is_bot, language_code,
|
||||
"", current_date, current_date
|
||||
if not await self.db.user_exists(user_id):
|
||||
# Create User object with current timestamp
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
user = User(
|
||||
user_id=user_id,
|
||||
first_name=first_name,
|
||||
full_name=full_name,
|
||||
username=username,
|
||||
is_bot=is_bot,
|
||||
language_code=language_code,
|
||||
emoji="",
|
||||
has_stickers=False,
|
||||
date_added=current_timestamp,
|
||||
date_changed=current_timestamp,
|
||||
voice_bot_welcome_received=False
|
||||
)
|
||||
metrics.record_db_query("add_new_user", 0.0, "users", "insert")
|
||||
await self.db.add_user(user)
|
||||
metrics.record_db_query("add_user", 0.0, "users", "insert")
|
||||
else:
|
||||
is_need_update = check_username_and_full_name(user_id, username, full_name, self.db)
|
||||
is_need_update = await check_username_and_full_name(user_id, username, full_name, self.db)
|
||||
if is_need_update:
|
||||
self.db.update_username_and_full_name(user_id, username, full_name)
|
||||
await self.db.update_user_info(user_id, username, full_name)
|
||||
metrics.record_db_query("update_username_fullname", 0.0, "users", "update")
|
||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
safe_username = html.escape(username) if username else "Без никнейма"
|
||||
@@ -115,8 +121,8 @@ class UserService:
|
||||
chat_id=self.settings.group_for_logs,
|
||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
||||
|
||||
self.db.update_date_for_user(current_date, user_id)
|
||||
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
||||
await self.db.update_user_date(user_id)
|
||||
metrics.record_db_query("update_user_date", 0.0, "users", "update")
|
||||
|
||||
|
||||
@track_errors("user_service", "log_user_message")
|
||||
@@ -146,7 +152,13 @@ class PostService:
|
||||
markup = get_reply_keyboard_for_post()
|
||||
|
||||
sent_message_id = await send_text_message(self.settings.group_for_posts, message, post_text, markup)
|
||||
self.db.add_post_in_db(sent_message_id, message.text, message.from_user.id)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message_id,
|
||||
text=message.text,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
@track_time("handle_photo_post", "post_service")
|
||||
@track_errors("post_service", "handle_photo_post")
|
||||
@@ -161,8 +173,16 @@ class PostService:
|
||||
self.settings.group_for_posts, message, message.photo[-1].file_id, post_caption, markup
|
||||
)
|
||||
|
||||
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
|
||||
await add_in_db_media(sent_message, self.db)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=sent_message.caption or "",
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
success = await add_in_db_media(sent_message, self.db)
|
||||
if not success:
|
||||
logger.warning(f"handle_photo_post: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
|
||||
@track_time("handle_video_post", "post_service")
|
||||
@track_errors("post_service", "handle_video_post")
|
||||
@@ -177,8 +197,16 @@ class PostService:
|
||||
self.settings.group_for_posts, message, message.video.file_id, post_caption, markup
|
||||
)
|
||||
|
||||
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
|
||||
await add_in_db_media(sent_message, self.db)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=sent_message.caption or "",
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
success = await add_in_db_media(sent_message, self.db)
|
||||
if not success:
|
||||
logger.warning(f"handle_photo_post: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
|
||||
@track_time("handle_video_note_post", "post_service")
|
||||
@track_errors("post_service", "handle_video_note_post")
|
||||
@@ -189,8 +217,16 @@ class PostService:
|
||||
self.settings.group_for_posts, message, message.video_note.file_id, markup
|
||||
)
|
||||
|
||||
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
|
||||
await add_in_db_media(sent_message, self.db)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=sent_message.caption or "",
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
success = await add_in_db_media(sent_message, self.db)
|
||||
if not success:
|
||||
logger.warning(f"handle_photo_post: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
|
||||
@track_time("handle_audio_post", "post_service")
|
||||
@track_errors("post_service", "handle_audio_post")
|
||||
@@ -205,8 +241,16 @@ class PostService:
|
||||
self.settings.group_for_posts, message, message.audio.file_id, post_caption, markup
|
||||
)
|
||||
|
||||
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
|
||||
await add_in_db_media(sent_message, self.db)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=sent_message.caption or "",
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
success = await add_in_db_media(sent_message, self.db)
|
||||
if not success:
|
||||
logger.warning(f"handle_photo_post: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
|
||||
@track_time("handle_voice_post", "post_service")
|
||||
@track_errors("post_service", "handle_voice_post")
|
||||
@@ -217,8 +261,16 @@ class PostService:
|
||||
self.settings.group_for_posts, message, message.voice.file_id, markup
|
||||
)
|
||||
|
||||
self.db.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
|
||||
await add_in_db_media(sent_message, self.db)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=sent_message.caption or "",
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
success = await add_in_db_media(sent_message, self.db)
|
||||
if not success:
|
||||
logger.warning(f"handle_photo_post: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
|
||||
@track_time("handle_media_group_post", "post_service")
|
||||
@track_errors("post_service", "handle_media_group_post")
|
||||
@@ -229,18 +281,41 @@ class PostService:
|
||||
if album and album[0].caption:
|
||||
post_caption = get_text_message(album[0].caption.lower(), first_name, message.from_user.username)
|
||||
|
||||
# Создаем основной пост для медиагруппы
|
||||
main_post = TelegramPost(
|
||||
message_id=message.message_id, # ID основного сообщения медиагруппы
|
||||
text=post_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(main_post)
|
||||
|
||||
# Отправляем медиагруппу в группу для модерации
|
||||
media_group = await prepare_media_group_from_middlewares(album, post_caption)
|
||||
media_group_message_id = await send_media_group_message_to_private_chat(
|
||||
self.settings.group_for_posts, message, media_group, self.db
|
||||
self.settings.group_for_posts, message, media_group, self.db, main_post.message_id
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
# Создаем helper сообщение с кнопками
|
||||
markup = get_reply_keyboard_for_post()
|
||||
help_message_id = await send_text_message(self.settings.group_for_posts, message, "^", markup)
|
||||
help_message_id = await send_text_message(self.settings.group_for_posts, message, "ВРУЧНУЮ ВЫКЛАДЫВАТЬ, ПОСЛЕ ВЫКЛАДКИ УДАЛИТЬ ОБА ПОСТА")
|
||||
|
||||
self.db.update_helper_message_in_db(
|
||||
message_id=media_group_message_id, helper_message_id=help_message_id
|
||||
# Создаем helper пост и связываем его с основным
|
||||
helper_post = TelegramPost(
|
||||
message_id=help_message_id, # ID helper сообщения
|
||||
text="^", # Специальный маркер для медиагруппы
|
||||
author_id=message.from_user.id,
|
||||
helper_text_message_id=main_post.message_id, # Ссылка на основной пост
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(helper_post)
|
||||
|
||||
# Обновляем основной пост, чтобы он ссылался на helper
|
||||
await self.db.update_helper_message(
|
||||
message_id=main_post.message_id,
|
||||
helper_message_id=help_message_id
|
||||
)
|
||||
|
||||
@track_time("process_post", "post_service")
|
||||
@@ -248,7 +323,7 @@ class PostService:
|
||||
async def process_post(self, message: types.Message, album: Union[list, None] = None) -> None:
|
||||
"""Process post based on content type"""
|
||||
first_name = get_first_name(message)
|
||||
|
||||
# TODO: Бесит меня этот функционал
|
||||
if message.media_group_id is not None:
|
||||
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
|
||||
await send_text_message(
|
||||
|
||||
3
helper_bot/handlers/voice/__init__.py
Normal file
3
helper_bot/handlers/voice/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .voice_handler import VoiceHandlers
|
||||
|
||||
__all__ = ["VoiceHandlers"]
|
||||
59
helper_bot/handlers/voice/constants.py
Normal file
59
helper_bot/handlers/voice/constants.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from typing import Final, Dict
|
||||
|
||||
# Voice bot constants
|
||||
VOICE_BOT_NAME = "voice"
|
||||
|
||||
# States
|
||||
STATE_START = "START"
|
||||
STATE_STANDUP_WRITE = "STANDUP_WRITE"
|
||||
|
||||
# Commands
|
||||
CMD_START = "start"
|
||||
CMD_HELP = "help"
|
||||
CMD_RESTART = "restart"
|
||||
CMD_EMOJI = "emoji"
|
||||
CMD_REFRESH = "refresh"
|
||||
|
||||
# Command to command mapping for metrics
|
||||
COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"start": "voice_start",
|
||||
"help": "voice_help",
|
||||
"restart": "voice_restart",
|
||||
"emoji": "voice_emoji",
|
||||
"refresh": "voice_refresh"
|
||||
}
|
||||
|
||||
# Button texts
|
||||
BTN_SPEAK = "🎤Высказаться"
|
||||
BTN_LISTEN = "🎧Послушать"
|
||||
|
||||
# Button to command mapping for metrics
|
||||
BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"🎤Высказаться": "voice_speak",
|
||||
"🎧Послушать": "voice_listen",
|
||||
"Отменить": "voice_cancel",
|
||||
"🔄Сбросить прослушивания": "voice_refresh_listen",
|
||||
"😊Узнать эмодзи": "voice_emoji"
|
||||
}
|
||||
|
||||
# Callback data
|
||||
CALLBACK_SAVE = "save"
|
||||
CALLBACK_DELETE = "delete"
|
||||
|
||||
# Callback to command mapping for metrics
|
||||
CALLBACK_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"save": "voice_save",
|
||||
"delete": "voice_delete"
|
||||
}
|
||||
|
||||
# File paths
|
||||
VOICE_USERS_DIR = "voice_users"
|
||||
STICK_DIR = "Stick"
|
||||
STICK_PATTERN = "Hello_*"
|
||||
|
||||
# Time delays
|
||||
STICKER_DELAY = 0.3
|
||||
MESSAGE_DELAY_1 = 1.0
|
||||
MESSAGE_DELAY_2 = 1.5
|
||||
MESSAGE_DELAY_3 = 1.3
|
||||
MESSAGE_DELAY_4 = 0.8
|
||||
23
helper_bot/handlers/voice/exceptions.py
Normal file
23
helper_bot/handlers/voice/exceptions.py
Normal file
@@ -0,0 +1,23 @@
|
||||
class VoiceBotError(Exception):
|
||||
"""Базовое исключение для voice_bot"""
|
||||
pass
|
||||
|
||||
|
||||
class VoiceMessageError(VoiceBotError):
|
||||
"""Ошибка при работе с голосовыми сообщениями"""
|
||||
pass
|
||||
|
||||
|
||||
class AudioProcessingError(VoiceBotError):
|
||||
"""Ошибка при обработке аудио"""
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(VoiceBotError):
|
||||
"""Ошибка базы данных"""
|
||||
pass
|
||||
|
||||
|
||||
class FileOperationError(VoiceBotError):
|
||||
"""Ошибка при работе с файлами"""
|
||||
pass
|
||||
336
helper_bot/handlers/voice/services.py
Normal file
336
helper_bot/handlers/voice/services.py
Normal file
@@ -0,0 +1,336 @@
|
||||
import random
|
||||
import asyncio
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError, DatabaseError, FileOperationError
|
||||
from helper_bot.handlers.voice.constants import (
|
||||
VOICE_USERS_DIR, STICK_DIR, STICK_PATTERN, STICKER_DELAY,
|
||||
MESSAGE_DELAY_1, MESSAGE_DELAY_2, MESSAGE_DELAY_3, MESSAGE_DELAY_4
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
class VoiceMessage:
|
||||
"""Модель голосового сообщения"""
|
||||
def __init__(self, file_name: str, user_id: int, date_added: datetime, file_id: int):
|
||||
self.file_name = file_name
|
||||
self.user_id = user_id
|
||||
self.date_added = date_added
|
||||
self.file_id = file_id
|
||||
|
||||
class VoiceBotService:
|
||||
"""Сервис для работы с голосовыми сообщениями"""
|
||||
|
||||
def __init__(self, bot_db, settings):
|
||||
self.bot_db = bot_db
|
||||
self.settings = settings
|
||||
|
||||
@track_time("get_welcome_sticker", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_welcome_sticker")
|
||||
async def get_welcome_sticker(self) -> Optional[FSInputFile]:
|
||||
"""Получить случайный приветственный стикер"""
|
||||
try:
|
||||
name_stick_hello = list(Path(STICK_DIR).rglob(STICK_PATTERN))
|
||||
if not name_stick_hello:
|
||||
return None
|
||||
|
||||
random_stick_hello = random.choice(name_stick_hello)
|
||||
random_stick_hello = FSInputFile(path=random_stick_hello)
|
||||
logger.info(f"Стикер успешно получен. Наименование стикера: {random_stick_hello}")
|
||||
return random_stick_hello
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении стикера: {e}")
|
||||
if self.settings['Settings']['logs']:
|
||||
await self._send_error_to_logs(f'Отправка приветственных стикеров лажает. Ошибка: {e}')
|
||||
return None
|
||||
|
||||
@track_time("send_welcome_messages", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "send_welcome_messages")
|
||||
async def send_welcome_messages(self, message, user_emoji: str):
|
||||
"""Отправить приветственные сообщения"""
|
||||
try:
|
||||
# Отправляем стикер
|
||||
sticker = await self.get_welcome_sticker()
|
||||
if sticker:
|
||||
await message.answer_sticker(sticker)
|
||||
await asyncio.sleep(STICKER_DELAY)
|
||||
|
||||
# Отправляем приветственное сообщение
|
||||
markup = self._get_main_keyboard()
|
||||
await message.answer(
|
||||
text="<b>Привет.</b>",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(STICKER_DELAY)
|
||||
|
||||
# Отправляем описание
|
||||
await message.answer(
|
||||
text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска</i>",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_1)
|
||||
|
||||
# Отправляем аналогию
|
||||
await message.answer(
|
||||
text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_2)
|
||||
|
||||
# Отправляем правила
|
||||
await message.answer(
|
||||
text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя бы на 5-10 секунд</i>",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_3)
|
||||
|
||||
# Отправляем информацию об анонимности
|
||||
await message.answer(
|
||||
text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
# Отправляем предложения
|
||||
await message.answer(
|
||||
text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
# Отправляем информацию об эмодзи
|
||||
await message.answer(
|
||||
text=f"Любые войсы будут помечены эмоджи. <b>Твой эмоджи - </b>{user_emoji}Таким эмоджи будут помечены твои сообщения для других Но другие люди не узнают кто за каким эмоджи скрывается:)",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
# Отправляем информацию о помощи
|
||||
await message.answer(
|
||||
text="Так же можешь ознакомиться с инструкцией к боту по команде /help",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
# Отправляем финальное сообщение
|
||||
await message.answer(
|
||||
text="<b>Ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
|
||||
parse_mode='html',
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке приветственных сообщений: {e}")
|
||||
raise VoiceMessageError(f"Не удалось отправить приветственные сообщения: {e}")
|
||||
|
||||
@track_time("get_random_audio", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_random_audio")
|
||||
async def get_random_audio(self, user_id: int) -> Optional[Tuple[str, str, str]]:
|
||||
"""Получить случайное аудио для прослушивания"""
|
||||
try:
|
||||
check_audio = await self.bot_db.check_listen_audio(user_id=user_id)
|
||||
list_audio = list(check_audio)
|
||||
|
||||
if not list_audio:
|
||||
return None
|
||||
|
||||
# Получаем случайное аудио
|
||||
number_element = random.randint(0, len(list_audio) - 1)
|
||||
audio_for_user = check_audio[number_element]
|
||||
|
||||
# Получаем информацию об авторе
|
||||
user_id_author = await self.bot_db.get_user_id_by_file_name(audio_for_user)
|
||||
date_added = await self.bot_db.get_date_by_file_name(audio_for_user)
|
||||
user_emoji = await self.bot_db.get_user_emoji(user_id_author)
|
||||
|
||||
return audio_for_user, date_added, user_emoji
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении случайного аудио: {e}")
|
||||
raise AudioProcessingError(f"Не удалось получить случайное аудио: {e}")
|
||||
|
||||
@track_time("mark_audio_as_listened", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "mark_audio_as_listened")
|
||||
async def mark_audio_as_listened(self, file_name: str, user_id: int) -> None:
|
||||
"""Пометить аудио как прослушанное"""
|
||||
try:
|
||||
await self.bot_db.mark_listened_audio(file_name, user_id=user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при пометке аудио как прослушанного: {e}")
|
||||
raise DatabaseError(f"Не удалось пометить аудио как прослушанное: {e}")
|
||||
|
||||
@track_time("clear_user_listenings", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "clear_user_listenings")
|
||||
async def clear_user_listenings(self, user_id: int) -> None:
|
||||
"""Очистить прослушивания пользователя"""
|
||||
try:
|
||||
await self.bot_db.delete_listen_count_for_user(user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке прослушиваний: {e}")
|
||||
raise DatabaseError(f"Не удалось очистить прослушивания: {e}")
|
||||
|
||||
@track_time("get_remaining_audio_count", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_remaining_audio_count")
|
||||
async def get_remaining_audio_count(self, user_id: int) -> int:
|
||||
"""Получить количество оставшихся непрослушанных аудио"""
|
||||
try:
|
||||
check_audio = await self.bot_db.check_listen_audio(user_id=user_id)
|
||||
return len(list(check_audio))
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении количества аудио: {e}")
|
||||
raise DatabaseError(f"Не удалось получить количество аудио: {e}")
|
||||
|
||||
@track_time("get_main_keyboard", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_main_keyboard")
|
||||
def _get_main_keyboard(self):
|
||||
"""Получить основную клавиатуру"""
|
||||
from helper_bot.keyboards.keyboards import get_main_keyboard
|
||||
return get_main_keyboard()
|
||||
|
||||
@track_time("send_error_to_logs", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "send_error_to_logs")
|
||||
async def _send_error_to_logs(self, message: str) -> None:
|
||||
"""Отправить ошибку в логи"""
|
||||
try:
|
||||
from helper_bot.utils.helper_func import send_voice_message
|
||||
await send_voice_message(
|
||||
self.settings['Telegram']['important_logs'],
|
||||
None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить ошибку в логи: {e}")
|
||||
|
||||
|
||||
class AudioFileService:
|
||||
"""Сервис для работы с аудио файлами"""
|
||||
|
||||
def __init__(self, bot_db):
|
||||
self.bot_db = bot_db
|
||||
|
||||
@track_time("generate_file_name", "audio_file_service")
|
||||
@track_errors("audio_file_service", "generate_file_name")
|
||||
async def generate_file_name(self, user_id: int) -> str:
|
||||
"""Сгенерировать имя файла для аудио"""
|
||||
try:
|
||||
# Проверяем есть ли запись о файле в базе данных
|
||||
user_audio_count = await self.bot_db.get_user_audio_records_count(user_id=user_id)
|
||||
|
||||
if user_audio_count == 0:
|
||||
# Если нет, то генерируем имя файла
|
||||
file_name = f'message_from_{user_id}_number_1'
|
||||
else:
|
||||
# Иначе берем последнюю запись из БД, добавляем к ней 1
|
||||
file_name = await self.bot_db.get_path_for_audio_record(user_id=user_id)
|
||||
if file_name:
|
||||
# Извлекаем номер из имени файла и увеличиваем на 1
|
||||
try:
|
||||
current_number = int(file_name.split('_')[-1])
|
||||
new_number = current_number + 1
|
||||
except (ValueError, IndexError):
|
||||
new_number = user_audio_count + 1
|
||||
else:
|
||||
new_number = user_audio_count + 1
|
||||
|
||||
file_name = f'message_from_{user_id}_number_{new_number}'
|
||||
|
||||
return file_name
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при генерации имени файла: {e}")
|
||||
raise FileOperationError(f"Не удалось сгенерировать имя файла: {e}")
|
||||
|
||||
@track_time("save_audio_file", "audio_file_service")
|
||||
@track_errors("audio_file_service", "save_audio_file")
|
||||
async def save_audio_file(self, file_name: str, user_id: int, date_added: datetime, file_id: str) -> None:
|
||||
"""Сохранить информацию об аудио файле в базу данных"""
|
||||
try:
|
||||
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении аудио файла в БД: {e}")
|
||||
raise DatabaseError(f"Не удалось сохранить аудио файл в БД: {e}")
|
||||
|
||||
@track_time("download_and_save_audio", "audio_file_service")
|
||||
@track_errors("audio_file_service", "download_and_save_audio")
|
||||
async def download_and_save_audio(self, bot, message, file_name: str) -> None:
|
||||
"""Скачать и сохранить аудио файл"""
|
||||
try:
|
||||
logger.info(f"Начинаем скачивание и сохранение аудио: {file_name}")
|
||||
|
||||
# Проверяем наличие голосового сообщения
|
||||
if not message or not message.voice:
|
||||
logger.error("Сообщение или голосовое сообщение не найдено")
|
||||
raise FileOperationError("Сообщение или голосовое сообщение не найдено")
|
||||
|
||||
file_id = message.voice.file_id
|
||||
logger.info(f"Получен file_id: {file_id}")
|
||||
|
||||
file_info = await bot.get_file(file_id=file_id)
|
||||
logger.info(f"Получена информация о файле: {file_info.file_path}")
|
||||
|
||||
downloaded_file = await bot.download_file(file_path=file_info.file_path)
|
||||
|
||||
# Проверяем что файл успешно скачан
|
||||
if not downloaded_file:
|
||||
logger.error("Не удалось скачать файл")
|
||||
raise FileOperationError("Не удалось скачать файл")
|
||||
|
||||
# Получаем размер файла без изменения позиции
|
||||
current_pos = downloaded_file.tell()
|
||||
downloaded_file.seek(0, 2) # Переходим в конец файла
|
||||
file_size = downloaded_file.tell()
|
||||
downloaded_file.seek(current_pos) # Возвращаемся в исходную позицию
|
||||
|
||||
logger.info(f"Файл скачан, размер: {file_size} bytes")
|
||||
|
||||
# Создаем директорию если она не существует
|
||||
import os
|
||||
os.makedirs(VOICE_USERS_DIR, exist_ok=True)
|
||||
logger.info(f"Директория {VOICE_USERS_DIR} создана/проверена")
|
||||
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
logger.info(f"Сохраняем файл по пути: {file_path}")
|
||||
|
||||
# Сбрасываем позицию в файле перед сохранением
|
||||
downloaded_file.seek(0)
|
||||
|
||||
# Сохраняем файл
|
||||
with open(file_path, 'wb') as new_file:
|
||||
new_file.write(downloaded_file.read())
|
||||
|
||||
logger.info(f"Файл успешно сохранен: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при скачивании и сохранении аудио: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise FileOperationError(f"Не удалось скачать и сохранить аудио: {e}")
|
||||
99
helper_bot/handlers/voice/utils.py
Normal file
99
helper_bot/handlers/voice/utils.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import time
|
||||
import html
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from helper_bot.handlers.voice.exceptions import DatabaseError
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
def format_time_ago(date_from_db: str) -> Optional[str]:
|
||||
"""Форматировать время с момента последней записи"""
|
||||
try:
|
||||
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()
|
||||
date_difference = time_now_timestamp - last_voice_time_timestamp
|
||||
|
||||
# Считаем минуты, часы, дни
|
||||
much_minutes_ago = round(date_difference / 60, 0)
|
||||
much_hour_ago = round(date_difference / 3600, 0)
|
||||
much_days_ago = int(round(much_hour_ago / 24, 0))
|
||||
|
||||
message_with_date = ''
|
||||
if much_minutes_ago <= 60:
|
||||
word_minute = plural_time(1, much_minutes_ago)
|
||||
# Экранируем потенциально проблемные символы
|
||||
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)
|
||||
# Экранируем потенциально проблемные символы
|
||||
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)
|
||||
# Экранируем потенциально проблемные символы
|
||||
word_day_escaped = html.escape(word_day)
|
||||
message_with_date = f'<b>Последнее сообщение было записано {word_day_escaped} назад</b>'
|
||||
|
||||
return message_with_date
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при форматировании времени: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def plural_time(type: int, n: float) -> str:
|
||||
"""Форматировать множественное число для времени"""
|
||||
word = []
|
||||
if type == 1:
|
||||
word = ['минуту', 'минуты', 'минут']
|
||||
elif type == 2:
|
||||
word = ['час', 'часа', 'часов']
|
||||
elif type == 3:
|
||||
word = ['день', 'дня', 'дней']
|
||||
else:
|
||||
return str(int(n))
|
||||
|
||||
if n % 10 == 1 and n % 100 != 11:
|
||||
p = 0
|
||||
elif 2 <= n % 10 <= 4 and (n % 100 < 10 or n % 100 >= 20):
|
||||
p = 1
|
||||
else:
|
||||
p = 2
|
||||
|
||||
new_number = int(n)
|
||||
return str(new_number) + ' ' + word[p]
|
||||
|
||||
|
||||
async def get_last_message_text(bot_db) -> Optional[str]:
|
||||
"""Получить текст сообщения о времени последней записи"""
|
||||
try:
|
||||
date_from_db = await bot_db.last_date_audio()
|
||||
if date_from_db is None:
|
||||
return None
|
||||
# Преобразуем UNIX timestamp в строку для format_time_ago
|
||||
date_string = datetime.fromtimestamp(date_from_db).strftime("%Y-%m-%d %H:%M:%S")
|
||||
return format_time_ago(date_string)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось получить дату последнего сообщения - {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def validate_voice_message(message) -> bool:
|
||||
"""Проверить валидность голосового сообщения"""
|
||||
return message.content_type == 'voice'
|
||||
|
||||
|
||||
async def get_user_emoji_safe(bot_db, user_id: int) -> str:
|
||||
"""Безопасно получить эмодзи пользователя"""
|
||||
try:
|
||||
user_emoji = await bot_db.get_user_emoji(user_id)
|
||||
return user_emoji if user_emoji and user_emoji != "Смайл еще не определен" else "😊"
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении эмодзи пользователя {user_id}: {e}")
|
||||
return "😊"
|
||||
422
helper_bot/handlers/voice/voice_handler.py
Normal file
422
helper_bot/handlers/voice/voice_handler.py
Normal file
@@ -0,0 +1,422 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from aiogram import Router, types, F
|
||||
from aiogram.filters import Command, StateFilter, MagicData
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
|
||||
from helper_bot.utils import messages
|
||||
from helper_bot.utils.helper_func import get_first_name, update_user_info, check_user_emoji, send_voice_message
|
||||
from logs.custom_logger import logger
|
||||
from helper_bot.handlers.voice.constants import *
|
||||
from helper_bot.handlers.voice.services import VoiceBotService
|
||||
from helper_bot.handlers.voice.utils import get_last_message_text, validate_voice_message, get_user_emoji_safe
|
||||
from helper_bot.keyboards.keyboards import get_main_keyboard, get_reply_keyboard_for_voice
|
||||
from helper_bot.keyboards import get_reply_keyboard
|
||||
from helper_bot.handlers.private.constants import FSM_STATES
|
||||
from helper_bot.handlers.private.constants import BUTTON_TEXTS
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
class VoiceHandlers:
|
||||
def __init__(self, db, settings):
|
||||
self.db = db.get_db() if hasattr(db, 'get_db') else db
|
||||
self.settings = settings
|
||||
self.router = Router()
|
||||
self._setup_handlers()
|
||||
self._setup_middleware()
|
||||
|
||||
def _setup_middleware(self):
|
||||
self.router.message.middleware(DependenciesMiddleware())
|
||||
self.router.message.middleware(BlacklistMiddleware())
|
||||
|
||||
def _setup_handlers(self):
|
||||
self.router.message.register(
|
||||
self.cancel_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "Отменить"
|
||||
)
|
||||
|
||||
# Обработчик кнопки "Голосовой бот"
|
||||
self.router.message.register(
|
||||
self.voice_bot_button_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["VOICE_BOT"]
|
||||
)
|
||||
|
||||
# Команды
|
||||
self.router.message.register(
|
||||
self.restart_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_RESTART)
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_EMOJI)
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.help_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_HELP)
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.start,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_START)
|
||||
)
|
||||
|
||||
# Дополнительные команды
|
||||
self.router.message.register(
|
||||
self.refresh_listen_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_REFRESH)
|
||||
)
|
||||
|
||||
# Обработчики состояний и кнопок
|
||||
self.router.message.register(
|
||||
self.standup_write,
|
||||
StateFilter(STATE_START),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BTN_SPEAK
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.suggest_voice,
|
||||
StateFilter(STATE_STANDUP_WRITE),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.standup_listen_audio,
|
||||
StateFilter(STATE_START),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BTN_LISTEN
|
||||
)
|
||||
|
||||
# Новые обработчики кнопок
|
||||
self.router.message.register(
|
||||
self.refresh_listen_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "🔄Сбросить прослушивания"
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "😊Узнать эмодзи"
|
||||
)
|
||||
|
||||
@track_time("voice_bot_button_handler", "voice_handlers")
|
||||
@track_errors("voice_handlers", "voice_bot_button_handler")
|
||||
async def voice_bot_button_handler(self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings")):
|
||||
"""Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
|
||||
try:
|
||||
# Проверяем, получал ли пользователь приветственное сообщение
|
||||
welcome_received = await bot_db.check_voice_bot_welcome_received(message.from_user.id)
|
||||
logger.info(f"Пользователь {message.from_user.id}: welcome_received = {welcome_received}")
|
||||
|
||||
if welcome_received:
|
||||
# Если уже получал приветствие, вызываем restart_function
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызываем restart_function")
|
||||
await self.restart_function(message, state, bot_db, settings)
|
||||
else:
|
||||
# Если не получал, вызываем start
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызываем start")
|
||||
await self.start(message, state, bot_db, settings)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке приветственного сообщения: {e}")
|
||||
# В случае ошибки вызываем start
|
||||
await self.start(message, state, bot_db, settings)
|
||||
|
||||
@track_time("restart_function", "voice_handlers")
|
||||
@track_errors("voice_handlers", "restart_function")
|
||||
async def restart_function(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызывается функция restart_function")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
await check_user_emoji(message)
|
||||
markup = get_main_keyboard()
|
||||
await message.answer(text='🎤 Записывайся или слушай!', reply_markup=markup)
|
||||
await state.set_state(STATE_START)
|
||||
|
||||
@track_time("handle_emoji_message", "voice_handlers")
|
||||
@track_errors("voice_handlers", "handle_emoji_message")
|
||||
async def handle_emoji_message(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
user_emoji = await check_user_emoji(message)
|
||||
await state.set_state(STATE_START)
|
||||
if user_emoji is not None:
|
||||
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
|
||||
|
||||
@track_time("help_function", "voice_handlers")
|
||||
@track_errors("voice_handlers", "help_function")
|
||||
async def help_function(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
help_message = messages.get_message(get_first_name(message), 'HELP_MESSAGE')
|
||||
await message.answer(
|
||||
text=help_message,
|
||||
disable_web_page_preview=not settings['Telegram']['preview_link']
|
||||
)
|
||||
await state.set_state(STATE_START)
|
||||
|
||||
@track_time("start", "voice_handlers")
|
||||
@track_errors("voice_handlers", "start")
|
||||
async def start(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызывается функция start")
|
||||
await state.set_state(STATE_START)
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
user_emoji = await get_user_emoji_safe(bot_db, message.from_user.id)
|
||||
|
||||
# Создаем сервис и отправляем приветственные сообщения
|
||||
voice_service = VoiceBotService(bot_db, settings)
|
||||
await voice_service.send_welcome_messages(message, user_emoji)
|
||||
|
||||
# Отмечаем, что пользователь получил приветственное сообщение
|
||||
try:
|
||||
await bot_db.mark_voice_bot_welcome_received(message.from_user.id)
|
||||
logger.info(f"Пользователь {message.from_user.id}: отмечен как получивший приветствие")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отметке получения приветствия: {e}")
|
||||
|
||||
@track_time("cancel_handler", "voice_handlers")
|
||||
@track_errors("voice_handlers", "cancel_handler")
|
||||
async def cancel_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
"""Обработчик кнопки 'Отменить' - возвращает в начальное состояние"""
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await message.answer(text='Добро пожаловать в меню!', reply_markup=markup, parse_mode='HTML')
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
@track_time("refresh_listen_function", "voice_handlers")
|
||||
@track_errors("voice_handlers", "refresh_listen_function")
|
||||
async def refresh_listen_function(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
markup = get_main_keyboard()
|
||||
|
||||
# Очищаем прослушивания через сервис
|
||||
voice_service = VoiceBotService(bot_db, settings)
|
||||
await voice_service.clear_user_listenings(message.from_user.id)
|
||||
|
||||
listenings_cleared_message = messages.get_message(get_first_name(message), 'LISTENINGS_CLEARED_MESSAGE')
|
||||
await message.answer(
|
||||
text=listenings_cleared_message,
|
||||
disable_web_page_preview=not settings['Telegram']['preview_link'],
|
||||
reply_markup=markup
|
||||
)
|
||||
await state.set_state(STATE_START)
|
||||
|
||||
|
||||
@track_time("standup_write", "voice_handlers")
|
||||
@track_errors("voice_handlers", "standup_write")
|
||||
async def standup_write(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
markup = types.ReplyKeyboardRemove()
|
||||
record_voice_message = messages.get_message(get_first_name(message), 'RECORD_VOICE_MESSAGE')
|
||||
await message.answer(text=record_voice_message, reply_markup=markup)
|
||||
|
||||
try:
|
||||
message_with_date = await get_last_message_text(bot_db)
|
||||
if message_with_date:
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить дату последнего сообщения - {e}')
|
||||
|
||||
await state.set_state(STATE_STANDUP_WRITE)
|
||||
|
||||
|
||||
@track_time("suggest_voice", "voice_handlers")
|
||||
@track_errors("voice_handlers", "suggest_voice")
|
||||
async def suggest_voice(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(
|
||||
f"Вызов функции suggest_voice. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}"
|
||||
)
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
markup = get_main_keyboard()
|
||||
|
||||
if await validate_voice_message(message):
|
||||
markup_for_voice = get_reply_keyboard_for_voice()
|
||||
|
||||
# Отправляем аудио в приватный канал
|
||||
sent_message = await send_voice_message(
|
||||
settings['Telegram']['group_for_posts'],
|
||||
message,
|
||||
message.voice.file_id,
|
||||
markup_for_voice
|
||||
)
|
||||
|
||||
# Сохраняем в базу инфо о посте
|
||||
await bot_db.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id)
|
||||
|
||||
# Отправляем юзеру ответ и возвращаем его в меню
|
||||
voice_saved_message = messages.get_message(get_first_name(message), 'VOICE_SAVED_MESSAGE')
|
||||
await message.answer(text=voice_saved_message, reply_markup=markup)
|
||||
await state.set_state(STATE_START)
|
||||
else:
|
||||
unknown_content_message = messages.get_message(get_first_name(message), 'UNKNOWN_CONTENT_MESSAGE')
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
await message.answer(text=unknown_content_message, reply_markup=markup)
|
||||
await state.set_state(STATE_STANDUP_WRITE)
|
||||
|
||||
|
||||
@track_time("standup_listen_audio", "voice_handlers")
|
||||
@track_errors("voice_handlers", "standup_listen_audio")
|
||||
async def standup_listen_audio(
|
||||
self,
|
||||
message: types.Message,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
markup = get_main_keyboard()
|
||||
|
||||
# Создаем сервис для работы с аудио
|
||||
voice_service = VoiceBotService(bot_db, settings)
|
||||
|
||||
try:
|
||||
# Получаем случайное аудио
|
||||
audio_data = await voice_service.get_random_audio(message.from_user.id)
|
||||
|
||||
if not audio_data:
|
||||
no_audio_message = messages.get_message(get_first_name(message), 'NO_AUDIO_MESSAGE')
|
||||
await message.answer(text=no_audio_message, reply_markup=markup)
|
||||
try:
|
||||
message_with_date = await get_last_message_text(bot_db)
|
||||
if message_with_date:
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить последнюю дату {e}')
|
||||
return
|
||||
|
||||
audio_for_user, date_added, user_emoji = audio_data
|
||||
|
||||
# Получаем путь к файлу
|
||||
path = Path(f'{VOICE_USERS_DIR}/{audio_for_user}.ogg')
|
||||
|
||||
# Проверяем существование файла
|
||||
if not path.exists():
|
||||
logger.error(f"Файл не найден: {path}")
|
||||
await message.answer(
|
||||
text="Файл аудио не найден. Обратитесь к администратору.",
|
||||
reply_markup=markup
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем размер файла
|
||||
if path.stat().st_size == 0:
|
||||
logger.error(f"Файл пустой: {path}")
|
||||
await message.answer(
|
||||
text="Файл аудио поврежден. Обратитесь к администратору.",
|
||||
reply_markup=markup
|
||||
)
|
||||
return
|
||||
|
||||
voice = FSInputFile(path)
|
||||
|
||||
# Формируем подпись
|
||||
if user_emoji:
|
||||
caption = f'{user_emoji}\nДата записи: {date_added}'
|
||||
else:
|
||||
caption = f'Дата записи: {date_added}'
|
||||
|
||||
try:
|
||||
await message.bot.send_voice(
|
||||
chat_id=message.chat.id,
|
||||
voice=voice,
|
||||
caption=caption,
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
# Маркируем сообщение как прослушанное только после успешной отправки
|
||||
await voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
|
||||
|
||||
# Получаем количество оставшихся аудио только после успешной отправки
|
||||
remaining_count = await voice_service.get_remaining_audio_count(message.from_user.id)
|
||||
await message.answer(
|
||||
text=f'Осталось непрослушанных: <b>{remaining_count}</b>',
|
||||
reply_markup=markup
|
||||
)
|
||||
|
||||
except Exception as voice_error:
|
||||
if "VOICE_MESSAGES_FORBIDDEN" in str(voice_error):
|
||||
# Если голосовые сообщения запрещены, отправляем информативное сообщение
|
||||
logger.info(f"Пользователь {message.from_user.id} запретил получение голосовых сообщений")
|
||||
|
||||
privacy_message = "🔇 К сожалению, у тебя закрыт доступ к получению голосовых сообщений.\n\nДля продолжения взаимодействия с ботом необходимо дать возможность мне присылать войсы в настройках приватности Telegram.\n\n💡 Как это сделать:\n1. Открой настройки Telegram\n2. Перейди в 'Конфиденциальность и безопасность'\n3. Выбери 'Голосовые сообщения'\n4. Разреши получение от 'Всех' или добавь меня в исключения"
|
||||
|
||||
await message.answer(text=privacy_message, reply_markup=markup)
|
||||
return # Выходим без записи о прослушивании
|
||||
|
||||
else:
|
||||
raise voice_error
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при прослушивании аудио: {e}")
|
||||
await message.answer(
|
||||
text="Произошла ошибка при получении аудио. Попробуйте позже.",
|
||||
reply_markup=markup
|
||||
)
|
||||
@@ -25,12 +25,13 @@ def get_reply_keyboard_for_post():
|
||||
|
||||
@track_time("get_reply_keyboard", "keyboard_service")
|
||||
@track_errors("keyboard_service", "get_reply_keyboard")
|
||||
def get_reply_keyboard(BotDB, user_id):
|
||||
async def get_reply_keyboard(db, user_id):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
||||
builder.row(types.KeyboardButton(text="📩Связаться с админами"))
|
||||
builder.row(types.KeyboardButton(text=" 🎤Голосовой бот"))
|
||||
builder.row(types.KeyboardButton(text="👋🏼Сказать пока!"))
|
||||
if not BotDB.get_info_about_stickers(user_id=user_id):
|
||||
if not await db.get_stickers_info(user_id):
|
||||
builder.row(types.KeyboardButton(text="🤪Хочу стикеры"))
|
||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||
return markup
|
||||
@@ -170,3 +171,33 @@ def create_keyboard_for_approve_ban():
|
||||
builder.add(types.KeyboardButton(text="Отменить"))
|
||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||
return markup
|
||||
|
||||
|
||||
def get_main_keyboard():
|
||||
builder = ReplyKeyboardBuilder()
|
||||
# Первая строка: Высказаться и послушать
|
||||
builder.row(
|
||||
types.KeyboardButton(text="🎤Высказаться"),
|
||||
types.KeyboardButton(text="🎧Послушать")
|
||||
)
|
||||
# Вторая строка: сбросить прослушивания и узнать эмодзи
|
||||
builder.row(
|
||||
types.KeyboardButton(text="🔄Сбросить прослушивания"),
|
||||
types.KeyboardButton(text="😊Узнать эмодзи")
|
||||
)
|
||||
# Третья строка: Вернуться в меню
|
||||
builder.row(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
|
||||
|
||||
@@ -2,14 +2,42 @@ from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.fsm.strategy import FSMStrategy
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
from helper_bot.handlers.admin import admin_router
|
||||
from helper_bot.handlers.callback import callback_router
|
||||
from helper_bot.handlers.group import group_router
|
||||
from helper_bot.handlers.private import private_router
|
||||
from helper_bot.handlers.voice import VoiceHandlers
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
|
||||
from helper_bot.server_prometheus import start_metrics_server, stop_metrics_server
|
||||
|
||||
|
||||
async def start_bot_with_retry(bot: Bot, dp: Dispatcher, max_retries: int = 5, base_delay: float = 1.0):
|
||||
"""Запуск бота с автоматическим перезапуском при сетевых ошибках"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logging.info(f"Запуск бота (попытка {attempt + 1}/{max_retries})")
|
||||
await dp.start_polling(bot, skip_updates=True)
|
||||
break
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
if any(keyword in error_msg for keyword in ['network', 'disconnected', 'timeout', 'connection']):
|
||||
if attempt < max_retries - 1:
|
||||
delay = base_delay * (2 ** attempt) # Exponential backoff
|
||||
logging.warning(f"Сетевая ошибка при запуске бота: {e}. Повтор через {delay:.1f}с (попытка {attempt + 1}/{max_retries})")
|
||||
await asyncio.sleep(delay)
|
||||
continue
|
||||
else:
|
||||
logging.error(f"Превышено максимальное количество попыток запуска бота: {e}")
|
||||
raise
|
||||
else:
|
||||
logging.error(f"Критическая ошибка при запуске бота: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def start_bot(bdf):
|
||||
@@ -17,7 +45,8 @@ 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)
|
||||
), timeout=60.0) # Увеличиваем timeout для стабильности
|
||||
|
||||
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
||||
|
||||
# ✅ Оптимизированная регистрация middleware
|
||||
@@ -25,12 +54,58 @@ async def start_bot(bdf):
|
||||
dp.update.outer_middleware(MetricsMiddleware())
|
||||
dp.update.outer_middleware(BlacklistMiddleware())
|
||||
|
||||
# Добавляем middleware напрямую к роутерам для тестирования
|
||||
admin_router.message.middleware(MetricsMiddleware())
|
||||
private_router.message.middleware(MetricsMiddleware())
|
||||
callback_router.callback_query.middleware(MetricsMiddleware())
|
||||
group_router.message.middleware(MetricsMiddleware())
|
||||
# Создаем экземпляр VoiceHandlers
|
||||
voice_handlers = VoiceHandlers(bdf, bdf.settings)
|
||||
voice_router = voice_handlers.router
|
||||
|
||||
# Middleware уже добавлены на уровне dispatcher
|
||||
dp.include_routers(admin_router, private_router, callback_router, group_router, voice_router)
|
||||
|
||||
# Добавляем обработчик завершения для корректного закрытия
|
||||
@dp.shutdown()
|
||||
async def on_shutdown():
|
||||
logging.info("Bot shutdown initiated, cleaning up resources...")
|
||||
try:
|
||||
await bot.session.close()
|
||||
logging.info("Bot session closed successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Error closing bot session during shutdown: {e}")
|
||||
|
||||
dp.include_routers(admin_router, private_router, callback_router, group_router)
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
await dp.start_polling(bot, skip_updates=True)
|
||||
|
||||
# Запускаем HTTP сервер для метрик параллельно с ботом
|
||||
metrics_host = bdf.settings.get('Metrics', {}).get('host', '0.0.0.0')
|
||||
metrics_port = bdf.settings.get('Metrics', {}).get('port', 8080)
|
||||
|
||||
try:
|
||||
# Запускаем метрики сервер
|
||||
await start_metrics_server(metrics_host, metrics_port)
|
||||
|
||||
logging.info(f"✅ Метрики сервер запущен на {metrics_host}:{metrics_port}")
|
||||
logging.info("✅ Метрики будут обновляться в реальном времени через middleware")
|
||||
|
||||
# Запускаем бота с retry логикой
|
||||
await start_bot_with_retry(bot, dp)
|
||||
|
||||
logging.info("✅ Бот запущен")
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка запуска метрик сервера: {e}")
|
||||
# Продолжаем работу бота даже если метрики не запустились
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in bot startup: {e}")
|
||||
raise
|
||||
finally:
|
||||
# Останавливаем метрики сервер при завершении
|
||||
try:
|
||||
await stop_metrics_server()
|
||||
except Exception as e:
|
||||
logging.error(f"Error stopping metrics server: {e}")
|
||||
|
||||
# Закрываем сессию бота
|
||||
try:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logging.error(f"Error closing bot session: {e}")
|
||||
|
||||
return bot
|
||||
|
||||
@@ -1,61 +1,82 @@
|
||||
import asyncio
|
||||
from typing import Any, Dict, Union
|
||||
from typing import Any, Dict, Union, List
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Message
|
||||
|
||||
|
||||
class AlbumMiddleware(BaseMiddleware):
|
||||
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 = {}
|
||||
"""
|
||||
Middleware для обработки медиа групп в Telegram.
|
||||
Собирает все сообщения одной медиа группы и передает их как album в data.
|
||||
"""
|
||||
|
||||
#
|
||||
def collect_album_messages(self, event: Message):
|
||||
def __init__(self, latency: Union[int, float] = 0.01):
|
||||
"""
|
||||
Collect messages of the same media group.
|
||||
Инициализация middleware.
|
||||
|
||||
Args:
|
||||
latency: Задержка в секундах для сбора всех сообщений медиа группы
|
||||
"""
|
||||
# # Check if media_group_id exists in album_data
|
||||
super().__init__()
|
||||
self.latency = latency
|
||||
self.album_data: Dict[str, Dict[str, List[Message]]] = {}
|
||||
|
||||
def collect_album_messages(self, event: Message) -> int:
|
||||
"""
|
||||
Собирает сообщения одной медиа группы.
|
||||
|
||||
Args:
|
||||
event: Сообщение для обработки
|
||||
|
||||
Returns:
|
||||
Количество сообщений в текущей медиа группе
|
||||
"""
|
||||
if not event.media_group_id:
|
||||
return 0
|
||||
|
||||
if event.media_group_id not in self.album_data:
|
||||
# # Create a new entry for the media group
|
||||
self.album_data[event.media_group_id] = {"messages": []}
|
||||
#
|
||||
# # Append the new message to the media group
|
||||
|
||||
self.album_data[event.media_group_id]["messages"].append(event)
|
||||
#
|
||||
# # Return the total number of messages in the current media group
|
||||
return len(self.album_data[event.media_group_id]["messages"])
|
||||
|
||||
#
|
||||
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Main middleware logic.
|
||||
Основная логика middleware.
|
||||
|
||||
Args:
|
||||
handler: Обработчик события
|
||||
event: Событие (сообщение)
|
||||
data: Данные для передачи в обработчик
|
||||
|
||||
Returns:
|
||||
Результат выполнения обработчика
|
||||
"""
|
||||
# # If the event has no media_group_id, pass it to the handler immediately
|
||||
# Если у события нет media_group_id, передаем его обработчику сразу
|
||||
if not event.media_group_id:
|
||||
return await handler(event, data)
|
||||
#
|
||||
# # Collect messages of the same media group
|
||||
|
||||
# Собираем сообщения одной медиа группы
|
||||
total_before = self.collect_album_messages(event)
|
||||
#
|
||||
# # Wait for a specified latency period
|
||||
|
||||
# Ждем указанный период для сбора всех сообщений
|
||||
await asyncio.sleep(self.latency)
|
||||
#
|
||||
# # Check the total number of messages after the latency
|
||||
|
||||
# Проверяем количество сообщений после задержки
|
||||
total_after = len(self.album_data[event.media_group_id]["messages"])
|
||||
#
|
||||
# # If new messages were added during the latency, exit
|
||||
|
||||
# Если за время задержки добавились новые сообщения, выходим
|
||||
if total_before != total_after:
|
||||
return
|
||||
#
|
||||
# # Sort the album messages by message_id and add to data
|
||||
|
||||
# Сортируем сообщения по message_id и добавляем в data
|
||||
album_messages = self.album_data[event.media_group_id]["messages"]
|
||||
album_messages.sort(key=lambda x: x.message_id)
|
||||
data["album"] = album_messages
|
||||
#
|
||||
# # Remove the media group from tracking to free up memory
|
||||
|
||||
# Удаляем медиа группу из отслеживания для освобождения памяти
|
||||
del self.album_data[event.media_group_id]
|
||||
# # Call the original event handler
|
||||
|
||||
# Вызываем оригинальный обработчик события
|
||||
return await handler(event, data)
|
||||
#
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Dict, Any
|
||||
import html
|
||||
from datetime import datetime
|
||||
|
||||
from aiogram import BaseMiddleware, types
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||
@@ -26,12 +27,21 @@ class BlacklistMiddleware(BaseMiddleware):
|
||||
logger.info(f'Вызов BlacklistMiddleware для пользователя {user.username}')
|
||||
|
||||
# Используем асинхронную версию для предотвращения блокировки
|
||||
if await BotDB.check_user_in_blacklist_async(user_id=user.id):
|
||||
if await BotDB.check_user_in_blacklist(user.id):
|
||||
logger.info(f'BlacklistMiddleware результат для пользователя: {user.username} заблокирован!')
|
||||
user_info = await BotDB.get_blacklist_users_by_id_async(user.id)
|
||||
user_info = await BotDB.get_blacklist_users_by_id(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 "Не указана"
|
||||
reason = html.escape(str(user_info[1])) if user_info and user_info[1] else "Не указана"
|
||||
|
||||
# Преобразуем timestamp в человекочитаемый формат
|
||||
if user_info and user_info[2]:
|
||||
try:
|
||||
timestamp = int(user_info[2])
|
||||
date_unban = datetime.fromtimestamp(timestamp).strftime("%d-%m-%Y %H:%M")
|
||||
except (ValueError, TypeError):
|
||||
date_unban = "Не указана"
|
||||
else:
|
||||
date_unban = "Не указана"
|
||||
|
||||
# Отправляем сообщение в зависимости от типа события
|
||||
if isinstance(event, Message):
|
||||
|
||||
@@ -1,69 +1,97 @@
|
||||
"""
|
||||
Metrics middleware for aiogram 3.x.
|
||||
Automatically collects metrics for message processing, command execution, and errors.
|
||||
Enhanced Metrics middleware for aiogram 3.x.
|
||||
Automatically collects ALL available metrics for comprehensive monitoring.
|
||||
"""
|
||||
|
||||
from typing import Any, Awaitable, Callable, Dict
|
||||
from typing import Any, Awaitable, Callable, Dict, Union, Optional
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||
from aiogram.enums import ChatType
|
||||
import time
|
||||
import logging
|
||||
import asyncio
|
||||
from ..utils.metrics import metrics
|
||||
|
||||
# Import button command mapping
|
||||
try:
|
||||
from ..handlers.private.constants import BUTTON_COMMAND_MAPPING
|
||||
from ..handlers.callback.constants import CALLBACK_COMMAND_MAPPING
|
||||
from ..handlers.admin.constants import ADMIN_BUTTON_COMMAND_MAPPING, ADMIN_COMMANDS
|
||||
from ..handlers.voice.constants import (
|
||||
BUTTON_COMMAND_MAPPING as VOICE_BUTTON_COMMAND_MAPPING,
|
||||
COMMAND_MAPPING as VOICE_COMMAND_MAPPING,
|
||||
CALLBACK_COMMAND_MAPPING as VOICE_CALLBACK_COMMAND_MAPPING
|
||||
)
|
||||
except ImportError:
|
||||
# Fallback if constants not available
|
||||
BUTTON_COMMAND_MAPPING = {}
|
||||
CALLBACK_COMMAND_MAPPING = {}
|
||||
ADMIN_BUTTON_COMMAND_MAPPING = {}
|
||||
ADMIN_COMMANDS = {}
|
||||
VOICE_BUTTON_COMMAND_MAPPING = {}
|
||||
VOICE_COMMAND_MAPPING = {}
|
||||
VOICE_CALLBACK_COMMAND_MAPPING = {}
|
||||
|
||||
|
||||
class MetricsMiddleware(BaseMiddleware):
|
||||
"""Middleware for automatic metrics collection in aiogram handlers."""
|
||||
"""Enhanced middleware for automatic collection of ALL available metrics."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Metrics update intervals
|
||||
self.last_active_users_update = 0
|
||||
self.active_users_update_interval = 300 # 5 minutes
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""Process event and collect metrics."""
|
||||
"""Process event and collect comprehensive metrics."""
|
||||
|
||||
# Добавляем логирование для диагностики
|
||||
self.logger.info(f"📊 MetricsMiddleware called for event type: {type(event).__name__}")
|
||||
# Update active users periodically
|
||||
current_time = time.time()
|
||||
if current_time - self.last_active_users_update > self.active_users_update_interval:
|
||||
await self._update_active_users_metric()
|
||||
self.last_active_users_update = current_time
|
||||
|
||||
# Extract command info before execution
|
||||
# Extract command and event info
|
||||
command_info = None
|
||||
if isinstance(event, Message):
|
||||
self.logger.info(f"📊 Processing Message event")
|
||||
await self._record_message_metrics(event)
|
||||
if event.text and event.text.startswith('/'):
|
||||
command_info = {
|
||||
'command': event.text.split()[0][1:], # Remove '/' and get command name
|
||||
'user_type': "user" if event.from_user else "unknown",
|
||||
'handler_type': "message_handler"
|
||||
}
|
||||
elif isinstance(event, CallbackQuery):
|
||||
self.logger.info(f"📊 Processing CallbackQuery event")
|
||||
await self._record_callback_metrics(event)
|
||||
if event.data:
|
||||
parts = event.data.split(':', 1)
|
||||
if parts:
|
||||
command_info = {
|
||||
'command': parts[0],
|
||||
'user_type': "user" if event.from_user else "unknown",
|
||||
'handler_type': "callback_handler"
|
||||
}
|
||||
else:
|
||||
self.logger.info(f"📊 Processing unknown event type: {type(event).__name__}")
|
||||
event_metrics = {}
|
||||
|
||||
# Execute handler with timing
|
||||
# Process event based on type
|
||||
if hasattr(event, 'message') and event.message:
|
||||
event_metrics = await self._record_comprehensive_message_metrics(event.message)
|
||||
command_info = self._extract_command_info_with_fallback(event.message)
|
||||
elif hasattr(event, 'callback_query') and event.callback_query:
|
||||
event_metrics = await self._record_comprehensive_callback_metrics(event.callback_query)
|
||||
command_info = self._extract_callback_command_info_with_fallback(event.callback_query)
|
||||
elif isinstance(event, Message):
|
||||
event_metrics = await self._record_comprehensive_message_metrics(event)
|
||||
command_info = self._extract_command_info_with_fallback(event)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
event_metrics = await self._record_comprehensive_callback_metrics(event)
|
||||
command_info = self._extract_callback_command_info_with_fallback(event)
|
||||
else:
|
||||
event_metrics = await self._record_unknown_event_metrics(event)
|
||||
|
||||
if command_info:
|
||||
self.logger.info(f"📊 Command info extracted: {command_info}")
|
||||
else:
|
||||
self.logger.warning(f"📊 No command info extracted for event type: {type(event).__name__}")
|
||||
|
||||
# Execute handler with comprehensive timing and metrics
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = await handler(event, data)
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Record successful execution
|
||||
# Record successful execution metrics
|
||||
handler_name = self._get_handler_name(handler)
|
||||
self.logger.info(f"📊 Recording successful execution: {handler_name}")
|
||||
|
||||
metrics.record_method_duration(
|
||||
handler_name,
|
||||
duration,
|
||||
@@ -71,7 +99,6 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
"success"
|
||||
)
|
||||
|
||||
# Record command with success status if applicable
|
||||
if command_info:
|
||||
metrics.record_command(
|
||||
command_info['command'],
|
||||
@@ -80,27 +107,30 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
"success"
|
||||
)
|
||||
|
||||
await self._record_additional_success_metrics(event, event_metrics, handler_name)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Record error and timing
|
||||
# Record error metrics
|
||||
handler_name = self._get_handler_name(handler)
|
||||
self.logger.error(f"📊 Recording error execution: {handler_name}, error: {type(e).__name__}")
|
||||
error_type = type(e).__name__
|
||||
|
||||
metrics.record_method_duration(
|
||||
handler_name,
|
||||
duration,
|
||||
"handler",
|
||||
"error"
|
||||
)
|
||||
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
error_type,
|
||||
"handler",
|
||||
handler_name
|
||||
)
|
||||
|
||||
# Record command with error status if applicable
|
||||
if command_info:
|
||||
metrics.record_command(
|
||||
command_info['command'],
|
||||
@@ -109,32 +139,46 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
"error"
|
||||
)
|
||||
|
||||
await self._record_additional_error_metrics(event, event_metrics, handler_name, error_type)
|
||||
|
||||
raise
|
||||
finally:
|
||||
# Record middleware execution time
|
||||
middleware_duration = time.time() - start_time
|
||||
metrics.record_middleware("MetricsMiddleware", middleware_duration, "success")
|
||||
|
||||
def _get_handler_name(self, handler: Callable) -> str:
|
||||
"""Extract handler name efficiently."""
|
||||
# Проверяем различные способы получения имени хендлера
|
||||
if hasattr(handler, '__name__') and handler.__name__ != '<lambda>':
|
||||
return handler.__name__
|
||||
elif hasattr(handler, '__qualname__') and handler.__qualname__ != '<lambda>':
|
||||
return handler.__qualname__
|
||||
elif hasattr(handler, 'callback') and hasattr(handler.callback, '__name__'):
|
||||
return handler.callback.__name__
|
||||
elif hasattr(handler, 'view') and hasattr(handler.view, '__name__'):
|
||||
return handler.view.__name__
|
||||
else:
|
||||
# Пытаемся получить имя из строкового представления
|
||||
handler_str = str(handler)
|
||||
if 'function' in handler_str:
|
||||
# Извлекаем имя функции из строки
|
||||
import re
|
||||
match = re.search(r'function\s+(\w+)', handler_str)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "unknown"
|
||||
async def _update_active_users_metric(self):
|
||||
"""Periodically update active users metric from database."""
|
||||
try:
|
||||
#TODO: Должна подключаться к базе данных, а не к глобальному экземпляру
|
||||
from ..utils.base_dependency_factory import get_global_instance
|
||||
bdf = get_global_instance()
|
||||
bot_db = bdf.get_db()
|
||||
|
||||
async def _record_message_metrics(self, message: Message):
|
||||
"""Record message metrics efficiently."""
|
||||
# Используем правильные методы AsyncBotDB для выполнения запросов
|
||||
# Простой подсчет всех пользователей в базе
|
||||
total_users_query = "SELECT COUNT(DISTINCT user_id) as total FROM our_users"
|
||||
total_users_result = await bot_db.fetch_one(total_users_query)
|
||||
total_users = total_users_result['total'] if total_users_result else 1
|
||||
|
||||
# Подсчет активных за день пользователей (date_changed - это Unix timestamp)
|
||||
daily_users_query = "SELECT COUNT(DISTINCT user_id) as daily FROM our_users WHERE date_changed > (strftime('%s', 'now', '-1 day'))"
|
||||
daily_users_result = await bot_db.fetch_one(daily_users_query)
|
||||
daily_users = daily_users_result['daily'] if daily_users_result else 1
|
||||
|
||||
# Устанавливаем метрики с правильными лейблами
|
||||
metrics.set_active_users(daily_users, "daily")
|
||||
metrics.set_total_users(total_users)
|
||||
self.logger.info(f"📊 Active users metric updated: {daily_users} (daily), {total_users} (total)")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ Failed to update users metric: {e}")
|
||||
# Устанавливаем 1 как fallback
|
||||
metrics.set_active_users(1, "daily")
|
||||
metrics.set_total_users(1)
|
||||
|
||||
async def _record_comprehensive_message_metrics(self, message: Message) -> Dict[str, Any]:
|
||||
"""Record comprehensive message metrics."""
|
||||
# Determine message type
|
||||
message_type = "text"
|
||||
if message.photo:
|
||||
@@ -164,13 +208,200 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
# Record message processing
|
||||
metrics.record_message(message_type, chat_type, "message_handler")
|
||||
|
||||
async def _record_callback_metrics(self, callback: CallbackQuery):
|
||||
"""Record callback metrics efficiently."""
|
||||
return {
|
||||
'message_type': message_type,
|
||||
'chat_type': chat_type,
|
||||
'user_id': message.from_user.id if message.from_user else None,
|
||||
'is_bot': message.from_user.is_bot if message.from_user else False
|
||||
}
|
||||
|
||||
async def _record_comprehensive_callback_metrics(self, callback: CallbackQuery) -> Dict[str, Any]:
|
||||
"""Record comprehensive callback metrics."""
|
||||
# Record callback message
|
||||
metrics.record_message("callback_query", "callback", "callback_handler")
|
||||
|
||||
return {
|
||||
'callback_data': callback.data,
|
||||
'user_id': callback.from_user.id if callback.from_user else None,
|
||||
'is_bot': callback.from_user.is_bot if callback.from_user else False
|
||||
}
|
||||
|
||||
async def _record_unknown_event_metrics(self, event: TelegramObject) -> Dict[str, Any]:
|
||||
"""Record metrics for unknown event types."""
|
||||
# Record unknown event
|
||||
metrics.record_message("unknown", "unknown", "unknown_handler")
|
||||
|
||||
return {
|
||||
'event_type': type(event).__name__,
|
||||
'event_data': str(event)[:100] if hasattr(event, '__str__') else "unknown"
|
||||
}
|
||||
|
||||
def _extract_command_info_with_fallback(self, message: Message) -> Optional[Dict[str, str]]:
|
||||
"""Extract command information with fallback for unknown commands."""
|
||||
if not message.text:
|
||||
return None
|
||||
|
||||
# Check if it's a slash command
|
||||
if message.text.startswith('/'):
|
||||
command_name = message.text.split()[0][1:] # Remove '/' and get command name
|
||||
|
||||
# Check if it's an admin command
|
||||
if command_name in ADMIN_COMMANDS:
|
||||
return {
|
||||
'command': ADMIN_COMMANDS[command_name],
|
||||
'user_type': "admin" if message.from_user else "unknown",
|
||||
'handler_type': "admin_handler"
|
||||
}
|
||||
# Check if it's a voice bot command
|
||||
elif command_name in VOICE_COMMAND_MAPPING:
|
||||
return {
|
||||
'command': VOICE_COMMAND_MAPPING[command_name],
|
||||
'user_type': "user" if message.from_user else "unknown",
|
||||
'handler_type': "voice_command_handler"
|
||||
}
|
||||
else:
|
||||
# FALLBACK: Record unknown command
|
||||
return {
|
||||
'command': command_name,
|
||||
'user_type': "user" if message.from_user else "unknown",
|
||||
'handler_type': "unknown_command_handler"
|
||||
}
|
||||
|
||||
# Check if it's an admin button click
|
||||
if message.text in ADMIN_BUTTON_COMMAND_MAPPING:
|
||||
return {
|
||||
'command': ADMIN_BUTTON_COMMAND_MAPPING[message.text],
|
||||
'user_type': "admin" if message.from_user else "unknown",
|
||||
'handler_type': "admin_button_handler"
|
||||
}
|
||||
|
||||
# Check if it's a regular button click (text button)
|
||||
if message.text in BUTTON_COMMAND_MAPPING:
|
||||
return {
|
||||
'command': BUTTON_COMMAND_MAPPING[message.text],
|
||||
'user_type': "user" if message.from_user else "unknown",
|
||||
'handler_type': "button_handler"
|
||||
}
|
||||
|
||||
# Check if it's a voice bot button click
|
||||
if message.text in VOICE_BUTTON_COMMAND_MAPPING:
|
||||
return {
|
||||
'command': VOICE_BUTTON_COMMAND_MAPPING[message.text],
|
||||
'user_type': "user" if message.from_user else "unknown",
|
||||
'handler_type': "voice_button_handler"
|
||||
}
|
||||
|
||||
# FALLBACK: Record ANY text message as a command for metrics
|
||||
if message.text and len(message.text.strip()) > 0:
|
||||
return {
|
||||
'command': f"text",
|
||||
'user_type': "user" if message.from_user else "unknown",
|
||||
'handler_type': "text_message_handler"
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _extract_callback_command_info_with_fallback(self, callback: CallbackQuery) -> Optional[Dict[str, str]]:
|
||||
"""Extract callback command information with fallback."""
|
||||
if not callback.data:
|
||||
return None
|
||||
|
||||
# Extract command from callback data
|
||||
parts = callback.data.split(':', 1)
|
||||
if parts and parts[0] in CALLBACK_COMMAND_MAPPING:
|
||||
return {
|
||||
'command': CALLBACK_COMMAND_MAPPING[parts[0]],
|
||||
'user_type': "user" if callback.from_user else "unknown",
|
||||
'handler_type': "callback_handler"
|
||||
}
|
||||
|
||||
# Check if it's a voice bot callback
|
||||
if parts and parts[0] in VOICE_CALLBACK_COMMAND_MAPPING:
|
||||
return {
|
||||
'command': VOICE_CALLBACK_COMMAND_MAPPING[parts[0]],
|
||||
'user_type': "user" if callback.from_user else "unknown",
|
||||
'handler_type': "voice_callback_handler"
|
||||
}
|
||||
|
||||
# FALLBACK: Record unknown callback
|
||||
if parts:
|
||||
callback_data = parts[0]
|
||||
|
||||
# Группируем похожие callback'и по паттернам
|
||||
if callback_data.startswith("ban_") and callback_data[4:].isdigit():
|
||||
# callback_ban_123456 -> callback_ban
|
||||
command = "callback_ban"
|
||||
elif callback_data.startswith("page_") and callback_data[5:].isdigit():
|
||||
# callback_page_2 -> callback_page
|
||||
command = "callback_page"
|
||||
else:
|
||||
# Для остальных неизвестных callback'ов оставляем как есть
|
||||
command = f"callback_{callback_data[:20]}"
|
||||
|
||||
return {
|
||||
'command': command,
|
||||
'user_type': "user" if callback.from_user else "unknown",
|
||||
'handler_type': "unknown_callback_handler"
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
async def _record_additional_success_metrics(self, event: TelegramObject, event_metrics: Dict[str, Any], handler_name: str):
|
||||
"""Record additional success metrics."""
|
||||
try:
|
||||
# Record rate limiting metrics (if applicable)
|
||||
if hasattr(event, 'from_user') and event.from_user:
|
||||
# You can add rate limiting logic here
|
||||
pass
|
||||
|
||||
# Record user activity metrics
|
||||
if event_metrics.get('user_id'):
|
||||
# This could trigger additional user activity tracking
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ Error recording additional success metrics: {e}")
|
||||
|
||||
async def _record_additional_error_metrics(self, event: TelegramObject, event_metrics: Dict[str, Any], handler_name: str, error_type: str):
|
||||
"""Record additional error metrics."""
|
||||
try:
|
||||
# Record specific error context
|
||||
if event_metrics.get('user_id'):
|
||||
# You can add user-specific error tracking here
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ Error recording additional error metrics: {e}")
|
||||
|
||||
def _get_handler_name(self, handler: Callable) -> str:
|
||||
"""Extract handler name efficiently."""
|
||||
# Check various ways to get handler name
|
||||
if hasattr(handler, '__name__') and handler.__name__ != '<lambda>':
|
||||
return handler.__name__
|
||||
elif hasattr(handler, '__qualname__') and handler.__qualname__ != '<lambda>':
|
||||
return handler.__qualname__
|
||||
elif hasattr(handler, 'callback') and hasattr(handler.callback, '__name__'):
|
||||
return handler.callback.__name__
|
||||
elif hasattr(handler, 'view') and hasattr(handler.view, '__name__'):
|
||||
return handler.view.__name__
|
||||
else:
|
||||
# Пытаемся получить имя из строкового представления
|
||||
handler_str = str(handler)
|
||||
if 'function' in handler_str:
|
||||
# Извлекаем имя функции из строки
|
||||
import re
|
||||
match = re.search(r'function\s+(\w+)', handler_str)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "unknown"
|
||||
|
||||
|
||||
class DatabaseMetricsMiddleware(BaseMiddleware):
|
||||
"""Middleware for database operation metrics."""
|
||||
"""Enhanced middleware for database operation metrics."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -183,14 +414,36 @@ class DatabaseMetricsMiddleware(BaseMiddleware):
|
||||
# Check if this handler involves database operations
|
||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||
|
||||
# You can add specific database operation detection logic here
|
||||
# For now, we'll just pass through and let individual decorators handle it
|
||||
# Record middleware start
|
||||
start_time = time.time()
|
||||
|
||||
return await handler(event, data)
|
||||
try:
|
||||
result = await handler(event, data)
|
||||
|
||||
# Record successful database operation
|
||||
duration = time.time() - start_time
|
||||
metrics.record_middleware("DatabaseMetricsMiddleware", duration, "success")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Record failed database operation
|
||||
duration = time.time() - start_time
|
||||
metrics.record_middleware("DatabaseMetricsMiddleware", duration, "error")
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"database_middleware",
|
||||
handler_name
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
class ErrorMetricsMiddleware(BaseMiddleware):
|
||||
"""Middleware for error tracking and metrics."""
|
||||
"""Enhanced middleware for error tracking and metrics."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -200,14 +453,28 @@ class ErrorMetricsMiddleware(BaseMiddleware):
|
||||
) -> Any:
|
||||
"""Process event and collect error metrics."""
|
||||
|
||||
# Record middleware start
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
return await handler(event, data)
|
||||
result = await handler(event, data)
|
||||
|
||||
# Record successful error handling
|
||||
duration = time.time() - start_time
|
||||
metrics.record_middleware("ErrorMetricsMiddleware", duration, "success")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Record error metrics
|
||||
duration = time.time() - start_time
|
||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||
|
||||
metrics.record_middleware("ErrorMetricsMiddleware", duration, "error")
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"handler",
|
||||
"error_middleware",
|
||||
handler_name
|
||||
)
|
||||
|
||||
raise
|
||||
|
||||
124
helper_bot/scripts/monitor_bot.sh
Executable file
124
helper_bot/scripts/monitor_bot.sh
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script for monitoring and auto-restarting the Telegram bot
|
||||
# Usage: ./monitor_bot.sh
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
BOT_CONTAINER="telegram-helper-bot"
|
||||
HEALTH_ENDPOINT="http://localhost:8080/health"
|
||||
CHECK_INTERVAL=60 # seconds
|
||||
MAX_FAILURES=3
|
||||
LOG_FILE="logs/bot_monitor.log"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Check if container is running
|
||||
check_container_running() {
|
||||
if docker ps --format "table {{.Names}}" | grep -q "^${BOT_CONTAINER}$"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check health endpoint
|
||||
check_health() {
|
||||
if curl -f --connect-timeout 5 --max-time 10 "$HEALTH_ENDPOINT" >/dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Restart container
|
||||
restart_container() {
|
||||
log "${YELLOW}Restarting container ${BOT_CONTAINER}...${NC}"
|
||||
|
||||
if docker restart "$BOT_CONTAINER" >/dev/null 2>&1; then
|
||||
log "${GREEN}Container restarted successfully${NC}"
|
||||
|
||||
# Wait for container to be ready
|
||||
log "Waiting for container to be ready..."
|
||||
sleep 30
|
||||
|
||||
# Check if container is healthy
|
||||
local attempts=0
|
||||
while [ $attempts -lt 10 ]; do
|
||||
if check_health; then
|
||||
log "${GREEN}Container is healthy after restart${NC}"
|
||||
return 0
|
||||
fi
|
||||
attempts=$((attempts + 1))
|
||||
sleep 10
|
||||
done
|
||||
|
||||
log "${RED}Container failed to become healthy after restart${NC}"
|
||||
return 1
|
||||
else
|
||||
log "${RED}Failed to restart container${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main monitoring loop
|
||||
main() {
|
||||
log "${GREEN}Starting bot monitoring...${NC}"
|
||||
log "Container: $BOT_CONTAINER"
|
||||
log "Health endpoint: $HEALTH_ENDPOINT"
|
||||
log "Check interval: ${CHECK_INTERVAL}s"
|
||||
log "Max failures: $MAX_FAILURES"
|
||||
|
||||
local failure_count=0
|
||||
|
||||
while true; do
|
||||
# Check if container is running
|
||||
if ! check_container_running; then
|
||||
log "${RED}Container $BOT_CONTAINER is not running!${NC}"
|
||||
if restart_container; then
|
||||
failure_count=0
|
||||
else
|
||||
failure_count=$((failure_count + 1))
|
||||
fi
|
||||
else
|
||||
# Check health endpoint
|
||||
if check_health; then
|
||||
if [ $failure_count -gt 0 ]; then
|
||||
log "${GREEN}Container recovered, resetting failure count${NC}"
|
||||
failure_count=0
|
||||
fi
|
||||
log "${GREEN}Container is healthy${NC}"
|
||||
else
|
||||
failure_count=$((failure_count + 1))
|
||||
log "${YELLOW}Health check failed (${failure_count}/${MAX_FAILURES})${NC}"
|
||||
|
||||
if [ $failure_count -ge $MAX_FAILURES ]; then
|
||||
log "${RED}Max failures reached, restarting container${NC}"
|
||||
if restart_container; then
|
||||
failure_count=0
|
||||
else
|
||||
log "${RED}Failed to restart container after max failures${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep "$CHECK_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
# Handle script interruption
|
||||
trap 'log "Monitoring stopped by user"; exit 0' INT TERM
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -1,623 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import psutil
|
||||
import time
|
||||
import platform
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Optional, Tuple
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServerMonitor:
|
||||
def __init__(self, bot, group_for_logs: str, important_logs: str):
|
||||
self.bot = bot
|
||||
self.group_for_logs = group_for_logs
|
||||
self.important_logs = important_logs
|
||||
|
||||
# Определяем ОС
|
||||
self.os_type = self._detect_os()
|
||||
logger.info(f"Обнаружена ОС: {self.os_type}")
|
||||
|
||||
# Пороговые значения для алертов
|
||||
self.threshold = 80.0
|
||||
self.recovery_threshold = 75.0
|
||||
|
||||
# Состояние алертов для предотвращения спама
|
||||
self.alert_states = {
|
||||
'cpu': False,
|
||||
'ram': False,
|
||||
'disk': False
|
||||
}
|
||||
|
||||
# PID файлы для отслеживания процессов
|
||||
self.pid_files = {
|
||||
'voice_bot': 'voice_bot.pid',
|
||||
'helper_bot': 'helper_bot.pid'
|
||||
}
|
||||
|
||||
# Время последней отправки статуса
|
||||
self.last_status_time = None
|
||||
|
||||
# Для расчета скорости диска
|
||||
self.last_disk_io = None
|
||||
self.last_disk_io_time = None
|
||||
|
||||
# Время запуска бота для расчета uptime
|
||||
self.bot_start_time = time.time()
|
||||
|
||||
def _detect_os(self) -> str:
|
||||
"""Определение типа операционной системы"""
|
||||
system = platform.system().lower()
|
||||
if system == "darwin":
|
||||
return "macos"
|
||||
elif system == "linux":
|
||||
return "ubuntu"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
def _get_disk_path(self) -> str:
|
||||
"""Получение пути к диску в зависимости от ОС"""
|
||||
if self.os_type == "macos":
|
||||
return "/"
|
||||
elif self.os_type == "ubuntu":
|
||||
return "/"
|
||||
else:
|
||||
return "/"
|
||||
|
||||
def _get_disk_usage(self) -> Optional[object]:
|
||||
"""Получение информации о диске с учетом ОС"""
|
||||
try:
|
||||
if self.os_type == "macos":
|
||||
# На macOS используем diskutil для получения реального использования диска
|
||||
return self._get_macos_disk_usage()
|
||||
else:
|
||||
disk_path = self._get_disk_path()
|
||||
return psutil.disk_usage(disk_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении информации о диске: {e}")
|
||||
return None
|
||||
|
||||
def _get_macos_disk_usage(self) -> Optional[object]:
|
||||
"""Получение информации о диске на macOS через diskutil"""
|
||||
try:
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
# Получаем информацию о диске через diskutil
|
||||
result = subprocess.run(['diskutil', 'info', '/'], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
# Fallback к psutil
|
||||
return psutil.disk_usage('/')
|
||||
|
||||
output = result.stdout
|
||||
|
||||
# Извлекаем размеры из вывода diskutil
|
||||
total_match = re.search(r'Container Total Space:\s+(\d+\.\d+)\s+GB', output)
|
||||
free_match = re.search(r'Container Free Space:\s+(\d+\.\d+)\s+GB', output)
|
||||
|
||||
if total_match and free_match:
|
||||
total_gb = float(total_match.group(1))
|
||||
free_gb = float(free_match.group(1))
|
||||
used_gb = total_gb - free_gb
|
||||
|
||||
# Создаем объект, похожий на результат psutil.disk_usage
|
||||
class DiskUsage:
|
||||
def __init__(self, total, used, free):
|
||||
self.total = total * (1024**3) # Конвертируем в байты
|
||||
self.used = used * (1024**3)
|
||||
self.free = free * (1024**3)
|
||||
|
||||
return DiskUsage(total_gb, used_gb, free_gb)
|
||||
else:
|
||||
# Fallback к psutil
|
||||
return psutil.disk_usage('/')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении информации о диске macOS: {e}")
|
||||
# Fallback к psutil
|
||||
return psutil.disk_usage('/')
|
||||
|
||||
def _get_disk_io_counters(self):
|
||||
"""Получение статистики диска с учетом ОС"""
|
||||
try:
|
||||
if self.os_type == "macos":
|
||||
# На macOS может быть несколько дисков, берем основной
|
||||
return psutil.disk_io_counters(perdisk=False)
|
||||
elif self.os_type == "ubuntu":
|
||||
# На Ubuntu обычно один диск
|
||||
return psutil.disk_io_counters(perdisk=False)
|
||||
else:
|
||||
return psutil.disk_io_counters()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики диска: {e}")
|
||||
return None
|
||||
|
||||
def _get_system_uptime(self) -> float:
|
||||
"""Получение uptime системы с учетом ОС"""
|
||||
try:
|
||||
if self.os_type == "macos":
|
||||
# На macOS используем boot_time
|
||||
boot_time = psutil.boot_time()
|
||||
return time.time() - boot_time
|
||||
elif self.os_type == "ubuntu":
|
||||
# На Ubuntu также используем boot_time
|
||||
boot_time = psutil.boot_time()
|
||||
return time.time() - boot_time
|
||||
else:
|
||||
boot_time = psutil.boot_time()
|
||||
return time.time() - boot_time
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении uptime системы: {e}")
|
||||
return 0.0
|
||||
|
||||
def get_bot_uptime(self) -> str:
|
||||
"""Получение uptime бота"""
|
||||
uptime_seconds = time.time() - self.bot_start_time
|
||||
return self._format_uptime(uptime_seconds)
|
||||
|
||||
def get_system_info(self) -> Dict:
|
||||
"""Получение информации о системе"""
|
||||
try:
|
||||
# CPU
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
load_avg = psutil.getloadavg()
|
||||
cpu_count = psutil.cpu_count()
|
||||
|
||||
# Память
|
||||
memory = psutil.virtual_memory()
|
||||
swap = psutil.swap_memory()
|
||||
|
||||
# Используем единый расчет для всех ОС: used / total для получения процента занятой памяти
|
||||
# Это обеспечивает консистентность между macOS и Ubuntu
|
||||
ram_percent = (memory.used / memory.total) * 100
|
||||
|
||||
# Диск
|
||||
disk = self._get_disk_usage()
|
||||
disk_io = self._get_disk_io_counters()
|
||||
|
||||
if disk is None:
|
||||
logger.error("Не удалось получить информацию о диске")
|
||||
return {}
|
||||
|
||||
# Расчет скорости диска
|
||||
disk_read_speed, disk_write_speed = self._calculate_disk_speed(disk_io)
|
||||
|
||||
# Система
|
||||
system_uptime = self._get_system_uptime()
|
||||
|
||||
# Получаем имя хоста в зависимости от ОС
|
||||
if self.os_type == "macos":
|
||||
hostname = os.uname().nodename
|
||||
elif self.os_type == "ubuntu":
|
||||
hostname = os.uname().nodename
|
||||
else:
|
||||
hostname = "unknown"
|
||||
|
||||
return {
|
||||
'cpu_percent': cpu_percent,
|
||||
'load_avg_1m': round(load_avg[0], 2),
|
||||
'load_avg_5m': round(load_avg[1], 2),
|
||||
'load_avg_15m': round(load_avg[2], 2),
|
||||
'cpu_count': cpu_count,
|
||||
'ram_used': round(memory.used / (1024**3), 2),
|
||||
'ram_total': round(memory.total / (1024**3), 2),
|
||||
'ram_percent': round(ram_percent, 1), # Исправленный процент занятой памяти
|
||||
'swap_used': round(swap.used / (1024**3), 2),
|
||||
'swap_total': round(swap.total / (1024**3), 2),
|
||||
'swap_percent': swap.percent,
|
||||
'disk_used': round(disk.used / (1024**3), 2),
|
||||
'disk_total': round(disk.total / (1024**3), 2),
|
||||
'disk_percent': round((disk.used / disk.total) * 100, 1),
|
||||
'disk_free': round(disk.free / (1024**3), 2),
|
||||
'disk_read_speed': disk_read_speed,
|
||||
'disk_write_speed': disk_write_speed,
|
||||
'disk_io_percent': self._calculate_disk_io_percent(),
|
||||
'system_uptime': self._format_uptime(system_uptime),
|
||||
'bot_uptime': self.get_bot_uptime(),
|
||||
'server_hostname': hostname,
|
||||
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении информации о системе: {e}")
|
||||
return {}
|
||||
|
||||
def _get_disk_space_emoji(self, disk_percent: float) -> str:
|
||||
"""Получение эмодзи для дискового пространства"""
|
||||
if disk_percent < 60:
|
||||
return "🟢"
|
||||
elif disk_percent < 90:
|
||||
return "⚠️"
|
||||
else:
|
||||
return "🚨"
|
||||
|
||||
def _format_bytes(self, bytes_value: int) -> str:
|
||||
"""Форматирование байтов в человекочитаемый вид"""
|
||||
if bytes_value == 0:
|
||||
return "0 B"
|
||||
|
||||
size_names = ["B", "KB", "MB", "GB", "TB"]
|
||||
i = 0
|
||||
while bytes_value >= 1024 and i < len(size_names) - 1:
|
||||
bytes_value /= 1024.0
|
||||
i += 1
|
||||
|
||||
return f"{bytes_value:.1f} {size_names[i]}"
|
||||
|
||||
def _format_uptime(self, seconds: float) -> str:
|
||||
"""Форматирование времени работы системы"""
|
||||
days = int(seconds // 86400)
|
||||
hours = int((seconds % 86400) // 3600)
|
||||
minutes = int((seconds % 3600) // 60)
|
||||
|
||||
if days > 0:
|
||||
return f"{days}д {hours}ч {minutes}м"
|
||||
elif hours > 0:
|
||||
return f"{hours}ч {minutes}м"
|
||||
else:
|
||||
return f"{minutes}м"
|
||||
|
||||
def check_process_status(self, process_name: str) -> Tuple[str, str]:
|
||||
"""Проверка статуса процесса и возврат статуса с uptime"""
|
||||
try:
|
||||
# Сначала проверяем по PID файлу
|
||||
pid_file = self.pid_files.get(process_name)
|
||||
if pid_file and os.path.exists(pid_file):
|
||||
try:
|
||||
with open(pid_file, 'r') as f:
|
||||
content = f.read().strip()
|
||||
if content and content != '# Этот файл будет автоматически обновляться при запуске бота':
|
||||
pid = int(content)
|
||||
if psutil.pid_exists(pid):
|
||||
# Получаем uptime процесса
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
proc_uptime = time.time() - proc.create_time()
|
||||
uptime_str = self._format_uptime(proc_uptime)
|
||||
return "✅", f"Uptime {uptime_str}"
|
||||
except:
|
||||
return "✅", "Uptime неизвестно"
|
||||
except (ValueError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Проверяем по имени процесса более точно
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||
try:
|
||||
proc_name = proc.info['name'].lower()
|
||||
cmdline = ' '.join(proc.info['cmdline']).lower() if proc.info['cmdline'] else ''
|
||||
|
||||
# Более точная проверка для каждого бота
|
||||
if process_name == 'voice_bot':
|
||||
# Проверяем voice_bot
|
||||
if ('voice_bot' in proc_name or
|
||||
'voice_bot' in cmdline or
|
||||
'voice_bot_v2.py' in cmdline):
|
||||
# Получаем uptime процесса
|
||||
try:
|
||||
proc_uptime = time.time() - proc.create_time()
|
||||
uptime_str = self._format_uptime(proc_uptime)
|
||||
return "✅", f"Uptime {uptime_str}"
|
||||
except:
|
||||
return "✅", "Uptime неизвестно"
|
||||
elif process_name == 'helper_bot':
|
||||
# Проверяем helper_bot
|
||||
if ('helper_bot' in proc_name or
|
||||
'helper_bot' in cmdline or
|
||||
'run_helper.py' in cmdline or
|
||||
'python' in proc_name and 'helper_bot' in cmdline):
|
||||
# Получаем uptime процесса
|
||||
try:
|
||||
proc_uptime = time.time() - proc.create_time()
|
||||
uptime_str = self._format_uptime(proc_uptime)
|
||||
return "✅", f"Uptime {uptime_str}"
|
||||
except:
|
||||
return "✅", "Uptime неизвестно"
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
return "❌", "Выключен"
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке процесса {process_name}: {e}")
|
||||
return "❌", "Выключен"
|
||||
|
||||
def should_send_status(self) -> bool:
|
||||
"""Проверка, нужно ли отправить статус (каждые 30 минут в 00 и 30 минут часа)"""
|
||||
now = datetime.now()
|
||||
|
||||
# Проверяем, что сейчас 00 или 30 минут часа
|
||||
if now.minute in [0, 30]:
|
||||
# Проверяем, не отправляли ли мы уже статус в эту минуту
|
||||
if (self.last_status_time is None or
|
||||
self.last_status_time.hour != now.hour or
|
||||
self.last_status_time.minute != now.minute):
|
||||
self.last_status_time = now
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _calculate_disk_speed(self, current_disk_io) -> Tuple[str, str]:
|
||||
"""Расчет скорости чтения/записи диска"""
|
||||
current_time = time.time()
|
||||
|
||||
if self.last_disk_io is None or self.last_disk_io_time is None:
|
||||
self.last_disk_io = current_disk_io
|
||||
self.last_disk_io_time = current_time
|
||||
return "0 B/s", "0 B/s"
|
||||
|
||||
time_diff = current_time - self.last_disk_io_time
|
||||
if time_diff < 1: # Минимальный интервал 1 секунда
|
||||
return "0 B/s", "0 B/s"
|
||||
|
||||
read_diff = current_disk_io.read_bytes - self.last_disk_io.read_bytes
|
||||
write_diff = current_disk_io.write_bytes - self.last_disk_io.write_bytes
|
||||
|
||||
read_speed = read_diff / time_diff
|
||||
write_speed = write_diff / time_diff
|
||||
|
||||
# Обновляем предыдущие значения
|
||||
self.last_disk_io = current_disk_io
|
||||
self.last_disk_io_time = current_time
|
||||
|
||||
return self._format_bytes(read_speed) + "/s", self._format_bytes(write_speed) + "/s"
|
||||
|
||||
def _calculate_disk_io_percent(self) -> int:
|
||||
"""Расчет процента загрузки диска на основе IOPS"""
|
||||
try:
|
||||
# Получаем статистику диска
|
||||
disk_io = self._get_disk_io_counters()
|
||||
if disk_io is None:
|
||||
return 0
|
||||
|
||||
# Простая эвристика: считаем общее количество операций
|
||||
total_ops = disk_io.read_count + disk_io.write_count
|
||||
|
||||
# Нормализуем к проценту (это приблизительная оценка)
|
||||
# На macOS обычно нормальная нагрузка до 1000-5000 операций в секунду
|
||||
if total_ops < 1000:
|
||||
return 10
|
||||
elif total_ops < 5000:
|
||||
return 30
|
||||
elif total_ops < 10000:
|
||||
return 50
|
||||
elif total_ops < 20000:
|
||||
return 70
|
||||
else:
|
||||
return 90
|
||||
except:
|
||||
return 0
|
||||
|
||||
def should_send_startup_status(self) -> bool:
|
||||
"""Проверка, нужно ли отправить статус при запуске"""
|
||||
return self.last_status_time is None
|
||||
|
||||
async def send_startup_message(self):
|
||||
"""Отправка сообщения о запуске бота"""
|
||||
try:
|
||||
message = f"""🚀 **Бот запущен!**
|
||||
---------------------------------
|
||||
**Время запуска:** <code>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</code>
|
||||
**Сервер:** `{psutil.os.uname().nodename}`
|
||||
**Система:** {psutil.os.uname().sysname} {psutil.os.uname().release}
|
||||
**ОС:** {self.os_type.upper()}
|
||||
|
||||
✅ Мониторинг сервера активирован
|
||||
✅ Статус будет отправляться каждые 30 минут (в 00 и 30 минут часа)
|
||||
✅ Алерты будут отправляться при превышении пороговых значений
|
||||
---------------------------------"""
|
||||
|
||||
await self.bot.send_message(
|
||||
chat_id=self.important_logs,
|
||||
text=message,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.info("Сообщение о запуске бота отправлено")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке сообщения о запуске: {e}")
|
||||
|
||||
async def send_shutdown_message(self):
|
||||
"""Отправка сообщения об отключении бота"""
|
||||
try:
|
||||
# Получаем финальную информацию о системе
|
||||
system_info = self.get_system_info()
|
||||
if not system_info:
|
||||
system_info = {
|
||||
'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'server_hostname': psutil.os.uname().nodename
|
||||
}
|
||||
|
||||
message = f"""🛑 **Бот отключен!**
|
||||
---------------------------------
|
||||
**Время отключения:** <code>{system_info['current_time']}</code>
|
||||
**Сервер:** `{system_info['server_hostname']}`
|
||||
|
||||
❌ Мониторинг сервера остановлен
|
||||
❌ Статус больше не будет отправляться
|
||||
❌ Алерты отключены
|
||||
|
||||
⚠️ **Внимание:** Проверьте состояние сервера!
|
||||
---------------------------------"""
|
||||
|
||||
await self.bot.send_message(
|
||||
chat_id=self.important_logs,
|
||||
text=message,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.info("Сообщение об отключении бота отправлено")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке сообщения об отключении: {e}")
|
||||
|
||||
def check_alerts(self, system_info: Dict) -> Tuple[bool, Optional[str]]:
|
||||
"""Проверка необходимости отправки алертов"""
|
||||
alerts = []
|
||||
|
||||
# Проверка CPU
|
||||
if system_info['cpu_percent'] > self.threshold and not self.alert_states['cpu']:
|
||||
self.alert_states['cpu'] = True
|
||||
alerts.append(('cpu', system_info['cpu_percent'], f"Нагрузка за 1 мин: {system_info['load_avg_1m']}"))
|
||||
|
||||
# Проверка RAM
|
||||
if system_info['ram_percent'] > self.threshold and not self.alert_states['ram']:
|
||||
self.alert_states['ram'] = True
|
||||
alerts.append(('ram', system_info['ram_percent'], f"Используется: {system_info['ram_used']} GB из {system_info['ram_total']} GB"))
|
||||
|
||||
# Проверка диска
|
||||
if system_info['disk_percent'] > self.threshold and not self.alert_states['disk']:
|
||||
self.alert_states['disk'] = True
|
||||
alerts.append(('disk', system_info['disk_percent'], f"Свободно: {system_info['disk_free']} GB на /"))
|
||||
|
||||
# Проверка восстановления
|
||||
recoveries = []
|
||||
if system_info['cpu_percent'] < self.recovery_threshold and self.alert_states['cpu']:
|
||||
self.alert_states['cpu'] = False
|
||||
recoveries.append(('cpu', system_info['cpu_percent']))
|
||||
|
||||
if system_info['ram_percent'] < self.recovery_threshold and self.alert_states['ram']:
|
||||
self.alert_states['ram'] = False
|
||||
recoveries.append(('ram', system_info['ram_percent']))
|
||||
|
||||
if system_info['disk_percent'] < self.recovery_threshold and self.alert_states['disk']:
|
||||
self.alert_states['disk'] = False
|
||||
recoveries.append(('disk', system_info['disk_percent']))
|
||||
|
||||
return alerts, recoveries
|
||||
|
||||
async def send_status_message(self, system_info: Dict):
|
||||
"""Отправка сообщения со статусом сервера"""
|
||||
try:
|
||||
voice_bot_status, voice_bot_uptime = self.check_process_status('voice_bot')
|
||||
helper_bot_status, helper_bot_uptime = self.check_process_status('helper_bot')
|
||||
|
||||
# Получаем эмодзи для дискового пространства
|
||||
disk_emoji = self._get_disk_space_emoji(system_info['disk_percent'])
|
||||
|
||||
message = f"""🖥 **Статус Сервера** | <code>{system_info['current_time']}</code>
|
||||
---------------------------------
|
||||
**📊 Общая нагрузка:**
|
||||
CPU: <b>{system_info['cpu_percent']}%</b> | LA: <b>{system_info['load_avg_1m']} / {system_info['cpu_count']}</b> | IO Wait: <b>{system_info['disk_percent']}%</b>
|
||||
|
||||
**💾 Память:**
|
||||
RAM: <b>{system_info['ram_used']}/{system_info['ram_total']} GB</b> ({system_info['ram_percent']}%)
|
||||
Swap: <b>{system_info['swap_used']}/{system_info['swap_total']} GB</b> ({system_info['swap_percent']}%)
|
||||
|
||||
**🗂️ Дисковое пространство:**
|
||||
Диск (/): <b>{system_info['disk_used']}/{system_info['disk_total']} GB</b> ({system_info['disk_percent']}%) {disk_emoji}
|
||||
|
||||
**💿 Диск I/O:**
|
||||
Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_write_speed']}</b>
|
||||
Диск загружен: <b>{system_info['disk_io_percent']}%</b>
|
||||
|
||||
**🤖 Процессы:**
|
||||
{voice_bot_status} voice-bot - {voice_bot_uptime}
|
||||
{helper_bot_status} helper-bot - {helper_bot_uptime}
|
||||
---------------------------------
|
||||
⏰ Uptime сервера: {system_info['system_uptime']}"""
|
||||
|
||||
await self.bot.send_message(
|
||||
chat_id=self.group_for_logs,
|
||||
text=message,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.info("Статус сервера отправлен")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке статуса сервера: {e}")
|
||||
|
||||
async def send_alert_message(self, metric_name: str, current_value: float, details: str):
|
||||
"""Отправка сообщения об алерте"""
|
||||
try:
|
||||
message = f"""🚨 **ALERT: Высокая нагрузка на сервере!**
|
||||
---------------------------------
|
||||
**Показатель:** {metric_name}
|
||||
**Текущее значение:** <b>{current_value}%</b> ⚠️
|
||||
**Пороговое значение:** 80%
|
||||
|
||||
**Детали:**
|
||||
{details}
|
||||
|
||||
**Сервер:** `{psutil.os.uname().nodename}`
|
||||
**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
|
||||
---------------------------------"""
|
||||
|
||||
await self.bot.send_message(
|
||||
chat_id=self.important_logs,
|
||||
text=message,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.warning(f"Алерт отправлен: {metric_name} - {current_value}%")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке алерта: {e}")
|
||||
|
||||
async def send_recovery_message(self, metric_name: str, current_value: float, peak_value: float):
|
||||
"""Отправка сообщения о восстановлении"""
|
||||
try:
|
||||
message = f"""✅ **RECOVERY: Нагрузка нормализовалась**
|
||||
---------------------------------
|
||||
**Показатель:** {metric_name}
|
||||
**Текущее значение:** <b>{current_value}%</b> ✔️
|
||||
**Было превышение:** До {peak_value}%
|
||||
|
||||
**Сервер:** `{psutil.os.uname().nodename}`
|
||||
**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
|
||||
---------------------------------"""
|
||||
|
||||
await self.bot.send_message(
|
||||
chat_id=self.important_logs,
|
||||
text=message,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
logger.info(f"Сообщение о восстановлении отправлено: {metric_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке сообщения о восстановлении: {e}")
|
||||
|
||||
async def monitor_loop(self):
|
||||
"""Основной цикл мониторинга"""
|
||||
logger.info(f"Модуль мониторинга сервера запущен на {self.os_type.upper()}")
|
||||
|
||||
# Отправляем сообщение о запуске при первом запуске
|
||||
if self.should_send_startup_status():
|
||||
await self.send_startup_message()
|
||||
|
||||
while True:
|
||||
try:
|
||||
system_info = self.get_system_info()
|
||||
if not system_info:
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
|
||||
# Проверка алертов
|
||||
alerts, recoveries = self.check_alerts(system_info)
|
||||
|
||||
# Отправка алертов
|
||||
for metric_type, value, details in alerts:
|
||||
metric_names = {
|
||||
'cpu': 'Использование CPU',
|
||||
'ram': 'Использование оперативной памяти',
|
||||
'disk': 'Заполнение диска (/)'
|
||||
}
|
||||
await self.send_alert_message(metric_names[metric_type], value, details)
|
||||
|
||||
# Отправка сообщений о восстановлении
|
||||
for metric_type, value in recoveries:
|
||||
metric_names = {
|
||||
'cpu': 'Использование CPU',
|
||||
'ram': 'Использование оперативной памяти',
|
||||
'disk': 'Заполнение диска (/)'
|
||||
}
|
||||
# Находим пиковое значение (используем 80% как пример)
|
||||
await self.send_recovery_message(metric_names[metric_type], value, 80.0)
|
||||
|
||||
# Отправка статуса каждые 30 минут в 00 и 30 минут часа
|
||||
if self.should_send_status():
|
||||
await self.send_status_message(system_info)
|
||||
|
||||
# Пауза между проверками (1 минута)
|
||||
await asyncio.sleep(60)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в цикле мониторинга: {e}")
|
||||
await asyncio.sleep(60)
|
||||
261
helper_bot/server_prometheus.py
Normal file
261
helper_bot/server_prometheus.py
Normal file
@@ -0,0 +1,261 @@
|
||||
|
||||
"""
|
||||
HTTP server for metrics endpoint integration with centralized Prometheus monitoring.
|
||||
Provides /metrics endpoint and health check for the bot.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
from typing import Optional
|
||||
from .utils.metrics import metrics
|
||||
|
||||
# Импортируем логгер из проекта
|
||||
try:
|
||||
from logs.custom_logger import logger
|
||||
except ImportError:
|
||||
# Fallback для случаев, когда custom_logger недоступен
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetricsServer:
|
||||
"""HTTP server for Prometheus metrics and health checks."""
|
||||
|
||||
def __init__(self, host: str = '0.0.0.0', port: int = 8080):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = web.Application()
|
||||
self.runner: Optional[web.AppRunner] = None
|
||||
self.site: Optional[web.TCPSite] = None
|
||||
|
||||
# Настраиваем роуты
|
||||
self.app.router.add_get('/metrics', self.metrics_handler)
|
||||
self.app.router.add_get('/health', self.health_handler)
|
||||
self.app.router.add_get('/status', self.status_handler)
|
||||
|
||||
async def metrics_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /metrics endpoint for Prometheus scraping."""
|
||||
try:
|
||||
logger.debug("Generating metrics...")
|
||||
|
||||
# Проверяем, что metrics доступен
|
||||
if not metrics:
|
||||
logger.error("Metrics object is not available")
|
||||
return web.Response(
|
||||
text="Metrics not available",
|
||||
status=500
|
||||
)
|
||||
|
||||
# Генерируем метрики в формате Prometheus
|
||||
metrics_data = metrics.get_metrics()
|
||||
logger.debug(f"Generated metrics: {len(metrics_data)} bytes")
|
||||
|
||||
return web.Response(
|
||||
body=metrics_data,
|
||||
content_type='text/plain; version=0.0.4'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating metrics: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return web.Response(
|
||||
text=f"Error generating metrics: {e}",
|
||||
status=500
|
||||
)
|
||||
|
||||
async def health_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /health endpoint for health checks."""
|
||||
try:
|
||||
# Проверяем доступность метрик
|
||||
if not metrics:
|
||||
return web.Response(
|
||||
text="ERROR: Metrics not available",
|
||||
content_type='text/plain',
|
||||
status=503
|
||||
)
|
||||
|
||||
# Проверяем, что можем получить метрики
|
||||
try:
|
||||
metrics_data = metrics.get_metrics()
|
||||
if not metrics_data:
|
||||
return web.Response(
|
||||
text="ERROR: Empty metrics",
|
||||
content_type='text/plain',
|
||||
status=503
|
||||
)
|
||||
except Exception as e:
|
||||
return web.Response(
|
||||
text=f"ERROR: Metrics generation failed: {e}",
|
||||
content_type='text/plain',
|
||||
status=503
|
||||
)
|
||||
|
||||
return web.Response(
|
||||
text="OK",
|
||||
content_type='text/plain',
|
||||
status=200
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {e}")
|
||||
return web.Response(
|
||||
text=f"ERROR: Health check failed: {e}",
|
||||
content_type='text/plain',
|
||||
status=500
|
||||
)
|
||||
|
||||
async def status_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /status endpoint for process status information."""
|
||||
try:
|
||||
import os
|
||||
import time
|
||||
import psutil
|
||||
|
||||
# Получаем PID текущего процесса
|
||||
current_pid = os.getpid()
|
||||
|
||||
try:
|
||||
# Получаем информацию о процессе
|
||||
process = psutil.Process(current_pid)
|
||||
create_time = process.create_time()
|
||||
uptime_seconds = time.time() - create_time
|
||||
|
||||
# Логируем для диагностики
|
||||
import datetime
|
||||
create_time_str = datetime.datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||
current_time_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
logger.info(f"Process PID {current_pid}: created at {create_time_str}, current time {current_time_str}, uptime {uptime_seconds:.1f}s")
|
||||
|
||||
# Форматируем uptime
|
||||
if uptime_seconds < 60:
|
||||
uptime_str = f"{int(uptime_seconds)}с"
|
||||
elif uptime_seconds < 3600:
|
||||
minutes = int(uptime_seconds // 60)
|
||||
uptime_str = f"{minutes}м"
|
||||
elif uptime_seconds < 86400:
|
||||
hours = int(uptime_seconds // 3600)
|
||||
minutes = int((uptime_seconds % 3600) // 60)
|
||||
uptime_str = f"{hours}ч {minutes}м"
|
||||
else:
|
||||
days = int(uptime_seconds // 86400)
|
||||
hours = int((uptime_seconds % 86400) // 3600)
|
||||
uptime_str = f"{days}д {hours}ч"
|
||||
|
||||
# Проверяем, что процесс активен
|
||||
if process.is_running():
|
||||
status = "running"
|
||||
else:
|
||||
status = "stopped"
|
||||
|
||||
# Формируем ответ
|
||||
response_data = {
|
||||
"status": status,
|
||||
"pid": current_pid,
|
||||
"uptime": uptime_str,
|
||||
"memory_usage_mb": round(process.memory_info().rss / 1024 / 1024, 2),
|
||||
"cpu_percent": process.cpu_percent(),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
import json
|
||||
return web.Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=200
|
||||
)
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
# Процесс не найден
|
||||
response_data = {
|
||||
"status": "not_found",
|
||||
"error": "Process not found",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
import json
|
||||
return web.Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=404
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Status check failed: {e}")
|
||||
import json
|
||||
response_data = {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
return web.Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=500
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the HTTP server."""
|
||||
try:
|
||||
self.runner = web.AppRunner(self.app)
|
||||
await self.runner.setup()
|
||||
|
||||
self.site = web.TCPSite(self.runner, self.host, self.port)
|
||||
await self.site.start()
|
||||
|
||||
logger.info(f"Metrics server started on {self.host}:{self.port}")
|
||||
logger.info("Available endpoints:")
|
||||
logger.info(f" - /metrics - Prometheus metrics")
|
||||
logger.info(f" - /health - Health check")
|
||||
logger.info(f" - /status - Process status")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start metrics server: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the HTTP server."""
|
||||
try:
|
||||
if self.site:
|
||||
await self.site.stop()
|
||||
logger.info("Metrics server site stopped")
|
||||
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
logger.info("Metrics server runner cleaned up")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping metrics server: {e}")
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit."""
|
||||
await self.stop()
|
||||
|
||||
|
||||
# Глобальный экземпляр сервера для использования в main.py
|
||||
metrics_server: Optional[MetricsServer] = None
|
||||
|
||||
|
||||
async def start_metrics_server(host: str = '0.0.0.0', port: int = 8080) -> MetricsServer:
|
||||
"""Start metrics server and return instance."""
|
||||
global metrics_server
|
||||
metrics_server = MetricsServer(host, port)
|
||||
await metrics_server.start()
|
||||
return metrics_server
|
||||
|
||||
|
||||
async def stop_metrics_server() -> None:
|
||||
"""Stop metrics server if running."""
|
||||
global metrics_server
|
||||
if metrics_server:
|
||||
try:
|
||||
await metrics_server.stop()
|
||||
logger.info("Metrics server stopped successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping metrics server: {e}")
|
||||
finally:
|
||||
metrics_server = None
|
||||
@@ -34,14 +34,13 @@ class AutoUnbanScheduler:
|
||||
try:
|
||||
logger.info("Запуск автоматического разбана пользователей")
|
||||
|
||||
# Получаем сегодняшнюю дату в формате YYYY-MM-DD
|
||||
moscow_tz = timezone(timedelta(hours=3)) # UTC+3 для Москвы
|
||||
today = datetime.now(moscow_tz).strftime("%Y-%m-%d")
|
||||
# Получаем текущий UNIX timestamp
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
|
||||
logger.info(f"Поиск пользователей для разблокировки на дату: {today}")
|
||||
logger.info(f"Поиск пользователей для разблокировки на timestamp: {current_timestamp}")
|
||||
|
||||
# Получаем список пользователей для разблокировки
|
||||
users_to_unban = self.bot_db.get_users_for_unblock_today(today)
|
||||
users_to_unban = await self.bot_db.get_users_for_unblock_today(current_timestamp)
|
||||
|
||||
if not users_to_unban:
|
||||
logger.info("Нет пользователей для разблокировки сегодня")
|
||||
@@ -55,20 +54,20 @@ class AutoUnbanScheduler:
|
||||
failed_users = []
|
||||
|
||||
# Разблокируем каждого пользователя
|
||||
for user_id, username in users_to_unban.items():
|
||||
for user_id in users_to_unban:
|
||||
try:
|
||||
result = self.bot_db.delete_user_blacklist(user_id)
|
||||
result = await self.bot_db.delete_user_blacklist(user_id)
|
||||
if result:
|
||||
success_count += 1
|
||||
logger.info(f"Пользователь {user_id} ({username}) успешно разблокирован")
|
||||
logger.info(f"Пользователь {user_id} успешно разблокирован")
|
||||
else:
|
||||
failed_count += 1
|
||||
failed_users.append(f"{user_id} ({username})")
|
||||
logger.error(f"Ошибка при разблокировке пользователя {user_id} ({username})")
|
||||
failed_users.append(f"{user_id}")
|
||||
logger.error(f"Ошибка при разблокировке пользователя {user_id}")
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
failed_users.append(f"{user_id} ({username})")
|
||||
logger.error(f"Исключение при разблокировке пользователя {user_id} ({username}): {e}")
|
||||
failed_users.append(f"{user_id}")
|
||||
logger.error(f"Исключение при разблокировке пользователя {user_id}: {e}")
|
||||
|
||||
# Формируем отчет
|
||||
report = self._generate_report(success_count, failed_count, failed_users, users_to_unban)
|
||||
@@ -93,10 +92,9 @@ class AutoUnbanScheduler:
|
||||
|
||||
if success_count > 0:
|
||||
report += "✅ <b>Разблокированные пользователи:</b>\n"
|
||||
for user_id, username in all_users.items():
|
||||
if f"{user_id} ({username})" not in failed_users:
|
||||
safe_username = username if username else "Неизвестный пользователь"
|
||||
report += f"• ID: {user_id}, Имя: {safe_username}\n"
|
||||
for user_id in all_users:
|
||||
if str(user_id) not in failed_users:
|
||||
report += f"• ID: {user_id}\n"
|
||||
report += "\n"
|
||||
|
||||
if failed_users:
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from database.db import BotDB
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
|
||||
class BaseDependencyFactory:
|
||||
@@ -18,10 +18,7 @@ class BaseDependencyFactory:
|
||||
if not os.path.isabs(database_path):
|
||||
database_path = os.path.join(project_dir, database_path)
|
||||
|
||||
database_dir = project_dir
|
||||
database_name = database_path.replace(project_dir + '/', '')
|
||||
|
||||
self.database = BotDB(database_dir, database_name)
|
||||
self.database = AsyncBotDB(database_path)
|
||||
|
||||
self._load_settings_from_env()
|
||||
|
||||
@@ -46,6 +43,11 @@ class BaseDependencyFactory:
|
||||
'test': self._parse_bool(os.getenv('TEST', 'false'))
|
||||
}
|
||||
|
||||
self.settings['Metrics'] = {
|
||||
'host': os.getenv('METRICS_HOST', '0.0.0.0'),
|
||||
'port': self._parse_int(os.getenv('METRICS_PORT', '8080'))
|
||||
}
|
||||
|
||||
def _parse_bool(self, value: str) -> bool:
|
||||
"""Парсит строковое значение в boolean."""
|
||||
return value.lower() in ('true', '1', 'yes', 'on')
|
||||
@@ -60,7 +62,7 @@ class BaseDependencyFactory:
|
||||
def get_settings(self):
|
||||
return self.settings
|
||||
|
||||
def get_db(self) -> BotDB:
|
||||
def get_db(self) -> AsyncBotDB:
|
||||
"""Возвращает подключение к базе данных."""
|
||||
return self.database
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
"""
|
||||
Configuration management for the Telegram bot.
|
||||
Supports both environment variables and .env files.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""Manages bot configuration with environment variable support."""
|
||||
|
||||
def __init__(self, env_file: str = ".env"):
|
||||
self.env_file = env_file
|
||||
self._load_env()
|
||||
|
||||
def _load_env(self):
|
||||
"""Load configuration from .env file if exists."""
|
||||
# Load from .env file if exists
|
||||
if os.path.exists(self.env_file):
|
||||
load_dotenv(self.env_file)
|
||||
|
||||
def get(self, section: str, key: str, default: Any = None) -> str:
|
||||
"""Get configuration value with environment variable override."""
|
||||
# Check environment variable first
|
||||
env_key = f"{section.upper()}_{key.upper()}"
|
||||
env_value = os.getenv(env_key)
|
||||
if env_value is not None:
|
||||
return env_value
|
||||
|
||||
# Fall back to direct environment variable
|
||||
direct_env_value = os.getenv(key.upper())
|
||||
if direct_env_value is not None:
|
||||
return direct_env_value
|
||||
|
||||
return default
|
||||
|
||||
def getboolean(self, section: str, key: str, default: bool = False) -> bool:
|
||||
"""Get boolean configuration value."""
|
||||
value = self.get(section, key, str(default))
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return value.lower() in ('true', '1', 'yes', 'on')
|
||||
|
||||
def getint(self, section: str, key: str, default: int = 0) -> int:
|
||||
"""Get integer configuration value."""
|
||||
value = self.get(section, key, str(default))
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def get_all_settings(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get all settings as dictionary."""
|
||||
settings = {}
|
||||
|
||||
# Telegram секция
|
||||
settings['Telegram'] = {
|
||||
'bot_token': self.get('Telegram', 'bot_token', ''),
|
||||
'listen_bot_token': self.get('Telegram', 'listen_bot_token', ''),
|
||||
'test_bot_token': self.get('Telegram', 'test_bot_token', ''),
|
||||
'preview_link': self.getboolean('Telegram', 'preview_link', False),
|
||||
'main_public': self.get('Telegram', 'main_public', ''),
|
||||
'group_for_posts': self.getint('Telegram', 'group_for_posts', 0),
|
||||
'group_for_message': self.getint('Telegram', 'group_for_message', 0),
|
||||
'group_for_logs': self.getint('Telegram', 'group_for_logs', 0),
|
||||
'important_logs': self.getint('Telegram', 'important_logs', 0),
|
||||
'archive': self.getint('Telegram', 'archive', 0),
|
||||
'test_group': self.getint('Telegram', 'test_group', 0)
|
||||
}
|
||||
|
||||
# Settings секция
|
||||
settings['Settings'] = {
|
||||
'logs': self.getboolean('Settings', 'logs', False),
|
||||
'test': self.getboolean('Settings', 'test', False)
|
||||
}
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
# Global config instance
|
||||
_config_instance: Optional[ConfigManager] = None
|
||||
|
||||
|
||||
def get_config() -> ConfigManager:
|
||||
"""Get global configuration instance."""
|
||||
global _config_instance
|
||||
if _config_instance is None:
|
||||
_config_instance = ConfigManager()
|
||||
return _config_instance
|
||||
@@ -1,19 +1,24 @@
|
||||
import html
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
from typing import List, Dict, Any, Optional
|
||||
from typing import List, Dict, Any, Optional, TYPE_CHECKING, Union
|
||||
|
||||
try:
|
||||
import emoji as _emoji_lib
|
||||
except Exception:
|
||||
_emoji_lib_available = True
|
||||
except ImportError:
|
||||
_emoji_lib = None
|
||||
_emoji_lib_available = False
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio
|
||||
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio, InputMediaDocument
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
||||
from logs.custom_logger import logger
|
||||
from database.models import TelegramPost
|
||||
|
||||
# Local imports - metrics
|
||||
from .metrics import (
|
||||
@@ -24,10 +29,11 @@ from .metrics import (
|
||||
)
|
||||
|
||||
bdf = get_global_instance()
|
||||
#TODO: поменять архитектуру и подключить правильный BotDB
|
||||
BotDB = bdf.get_db()
|
||||
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
||||
|
||||
if _emoji_lib is not None:
|
||||
if _emoji_lib_available and _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)
|
||||
@@ -110,44 +116,92 @@ def get_text_message(post_text: str, first_name: str, username: str = None):
|
||||
return f'Пост из ТГ:\n{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
|
||||
|
||||
async def download_file(message: types.Message, file_id: str):
|
||||
async def download_file(message: types.Message, file_id: str, content_type: str = None) -> Optional[str]:
|
||||
"""
|
||||
Скачивает файл по file_id из Telegram.
|
||||
Скачивает файл по file_id из Telegram и сохраняет в соответствующую папку.
|
||||
|
||||
Args:
|
||||
message: сообщение
|
||||
file_id: File ID фотографии
|
||||
filename: Имя файла, под которым будет сохранено фото
|
||||
file_id: File ID файла
|
||||
content_type: тип контента (photo, video, audio, voice, video_note)
|
||||
|
||||
Returns:
|
||||
Путь к сохраненному файлу, если файл был скачан успешно, иначе None
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
os.makedirs("files", exist_ok=True)
|
||||
os.makedirs("files/photos", exist_ok=True)
|
||||
os.makedirs("files/videos", exist_ok=True)
|
||||
os.makedirs("files/music", exist_ok=True)
|
||||
os.makedirs("files/voice", exist_ok=True)
|
||||
os.makedirs("files/video_notes", exist_ok=True)
|
||||
# Валидация параметров
|
||||
if not file_id or not message or not message.bot:
|
||||
logger.error("download_file: Неверные параметры - file_id, message или bot отсутствуют")
|
||||
return None
|
||||
|
||||
# Определяем папку по типу контента
|
||||
type_folders = {
|
||||
'photo': 'photos',
|
||||
'video': 'videos',
|
||||
'audio': 'music',
|
||||
'voice': 'voice',
|
||||
'video_note': 'video_notes'
|
||||
}
|
||||
|
||||
folder = type_folders.get(content_type, 'other')
|
||||
base_path = "files"
|
||||
full_folder_path = os.path.join(base_path, folder)
|
||||
|
||||
# Создаем необходимые папки
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
os.makedirs(full_folder_path, exist_ok=True)
|
||||
|
||||
logger.debug(f"download_file: Начинаю скачивание файла {file_id} типа {content_type} в папку {folder}")
|
||||
|
||||
# Получаем информацию о файле
|
||||
file = await message.bot.get_file(file_id)
|
||||
file_path = os.path.join("files", file.file_path)
|
||||
if not file or not file.file_path:
|
||||
logger.error(f"download_file: Не удалось получить информацию о файле {file_id}")
|
||||
return None
|
||||
|
||||
# Генерируем уникальное имя файла
|
||||
original_filename = os.path.basename(file.file_path)
|
||||
file_extension = os.path.splitext(original_filename)[1] or '.bin'
|
||||
safe_filename = f"{file_id}{file_extension}"
|
||||
file_path = os.path.join(full_folder_path, safe_filename)
|
||||
|
||||
# Скачиваем файл
|
||||
await message.bot.download_file(file_path=file.file_path, destination=file_path)
|
||||
|
||||
# Проверяем, что файл действительно скачался
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"download_file: Файл не был скачан - {file_path}")
|
||||
return None
|
||||
|
||||
file_size = os.path.getsize(file_path)
|
||||
download_time = time.time() - start_time
|
||||
|
||||
logger.info(f"download_file: Файл успешно скачан - {file_path}, размер: {file_size} байт, время: {download_time:.2f}с")
|
||||
|
||||
# Записываем метрики
|
||||
metrics.record_file_download(content_type or 'unknown', file_size, download_time)
|
||||
|
||||
return file_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка скачивания фотографии: {e}")
|
||||
download_time = time.time() - start_time
|
||||
logger.error(f"download_file: Ошибка скачивания файла {file_id}: {e}, время: {download_time:.2f}с")
|
||||
metrics.record_file_download_error(content_type or 'unknown', str(e))
|
||||
return None
|
||||
|
||||
|
||||
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
"""
|
||||
Создает MediaGroup.
|
||||
Создает MediaGroup согласно best practices aiogram 3.x.
|
||||
|
||||
Args:
|
||||
album: Album объект из Telegram API.
|
||||
post_caption: Текст подписи к первому фото.
|
||||
album: Album объект из Telegram API (список сообщений).
|
||||
post_caption: Текст подписи к первому медиа файлу.
|
||||
|
||||
Returns:
|
||||
Список InputMediaPhoto (MediaGroup).
|
||||
Список InputMedia объектов для MediaGroup.
|
||||
"""
|
||||
# Экранируем post_caption для безопасного использования в HTML
|
||||
safe_post_caption = html.escape(str(post_caption)) if post_caption else ""
|
||||
@@ -157,101 +211,253 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
for i, message in enumerate(album):
|
||||
if message.photo:
|
||||
file_id = message.photo[-1].file_id
|
||||
media_type = 'photo'
|
||||
# Для фото используем InputMediaPhoto
|
||||
if i == 0: # Первое фото получает подпись
|
||||
media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
media_group.append(InputMediaPhoto(media=file_id))
|
||||
elif message.video:
|
||||
file_id = message.video.file_id
|
||||
media_type = 'video'
|
||||
# Для видео используем InputMediaVideo
|
||||
if i == 0: # Первое видео получает подпись
|
||||
media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
media_group.append(InputMediaVideo(media=file_id))
|
||||
elif message.audio:
|
||||
file_id = message.audio.file_id
|
||||
media_type = 'audio'
|
||||
else:
|
||||
# Если нет фото, видео или аудио, пропускаем сообщение
|
||||
continue
|
||||
|
||||
# Формируем объект MediaGroup с учетом типа медиа
|
||||
if i == len(album) - 1:
|
||||
if media_type == 'photo':
|
||||
media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption))
|
||||
elif media_type == 'video':
|
||||
media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption))
|
||||
elif media_type == 'audio':
|
||||
# Для аудио используем InputMediaAudio
|
||||
if i == 0: # Первое аудио получает подпись
|
||||
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
if media_type == 'photo':
|
||||
media_group.append(InputMediaPhoto(media=file_id))
|
||||
elif media_type == 'video':
|
||||
media_group.append(InputMediaVideo(media=file_id))
|
||||
elif media_type == 'audio':
|
||||
media_group.append(InputMediaAudio(media=file_id))
|
||||
|
||||
return media_group # Возвращаем MediaGroup
|
||||
|
||||
|
||||
async def add_in_db_media_mediagroup(sent_message, bot_db):
|
||||
"""
|
||||
Идентификатор медиа-группы
|
||||
|
||||
Args:
|
||||
sent_message: sent_message объект из Telegram API
|
||||
bot_db: Экземпляр базы данных
|
||||
|
||||
Returns:
|
||||
Список InputFile (FSInputFile).
|
||||
"""
|
||||
media_group_message_id = sent_message[-1].message_id # Получаем идентификатор медиа-группы
|
||||
for i, message in enumerate(sent_message):
|
||||
if message.photo:
|
||||
file_id = message.photo[-1].file_id
|
||||
file_path = await download_file(message, file_id=file_id)
|
||||
bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo')
|
||||
elif message.video:
|
||||
file_id = message.video.file_id
|
||||
file_path = await download_file(message, file_id=file_id)
|
||||
bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video')
|
||||
elif message.document:
|
||||
file_id = message.document.file_id
|
||||
# Для документов используем InputMediaDocument (если поддерживается)
|
||||
if i == 0: # Первый документ получает подпись
|
||||
media_group.append(InputMediaDocument(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
|
||||
media_group.append(InputMediaDocument(media=file_id))
|
||||
else:
|
||||
# Если нет поддерживаемого медиа, пропускаем сообщение
|
||||
continue
|
||||
|
||||
return media_group
|
||||
|
||||
async def add_in_db_media(sent_message, bot_db):
|
||||
|
||||
async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db: Any, main_post_id: Optional[int] = None) -> bool:
|
||||
"""
|
||||
Добавляет контент медиа-группы в базу данных
|
||||
|
||||
Args:
|
||||
sent_message: sent_message объект из Telegram API
|
||||
bot_db: Экземпляр базы данных
|
||||
main_post_id: ID основного поста медиагруппы (если не указан, используется последний message_id)
|
||||
|
||||
Returns:
|
||||
bool: True если весь контент успешно добавлен, False в случае ошибки
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Валидация параметров
|
||||
if not sent_message or not bot_db or not isinstance(sent_message, list):
|
||||
logger.error("add_in_db_media_mediagroup: Неверные параметры - sent_message, bot_db или sent_message не является списком")
|
||||
return False
|
||||
|
||||
if len(sent_message) == 0:
|
||||
logger.warning("add_in_db_media_mediagroup: Пустая медиагруппа")
|
||||
return False
|
||||
|
||||
# Используем переданный main_post_id или ID последнего сообщения
|
||||
post_id = main_post_id or sent_message[-1].message_id
|
||||
logger.debug(f"add_in_db_media_mediagroup: Обрабатываю медиагруппу из {len(sent_message)} сообщений, post_id: {post_id}")
|
||||
|
||||
processed_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for i, message in enumerate(sent_message):
|
||||
try:
|
||||
content_type = None
|
||||
file_id = None
|
||||
|
||||
# Определяем тип контента и file_id
|
||||
if message.photo:
|
||||
content_type = 'photo'
|
||||
file_id = message.photo[-1].file_id
|
||||
elif message.video:
|
||||
content_type = 'video'
|
||||
file_id = message.video.file_id
|
||||
elif message.audio:
|
||||
content_type = 'audio'
|
||||
file_id = message.audio.file_id
|
||||
elif message.voice:
|
||||
content_type = 'voice'
|
||||
file_id = message.voice.file_id
|
||||
elif message.video_note:
|
||||
content_type = 'video_note'
|
||||
file_id = message.video_note.file_id
|
||||
else:
|
||||
logger.warning(f"add_in_db_media_mediagroup: Неподдерживаемый тип контента в сообщении {i+1}/{len(sent_message)}")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
if not file_id:
|
||||
logger.error(f"add_in_db_media_mediagroup: file_id отсутствует в сообщении {i+1}/{len(sent_message)}")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
logger.debug(f"add_in_db_media_mediagroup: Обрабатываю {content_type} в сообщении {i+1}/{len(sent_message)}")
|
||||
|
||||
# Скачиваем файл
|
||||
file_path = await download_file(message, file_id=file_id, content_type=content_type)
|
||||
if not file_path:
|
||||
logger.error(f"add_in_db_media_mediagroup: Не удалось скачать файл {file_id} в сообщении {i+1}/{len(sent_message)}")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
# Добавляем в базу данных
|
||||
success = await bot_db.add_post_content(post_id, message.message_id, file_path, content_type)
|
||||
if not success:
|
||||
logger.error(f"add_in_db_media_mediagroup: Не удалось добавить контент в БД для сообщения {i+1}/{len(sent_message)}")
|
||||
# Удаляем скачанный файл при ошибке БД
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.debug(f"add_in_db_media_mediagroup: Удален файл {file_path} после ошибки БД")
|
||||
except Exception as e:
|
||||
logger.warning(f"add_in_db_media_mediagroup: Не удалось удалить файл {file_path}: {e}")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
processed_count += 1
|
||||
logger.debug(f"add_in_db_media_mediagroup: Успешно обработано сообщение {i+1}/{len(sent_message)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"add_in_db_media_mediagroup: Ошибка обработки сообщения {i+1}/{len(sent_message)}: {e}")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
if processed_count == 0:
|
||||
logger.error(f"add_in_db_media_mediagroup: Не удалось обработать ни одного сообщения из медиагруппы {post_id}")
|
||||
metrics.record_media_processing('media_group', processing_time, False)
|
||||
return False
|
||||
|
||||
if failed_count > 0:
|
||||
logger.warning(f"add_in_db_media_mediagroup: Обработано {processed_count}/{len(sent_message)} сообщений медиагруппы {post_id}, ошибок: {failed_count}")
|
||||
else:
|
||||
logger.info(f"add_in_db_media_mediagroup: Успешно обработана медиагруппа {post_id} - {processed_count} сообщений, время: {processing_time:.2f}с")
|
||||
|
||||
# Записываем метрики
|
||||
metrics.record_media_processing('media_group', processing_time, failed_count == 0)
|
||||
|
||||
return failed_count == 0
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
logger.error(f"add_in_db_media_mediagroup: Критическая ошибка обработки медиагруппы: {e}, время: {processing_time:.2f}с")
|
||||
metrics.record_media_processing('media_group', processing_time, False)
|
||||
return False
|
||||
|
||||
|
||||
async def add_in_db_media(sent_message: types.Message, bot_db: Any) -> bool:
|
||||
"""
|
||||
Добавляет контент одиночного сообщения в базу данных
|
||||
|
||||
Args:
|
||||
sent_message: sent_message объект из Telegram API
|
||||
bot_db: Экземпляр базы данных
|
||||
|
||||
Returns:
|
||||
Список InputFile (FSInputFile).
|
||||
bool: True если контент успешно добавлен, False в случае ошибки
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Валидация параметров
|
||||
if not sent_message or not bot_db:
|
||||
logger.error("add_in_db_media: Неверные параметры - sent_message или bot_db отсутствуют")
|
||||
return False
|
||||
|
||||
post_id = sent_message.message_id # ID поста (это же сообщение)
|
||||
content_type = None
|
||||
file_id = None
|
||||
|
||||
# Определяем тип контента и file_id
|
||||
if sent_message.photo:
|
||||
content_type = 'photo'
|
||||
file_id = sent_message.photo[-1].file_id
|
||||
file_path = await download_file(sent_message, file_id=file_id)
|
||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo')
|
||||
elif sent_message.video:
|
||||
content_type = 'video'
|
||||
file_id = sent_message.video.file_id
|
||||
file_path = await download_file(sent_message, file_id=file_id)
|
||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video')
|
||||
elif sent_message.voice:
|
||||
content_type = 'voice'
|
||||
file_id = sent_message.voice.file_id
|
||||
file_path = await download_file(sent_message, file_id=file_id)
|
||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice')
|
||||
elif sent_message.audio:
|
||||
content_type = 'audio'
|
||||
file_id = sent_message.audio.file_id
|
||||
file_path = await download_file(sent_message, file_id=file_id)
|
||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio')
|
||||
elif sent_message.video_note:
|
||||
content_type = 'video_note'
|
||||
file_id = sent_message.video_note.file_id
|
||||
file_path = await download_file(sent_message, file_id=file_id)
|
||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note')
|
||||
else:
|
||||
logger.warning(f"add_in_db_media: Неподдерживаемый тип контента для сообщения {post_id}")
|
||||
return False
|
||||
|
||||
if not file_id:
|
||||
logger.error(f"add_in_db_media: file_id отсутствует для сообщения {post_id}")
|
||||
return False
|
||||
|
||||
logger.debug(f"add_in_db_media: Обрабатываю {content_type} для сообщения {post_id}")
|
||||
|
||||
# Скачиваем файл
|
||||
file_path = await download_file(sent_message, file_id=file_id, content_type=content_type)
|
||||
if not file_path:
|
||||
logger.error(f"add_in_db_media: Не удалось скачать файл {file_id} для сообщения {post_id}")
|
||||
return False
|
||||
|
||||
# Добавляем в базу данных
|
||||
success = await bot_db.add_post_content(post_id, sent_message.message_id, file_path, content_type)
|
||||
if not success:
|
||||
logger.error(f"add_in_db_media: Не удалось добавить контент в БД для сообщения {post_id}")
|
||||
# Удаляем скачанный файл при ошибке БД
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.debug(f"add_in_db_media: Удален файл {file_path} после ошибки БД")
|
||||
except Exception as e:
|
||||
logger.warning(f"add_in_db_media: Не удалось удалить файл {file_path}: {e}")
|
||||
return False
|
||||
|
||||
processing_time = time.time() - start_time
|
||||
logger.info(f"add_in_db_media: Контент успешно добавлен для сообщения {post_id}, тип: {content_type}, время: {processing_time:.2f}с")
|
||||
|
||||
# Записываем метрики
|
||||
metrics.record_media_processing(content_type, processing_time, True)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
processing_time = time.time() - start_time
|
||||
logger.error(f"add_in_db_media: Ошибка обработки медиа для сообщения {post_id}: {e}, время: {processing_time:.2f}с")
|
||||
metrics.record_media_processing(content_type or 'unknown', processing_time, False)
|
||||
return False
|
||||
|
||||
|
||||
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
|
||||
media_group: List, bot_db):
|
||||
media_group: List, bot_db: Any, main_post_id: Optional[int] = None) -> int:
|
||||
sent_message = await message.bot.send_media_group(
|
||||
chat_id=chat_id,
|
||||
media=media_group,
|
||||
)
|
||||
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)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message[-1].message_id,
|
||||
text=sent_message[-1].caption or "",
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await bot_db.add_post(post)
|
||||
success = await add_in_db_media_mediagroup(sent_message, bot_db, main_post_id)
|
||||
if not success:
|
||||
logger.warning(f"send_media_group_message_to_private_chat: Не удалось сохранить медиа для медиагруппы {sent_message[-1].message_id}")
|
||||
message_id = sent_message[-1].message_id
|
||||
return message_id
|
||||
|
||||
@@ -266,26 +472,43 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: List, pos
|
||||
post_content: Список кортежей с путями к файлам.
|
||||
post_text: Текст подписи.
|
||||
"""
|
||||
logger.info(f"Начинаю отправку медиа-группы в чат {chat_id}, количество файлов: {len(post_content)}")
|
||||
|
||||
media = []
|
||||
for file_path in post_content:
|
||||
for i, file_path in enumerate(post_content):
|
||||
try:
|
||||
file = FSInputFile(path=file_path[0])
|
||||
type = file_path[1]
|
||||
logger.debug(f"Обрабатываю файл {i+1}/{len(post_content)}: {file_path[0]} (тип: {type})")
|
||||
|
||||
if type == 'video':
|
||||
media.append(types.InputMediaVideo(media=file))
|
||||
if type == 'photo':
|
||||
elif type == 'photo':
|
||||
media.append(types.InputMediaPhoto(media=file))
|
||||
else:
|
||||
logger.warning(f"Неизвестный тип файла: {type} для {file_path[0]}")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Файл не найден: {file_path[0]}")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке файла {file_path[0]}: {e}")
|
||||
return
|
||||
|
||||
logger.info(f"Подготовлено {len(media)} медиа-файлов для отправки")
|
||||
|
||||
# Добавляем подпись к последнему файлу
|
||||
if media:
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
media[-1].caption = safe_post_text
|
||||
logger.debug(f"Добавлена подпись к последнему файлу: {safe_post_text[:50]}{'...' if len(safe_post_text) > 50 else ''}")
|
||||
|
||||
try:
|
||||
await bot.send_media_group(chat_id=chat_id, media=media)
|
||||
logger.info(f"Медиа-группа успешно отправлена в чат {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке медиа-группы в чат {chat_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def send_text_message(chat_id, message: types.Message, post_text: str, markup: types.ReplyKeyboardMarkup = None):
|
||||
@@ -404,20 +627,22 @@ async def send_voice_message(chat_id, message: types.Message, voice: str,
|
||||
return sent_message
|
||||
|
||||
|
||||
def check_access(user_id: int, bot_db):
|
||||
async def check_access(user_id: int, bot_db):
|
||||
"""Проверка прав на совершение действий"""
|
||||
return bot_db.is_admin(user_id)
|
||||
from logs.custom_logger import logger
|
||||
result = await bot_db.is_admin(user_id)
|
||||
logger.info(f"check_access: пользователь {user_id} - результат: {result}")
|
||||
return result
|
||||
|
||||
|
||||
def add_days_to_date(days: int):
|
||||
"""Прибавляет указанное количество дней к текущей дате и возвращает дату в формате DD-MM-YYYY."""
|
||||
"""Прибавляет указанное количество дней к текущей дате и возвращает UNIX timestamp."""
|
||||
current_date = datetime.now()
|
||||
future_date = current_date + timedelta(days=days)
|
||||
formatted_date = future_date.strftime("%d-%m-%Y")
|
||||
return formatted_date
|
||||
return int(future_date.timestamp())
|
||||
|
||||
|
||||
def get_banned_users_list(offset: int, bot_db):
|
||||
async def get_banned_users_list(offset: int, bot_db):
|
||||
"""
|
||||
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
|
||||
|
||||
@@ -429,22 +654,43 @@ def get_banned_users_list(offset: int, bot_db):
|
||||
message - текст сообщения
|
||||
user_ids - лист кортежей [(user_name: user_id)]
|
||||
"""
|
||||
users = bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
|
||||
users = await bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
|
||||
message = "Список заблокированных пользователей:\n"
|
||||
|
||||
for user in users:
|
||||
# Экранируем пользовательские данные для безопасного использования
|
||||
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 "Дата не указана"
|
||||
user_id, ban_reason, unban_date = user
|
||||
# Получаем имя пользователя из таблицы users
|
||||
username = await bot_db.get_username(user_id)
|
||||
full_name = await bot_db.get_full_name_by_id(user_id)
|
||||
safe_user_name = username or full_name or f"User_{user_id}"
|
||||
|
||||
message += f"Пользователь: {safe_user_name}\n"
|
||||
message += f"Причина бана: {safe_ban_reason}\n"
|
||||
message += f"Дата разбана: {safe_unban_date}\n\n"
|
||||
# Экранируем пользовательские данные для безопасного использования
|
||||
safe_user_name = html.escape(str(safe_user_name))
|
||||
safe_ban_reason = html.escape(str(ban_reason)) if ban_reason else "Причина не указана"
|
||||
|
||||
# Форматируем дату разбана в человекочитаемый формат
|
||||
if unban_date:
|
||||
try:
|
||||
# Предполагаем, что unban_date это UNIX timestamp
|
||||
if isinstance(unban_date, (int, float)):
|
||||
unban_datetime = datetime.fromtimestamp(unban_date)
|
||||
safe_unban_date = unban_datetime.strftime("%d-%m-%Y %H:%M")
|
||||
else:
|
||||
# Если это уже datetime объект
|
||||
safe_unban_date = unban_date.strftime("%d-%m-%Y %H:%M")
|
||||
except (ValueError, TypeError, OSError):
|
||||
# В случае ошибки показываем исходное значение
|
||||
safe_unban_date = html.escape(str(unban_date))
|
||||
else:
|
||||
safe_unban_date = "Дата не указана"
|
||||
|
||||
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(bot_db):
|
||||
async def get_banned_users_buttons(bot_db):
|
||||
"""
|
||||
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
|
||||
|
||||
@@ -455,42 +701,58 @@ def get_banned_users_buttons(bot_db):
|
||||
message - текст сообщения
|
||||
user_ids - лист кортежей [(user_name: user_id)]
|
||||
"""
|
||||
users = bot_db.get_banned_users_from_db()
|
||||
users = await bot_db.get_banned_users_from_db()
|
||||
user_ids = []
|
||||
|
||||
for user in users:
|
||||
user_id, ban_reason, unban_date = user
|
||||
# Получаем имя пользователя из таблицы users
|
||||
username = await bot_db.get_username(user_id)
|
||||
full_name = await bot_db.get_full_name_by_id(user_id)
|
||||
safe_user_name = username or full_name or f"User_{user_id}"
|
||||
|
||||
# Экранируем user_name для безопасного использования
|
||||
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
|
||||
user_ids.append((safe_user_name, user[1]))
|
||||
safe_user_name = html.escape(str(safe_user_name))
|
||||
user_ids.append((safe_user_name, user_id))
|
||||
return user_ids
|
||||
|
||||
|
||||
def delete_user_blacklist(user_id: int, bot_db):
|
||||
return bot_db.delete_user_blacklist(user_id=user_id)
|
||||
async def delete_user_blacklist(user_id: int, bot_db):
|
||||
return await bot_db.delete_user_blacklist(user_id=user_id)
|
||||
|
||||
|
||||
@track_time("check_username_and_full_name", "helper_func")
|
||||
@track_errors("helper_func", "check_username_and_full_name")
|
||||
@db_query_time("get_username_and_full_name", "users", "select")
|
||||
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)
|
||||
@db_query_time("check_username_and_full_name", "users", "select")
|
||||
async def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
|
||||
"""Проверяет, изменились ли username или full_name пользователя"""
|
||||
try:
|
||||
username_db = await bot_db.get_username(user_id)
|
||||
full_name_db = await bot_db.get_full_name_by_id(user_id)
|
||||
return username != username_db or full_name != full_name_db
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке username и full_name: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def unban_notifier(self):
|
||||
# Получение сегодняшней даты в формате DD-MM-YYYY
|
||||
async def unban_notifier(bot, BotDB, GROUP_FOR_MESSAGE):
|
||||
# Получение текущего UNIX timestamp
|
||||
current_date = datetime.now()
|
||||
today = current_date.strftime("%d-%m-%Y")
|
||||
current_timestamp = int(current_date.timestamp())
|
||||
# Получение списка разблокированных пользователей
|
||||
unblocked_users = self.BotDB.get_users_for_unblock_today(today)
|
||||
unblocked_users = await BotDB.get_users_for_unblock_today(current_timestamp)
|
||||
message = "Разблокированные пользователи:\n"
|
||||
for user_id, user_name in unblocked_users.items():
|
||||
for user_id in unblocked_users:
|
||||
# Получаем имя пользователя из таблицы users
|
||||
username = await BotDB.get_username(user_id)
|
||||
full_name = await BotDB.get_full_name_by_id(user_id)
|
||||
user_name = username or full_name or f"User_{user_id}"
|
||||
# Экранируем user_name для безопасного использования
|
||||
safe_user_name = html.escape(str(user_name)) if user_name else "Неизвестный пользователь"
|
||||
safe_user_name = html.escape(str(user_name))
|
||||
message += f"ID: {user_id}, Имя: {safe_user_name}\n"
|
||||
|
||||
# Отправка сообщения в канал
|
||||
self.bot.send_message(self.GROUP_FOR_MESSAGE, message)
|
||||
await bot.send_message(GROUP_FOR_MESSAGE, message)
|
||||
|
||||
|
||||
@track_time("update_user_info", "helper_func")
|
||||
@@ -503,51 +765,65 @@ async def update_user_info(source: str, message: types.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)
|
||||
metrics.record_db_query("add_new_user_in_db", 0.0, "users", "insert")
|
||||
# Выбираем эмодзю, пробегаемся циклом и смотрим что в базе такого еще не было
|
||||
user_emoji = await get_random_emoji()
|
||||
|
||||
if not await BotDB.user_exists(user_id):
|
||||
# Create User object with current timestamp
|
||||
from database.models import User
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
user = User(
|
||||
user_id=user_id,
|
||||
first_name=first_name,
|
||||
full_name=full_name,
|
||||
username=username,
|
||||
is_bot=is_bot,
|
||||
language_code=language_code,
|
||||
emoji=user_emoji,
|
||||
has_stickers=False,
|
||||
date_added=current_timestamp,
|
||||
date_changed=current_timestamp,
|
||||
voice_bot_welcome_received=False
|
||||
)
|
||||
await BotDB.add_user(user)
|
||||
metrics.record_db_query("add_user", 0.0, "users", "insert")
|
||||
else:
|
||||
is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
|
||||
is_need_update = await 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)
|
||||
metrics.record_db_query("update_username_and_full_name", 0.0, "users", "update")
|
||||
await BotDB.update_user_info(user_id, username, full_name)
|
||||
metrics.record_db_query("update_user_info", 0.0, "users", "update")
|
||||
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)
|
||||
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
||||
await BotDB.update_user_date(user_id)
|
||||
metrics.record_db_query("update_user_date", 0.0, "users", "update")
|
||||
|
||||
|
||||
@track_time("check_user_emoji", "helper_func")
|
||||
@track_errors("helper_func", "check_user_emoji")
|
||||
@db_query_time("check_emoji_for_user", "users", "select")
|
||||
def check_user_emoji(message: types.Message):
|
||||
async def check_user_emoji(message: types.Message):
|
||||
user_id = message.from_user.id
|
||||
user_emoji = BotDB.check_emoji_for_user(user_id=user_id)
|
||||
user_emoji = await BotDB.get_user_emoji(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)
|
||||
metrics.record_db_query("update_emoji_for_user", 0.0, "users", "update")
|
||||
user_emoji = await get_random_emoji()
|
||||
await BotDB.update_user_emoji(user_id=user_id, emoji=user_emoji)
|
||||
metrics.record_db_query("update_user_emoji", 0.0, "users", "update")
|
||||
return user_emoji
|
||||
|
||||
|
||||
@track_time("get_random_emoji", "helper_func")
|
||||
@track_errors("helper_func", "get_random_emoji")
|
||||
@db_query_time("check_emoji", "users", "select")
|
||||
def get_random_emoji():
|
||||
async def get_random_emoji():
|
||||
attempts = 0
|
||||
while attempts < 100:
|
||||
user_emoji = random.choice(emoji_list)
|
||||
if not BotDB.check_emoji(user_emoji):
|
||||
if not await BotDB.check_emoji_exists(user_emoji):
|
||||
return user_emoji
|
||||
attempts += 1
|
||||
logger.error("Не удалось найти уникальный эмодзи после нескольких попыток.")
|
||||
|
||||
@@ -12,13 +12,14 @@ constants = {
|
||||
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
||||
"&Я был создан для того, чтобы помочь тебе выложить пост в наш канал и если это необходимо, связаться с админами ✍✉"
|
||||
"&Так же я могу выдать тебе набор стикеров, где я буду главным героем🦸♂"
|
||||
"&Наш бот голосового общения переехал сюда: https://t.me/podslushano_biysk_bot 🎤&Там можно послушать о чем говорит наш город🎧"
|
||||
"&Наш бот голосового общения переехал ко мне! Доступен по кнопке 🎤Голосовой бот &Там можно послушать о чем говорит наш город🎧"
|
||||
"&Предлагай свой пост мне и я обязательно его опубликую😉"
|
||||
"&Для продолжения взаимодействия воспользуйся меню внизу твоего дисплея⬇"
|
||||
"&&Если что-то пошло не так: введи в чат команду /start, это перезапустит сценарий сначала."
|
||||
"&&Если что-то пошло не так: введи в чат команду /start или /restart, это перезапустит сценарий сначала."
|
||||
"Почитать инструкцию к боту можно по команде /help. Если есть вопросы, то пиши в личку: @Kerrad1"
|
||||
"&Не жми кнопку несколько раз если я не ответил с первого раза. Возможно ведутся тех.работы и я отвечу позже"
|
||||
"&&Основная группа в ВК: https://vk.com/love_bsk"
|
||||
"&Основной канал в ТГ: https://t.me/love_bsk",
|
||||
"&&Группа в ВК: https://vk.com/love_bsk"
|
||||
"&Канал в ТГ: https://t.me/love_bsk",
|
||||
'SUGGEST_NEWS': "username, окей, жду от тебя текст поста🙌🏼"
|
||||
"&Обрати внимание, что я умный и смогу из твоего текста понять команды указанные ниже😉"
|
||||
"&Если хочешь чтобы пост был опубликован анонимно, напиши в любом месте своего поста слово 'анон'."
|
||||
@@ -35,12 +36,28 @@ constants = {
|
||||
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
|
||||
"QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉",
|
||||
"SUCCESS_SEND_MESSAGE": "Пост успешно отправлен❤️ Ожидай одобрения😊",
|
||||
# Voice handler messages
|
||||
"MESSAGE_FOR_STANDUP": "Отлично, ты вошел в режим стендапа 📣"
|
||||
"&Это свободное пространство, в котором может высказаться каждый житель нашего города, и он будет услышан🙌🏼"
|
||||
"&Для того чтобы высказаться, нажми кнопку: 'Высказаться' и запиши голосовое сообщение, оно выпадет анонимно кому-то другому🗣"
|
||||
"&Для того чтобы послушать о чем говорит наш город, нажми кнопку: 'Послушать'👂"
|
||||
"&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤"
|
||||
"&❗️Но пожалуйста не оскорбляй никого, и будь вежлив."
|
||||
"&❗️Но пожалуйста не оскорбляй никого, и будь вежлив.",
|
||||
'WELCOME_MESSAGE': "<b>Привет.</b>",
|
||||
'DESCRIPTION_MESSAGE': "<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска</i>",
|
||||
'ANALOGY_MESSAGE': "Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
|
||||
'RULES_MESSAGE': "Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя бы на 5-10 секунд</i>",
|
||||
'ANONYMITY_MESSAGE': "Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)",
|
||||
'SUGGESTION_MESSAGE': "Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
|
||||
'EMOJI_INFO_MESSAGE': "Любые войсы будут помечены эмоджи. <b>Твой эмоджи - </b>{emoji}Таким эмоджи будут помечены твои сообщения для других Но другие люди не узнают кто за каким эмоджи скрывается:)",
|
||||
'HELP_INFO_MESSAGE': "Так же можешь ознакомиться с инструкцией к боту по команде /help",
|
||||
'FINAL_MESSAGE': "<b>Ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
|
||||
'HELP_MESSAGE': "Когда-нибудь здесь будет инструкция к боту. А пока по вопросам пиши в личку: @Kerrad1 или в Связаться с админами",
|
||||
'VOICE_SAVED_MESSAGE': "Окей, сохранил!👌",
|
||||
'LISTENINGS_CLEARED_MESSAGE': "Прослушивания очищены. Можешь начать слушать заново🤗",
|
||||
'NO_AUDIO_MESSAGE': "Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится",
|
||||
'UNKNOWN_CONTENT_MESSAGE': "Я тебя не понимаю🤷♀️ запиши голосовое",
|
||||
'RECORD_VOICE_MESSAGE': "Хорошо, теперь пришли мне свое голосовое сообщение"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ class BotMetrics:
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Total users gauge (отдельная метрика)
|
||||
self.total_users = Gauge(
|
||||
'total_users',
|
||||
'Total number of users in database',
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Database query metrics
|
||||
self.db_query_duration_seconds = Histogram(
|
||||
'db_query_duration_seconds',
|
||||
@@ -70,6 +77,14 @@ class BotMetrics:
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Database errors counter
|
||||
self.db_errors_total = Counter(
|
||||
'db_errors_total',
|
||||
'Total number of database errors',
|
||||
['error_type', 'query_type', 'table_name', 'operation'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Message processing metrics
|
||||
self.messages_processed_total = Counter(
|
||||
'messages_processed_total',
|
||||
@@ -92,7 +107,54 @@ class BotMetrics:
|
||||
self.rate_limit_hits_total = Counter(
|
||||
'rate_limit_hits_total',
|
||||
'Total number of rate limit hits',
|
||||
['limit_type', 'handler_type'],
|
||||
['limit_type', 'user_id', 'action'],
|
||||
registry=self.registry
|
||||
)
|
||||
# User activity metrics
|
||||
self.user_activity_total = Counter(
|
||||
'user_activity_total',
|
||||
'Total user activity events',
|
||||
['activity_type', 'user_type', 'chat_type'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# File download metrics
|
||||
self.file_downloads_total = Counter(
|
||||
'file_downloads_total',
|
||||
'Total number of file downloads',
|
||||
['content_type', 'status'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
self.file_download_duration_seconds = Histogram(
|
||||
'file_download_duration_seconds',
|
||||
'Time spent downloading files',
|
||||
['content_type'],
|
||||
buckets=[0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
self.file_download_size_bytes = Histogram(
|
||||
'file_download_size_bytes',
|
||||
'Size of downloaded files in bytes',
|
||||
['content_type'],
|
||||
buckets=[1024, 10240, 102400, 1048576, 10485760, 104857600, 1073741824],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
# Media processing metrics
|
||||
self.media_processing_total = Counter(
|
||||
'media_processing_total',
|
||||
'Total number of media processing operations',
|
||||
['content_type', 'status'],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
self.media_processing_duration_seconds = Histogram(
|
||||
'media_processing_duration_seconds',
|
||||
'Time spent processing media',
|
||||
['content_type'],
|
||||
buckets=[0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0],
|
||||
registry=self.registry
|
||||
)
|
||||
|
||||
@@ -121,10 +183,14 @@ class BotMetrics:
|
||||
status=status
|
||||
).observe(duration)
|
||||
|
||||
def set_active_users(self, count: int, user_type: str = "total"):
|
||||
"""Set the number of active users."""
|
||||
def set_active_users(self, count: int, user_type: str = "daily"):
|
||||
"""Set the number of active users for a specific type."""
|
||||
self.active_users.labels(user_type=user_type).set(count)
|
||||
|
||||
def set_total_users(self, count: int):
|
||||
"""Set the total number of users in database."""
|
||||
self.total_users.set(count)
|
||||
|
||||
def record_db_query(self, query_type: str, duration: float, table_name: str = "unknown", operation: str = "unknown"):
|
||||
"""Record database query duration."""
|
||||
self.db_query_duration_seconds.labels(
|
||||
@@ -153,6 +219,54 @@ class BotMetrics:
|
||||
status=status
|
||||
).observe(duration)
|
||||
|
||||
def record_file_download(self, content_type: str, file_size: int, duration: float):
|
||||
"""Record file download metrics."""
|
||||
self.file_downloads_total.labels(
|
||||
content_type=content_type,
|
||||
status="success"
|
||||
).inc()
|
||||
|
||||
self.file_download_duration_seconds.labels(
|
||||
content_type=content_type
|
||||
).observe(duration)
|
||||
|
||||
self.file_download_size_bytes.labels(
|
||||
content_type=content_type
|
||||
).observe(file_size)
|
||||
|
||||
def record_file_download_error(self, content_type: str, error_message: str):
|
||||
"""Record file download error metrics."""
|
||||
self.file_downloads_total.labels(
|
||||
content_type=content_type,
|
||||
status="error"
|
||||
).inc()
|
||||
|
||||
self.errors_total.labels(
|
||||
error_type="file_download_error",
|
||||
handler_type="media_processing",
|
||||
method_name="download_file"
|
||||
).inc()
|
||||
|
||||
def record_media_processing(self, content_type: str, duration: float, success: bool):
|
||||
"""Record media processing metrics."""
|
||||
status = "success" if success else "error"
|
||||
|
||||
self.media_processing_total.labels(
|
||||
content_type=content_type,
|
||||
status=status
|
||||
).inc()
|
||||
|
||||
self.media_processing_duration_seconds.labels(
|
||||
content_type=content_type
|
||||
).observe(duration)
|
||||
|
||||
if not success:
|
||||
self.errors_total.labels(
|
||||
error_type="media_processing_error",
|
||||
handler_type="media_processing",
|
||||
method_name="add_in_db_media"
|
||||
).inc()
|
||||
|
||||
def get_metrics(self) -> bytes:
|
||||
"""Generate metrics in Prometheus format."""
|
||||
return generate_latest(self.registry)
|
||||
@@ -275,6 +389,12 @@ def db_query_time(query_type: str = "unknown", table_name: str = "unknown", oper
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
metrics.record_db_error(
|
||||
type(e).__name__,
|
||||
query_type,
|
||||
table_name,
|
||||
operation
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"database",
|
||||
@@ -293,6 +413,12 @@ def db_query_time(query_type: str = "unknown", table_name: str = "unknown", oper
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
metrics.record_db_error(
|
||||
type(e).__name__,
|
||||
query_type,
|
||||
table_name,
|
||||
operation
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"database",
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
"""
|
||||
Metrics exporter for Prometheus.
|
||||
Provides HTTP endpoint for metrics collection and background metrics collection.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from typing import Optional, Dict, Any, Protocol
|
||||
from .metrics import metrics
|
||||
|
||||
|
||||
class DatabaseProvider(Protocol):
|
||||
"""Protocol for database operations."""
|
||||
|
||||
async def fetch_one(self, query: str) -> Optional[Dict[str, Any]]:
|
||||
"""Execute query and return single result."""
|
||||
...
|
||||
|
||||
|
||||
class MetricsCollector(Protocol):
|
||||
"""Protocol for metrics collection operations."""
|
||||
|
||||
async def collect_user_metrics(self, db: DatabaseProvider) -> None:
|
||||
"""Collect user-related metrics."""
|
||||
...
|
||||
|
||||
|
||||
class UserMetricsCollector:
|
||||
"""Concrete implementation of user metrics collection."""
|
||||
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
|
||||
async def collect_user_metrics(self, db: DatabaseProvider) -> None:
|
||||
"""Collect user-related metrics from database."""
|
||||
try:
|
||||
# Проверяем, есть ли метод fetch_one (асинхронная БД)
|
||||
if hasattr(db, 'fetch_one'):
|
||||
active_users_query = """
|
||||
SELECT COUNT(DISTINCT user_id) as active_users
|
||||
FROM our_users
|
||||
WHERE date_changed > datetime('now', '-1 day')
|
||||
"""
|
||||
result = await db.fetch_one(active_users_query)
|
||||
if result:
|
||||
metrics.set_active_users(result['active_users'], 'daily')
|
||||
self.logger.debug(f"Updated active users: {result['active_users']}")
|
||||
else:
|
||||
metrics.set_active_users(0, 'daily')
|
||||
self.logger.debug("Updated active users: 0")
|
||||
# Проверяем синхронную БД BotDB
|
||||
elif hasattr(db, 'connect') and hasattr(db, 'cursor'):
|
||||
# Используем синхронный запрос для BotDB в отдельном потоке
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
active_users_query = """
|
||||
SELECT COUNT(DISTINCT user_id) as active_users
|
||||
FROM our_users
|
||||
WHERE date_changed > datetime('now', '-1 day')
|
||||
"""
|
||||
|
||||
def sync_db_query():
|
||||
try:
|
||||
db.connect()
|
||||
db.cursor.execute(active_users_query)
|
||||
result = db.cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Выполняем синхронный запрос в отдельном потоке
|
||||
loop = asyncio.get_event_loop()
|
||||
with ThreadPoolExecutor() as executor:
|
||||
result = await loop.run_in_executor(executor, sync_db_query)
|
||||
|
||||
metrics.set_active_users(result, 'daily')
|
||||
self.logger.debug(f"Updated active users: {result}")
|
||||
else:
|
||||
metrics.set_active_users(0, 'daily')
|
||||
self.logger.warning("Database doesn't support fetch_one or connect methods")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error collecting user metrics: {e}")
|
||||
metrics.set_active_users(0, 'daily')
|
||||
|
||||
|
||||
class DependencyProvider(Protocol):
|
||||
"""Protocol for dependency injection."""
|
||||
|
||||
def get_db(self) -> DatabaseProvider:
|
||||
"""Get database instance."""
|
||||
...
|
||||
|
||||
|
||||
class BackgroundMetricsCollector:
|
||||
"""Background service for collecting periodic metrics using dependency injection."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dependency_provider: DependencyProvider,
|
||||
metrics_collector: MetricsCollector,
|
||||
interval: int = 60
|
||||
):
|
||||
self.dependency_provider = dependency_provider
|
||||
self.metrics_collector = metrics_collector
|
||||
self.interval = interval
|
||||
self.running = False
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def start(self):
|
||||
"""Start background metrics collection."""
|
||||
self.running = True
|
||||
self.logger.info("Background metrics collector started")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
await self._collect_metrics()
|
||||
await asyncio.sleep(self.interval)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in background metrics collection: {e}")
|
||||
await asyncio.sleep(self.interval)
|
||||
|
||||
async def stop(self):
|
||||
"""Stop background metrics collection."""
|
||||
self.running = False
|
||||
self.logger.info("Background metrics collector stopped")
|
||||
|
||||
async def _collect_metrics(self):
|
||||
"""Collect periodic metrics using dependency injection."""
|
||||
try:
|
||||
db = self.dependency_provider.get_db()
|
||||
if db:
|
||||
await self.metrics_collector.collect_user_metrics(db)
|
||||
else:
|
||||
self.logger.warning("Database not available for metrics collection")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error collecting metrics: {e}")
|
||||
|
||||
|
||||
class MetricsExporter:
|
||||
"""HTTP server for exposing Prometheus metrics."""
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = web.Application()
|
||||
self.runner: Optional[web.AppRunner] = None
|
||||
self.site: Optional[web.TCPSite] = None
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Setup routes
|
||||
self.app.router.add_get('/metrics', self.metrics_handler)
|
||||
self.app.router.add_get('/health', self.health_handler)
|
||||
self.app.router.add_get('/', self.root_handler)
|
||||
|
||||
async def start(self):
|
||||
"""Start the metrics server."""
|
||||
try:
|
||||
self.runner = web.AppRunner(self.app)
|
||||
await self.runner.setup()
|
||||
|
||||
self.site = web.TCPSite(self.runner, self.host, self.port)
|
||||
await self.site.start()
|
||||
|
||||
self.logger.info(f"Metrics server started on {self.host}:{self.port}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to start metrics server: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the metrics server."""
|
||||
if self.site:
|
||||
await self.site.stop()
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
self.logger.info("Metrics server stopped")
|
||||
|
||||
async def metrics_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /metrics endpoint for Prometheus."""
|
||||
try:
|
||||
metrics_data = metrics.get_metrics()
|
||||
self.logger.debug(f"Generated metrics: {len(metrics_data)} bytes")
|
||||
|
||||
return web.Response(
|
||||
body=metrics_data,
|
||||
content_type='text/plain; version=0.0.4'
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating metrics: {e}")
|
||||
return web.Response(
|
||||
text=f"Error generating metrics: {e}",
|
||||
status=500
|
||||
)
|
||||
|
||||
async def health_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /health endpoint for health checks."""
|
||||
return web.json_response({
|
||||
"status": "healthy",
|
||||
"service": "telegram-bot-metrics"
|
||||
})
|
||||
|
||||
async def root_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle root endpoint with basic info."""
|
||||
return web.json_response({
|
||||
"service": "Telegram Bot Metrics Exporter",
|
||||
"endpoints": {
|
||||
"/metrics": "Prometheus metrics",
|
||||
"/health": "Health check",
|
||||
"/": "This info"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class MetricsManager:
|
||||
"""Main class for managing metrics collection and export."""
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
||||
self.exporter = MetricsExporter(host, port)
|
||||
|
||||
# Dependency injection setup
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
dependency_provider = get_global_instance()
|
||||
metrics_collector = UserMetricsCollector(logging.getLogger(__name__))
|
||||
|
||||
self.collector = BackgroundMetricsCollector(
|
||||
dependency_provider=dependency_provider,
|
||||
metrics_collector=metrics_collector
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
async def start(self):
|
||||
"""Start metrics collection and export."""
|
||||
try:
|
||||
# Start metrics exporter
|
||||
await self.exporter.start()
|
||||
|
||||
# Start background collector
|
||||
asyncio.create_task(self.collector.start())
|
||||
|
||||
self.logger.info("Metrics manager started successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to start metrics manager: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Stop metrics collection and export."""
|
||||
try:
|
||||
await self.collector.stop()
|
||||
await self.exporter.stop()
|
||||
self.logger.info("Metrics manager stopped successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping metrics manager: {e}")
|
||||
raise
|
||||
@@ -1,38 +0,0 @@
|
||||
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():
|
||||
migrations_init = """
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
version INTEGER PRIMARY KEY NOT NULL,
|
||||
script_name TEXT NOT NULL,
|
||||
created_at TEXT
|
||||
);
|
||||
"""
|
||||
BotDB.create_table(migrations_init)
|
||||
BotDB.update_version(0, get_filename())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,67 +0,0 @@
|
||||
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 < 1:
|
||||
# Скрипты миграции
|
||||
create_table_sql_1 = """
|
||||
CREATE TABLE IF NOT EXISTS "admins" (
|
||||
user_id INTEGER NOT NULL,
|
||||
"role" TEXT
|
||||
);
|
||||
"""
|
||||
create_table_sql_2 = """CREATE TABLE IF NOT EXISTS "blacklist"
|
||||
(
|
||||
"user_id" INTEGER NOT NULL UNIQUE,
|
||||
"user_name" INTEGER,
|
||||
"message_for_user" INTEGER,
|
||||
"date_to_unban" INTEGER
|
||||
);
|
||||
"""
|
||||
create_table_sql_3 = """
|
||||
CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT,
|
||||
user_id INTEGER,
|
||||
message_id INTEGER NOT NULL,
|
||||
date TEXT
|
||||
);
|
||||
"""
|
||||
# Применение миграции
|
||||
BotDB.create_table(create_table_sql_1)
|
||||
BotDB.create_table(create_table_sql_2)
|
||||
BotDB.create_table(create_table_sql_3)
|
||||
BotDB.add_admin(842766148, 'creator')
|
||||
BotDB.add_admin(920057022, 'admin')
|
||||
filename = get_filename()
|
||||
|
||||
BotDB.update_version(1, filename)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,65 +0,0 @@
|
||||
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 < 2:
|
||||
# Скрипты миграции
|
||||
create_table_sql_1 = """
|
||||
CREATE TABLE IF NOT EXISTS "post_from_telegram_suggest"
|
||||
(
|
||||
message_id INTEGER not null,
|
||||
text TEXT,
|
||||
helper_text_message_id INTEGER,
|
||||
author_id INTEGER,
|
||||
created_at TEXT
|
||||
);
|
||||
"""
|
||||
create_table_sql_2 = """
|
||||
CREATE TABLE IF NOT EXISTS message_link_to_content (
|
||||
post_id INTEGER NOT NULL,
|
||||
message_id INTEGER NOT NULL
|
||||
);
|
||||
"""
|
||||
create_table_sql_3 = """
|
||||
CREATE TABLE IF NOT EXISTS content_post_from_telegram (
|
||||
message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
content_type TEXT
|
||||
);
|
||||
"""
|
||||
# Применение миграции
|
||||
BotDB.create_table(create_table_sql_1)
|
||||
BotDB.create_table(create_table_sql_2)
|
||||
BotDB.create_table(create_table_sql_3)
|
||||
filename = get_filename()
|
||||
|
||||
BotDB.update_version(2, filename)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,56 +0,0 @@
|
||||
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()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
rule_files:
|
||||
# - "first_rules.yml"
|
||||
# - "second_rules.yml"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'telegram-bot'
|
||||
static_configs:
|
||||
- targets: ['telegram-bot:8000']
|
||||
metrics_path: '/metrics'
|
||||
scrape_interval: 10s
|
||||
scrape_timeout: 10s
|
||||
honor_labels: true
|
||||
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets:
|
||||
# - alertmanager:9093
|
||||
@@ -18,6 +18,11 @@ apscheduler~=3.10.4
|
||||
prometheus-client==0.19.0
|
||||
aiohttp==3.9.1
|
||||
|
||||
# Network stability improvements
|
||||
aiohttp[speedups]>=3.9.1
|
||||
aiodns>=3.0.0
|
||||
cchardet>=2.1.7
|
||||
|
||||
# Development tools
|
||||
pluggy==1.5.0
|
||||
attrs~=23.2.0
|
||||
|
||||
145
run_helper.py
145
run_helper.py
@@ -10,100 +10,140 @@ if CURRENT_DIR not in sys.path:
|
||||
|
||||
from helper_bot.main import start_bot
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from helper_bot.server_monitor import ServerMonitor
|
||||
from helper_bot.utils.auto_unban_scheduler import get_auto_unban_scheduler
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Импортируем PID менеджер из инфраструктуры (если доступен)
|
||||
import sys
|
||||
import os
|
||||
|
||||
async def start_monitoring(bdf, bot):
|
||||
"""Запуск модуля мониторинга сервера"""
|
||||
monitor = ServerMonitor(
|
||||
bot=bot,
|
||||
group_for_logs=bdf.settings['Telegram']['group_for_logs'],
|
||||
important_logs=bdf.settings['Telegram']['important_logs']
|
||||
)
|
||||
return monitor
|
||||
def get_pid_manager():
|
||||
"""Получение PID менеджера из инфраструктуры проекта"""
|
||||
try:
|
||||
# Пытаемся импортировать из инфраструктуры проекта
|
||||
infra_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'infra', 'monitoring')
|
||||
if infra_path not in sys.path:
|
||||
sys.path.insert(0, infra_path)
|
||||
|
||||
from pid_manager import get_bot_pid_manager
|
||||
return get_bot_pid_manager
|
||||
|
||||
except ImportError:
|
||||
# В изолированном запуске PID менеджер не нужен
|
||||
logger.info("PID менеджер недоступен (изолированный запуск), PID файл не создается")
|
||||
return None
|
||||
|
||||
# Получаем функцию создания PID менеджера
|
||||
get_bot_pid_manager = get_pid_manager()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Основная функция запуска"""
|
||||
# Создаем PID менеджер для отслеживания процесса (если доступен)
|
||||
pid_manager = None
|
||||
if get_bot_pid_manager:
|
||||
pid_manager = get_bot_pid_manager("helper_bot")
|
||||
if not pid_manager.create_pid_file():
|
||||
logger.error("Не удалось создать PID файл, завершаем работу")
|
||||
return
|
||||
else:
|
||||
logger.info("PID менеджер недоступен, запуск без PID файла")
|
||||
|
||||
bdf = get_global_instance()
|
||||
|
||||
# Создаем бота для мониторинга
|
||||
# Создаем бота для автоматического разбана
|
||||
from aiogram import Bot
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
|
||||
monitor_bot = Bot(
|
||||
auto_unban_bot = Bot(
|
||||
token=bdf.settings['Telegram']['bot_token'],
|
||||
default=DefaultBotProperties(parse_mode='HTML'),
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
# Создаем экземпляр монитора
|
||||
monitor = await start_monitoring(bdf, monitor_bot)
|
||||
|
||||
# Инициализируем планировщик автоматического разбана
|
||||
auto_unban_scheduler = get_auto_unban_scheduler()
|
||||
auto_unban_scheduler.set_bot(monitor_bot)
|
||||
auto_unban_scheduler.set_bot(auto_unban_bot)
|
||||
auto_unban_scheduler.start_scheduler()
|
||||
|
||||
# Инициализируем метрики ПОСЛЕ импорта всех модулей
|
||||
# Это гарантирует, что global instance полностью инициализирован
|
||||
from helper_bot.utils.metrics_exporter import MetricsManager
|
||||
metrics_manager = MetricsManager(host="0.0.0.0", port=8000)
|
||||
# Метрики запускаются в main.py через server_prometheus.py
|
||||
# Здесь не нужно дублировать функциональность
|
||||
|
||||
# Флаг для корректного завершения
|
||||
shutdown_event = asyncio.Event()
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""Обработчик сигналов для корректного завершения"""
|
||||
print(f"\nПолучен сигнал {signum}, завершаем работу...")
|
||||
logger.info(f"Получен сигнал {signum}, завершаем работу...")
|
||||
shutdown_event.set()
|
||||
|
||||
# Регистрируем обработчики сигналов
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Запускаем бота, мониторинг и метрики
|
||||
# Запускаем бота (метрики запускаются внутри start_bot)
|
||||
bot_task = asyncio.create_task(start_bot(bdf))
|
||||
monitor_task = asyncio.create_task(monitor.monitor_loop())
|
||||
metrics_task = asyncio.create_task(metrics_manager.start())
|
||||
|
||||
main_bot = None
|
||||
|
||||
try:
|
||||
# Ждем сигнала завершения
|
||||
await shutdown_event.wait()
|
||||
print("Начинаем корректное завершение...")
|
||||
logger.info("Начинаем корректное завершение...")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("Получен сигнал завершения...")
|
||||
logger.info("Получен сигнал завершения...")
|
||||
finally:
|
||||
print("Отправляем сообщение об отключении...")
|
||||
try:
|
||||
# Отправляем сообщение об отключении
|
||||
await monitor.send_shutdown_message()
|
||||
except Exception as e:
|
||||
print(f"Ошибка при отправке сообщения об отключении: {e}")
|
||||
|
||||
print("Останавливаем планировщик автоматического разбана...")
|
||||
logger.info("Останавливаем планировщик автоматического разбана...")
|
||||
auto_unban_scheduler.stop_scheduler()
|
||||
|
||||
print("Останавливаем метрики...")
|
||||
await metrics_manager.stop()
|
||||
|
||||
print("Останавливаем задачи...")
|
||||
# Отменяем задачи
|
||||
bot_task.cancel()
|
||||
monitor_task.cancel()
|
||||
metrics_task.cancel()
|
||||
|
||||
# Ждем завершения задач
|
||||
# Останавливаем планировщик метрик
|
||||
try:
|
||||
await asyncio.gather(bot_task, monitor_task, metrics_task, return_exceptions=True)
|
||||
from helper_bot.utils.metrics_scheduler import stop_metrics_scheduler
|
||||
stop_metrics_scheduler()
|
||||
logger.info("Планировщик метрик остановлен")
|
||||
except Exception as e:
|
||||
print(f"Ошибка при остановке задач: {e}")
|
||||
logger.error(f"Ошибка при остановке планировщика метрик: {e}")
|
||||
|
||||
# Закрываем сессию бота
|
||||
await monitor_bot.session.close()
|
||||
print("Бот корректно остановлен")
|
||||
# Метрики останавливаются в main.py
|
||||
|
||||
logger.info("Останавливаем задачи...")
|
||||
# Отменяем задачу бота
|
||||
bot_task.cancel()
|
||||
|
||||
# Очищаем PID файл (если PID менеджер доступен)
|
||||
if pid_manager:
|
||||
pid_manager.cleanup_pid_file()
|
||||
|
||||
# Ждем завершения задачи бота и получаем результат main bot
|
||||
try:
|
||||
results = await asyncio.gather(bot_task, return_exceptions=True)
|
||||
# Результат - это main bot
|
||||
if results[0] and not isinstance(results[0], Exception):
|
||||
main_bot = results[0]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при остановке задач: {e}")
|
||||
|
||||
# Закрываем сессию основного бота (если она еще не закрыта)
|
||||
if main_bot and hasattr(main_bot, 'session') and not main_bot.session.closed:
|
||||
try:
|
||||
await main_bot.session.close()
|
||||
logger.info("Сессия основного бота корректно закрыта")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при закрытии сессии основного бота: {e}")
|
||||
|
||||
# Закрываем сессию бота для автоматического разбана
|
||||
if not auto_unban_bot.session.closed:
|
||||
try:
|
||||
await auto_unban_bot.session.close()
|
||||
logger.info("Сессия бота автоматического разбана корректно закрыта")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при закрытии сессии бота автоматического разбана: {e}")
|
||||
|
||||
# Даем время на завершение всех aiohttp соединений
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
logger.info("Бот корректно остановлен")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -115,4 +155,13 @@ if __name__ == '__main__':
|
||||
try:
|
||||
loop.run_until_complete(main())
|
||||
finally:
|
||||
# Закрываем все pending tasks
|
||||
pending = asyncio.all_tasks(loop)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
# Ждем завершения всех задач
|
||||
if pending:
|
||||
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||
|
||||
loop.close()
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Standalone metrics server for testing.
|
||||
Run this to start just the metrics system without the bot.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import signal
|
||||
import sys
|
||||
from helper_bot.utils.metrics_exporter import MetricsManager
|
||||
|
||||
|
||||
class MetricsServer:
|
||||
"""Standalone metrics server."""
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.metrics_manager = MetricsManager(host, port)
|
||||
self.running = False
|
||||
|
||||
async def start(self):
|
||||
"""Start the metrics server."""
|
||||
try:
|
||||
await self.metrics_manager.start()
|
||||
self.running = True
|
||||
print(f"🚀 Metrics server started on {self.host}:{self.port}")
|
||||
print(f"📊 Metrics endpoint: http://{self.host}:{self.port}/metrics")
|
||||
print(f"🏥 Health check: http://{self.host}:{self.port}/health")
|
||||
print(f"ℹ️ Info: http://{self.host}:{self.port}/")
|
||||
print("\nPress Ctrl+C to stop the server")
|
||||
|
||||
# Keep the server running
|
||||
while self.running:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error starting metrics server: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the metrics server."""
|
||||
if self.running:
|
||||
self.running = False
|
||||
await self.metrics_manager.stop()
|
||||
print("\n🛑 Metrics server stopped")
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
"""Handle shutdown signals."""
|
||||
print(f"\n📡 Received signal {signum}, shutting down...")
|
||||
asyncio.create_task(self.stop())
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function."""
|
||||
# Parse command line arguments
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
host = sys.argv[1]
|
||||
if len(sys.argv) > 2:
|
||||
port = int(sys.argv[2])
|
||||
|
||||
# Create and start server
|
||||
server = MetricsServer(host, port)
|
||||
|
||||
# Setup signal handlers
|
||||
signal.signal(signal.SIGINT, server.signal_handler)
|
||||
signal.signal(signal.SIGTERM, server.signal_handler)
|
||||
|
||||
try:
|
||||
await server.start()
|
||||
except KeyboardInterrupt:
|
||||
print("\n📡 Keyboard interrupt received")
|
||||
finally:
|
||||
await server.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔧 Starting standalone metrics server...")
|
||||
print("Usage: python run_metrics_only.py [host] [port]")
|
||||
print("Default: host=0.0.0.0, port=8000")
|
||||
print()
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Server stopped by user")
|
||||
except Exception as e:
|
||||
print(f"❌ Server error: {e}")
|
||||
sys.exit(1)
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
PROJECT_NAME="telegram-helper-bot"
|
||||
DOCKER_COMPOSE_FILE="docker-compose.yml"
|
||||
ENV_FILE=".env"
|
||||
|
||||
echo -e "${GREEN}🚀 Starting deployment of $PROJECT_NAME${NC}"
|
||||
|
||||
# Check if .env file exists
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo -e "${RED}❌ Error: $ENV_FILE file not found!${NC}"
|
||||
echo -e "${YELLOW}Please copy env.example to .env and configure your settings${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load environment variables
|
||||
source "$ENV_FILE"
|
||||
|
||||
# Validate required environment variables
|
||||
required_vars=("BOT_TOKEN" "MAIN_PUBLIC" "GROUP_FOR_POSTS" "GROUP_FOR_MESSAGE" "GROUP_FOR_LOGS")
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo -e "${RED}❌ Error: Required environment variable $var is not set${NC}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${GREEN}✅ Environment variables validated${NC}"
|
||||
|
||||
# Create necessary directories
|
||||
echo -e "${YELLOW}📁 Creating necessary directories...${NC}"
|
||||
mkdir -p database logs
|
||||
|
||||
# Set proper permissions
|
||||
echo -e "${YELLOW}🔐 Setting proper permissions...${NC}"
|
||||
chmod 600 "$ENV_FILE"
|
||||
chmod 755 database logs
|
||||
|
||||
# Stop existing containers
|
||||
echo -e "${YELLOW}🛑 Stopping existing containers...${NC}"
|
||||
docker-compose -f "$DOCKER_COMPOSE_FILE" down --remove-orphans || true
|
||||
|
||||
# Remove old images
|
||||
echo -e "${YELLOW}🧹 Cleaning up old images...${NC}"
|
||||
docker system prune -f
|
||||
|
||||
# Build and start services
|
||||
echo -e "${YELLOW}🔨 Building and starting services...${NC}"
|
||||
docker-compose -f "$DOCKER_COMPOSE_FILE" up -d --build
|
||||
|
||||
# Wait for services to be healthy
|
||||
echo -e "${YELLOW}⏳ Waiting for services to be healthy...${NC}"
|
||||
sleep 30
|
||||
|
||||
# Check service health
|
||||
echo -e "${YELLOW}🏥 Checking service health...${NC}"
|
||||
if docker-compose -f "$DOCKER_COMPOSE_FILE" ps | grep -q "unhealthy"; then
|
||||
echo -e "${RED}❌ Some services are unhealthy!${NC}"
|
||||
docker-compose -f "$DOCKER_COMPOSE_FILE" logs
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show service status
|
||||
echo -e "${GREEN}📊 Service status:${NC}"
|
||||
docker-compose -f "$DOCKER_COMPOSE_FILE" ps
|
||||
|
||||
echo -e "${GREEN}✅ Deployment completed successfully!${NC}"
|
||||
echo -e "${GREEN}📊 Monitoring URLs:${NC}"
|
||||
echo -e " Prometheus: http://localhost:9090"
|
||||
echo -e " Grafana: http://localhost:3000"
|
||||
echo -e " Bot Metrics: http://localhost:8000/metrics"
|
||||
echo -e " Bot Health: http://localhost:8000/health"
|
||||
echo -e ""
|
||||
echo -e "${YELLOW}📝 Useful commands:${NC}"
|
||||
echo -e " View logs: docker-compose logs -f"
|
||||
echo -e " Restart: docker-compose restart"
|
||||
echo -e " Stop: docker-compose down"
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🔄 Starting migration from systemctl + cron to Docker${NC}"
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}❌ This script must be run as root for systemctl operations${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
SERVICE_NAME="telegram-helper-bot"
|
||||
CRON_USER="root"
|
||||
|
||||
echo -e "${YELLOW}📋 Migration steps:${NC}"
|
||||
echo "1. Stop systemctl service"
|
||||
echo "2. Disable systemctl service"
|
||||
echo "3. Remove cron jobs"
|
||||
echo "4. Backup existing data"
|
||||
echo "5. Deploy Docker version"
|
||||
|
||||
# Step 1: Stop systemctl service
|
||||
echo -e "${YELLOW}🛑 Stopping systemctl service...${NC}"
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
systemctl stop "$SERVICE_NAME"
|
||||
echo -e "${GREEN}✅ Service stopped${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Service was not running${NC}"
|
||||
fi
|
||||
|
||||
# Step 2: Disable systemctl service
|
||||
echo -e "${YELLOW}🚫 Disabling systemctl service...${NC}"
|
||||
if systemctl is-enabled --quiet "$SERVICE_NAME"; then
|
||||
systemctl disable "$SERVICE_NAME"
|
||||
echo -e "${GREEN}✅ Service disabled${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Service was not enabled${NC}"
|
||||
fi
|
||||
|
||||
# Step 3: Remove cron jobs
|
||||
echo -e "${YELLOW}🗑️ Removing cron jobs...${NC}"
|
||||
if crontab -u "$CRON_USER" -l 2>/dev/null | grep -q "telegram-helper-bot"; then
|
||||
crontab -u "$CRON_USER" -l 2>/dev/null | grep -v "telegram-helper-bot" | crontab -u "$CRON_USER" -
|
||||
echo -e "${GREEN}✅ Cron jobs removed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ No cron jobs found${NC}"
|
||||
fi
|
||||
|
||||
# Step 4: Backup existing data
|
||||
echo -e "${YELLOW}💾 Creating backup...${NC}"
|
||||
BACKUP_DIR="/backup/telegram-bot-$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Backup database
|
||||
if [ -f "database/tg-bot-database.db" ]; then
|
||||
cp -r database "$BACKUP_DIR/"
|
||||
echo -e "${GREEN}✅ Database backed up to $BACKUP_DIR/database${NC}"
|
||||
fi
|
||||
|
||||
# Backup logs
|
||||
if [ -d "logs" ]; then
|
||||
cp -r logs "$BACKUP_DIR/"
|
||||
echo -e "${GREEN}✅ Logs backed up to $BACKUP_DIR/logs${NC}"
|
||||
fi
|
||||
|
||||
# Backup settings
|
||||
if [ -f ".env" ]; then
|
||||
cp .env "$BACKUP_DIR/"
|
||||
echo -e "${GREEN}✅ Settings backed up to $BACKUP_DIR/.env${NC}"
|
||||
fi
|
||||
|
||||
# Step 5: Deploy Docker version
|
||||
echo -e "${YELLOW}🐳 Deploying Docker version...${NC}"
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not installed. Please install Docker first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker Compose is not installed. Please install Docker Compose first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make deploy script executable and run it
|
||||
chmod +x scripts/deploy.sh
|
||||
./scripts/deploy.sh
|
||||
|
||||
echo -e "${GREEN}✅ Migration completed successfully!${NC}"
|
||||
echo -e "${GREEN}📁 Backup location: $BACKUP_DIR${NC}"
|
||||
echo -e "${YELLOW}📝 Next steps:${NC}"
|
||||
echo "1. Verify the bot is working correctly"
|
||||
echo "2. Check monitoring dashboards"
|
||||
echo "3. Remove old systemctl service file if no longer needed"
|
||||
echo "4. Update any external monitoring/alerting systems"
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🐍 Запуск Telegram Bot с Python 3.9 (стандартная версия)..."
|
||||
echo ""
|
||||
|
||||
echo "🔧 Сборка Docker образа с Python 3.9..."
|
||||
make build
|
||||
|
||||
echo ""
|
||||
echo "🚀 Запуск сервисов..."
|
||||
make up
|
||||
|
||||
echo ""
|
||||
echo "🐍 Проверка версии Python в контейнере..."
|
||||
make check-python
|
||||
|
||||
echo ""
|
||||
echo "📦 Проверка установленных пакетов..."
|
||||
docker exec telegram-bot .venv/bin/pip list
|
||||
|
||||
echo ""
|
||||
echo "✅ Сервисы успешно запущены!"
|
||||
echo ""
|
||||
echo "📝 Полезные команды:"
|
||||
echo " Логи бота: make logs-bot"
|
||||
echo " Статус: make status"
|
||||
echo " Остановка: make stop"
|
||||
echo " Перезапуск: make restart"
|
||||
echo ""
|
||||
echo "📊 Мониторинг:"
|
||||
echo " Prometheus: http://localhost:9090"
|
||||
echo " Grafana: http://localhost:3000 (admin/admin)"
|
||||
@@ -6,7 +6,7 @@ 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
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
# Импортируем моки в самом начале
|
||||
import tests.mocks
|
||||
@@ -58,15 +58,15 @@ def mock_state():
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
"""Создает мок базы данных для тестов"""
|
||||
db = Mock(spec=BotDB)
|
||||
db = Mock(spec=AsyncBotDB)
|
||||
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_new_user = Mock()
|
||||
db.update_user_date = Mock()
|
||||
db.update_user_info = Mock()
|
||||
db.add_post_in_db = Mock()
|
||||
db.update_info_about_stickers = Mock()
|
||||
db.update_stickers_info = Mock()
|
||||
db.add_new_message_in_db = Mock()
|
||||
db.get_info_about_stickers = Mock(return_value=False)
|
||||
db.get_stickers_info = Mock(return_value=False)
|
||||
db.get_username_and_full_name = Mock(return_value=("testuser", "Test User"))
|
||||
return db
|
||||
|
||||
|
||||
125
tests/conftest_message_repository.py
Normal file
125
tests/conftest_message_repository.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from datetime import datetime
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.models import UserMessage
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_db_path():
|
||||
"""Фикстура для пути к тестовой БД (сессионная область)."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
yield temp_path
|
||||
|
||||
# Очистка после всех тестов
|
||||
try:
|
||||
os.unlink(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message_repository(test_db_path):
|
||||
"""Фикстура для MessageRepository."""
|
||||
return MessageRepository(test_db_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_messages():
|
||||
"""Фикстура для набора тестовых сообщений."""
|
||||
base_timestamp = int(datetime.now().timestamp())
|
||||
|
||||
return [
|
||||
UserMessage(
|
||||
message_text="Первое тестовое сообщение",
|
||||
user_id=1001,
|
||||
telegram_message_id=2001,
|
||||
date=base_timestamp
|
||||
),
|
||||
UserMessage(
|
||||
message_text="Второе тестовое сообщение",
|
||||
user_id=1002,
|
||||
telegram_message_id=2002,
|
||||
date=base_timestamp + 1
|
||||
),
|
||||
UserMessage(
|
||||
message_text="Третье тестовое сообщение",
|
||||
user_id=1003,
|
||||
telegram_message_id=2003,
|
||||
date=base_timestamp + 2
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message_without_date():
|
||||
"""Фикстура для сообщения без даты."""
|
||||
return UserMessage(
|
||||
message_text="Сообщение без даты",
|
||||
user_id=1004,
|
||||
telegram_message_id=2004,
|
||||
date=None
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message_with_zero_date():
|
||||
"""Фикстура для сообщения с нулевой датой."""
|
||||
return UserMessage(
|
||||
message_text="Сообщение с нулевой датой",
|
||||
user_id=1005,
|
||||
telegram_message_id=2005,
|
||||
date=0
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message_with_special_chars():
|
||||
"""Фикстура для сообщения со специальными символами."""
|
||||
return UserMessage(
|
||||
message_text="Сообщение с 'кавычками', \"двойными кавычками\" и эмодзи 😊\nНовая строка",
|
||||
user_id=1006,
|
||||
telegram_message_id=2006,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def long_message():
|
||||
"""Фикстура для длинного сообщения."""
|
||||
long_text = "Очень длинное сообщение " * 100 # ~2400 символов
|
||||
return UserMessage(
|
||||
message_text=long_text,
|
||||
user_id=1007,
|
||||
telegram_message_id=2007,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message_with_unicode():
|
||||
"""Фикстура для сообщения с Unicode символами."""
|
||||
return UserMessage(
|
||||
message_text="Сообщение с Unicode: 你好世界 🌍 Привет мир",
|
||||
user_id=1008,
|
||||
telegram_message_id=2008,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def initialized_repository(message_repository):
|
||||
"""Фикстура для инициализированного репозитория с созданными таблицами."""
|
||||
await message_repository.create_tables()
|
||||
return message_repository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def repository_with_data(initialized_repository, sample_messages):
|
||||
"""Фикстура для репозитория с тестовыми данными."""
|
||||
for message in sample_messages:
|
||||
await initialized_repository.add_message(message)
|
||||
return initialized_repository
|
||||
208
tests/conftest_post_repository.py
Normal file
208
tests/conftest_post_repository.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, AsyncMock
|
||||
from database.repositories.post_repository import PostRepository
|
||||
from database.models import TelegramPost, PostContent, MessageContentLink
|
||||
|
||||
|
||||
@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_post_repository():
|
||||
"""Создает мок PostRepository для unit тестов"""
|
||||
mock_repo = Mock(spec=PostRepository)
|
||||
mock_repo._execute_query = AsyncMock()
|
||||
mock_repo._execute_query_with_result = AsyncMock()
|
||||
mock_repo.logger = Mock()
|
||||
return mock_repo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_telegram_post():
|
||||
"""Создает тестовый объект TelegramPost"""
|
||||
return TelegramPost(
|
||||
message_id=12345,
|
||||
text="Тестовый пост для unit тестов",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_telegram_post_with_helper():
|
||||
"""Создает тестовый объект TelegramPost с helper сообщением"""
|
||||
return TelegramPost(
|
||||
message_id=12346,
|
||||
text="Тестовый пост с helper сообщением",
|
||||
author_id=67890,
|
||||
helper_text_message_id=99999,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_telegram_post_no_date():
|
||||
"""Создает тестовый объект TelegramPost без даты"""
|
||||
return TelegramPost(
|
||||
message_id=12347,
|
||||
text="Тестовый пост без даты",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=None
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post_content():
|
||||
"""Создает тестовый объект PostContent"""
|
||||
return PostContent(
|
||||
message_id=12345,
|
||||
content_name="/path/to/test/file.jpg",
|
||||
content_type="photo"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message_content_link():
|
||||
"""Создает тестовый объект MessageContentLink"""
|
||||
return MessageContentLink(
|
||||
post_id=12345,
|
||||
message_id=67890
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_execute_query():
|
||||
"""Создает мок для _execute_query"""
|
||||
return AsyncMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_execute_query_with_result():
|
||||
"""Создает мок для _execute_query_with_result"""
|
||||
return AsyncMock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_logger():
|
||||
"""Создает мок для logger"""
|
||||
return Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db_file():
|
||||
"""Создает временный файл БД для интеграционных тестов"""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp_file:
|
||||
db_path = tmp_file.name
|
||||
|
||||
yield db_path
|
||||
|
||||
# Очищаем временный файл после тестов
|
||||
try:
|
||||
os.unlink(db_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def real_post_repository(temp_db_file):
|
||||
"""Создает реальный PostRepository с временной БД для интеграционных тестов"""
|
||||
return PostRepository(temp_db_file)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_posts_batch():
|
||||
"""Создает набор тестовых постов для batch тестов"""
|
||||
return [
|
||||
TelegramPost(
|
||||
message_id=10001,
|
||||
text="Первый тестовый пост",
|
||||
author_id=11111,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
),
|
||||
TelegramPost(
|
||||
message_id=10002,
|
||||
text="Второй тестовый пост",
|
||||
author_id=22222,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
),
|
||||
TelegramPost(
|
||||
message_id=10003,
|
||||
text="Третий тестовый пост",
|
||||
author_id=33333,
|
||||
helper_text_message_id=88888,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_content_batch():
|
||||
"""Создает набор тестового контента для batch тестов"""
|
||||
return [
|
||||
(10001, "/path/to/photo1.jpg", "photo"),
|
||||
(10002, "/path/to/video1.mp4", "video"),
|
||||
(10003, "/path/to/audio1.mp3", "audio"),
|
||||
(10004, "/path/to/photo2.jpg", "photo"),
|
||||
(10005, "/path/to/video2.mp4", "video")
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_database_connection():
|
||||
"""Создает мок для DatabaseConnection"""
|
||||
mock_conn = Mock()
|
||||
mock_conn._execute_query = AsyncMock()
|
||||
mock_conn._execute_query_with_result = AsyncMock()
|
||||
mock_conn.logger = Mock()
|
||||
return mock_conn
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_helper_message_ids():
|
||||
"""Создает набор тестовых helper message ID"""
|
||||
return [11111, 22222, 33333, 44444, 55555]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message_ids():
|
||||
"""Создает набор тестовых message ID"""
|
||||
return [10001, 10002, 10003, 10004, 10005]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_author_ids():
|
||||
"""Создает набор тестовых author ID"""
|
||||
return [11111, 22222, 33333, 44444, 55555]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sql_queries():
|
||||
"""Создает мок для SQL запросов"""
|
||||
return {
|
||||
'create_tables': [
|
||||
"CREATE TABLE IF NOT EXISTS post_from_telegram_suggest",
|
||||
"CREATE TABLE IF NOT EXISTS content_post_from_telegram",
|
||||
"CREATE TABLE IF NOT EXISTS message_link_to_content"
|
||||
],
|
||||
'add_post': "INSERT INTO post_from_telegram_suggest",
|
||||
'update_helper': "UPDATE post_from_telegram_suggest SET helper_text_message_id",
|
||||
'add_content': "INSERT OR IGNORE INTO content_post_from_telegram",
|
||||
'add_link': "INSERT OR IGNORE INTO message_link_to_content",
|
||||
'get_content': "SELECT cpft.content_name, cpft.content_type",
|
||||
'get_text': "SELECT text FROM post_from_telegram_suggest",
|
||||
'get_ids': "SELECT mltc.message_id",
|
||||
'get_author': "SELECT author_id FROM post_from_telegram_suggest"
|
||||
}
|
||||
@@ -31,9 +31,9 @@ def setup_test_mocks():
|
||||
env_patcher = patch('os.getenv', side_effect=mock_getenv)
|
||||
env_patcher.start()
|
||||
|
||||
# Мокаем BotDB
|
||||
# Мокаем AsyncBotDB
|
||||
mock_db = Mock()
|
||||
db_patcher = patch('helper_bot.utils.base_dependency_factory.BotDB', mock_db)
|
||||
db_patcher = patch('helper_bot.utils.base_dependency_factory.AsyncBotDB', mock_db)
|
||||
db_patcher.start()
|
||||
|
||||
return env_patcher, db_patcher
|
||||
|
||||
295
tests/test_admin_repository.py
Normal file
295
tests/test_admin_repository.py
Normal file
@@ -0,0 +1,295 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from database.repositories.admin_repository import AdminRepository
|
||||
from database.models import Admin
|
||||
|
||||
|
||||
class TestAdminRepository:
|
||||
"""Тесты для AdminRepository"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_connection(self):
|
||||
"""Мок для DatabaseConnection"""
|
||||
mock_connection = Mock()
|
||||
mock_connection._execute_query = AsyncMock()
|
||||
mock_connection._execute_query_with_result = AsyncMock()
|
||||
mock_connection.logger = Mock()
|
||||
return mock_connection
|
||||
|
||||
@pytest.fixture
|
||||
def admin_repository(self, mock_db_connection):
|
||||
"""Экземпляр AdminRepository для тестов"""
|
||||
# Патчим наследование от DatabaseConnection
|
||||
with patch.object(AdminRepository, '__init__', return_value=None):
|
||||
repo = AdminRepository()
|
||||
repo._execute_query = mock_db_connection._execute_query
|
||||
repo._execute_query_with_result = mock_db_connection._execute_query_with_result
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def sample_admin(self):
|
||||
"""Тестовый администратор"""
|
||||
return Admin(
|
||||
user_id=12345,
|
||||
role="admin"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_admin_with_created_at(self):
|
||||
"""Тестовый администратор с датой создания"""
|
||||
return Admin(
|
||||
user_id=12345,
|
||||
role="admin",
|
||||
created_at="1705312200" # UNIX timestamp
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables(self, admin_repository):
|
||||
"""Тест создания таблицы администраторов"""
|
||||
await admin_repository.create_tables()
|
||||
|
||||
# Проверяем, что включены внешние ключи
|
||||
admin_repository._execute_query.assert_called()
|
||||
calls = admin_repository._execute_query.call_args_list
|
||||
|
||||
# Первый вызов должен быть для включения внешних ключей
|
||||
assert calls[0][0][0] == "PRAGMA foreign_keys = ON"
|
||||
|
||||
# Второй вызов должен быть для создания таблицы
|
||||
create_table_call = calls[1]
|
||||
assert "CREATE TABLE IF NOT EXISTS admins" in create_table_call[0][0]
|
||||
assert "user_id INTEGER NOT NULL PRIMARY KEY" in create_table_call[0][0]
|
||||
assert "role TEXT DEFAULT 'admin'" in create_table_call[0][0]
|
||||
assert "created_at INTEGER DEFAULT (strftime('%s', 'now'))" in create_table_call[0][0]
|
||||
assert "FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in create_table_call[0][0]
|
||||
|
||||
# Проверяем логирование
|
||||
admin_repository.logger.info.assert_called_once_with("Таблица администраторов создана")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_admin(self, admin_repository, sample_admin):
|
||||
"""Тест добавления администратора"""
|
||||
await admin_repository.add_admin(sample_admin)
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
admin_repository._execute_query.assert_called_once()
|
||||
call_args = admin_repository._execute_query.call_args
|
||||
|
||||
assert call_args[0][0] == "INSERT INTO admins (user_id, role) VALUES (?, ?)"
|
||||
assert call_args[0][1] == (12345, "admin")
|
||||
|
||||
# Проверяем логирование
|
||||
admin_repository.logger.info.assert_called_once_with(
|
||||
"Администратор добавлен: user_id=12345, role=admin"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_admin_with_custom_role(self, admin_repository):
|
||||
"""Тест добавления администратора с кастомной ролью"""
|
||||
admin = Admin(user_id=67890, role="super_admin")
|
||||
await admin_repository.add_admin(admin)
|
||||
|
||||
call_args = admin_repository._execute_query.call_args
|
||||
assert call_args[0][1] == (67890, "super_admin")
|
||||
|
||||
admin_repository.logger.info.assert_called_once_with(
|
||||
"Администратор добавлен: user_id=67890, role=super_admin"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_admin(self, admin_repository):
|
||||
"""Тест удаления администратора"""
|
||||
user_id = 12345
|
||||
await admin_repository.remove_admin(user_id)
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
admin_repository._execute_query.assert_called_once()
|
||||
call_args = admin_repository._execute_query.call_args
|
||||
|
||||
assert call_args[0][0] == "DELETE FROM admins WHERE user_id = ?"
|
||||
assert call_args[0][1] == (user_id,)
|
||||
|
||||
# Проверяем логирование
|
||||
admin_repository.logger.info.assert_called_once_with(
|
||||
"Администратор удален: user_id=12345"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_admin_true(self, admin_repository):
|
||||
"""Тест проверки администратора - пользователь является администратором"""
|
||||
user_id = 12345
|
||||
# Мокаем результат запроса - пользователь найден
|
||||
admin_repository._execute_query_with_result.return_value = [(1,)]
|
||||
|
||||
result = await admin_repository.is_admin(user_id)
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
admin_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = admin_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT 1 FROM admins WHERE user_id = ?"
|
||||
assert call_args[0][1] == (user_id,)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_admin_false(self, admin_repository):
|
||||
"""Тест проверки администратора - пользователь не является администратором"""
|
||||
user_id = 12345
|
||||
# Мокаем результат запроса - пользователь не найден
|
||||
admin_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await admin_repository.is_admin(user_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_admin_found(self, admin_repository):
|
||||
"""Тест получения информации об администраторе - администратор найден"""
|
||||
user_id = 12345
|
||||
# Мокаем результат запроса
|
||||
admin_repository._execute_query_with_result.return_value = [
|
||||
(12345, "admin", "1705312200")
|
||||
]
|
||||
|
||||
result = await admin_repository.get_admin(user_id)
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
admin_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = admin_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT user_id, role, created_at FROM admins WHERE user_id = ?"
|
||||
assert call_args[0][1] == (user_id,)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is not None
|
||||
assert result.user_id == 12345
|
||||
assert result.role == "admin"
|
||||
assert result.created_at == "1705312200"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_admin_not_found(self, admin_repository):
|
||||
"""Тест получения информации об администраторе - администратор не найден"""
|
||||
user_id = 12345
|
||||
# Мокаем результат запроса - администратор не найден
|
||||
admin_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await admin_repository.get_admin(user_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_admin_without_created_at(self, admin_repository):
|
||||
"""Тест получения информации об администраторе без даты создания"""
|
||||
user_id = 12345
|
||||
# Мокаем результат запроса без created_at
|
||||
admin_repository._execute_query_with_result.return_value = [
|
||||
(12345, "admin")
|
||||
]
|
||||
|
||||
result = await admin_repository.get_admin(user_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is not None
|
||||
assert result.user_id == 12345
|
||||
assert result.role == "admin"
|
||||
assert result.created_at is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_admin_error_handling(self, admin_repository, sample_admin):
|
||||
"""Тест обработки ошибок при добавлении администратора"""
|
||||
# Мокаем ошибку при выполнении запроса
|
||||
admin_repository._execute_query.side_effect = Exception("Database error")
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await admin_repository.add_admin(sample_admin)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_admin_error_handling(self, admin_repository):
|
||||
"""Тест обработки ошибок при удалении администратора"""
|
||||
# Мокаем ошибку при выполнении запроса
|
||||
admin_repository._execute_query.side_effect = Exception("Database error")
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await admin_repository.remove_admin(12345)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_admin_error_handling(self, admin_repository):
|
||||
"""Тест обработки ошибок при проверке администратора"""
|
||||
# Мокаем ошибку при выполнении запроса
|
||||
admin_repository._execute_query_with_result.side_effect = Exception("Database error")
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await admin_repository.is_admin(12345)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_admin_error_handling(self, admin_repository):
|
||||
"""Тест обработки ошибок при получении информации об администраторе"""
|
||||
# Мокаем ошибку при выполнении запроса
|
||||
admin_repository._execute_query_with_result.side_effect = Exception("Database error")
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await admin_repository.get_admin(12345)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables_error_handling(self, admin_repository):
|
||||
"""Тест обработки ошибок при создании таблиц"""
|
||||
# Мокаем ошибку при выполнении первого запроса
|
||||
admin_repository._execute_query.side_effect = Exception("Database error")
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await admin_repository.create_tables()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_model_compatibility(self, admin_repository):
|
||||
"""Тест совместимости с моделью Admin"""
|
||||
user_id = 12345
|
||||
role = "moderator"
|
||||
|
||||
# Создаем администратора с помощью модели
|
||||
admin = Admin(user_id=user_id, role=role)
|
||||
|
||||
# Проверяем, что можем передать его в репозиторий
|
||||
await admin_repository.add_admin(admin)
|
||||
|
||||
# Проверяем, что вызов был с правильными параметрами
|
||||
call_args = admin_repository._execute_query.call_args
|
||||
assert call_args[0][1] == (user_id, role)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_admin_operations(self, admin_repository):
|
||||
"""Тест множественных операций с администраторами"""
|
||||
# Добавляем первого администратора
|
||||
admin1 = Admin(user_id=111, role="admin")
|
||||
await admin_repository.add_admin(admin1)
|
||||
|
||||
# Добавляем второго администратора
|
||||
admin2 = Admin(user_id=222, role="moderator")
|
||||
await admin_repository.add_admin(admin2)
|
||||
|
||||
# Проверяем, что оба добавлены
|
||||
assert admin_repository._execute_query.call_count == 2
|
||||
|
||||
# Проверяем, что первый администратор существует
|
||||
admin_repository._execute_query_with_result.return_value = [(1,)]
|
||||
result1 = await admin_repository.is_admin(111)
|
||||
assert result1 is True
|
||||
|
||||
# Проверяем, что второй администратор существует
|
||||
result2 = await admin_repository.is_admin(222)
|
||||
assert result2 is True
|
||||
|
||||
# Удаляем первого администратора
|
||||
await admin_repository.remove_admin(111)
|
||||
|
||||
# Проверяем, что он больше не существует
|
||||
admin_repository._execute_query_with_result.return_value = []
|
||||
result3 = await admin_repository.is_admin(111)
|
||||
assert result3 is False
|
||||
@@ -1,190 +0,0 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
import sqlite3
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def temp_db():
|
||||
"""Создает временную базу данных для тестирования."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp:
|
||||
db_path = tmp.name
|
||||
|
||||
db = AsyncBotDB(db_path)
|
||||
yield db
|
||||
|
||||
# Очистка
|
||||
try:
|
||||
os.unlink(db_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def event_loop():
|
||||
"""Создает новый event loop для каждого теста."""
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables(temp_db):
|
||||
"""Тест создания таблиц."""
|
||||
await temp_db.create_tables()
|
||||
# Если не возникло исключение, значит таблицы созданы успешно
|
||||
assert True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_and_get_user(temp_db):
|
||||
"""Тест добавления и получения пользователя."""
|
||||
await temp_db.create_tables()
|
||||
|
||||
# Добавляем пользователя
|
||||
user_id = 12345
|
||||
first_name = "Test"
|
||||
full_name = "Test User"
|
||||
username = "testuser"
|
||||
|
||||
await temp_db.add_new_user(user_id, first_name, full_name, username)
|
||||
|
||||
# Проверяем существование
|
||||
exists = await temp_db.user_exists(user_id)
|
||||
assert exists is True
|
||||
|
||||
# Получаем информацию
|
||||
user_info = await temp_db.get_user_info(user_id)
|
||||
assert user_info is not None
|
||||
assert user_info['username'] == username
|
||||
assert user_info['full_name'] == full_name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blacklist_operations(temp_db):
|
||||
"""Тест операций с черным списком."""
|
||||
await temp_db.create_tables()
|
||||
|
||||
user_id = 12345
|
||||
user_name = "Test User"
|
||||
message = "Test ban"
|
||||
date_to_unban = "01-01-2025"
|
||||
|
||||
# Добавляем в черный список
|
||||
await temp_db.add_to_blacklist(user_id, user_name, message, date_to_unban)
|
||||
|
||||
# Проверяем наличие
|
||||
is_banned = await temp_db.check_blacklist(user_id)
|
||||
assert is_banned is True
|
||||
|
||||
# Получаем список
|
||||
banned_users = await temp_db.get_blacklist_users()
|
||||
assert len(banned_users) == 1
|
||||
assert banned_users[0][1] == user_id # user_id
|
||||
|
||||
# Удаляем из черного списка
|
||||
removed = await temp_db.remove_from_blacklist(user_id)
|
||||
assert removed is True
|
||||
|
||||
# Проверяем удаление
|
||||
is_banned = await temp_db.check_blacklist(user_id)
|
||||
assert is_banned is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(reason="FOREIGN KEY constraint failed - требует исправления порядка операций")
|
||||
async def test_admin_operations(temp_db):
|
||||
"""Тест операций с администраторами."""
|
||||
await temp_db.create_tables()
|
||||
|
||||
user_id = 12345
|
||||
role = "admin"
|
||||
|
||||
# Добавляем пользователя
|
||||
await temp_db.add_new_user(user_id, "Test", "Test User", "testuser")
|
||||
|
||||
# Добавляем администратора
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
await temp_db.add_admin(user_id, role)
|
||||
|
||||
# # Проверяем права
|
||||
# is_admin = await temp_db.is_admin(user_id)
|
||||
# assert is_admin is True
|
||||
|
||||
# # Удаляем администратора
|
||||
# await temp_db.remove_admin(user_id)
|
||||
|
||||
# # Проверяем удаление
|
||||
# is_admin = await temp_db.is_admin(user_id)
|
||||
# assert is_admin is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(reason="FOREIGN KEY constraint failed - требует исправления порядка операций")
|
||||
async def test_audio_operations(temp_db):
|
||||
"""Тест операций с аудио."""
|
||||
await temp_db.create_tables()
|
||||
|
||||
user_id = 12345
|
||||
file_name = "test_audio.mp3"
|
||||
file_id = "test_file_id"
|
||||
|
||||
# Добавляем пользователя
|
||||
await temp_db.add_new_user(user_id, "Test", "Test User", "testuser")
|
||||
|
||||
# Добавляем аудио запись
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
await temp_db.add_audio_record(file_name, user_id, file_id)
|
||||
|
||||
# # Получаем file_id
|
||||
# retrieved_file_id = await temp_db.get_audio_file_id(user_id)
|
||||
# assert retrieved_file_id == file_id
|
||||
|
||||
# # Получаем имя файла
|
||||
# retrieved_file_name = await temp_db.get_audio_file_name(user_id)
|
||||
# assert retrieved_file_name == file_name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(reason="FOREIGN KEY constraint failed - требует исправления порядка операций")
|
||||
async def test_post_operations(temp_db):
|
||||
"""Тест операций с постами."""
|
||||
await temp_db.create_tables()
|
||||
|
||||
message_id = 12345
|
||||
text = "Test post text"
|
||||
author_id = 67890
|
||||
|
||||
# Добавляем пользователя
|
||||
await temp_db.add_new_user(author_id, "Test", "Test User", "testuser")
|
||||
|
||||
# Добавляем пост
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
await temp_db.add_post(message_id, text, author_id)
|
||||
|
||||
# # Обновляем helper сообщение
|
||||
# helper_message_id = 54321
|
||||
# await temp_db.update_helper_message(message_id, helper_message_id)
|
||||
|
||||
# # Получаем текст поста
|
||||
# retrieved_text = await temp_db.get_post_text(helper_message_id)
|
||||
# assert retrieved_text == text
|
||||
|
||||
# # Получаем ID автора
|
||||
# retrieved_author_id = await temp_db.get_author_id_by_helper_message(helper_message_id)
|
||||
# assert retrieved_author_id == author_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_handling(temp_db):
|
||||
"""Тест обработки ошибок."""
|
||||
# Пытаемся получить пользователя без создания таблиц
|
||||
with pytest.raises(Exception):
|
||||
await temp_db.user_exists(12345)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Запуск тестов
|
||||
pytest.main([__file__, "-v"])
|
||||
266
tests/test_audio_file_service.py
Normal file
266
tests/test_audio_file_service.py
Normal file
@@ -0,0 +1,266 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from helper_bot.handlers.voice.services import AudioFileService
|
||||
from helper_bot.handlers.voice.exceptions import FileOperationError, DatabaseError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bot_db():
|
||||
"""Мок для базы данных"""
|
||||
mock_db = Mock()
|
||||
mock_db.get_user_audio_records_count = AsyncMock(return_value=0)
|
||||
mock_db.get_path_for_audio_record = AsyncMock(return_value=None)
|
||||
mock_db.add_audio_record_simple = AsyncMock()
|
||||
return mock_db
|
||||
|
||||
@pytest.fixture
|
||||
def audio_service(mock_bot_db):
|
||||
"""Экземпляр AudioFileService для тестов"""
|
||||
return AudioFileService(mock_bot_db)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_datetime():
|
||||
"""Тестовая дата"""
|
||||
return datetime(2025, 1, 15, 14, 30, 0)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bot():
|
||||
"""Мок для бота"""
|
||||
bot = Mock()
|
||||
bot.get_file = AsyncMock()
|
||||
bot.download_file = AsyncMock()
|
||||
return bot
|
||||
|
||||
@pytest.fixture
|
||||
def mock_message():
|
||||
"""Мок для сообщения"""
|
||||
message = Mock()
|
||||
message.voice = Mock()
|
||||
message.voice.file_id = "test_file_id"
|
||||
return message
|
||||
|
||||
@pytest.fixture
|
||||
def mock_file_info():
|
||||
"""Мок для информации о файле"""
|
||||
file_info = Mock()
|
||||
file_info.file_path = "voice/test_file_id.ogg"
|
||||
return file_info
|
||||
|
||||
|
||||
class TestGenerateFileName:
|
||||
"""Тесты для метода generate_file_name"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_file_name_first_record(self, audio_service, mock_bot_db):
|
||||
"""Тест генерации имени файла для первой записи пользователя"""
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 0
|
||||
|
||||
result = await audio_service.generate_file_name(12345)
|
||||
|
||||
assert result == "message_from_12345_number_1"
|
||||
mock_bot_db.get_user_audio_records_count.assert_called_once_with(user_id=12345)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_file_name_existing_records(self, audio_service, mock_bot_db):
|
||||
"""Тест генерации имени файла для существующих записей"""
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 3
|
||||
mock_bot_db.get_path_for_audio_record.return_value = "message_from_12345_number_3"
|
||||
|
||||
result = await audio_service.generate_file_name(12345)
|
||||
|
||||
assert result == "message_from_12345_number_4"
|
||||
mock_bot_db.get_user_audio_records_count.assert_called_once_with(user_id=12345)
|
||||
mock_bot_db.get_path_for_audio_record.assert_called_once_with(user_id=12345)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_file_name_no_last_record(self, audio_service, mock_bot_db):
|
||||
"""Тест генерации имени файла когда нет последней записи"""
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 2
|
||||
mock_bot_db.get_path_for_audio_record.return_value = None
|
||||
|
||||
result = await audio_service.generate_file_name(12345)
|
||||
|
||||
assert result == "message_from_12345_number_3"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_file_name_invalid_last_record_format(self, audio_service, mock_bot_db):
|
||||
"""Тест генерации имени файла с некорректным форматом последней записи"""
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 2
|
||||
mock_bot_db.get_path_for_audio_record.return_value = "invalid_format"
|
||||
|
||||
result = await audio_service.generate_file_name(12345)
|
||||
|
||||
assert result == "message_from_12345_number_3"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_file_name_exception_handling(self, audio_service, mock_bot_db):
|
||||
"""Тест обработки исключений при генерации имени файла"""
|
||||
mock_bot_db.get_user_audio_records_count.side_effect = Exception("Database error")
|
||||
|
||||
with pytest.raises(FileOperationError) as exc_info:
|
||||
await audio_service.generate_file_name(12345)
|
||||
|
||||
assert "Не удалось сгенерировать имя файла" in str(exc_info.value)
|
||||
|
||||
|
||||
class TestSaveAudioFile:
|
||||
"""Тесты для метода save_audio_file"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_audio_file_success(self, audio_service, mock_bot_db, sample_datetime):
|
||||
"""Тест успешного сохранения аудио файла"""
|
||||
file_name = "test_audio.ogg"
|
||||
user_id = 12345
|
||||
file_id = "test_file_id"
|
||||
|
||||
await audio_service.save_audio_file(file_name, user_id, sample_datetime, file_id)
|
||||
|
||||
mock_bot_db.add_audio_record_simple.assert_called_once_with(file_name, user_id, sample_datetime)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_audio_file_with_string_date(self, audio_service, mock_bot_db):
|
||||
"""Тест сохранения аудио файла со строковой датой"""
|
||||
file_name = "test_audio.ogg"
|
||||
user_id = 12345
|
||||
date_string = "2025-01-15 14:30:00"
|
||||
file_id = "test_file_id"
|
||||
|
||||
await audio_service.save_audio_file(file_name, user_id, date_string, file_id)
|
||||
|
||||
mock_bot_db.add_audio_record_simple.assert_called_once_with(file_name, user_id, date_string)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_audio_file_exception_handling(self, audio_service, mock_bot_db, sample_datetime):
|
||||
"""Тест обработки исключений при сохранении аудио файла"""
|
||||
mock_bot_db.add_audio_record_simple.side_effect = Exception("Database error")
|
||||
|
||||
with pytest.raises(DatabaseError) as exc_info:
|
||||
await audio_service.save_audio_file("test.ogg", 12345, sample_datetime, "file_id")
|
||||
|
||||
assert "Не удалось сохранить аудио файл в БД" in str(exc_info.value)
|
||||
|
||||
|
||||
class TestDownloadAndSaveAudio:
|
||||
"""Тесты для метода download_and_save_audio"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_and_save_audio_success(self, audio_service, mock_bot, mock_message, mock_file_info):
|
||||
"""Тест успешного скачивания и сохранения аудио"""
|
||||
mock_bot.get_file.return_value = mock_file_info
|
||||
|
||||
# Мокаем скачанный файл
|
||||
mock_downloaded_file = Mock()
|
||||
mock_downloaded_file.tell.return_value = 0
|
||||
mock_downloaded_file.seek = Mock()
|
||||
mock_downloaded_file.read.return_value = b"audio_data"
|
||||
mock_bot.download_file.return_value = mock_downloaded_file
|
||||
|
||||
with patch('builtins.open', mock_open()) as mock_file:
|
||||
with patch('os.makedirs'):
|
||||
await audio_service.download_and_save_audio(mock_bot, mock_message, "test_audio")
|
||||
|
||||
mock_bot.get_file.assert_called_once_with(file_id="test_file_id")
|
||||
mock_bot.download_file.assert_called_once_with(file_path="voice/test_file_id.ogg")
|
||||
mock_file.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_and_save_audio_no_message(self, audio_service, mock_bot):
|
||||
"""Тест скачивания когда сообщение отсутствует"""
|
||||
with pytest.raises(FileOperationError) as exc_info:
|
||||
await audio_service.download_and_save_audio(mock_bot, None, "test_audio")
|
||||
|
||||
assert "Сообщение или голосовое сообщение не найдено" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_and_save_audio_no_voice(self, audio_service, mock_bot):
|
||||
"""Тест скачивания когда у сообщения нет voice атрибута"""
|
||||
message = Mock()
|
||||
message.voice = None
|
||||
|
||||
with pytest.raises(FileOperationError) as exc_info:
|
||||
await audio_service.download_and_save_audio(mock_bot, message, "test_audio")
|
||||
|
||||
assert "Сообщение или голосовое сообщение не найдено" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_and_save_audio_download_failed(self, audio_service, mock_bot, mock_message, mock_file_info):
|
||||
"""Тест скачивания когда загрузка не удалась"""
|
||||
mock_bot.get_file.return_value = mock_file_info
|
||||
mock_bot.download_file.return_value = None
|
||||
|
||||
with pytest.raises(FileOperationError) as exc_info:
|
||||
await audio_service.download_and_save_audio(mock_bot, mock_message, "test_audio")
|
||||
|
||||
assert "Не удалось скачать файл" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_and_save_audio_exception_handling(self, audio_service, mock_bot, mock_message):
|
||||
"""Тест обработки исключений при скачивании"""
|
||||
mock_bot.get_file.side_effect = Exception("Network error")
|
||||
|
||||
with pytest.raises(FileOperationError) as exc_info:
|
||||
await audio_service.download_and_save_audio(mock_bot, mock_message, "test_audio")
|
||||
|
||||
assert "Не удалось скачать и сохранить аудио" in str(exc_info.value)
|
||||
|
||||
|
||||
def mock_open():
|
||||
"""Мок для функции open"""
|
||||
from unittest.mock import mock_open as _mock_open
|
||||
return _mock_open()
|
||||
|
||||
|
||||
class TestAudioFileServiceIntegration:
|
||||
"""Интеграционные тесты для AudioFileService"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_audio_processing_workflow(self, mock_bot_db):
|
||||
"""Тест полного рабочего процесса обработки аудио"""
|
||||
service = AudioFileService(mock_bot_db)
|
||||
|
||||
# Настраиваем моки
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 1
|
||||
mock_bot_db.get_path_for_audio_record.return_value = "message_from_12345_number_1"
|
||||
mock_bot_db.add_audio_record_simple = AsyncMock()
|
||||
|
||||
# Тестируем генерацию имени файла
|
||||
file_name = await service.generate_file_name(12345)
|
||||
assert file_name == "message_from_12345_number_2"
|
||||
|
||||
# Тестируем сохранение в БД
|
||||
test_date = datetime.now()
|
||||
await service.save_audio_file(file_name, 12345, test_date, "test_file_id")
|
||||
|
||||
# Проверяем вызовы
|
||||
mock_bot_db.get_user_audio_records_count.assert_called_once_with(user_id=12345)
|
||||
mock_bot_db.get_path_for_audio_record.assert_called_once_with(user_id=12345)
|
||||
mock_bot_db.add_audio_record_simple.assert_called_once_with(file_name, 12345, test_date)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_name_generation_sequence(self, mock_bot_db):
|
||||
"""Тест последовательности генерации имен файлов"""
|
||||
service = AudioFileService(mock_bot_db)
|
||||
|
||||
# Первая запись
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 0
|
||||
file_name_1 = await service.generate_file_name(12345)
|
||||
assert file_name_1 == "message_from_12345_number_1"
|
||||
|
||||
# Вторая запись
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 1
|
||||
mock_bot_db.get_path_for_audio_record.return_value = "message_from_12345_number_1"
|
||||
file_name_2 = await service.generate_file_name(12345)
|
||||
assert file_name_2 == "message_from_12345_number_2"
|
||||
|
||||
# Третья запись
|
||||
mock_bot_db.get_user_audio_records_count.return_value = 2
|
||||
mock_bot_db.get_path_for_audio_record.return_value = "message_from_12345_number_2"
|
||||
file_name_3 = await service.generate_file_name(12345)
|
||||
assert file_name_3 == "message_from_12345_number_3"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
408
tests/test_audio_repository.py
Normal file
408
tests/test_audio_repository.py
Normal file
@@ -0,0 +1,408 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
from database.models import AudioMessage, AudioListenRecord, AudioModerate
|
||||
|
||||
|
||||
class TestAudioRepository:
|
||||
"""Тесты для AudioRepository"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_connection(self):
|
||||
"""Мок для DatabaseConnection"""
|
||||
mock_connection = Mock()
|
||||
mock_connection._execute_query = AsyncMock()
|
||||
mock_connection._execute_query_with_result = AsyncMock()
|
||||
mock_connection.logger = Mock()
|
||||
return mock_connection
|
||||
|
||||
@pytest.fixture
|
||||
def audio_repository(self, mock_db_connection):
|
||||
"""Экземпляр AudioRepository для тестов"""
|
||||
# Патчим наследование от DatabaseConnection
|
||||
with patch.object(AudioRepository, '__init__', return_value=None):
|
||||
repo = AudioRepository()
|
||||
repo._execute_query = mock_db_connection._execute_query
|
||||
repo._execute_query_with_result = mock_db_connection._execute_query_with_result
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def sample_audio_message(self):
|
||||
"""Тестовое аудио сообщение"""
|
||||
return AudioMessage(
|
||||
file_name="test_audio_123.ogg",
|
||||
author_id=12345,
|
||||
date_added="2025-01-15 14:30:00",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_datetime(self):
|
||||
"""Тестовая дата"""
|
||||
return datetime(2025, 1, 15, 14, 30, 0)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_timestamp(self):
|
||||
"""Тестовый UNIX timestamp"""
|
||||
return int(time.mktime(datetime(2025, 1, 15, 14, 30, 0).timetuple()))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enable_foreign_keys(self, audio_repository):
|
||||
"""Тест включения внешних ключей"""
|
||||
await audio_repository.enable_foreign_keys()
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with("PRAGMA foreign_keys = ON;")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables(self, audio_repository):
|
||||
"""Тест создания таблиц"""
|
||||
await audio_repository.create_tables()
|
||||
|
||||
# Проверяем, что все три таблицы созданы
|
||||
assert audio_repository._execute_query.call_count == 3
|
||||
|
||||
# Проверяем вызовы для каждой таблицы
|
||||
calls = audio_repository._execute_query.call_args_list
|
||||
assert any("audio_message_reference" in str(call) for call in calls)
|
||||
assert any("user_audio_listens" in str(call) for call in calls)
|
||||
assert any("audio_moderate" in str(call) for call in calls)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_with_string_date(self, audio_repository, sample_audio_message):
|
||||
"""Тест добавления аудио записи со строковой датой"""
|
||||
await audio_repository.add_audio_record(sample_audio_message)
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
audio_repository._execute_query.assert_called_once()
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
assert call_args[0][0] == """
|
||||
INSERT INTO audio_message_reference (file_name, author_id, date_added)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
# Проверяем, что date_added преобразован в timestamp
|
||||
assert call_args[0][1][0] == "test_audio_123.ogg"
|
||||
assert call_args[0][1][1] == 12345
|
||||
assert isinstance(call_args[0][1][2], int) # timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_with_datetime_date(self, audio_repository):
|
||||
"""Тест добавления аудио записи с datetime датой"""
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio_456.ogg",
|
||||
author_id=67890,
|
||||
date_added=datetime(2025, 1, 20, 10, 15, 0),
|
||||
file_id="test_file_id_2",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
# Проверяем, что date_added преобразован в timestamp
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
assert isinstance(call_args[0][1][2], int) # timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_with_timestamp_date(self, audio_repository):
|
||||
"""Тест добавления аудио записи с timestamp датой"""
|
||||
timestamp = int(time.time())
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio_789.ogg",
|
||||
author_id=11111,
|
||||
date_added=timestamp,
|
||||
file_id="test_file_id_3",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
# Проверяем, что date_added остался timestamp
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
assert call_args[0][1][2] == timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_with_string_date(self, audio_repository):
|
||||
"""Тест упрощенного добавления аудио записи со строковой датой"""
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, "2025-01-15 14:30:00")
|
||||
|
||||
# Проверяем, что метод вызван
|
||||
audio_repository._execute_query.assert_called_once()
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
assert call_args[0][1][0] == "test_audio.ogg" # file_name
|
||||
assert call_args[0][1][1] == 12345 # user_id
|
||||
assert isinstance(call_args[0][1][2], int) # timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_with_datetime_date(self, audio_repository, sample_datetime):
|
||||
"""Тест упрощенного добавления аудио записи с datetime датой"""
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, sample_datetime)
|
||||
|
||||
# Проверяем, что date_added преобразован в timestamp
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
assert isinstance(call_args[0][1][2], int) # timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_last_date_audio(self, audio_repository):
|
||||
"""Тест получения даты последнего аудио"""
|
||||
expected_timestamp = 1642248600 # 2022-01-17 10:30:00
|
||||
audio_repository._execute_query_with_result.return_value = [(expected_timestamp,)]
|
||||
|
||||
result = await audio_repository.get_last_date_audio()
|
||||
|
||||
assert result == expected_timestamp
|
||||
audio_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT date_added FROM audio_message_reference ORDER BY date_added DESC LIMIT 1"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_last_date_audio_no_records(self, audio_repository):
|
||||
"""Тест получения даты последнего аудио когда записей нет"""
|
||||
audio_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await audio_repository.get_last_date_audio()
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_audio_records_count(self, audio_repository):
|
||||
"""Тест получения количества аудио записей пользователя"""
|
||||
audio_repository._execute_query_with_result.return_value = [(5,)]
|
||||
|
||||
result = await audio_repository.get_user_audio_records_count(12345)
|
||||
|
||||
assert result == 5
|
||||
audio_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT COUNT(*) FROM audio_message_reference WHERE author_id = ?", (12345,)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_path_for_audio_record(self, audio_repository):
|
||||
"""Тест получения пути к аудио записи пользователя"""
|
||||
audio_repository._execute_query_with_result.return_value = [("test_audio.ogg",)]
|
||||
|
||||
result = await audio_repository.get_path_for_audio_record(12345)
|
||||
|
||||
assert result == "test_audio.ogg"
|
||||
audio_repository._execute_query_with_result.assert_called_once_with(
|
||||
"""
|
||||
SELECT file_name FROM audio_message_reference
|
||||
WHERE author_id = ? ORDER BY date_added DESC LIMIT 1
|
||||
""", (12345,)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_path_for_audio_record_no_records(self, audio_repository):
|
||||
"""Тест получения пути к аудио записи когда записей нет"""
|
||||
audio_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await audio_repository.get_path_for_audio_record(12345)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_listen_audio(self, audio_repository):
|
||||
"""Тест проверки непрослушанных аудио"""
|
||||
# Мокаем результаты запросов
|
||||
audio_repository._execute_query_with_result.side_effect = [
|
||||
[("audio1.ogg",), ("audio2.ogg",)], # прослушанные
|
||||
[("audio1.ogg",), ("audio2.ogg",), ("audio3.ogg",)] # все аудио
|
||||
]
|
||||
|
||||
result = await audio_repository.check_listen_audio(12345)
|
||||
|
||||
# Должно вернуться только непрослушанные (audio3.ogg)
|
||||
assert result == ["audio3.ogg"]
|
||||
assert audio_repository._execute_query_with_result.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_listened_audio(self, audio_repository):
|
||||
"""Тест отметки аудио как прослушанного"""
|
||||
await audio_repository.mark_listened_audio("test_audio.ogg", 12345)
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with(
|
||||
"INSERT OR IGNORE INTO user_audio_listens (file_name, user_id) VALUES (?, ?)",
|
||||
("test_audio.ogg", 12345)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_id_by_file_name(self, audio_repository):
|
||||
"""Тест получения user_id по имени файла"""
|
||||
audio_repository._execute_query_with_result.return_value = [(12345,)]
|
||||
|
||||
result = await audio_repository.get_user_id_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == 12345
|
||||
audio_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT author_id FROM audio_message_reference WHERE file_name = ?", ("test_audio.ogg",)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_id_by_file_name_not_found(self, audio_repository):
|
||||
"""Тест получения user_id по имени файла когда файл не найден"""
|
||||
audio_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await audio_repository.get_user_id_by_file_name("nonexistent.ogg")
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name(self, audio_repository):
|
||||
"""Тест получения даты по имени файла"""
|
||||
timestamp = 1642404600 # 2022-01-17 10:30:00
|
||||
audio_repository._execute_query_with_result.return_value = [(timestamp,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
# Должна вернуться читаемая дата
|
||||
assert result == "17.01.2022 10:30"
|
||||
audio_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT date_added FROM audio_message_reference WHERE file_name = ?", ("test_audio.ogg",)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_not_found(self, audio_repository):
|
||||
"""Тест получения даты по имени файла когда файл не найден"""
|
||||
audio_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("nonexistent.ogg")
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_listen_audio(self, audio_repository):
|
||||
"""Тест очистки записей прослушивания пользователя"""
|
||||
await audio_repository.refresh_listen_audio(12345)
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with(
|
||||
"DELETE FROM user_audio_listens WHERE user_id = ?", (12345,)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_listen_count_for_user(self, audio_repository):
|
||||
"""Тест удаления данных о прослушанных аудио пользователя"""
|
||||
await audio_repository.delete_listen_count_for_user(12345)
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with(
|
||||
"DELETE FROM user_audio_listens WHERE user_id = ?", (12345,)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_user_id_and_message_id_for_voice_bot_success(self, audio_repository):
|
||||
"""Тест успешной установки связи для voice bot"""
|
||||
result = await audio_repository.set_user_id_and_message_id_for_voice_bot(123, 456)
|
||||
|
||||
assert result is True
|
||||
audio_repository._execute_query.assert_called_once_with(
|
||||
"INSERT OR IGNORE INTO audio_moderate (user_id, message_id) VALUES (?, ?)",
|
||||
(456, 123)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_user_id_and_message_id_for_voice_bot_exception(self, audio_repository):
|
||||
"""Тест установки связи для voice bot при ошибке"""
|
||||
audio_repository._execute_query.side_effect = Exception("Database error")
|
||||
|
||||
result = await audio_repository.set_user_id_and_message_id_for_voice_bot(123, 456)
|
||||
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_id_by_message_id_for_voice_bot(self, audio_repository):
|
||||
"""Тест получения user_id по message_id для voice bot"""
|
||||
audio_repository._execute_query_with_result.return_value = [(456,)]
|
||||
|
||||
result = await audio_repository.get_user_id_by_message_id_for_voice_bot(123)
|
||||
|
||||
assert result == 456
|
||||
audio_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT user_id FROM audio_moderate WHERE message_id = ?", (123,)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_id_by_message_id_for_voice_bot_not_found(self, audio_repository):
|
||||
"""Тест получения user_id по message_id когда связь не найдена"""
|
||||
audio_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await audio_repository.get_user_id_by_message_id_for_voice_bot(123)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_audio_moderate_record(self, audio_repository):
|
||||
"""Тест удаления записи из таблицы audio_moderate"""
|
||||
message_id = 12345
|
||||
|
||||
await audio_repository.delete_audio_moderate_record(message_id)
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with(
|
||||
"DELETE FROM audio_moderate WHERE message_id = ?", (message_id,)
|
||||
)
|
||||
audio_repository.logger.info.assert_called_once_with(
|
||||
f"Удалена запись из audio_moderate для message_id {message_id}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_logging(self, audio_repository, sample_audio_message):
|
||||
"""Тест логирования при добавлении аудио записи"""
|
||||
await audio_repository.add_audio_record(sample_audio_message)
|
||||
|
||||
# Проверяем, что лог записан
|
||||
audio_repository.logger.info.assert_called_once()
|
||||
log_message = audio_repository.logger.info.call_args[0][0]
|
||||
assert "Аудио добавлено" in log_message
|
||||
assert "test_audio_123.ogg" in log_message
|
||||
assert "12345" in log_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_logging(self, audio_repository):
|
||||
"""Тест логирования при упрощенном добавлении аудио записи"""
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, "2025-01-15 14:30:00")
|
||||
|
||||
# Проверяем, что лог записан
|
||||
audio_repository.logger.info.assert_called_once()
|
||||
log_message = audio_repository.logger.info.call_args[0][0]
|
||||
assert "Аудио добавлено" in log_message
|
||||
assert "test_audio.ogg" in log_message
|
||||
assert "12345" in log_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_logging(self, audio_repository):
|
||||
"""Тест логирования при получении даты по имени файла"""
|
||||
timestamp = 1642404600 # 2022-01-17 10:30:00
|
||||
audio_repository._execute_query_with_result.return_value = [(timestamp,)]
|
||||
|
||||
await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
# Проверяем, что лог записан
|
||||
audio_repository.logger.info.assert_called_once()
|
||||
log_message = audio_repository.logger.info.call_args[0][0]
|
||||
assert "Получена дата" in log_message
|
||||
assert "17.01.2022 10:30" in log_message
|
||||
assert "test_audio.ogg" in log_message
|
||||
|
||||
|
||||
class TestAudioRepositoryIntegration:
|
||||
"""Интеграционные тесты для AudioRepository"""
|
||||
|
||||
@pytest.fixture
|
||||
def real_audio_repository(self):
|
||||
"""Реальный экземпляр AudioRepository для интеграционных тестов"""
|
||||
# Здесь можно создать реальное подключение к тестовой БД
|
||||
# Но для простоты используем мок
|
||||
return Mock()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_audio_workflow(self, real_audio_repository):
|
||||
"""Тест полного рабочего процесса с аудио"""
|
||||
# Этот тест можно расширить для реальной БД
|
||||
assert True # Placeholder для будущих интеграционных тестов
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_foreign_keys_enabled(self, real_audio_repository):
|
||||
"""Тест что внешние ключи включены"""
|
||||
# Этот тест можно расширить для реальной БД
|
||||
assert True # Placeholder для будущих интеграционных тестов
|
||||
397
tests/test_audio_repository_schema.py
Normal file
397
tests/test_audio_repository_schema.py
Normal file
@@ -0,0 +1,397 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
|
||||
|
||||
class TestAudioRepositoryNewSchema:
|
||||
"""Тесты для AudioRepository с новой схемой БД"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_connection(self):
|
||||
"""Мок для DatabaseConnection"""
|
||||
mock_connection = Mock()
|
||||
mock_connection._execute_query = AsyncMock()
|
||||
mock_connection._execute_query_with_result = AsyncMock()
|
||||
mock_connection.logger = Mock()
|
||||
return mock_connection
|
||||
|
||||
@pytest.fixture
|
||||
def audio_repository(self, mock_db_connection):
|
||||
"""Экземпляр AudioRepository для тестов"""
|
||||
with patch.object(AudioRepository, '__init__', return_value=None):
|
||||
repo = AudioRepository()
|
||||
repo._execute_query = mock_db_connection._execute_query
|
||||
repo._execute_query_with_result = mock_db_connection._execute_query_with_result
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables_new_schema(self, audio_repository):
|
||||
"""Тест создания таблиц с новой схемой БД"""
|
||||
await audio_repository.create_tables()
|
||||
|
||||
# Проверяем, что все три таблицы созданы
|
||||
assert audio_repository._execute_query.call_count == 3
|
||||
|
||||
# Получаем все вызовы
|
||||
calls = audio_repository._execute_query.call_args_list
|
||||
|
||||
# Проверяем таблицу audio_message_reference
|
||||
audio_table_call = next(call for call in calls if "audio_message_reference" in str(call))
|
||||
assert "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT" in str(audio_table_call)
|
||||
assert "file_name TEXT NOT NULL UNIQUE" in str(audio_table_call)
|
||||
assert "author_id INTEGER NOT NULL" in str(audio_table_call)
|
||||
assert "date_added INTEGER NOT NULL" in str(audio_table_call)
|
||||
assert "FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in str(audio_table_call)
|
||||
|
||||
# Проверяем таблицу user_audio_listens
|
||||
listens_table_call = next(call for call in calls if "user_audio_listens" in str(call))
|
||||
assert "file_name TEXT NOT NULL" in str(listens_table_call)
|
||||
assert "user_id INTEGER NOT NULL" in str(listens_table_call)
|
||||
assert "PRIMARY KEY (file_name, user_id)" in str(listens_table_call)
|
||||
assert "FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in str(listens_table_call)
|
||||
|
||||
# Проверяем таблицу audio_moderate
|
||||
moderate_table_call = next(call for call in calls if "audio_moderate" in str(call))
|
||||
assert "user_id INTEGER NOT NULL" in str(moderate_table_call)
|
||||
assert "message_id INTEGER" in str(moderate_table_call)
|
||||
assert "PRIMARY KEY (user_id, message_id)" in str(moderate_table_call)
|
||||
assert "FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in str(moderate_table_call)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_string_date_conversion(self, audio_repository):
|
||||
"""Тест преобразования строковой даты в UNIX timestamp"""
|
||||
from database.models import AudioMessage
|
||||
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio.ogg",
|
||||
author_id=12345,
|
||||
date_added="2025-01-15 14:30:00",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
# Проверяем, что метод вызван
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
|
||||
# Проверяем параметры
|
||||
assert params[0] == "test_audio.ogg"
|
||||
assert params[1] == 12345
|
||||
assert isinstance(params[2], int) # timestamp
|
||||
|
||||
# Проверяем, что timestamp соответствует дате
|
||||
expected_timestamp = int(datetime(2025, 1, 15, 14, 30, 0).timestamp())
|
||||
assert params[2] == expected_timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_datetime_conversion(self, audio_repository):
|
||||
"""Тест преобразования datetime в UNIX timestamp"""
|
||||
from database.models import AudioMessage
|
||||
|
||||
test_datetime = datetime(2025, 1, 20, 10, 15, 30)
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio.ogg",
|
||||
author_id=12345,
|
||||
date_added=test_datetime,
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
# Проверяем параметры
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
|
||||
expected_timestamp = int(test_datetime.timestamp())
|
||||
assert params[2] == expected_timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_timestamp_no_conversion(self, audio_repository):
|
||||
"""Тест что timestamp остается timestamp без преобразования"""
|
||||
from database.models import AudioMessage
|
||||
|
||||
test_timestamp = int(time.time())
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio.ogg",
|
||||
author_id=12345,
|
||||
date_added=test_timestamp,
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
# Проверяем параметры
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
|
||||
assert params[2] == test_timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_string_date(self, audio_repository):
|
||||
"""Тест упрощенного добавления со строковой датой"""
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, "2025-01-15 14:30:00")
|
||||
|
||||
# Проверяем параметры
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
|
||||
assert params[0] == "test_audio.ogg"
|
||||
assert params[1] == 12345
|
||||
assert isinstance(params[2], int) # timestamp
|
||||
|
||||
# Проверяем timestamp
|
||||
expected_timestamp = int(datetime(2025, 1, 15, 14, 30, 0).timestamp())
|
||||
assert params[2] == expected_timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_datetime(self, audio_repository):
|
||||
"""Тест упрощенного добавления с datetime"""
|
||||
test_datetime = datetime(2025, 1, 25, 16, 45, 0)
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, test_datetime)
|
||||
|
||||
# Проверяем параметры
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
|
||||
expected_timestamp = int(test_datetime.timestamp())
|
||||
assert params[2] == expected_timestamp
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_timestamp_conversion(self, audio_repository):
|
||||
"""Тест преобразования UNIX timestamp в читаемую дату"""
|
||||
test_timestamp = 1642248600 # 2022-01-17 10:30:00
|
||||
audio_repository._execute_query_with_result.return_value = [(test_timestamp,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
# Должна вернуться читаемая дата в формате dd.mm.yyyy HH:MM
|
||||
assert result == "15.01.2022 15:10"
|
||||
assert isinstance(result, str)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_different_timestamp(self, audio_repository):
|
||||
"""Тест преобразования другого timestamp в читаемую дату"""
|
||||
test_timestamp = 1705312800 # 2024-01-16 12:00:00
|
||||
audio_repository._execute_query_with_result.return_value = [(test_timestamp,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == "15.01.2024 13:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_midnight(self, audio_repository):
|
||||
"""Тест преобразования timestamp для полуночи"""
|
||||
test_timestamp = 1705190400 # 2024-01-15 00:00:00
|
||||
audio_repository._execute_query_with_result.return_value = [(test_timestamp,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == "14.01.2024 03:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_year_end(self, audio_repository):
|
||||
"""Тест преобразования timestamp для конца года"""
|
||||
test_timestamp = 1704067200 # 2023-12-31 23:59:59
|
||||
audio_repository._execute_query_with_result.return_value = [(test_timestamp,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == "01.01.2024 03:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_foreign_keys_enabled_called(self, audio_repository):
|
||||
"""Тест что метод enable_foreign_keys вызывается"""
|
||||
await audio_repository.enable_foreign_keys()
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with("PRAGMA foreign_keys = ON;")
|
||||
audio_repository.logger.info.assert_not_called() # Этот метод не логирует
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables_logging(self, audio_repository):
|
||||
"""Тест логирования при создании таблиц"""
|
||||
await audio_repository.create_tables()
|
||||
|
||||
# Проверяем, что лог записан
|
||||
audio_repository.logger.info.assert_called_once_with("Таблицы для аудио созданы")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_logging_format(self, audio_repository):
|
||||
"""Тест формата лога при добавлении аудио записи"""
|
||||
from database.models import AudioMessage
|
||||
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio.ogg",
|
||||
author_id=12345,
|
||||
date_added="2025-01-15 14:30:00",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
# Проверяем формат лога
|
||||
log_call = audio_repository.logger.info.call_args
|
||||
log_message = log_call[0][0]
|
||||
|
||||
assert "Аудио добавлено:" in log_message
|
||||
assert "file_name=test_audio.ogg" in log_message
|
||||
assert "author_id=12345" in log_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_logging_format(self, audio_repository):
|
||||
"""Тест формата лога при упрощенном добавлении"""
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, "2025-01-15 14:30:00")
|
||||
|
||||
# Проверяем формат лога
|
||||
log_call = audio_repository.logger.info.call_args
|
||||
log_message = log_call[0][0]
|
||||
|
||||
assert "Аудио добавлено:" in log_message
|
||||
assert "file_name=test_audio.ogg" in log_message
|
||||
assert "user_id=12345" in log_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_logging_format(self, audio_repository):
|
||||
"""Тест формата лога при получении даты"""
|
||||
test_timestamp = 1642248600 # 2022-01-17 10:30:00
|
||||
audio_repository._execute_query_with_result.return_value = [(test_timestamp,)]
|
||||
|
||||
await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
# Проверяем формат лога
|
||||
log_call = audio_repository.logger.info.call_args
|
||||
log_message = log_call[0][0]
|
||||
|
||||
assert "Получена дата" in log_message
|
||||
assert "15.01.2022 15:10" in log_message
|
||||
assert "test_audio.ogg" in log_message
|
||||
|
||||
|
||||
class TestAudioRepositoryEdgeCases:
|
||||
"""Тесты граничных случаев для AudioRepository"""
|
||||
|
||||
@pytest.fixture
|
||||
def audio_repository(self):
|
||||
"""Экземпляр AudioRepository для тестов"""
|
||||
with patch.object(AudioRepository, '__init__', return_value=None):
|
||||
repo = AudioRepository()
|
||||
repo._execute_query = AsyncMock()
|
||||
repo._execute_query_with_result = AsyncMock()
|
||||
repo.logger = Mock()
|
||||
return repo
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_empty_string_date(self, audio_repository):
|
||||
"""Тест добавления с пустой строковой датой"""
|
||||
from database.models import AudioMessage
|
||||
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio.ogg",
|
||||
author_id=12345,
|
||||
date_added="",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
# Должно вызвать ValueError при парсинге пустой строки
|
||||
with pytest.raises(ValueError):
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_invalid_string_date(self, audio_repository):
|
||||
"""Тест добавления с некорректной строковой датой"""
|
||||
from database.models import AudioMessage
|
||||
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio.ogg",
|
||||
author_id=12345,
|
||||
date_added="invalid_date",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
# Должно вызвать ValueError при парсинге некорректной даты
|
||||
with pytest.raises(ValueError):
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_none_date(self, audio_repository):
|
||||
"""Тест добавления с None датой"""
|
||||
from database.models import AudioMessage
|
||||
|
||||
audio_msg = AudioMessage(
|
||||
file_name="test_audio.ogg",
|
||||
author_id=12345,
|
||||
date_added=None,
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
)
|
||||
|
||||
# Метод обрабатывает None как timestamp без преобразования
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
|
||||
# Проверяем, что метод был вызван с None
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
assert params[2] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_empty_string_date(self, audio_repository):
|
||||
"""Тест упрощенного добавления с пустой строковой датой"""
|
||||
# Должно вызвать ValueError при парсинге пустой строки
|
||||
with pytest.raises(ValueError):
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, "")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_invalid_string_date(self, audio_repository):
|
||||
"""Тест упрощенного добавления с некорректной строковой датой"""
|
||||
# Должно вызвать ValueError при парсинге некорректной даты
|
||||
with pytest.raises(ValueError):
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, "invalid_date")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_simple_none_date(self, audio_repository):
|
||||
"""Тест упрощенного добавления с None датой"""
|
||||
# Метод обрабатывает None как timestamp без преобразования
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, None)
|
||||
|
||||
# Проверяем, что метод был вызван с None
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
assert params[2] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_zero_timestamp(self, audio_repository):
|
||||
"""Тест получения даты для timestamp = 0 (1970-01-01)"""
|
||||
audio_repository._execute_query_with_result.return_value = [(0,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == "01.01.1970 03:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_negative_timestamp(self, audio_repository):
|
||||
"""Тест получения даты для отрицательного timestamp"""
|
||||
audio_repository._execute_query_with_result.return_value = [(-3600,)] # 1969-12-31 23:00:00
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == "01.01.1970 02:00"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_future_timestamp(self, audio_repository):
|
||||
"""Тест получения даты для будущего timestamp"""
|
||||
future_timestamp = int(datetime(2030, 12, 31, 23, 59, 59).timestamp())
|
||||
audio_repository._execute_query_with_result.return_value = [(future_timestamp,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == "31.12.2030 23:59"
|
||||
@@ -32,18 +32,18 @@ class TestAutoUnbanIntegration:
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
user_name TEXT,
|
||||
message_for_user TEXT,
|
||||
date_to_unban TEXT
|
||||
date_to_unban INTEGER
|
||||
)
|
||||
''')
|
||||
|
||||
# Добавляем тестовые данные
|
||||
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
|
||||
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
today_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
tomorrow_timestamp = int((datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).timestamp())
|
||||
|
||||
test_data = [
|
||||
(123, "test_user1", "Test ban 1", today), # Разблокируется сегодня
|
||||
(456, "test_user2", "Test ban 2", today), # Разблокируется сегодня
|
||||
(789, "test_user3", "Test ban 3", tomorrow), # Разблокируется завтра
|
||||
(123, "test_user1", "Test ban 1", today_timestamp), # Разблокируется сегодня
|
||||
(456, "test_user2", "Test ban 2", today_timestamp), # Разблокируется сегодня
|
||||
(789, "test_user3", "Test ban 3", tomorrow_timestamp), # Разблокируется завтра
|
||||
(999, "test_user4", "Test ban 4", None), # Навсегда заблокирован
|
||||
]
|
||||
|
||||
@@ -73,10 +73,9 @@ class TestAutoUnbanIntegration:
|
||||
}
|
||||
|
||||
# Создаем реальный экземпляр базы данных с тестовым файлом
|
||||
from database.db import BotDB
|
||||
from database.async_db import AsyncBotDB
|
||||
import os
|
||||
current_dir = os.getcwd()
|
||||
mock_factory.database = BotDB(current_dir, test_db_path)
|
||||
mock_factory.database = AsyncBotDB(test_db_path)
|
||||
|
||||
return mock_factory
|
||||
|
||||
@@ -110,14 +109,15 @@ class TestAutoUnbanIntegration:
|
||||
await scheduler.auto_unban_users()
|
||||
|
||||
# Проверяем, что пользователи с сегодняшней датой разблокированы
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?",
|
||||
(datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d"),))
|
||||
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?",
|
||||
(current_timestamp,))
|
||||
today_count = cursor.fetchone()[0]
|
||||
assert today_count == 0
|
||||
|
||||
# Проверяем, что пользователи с завтрашней датой остались
|
||||
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?", (tomorrow,))
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban > ?",
|
||||
(current_timestamp,))
|
||||
tomorrow_count = cursor.fetchone()[0]
|
||||
assert tomorrow_count == 1
|
||||
|
||||
@@ -146,8 +146,8 @@ class TestAutoUnbanIntegration:
|
||||
# Удаляем пользователей с сегодняшней датой
|
||||
conn = sqlite3.connect(setup_test_db)
|
||||
cursor = conn.cursor()
|
||||
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
|
||||
cursor.execute("DELETE FROM blacklist WHERE date_to_unban = ?", (today,))
|
||||
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
cursor.execute("DELETE FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?", (current_timestamp,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -195,7 +195,7 @@ class TestAutoUnbanIntegration:
|
||||
scheduler = AutoUnbanScheduler()
|
||||
scheduler.bot_db = mock_bdf.database
|
||||
|
||||
# Проверяем, что дата в базе соответствует ожидаемому формату
|
||||
# Проверяем, что дата в базе соответствует ожидаемому формату (timestamp)
|
||||
conn = sqlite3.connect(setup_test_db)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT date_to_unban FROM blacklist WHERE date_to_unban IS NOT NULL LIMIT 1")
|
||||
@@ -203,13 +203,13 @@ class TestAutoUnbanIntegration:
|
||||
conn.close()
|
||||
|
||||
if result and result[0]:
|
||||
date_str = result[0]
|
||||
# Проверяем формат YYYY-MM-DD
|
||||
assert len(date_str) == 10
|
||||
assert date_str.count('-') == 2
|
||||
assert date_str[:4].isdigit() # Год
|
||||
assert date_str[5:7].isdigit() # Месяц
|
||||
assert date_str[8:10].isdigit() # День
|
||||
timestamp = result[0]
|
||||
# Проверяем, что это валидный timestamp (целое число)
|
||||
assert isinstance(timestamp, int)
|
||||
assert timestamp > 0
|
||||
# Проверяем, что timestamp можно преобразовать в дату
|
||||
date_obj = datetime.fromtimestamp(timestamp)
|
||||
assert isinstance(date_obj, datetime)
|
||||
|
||||
|
||||
class TestSchedulerLifecycle:
|
||||
|
||||
@@ -18,11 +18,11 @@ class TestAutoUnbanScheduler:
|
||||
def mock_bot_db(self):
|
||||
"""Создает мок базы данных"""
|
||||
mock_db = Mock()
|
||||
mock_db.get_users_for_unblock_today.return_value = {
|
||||
mock_db.get_users_for_unblock_today = AsyncMock(return_value={
|
||||
123: "test_user1",
|
||||
456: "test_user2"
|
||||
}
|
||||
mock_db.delete_user_blacklist.return_value = True
|
||||
})
|
||||
mock_db.delete_user_blacklist = AsyncMock(return_value=True)
|
||||
return mock_db
|
||||
|
||||
@pytest.fixture
|
||||
@@ -78,7 +78,7 @@ class TestAutoUnbanScheduler:
|
||||
"""Тест разбана когда нет пользователей для разблокировки"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
mock_bot_db.get_users_for_unblock_today.return_value = {}
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={})
|
||||
scheduler.bot_db = mock_bot_db
|
||||
scheduler.set_bot(mock_bot)
|
||||
|
||||
@@ -96,12 +96,12 @@ class TestAutoUnbanScheduler:
|
||||
"""Тест разбана с частичными ошибками"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
mock_bot_db.get_users_for_unblock_today.return_value = {
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={
|
||||
123: "test_user1",
|
||||
456: "test_user2"
|
||||
}
|
||||
})
|
||||
# Первый вызов успешен, второй - ошибка
|
||||
mock_bot_db.delete_user_blacklist.side_effect = [True, False]
|
||||
mock_bot_db.delete_user_blacklist = AsyncMock(side_effect=[True, False])
|
||||
scheduler.bot_db = mock_bot_db
|
||||
scheduler.set_bot(mock_bot)
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestAutoUnbanScheduler:
|
||||
"""Тест разбана с исключением"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
mock_bot_db.get_users_for_unblock_today.side_effect = Exception("Database error")
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(side_effect=Exception("Database error"))
|
||||
scheduler.bot_db = mock_bot_db
|
||||
scheduler.set_bot(mock_bot)
|
||||
|
||||
@@ -141,7 +141,7 @@ class TestAutoUnbanScheduler:
|
||||
assert "Отчет об автоматическом разбане" in report
|
||||
assert "Успешно разблокировано: 1" in report
|
||||
assert "Ошибок: 1" in report
|
||||
assert "test_user1" in report
|
||||
assert "ID: 123" in report
|
||||
assert "456 (test_user2)" in report
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -268,8 +268,8 @@ class TestAsyncOperations:
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
|
||||
mock_bot_db = Mock()
|
||||
mock_bot_db.get_users_for_unblock_today.return_value = {123: "test_user"}
|
||||
mock_bot_db.delete_user_blacklist.return_value = True
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={123: "test_user"})
|
||||
mock_bot_db.delete_user_blacklist = AsyncMock(return_value=True)
|
||||
|
||||
mock_bot = Mock()
|
||||
mock_bot.send_message = AsyncMock()
|
||||
|
||||
423
tests/test_blacklist_repository.py
Normal file
423
tests/test_blacklist_repository.py
Normal file
@@ -0,0 +1,423 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
from database.models import BlacklistUser
|
||||
|
||||
|
||||
class TestBlacklistRepository:
|
||||
"""Тесты для BlacklistRepository"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_connection(self):
|
||||
"""Мок для DatabaseConnection"""
|
||||
mock_connection = Mock()
|
||||
mock_connection._execute_query = AsyncMock()
|
||||
mock_connection._execute_query_with_result = AsyncMock()
|
||||
mock_connection.logger = Mock()
|
||||
return mock_connection
|
||||
|
||||
@pytest.fixture
|
||||
def blacklist_repository(self, mock_db_connection):
|
||||
"""Экземпляр BlacklistRepository для тестов"""
|
||||
# Патчим наследование от DatabaseConnection
|
||||
with patch.object(BlacklistRepository, '__init__', return_value=None):
|
||||
repo = BlacklistRepository()
|
||||
repo._execute_query = mock_db_connection._execute_query
|
||||
repo._execute_query_with_result = mock_db_connection._execute_query_with_result
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@pytest.fixture
|
||||
def sample_blacklist_user(self):
|
||||
"""Тестовый пользователь в черном списке"""
|
||||
return BlacklistUser(
|
||||
user_id=12345,
|
||||
message_for_user="Нарушение правил",
|
||||
date_to_unban=int(time.time()) + 86400, # +1 день
|
||||
created_at=int(time.time())
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_blacklist_user_permanent(self):
|
||||
"""Тестовый пользователь с постоянным баном"""
|
||||
return BlacklistUser(
|
||||
user_id=67890,
|
||||
message_for_user="Постоянный бан",
|
||||
date_to_unban=None,
|
||||
created_at=int(time.time())
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables(self, blacklist_repository):
|
||||
"""Тест создания таблицы черного списка"""
|
||||
await blacklist_repository.create_tables()
|
||||
|
||||
# Проверяем, что метод вызван
|
||||
blacklist_repository._execute_query.assert_called()
|
||||
calls = blacklist_repository._execute_query.call_args_list
|
||||
|
||||
# Проверяем, что создается таблица с правильной структурой
|
||||
create_table_call = calls[0]
|
||||
assert "CREATE TABLE IF NOT EXISTS blacklist" in create_table_call[0][0]
|
||||
assert "user_id INTEGER NOT NULL PRIMARY KEY" in create_table_call[0][0]
|
||||
assert "message_for_user TEXT" in create_table_call[0][0]
|
||||
assert "date_to_unban INTEGER" in create_table_call[0][0]
|
||||
assert "created_at INTEGER DEFAULT (strftime('%s', 'now'))" in create_table_call[0][0]
|
||||
assert "FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in create_table_call[0][0]
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with("Таблица черного списка создана")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_user(self, blacklist_repository, sample_blacklist_user):
|
||||
"""Тест добавления пользователя в черный список"""
|
||||
await blacklist_repository.add_user(sample_blacklist_user)
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
blacklist_repository._execute_query.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query.call_args
|
||||
|
||||
# Проверяем SQL запрос (учитываем форматирование)
|
||||
sql_query = call_args[0][0].replace('\n', ' ').replace(' ', ' ').replace(' ', ' ').strip()
|
||||
expected_sql = "INSERT INTO blacklist (user_id, message_for_user, date_to_unban) VALUES (?, ?, ?)"
|
||||
assert sql_query == expected_sql
|
||||
|
||||
# Проверяем параметры
|
||||
assert call_args[0][1] == (12345, "Нарушение правил", sample_blacklist_user.date_to_unban)
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Пользователь добавлен в черный список: user_id=12345"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_user_permanent_ban(self, blacklist_repository, sample_blacklist_user_permanent):
|
||||
"""Тест добавления пользователя с постоянным баном"""
|
||||
await blacklist_repository.add_user(sample_blacklist_user_permanent)
|
||||
|
||||
call_args = blacklist_repository._execute_query.call_args
|
||||
assert call_args[0][1] == (67890, "Постоянный бан", None)
|
||||
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Пользователь добавлен в черный список: user_id=67890"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_user_success(self, blacklist_repository):
|
||||
"""Тест успешного удаления пользователя из черного списка"""
|
||||
await blacklist_repository.remove_user(12345)
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
blacklist_repository._execute_query.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query.call_args
|
||||
|
||||
assert call_args[0][0] == "DELETE FROM blacklist WHERE user_id = ?"
|
||||
assert call_args[0][1] == (12345,)
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Пользователь с идентификатором 12345 успешно удален из черного списка."
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_user_failure(self, blacklist_repository):
|
||||
"""Тест неудачного удаления пользователя из черного списка"""
|
||||
# Симулируем ошибку при удалении
|
||||
blacklist_repository._execute_query.side_effect = Exception("Database error")
|
||||
|
||||
result = await blacklist_repository.remove_user(12345)
|
||||
|
||||
# Проверяем, что возвращается False при ошибке
|
||||
assert result is False
|
||||
|
||||
# Проверяем логирование ошибки
|
||||
blacklist_repository.logger.error.assert_called_once()
|
||||
error_log = blacklist_repository.logger.error.call_args[0][0]
|
||||
assert "Ошибка удаления пользователя с идентификатором 12345" in error_log
|
||||
assert "Database error" in error_log
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_exists_true(self, blacklist_repository):
|
||||
"""Тест проверки существования пользователя (пользователь существует)"""
|
||||
# Симулируем результат запроса - пользователь найден
|
||||
blacklist_repository._execute_query_with_result.return_value = [(1,)]
|
||||
|
||||
result = await blacklist_repository.user_exists(12345)
|
||||
|
||||
# Проверяем, что возвращается True
|
||||
assert result is True
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT 1 FROM blacklist WHERE user_id = ?"
|
||||
assert call_args[0][1] == (12345,)
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Существует ли пользователь: user_id=12345 Итог: [(1,)]"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_exists_false(self, blacklist_repository):
|
||||
"""Тест проверки существования пользователя (пользователь не существует)"""
|
||||
# Симулируем результат запроса - пользователь не найден
|
||||
blacklist_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await blacklist_repository.user_exists(12345)
|
||||
|
||||
# Проверяем, что возвращается False
|
||||
assert result is False
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Существует ли пользователь: user_id=12345 Итог: []"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_success(self, blacklist_repository):
|
||||
"""Тест успешного получения пользователя по ID"""
|
||||
# Симулируем результат запроса
|
||||
mock_row = (12345, "Нарушение правил", int(time.time()) + 86400, int(time.time()))
|
||||
blacklist_repository._execute_query_with_result.return_value = [mock_row]
|
||||
|
||||
result = await blacklist_repository.get_user(12345)
|
||||
|
||||
# Проверяем, что возвращается правильный объект
|
||||
assert result is not None
|
||||
assert result.user_id == 12345
|
||||
assert result.message_for_user == "Нарушение правил"
|
||||
assert result.date_to_unban == mock_row[2]
|
||||
assert result.created_at == mock_row[3]
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist WHERE user_id = ?"
|
||||
assert call_args[0][1] == (12345,)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_not_found(self, blacklist_repository):
|
||||
"""Тест получения пользователя по ID (пользователь не найден)"""
|
||||
# Симулируем результат запроса - пользователь не найден
|
||||
blacklist_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await blacklist_repository.get_user(12345)
|
||||
|
||||
# Проверяем, что возвращается None
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_users_with_limits(self, blacklist_repository):
|
||||
"""Тест получения пользователей с лимитами"""
|
||||
# Симулируем результат запроса
|
||||
mock_rows = [
|
||||
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time())),
|
||||
(67890, "Постоянный бан", None, int(time.time()) - 86400)
|
||||
]
|
||||
blacklist_repository._execute_query_with_result.return_value = mock_rows
|
||||
|
||||
result = await blacklist_repository.get_all_users(offset=0, limit=10)
|
||||
|
||||
# Проверяем, что возвращается правильный список
|
||||
assert len(result) == 2
|
||||
assert result[0].user_id == 12345
|
||||
assert result[0].message_for_user == "Нарушение правил"
|
||||
assert result[1].user_id == 67890
|
||||
assert result[1].message_for_user == "Постоянный бан"
|
||||
assert result[1].date_to_unban is None
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist LIMIT ?, ?"
|
||||
assert call_args[0][1] == (0, 10)
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Получен список пользователей в черном списке (offset=0, limit=10): 2"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_users_no_limit(self, blacklist_repository):
|
||||
"""Тест получения всех пользователей без лимитов"""
|
||||
# Симулируем результат запроса
|
||||
mock_rows = [
|
||||
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time())),
|
||||
(67890, "Постоянный бан", None, int(time.time()) - 86400)
|
||||
]
|
||||
blacklist_repository._execute_query_with_result.return_value = mock_rows
|
||||
|
||||
result = await blacklist_repository.get_all_users_no_limit()
|
||||
|
||||
# Проверяем, что возвращается правильный список
|
||||
assert len(result) == 2
|
||||
|
||||
# Проверяем, что метод вызван без лимитов
|
||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist"
|
||||
# Проверяем, что параметры пустые (без лимитов)
|
||||
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Получен список всех пользователей в черном списке: 2"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_for_unblock_today(self, blacklist_repository):
|
||||
"""Тест получения пользователей для разблокировки сегодня"""
|
||||
current_timestamp = int(time.time())
|
||||
|
||||
# Симулируем результат запроса - пользователи с истекшим сроком
|
||||
mock_rows = [(12345,), (67890,)]
|
||||
blacklist_repository._execute_query_with_result.return_value = mock_rows
|
||||
|
||||
result = await blacklist_repository.get_users_for_unblock_today(current_timestamp)
|
||||
|
||||
# Проверяем, что возвращается правильный словарь
|
||||
assert len(result) == 2
|
||||
assert 12345 in result
|
||||
assert 67890 in result
|
||||
assert result[12345] == 12345
|
||||
assert result[67890] == 67890
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT user_id FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?"
|
||||
assert call_args[0][1] == (current_timestamp,)
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Получен список пользователей для разблокировки: {12345: 12345, 67890: 67890}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_for_unblock_today_empty(self, blacklist_repository):
|
||||
"""Тест получения пользователей для разблокировки (пустой результат)"""
|
||||
current_timestamp = int(time.time())
|
||||
|
||||
# Симулируем пустой результат запроса
|
||||
blacklist_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await blacklist_repository.get_users_for_unblock_today(current_timestamp)
|
||||
|
||||
# Проверяем, что возвращается пустой словарь
|
||||
assert result == {}
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Получен список пользователей для разблокировки: {}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_count(self, blacklist_repository):
|
||||
"""Тест получения количества пользователей в черном списке"""
|
||||
# Симулируем результат запроса
|
||||
blacklist_repository._execute_query_with_result.return_value = [(5,)]
|
||||
|
||||
result = await blacklist_repository.get_count()
|
||||
|
||||
# Проверяем, что возвращается правильное количество
|
||||
assert result == 5
|
||||
|
||||
# Проверяем, что метод вызван с правильными параметрами
|
||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
assert call_args[0][0] == "SELECT COUNT(*) FROM blacklist"
|
||||
# Проверяем, что параметры пустые
|
||||
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_count_zero(self, blacklist_repository):
|
||||
"""Тест получения количества пользователей (0 пользователей)"""
|
||||
# Симулируем пустой результат запроса
|
||||
blacklist_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await blacklist_repository.get_count()
|
||||
|
||||
# Проверяем, что возвращается 0
|
||||
assert result == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_count_none_result(self, blacklist_repository):
|
||||
"""Тест получения количества пользователей (None результат)"""
|
||||
# Симулируем None результат запроса
|
||||
blacklist_repository._execute_query_with_result.return_value = None
|
||||
|
||||
result = await blacklist_repository.get_count()
|
||||
|
||||
# Проверяем, что возвращается 0
|
||||
assert result == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_handling_in_get_user(self, blacklist_repository):
|
||||
"""Тест обработки ошибок при получении пользователя"""
|
||||
# Симулируем ошибку базы данных
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await blacklist_repository.get_user(12345)
|
||||
|
||||
assert "Database connection failed" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_handling_in_get_all_users(self, blacklist_repository):
|
||||
"""Тест обработки ошибок при получении всех пользователей"""
|
||||
# Симулируем ошибку базы данных
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await blacklist_repository.get_all_users()
|
||||
|
||||
assert "Database connection failed" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_handling_in_get_count(self, blacklist_repository):
|
||||
"""Тест обработки ошибок при получении количества"""
|
||||
# Симулируем ошибку базы данных
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await blacklist_repository.get_count()
|
||||
|
||||
assert "Database connection failed" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_handling_in_get_users_for_unblock_today(self, blacklist_repository):
|
||||
"""Тест обработки ошибок при получении пользователей для разблокировки"""
|
||||
# Симулируем ошибку базы данных
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await blacklist_repository.get_users_for_unblock_today(int(time.time()))
|
||||
|
||||
assert "Database connection failed" in str(exc_info.value)
|
||||
|
||||
# TODO: 20-й тест - test_integration_workflow
|
||||
# Этот тест должен проверять полный рабочий процесс:
|
||||
# 1. Добавление пользователя в черный список
|
||||
# 2. Проверка существования пользователя
|
||||
# 3. Получение информации о пользователе
|
||||
# 4. Получение общего количества пользователей
|
||||
# 5. Удаление пользователя из черного списка
|
||||
# 6. Проверка, что пользователь больше не существует
|
||||
#
|
||||
# Проблема: тест падает из-за сложности мокирования возвращаемых значений
|
||||
# при создании объектов BlacklistUser из результатов запросов к БД.
|
||||
# Требует более сложной настройки моков для корректной работы.
|
||||
297
tests/test_callback_handlers.py
Normal file
297
tests/test_callback_handlers.py
Normal file
@@ -0,0 +1,297 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from helper_bot.handlers.callback.callback_handlers import (
|
||||
save_voice_message,
|
||||
delete_voice_message
|
||||
)
|
||||
from helper_bot.handlers.voice.constants import CALLBACK_SAVE, CALLBACK_DELETE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_call():
|
||||
"""Мок для CallbackQuery"""
|
||||
call = Mock()
|
||||
call.message = Mock()
|
||||
call.message.message_id = 12345
|
||||
call.message.voice = Mock()
|
||||
call.message.voice.file_id = "test_file_id_123"
|
||||
call.bot = Mock()
|
||||
call.bot.delete_message = AsyncMock()
|
||||
call.answer = AsyncMock()
|
||||
return call
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bot_db():
|
||||
"""Мок для базы данных"""
|
||||
mock_db = Mock()
|
||||
mock_db.get_user_id_by_message_id_for_voice_bot = AsyncMock(return_value=67890)
|
||||
mock_db.delete_audio_moderate_record = AsyncMock()
|
||||
return mock_db
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings():
|
||||
"""Мок для настроек"""
|
||||
return {
|
||||
'Telegram': {
|
||||
'group_for_posts': 'test_group_id'
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_audio_service():
|
||||
"""Мок для AudioFileService"""
|
||||
mock_service = Mock()
|
||||
mock_service.generate_file_name = AsyncMock(return_value="message_from_67890_number_1")
|
||||
mock_service.save_audio_file = AsyncMock()
|
||||
mock_service.download_and_save_audio = AsyncMock()
|
||||
return mock_service
|
||||
|
||||
|
||||
class TestSaveVoiceMessage:
|
||||
"""Тесты для функции save_voice_message"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_success(self, mock_call, mock_bot_db, mock_settings, mock_audio_service):
|
||||
"""Тест успешного сохранения голосового сообщения"""
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService') as mock_service_class:
|
||||
mock_service_class.return_value = mock_audio_service
|
||||
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем, что все методы вызваны
|
||||
mock_bot_db.get_user_id_by_message_id_for_voice_bot.assert_called_once_with(12345)
|
||||
mock_audio_service.generate_file_name.assert_called_once_with(67890)
|
||||
mock_audio_service.save_audio_file.assert_called_once()
|
||||
mock_audio_service.download_and_save_audio.assert_called_once_with(
|
||||
mock_call.bot, mock_call.message, "message_from_67890_number_1"
|
||||
)
|
||||
|
||||
# Проверяем удаление сообщения из чата
|
||||
mock_call.bot.delete_message.assert_called_once_with(
|
||||
chat_id='test_group_id',
|
||||
message_id=12345
|
||||
)
|
||||
|
||||
# Проверяем удаление записи из audio_moderate
|
||||
mock_bot_db.delete_audio_moderate_record.assert_called_once_with(12345)
|
||||
|
||||
# Проверяем ответ пользователю
|
||||
mock_call.answer.assert_called_once_with(text='Сохранено!', cache_time=3)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_with_correct_parameters(self, mock_call, mock_bot_db, mock_settings, mock_audio_service):
|
||||
"""Тест сохранения с правильными параметрами"""
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService') as mock_service_class:
|
||||
mock_service_class.return_value = mock_audio_service
|
||||
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем параметры save_audio_file
|
||||
save_call_args = mock_audio_service.save_audio_file.call_args
|
||||
assert save_call_args[0][0] == "message_from_67890_number_1" # file_name
|
||||
assert save_call_args[0][1] == 67890 # user_id
|
||||
assert isinstance(save_call_args[0][2], datetime) # date_added
|
||||
assert save_call_args[0][3] == "test_file_id_123" # file_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_exception_handling(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест обработки исключений при сохранении"""
|
||||
mock_bot_db.get_user_id_by_message_id_for_voice_bot.side_effect = Exception("Database error")
|
||||
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем, что при ошибке отправляется соответствующий ответ
|
||||
mock_call.answer.assert_called_once_with(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_audio_service_exception(self, mock_call, mock_bot_db, mock_settings, mock_audio_service):
|
||||
"""Тест обработки исключений в AudioFileService"""
|
||||
mock_audio_service.save_audio_file.side_effect = Exception("Save error")
|
||||
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService') as mock_service_class:
|
||||
mock_service_class.return_value = mock_audio_service
|
||||
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем, что при ошибке отправляется соответствующий ответ
|
||||
mock_call.answer.assert_called_once_with(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_download_exception(self, mock_call, mock_bot_db, mock_settings, mock_audio_service):
|
||||
"""Тест обработки исключений при скачивании файла"""
|
||||
mock_audio_service.download_and_save_audio.side_effect = Exception("Download error")
|
||||
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService') as mock_service_class:
|
||||
mock_service_class.return_value = mock_audio_service
|
||||
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем, что при ошибке отправляется соответствующий ответ
|
||||
mock_call.answer.assert_called_once_with(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
|
||||
class TestDeleteVoiceMessage:
|
||||
"""Тесты для функции delete_voice_message"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_voice_message_success(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест успешного удаления голосового сообщения"""
|
||||
await delete_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем удаление сообщения из чата
|
||||
mock_call.bot.delete_message.assert_called_once_with(
|
||||
chat_id='test_group_id',
|
||||
message_id=12345
|
||||
)
|
||||
|
||||
# Проверяем удаление записи из audio_moderate
|
||||
mock_bot_db.delete_audio_moderate_record.assert_called_once_with(12345)
|
||||
|
||||
# Проверяем ответ пользователю
|
||||
mock_call.answer.assert_called_once_with(text='Удалено!', cache_time=3)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_voice_message_exception_handling(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест обработки исключений при удалении"""
|
||||
mock_call.bot.delete_message.side_effect = Exception("Delete error")
|
||||
|
||||
await delete_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем, что при ошибке отправляется соответствующий ответ
|
||||
mock_call.answer.assert_called_once_with(text='Ошибка при удалении!', cache_time=3)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_voice_message_database_exception(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест обработки исключений в базе данных при удалении"""
|
||||
mock_bot_db.delete_audio_moderate_record.side_effect = Exception("Database error")
|
||||
|
||||
await delete_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем, что при ошибке отправляется соответствующий ответ
|
||||
mock_call.answer.assert_called_once_with(text='Ошибка при удалении!', cache_time=3)
|
||||
|
||||
|
||||
class TestCallbackHandlersIntegration:
|
||||
"""Интеграционные тесты для callback handlers"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_full_workflow(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест полного рабочего процесса сохранения"""
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService') as mock_service_class:
|
||||
mock_service = Mock()
|
||||
mock_service.generate_file_name = AsyncMock(return_value="message_from_67890_number_1")
|
||||
mock_service.save_audio_file = AsyncMock()
|
||||
mock_service.download_and_save_audio = AsyncMock()
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем последовательность вызовов
|
||||
assert mock_bot_db.get_user_id_by_message_id_for_voice_bot.called
|
||||
assert mock_service.generate_file_name.called
|
||||
assert mock_service.save_audio_file.called
|
||||
assert mock_service.download_and_save_audio.called
|
||||
assert mock_call.bot.delete_message.called
|
||||
assert mock_bot_db.delete_audio_moderate_record.called
|
||||
assert mock_call.answer.called
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_voice_message_full_workflow(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест полного рабочего процесса удаления"""
|
||||
await delete_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем последовательность вызовов
|
||||
assert mock_call.bot.delete_message.called
|
||||
assert mock_bot_db.delete_audio_moderate_record.called
|
||||
assert mock_call.answer.called
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audio_moderate_cleanup_consistency(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест консистентности очистки audio_moderate"""
|
||||
# Тестируем, что в обоих случаях (сохранение и удаление)
|
||||
# вызывается delete_audio_moderate_record
|
||||
|
||||
# Создаем отдельные моки для каждого теста
|
||||
mock_bot_db_save = Mock()
|
||||
mock_bot_db_save.get_user_id_by_message_id_for_voice_bot = AsyncMock(return_value=67890)
|
||||
mock_bot_db_save.delete_audio_moderate_record = AsyncMock()
|
||||
|
||||
mock_bot_db_delete = Mock()
|
||||
mock_bot_db_delete.delete_audio_moderate_record = AsyncMock()
|
||||
|
||||
# Тест для сохранения
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService') as mock_service_class:
|
||||
mock_service = Mock()
|
||||
mock_service.generate_file_name = AsyncMock(return_value="message_from_67890_number_1")
|
||||
mock_service.save_audio_file = AsyncMock()
|
||||
mock_service.download_and_save_audio = AsyncMock()
|
||||
mock_service_class.return_value = mock_service
|
||||
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db_save, settings=mock_settings)
|
||||
save_calls = mock_bot_db_save.delete_audio_moderate_record.call_count
|
||||
|
||||
# Тест для удаления
|
||||
await delete_voice_message(mock_call, bot_db=mock_bot_db_delete, settings=mock_settings)
|
||||
delete_calls = mock_bot_db_delete.delete_audio_moderate_record.call_count
|
||||
|
||||
# Проверяем, что в обоих случаях вызывается очистка
|
||||
assert save_calls == 1
|
||||
assert delete_calls == 1
|
||||
|
||||
|
||||
class TestCallbackHandlersEdgeCases:
|
||||
"""Тесты граничных случаев для callback handlers"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_no_voice_attribute(self, mock_bot_db, mock_settings):
|
||||
"""Тест сохранения когда у сообщения нет voice атрибута"""
|
||||
call = Mock()
|
||||
call.message = Mock()
|
||||
call.message.message_id = 12345
|
||||
call.message.voice = None # Нет голосового сообщения
|
||||
call.bot = Mock()
|
||||
call.bot.delete_message = AsyncMock()
|
||||
call.answer = AsyncMock()
|
||||
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService'):
|
||||
await save_voice_message(call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Должна быть ошибка
|
||||
call.answer.assert_called_once_with(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_user_not_found(self, mock_call, mock_bot_db, mock_settings):
|
||||
"""Тест сохранения когда пользователь не найден"""
|
||||
mock_bot_db.get_user_id_by_message_id_for_voice_bot.return_value = None
|
||||
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService'):
|
||||
await save_voice_message(mock_call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Должна быть ошибка
|
||||
mock_call.answer.assert_called_once_with(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_voice_message_with_different_message_id(self, mock_bot_db, mock_settings):
|
||||
"""Тест удаления с другим message_id"""
|
||||
call = Mock()
|
||||
call.message = Mock()
|
||||
call.message.message_id = 99999 # Другой ID
|
||||
call.bot = Mock()
|
||||
call.bot.delete_message = AsyncMock()
|
||||
call.answer = AsyncMock()
|
||||
|
||||
await delete_voice_message(call, bot_db=mock_bot_db, settings=mock_settings)
|
||||
|
||||
# Проверяем, что используется правильный message_id
|
||||
call.bot.delete_message.assert_called_once_with(
|
||||
chat_id='test_group_id',
|
||||
message_id=99999
|
||||
)
|
||||
mock_bot_db.delete_audio_moderate_record.assert_called_once_with(99999)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
808
tests/test_db.py
808
tests/test_db.py
@@ -1,808 +0,0 @@
|
||||
import os
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from database.db import BotDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bot():
|
||||
"""Фикстура для создания объекта BotDB."""
|
||||
current_dir = os.getcwd()
|
||||
return BotDB(current_dir, "database/test.db")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, )
|
||||
def setup_db():
|
||||
"""Фикстура для создания всей базы перед каждым тестом."""
|
||||
# Mock data 1st user
|
||||
user_id = 12345
|
||||
first_name = "Иван"
|
||||
full_name = "Иван Иванович"
|
||||
username = "@iban"
|
||||
message_text = 'Hello, planet'
|
||||
message_id = 1
|
||||
message_for_user = "LOL"
|
||||
has_stickers = 0
|
||||
# Mock data 2nd user
|
||||
user_id_2 = 14278
|
||||
first_name_2 = "Борис"
|
||||
full_name_2 = "Борис Петрович"
|
||||
username_2 = "@boris"
|
||||
message_text_2 = 'Hello, world'
|
||||
message_id_2 = 2
|
||||
message_for_user_2 = "LOL2"
|
||||
has_stickers_2 = 1
|
||||
# Other data
|
||||
date = "2024-07-10"
|
||||
next_date = "2024-07-11"
|
||||
conn = sqlite3.connect("database/test.db")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS "admins" (
|
||||
user_id INTEGER NOT NULL,
|
||||
"role" TEXT
|
||||
);
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS "audio_message_reference"
|
||||
(
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"file_name" TEXT NOT NULL UNIQUE,
|
||||
"author_id" INTEGER NOT NULL,
|
||||
"date_added" DATE NOT NULL,
|
||||
"listen_count" INTEGER NOT NULL,
|
||||
"file_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS "blacklist"
|
||||
(
|
||||
"user_id" INTEGER NOT NULL UNIQUE,
|
||||
"user_name" INTEGER,
|
||||
"message_for_user" INTEGER,
|
||||
"date_to_unban" INTEGER
|
||||
);
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS "messages" (
|
||||
"ID" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
"Message" TEXT NOT NULL,
|
||||
"type" INTEGER
|
||||
);
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS "our_users" (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||
"user_id" INTEGER NOT NULL UNIQUE,
|
||||
"first_name" STRING,
|
||||
"full_name" STRING,
|
||||
"username" STRING,
|
||||
"is_bot" BOOLEAN,
|
||||
"language_code" STRING,
|
||||
"has_stickers" INTEGER NOT NULL DEFAULT 0,
|
||||
"date_added" DATE NOT NULL,
|
||||
"date_changed" DATE NOT NULL
|
||||
, state_user TEXT(20));
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT,
|
||||
user_id INTEGER,
|
||||
message_id INTEGER NOT NULL,
|
||||
date TEXT
|
||||
);
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE TABLE migrations (
|
||||
version INTEGER PRIMARY KEY NOT NULL,
|
||||
script_name TEXT NOT NULL,
|
||||
created_at TEXT
|
||||
);
|
||||
""")
|
||||
|
||||
# blacklist mock data
|
||||
cursor.execute("INSERT INTO blacklist (user_id, user_name, message_for_user, date_to_unban) VALUES (?, ?, ?, ?)",
|
||||
(user_id, username, message_for_user, next_date))
|
||||
cursor.execute("INSERT INTO blacklist (user_id, user_name, message_for_user, date_to_unban) VALUES (?, ?, ?, ?)",
|
||||
(user_id_2, username_2, message_for_user_2, date))
|
||||
# our_users mock data
|
||||
cursor.execute(
|
||||
"INSERT INTO our_users (user_id, first_name, full_name, username, date_added, date_changed, has_stickers)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?)", (user_id, first_name, full_name, username, date, date, has_stickers)
|
||||
)
|
||||
cursor.execute(
|
||||
"INSERT INTO our_users (user_id, first_name, full_name, username, date_added, date_changed, has_stickers)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?)", (user_id_2, first_name_2, full_name_2, username_2, date, date, has_stickers_2)
|
||||
)
|
||||
# messages mock data
|
||||
cursor.execute(
|
||||
"INSERT INTO user_messages (message_text, user_id, message_id, date) "
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(message_text, user_id, message_id, date))
|
||||
cursor.execute(
|
||||
"INSERT INTO user_messages (message_text, user_id, message_id, date) "
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(message_text_2, user_id_2, message_id_2, date))
|
||||
# mock admins
|
||||
cursor.execute(
|
||||
"INSERT INTO admins (user_id, role) "
|
||||
"VALUES (?, ?)",
|
||||
(user_id, 'creator'))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
yield
|
||||
os.remove('database/test.db')
|
||||
|
||||
|
||||
def test_bot_init(bot):
|
||||
"""Проверяет, что объект BotDB инициализируется с правильным именем файла."""
|
||||
assert bot.db_file == os.path.join(os.getcwd(), "database", "test.db")
|
||||
# Проверьте, что соединения с базой данных нет, так как оно не устанавливается в init
|
||||
assert bot.conn is None
|
||||
assert bot.cursor is None
|
||||
|
||||
|
||||
def test_bot_connect(bot):
|
||||
"""Проверяет, что метод connect создает подключение к базе данных."""
|
||||
bot.connect()
|
||||
assert bot.conn is not None
|
||||
assert bot.cursor is not None
|
||||
bot.close()
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_bot_close(bot):
|
||||
"""Проверяет, что метод close закрывает подключение к базе данных."""
|
||||
bot.connect()
|
||||
assert bot.conn is not None
|
||||
assert bot.cursor is not None
|
||||
bot.close()
|
||||
assert bot.conn is None
|
||||
assert bot.cursor is None
|
||||
|
||||
|
||||
def test_create_table_success(bot):
|
||||
sql_script = 'CREATE TABLE test_table (id INTEGER PRIMARY KEY);'
|
||||
bot.create_table(sql_script)
|
||||
|
||||
# Проверяем, что таблица создана
|
||||
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()
|
||||
conn.close()
|
||||
|
||||
assert result is not None
|
||||
|
||||
|
||||
def test_create_table_error(bot):
|
||||
sql_script = 'CREATE TABLE test_table (id INTEGER PRIMARY KEY);'
|
||||
bot.create_table(sql_script)
|
||||
|
||||
with pytest.raises(sqlite3.OperationalError):
|
||||
bot.create_table(sql_script)
|
||||
|
||||
|
||||
def test_get_current_version_success(bot):
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Вызываем функцию и проверяем результат
|
||||
version = bot.get_current_version()
|
||||
assert version == 123
|
||||
|
||||
|
||||
def test_get_current_version_error(bot):
|
||||
__drop_table('migrations')
|
||||
with pytest.raises(sqlite3.OperationalError):
|
||||
bot.get_current_version()
|
||||
|
||||
|
||||
def test_update_version_success(bot):
|
||||
# Вызываем функцию update_version
|
||||
new_version = 124
|
||||
script_name = "migration_script.sql"
|
||||
bot.update_version(new_version, script_name)
|
||||
|
||||
# Проверяем, что данные записаны в таблицу
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM migrations WHERE version = ?", (new_version,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
assert result is not None
|
||||
assert result[0] == new_version
|
||||
assert result[1] == script_name
|
||||
assert result[2] == datetime.now().strftime("%d-%m-%Y %H:%M:%S")
|
||||
|
||||
|
||||
def test_update_version_integrity_error(bot):
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# Пытаемся обновить версию с уже существующим значением
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
bot.update_version(123, "script_2.sql")
|
||||
|
||||
|
||||
def test_update_version_error(bot):
|
||||
__drop_table('migrations')
|
||||
with pytest.raises(sqlite3.OperationalError):
|
||||
bot.update_version(123, "script_2.sql")()
|
||||
|
||||
|
||||
def test_add_new_user_in_db(bot):
|
||||
"""Проверяет добавление нового пользователя в базу данных."""
|
||||
user_id = 50
|
||||
first_name = "Петр"
|
||||
full_name = "Петр Иванов"
|
||||
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, emoji, date_added, date_changed
|
||||
)
|
||||
|
||||
# Проверяем наличие записи в базе данных
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM our_users WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
assert result is not None
|
||||
assert result[1] == user_id
|
||||
assert result[2] == first_name
|
||||
assert result[3] == full_name
|
||||
assert result[4] == username
|
||||
assert result[5] == is_bot
|
||||
assert result[6] == language_code
|
||||
assert result[8] == date_added
|
||||
assert result[9] == date_changed
|
||||
|
||||
|
||||
def test_add_new_user_in_db_duplicate_user_id(bot, setup_db):
|
||||
"""Проверяет поведение при попытке добавить пользователя с уже существующим user_id."""
|
||||
user_id = 12345
|
||||
|
||||
# Попытка добавить пользователя с тем же user_id
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
bot.add_new_user_in_db(
|
||||
user_id, "Марина", "Марина Альфредовна", "marina", False, "bg", "🦀", "2024-07-09", "2024-07-09"
|
||||
)
|
||||
|
||||
|
||||
def test_add_new_user_in_db_empty_first_name(bot):
|
||||
""" Проверяет добавление пользователя с пустым именем (first_name) """
|
||||
user_id = 43
|
||||
first_name = "" # Пустое имя
|
||||
full_name = "Boris Petrov"
|
||||
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, emoji, date_added, date_changed
|
||||
)
|
||||
|
||||
# Проверяем наличие записи в базе данных
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"SELECT * FROM our_users WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
assert result is not None
|
||||
assert result[1] == user_id
|
||||
assert result[2] == first_name
|
||||
assert result[3] == full_name
|
||||
assert result[4] == username
|
||||
assert result[5] == is_bot
|
||||
assert result[6] == language_code
|
||||
assert result[8] == date_added
|
||||
assert result[9] == date_changed
|
||||
|
||||
|
||||
def test_user_exists_found(bot):
|
||||
"""Проверяет, что функция возвращает True, если пользователь найден."""
|
||||
user_id = 12345
|
||||
|
||||
# Проверяем наличие записи в базе данных
|
||||
assert bot.user_exists(user_id) is True
|
||||
|
||||
|
||||
def test_user_exists_not_found(bot):
|
||||
"""Проверяет, что функция возвращает False, если пользователь не найден."""
|
||||
user_id = 99999
|
||||
assert bot.user_exists(user_id) is False
|
||||
|
||||
|
||||
def test_user_exists_error(bot):
|
||||
"""Проверяет, что функция возвращает ошибки"""
|
||||
__drop_table('our_users')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.user_exists(12345)
|
||||
|
||||
|
||||
def test_get_user_id_found(bot):
|
||||
"""Проверяет, что функция возвращает ID пользователя, если он найден."""
|
||||
user_id = 12345
|
||||
# Проверяем, что возвращается правильный ID из базы
|
||||
user_id_db = bot.get_user_id(user_id)
|
||||
assert user_id_db == 1
|
||||
|
||||
|
||||
def test_get_user_id_not_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает None, если пользователь не найден."""
|
||||
user_id = 99999
|
||||
assert bot.get_user_id(user_id) is None
|
||||
|
||||
|
||||
def test_get_user_id_error(bot):
|
||||
"""Проверяет, что функция обрабатывает некорректный user_id."""
|
||||
__drop_table('our_users')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_user_id(12345)
|
||||
|
||||
|
||||
def test_get_username_found(bot):
|
||||
"""Проверяет, что функция возвращает username пользователя, если он найден."""
|
||||
user_id = 12345
|
||||
username = "@iban"
|
||||
# Проверяем, что возвращается правильный username из базы
|
||||
username_db = bot.get_username(user_id)
|
||||
assert username_db == username
|
||||
|
||||
|
||||
def test_get_username_not_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает None, если пользователь не найден."""
|
||||
user_id = 99999
|
||||
assert bot.get_username(user_id) is None
|
||||
|
||||
|
||||
def test_get_username_error(bot):
|
||||
"""Проверяет, что функция возвращает ошибку"""
|
||||
__drop_table('our_users')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_username(12345)
|
||||
|
||||
|
||||
def test_get_all_user_id_empty(bot):
|
||||
"""Проверяет, что функция возвращает пустой список, если в базе нет пользователей."""
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM our_users")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# Проверяем наличие записей в базе данных
|
||||
user_ids = bot.get_all_user_id()
|
||||
assert user_ids == []
|
||||
|
||||
|
||||
def test_get_all_user_id_non_empty(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает список всех user_id из базы данных."""
|
||||
# Проверяем наличие записи в базе данных
|
||||
user_ids = bot.get_all_user_id()
|
||||
assert user_ids == [12345, 14278] # Проверяем, что в списке два ожидаемых user_id
|
||||
|
||||
|
||||
def test_get_all_user_id_error(bot):
|
||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
||||
__drop_table('our_users')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_all_user_id()
|
||||
|
||||
|
||||
def test_get_user_first_name_found(bot):
|
||||
"""Проверяет, что функция возвращает имя пользователя, если он найден."""
|
||||
user_id = 12345
|
||||
first_name = bot.get_user_first_name(user_id)
|
||||
assert first_name == "Иван"
|
||||
|
||||
|
||||
def test_get_user_first_name_not_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает None, если пользователь не найден."""
|
||||
user_id = 99999
|
||||
assert bot.get_user_first_name(user_id) is None
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_get_user_first_name_invalid_user_id(bot):
|
||||
"""Проверяет, что функция обрабатывает некорректный user_id."""
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_user_first_name("invalid_user_id") # Передача строки
|
||||
|
||||
|
||||
def test_get_user_first_name_error(bot):
|
||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
||||
__drop_table('our_users')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_user_first_name(12345)
|
||||
|
||||
|
||||
def test_get_info_about_stickers_found_received(bot):
|
||||
"""Проверяет, что функция возвращает True, если пользователь получил стикеры."""
|
||||
user_id = 14278
|
||||
assert bot.get_info_about_stickers(user_id) is True
|
||||
|
||||
|
||||
def test_get_info_about_stickers_found_not_received(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает False, если пользователь не получил стикеры."""
|
||||
user_id = 12345
|
||||
assert bot.get_info_about_stickers(user_id) is False
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_get_info_about_stickers_not_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает None, если пользователь не найден."""
|
||||
user_id = 99999
|
||||
assert bot.get_info_about_stickers(user_id) is None
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_get_info_about_stickers_invalid_user_id(bot):
|
||||
"""Проверяет, что функция обрабатывает некорректный user_id."""
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_info_about_stickers("invalid_user_id")
|
||||
|
||||
|
||||
def test_get_info_about_stickers_error(bot):
|
||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
||||
__drop_table('our_users')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_info_about_stickers(12345)
|
||||
|
||||
|
||||
def test_update_info_about_stickers_success(bot):
|
||||
"""Проверяет, что функция успешно обновляет информацию о получении стикеров."""
|
||||
user_id = 12345
|
||||
bot.update_info_about_stickers(user_id)
|
||||
|
||||
# Проверяем, что информация обновлена
|
||||
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()
|
||||
conn.close()
|
||||
assert result[0] == 1
|
||||
|
||||
|
||||
def test_update_info_about_stickers_not_found(bot):
|
||||
"""Проверяет, что функция не вызывает ошибки, если пользователь не найден."""
|
||||
user_id = 99999
|
||||
bot.update_info_about_stickers(user_id)
|
||||
|
||||
# Проверяем, что база данных не изменилась
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM our_users WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
assert result[0] == 0
|
||||
|
||||
|
||||
def test_update_info_about_stickers_error(bot):
|
||||
"""Проверяет, что функция вызывает ошибки"""
|
||||
__drop_table('our_users')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.update_info_about_stickers(12345)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_get_blacklist_users_by_id_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает информацию о пользователе, если он найден в черном списке."""
|
||||
user_id = 12345
|
||||
|
||||
result = bot.get_blacklist_users_by_id(user_id)
|
||||
assert result == (12345, "@iban", "LOL", "2024-07-11")
|
||||
|
||||
|
||||
def test_get_blacklist_users_by_id_not_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает None, если пользователь не найден в черном списке."""
|
||||
user_id = 99999
|
||||
assert bot.get_blacklist_users_by_id(user_id) is None
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_get_blacklist_users_by_id_invalid_user_id(bot):
|
||||
"""Проверяет, что функция обрабатывает некорректный user_id."""
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_blacklist_users_by_id("invalid_user_id") # Передача строки
|
||||
|
||||
|
||||
def test_get_blacklist_users_by_id_error(bot):
|
||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
||||
__drop_table('blacklist')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_blacklist_users_by_id(12345)
|
||||
|
||||
|
||||
def test_get_users_for_unblock_today_found(bot):
|
||||
"""Проверяет, что функция возвращает словарь с пользователями, у которых истекает блокировка сегодня."""
|
||||
date_to_unban = "2024-07-11"
|
||||
result = bot.get_users_for_unblock_today(date_to_unban)
|
||||
assert result == {12345: "@iban"}
|
||||
|
||||
|
||||
def test_get_users_for_unblock_today_not_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает пустой словарь, если сегодня нет пользователей, у которых истекает блокировка."""
|
||||
date_to_unban = "2024-07-12"
|
||||
result = bot.get_users_for_unblock_today(date_to_unban)
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_get_users_for_unblock_today_error(bot):
|
||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
||||
__drop_table('blacklist')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_users_for_unblock_today("2023-12-26")
|
||||
|
||||
|
||||
def test_check_user_in_blacklist_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает True, если пользователь найден в черном списке."""
|
||||
user_id = 12345
|
||||
bot.set_user_blacklist(user_id, "JohnDoe") # Добавляем пользователя в черный список
|
||||
|
||||
assert bot.check_user_in_blacklist(user_id) is True
|
||||
|
||||
|
||||
def test_check_user_in_blacklist_not_found(bot, setup_db):
|
||||
"""Проверяет, что функция возвращает False, если пользователь не найден в черном списке."""
|
||||
user_id = 99999
|
||||
assert bot.check_user_in_blacklist(user_id) is False
|
||||
|
||||
|
||||
def test_check_user_in_blacklist_error(bot, setup_db):
|
||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
||||
__drop_table('blacklist')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.check_user_in_blacklist(12345)
|
||||
|
||||
|
||||
def test_set_user_blacklist_success(bot):
|
||||
"""Проверяет, что функция успешно добавляет пользователя в черный список."""
|
||||
user_id = 11
|
||||
user_name = "Гриша"
|
||||
message_for_user = "Лови бан!"
|
||||
date_to_unban = datetime.now().strftime("%Y-%m-%d") # Текущая дата
|
||||
|
||||
assert bot.set_user_blacklist(user_id, user_name, message_for_user, date_to_unban) is None
|
||||
|
||||
# Проверяем, что запись добавлена в базу
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,))
|
||||
result = cursor.fetchone()
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
assert result is not None
|
||||
assert result[1] == user_name
|
||||
assert result[2] == message_for_user
|
||||
assert result[3] == date_to_unban
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_set_user_blacklist_duplicate_user_id(bot, setup_db):
|
||||
"""Проверяет, что функция не добавляет дубликат user_id в черный список."""
|
||||
user_id = 12345
|
||||
bot.set_user_blacklist(user_id, "JohnDoe")
|
||||
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
bot.set_user_blacklist(user_id, "JaneSmith") # Попытка добавить дубликат
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_set_user_blacklist_error(bot, setup_db):
|
||||
"""Проверяет, что функция вызывает sqlite3. Error при ошибке запроса."""
|
||||
__drop_table('blacklist')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.set_user_blacklist(12345, "JohnDoe", "You are banned!", "2024-01-01")
|
||||
|
||||
|
||||
def test_delete_user_blacklist_success(bot):
|
||||
bot.delete_user_blacklist(12345)
|
||||
assert bot.check_user_in_blacklist(12345) is False
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_delete_user_blacklist_not_found(bot):
|
||||
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"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
result = bot.delete_user_blacklist(514)
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_delete_user_blacklist_error(bot):
|
||||
__drop_table('blacklist')
|
||||
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.delete_user_blacklist(12345)
|
||||
|
||||
|
||||
def test_add_new_message_in_db_success(bot):
|
||||
result = bot.add_new_message_in_db('hello', 4232187, 5, '2024-01-01')
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_add_new_message_in_db_error(bot):
|
||||
__drop_table('user_messages')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.add_new_message_in_db('hello', 12345, 1, '2024-01-01')
|
||||
|
||||
|
||||
def test_update_date_for_user_success(bot):
|
||||
bot.update_date_for_user('2024-07-15', 12345)
|
||||
|
||||
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]
|
||||
conn.close()
|
||||
assert new_date == '2024-07-15'
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_update_date_for_user_error(bot):
|
||||
__drop_table('our_users')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.update_date_for_user('2024-07-15', 12345)
|
||||
|
||||
|
||||
def test_is_admin_success(bot):
|
||||
assert bot.is_admin(12345) is True
|
||||
|
||||
|
||||
def test_is_admin_not_found(bot):
|
||||
assert bot.is_admin(1) is False
|
||||
|
||||
|
||||
def test_is_admin_error(bot):
|
||||
__drop_table('admins')
|
||||
assert bot.is_admin(1) is None
|
||||
|
||||
|
||||
def test_get_user_by_message_id_success(bot):
|
||||
assert bot.get_user_by_message_id(1) == 12345
|
||||
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_get_user_by_message_id_not_found(bot):
|
||||
assert bot.get_user_by_message_id(124) == None
|
||||
|
||||
|
||||
def test_get_user_by_message_id_error(bot):
|
||||
__drop_table('user_messages')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_user_by_message_id(14)
|
||||
|
||||
|
||||
def test_get_last_users_from_db_success(bot):
|
||||
users = bot.get_last_users_from_db()
|
||||
assert users is not None
|
||||
assert len(users) == 2
|
||||
|
||||
|
||||
def test_get_last_users_from_db_empty(bot):
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM our_users")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
users = bot.get_last_users_from_db()
|
||||
assert users == []
|
||||
assert len(users) == 0
|
||||
|
||||
|
||||
def test_get_user_by_message_id_error(bot):
|
||||
__drop_table('our_users')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_last_users_from_db()
|
||||
|
||||
|
||||
def test_get_banned_users_from_db_success(bot):
|
||||
users = bot.get_banned_users_from_db()
|
||||
assert users[0][0] == '@iban'
|
||||
assert users[0][1] == 12345
|
||||
assert users[0][2] == 'LOL'
|
||||
assert users[1][0] == '@boris'
|
||||
assert users[1][1] == 14278
|
||||
assert users[1][2] == 'LOL2'
|
||||
|
||||
|
||||
def test_get_banned_users_from_db_empty(bot):
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM blacklist")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
users = bot.get_banned_users_from_db()
|
||||
assert users == []
|
||||
assert len(users) == 0
|
||||
|
||||
|
||||
def test_get_banned_users_from_db_error(bot):
|
||||
__drop_table('blacklist')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_banned_users_from_db()
|
||||
|
||||
|
||||
def test_get_banned_users_from_db_with_limits_success_limit(bot):
|
||||
users = bot.get_banned_users_from_db_with_limits(0, 1)
|
||||
assert users[0][0] == '@iban'
|
||||
assert users[0][1] == 12345
|
||||
assert users[0][2] == 'LOL'
|
||||
assert len(users) == 1
|
||||
|
||||
|
||||
def test_get_banned_users_from_db_with_limits_success_offset(bot):
|
||||
users = bot.get_banned_users_from_db_with_limits(1, 2)
|
||||
assert users[0][0] == '@boris'
|
||||
assert users[0][1] == 14278
|
||||
assert users[0][2] == 'LOL2'
|
||||
assert len(users) == 1
|
||||
|
||||
|
||||
def test_get_banned_users_from_db_with_limits_empty(bot):
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM blacklist")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
users = bot.get_banned_users_from_db_with_limits(0, 2)
|
||||
assert users == []
|
||||
assert len(users) == 0
|
||||
|
||||
|
||||
def test_get_banned_users_from_db_with_limits_error(bot):
|
||||
__drop_table('blacklist')
|
||||
with pytest.raises(sqlite3.Error):
|
||||
bot.get_banned_users_from_db_with_limits(0, 2)
|
||||
|
||||
|
||||
def __drop_table(table_name: str):
|
||||
conn = sqlite3.connect('database/test.db')
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"DROP TABLE {table_name}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main()
|
||||
301
tests/test_improved_media_processing.py
Normal file
301
tests/test_improved_media_processing.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""
|
||||
Тесты для улучшенных методов обработки медиа
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
from aiogram import types
|
||||
|
||||
from helper_bot.utils.helper_func import (
|
||||
download_file,
|
||||
add_in_db_media,
|
||||
add_in_db_media_mediagroup,
|
||||
send_media_group_message_to_private_chat
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadFile:
|
||||
"""Тесты для функции download_file"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_file_success_photo(self):
|
||||
"""Тест успешного скачивания фото"""
|
||||
# Создаем временную директорию
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with patch('helper_bot.utils.helper_func.os.makedirs'), \
|
||||
patch('helper_bot.utils.helper_func.os.path.exists', return_value=True), \
|
||||
patch('helper_bot.utils.helper_func.os.path.getsize', return_value=1024), \
|
||||
patch('helper_bot.utils.helper_func.os.path.basename', return_value='photo.jpg'), \
|
||||
patch('helper_bot.utils.helper_func.os.path.splitext', return_value=('photo', '.jpg')):
|
||||
|
||||
# Мокаем сообщение и бота
|
||||
mock_message = Mock()
|
||||
mock_message.bot = Mock()
|
||||
mock_file = Mock()
|
||||
mock_file.file_path = 'photos/photo.jpg'
|
||||
mock_message.bot.get_file = AsyncMock(return_value=mock_file)
|
||||
mock_message.bot.download_file = AsyncMock()
|
||||
|
||||
# Вызываем функцию
|
||||
result = await download_file(mock_message, 'test_file_id', 'photo')
|
||||
|
||||
# Проверяем результат
|
||||
assert result is not None
|
||||
assert 'files/photos/test_file_id.jpg' in result
|
||||
mock_message.bot.get_file.assert_called_once_with('test_file_id')
|
||||
mock_message.bot.download_file.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_file_invalid_parameters(self):
|
||||
"""Тест с неверными параметрами"""
|
||||
result = await download_file(None, 'test_file_id', 'photo')
|
||||
assert result is None
|
||||
|
||||
mock_message = Mock()
|
||||
mock_message.bot = None
|
||||
result = await download_file(mock_message, 'test_file_id', 'photo')
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_file_error(self):
|
||||
"""Тест обработки ошибки при скачивании"""
|
||||
mock_message = Mock()
|
||||
mock_message.bot = Mock()
|
||||
mock_message.bot.get_file = AsyncMock(side_effect=Exception("Network error"))
|
||||
|
||||
result = await download_file(mock_message, 'test_file_id', 'photo')
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestAddInDbMedia:
|
||||
"""Тесты для функции add_in_db_media"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_success_photo(self):
|
||||
"""Тест успешного добавления фото в БД"""
|
||||
# Мокаем сообщение
|
||||
mock_message = Mock()
|
||||
mock_message.message_id = 123
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = 'photo_123'
|
||||
mock_message.video = None
|
||||
mock_message.voice = None
|
||||
mock_message.audio = None
|
||||
mock_message.video_note = None
|
||||
|
||||
# Мокаем БД
|
||||
mock_db = AsyncMock()
|
||||
mock_db.add_post_content = AsyncMock(return_value=True)
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value='files/photos/photo_123.jpg'):
|
||||
result = await add_in_db_media(mock_message, mock_db)
|
||||
|
||||
assert result is True
|
||||
mock_db.add_post_content.assert_called_once_with(123, 123, 'files/photos/photo_123.jpg', 'photo')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_download_fails(self):
|
||||
"""Тест когда скачивание файла не удается"""
|
||||
mock_message = Mock()
|
||||
mock_message.message_id = 123
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = 'photo_123'
|
||||
mock_message.video = None
|
||||
mock_message.voice = None
|
||||
mock_message.audio = None
|
||||
mock_message.video_note = None
|
||||
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value=None):
|
||||
result = await add_in_db_media(mock_message, mock_db)
|
||||
|
||||
assert result is False
|
||||
mock_db.add_post_content.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_db_fails(self):
|
||||
"""Тест когда добавление в БД не удается"""
|
||||
mock_message = Mock()
|
||||
mock_message.message_id = 123
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = 'photo_123'
|
||||
mock_message.video = None
|
||||
mock_message.voice = None
|
||||
mock_message.audio = None
|
||||
mock_message.video_note = None
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_db.add_post_content = AsyncMock(return_value=False)
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value='files/photos/photo_123.jpg'), \
|
||||
patch('helper_bot.utils.helper_func.os.remove'):
|
||||
|
||||
result = await add_in_db_media(mock_message, mock_db)
|
||||
|
||||
assert result is False
|
||||
mock_db.add_post_content.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_unsupported_content(self):
|
||||
"""Тест с неподдерживаемым типом контента"""
|
||||
mock_message = Mock()
|
||||
mock_message.message_id = 123
|
||||
mock_message.photo = None
|
||||
mock_message.video = None
|
||||
mock_message.voice = None
|
||||
mock_message.audio = None
|
||||
mock_message.video_note = None
|
||||
|
||||
mock_db = AsyncMock()
|
||||
|
||||
result = await add_in_db_media(mock_message, mock_db)
|
||||
|
||||
assert result is False
|
||||
mock_db.add_post_content.assert_not_called()
|
||||
|
||||
|
||||
class TestAddInDbMediaMediagroup:
|
||||
"""Тесты для функции add_in_db_media_mediagroup"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_mediagroup_success(self):
|
||||
"""Тест успешного добавления медиагруппы в БД"""
|
||||
# Создаем моки сообщений
|
||||
mock_message1 = Mock()
|
||||
mock_message1.message_id = 1
|
||||
mock_message1.photo = [Mock()]
|
||||
mock_message1.photo[-1].file_id = 'photo_1'
|
||||
mock_message1.video = None
|
||||
mock_message1.voice = None
|
||||
mock_message1.audio = None
|
||||
mock_message1.video_note = None
|
||||
|
||||
mock_message2 = Mock()
|
||||
mock_message2.message_id = 2
|
||||
mock_message2.photo = None
|
||||
mock_message2.video = Mock()
|
||||
mock_message2.video.file_id = 'video_1'
|
||||
mock_message2.voice = None
|
||||
mock_message2.audio = None
|
||||
mock_message2.video_note = None
|
||||
|
||||
sent_messages = [mock_message1, mock_message2]
|
||||
|
||||
# Мокаем БД
|
||||
mock_db = AsyncMock()
|
||||
mock_db.add_post_content = AsyncMock(return_value=True)
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value='files/test.jpg'):
|
||||
result = await add_in_db_media_mediagroup(sent_messages, mock_db, main_post_id=100)
|
||||
|
||||
assert result is True
|
||||
assert mock_db.add_post_content.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_mediagroup_empty_list(self):
|
||||
"""Тест с пустым списком сообщений"""
|
||||
mock_db = AsyncMock()
|
||||
|
||||
result = await add_in_db_media_mediagroup([], mock_db)
|
||||
|
||||
assert result is False
|
||||
mock_db.add_post_content.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_mediagroup_partial_failure(self):
|
||||
"""Тест когда часть сообщений обрабатывается успешно"""
|
||||
# Создаем моки сообщений
|
||||
mock_message1 = Mock()
|
||||
mock_message1.message_id = 1
|
||||
mock_message1.photo = [Mock()]
|
||||
mock_message1.photo[-1].file_id = 'photo_1'
|
||||
mock_message1.video = None
|
||||
mock_message1.voice = None
|
||||
mock_message1.audio = None
|
||||
mock_message1.video_note = None
|
||||
|
||||
mock_message2 = Mock()
|
||||
mock_message2.message_id = 2
|
||||
mock_message2.photo = None
|
||||
mock_message2.video = None
|
||||
mock_message2.voice = None
|
||||
mock_message2.audio = None
|
||||
mock_message2.video_note = None # Неподдерживаемый тип
|
||||
|
||||
sent_messages = [mock_message1, mock_message2]
|
||||
|
||||
# Мокаем БД
|
||||
mock_db = AsyncMock()
|
||||
mock_db.add_post_content = AsyncMock(return_value=True)
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value='files/test.jpg'):
|
||||
result = await add_in_db_media_mediagroup(sent_messages, mock_db)
|
||||
|
||||
# Должен вернуть False, так как есть ошибки (второе сообщение не поддерживается)
|
||||
assert result is False
|
||||
assert mock_db.add_post_content.call_count == 1
|
||||
|
||||
|
||||
class TestSendMediaGroupMessageToPrivateChat:
|
||||
"""Тесты для функции send_media_group_message_to_private_chat"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_media_group_message_success(self):
|
||||
"""Тест успешной отправки медиагруппы"""
|
||||
# Мокаем сообщение
|
||||
mock_message = Mock()
|
||||
mock_message.from_user.id = 123
|
||||
mock_message.bot = Mock()
|
||||
|
||||
# Мокаем отправленное сообщение
|
||||
mock_sent_message = Mock()
|
||||
mock_sent_message.message_id = 456
|
||||
mock_sent_message.caption = "Test caption"
|
||||
mock_message.bot.send_media_group = AsyncMock(return_value=[mock_sent_message])
|
||||
|
||||
# Мокаем БД
|
||||
mock_db = AsyncMock()
|
||||
mock_db.add_post = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=True):
|
||||
result = await send_media_group_message_to_private_chat(
|
||||
100, mock_message, [], mock_db, main_post_id=789
|
||||
)
|
||||
|
||||
assert result == 456
|
||||
mock_message.bot.send_media_group.assert_called_once()
|
||||
mock_db.add_post.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_media_group_message_media_processing_fails(self):
|
||||
"""Тест когда обработка медиа не удается"""
|
||||
# Мокаем сообщение
|
||||
mock_message = Mock()
|
||||
mock_message.from_user.id = 123
|
||||
mock_message.bot = Mock()
|
||||
|
||||
# Мокаем отправленное сообщение
|
||||
mock_sent_message = Mock()
|
||||
mock_sent_message.message_id = 456
|
||||
mock_sent_message.caption = "Test caption"
|
||||
mock_message.bot.send_media_group = AsyncMock(return_value=[mock_sent_message])
|
||||
|
||||
# Мокаем БД
|
||||
mock_db = AsyncMock()
|
||||
mock_db.add_post = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=False):
|
||||
result = await send_media_group_message_to_private_chat(
|
||||
100, mock_message, [], mock_db, main_post_id=789
|
||||
)
|
||||
|
||||
assert result == 456 # Функция все равно возвращает message_id
|
||||
mock_message.bot.send_media_group.assert_called_once()
|
||||
mock_db.add_post.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
@@ -10,7 +10,7 @@ from helper_bot.keyboards.keyboards import (
|
||||
create_keyboard_with_pagination
|
||||
)
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from database.db import BotDB
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
|
||||
class TestKeyboards:
|
||||
@@ -19,18 +19,19 @@ class TestKeyboards:
|
||||
@pytest.fixture
|
||||
def mock_db(self):
|
||||
"""Создает мок базы данных"""
|
||||
db = Mock(spec=BotDB)
|
||||
db = Mock(spec=AsyncBotDB)
|
||||
db.get_user_info = Mock(return_value={
|
||||
'stickers': True,
|
||||
'admin': False
|
||||
})
|
||||
return db
|
||||
|
||||
def test_get_reply_keyboard_basic(self, mock_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_reply_keyboard_basic(self, mock_db):
|
||||
"""Тест базовой клавиатуры"""
|
||||
user_id = 123456
|
||||
|
||||
keyboard = get_reply_keyboard(mock_db, user_id)
|
||||
keyboard = await get_reply_keyboard(mock_db, user_id)
|
||||
|
||||
# Проверяем, что возвращается клавиатура
|
||||
assert isinstance(keyboard, ReplyKeyboardMarkup)
|
||||
@@ -52,13 +53,14 @@ class TestKeyboards:
|
||||
assert '👋🏼Сказать пока!' in all_buttons
|
||||
assert '📩Связаться с админами' in all_buttons
|
||||
|
||||
def test_get_reply_keyboard_with_stickers(self, mock_db):
|
||||
@pytest.mark.asyncio
|
||||
async 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)
|
||||
# Мокаем метод get_stickers_info
|
||||
mock_db.get_stickers_info = AsyncMock(return_value=False)
|
||||
|
||||
keyboard = get_reply_keyboard(mock_db, user_id)
|
||||
keyboard = await get_reply_keyboard(mock_db, user_id)
|
||||
|
||||
all_buttons = []
|
||||
for row in keyboard.keyboard:
|
||||
@@ -68,13 +70,14 @@ class TestKeyboards:
|
||||
# Проверяем наличие кнопки стикеров
|
||||
assert '🤪Хочу стикеры' in all_buttons
|
||||
|
||||
def test_get_reply_keyboard_without_stickers(self, mock_db):
|
||||
@pytest.mark.asyncio
|
||||
async 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)
|
||||
# Мокаем метод get_stickers_info
|
||||
mock_db.get_stickers_info = AsyncMock(return_value=True)
|
||||
|
||||
keyboard = get_reply_keyboard(mock_db, user_id)
|
||||
keyboard = await get_reply_keyboard(mock_db, user_id)
|
||||
|
||||
all_buttons = []
|
||||
for row in keyboard.keyboard:
|
||||
@@ -84,13 +87,14 @@ class TestKeyboards:
|
||||
# Проверяем отсутствие кнопки стикеров
|
||||
assert '🤪Хочу стикеры' not in all_buttons
|
||||
|
||||
def test_get_reply_keyboard_admin(self, mock_db):
|
||||
@pytest.mark.asyncio
|
||||
async 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)
|
||||
# Мокаем метод get_stickers_info
|
||||
mock_db.get_stickers_info = AsyncMock(return_value=False)
|
||||
|
||||
keyboard = get_reply_keyboard(mock_db, user_id)
|
||||
keyboard = await get_reply_keyboard(mock_db, user_id)
|
||||
|
||||
all_buttons = []
|
||||
for row in keyboard.keyboard:
|
||||
@@ -282,44 +286,41 @@ class TestChatTypeFilter:
|
||||
class TestKeyboardIntegration:
|
||||
"""Интеграционные тесты клавиатур"""
|
||||
|
||||
def test_keyboard_structure_consistency(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_keyboard_structure_consistency(self):
|
||||
"""Тест консистентности структуры клавиатур"""
|
||||
# Мокаем базу данных
|
||||
mock_db = Mock(spec=BotDB)
|
||||
mock_db.get_info_about_stickers = Mock(return_value=False)
|
||||
mock_db = Mock(spec=AsyncBotDB)
|
||||
mock_db.get_stickers_info = AsyncMock(return_value=False)
|
||||
|
||||
# Тестируем все типы клавиатур
|
||||
keyboards = [
|
||||
get_reply_keyboard(mock_db, 123456),
|
||||
get_reply_keyboard_for_post(),
|
||||
get_reply_keyboard_leave_chat()
|
||||
]
|
||||
keyboard1 = await get_reply_keyboard(mock_db, 123456)
|
||||
keyboard2 = get_reply_keyboard_for_post()
|
||||
keyboard3 = 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):
|
||||
@pytest.mark.asyncio
|
||||
async def test_keyboard_button_texts(self):
|
||||
"""Тест текстов кнопок клавиатур"""
|
||||
# Тестируем основные кнопки
|
||||
db = Mock(spec=BotDB)
|
||||
db.get_info_about_stickers = Mock(return_value=False)
|
||||
db = Mock(spec=AsyncBotDB)
|
||||
db.get_stickers_info = AsyncMock(return_value=False)
|
||||
|
||||
main_keyboard = get_reply_keyboard(db, 123456)
|
||||
main_keyboard = await get_reply_keyboard(db, 123456)
|
||||
post_keyboard = get_reply_keyboard_for_post()
|
||||
leave_keyboard = get_reply_keyboard_leave_chat()
|
||||
|
||||
|
||||
204
tests/test_message_repository.py
Normal file
204
tests/test_message_repository.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.models import UserMessage
|
||||
|
||||
|
||||
class TestMessageRepository:
|
||||
"""Тесты для MessageRepository."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_path(self):
|
||||
"""Фикстура для пути к тестовой БД."""
|
||||
return ":memory:"
|
||||
|
||||
@pytest.fixture
|
||||
def message_repository(self, mock_db_path):
|
||||
"""Фикстура для MessageRepository."""
|
||||
return MessageRepository(mock_db_path)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message(self):
|
||||
"""Фикстура для тестового сообщения."""
|
||||
return UserMessage(
|
||||
message_text="Тестовое сообщение",
|
||||
user_id=12345,
|
||||
telegram_message_id=67890,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message_no_date(self):
|
||||
"""Фикстура для тестового сообщения без даты."""
|
||||
return UserMessage(
|
||||
message_text="Тестовое сообщение без даты",
|
||||
user_id=12345,
|
||||
telegram_message_id=67891,
|
||||
date=None
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables(self, message_repository):
|
||||
"""Тест создания таблиц."""
|
||||
# Мокаем _execute_query
|
||||
message_repository._execute_query = AsyncMock()
|
||||
|
||||
await message_repository.create_tables()
|
||||
|
||||
message_repository._execute_query.assert_called_once()
|
||||
call_args = message_repository._execute_query.call_args[0][0]
|
||||
assert "CREATE TABLE IF NOT EXISTS user_messages" in call_args
|
||||
assert "telegram_message_id INTEGER NOT NULL" in call_args
|
||||
assert "date INTEGER NOT NULL" in call_args
|
||||
assert "FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_with_date(self, message_repository, sample_message):
|
||||
"""Тест добавления сообщения с датой."""
|
||||
# Мокаем _execute_query
|
||||
message_repository._execute_query = AsyncMock()
|
||||
|
||||
await message_repository.add_message(sample_message)
|
||||
|
||||
message_repository._execute_query.assert_called_once()
|
||||
call_args = message_repository._execute_query.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "INSERT INTO user_messages" in query
|
||||
assert "VALUES (?, ?, ?, ?)" in query
|
||||
assert params == (
|
||||
sample_message.message_text,
|
||||
sample_message.user_id,
|
||||
sample_message.telegram_message_id,
|
||||
sample_message.date
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_without_date(self, message_repository, sample_message_no_date):
|
||||
"""Тест добавления сообщения без даты (должна генерироваться автоматически)."""
|
||||
# Мокаем _execute_query
|
||||
message_repository._execute_query = AsyncMock()
|
||||
|
||||
await message_repository.add_message(sample_message_no_date)
|
||||
|
||||
# Проверяем, что дата была установлена
|
||||
assert sample_message_no_date.date is not None
|
||||
assert isinstance(sample_message_no_date.date, int)
|
||||
assert sample_message_no_date.date > 0
|
||||
|
||||
message_repository._execute_query.assert_called_once()
|
||||
call_args = message_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
|
||||
assert params[3] == sample_message_no_date.date # date field
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_logs_correctly(self, message_repository, sample_message):
|
||||
"""Тест логирования при добавлении сообщения."""
|
||||
# Мокаем _execute_query и logger
|
||||
message_repository._execute_query = AsyncMock()
|
||||
message_repository.logger = MagicMock()
|
||||
|
||||
await message_repository.add_message(sample_message)
|
||||
|
||||
message_repository.logger.info.assert_called_once()
|
||||
log_message = message_repository.logger.info.call_args[0][0]
|
||||
assert f"telegram_message_id={sample_message.telegram_message_id}" in log_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_message_id_found(self, message_repository):
|
||||
"""Тест получения пользователя по message_id (пользователь найден)."""
|
||||
message_id = 67890
|
||||
expected_user_id = 12345
|
||||
|
||||
# Мокаем _execute_query_with_result
|
||||
message_repository._execute_query_with_result = AsyncMock(
|
||||
return_value=[[expected_user_id]]
|
||||
)
|
||||
|
||||
result = await message_repository.get_user_by_message_id(message_id)
|
||||
|
||||
assert result == expected_user_id
|
||||
message_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT user_id FROM user_messages WHERE telegram_message_id = ?",
|
||||
(message_id,)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_message_id_not_found(self, message_repository):
|
||||
"""Тест получения пользователя по message_id (пользователь не найден)."""
|
||||
message_id = 99999
|
||||
|
||||
# Мокаем _execute_query_with_result
|
||||
message_repository._execute_query_with_result = AsyncMock(return_value=[])
|
||||
|
||||
result = await message_repository.get_user_by_message_id(message_id)
|
||||
|
||||
assert result is None
|
||||
message_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT user_id FROM user_messages WHERE telegram_message_id = ?",
|
||||
(message_id,)
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_message_id_empty_result(self, message_repository):
|
||||
"""Тест получения пользователя по message_id (пустой результат)."""
|
||||
message_id = 99999
|
||||
|
||||
# Мокаем _execute_query_with_result
|
||||
message_repository._execute_query_with_result = AsyncMock(return_value=[[]])
|
||||
|
||||
result = await message_repository.get_user_by_message_id(message_id)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_handles_exception(self, message_repository, sample_message):
|
||||
"""Тест обработки исключений при добавлении сообщения."""
|
||||
# Мокаем _execute_query для вызова исключения
|
||||
message_repository._execute_query = AsyncMock(side_effect=Exception("Database error"))
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await message_repository.add_message(sample_message)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_message_id_handles_exception(self, message_repository):
|
||||
"""Тест обработки исключений при получении пользователя."""
|
||||
# Мокаем _execute_query_with_result для вызова исключения
|
||||
message_repository._execute_query_with_result = AsyncMock(
|
||||
side_effect=Exception("Database error")
|
||||
)
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await message_repository.get_user_by_message_id(12345)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_with_zero_date(self, message_repository):
|
||||
"""Тест добавления сообщения с датой равной 0 (должна генерироваться новая)."""
|
||||
message = UserMessage(
|
||||
message_text="Тестовое сообщение с нулевой датой",
|
||||
user_id=12345,
|
||||
telegram_message_id=67892,
|
||||
date=0
|
||||
)
|
||||
|
||||
# Мокаем _execute_query
|
||||
message_repository._execute_query = AsyncMock()
|
||||
|
||||
await message_repository.add_message(message)
|
||||
|
||||
# Проверяем, что дата была изменена с 0 (теперь это происходит только если date is None)
|
||||
# В текущей реализации дата 0 считается валидной и не изменяется
|
||||
assert isinstance(message.date, int)
|
||||
assert message.date >= 0
|
||||
|
||||
message_repository._execute_query.assert_called_once()
|
||||
params = message_repository._execute_query.call_args[0][1]
|
||||
assert params[3] == message.date # date field
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
215
tests/test_message_repository_integration.py
Normal file
215
tests/test_message_repository_integration.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import tempfile
|
||||
import os
|
||||
from datetime import datetime
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.models import UserMessage
|
||||
|
||||
|
||||
class TestMessageRepositoryIntegration:
|
||||
"""Интеграционные тесты для MessageRepository с реальной БД."""
|
||||
|
||||
async def _setup_test_database(self, message_repository):
|
||||
"""Вспомогательная функция для настройки тестовой БД."""
|
||||
# Сначала создаем таблицу our_users для тестов
|
||||
await message_repository._execute_query('''
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
full_name TEXT,
|
||||
username TEXT,
|
||||
is_bot BOOLEAN DEFAULT 0,
|
||||
language_code TEXT,
|
||||
has_stickers BOOLEAN DEFAULT 0 NOT NULL,
|
||||
emoji TEXT,
|
||||
date_added INTEGER NOT NULL,
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
)
|
||||
''')
|
||||
|
||||
# Добавляем тестового пользователя
|
||||
await message_repository._execute_query(
|
||||
"INSERT OR REPLACE INTO our_users (user_id, first_name, full_name, date_added, date_changed) VALUES (?, ?, ?, ?, ?)",
|
||||
(12345, "Test", "Test User", int(datetime.now().timestamp()), int(datetime.now().timestamp()))
|
||||
)
|
||||
|
||||
# Теперь создаем таблицу user_messages
|
||||
await message_repository.create_tables()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db_path(self):
|
||||
"""Фикстура для временного пути к БД."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
yield temp_path
|
||||
|
||||
# Очистка после тестов
|
||||
try:
|
||||
os.unlink(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@pytest.fixture
|
||||
def message_repository(self, temp_db_path):
|
||||
"""Фикстура для MessageRepository с реальной БД."""
|
||||
return MessageRepository(temp_db_path)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message(self):
|
||||
"""Фикстура для тестового сообщения."""
|
||||
return UserMessage(
|
||||
message_text="Интеграционное тестовое сообщение",
|
||||
user_id=12345,
|
||||
telegram_message_id=67890,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message_no_date(self):
|
||||
"""Фикстура для тестового сообщения без даты."""
|
||||
return UserMessage(
|
||||
message_text="Интеграционное тестовое сообщение без даты",
|
||||
user_id=12345,
|
||||
telegram_message_id=67891,
|
||||
date=None
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables_integration(self, message_repository):
|
||||
"""Интеграционный тест создания таблиц."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(message_repository)
|
||||
|
||||
# Проверяем, что таблица создана, пытаясь добавить сообщение
|
||||
message = UserMessage(
|
||||
message_text="Тест создания таблиц",
|
||||
user_id=12345,
|
||||
telegram_message_id=67890,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
# Не должно вызывать ошибку
|
||||
await message_repository.add_message(message)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_and_retrieve_message_integration(self, message_repository, sample_message):
|
||||
"""Интеграционный тест добавления и получения сообщения."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(message_repository)
|
||||
|
||||
# Добавляем сообщение
|
||||
await message_repository.add_message(sample_message)
|
||||
|
||||
# Получаем пользователя по message_id
|
||||
user_id = await message_repository.get_user_by_message_id(sample_message.telegram_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert user_id == sample_message.user_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_without_date_integration(self, message_repository, sample_message_no_date):
|
||||
"""Интеграционный тест добавления сообщения без даты."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(message_repository)
|
||||
|
||||
# Добавляем сообщение без даты
|
||||
await message_repository.add_message(sample_message_no_date)
|
||||
|
||||
# Проверяем, что дата была установлена
|
||||
assert sample_message_no_date.date is not None
|
||||
assert isinstance(sample_message_no_date.date, int)
|
||||
assert sample_message_no_date.date > 0
|
||||
|
||||
# Проверяем, что сообщение можно найти
|
||||
user_id = await message_repository.get_user_by_message_id(sample_message_no_date.telegram_message_id)
|
||||
assert user_id == sample_message_no_date.user_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_message_id_not_found_integration(self, message_repository):
|
||||
"""Интеграционный тест поиска несуществующего сообщения."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(message_repository)
|
||||
|
||||
# Ищем несуществующее сообщение
|
||||
user_id = await message_repository.get_user_by_message_id(99999)
|
||||
|
||||
# Должно вернуть None
|
||||
assert user_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_messages_integration(self, message_repository):
|
||||
"""Интеграционный тест работы с несколькими сообщениями."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(message_repository)
|
||||
|
||||
# Добавляем несколько сообщений (используем существующий user_id 12345)
|
||||
messages = [
|
||||
UserMessage(
|
||||
message_text=f"Сообщение {i}",
|
||||
user_id=12345, # Используем существующий user_id
|
||||
telegram_message_id=2000 + i,
|
||||
date=int(datetime.now().timestamp()) + i
|
||||
)
|
||||
for i in range(1, 4)
|
||||
]
|
||||
|
||||
for message in messages:
|
||||
await message_repository.add_message(message)
|
||||
|
||||
# Проверяем, что все сообщения можно найти
|
||||
for message in messages:
|
||||
user_id = await message_repository.get_user_by_message_id(message.telegram_message_id)
|
||||
assert user_id == message.user_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_message_with_special_characters_integration(self, message_repository):
|
||||
"""Интеграционный тест сообщения со специальными символами."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(message_repository)
|
||||
|
||||
# Сообщение со специальными символами
|
||||
special_message = UserMessage(
|
||||
message_text="Сообщение с 'кавычками' и \"двойными кавычками\" и эмодзи 😊",
|
||||
user_id=12345,
|
||||
telegram_message_id=67892,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
# Добавляем сообщение
|
||||
await message_repository.add_message(special_message)
|
||||
|
||||
# Проверяем, что можно найти
|
||||
user_id = await message_repository.get_user_by_message_id(special_message.telegram_message_id)
|
||||
assert user_id == special_message.user_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_foreign_key_constraint_integration(self, message_repository):
|
||||
"""Интеграционный тест ограничения внешнего ключа."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(message_repository)
|
||||
|
||||
# Пытаемся добавить сообщение с несуществующим user_id
|
||||
invalid_message = UserMessage(
|
||||
message_text="Сообщение с несуществующим пользователем",
|
||||
user_id=99999, # Несуществующий пользователь
|
||||
telegram_message_id=67893,
|
||||
date=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
# В SQLite с включенными внешними ключами это должно вызвать ошибку
|
||||
# Теперь у нас есть таблица our_users, поэтому внешний ключ должен работать
|
||||
try:
|
||||
await message_repository.add_message(invalid_message)
|
||||
# Если не вызвало ошибку, проверяем что сообщение не добавилось
|
||||
user_id = await message_repository.get_user_by_message_id(invalid_message.telegram_message_id)
|
||||
assert user_id is None
|
||||
except Exception:
|
||||
# Ожидаемое поведение при нарушении внешнего ключа
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Тестовый скрипт для проверки модуля мониторинга сервера
|
||||
"""
|
||||
import pytest
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Добавляем путь к проекту
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from helper_bot.server_monitor import ServerMonitor
|
||||
|
||||
|
||||
class MockBot:
|
||||
"""Мок объект бота для тестирования"""
|
||||
|
||||
async def send_message(self, chat_id, text, parse_mode=None):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Отправка в чат: {chat_id}")
|
||||
print(f"Текст сообщения:")
|
||||
print(text)
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_monitor():
|
||||
"""Тестирование модуля мониторинга"""
|
||||
print("🧪 Тестирование модуля мониторинга сервера")
|
||||
print("=" * 60)
|
||||
|
||||
# Создаем мок бота
|
||||
mock_bot = MockBot()
|
||||
|
||||
# Создаем монитор
|
||||
monitor = ServerMonitor(
|
||||
bot=mock_bot,
|
||||
group_for_logs="-123456789",
|
||||
important_logs="-987654321"
|
||||
)
|
||||
|
||||
print("📊 Получение информации о системе...")
|
||||
system_info = monitor.get_system_info()
|
||||
|
||||
if system_info:
|
||||
print("✅ Информация о системе получена успешно")
|
||||
print(f"CPU: {system_info['cpu_percent']}%")
|
||||
print(f"RAM: {system_info['ram_percent']}%")
|
||||
print(f"Disk: {system_info['disk_percent']}%")
|
||||
print(f"Uptime: {system_info['system_uptime']}")
|
||||
|
||||
print("\n🤖 Проверка статуса процессов...")
|
||||
voice_status, voice_uptime = monitor.check_process_status('voice_bot')
|
||||
helper_status, helper_uptime = monitor.check_process_status('helper_bot')
|
||||
print(f"Voice Bot: {voice_status} - {voice_uptime}")
|
||||
print(f"Helper Bot: {helper_status} - {helper_uptime}")
|
||||
|
||||
print("\n📝 Тестирование отправки статуса...")
|
||||
await monitor.send_status_message(system_info)
|
||||
|
||||
print("\n🚨 Тестирование отправки алерта...")
|
||||
await monitor.send_alert_message(
|
||||
"Использование CPU",
|
||||
85.5,
|
||||
"Нагрузка за 1 мин: 2.5"
|
||||
)
|
||||
|
||||
print("\n✅ Тестирование отправки сообщения о восстановлении...")
|
||||
await monitor.send_recovery_message(
|
||||
"Использование CPU",
|
||||
70.0,
|
||||
85.5
|
||||
)
|
||||
|
||||
else:
|
||||
print("❌ Не удалось получить информацию о системе")
|
||||
|
||||
print("\n🎯 Тестирование завершено!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_monitor())
|
||||
438
tests/test_post_repository.py
Normal file
438
tests/test_post_repository.py
Normal file
@@ -0,0 +1,438 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from database.repositories.post_repository import PostRepository
|
||||
from database.models import TelegramPost, PostContent, MessageContentLink
|
||||
|
||||
|
||||
class TestPostRepository:
|
||||
"""Тесты для PostRepository."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_path(self):
|
||||
"""Фикстура для пути к тестовой БД."""
|
||||
return ":memory:"
|
||||
|
||||
@pytest.fixture
|
||||
def post_repository(self, mock_db_path):
|
||||
"""Фикстура для PostRepository."""
|
||||
return PostRepository(mock_db_path)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post(self):
|
||||
"""Фикстура для тестового поста."""
|
||||
return TelegramPost(
|
||||
message_id=12345,
|
||||
text="Тестовый пост",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post_no_date(self):
|
||||
"""Фикстура для тестового поста без даты."""
|
||||
return TelegramPost(
|
||||
message_id=12346,
|
||||
text="Тестовый пост без даты",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=None
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post_content(self):
|
||||
"""Фикстура для тестового контента поста."""
|
||||
return PostContent(
|
||||
message_id=12345,
|
||||
content_name="/path/to/file.jpg",
|
||||
content_type="photo"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_message_link(self):
|
||||
"""Фикстура для тестовой связи сообщения с контентом."""
|
||||
return MessageContentLink(
|
||||
post_id=12345,
|
||||
message_id=67890
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables(self, post_repository):
|
||||
"""Тест создания таблиц."""
|
||||
# Мокаем _execute_query
|
||||
post_repository._execute_query = AsyncMock()
|
||||
|
||||
await post_repository.create_tables()
|
||||
|
||||
# Проверяем, что create_tables вызвался 3 раза (для каждой таблицы)
|
||||
assert post_repository._execute_query.call_count == 3
|
||||
|
||||
# Проверяем создание таблицы постов
|
||||
calls = post_repository._execute_query.call_args_list
|
||||
post_table_call = calls[0][0][0]
|
||||
assert "CREATE TABLE IF NOT EXISTS post_from_telegram_suggest" in post_table_call
|
||||
assert "message_id INTEGER NOT NULL PRIMARY KEY" in post_table_call
|
||||
assert "created_at INTEGER NOT NULL" in post_table_call
|
||||
assert "FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in post_table_call
|
||||
|
||||
# Проверяем создание таблицы контента
|
||||
content_table_call = calls[1][0][0]
|
||||
assert "CREATE TABLE IF NOT EXISTS content_post_from_telegram" in content_table_call
|
||||
assert "PRIMARY KEY (message_id, content_name)" in content_table_call
|
||||
|
||||
# Проверяем создание таблицы связей
|
||||
link_table_call = calls[2][0][0]
|
||||
assert "CREATE TABLE IF NOT EXISTS message_link_to_content" in link_table_call
|
||||
assert "PRIMARY KEY (post_id, message_id)" in link_table_call
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_with_date(self, post_repository, sample_post):
|
||||
"""Тест добавления поста с датой."""
|
||||
# Мокаем _execute_query
|
||||
post_repository._execute_query = AsyncMock()
|
||||
|
||||
await post_repository.add_post(sample_post)
|
||||
|
||||
post_repository._execute_query.assert_called_once()
|
||||
call_args = post_repository._execute_query.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "INSERT INTO post_from_telegram_suggest" in query
|
||||
assert "VALUES (?, ?, ?, ?)" in query
|
||||
assert params == (
|
||||
sample_post.message_id,
|
||||
sample_post.text,
|
||||
sample_post.author_id,
|
||||
sample_post.created_at
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_without_date(self, post_repository, sample_post_no_date):
|
||||
"""Тест добавления поста без даты (должна генерироваться автоматически)."""
|
||||
# Мокаем _execute_query
|
||||
post_repository._execute_query = AsyncMock()
|
||||
|
||||
await post_repository.add_post(sample_post_no_date)
|
||||
|
||||
# Проверяем, что дата была установлена
|
||||
assert sample_post_no_date.created_at is not None
|
||||
assert isinstance(sample_post_no_date.created_at, int)
|
||||
assert sample_post_no_date.created_at > 0
|
||||
|
||||
post_repository._execute_query.assert_called_once()
|
||||
call_args = post_repository._execute_query.call_args
|
||||
params = call_args[0][1]
|
||||
|
||||
assert params[3] == sample_post_no_date.created_at # created_at field
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_logs_correctly(self, post_repository, sample_post):
|
||||
"""Тест логирования при добавлении поста."""
|
||||
# Мокаем _execute_query и logger
|
||||
post_repository._execute_query = AsyncMock()
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
await post_repository.add_post(sample_post)
|
||||
|
||||
post_repository.logger.info.assert_called_once_with(
|
||||
f"Пост добавлен: message_id={sample_post.message_id}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_helper_message(self, post_repository):
|
||||
"""Тест обновления helper сообщения."""
|
||||
# Мокаем _execute_query
|
||||
post_repository._execute_query = AsyncMock()
|
||||
|
||||
message_id = 12345
|
||||
helper_message_id = 67890
|
||||
|
||||
await post_repository.update_helper_message(message_id, helper_message_id)
|
||||
|
||||
post_repository._execute_query.assert_called_once()
|
||||
call_args = post_repository._execute_query.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "UPDATE post_from_telegram_suggest SET helper_text_message_id = ? WHERE message_id = ?" in query
|
||||
assert params == (helper_message_id, message_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_content_success(self, post_repository):
|
||||
"""Тест успешного добавления контента поста."""
|
||||
# Мокаем _execute_query
|
||||
post_repository._execute_query = AsyncMock()
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
post_id = 12345
|
||||
message_id = 67890
|
||||
content_name = "/path/to/file.jpg"
|
||||
content_type = "photo"
|
||||
|
||||
result = await post_repository.add_post_content(post_id, message_id, content_name, content_type)
|
||||
|
||||
# Проверяем, что результат True
|
||||
assert result is True
|
||||
|
||||
# Проверяем, что _execute_query вызвался 2 раза (для связи и контента)
|
||||
assert post_repository._execute_query.call_count == 2
|
||||
|
||||
# Проверяем вызов для связи
|
||||
link_call = post_repository._execute_query.call_args_list[0]
|
||||
link_query = link_call[0][0]
|
||||
link_params = link_call[0][1]
|
||||
assert "INSERT OR IGNORE INTO message_link_to_content" in link_query
|
||||
assert link_params == (post_id, message_id)
|
||||
|
||||
# Проверяем вызов для контента
|
||||
content_call = post_repository._execute_query.call_args_list[1]
|
||||
content_query = content_call[0][0]
|
||||
content_params = content_call[0][1]
|
||||
assert "INSERT OR IGNORE INTO content_post_from_telegram" in content_query
|
||||
assert content_params == (message_id, content_name, content_type)
|
||||
|
||||
# Проверяем логирование
|
||||
post_repository.logger.info.assert_called_once_with(
|
||||
f"Контент поста добавлен: post_id={post_id}, message_id={message_id}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_content_exception(self, post_repository):
|
||||
"""Тест обработки исключения при добавлении контента поста."""
|
||||
# Мокаем _execute_query чтобы вызвать исключение
|
||||
post_repository._execute_query = AsyncMock(side_effect=Exception("Database error"))
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
post_id = 12345
|
||||
message_id = 67890
|
||||
content_name = "/path/to/file.jpg"
|
||||
content_type = "photo"
|
||||
|
||||
result = await post_repository.add_post_content(post_id, message_id, content_name, content_type)
|
||||
|
||||
# Проверяем, что результат False
|
||||
assert result is False
|
||||
|
||||
# Проверяем логирование ошибки
|
||||
post_repository.logger.error.assert_called_once()
|
||||
error_call = post_repository.logger.error.call_args[0][0]
|
||||
assert "Ошибка при добавлении контента поста:" in error_call
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_post_content_by_helper_id(self, post_repository):
|
||||
"""Тест получения контента поста по helper ID."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = [
|
||||
("/path/to/photo1.jpg", "photo"),
|
||||
("/path/to/video1.mp4", "video"),
|
||||
("/path/to/photo2.jpg", "photo")
|
||||
]
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
helper_message_id = 67890
|
||||
|
||||
result = await post_repository.get_post_content_by_helper_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result == mock_result
|
||||
|
||||
# Проверяем вызов _execute_query_with_result
|
||||
post_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = post_repository._execute_query_with_result.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "SELECT cpft.content_name, cpft.content_type" in query
|
||||
assert "WHERE pft.helper_text_message_id = ?" in query
|
||||
assert params == (helper_message_id,)
|
||||
|
||||
# Проверяем логирование
|
||||
post_repository.logger.info.assert_called_once_with(
|
||||
f"Получен контент поста: {len(mock_result)} элементов"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_post_text_by_helper_id_found(self, post_repository):
|
||||
"""Тест получения текста поста по helper ID (пост найден)."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = [("Тестовый текст поста",)]
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
helper_message_id = 67890
|
||||
|
||||
result = await post_repository.get_post_text_by_helper_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result == "Тестовый текст поста"
|
||||
|
||||
# Проверяем вызов _execute_query_with_result
|
||||
post_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = post_repository._execute_query_with_result.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "SELECT text FROM post_from_telegram_suggest WHERE helper_text_message_id = ?" in query
|
||||
assert params == (helper_message_id,)
|
||||
|
||||
# Проверяем логирование
|
||||
post_repository.logger.info.assert_called_once_with(
|
||||
f"Получен текст поста для helper_message_id={helper_message_id}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_post_text_by_helper_id_not_found(self, post_repository):
|
||||
"""Тест получения текста поста по helper ID (пост не найден)."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = []
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
helper_message_id = 67890
|
||||
|
||||
result = await post_repository.get_post_text_by_helper_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is None
|
||||
|
||||
# Проверяем, что logger.info не вызывался
|
||||
post_repository.logger.info.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_post_ids_by_helper_id(self, post_repository):
|
||||
"""Тест получения ID сообщений по helper ID."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = [(12345,), (67890,), (11111,)]
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
helper_message_id = 67890
|
||||
|
||||
result = await post_repository.get_post_ids_by_helper_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result == [12345, 67890, 11111]
|
||||
|
||||
# Проверяем вызов _execute_query_with_result
|
||||
post_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = post_repository._execute_query_with_result.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "SELECT mltc.message_id" in query
|
||||
assert "WHERE pft.helper_text_message_id = ?" in query
|
||||
assert params == (helper_message_id,)
|
||||
|
||||
# Проверяем логирование
|
||||
post_repository.logger.info.assert_called_once_with(
|
||||
f"Получены ID сообщений: {len(mock_result)} элементов"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_message_id_found(self, post_repository):
|
||||
"""Тест получения ID автора по message ID (автор найден)."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = [(67890,)]
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
message_id = 12345
|
||||
|
||||
result = await post_repository.get_author_id_by_message_id(message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result == 67890
|
||||
|
||||
# Проверяем вызов _execute_query_with_result
|
||||
post_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = post_repository._execute_query_with_result.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "SELECT author_id FROM post_from_telegram_suggest WHERE message_id = ?" in query
|
||||
assert params == (message_id,)
|
||||
|
||||
# Проверяем логирование
|
||||
post_repository.logger.info.assert_called_once_with(
|
||||
f"Получен author_id: {67890} для message_id={message_id}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_message_id_not_found(self, post_repository):
|
||||
"""Тест получения ID автора по message ID (автор не найден)."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = []
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
message_id = 12345
|
||||
|
||||
result = await post_repository.get_author_id_by_message_id(message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is None
|
||||
|
||||
# Проверяем, что logger.info не вызывался
|
||||
post_repository.logger.info.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_helper_message_id_found(self, post_repository):
|
||||
"""Тест получения ID автора по helper message ID (автор найден)."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = [(67890,)]
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
helper_message_id = 12345
|
||||
|
||||
result = await post_repository.get_author_id_by_helper_message_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result == 67890
|
||||
|
||||
# Проверяем вызов _execute_query_with_result
|
||||
post_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = post_repository._execute_query_with_result.call_args
|
||||
query = call_args[0][0]
|
||||
params = call_args[0][1]
|
||||
|
||||
assert "SELECT author_id FROM post_from_telegram_suggest WHERE helper_text_message_id = ?" in query
|
||||
assert params == (helper_message_id,)
|
||||
|
||||
# Проверяем логирование
|
||||
post_repository.logger.info.assert_called_once_with(
|
||||
f"Получен author_id: {67890} для helper_message_id={helper_message_id}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_helper_message_id_not_found(self, post_repository):
|
||||
"""Тест получения ID автора по helper message ID (автор не найден)."""
|
||||
# Мокаем _execute_query_with_result
|
||||
mock_result = []
|
||||
post_repository._execute_query_with_result = AsyncMock(return_value=mock_result)
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
helper_message_id = 12345
|
||||
|
||||
result = await post_repository.get_author_id_by_helper_message_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert result is None
|
||||
|
||||
# Проверяем, что logger.info не вызывался
|
||||
post_repository.logger.info.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables_logs_success(self, post_repository):
|
||||
"""Тест логирования успешного создания таблиц."""
|
||||
# Мокаем _execute_query и logger
|
||||
post_repository._execute_query = AsyncMock()
|
||||
post_repository.logger = MagicMock()
|
||||
|
||||
await post_repository.create_tables()
|
||||
|
||||
post_repository.logger.info.assert_called_once_with("Таблицы для постов созданы")
|
||||
497
tests/test_post_repository_integration.py
Normal file
497
tests/test_post_repository_integration.py
Normal file
@@ -0,0 +1,497 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from database.repositories.post_repository import PostRepository
|
||||
from database.models import TelegramPost, PostContent, MessageContentLink
|
||||
|
||||
|
||||
class TestPostRepositoryIntegration:
|
||||
"""Интеграционные тесты для PostRepository с реальной БД."""
|
||||
|
||||
async def _setup_test_database(self, post_repository):
|
||||
"""Вспомогательная функция для настройки тестовой БД."""
|
||||
# Сначала создаем таблицу our_users для тестов
|
||||
await post_repository._execute_query('''
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
full_name TEXT,
|
||||
username TEXT,
|
||||
is_bot BOOLEAN DEFAULT 0,
|
||||
language_code TEXT,
|
||||
has_stickers BOOLEAN DEFAULT 0 NOT NULL,
|
||||
emoji TEXT,
|
||||
date_added INTEGER NOT NULL,
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
)
|
||||
''')
|
||||
|
||||
# Добавляем тестовых пользователей
|
||||
await post_repository._execute_query(
|
||||
"INSERT OR REPLACE INTO our_users (user_id, first_name, full_name, date_added, date_changed) VALUES (?, ?, ?, ?, ?)",
|
||||
(67890, "Test", "Test User", int(datetime.now().timestamp()), int(datetime.now().timestamp()))
|
||||
)
|
||||
await post_repository._execute_query(
|
||||
"INSERT OR REPLACE INTO our_users (user_id, first_name, full_name, date_added, date_changed) VALUES (?, ?, ?, ?, ?)",
|
||||
(11111, "Test2", "Test User 2", int(datetime.now().timestamp()), int(datetime.now().timestamp()))
|
||||
)
|
||||
|
||||
# Теперь создаем таблицы для постов
|
||||
await post_repository.create_tables()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db_path(self):
|
||||
"""Фикстура для временного файла БД."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp_file:
|
||||
db_path = tmp_file.name
|
||||
|
||||
yield db_path
|
||||
|
||||
# Очищаем временный файл после тестов
|
||||
try:
|
||||
os.unlink(db_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@pytest.fixture
|
||||
def post_repository(self, temp_db_path):
|
||||
"""Фикстура для PostRepository с реальной БД."""
|
||||
return PostRepository(temp_db_path)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post(self):
|
||||
"""Фикстура для тестового поста."""
|
||||
return TelegramPost(
|
||||
message_id=12345,
|
||||
text="Тестовый пост для интеграционных тестов",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post_2(self):
|
||||
"""Фикстура для второго тестового поста."""
|
||||
return TelegramPost(
|
||||
message_id=12346,
|
||||
text="Второй тестовый пост",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_post_with_helper(self):
|
||||
"""Фикстура для тестового поста с helper сообщением."""
|
||||
return TelegramPost(
|
||||
message_id=12347,
|
||||
text="Пост с helper сообщением",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None, # Будет установлен позже
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables_integration(self, post_repository):
|
||||
"""Интеграционный тест создания таблиц."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Проверяем, что таблицы созданы (попробуем вставить тестовые данные)
|
||||
test_post = TelegramPost(
|
||||
message_id=99999,
|
||||
text="Тест создания таблиц",
|
||||
author_id=67890, # Используем существующего пользователя
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
|
||||
# Если таблицы созданы, то insert должен пройти успешно
|
||||
await post_repository.add_post(test_post)
|
||||
|
||||
# Проверяем, что пост действительно добавлен
|
||||
author_id = await post_repository.get_author_id_by_message_id(99999)
|
||||
assert author_id == 67890
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_integration(self, post_repository, sample_post):
|
||||
"""Интеграционный тест добавления поста."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post)
|
||||
|
||||
# Проверяем, что пост добавлен
|
||||
author_id = await post_repository.get_author_id_by_message_id(sample_post.message_id)
|
||||
assert author_id == sample_post.author_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_without_date_integration(self, post_repository):
|
||||
"""Интеграционный тест добавления поста без даты."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
post_without_date = TelegramPost(
|
||||
message_id=12348,
|
||||
text="Пост без даты",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=None
|
||||
)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(post_without_date)
|
||||
|
||||
# Проверяем, что дата была установлена автоматически
|
||||
assert post_without_date.created_at is not None
|
||||
assert isinstance(post_without_date.created_at, int)
|
||||
assert post_without_date.created_at > 0
|
||||
|
||||
# Проверяем, что пост добавлен
|
||||
author_id = await post_repository.get_author_id_by_message_id(post_without_date.message_id)
|
||||
assert author_id == post_without_date.author_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_helper_message_integration(self, post_repository, sample_post):
|
||||
"""Интеграционный тест обновления helper сообщения."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post)
|
||||
|
||||
# Обновляем helper сообщение
|
||||
helper_message_id = 88888
|
||||
await post_repository.update_helper_message(sample_post.message_id, helper_message_id)
|
||||
|
||||
# Проверяем, что helper сообщение обновлено
|
||||
# Для этого нужно получить пост и проверить helper_text_message_id
|
||||
# Но у нас нет метода для получения поста по ID, поэтому проверяем косвенно
|
||||
# через get_author_id_by_helper_message_id
|
||||
author_id = await post_repository.get_author_id_by_helper_message_id(helper_message_id)
|
||||
assert author_id == sample_post.author_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_content_integration(self, post_repository, sample_post):
|
||||
"""Интеграционный тест добавления контента поста."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post)
|
||||
|
||||
# Добавляем контент
|
||||
message_id = 11111
|
||||
content_name = "/path/to/test/photo.jpg"
|
||||
content_type = "photo"
|
||||
|
||||
# Сначала нужно добавить сообщение с этим message_id в post_from_telegram_suggest
|
||||
# или использовать существующий message_id
|
||||
content_post = TelegramPost(
|
||||
message_id=message_id,
|
||||
text="Сообщение с контентом",
|
||||
author_id=11111, # Используем существующего пользователя
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(content_post)
|
||||
|
||||
result = await post_repository.add_post_content(
|
||||
sample_post.message_id, message_id, content_name, content_type
|
||||
)
|
||||
|
||||
# Проверяем, что контент добавлен успешно
|
||||
assert result is True
|
||||
|
||||
# Проверяем, что контент действительно добавлен
|
||||
post_content = await post_repository.get_post_content_by_helper_id(sample_post.message_id)
|
||||
# Поскольку у нас нет helper_message_id, контент не будет найден
|
||||
# Это нормальное поведение для данного теста
|
||||
assert isinstance(post_content, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_post_content_with_helper_message_integration(self, post_repository, sample_post_with_helper):
|
||||
"""Интеграционный тест добавления контента поста с helper сообщением."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post_with_helper)
|
||||
|
||||
# Создаем helper сообщение
|
||||
helper_message_id = 99999
|
||||
helper_post = TelegramPost(
|
||||
message_id=helper_message_id,
|
||||
text="Helper сообщение",
|
||||
author_id=67890,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(helper_post)
|
||||
|
||||
# Обновляем пост, чтобы он ссылался на helper сообщение
|
||||
await post_repository.update_helper_message(sample_post_with_helper.message_id, helper_message_id)
|
||||
|
||||
# Добавляем контент
|
||||
message_id = 22222
|
||||
content_name = "/path/to/test/video.mp4"
|
||||
content_type = "video"
|
||||
|
||||
# Сначала нужно добавить сообщение с этим message_id в post_from_telegram_suggest
|
||||
content_post = TelegramPost(
|
||||
message_id=message_id,
|
||||
text="Сообщение с видео контентом",
|
||||
author_id=11111, # Используем существующего пользователя
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(content_post)
|
||||
|
||||
result = await post_repository.add_post_content(
|
||||
sample_post_with_helper.message_id, message_id, content_name, content_type
|
||||
)
|
||||
|
||||
# Проверяем, что контент добавлен успешно
|
||||
assert result is True
|
||||
|
||||
# Проверяем, что контент действительно добавлен
|
||||
post_content = await post_repository.get_post_content_by_helper_id(helper_message_id)
|
||||
assert len(post_content) == 1
|
||||
assert post_content[0][0] == content_name
|
||||
assert post_content[0][1] == content_type
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_post_text_by_helper_id_integration(self, post_repository, sample_post_with_helper):
|
||||
"""Интеграционный тест получения текста поста по helper ID."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post_with_helper)
|
||||
|
||||
# Создаем helper сообщение
|
||||
helper_message_id = 99999
|
||||
helper_post = TelegramPost(
|
||||
message_id=helper_message_id,
|
||||
text="Helper сообщение",
|
||||
author_id=67890,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(helper_post)
|
||||
|
||||
# Обновляем пост, чтобы он ссылался на helper сообщение
|
||||
await post_repository.update_helper_message(sample_post_with_helper.message_id, helper_message_id)
|
||||
|
||||
# Получаем текст поста
|
||||
post_text = await post_repository.get_post_text_by_helper_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert post_text == sample_post_with_helper.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_post_text_by_helper_id_not_found_integration(self, post_repository):
|
||||
"""Интеграционный тест получения текста поста по несуществующему helper ID."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Пытаемся получить текст поста по несуществующему helper ID
|
||||
post_text = await post_repository.get_post_text_by_helper_id(99999)
|
||||
|
||||
# Проверяем, что результат None
|
||||
assert post_text is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_post_ids_by_helper_id_integration(self, post_repository, sample_post_with_helper):
|
||||
"""Интеграционный тест получения ID сообщений по helper ID."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post_with_helper)
|
||||
|
||||
# Создаем helper сообщение
|
||||
helper_message_id = 99999
|
||||
helper_post = TelegramPost(
|
||||
message_id=helper_message_id,
|
||||
text="Helper сообщение",
|
||||
author_id=67890,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(helper_post)
|
||||
|
||||
# Обновляем пост, чтобы он ссылался на helper сообщение
|
||||
await post_repository.update_helper_message(sample_post_with_helper.message_id, helper_message_id)
|
||||
|
||||
# Добавляем несколько сообщений с контентом
|
||||
message_ids = [33333, 44444, 55555]
|
||||
content_names = ["/path/to/photo1.jpg", "/path/to/photo2.jpg", "/path/to/video.mp4"]
|
||||
content_types = ["photo", "photo", "video"]
|
||||
|
||||
for i, (msg_id, content_name, content_type) in enumerate(zip(message_ids, content_names, content_types)):
|
||||
# Сначала нужно добавить сообщение с этим message_id в post_from_telegram_suggest
|
||||
content_post = TelegramPost(
|
||||
message_id=msg_id,
|
||||
text=f"Сообщение с контентом {i+1}",
|
||||
author_id=11111, # Используем существующего пользователя
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(content_post)
|
||||
|
||||
result = await post_repository.add_post_content(
|
||||
sample_post_with_helper.message_id, msg_id, content_name, content_type
|
||||
)
|
||||
assert result is True
|
||||
|
||||
# Получаем ID сообщений
|
||||
post_ids = await post_repository.get_post_ids_by_helper_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert len(post_ids) == 3
|
||||
for msg_id in message_ids:
|
||||
assert msg_id in post_ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_message_id_integration(self, post_repository, sample_post):
|
||||
"""Интеграционный тест получения ID автора по message ID."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post)
|
||||
|
||||
# Получаем ID автора
|
||||
author_id = await post_repository.get_author_id_by_message_id(sample_post.message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert author_id == sample_post.author_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_message_id_not_found_integration(self, post_repository):
|
||||
"""Интеграционный тест получения ID автора по несуществующему message ID."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Пытаемся получить ID автора по несуществующему message ID
|
||||
author_id = await post_repository.get_author_id_by_message_id(99999)
|
||||
|
||||
# Проверяем, что результат None
|
||||
assert author_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_helper_message_id_integration(self, post_repository, sample_post_with_helper):
|
||||
"""Интеграционный тест получения ID автора по helper message ID."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post_with_helper)
|
||||
|
||||
# Создаем helper сообщение
|
||||
helper_message_id = 99999
|
||||
helper_post = TelegramPost(
|
||||
message_id=helper_message_id,
|
||||
text="Helper сообщение",
|
||||
author_id=67890,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(helper_post)
|
||||
|
||||
# Обновляем пост, чтобы он ссылался на helper сообщение
|
||||
await post_repository.update_helper_message(sample_post_with_helper.message_id, helper_message_id)
|
||||
|
||||
# Получаем ID автора
|
||||
author_id = await post_repository.get_author_id_by_helper_message_id(helper_message_id)
|
||||
|
||||
# Проверяем результат
|
||||
assert author_id == sample_post_with_helper.author_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_author_id_by_helper_message_id_not_found_integration(self, post_repository):
|
||||
"""Интеграционный тест получения ID автора по несуществующему helper message ID."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Пытаемся получить ID автора по несуществующему helper message ID
|
||||
author_id = await post_repository.get_author_id_by_helper_message_id(99999)
|
||||
|
||||
# Проверяем, что результат None
|
||||
assert author_id is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_posts_integration(self, post_repository, sample_post, sample_post_2):
|
||||
"""Интеграционный тест работы с несколькими постами."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем несколько постов
|
||||
await post_repository.add_post(sample_post)
|
||||
await post_repository.add_post(sample_post_2)
|
||||
|
||||
# Проверяем, что оба поста добавлены
|
||||
author_id_1 = await post_repository.get_author_id_by_message_id(sample_post.message_id)
|
||||
author_id_2 = await post_repository.get_author_id_by_message_id(sample_post_2.message_id)
|
||||
|
||||
assert author_id_1 == sample_post.author_id
|
||||
assert author_id_2 == sample_post_2.author_id
|
||||
|
||||
# Проверяем, что посты имеют разные ID
|
||||
assert sample_post.message_id != sample_post_2.message_id
|
||||
assert sample_post.text != sample_post_2.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_content_relationships_integration(self, post_repository, sample_post_with_helper):
|
||||
"""Интеграционный тест связей между постами и контентом."""
|
||||
# Настраиваем тестовую БД
|
||||
await self._setup_test_database(post_repository)
|
||||
|
||||
# Добавляем пост
|
||||
await post_repository.add_post(sample_post_with_helper)
|
||||
|
||||
# Создаем helper сообщение
|
||||
helper_message_id = 99999
|
||||
helper_post = TelegramPost(
|
||||
message_id=helper_message_id,
|
||||
text="Helper сообщение",
|
||||
author_id=67890,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(helper_post)
|
||||
|
||||
# Обновляем пост, чтобы он ссылался на helper сообщение
|
||||
await post_repository.update_helper_message(sample_post_with_helper.message_id, helper_message_id)
|
||||
|
||||
# Добавляем контент разных типов
|
||||
content_data = [
|
||||
(11111, "/path/to/photo1.jpg", "photo"),
|
||||
(22222, "/path/to/video1.mp4", "video"),
|
||||
(33333, "/path/to/audio1.mp3", "audio"),
|
||||
(44444, "/path/to/photo2.jpg", "photo")
|
||||
]
|
||||
|
||||
for message_id, content_name, content_type in content_data:
|
||||
# Сначала нужно добавить сообщение с этим message_id в post_from_telegram_suggest
|
||||
content_post = TelegramPost(
|
||||
message_id=message_id,
|
||||
text=f"Сообщение с контентом {content_type}",
|
||||
author_id=11111, # Используем существующего пользователя
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await post_repository.add_post(content_post)
|
||||
|
||||
result = await post_repository.add_post_content(
|
||||
sample_post_with_helper.message_id, message_id, content_name, content_type
|
||||
)
|
||||
assert result is True
|
||||
|
||||
# Проверяем, что весь контент добавлен
|
||||
post_content = await post_repository.get_post_content_by_helper_id(helper_message_id)
|
||||
assert len(post_content) == 4
|
||||
|
||||
# Проверяем, что ID сообщений получены правильно
|
||||
post_ids = await post_repository.get_post_ids_by_helper_id(helper_message_id)
|
||||
assert len(post_ids) == 4
|
||||
|
||||
# Проверяем, что все ожидаемые ID присутствуют
|
||||
expected_message_ids = [11111, 22222, 33333, 44444]
|
||||
for expected_id in expected_message_ids:
|
||||
assert expected_id in post_ids
|
||||
@@ -19,7 +19,8 @@ class TestAdminService:
|
||||
self.mock_db = Mock()
|
||||
self.admin_service = AdminService(self.mock_db)
|
||||
|
||||
def test_get_last_users_success(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_last_users_success(self):
|
||||
"""Тест успешного получения списка последних пользователей"""
|
||||
# Arrange
|
||||
# Формат данных: кортежи (full_name, user_id) как возвращает БД
|
||||
@@ -27,10 +28,10 @@ class TestAdminService:
|
||||
('User One', 1), # (full_name, user_id)
|
||||
('User Two', 2) # (full_name, user_id)
|
||||
]
|
||||
self.mock_db.get_last_users_from_db.return_value = mock_users_data
|
||||
self.mock_db.get_last_users = AsyncMock(return_value=mock_users_data)
|
||||
|
||||
# Act
|
||||
result = self.admin_service.get_last_users()
|
||||
result = await self.admin_service.get_last_users()
|
||||
|
||||
# Assert
|
||||
assert len(result) == 2
|
||||
@@ -41,17 +42,18 @@ class TestAdminService:
|
||||
assert result[1].username == 'Неизвестно' # username не возвращается из БД
|
||||
assert result[1].full_name == 'User Two'
|
||||
|
||||
def test_get_user_by_username_success(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_username_success(self):
|
||||
"""Тест успешного получения пользователя по username"""
|
||||
# Arrange
|
||||
user_id = 123
|
||||
username = "test_user"
|
||||
full_name = "Test User"
|
||||
self.mock_db.get_user_id_by_username.return_value = user_id
|
||||
self.mock_db.get_full_name_by_id.return_value = full_name
|
||||
self.mock_db.get_user_id_by_username = AsyncMock(return_value=user_id)
|
||||
self.mock_db.get_full_name_by_id = AsyncMock(return_value=full_name)
|
||||
|
||||
# Act
|
||||
result = self.admin_service.get_user_by_username(username)
|
||||
result = await self.admin_service.get_user_by_username(username)
|
||||
|
||||
# Assert
|
||||
assert result is not None
|
||||
@@ -59,27 +61,35 @@ class TestAdminService:
|
||||
assert result.username == username
|
||||
assert result.full_name == full_name
|
||||
|
||||
def test_get_user_by_username_not_found(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_username_not_found(self):
|
||||
"""Тест получения пользователя по несуществующему username"""
|
||||
# Arrange
|
||||
username = "nonexistent_user"
|
||||
self.mock_db.get_user_id_by_username.return_value = None
|
||||
self.mock_db.get_user_id_by_username = AsyncMock(return_value=None)
|
||||
|
||||
# Act
|
||||
result = self.admin_service.get_user_by_username(username)
|
||||
result = await self.admin_service.get_user_by_username(username)
|
||||
|
||||
# Assert
|
||||
assert result is None
|
||||
|
||||
def test_get_user_by_id_success(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_id_success(self):
|
||||
"""Тест успешного получения пользователя по ID"""
|
||||
# Arrange
|
||||
user_id = 123
|
||||
user_info = {'username': 'test_user', 'full_name': 'Test User'}
|
||||
self.mock_db.get_user_info_by_id.return_value = user_info
|
||||
from database.models import User as DBUser
|
||||
user_info = DBUser(
|
||||
user_id=user_id,
|
||||
first_name="Test",
|
||||
full_name="Test User",
|
||||
username="test_user"
|
||||
)
|
||||
self.mock_db.get_user_by_id = AsyncMock(return_value=user_info)
|
||||
|
||||
# Act
|
||||
result = self.admin_service.get_user_by_id(user_id)
|
||||
result = await self.admin_service.get_user_by_id(user_id)
|
||||
|
||||
# Assert
|
||||
assert result is not None
|
||||
@@ -87,45 +97,51 @@ class TestAdminService:
|
||||
assert result.username == 'test_user'
|
||||
assert result.full_name == 'Test User'
|
||||
|
||||
def test_get_user_by_id_not_found(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_id_not_found(self):
|
||||
"""Тест получения пользователя по несуществующему ID"""
|
||||
# Arrange
|
||||
user_id = 999
|
||||
self.mock_db.get_user_info_by_id.return_value = None
|
||||
self.mock_db.get_user_by_id = AsyncMock(return_value=None)
|
||||
|
||||
# Act
|
||||
result = self.admin_service.get_user_by_id(user_id)
|
||||
result = await self.admin_service.get_user_by_id(user_id)
|
||||
|
||||
# Assert
|
||||
assert result is None
|
||||
|
||||
def test_validate_user_input_success(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_user_input_success(self):
|
||||
"""Тест успешной валидации ID пользователя"""
|
||||
# Act
|
||||
result = self.admin_service.validate_user_input("123")
|
||||
result = await self.admin_service.validate_user_input("123")
|
||||
|
||||
# Assert
|
||||
assert result == 123
|
||||
|
||||
def test_validate_user_input_invalid_number(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_user_input_invalid_number(self):
|
||||
"""Тест валидации некорректного ID"""
|
||||
# Act & Assert
|
||||
with pytest.raises(InvalidInputError, match="ID пользователя должен быть числом"):
|
||||
self.admin_service.validate_user_input("abc")
|
||||
await self.admin_service.validate_user_input("abc")
|
||||
|
||||
def test_validate_user_input_negative_number(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_user_input_negative_number(self):
|
||||
"""Тест валидации отрицательного ID"""
|
||||
# Act & Assert
|
||||
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
|
||||
self.admin_service.validate_user_input("-1")
|
||||
await self.admin_service.validate_user_input("-1")
|
||||
|
||||
def test_validate_user_input_zero(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_user_input_zero(self):
|
||||
"""Тест валидации нулевого ID"""
|
||||
# Act & Assert
|
||||
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
|
||||
self.admin_service.validate_user_input("0")
|
||||
await self.admin_service.validate_user_input("0")
|
||||
|
||||
def test_ban_user_success(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_ban_user_success(self):
|
||||
"""Тест успешной блокировки пользователя"""
|
||||
# Arrange
|
||||
user_id = 123
|
||||
@@ -133,17 +149,18 @@ class TestAdminService:
|
||||
reason = "Test ban"
|
||||
ban_days = 7
|
||||
|
||||
self.mock_db.check_user_in_blacklist.return_value = False
|
||||
self.mock_db.set_user_blacklist.return_value = None
|
||||
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
|
||||
self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
|
||||
|
||||
# Act
|
||||
self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||
await self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||
|
||||
# Assert
|
||||
self.mock_db.check_user_in_blacklist.assert_called_once_with(user_id)
|
||||
self.mock_db.set_user_blacklist.assert_called_once()
|
||||
|
||||
def test_ban_user_already_banned(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_ban_user_already_banned(self):
|
||||
"""Тест попытки заблокировать уже заблокированного пользователя"""
|
||||
# Arrange
|
||||
user_id = 123
|
||||
@@ -151,13 +168,14 @@ class TestAdminService:
|
||||
reason = "Test ban"
|
||||
ban_days = 7
|
||||
|
||||
self.mock_db.check_user_in_blacklist.return_value = True
|
||||
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=True)
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(UserAlreadyBannedError, match=f"Пользователь {user_id} уже заблокирован"):
|
||||
self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||
await self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||
|
||||
def test_ban_user_permanent(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_ban_user_permanent(self):
|
||||
"""Тест постоянной блокировки пользователя"""
|
||||
# Arrange
|
||||
user_id = 123
|
||||
@@ -165,23 +183,24 @@ class TestAdminService:
|
||||
reason = "Permanent ban"
|
||||
ban_days = None
|
||||
|
||||
self.mock_db.check_user_in_blacklist.return_value = False
|
||||
self.mock_db.set_user_blacklist.return_value = None
|
||||
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
|
||||
self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
|
||||
|
||||
# Act
|
||||
self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||
await self.admin_service.ban_user(user_id, username, reason, ban_days)
|
||||
|
||||
# Assert
|
||||
self.mock_db.set_user_blacklist.assert_called_once_with(user_id, username, reason, None)
|
||||
self.mock_db.set_user_blacklist.assert_called_once_with(user_id, None, reason, None)
|
||||
|
||||
def test_unban_user_success(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_unban_user_success(self):
|
||||
"""Тест успешной разблокировки пользователя"""
|
||||
# Arrange
|
||||
user_id = 123
|
||||
self.mock_db.delete_user_blacklist.return_value = None
|
||||
self.mock_db.delete_user_blacklist = AsyncMock(return_value=None)
|
||||
|
||||
# Act
|
||||
self.admin_service.unban_user(user_id)
|
||||
await self.admin_service.unban_user(user_id)
|
||||
|
||||
# Assert
|
||||
self.mock_db.delete_user_blacklist.assert_called_once_with(user_id)
|
||||
|
||||
@@ -75,9 +75,10 @@ class TestGroupHandlers:
|
||||
assert handlers.admin_reply_service is not None
|
||||
assert handlers.router is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_message_success(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
|
||||
"""Test successful message handling"""
|
||||
mock_db.get_user_by_message_id.return_value = 99999
|
||||
mock_db.get_user_by_message_id = AsyncMock(return_value=99999)
|
||||
|
||||
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||
|
||||
@@ -97,6 +98,7 @@ class TestGroupHandlers:
|
||||
# Verify state was set
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["CHAT"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_message_no_reply(self, mock_db, mock_keyboard_markup, mock_message, mock_state):
|
||||
"""Test message handling without reply"""
|
||||
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||
@@ -121,9 +123,10 @@ class TestGroupHandlers:
|
||||
# Verify state was not set
|
||||
mock_state.set_state.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_message_user_not_found(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
|
||||
"""Test message handling when user is not found"""
|
||||
mock_db.get_user_by_message_id.return_value = None
|
||||
mock_db.get_user_by_message_id = AsyncMock(return_value=None)
|
||||
|
||||
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||
|
||||
@@ -154,24 +157,27 @@ class TestAdminReplyService:
|
||||
"""Create service instance"""
|
||||
return AdminReplyService(mock_db)
|
||||
|
||||
def test_get_user_id_for_reply_success(self, service, mock_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_id_for_reply_success(self, service, mock_db):
|
||||
"""Test successful user ID retrieval"""
|
||||
mock_db.get_user_by_message_id.return_value = 12345
|
||||
mock_db.get_user_by_message_id = AsyncMock(return_value=12345)
|
||||
|
||||
result = service.get_user_id_for_reply(111)
|
||||
result = await service.get_user_id_for_reply(111)
|
||||
|
||||
assert result == 12345
|
||||
mock_db.get_user_by_message_id.assert_called_once_with(111)
|
||||
|
||||
def test_get_user_id_for_reply_not_found(self, service, mock_db):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_id_for_reply_not_found(self, service, mock_db):
|
||||
"""Test user ID retrieval when user not found"""
|
||||
mock_db.get_user_by_message_id.return_value = None
|
||||
mock_db.get_user_by_message_id = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(UserNotFoundError, match="User not found for message_id: 111"):
|
||||
service.get_user_id_for_reply(111)
|
||||
await service.get_user_id_for_reply(111)
|
||||
|
||||
mock_db.get_user_by_message_id.assert_called_once_with(111)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_reply_to_user(self, service, mock_db):
|
||||
"""Test sending reply to user"""
|
||||
message = Mock()
|
||||
|
||||
@@ -19,13 +19,13 @@ class TestPrivateHandlers:
|
||||
def mock_db(self):
|
||||
"""Mock database"""
|
||||
db = Mock()
|
||||
db.user_exists.return_value = False
|
||||
db.add_new_user_in_db = Mock()
|
||||
db.update_date_for_user = Mock()
|
||||
db.update_info_about_stickers = Mock()
|
||||
db.add_post_in_db = Mock()
|
||||
db.add_new_message_in_db = Mock()
|
||||
db.update_helper_message_in_db = Mock()
|
||||
db.user_exists = AsyncMock(return_value=False)
|
||||
db.add_user = AsyncMock()
|
||||
db.update_user_date = AsyncMock()
|
||||
db.update_stickers_info = AsyncMock()
|
||||
db.add_post = AsyncMock()
|
||||
db.add_message = AsyncMock()
|
||||
db.update_helper_message = AsyncMock()
|
||||
return db
|
||||
|
||||
@pytest.fixture
|
||||
@@ -101,7 +101,8 @@ class TestPrivateHandlers:
|
||||
|
||||
# Mock the check_user_emoji function
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.check_user_emoji', lambda x: "😊")
|
||||
mock_check_emoji = AsyncMock(return_value="😊")
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.check_user_emoji', mock_check_emoji)
|
||||
|
||||
# Test the handler
|
||||
await handlers.handle_emoji_message(mock_message, mock_state)
|
||||
@@ -121,7 +122,8 @@ class TestPrivateHandlers:
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_first_name', lambda x: "Test")
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.messages.get_message', lambda x, y: "Hello Test!")
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', lambda x, y: Mock())
|
||||
mock_keyboard = AsyncMock(return_value=Mock())
|
||||
m.setattr('helper_bot.handlers.private.private_handlers.get_reply_keyboard', mock_keyboard)
|
||||
|
||||
# Test the handler
|
||||
await handlers.handle_start_message(mock_message, mock_state)
|
||||
@@ -130,8 +132,8 @@ class TestPrivateHandlers:
|
||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||
|
||||
# Verify user was ensured to exist
|
||||
mock_db.add_new_user_in_db.assert_called_once()
|
||||
mock_db.update_date_for_user.assert_called_once()
|
||||
mock_db.add_user.assert_called_once()
|
||||
mock_db.update_user_date.assert_called_once()
|
||||
|
||||
|
||||
class TestBotSettings:
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
[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
|
||||
@@ -31,7 +31,7 @@ from helper_bot.utils.helper_func import (
|
||||
)
|
||||
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
|
||||
from database.async_db import AsyncBotDB
|
||||
import helper_bot.utils.messages as messages # Import for patching constants
|
||||
|
||||
class TestHelperFunctions:
|
||||
@@ -83,25 +83,27 @@ class TestHelperFunctions:
|
||||
assert "testuser" in result
|
||||
assert "Обычный текст без специальных слов" in result
|
||||
|
||||
def test_check_username_and_full_name(self):
|
||||
@pytest.mark.asyncio
|
||||
async 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"))
|
||||
mock_db = Mock(spec=AsyncBotDB)
|
||||
mock_db.get_username = AsyncMock(return_value="olduser")
|
||||
mock_db.get_full_name_by_id = AsyncMock(return_value="Old User")
|
||||
|
||||
# Тест с измененными данными
|
||||
result = check_username_and_full_name(123456, "newuser", "New User", mock_db)
|
||||
result = await 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)
|
||||
result = await 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)
|
||||
result = await 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)
|
||||
result = await check_username_and_full_name(123456, "newuser", "Old User", mock_db)
|
||||
assert result is True
|
||||
|
||||
|
||||
@@ -286,10 +288,15 @@ class TestDownloadFile:
|
||||
# Мокаем download_file
|
||||
mock_message.bot.download_file = AsyncMock()
|
||||
|
||||
# Мокаем os.makedirs
|
||||
# Мокаем os.makedirs и другие зависимости
|
||||
with patch('os.makedirs') as mock_makedirs:
|
||||
with patch('os.path.join', return_value="files/photos/file_123.jpg"):
|
||||
result = await download_file(mock_message, "file_id_123")
|
||||
with patch('os.path.exists', return_value=True):
|
||||
with patch('os.path.getsize', return_value=1024):
|
||||
with patch('os.path.basename', return_value='file_123.jpg'):
|
||||
with patch('os.path.splitext', return_value=('file_123', '.jpg')):
|
||||
with patch('helper_bot.utils.helper_func.metrics') as mock_metrics:
|
||||
result = await download_file(mock_message, "file_id_123", "photo")
|
||||
|
||||
assert result == "files/photos/file_123.jpg"
|
||||
mock_makedirs.assert_called()
|
||||
@@ -330,7 +337,7 @@ class TestPrepareMediaGroup:
|
||||
assert result[0].media == "photo_0"
|
||||
assert result[1].media == "photo_1"
|
||||
assert result[2].media == "photo_2"
|
||||
assert result[2].caption == "Тестовая подпись"
|
||||
assert result[0].caption == "Тестовая подпись" # Первое фото должно иметь caption
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_media_group_mixed_types(self):
|
||||
@@ -364,7 +371,7 @@ class TestPrepareMediaGroup:
|
||||
assert result[0].media == "photo_1"
|
||||
assert result[1].media == "video_1"
|
||||
assert result[2].media == "audio_1"
|
||||
assert result[2].caption == "Смешанная группа"
|
||||
assert result[0].caption == "Смешанная группа" # Первое медиа должно иметь caption
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_media_group_empty_album(self):
|
||||
@@ -381,6 +388,7 @@ class TestPrepareMediaGroup:
|
||||
message.photo = None
|
||||
message.video = None
|
||||
message.audio = None
|
||||
message.document = None # Добавляем document = None
|
||||
album.append(message)
|
||||
|
||||
result = await prepare_media_group_from_middlewares(album, "Тест")
|
||||
@@ -401,12 +409,12 @@ class TestMediaDatabaseOperations:
|
||||
message.photo[-1].file_id = f"photo_{i}"
|
||||
sent_message.append(message)
|
||||
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value=f"files/photo_{i}.jpg"):
|
||||
await add_in_db_media_mediagroup(sent_message, mock_db)
|
||||
|
||||
assert mock_db.add_post_content_in_db.call_count == 2
|
||||
assert mock_db.add_post_content.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_in_db_media_photo(self):
|
||||
@@ -416,12 +424,12 @@ class TestMediaDatabaseOperations:
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = "photo_123"
|
||||
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value="files/photo_123.jpg"):
|
||||
await add_in_db_media(mock_message, mock_db)
|
||||
|
||||
mock_db.add_post_content_in_db.assert_called_once_with(
|
||||
mock_db.add_post_content.assert_called_once_with(
|
||||
123, 123, "files/photo_123.jpg", 'photo'
|
||||
)
|
||||
|
||||
@@ -434,12 +442,12 @@ class TestMediaDatabaseOperations:
|
||||
mock_message.video = Mock()
|
||||
mock_message.video.file_id = "video_123"
|
||||
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value="files/video_123.mp4"):
|
||||
await add_in_db_media(mock_message, mock_db)
|
||||
|
||||
mock_db.add_post_content_in_db.assert_called_once_with(
|
||||
mock_db.add_post_content.assert_called_once_with(
|
||||
123, 123, "files/video_123.mp4", 'video'
|
||||
)
|
||||
|
||||
@@ -453,12 +461,12 @@ class TestMediaDatabaseOperations:
|
||||
mock_message.voice = Mock()
|
||||
mock_message.voice.file_id = "voice_123"
|
||||
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value="files/voice_123.ogg"):
|
||||
await add_in_db_media(mock_message, mock_db)
|
||||
|
||||
mock_db.add_post_content_in_db.assert_called_once_with(
|
||||
mock_db.add_post_content.assert_called_once_with(
|
||||
123, 123, "files/voice_123.ogg", 'voice'
|
||||
)
|
||||
|
||||
@@ -548,16 +556,17 @@ class TestSendMessageFunctions:
|
||||
class TestUtilityFunctions:
|
||||
"""Тесты для утилитарных функций"""
|
||||
|
||||
def test_check_access(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_access(self):
|
||||
"""Тест проверки доступа"""
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
mock_db.is_admin.return_value = True
|
||||
|
||||
result = check_access(123, mock_db)
|
||||
result = await check_access(123, mock_db)
|
||||
assert result is True
|
||||
|
||||
mock_db.is_admin.return_value = False
|
||||
result = check_access(123, mock_db)
|
||||
result = await check_access(123, mock_db)
|
||||
assert result is False
|
||||
|
||||
def test_add_days_to_date(self):
|
||||
@@ -569,45 +578,51 @@ class TestUtilityFunctions:
|
||||
mock_datetime.timedelta = timedelta
|
||||
|
||||
result = add_days_to_date(5)
|
||||
expected_date = (mock_now + timedelta(days=5)).strftime("%d-%m-%Y")
|
||||
assert result == expected_date
|
||||
expected_timestamp = int((mock_now + timedelta(days=5)).timestamp())
|
||||
assert result == expected_timestamp
|
||||
|
||||
def test_get_banned_users_list(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_banned_users_list(self):
|
||||
"""Тест получения списка заблокированных пользователей"""
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
mock_db.get_banned_users_from_db_with_limits.return_value = [
|
||||
("User1", 123, "Spam", "01-01-2025"),
|
||||
("User2", 456, "Violation", "02-01-2025")
|
||||
(123, "Spam", 1704067200), # user_id, ban_reason, unban_date (timestamp)
|
||||
(456, "Violation", 1704153600)
|
||||
]
|
||||
mock_db.get_username.return_value = None
|
||||
mock_db.get_full_name_by_id.return_value = "Test User"
|
||||
|
||||
result = get_banned_users_list(0, mock_db)
|
||||
result = await get_banned_users_list(0, mock_db)
|
||||
|
||||
assert "Список заблокированных пользователей:" in result
|
||||
assert "User1" in result
|
||||
assert "User2" in result
|
||||
assert "Test User" in result
|
||||
assert "Spam" in result
|
||||
assert "Violation" in result
|
||||
|
||||
def test_get_banned_users_buttons(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_banned_users_buttons(self):
|
||||
"""Тест получения кнопок заблокированных пользователей"""
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
mock_db.get_banned_users_from_db.return_value = [
|
||||
("User1", 123),
|
||||
("User2", 456)
|
||||
(123, "Spam", 1704067200), # user_id, ban_reason, unban_date
|
||||
(456, "Violation", 1704153600)
|
||||
]
|
||||
mock_db.get_username.return_value = None
|
||||
mock_db.get_full_name_by_id.return_value = "Test User"
|
||||
|
||||
result = get_banned_users_buttons(mock_db)
|
||||
result = await get_banned_users_buttons(mock_db)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0] == ("User1", 123)
|
||||
assert result[1] == ("User2", 456)
|
||||
assert result[0] == ("Test User", 123)
|
||||
assert result[1] == ("Test User", 456)
|
||||
|
||||
def test_delete_user_blacklist(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_blacklist(self):
|
||||
"""Тест удаления пользователя из черного списка"""
|
||||
mock_db = Mock()
|
||||
mock_db = AsyncMock()
|
||||
mock_db.delete_user_blacklist.return_value = True
|
||||
|
||||
result = delete_user_blacklist(123, mock_db)
|
||||
result = await delete_user_blacklist(123, mock_db)
|
||||
assert result is True
|
||||
|
||||
mock_db.delete_user_blacklist.assert_called_once_with(user_id=123)
|
||||
@@ -631,57 +646,61 @@ class TestUserManagement:
|
||||
with patch('helper_bot.utils.helper_func.get_first_name', return_value="Test"):
|
||||
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
|
||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
||||
mock_bot_db.user_exists.return_value = False
|
||||
mock_bot_db.add_new_user_in_db = Mock()
|
||||
mock_bot_db.update_date_for_user = Mock()
|
||||
mock_bot_db.user_exists = AsyncMock(return_value=False)
|
||||
mock_bot_db.add_user = AsyncMock()
|
||||
mock_bot_db.update_user_date = AsyncMock()
|
||||
|
||||
await update_user_info("test", mock_message)
|
||||
|
||||
mock_bot_db.add_new_user_in_db.assert_called_once()
|
||||
mock_bot_db.update_date_for_user.assert_called_once()
|
||||
mock_bot_db.add_user.assert_called_once()
|
||||
mock_bot_db.update_user_date.assert_called_once()
|
||||
|
||||
def test_check_user_emoji_existing(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_user_emoji_existing(self):
|
||||
"""Тест проверки эмодзи пользователя (существующий)"""
|
||||
mock_message = Mock()
|
||||
mock_message.from_user.id = 123
|
||||
|
||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
||||
mock_bot_db.check_emoji_for_user.return_value = "😀"
|
||||
mock_bot_db.get_user_emoji = AsyncMock(return_value="😀")
|
||||
|
||||
result = check_user_emoji(mock_message)
|
||||
result = await check_user_emoji(mock_message)
|
||||
assert result == "😀"
|
||||
|
||||
def test_check_user_emoji_new(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_user_emoji_new(self):
|
||||
"""Тест проверки эмодзи пользователя (новый)"""
|
||||
mock_message = Mock()
|
||||
mock_message.from_user.id = 123
|
||||
|
||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
||||
mock_bot_db.check_emoji_for_user.return_value = None
|
||||
mock_bot_db.update_emoji_for_user = Mock()
|
||||
mock_bot_db.get_user_emoji = AsyncMock(return_value=None)
|
||||
mock_bot_db.update_user_emoji = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
|
||||
result = check_user_emoji(mock_message)
|
||||
result = await check_user_emoji(mock_message)
|
||||
assert result == "😀"
|
||||
mock_bot_db.update_emoji_for_user.assert_called_once_with(user_id=123, emoji="😀")
|
||||
mock_bot_db.update_user_emoji.assert_called_once_with(user_id=123, emoji="😀")
|
||||
|
||||
def test_get_random_emoji_success(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_random_emoji_success(self):
|
||||
"""Тест получения случайного эмодзи (успех)"""
|
||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
||||
mock_bot_db.check_emoji.return_value = False
|
||||
mock_bot_db.check_emoji_exists = AsyncMock(return_value=False)
|
||||
|
||||
with patch('helper_bot.utils.helper_func.random.choice', return_value="😀"):
|
||||
result = get_random_emoji()
|
||||
result = await get_random_emoji()
|
||||
assert result == "😀"
|
||||
|
||||
def test_get_random_emoji_fallback(self):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_random_emoji_fallback(self):
|
||||
"""Тест получения случайного эмодзи (fallback)"""
|
||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
||||
mock_bot_db.check_emoji.return_value = True # Все эмодзи заняты
|
||||
mock_bot_db.check_emoji_exists = AsyncMock(return_value=True) # Все эмодзи заняты
|
||||
|
||||
with patch('helper_bot.utils.helper_func.random.choice', return_value="😀"):
|
||||
with patch('helper_bot.utils.helper_func.logger') as mock_logger:
|
||||
result = get_random_emoji()
|
||||
result = await get_random_emoji()
|
||||
assert result == "Эмоджи не определен"
|
||||
mock_logger.error.assert_called_once()
|
||||
|
||||
|
||||
281
tests/test_voice_bot_architecture.py
Normal file
281
tests/test_voice_bot_architecture.py
Normal file
@@ -0,0 +1,281 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from helper_bot.handlers.voice.services import VoiceBotService
|
||||
from helper_bot.handlers.voice.exceptions import VoiceMessageError, AudioProcessingError
|
||||
from helper_bot.handlers.voice.utils import get_last_message_text, validate_voice_message, get_user_emoji_safe
|
||||
|
||||
|
||||
class TestVoiceBotService:
|
||||
"""Тесты для VoiceBotService"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bot_db(self):
|
||||
"""Мок для базы данных"""
|
||||
mock_db = Mock()
|
||||
mock_db.settings = {
|
||||
'Settings': {'logs': True},
|
||||
'Telegram': {'important_logs': 'test_chat_id'}
|
||||
}
|
||||
return mock_db
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings(self):
|
||||
"""Мок для настроек"""
|
||||
return {
|
||||
'Settings': {'logs': True},
|
||||
'Telegram': {'preview_link': True}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def voice_service(self, mock_bot_db, mock_settings):
|
||||
"""Экземпляр VoiceBotService для тестов"""
|
||||
return VoiceBotService(mock_bot_db, mock_settings)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_welcome_sticker_success(self, voice_service, mock_settings):
|
||||
"""Тест успешного получения стикера"""
|
||||
with patch('pathlib.Path.rglob') as mock_rglob:
|
||||
mock_rglob.return_value = ['/path/to/sticker1.tgs', '/path/to/sticker2.tgs']
|
||||
|
||||
sticker = await voice_service.get_welcome_sticker()
|
||||
|
||||
assert sticker is not None
|
||||
mock_rglob.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_welcome_sticker_no_stickers(self, voice_service, mock_settings):
|
||||
"""Тест получения стикера когда их нет"""
|
||||
with patch('pathlib.Path.rglob') as mock_rglob:
|
||||
mock_rglob.return_value = []
|
||||
|
||||
sticker = await voice_service.get_welcome_sticker()
|
||||
|
||||
assert sticker is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_random_audio_success(self, voice_service, mock_bot_db):
|
||||
"""Тест успешного получения случайного аудио"""
|
||||
mock_bot_db.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2'])
|
||||
mock_bot_db.get_user_id_by_file_name = AsyncMock(return_value=123)
|
||||
mock_bot_db.get_date_by_file_name = AsyncMock(return_value='2025-01-01 12:00:00')
|
||||
mock_bot_db.get_user_emoji = AsyncMock(return_value='😊')
|
||||
|
||||
result = await voice_service.get_random_audio(456)
|
||||
|
||||
assert result is not None
|
||||
assert len(result) == 3
|
||||
assert result[0] in ['audio1', 'audio2']
|
||||
assert result[1] == '2025-01-01 12:00:00'
|
||||
assert result[2] == '😊'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_random_audio_no_audio(self, voice_service, mock_bot_db):
|
||||
"""Тест получения аудио когда их нет"""
|
||||
mock_bot_db.check_listen_audio = AsyncMock(return_value=[])
|
||||
|
||||
result = await voice_service.get_random_audio(456)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_audio_as_listened_success(self, voice_service, mock_bot_db):
|
||||
"""Тест успешной пометки аудио как прослушанного"""
|
||||
mock_bot_db.mark_listened_audio = AsyncMock()
|
||||
|
||||
await voice_service.mark_audio_as_listened('test_audio', 123)
|
||||
|
||||
mock_bot_db.mark_listened_audio.assert_called_once_with('test_audio', user_id=123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_user_listenings_success(self, voice_service, mock_bot_db):
|
||||
"""Тест успешной очистки прослушиваний"""
|
||||
mock_bot_db.delete_listen_count_for_user = AsyncMock()
|
||||
|
||||
await voice_service.clear_user_listenings(123)
|
||||
|
||||
mock_bot_db.delete_listen_count_for_user.assert_called_once_with(123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_remaining_audio_count_success(self, voice_service, mock_bot_db):
|
||||
"""Тест получения количества оставшихся аудио"""
|
||||
mock_bot_db.check_listen_audio = AsyncMock(return_value=['audio1', 'audio2', 'audio3'])
|
||||
|
||||
result = await voice_service.get_remaining_audio_count(123)
|
||||
|
||||
assert result == 3
|
||||
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_remaining_audio_count_zero(self, voice_service, mock_bot_db):
|
||||
"""Тест получения количества оставшихся аудио когда их нет"""
|
||||
mock_bot_db.check_listen_audio = AsyncMock(return_value=[])
|
||||
|
||||
result = await voice_service.get_remaining_audio_count(123)
|
||||
|
||||
assert result == 0
|
||||
mock_bot_db.check_listen_audio.assert_called_once_with(user_id=123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_welcome_messages_success(self, voice_service, mock_bot_db, mock_settings):
|
||||
"""Тест успешной отправки приветственных сообщений"""
|
||||
mock_message = Mock()
|
||||
mock_message.from_user.id = 123
|
||||
mock_message.answer = AsyncMock()
|
||||
mock_message.answer.return_value = Mock()
|
||||
mock_message.answer_sticker = AsyncMock()
|
||||
|
||||
with patch.object(voice_service, 'get_welcome_sticker') as mock_sticker:
|
||||
mock_sticker.return_value = 'test_sticker.tgs'
|
||||
|
||||
await voice_service.send_welcome_messages(mock_message, '😊')
|
||||
|
||||
# Проверяем, что сообщения отправлены
|
||||
assert mock_message.answer.call_count >= 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_welcome_messages_no_sticker(self, voice_service, mock_bot_db, mock_settings):
|
||||
"""Тест отправки приветственных сообщений без стикера"""
|
||||
mock_message = Mock()
|
||||
mock_message.from_user.id = 123
|
||||
mock_message.answer = AsyncMock()
|
||||
mock_message.answer.return_value = Mock()
|
||||
|
||||
with patch.object(voice_service, 'get_welcome_sticker') as mock_sticker:
|
||||
mock_sticker.return_value = None
|
||||
|
||||
await voice_service.send_welcome_messages(mock_message, '😊')
|
||||
|
||||
# Проверяем, что сообщения отправлены
|
||||
assert mock_message.answer.call_count >= 1
|
||||
|
||||
|
||||
class TestVoiceHandlers:
|
||||
"""Тесты для VoiceHandlers"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db(self):
|
||||
"""Мок для базы данных"""
|
||||
return Mock()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings(self):
|
||||
"""Мок для настроек"""
|
||||
return {
|
||||
'Telegram': {
|
||||
'group_for_logs': 'test_logs_chat',
|
||||
'group_for_posts': 'test_posts_chat',
|
||||
'preview_link': True
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def voice_handlers(self, mock_db, mock_settings):
|
||||
"""Экземпляр VoiceHandlers для тестов"""
|
||||
from helper_bot.handlers.voice.voice_handler import VoiceHandlers
|
||||
return VoiceHandlers(mock_db, mock_settings)
|
||||
|
||||
def test_voice_handlers_initialization(self, voice_handlers):
|
||||
"""Тест инициализации VoiceHandlers"""
|
||||
assert voice_handlers.db is not None
|
||||
assert voice_handlers.settings is not None
|
||||
assert voice_handlers.router is not None
|
||||
|
||||
def test_setup_handlers(self, voice_handlers):
|
||||
"""Тест настройки обработчиков"""
|
||||
# Проверяем, что роутер содержит обработчики
|
||||
assert len(voice_handlers.router.message.handlers) > 0
|
||||
|
||||
|
||||
class TestUtils:
|
||||
"""Тесты для утилит"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bot_db(self):
|
||||
"""Мок для базы данных"""
|
||||
return Mock()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_last_message_text(self, mock_bot_db):
|
||||
"""Тест получения последнего сообщения"""
|
||||
# Возвращаем UNIX timestamp
|
||||
mock_bot_db.last_date_audio = AsyncMock(return_value=1641034800) # 2022-01-01 12:00:00
|
||||
|
||||
result = await get_last_message_text(mock_bot_db)
|
||||
|
||||
assert result is not None
|
||||
assert "минут" in result or "часа" in result or "дня" in result or "день" in result or "дней" in result
|
||||
mock_bot_db.last_date_audio.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_voice_message_valid(self):
|
||||
"""Тест валидации голосового сообщения"""
|
||||
mock_message = Mock()
|
||||
mock_message.content_type = 'voice'
|
||||
mock_message.voice = Mock()
|
||||
|
||||
result = await validate_voice_message(mock_message)
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_voice_message_invalid(self):
|
||||
"""Тест валидации невалидного сообщения"""
|
||||
mock_message = Mock()
|
||||
mock_message.voice = None
|
||||
|
||||
result = await validate_voice_message(mock_message)
|
||||
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_emoji_safe(self, mock_bot_db):
|
||||
"""Тест безопасного получения эмодзи пользователя"""
|
||||
mock_bot_db.get_user_emoji = AsyncMock(return_value="😊")
|
||||
|
||||
result = await get_user_emoji_safe(mock_bot_db, 123)
|
||||
|
||||
assert result == "😊"
|
||||
mock_bot_db.get_user_emoji.assert_called_once_with(123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_emoji_safe_none(self, mock_bot_db):
|
||||
"""Тест безопасного получения эмодзи когда его нет"""
|
||||
mock_bot_db.get_user_emoji = AsyncMock(return_value=None)
|
||||
|
||||
result = await get_user_emoji_safe(mock_bot_db, 123)
|
||||
|
||||
assert result == "😊"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_emoji_safe_error(self, mock_bot_db):
|
||||
"""Тест безопасного получения эмодзи при ошибке"""
|
||||
mock_bot_db.get_user_emoji = AsyncMock(return_value="Ошибка")
|
||||
|
||||
result = await get_user_emoji_safe(mock_bot_db, 123)
|
||||
|
||||
assert result == "Ошибка"
|
||||
|
||||
|
||||
class TestExceptions:
|
||||
"""Тесты для исключений"""
|
||||
|
||||
def test_voice_message_error(self):
|
||||
"""Тест VoiceMessageError"""
|
||||
try:
|
||||
raise VoiceMessageError("Тестовая ошибка")
|
||||
except VoiceMessageError as e:
|
||||
assert str(e) == "Тестовая ошибка"
|
||||
|
||||
def test_audio_processing_error(self):
|
||||
"""Тест AudioProcessingError"""
|
||||
try:
|
||||
raise AudioProcessingError("Ошибка обработки")
|
||||
except AudioProcessingError as e:
|
||||
assert str(e) == "Ошибка обработки"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user