Dev 8 #10
@@ -72,8 +72,6 @@ docker-compose*.yml
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
||||||
# Development files
|
# Development files
|
||||||
Makefile
|
|
||||||
start_docker.sh
|
|
||||||
*.sh
|
*.sh
|
||||||
|
|
||||||
# Stickers and media
|
# Stickers and media
|
||||||
@@ -94,4 +92,4 @@ Stick/
|
|||||||
|
|
||||||
# Monitoring configs (will be mounted)
|
# Monitoring configs (will be mounted)
|
||||||
prometheus.yml
|
prometheus.yml
|
||||||
grafana/
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -89,3 +89,7 @@ env/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Other files
|
||||||
|
voice_users/
|
||||||
|
files/
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -28,6 +28,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
# Install runtime dependencies only
|
# Install runtime dependencies only
|
||||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
|
||||||
curl \
|
curl \
|
||||||
|
sqlite3 \
|
||||||
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& apt-get clean
|
&& apt-get clean
|
||||||
|
|
||||||
@@ -37,28 +39,38 @@ RUN groupadd -g 1001 deploy && useradd -u 1001 -g deploy deploy
|
|||||||
# Copy virtual environment from builder
|
# Copy virtual environment from builder
|
||||||
COPY --from=builder /opt/venv /opt/venv
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
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
|
# Create app directory and set permissions
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN mkdir -p /app/database /app/logs && \
|
RUN mkdir -p /app/database /app/logs /app/voice_users && \
|
||||||
chown -R deploy:deploy /app
|
chown -R 1001:1001 /app
|
||||||
|
|
||||||
# Copy application code
|
# 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
|
# Switch to non-root user
|
||||||
USER deploy
|
USER deploy
|
||||||
|
|
||||||
# Health check
|
# Health check with better timeout handling
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=15s --start-period=10s --retries=5 \
|
||||||
CMD curl -f http://localhost:8000/health || exit 1
|
CMD curl -f --connect-timeout 5 --max-time 10 http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
# Expose metrics port
|
# Expose metrics port
|
||||||
EXPOSE 8000
|
EXPOSE 8080
|
||||||
|
|
||||||
# Graceful shutdown
|
# Graceful shutdown with longer timeout
|
||||||
STOPSIGNAL SIGTERM
|
STOPSIGNAL SIGTERM
|
||||||
|
|
||||||
# Run application
|
# Set environment variables for better network stability
|
||||||
CMD ["python", "run_helper.py"]
|
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'
|
||||||
|
]
|
||||||
|
|
||||||
1185
database/async_db.py
1185
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
|
||||||
DATABASE_PATH=database/tg-bot-database.db
|
DATABASE_PATH=database/tg-bot-database.db
|
||||||
|
|
||||||
# Monitoring
|
# Monitoring (Centralized Prometheus)
|
||||||
METRICS_HOST=0.0.0.0
|
METRICS_HOST=0.0.0.0
|
||||||
METRICS_PORT=8000
|
METRICS_PORT=8080
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=INFO
|
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
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
# Local imports - metrics
|
||||||
|
from helper_bot.utils.metrics import (
|
||||||
|
metrics,
|
||||||
|
track_time,
|
||||||
|
track_errors
|
||||||
|
)
|
||||||
|
|
||||||
# Создаем роутер с middleware для проверки доступа
|
# Создаем роутер с middleware для проверки доступа
|
||||||
admin_router = Router()
|
admin_router = Router()
|
||||||
admin_router.message.middleware(AdminAccessMiddleware())
|
admin_router.message.middleware(AdminAccessMiddleware())
|
||||||
@@ -38,9 +45,12 @@ admin_router.message.middleware(AdminAccessMiddleware())
|
|||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
Command('admin')
|
Command('admin')
|
||||||
)
|
)
|
||||||
|
@track_time("admin_panel", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "admin_panel")
|
||||||
async def admin_panel(
|
async def admin_panel(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext
|
state: FSMContext,
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
"""Главное меню администратора"""
|
"""Главное меню администратора"""
|
||||||
try:
|
try:
|
||||||
@@ -52,11 +62,38 @@ async def admin_panel(
|
|||||||
await handle_admin_error(message, e, state, "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(
|
@admin_router.message(
|
||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
StateFilter("ADMIN"),
|
StateFilter("ADMIN"),
|
||||||
F.text == 'Бан (Список)'
|
F.text == 'Бан (Список)'
|
||||||
)
|
)
|
||||||
|
@track_time("get_last_users", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "get_last_users")
|
||||||
async def get_last_users(
|
async def get_last_users(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
@@ -66,11 +103,11 @@ async def get_last_users(
|
|||||||
try:
|
try:
|
||||||
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
|
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
|
||||||
admin_service = AdminService(bot_db)
|
admin_service = AdminService(bot_db)
|
||||||
users = admin_service.get_last_users()
|
users = await admin_service.get_last_users()
|
||||||
|
|
||||||
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
|
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
|
||||||
users_data = [
|
users_data = [
|
||||||
(user.full_name, user.username) # (full_name, username) - формат кортежей
|
(user.full_name, user.user_id)
|
||||||
for user in users
|
for user in users
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -88,6 +125,8 @@ async def get_last_users(
|
|||||||
StateFilter("ADMIN"),
|
StateFilter("ADMIN"),
|
||||||
F.text == 'Разбан (список)'
|
F.text == 'Разбан (список)'
|
||||||
)
|
)
|
||||||
|
@track_time("get_banned_users", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "get_banned_users")
|
||||||
async def get_banned_users(
|
async def get_banned_users(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
@@ -97,7 +136,7 @@ async def get_banned_users(
|
|||||||
try:
|
try:
|
||||||
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
|
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
|
||||||
admin_service = AdminService(bot_db)
|
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:
|
if buttons_list:
|
||||||
keyboard = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
keyboard = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
||||||
@@ -117,9 +156,12 @@ async def get_banned_users(
|
|||||||
StateFilter("ADMIN"),
|
StateFilter("ADMIN"),
|
||||||
F.text.in_(['Бан по нику', 'Бан по ID'])
|
F.text.in_(['Бан по нику', 'Бан по ID'])
|
||||||
)
|
)
|
||||||
|
@track_time("start_ban_process", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "start_ban_process")
|
||||||
async def start_ban_process(
|
async def start_ban_process(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
"""Начало процесса блокировки пользователя"""
|
"""Начало процесса блокировки пользователя"""
|
||||||
try:
|
try:
|
||||||
@@ -137,38 +179,50 @@ async def start_ban_process(
|
|||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
StateFilter("AWAIT_BAN_TARGET")
|
StateFilter("AWAIT_BAN_TARGET")
|
||||||
)
|
)
|
||||||
|
@track_time("process_ban_target", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "process_ban_target")
|
||||||
async def process_ban_target(
|
async def process_ban_target(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
bot_db: MagicData("bot_db")
|
bot_db: MagicData("bot_db")
|
||||||
):
|
):
|
||||||
"""Обработка введенного username/ID для блокировки"""
|
"""Обработка введенного username/ID для блокировки"""
|
||||||
|
logger.info(f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_data = await state.get_data()
|
user_data = await state.get_data()
|
||||||
ban_type = user_data.get('ban_type')
|
ban_type = user_data.get('ban_type')
|
||||||
admin_service = AdminService(bot_db)
|
admin_service = AdminService(bot_db)
|
||||||
|
|
||||||
|
logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}")
|
||||||
|
|
||||||
# Определяем пользователя
|
# Определяем пользователя
|
||||||
if ban_type == "username":
|
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:
|
if not user:
|
||||||
|
logger.warning(f"process_ban_target: Пользователь с username '{message.text}' не найден")
|
||||||
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||||||
await return_to_admin_menu(message, state)
|
await return_to_admin_menu(message, state)
|
||||||
return
|
return
|
||||||
else: # ban_type == "id"
|
else: # ban_type == "id"
|
||||||
try:
|
try:
|
||||||
user_id = admin_service.validate_user_input(message.text)
|
logger.info(f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}")
|
||||||
user = admin_service.get_user_by_id(user_id)
|
user_id = await admin_service.validate_user_input(message.text)
|
||||||
|
user = await admin_service.get_user_by_id(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
logger.warning(f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных")
|
||||||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||||||
await return_to_admin_menu(message, state)
|
await return_to_admin_menu(message, state)
|
||||||
return
|
return
|
||||||
except InvalidInputError as e:
|
except InvalidInputError as e:
|
||||||
|
logger.error(f"process_ban_target: Ошибка валидации ID: {e}")
|
||||||
await message.answer(str(e))
|
await message.answer(str(e))
|
||||||
await return_to_admin_menu(message, state)
|
await return_to_admin_menu(message, state)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.info(f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}")
|
||||||
|
|
||||||
# Сохраняем данные пользователя
|
# Сохраняем данные пользователя
|
||||||
await state.update_data(
|
await state.update_data(
|
||||||
target_user_id=user.user_id,
|
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)
|
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||||||
markup = create_keyboard_for_ban_reason()
|
markup = create_keyboard_for_ban_reason()
|
||||||
|
logger.info(f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}")
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||||
reply_markup=markup
|
reply_markup=markup
|
||||||
)
|
)
|
||||||
await state.set_state('AWAIT_BAN_DETAILS')
|
await state.set_state('AWAIT_BAN_DETAILS')
|
||||||
|
logger.info("process_ban_target: Состояние изменено на AWAIT_BAN_DETAILS")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"process_ban_target: Неожиданная ошибка: {e}", exc_info=True)
|
||||||
await handle_admin_error(message, e, state, "process_ban_target")
|
await handle_admin_error(message, e, state, "process_ban_target")
|
||||||
|
|
||||||
|
|
||||||
@@ -193,21 +251,41 @@ async def process_ban_target(
|
|||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
StateFilter("AWAIT_BAN_DETAILS")
|
StateFilter("AWAIT_BAN_DETAILS")
|
||||||
)
|
)
|
||||||
|
@track_time("process_ban_reason", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "process_ban_reason")
|
||||||
async def process_ban_reason(
|
async def process_ban_reason(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext
|
state: FSMContext,
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
"""Обработка причины блокировки"""
|
"""Обработка причины блокировки"""
|
||||||
|
logger.info(f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||||
|
|
||||||
try:
|
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)
|
await state.update_data(ban_reason=message.text)
|
||||||
|
|
||||||
markup = create_keyboard_for_ban_days()
|
markup = create_keyboard_for_ban_days()
|
||||||
safe_reason = escape_html(message.text)
|
safe_reason = escape_html(message.text)
|
||||||
|
logger.info(f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}")
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
||||||
reply_markup=markup
|
reply_markup=markup
|
||||||
)
|
)
|
||||||
await state.set_state('AWAIT_BAN_DURATION')
|
await state.set_state('AWAIT_BAN_DURATION')
|
||||||
|
logger.info("process_ban_reason: Состояние изменено на AWAIT_BAN_DURATION")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"process_ban_reason: Неожиданная ошибка: {e}", exc_info=True)
|
||||||
await handle_admin_error(message, e, state, "process_ban_reason")
|
await handle_admin_error(message, e, state, "process_ban_reason")
|
||||||
|
|
||||||
|
|
||||||
@@ -215,9 +293,12 @@ async def process_ban_reason(
|
|||||||
ChatTypeFilter(chat_type=["private"]),
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
StateFilter("AWAIT_BAN_DURATION")
|
StateFilter("AWAIT_BAN_DURATION")
|
||||||
)
|
)
|
||||||
|
@track_time("process_ban_duration", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "process_ban_duration")
|
||||||
async def process_ban_duration(
|
async def process_ban_duration(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
"""Обработка срока блокировки"""
|
"""Обработка срока блокировки"""
|
||||||
try:
|
try:
|
||||||
@@ -257,10 +338,13 @@ async def process_ban_duration(
|
|||||||
StateFilter("BAN_CONFIRMATION"),
|
StateFilter("BAN_CONFIRMATION"),
|
||||||
F.text == 'Подтвердить'
|
F.text == 'Подтвердить'
|
||||||
)
|
)
|
||||||
|
@track_time("confirm_ban", "admin_handlers")
|
||||||
|
@track_errors("admin_handlers", "confirm_ban")
|
||||||
async def confirm_ban(
|
async def confirm_ban(
|
||||||
message: types.Message,
|
message: types.Message,
|
||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
bot_db: MagicData("bot_db")
|
bot_db: MagicData("bot_db"),
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
"""Подтверждение блокировки пользователя"""
|
"""Подтверждение блокировки пользователя"""
|
||||||
try:
|
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'],
|
user_id=user_data['target_user_id'],
|
||||||
username=user_data['target_username'],
|
username=user_data['target_username'],
|
||||||
reason=user_data['ban_reason'],
|
reason=user_data['ban_reason'],
|
||||||
@@ -285,66 +369,3 @@ async def confirm_ban(
|
|||||||
await return_to_admin_menu(message, state)
|
await return_to_admin_menu(message, state)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await handle_admin_error(message, e, state, "confirm_ban")
|
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:
|
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||||
if hasattr(event, 'from_user'):
|
if hasattr(event, 'from_user'):
|
||||||
user_id = event.from_user.id
|
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 (внедренного DependenciesMiddleware)
|
||||||
bot_db = data.get('bot_db')
|
bot_db = data.get('bot_db')
|
||||||
@@ -25,7 +28,11 @@ class AdminAccessMiddleware(BaseMiddleware):
|
|||||||
bdf = get_global_instance()
|
bdf = get_global_instance()
|
||||||
bot_db = bdf.get_db()
|
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'):
|
if hasattr(event, 'answer'):
|
||||||
await event.answer('Доступ запрещен!')
|
await event.answer('Доступ запрещен!')
|
||||||
return
|
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 helper_bot.handlers.admin.exceptions import UserAlreadyBannedError, InvalidInputError
|
||||||
from logs.custom_logger import logger
|
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:
|
class User:
|
||||||
"""Модель пользователя"""
|
"""Модель пользователя"""
|
||||||
@@ -29,10 +37,12 @@ class AdminService:
|
|||||||
def __init__(self, bot_db):
|
def __init__(self, bot_db):
|
||||||
self.bot_db = 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:
|
try:
|
||||||
users_data = self.bot_db.get_last_users_from_db()
|
users_data = await self.bot_db.get_last_users(30)
|
||||||
return [
|
return [
|
||||||
User(
|
User(
|
||||||
user_id=user[1],
|
user_id=user[1],
|
||||||
@@ -45,31 +55,41 @@ class AdminService:
|
|||||||
logger.error(f"Ошибка при получении списка последних пользователей: {e}")
|
logger.error(f"Ошибка при получении списка последних пользователей: {e}")
|
||||||
raise
|
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:
|
try:
|
||||||
banned_users_data = self.bot_db.get_banned_users_from_db()
|
banned_users_data = await self.bot_db.get_banned_users_from_db()
|
||||||
return [
|
banned_users = []
|
||||||
BannedUser(
|
for user_data in banned_users_data:
|
||||||
user_id=user[1], # user_id
|
user_id, reason, unban_date = user_data
|
||||||
username=user[0], # user_name
|
# Получаем username и full_name из таблицы users
|
||||||
reason=user[2], # message_for_user
|
username = await self.bot_db.get_username(user_id)
|
||||||
unban_date=user[3] # date_to_unban
|
full_name = await self.bot_db.get_full_name_by_id(user_id)
|
||||||
)
|
user_name = username or full_name or f"User_{user_id}"
|
||||||
for user in banned_users_data
|
|
||||||
]
|
banned_users.append(BannedUser(
|
||||||
|
user_id=user_id,
|
||||||
|
username=user_name,
|
||||||
|
reason=reason,
|
||||||
|
unban_date=unban_date
|
||||||
|
))
|
||||||
|
return banned_users
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
|
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
|
||||||
raise
|
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"""
|
"""Получить пользователя по username"""
|
||||||
try:
|
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:
|
if not user_id:
|
||||||
return None
|
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(
|
return User(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
username=username,
|
username=username,
|
||||||
@@ -79,27 +99,31 @@ class AdminService:
|
|||||||
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
|
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
|
||||||
raise
|
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"""
|
"""Получить пользователя по ID"""
|
||||||
try:
|
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:
|
if not user_info:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
username=user_info.get('username', 'Неизвестно'),
|
username=user_info.username or 'Неизвестно',
|
||||||
full_name=user_info.get('full_name', 'Неизвестно')
|
full_name=user_info.full_name or 'Неизвестно'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
|
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
|
||||||
raise
|
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:
|
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} уже заблокирован")
|
raise UserAlreadyBannedError(f"Пользователь {user_id} уже заблокирован")
|
||||||
|
|
||||||
# Рассчитываем дату разблокировки
|
# Рассчитываем дату разблокировки
|
||||||
@@ -107,8 +131,8 @@ class AdminService:
|
|||||||
if ban_days is not None:
|
if ban_days is not None:
|
||||||
date_to_unban = add_days_to_date(ban_days)
|
date_to_unban = add_days_to_date(ban_days)
|
||||||
|
|
||||||
# Сохраняем в БД
|
# Сохраняем в БД (username больше не передается, так как не используется в новой схеме)
|
||||||
self.bot_db.set_user_blacklist(user_id, username, reason, date_to_unban)
|
await self.bot_db.set_user_blacklist(user_id, None, reason, date_to_unban)
|
||||||
|
|
||||||
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
|
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
|
||||||
|
|
||||||
@@ -116,16 +140,20 @@ class AdminService:
|
|||||||
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
|
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
|
||||||
raise
|
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:
|
try:
|
||||||
self.bot_db.delete_user_blacklist(user_id)
|
await self.bot_db.delete_user_blacklist(user_id)
|
||||||
logger.info(f"Пользователь {user_id} разблокирован")
|
logger.info(f"Пользователь {user_id} разблокирован")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}")
|
logger.error(f"Ошибка при разблокировке пользователя {user_id}: {e}")
|
||||||
raise
|
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 пользователя"""
|
"""Валидация введенного ID пользователя"""
|
||||||
try:
|
try:
|
||||||
user_id = int(input_text.strip())
|
user_id = int(input_text.strip())
|
||||||
@@ -135,11 +163,14 @@ class AdminService:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise InvalidInputError("ID пользователя должен быть числом")
|
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:
|
try:
|
||||||
message_text = get_banned_users_list(page, self.bot_db)
|
message_text = await get_banned_users_list(page, self.bot_db)
|
||||||
buttons_list = get_banned_users_buttons(self.bot_db)
|
|
||||||
|
buttons_list = await get_banned_users_buttons(self.bot_db)
|
||||||
return message_text, buttons_list
|
return message_text, buttons_list
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при получении данных заблокированных пользователей: {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,
|
async def return_to_admin_menu(message: types.Message, state: FSMContext,
|
||||||
additional_message: Optional[str] = None) -> None:
|
additional_message: Optional[str] = None) -> None:
|
||||||
"""Универсальная функция для возврата в админ-меню"""
|
"""Универсальная функция для возврата в админ-меню"""
|
||||||
|
logger.info(f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}")
|
||||||
|
|
||||||
await state.set_data({})
|
await state.set_data({})
|
||||||
await state.set_state("ADMIN")
|
await state.set_state("ADMIN")
|
||||||
markup = get_reply_keyboard_admin()
|
markup = get_reply_keyboard_admin()
|
||||||
|
|
||||||
if additional_message:
|
if additional_message:
|
||||||
|
logger.info(f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}")
|
||||||
await message.answer(additional_message)
|
await message.answer(additional_message)
|
||||||
|
|
||||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
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,
|
async def handle_admin_error(message: types.Message, error: Exception,
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import html
|
import html
|
||||||
import traceback
|
import traceback
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aiogram import Router
|
from aiogram import Router, F
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
from aiogram import F
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.filters import MagicData
|
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, \
|
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
|
||||||
create_keyboard_for_ban_reason
|
create_keyboard_for_ban_reason
|
||||||
from helper_bot.utils.helper_func import get_banned_users_list, get_banned_users_buttons
|
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
|
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 = Router()
|
||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(F.data == CALLBACK_PUBLISH)
|
@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(
|
async def post_for_group(
|
||||||
call: CallbackQuery,
|
call: CallbackQuery,
|
||||||
settings: MagicData("settings")
|
settings: MagicData("settings")
|
||||||
@@ -56,6 +69,8 @@ async def post_for_group(
|
|||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(F.data == CALLBACK_DECLINE)
|
@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(
|
async def decline_post_for_group(
|
||||||
call: CallbackQuery,
|
call: CallbackQuery,
|
||||||
settings: MagicData("settings")
|
settings: MagicData("settings")
|
||||||
@@ -86,7 +101,9 @@ async def decline_post_for_group(
|
|||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(F.data == CALLBACK_BAN)
|
@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()
|
ban_service = get_ban_service()
|
||||||
# TODO: переделать на MagicData
|
# TODO: переделать на MagicData
|
||||||
try:
|
try:
|
||||||
@@ -106,21 +123,31 @@ async def ban_user_from_post(call: CallbackQuery):
|
|||||||
|
|
||||||
|
|
||||||
@callback_router.callback_query(F.data.contains(CALLBACK_BAN))
|
@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()
|
ban_service = get_ban_service()
|
||||||
# TODO: переделать на MagicData
|
# TODO: переделать на MagicData
|
||||||
user_id = call.data[4:]
|
user_id = call.data[4:]
|
||||||
logger.info(f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
|
logger.info(f"Вызов функции process_ban_user. Данные callback: {call.data} пользователь: {user_id}")
|
||||||
|
|
||||||
|
# Проверяем, что user_id является валидным числом
|
||||||
try:
|
try:
|
||||||
user_name = await ban_service.ban_user(user_id, "")
|
user_id_int = int(user_id)
|
||||||
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None, date_to_unban=None)
|
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()
|
markup = create_keyboard_for_ban_reason()
|
||||||
|
|
||||||
user_name_escaped = html.escape(str(user_name))
|
user_name_escaped = html.escape(str(user_name))
|
||||||
full_name_escaped = html.escape(str(call.message.from_user.full_name))
|
full_name_escaped = html.escape(str(call.message.from_user.full_name))
|
||||||
await call.message.answer(
|
await call.message.answer(
|
||||||
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name_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
|
reply_markup=markup
|
||||||
)
|
)
|
||||||
await state.set_state('BAN_2')
|
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))
|
@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()
|
ban_service = get_ban_service()
|
||||||
# TODO: переделать на MagicData
|
# TODO: переделать на MagicData
|
||||||
user_id = call.data[7:]
|
user_id = call.data[7:]
|
||||||
|
|
||||||
|
# Проверяем, что user_id является валидным числом
|
||||||
try:
|
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)
|
await call.answer(f'{MESSAGE_USER_UNLOCKED} {username}', show_alert=True)
|
||||||
except UserNotFoundError:
|
except UserNotFoundError:
|
||||||
await call.answer(text='Пользователь не найден в базе', show_alert=True, cache_time=3)
|
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)
|
@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()
|
await call.message.delete()
|
||||||
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
|
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
|
||||||
markup = get_reply_keyboard_admin()
|
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))
|
@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(
|
async def change_page(
|
||||||
call: CallbackQuery,
|
call: CallbackQuery,
|
||||||
bot_db: MagicData("bot_db")
|
bot_db: MagicData("bot_db"),
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
page_number = int(call.data[5:])
|
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}")
|
logger.info(f"Переход на страницу {page_number}")
|
||||||
|
|
||||||
if call.message.text == 'Список пользователей которые последними обращались к боту':
|
if call.message.text == 'Список пользователей которые последними обращались к боту':
|
||||||
list_users = bot_db.get_last_users_from_db()
|
list_users = await bot_db.get_last_users(30)
|
||||||
keyboard = create_keyboard_with_pagination(int(page_number), len(list_users), list_users, 'ban')
|
keyboard = create_keyboard_with_pagination(page_number, len(list_users), list_users, 'ban')
|
||||||
await call.bot.edit_message_reply_markup(
|
await call.bot.edit_message_reply_markup(
|
||||||
chat_id=call.message.chat.id,
|
chat_id=call.message.chat.id,
|
||||||
message_id=call.message.message_id,
|
message_id=call.message.message_id,
|
||||||
@@ -179,9 +227,83 @@ async def change_page(
|
|||||||
)
|
)
|
||||||
|
|
||||||
buttons = get_banned_users_buttons(bot_db)
|
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(
|
await call.bot.edit_message_reply_markup(
|
||||||
chat_id=call.message.chat.id,
|
chat_id=call.message.chat.id,
|
||||||
message_id=call.message.message_id,
|
message_id=call.message.message_id,
|
||||||
reply_markup=keyboard
|
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 data constants
|
||||||
CALLBACK_PUBLISH = "publish"
|
CALLBACK_PUBLISH = "publish"
|
||||||
CALLBACK_DECLINE = "decline"
|
CALLBACK_DECLINE = "decline"
|
||||||
@@ -27,3 +29,13 @@ MESSAGE_USER_BANNED_SPAM = "Ты заблокирован за спам. Дат
|
|||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
ERROR_BOT_BLOCKED = "Forbidden: bot was blocked by the user"
|
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:
|
def get_post_publish_service() -> PostPublishService:
|
||||||
"""Фабрика для PostPublishService"""
|
"""Фабрика для PostPublishService"""
|
||||||
bdf = get_global_instance()
|
bdf = get_global_instance()
|
||||||
bot = Bot(
|
|
||||||
token=bdf.settings['Telegram']['bot_token'],
|
|
||||||
default=DefaultBotProperties(parse_mode='HTML'),
|
|
||||||
timeout=30.0
|
|
||||||
)
|
|
||||||
db = bdf.get_db()
|
db = bdf.get_db()
|
||||||
settings = bdf.settings
|
settings = bdf.settings
|
||||||
return PostPublishService(bot, db, settings)
|
return PostPublishService(None, db, settings)
|
||||||
|
|
||||||
|
|
||||||
def get_ban_service() -> BanService:
|
def get_ban_service() -> BanService:
|
||||||
"""Фабрика для BanService"""
|
"""Фабрика для BanService"""
|
||||||
bdf = get_global_instance()
|
bdf = get_global_instance()
|
||||||
bot = Bot(
|
|
||||||
token=bdf.settings['Telegram']['bot_token'],
|
|
||||||
default=DefaultBotProperties(parse_mode='HTML'),
|
|
||||||
timeout=30.0
|
|
||||||
)
|
|
||||||
db = bdf.get_db()
|
db = bdf.get_db()
|
||||||
settings = bdf.settings
|
settings = bdf.settings
|
||||||
return BanService(bot, db, settings)
|
return BanService(None, db, settings)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import html
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import html
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
@@ -23,21 +23,43 @@ from .constants import (
|
|||||||
)
|
)
|
||||||
from logs.custom_logger import logger
|
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:
|
class PostPublishService:
|
||||||
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
|
def __init__(self, bot: Bot, db, settings: Dict[str, Any]):
|
||||||
|
# bot может быть None - в этом случае используем бота из контекста сообщения
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.db = db
|
self.db = db
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.group_for_posts = settings['Telegram']['group_for_posts']
|
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||||
self.main_public = settings['Telegram']['main_public']
|
self.main_public = settings['Telegram']['main_public']
|
||||||
self.important_logs = settings['Telegram']['important_logs']
|
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:
|
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
|
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)
|
await self._publish_text_post(call)
|
||||||
elif content_type == CONTENT_TYPE_PHOTO:
|
elif content_type == CONTENT_TYPE_PHOTO:
|
||||||
await self._publish_photo_post(call)
|
await self._publish_photo_post(call)
|
||||||
@@ -49,129 +71,234 @@ class PostPublishService:
|
|||||||
await self._publish_audio_post(call)
|
await self._publish_audio_post(call)
|
||||||
elif content_type == CONTENT_TYPE_VOICE:
|
elif content_type == CONTENT_TYPE_VOICE:
|
||||||
await self._publish_voice_post(call)
|
await self._publish_voice_post(call)
|
||||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
|
||||||
await self._publish_media_group(call)
|
|
||||||
else:
|
else:
|
||||||
raise PublishError(f"Неподдерживаемый тип контента: {content_type}")
|
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:
|
async def _publish_text_post(self, call: CallbackQuery) -> None:
|
||||||
"""Публикация текстового поста"""
|
"""Публикация текстового поста"""
|
||||||
text_post = html.escape(str(call.message.text))
|
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 send_text_message(self.main_public, call.message, text_post)
|
||||||
await self._delete_post_and_notify_author(call, author_id)
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
logger.info(f'Текст сообщения опубликован в канале {self.main_public}.')
|
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:
|
async def _publish_photo_post(self, call: CallbackQuery) -> None:
|
||||||
"""Публикация поста с фото"""
|
"""Публикация поста с фото"""
|
||||||
text_post_with_photo = html.escape(str(call.message.caption))
|
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 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)
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
logger.info(f'Пост с фото опубликован в канале {self.main_public}.')
|
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:
|
async def _publish_video_post(self, call: CallbackQuery) -> None:
|
||||||
"""Публикация поста с видео"""
|
"""Публикация поста с видео"""
|
||||||
text_post_with_photo = html.escape(str(call.message.caption))
|
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 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)
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
logger.info(f'Пост с видео опубликован в канале {self.main_public}.')
|
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:
|
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 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)
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
logger.info(f'Пост с кружком опубликован в канале {self.main_public}.')
|
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:
|
async def _publish_audio_post(self, call: CallbackQuery) -> None:
|
||||||
"""Публикация поста с аудио"""
|
"""Публикация поста с аудио"""
|
||||||
text_post_with_photo = html.escape(str(call.message.caption))
|
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 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)
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
logger.info(f'Пост с аудио опубликован в канале {self.main_public}.')
|
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:
|
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 send_voice_message(self.main_public, call.message, call.message.voice.file_id)
|
||||||
await self._delete_post_and_notify_author(call, author_id)
|
await self._delete_post_and_notify_author(call, author_id)
|
||||||
logger.info(f'Пост с войсом опубликован в канале {self.main_public}.')
|
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:
|
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)
|
logger.info(f"Начинаю публикацию медиагруппы. Helper message ID: {call.message.message_id}")
|
||||||
pre_text = self.db.get_post_text_from_telegram_by_last_id(call.message.message_id)
|
try:
|
||||||
post_text = html.escape(str(pre_text))
|
# call.message.message_id - это ID helper сообщения
|
||||||
author_id = self._get_author_id_for_media_group(call.message.message_id)
|
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
|
||||||
await self._delete_media_group_and_notify_author(call, author_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:
|
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
|
content_type = call.message.content_type
|
||||||
|
|
||||||
if (content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP) or \
|
if content_type in [CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO,
|
||||||
content_type in [CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||||
|
logger.debug(f"Отклоняю одиночный пост типа: {content_type}")
|
||||||
await self._decline_single_post(call)
|
await self._decline_single_post(call)
|
||||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
|
||||||
await self._decline_media_group(call)
|
|
||||||
else:
|
else:
|
||||||
|
logger.error(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||||
raise PublishError(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:
|
async def _decline_single_post(self, call: CallbackQuery) -> None:
|
||||||
"""Отклонение одиночного поста"""
|
"""Отклонение одиночного поста"""
|
||||||
author_id = self._get_author_id(call.message.message_id)
|
logger.debug(f"Отклоняю одиночный пост. Message ID: {call.message.message_id}")
|
||||||
await self.bot.delete_message(chat_id=self.group_for_posts, 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:
|
try:
|
||||||
|
logger.debug(f"Отправляю уведомление об отклонении автору {author_id}")
|
||||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == ERROR_BOT_BLOCKED:
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
logger.error(f"Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||||
raise
|
raise
|
||||||
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
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:
|
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)
|
logger.debug(f"Отклоняю медиагруппу. Helper message ID: {call.message.message_id}")
|
||||||
message_ids = [row[0] for row in post_ids]
|
|
||||||
message_ids.append(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:
|
try:
|
||||||
|
logger.debug(f"Отправляю уведомление об отклонении автору медиагруппы {author_id}")
|
||||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == ERROR_BOT_BLOCKED:
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
logger.error(f"Ошибка при отправке уведомления автору медиагруппы {author_id}: {e}")
|
||||||
raise
|
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 сообщения"""
|
"""Получение 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:
|
if not author_id:
|
||||||
raise PostNotFoundError(f"Автор не найден для сообщения {message_id}")
|
raise PostNotFoundError(f"Автор не найден для сообщения {message_id}")
|
||||||
return author_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 автора для медиагруппы"""
|
"""Получение 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:
|
if not author_id:
|
||||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
|
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
|
||||||
return author_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:
|
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:
|
try:
|
||||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -179,12 +306,15 @@ class PostPublishService:
|
|||||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
raise
|
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:
|
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)
|
post_ids = await 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)
|
#message_ids = post_ids.copy()
|
||||||
await self.bot.delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
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:
|
try:
|
||||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -201,26 +331,33 @@ class BanService:
|
|||||||
self.group_for_posts = settings['Telegram']['group_for_posts']
|
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||||
self.important_logs = settings['Telegram']['important_logs']
|
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:
|
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:
|
if not author_id:
|
||||||
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")
|
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")
|
||||||
|
|
||||||
user_name = self.db.get_username(user_id=author_id)
|
|
||||||
current_date = datetime.now()
|
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_id=author_id,
|
||||||
user_name=user_name,
|
user_name=None,
|
||||||
message_for_user="Спам",
|
message_for_user="Спам",
|
||||||
date_to_unban=date_to_unban
|
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:
|
try:
|
||||||
await send_text_message(author_id, call.message, MESSAGE_USER_BANNED_SPAM.format(date=date_str))
|
await send_text_message(author_id, call.message, MESSAGE_USER_BANNED_SPAM.format(date=date_str))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -230,20 +367,24 @@ class BanService:
|
|||||||
|
|
||||||
logger.info(f"Пользователь {author_id} заблокирован за спам до {date_str}")
|
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:
|
async def ban_user(self, user_id: str, user_name: str) -> str:
|
||||||
"""Бан пользователя по ID"""
|
"""Бан пользователя по 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:
|
if not user_name:
|
||||||
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
||||||
|
|
||||||
return user_name
|
return user_name
|
||||||
|
|
||||||
|
@track_time("unlock_user", "ban_service")
|
||||||
|
@track_errors("ban_service", "unlock_user")
|
||||||
async def unlock_user(self, user_id: str) -> str:
|
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:
|
if not user_name:
|
||||||
raise UserNotFoundError(f"Пользователь с ID {user_id} не найден в базе")
|
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}")
|
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
|
||||||
return user_name
|
return user_name
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from aiogram import Router, types
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
# Local imports - filters
|
# Local imports - filters
|
||||||
|
from database.async_db import AsyncBotDB
|
||||||
from helper_bot.filters.main import ChatTypeFilter
|
from helper_bot.filters.main import ChatTypeFilter
|
||||||
|
|
||||||
# Local imports - modular components
|
# Local imports - modular components
|
||||||
@@ -26,7 +27,7 @@ from helper_bot.utils.metrics import (
|
|||||||
class GroupHandlers:
|
class GroupHandlers:
|
||||||
"""Main handler class for group messages"""
|
"""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.db = db
|
||||||
self.keyboard_markup = keyboard_markup
|
self.keyboard_markup = keyboard_markup
|
||||||
self.admin_reply_service = AdminReplyService(db)
|
self.admin_reply_service = AdminReplyService(db)
|
||||||
@@ -45,7 +46,9 @@ class GroupHandlers:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@error_handler
|
@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"""
|
"""Handle admin reply to user through group chat"""
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -67,7 +70,7 @@ class GroupHandlers:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Get user ID for reply
|
# 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
|
# Send reply to user
|
||||||
await self.admin_reply_service.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
|
# 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"""
|
"""Create group handlers instance with dependencies"""
|
||||||
return GroupHandlers(db, keyboard_markup)
|
return GroupHandlers(db, keyboard_markup)
|
||||||
|
|
||||||
@@ -103,6 +106,7 @@ def init_legacy_router():
|
|||||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
||||||
|
|
||||||
bdf = get_global_instance()
|
bdf = get_global_instance()
|
||||||
|
#TODO: поменять архитектуру и подключить правильный BotDB
|
||||||
db = bdf.get_db()
|
db = bdf.get_db()
|
||||||
keyboard_markup = get_reply_keyboard_leave_chat()
|
keyboard_markup = get_reply_keyboard_leave_chat()
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ from helper_bot.utils.metrics import (
|
|||||||
|
|
||||||
class DatabaseProtocol(Protocol):
|
class DatabaseProtocol(Protocol):
|
||||||
"""Protocol for database operations"""
|
"""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:
|
class AdminReplyService:
|
||||||
@@ -31,7 +32,9 @@ class AdminReplyService:
|
|||||||
def __init__(self, db: DatabaseProtocol) -> None:
|
def __init__(self, db: DatabaseProtocol) -> None:
|
||||||
self.db = db
|
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.
|
Get user ID for reply by message ID.
|
||||||
|
|
||||||
@@ -44,11 +47,13 @@ class AdminReplyService:
|
|||||||
Raises:
|
Raises:
|
||||||
UserNotFoundError: If user is not found in database
|
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:
|
if user_id is None:
|
||||||
raise UserNotFoundError(f"User not found for message_id: {message_id}")
|
raise UserNotFoundError(f"User not found for message_id: {message_id}")
|
||||||
return user_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(
|
async def send_reply_to_user(
|
||||||
self,
|
self,
|
||||||
chat_id: int,
|
chat_id: int,
|
||||||
|
|||||||
@@ -17,7 +17,19 @@ BUTTON_TEXTS: Final[Dict[str, str]] = {
|
|||||||
"LEAVE_CHAT": "Выйти из чата",
|
"LEAVE_CHAT": "Выйти из чата",
|
||||||
"RETURN_TO_BOT": "Вернуться в бота",
|
"RETURN_TO_BOT": "Вернуться в бота",
|
||||||
"WANT_STICKERS": "🤪Хочу стикеры",
|
"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
|
# Error messages
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from aiogram.filters import Command, StateFilter
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
# Local imports - filters and middlewares
|
# Local imports - filters and middlewares
|
||||||
|
from database.async_db import AsyncBotDB
|
||||||
from helper_bot.filters.main import ChatTypeFilter
|
from helper_bot.filters.main import ChatTypeFilter
|
||||||
from helper_bot.middlewares.album_middleware import AlbumMiddleware
|
from helper_bot.middlewares.album_middleware import AlbumMiddleware
|
||||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||||
@@ -28,7 +29,8 @@ from helper_bot.utils.helper_func import (
|
|||||||
from helper_bot.utils.metrics import (
|
from helper_bot.utils.metrics import (
|
||||||
metrics,
|
metrics,
|
||||||
track_time,
|
track_time,
|
||||||
track_errors
|
track_errors,
|
||||||
|
db_query_time
|
||||||
)
|
)
|
||||||
|
|
||||||
# Local imports - modular components
|
# Local imports - modular components
|
||||||
@@ -43,7 +45,7 @@ sleep = asyncio.sleep
|
|||||||
class PrivateHandlers:
|
class PrivateHandlers:
|
||||||
"""Main handler class for private messages"""
|
"""Main handler class for private messages"""
|
||||||
|
|
||||||
def __init__(self, db, settings: BotSettings):
|
def __init__(self, db: AsyncBotDB, settings: BotSettings):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.user_service = UserService(db, settings)
|
self.user_service = UserService(db, settings)
|
||||||
@@ -72,32 +74,39 @@ class PrivateHandlers:
|
|||||||
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["LEAVE_CHAT"])
|
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["LEAVE_CHAT"])
|
||||||
self.router.message.register(self.stickers, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["WANT_STICKERS"])
|
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"])
|
self.router.message.register(self.connect_with_admin, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["CONNECT_ADMIN"])
|
||||||
|
|
||||||
|
|
||||||
# State handlers
|
# State handlers
|
||||||
self.router.message.register(self.suggest_router, StateFilter(FSM_STATES["SUGGEST"]), ChatTypeFilter(chat_type=["private"]))
|
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["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"]))
|
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["CHAT"]), ChatTypeFilter(chat_type=["private"]))
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def handle_emoji_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle emoji command"""
|
"""Handle emoji command"""
|
||||||
await self.user_service.log_user_message(message)
|
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"])
|
await state.set_state(FSM_STATES["START"])
|
||||||
if user_emoji is not None:
|
if user_emoji is not None:
|
||||||
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
|
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def handle_restart_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle restart command"""
|
"""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 self.user_service.log_user_message(message)
|
||||||
await state.set_state(FSM_STATES["START"])
|
await state.set_state(FSM_STATES["START"])
|
||||||
await update_user_info('love', message)
|
await update_user_info('love', message)
|
||||||
check_user_emoji(message)
|
await check_user_emoji(message)
|
||||||
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
|
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle start command and return to bot button with metrics tracking"""
|
"""Handle start command and return to bot button with metrics tracking"""
|
||||||
# User service operations with metrics
|
# User service operations with metrics
|
||||||
@@ -109,11 +118,13 @@ class PrivateHandlers:
|
|||||||
await self.sticker_service.send_random_hello_sticker(message)
|
await self.sticker_service.send_random_hello_sticker(message)
|
||||||
|
|
||||||
# Send welcome message with metrics
|
# 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')
|
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
|
||||||
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def suggest_post(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle suggest post button"""
|
"""Handle suggest post button"""
|
||||||
# User service operations with metrics
|
# User service operations with metrics
|
||||||
@@ -126,6 +137,8 @@ class PrivateHandlers:
|
|||||||
await message.answer(suggest_news, reply_markup=markup)
|
await message.answer(suggest_news, reply_markup=markup)
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def end_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle goodbye button"""
|
"""Handle goodbye button"""
|
||||||
# User service operations with metrics
|
# User service operations with metrics
|
||||||
@@ -142,6 +155,8 @@ class PrivateHandlers:
|
|||||||
await state.set_state(FSM_STATES["START"])
|
await state.set_state(FSM_STATES["START"])
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs):
|
||||||
"""Handle post submission in suggest state"""
|
"""Handle post submission in suggest state"""
|
||||||
# Post service operations with metrics
|
# Post service operations with metrics
|
||||||
@@ -150,17 +165,19 @@ class PrivateHandlers:
|
|||||||
await self.post_service.process_post(message, album)
|
await self.post_service.process_post(message, album)
|
||||||
|
|
||||||
# Send success message and return to start state
|
# 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')
|
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 message.answer(success_send_message, reply_markup=markup_for_user)
|
||||||
await state.set_state(FSM_STATES["START"])
|
await state.set_state(FSM_STATES["START"])
|
||||||
|
|
||||||
@error_handler
|
@error_handler
|
||||||
|
@track_errors("private_handlers", "stickers")
|
||||||
|
@track_time("stickers", "private_handlers")
|
||||||
async def stickers(self, message: types.Message, state: FSMContext, **kwargs):
|
async def stickers(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle stickers request"""
|
"""Handle stickers request"""
|
||||||
# User service operations with metrics
|
# User service operations with metrics
|
||||||
markup = get_reply_keyboard(self.db, message.from_user.id)
|
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||||
self.db.update_info_about_stickers(user_id=message.from_user.id)
|
await self.db.update_stickers_info(message.from_user.id)
|
||||||
await self.user_service.log_user_message(message)
|
await self.user_service.log_user_message(message)
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=ERROR_MESSAGES["STICKERS_LINK"],
|
text=ERROR_MESSAGES["STICKERS_LINK"],
|
||||||
@@ -169,6 +186,8 @@ class PrivateHandlers:
|
|||||||
await state.set_state(FSM_STATES["START"])
|
await state.set_state(FSM_STATES["START"])
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def connect_with_admin(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle connect with admin button"""
|
"""Handle connect with admin button"""
|
||||||
# User service operations with metrics
|
# User service operations with metrics
|
||||||
@@ -179,6 +198,8 @@ class PrivateHandlers:
|
|||||||
await state.set_state(FSM_STATES["PRE_CHAT"])
|
await state.set_state(FSM_STATES["PRE_CHAT"])
|
||||||
|
|
||||||
@error_handler
|
@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):
|
async def resend_message_in_group_for_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||||
"""Handle messages in admin chat states"""
|
"""Handle messages in admin chat states"""
|
||||||
# User service operations with metrics
|
# User service operations with metrics
|
||||||
@@ -186,14 +207,14 @@ class PrivateHandlers:
|
|||||||
await message.forward(chat_id=self.settings.group_for_message)
|
await message.forward(chat_id=self.settings.group_for_message)
|
||||||
|
|
||||||
current_date = datetime.now()
|
current_date = datetime.now()
|
||||||
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
|
date = int(current_date.timestamp())
|
||||||
self.db.add_new_message_in_db(message.text, message.from_user.id, message.message_id + 1, date)
|
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')
|
question = messages.get_message(get_first_name(message), 'QUESTION')
|
||||||
user_state = await state.get_state()
|
user_state = await state.get_state()
|
||||||
|
|
||||||
if user_state == FSM_STATES["PRE_CHAT"]:
|
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 message.answer(question, reply_markup=markup)
|
||||||
await state.set_state(FSM_STATES["START"])
|
await state.set_state(FSM_STATES["START"])
|
||||||
elif user_state == FSM_STATES["CHAT"]:
|
elif user_state == FSM_STATES["CHAT"]:
|
||||||
@@ -202,7 +223,7 @@ class PrivateHandlers:
|
|||||||
|
|
||||||
|
|
||||||
# Factory function to create handlers with dependencies
|
# 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"""
|
"""Create private handlers instance with dependencies"""
|
||||||
return PrivateHandlers(db, settings)
|
return PrivateHandlers(db, settings)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from dataclasses import dataclass
|
|||||||
# Third-party imports
|
# Third-party imports
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
from aiogram.types import FSInputFile
|
from aiogram.types import FSInputFile
|
||||||
|
from database.models import TelegramPost, User
|
||||||
|
|
||||||
# Local imports - utilities
|
# Local imports - utilities
|
||||||
from helper_bot.utils.helper_func import (
|
from helper_bot.utils.helper_func import (
|
||||||
@@ -41,16 +42,14 @@ from helper_bot.utils.metrics import (
|
|||||||
|
|
||||||
class DatabaseProtocol(Protocol):
|
class DatabaseProtocol(Protocol):
|
||||||
"""Protocol for database operations"""
|
"""Protocol for database operations"""
|
||||||
def user_exists(self, user_id: int) -> bool: ...
|
async def user_exists(self, user_id: int) -> bool: ...
|
||||||
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str,
|
async def add_user(self, user: User) -> None: ...
|
||||||
username: str, is_bot: bool, language_code: str,
|
async def update_user_info(self, user_id: int, username: str = None, full_name: str = None) -> None: ...
|
||||||
emoji: str, created_date: str, updated_date: str) -> None: ...
|
async def update_user_date(self, user_id: int) -> None: ...
|
||||||
def update_username_and_full_name(self, user_id: int, username: str, full_name: str) -> None: ...
|
async def add_post(self, post: TelegramPost) -> None: ...
|
||||||
def update_date_for_user(self, date: str, user_id: int) -> None: ...
|
async def update_stickers_info(self, user_id: int) -> None: ...
|
||||||
def add_post_in_db(self, message_id: int, text: str, user_id: int) -> None: ...
|
async def add_message(self, message_text: str, user_id: int, message_id: int, date: int = None) -> None: ...
|
||||||
def update_info_about_stickers(self, user_id: int) -> None: ...
|
async def update_helper_message(self, message_id: int, helper_message_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: ...
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -75,11 +74,9 @@ class UserService:
|
|||||||
|
|
||||||
@track_time("update_user_activity", "user_service")
|
@track_time("update_user_activity", "user_service")
|
||||||
@track_errors("user_service", "update_user_activity")
|
@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:
|
async def update_user_activity(self, user_id: int) -> None:
|
||||||
"""Update user's last activity timestamp with metrics tracking"""
|
"""Update user's last activity timestamp with metrics tracking"""
|
||||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
await self.db.update_user_date(user_id)
|
||||||
self.db.update_date_for_user(current_date, user_id)
|
|
||||||
|
|
||||||
@track_time("ensure_user_exists", "user_service")
|
@track_time("ensure_user_exists", "user_service")
|
||||||
@track_errors("user_service", "ensure_user_exists")
|
@track_errors("user_service", "ensure_user_exists")
|
||||||
@@ -92,19 +89,28 @@ class UserService:
|
|||||||
is_bot = message.from_user.is_bot
|
is_bot = message.from_user.is_bot
|
||||||
language_code = message.from_user.language_code
|
language_code = message.from_user.language_code
|
||||||
|
|
||||||
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
if not await self.db.user_exists(user_id):
|
||||||
|
# Create User object with current timestamp
|
||||||
if not self.db.user_exists(user_id):
|
current_timestamp = int(datetime.now().timestamp())
|
||||||
# Record database operation
|
user = User(
|
||||||
self.db.add_new_user_in_db(
|
user_id=user_id,
|
||||||
user_id, first_name, full_name, username, is_bot, language_code,
|
first_name=first_name,
|
||||||
"", current_date, current_date
|
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:
|
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:
|
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")
|
metrics.record_db_query("update_username_fullname", 0.0, "users", "update")
|
||||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||||
safe_username = html.escape(username) if username else "Без никнейма"
|
safe_username = html.escape(username) if username else "Без никнейма"
|
||||||
@@ -115,8 +121,8 @@ class UserService:
|
|||||||
chat_id=self.settings.group_for_logs,
|
chat_id=self.settings.group_for_logs,
|
||||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}')
|
||||||
|
|
||||||
self.db.update_date_for_user(current_date, user_id)
|
await self.db.update_user_date(user_id)
|
||||||
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
metrics.record_db_query("update_user_date", 0.0, "users", "update")
|
||||||
|
|
||||||
|
|
||||||
@track_errors("user_service", "log_user_message")
|
@track_errors("user_service", "log_user_message")
|
||||||
@@ -146,7 +152,13 @@ class PostService:
|
|||||||
markup = get_reply_keyboard_for_post()
|
markup = get_reply_keyboard_for_post()
|
||||||
|
|
||||||
sent_message_id = await send_text_message(self.settings.group_for_posts, message, post_text, markup)
|
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_time("handle_photo_post", "post_service")
|
||||||
@track_errors("post_service", "handle_photo_post")
|
@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.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)
|
post = TelegramPost(
|
||||||
await add_in_db_media(sent_message, self.db)
|
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_time("handle_video_post", "post_service")
|
||||||
@track_errors("post_service", "handle_video_post")
|
@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.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)
|
post = TelegramPost(
|
||||||
await add_in_db_media(sent_message, self.db)
|
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_time("handle_video_note_post", "post_service")
|
||||||
@track_errors("post_service", "handle_video_note_post")
|
@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.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)
|
post = TelegramPost(
|
||||||
await add_in_db_media(sent_message, self.db)
|
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_time("handle_audio_post", "post_service")
|
||||||
@track_errors("post_service", "handle_audio_post")
|
@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.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)
|
post = TelegramPost(
|
||||||
await add_in_db_media(sent_message, self.db)
|
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_time("handle_voice_post", "post_service")
|
||||||
@track_errors("post_service", "handle_voice_post")
|
@track_errors("post_service", "handle_voice_post")
|
||||||
@@ -217,11 +261,19 @@ class PostService:
|
|||||||
self.settings.group_for_posts, message, message.voice.file_id, markup
|
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)
|
post = TelegramPost(
|
||||||
await add_in_db_media(sent_message, self.db)
|
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_time("handle_media_group_post", "post_service")
|
||||||
@track_errors("post_service", "handle_media_group_post")
|
@track_errors("post_service", "handle_media_group_post")
|
||||||
async def handle_media_group_post(self, message: types.Message, album: list, first_name: str) -> None:
|
async def handle_media_group_post(self, message: types.Message, album: list, first_name: str) -> None:
|
||||||
"""Handle media group post submission"""
|
"""Handle media group post submission"""
|
||||||
post_caption = " "
|
post_caption = " "
|
||||||
@@ -229,18 +281,41 @@ class PostService:
|
|||||||
if album and album[0].caption:
|
if album and album[0].caption:
|
||||||
post_caption = get_text_message(album[0].caption.lower(), first_name, message.from_user.username)
|
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 = await prepare_media_group_from_middlewares(album, post_caption)
|
||||||
media_group_message_id = await send_media_group_message_to_private_chat(
|
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)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
# Создаем helper сообщение с кнопками
|
||||||
markup = get_reply_keyboard_for_post()
|
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(
|
# Создаем helper пост и связываем его с основным
|
||||||
message_id=media_group_message_id, helper_message_id=help_message_id
|
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")
|
@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:
|
async def process_post(self, message: types.Message, album: Union[list, None] = None) -> None:
|
||||||
"""Process post based on content type"""
|
"""Process post based on content type"""
|
||||||
first_name = get_first_name(message)
|
first_name = get_first_name(message)
|
||||||
|
# TODO: Бесит меня этот функционал
|
||||||
if message.media_group_id is not None:
|
if message.media_group_id is not None:
|
||||||
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
|
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
|
||||||
await send_text_message(
|
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_time("get_reply_keyboard", "keyboard_service")
|
||||||
@track_errors("keyboard_service", "get_reply_keyboard")
|
@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 = ReplyKeyboardBuilder()
|
||||||
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
||||||
builder.row(types.KeyboardButton(text="📩Связаться с админами"))
|
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="🤪Хочу стикеры"))
|
builder.row(types.KeyboardButton(text="🤪Хочу стикеры"))
|
||||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||||
return markup
|
return markup
|
||||||
@@ -170,3 +171,33 @@ def create_keyboard_for_approve_ban():
|
|||||||
builder.add(types.KeyboardButton(text="Отменить"))
|
builder.add(types.KeyboardButton(text="Отменить"))
|
||||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||||
return markup
|
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.client.default import DefaultBotProperties
|
||||||
from aiogram.fsm.storage.memory import MemoryStorage
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
from aiogram.fsm.strategy import FSMStrategy
|
from aiogram.fsm.strategy import FSMStrategy
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from helper_bot.handlers.admin import admin_router
|
from helper_bot.handlers.admin import admin_router
|
||||||
from helper_bot.handlers.callback import callback_router
|
from helper_bot.handlers.callback import callback_router
|
||||||
from helper_bot.handlers.group import group_router
|
from helper_bot.handlers.group import group_router
|
||||||
from helper_bot.handlers.private import private_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.dependencies_middleware import DependenciesMiddleware
|
||||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||||
from helper_bot.middlewares.metrics_middleware import MetricsMiddleware, ErrorMetricsMiddleware
|
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):
|
async def start_bot(bdf):
|
||||||
@@ -17,7 +45,8 @@ async def start_bot(bdf):
|
|||||||
bot = Bot(token=token, default=DefaultBotProperties(
|
bot = Bot(token=token, default=DefaultBotProperties(
|
||||||
parse_mode='HTML',
|
parse_mode='HTML',
|
||||||
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
|
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
|
||||||
), timeout=30.0)
|
), timeout=60.0) # Увеличиваем timeout для стабильности
|
||||||
|
|
||||||
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
||||||
|
|
||||||
# ✅ Оптимизированная регистрация middleware
|
# ✅ Оптимизированная регистрация middleware
|
||||||
@@ -25,12 +54,58 @@ async def start_bot(bdf):
|
|||||||
dp.update.outer_middleware(MetricsMiddleware())
|
dp.update.outer_middleware(MetricsMiddleware())
|
||||||
dp.update.outer_middleware(BlacklistMiddleware())
|
dp.update.outer_middleware(BlacklistMiddleware())
|
||||||
|
|
||||||
# Добавляем middleware напрямую к роутерам для тестирования
|
# Создаем экземпляр VoiceHandlers
|
||||||
admin_router.message.middleware(MetricsMiddleware())
|
voice_handlers = VoiceHandlers(bdf, bdf.settings)
|
||||||
private_router.message.middleware(MetricsMiddleware())
|
voice_router = voice_handlers.router
|
||||||
callback_router.callback_query.middleware(MetricsMiddleware())
|
|
||||||
group_router.message.middleware(MetricsMiddleware())
|
# 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 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
|
import asyncio
|
||||||
from typing import Any, Dict, Union
|
from typing import Any, Dict, Union, List
|
||||||
|
|
||||||
from aiogram import BaseMiddleware
|
from aiogram import BaseMiddleware
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
|
||||||
class AlbumMiddleware(BaseMiddleware):
|
class AlbumMiddleware(BaseMiddleware):
|
||||||
def __init__(self, latency: Union[int, float] = 0.01): # Уменьшено с 0.1 до 0.01
|
"""
|
||||||
# Initialize latency and album_data dictionary
|
Middleware для обработки медиа групп в Telegram.
|
||||||
|
Собирает все сообщения одной медиа группы и передает их как album в data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, latency: Union[int, float] = 0.01):
|
||||||
|
"""
|
||||||
|
Инициализация middleware.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
latency: Задержка в секундах для сбора всех сообщений медиа группы
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
self.latency = latency
|
self.latency = latency
|
||||||
self.album_data = {}
|
self.album_data: Dict[str, Dict[str, List[Message]]] = {}
|
||||||
|
|
||||||
#
|
def collect_album_messages(self, event: Message) -> int:
|
||||||
def collect_album_messages(self, event: Message):
|
|
||||||
"""
|
"""
|
||||||
Collect messages of the same media group.
|
Собирает сообщения одной медиа группы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Сообщение для обработки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Количество сообщений в текущей медиа группе
|
||||||
"""
|
"""
|
||||||
# # Check if media_group_id exists in album_data
|
if not event.media_group_id:
|
||||||
|
return 0
|
||||||
|
|
||||||
if event.media_group_id not in self.album_data:
|
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": []}
|
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)
|
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"])
|
return len(self.album_data[event.media_group_id]["messages"])
|
||||||
|
|
||||||
#
|
|
||||||
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
|
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:
|
if not event.media_group_id:
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
#
|
|
||||||
# # Collect messages of the same media group
|
# Собираем сообщения одной медиа группы
|
||||||
total_before = self.collect_album_messages(event)
|
total_before = self.collect_album_messages(event)
|
||||||
#
|
|
||||||
# # Wait for a specified latency period
|
# Ждем указанный период для сбора всех сообщений
|
||||||
await asyncio.sleep(self.latency)
|
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"])
|
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:
|
if total_before != total_after:
|
||||||
return
|
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 = self.album_data[event.media_group_id]["messages"]
|
||||||
album_messages.sort(key=lambda x: x.message_id)
|
album_messages.sort(key=lambda x: x.message_id)
|
||||||
data["album"] = album_messages
|
data["album"] = album_messages
|
||||||
#
|
|
||||||
# # Remove the media group from tracking to free up memory
|
# Удаляем медиа группу из отслеживания для освобождения памяти
|
||||||
del self.album_data[event.media_group_id]
|
del self.album_data[event.media_group_id]
|
||||||
# # Call the original event handler
|
|
||||||
|
# Вызываем оригинальный обработчик события
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
#
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import html
|
import html
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from aiogram import BaseMiddleware, types
|
from aiogram import BaseMiddleware, types
|
||||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||||
@@ -26,12 +27,21 @@ class BlacklistMiddleware(BaseMiddleware):
|
|||||||
logger.info(f'Вызов BlacklistMiddleware для пользователя {user.username}')
|
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} заблокирован!')
|
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 "Не указана"
|
reason = html.escape(str(user_info[1])) if user_info and user_info[1] else "Не указана"
|
||||||
date_unban = html.escape(str(user_info[3])) if user_info[3] 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):
|
if isinstance(event, Message):
|
||||||
|
|||||||
@@ -1,23 +1,48 @@
|
|||||||
"""
|
"""
|
||||||
Metrics middleware for aiogram 3.x.
|
Enhanced Metrics middleware for aiogram 3.x.
|
||||||
Automatically collects metrics for message processing, command execution, and errors.
|
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 import BaseMiddleware
|
||||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||||
from aiogram.enums import ChatType
|
from aiogram.enums import ChatType
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
from ..utils.metrics import metrics
|
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):
|
class MetricsMiddleware(BaseMiddleware):
|
||||||
"""Middleware for automatic metrics collection in aiogram handlers."""
|
"""Enhanced middleware for automatic collection of ALL available metrics."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.logger = logging.getLogger(__name__)
|
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__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -25,45 +50,48 @@ class MetricsMiddleware(BaseMiddleware):
|
|||||||
event: TelegramObject,
|
event: TelegramObject,
|
||||||
data: Dict[str, Any]
|
data: Dict[str, Any]
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Process event and collect metrics."""
|
"""Process event and collect comprehensive metrics."""
|
||||||
|
|
||||||
|
# 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 and event info
|
||||||
self.logger.info(f"📊 MetricsMiddleware called for event type: {type(event).__name__}")
|
|
||||||
|
|
||||||
# Extract command info before execution
|
|
||||||
command_info = None
|
command_info = None
|
||||||
if isinstance(event, Message):
|
event_metrics = {}
|
||||||
self.logger.info(f"📊 Processing Message event")
|
|
||||||
await self._record_message_metrics(event)
|
# Process event based on type
|
||||||
if event.text and event.text.startswith('/'):
|
if hasattr(event, 'message') and event.message:
|
||||||
command_info = {
|
event_metrics = await self._record_comprehensive_message_metrics(event.message)
|
||||||
'command': event.text.split()[0][1:], # Remove '/' and get command name
|
command_info = self._extract_command_info_with_fallback(event.message)
|
||||||
'user_type': "user" if event.from_user else "unknown",
|
elif hasattr(event, 'callback_query') and event.callback_query:
|
||||||
'handler_type': "message_handler"
|
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):
|
elif isinstance(event, CallbackQuery):
|
||||||
self.logger.info(f"📊 Processing CallbackQuery event")
|
event_metrics = await self._record_comprehensive_callback_metrics(event)
|
||||||
await self._record_callback_metrics(event)
|
command_info = self._extract_callback_command_info_with_fallback(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:
|
else:
|
||||||
self.logger.info(f"📊 Processing unknown event type: {type(event).__name__}")
|
event_metrics = await self._record_unknown_event_metrics(event)
|
||||||
|
|
||||||
# Execute handler with timing
|
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()
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
result = await handler(event, data)
|
result = await handler(event, data)
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
|
|
||||||
# Record successful execution
|
# Record successful execution metrics
|
||||||
handler_name = self._get_handler_name(handler)
|
handler_name = self._get_handler_name(handler)
|
||||||
self.logger.info(f"📊 Recording successful execution: {handler_name}")
|
|
||||||
metrics.record_method_duration(
|
metrics.record_method_duration(
|
||||||
handler_name,
|
handler_name,
|
||||||
duration,
|
duration,
|
||||||
@@ -71,7 +99,6 @@ class MetricsMiddleware(BaseMiddleware):
|
|||||||
"success"
|
"success"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Record command with success status if applicable
|
|
||||||
if command_info:
|
if command_info:
|
||||||
metrics.record_command(
|
metrics.record_command(
|
||||||
command_info['command'],
|
command_info['command'],
|
||||||
@@ -80,27 +107,30 @@ class MetricsMiddleware(BaseMiddleware):
|
|||||||
"success"
|
"success"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self._record_additional_success_metrics(event, event_metrics, handler_name)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
|
|
||||||
# Record error and timing
|
# Record error metrics
|
||||||
handler_name = self._get_handler_name(handler)
|
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(
|
metrics.record_method_duration(
|
||||||
handler_name,
|
handler_name,
|
||||||
duration,
|
duration,
|
||||||
"handler",
|
"handler",
|
||||||
"error"
|
"error"
|
||||||
)
|
)
|
||||||
|
|
||||||
metrics.record_error(
|
metrics.record_error(
|
||||||
type(e).__name__,
|
error_type,
|
||||||
"handler",
|
"handler",
|
||||||
handler_name
|
handler_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# Record command with error status if applicable
|
|
||||||
if command_info:
|
if command_info:
|
||||||
metrics.record_command(
|
metrics.record_command(
|
||||||
command_info['command'],
|
command_info['command'],
|
||||||
@@ -109,32 +139,46 @@ class MetricsMiddleware(BaseMiddleware):
|
|||||||
"error"
|
"error"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self._record_additional_error_metrics(event, event_metrics, handler_name, error_type)
|
||||||
|
|
||||||
raise
|
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:
|
async def _update_active_users_metric(self):
|
||||||
"""Extract handler name efficiently."""
|
"""Periodically update active users metric from database."""
|
||||||
# Проверяем различные способы получения имени хендлера
|
try:
|
||||||
if hasattr(handler, '__name__') and handler.__name__ != '<lambda>':
|
#TODO: Должна подключаться к базе данных, а не к глобальному экземпляру
|
||||||
return handler.__name__
|
from ..utils.base_dependency_factory import get_global_instance
|
||||||
elif hasattr(handler, '__qualname__') and handler.__qualname__ != '<lambda>':
|
bdf = get_global_instance()
|
||||||
return handler.__qualname__
|
bot_db = bdf.get_db()
|
||||||
elif hasattr(handler, 'callback') and hasattr(handler.callback, '__name__'):
|
|
||||||
return handler.callback.__name__
|
# Используем правильные методы AsyncBotDB для выполнения запросов
|
||||||
elif hasattr(handler, 'view') and hasattr(handler.view, '__name__'):
|
# Простой подсчет всех пользователей в базе
|
||||||
return handler.view.__name__
|
total_users_query = "SELECT COUNT(DISTINCT user_id) as total FROM our_users"
|
||||||
else:
|
total_users_result = await bot_db.fetch_one(total_users_query)
|
||||||
# Пытаемся получить имя из строкового представления
|
total_users = total_users_result['total'] if total_users_result else 1
|
||||||
handler_str = str(handler)
|
|
||||||
if 'function' in handler_str:
|
# Подсчет активных за день пользователей (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'))"
|
||||||
import re
|
daily_users_result = await bot_db.fetch_one(daily_users_query)
|
||||||
match = re.search(r'function\s+(\w+)', handler_str)
|
daily_users = daily_users_result['daily'] if daily_users_result else 1
|
||||||
if match:
|
|
||||||
return match.group(1)
|
# Устанавливаем метрики с правильными лейблами
|
||||||
return "unknown"
|
metrics.set_active_users(daily_users, "daily")
|
||||||
|
metrics.set_total_users(total_users)
|
||||||
async def _record_message_metrics(self, message: Message):
|
self.logger.info(f"📊 Active users metric updated: {daily_users} (daily), {total_users} (total)")
|
||||||
"""Record message metrics efficiently."""
|
|
||||||
|
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
|
# Determine message type
|
||||||
message_type = "text"
|
message_type = "text"
|
||||||
if message.photo:
|
if message.photo:
|
||||||
@@ -163,14 +207,201 @@ class MetricsMiddleware(BaseMiddleware):
|
|||||||
|
|
||||||
# Record message processing
|
# Record message processing
|
||||||
metrics.record_message(message_type, chat_type, "message_handler")
|
metrics.record_message(message_type, chat_type, "message_handler")
|
||||||
|
|
||||||
|
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_callback_metrics(self, callback: CallbackQuery):
|
async def _record_comprehensive_callback_metrics(self, callback: CallbackQuery) -> Dict[str, Any]:
|
||||||
"""Record callback metrics efficiently."""
|
"""Record comprehensive callback metrics."""
|
||||||
|
# Record callback message
|
||||||
metrics.record_message("callback_query", "callback", "callback_handler")
|
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):
|
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__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -183,14 +414,36 @@ class DatabaseMetricsMiddleware(BaseMiddleware):
|
|||||||
# Check if this handler involves database operations
|
# Check if this handler involves database operations
|
||||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||||
|
|
||||||
# You can add specific database operation detection logic here
|
# Record middleware start
|
||||||
# For now, we'll just pass through and let individual decorators handle it
|
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):
|
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__(
|
async def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -200,14 +453,28 @@ class ErrorMetricsMiddleware(BaseMiddleware):
|
|||||||
) -> Any:
|
) -> Any:
|
||||||
"""Process event and collect error metrics."""
|
"""Process event and collect error metrics."""
|
||||||
|
|
||||||
|
# Record middleware start
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
# Record error metrics
|
# Record error metrics
|
||||||
|
duration = time.time() - start_time
|
||||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||||
|
|
||||||
|
metrics.record_middleware("ErrorMetricsMiddleware", duration, "error")
|
||||||
metrics.record_error(
|
metrics.record_error(
|
||||||
type(e).__name__,
|
type(e).__name__,
|
||||||
"handler",
|
"error_middleware",
|
||||||
handler_name
|
handler_name
|
||||||
)
|
)
|
||||||
|
|
||||||
raise
|
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:
|
try:
|
||||||
logger.info("Запуск автоматического разбана пользователей")
|
logger.info("Запуск автоматического разбана пользователей")
|
||||||
|
|
||||||
# Получаем сегодняшнюю дату в формате YYYY-MM-DD
|
# Получаем текущий UNIX timestamp
|
||||||
moscow_tz = timezone(timedelta(hours=3)) # UTC+3 для Москвы
|
current_timestamp = int(datetime.now().timestamp())
|
||||||
today = datetime.now(moscow_tz).strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
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:
|
if not users_to_unban:
|
||||||
logger.info("Нет пользователей для разблокировки сегодня")
|
logger.info("Нет пользователей для разблокировки сегодня")
|
||||||
@@ -55,20 +54,20 @@ class AutoUnbanScheduler:
|
|||||||
failed_users = []
|
failed_users = []
|
||||||
|
|
||||||
# Разблокируем каждого пользователя
|
# Разблокируем каждого пользователя
|
||||||
for user_id, username in users_to_unban.items():
|
for user_id in users_to_unban:
|
||||||
try:
|
try:
|
||||||
result = self.bot_db.delete_user_blacklist(user_id)
|
result = await self.bot_db.delete_user_blacklist(user_id)
|
||||||
if result:
|
if result:
|
||||||
success_count += 1
|
success_count += 1
|
||||||
logger.info(f"Пользователь {user_id} ({username}) успешно разблокирован")
|
logger.info(f"Пользователь {user_id} успешно разблокирован")
|
||||||
else:
|
else:
|
||||||
failed_count += 1
|
failed_count += 1
|
||||||
failed_users.append(f"{user_id} ({username})")
|
failed_users.append(f"{user_id}")
|
||||||
logger.error(f"Ошибка при разблокировке пользователя {user_id} ({username})")
|
logger.error(f"Ошибка при разблокировке пользователя {user_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed_count += 1
|
failed_count += 1
|
||||||
failed_users.append(f"{user_id} ({username})")
|
failed_users.append(f"{user_id}")
|
||||||
logger.error(f"Исключение при разблокировке пользователя {user_id} ({username}): {e}")
|
logger.error(f"Исключение при разблокировке пользователя {user_id}: {e}")
|
||||||
|
|
||||||
# Формируем отчет
|
# Формируем отчет
|
||||||
report = self._generate_report(success_count, failed_count, failed_users, users_to_unban)
|
report = self._generate_report(success_count, failed_count, failed_users, users_to_unban)
|
||||||
@@ -93,10 +92,9 @@ class AutoUnbanScheduler:
|
|||||||
|
|
||||||
if success_count > 0:
|
if success_count > 0:
|
||||||
report += "✅ <b>Разблокированные пользователи:</b>\n"
|
report += "✅ <b>Разблокированные пользователи:</b>\n"
|
||||||
for user_id, username in all_users.items():
|
for user_id in all_users:
|
||||||
if f"{user_id} ({username})" not in failed_users:
|
if str(user_id) not in failed_users:
|
||||||
safe_username = username if username else "Неизвестный пользователь"
|
report += f"• ID: {user_id}\n"
|
||||||
report += f"• ID: {user_id}, Имя: {safe_username}\n"
|
|
||||||
report += "\n"
|
report += "\n"
|
||||||
|
|
||||||
if failed_users:
|
if failed_users:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from database.db import BotDB
|
from database.async_db import AsyncBotDB
|
||||||
|
|
||||||
|
|
||||||
class BaseDependencyFactory:
|
class BaseDependencyFactory:
|
||||||
@@ -18,10 +18,7 @@ class BaseDependencyFactory:
|
|||||||
if not os.path.isabs(database_path):
|
if not os.path.isabs(database_path):
|
||||||
database_path = os.path.join(project_dir, database_path)
|
database_path = os.path.join(project_dir, database_path)
|
||||||
|
|
||||||
database_dir = project_dir
|
self.database = AsyncBotDB(database_path)
|
||||||
database_name = database_path.replace(project_dir + '/', '')
|
|
||||||
|
|
||||||
self.database = BotDB(database_dir, database_name)
|
|
||||||
|
|
||||||
self._load_settings_from_env()
|
self._load_settings_from_env()
|
||||||
|
|
||||||
@@ -46,6 +43,11 @@ class BaseDependencyFactory:
|
|||||||
'test': self._parse_bool(os.getenv('TEST', 'false'))
|
'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:
|
def _parse_bool(self, value: str) -> bool:
|
||||||
"""Парсит строковое значение в boolean."""
|
"""Парсит строковое значение в boolean."""
|
||||||
return value.lower() in ('true', '1', 'yes', 'on')
|
return value.lower() in ('true', '1', 'yes', 'on')
|
||||||
@@ -60,7 +62,7 @@ class BaseDependencyFactory:
|
|||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
return self.settings
|
return self.settings
|
||||||
|
|
||||||
def get_db(self) -> BotDB:
|
def get_db(self) -> AsyncBotDB:
|
||||||
"""Возвращает подключение к базе данных."""
|
"""Возвращает подключение к базе данных."""
|
||||||
return self.database
|
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 html
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import emoji as _emoji_lib
|
import emoji as _emoji_lib
|
||||||
except Exception:
|
_emoji_lib_available = True
|
||||||
|
except ImportError:
|
||||||
_emoji_lib = None
|
_emoji_lib = None
|
||||||
|
_emoji_lib_available = False
|
||||||
|
|
||||||
from aiogram import types
|
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 helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
||||||
from logs.custom_logger import logger
|
from logs.custom_logger import logger
|
||||||
|
from database.models import TelegramPost
|
||||||
|
|
||||||
# Local imports - metrics
|
# Local imports - metrics
|
||||||
from .metrics import (
|
from .metrics import (
|
||||||
@@ -24,10 +29,11 @@ from .metrics import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
bdf = get_global_instance()
|
bdf = get_global_instance()
|
||||||
|
#TODO: поменять архитектуру и подключить правильный BotDB
|
||||||
BotDB = bdf.get_db()
|
BotDB = bdf.get_db()
|
||||||
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
|
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())
|
emoji_list = list(_emoji_lib.EMOJI_DATA.keys())
|
||||||
else:
|
else:
|
||||||
# Fallback minimal emoji set for environments without the 'emoji' package (e.g., CI tests)
|
# 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}'
|
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:
|
Args:
|
||||||
message: сообщение
|
message: сообщение
|
||||||
file_id: File ID фотографии
|
file_id: File ID файла
|
||||||
filename: Имя файла, под которым будет сохранено фото
|
content_type: тип контента (photo, video, audio, voice, video_note)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Путь к сохраненному файлу, если файл был скачан успешно, иначе None
|
Путь к сохраненному файлу, если файл был скачан успешно, иначе None
|
||||||
"""
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs("files", exist_ok=True)
|
# Валидация параметров
|
||||||
os.makedirs("files/photos", exist_ok=True)
|
if not file_id or not message or not message.bot:
|
||||||
os.makedirs("files/videos", exist_ok=True)
|
logger.error("download_file: Неверные параметры - file_id, message или bot отсутствуют")
|
||||||
os.makedirs("files/music", exist_ok=True)
|
return None
|
||||||
os.makedirs("files/voice", exist_ok=True)
|
|
||||||
os.makedirs("files/video_notes", exist_ok=True)
|
# Определяем папку по типу контента
|
||||||
|
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 = 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)
|
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
|
return file_path
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||||
"""
|
"""
|
||||||
Создает MediaGroup.
|
Создает MediaGroup согласно best practices aiogram 3.x.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
album: Album объект из Telegram API.
|
album: Album объект из Telegram API (список сообщений).
|
||||||
post_caption: Текст подписи к первому фото.
|
post_caption: Текст подписи к первому медиа файлу.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Список InputMediaPhoto (MediaGroup).
|
Список InputMedia объектов для MediaGroup.
|
||||||
"""
|
"""
|
||||||
# Экранируем post_caption для безопасного использования в HTML
|
# Экранируем post_caption для безопасного использования в HTML
|
||||||
safe_post_caption = html.escape(str(post_caption)) if post_caption else ""
|
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):
|
for i, message in enumerate(album):
|
||||||
if message.photo:
|
if message.photo:
|
||||||
file_id = message.photo[-1].file_id
|
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:
|
elif message.video:
|
||||||
file_id = message.video.file_id
|
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:
|
elif message.audio:
|
||||||
file_id = message.audio.file_id
|
file_id = message.audio.file_id
|
||||||
media_type = 'audio'
|
# Для аудио используем InputMediaAudio
|
||||||
else:
|
if i == 0: # Первое аудио получает подпись
|
||||||
# Если нет фото, видео или аудио, пропускаем сообщение
|
|
||||||
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':
|
|
||||||
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
||||||
else:
|
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))
|
media_group.append(InputMediaAudio(media=file_id))
|
||||||
|
elif message.document:
|
||||||
return media_group # Возвращаем MediaGroup
|
file_id = message.document.file_id
|
||||||
|
# Для документов используем InputMediaDocument (если поддерживается)
|
||||||
|
if i == 0: # Первый документ получает подпись
|
||||||
async def add_in_db_media_mediagroup(sent_message, bot_db):
|
media_group.append(InputMediaDocument(media=file_id, caption=safe_post_caption))
|
||||||
"""
|
else:
|
||||||
Идентификатор медиа-группы
|
media_group.append(InputMediaDocument(media=file_id))
|
||||||
|
|
||||||
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')
|
|
||||||
else:
|
else:
|
||||||
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
|
# Если нет поддерживаемого медиа, пропускаем сообщение
|
||||||
continue
|
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:
|
Args:
|
||||||
sent_message: sent_message объект из Telegram API
|
sent_message: sent_message объект из Telegram API
|
||||||
bot_db: Экземпляр базы данных
|
bot_db: Экземпляр базы данных
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Список InputFile (FSInputFile).
|
bool: True если контент успешно добавлен, False в случае ошибки
|
||||||
"""
|
"""
|
||||||
if sent_message.photo:
|
start_time = time.time()
|
||||||
file_id = sent_message.photo[-1].file_id
|
|
||||||
file_path = await download_file(sent_message, file_id=file_id)
|
try:
|
||||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo')
|
# Валидация параметров
|
||||||
elif sent_message.video:
|
if not sent_message or not bot_db:
|
||||||
file_id = sent_message.video.file_id
|
logger.error("add_in_db_media: Неверные параметры - sent_message или bot_db отсутствуют")
|
||||||
file_path = await download_file(sent_message, file_id=file_id)
|
return False
|
||||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video')
|
|
||||||
elif sent_message.voice:
|
post_id = sent_message.message_id # ID поста (это же сообщение)
|
||||||
file_id = sent_message.voice.file_id
|
content_type = None
|
||||||
file_path = await download_file(sent_message, file_id=file_id)
|
file_id = None
|
||||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice')
|
|
||||||
elif sent_message.audio:
|
# Определяем тип контента и file_id
|
||||||
file_id = sent_message.audio.file_id
|
if sent_message.photo:
|
||||||
file_path = await download_file(sent_message, file_id=file_id)
|
content_type = 'photo'
|
||||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio')
|
file_id = sent_message.photo[-1].file_id
|
||||||
elif sent_message.video_note:
|
elif sent_message.video:
|
||||||
file_id = sent_message.video_note.file_id
|
content_type = 'video'
|
||||||
file_path = await download_file(sent_message, file_id=file_id)
|
file_id = sent_message.video.file_id
|
||||||
bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note')
|
elif sent_message.voice:
|
||||||
|
content_type = 'voice'
|
||||||
|
file_id = sent_message.voice.file_id
|
||||||
|
elif sent_message.audio:
|
||||||
|
content_type = 'audio'
|
||||||
|
file_id = sent_message.audio.file_id
|
||||||
|
elif sent_message.video_note:
|
||||||
|
content_type = 'video_note'
|
||||||
|
file_id = sent_message.video_note.file_id
|
||||||
|
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,
|
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(
|
sent_message = await message.bot.send_media_group(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
media=media_group,
|
media=media_group,
|
||||||
)
|
)
|
||||||
bot_db.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id)
|
post = TelegramPost(
|
||||||
await add_in_db_media_mediagroup(sent_message, bot_db)
|
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
|
message_id = sent_message[-1].message_id
|
||||||
return 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_content: Список кортежей с путями к файлам.
|
||||||
post_text: Текст подписи.
|
post_text: Текст подписи.
|
||||||
"""
|
"""
|
||||||
|
logger.info(f"Начинаю отправку медиа-группы в чат {chat_id}, количество файлов: {len(post_content)}")
|
||||||
|
|
||||||
media = []
|
media = []
|
||||||
for file_path in post_content:
|
for i, file_path in enumerate(post_content):
|
||||||
try:
|
try:
|
||||||
file = FSInputFile(path=file_path[0])
|
file = FSInputFile(path=file_path[0])
|
||||||
type = file_path[1]
|
type = file_path[1]
|
||||||
|
logger.debug(f"Обрабатываю файл {i+1}/{len(post_content)}: {file_path[0]} (тип: {type})")
|
||||||
|
|
||||||
if type == 'video':
|
if type == 'video':
|
||||||
media.append(types.InputMediaVideo(media=file))
|
media.append(types.InputMediaVideo(media=file))
|
||||||
if type == 'photo':
|
elif type == 'photo':
|
||||||
media.append(types.InputMediaPhoto(media=file))
|
media.append(types.InputMediaPhoto(media=file))
|
||||||
|
else:
|
||||||
|
logger.warning(f"Неизвестный тип файла: {type} для {file_path[0]}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error(f"Файл не найден: {file_path[0]}")
|
logger.error(f"Файл не найден: {file_path[0]}")
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при обработке файла {file_path[0]}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Подготовлено {len(media)} медиа-файлов для отправки")
|
||||||
|
|
||||||
# Добавляем подпись к последнему файлу
|
# Добавляем подпись к последнему файлу
|
||||||
if media:
|
if media:
|
||||||
# Экранируем post_text для безопасного использования в HTML
|
# Экранируем post_text для безопасного использования в HTML
|
||||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||||
media[-1].caption = safe_post_text
|
media[-1].caption = safe_post_text
|
||||||
|
logger.debug(f"Добавлена подпись к последнему файлу: {safe_post_text[:50]}{'...' if len(safe_post_text) > 50 else ''}")
|
||||||
|
|
||||||
await bot.send_media_group(chat_id=chat_id, media=media)
|
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):
|
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
|
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):
|
def add_days_to_date(days: int):
|
||||||
"""Прибавляет указанное количество дней к текущей дате и возвращает дату в формате DD-MM-YYYY."""
|
"""Прибавляет указанное количество дней к текущей дате и возвращает UNIX timestamp."""
|
||||||
current_date = datetime.now()
|
current_date = datetime.now()
|
||||||
future_date = current_date + timedelta(days=days)
|
future_date = current_date + timedelta(days=days)
|
||||||
formatted_date = future_date.strftime("%d-%m-%Y")
|
return int(future_date.timestamp())
|
||||||
return formatted_date
|
|
||||||
|
|
||||||
|
|
||||||
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 - текст сообщения
|
message - текст сообщения
|
||||||
user_ids - лист кортежей [(user_name: user_id)]
|
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"
|
message = "Список заблокированных пользователей:\n"
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
# Экранируем пользовательские данные для безопасного использования
|
user_id, ban_reason, unban_date = user
|
||||||
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
|
# Получаем имя пользователя из таблицы users
|
||||||
safe_ban_reason = html.escape(str(user[2])) if user[2] else "Причина не указана"
|
username = await bot_db.get_username(user_id)
|
||||||
safe_unban_date = html.escape(str(user[3])) if user[3] else "Дата не указана"
|
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"
|
safe_user_name = html.escape(str(safe_user_name))
|
||||||
message += f"Дата разбана: {safe_unban_date}\n\n"
|
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
|
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 - текст сообщения
|
message - текст сообщения
|
||||||
user_ids - лист кортежей [(user_name: user_id)]
|
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 = []
|
user_ids = []
|
||||||
|
|
||||||
for user in users:
|
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 для безопасного использования
|
# Экранируем user_name для безопасного использования
|
||||||
safe_user_name = html.escape(str(user[0])) if user[0] else "Неизвестный пользователь"
|
safe_user_name = html.escape(str(safe_user_name))
|
||||||
user_ids.append((safe_user_name, user[1]))
|
user_ids.append((safe_user_name, user_id))
|
||||||
return user_ids
|
return user_ids
|
||||||
|
|
||||||
|
|
||||||
def delete_user_blacklist(user_id: int, bot_db):
|
async def delete_user_blacklist(user_id: int, bot_db):
|
||||||
return bot_db.delete_user_blacklist(user_id=user_id)
|
return await bot_db.delete_user_blacklist(user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
@track_time("check_username_and_full_name", "helper_func")
|
@track_time("check_username_and_full_name", "helper_func")
|
||||||
@track_errors("helper_func", "check_username_and_full_name")
|
@track_errors("helper_func", "check_username_and_full_name")
|
||||||
@db_query_time("get_username_and_full_name", "users", "select")
|
@db_query_time("check_username_and_full_name", "users", "select")
|
||||||
def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
|
async 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)
|
"""Проверяет, изменились ли username или full_name пользователя"""
|
||||||
return username != username_db or full_name != full_name_db
|
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):
|
async def unban_notifier(bot, BotDB, GROUP_FOR_MESSAGE):
|
||||||
# Получение сегодняшней даты в формате DD-MM-YYYY
|
# Получение текущего UNIX timestamp
|
||||||
current_date = datetime.now()
|
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"
|
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 для безопасного использования
|
# Экранируем 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"
|
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")
|
@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
|
is_bot = message.from_user.is_bot
|
||||||
language_code = message.from_user.language_code
|
language_code = message.from_user.language_code
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
current_date = datetime.now()
|
|
||||||
date = current_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
# Выбираем эмодзю, пробегаемся циклом и смотрим что в базе такого еще не было
|
# Выбираем эмодзю, пробегаемся циклом и смотрим что в базе такого еще не было
|
||||||
user_emoji = get_random_emoji()
|
user_emoji = await get_random_emoji()
|
||||||
|
|
||||||
if not BotDB.user_exists(user_id):
|
if not await 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,
|
# Create User object with current timestamp
|
||||||
date)
|
from database.models import User
|
||||||
metrics.record_db_query("add_new_user_in_db", 0.0, "users", "insert")
|
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:
|
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:
|
if is_need_update:
|
||||||
BotDB.update_username_and_full_name(user_id, username, full_name)
|
await BotDB.update_user_info(user_id, username, full_name)
|
||||||
metrics.record_db_query("update_username_and_full_name", 0.0, "users", "update")
|
metrics.record_db_query("update_user_info", 0.0, "users", "update")
|
||||||
if source != 'voice':
|
if source != 'voice':
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
|
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
|
||||||
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
|
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
|
||||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
|
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
|
||||||
sleep(1)
|
sleep(1)
|
||||||
BotDB.update_date_for_user(date, user_id)
|
await BotDB.update_user_date(user_id)
|
||||||
metrics.record_db_query("update_date_for_user", 0.0, "users", "update")
|
metrics.record_db_query("update_user_date", 0.0, "users", "update")
|
||||||
|
|
||||||
|
|
||||||
@track_time("check_user_emoji", "helper_func")
|
@track_time("check_user_emoji", "helper_func")
|
||||||
@track_errors("helper_func", "check_user_emoji")
|
@track_errors("helper_func", "check_user_emoji")
|
||||||
@db_query_time("check_emoji_for_user", "users", "select")
|
@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_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 ("Смайл еще не определен", "Эмоджи не определен", ""):
|
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
|
||||||
user_emoji = get_random_emoji()
|
user_emoji = await get_random_emoji()
|
||||||
BotDB.update_emoji_for_user(user_id=user_id, emoji=user_emoji)
|
await BotDB.update_user_emoji(user_id=user_id, emoji=user_emoji)
|
||||||
metrics.record_db_query("update_emoji_for_user", 0.0, "users", "update")
|
metrics.record_db_query("update_user_emoji", 0.0, "users", "update")
|
||||||
return user_emoji
|
return user_emoji
|
||||||
|
|
||||||
|
|
||||||
@track_time("get_random_emoji", "helper_func")
|
@track_time("get_random_emoji", "helper_func")
|
||||||
@track_errors("helper_func", "get_random_emoji")
|
@track_errors("helper_func", "get_random_emoji")
|
||||||
@db_query_time("check_emoji", "users", "select")
|
@db_query_time("check_emoji", "users", "select")
|
||||||
def get_random_emoji():
|
async def get_random_emoji():
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while attempts < 100:
|
while attempts < 100:
|
||||||
user_emoji = random.choice(emoji_list)
|
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
|
return user_emoji
|
||||||
attempts += 1
|
attempts += 1
|
||||||
logger.error("Не удалось найти уникальный эмодзи после нескольких попыток.")
|
logger.error("Не удалось найти уникальный эмодзи после нескольких попыток.")
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ constants = {
|
|||||||
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
||||||
"&Я был создан для того, чтобы помочь тебе выложить пост в наш канал и если это необходимо, связаться с админами ✍✉"
|
"&Я был создан для того, чтобы помочь тебе выложить пост в наш канал и если это необходимо, связаться с админами ✍✉"
|
||||||
"&Так же я могу выдать тебе набор стикеров, где я буду главным героем🦸♂"
|
"&Так же я могу выдать тебе набор стикеров, где я буду главным героем🦸♂"
|
||||||
"&Наш бот голосового общения переехал сюда: https://t.me/podslushano_biysk_bot 🎤&Там можно послушать о чем говорит наш город🎧"
|
"&Наш бот голосового общения переехал ко мне! Доступен по кнопке 🎤Голосовой бот &Там можно послушать о чем говорит наш город🎧"
|
||||||
"&Предлагай свой пост мне и я обязательно его опубликую😉"
|
"&Предлагай свой пост мне и я обязательно его опубликую😉"
|
||||||
"&Для продолжения взаимодействия воспользуйся меню внизу твоего дисплея⬇"
|
"&Для продолжения взаимодействия воспользуйся меню внизу твоего дисплея⬇"
|
||||||
"&&Если что-то пошло не так: введи в чат команду /start, это перезапустит сценарий сначала."
|
"&&Если что-то пошло не так: введи в чат команду /start или /restart, это перезапустит сценарий сначала."
|
||||||
|
"Почитать инструкцию к боту можно по команде /help. Если есть вопросы, то пиши в личку: @Kerrad1"
|
||||||
"&Не жми кнопку несколько раз если я не ответил с первого раза. Возможно ведутся тех.работы и я отвечу позже"
|
"&Не жми кнопку несколько раз если я не ответил с первого раза. Возможно ведутся тех.работы и я отвечу позже"
|
||||||
"&&Основная группа в ВК: https://vk.com/love_bsk"
|
"&&Группа в ВК: https://vk.com/love_bsk"
|
||||||
"&Основной канал в ТГ: https://t.me/love_bsk",
|
"&Канал в ТГ: https://t.me/love_bsk",
|
||||||
'SUGGEST_NEWS': "username, окей, жду от тебя текст поста🙌🏼"
|
'SUGGEST_NEWS': "username, окей, жду от тебя текст поста🙌🏼"
|
||||||
"&Обрати внимание, что я умный и смогу из твоего текста понять команды указанные ниже😉"
|
"&Обрати внимание, что я умный и смогу из твоего текста понять команды указанные ниже😉"
|
||||||
"&Если хочешь чтобы пост был опубликован анонимно, напиши в любом месте своего поста слово 'анон'."
|
"&Если хочешь чтобы пост был опубликован анонимно, напиши в любом месте своего поста слово 'анон'."
|
||||||
@@ -35,12 +36,28 @@ constants = {
|
|||||||
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
|
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
|
||||||
"QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉",
|
"QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉",
|
||||||
"SUCCESS_SEND_MESSAGE": "Пост успешно отправлен❤️ Ожидай одобрения😊",
|
"SUCCESS_SEND_MESSAGE": "Пост успешно отправлен❤️ Ожидай одобрения😊",
|
||||||
|
# Voice handler messages
|
||||||
"MESSAGE_FOR_STANDUP": "Отлично, ты вошел в режим стендапа 📣"
|
"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
|
registry=self.registry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Total users gauge (отдельная метрика)
|
||||||
|
self.total_users = Gauge(
|
||||||
|
'total_users',
|
||||||
|
'Total number of users in database',
|
||||||
|
registry=self.registry
|
||||||
|
)
|
||||||
|
|
||||||
# Database query metrics
|
# Database query metrics
|
||||||
self.db_query_duration_seconds = Histogram(
|
self.db_query_duration_seconds = Histogram(
|
||||||
'db_query_duration_seconds',
|
'db_query_duration_seconds',
|
||||||
@@ -70,6 +77,14 @@ class BotMetrics:
|
|||||||
registry=self.registry
|
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
|
# Message processing metrics
|
||||||
self.messages_processed_total = Counter(
|
self.messages_processed_total = Counter(
|
||||||
'messages_processed_total',
|
'messages_processed_total',
|
||||||
@@ -92,7 +107,54 @@ class BotMetrics:
|
|||||||
self.rate_limit_hits_total = Counter(
|
self.rate_limit_hits_total = Counter(
|
||||||
'rate_limit_hits_total',
|
'rate_limit_hits_total',
|
||||||
'Total number of rate limit hits',
|
'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
|
registry=self.registry
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,10 +183,14 @@ class BotMetrics:
|
|||||||
status=status
|
status=status
|
||||||
).observe(duration)
|
).observe(duration)
|
||||||
|
|
||||||
def set_active_users(self, count: int, user_type: str = "total"):
|
def set_active_users(self, count: int, user_type: str = "daily"):
|
||||||
"""Set the number of active users."""
|
"""Set the number of active users for a specific type."""
|
||||||
self.active_users.labels(user_type=user_type).set(count)
|
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"):
|
def record_db_query(self, query_type: str, duration: float, table_name: str = "unknown", operation: str = "unknown"):
|
||||||
"""Record database query duration."""
|
"""Record database query duration."""
|
||||||
self.db_query_duration_seconds.labels(
|
self.db_query_duration_seconds.labels(
|
||||||
@@ -153,6 +219,54 @@ class BotMetrics:
|
|||||||
status=status
|
status=status
|
||||||
).observe(duration)
|
).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:
|
def get_metrics(self) -> bytes:
|
||||||
"""Generate metrics in Prometheus format."""
|
"""Generate metrics in Prometheus format."""
|
||||||
return generate_latest(self.registry)
|
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:
|
except Exception as e:
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
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(
|
metrics.record_error(
|
||||||
type(e).__name__,
|
type(e).__name__,
|
||||||
"database",
|
"database",
|
||||||
@@ -293,6 +413,12 @@ def db_query_time(query_type: str = "unknown", table_name: str = "unknown", oper
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
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(
|
metrics.record_error(
|
||||||
type(e).__name__,
|
type(e).__name__,
|
||||||
"database",
|
"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
|
prometheus-client==0.19.0
|
||||||
aiohttp==3.9.1
|
aiohttp==3.9.1
|
||||||
|
|
||||||
|
# Network stability improvements
|
||||||
|
aiohttp[speedups]>=3.9.1
|
||||||
|
aiodns>=3.0.0
|
||||||
|
cchardet>=2.1.7
|
||||||
|
|
||||||
# Development tools
|
# Development tools
|
||||||
pluggy==1.5.0
|
pluggy==1.5.0
|
||||||
attrs~=23.2.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.main import start_bot
|
||||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
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 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):
|
def get_pid_manager():
|
||||||
"""Запуск модуля мониторинга сервера"""
|
"""Получение PID менеджера из инфраструктуры проекта"""
|
||||||
monitor = ServerMonitor(
|
try:
|
||||||
bot=bot,
|
# Пытаемся импортировать из инфраструктуры проекта
|
||||||
group_for_logs=bdf.settings['Telegram']['group_for_logs'],
|
infra_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'infra', 'monitoring')
|
||||||
important_logs=bdf.settings['Telegram']['important_logs']
|
if infra_path not in sys.path:
|
||||||
)
|
sys.path.insert(0, infra_path)
|
||||||
return monitor
|
|
||||||
|
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():
|
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()
|
bdf = get_global_instance()
|
||||||
|
|
||||||
# Создаем бота для мониторинга
|
# Создаем бота для автоматического разбана
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
|
||||||
monitor_bot = Bot(
|
auto_unban_bot = Bot(
|
||||||
token=bdf.settings['Telegram']['bot_token'],
|
token=bdf.settings['Telegram']['bot_token'],
|
||||||
default=DefaultBotProperties(parse_mode='HTML'),
|
default=DefaultBotProperties(parse_mode='HTML'),
|
||||||
timeout=30.0
|
timeout=30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
# Создаем экземпляр монитора
|
|
||||||
monitor = await start_monitoring(bdf, monitor_bot)
|
|
||||||
|
|
||||||
# Инициализируем планировщик автоматического разбана
|
# Инициализируем планировщик автоматического разбана
|
||||||
auto_unban_scheduler = get_auto_unban_scheduler()
|
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()
|
auto_unban_scheduler.start_scheduler()
|
||||||
|
|
||||||
# Инициализируем метрики ПОСЛЕ импорта всех модулей
|
# Метрики запускаются в main.py через server_prometheus.py
|
||||||
# Это гарантирует, что global instance полностью инициализирован
|
# Здесь не нужно дублировать функциональность
|
||||||
from helper_bot.utils.metrics_exporter import MetricsManager
|
|
||||||
metrics_manager = MetricsManager(host="0.0.0.0", port=8000)
|
|
||||||
|
|
||||||
# Флаг для корректного завершения
|
# Флаг для корректного завершения
|
||||||
shutdown_event = asyncio.Event()
|
shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
def signal_handler(signum, frame):
|
def signal_handler(signum, frame):
|
||||||
"""Обработчик сигналов для корректного завершения"""
|
"""Обработчик сигналов для корректного завершения"""
|
||||||
print(f"\nПолучен сигнал {signum}, завершаем работу...")
|
logger.info(f"Получен сигнал {signum}, завершаем работу...")
|
||||||
shutdown_event.set()
|
shutdown_event.set()
|
||||||
|
|
||||||
# Регистрируем обработчики сигналов
|
# Регистрируем обработчики сигналов
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
# Запускаем бота, мониторинг и метрики
|
# Запускаем бота (метрики запускаются внутри start_bot)
|
||||||
bot_task = asyncio.create_task(start_bot(bdf))
|
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:
|
try:
|
||||||
# Ждем сигнала завершения
|
# Ждем сигнала завершения
|
||||||
await shutdown_event.wait()
|
await shutdown_event.wait()
|
||||||
print("Начинаем корректное завершение...")
|
logger.info("Начинаем корректное завершение...")
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Получен сигнал завершения...")
|
logger.info("Получен сигнал завершения...")
|
||||||
finally:
|
finally:
|
||||||
print("Отправляем сообщение об отключении...")
|
logger.info("Останавливаем планировщик автоматического разбана...")
|
||||||
try:
|
|
||||||
# Отправляем сообщение об отключении
|
|
||||||
await monitor.send_shutdown_message()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка при отправке сообщения об отключении: {e}")
|
|
||||||
|
|
||||||
print("Останавливаем планировщик автоматического разбана...")
|
|
||||||
auto_unban_scheduler.stop_scheduler()
|
auto_unban_scheduler.stop_scheduler()
|
||||||
|
|
||||||
print("Останавливаем метрики...")
|
# Останавливаем планировщик метрик
|
||||||
await metrics_manager.stop()
|
|
||||||
|
|
||||||
print("Останавливаем задачи...")
|
|
||||||
# Отменяем задачи
|
|
||||||
bot_task.cancel()
|
|
||||||
monitor_task.cancel()
|
|
||||||
metrics_task.cancel()
|
|
||||||
|
|
||||||
# Ждем завершения задач
|
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
print(f"Ошибка при остановке задач: {e}")
|
logger.error(f"Ошибка при остановке планировщика метрик: {e}")
|
||||||
|
|
||||||
# Закрываем сессию бота
|
# Метрики останавливаются в main.py
|
||||||
await monitor_bot.session.close()
|
|
||||||
print("Бот корректно остановлен")
|
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__':
|
if __name__ == '__main__':
|
||||||
@@ -115,4 +155,13 @@ if __name__ == '__main__':
|
|||||||
try:
|
try:
|
||||||
loop.run_until_complete(main())
|
loop.run_until_complete(main())
|
||||||
finally:
|
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()
|
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.types import Message, User, Chat
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
from database.db import BotDB
|
from database.async_db import AsyncBotDB
|
||||||
|
|
||||||
# Импортируем моки в самом начале
|
# Импортируем моки в самом начале
|
||||||
import tests.mocks
|
import tests.mocks
|
||||||
@@ -58,15 +58,15 @@ def mock_state():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_db():
|
def mock_db():
|
||||||
"""Создает мок базы данных для тестов"""
|
"""Создает мок базы данных для тестов"""
|
||||||
db = Mock(spec=BotDB)
|
db = Mock(spec=AsyncBotDB)
|
||||||
db.user_exists = Mock(return_value=False)
|
db.user_exists = Mock(return_value=False)
|
||||||
db.add_new_user_in_db = Mock()
|
db.add_new_user = Mock()
|
||||||
db.update_date_for_user = Mock()
|
db.update_user_date = Mock()
|
||||||
db.update_username_and_full_name = Mock()
|
db.update_user_info = Mock()
|
||||||
db.add_post_in_db = 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.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"))
|
db.get_username_and_full_name = Mock(return_value=("testuser", "Test User"))
|
||||||
return db
|
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 = patch('os.getenv', side_effect=mock_getenv)
|
||||||
env_patcher.start()
|
env_patcher.start()
|
||||||
|
|
||||||
# Мокаем BotDB
|
# Мокаем AsyncBotDB
|
||||||
mock_db = Mock()
|
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()
|
db_patcher.start()
|
||||||
|
|
||||||
return env_patcher, db_patcher
|
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,19 +32,19 @@ class TestAutoUnbanIntegration:
|
|||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
user_name TEXT,
|
user_name TEXT,
|
||||||
message_for_user TEXT,
|
message_for_user TEXT,
|
||||||
date_to_unban TEXT
|
date_to_unban INTEGER
|
||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
# Добавляем тестовые данные
|
# Добавляем тестовые данные
|
||||||
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
|
today_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||||
tomorrow = (datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).strftime("%Y-%m-%d")
|
tomorrow_timestamp = int((datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).timestamp())
|
||||||
|
|
||||||
test_data = [
|
test_data = [
|
||||||
(123, "test_user1", "Test ban 1", today), # Разблокируется сегодня
|
(123, "test_user1", "Test ban 1", today_timestamp), # Разблокируется сегодня
|
||||||
(456, "test_user2", "Test ban 2", today), # Разблокируется сегодня
|
(456, "test_user2", "Test ban 2", today_timestamp), # Разблокируется сегодня
|
||||||
(789, "test_user3", "Test ban 3", tomorrow), # Разблокируется завтра
|
(789, "test_user3", "Test ban 3", tomorrow_timestamp), # Разблокируется завтра
|
||||||
(999, "test_user4", "Test ban 4", None), # Навсегда заблокирован
|
(999, "test_user4", "Test ban 4", None), # Навсегда заблокирован
|
||||||
]
|
]
|
||||||
|
|
||||||
cursor.executemany(
|
cursor.executemany(
|
||||||
@@ -73,10 +73,9 @@ class TestAutoUnbanIntegration:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Создаем реальный экземпляр базы данных с тестовым файлом
|
# Создаем реальный экземпляр базы данных с тестовым файлом
|
||||||
from database.db import BotDB
|
from database.async_db import AsyncBotDB
|
||||||
import os
|
import os
|
||||||
current_dir = os.getcwd()
|
mock_factory.database = AsyncBotDB(test_db_path)
|
||||||
mock_factory.database = BotDB(current_dir, test_db_path)
|
|
||||||
|
|
||||||
return mock_factory
|
return mock_factory
|
||||||
|
|
||||||
@@ -110,14 +109,15 @@ class TestAutoUnbanIntegration:
|
|||||||
await scheduler.auto_unban_users()
|
await scheduler.auto_unban_users()
|
||||||
|
|
||||||
# Проверяем, что пользователи с сегодняшней датой разблокированы
|
# Проверяем, что пользователи с сегодняшней датой разблокированы
|
||||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?",
|
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||||
(datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d"),))
|
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?",
|
||||||
|
(current_timestamp,))
|
||||||
today_count = cursor.fetchone()[0]
|
today_count = cursor.fetchone()[0]
|
||||||
assert today_count == 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 IS NOT NULL AND date_to_unban > ?",
|
||||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban = ?", (tomorrow,))
|
(current_timestamp,))
|
||||||
tomorrow_count = cursor.fetchone()[0]
|
tomorrow_count = cursor.fetchone()[0]
|
||||||
assert tomorrow_count == 1
|
assert tomorrow_count == 1
|
||||||
|
|
||||||
@@ -146,8 +146,8 @@ class TestAutoUnbanIntegration:
|
|||||||
# Удаляем пользователей с сегодняшней датой
|
# Удаляем пользователей с сегодняшней датой
|
||||||
conn = sqlite3.connect(setup_test_db)
|
conn = sqlite3.connect(setup_test_db)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
today = datetime.now(timezone(timedelta(hours=3))).strftime("%Y-%m-%d")
|
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||||
cursor.execute("DELETE FROM blacklist WHERE date_to_unban = ?", (today,))
|
cursor.execute("DELETE FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?", (current_timestamp,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ class TestAutoUnbanIntegration:
|
|||||||
scheduler = AutoUnbanScheduler()
|
scheduler = AutoUnbanScheduler()
|
||||||
scheduler.bot_db = mock_bdf.database
|
scheduler.bot_db = mock_bdf.database
|
||||||
|
|
||||||
# Проверяем, что дата в базе соответствует ожидаемому формату
|
# Проверяем, что дата в базе соответствует ожидаемому формату (timestamp)
|
||||||
conn = sqlite3.connect(setup_test_db)
|
conn = sqlite3.connect(setup_test_db)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute("SELECT date_to_unban FROM blacklist WHERE date_to_unban IS NOT NULL LIMIT 1")
|
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()
|
conn.close()
|
||||||
|
|
||||||
if result and result[0]:
|
if result and result[0]:
|
||||||
date_str = result[0]
|
timestamp = result[0]
|
||||||
# Проверяем формат YYYY-MM-DD
|
# Проверяем, что это валидный timestamp (целое число)
|
||||||
assert len(date_str) == 10
|
assert isinstance(timestamp, int)
|
||||||
assert date_str.count('-') == 2
|
assert timestamp > 0
|
||||||
assert date_str[:4].isdigit() # Год
|
# Проверяем, что timestamp можно преобразовать в дату
|
||||||
assert date_str[5:7].isdigit() # Месяц
|
date_obj = datetime.fromtimestamp(timestamp)
|
||||||
assert date_str[8:10].isdigit() # День
|
assert isinstance(date_obj, datetime)
|
||||||
|
|
||||||
|
|
||||||
class TestSchedulerLifecycle:
|
class TestSchedulerLifecycle:
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ class TestAutoUnbanScheduler:
|
|||||||
def mock_bot_db(self):
|
def mock_bot_db(self):
|
||||||
"""Создает мок базы данных"""
|
"""Создает мок базы данных"""
|
||||||
mock_db = Mock()
|
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",
|
123: "test_user1",
|
||||||
456: "test_user2"
|
456: "test_user2"
|
||||||
}
|
})
|
||||||
mock_db.delete_user_blacklist.return_value = True
|
mock_db.delete_user_blacklist = AsyncMock(return_value=True)
|
||||||
return mock_db
|
return mock_db
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -78,7 +78,7 @@ class TestAutoUnbanScheduler:
|
|||||||
"""Тест разбана когда нет пользователей для разблокировки"""
|
"""Тест разбана когда нет пользователей для разблокировки"""
|
||||||
# Настройка моков
|
# Настройка моков
|
||||||
mock_get_instance.return_value = mock_bdf
|
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.bot_db = mock_bot_db
|
||||||
scheduler.set_bot(mock_bot)
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
@@ -96,12 +96,12 @@ class TestAutoUnbanScheduler:
|
|||||||
"""Тест разбана с частичными ошибками"""
|
"""Тест разбана с частичными ошибками"""
|
||||||
# Настройка моков
|
# Настройка моков
|
||||||
mock_get_instance.return_value = mock_bdf
|
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",
|
123: "test_user1",
|
||||||
456: "test_user2"
|
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.bot_db = mock_bot_db
|
||||||
scheduler.set_bot(mock_bot)
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ class TestAutoUnbanScheduler:
|
|||||||
"""Тест разбана с исключением"""
|
"""Тест разбана с исключением"""
|
||||||
# Настройка моков
|
# Настройка моков
|
||||||
mock_get_instance.return_value = mock_bdf
|
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.bot_db = mock_bot_db
|
||||||
scheduler.set_bot(mock_bot)
|
scheduler.set_bot(mock_bot)
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ class TestAutoUnbanScheduler:
|
|||||||
assert "Отчет об автоматическом разбане" in report
|
assert "Отчет об автоматическом разбане" in report
|
||||||
assert "Успешно разблокировано: 1" in report
|
assert "Успешно разблокировано: 1" 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
|
assert "456 (test_user2)" in report
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -268,8 +268,8 @@ class TestAsyncOperations:
|
|||||||
mock_get_instance.return_value = mock_bdf
|
mock_get_instance.return_value = mock_bdf
|
||||||
|
|
||||||
mock_bot_db = Mock()
|
mock_bot_db = Mock()
|
||||||
mock_bot_db.get_users_for_unblock_today.return_value = {123: "test_user"}
|
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={123: "test_user"})
|
||||||
mock_bot_db.delete_user_blacklist.return_value = True
|
mock_bot_db.delete_user_blacklist = AsyncMock(return_value=True)
|
||||||
|
|
||||||
mock_bot = Mock()
|
mock_bot = Mock()
|
||||||
mock_bot.send_message = AsyncMock()
|
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
|
import pytest
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch, AsyncMock
|
||||||
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
|
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
from helper_bot.keyboards.keyboards import (
|
from helper_bot.keyboards.keyboards import (
|
||||||
@@ -10,7 +10,7 @@ from helper_bot.keyboards.keyboards import (
|
|||||||
create_keyboard_with_pagination
|
create_keyboard_with_pagination
|
||||||
)
|
)
|
||||||
from helper_bot.filters.main import ChatTypeFilter
|
from helper_bot.filters.main import ChatTypeFilter
|
||||||
from database.db import BotDB
|
from database.async_db import AsyncBotDB
|
||||||
|
|
||||||
|
|
||||||
class TestKeyboards:
|
class TestKeyboards:
|
||||||
@@ -19,18 +19,19 @@ class TestKeyboards:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_db(self):
|
def mock_db(self):
|
||||||
"""Создает мок базы данных"""
|
"""Создает мок базы данных"""
|
||||||
db = Mock(spec=BotDB)
|
db = Mock(spec=AsyncBotDB)
|
||||||
db.get_user_info = Mock(return_value={
|
db.get_user_info = Mock(return_value={
|
||||||
'stickers': True,
|
'stickers': True,
|
||||||
'admin': False
|
'admin': False
|
||||||
})
|
})
|
||||||
return db
|
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
|
user_id = 123456
|
||||||
|
|
||||||
keyboard = get_reply_keyboard(mock_db, user_id)
|
keyboard = await get_reply_keyboard(mock_db, user_id)
|
||||||
|
|
||||||
# Проверяем, что возвращается клавиатура
|
# Проверяем, что возвращается клавиатура
|
||||||
assert isinstance(keyboard, ReplyKeyboardMarkup)
|
assert isinstance(keyboard, ReplyKeyboardMarkup)
|
||||||
@@ -52,13 +53,14 @@ class TestKeyboards:
|
|||||||
assert '👋🏼Сказать пока!' in all_buttons
|
assert '👋🏼Сказать пока!' in all_buttons
|
||||||
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
|
user_id = 123456
|
||||||
# Мокаем метод get_info_about_stickers
|
# Мокаем метод get_stickers_info
|
||||||
mock_db.get_info_about_stickers = Mock(return_value=False)
|
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 = []
|
all_buttons = []
|
||||||
for row in keyboard.keyboard:
|
for row in keyboard.keyboard:
|
||||||
@@ -68,13 +70,14 @@ class TestKeyboards:
|
|||||||
# Проверяем наличие кнопки стикеров
|
# Проверяем наличие кнопки стикеров
|
||||||
assert '🤪Хочу стикеры' in all_buttons
|
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
|
user_id = 123456
|
||||||
# Мокаем метод get_info_about_stickers
|
# Мокаем метод get_stickers_info
|
||||||
mock_db.get_info_about_stickers = Mock(return_value=True)
|
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 = []
|
all_buttons = []
|
||||||
for row in keyboard.keyboard:
|
for row in keyboard.keyboard:
|
||||||
@@ -84,13 +87,14 @@ class TestKeyboards:
|
|||||||
# Проверяем отсутствие кнопки стикеров
|
# Проверяем отсутствие кнопки стикеров
|
||||||
assert '🤪Хочу стикеры' not in all_buttons
|
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
|
user_id = 123456
|
||||||
# Мокаем метод get_info_about_stickers
|
# Мокаем метод get_stickers_info
|
||||||
mock_db.get_info_about_stickers = Mock(return_value=False)
|
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 = []
|
all_buttons = []
|
||||||
for row in keyboard.keyboard:
|
for row in keyboard.keyboard:
|
||||||
@@ -282,44 +286,41 @@ class TestChatTypeFilter:
|
|||||||
class TestKeyboardIntegration:
|
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 = Mock(spec=AsyncBotDB)
|
||||||
mock_db.get_info_about_stickers = Mock(return_value=False)
|
mock_db.get_stickers_info = AsyncMock(return_value=False)
|
||||||
|
|
||||||
# Тестируем все типы клавиатур
|
# Тестируем все типы клавиатур
|
||||||
keyboards = [
|
keyboard1 = await get_reply_keyboard(mock_db, 123456)
|
||||||
get_reply_keyboard(mock_db, 123456),
|
keyboard2 = get_reply_keyboard_for_post()
|
||||||
get_reply_keyboard_for_post(),
|
keyboard3 = get_reply_keyboard_leave_chat()
|
||||||
get_reply_keyboard_leave_chat()
|
|
||||||
]
|
|
||||||
|
|
||||||
# Проверяем первую клавиатуру (ReplyKeyboardMarkup)
|
# Проверяем первую клавиатуру (ReplyKeyboardMarkup)
|
||||||
keyboard1 = keyboards[0]
|
|
||||||
assert isinstance(keyboard1, ReplyKeyboardMarkup)
|
assert isinstance(keyboard1, ReplyKeyboardMarkup)
|
||||||
assert hasattr(keyboard1, 'keyboard')
|
assert hasattr(keyboard1, 'keyboard')
|
||||||
assert isinstance(keyboard1.keyboard, list)
|
assert isinstance(keyboard1.keyboard, list)
|
||||||
|
|
||||||
# Проверяем вторую клавиатуру (InlineKeyboardMarkup)
|
# Проверяем вторую клавиатуру (InlineKeyboardMarkup)
|
||||||
keyboard2 = keyboards[1]
|
|
||||||
assert isinstance(keyboard2, InlineKeyboardMarkup)
|
assert isinstance(keyboard2, InlineKeyboardMarkup)
|
||||||
assert hasattr(keyboard2, 'inline_keyboard')
|
assert hasattr(keyboard2, 'inline_keyboard')
|
||||||
assert isinstance(keyboard2.inline_keyboard, list)
|
assert isinstance(keyboard2.inline_keyboard, list)
|
||||||
|
|
||||||
# Проверяем третью клавиатуру (ReplyKeyboardMarkup)
|
# Проверяем третью клавиатуру (ReplyKeyboardMarkup)
|
||||||
keyboard3 = keyboards[2]
|
|
||||||
assert isinstance(keyboard3, ReplyKeyboardMarkup)
|
assert isinstance(keyboard3, ReplyKeyboardMarkup)
|
||||||
assert hasattr(keyboard3, 'keyboard')
|
assert hasattr(keyboard3, 'keyboard')
|
||||||
assert isinstance(keyboard3.keyboard, list)
|
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 = Mock(spec=AsyncBotDB)
|
||||||
db.get_info_about_stickers = Mock(return_value=False)
|
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()
|
post_keyboard = get_reply_keyboard_for_post()
|
||||||
leave_keyboard = get_reply_keyboard_leave_chat()
|
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.mock_db = Mock()
|
||||||
self.admin_service = AdminService(self.mock_db)
|
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
|
# Arrange
|
||||||
# Формат данных: кортежи (full_name, user_id) как возвращает БД
|
# Формат данных: кортежи (full_name, user_id) как возвращает БД
|
||||||
@@ -27,10 +28,10 @@ class TestAdminService:
|
|||||||
('User One', 1), # (full_name, user_id)
|
('User One', 1), # (full_name, user_id)
|
||||||
('User Two', 2) # (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
|
# Act
|
||||||
result = self.admin_service.get_last_users()
|
result = await self.admin_service.get_last_users()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
@@ -41,17 +42,18 @@ class TestAdminService:
|
|||||||
assert result[1].username == 'Неизвестно' # username не возвращается из БД
|
assert result[1].username == 'Неизвестно' # username не возвращается из БД
|
||||||
assert result[1].full_name == 'User Two'
|
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"""
|
"""Тест успешного получения пользователя по username"""
|
||||||
# Arrange
|
# Arrange
|
||||||
user_id = 123
|
user_id = 123
|
||||||
username = "test_user"
|
username = "test_user"
|
||||||
full_name = "Test User"
|
full_name = "Test User"
|
||||||
self.mock_db.get_user_id_by_username.return_value = user_id
|
self.mock_db.get_user_id_by_username = AsyncMock(return_value=user_id)
|
||||||
self.mock_db.get_full_name_by_id.return_value = full_name
|
self.mock_db.get_full_name_by_id = AsyncMock(return_value=full_name)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
result = self.admin_service.get_user_by_username(username)
|
result = await self.admin_service.get_user_by_username(username)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result is not None
|
assert result is not None
|
||||||
@@ -59,27 +61,35 @@ class TestAdminService:
|
|||||||
assert result.username == username
|
assert result.username == username
|
||||||
assert result.full_name == full_name
|
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"""
|
"""Тест получения пользователя по несуществующему username"""
|
||||||
# Arrange
|
# Arrange
|
||||||
username = "nonexistent_user"
|
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
|
# Act
|
||||||
result = self.admin_service.get_user_by_username(username)
|
result = await self.admin_service.get_user_by_username(username)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result is None
|
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"""
|
"""Тест успешного получения пользователя по ID"""
|
||||||
# Arrange
|
# Arrange
|
||||||
user_id = 123
|
user_id = 123
|
||||||
user_info = {'username': 'test_user', 'full_name': 'Test User'}
|
from database.models import User as DBUser
|
||||||
self.mock_db.get_user_info_by_id.return_value = user_info
|
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
|
# Act
|
||||||
result = self.admin_service.get_user_by_id(user_id)
|
result = await self.admin_service.get_user_by_id(user_id)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result is not None
|
assert result is not None
|
||||||
@@ -87,45 +97,51 @@ class TestAdminService:
|
|||||||
assert result.username == 'test_user'
|
assert result.username == 'test_user'
|
||||||
assert result.full_name == '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"""
|
"""Тест получения пользователя по несуществующему ID"""
|
||||||
# Arrange
|
# Arrange
|
||||||
user_id = 999
|
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
|
# Act
|
||||||
result = self.admin_service.get_user_by_id(user_id)
|
result = await self.admin_service.get_user_by_id(user_id)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
def test_validate_user_input_success(self):
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_user_input_success(self):
|
||||||
"""Тест успешной валидации ID пользователя"""
|
"""Тест успешной валидации ID пользователя"""
|
||||||
# Act
|
# Act
|
||||||
result = self.admin_service.validate_user_input("123")
|
result = await self.admin_service.validate_user_input("123")
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result == 123
|
assert result == 123
|
||||||
|
|
||||||
def test_validate_user_input_invalid_number(self):
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_user_input_invalid_number(self):
|
||||||
"""Тест валидации некорректного ID"""
|
"""Тест валидации некорректного ID"""
|
||||||
# Act & Assert
|
# Act & Assert
|
||||||
with pytest.raises(InvalidInputError, match="ID пользователя должен быть числом"):
|
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"""
|
"""Тест валидации отрицательного ID"""
|
||||||
# Act & Assert
|
# Act & Assert
|
||||||
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
|
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"""
|
"""Тест валидации нулевого ID"""
|
||||||
# Act & Assert
|
# Act & Assert
|
||||||
with pytest.raises(InvalidInputError, match="ID пользователя должен быть положительным числом"):
|
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
|
# Arrange
|
||||||
user_id = 123
|
user_id = 123
|
||||||
@@ -133,17 +149,18 @@ class TestAdminService:
|
|||||||
reason = "Test ban"
|
reason = "Test ban"
|
||||||
ban_days = 7
|
ban_days = 7
|
||||||
|
|
||||||
self.mock_db.check_user_in_blacklist.return_value = False
|
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
|
||||||
self.mock_db.set_user_blacklist.return_value = None
|
self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
|
||||||
|
|
||||||
# Act
|
# 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
|
# Assert
|
||||||
self.mock_db.check_user_in_blacklist.assert_called_once_with(user_id)
|
self.mock_db.check_user_in_blacklist.assert_called_once_with(user_id)
|
||||||
self.mock_db.set_user_blacklist.assert_called_once()
|
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
|
# Arrange
|
||||||
user_id = 123
|
user_id = 123
|
||||||
@@ -151,13 +168,14 @@ class TestAdminService:
|
|||||||
reason = "Test ban"
|
reason = "Test ban"
|
||||||
ban_days = 7
|
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
|
# Act & Assert
|
||||||
with pytest.raises(UserAlreadyBannedError, match=f"Пользователь {user_id} уже заблокирован"):
|
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
|
# Arrange
|
||||||
user_id = 123
|
user_id = 123
|
||||||
@@ -165,23 +183,24 @@ class TestAdminService:
|
|||||||
reason = "Permanent ban"
|
reason = "Permanent ban"
|
||||||
ban_days = None
|
ban_days = None
|
||||||
|
|
||||||
self.mock_db.check_user_in_blacklist.return_value = False
|
self.mock_db.check_user_in_blacklist = AsyncMock(return_value=False)
|
||||||
self.mock_db.set_user_blacklist.return_value = None
|
self.mock_db.set_user_blacklist = AsyncMock(return_value=None)
|
||||||
|
|
||||||
# Act
|
# 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
|
# 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
|
# Arrange
|
||||||
user_id = 123
|
user_id = 123
|
||||||
self.mock_db.delete_user_blacklist.return_value = None
|
self.mock_db.delete_user_blacklist = AsyncMock(return_value=None)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.admin_service.unban_user(user_id)
|
await self.admin_service.unban_user(user_id)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.mock_db.delete_user_blacklist.assert_called_once_with(user_id)
|
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.admin_reply_service is not None
|
||||||
assert handlers.router 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):
|
async def test_handle_message_success(self, mock_db, mock_keyboard_markup, mock_reply_message, mock_state):
|
||||||
"""Test successful message handling"""
|
"""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)
|
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ class TestGroupHandlers:
|
|||||||
# Verify state was set
|
# Verify state was set
|
||||||
mock_state.set_state.assert_called_once_with(FSM_STATES["CHAT"])
|
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):
|
async def test_handle_message_no_reply(self, mock_db, mock_keyboard_markup, mock_message, mock_state):
|
||||||
"""Test message handling without reply"""
|
"""Test message handling without reply"""
|
||||||
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||||
@@ -121,9 +123,10 @@ class TestGroupHandlers:
|
|||||||
# Verify state was not set
|
# Verify state was not set
|
||||||
mock_state.set_state.assert_not_called()
|
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):
|
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"""
|
"""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)
|
handlers = create_group_handlers(mock_db, mock_keyboard_markup)
|
||||||
|
|
||||||
@@ -154,24 +157,27 @@ class TestAdminReplyService:
|
|||||||
"""Create service instance"""
|
"""Create service instance"""
|
||||||
return AdminReplyService(mock_db)
|
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"""
|
"""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
|
assert result == 12345
|
||||||
mock_db.get_user_by_message_id.assert_called_once_with(111)
|
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"""
|
"""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"):
|
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)
|
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):
|
async def test_send_reply_to_user(self, service, mock_db):
|
||||||
"""Test sending reply to user"""
|
"""Test sending reply to user"""
|
||||||
message = Mock()
|
message = Mock()
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ class TestPrivateHandlers:
|
|||||||
def mock_db(self):
|
def mock_db(self):
|
||||||
"""Mock database"""
|
"""Mock database"""
|
||||||
db = Mock()
|
db = Mock()
|
||||||
db.user_exists.return_value = False
|
db.user_exists = AsyncMock(return_value=False)
|
||||||
db.add_new_user_in_db = Mock()
|
db.add_user = AsyncMock()
|
||||||
db.update_date_for_user = Mock()
|
db.update_user_date = AsyncMock()
|
||||||
db.update_info_about_stickers = Mock()
|
db.update_stickers_info = AsyncMock()
|
||||||
db.add_post_in_db = Mock()
|
db.add_post = AsyncMock()
|
||||||
db.add_new_message_in_db = Mock()
|
db.add_message = AsyncMock()
|
||||||
db.update_helper_message_in_db = Mock()
|
db.update_helper_message = AsyncMock()
|
||||||
return db
|
return db
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -101,7 +101,8 @@ class TestPrivateHandlers:
|
|||||||
|
|
||||||
# Mock the check_user_emoji function
|
# Mock the check_user_emoji function
|
||||||
with pytest.MonkeyPatch().context() as m:
|
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
|
# Test the handler
|
||||||
await handlers.handle_emoji_message(mock_message, mock_state)
|
await handlers.handle_emoji_message(mock_message, mock_state)
|
||||||
@@ -121,7 +122,8 @@ class TestPrivateHandlers:
|
|||||||
with pytest.MonkeyPatch().context() as m:
|
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.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.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
|
# Test the handler
|
||||||
await handlers.handle_start_message(mock_message, mock_state)
|
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"])
|
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||||
|
|
||||||
# Verify user was ensured to exist
|
# Verify user was ensured to exist
|
||||||
mock_db.add_new_user_in_db.assert_called_once()
|
mock_db.add_user.assert_called_once()
|
||||||
mock_db.update_date_for_user.assert_called_once()
|
mock_db.update_user_date.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class TestBotSettings:
|
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.messages import get_message
|
||||||
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
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
|
import helper_bot.utils.messages as messages # Import for patching constants
|
||||||
|
|
||||||
class TestHelperFunctions:
|
class TestHelperFunctions:
|
||||||
@@ -83,25 +83,27 @@ class TestHelperFunctions:
|
|||||||
assert "testuser" in result
|
assert "testuser" in result
|
||||||
assert "Обычный текст без специальных слов" 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"""
|
"""Тест функции проверки изменений username и full_name"""
|
||||||
# Создаем мок базы данных
|
# Создаем мок базы данных
|
||||||
mock_db = Mock(spec=BotDB)
|
mock_db = Mock(spec=AsyncBotDB)
|
||||||
mock_db.get_username_and_full_name = Mock(return_value=("olduser", "Old User"))
|
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
|
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
|
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
|
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
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
@@ -286,15 +288,20 @@ class TestDownloadFile:
|
|||||||
# Мокаем download_file
|
# Мокаем download_file
|
||||||
mock_message.bot.download_file = AsyncMock()
|
mock_message.bot.download_file = AsyncMock()
|
||||||
|
|
||||||
# Мокаем os.makedirs
|
# Мокаем os.makedirs и другие зависимости
|
||||||
with patch('os.makedirs') as mock_makedirs:
|
with patch('os.makedirs') as mock_makedirs:
|
||||||
with patch('os.path.join', return_value="files/photos/file_123.jpg"):
|
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):
|
||||||
assert result == "files/photos/file_123.jpg"
|
with patch('os.path.basename', return_value='file_123.jpg'):
|
||||||
mock_makedirs.assert_called()
|
with patch('os.path.splitext', return_value=('file_123', '.jpg')):
|
||||||
mock_message.bot.get_file.assert_called_once_with("file_id_123")
|
with patch('helper_bot.utils.helper_func.metrics') as mock_metrics:
|
||||||
mock_message.bot.download_file.assert_called_once()
|
result = await download_file(mock_message, "file_id_123", "photo")
|
||||||
|
|
||||||
|
assert result == "files/photos/file_123.jpg"
|
||||||
|
mock_makedirs.assert_called()
|
||||||
|
mock_message.bot.get_file.assert_called_once_with("file_id_123")
|
||||||
|
mock_message.bot.download_file.assert_called_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_download_file_exception(self):
|
async def test_download_file_exception(self):
|
||||||
@@ -330,7 +337,7 @@ class TestPrepareMediaGroup:
|
|||||||
assert result[0].media == "photo_0"
|
assert result[0].media == "photo_0"
|
||||||
assert result[1].media == "photo_1"
|
assert result[1].media == "photo_1"
|
||||||
assert result[2].media == "photo_2"
|
assert result[2].media == "photo_2"
|
||||||
assert result[2].caption == "Тестовая подпись"
|
assert result[0].caption == "Тестовая подпись" # Первое фото должно иметь caption
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_prepare_media_group_mixed_types(self):
|
async def test_prepare_media_group_mixed_types(self):
|
||||||
@@ -364,7 +371,7 @@ class TestPrepareMediaGroup:
|
|||||||
assert result[0].media == "photo_1"
|
assert result[0].media == "photo_1"
|
||||||
assert result[1].media == "video_1"
|
assert result[1].media == "video_1"
|
||||||
assert result[2].media == "audio_1"
|
assert result[2].media == "audio_1"
|
||||||
assert result[2].caption == "Смешанная группа"
|
assert result[0].caption == "Смешанная группа" # Первое медиа должно иметь caption
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_prepare_media_group_empty_album(self):
|
async def test_prepare_media_group_empty_album(self):
|
||||||
@@ -381,6 +388,7 @@ class TestPrepareMediaGroup:
|
|||||||
message.photo = None
|
message.photo = None
|
||||||
message.video = None
|
message.video = None
|
||||||
message.audio = None
|
message.audio = None
|
||||||
|
message.document = None # Добавляем document = None
|
||||||
album.append(message)
|
album.append(message)
|
||||||
|
|
||||||
result = await prepare_media_group_from_middlewares(album, "Тест")
|
result = await prepare_media_group_from_middlewares(album, "Тест")
|
||||||
@@ -401,12 +409,12 @@ class TestMediaDatabaseOperations:
|
|||||||
message.photo[-1].file_id = f"photo_{i}"
|
message.photo[-1].file_id = f"photo_{i}"
|
||||||
sent_message.append(message)
|
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"):
|
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)
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_add_in_db_media_photo(self):
|
async def test_add_in_db_media_photo(self):
|
||||||
@@ -416,12 +424,12 @@ class TestMediaDatabaseOperations:
|
|||||||
mock_message.photo = [Mock()]
|
mock_message.photo = [Mock()]
|
||||||
mock_message.photo[-1].file_id = "photo_123"
|
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"):
|
with patch('helper_bot.utils.helper_func.download_file', return_value="files/photo_123.jpg"):
|
||||||
await add_in_db_media(mock_message, mock_db)
|
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'
|
123, 123, "files/photo_123.jpg", 'photo'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -434,12 +442,12 @@ class TestMediaDatabaseOperations:
|
|||||||
mock_message.video = Mock()
|
mock_message.video = Mock()
|
||||||
mock_message.video.file_id = "video_123"
|
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"):
|
with patch('helper_bot.utils.helper_func.download_file', return_value="files/video_123.mp4"):
|
||||||
await add_in_db_media(mock_message, mock_db)
|
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'
|
123, 123, "files/video_123.mp4", 'video'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -453,12 +461,12 @@ class TestMediaDatabaseOperations:
|
|||||||
mock_message.voice = Mock()
|
mock_message.voice = Mock()
|
||||||
mock_message.voice.file_id = "voice_123"
|
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"):
|
with patch('helper_bot.utils.helper_func.download_file', return_value="files/voice_123.ogg"):
|
||||||
await add_in_db_media(mock_message, mock_db)
|
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'
|
123, 123, "files/voice_123.ogg", 'voice'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -548,16 +556,17 @@ class TestSendMessageFunctions:
|
|||||||
class TestUtilityFunctions:
|
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
|
mock_db.is_admin.return_value = True
|
||||||
|
|
||||||
result = check_access(123, mock_db)
|
result = await check_access(123, mock_db)
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
mock_db.is_admin.return_value = False
|
mock_db.is_admin.return_value = False
|
||||||
result = check_access(123, mock_db)
|
result = await check_access(123, mock_db)
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
def test_add_days_to_date(self):
|
def test_add_days_to_date(self):
|
||||||
@@ -569,45 +578,51 @@ class TestUtilityFunctions:
|
|||||||
mock_datetime.timedelta = timedelta
|
mock_datetime.timedelta = timedelta
|
||||||
|
|
||||||
result = add_days_to_date(5)
|
result = add_days_to_date(5)
|
||||||
expected_date = (mock_now + timedelta(days=5)).strftime("%d-%m-%Y")
|
expected_timestamp = int((mock_now + timedelta(days=5)).timestamp())
|
||||||
assert result == expected_date
|
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 = [
|
mock_db.get_banned_users_from_db_with_limits.return_value = [
|
||||||
("User1", 123, "Spam", "01-01-2025"),
|
(123, "Spam", 1704067200), # user_id, ban_reason, unban_date (timestamp)
|
||||||
("User2", 456, "Violation", "02-01-2025")
|
(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 "Список заблокированных пользователей:" in result
|
||||||
assert "User1" in result
|
assert "Test User" in result
|
||||||
assert "User2" in result
|
|
||||||
assert "Spam" in result
|
assert "Spam" in result
|
||||||
assert "Violation" 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 = [
|
mock_db.get_banned_users_from_db.return_value = [
|
||||||
("User1", 123),
|
(123, "Spam", 1704067200), # user_id, ban_reason, unban_date
|
||||||
("User2", 456)
|
(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 len(result) == 2
|
||||||
assert result[0] == ("User1", 123)
|
assert result[0] == ("Test User", 123)
|
||||||
assert result[1] == ("User2", 456)
|
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
|
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
|
assert result is True
|
||||||
|
|
||||||
mock_db.delete_user_blacklist.assert_called_once_with(user_id=123)
|
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_first_name', return_value="Test"):
|
||||||
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
|
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
|
||||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
||||||
mock_bot_db.user_exists.return_value = False
|
mock_bot_db.user_exists = AsyncMock(return_value=False)
|
||||||
mock_bot_db.add_new_user_in_db = Mock()
|
mock_bot_db.add_user = AsyncMock()
|
||||||
mock_bot_db.update_date_for_user = Mock()
|
mock_bot_db.update_user_date = AsyncMock()
|
||||||
|
|
||||||
await update_user_info("test", mock_message)
|
await update_user_info("test", mock_message)
|
||||||
|
|
||||||
mock_bot_db.add_new_user_in_db.assert_called_once()
|
mock_bot_db.add_user.assert_called_once()
|
||||||
mock_bot_db.update_date_for_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 = Mock()
|
||||||
mock_message.from_user.id = 123
|
mock_message.from_user.id = 123
|
||||||
|
|
||||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
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 == "😀"
|
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 = Mock()
|
||||||
mock_message.from_user.id = 123
|
mock_message.from_user.id = 123
|
||||||
|
|
||||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
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.get_user_emoji = AsyncMock(return_value=None)
|
||||||
mock_bot_db.update_emoji_for_user = Mock()
|
mock_bot_db.update_user_emoji = AsyncMock()
|
||||||
|
|
||||||
with patch('helper_bot.utils.helper_func.get_random_emoji', return_value="😀"):
|
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 == "😀"
|
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:
|
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="😀"):
|
with patch('helper_bot.utils.helper_func.random.choice', return_value="😀"):
|
||||||
result = get_random_emoji()
|
result = await get_random_emoji()
|
||||||
assert result == "😀"
|
assert result == "😀"
|
||||||
|
|
||||||
def test_get_random_emoji_fallback(self):
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_random_emoji_fallback(self):
|
||||||
"""Тест получения случайного эмодзи (fallback)"""
|
"""Тест получения случайного эмодзи (fallback)"""
|
||||||
with patch('helper_bot.utils.helper_func.BotDB') as mock_bot_db:
|
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.random.choice', return_value="😀"):
|
||||||
with patch('helper_bot.utils.helper_func.logger') as mock_logger:
|
with patch('helper_bot.utils.helper_func.logger') as mock_logger:
|
||||||
result = get_random_emoji()
|
result = await get_random_emoji()
|
||||||
assert result == "Эмоджи не определен"
|
assert result == "Эмоджи не определен"
|
||||||
mock_logger.error.assert_called_once()
|
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