Переписал почти все тесты #16
94
.github/workflows/ci.yml
vendored
Normal file
94
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: CI pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'dev-*', 'feature-*' ]
|
||||
pull_request:
|
||||
branches: [ 'dev-*', 'feature-*', 'main' ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test & Code Quality
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
- name: Code style check (isort + Black, one order — no conflict)
|
||||
run: |
|
||||
echo "🔍 Applying isort then black (pyproject.toml: isort profile=black)..."
|
||||
python -m isort .
|
||||
python -m black .
|
||||
echo "🔍 Checking that repo is already formatted (no diff after isort+black)..."
|
||||
git diff --exit-code || (
|
||||
echo "❌ Code style drift. Locally run: isort . && black . && git add -A && git commit -m 'style: isort + black'"
|
||||
exit 1
|
||||
)
|
||||
|
||||
- name: Linting (flake8) - Critical errors
|
||||
run: |
|
||||
echo "🔍 Running flake8 linter (critical errors only)..."
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics || true
|
||||
|
||||
- name: Linting (flake8) - Warnings
|
||||
run: |
|
||||
echo "🔍 Running flake8 linter (warnings)..."
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics || true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "🧪 Running tests..."
|
||||
python -m pytest tests/ -v --tb=short
|
||||
|
||||
- name: Send test success notification
|
||||
if: success()
|
||||
uses: appleboy/telegram-action@v1.0.0
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
message: |
|
||||
✅ CI Tests Passed
|
||||
|
||||
📦 Repository: telegram-helper-bot
|
||||
🌿 Branch: ${{ github.ref_name }}
|
||||
📝 Commit: ${{ github.sha }}
|
||||
👤 Author: ${{ github.actor }}
|
||||
|
||||
✅ All tests passed! Code quality checks completed successfully.
|
||||
|
||||
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Send test failure notification
|
||||
if: failure()
|
||||
uses: appleboy/telegram-action@v1.0.0
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
message: |
|
||||
❌ CI Tests Failed
|
||||
|
||||
📦 Repository: telegram-helper-bot
|
||||
🌿 Branch: ${{ github.ref_name }}
|
||||
📝 Commit: ${{ github.sha }}
|
||||
👤 Author: ${{ github.actor }}
|
||||
|
||||
❌ Tests failed! Deployment blocked. Please fix the issues and try again.
|
||||
|
||||
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
continue-on-error: true
|
||||
357
.github/workflows/deploy.yml
vendored
Normal file
357
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,357 @@
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- deploy
|
||||
- rollback
|
||||
rollback_commit:
|
||||
description: 'Commit hash to rollback to (optional, uses last successful if empty)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy to Production
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'deploy')
|
||||
concurrency:
|
||||
group: production-deploy-telegram-helper-bot
|
||||
cancel-in-progress: false
|
||||
environment:
|
||||
name: production
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }}
|
||||
username: ${{ vars.SERVER_USER || secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }}
|
||||
script: |
|
||||
set -e
|
||||
export TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}"
|
||||
export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}"
|
||||
|
||||
echo "🚀 Starting deployment to production..."
|
||||
|
||||
cd /home/prod
|
||||
|
||||
# Сохраняем информацию о коммите
|
||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||
COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" || echo "Unknown")
|
||||
COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an" || echo "Unknown")
|
||||
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
echo "📝 Current commit: $CURRENT_COMMIT"
|
||||
echo "📝 Commit message: $COMMIT_MESSAGE"
|
||||
echo "📝 Author: $COMMIT_AUTHOR"
|
||||
|
||||
# Записываем в историю деплоев
|
||||
HISTORY_FILE="/home/prod/.deploy_history_telegram_helper_bot.txt"
|
||||
HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}"
|
||||
echo "${TIMESTAMP}|${CURRENT_COMMIT}|${COMMIT_MESSAGE}|${COMMIT_AUTHOR}|deploying" >> "$HISTORY_FILE"
|
||||
tail -n "$HISTORY_SIZE" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" && mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
|
||||
|
||||
# Обновляем код
|
||||
echo "📥 Pulling latest changes from main..."
|
||||
sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true
|
||||
cd /home/prod/bots/telegram-helper-bot
|
||||
git fetch origin main
|
||||
git reset --hard origin/main
|
||||
sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true
|
||||
|
||||
NEW_COMMIT=$(git rev-parse HEAD)
|
||||
echo "✅ Code updated: $CURRENT_COMMIT → $NEW_COMMIT"
|
||||
|
||||
# Применяем миграции БД перед перезапуском контейнера
|
||||
echo "🔄 Applying database migrations..."
|
||||
DB_PATH="/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db"
|
||||
|
||||
if [ -f "$DB_PATH" ]; then
|
||||
cd /home/prod/bots/telegram-helper-bot
|
||||
python3 scripts/apply_migrations.py --db "$DB_PATH" || {
|
||||
echo "❌ Ошибка при применении миграций!"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Миграции применены успешно"
|
||||
else
|
||||
echo "⚠️ База данных не найдена, пропускаем миграции (будет создана при первом запуске)"
|
||||
fi
|
||||
|
||||
# Валидация docker-compose
|
||||
echo "🔍 Validating docker-compose configuration..."
|
||||
cd /home/prod
|
||||
docker-compose config > /dev/null || exit 1
|
||||
echo "✅ docker-compose.yml is valid"
|
||||
|
||||
# Проверка дискового пространства
|
||||
MIN_FREE_GB=5
|
||||
AVAILABLE_SPACE=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0")
|
||||
echo "💾 Available disk space: ${AVAILABLE_SPACE}GB"
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$MIN_FREE_GB" ]; then
|
||||
echo "⚠️ Insufficient disk space! Cleaning up Docker resources..."
|
||||
docker system prune -f --volumes || true
|
||||
fi
|
||||
|
||||
# Пересобираем и перезапускаем контейнер бота
|
||||
echo "🔨 Rebuilding and restarting telegram-bot container..."
|
||||
cd /home/prod
|
||||
|
||||
export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN
|
||||
docker-compose stop telegram-bot || true
|
||||
docker-compose build --pull telegram-bot
|
||||
docker-compose up -d telegram-bot
|
||||
|
||||
echo "✅ Telegram bot container rebuilt and started"
|
||||
|
||||
# Ждем немного и проверяем healthcheck
|
||||
echo "⏳ Waiting for container to start..."
|
||||
sleep 10
|
||||
|
||||
if docker ps | grep -q bots_telegram_bot; then
|
||||
echo "✅ Container is running"
|
||||
else
|
||||
echo "❌ Container failed to start!"
|
||||
docker logs bots_telegram_bot --tail 50 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Update deploy history
|
||||
if: always()
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }}
|
||||
username: ${{ vars.SERVER_USER || secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }}
|
||||
script: |
|
||||
HISTORY_FILE="/home/prod/.deploy_history_telegram_helper_bot.txt"
|
||||
|
||||
if [ -f "$HISTORY_FILE" ]; then
|
||||
DEPLOY_STATUS="failed"
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
DEPLOY_STATUS="success"
|
||||
fi
|
||||
|
||||
sed -i '$s/|deploying$/|'"$DEPLOY_STATUS"'/' "$HISTORY_FILE"
|
||||
echo "✅ Deploy history updated: $DEPLOY_STATUS"
|
||||
fi
|
||||
|
||||
- name: Send deployment notification
|
||||
if: always()
|
||||
uses: appleboy/telegram-action@v1.0.0
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
message: |
|
||||
${{ job.status == 'success' && '✅' || '❌' }} Deployment: ${{ job.status }}
|
||||
|
||||
📦 Repository: telegram-helper-bot
|
||||
🌿 Branch: main
|
||||
📝 Commit: ${{ github.sha }}
|
||||
👤 Author: ${{ github.actor }}
|
||||
|
||||
${{ job.status == 'success' && '✅ Deployment successful! Container restarted with migrations applied.' || '❌ Deployment failed! Check logs for details.' }}
|
||||
|
||||
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
continue-on-error: true
|
||||
|
||||
- name: Get PR body from merged PR
|
||||
if: job.status == 'success' && github.event_name == 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "🔍 Searching for merged PR associated with commit ${{ github.sha }}..."
|
||||
|
||||
# Находим последний мерженный PR для main ветки по merge commit SHA
|
||||
COMMIT_SHA="${{ github.sha }}"
|
||||
PR_NUMBER=$(gh pr list --state merged --base main --limit 10 --json number,mergeCommit --jq ".[] | select(.mergeCommit.oid == \"$COMMIT_SHA\") | .number" | head -1)
|
||||
|
||||
# Если не нашли по merge commit, ищем последний мерженный PR
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "⚠️ PR not found by merge commit, trying to get latest merged PR..."
|
||||
PR_NUMBER=$(gh pr list --state merged --base main --limit 1 --json number --jq '.[0].number')
|
||||
fi
|
||||
|
||||
if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then
|
||||
echo "✅ Found PR #$PR_NUMBER"
|
||||
PR_BODY=$(gh pr view $PR_NUMBER --json body --jq '.body // ""')
|
||||
|
||||
if [ -n "$PR_BODY" ] && [ "$PR_BODY" != "null" ]; then
|
||||
echo "PR_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$PR_BODY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
echo "✅ PR body extracted successfully"
|
||||
else
|
||||
echo "⚠️ PR body is empty"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ No merged PR found for this commit"
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Send PR body to important logs
|
||||
if: job.status == 'success' && github.event_name == 'push' && env.PR_BODY != ''
|
||||
uses: appleboy/telegram-action@v1.0.0
|
||||
with:
|
||||
to: ${{ secrets.IMPORTANT_LOGS_CHAT }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
message: |
|
||||
📋 Pull Request Description (PR #${{ env.PR_NUMBER }}):
|
||||
|
||||
${{ env.PR_BODY }}
|
||||
|
||||
🔗 PR: ${{ github.server_url }}/${{ github.repository }}/pull/${{ env.PR_NUMBER }}
|
||||
📝 Commit: ${{ github.sha }}
|
||||
continue-on-error: true
|
||||
|
||||
rollback:
|
||||
runs-on: ubuntu-latest
|
||||
name: Rollback to Previous Version
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.action == 'rollback'
|
||||
environment:
|
||||
name: production
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
|
||||
- name: Rollback on server
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }}
|
||||
username: ${{ vars.SERVER_USER || secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }}
|
||||
script: |
|
||||
set -e
|
||||
export TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}"
|
||||
export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}"
|
||||
|
||||
echo "🔄 Starting rollback..."
|
||||
|
||||
cd /home/prod
|
||||
|
||||
# Определяем коммит для отката
|
||||
ROLLBACK_COMMIT="${{ github.event.inputs.rollback_commit }}"
|
||||
HISTORY_FILE="/home/prod/.deploy_history_telegram_helper_bot.txt"
|
||||
|
||||
if [ -z "$ROLLBACK_COMMIT" ]; then
|
||||
echo "📝 No commit specified, finding last successful deploy..."
|
||||
if [ -f "$HISTORY_FILE" ]; then
|
||||
ROLLBACK_COMMIT=$(grep "|success$" "$HISTORY_FILE" | tail -1 | cut -d'|' -f2 || echo "")
|
||||
fi
|
||||
|
||||
if [ -z "$ROLLBACK_COMMIT" ]; then
|
||||
echo "❌ No successful deploy found in history!"
|
||||
echo "💡 Please specify commit hash manually or check deploy history"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "📝 Rolling back to commit: $ROLLBACK_COMMIT"
|
||||
|
||||
# Проверяем, что коммит существует
|
||||
cd /home/prod/bots/telegram-helper-bot
|
||||
if ! git cat-file -e "$ROLLBACK_COMMIT" 2>/dev/null; then
|
||||
echo "❌ Commit $ROLLBACK_COMMIT not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Сохраняем текущий коммит
|
||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||
COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" "$ROLLBACK_COMMIT" || echo "Rollback")
|
||||
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
|
||||
echo "📝 Current commit: $CURRENT_COMMIT"
|
||||
echo "📝 Target commit: $ROLLBACK_COMMIT"
|
||||
echo "📝 Commit message: $COMMIT_MESSAGE"
|
||||
|
||||
# Исправляем права перед откатом
|
||||
sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true
|
||||
|
||||
# Откатываем код
|
||||
echo "🔄 Rolling back code..."
|
||||
git fetch origin main
|
||||
git reset --hard "$ROLLBACK_COMMIT"
|
||||
|
||||
# Исправляем права после отката
|
||||
sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true
|
||||
|
||||
echo "✅ Code rolled back: $CURRENT_COMMIT → $ROLLBACK_COMMIT"
|
||||
|
||||
# Валидация docker-compose
|
||||
echo "🔍 Validating docker-compose configuration..."
|
||||
cd /home/prod
|
||||
docker-compose config > /dev/null || exit 1
|
||||
echo "✅ docker-compose.yml is valid"
|
||||
|
||||
# Проверка дискового пространства
|
||||
MIN_FREE_GB=5
|
||||
AVAILABLE_SPACE=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0")
|
||||
echo "💾 Available disk space: ${AVAILABLE_SPACE}GB"
|
||||
|
||||
if [ "$AVAILABLE_SPACE" -lt "$MIN_FREE_GB" ]; then
|
||||
echo "⚠️ Insufficient disk space! Cleaning up Docker resources..."
|
||||
docker system prune -f --volumes || true
|
||||
fi
|
||||
|
||||
# Пересобираем и перезапускаем контейнер
|
||||
echo "🔨 Rebuilding and restarting telegram-bot container..."
|
||||
cd /home/prod
|
||||
|
||||
export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN
|
||||
docker-compose stop telegram-bot || true
|
||||
docker-compose build --pull telegram-bot
|
||||
docker-compose up -d telegram-bot
|
||||
|
||||
echo "✅ Telegram bot container rebuilt and started"
|
||||
|
||||
# Записываем в историю
|
||||
echo "${TIMESTAMP}|${ROLLBACK_COMMIT}|Rollback to: ${COMMIT_MESSAGE}|github-actions|rolled_back" >> "$HISTORY_FILE"
|
||||
HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}"
|
||||
tail -n "$HISTORY_SIZE" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" && mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
|
||||
|
||||
echo "✅ Rollback completed successfully"
|
||||
|
||||
- name: Send rollback notification
|
||||
if: always()
|
||||
uses: appleboy/telegram-action@v1.0.0
|
||||
with:
|
||||
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
message: |
|
||||
${{ job.status == 'success' && '🔄' || '❌' }} Rollback: ${{ job.status }}
|
||||
|
||||
📦 Repository: telegram-helper-bot
|
||||
🌿 Branch: main
|
||||
📝 Rolled back to: ${{ github.event.inputs.rollback_commit || 'Last successful commit' }}
|
||||
👤 Triggered by: ${{ github.actor }}
|
||||
|
||||
${{ job.status == 'success' && '✅ Rollback completed successfully! Services restored to previous version.' || '❌ Rollback failed! Check logs for details.' }}
|
||||
|
||||
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
continue-on-error: true
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,6 +34,9 @@ database/test.db
|
||||
test.db
|
||||
*.db
|
||||
|
||||
# Случайно созданный файл при использовании SQLite :memory: не по назначению
|
||||
:memory:
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
@@ -11,15 +11,35 @@
|
||||
|
||||
from .async_db import AsyncBotDB
|
||||
from .base import DatabaseConnection
|
||||
from .models import (Admin, AudioListenRecord, AudioMessage, AudioModerate,
|
||||
BlacklistUser, MessageContentLink, Migration, PostContent,
|
||||
TelegramPost, User, UserMessage)
|
||||
from .models import (
|
||||
Admin,
|
||||
AudioListenRecord,
|
||||
AudioMessage,
|
||||
AudioModerate,
|
||||
BlacklistUser,
|
||||
MessageContentLink,
|
||||
Migration,
|
||||
PostContent,
|
||||
TelegramPost,
|
||||
User,
|
||||
UserMessage,
|
||||
)
|
||||
from .repository_factory import RepositoryFactory
|
||||
|
||||
# Для обратной совместимости экспортируем старый интерфейс
|
||||
__all__ = [
|
||||
'User', 'BlacklistUser', 'UserMessage', 'TelegramPost', 'PostContent',
|
||||
'MessageContentLink', 'Admin', 'Migration', 'AudioMessage', 'AudioListenRecord', 'AudioModerate',
|
||||
'RepositoryFactory', 'DatabaseConnection', 'AsyncBotDB'
|
||||
"User",
|
||||
"BlacklistUser",
|
||||
"UserMessage",
|
||||
"TelegramPost",
|
||||
"PostContent",
|
||||
"MessageContentLink",
|
||||
"Admin",
|
||||
"Migration",
|
||||
"AudioMessage",
|
||||
"AudioListenRecord",
|
||||
"AudioModerate",
|
||||
"RepositoryFactory",
|
||||
"DatabaseConnection",
|
||||
"AsyncBotDB",
|
||||
]
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@ from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import aiosqlite
|
||||
from database.models import (Admin, AudioMessage, BlacklistHistoryRecord,
|
||||
BlacklistUser, PostContent, TelegramPost, User,
|
||||
UserMessage)
|
||||
|
||||
from database.models import (
|
||||
Admin,
|
||||
AudioMessage,
|
||||
BlacklistHistoryRecord,
|
||||
BlacklistUser,
|
||||
PostContent,
|
||||
TelegramPost,
|
||||
User,
|
||||
UserMessage,
|
||||
)
|
||||
from database.repository_factory import RepositoryFactory
|
||||
|
||||
|
||||
@@ -34,10 +42,10 @@ class AsyncBotDB:
|
||||
user = await self.factory.users.get_user_info(user_id)
|
||||
if user:
|
||||
return {
|
||||
'username': user.username,
|
||||
'full_name': user.full_name,
|
||||
'has_stickers': user.has_stickers,
|
||||
'emoji': user.emoji
|
||||
"username": user.username,
|
||||
"full_name": user.full_name,
|
||||
"has_stickers": user.has_stickers,
|
||||
"emoji": user.emoji,
|
||||
}
|
||||
return None
|
||||
|
||||
@@ -53,7 +61,9 @@ class AsyncBotDB:
|
||||
"""Возвращает full_name пользователя."""
|
||||
return await self.factory.users.get_full_name_by_id(user_id)
|
||||
|
||||
async def get_username_and_full_name(self, user_id: int) -> tuple[Optional[str], Optional[str]]:
|
||||
async def get_username_and_full_name(
|
||||
self, user_id: int
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Возвращает username и full_name пользователя."""
|
||||
username = await self.get_username(user_id)
|
||||
full_name = await self.get_full_name_by_id(user_id)
|
||||
@@ -79,7 +89,9 @@ class AsyncBotDB:
|
||||
"""Обновление даты последнего изменения пользователя."""
|
||||
await self.factory.users.update_user_date(user_id)
|
||||
|
||||
async def update_user_info(self, user_id: int, username: str = None, full_name: str = None):
|
||||
async def update_user_info(
|
||||
self, user_id: int, username: str = None, full_name: str = None
|
||||
):
|
||||
"""Обновление информации о пользователе."""
|
||||
await self.factory.users.update_user_info(user_id, username, full_name)
|
||||
|
||||
@@ -108,17 +120,20 @@ class AsyncBotDB:
|
||||
return await self.factory.users.check_emoji_for_user(user_id)
|
||||
|
||||
# Методы для работы с сообщениями
|
||||
async def add_message(self, message_text: str, user_id: int, message_id: int, date: int = None):
|
||||
async def add_message(
|
||||
self, message_text: str, user_id: int, message_id: int, date: int = None
|
||||
):
|
||||
"""Добавление сообщения пользователя."""
|
||||
if date is None:
|
||||
from datetime import datetime
|
||||
|
||||
date = int(datetime.now().timestamp())
|
||||
|
||||
message = UserMessage(
|
||||
message_text=message_text,
|
||||
user_id=user_id,
|
||||
telegram_message_id=message_id,
|
||||
date=date
|
||||
date=date,
|
||||
)
|
||||
await self.factory.messages.add_message(message)
|
||||
|
||||
@@ -135,39 +150,61 @@ class AsyncBotDB:
|
||||
"""Обновление helper сообщения."""
|
||||
await self.factory.posts.update_helper_message(message_id, helper_message_id)
|
||||
|
||||
async def add_post_content(self, post_id: int, message_id: int, content_name: str, content_type: str):
|
||||
async def add_post_content(
|
||||
self, post_id: int, message_id: int, content_name: str, content_type: str
|
||||
):
|
||||
"""Добавление контента поста."""
|
||||
return await self.factory.posts.add_post_content(post_id, message_id, content_name, content_type)
|
||||
return await self.factory.posts.add_post_content(
|
||||
post_id, message_id, content_name, content_type
|
||||
)
|
||||
|
||||
async def add_message_link(self, post_id: int, message_id: int) -> bool:
|
||||
"""Добавляет связь между post_id и message_id в таблицу message_link_to_content."""
|
||||
return await self.factory.posts.add_message_link(post_id, message_id)
|
||||
|
||||
async def get_post_content_from_telegram_by_last_id(self, last_post_id: int) -> List[Tuple[str, str]]:
|
||||
async def get_post_content_from_telegram_by_last_id(
|
||||
self, last_post_id: int
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Получает контент поста по helper_text_message_id."""
|
||||
return await self.factory.posts.get_post_content_by_helper_id(last_post_id)
|
||||
|
||||
async def get_post_content_by_helper_id(self, helper_message_id: int) -> List[Tuple[str, str]]:
|
||||
async def get_post_content_by_helper_id(
|
||||
self, helper_message_id: int
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Алиас для get_post_content_from_telegram_by_last_id (используется callback-сервисом)."""
|
||||
return await self.get_post_content_from_telegram_by_last_id(helper_message_id)
|
||||
|
||||
async def get_post_content_by_message_id(self, message_id: int) -> List[Tuple[str, str]]:
|
||||
async def get_post_content_by_message_id(
|
||||
self, message_id: int
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Получает контент одиночного поста по message_id."""
|
||||
return await self.factory.posts.get_post_content_by_message_id(message_id)
|
||||
|
||||
async def update_published_message_id(self, original_message_id: int, published_message_id: int):
|
||||
async def update_published_message_id(
|
||||
self, original_message_id: int, published_message_id: int
|
||||
):
|
||||
"""Обновляет published_message_id для опубликованного поста."""
|
||||
await self.factory.posts.update_published_message_id(original_message_id, published_message_id)
|
||||
await self.factory.posts.update_published_message_id(
|
||||
original_message_id, published_message_id
|
||||
)
|
||||
|
||||
async def add_published_post_content(self, published_message_id: int, content_path: str, content_type: str):
|
||||
async def add_published_post_content(
|
||||
self, published_message_id: int, content_path: str, content_type: str
|
||||
):
|
||||
"""Добавляет контент опубликованного поста."""
|
||||
return await self.factory.posts.add_published_post_content(published_message_id, content_path, content_type)
|
||||
return await self.factory.posts.add_published_post_content(
|
||||
published_message_id, content_path, content_type
|
||||
)
|
||||
|
||||
async def get_published_post_content(self, published_message_id: int) -> List[Tuple[str, str]]:
|
||||
async def get_published_post_content(
|
||||
self, published_message_id: int
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Получает контент опубликованного поста."""
|
||||
return await self.factory.posts.get_published_post_content(published_message_id)
|
||||
|
||||
async def get_post_text_from_telegram_by_last_id(self, last_post_id: int) -> Optional[str]:
|
||||
async def get_post_text_from_telegram_by_last_id(
|
||||
self, last_post_id: int
|
||||
) -> Optional[str]:
|
||||
"""Получает текст поста по helper_text_message_id."""
|
||||
return await self.factory.posts.get_post_text_by_helper_id(last_post_id)
|
||||
|
||||
@@ -175,7 +212,9 @@ class AsyncBotDB:
|
||||
"""Алиас для get_post_text_from_telegram_by_last_id (используется callback-сервисом)."""
|
||||
return await self.get_post_text_from_telegram_by_last_id(helper_message_id)
|
||||
|
||||
async def get_post_ids_from_telegram_by_last_id(self, last_post_id: int) -> List[int]:
|
||||
async def get_post_ids_from_telegram_by_last_id(
|
||||
self, last_post_id: int
|
||||
) -> List[int]:
|
||||
"""Получает ID сообщений по helper_text_message_id."""
|
||||
return await self.factory.posts.get_post_ids_by_helper_id(last_post_id)
|
||||
|
||||
@@ -187,17 +226,29 @@ class AsyncBotDB:
|
||||
"""Получает ID автора по message_id."""
|
||||
return await self.factory.posts.get_author_id_by_message_id(message_id)
|
||||
|
||||
async def get_author_id_by_helper_message_id(self, helper_text_message_id: int) -> Optional[int]:
|
||||
async def get_author_id_by_helper_message_id(
|
||||
self, helper_text_message_id: int
|
||||
) -> Optional[int]:
|
||||
"""Получает ID автора по helper_text_message_id."""
|
||||
return await self.factory.posts.get_author_id_by_helper_message_id(helper_text_message_id)
|
||||
return await self.factory.posts.get_author_id_by_helper_message_id(
|
||||
helper_text_message_id
|
||||
)
|
||||
|
||||
async def get_post_text_and_anonymity_by_message_id(self, message_id: int) -> tuple[Optional[str], Optional[bool]]:
|
||||
async def get_post_text_and_anonymity_by_message_id(
|
||||
self, message_id: int
|
||||
) -> tuple[Optional[str], Optional[bool]]:
|
||||
"""Получает текст и is_anonymous поста по message_id."""
|
||||
return await self.factory.posts.get_post_text_and_anonymity_by_message_id(message_id)
|
||||
return await self.factory.posts.get_post_text_and_anonymity_by_message_id(
|
||||
message_id
|
||||
)
|
||||
|
||||
async def get_post_text_and_anonymity_by_helper_id(self, helper_message_id: int) -> tuple[Optional[str], Optional[bool]]:
|
||||
async def get_post_text_and_anonymity_by_helper_id(
|
||||
self, helper_message_id: int
|
||||
) -> tuple[Optional[str], Optional[bool]]:
|
||||
"""Получает текст и is_anonymous поста по helper_text_message_id."""
|
||||
return await self.factory.posts.get_post_text_and_anonymity_by_helper_id(helper_message_id)
|
||||
return await self.factory.posts.get_post_text_and_anonymity_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
|
||||
async def update_status_by_message_id(self, message_id: int, status: str) -> int:
|
||||
"""Обновление статуса поста по message_id (одиночные посты). Возвращает число обновлённых строк."""
|
||||
@@ -288,20 +339,30 @@ class AsyncBotDB:
|
||||
"""Проверяет, существует ли запись с данным user_id в blacklist."""
|
||||
return await self.factory.blacklist.user_exists(user_id)
|
||||
|
||||
async def get_blacklist_users(self, offset: int = 0, limit: int = 10) -> List[tuple]:
|
||||
async def get_blacklist_users(
|
||||
self, offset: int = 0, limit: int = 10
|
||||
) -> List[tuple]:
|
||||
"""Получение пользователей из черного списка."""
|
||||
users = await self.factory.blacklist.get_all_users(offset, limit)
|
||||
return [(user.user_id, user.message_for_user, user.date_to_unban) for user in users]
|
||||
return [
|
||||
(user.user_id, user.message_for_user, user.date_to_unban) for user in users
|
||||
]
|
||||
|
||||
async def get_banned_users_from_db(self) -> List[tuple]:
|
||||
"""Возвращает список пользователей в черном списке."""
|
||||
users = await self.factory.blacklist.get_all_users_no_limit()
|
||||
return [(user.user_id, user.message_for_user, user.date_to_unban) for user in users]
|
||||
return [
|
||||
(user.user_id, user.message_for_user, user.date_to_unban) for user in users
|
||||
]
|
||||
|
||||
async def get_banned_users_from_db_with_limits(self, offset: int, limit: int) -> List[tuple]:
|
||||
async def get_banned_users_from_db_with_limits(
|
||||
self, offset: int, limit: int
|
||||
) -> List[tuple]:
|
||||
"""Возвращает список пользователей в черном списке с учетом смещения и ограничения."""
|
||||
users = await self.factory.blacklist.get_all_users(offset, limit)
|
||||
return [(user.user_id, user.message_for_user, user.date_to_unban) for user in users]
|
||||
return [
|
||||
(user.user_id, user.message_for_user, user.date_to_unban) for user in users
|
||||
]
|
||||
|
||||
async def get_blacklist_users_by_id(self, user_id: int) -> Optional[tuple]:
|
||||
"""Возвращает информацию о пользователе в черном списке по user_id."""
|
||||
@@ -314,9 +375,13 @@ class AsyncBotDB:
|
||||
"""Получение количества пользователей в черном списке."""
|
||||
return await self.factory.blacklist.get_count()
|
||||
|
||||
async def get_users_for_unblock_today(self, current_timestamp: int) -> Dict[int, int]:
|
||||
async def get_users_for_unblock_today(
|
||||
self, current_timestamp: int
|
||||
) -> Dict[int, int]:
|
||||
"""Возвращает список пользователей, у которых истек срок блокировки."""
|
||||
return await self.factory.blacklist.get_users_for_unblock_today(current_timestamp)
|
||||
return await self.factory.blacklist.get_users_for_unblock_today(
|
||||
current_timestamp
|
||||
)
|
||||
|
||||
# Методы для работы с администраторами
|
||||
async def add_admin(self, user_id: int, role: str = "admin"):
|
||||
@@ -337,19 +402,27 @@ class AsyncBotDB:
|
||||
return await self.factory.admins.get_all_admins()
|
||||
|
||||
# Методы для работы с аудио
|
||||
async def add_audio_record(self, file_name: str, author_id: int, date_added: str,
|
||||
listen_count: int, file_id: str):
|
||||
async def add_audio_record(
|
||||
self,
|
||||
file_name: str,
|
||||
author_id: int,
|
||||
date_added: str,
|
||||
listen_count: int,
|
||||
file_id: str,
|
||||
):
|
||||
"""Добавляет информацию о войсе пользователя."""
|
||||
audio = AudioMessage(
|
||||
file_name=file_name,
|
||||
author_id=author_id,
|
||||
date_added=date_added,
|
||||
listen_count=listen_count,
|
||||
file_id=file_id
|
||||
file_id=file_id,
|
||||
)
|
||||
await self.factory.audio.add_audio_record(audio)
|
||||
|
||||
async def add_audio_record_simple(self, file_name: str, user_id: int, date_added) -> None:
|
||||
async def add_audio_record_simple(
|
||||
self, file_name: str, user_id: int, date_added
|
||||
) -> None:
|
||||
"""Добавляет простую запись об аудио файле."""
|
||||
await self.factory.audio.add_audio_record_simple(file_name, user_id, date_added)
|
||||
|
||||
@@ -399,13 +472,21 @@ class AsyncBotDB:
|
||||
return await self.factory.audio.get_date_by_file_name(file_name)
|
||||
|
||||
# Методы для voice bot
|
||||
async def set_user_id_and_message_id_for_voice_bot(self, message_id: int, user_id: int) -> bool:
|
||||
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."""
|
||||
return await self.factory.audio.set_user_id_and_message_id_for_voice_bot(message_id, user_id)
|
||||
return await self.factory.audio.set_user_id_and_message_id_for_voice_bot(
|
||||
message_id, user_id
|
||||
)
|
||||
|
||||
async def get_user_id_by_message_id_for_voice_bot(self, message_id: int) -> Optional[int]:
|
||||
async def get_user_id_by_message_id_for_voice_bot(
|
||||
self, message_id: int
|
||||
) -> Optional[int]:
|
||||
"""Получает user_id пользователя по message_id для voice bot."""
|
||||
return await self.factory.audio.get_user_id_by_message_id_for_voice_bot(message_id)
|
||||
return await self.factory.audio.get_user_id_by_message_id_for_voice_bot(
|
||||
message_id
|
||||
)
|
||||
|
||||
async def delete_audio_moderate_record(self, message_id: int) -> None:
|
||||
"""Удаляет запись из таблицы audio_moderate по message_id."""
|
||||
@@ -447,7 +528,9 @@ class AsyncBotDB:
|
||||
# Соединения закрываются в каждом методе
|
||||
pass
|
||||
|
||||
async def fetch_one(self, query: str, params: tuple = ()) -> Optional[Dict[str, Any]]:
|
||||
async def fetch_one(
|
||||
self, query: str, params: tuple = ()
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Выполняет SQL запрос и возвращает один результат."""
|
||||
try:
|
||||
async with aiosqlite.connect(self.factory.db_path) as conn:
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
from typing import Optional
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
@@ -11,7 +12,7 @@ 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}')
|
||||
self.logger.info(f"Инициация базы данных: {self.db_path}")
|
||||
|
||||
async def _get_connection(self):
|
||||
"""Получение асинхронного соединения с базой данных."""
|
||||
@@ -90,7 +91,9 @@ class DatabaseConnection:
|
||||
await conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
self.logger.info("WAL файлы очищены")
|
||||
else:
|
||||
self.logger.warning(f"Проблемы с целостностью базы данных: {integrity_result}")
|
||||
self.logger.warning(
|
||||
f"Проблемы с целостностью базы данных: {integrity_result}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при проверке целостности базы данных: {e}")
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import List, Optional
|
||||
@dataclass
|
||||
class User:
|
||||
"""Модель пользователя."""
|
||||
|
||||
user_id: int
|
||||
first_name: str
|
||||
full_name: str
|
||||
@@ -22,6 +23,7 @@ class User:
|
||||
@dataclass
|
||||
class BlacklistUser:
|
||||
"""Модель пользователя в черном списке."""
|
||||
|
||||
user_id: int
|
||||
message_for_user: Optional[str] = None
|
||||
date_to_unban: Optional[int] = None
|
||||
@@ -32,6 +34,7 @@ class BlacklistUser:
|
||||
@dataclass
|
||||
class BlacklistHistoryRecord:
|
||||
"""Модель записи истории банов/разбанов."""
|
||||
|
||||
user_id: int
|
||||
message_for_user: Optional[str] = None
|
||||
date_ban: int = 0
|
||||
@@ -45,6 +48,7 @@ class BlacklistHistoryRecord:
|
||||
@dataclass
|
||||
class UserMessage:
|
||||
"""Модель сообщения пользователя."""
|
||||
|
||||
message_text: str
|
||||
user_id: int
|
||||
telegram_message_id: int
|
||||
@@ -54,6 +58,7 @@ class UserMessage:
|
||||
@dataclass
|
||||
class TelegramPost:
|
||||
"""Модель поста из Telegram."""
|
||||
|
||||
message_id: int
|
||||
text: str
|
||||
author_id: int
|
||||
@@ -66,6 +71,7 @@ class TelegramPost:
|
||||
@dataclass
|
||||
class PostContent:
|
||||
"""Модель контента поста."""
|
||||
|
||||
message_id: int
|
||||
content_name: str
|
||||
content_type: str
|
||||
@@ -74,6 +80,7 @@ class PostContent:
|
||||
@dataclass
|
||||
class MessageContentLink:
|
||||
"""Модель связи сообщения с контентом."""
|
||||
|
||||
post_id: int
|
||||
message_id: int
|
||||
|
||||
@@ -81,6 +88,7 @@ class MessageContentLink:
|
||||
@dataclass
|
||||
class Admin:
|
||||
"""Модель администратора."""
|
||||
|
||||
user_id: int
|
||||
role: str = "admin"
|
||||
created_at: Optional[str] = None
|
||||
@@ -89,6 +97,7 @@ class Admin:
|
||||
@dataclass
|
||||
class Migration:
|
||||
"""Модель миграции."""
|
||||
|
||||
script_name: str
|
||||
applied_at: Optional[str] = None
|
||||
|
||||
@@ -96,6 +105,7 @@ class Migration:
|
||||
@dataclass
|
||||
class AudioMessage:
|
||||
"""Модель аудио сообщения."""
|
||||
|
||||
file_name: str
|
||||
author_id: int
|
||||
date_added: str
|
||||
@@ -106,6 +116,7 @@ class AudioMessage:
|
||||
@dataclass
|
||||
class AudioListenRecord:
|
||||
"""Модель записи прослушивания аудио."""
|
||||
|
||||
file_name: str
|
||||
user_id: int
|
||||
is_listen: bool = False
|
||||
@@ -114,5 +125,6 @@ class AudioListenRecord:
|
||||
@dataclass
|
||||
class AudioModerate:
|
||||
"""Модель для voice bot."""
|
||||
|
||||
message_id: int
|
||||
user_id: int
|
||||
|
||||
@@ -22,7 +22,12 @@ from .post_repository import PostRepository
|
||||
from .user_repository import UserRepository
|
||||
|
||||
__all__ = [
|
||||
'UserRepository', 'BlacklistRepository', 'BlacklistHistoryRepository',
|
||||
'MessageRepository', 'PostRepository', 'AdminRepository', 'AudioRepository',
|
||||
'MigrationRepository'
|
||||
"UserRepository",
|
||||
"BlacklistRepository",
|
||||
"BlacklistHistoryRepository",
|
||||
"MessageRepository",
|
||||
"PostRepository",
|
||||
"AdminRepository",
|
||||
"AudioRepository",
|
||||
"MigrationRepository",
|
||||
]
|
||||
|
||||
@@ -12,14 +12,14 @@ class AdminRepository(DatabaseConnection):
|
||||
# Включаем поддержку внешних ключей
|
||||
await self._execute_query("PRAGMA foreign_keys = ON")
|
||||
|
||||
query = '''
|
||||
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("Таблица администраторов создана")
|
||||
|
||||
@@ -29,7 +29,9 @@ class AdminRepository(DatabaseConnection):
|
||||
params = (admin.user_id, admin.role)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Администратор добавлен: user_id={admin.user_id}, role={admin.role}")
|
||||
self.logger.info(
|
||||
f"Администратор добавлен: user_id={admin.user_id}, role={admin.role}"
|
||||
)
|
||||
|
||||
async def remove_admin(self, user_id: int) -> None:
|
||||
"""Удаление администратора."""
|
||||
@@ -52,9 +54,7 @@ class AdminRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
return Admin(
|
||||
user_id=row[0],
|
||||
role=row[1],
|
||||
created_at=row[2] if len(row) > 2 else None
|
||||
user_id=row[0], role=row[1], created_at=row[2] if len(row) > 2 else None
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -66,9 +66,7 @@ class AdminRepository(DatabaseConnection):
|
||||
admins = []
|
||||
for row in rows:
|
||||
admin = Admin(
|
||||
user_id=row[0],
|
||||
role=row[1],
|
||||
created_at=row[2] if len(row) > 2 else None
|
||||
user_id=row[0], role=row[1], created_at=row[2] if len(row) > 2 else None
|
||||
)
|
||||
admins.append(admin)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from database.base import DatabaseConnection
|
||||
@@ -15,7 +15,7 @@ class AudioRepository(DatabaseConnection):
|
||||
async def create_tables(self):
|
||||
"""Создание таблиц для аудио."""
|
||||
# Таблица аудио сообщений
|
||||
audio_query = '''
|
||||
audio_query = """
|
||||
CREATE TABLE IF NOT EXISTS audio_message_reference (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
file_name TEXT NOT NULL UNIQUE,
|
||||
@@ -23,29 +23,29 @@ class AudioRepository(DatabaseConnection):
|
||||
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 = '''
|
||||
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 = '''
|
||||
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("Таблицы для аудио созданы")
|
||||
@@ -67,9 +67,13 @@ class AudioRepository(DatabaseConnection):
|
||||
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}")
|
||||
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:
|
||||
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)
|
||||
@@ -127,7 +131,9 @@ class AudioRepository(DatabaseConnection):
|
||||
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_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,))
|
||||
|
||||
# Находим непрослушанные
|
||||
@@ -135,7 +141,9 @@ class AudioRepository(DatabaseConnection):
|
||||
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}")
|
||||
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:
|
||||
@@ -144,7 +152,9 @@ class AudioRepository(DatabaseConnection):
|
||||
params = (file_name, user_id)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Аудио {file_name} отмечено как прослушанное для пользователя {user_id}")
|
||||
self.logger.info(
|
||||
f"Аудио {file_name} отмечено как прослушанное для пользователя {user_id}"
|
||||
)
|
||||
|
||||
async def get_user_id_by_file_name(self, file_name: str) -> Optional[int]:
|
||||
"""Получает user_id пользователя по имени файла."""
|
||||
@@ -166,8 +176,10 @@ class AudioRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
date_added = row[0]
|
||||
# Преобразуем UNIX timestamp в читаемую дату
|
||||
readable_date = datetime.fromtimestamp(date_added).strftime('%d.%m.%Y %H:%M')
|
||||
# Преобразуем UNIX timestamp в читаемую дату (UTC для одинакового результата везде)
|
||||
readable_date = datetime.fromtimestamp(
|
||||
date_added, tz=timezone.utc
|
||||
).strftime("%d.%m.%Y %H:%M")
|
||||
self.logger.info(f"Получена дата {readable_date} для файла {file_name}")
|
||||
return readable_date
|
||||
return None
|
||||
@@ -185,20 +197,26 @@ class AudioRepository(DatabaseConnection):
|
||||
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:
|
||||
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}")
|
||||
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]:
|
||||
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,))
|
||||
@@ -214,7 +232,9 @@ class AudioRepository(DatabaseConnection):
|
||||
"""Удаляет запись из таблицы 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}")
|
||||
self.logger.info(
|
||||
f"Удалена запись из audio_moderate для message_id {message_id}"
|
||||
)
|
||||
|
||||
async def get_all_audio_records(self) -> List[Dict[str, Any]]:
|
||||
"""Получить все записи аудио сообщений."""
|
||||
@@ -223,11 +243,9 @@ class AudioRepository(DatabaseConnection):
|
||||
|
||||
records = []
|
||||
for row in rows:
|
||||
records.append({
|
||||
'file_name': row[0],
|
||||
'author_id': row[1],
|
||||
'date_added': row[2]
|
||||
})
|
||||
records.append(
|
||||
{"file_name": row[0], "author_id": row[1], "date_added": row[2]}
|
||||
)
|
||||
|
||||
self.logger.info(f"Получено {len(records)} записей аудио сообщений")
|
||||
return records
|
||||
|
||||
@@ -9,7 +9,7 @@ class BlacklistHistoryRepository(DatabaseConnection):
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы истории банов/разбанов."""
|
||||
query = '''
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS blacklist_history (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -22,7 +22,7 @@ class BlacklistHistoryRepository(DatabaseConnection):
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ban_author) REFERENCES our_users(user_id) ON DELETE SET NULL
|
||||
)
|
||||
'''
|
||||
"""
|
||||
await self._execute_query(query)
|
||||
|
||||
# Создаем индексы
|
||||
@@ -48,6 +48,7 @@ class BlacklistHistoryRepository(DatabaseConnection):
|
||||
"""
|
||||
# Используем текущее время, если не указано
|
||||
from datetime import datetime
|
||||
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
|
||||
params = (
|
||||
@@ -79,6 +80,7 @@ class BlacklistHistoryRepository(DatabaseConnection):
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
|
||||
# SQLite не поддерживает ORDER BY в UPDATE, поэтому используем подзапрос
|
||||
|
||||
@@ -9,7 +9,7 @@ class BlacklistRepository(DatabaseConnection):
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы черного списка."""
|
||||
query = '''
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS blacklist (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
message_for_user TEXT,
|
||||
@@ -19,7 +19,7 @@ class BlacklistRepository(DatabaseConnection):
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ban_author) REFERENCES our_users (user_id) ON DELETE SET NULL
|
||||
)
|
||||
'''
|
||||
"""
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица черного списка создана")
|
||||
|
||||
@@ -37,18 +37,24 @@ class BlacklistRepository(DatabaseConnection):
|
||||
)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Пользователь добавлен в черный список: user_id={blacklist_user.user_id}")
|
||||
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} успешно удален из черного списка.")
|
||||
self.logger.info(
|
||||
f"Пользователь с идентификатором {user_id} успешно удален из черного списка."
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка удаления пользователя с идентификатором {user_id} "
|
||||
f"из таблицы blacklist. Ошибка: {str(e)}")
|
||||
self.logger.error(
|
||||
f"Ошибка удаления пользователя с идентификатором {user_id} "
|
||||
f"из таблицы blacklist. Ошибка: {str(e)}"
|
||||
)
|
||||
return False
|
||||
|
||||
async def user_exists(self, user_id: int) -> bool:
|
||||
@@ -78,7 +84,9 @@ class BlacklistRepository(DatabaseConnection):
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_all_users(self, offset: int = 0, limit: int = 10) -> List[BlacklistUser]:
|
||||
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, ban_author
|
||||
@@ -99,7 +107,9 @@ class BlacklistRepository(DatabaseConnection):
|
||||
)
|
||||
)
|
||||
|
||||
self.logger.info(f"Получен список пользователей в черном списке (offset={offset}, limit={limit}): {len(users)}")
|
||||
self.logger.info(
|
||||
f"Получен список пользователей в черном списке (offset={offset}, limit={limit}): {len(users)}"
|
||||
)
|
||||
return users
|
||||
|
||||
async def get_all_users_no_limit(self) -> List[BlacklistUser]:
|
||||
@@ -122,10 +132,14 @@ class BlacklistRepository(DatabaseConnection):
|
||||
)
|
||||
)
|
||||
|
||||
self.logger.info(f"Получен список всех пользователей в черном списке: {len(users)}")
|
||||
self.logger.info(
|
||||
f"Получен список всех пользователей в черном списке: {len(users)}"
|
||||
)
|
||||
return users
|
||||
|
||||
async def get_users_for_unblock_today(self, current_timestamp: int) -> Dict[int, int]:
|
||||
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,))
|
||||
|
||||
@@ -10,7 +10,7 @@ class MessageRepository(DatabaseConnection):
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы сообщений пользователей."""
|
||||
query = '''
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT,
|
||||
@@ -19,7 +19,7 @@ class MessageRepository(DatabaseConnection):
|
||||
date INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
"""
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица сообщений пользователей создана")
|
||||
|
||||
@@ -32,10 +32,17 @@ class MessageRepository(DatabaseConnection):
|
||||
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)
|
||||
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}")
|
||||
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."""
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Репозиторий для работы с миграциями базы данных."""
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from database.base import DatabaseConnection
|
||||
|
||||
|
||||
@@ -23,7 +25,9 @@ class MigrationRepository(DatabaseConnection):
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
cursor = await conn.execute("SELECT script_name FROM migrations ORDER BY applied_at")
|
||||
cursor = await conn.execute(
|
||||
"SELECT script_name FROM migrations ORDER BY applied_at"
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
return [row[0] for row in rows]
|
||||
@@ -40,8 +44,7 @@ class MigrationRepository(DatabaseConnection):
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
cursor = await conn.execute(
|
||||
"SELECT COUNT(*) FROM migrations WHERE script_name = ?",
|
||||
(script_name,)
|
||||
"SELECT COUNT(*) FROM migrations WHERE script_name = ?", (script_name,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
await cursor.close()
|
||||
@@ -59,8 +62,7 @@ class MigrationRepository(DatabaseConnection):
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
await conn.execute(
|
||||
"INSERT INTO migrations (script_name) VALUES (?)",
|
||||
(script_name,)
|
||||
"INSERT INTO migrations (script_name) VALUES (?)", (script_name,)
|
||||
)
|
||||
await conn.commit()
|
||||
self.logger.info(f"Миграция {script_name} отмечена как примененная")
|
||||
|
||||
@@ -11,7 +11,7 @@ class PostRepository(DatabaseConnection):
|
||||
async def create_tables(self):
|
||||
"""Создание таблиц для постов."""
|
||||
# Таблица постов из Telegram
|
||||
post_query = '''
|
||||
post_query = """
|
||||
CREATE TABLE IF NOT EXISTS post_from_telegram_suggest (
|
||||
message_id INTEGER NOT NULL PRIMARY KEY,
|
||||
text TEXT,
|
||||
@@ -23,7 +23,7 @@ class PostRepository(DatabaseConnection):
|
||||
published_message_id INTEGER,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
"""
|
||||
await self._execute_query(post_query)
|
||||
|
||||
# Добавляем поле published_message_id если его нет (для существующих БД)
|
||||
@@ -34,19 +34,27 @@ class PostRepository(DatabaseConnection):
|
||||
"""
|
||||
existing_columns = await self._execute_query_with_result(check_column_query)
|
||||
if not existing_columns:
|
||||
await self._execute_query('ALTER TABLE post_from_telegram_suggest ADD COLUMN published_message_id INTEGER')
|
||||
self.logger.info("Столбец published_message_id добавлен в post_from_telegram_suggest")
|
||||
await self._execute_query(
|
||||
"ALTER TABLE post_from_telegram_suggest ADD COLUMN published_message_id INTEGER"
|
||||
)
|
||||
self.logger.info(
|
||||
"Столбец published_message_id добавлен в post_from_telegram_suggest"
|
||||
)
|
||||
except Exception as e:
|
||||
# Если проверка не удалась, пытаемся добавить столбец (может быть уже существует)
|
||||
try:
|
||||
await self._execute_query('ALTER TABLE post_from_telegram_suggest ADD COLUMN published_message_id INTEGER')
|
||||
self.logger.info("Столбец published_message_id добавлен в post_from_telegram_suggest (fallback)")
|
||||
await self._execute_query(
|
||||
"ALTER TABLE post_from_telegram_suggest ADD COLUMN published_message_id INTEGER"
|
||||
)
|
||||
self.logger.info(
|
||||
"Столбец published_message_id добавлен в post_from_telegram_suggest (fallback)"
|
||||
)
|
||||
except Exception:
|
||||
# Столбец уже существует, игнорируем ошибку
|
||||
pass
|
||||
|
||||
# Таблица контента постов
|
||||
content_query = '''
|
||||
content_query = """
|
||||
CREATE TABLE IF NOT EXISTS content_post_from_telegram (
|
||||
message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
@@ -54,22 +62,22 @@ class PostRepository(DatabaseConnection):
|
||||
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 = '''
|
||||
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)
|
||||
|
||||
# Таблица контента опубликованных постов
|
||||
published_content_query = '''
|
||||
published_content_query = """
|
||||
CREATE TABLE IF NOT EXISTS published_post_content (
|
||||
published_message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
@@ -77,13 +85,17 @@ class PostRepository(DatabaseConnection):
|
||||
published_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (published_message_id, content_name)
|
||||
)
|
||||
'''
|
||||
"""
|
||||
await self._execute_query(published_content_query)
|
||||
|
||||
# Создаем индексы
|
||||
try:
|
||||
await self._execute_query('CREATE INDEX IF NOT EXISTS idx_published_post_content_message_id ON published_post_content(published_message_id)')
|
||||
await self._execute_query('CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_published ON post_from_telegram_suggest(published_message_id)')
|
||||
await self._execute_query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_published_post_content_message_id ON published_post_content(published_message_id)"
|
||||
)
|
||||
await self._execute_query(
|
||||
"CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_published ON post_from_telegram_suggest(published_message_id)"
|
||||
)
|
||||
except Exception:
|
||||
# Индексы уже существуют, игнорируем ошибку
|
||||
pass
|
||||
@@ -96,19 +108,32 @@ class PostRepository(DatabaseConnection):
|
||||
post.created_at = int(datetime.now().timestamp())
|
||||
status = post.status if post.status else "suggest"
|
||||
# Преобразуем bool в int для SQLite (True -> 1, False -> 0, None -> None)
|
||||
is_anonymous_int = None if post.is_anonymous is None else (1 if post.is_anonymous else 0)
|
||||
is_anonymous_int = (
|
||||
None if post.is_anonymous is None else (1 if post.is_anonymous else 0)
|
||||
)
|
||||
|
||||
# Используем INSERT OR IGNORE чтобы избежать ошибок при повторном создании
|
||||
query = """
|
||||
INSERT OR IGNORE INTO post_from_telegram_suggest (message_id, text, author_id, created_at, status, is_anonymous)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
params = (post.message_id, post.text, post.author_id, post.created_at, status, is_anonymous_int)
|
||||
params = (
|
||||
post.message_id,
|
||||
post.text,
|
||||
post.author_id,
|
||||
post.created_at,
|
||||
status,
|
||||
is_anonymous_int,
|
||||
)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Пост добавлен (или уже существует): message_id={post.message_id}, text длина={len(post.text) if post.text else 0}, is_anonymous={is_anonymous_int}")
|
||||
self.logger.info(
|
||||
f"Пост добавлен (или уже существует): message_id={post.message_id}, text длина={len(post.text) if post.text else 0}, is_anonymous={is_anonymous_int}"
|
||||
)
|
||||
|
||||
async def update_helper_message(self, message_id: int, helper_message_id: int) -> None:
|
||||
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))
|
||||
@@ -131,12 +156,16 @@ class PostRepository(DatabaseConnection):
|
||||
f"update_status_by_message_id: 0 строк обновлено для message_id={message_id}, status={status}"
|
||||
)
|
||||
else:
|
||||
self.logger.info(f"Статус поста message_id={message_id} обновлён на {status}")
|
||||
self.logger.info(
|
||||
f"Статус поста message_id={message_id} обновлён на {status}"
|
||||
)
|
||||
return n
|
||||
except Exception as e:
|
||||
if conn:
|
||||
await conn.rollback()
|
||||
self.logger.error(f"Ошибка при обновлении статуса message_id={message_id}: {e}")
|
||||
self.logger.error(
|
||||
f"Ошибка при обновлении статуса message_id={message_id}: {e}"
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
@@ -182,7 +211,9 @@ class PostRepository(DatabaseConnection):
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def add_post_content(self, post_id: int, message_id: int, content_name: str, content_type: str) -> bool:
|
||||
async def add_post_content(
|
||||
self, post_id: int, message_id: int, content_name: str, content_type: str
|
||||
) -> bool:
|
||||
"""Добавление контента поста."""
|
||||
try:
|
||||
# Сначала добавляем связь
|
||||
@@ -194,9 +225,13 @@ class PostRepository(DatabaseConnection):
|
||||
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))
|
||||
await self._execute_query(
|
||||
content_query, (message_id, content_name, content_type)
|
||||
)
|
||||
|
||||
self.logger.info(f"Контент поста добавлен: post_id={post_id}, message_id={message_id}")
|
||||
self.logger.info(
|
||||
f"Контент поста добавлен: post_id={post_id}, message_id={message_id}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при добавлении контента поста: {e}")
|
||||
@@ -205,16 +240,24 @@ class PostRepository(DatabaseConnection):
|
||||
async def add_message_link(self, post_id: int, message_id: int) -> bool:
|
||||
"""Добавляет связь между post_id и message_id в таблицу message_link_to_content."""
|
||||
try:
|
||||
self.logger.info(f"Добавление связи: post_id={post_id}, message_id={message_id}")
|
||||
self.logger.info(
|
||||
f"Добавление связи: post_id={post_id}, message_id={message_id}"
|
||||
)
|
||||
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))
|
||||
self.logger.info(f"Связь успешно добавлена: post_id={post_id}, message_id={message_id}")
|
||||
self.logger.info(
|
||||
f"Связь успешно добавлена: post_id={post_id}, message_id={message_id}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при добавлении связи post_id={post_id}, message_id={message_id}: {e}")
|
||||
self.logger.error(
|
||||
f"Ошибка при добавлении связи post_id={post_id}, message_id={message_id}: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
async def get_post_content_by_helper_id(self, helper_message_id: int) -> List[Tuple[str, str]]:
|
||||
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
|
||||
@@ -223,12 +266,16 @@ class PostRepository(DatabaseConnection):
|
||||
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,))
|
||||
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_content_by_message_id(self, message_id: int) -> List[Tuple[str, str]]:
|
||||
async def get_post_content_by_message_id(
|
||||
self, message_id: int
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Получает контент одиночного поста по message_id."""
|
||||
query = """
|
||||
SELECT cpft.content_name, cpft.content_type
|
||||
@@ -239,7 +286,9 @@ class PostRepository(DatabaseConnection):
|
||||
"""
|
||||
post_content = await self._execute_query_with_result(query, (message_id,))
|
||||
|
||||
self.logger.info(f"Получен контент одиночного поста: {len(post_content)} элементов для message_id={message_id}")
|
||||
self.logger.info(
|
||||
f"Получен контент одиночного поста: {len(post_content)} элементов для message_id={message_id}"
|
||||
)
|
||||
return post_content
|
||||
|
||||
async def get_post_text_by_helper_id(self, helper_message_id: int) -> Optional[str]:
|
||||
@@ -249,7 +298,9 @@ class PostRepository(DatabaseConnection):
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
self.logger.info(f"Получен текст поста для helper_message_id={helper_message_id}")
|
||||
self.logger.info(
|
||||
f"Получен текст поста для helper_message_id={helper_message_id}"
|
||||
)
|
||||
return row[0]
|
||||
return None
|
||||
|
||||
@@ -275,11 +326,15 @@ class PostRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
author_id = row[0]
|
||||
self.logger.info(f"Получен author_id: {author_id} для message_id={message_id}")
|
||||
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]:
|
||||
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,))
|
||||
@@ -287,11 +342,15 @@ class PostRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
author_id = row[0]
|
||||
self.logger.info(f"Получен author_id: {author_id} для helper_message_id={helper_message_id}")
|
||||
self.logger.info(
|
||||
f"Получен author_id: {author_id} для helper_message_id={helper_message_id}"
|
||||
)
|
||||
return author_id
|
||||
return None
|
||||
|
||||
async def get_post_text_and_anonymity_by_message_id(self, message_id: int) -> Tuple[Optional[str], Optional[bool]]:
|
||||
async def get_post_text_and_anonymity_by_message_id(
|
||||
self, message_id: int
|
||||
) -> Tuple[Optional[str], Optional[bool]]:
|
||||
"""Получает текст и is_anonymous поста по message_id."""
|
||||
query = "SELECT text, is_anonymous FROM post_from_telegram_suggest WHERE message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (message_id,))
|
||||
@@ -302,11 +361,15 @@ class PostRepository(DatabaseConnection):
|
||||
is_anonymous_int = row[1]
|
||||
# Преобразуем int в bool (1 -> True, 0 -> False, NULL -> None)
|
||||
is_anonymous = None if is_anonymous_int is None else bool(is_anonymous_int)
|
||||
self.logger.info(f"Получены текст и is_anonymous для message_id={message_id}")
|
||||
self.logger.info(
|
||||
f"Получены текст и is_anonymous для message_id={message_id}"
|
||||
)
|
||||
return text, is_anonymous
|
||||
return None, None
|
||||
|
||||
async def get_post_text_and_anonymity_by_helper_id(self, helper_message_id: int) -> Tuple[Optional[str], Optional[bool]]:
|
||||
async def get_post_text_and_anonymity_by_helper_id(
|
||||
self, helper_message_id: int
|
||||
) -> Tuple[Optional[str], Optional[bool]]:
|
||||
"""Получает текст и is_anonymous поста по helper_text_message_id."""
|
||||
query = "SELECT text, is_anonymous FROM post_from_telegram_suggest WHERE helper_text_message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
@@ -317,15 +380,21 @@ class PostRepository(DatabaseConnection):
|
||||
is_anonymous_int = row[1]
|
||||
# Преобразуем int в bool (1 -> True, 0 -> False, NULL -> None)
|
||||
is_anonymous = None if is_anonymous_int is None else bool(is_anonymous_int)
|
||||
self.logger.info(f"Получены текст и is_anonymous для helper_message_id={helper_message_id}")
|
||||
self.logger.info(
|
||||
f"Получены текст и is_anonymous для helper_message_id={helper_message_id}"
|
||||
)
|
||||
return text, is_anonymous
|
||||
return None, None
|
||||
|
||||
async def update_published_message_id(self, original_message_id: int, published_message_id: int) -> None:
|
||||
async def update_published_message_id(
|
||||
self, original_message_id: int, published_message_id: int
|
||||
) -> None:
|
||||
"""Обновляет published_message_id для опубликованного поста."""
|
||||
query = "UPDATE post_from_telegram_suggest SET published_message_id = ? WHERE message_id = ?"
|
||||
await self._execute_query(query, (published_message_id, original_message_id))
|
||||
self.logger.info(f"Обновлен published_message_id: {original_message_id} -> {published_message_id}")
|
||||
self.logger.info(
|
||||
f"Обновлен published_message_id: {original_message_id} -> {published_message_id}"
|
||||
)
|
||||
|
||||
async def add_published_post_content(
|
||||
self, published_message_id: int, content_path: str, content_type: str
|
||||
@@ -333,6 +402,7 @@ class PostRepository(DatabaseConnection):
|
||||
"""Добавляет контент опубликованного поста."""
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
published_at = int(datetime.now().timestamp())
|
||||
|
||||
query = """
|
||||
@@ -340,22 +410,34 @@ class PostRepository(DatabaseConnection):
|
||||
(published_message_id, content_name, content_type, published_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
await self._execute_query(query, (published_message_id, content_path, content_type, published_at))
|
||||
self.logger.info(f"Добавлен контент опубликованного поста: published_message_id={published_message_id}, type={content_type}")
|
||||
await self._execute_query(
|
||||
query, (published_message_id, content_path, content_type, published_at)
|
||||
)
|
||||
self.logger.info(
|
||||
f"Добавлен контент опубликованного поста: published_message_id={published_message_id}, type={content_type}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при добавлении контента опубликованного поста: {e}")
|
||||
self.logger.error(
|
||||
f"Ошибка при добавлении контента опубликованного поста: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
async def get_published_post_content(self, published_message_id: int) -> List[Tuple[str, str]]:
|
||||
async def get_published_post_content(
|
||||
self, published_message_id: int
|
||||
) -> List[Tuple[str, str]]:
|
||||
"""Получает контент опубликованного поста."""
|
||||
query = """
|
||||
SELECT content_name, content_type
|
||||
FROM published_post_content
|
||||
WHERE published_message_id = ?
|
||||
"""
|
||||
post_content = await self._execute_query_with_result(query, (published_message_id,))
|
||||
self.logger.info(f"Получен контент опубликованного поста: {len(post_content)} элементов для published_message_id={published_message_id}")
|
||||
post_content = await self._execute_query_with_result(
|
||||
query, (published_message_id,)
|
||||
)
|
||||
self.logger.info(
|
||||
f"Получен контент опубликованного поста: {len(post_content)} элементов для published_message_id={published_message_id}"
|
||||
)
|
||||
return post_content
|
||||
|
||||
# ============================================
|
||||
@@ -379,7 +461,9 @@ class PostRepository(DatabaseConnection):
|
||||
self.logger.info(f"ML-скоры обновлены для message_id={message_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка обновления ML-скоров для message_id={message_id}: {e}")
|
||||
self.logger.error(
|
||||
f"Ошибка обновления ML-скоров для message_id={message_id}: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
async def get_ml_scores_by_message_id(self, message_id: int) -> Optional[str]:
|
||||
@@ -461,4 +545,3 @@ class PostRepository(DatabaseConnection):
|
||||
texts = [row[0] for row in rows if row[0]]
|
||||
self.logger.info(f"Получено {len(texts)} отклоненных постов для обучения")
|
||||
return texts
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class UserRepository(DatabaseConnection):
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы пользователей."""
|
||||
query = '''
|
||||
query = """
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
@@ -24,7 +24,7 @@ class UserRepository(DatabaseConnection):
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
)
|
||||
'''
|
||||
"""
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица пользователей создана")
|
||||
|
||||
@@ -32,7 +32,9 @@ class UserRepository(DatabaseConnection):
|
||||
"""Проверяет, существует ли пользователь в базе данных."""
|
||||
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}")
|
||||
self.logger.info(
|
||||
f"Проверка существования пользователя: user_id={user_id}, результат={rows}"
|
||||
)
|
||||
return bool(len(rows))
|
||||
|
||||
async def add_user(self, user: User) -> None:
|
||||
@@ -47,12 +49,24 @@ class UserRepository(DatabaseConnection):
|
||||
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)
|
||||
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}")
|
||||
self.logger.info(
|
||||
f"Пользователь обработан (создан или уже существует): {user.user_id}"
|
||||
)
|
||||
|
||||
async def get_user_info(self, user_id: int) -> Optional[User]:
|
||||
"""Получение информации о пользователе."""
|
||||
@@ -67,7 +81,7 @@ class UserRepository(DatabaseConnection):
|
||||
full_name=row[1],
|
||||
username=row[0],
|
||||
has_stickers=bool(row[2]) if row[2] is not None else False,
|
||||
emoji=row[3]
|
||||
emoji=row[3],
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -89,7 +103,7 @@ class UserRepository(DatabaseConnection):
|
||||
emoji=row[7],
|
||||
date_added=row[8],
|
||||
date_changed=row[9],
|
||||
voice_bot_welcome_received=bool(row[10]) if len(row) > 10 else False
|
||||
voice_bot_welcome_received=bool(row[10]) if len(row) > 10 else False,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -101,7 +115,9 @@ class UserRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
username = row[0]
|
||||
self.logger.info(f"Username пользователя найден: user_id={user_id}, username={username}")
|
||||
self.logger.info(
|
||||
f"Username пользователя найден: user_id={user_id}, username={username}"
|
||||
)
|
||||
return username
|
||||
return None
|
||||
|
||||
@@ -113,7 +129,9 @@ class UserRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
user_id = row[0]
|
||||
self.logger.info(f"User_id пользователя найден: username={username}, user_id={user_id}")
|
||||
self.logger.info(
|
||||
f"User_id пользователя найден: username={username}, user_id={user_id}"
|
||||
)
|
||||
return user_id
|
||||
return None
|
||||
|
||||
@@ -125,7 +143,9 @@ class UserRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
full_name = row[0]
|
||||
self.logger.info(f"Full_name пользователя найден: user_id={user_id}, full_name={full_name}")
|
||||
self.logger.info(
|
||||
f"Full_name пользователя найден: user_id={user_id}, full_name={full_name}"
|
||||
)
|
||||
return full_name
|
||||
return None
|
||||
|
||||
@@ -137,7 +157,9 @@ class UserRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
first_name = row[0]
|
||||
self.logger.info(f"First_name пользователя найден: user_id={user_id}, first_name={first_name}")
|
||||
self.logger.info(
|
||||
f"First_name пользователя найден: user_id={user_id}, first_name={first_name}"
|
||||
)
|
||||
return first_name
|
||||
return None
|
||||
|
||||
@@ -161,7 +183,9 @@ class UserRepository(DatabaseConnection):
|
||||
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:
|
||||
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 = ?"
|
||||
@@ -217,7 +241,9 @@ class UserRepository(DatabaseConnection):
|
||||
|
||||
if row and row[0]:
|
||||
emoji = row[0]
|
||||
self.logger.info(f"Эмодзи пользователя найден: user_id={user_id}, emoji={emoji}")
|
||||
self.logger.info(
|
||||
f"Эмодзи пользователя найден: user_id={user_id}, emoji={emoji}"
|
||||
)
|
||||
return str(emoji)
|
||||
else:
|
||||
self.logger.info(f"Эмодзи пользователя не найден: user_id={user_id}")
|
||||
@@ -243,16 +269,22 @@ class UserRepository(DatabaseConnection):
|
||||
|
||||
if row:
|
||||
welcome_received = bool(row[0])
|
||||
self.logger.info(f"Пользователь {user_id} получал приветствие: {welcome_received}")
|
||||
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 = ?"
|
||||
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} отмечен как получивший приветствие")
|
||||
self.logger.info(
|
||||
f"Пользователь {user_id} отмечен как получивший приветствие"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при отметке получения приветствия: {e}")
|
||||
|
||||
@@ -2,8 +2,9 @@ from typing import Optional
|
||||
|
||||
from database.repositories.admin_repository import AdminRepository
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
from database.repositories.blacklist_history_repository import \
|
||||
BlacklistHistoryRepository
|
||||
from database.repositories.blacklist_history_repository import (
|
||||
BlacklistHistoryRepository,
|
||||
)
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.repositories.migration_repository import MigrationRepository
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Конфигурация для rate limiting
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
@@ -19,15 +20,17 @@ class RateLimitSettings:
|
||||
# Специальные настройки для разных типов сообщений
|
||||
voice_message_delay: float = 2.0 # Дополнительная задержка для голосовых сообщений
|
||||
media_message_delay: float = 1.5 # Дополнительная задержка для медиа сообщений
|
||||
text_message_delay: float = 1.0 # Дополнительная задержка для текстовых сообщений
|
||||
text_message_delay: float = 1.0 # Дополнительная задержка для текстовых сообщений
|
||||
|
||||
# Настройки для разных типов чатов
|
||||
private_chat_multiplier: float = 1.0 # Множитель для приватных чатов
|
||||
group_chat_multiplier: float = 0.8 # Множитель для групповых чатов
|
||||
channel_multiplier: float = 0.6 # Множитель для каналов
|
||||
group_chat_multiplier: float = 0.8 # Множитель для групповых чатов
|
||||
channel_multiplier: float = 0.6 # Множитель для каналов
|
||||
|
||||
# Глобальные ограничения
|
||||
global_messages_per_second: float = 10.0 # Максимум 10 сообщений в секунду глобально
|
||||
global_messages_per_second: float = (
|
||||
10.0 # Максимум 10 сообщений в секунду глобально
|
||||
)
|
||||
global_burst_limit: int = 20 # Максимум 20 сообщений подряд глобально
|
||||
|
||||
|
||||
@@ -37,7 +40,7 @@ DEVELOPMENT_CONFIG = RateLimitSettings(
|
||||
burst_limit=3,
|
||||
retry_after_multiplier=1.2,
|
||||
max_retry_delay=15.0,
|
||||
max_retries=2
|
||||
max_retries=2,
|
||||
)
|
||||
|
||||
PRODUCTION_CONFIG = RateLimitSettings(
|
||||
@@ -48,7 +51,7 @@ PRODUCTION_CONFIG = RateLimitSettings(
|
||||
max_retries=3,
|
||||
voice_message_delay=2.5,
|
||||
media_message_delay=2.0,
|
||||
text_message_delay=1.5
|
||||
text_message_delay=1.5,
|
||||
)
|
||||
|
||||
STRICT_CONFIG = RateLimitSettings(
|
||||
@@ -59,7 +62,7 @@ STRICT_CONFIG = RateLimitSettings(
|
||||
max_retries=5,
|
||||
voice_message_delay=3.0,
|
||||
media_message_delay=2.5,
|
||||
text_message_delay=2.0
|
||||
text_message_delay=2.0,
|
||||
)
|
||||
|
||||
|
||||
@@ -76,15 +79,14 @@ def get_rate_limit_config(environment: str = "production") -> RateLimitSettings:
|
||||
configs = {
|
||||
"development": DEVELOPMENT_CONFIG,
|
||||
"production": PRODUCTION_CONFIG,
|
||||
"strict": STRICT_CONFIG
|
||||
"strict": STRICT_CONFIG,
|
||||
}
|
||||
|
||||
return configs.get(environment, PRODUCTION_CONFIG)
|
||||
|
||||
|
||||
def get_adaptive_config(
|
||||
current_error_rate: float,
|
||||
base_config: Optional[RateLimitSettings] = None
|
||||
current_error_rate: float, base_config: Optional[RateLimitSettings] = None
|
||||
) -> RateLimitSettings:
|
||||
"""
|
||||
Получает адаптивную конфигурацию на основе текущего уровня ошибок
|
||||
@@ -109,7 +111,7 @@ def get_adaptive_config(
|
||||
max_retries=base_config.max_retries + 1,
|
||||
voice_message_delay=base_config.voice_message_delay * 1.5,
|
||||
media_message_delay=base_config.media_message_delay * 1.3,
|
||||
text_message_delay=base_config.text_message_delay * 1.2
|
||||
text_message_delay=base_config.text_message_delay * 1.2,
|
||||
)
|
||||
|
||||
# Если уровень ошибок низкий, можно немного ослабить ограничения
|
||||
@@ -122,7 +124,7 @@ def get_adaptive_config(
|
||||
max_retries=max(1, base_config.max_retries - 1),
|
||||
voice_message_delay=base_config.voice_message_delay * 0.8,
|
||||
media_message_delay=base_config.media_message_delay * 0.9,
|
||||
text_message_delay=base_config.text_message_delay * 0.9
|
||||
text_message_delay=base_config.text_message_delay * 0.9,
|
||||
)
|
||||
|
||||
# Возвращаем базовую конфигурацию
|
||||
|
||||
@@ -5,7 +5,7 @@ from aiogram.types import Message
|
||||
|
||||
|
||||
class ChatTypeFilter(BaseFilter): # [1]
|
||||
def __init__(self, chat_type: Union[str, list]): # [2]
|
||||
def __init__(self, chat_type: Union[str, list]): # [2]
|
||||
self.chat_type = chat_type
|
||||
|
||||
async def __call__(self, message: Message) -> bool: # [3]
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
from .admin_handlers import admin_router
|
||||
from .dependencies import AdminAccessMiddleware, BotDB, Settings
|
||||
from .exceptions import (AdminAccessDeniedError, AdminError, InvalidInputError,
|
||||
UserAlreadyBannedError, UserNotFoundError)
|
||||
from .exceptions import (
|
||||
AdminAccessDeniedError,
|
||||
AdminError,
|
||||
InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
from .services import AdminService, BannedUser, User
|
||||
from .utils import (escape_html, format_ban_confirmation, format_user_info,
|
||||
handle_admin_error, return_to_admin_menu)
|
||||
from .utils import (
|
||||
escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'admin_router',
|
||||
'AdminAccessMiddleware',
|
||||
'BotDB',
|
||||
'Settings',
|
||||
'AdminService',
|
||||
'User',
|
||||
'BannedUser',
|
||||
'AdminError',
|
||||
'AdminAccessDeniedError',
|
||||
'UserNotFoundError',
|
||||
'InvalidInputError',
|
||||
'UserAlreadyBannedError',
|
||||
'return_to_admin_menu',
|
||||
'handle_admin_error',
|
||||
'format_user_info',
|
||||
'format_ban_confirmation',
|
||||
'escape_html'
|
||||
"admin_router",
|
||||
"AdminAccessMiddleware",
|
||||
"BotDB",
|
||||
"Settings",
|
||||
"AdminService",
|
||||
"User",
|
||||
"BannedUser",
|
||||
"AdminError",
|
||||
"AdminAccessDeniedError",
|
||||
"UserNotFoundError",
|
||||
"InvalidInputError",
|
||||
"UserAlreadyBannedError",
|
||||
"return_to_admin_menu",
|
||||
"handle_admin_error",
|
||||
"format_user_info",
|
||||
"format_ban_confirmation",
|
||||
"escape_html",
|
||||
]
|
||||
@@ -1,22 +1,30 @@
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, MagicData, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.handlers.admin.dependencies import AdminAccessMiddleware
|
||||
from helper_bot.handlers.admin.exceptions import (InvalidInputError,
|
||||
UserAlreadyBannedError)
|
||||
from helper_bot.handlers.admin.exceptions import (
|
||||
InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
)
|
||||
from helper_bot.handlers.admin.services import AdminService
|
||||
from helper_bot.handlers.admin.utils import (escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu)
|
||||
from helper_bot.keyboards.keyboards import (create_keyboard_for_approve_ban,
|
||||
create_keyboard_for_ban_days,
|
||||
create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin)
|
||||
from helper_bot.handlers.admin.utils import (
|
||||
escape_html,
|
||||
format_ban_confirmation,
|
||||
format_user_info,
|
||||
handle_admin_error,
|
||||
return_to_admin_menu,
|
||||
)
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
create_keyboard_for_approve_ban,
|
||||
create_keyboard_for_ban_days,
|
||||
create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin,
|
||||
)
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -30,23 +38,19 @@ admin_router.message.middleware(AdminAccessMiddleware())
|
||||
# ХЕНДЛЕРЫ МЕНЮ
|
||||
# ============================================================================
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command('admin')
|
||||
)
|
||||
|
||||
@admin_router.message(ChatTypeFilter(chat_type=["private"]), Command("admin"))
|
||||
@track_time("admin_panel", "admin_handlers")
|
||||
@track_errors("admin_handlers", "admin_panel")
|
||||
async def admin_panel(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def admin_panel(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Главное меню администратора"""
|
||||
try:
|
||||
await state.set_state("ADMIN")
|
||||
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
|
||||
markup = get_reply_keyboard_admin()
|
||||
await message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
|
||||
await message.answer(
|
||||
"Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup
|
||||
)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "admin_panel")
|
||||
|
||||
@@ -55,18 +59,20 @@ async def admin_panel(
|
||||
# ХЕНДЛЕР ОТМЕНЫ
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION"),
|
||||
F.text == 'Отменить'
|
||||
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
|
||||
):
|
||||
async def cancel_ban_process(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Отмена процесса блокировки"""
|
||||
try:
|
||||
current_state = await state.get_state()
|
||||
@@ -79,32 +85,31 @@ async def cancel_ban_process(
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
F.text == 'Бан (Список)'
|
||||
F.text == "Бан (Список)",
|
||||
)
|
||||
@track_time("get_last_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_last_users")
|
||||
@db_query_time("get_last_users", "users", "select")
|
||||
async def get_last_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Получение списка последних пользователей"""
|
||||
try:
|
||||
logger.info(f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}")
|
||||
logger.info(
|
||||
f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}"
|
||||
)
|
||||
admin_service = AdminService(bot_db)
|
||||
users = await admin_service.get_last_users()
|
||||
|
||||
# Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination)
|
||||
users_data = [
|
||||
(user.full_name, user.user_id)
|
||||
for user in users
|
||||
]
|
||||
users_data = [(user.full_name, user.user_id) for user in users]
|
||||
|
||||
keyboard = create_keyboard_with_pagination(1, len(users_data), users_data, 'ban')
|
||||
keyboard = create_keyboard_with_pagination(
|
||||
1, len(users_data), users_data, "ban"
|
||||
)
|
||||
await message.answer(
|
||||
text="Список пользователей которые последними обращались к боту",
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "get_last_users")
|
||||
@@ -113,27 +118,31 @@ async def get_last_users(
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
F.text == 'Разбан (список)'
|
||||
F.text == "Разбан (список)",
|
||||
)
|
||||
@track_time("get_banned_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_banned_users")
|
||||
@db_query_time("get_banned_users", "users", "select")
|
||||
async def get_banned_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Получение списка заблокированных пользователей"""
|
||||
try:
|
||||
logger.info(f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}")
|
||||
logger.info(
|
||||
f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}"
|
||||
)
|
||||
admin_service = AdminService(bot_db)
|
||||
message_text, buttons_list = await admin_service.get_banned_users_for_display(0)
|
||||
|
||||
if buttons_list:
|
||||
keyboard = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
|
||||
keyboard = create_keyboard_with_pagination(
|
||||
1, len(buttons_list), buttons_list, "unlock"
|
||||
)
|
||||
await message.answer(text=message_text, reply_markup=keyboard)
|
||||
else:
|
||||
await message.answer(text="В списке заблокированных пользователей никого нет")
|
||||
await message.answer(
|
||||
text="В списке заблокированных пользователей никого нет"
|
||||
)
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "get_banned_users")
|
||||
|
||||
@@ -141,24 +150,24 @@ async def get_banned_users(
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("ADMIN"),
|
||||
F.text == '📊 ML Статистика'
|
||||
F.text == "📊 ML Статистика",
|
||||
)
|
||||
@track_time("get_ml_stats", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_ml_stats")
|
||||
async def get_ml_stats(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def get_ml_stats(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Получение статистики ML-скоринга"""
|
||||
try:
|
||||
logger.info(f"Запрос ML статистики от пользователя: {message.from_user.full_name}")
|
||||
logger.info(
|
||||
f"Запрос ML статистики от пользователя: {message.from_user.full_name}"
|
||||
)
|
||||
|
||||
bdf = get_global_instance()
|
||||
scoring_manager = bdf.get_scoring_manager()
|
||||
|
||||
if not scoring_manager:
|
||||
await message.answer("📊 ML Scoring отключен\n\nДля включения установите RAG_ENABLED=true или DEEPSEEK_ENABLED=true в .env")
|
||||
await message.answer(
|
||||
"📊 ML Scoring отключен\n\nДля включения установите RAG_ENABLED=true или DEEPSEEK_ENABLED=true в .env"
|
||||
)
|
||||
return
|
||||
|
||||
stats = await scoring_manager.get_stats()
|
||||
@@ -175,8 +184,10 @@ async def get_ml_stats(
|
||||
if "model_loaded" in rag or "vector_store" in rag:
|
||||
# Данные из API /stats
|
||||
if "model_loaded" in rag:
|
||||
model_loaded = rag.get('model_loaded', False)
|
||||
lines.append(f" • Модель загружена: {'✅' if model_loaded else '❌'}")
|
||||
model_loaded = rag.get("model_loaded", False)
|
||||
lines.append(
|
||||
f" • Модель загружена: {'✅' if model_loaded else '❌'}"
|
||||
)
|
||||
if "model_name" in rag:
|
||||
lines.append(f" • Модель: {rag.get('model_name', 'N/A')}")
|
||||
if "device" in rag:
|
||||
@@ -194,14 +205,20 @@ async def get_ml_stats(
|
||||
lines.append(f" • Всего примеров: {total_count}")
|
||||
|
||||
if "vector_dim" in vector_store:
|
||||
lines.append(f" • Размерность векторов: {vector_store.get('vector_dim', 'N/A')}")
|
||||
lines.append(
|
||||
f" • Размерность векторов: {vector_store.get('vector_dim', 'N/A')}"
|
||||
)
|
||||
if "max_examples" in vector_store:
|
||||
lines.append(f" • Макс. примеров: {vector_store.get('max_examples', 'N/A')}")
|
||||
lines.append(
|
||||
f" • Макс. примеров: {vector_store.get('max_examples', 'N/A')}"
|
||||
)
|
||||
else:
|
||||
# Fallback на синхронные данные (если API недоступен)
|
||||
lines.append(f" • API URL: {rag.get('api_url', 'N/A')}")
|
||||
if "enabled" in rag:
|
||||
lines.append(f" • Статус: {'✅ Включен' if rag.get('enabled') else '❌ Отключен'}")
|
||||
lines.append(
|
||||
f" • Статус: {'✅ Включен' if rag.get('enabled') else '❌ Отключен'}"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
|
||||
@@ -209,7 +226,9 @@ async def get_ml_stats(
|
||||
if "deepseek" in stats:
|
||||
ds = stats["deepseek"]
|
||||
lines.append("🔮 <b>DeepSeek API:</b>")
|
||||
lines.append(f" • Статус: {'✅ Включен' if ds.get('enabled') else '❌ Отключен'}")
|
||||
lines.append(
|
||||
f" • Статус: {'✅ Включен' if ds.get('enabled') else '❌ Отключен'}"
|
||||
)
|
||||
lines.append(f" • Модель: {ds.get('model', 'N/A')}")
|
||||
lines.append(f" • Таймаут: {ds.get('timeout', 'N/A')}с")
|
||||
lines.append("")
|
||||
@@ -229,68 +248,80 @@ async def get_ml_stats(
|
||||
# ХЕНДЛЕРЫ ПРОЦЕССА БАНА
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
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(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def start_ban_process(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Начало процесса блокировки пользователя"""
|
||||
try:
|
||||
ban_type = "username" if message.text == 'Бан по нику' else "id"
|
||||
ban_type = "username" if message.text == "Бан по нику" else "id"
|
||||
await state.update_data(ban_type=ban_type)
|
||||
|
||||
prompt_text = "Пришли мне username блокируемого пользователя" if ban_type == "username" else "Пришли мне ID блокируемого пользователя"
|
||||
prompt_text = (
|
||||
"Пришли мне username блокируемого пользователя"
|
||||
if ban_type == "username"
|
||||
else "Пришли мне ID блокируемого пользователя"
|
||||
)
|
||||
await message.answer(prompt_text)
|
||||
await state.set_state('AWAIT_BAN_TARGET')
|
||||
await state.set_state("AWAIT_BAN_TARGET")
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "start_ban_process")
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_TARGET")
|
||||
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_TARGET")
|
||||
)
|
||||
@track_time("process_ban_target", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_target")
|
||||
async def process_ban_target(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Обработка введенного username/ID для блокировки"""
|
||||
logger.info(f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
logger.info(
|
||||
f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}"
|
||||
)
|
||||
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
ban_type = user_data.get('ban_type')
|
||||
ban_type = user_data.get("ban_type")
|
||||
admin_service = AdminService(bot_db)
|
||||
|
||||
logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}")
|
||||
|
||||
# Определяем пользователя
|
||||
if ban_type == "username":
|
||||
logger.info(f"process_ban_target: Поиск пользователя по username: {message.text}")
|
||||
logger.info(
|
||||
f"process_ban_target: Поиск пользователя по username: {message.text}"
|
||||
)
|
||||
user = await admin_service.get_user_by_username(message.text)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с username '{message.text}' не найден")
|
||||
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||||
logger.warning(
|
||||
f"process_ban_target: Пользователь с username '{message.text}' не найден"
|
||||
)
|
||||
await message.answer(
|
||||
f"Пользователь с username '{escape_html(message.text)}' не найден."
|
||||
)
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
else: # ban_type == "id"
|
||||
try:
|
||||
logger.info(f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}")
|
||||
logger.info(
|
||||
f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}"
|
||||
)
|
||||
user_id = await admin_service.validate_user_input(message.text)
|
||||
user = await admin_service.get_user_by_id(user_id)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных")
|
||||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||||
logger.warning(
|
||||
f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных"
|
||||
)
|
||||
await message.answer(
|
||||
f"Пользователь с ID {user_id} не найден в базе данных."
|
||||
)
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
except InvalidInputError as e:
|
||||
@@ -299,25 +330,29 @@ async def process_ban_target(
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
|
||||
logger.info(f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}")
|
||||
logger.info(
|
||||
f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}"
|
||||
)
|
||||
|
||||
# Сохраняем данные пользователя
|
||||
await state.update_data(
|
||||
target_user_id=user.user_id,
|
||||
target_username=user.username,
|
||||
target_full_name=user.full_name
|
||||
target_full_name=user.full_name,
|
||||
)
|
||||
|
||||
# Показываем информацию о пользователе и запрашиваем причину
|
||||
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||||
markup = create_keyboard_for_ban_reason()
|
||||
logger.info(f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}")
|
||||
logger.info(
|
||||
f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
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:
|
||||
@@ -326,18 +361,15 @@ async def process_ban_target(
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_DETAILS")
|
||||
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DETAILS")
|
||||
)
|
||||
@track_time("process_ban_reason", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_reason")
|
||||
async def process_ban_reason(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def process_ban_reason(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Обработка причины блокировки"""
|
||||
logger.info(f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
logger.info(
|
||||
f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Проверяем текущее состояние
|
||||
@@ -348,18 +380,22 @@ async def process_ban_reason(
|
||||
state_data = await state.get_data()
|
||||
logger.info(f"process_ban_reason: Данные состояния: {state_data}")
|
||||
|
||||
logger.info(f"process_ban_reason: Обновление данных состояния с причиной: {message.text}")
|
||||
logger.info(
|
||||
f"process_ban_reason: Обновление данных состояния с причиной: {message.text}"
|
||||
)
|
||||
await state.update_data(ban_reason=message.text)
|
||||
|
||||
markup = create_keyboard_for_ban_days()
|
||||
safe_reason = escape_html(message.text)
|
||||
logger.info(f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}")
|
||||
logger.info(
|
||||
f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
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:
|
||||
@@ -368,44 +404,41 @@ async def process_ban_reason(
|
||||
|
||||
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("AWAIT_BAN_DURATION")
|
||||
ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DURATION")
|
||||
)
|
||||
@track_time("process_ban_duration", "admin_handlers")
|
||||
@track_errors("admin_handlers", "process_ban_duration")
|
||||
async def process_ban_duration(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
**kwargs
|
||||
):
|
||||
async def process_ban_duration(message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Обработка срока блокировки"""
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
|
||||
# Определяем срок блокировки
|
||||
if message.text == 'Навсегда':
|
||||
if message.text == "Навсегда":
|
||||
ban_days = None
|
||||
else:
|
||||
try:
|
||||
ban_days = int(message.text)
|
||||
if ban_days <= 0:
|
||||
await message.answer("Срок блокировки должен быть положительным числом.")
|
||||
await message.answer(
|
||||
"Срок блокировки должен быть положительным числом."
|
||||
)
|
||||
return
|
||||
except ValueError:
|
||||
await message.answer("Пожалуйста, введите корректное число дней или выберите 'Навсегда'.")
|
||||
await message.answer(
|
||||
"Пожалуйста, введите корректное число дней или выберите 'Навсегда'."
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(ban_days=ban_days)
|
||||
|
||||
# Показываем подтверждение
|
||||
confirmation_text = format_ban_confirmation(
|
||||
user_data['target_user_id'],
|
||||
user_data['ban_reason'],
|
||||
ban_days
|
||||
user_data["target_user_id"], user_data["ban_reason"], ban_days
|
||||
)
|
||||
markup = create_keyboard_for_approve_ban()
|
||||
await message.answer(confirmation_text, reply_markup=markup)
|
||||
await state.set_state('BAN_CONFIRMATION')
|
||||
await state.set_state("BAN_CONFIRMATION")
|
||||
|
||||
except Exception as e:
|
||||
await handle_admin_error(message, e, state, "process_ban_duration")
|
||||
@@ -414,32 +447,28 @@ async def process_ban_duration(
|
||||
@admin_router.message(
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
StateFilter("BAN_CONFIRMATION"),
|
||||
F.text == 'Подтвердить'
|
||||
F.text == "Подтвердить",
|
||||
)
|
||||
@track_time("confirm_ban", "admin_handlers")
|
||||
@track_errors("admin_handlers", "confirm_ban")
|
||||
async def confirm_ban(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
**kwargs
|
||||
):
|
||||
message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs
|
||||
):
|
||||
"""Подтверждение блокировки пользователя"""
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
admin_service = AdminService(bot_db)
|
||||
|
||||
|
||||
# Выполняем блокировку
|
||||
await admin_service.ban_user(
|
||||
user_id=user_data['target_user_id'],
|
||||
username=user_data['target_username'],
|
||||
reason=user_data['ban_reason'],
|
||||
ban_days=user_data['ban_days'],
|
||||
user_id=user_data["target_user_id"],
|
||||
username=user_data["target_username"],
|
||||
reason=user_data["ban_reason"],
|
||||
ban_days=user_data["ban_days"],
|
||||
ban_author_id=message.from_user.id,
|
||||
)
|
||||
|
||||
safe_username = escape_html(user_data['target_username'])
|
||||
safe_username = escape_html(user_data["target_username"])
|
||||
await message.reply(f"Пользователь {safe_username} успешно заблокирован.")
|
||||
await return_to_admin_menu(message, state)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ ADMIN_BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||
"BAN_BY_ID": "Бан по ID",
|
||||
"UNBAN_LIST": "Разбан (список)",
|
||||
"RETURN_TO_BOT": "Вернуться в бота",
|
||||
"CANCEL": "Отменить"
|
||||
"CANCEL": "Отменить",
|
||||
}
|
||||
|
||||
# Admin button to command mapping for metrics
|
||||
@@ -19,11 +19,11 @@ ADMIN_BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"Бан по ID": "admin_ban_by_id",
|
||||
"Разбан (список)": "admin_unban_list",
|
||||
"Вернуться в бота": "admin_return_to_bot",
|
||||
"Отменить": "admin_cancel"
|
||||
"Отменить": "admin_cancel",
|
||||
}
|
||||
|
||||
# Admin commands
|
||||
ADMIN_COMMANDS: Final[Dict[str, str]] = {
|
||||
"ADMIN": "admin",
|
||||
"TEST_METRICS": "test_metrics"
|
||||
"TEST_METRICS": "test_metrics",
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ except ImportError:
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from helper_bot.utils.helper_func import check_access
|
||||
from logs.custom_logger import logger
|
||||
@@ -15,27 +16,35 @@ from logs.custom_logger import logger
|
||||
class AdminAccessMiddleware(BaseMiddleware):
|
||||
"""Middleware для проверки административного доступа"""
|
||||
|
||||
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||
if hasattr(event, 'from_user'):
|
||||
async def __call__(
|
||||
self, handler, event: TelegramObject, data: Dict[str, Any]
|
||||
) -> Any:
|
||||
if hasattr(event, "from_user"):
|
||||
user_id = event.from_user.id
|
||||
username = getattr(event.from_user, 'username', 'Unknown')
|
||||
username = getattr(event.from_user, "username", "Unknown")
|
||||
|
||||
logger.info(f"AdminAccessMiddleware: проверка доступа для пользователя {username} (ID: {user_id})")
|
||||
logger.info(
|
||||
f"AdminAccessMiddleware: проверка доступа для пользователя {username} (ID: {user_id})"
|
||||
)
|
||||
|
||||
# Получаем bot_db из data (внедренного DependenciesMiddleware)
|
||||
bot_db = data.get('bot_db')
|
||||
bot_db = data.get("bot_db")
|
||||
if not bot_db:
|
||||
# Fallback: получаем напрямую если middleware не сработала
|
||||
bdf = get_global_instance()
|
||||
bot_db = bdf.get_db()
|
||||
|
||||
is_admin_result = await check_access(user_id, bot_db)
|
||||
logger.info(f"AdminAccessMiddleware: результат проверки для {username}: {is_admin_result}")
|
||||
logger.info(
|
||||
f"AdminAccessMiddleware: результат проверки для {username}: {is_admin_result}"
|
||||
)
|
||||
|
||||
if not is_admin_result:
|
||||
logger.warning(f"AdminAccessMiddleware: доступ запрещен для пользователя {username} (ID: {user_id})")
|
||||
if hasattr(event, 'answer'):
|
||||
await event.answer('Доступ запрещен!')
|
||||
logger.warning(
|
||||
f"AdminAccessMiddleware: доступ запрещен для пользователя {username} (ID: {user_id})"
|
||||
)
|
||||
if hasattr(event, "answer"):
|
||||
await event.answer("Доступ запрещен!")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -43,7 +52,9 @@ class AdminAccessMiddleware(BaseMiddleware):
|
||||
return await handler(event, data)
|
||||
except TypeError as e:
|
||||
if "missing 1 required positional argument: 'data'" in str(e):
|
||||
logger.error(f"Ошибка в AdminAccessMiddleware: {e}. Хендлер не принимает параметр 'data'")
|
||||
logger.error(
|
||||
f"Ошибка в AdminAccessMiddleware: {e}. Хендлер не принимает параметр 'data'"
|
||||
)
|
||||
# Пытаемся вызвать хендлер без data (для совместимости с MagicData)
|
||||
return await handler(event)
|
||||
else:
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
class AdminError(Exception):
|
||||
"""Базовое исключение для административных операций"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AdminAccessDeniedError(AdminError):
|
||||
"""Исключение при отказе в административном доступе"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFoundError(AdminError):
|
||||
"""Исключение при отсутствии пользователя"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidInputError(AdminError):
|
||||
"""Исключение при некорректном вводе данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserAlreadyBannedError(AdminError):
|
||||
"""Исключение при попытке забанить уже заблокированного пользователя"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
"""
|
||||
Обработчики команд для мониторинга rate limiting
|
||||
"""
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, MagicData
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.middlewares.dependencies_middleware import \
|
||||
DependenciesMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import track_errors, track_time
|
||||
from helper_bot.utils.rate_limit_metrics import (
|
||||
get_rate_limit_metrics_summary, update_rate_limit_gauges)
|
||||
from helper_bot.utils.rate_limit_monitor import (get_rate_limit_summary,
|
||||
rate_limit_monitor)
|
||||
get_rate_limit_metrics_summary,
|
||||
update_rate_limit_gauges,
|
||||
)
|
||||
from helper_bot.utils.rate_limit_monitor import (
|
||||
get_rate_limit_summary,
|
||||
rate_limit_monitor,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class RateLimitHandlers:
|
||||
def __init__(self, db, settings):
|
||||
self.db = db.get_db() if hasattr(db, 'get_db') else db
|
||||
self.db = db.get_db() if hasattr(db, "get_db") else db
|
||||
self.settings = settings
|
||||
self.router = Router()
|
||||
self._setup_handlers()
|
||||
@@ -33,28 +39,28 @@ class RateLimitHandlers:
|
||||
self.router.message.register(
|
||||
self.rate_limit_stats_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_stats")
|
||||
Command("ratelimit_stats"),
|
||||
)
|
||||
|
||||
# Команда для сброса статистики rate limiting
|
||||
self.router.message.register(
|
||||
self.reset_rate_limit_stats_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("reset_ratelimit_stats")
|
||||
Command("reset_ratelimit_stats"),
|
||||
)
|
||||
|
||||
# Команда для просмотра ошибок rate limiting
|
||||
self.router.message.register(
|
||||
self.rate_limit_errors_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_errors")
|
||||
Command("ratelimit_errors"),
|
||||
)
|
||||
|
||||
# Команда для просмотра Prometheus метрик
|
||||
self.router.message.register(
|
||||
self.rate_limit_prometheus_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_prometheus")
|
||||
Command("ratelimit_prometheus"),
|
||||
)
|
||||
|
||||
@track_time("rate_limit_stats_handler", "rate_limit_handlers")
|
||||
@@ -64,7 +70,7 @@ class RateLimitHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Показывает статистику rate limiting"""
|
||||
try:
|
||||
@@ -96,7 +102,9 @@ class RateLimitHandlers:
|
||||
stats_text += f"• Неудачных запросов: {global_stats.failed_requests}\n"
|
||||
stats_text += f"• RetryAfter ошибок: {global_stats.retry_after_errors}\n"
|
||||
stats_text += f"• Других ошибок: {global_stats.other_errors}\n"
|
||||
stats_text += f"• Общее время ожидания: {global_stats.total_wait_time:.2f}с\n\n"
|
||||
stats_text += (
|
||||
f"• Общее время ожидания: {global_stats.total_wait_time:.2f}с\n\n"
|
||||
)
|
||||
|
||||
# Добавляем топ чатов по запросам
|
||||
top_chats = rate_limit_monitor.get_top_chats_by_requests(5)
|
||||
@@ -113,7 +121,7 @@ class RateLimitHandlers:
|
||||
for chat_id, chat_stats in high_error_chats[:3]:
|
||||
stats_text += f"• Chat {chat_id}: {chat_stats.error_rate:.1%} ошибок ({chat_stats.failed_requests}/{chat_stats.total_requests})\n"
|
||||
|
||||
await message.answer(stats_text, parse_mode='HTML')
|
||||
await message.answer(stats_text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики rate limiting: {e}")
|
||||
@@ -126,7 +134,7 @@ class RateLimitHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Сбрасывает статистику rate limiting"""
|
||||
try:
|
||||
@@ -151,7 +159,7 @@ class RateLimitHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Показывает недавние ошибки rate limiting"""
|
||||
try:
|
||||
@@ -165,7 +173,9 @@ class RateLimitHandlers:
|
||||
error_summary = rate_limit_monitor.get_error_summary(60)
|
||||
|
||||
if not recent_errors:
|
||||
await message.answer("✅ Ошибок rate limiting за последний час не было.")
|
||||
await message.answer(
|
||||
"✅ Ошибок rate limiting за последний час не было."
|
||||
)
|
||||
return
|
||||
|
||||
# Формируем сообщение с ошибками
|
||||
@@ -179,7 +189,10 @@ class RateLimitHandlers:
|
||||
errors_text += f"🔍 <b>Последние ошибки:</b>\n"
|
||||
for i, error in enumerate(recent_errors[-10:], 1):
|
||||
from datetime import datetime
|
||||
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
|
||||
|
||||
timestamp = datetime.fromtimestamp(error["timestamp"]).strftime(
|
||||
"%H:%M:%S"
|
||||
)
|
||||
errors_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
|
||||
|
||||
# Если сообщение слишком длинное, разбиваем на части
|
||||
@@ -191,22 +204,27 @@ class RateLimitHandlers:
|
||||
summary_text += f"• {error_type}: {count}\n"
|
||||
summary_text += f"\nВсего ошибок: {len(recent_errors)}"
|
||||
|
||||
await message.answer(summary_text, parse_mode='HTML')
|
||||
await message.answer(summary_text, parse_mode="HTML")
|
||||
|
||||
# Отправляем детали отдельным сообщением
|
||||
details_text = f"🔍 <b>Последние ошибки:</b>\n"
|
||||
for i, error in enumerate(recent_errors[-10:], 1):
|
||||
from datetime import datetime
|
||||
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
|
||||
|
||||
timestamp = datetime.fromtimestamp(error["timestamp"]).strftime(
|
||||
"%H:%M:%S"
|
||||
)
|
||||
details_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
|
||||
|
||||
await message.answer(details_text, parse_mode='HTML')
|
||||
await message.answer(details_text, parse_mode="HTML")
|
||||
else:
|
||||
await message.answer(errors_text, parse_mode='HTML')
|
||||
await message.answer(errors_text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении ошибок rate limiting: {e}")
|
||||
await message.answer("Произошла ошибка при получении информации об ошибках.")
|
||||
await message.answer(
|
||||
"Произошла ошибка при получении информации об ошибках."
|
||||
)
|
||||
|
||||
@track_time("rate_limit_prometheus_handler", "rate_limit_handlers")
|
||||
@track_errors("rate_limit_handlers", "rate_limit_prometheus_handler")
|
||||
@@ -215,7 +233,7 @@ class RateLimitHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Показывает Prometheus метрики rate limiting"""
|
||||
try:
|
||||
@@ -244,18 +262,28 @@ class RateLimitHandlers:
|
||||
|
||||
# Добавляем детальные метрики
|
||||
metrics_text += f"🔍 <b>Детальные метрики:</b>\n"
|
||||
metrics_text += f"• Успешных запросов: {metrics_summary['successful_requests']}\n"
|
||||
metrics_text += f"• Неудачных запросов: {metrics_summary['failed_requests']}\n"
|
||||
metrics_text += f"• RetryAfter ошибок: {metrics_summary['retry_after_errors']}\n"
|
||||
metrics_text += (
|
||||
f"• Успешных запросов: {metrics_summary['successful_requests']}\n"
|
||||
)
|
||||
metrics_text += (
|
||||
f"• Неудачных запросов: {metrics_summary['failed_requests']}\n"
|
||||
)
|
||||
metrics_text += (
|
||||
f"• RetryAfter ошибок: {metrics_summary['retry_after_errors']}\n"
|
||||
)
|
||||
metrics_text += f"• Других ошибок: {metrics_summary['other_errors']}\n"
|
||||
metrics_text += f"• Общее время ожидания: {metrics_summary['total_wait_time']:.2f}s\n\n"
|
||||
metrics_text += (
|
||||
f"• Общее время ожидания: {metrics_summary['total_wait_time']:.2f}s\n\n"
|
||||
)
|
||||
|
||||
# Добавляем информацию о доступных метриках
|
||||
metrics_text += f"📈 <b>Доступные Prometheus метрики:</b>\n"
|
||||
metrics_text += f"• rate_limit_requests_total - общее количество запросов\n"
|
||||
metrics_text += f"• rate_limit_errors_total - количество ошибок по типам\n"
|
||||
metrics_text += f"• rate_limit_wait_duration_seconds - время ожидания\n"
|
||||
metrics_text += f"• rate_limit_request_interval_seconds - интервалы между запросами\n"
|
||||
metrics_text += (
|
||||
f"• rate_limit_request_interval_seconds - интервалы между запросами\n"
|
||||
)
|
||||
metrics_text += f"• rate_limit_active_chats - количество активных чатов\n"
|
||||
metrics_text += f"• rate_limit_success_rate - процент успеха по чатам\n"
|
||||
metrics_text += f"• rate_limit_requests_per_minute - запросов в минуту\n"
|
||||
@@ -263,7 +291,7 @@ class RateLimitHandlers:
|
||||
metrics_text += f"• rate_limit_total_errors - количество ошибок\n"
|
||||
metrics_text += f"• rate_limit_avg_wait_time - среднее время ожидания\n"
|
||||
|
||||
await message.answer(metrics_text, parse_mode='HTML')
|
||||
await message.answer(metrics_text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении Prometheus метрик: {e}")
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from helper_bot.handlers.admin.exceptions import (InvalidInputError,
|
||||
UserAlreadyBannedError)
|
||||
from helper_bot.utils.helper_func import (add_days_to_date,
|
||||
get_banned_users_buttons,
|
||||
get_banned_users_list)
|
||||
from helper_bot.handlers.admin.exceptions import (
|
||||
InvalidInputError,
|
||||
UserAlreadyBannedError,
|
||||
)
|
||||
from helper_bot.utils.helper_func import (
|
||||
add_days_to_date,
|
||||
get_banned_users_buttons,
|
||||
get_banned_users_list,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -13,6 +18,7 @@ from logs.custom_logger import logger
|
||||
|
||||
class User:
|
||||
"""Модель пользователя"""
|
||||
|
||||
def __init__(self, user_id: int, username: str, full_name: str):
|
||||
self.user_id = user_id
|
||||
self.username = username
|
||||
@@ -21,7 +27,10 @@ class User:
|
||||
|
||||
class BannedUser:
|
||||
"""Модель заблокированного пользователя"""
|
||||
def __init__(self, user_id: int, username: str, reason: str, unban_date: Optional[datetime]):
|
||||
|
||||
def __init__(
|
||||
self, user_id: int, username: str, reason: str, unban_date: Optional[datetime]
|
||||
):
|
||||
self.user_id = user_id
|
||||
self.username = username
|
||||
self.reason = reason
|
||||
@@ -41,11 +50,7 @@ class AdminService:
|
||||
try:
|
||||
users_data = await self.bot_db.get_last_users(30)
|
||||
return [
|
||||
User(
|
||||
user_id=user[1],
|
||||
username='Неизвестно',
|
||||
full_name=user[0]
|
||||
)
|
||||
User(user_id=user[1], username="Неизвестно", full_name=user[0])
|
||||
for user in users_data
|
||||
]
|
||||
except Exception as e:
|
||||
@@ -66,15 +71,19 @@ class AdminService:
|
||||
full_name = await self.bot_db.get_full_name_by_id(user_id)
|
||||
user_name = username or full_name or f"User_{user_id}"
|
||||
|
||||
banned_users.append(BannedUser(
|
||||
user_id=user_id,
|
||||
username=user_name,
|
||||
reason=reason,
|
||||
unban_date=unban_date
|
||||
))
|
||||
banned_users.append(
|
||||
BannedUser(
|
||||
user_id=user_id,
|
||||
username=user_name,
|
||||
reason=reason,
|
||||
unban_date=unban_date,
|
||||
)
|
||||
)
|
||||
return banned_users
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении списка заблокированных пользователей: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при получении списка заблокированных пользователей: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
@track_time("get_user_by_username", "admin_service")
|
||||
@@ -88,9 +97,7 @@ class AdminService:
|
||||
|
||||
full_name = await self.bot_db.get_full_name_by_id(user_id)
|
||||
return User(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
full_name=full_name or 'Неизвестно'
|
||||
user_id=user_id, username=username, full_name=full_name or "Неизвестно"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске пользователя по username {username}: {e}")
|
||||
@@ -107,8 +114,8 @@ class AdminService:
|
||||
|
||||
return User(
|
||||
user_id=user_id,
|
||||
username=user_info.username or 'Неизвестно',
|
||||
full_name=user_info.full_name or 'Неизвестно'
|
||||
username=user_info.username or "Неизвестно",
|
||||
full_name=user_info.full_name or "Неизвестно",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске пользователя по ID {user_id}: {e}")
|
||||
@@ -116,7 +123,14 @@ class AdminService:
|
||||
|
||||
@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], ban_author_id: int) -> None:
|
||||
async def ban_user(
|
||||
self,
|
||||
user_id: int,
|
||||
username: str,
|
||||
reason: str,
|
||||
ban_days: Optional[int],
|
||||
ban_author_id: int,
|
||||
) -> None:
|
||||
"""Заблокировать пользователя"""
|
||||
try:
|
||||
# Проверяем, не заблокирован ли уже пользователь
|
||||
@@ -129,9 +143,13 @@ class AdminService:
|
||||
date_to_unban = add_days_to_date(ban_days)
|
||||
|
||||
# Сохраняем в БД (username больше не передается, так как не используется в новой схеме)
|
||||
await self.bot_db.set_user_blacklist(user_id, None, reason, date_to_unban, ban_author=ban_author_id)
|
||||
await self.bot_db.set_user_blacklist(
|
||||
user_id, None, reason, date_to_unban, ban_author=ban_author_id
|
||||
)
|
||||
|
||||
logger.info(f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней")
|
||||
logger.info(
|
||||
f"Пользователь {user_id} ({username}) заблокирован. Причина: {reason}, срок: {ban_days} дней"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при блокировке пользователя {user_id}: {e}")
|
||||
@@ -155,7 +173,9 @@ class AdminService:
|
||||
try:
|
||||
user_id = int(input_text.strip())
|
||||
if user_id <= 0:
|
||||
raise InvalidInputError("ID пользователя должен быть положительным числом")
|
||||
raise InvalidInputError(
|
||||
"ID пользователя должен быть положительным числом"
|
||||
)
|
||||
return user_id
|
||||
except ValueError:
|
||||
raise InvalidInputError("ID пользователя должен быть числом")
|
||||
@@ -170,5 +190,7 @@ class AdminService:
|
||||
buttons_list = await get_banned_users_buttons(self.bot_db)
|
||||
return message_text, buttons_list
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении данных заблокированных пользователей: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при получении данных заблокированных пользователей: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.handlers.admin.exceptions import AdminError
|
||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin
|
||||
from logs.custom_logger import logger
|
||||
@@ -13,25 +14,33 @@ def escape_html(text: str) -> str:
|
||||
return html.escape(str(text)) if text else ""
|
||||
|
||||
|
||||
async def return_to_admin_menu(message: types.Message, state: FSMContext,
|
||||
additional_message: Optional[str] = None) -> None:
|
||||
async def return_to_admin_menu(
|
||||
message: types.Message, state: FSMContext, additional_message: Optional[str] = None
|
||||
) -> None:
|
||||
"""Универсальная функция для возврата в админ-меню"""
|
||||
logger.info(f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}")
|
||||
logger.info(
|
||||
f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}"
|
||||
)
|
||||
|
||||
await state.set_data({})
|
||||
await state.set_state("ADMIN")
|
||||
markup = get_reply_keyboard_admin()
|
||||
|
||||
if additional_message:
|
||||
logger.info(f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}")
|
||||
logger.info(
|
||||
f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}"
|
||||
)
|
||||
await message.answer(additional_message)
|
||||
|
||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
||||
logger.info(f"return_to_admin_menu: Пользователь {message.from_user.id} успешно возвращен в админ-меню")
|
||||
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,
|
||||
state: FSMContext, error_context: str = "") -> None:
|
||||
async def handle_admin_error(
|
||||
message: types.Message, error: Exception, state: FSMContext, error_context: str = ""
|
||||
) -> None:
|
||||
"""Централизованная обработка ошибок административных операций"""
|
||||
logger.error(f"Ошибка в {error_context}: {error}")
|
||||
|
||||
@@ -48,10 +57,12 @@ def format_user_info(user_id: int, username: str, full_name: str) -> str:
|
||||
safe_username = escape_html(username)
|
||||
safe_full_name = escape_html(full_name)
|
||||
|
||||
return (f"<b>Выбран пользователь:</b>\n"
|
||||
f"<b>ID:</b> {user_id}\n"
|
||||
f"<b>Username:</b> {safe_username}\n"
|
||||
f"<b>Имя:</b> {safe_full_name}")
|
||||
return (
|
||||
f"<b>Выбран пользователь:</b>\n"
|
||||
f"<b>ID:</b> {user_id}\n"
|
||||
f"<b>Username:</b> {safe_username}\n"
|
||||
f"<b>Имя:</b> {safe_full_name}"
|
||||
)
|
||||
|
||||
|
||||
def format_ban_confirmation(user_id: int, reason: str, ban_days: Optional[int]) -> str:
|
||||
@@ -59,7 +70,9 @@ def format_ban_confirmation(user_id: int, reason: str, ban_days: Optional[int])
|
||||
safe_reason = escape_html(reason)
|
||||
ban_text = "Навсегда" if ban_days is None else f"{ban_days} дней"
|
||||
|
||||
return (f"<b>Необходимо подтверждение:</b>\n"
|
||||
f"<b>Пользователь:</b> {user_id}\n"
|
||||
f"<b>Причина бана:</b> {safe_reason}\n"
|
||||
f"<b>Срок бана:</b> {ban_text}")
|
||||
return (
|
||||
f"<b>Необходимо подтверждение:</b>\n"
|
||||
f"<b>Пользователь:</b> {user_id}\n"
|
||||
f"<b>Причина бана:</b> {safe_reason}\n"
|
||||
f"<b>Срок бана:</b> {ban_text}"
|
||||
)
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
from .callback_handlers import callback_router
|
||||
from .constants import (CALLBACK_BAN, CALLBACK_DECLINE, CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH, CALLBACK_RETURN, CALLBACK_UNLOCK)
|
||||
from .exceptions import (BanError, PostNotFoundError, PublishError,
|
||||
UserBlockedBotError, UserNotFoundError)
|
||||
from .constants import (
|
||||
CALLBACK_BAN,
|
||||
CALLBACK_DECLINE,
|
||||
CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH,
|
||||
CALLBACK_RETURN,
|
||||
CALLBACK_UNLOCK,
|
||||
)
|
||||
from .exceptions import (
|
||||
BanError,
|
||||
PostNotFoundError,
|
||||
PublishError,
|
||||
UserBlockedBotError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
from .services import BanService, PostPublishService
|
||||
|
||||
__all__ = [
|
||||
'callback_router',
|
||||
'PostPublishService',
|
||||
'BanService',
|
||||
'UserBlockedBotError',
|
||||
'PostNotFoundError',
|
||||
'UserNotFoundError',
|
||||
'PublishError',
|
||||
'BanError',
|
||||
'CALLBACK_PUBLISH',
|
||||
'CALLBACK_DECLINE',
|
||||
'CALLBACK_BAN',
|
||||
'CALLBACK_UNLOCK',
|
||||
'CALLBACK_RETURN',
|
||||
'CALLBACK_PAGE'
|
||||
"callback_router",
|
||||
"PostPublishService",
|
||||
"BanService",
|
||||
"UserBlockedBotError",
|
||||
"PostNotFoundError",
|
||||
"UserNotFoundError",
|
||||
"PublishError",
|
||||
"BanError",
|
||||
"CALLBACK_PUBLISH",
|
||||
"CALLBACK_DECLINE",
|
||||
"CALLBACK_BAN",
|
||||
"CALLBACK_UNLOCK",
|
||||
"CALLBACK_RETURN",
|
||||
"CALLBACK_PAGE",
|
||||
]
|
||||
|
||||
@@ -7,28 +7,49 @@ from aiogram import F, Router
|
||||
from aiogram.filters import MagicData
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from helper_bot.handlers.admin.utils import format_user_info
|
||||
from helper_bot.handlers.voice.constants import CALLBACK_DELETE, CALLBACK_SAVE
|
||||
from helper_bot.handlers.voice.services import AudioFileService
|
||||
from helper_bot.keyboards.keyboards import (create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin)
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
create_keyboard_for_ban_reason,
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard_admin,
|
||||
)
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from helper_bot.utils.helper_func import (get_banned_users_buttons,
|
||||
get_banned_users_list)
|
||||
from helper_bot.utils.helper_func import get_banned_users_buttons, get_banned_users_list
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_file_operations, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_file_operations,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
from .constants import (CALLBACK_BAN, CALLBACK_DECLINE, CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH, CALLBACK_RETURN, CALLBACK_UNLOCK,
|
||||
ERROR_BOT_BLOCKED, MESSAGE_DECLINED, MESSAGE_ERROR,
|
||||
MESSAGE_PUBLISHED, MESSAGE_USER_BANNED,
|
||||
MESSAGE_USER_UNLOCKED)
|
||||
from .constants import (
|
||||
CALLBACK_BAN,
|
||||
CALLBACK_DECLINE,
|
||||
CALLBACK_PAGE,
|
||||
CALLBACK_PUBLISH,
|
||||
CALLBACK_RETURN,
|
||||
CALLBACK_UNLOCK,
|
||||
ERROR_BOT_BLOCKED,
|
||||
MESSAGE_DECLINED,
|
||||
MESSAGE_ERROR,
|
||||
MESSAGE_PUBLISHED,
|
||||
MESSAGE_USER_BANNED,
|
||||
MESSAGE_USER_UNLOCKED,
|
||||
)
|
||||
from .dependency_factory import get_ban_service, get_post_publish_service
|
||||
from .exceptions import (BanError, PostNotFoundError, PublishError,
|
||||
UserBlockedBotError, UserNotFoundError)
|
||||
from .exceptions import (
|
||||
BanError,
|
||||
PostNotFoundError,
|
||||
PublishError,
|
||||
UserBlockedBotError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
|
||||
callback_router = Router()
|
||||
|
||||
@@ -36,14 +57,12 @@ callback_router = Router()
|
||||
@callback_router.callback_query(F.data == CALLBACK_PUBLISH)
|
||||
@track_time("post_for_group", "callback_handlers")
|
||||
@track_errors("callback_handlers", "post_for_group")
|
||||
async def post_for_group(
|
||||
call: CallbackQuery,
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
async def post_for_group(call: CallbackQuery, settings: MagicData("settings")):
|
||||
publish_service = get_post_publish_service()
|
||||
# TODO: переделать на MagicData
|
||||
logger.info(
|
||||
f'Получен callback-запрос с действием: {call.data} от пользователя {call.from_user.full_name} (ID сообщения: {call.message.message_id})')
|
||||
f"Получен callback-запрос с действием: {call.data} от пользователя {call.from_user.full_name} (ID сообщения: {call.message.message_id})"
|
||||
)
|
||||
|
||||
try:
|
||||
await publish_service.publish_post(call)
|
||||
@@ -51,50 +70,48 @@ async def post_for_group(
|
||||
except UserBlockedBotError:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except (PostNotFoundError, PublishError) as e:
|
||||
logger.error(f'Ошибка при публикации поста: {str(e)}')
|
||||
logger.error(f"Ошибка при публикации поста: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
else:
|
||||
important_logs = settings['Telegram']['important_logs']
|
||||
important_logs = settings["Telegram"]["important_logs"]
|
||||
await call.bot.send_message(
|
||||
chat_id=important_logs,
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}",
|
||||
)
|
||||
logger.error(f'Неожиданная ошибка при публикации поста: {str(e)}')
|
||||
logger.error(f"Неожиданная ошибка при публикации поста: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_DECLINE)
|
||||
@track_time("decline_post_for_group", "callback_handlers")
|
||||
@track_errors("callback_handlers", "decline_post_for_group")
|
||||
async def decline_post_for_group(
|
||||
call: CallbackQuery,
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
async def decline_post_for_group(call: CallbackQuery, settings: MagicData("settings")):
|
||||
publish_service = get_post_publish_service()
|
||||
# TODO: переделать на MagicData
|
||||
logger.info(
|
||||
f'Получен callback-запрос с данными: {call.data} от пользователя {call.from_user.full_name} (ID: {call.from_user.id})')
|
||||
f"Получен callback-запрос с данными: {call.data} от пользователя {call.from_user.full_name} (ID: {call.from_user.id})"
|
||||
)
|
||||
try:
|
||||
await publish_service.decline_post(call)
|
||||
await call.answer(text=MESSAGE_DECLINED, cache_time=3)
|
||||
except UserBlockedBotError:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except (PostNotFoundError, PublishError) as e:
|
||||
logger.error(f'Ошибка при отклонении поста: {str(e)}')
|
||||
logger.error(f"Ошибка при отклонении поста: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
else:
|
||||
important_logs = settings['Telegram']['important_logs']
|
||||
important_logs = settings["Telegram"]["important_logs"]
|
||||
await call.bot.send_message(
|
||||
chat_id=important_logs,
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}",
|
||||
)
|
||||
logger.error(f'Неожиданная ошибка при отклонении поста: {str(e)}')
|
||||
logger.error(f"Неожиданная ошибка при отклонении поста: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
|
||||
|
||||
@@ -110,31 +127,37 @@ async def ban_user_from_post(call: CallbackQuery, **kwargs):
|
||||
except UserBlockedBotError:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except (UserNotFoundError, BanError) as e:
|
||||
logger.error(f'Ошибка при блокировке пользователя: {str(e)}')
|
||||
logger.error(f"Ошибка при блокировке пользователя: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
else:
|
||||
logger.error(f'Неожиданная ошибка при блокировке пользователя: {str(e)}')
|
||||
logger.error(f"Неожиданная ошибка при блокировке пользователя: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_BAN))
|
||||
@track_time("process_ban_user", "callback_handlers")
|
||||
@track_errors("callback_handlers", "process_ban_user")
|
||||
async def process_ban_user(call: CallbackQuery, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs):
|
||||
async def process_ban_user(
|
||||
call: CallbackQuery, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs
|
||||
):
|
||||
ban_service = get_ban_service()
|
||||
# TODO: переделать на MagicData
|
||||
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:
|
||||
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)
|
||||
await call.answer(
|
||||
text="Ошибка: некорректный ID пользователя", show_alert=True, cache_time=3
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -146,13 +169,13 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext, bot_db: Magic
|
||||
# Получаем full_name пользователя из базы данных
|
||||
full_name = await bot_db.get_full_name_by_id(user_id_int)
|
||||
if not full_name:
|
||||
full_name = 'Неизвестно'
|
||||
full_name = "Неизвестно"
|
||||
|
||||
# Сохраняем данные в формате, совместимом с admin_handlers
|
||||
await state.update_data(
|
||||
target_user_id=user_id_int,
|
||||
target_username=username,
|
||||
target_full_name=full_name
|
||||
target_full_name=full_name,
|
||||
)
|
||||
|
||||
# Используем единый формат отображения информации о пользователе
|
||||
@@ -161,14 +184,18 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext, bot_db: Magic
|
||||
|
||||
await call.message.answer(
|
||||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
await state.set_state("AWAIT_BAN_DETAILS")
|
||||
logger.info(
|
||||
f"process_ban_user: Состояние изменено на AWAIT_BAN_DETAILS для пользователя {user_id_int}"
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DETAILS')
|
||||
logger.info(f"process_ban_user: Состояние изменено на AWAIT_BAN_DETAILS для пользователя {user_id_int}")
|
||||
except UserNotFoundError:
|
||||
markup = get_reply_keyboard_admin()
|
||||
await call.message.answer(text='Пользователь с таким ID не найден в базе', reply_markup=markup)
|
||||
await state.set_state('ADMIN')
|
||||
await call.message.answer(
|
||||
text="Пользователь с таким ID не найден в базе", reply_markup=markup
|
||||
)
|
||||
await state.set_state("ADMIN")
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_UNLOCK))
|
||||
@@ -184,16 +211,20 @@ async def process_unlock_user(call: CallbackQuery, **kwargs):
|
||||
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)
|
||||
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:
|
||||
await call.answer(text='Пользователь не найден в базе', show_alert=True, cache_time=3)
|
||||
await call.answer(
|
||||
text="Пользователь не найден в базе", show_alert=True, cache_time=3
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка при разблокировке пользователя: {str(e)}')
|
||||
logger.error(f"Ошибка при разблокировке пользователя: {str(e)}")
|
||||
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||
|
||||
|
||||
@@ -204,48 +235,52 @@ async def return_to_main_menu(call: CallbackQuery, **kwargs):
|
||||
await call.message.delete()
|
||||
logger.info(f"Запуск админ панели для пользователя: {call.message.from_user.id}")
|
||||
markup = get_reply_keyboard_admin()
|
||||
await call.message.answer("Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup)
|
||||
await call.message.answer(
|
||||
"Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup
|
||||
)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data.contains(CALLBACK_PAGE))
|
||||
@track_time("change_page", "callback_handlers")
|
||||
@track_errors("callback_handlers", "change_page")
|
||||
async def change_page(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
**kwargs
|
||||
):
|
||||
async def change_page(call: CallbackQuery, bot_db: MagicData("bot_db"), **kwargs):
|
||||
try:
|
||||
page_number = int(call.data[5:])
|
||||
except ValueError:
|
||||
logger.error(f"Некорректный номер страницы в callback: {call.data}")
|
||||
await call.answer(text="Ошибка: некорректный номер страницы", show_alert=True, cache_time=3)
|
||||
await call.answer(
|
||||
text="Ошибка: некорректный номер страницы", show_alert=True, cache_time=3
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(f"Переход на страницу {page_number}")
|
||||
|
||||
if call.message.text == 'Список пользователей которые последними обращались к боту':
|
||||
if call.message.text == "Список пользователей которые последними обращались к боту":
|
||||
list_users = await bot_db.get_last_users(30)
|
||||
keyboard = create_keyboard_with_pagination(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(
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
else:
|
||||
message_user = await get_banned_users_list(int(page_number) * 7 - 7, bot_db)
|
||||
await call.bot.edit_message_text(
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
text=message_user
|
||||
text=message_user,
|
||||
)
|
||||
|
||||
buttons = await get_banned_users_buttons(bot_db)
|
||||
keyboard = create_keyboard_with_pagination(page_number, len(buttons), buttons, 'unlock')
|
||||
keyboard = create_keyboard_with_pagination(
|
||||
page_number, len(buttons), buttons, "unlock"
|
||||
)
|
||||
await call.bot.edit_message_reply_markup(
|
||||
chat_id=call.message.chat.id,
|
||||
message_id=call.message.message_id,
|
||||
reply_markup=keyboard
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
|
||||
|
||||
@@ -258,16 +293,20 @@ async def save_voice_message(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
**kwargs
|
||||
):
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
logger.info(f"Начинаем сохранение голосового сообщения. Message ID: {call.message.message_id}")
|
||||
logger.info(
|
||||
f"Начинаем сохранение голосового сообщения. Message ID: {call.message.message_id}"
|
||||
)
|
||||
|
||||
# Создаем сервис для работы с аудио файлами
|
||||
audio_service = AudioFileService(bot_db)
|
||||
|
||||
# Получаем ID пользователя из базы
|
||||
user_id = await bot_db.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
|
||||
user_id = await bot_db.get_user_id_by_message_id_for_voice_bot(
|
||||
call.message.message_id
|
||||
)
|
||||
logger.info(f"Получен user_id: {user_id}")
|
||||
|
||||
# Генерируем имя файла
|
||||
@@ -295,8 +334,8 @@ async def save_voice_message(
|
||||
# Удаляем сообщение из предложки
|
||||
logger.info("Удаляем сообщение из предложки...")
|
||||
await call.bot.delete_message(
|
||||
chat_id=settings['Telegram']['group_for_posts'],
|
||||
message_id=call.message.message_id
|
||||
chat_id=settings["Telegram"]["group_for_posts"],
|
||||
message_id=call.message.message_id,
|
||||
)
|
||||
logger.info("Сообщение удалено из предложки")
|
||||
|
||||
@@ -305,7 +344,7 @@ async def save_voice_message(
|
||||
await bot_db.delete_audio_moderate_record(call.message.message_id)
|
||||
logger.info("Запись удалена из таблицы audio_moderate")
|
||||
|
||||
await call.answer(text='Сохранено!', cache_time=3)
|
||||
await call.answer(text="Сохранено!", cache_time=3)
|
||||
logger.info(f"Голосовое сообщение успешно сохранено: {file_name}")
|
||||
|
||||
except Exception as e:
|
||||
@@ -314,14 +353,18 @@ async def save_voice_message(
|
||||
|
||||
# Дополнительная информация для диагностики
|
||||
try:
|
||||
if 'call' in locals() and call.message:
|
||||
if "call" in locals() and call.message:
|
||||
logger.error(f"Message ID: {call.message.message_id}")
|
||||
logger.error(f"User ID: {user_id if 'user_id' in locals() else 'не определен'}")
|
||||
logger.error(f"File name: {file_name if 'file_name' in locals() else 'не определен'}")
|
||||
logger.error(
|
||||
f"User ID: {user_id if 'user_id' in locals() else 'не определен'}"
|
||||
)
|
||||
logger.error(
|
||||
f"File name: {file_name if 'file_name' in locals() else 'не определен'}"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
await call.answer(text='Ошибка при сохранении!', cache_time=3)
|
||||
await call.answer(text="Ошибка при сохранении!", cache_time=3)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_DELETE)
|
||||
@@ -332,20 +375,20 @@ async def delete_voice_message(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
**kwargs
|
||||
):
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
# Удаляем сообщение из предложки
|
||||
await call.bot.delete_message(
|
||||
chat_id=settings['Telegram']['group_for_posts'],
|
||||
message_id=call.message.message_id
|
||||
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)
|
||||
await call.answer(text="Удалено!", cache_time=3)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении голосового сообщения: {e}")
|
||||
await call.answer(text='Ошибка при удалении!', cache_time=3)
|
||||
await call.answer(text="Ошибка при удалении!", cache_time=3)
|
||||
|
||||
@@ -37,5 +37,5 @@ CALLBACK_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"ban": "ban",
|
||||
"unlock": "unlock",
|
||||
"return": "return",
|
||||
"page": "page"
|
||||
"page": "page",
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Callable
|
||||
from aiogram import Bot
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
|
||||
from .services import BanService, PostPublishService
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
class UserBlockedBotError(Exception):
|
||||
"""Исключение, возникающее когда пользователь заблокировал бота"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PostNotFoundError(Exception):
|
||||
"""Исключение, возникающее когда пост не найден в базе данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFoundError(Exception):
|
||||
"""Исключение, возникающее когда пользователь не найден в базе данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PublishError(Exception):
|
||||
"""Общее исключение для ошибок публикации"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BanError(Exception):
|
||||
"""Исключение для ошибок бана/разбана пользователей"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -4,41 +4,69 @@ from typing import Any, Dict
|
||||
|
||||
from aiogram import Bot, types
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason
|
||||
from helper_bot.utils.helper_func import (delete_user_blacklist,
|
||||
get_text_message, send_audio_message,
|
||||
send_media_group_to_channel,
|
||||
send_photo_message,
|
||||
send_text_message,
|
||||
send_video_message,
|
||||
send_video_note_message,
|
||||
send_voice_message)
|
||||
from helper_bot.utils.helper_func import (
|
||||
delete_user_blacklist,
|
||||
get_text_message,
|
||||
send_audio_message,
|
||||
send_media_group_to_channel,
|
||||
send_photo_message,
|
||||
send_text_message,
|
||||
send_video_message,
|
||||
send_video_note_message,
|
||||
send_voice_message,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_media_processing, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_media_processing,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
from .constants import (CONTENT_TYPE_AUDIO, CONTENT_TYPE_MEDIA_GROUP,
|
||||
CONTENT_TYPE_PHOTO, CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE,
|
||||
CONTENT_TYPE_VOICE, ERROR_BOT_BLOCKED,
|
||||
MESSAGE_POST_DECLINED, MESSAGE_POST_PUBLISHED,
|
||||
MESSAGE_USER_BANNED_SPAM)
|
||||
from .exceptions import (BanError, PostNotFoundError, PublishError,
|
||||
UserBlockedBotError, UserNotFoundError)
|
||||
from .constants import (
|
||||
CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_MEDIA_GROUP,
|
||||
CONTENT_TYPE_PHOTO,
|
||||
CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_VIDEO,
|
||||
CONTENT_TYPE_VIDEO_NOTE,
|
||||
CONTENT_TYPE_VOICE,
|
||||
ERROR_BOT_BLOCKED,
|
||||
MESSAGE_POST_DECLINED,
|
||||
MESSAGE_POST_PUBLISHED,
|
||||
MESSAGE_USER_BANNED_SPAM,
|
||||
)
|
||||
from .exceptions import (
|
||||
BanError,
|
||||
PostNotFoundError,
|
||||
PublishError,
|
||||
UserBlockedBotError,
|
||||
UserNotFoundError,
|
||||
)
|
||||
|
||||
|
||||
class PostPublishService:
|
||||
def __init__(self, bot: Bot, db, settings: Dict[str, Any], s3_storage=None, scoring_manager=None):
|
||||
def __init__(
|
||||
self,
|
||||
bot: Bot,
|
||||
db,
|
||||
settings: Dict[str, Any],
|
||||
s3_storage=None,
|
||||
scoring_manager=None,
|
||||
):
|
||||
# bot может быть None - в этом случае используем бота из контекста сообщения
|
||||
self.bot = bot
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.s3_storage = s3_storage
|
||||
self.scoring_manager = scoring_manager
|
||||
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||
self.main_public = settings['Telegram']['main_public']
|
||||
self.important_logs = settings['Telegram']['important_logs']
|
||||
self.group_for_posts = settings["Telegram"]["group_for_posts"]
|
||||
self.main_public = settings["Telegram"]["main_public"]
|
||||
self.important_logs = settings["Telegram"]["important_logs"]
|
||||
|
||||
def _get_bot(self, message) -> Bot:
|
||||
"""Получает бота из контекста сообщения или использует переданного"""
|
||||
@@ -83,13 +111,23 @@ class PostPublishService:
|
||||
"""Публикация текстового поста"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
@@ -99,18 +137,24 @@ class PostPublishService:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_text_message(self.main_public, call.message, formatted_text)
|
||||
sent_message = await send_text_message(
|
||||
self.main_public, call.message, formatted_text
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Текст сообщение опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Текст сообщение опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_photo_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_photo_post")
|
||||
@@ -118,13 +162,23 @@ class PostPublishService:
|
||||
"""Публикация поста с фото"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
@@ -134,21 +188,32 @@ class PostPublishService:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_photo_message(self.main_public, call.message, call.message.photo[-1].file_id, formatted_text)
|
||||
sent_message = await send_photo_message(
|
||||
self.main_public,
|
||||
call.message,
|
||||
call.message.photo[-1].file_id,
|
||||
formatted_text,
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с фото опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с фото опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_video_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_video_post")
|
||||
@@ -156,13 +221,23 @@ class PostPublishService:
|
||||
"""Публикация поста с видео"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
@@ -172,21 +247,29 @@ class PostPublishService:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_video_message(self.main_public, call.message, call.message.video.file_id, formatted_text)
|
||||
sent_message = await send_video_message(
|
||||
self.main_public, call.message, call.message.video.file_id, formatted_text
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с видео опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с видео опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_video_note_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_video_note_post")
|
||||
@@ -194,24 +277,36 @@ class PostPublishService:
|
||||
"""Публикация поста с кружком"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
sent_message = await send_video_note_message(self.main_public, call.message, call.message.video_note.file_id)
|
||||
sent_message = await send_video_note_message(
|
||||
self.main_public, call.message, call.message.video_note.file_id
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с кружком опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с кружком опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_audio_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_audio_post")
|
||||
@@ -219,13 +314,23 @@ class PostPublishService:
|
||||
"""Публикация поста с аудио"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
# Получаем сырой текст и is_anonymous из базы
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_message_id(call.message.message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
@@ -235,21 +340,29 @@ class PostPublishService:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
|
||||
# Формируем финальный текст с учетом is_anonymous
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
sent_message = await send_audio_message(self.main_public, call.message, call.message.audio.file_id, formatted_text)
|
||||
sent_message = await send_audio_message(
|
||||
self.main_public, call.message, call.message.audio.file_id, formatted_text
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с аудио опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с аудио опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_voice_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_voice_post")
|
||||
@@ -257,24 +370,36 @@ class PostPublishService:
|
||||
"""Публикация поста с войсом"""
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "approved")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "approved"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'approved'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с message_id={call.message.message_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
sent_message = await send_voice_message(self.main_public, call.message, call.message.voice.file_id)
|
||||
sent_message = await send_voice_message(
|
||||
self.main_public, call.message, call.message.voice.file_id
|
||||
)
|
||||
|
||||
# Сохраняем published_message_id
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=call.message.message_id,
|
||||
published_message_id=sent_message.message_id
|
||||
published_message_id=sent_message.message_id,
|
||||
)
|
||||
|
||||
# Сохраняем медиафайл из опубликованного поста (используем уже сохраненный файл)
|
||||
await self._save_published_post_content(sent_message, sent_message.message_id, call.message.message_id)
|
||||
await self._save_published_post_content(
|
||||
sent_message, sent_message.message_id, call.message.message_id
|
||||
)
|
||||
|
||||
await self._delete_post_and_notify_author(call, author_id)
|
||||
logger.info(f'Пост с войсом опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}.')
|
||||
logger.info(
|
||||
f"Пост с войсом опубликован в канале {self.main_public}, published_message_id={sent_message.message_id}."
|
||||
)
|
||||
|
||||
@track_time("_publish_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_media_group")
|
||||
@@ -284,45 +409,68 @@ class PostPublishService:
|
||||
try:
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
if not media_group_message_ids:
|
||||
logger.error(f"_publish_media_group: Не найдены message_id медиагруппы для helper_message_id={helper_message_id}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Не найдены message_id медиагруппы для helper_message_id={helper_message_id}"
|
||||
)
|
||||
raise PublishError("Не найдены message_id медиагруппы в базе данных")
|
||||
|
||||
post_content = await self.db.get_post_content_by_helper_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"_publish_media_group: Контент медиагруппы не найден в базе данных для helper_message_id={helper_message_id}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Контент медиагруппы не найден в базе данных для helper_message_id={helper_message_id}"
|
||||
)
|
||||
raise PublishError("Контент медиагруппы не найден в базе данных")
|
||||
|
||||
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_helper_id(helper_message_id)
|
||||
raw_text, is_anonymous = (
|
||||
await self.db.get_post_text_and_anonymity_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
)
|
||||
if raw_text is None:
|
||||
raw_text = ""
|
||||
|
||||
author_id = await self.db.get_author_id_by_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"_publish_media_group: Автор не найден для медиагруппы helper_message_id={helper_message_id}")
|
||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {helper_message_id}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Автор не найден для медиагруппы helper_message_id={helper_message_id}"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Автор не найден для медиагруппы {helper_message_id}"
|
||||
)
|
||||
|
||||
user = await self.db.get_user_by_id(author_id)
|
||||
if not user:
|
||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||
raise PostNotFoundError(
|
||||
f"Пользователь {author_id} не найден в базе данных"
|
||||
)
|
||||
|
||||
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
|
||||
formatted_text = get_text_message(
|
||||
raw_text, user.first_name, user.username, is_anonymous
|
||||
)
|
||||
|
||||
try:
|
||||
await self._get_bot(call.message).delete_messages(
|
||||
chat_id=self.group_for_posts,
|
||||
message_ids=media_group_message_ids
|
||||
chat_id=self.group_for_posts, message_ids=media_group_message_ids
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"_publish_media_group: Ошибка при удалении медиагруппы из чата модерации: {e}")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Ошибка при удалении медиагруппы из чата модерации: {e}"
|
||||
)
|
||||
|
||||
sent_messages = await send_media_group_to_channel(
|
||||
bot=self._get_bot(call.message),
|
||||
chat_id=self.main_public,
|
||||
post_content=post_content,
|
||||
post_text=formatted_text,
|
||||
s3_storage=self.s3_storage
|
||||
s3_storage=self.s3_storage,
|
||||
)
|
||||
|
||||
if len(sent_messages) == len(media_group_message_ids):
|
||||
@@ -331,43 +479,59 @@ class PostPublishService:
|
||||
try:
|
||||
await self.db.update_published_message_id(
|
||||
original_message_id=original_message_id,
|
||||
published_message_id=published_message_id
|
||||
published_message_id=published_message_id,
|
||||
)
|
||||
await self._save_published_post_content(
|
||||
sent_messages[i], published_message_id, original_message_id
|
||||
)
|
||||
await self._save_published_post_content(sent_messages[i], published_message_id, original_message_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"_publish_media_group: Ошибка при сохранении published_message_id для {original_message_id}: {e}")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Ошибка при сохранении published_message_id для {original_message_id}: {e}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"_publish_media_group: Количество опубликованных сообщений ({len(sent_messages)}) не совпадает с количеством оригинальных ({len(media_group_message_ids)})")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Количество опубликованных сообщений ({len(sent_messages)}) не совпадает с количеством оригинальных ({len(media_group_message_ids)})"
|
||||
)
|
||||
|
||||
await self.db.update_status_for_media_group_by_helper_id(helper_message_id, "approved")
|
||||
await self.db.update_status_for_media_group_by_helper_id(
|
||||
helper_message_id, "approved"
|
||||
)
|
||||
|
||||
# Удаляем helper сообщение - это критично, делаем это всегда
|
||||
try:
|
||||
await self._get_bot(call.message).delete_message(
|
||||
chat_id=self.group_for_posts,
|
||||
message_id=helper_message_id
|
||||
chat_id=self.group_for_posts, message_id=helper_message_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"_publish_media_group: Ошибка при удалении helper сообщения: {e}")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Ошибка при удалении helper сообщения: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"_publish_media_group: Пользователь {author_id} заблокировал бота")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Пользователь {author_id} заблокировал бота"
|
||||
)
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"_publish_media_group: Ошибка при отправке уведомления автору: {e}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Ошибка при отправке уведомления автору: {e}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"_publish_media_group: Ошибка при публикации медиагруппы: {e}")
|
||||
logger.error(
|
||||
f"_publish_media_group: Ошибка при публикации медиагруппы: {e}"
|
||||
)
|
||||
# Пытаемся удалить helper сообщение даже при ошибке
|
||||
try:
|
||||
await self._get_bot(call.message).delete_message(
|
||||
chat_id=self.group_for_posts,
|
||||
message_id=call.message.message_id
|
||||
chat_id=self.group_for_posts, message_id=call.message.message_id
|
||||
)
|
||||
except Exception as delete_error:
|
||||
logger.warning(f"_publish_media_group: Не удалось удалить helper сообщение при ошибке: {delete_error}")
|
||||
logger.warning(
|
||||
f"_publish_media_group: Не удалось удалить helper сообщение при ошибке: {delete_error}"
|
||||
)
|
||||
raise PublishError(f"Не удалось опубликовать медиагруппу: {str(e)}")
|
||||
|
||||
@track_time("decline_post", "post_publish_service")
|
||||
@@ -381,12 +545,22 @@ class PostPublishService:
|
||||
|
||||
content_type = call.message.content_type
|
||||
|
||||
if content_type in [CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||
if content_type in [
|
||||
CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_PHOTO,
|
||||
CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_VOICE,
|
||||
CONTENT_TYPE_VIDEO,
|
||||
CONTENT_TYPE_VIDEO_NOTE,
|
||||
]:
|
||||
await self._decline_single_post(call)
|
||||
else:
|
||||
logger.error(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
logger.error(
|
||||
f"Неподдерживаемый тип контента для отклонения: {content_type}"
|
||||
)
|
||||
raise PublishError(
|
||||
f"Неподдерживаемый тип контента для отклонения: {content_type}"
|
||||
)
|
||||
|
||||
@track_time("_decline_single_post", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_decline_single_post")
|
||||
@@ -397,12 +571,20 @@ class PostPublishService:
|
||||
# Обучаем RAG на отклоненном посте перед удалением
|
||||
await self._train_on_declined(call.message.message_id)
|
||||
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "declined")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "declined"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.error(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'declined'")
|
||||
raise PostNotFoundError(f"Пост с message_id={call.message.message_id} не найден в базе данных")
|
||||
logger.error(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'declined'"
|
||||
)
|
||||
raise PostNotFoundError(
|
||||
f"Пост с 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)
|
||||
await self._get_bot(call.message).delete_message(
|
||||
chat_id=self.group_for_posts, message_id=call.message.message_id
|
||||
)
|
||||
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||
@@ -412,7 +594,9 @@ class PostPublishService:
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||
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")
|
||||
@@ -421,9 +605,13 @@ class PostPublishService:
|
||||
"""Отклонение медиагруппы"""
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
await self.db.update_status_for_media_group_by_helper_id(helper_message_id, "declined")
|
||||
await self.db.update_status_for_media_group_by_helper_id(
|
||||
helper_message_id, "declined"
|
||||
)
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
|
||||
message_ids_to_delete = media_group_message_ids.copy()
|
||||
message_ids_to_delete.append(helper_message_id)
|
||||
@@ -432,8 +620,7 @@ class PostPublishService:
|
||||
|
||||
try:
|
||||
await self._get_bot(call.message).delete_messages(
|
||||
chat_id=self.group_for_posts,
|
||||
message_ids=message_ids_to_delete
|
||||
chat_id=self.group_for_posts, message_ids=message_ids_to_delete
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"_decline_media_group: Ошибка при удалении сообщений: {e}")
|
||||
@@ -442,9 +629,13 @@ class PostPublishService:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"_decline_media_group: Пользователь {author_id} заблокировал бота")
|
||||
logger.warning(
|
||||
f"_decline_media_group: Пользователь {author_id} заблокировал бота"
|
||||
)
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"_decline_media_group: Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||
logger.error(
|
||||
f"_decline_media_group: Ошибка при отправке уведомления автору {author_id}: {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
@track_time("_get_author_id", "post_publish_service")
|
||||
@@ -487,12 +678,16 @@ class PostPublishService:
|
||||
|
||||
@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:
|
||||
"""Удаление поста и уведомление автора"""
|
||||
# Получаем текст поста для обучения RAG перед удалением
|
||||
await self._train_on_published(call.message.message_id)
|
||||
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
await self._get_bot(call.message).delete_message(
|
||||
chat_id=self.group_for_posts, message_id=call.message.message_id
|
||||
)
|
||||
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
@@ -512,7 +707,9 @@ class PostPublishService:
|
||||
await self.scoring_manager.on_post_published(text)
|
||||
logger.debug(f"RAG обучен на опубликованном посте: {message_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обучения RAG на опубликованном посте {message_id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка обучения RAG на опубликованном посте {message_id}: {e}"
|
||||
)
|
||||
|
||||
async def _train_on_declined(self, message_id: int) -> None:
|
||||
"""Обучает RAG на отклоненном посте."""
|
||||
@@ -530,16 +727,22 @@ class PostPublishService:
|
||||
@track_time("_delete_media_group_and_notify_author", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_delete_media_group_and_notify_author")
|
||||
@track_media_processing("media_group")
|
||||
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:
|
||||
"""Удаление медиагруппы и уведомление автора (legacy метод, используется для обратной совместимости)"""
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
|
||||
media_group_message_ids = await self.db.get_post_ids_by_helper_id(
|
||||
helper_message_id
|
||||
)
|
||||
|
||||
message_ids_to_delete = media_group_message_ids.copy()
|
||||
message_ids_to_delete.append(helper_message_id)
|
||||
|
||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids_to_delete)
|
||||
await self._get_bot(call.message).delete_messages(
|
||||
chat_id=self.group_for_posts, message_ids=message_ids_to_delete
|
||||
)
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
except Exception as e:
|
||||
@@ -549,30 +752,47 @@ class PostPublishService:
|
||||
|
||||
@track_time("_save_published_post_content", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_save_published_post_content")
|
||||
async def _save_published_post_content(self, published_message: types.Message, published_message_id: int, original_message_id: int) -> None:
|
||||
async def _save_published_post_content(
|
||||
self,
|
||||
published_message: types.Message,
|
||||
published_message_id: int,
|
||||
original_message_id: int,
|
||||
) -> None:
|
||||
"""Сохраняет ссылку на медиафайл из опубликованного поста (файл уже в S3 или на диске)."""
|
||||
try:
|
||||
# Получаем уже сохраненный путь/S3 ключ из оригинального поста
|
||||
saved_content = await self.db.get_post_content_by_message_id(original_message_id)
|
||||
saved_content = await self.db.get_post_content_by_message_id(
|
||||
original_message_id
|
||||
)
|
||||
|
||||
if saved_content and len(saved_content) > 0:
|
||||
# Копируем тот же путь/S3 ключ
|
||||
file_path, content_type = saved_content[0]
|
||||
logger.debug(f"Копируем путь/S3 ключ для опубликованного поста: {file_path}")
|
||||
logger.debug(
|
||||
f"Копируем путь/S3 ключ для опубликованного поста: {file_path}"
|
||||
)
|
||||
|
||||
success = await self.db.add_published_post_content(
|
||||
published_message_id=published_message_id,
|
||||
content_path=file_path, # Тот же путь/S3 ключ
|
||||
content_type=content_type
|
||||
content_type=content_type,
|
||||
)
|
||||
if success:
|
||||
logger.info(f"Ссылка на файл сохранена для опубликованного поста: published_message_id={published_message_id}, path={file_path}")
|
||||
logger.info(
|
||||
f"Ссылка на файл сохранена для опубликованного поста: published_message_id={published_message_id}, path={file_path}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Не удалось сохранить ссылку на файл: published_message_id={published_message_id}")
|
||||
logger.warning(
|
||||
f"Не удалось сохранить ссылку на файл: published_message_id={published_message_id}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Контент не найден для оригинального поста message_id={original_message_id}")
|
||||
logger.warning(
|
||||
f"Контент не найден для оригинального поста message_id={original_message_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении ссылки на контент опубликованного поста {published_message_id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при сохранении ссылки на контент опубликованного поста {published_message_id}: {e}"
|
||||
)
|
||||
# Не прерываем публикацию, если сохранение контента не удалось
|
||||
|
||||
|
||||
@@ -581,8 +801,8 @@ class BanService:
|
||||
self.bot = bot
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.group_for_posts = settings['Telegram']['group_for_posts']
|
||||
self.important_logs = settings['Telegram']['important_logs']
|
||||
self.group_for_posts = settings["Telegram"]["group_for_posts"]
|
||||
self.important_logs = settings["Telegram"]["important_logs"]
|
||||
|
||||
def _get_bot(self, message) -> Bot:
|
||||
"""Получает бота из контекста сообщения или использует переданного"""
|
||||
@@ -597,12 +817,18 @@ class BanService:
|
||||
"""Бан пользователя за спам"""
|
||||
# Если это helper-сообщение медиагруппы, используем специальный метод
|
||||
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(call.message.message_id)
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
else:
|
||||
author_id = await self.db.get_author_id_by_message_id(call.message.message_id)
|
||||
author_id = await self.db.get_author_id_by_message_id(
|
||||
call.message.message_id
|
||||
)
|
||||
|
||||
if not author_id:
|
||||
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")
|
||||
raise UserNotFoundError(
|
||||
f"Автор не найден для сообщения {call.message.message_id}"
|
||||
)
|
||||
|
||||
current_date = datetime.now()
|
||||
date_to_unban = int((current_date + timedelta(days=7)).timestamp())
|
||||
@@ -624,18 +850,28 @@ class BanService:
|
||||
call.message.message_id, "declined"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.warning(f"Не удалось обновить статус медиагруппы helper_message_id={call.message.message_id} на 'declined'")
|
||||
logger.warning(
|
||||
f"Не удалось обновить статус медиагруппы helper_message_id={call.message.message_id} на 'declined'"
|
||||
)
|
||||
else:
|
||||
# Для одиночного поста обновляем статус по message_id
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "declined")
|
||||
updated_rows = await self.db.update_status_by_message_id(
|
||||
call.message.message_id, "declined"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.warning(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'declined'")
|
||||
logger.warning(
|
||||
f"Не удалось обновить статус поста message_id={call.message.message_id} на 'declined'"
|
||||
)
|
||||
|
||||
await self._get_bot(call.message).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 = (current_date + timedelta(days=7)).strftime("%d.%m.%Y %H:%M")
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_USER_BANNED_SPAM.format(date=date_str))
|
||||
await send_text_message(
|
||||
author_id, call.message, MESSAGE_USER_BANNED_SPAM.format(date=date_str)
|
||||
)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
|
||||
@@ -6,27 +6,24 @@ from .constants import ERROR_MESSAGES, FSM_STATES
|
||||
from .decorators import error_handler
|
||||
from .exceptions import NoReplyToMessageError, UserNotFoundError
|
||||
from .group_handlers import GroupHandlers, create_group_handlers, group_router
|
||||
|
||||
# Local imports - services
|
||||
from .services import AdminReplyService, DatabaseProtocol
|
||||
|
||||
__all__ = [
|
||||
# Main components
|
||||
'group_router',
|
||||
'create_group_handlers',
|
||||
'GroupHandlers',
|
||||
|
||||
"group_router",
|
||||
"create_group_handlers",
|
||||
"GroupHandlers",
|
||||
# Services
|
||||
'AdminReplyService',
|
||||
'DatabaseProtocol',
|
||||
|
||||
"AdminReplyService",
|
||||
"DatabaseProtocol",
|
||||
# Constants
|
||||
'FSM_STATES',
|
||||
'ERROR_MESSAGES',
|
||||
|
||||
"FSM_STATES",
|
||||
"ERROR_MESSAGES",
|
||||
# Exceptions
|
||||
'NoReplyToMessageError',
|
||||
'UserNotFoundError',
|
||||
|
||||
"NoReplyToMessageError",
|
||||
"UserNotFoundError",
|
||||
# Utilities
|
||||
'error_handler'
|
||||
"error_handler",
|
||||
]
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
from typing import Dict, Final
|
||||
|
||||
# FSM States
|
||||
FSM_STATES: Final[Dict[str, str]] = {
|
||||
"CHAT": "CHAT"
|
||||
}
|
||||
FSM_STATES: Final[Dict[str, str]] = {"CHAT": "CHAT"}
|
||||
|
||||
# Error messages
|
||||
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||
"NO_REPLY_TO_MESSAGE": "Блять, выдели сообщение!",
|
||||
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение."
|
||||
"USER_NOT_FOUND": "Не могу найти кому ответить в базе, проебали сообщение.",
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ from typing import Any, Callable
|
||||
|
||||
# Third-party imports
|
||||
from aiogram import types
|
||||
|
||||
# Local imports
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Decorator for centralized error handling"""
|
||||
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
@@ -19,18 +21,23 @@ def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
logger.error(f"Error in {func.__name__}: {str(e)}")
|
||||
# Try to send error to logs if possible
|
||||
try:
|
||||
message = next((arg for arg in args if isinstance(arg, types.Message)), None)
|
||||
if message and hasattr(message, 'bot'):
|
||||
from helper_bot.utils.base_dependency_factory import \
|
||||
get_global_instance
|
||||
message = next(
|
||||
(arg for arg in args if isinstance(arg, types.Message)), None
|
||||
)
|
||||
if message and hasattr(message, "bot"):
|
||||
from helper_bot.utils.base_dependency_factory import (
|
||||
get_global_instance,
|
||||
)
|
||||
|
||||
bdf = get_global_instance()
|
||||
important_logs = bdf.settings['Telegram']['important_logs']
|
||||
important_logs = bdf.settings["Telegram"]["important_logs"]
|
||||
await message.bot.send_message(
|
||||
chat_id=important_logs,
|
||||
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}",
|
||||
)
|
||||
except Exception:
|
||||
# If we can't log the error, at least it was logged to logger
|
||||
pass
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
class NoReplyToMessageError(Exception):
|
||||
"""Raised when admin tries to reply without selecting a message"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFoundError(Exception):
|
||||
"""Raised when user is not found in database for the given message_id"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
# Third-party imports
|
||||
from aiogram import Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
# Local imports - filters
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import metrics, track_errors, track_time
|
||||
|
||||
# Local imports - utilities
|
||||
from logs.custom_logger import logger
|
||||
|
||||
@@ -35,8 +38,7 @@ class GroupHandlers:
|
||||
def _register_handlers(self):
|
||||
"""Register all message handlers"""
|
||||
self.router.message.register(
|
||||
self.handle_message,
|
||||
ChatTypeFilter(chat_type=["group", "supergroup"])
|
||||
self.handle_message, ChatTypeFilter(chat_type=["group", "supergroup"])
|
||||
)
|
||||
|
||||
@error_handler
|
||||
@@ -46,7 +48,7 @@ class GroupHandlers:
|
||||
"""Handle admin reply to user through group chat"""
|
||||
|
||||
logger.info(
|
||||
f'Получено сообщение в группе {message.chat.title} (ID: {message.chat.id}) '
|
||||
f"Получено сообщение в группе {message.chat.title} (ID: {message.chat.id}) "
|
||||
f'от пользователя {message.from_user.full_name} (ID: {message.from_user.id}): "{message.text}"'
|
||||
)
|
||||
|
||||
@@ -54,8 +56,8 @@ class GroupHandlers:
|
||||
if not message.reply_to_message:
|
||||
await message.answer(ERROR_MESSAGES["NO_REPLY_TO_MESSAGE"])
|
||||
logger.warning(
|
||||
f'В группе {message.chat.title} (ID: {message.chat.id}) '
|
||||
f'админ не выделил сообщение для ответа.'
|
||||
f"В группе {message.chat.title} (ID: {message.chat.id}) "
|
||||
f"админ не выделил сообщение для ответа."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -77,13 +79,15 @@ class GroupHandlers:
|
||||
except UserNotFoundError:
|
||||
await message.answer(ERROR_MESSAGES["USER_NOT_FOUND"])
|
||||
logger.error(
|
||||
f'Ошибка при поиске пользователя в базе для ответа на сообщение: {reply_text} '
|
||||
f'в группе {message.chat.title} (ID сообщения: {message.message_id})'
|
||||
f"Ошибка при поиске пользователя в базе для ответа на сообщение: {reply_text} "
|
||||
f"в группе {message.chat.title} (ID сообщения: {message.message_id})"
|
||||
)
|
||||
|
||||
|
||||
# Factory function to create handlers with dependencies
|
||||
def create_group_handlers(db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMarkup) -> GroupHandlers:
|
||||
def create_group_handlers(
|
||||
db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMarkup
|
||||
) -> GroupHandlers:
|
||||
"""Create group handlers instance with dependencies"""
|
||||
return GroupHandlers(db, keyboard_markup)
|
||||
|
||||
@@ -91,6 +95,7 @@ def create_group_handlers(db: AsyncBotDB, keyboard_markup: types.ReplyKeyboardMa
|
||||
# Legacy router for backward compatibility
|
||||
group_router = Router()
|
||||
|
||||
|
||||
# Initialize with global dependencies (for backward compatibility)
|
||||
def init_legacy_router():
|
||||
"""Initialize legacy router with global dependencies"""
|
||||
@@ -100,12 +105,13 @@ def init_legacy_router():
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
|
||||
bdf = get_global_instance()
|
||||
#TODO: поменять архитектуру и подключить правильный BotDB
|
||||
# TODO: поменять архитектуру и подключить правильный BotDB
|
||||
db = bdf.get_db()
|
||||
keyboard_markup = get_reply_keyboard_leave_chat()
|
||||
|
||||
handlers = create_group_handlers(db, keyboard_markup)
|
||||
group_router = handlers.router
|
||||
|
||||
|
||||
# Initialize legacy router
|
||||
init_legacy_router()
|
||||
|
||||
@@ -5,8 +5,10 @@ from typing import Optional, Protocol
|
||||
|
||||
# Third-party imports
|
||||
from aiogram import types
|
||||
|
||||
# Local imports
|
||||
from helper_bot.utils.helper_func import send_text_message
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -16,8 +18,11 @@ from .exceptions import NoReplyToMessageError, UserNotFoundError
|
||||
|
||||
class DatabaseProtocol(Protocol):
|
||||
"""Protocol for database operations"""
|
||||
|
||||
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): ...
|
||||
async def add_message(
|
||||
self, message_text: str, user_id: int, message_id: int, date: int = None
|
||||
): ...
|
||||
|
||||
|
||||
class AdminReplyService:
|
||||
@@ -54,7 +59,7 @@ class AdminReplyService:
|
||||
chat_id: int,
|
||||
message: types.Message,
|
||||
reply_text: str,
|
||||
markup: types.ReplyKeyboardMarkup
|
||||
markup: types.ReplyKeyboardMarkup,
|
||||
) -> None:
|
||||
"""
|
||||
Send reply to user.
|
||||
|
||||
@@ -4,28 +4,25 @@
|
||||
# Local imports - constants and utilities
|
||||
from .constants import BUTTON_TEXTS, ERROR_MESSAGES, FSM_STATES
|
||||
from .decorators import error_handler
|
||||
from .private_handlers import (PrivateHandlers, create_private_handlers,
|
||||
private_router)
|
||||
from .private_handlers import PrivateHandlers, create_private_handlers, private_router
|
||||
|
||||
# Local imports - services
|
||||
from .services import BotSettings, PostService, StickerService, UserService
|
||||
|
||||
__all__ = [
|
||||
# Main components
|
||||
'private_router',
|
||||
'create_private_handlers',
|
||||
'PrivateHandlers',
|
||||
|
||||
"private_router",
|
||||
"create_private_handlers",
|
||||
"PrivateHandlers",
|
||||
# Services
|
||||
'BotSettings',
|
||||
'UserService',
|
||||
'PostService',
|
||||
'StickerService',
|
||||
|
||||
"BotSettings",
|
||||
"UserService",
|
||||
"PostService",
|
||||
"StickerService",
|
||||
# Constants
|
||||
'FSM_STATES',
|
||||
'BUTTON_TEXTS',
|
||||
'ERROR_MESSAGES',
|
||||
|
||||
"FSM_STATES",
|
||||
"BUTTON_TEXTS",
|
||||
"ERROR_MESSAGES",
|
||||
# Utilities
|
||||
'error_handler'
|
||||
"error_handler",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@ FSM_STATES: Final[Dict[str, str]] = {
|
||||
"START": "START",
|
||||
"SUGGEST": "SUGGEST",
|
||||
"PRE_CHAT": "PRE_CHAT",
|
||||
"CHAT": "CHAT"
|
||||
"CHAT": "CHAT",
|
||||
}
|
||||
|
||||
# Button texts
|
||||
@@ -18,7 +18,7 @@ BUTTON_TEXTS: Final[Dict[str, str]] = {
|
||||
"RETURN_TO_BOT": "Вернуться в бота",
|
||||
"WANT_STICKERS": "🤪Хочу стикеры",
|
||||
"CONNECT_ADMIN": "📩Связаться с админами",
|
||||
"VOICE_BOT": "🎤Голосовой бот"
|
||||
"VOICE_BOT": "🎤Голосовой бот",
|
||||
}
|
||||
|
||||
# Button to command mapping for metrics
|
||||
@@ -29,15 +29,15 @@ BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"Вернуться в бота": "return_to_bot",
|
||||
"🤪Хочу стикеры": "want_stickers",
|
||||
"📩Связаться с админами": "connect_admin",
|
||||
"🎤Голосовой бот": "voice_bot"
|
||||
"🎤Голосовой бот": "voice_bot",
|
||||
}
|
||||
|
||||
# Error messages
|
||||
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
||||
"UNSUPPORTED_CONTENT": (
|
||||
'Я пока не умею работать с таким сообщением. '
|
||||
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
|
||||
'Мы добавим его к обработке если необходимо'
|
||||
"Я пока не умею работать с таким сообщением. "
|
||||
"Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n"
|
||||
"Мы добавим его к обработке если необходимо"
|
||||
),
|
||||
"STICKERS_LINK": "Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk"
|
||||
"STICKERS_LINK": "Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk",
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ from typing import Any, Callable
|
||||
|
||||
# Third-party imports
|
||||
from aiogram import types
|
||||
|
||||
# Local imports
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Decorator for centralized error handling"""
|
||||
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
@@ -19,18 +21,23 @@ def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
logger.error(f"Error in {func.__name__}: {str(e)}")
|
||||
# Try to send error to logs if possible
|
||||
try:
|
||||
message = next((arg for arg in args if isinstance(arg, types.Message)), None)
|
||||
if message and hasattr(message, 'bot'):
|
||||
from helper_bot.utils.base_dependency_factory import \
|
||||
get_global_instance
|
||||
message = next(
|
||||
(arg for arg in args if isinstance(arg, types.Message)), None
|
||||
)
|
||||
if message and hasattr(message, "bot"):
|
||||
from helper_bot.utils.base_dependency_factory import (
|
||||
get_global_instance,
|
||||
)
|
||||
|
||||
bdf = get_global_instance()
|
||||
important_logs = bdf.settings['Telegram']['important_logs']
|
||||
important_logs = bdf.settings["Telegram"]["important_logs"]
|
||||
await message.bot.send_message(
|
||||
chat_id=important_logs,
|
||||
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}",
|
||||
)
|
||||
except Exception:
|
||||
# If we can't log the error, at least it was logged to logger
|
||||
pass
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -8,18 +8,23 @@ from datetime import datetime
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
# Local imports - filters and middlewares
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
|
||||
# Local imports - utilities
|
||||
from helper_bot.keyboards import (get_reply_keyboard,
|
||||
get_reply_keyboard_for_post)
|
||||
from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post
|
||||
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
|
||||
from helper_bot.middlewares.album_middleware import AlbumMiddleware
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.utils import messages
|
||||
from helper_bot.utils.helper_func import (check_user_emoji, get_first_name,
|
||||
update_user_info)
|
||||
from helper_bot.utils.helper_func import (
|
||||
check_user_emoji,
|
||||
get_first_name,
|
||||
update_user_info,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
|
||||
@@ -35,7 +40,13 @@ sleep = asyncio.sleep
|
||||
class PrivateHandlers:
|
||||
"""Main handler class for private messages"""
|
||||
|
||||
def __init__(self, db: AsyncBotDB, settings: BotSettings, s3_storage=None, scoring_manager=None):
|
||||
def __init__(
|
||||
self,
|
||||
db: AsyncBotDB,
|
||||
settings: BotSettings,
|
||||
s3_storage=None,
|
||||
scoring_manager=None,
|
||||
):
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.user_service = UserService(db, settings)
|
||||
@@ -52,51 +63,106 @@ class PrivateHandlers:
|
||||
def _register_handlers(self):
|
||||
"""Register all message handlers"""
|
||||
# Command handlers
|
||||
self.router.message.register(self.handle_emoji_message, ChatTypeFilter(chat_type=["private"]), Command("emoji"))
|
||||
self.router.message.register(self.handle_restart_message, ChatTypeFilter(chat_type=["private"]), Command("restart"))
|
||||
self.router.message.register(self.handle_start_message, ChatTypeFilter(chat_type=["private"]), Command("start"))
|
||||
self.router.message.register(self.handle_start_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["RETURN_TO_BOT"])
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("emoji"),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.handle_restart_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("restart"),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.handle_start_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("start"),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.handle_start_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["RETURN_TO_BOT"],
|
||||
)
|
||||
|
||||
# Button handlers
|
||||
self.router.message.register(self.suggest_post, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["SUGGEST_POST"])
|
||||
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["SAY_GOODBYE"])
|
||||
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.connect_with_admin, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["CONNECT_ADMIN"])
|
||||
|
||||
self.router.message.register(
|
||||
self.suggest_post,
|
||||
StateFilter(FSM_STATES["START"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["SUGGEST_POST"],
|
||||
)
|
||||
self.router.message.register(
|
||||
self.end_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["SAY_GOODBYE"],
|
||||
)
|
||||
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.connect_with_admin,
|
||||
StateFilter(FSM_STATES["START"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["CONNECT_ADMIN"],
|
||||
)
|
||||
|
||||
# State handlers
|
||||
self.router.message.register(self.suggest_router, StateFilter(FSM_STATES["SUGGEST"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["PRE_CHAT"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["CHAT"]), ChatTypeFilter(chat_type=["private"]))
|
||||
self.router.message.register(
|
||||
self.suggest_router,
|
||||
StateFilter(FSM_STATES["SUGGEST"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.resend_message_in_group_for_message,
|
||||
StateFilter(FSM_STATES["PRE_CHAT"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
self.router.message.register(
|
||||
self.resend_message_in_group_for_message,
|
||||
StateFilter(FSM_STATES["CHAT"]),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
)
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_emoji_message")
|
||||
@track_time("handle_emoji_message", "private_handlers")
|
||||
async def handle_emoji_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def handle_emoji_message(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle emoji command"""
|
||||
await self.user_service.log_user_message(message)
|
||||
user_emoji = await check_user_emoji(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
if user_emoji is not None:
|
||||
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
|
||||
await message.answer(f"Твоя эмодзя - {user_emoji}", parse_mode="HTML")
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "handle_restart_message")
|
||||
@track_time("handle_restart_message", "private_handlers")
|
||||
async def handle_restart_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def handle_restart_message(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle restart command"""
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
await update_user_info('love', message)
|
||||
await update_user_info("love", 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
|
||||
@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"""
|
||||
# User service operations with metrics
|
||||
await self.user_service.log_user_message(message)
|
||||
@@ -108,8 +174,8 @@ class PrivateHandlers:
|
||||
|
||||
# Send welcome message with metrics
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
|
||||
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
|
||||
hello_message = messages.get_message(get_first_name(message), "HELLO_MESSAGE")
|
||||
await message.answer(hello_message, reply_markup=markup, parse_mode="HTML")
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "suggest_post")
|
||||
@@ -122,7 +188,7 @@ class PrivateHandlers:
|
||||
await state.set_state(FSM_STATES["SUGGEST"])
|
||||
|
||||
markup = types.ReplyKeyboardRemove()
|
||||
suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS')
|
||||
suggest_news = messages.get_message(get_first_name(message), "SUGGEST_NEWS")
|
||||
await message.answer(suggest_news, reply_markup=markup)
|
||||
|
||||
@error_handler
|
||||
@@ -139,18 +205,22 @@ class PrivateHandlers:
|
||||
|
||||
# Send goodbye message
|
||||
markup = types.ReplyKeyboardRemove()
|
||||
bye_message = messages.get_message(get_first_name(message), 'BYE_MESSAGE')
|
||||
bye_message = messages.get_message(get_first_name(message), "BYE_MESSAGE")
|
||||
await message.answer(bye_message, reply_markup=markup)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "suggest_router")
|
||||
@track_time("suggest_router", "private_handlers")
|
||||
async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs):
|
||||
async def suggest_router(
|
||||
self, message: types.Message, state: FSMContext, album: list = None, **kwargs
|
||||
):
|
||||
"""Handle post submission in suggest state - сразу отвечает пользователю, обработка в фоне"""
|
||||
# Сразу отвечаем пользователю
|
||||
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 state.set_state(FSM_STATES["START"])
|
||||
|
||||
@@ -177,6 +247,7 @@ class PrivateHandlers:
|
||||
await self.post_service.process_post(message, album)
|
||||
except Exception as e:
|
||||
from logs.custom_logger import logger
|
||||
|
||||
logger.error(f"Ошибка при фоновой обработке поста: {e}")
|
||||
|
||||
asyncio.create_task(process_post_background())
|
||||
@@ -191,20 +262,21 @@ class PrivateHandlers:
|
||||
markup = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
await self.db.update_stickers_info(message.from_user.id)
|
||||
await self.user_service.log_user_message(message)
|
||||
await message.answer(
|
||||
text=ERROR_MESSAGES["STICKERS_LINK"],
|
||||
reply_markup=markup
|
||||
)
|
||||
await message.answer(text=ERROR_MESSAGES["STICKERS_LINK"], reply_markup=markup)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "connect_with_admin")
|
||||
@track_time("connect_with_admin", "private_handlers")
|
||||
async def connect_with_admin(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
async def connect_with_admin(
|
||||
self, message: types.Message, state: FSMContext, **kwargs
|
||||
):
|
||||
"""Handle connect with admin button"""
|
||||
# User service operations with metrics
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
admin_message = messages.get_message(get_first_name(message), 'CONNECT_WITH_ADMIN')
|
||||
admin_message = messages.get_message(
|
||||
get_first_name(message), "CONNECT_WITH_ADMIN"
|
||||
)
|
||||
await message.answer(admin_message, parse_mode="html")
|
||||
await self.user_service.log_user_message(message)
|
||||
await state.set_state(FSM_STATES["PRE_CHAT"])
|
||||
@@ -213,7 +285,9 @@ class PrivateHandlers:
|
||||
@track_errors("private_handlers", "resend_message_in_group_for_message")
|
||||
@track_time("resend_message_in_group_for_message", "private_handlers")
|
||||
@db_query_time("resend_message_in_group_for_message", "messages", "insert")
|
||||
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"""
|
||||
# User service operations with metrics
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
@@ -221,9 +295,11 @@ class PrivateHandlers:
|
||||
|
||||
current_date = datetime.now()
|
||||
date = int(current_date.timestamp())
|
||||
await self.db.add_message(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()
|
||||
|
||||
if user_state == FSM_STATES["PRE_CHAT"]:
|
||||
@@ -236,7 +312,9 @@ class PrivateHandlers:
|
||||
|
||||
|
||||
# Factory function to create handlers with dependencies
|
||||
def create_private_handlers(db: AsyncBotDB, settings: BotSettings, s3_storage=None, scoring_manager=None) -> PrivateHandlers:
|
||||
def create_private_handlers(
|
||||
db: AsyncBotDB, settings: BotSettings, s3_storage=None, scoring_manager=None
|
||||
) -> PrivateHandlers:
|
||||
"""Create private handlers instance with dependencies"""
|
||||
return PrivateHandlers(db, settings, s3_storage, scoring_manager)
|
||||
|
||||
@@ -247,6 +325,7 @@ private_router = Router()
|
||||
# Флаг инициализации для защиты от повторного вызова
|
||||
_legacy_router_initialized = False
|
||||
|
||||
|
||||
# Initialize with global dependencies (for backward compatibility)
|
||||
def init_legacy_router():
|
||||
"""Initialize legacy router with global dependencies"""
|
||||
@@ -259,14 +338,14 @@ def init_legacy_router():
|
||||
|
||||
bdf = get_global_instance()
|
||||
settings = BotSettings(
|
||||
group_for_posts=bdf.settings['Telegram']['group_for_posts'],
|
||||
group_for_message=bdf.settings['Telegram']['group_for_message'],
|
||||
main_public=bdf.settings['Telegram']['main_public'],
|
||||
group_for_logs=bdf.settings['Telegram']['group_for_logs'],
|
||||
important_logs=bdf.settings['Telegram']['important_logs'],
|
||||
preview_link=bdf.settings['Telegram']['preview_link'],
|
||||
logs=bdf.settings['Settings']['logs'],
|
||||
test=bdf.settings['Settings']['test']
|
||||
group_for_posts=bdf.settings["Telegram"]["group_for_posts"],
|
||||
group_for_message=bdf.settings["Telegram"]["group_for_message"],
|
||||
main_public=bdf.settings["Telegram"]["main_public"],
|
||||
group_for_logs=bdf.settings["Telegram"]["group_for_logs"],
|
||||
important_logs=bdf.settings["Telegram"]["important_logs"],
|
||||
preview_link=bdf.settings["Telegram"]["preview_link"],
|
||||
logs=bdf.settings["Settings"]["logs"],
|
||||
test=bdf.settings["Settings"]["test"],
|
||||
)
|
||||
|
||||
db = bdf.get_db()
|
||||
@@ -279,5 +358,6 @@ def init_legacy_router():
|
||||
private_router = handlers.router
|
||||
_legacy_router_initialized = True
|
||||
|
||||
|
||||
# Initialize legacy router
|
||||
init_legacy_router()
|
||||
|
||||
@@ -12,37 +12,61 @@ from typing import Any, Callable, Dict, Protocol, Union
|
||||
# Third-party imports
|
||||
from aiogram import types
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from database.models import TelegramPost, User
|
||||
from helper_bot.keyboards import get_reply_keyboard_for_post
|
||||
|
||||
# Local imports - utilities
|
||||
from helper_bot.utils.helper_func import (
|
||||
add_in_db_media, check_username_and_full_name, determine_anonymity,
|
||||
get_first_name, get_text_message, prepare_media_group_from_middlewares,
|
||||
send_audio_message, send_media_group_message_to_private_chat,
|
||||
send_photo_message, send_text_message, send_video_message,
|
||||
send_video_note_message, send_voice_message)
|
||||
add_in_db_media,
|
||||
check_username_and_full_name,
|
||||
determine_anonymity,
|
||||
get_first_name,
|
||||
get_text_message,
|
||||
prepare_media_group_from_middlewares,
|
||||
send_audio_message,
|
||||
send_media_group_message_to_private_chat,
|
||||
send_photo_message,
|
||||
send_text_message,
|
||||
send_video_message,
|
||||
send_video_note_message,
|
||||
send_voice_message,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_file_operations,
|
||||
track_media_processing, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_file_operations,
|
||||
track_media_processing,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class DatabaseProtocol(Protocol):
|
||||
"""Protocol for database operations"""
|
||||
|
||||
async def user_exists(self, user_id: int) -> bool: ...
|
||||
async def add_user(self, user: User) -> None: ...
|
||||
async def update_user_info(self, user_id: int, username: str = None, full_name: str = None) -> None: ...
|
||||
async def update_user_info(
|
||||
self, user_id: int, username: str = None, full_name: str = None
|
||||
) -> None: ...
|
||||
async def update_user_date(self, user_id: int) -> None: ...
|
||||
async def add_post(self, post: TelegramPost) -> None: ...
|
||||
async def update_stickers_info(self, user_id: int) -> None: ...
|
||||
async def add_message(self, message_text: str, user_id: int, message_id: int, date: int = None) -> None: ...
|
||||
async def update_helper_message(self, message_id: int, helper_message_id: int) -> None: ...
|
||||
async def add_message(
|
||||
self, message_text: str, user_id: int, message_id: int, date: int = None
|
||||
) -> None: ...
|
||||
async def update_helper_message(
|
||||
self, message_id: int, helper_message_id: int
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class BotSettings:
|
||||
"""Bot configuration settings"""
|
||||
|
||||
group_for_posts: str
|
||||
group_for_message: str
|
||||
main_public: str
|
||||
@@ -93,7 +117,7 @@ class UserService:
|
||||
has_stickers=False,
|
||||
date_added=current_timestamp,
|
||||
date_changed=current_timestamp,
|
||||
voice_bot_welcome_received=False
|
||||
voice_bot_welcome_received=False,
|
||||
)
|
||||
|
||||
# Пытаемся создать пользователя (если уже существует - игнорируем)
|
||||
@@ -101,18 +125,24 @@ class UserService:
|
||||
await self.db.add_user(user)
|
||||
|
||||
# Проверяем, нужно ли обновить информацию о существующем пользователе
|
||||
is_need_update = await 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:
|
||||
await self.db.update_user_info(user_id, username, full_name)
|
||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
safe_full_name = (
|
||||
html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
)
|
||||
# Для отображения используем подстановочное значение, но в БД сохраняем только реальный username
|
||||
safe_username = html.escape(username) if username else "Без никнейма"
|
||||
|
||||
await message.answer(
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}")
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}"
|
||||
)
|
||||
await message.bot.send_message(
|
||||
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}",
|
||||
)
|
||||
|
||||
await self.db.update_user_date(user_id)
|
||||
|
||||
@@ -130,20 +160,32 @@ class UserService:
|
||||
class PostService:
|
||||
"""Service for post-related operations"""
|
||||
|
||||
def __init__(self, db: DatabaseProtocol, settings: BotSettings, s3_storage=None, scoring_manager=None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
db: DatabaseProtocol,
|
||||
settings: BotSettings,
|
||||
s3_storage=None,
|
||||
scoring_manager=None,
|
||||
) -> None:
|
||||
self.db = db
|
||||
self.settings = settings
|
||||
self.s3_storage = s3_storage
|
||||
self.scoring_manager = scoring_manager
|
||||
|
||||
async def _save_media_background(self, sent_message: types.Message, bot_db: Any, s3_storage) -> None:
|
||||
async def _save_media_background(
|
||||
self, sent_message: types.Message, bot_db: Any, s3_storage
|
||||
) -> None:
|
||||
"""Сохраняет медиа в фоне, чтобы не блокировать ответ пользователю"""
|
||||
try:
|
||||
success = await add_in_db_media(sent_message, bot_db, s3_storage)
|
||||
if not success:
|
||||
logger.warning(f"_save_media_background: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
logger.warning(
|
||||
f"_save_media_background: Не удалось сохранить медиа для поста {sent_message.message_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"_save_media_background: Ошибка при сохранении медиа для поста {sent_message.message_id}: {e}")
|
||||
logger.error(
|
||||
f"_save_media_background: Ошибка при сохранении медиа для поста {sent_message.message_id}: {e}"
|
||||
)
|
||||
|
||||
async def _get_scores(self, text: str) -> tuple:
|
||||
"""
|
||||
@@ -160,24 +202,39 @@ class PostService:
|
||||
|
||||
# Формируем JSON для сохранения в БД
|
||||
import json
|
||||
ml_scores_json = json.dumps(scores.to_json_dict()) if scores.has_any_score() else None
|
||||
|
||||
ml_scores_json = (
|
||||
json.dumps(scores.to_json_dict()) if scores.has_any_score() else None
|
||||
)
|
||||
|
||||
# Получаем данные от RAG
|
||||
rag_confidence = scores.rag.confidence if scores.rag else None
|
||||
rag_score_pos_only = scores.rag.metadata.get("rag_score_pos_only") if scores.rag else None
|
||||
rag_score_pos_only = (
|
||||
scores.rag.metadata.get("rag_score_pos_only") if scores.rag else None
|
||||
)
|
||||
|
||||
return scores.deepseek_score, scores.rag_score, rag_confidence, rag_score_pos_only, ml_scores_json
|
||||
return (
|
||||
scores.deepseek_score,
|
||||
scores.rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Ошибка получения скоров: {e}")
|
||||
return None, None, None, None, None
|
||||
|
||||
async def _save_scores_background(self, message_id: int, ml_scores_json: str) -> None:
|
||||
async def _save_scores_background(
|
||||
self, message_id: int, ml_scores_json: str
|
||||
) -> None:
|
||||
"""Сохраняет скоры в БД в фоне."""
|
||||
if ml_scores_json:
|
||||
try:
|
||||
await self.db.update_ml_scores(message_id, ml_scores_json)
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Ошибка сохранения скоров для {message_id}: {e}")
|
||||
logger.error(
|
||||
f"PostService: Ошибка сохранения скоров для {message_id}: {e}"
|
||||
)
|
||||
|
||||
async def _get_scores_with_error_handling(self, text: str) -> tuple:
|
||||
"""
|
||||
@@ -199,13 +256,25 @@ class PostService:
|
||||
|
||||
# Формируем JSON для сохранения в БД
|
||||
import json
|
||||
ml_scores_json = json.dumps(scores.to_json_dict()) if scores.has_any_score() else None
|
||||
|
||||
ml_scores_json = (
|
||||
json.dumps(scores.to_json_dict()) if scores.has_any_score() else None
|
||||
)
|
||||
|
||||
# Получаем данные от RAG
|
||||
rag_confidence = scores.rag.confidence if scores.rag else None
|
||||
rag_score_pos_only = scores.rag.metadata.get("rag_score_pos_only") if scores.rag else None
|
||||
rag_score_pos_only = (
|
||||
scores.rag.metadata.get("rag_score_pos_only") if scores.rag else None
|
||||
)
|
||||
|
||||
return scores.deepseek_score, scores.rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, None
|
||||
return (
|
||||
scores.deepseek_score,
|
||||
scores.rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Ошибка получения скоров: {e}")
|
||||
# Возвращаем частичные скоры если есть, или сообщение об ошибке
|
||||
@@ -219,7 +288,7 @@ class PostService:
|
||||
message: types.Message,
|
||||
first_name: str,
|
||||
content_type: str,
|
||||
album: Union[list, None] = None
|
||||
album: Union[list, None] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Обрабатывает пост в фоне: получает скоры, отправляет в группу модерации, сохраняет в БД.
|
||||
@@ -236,13 +305,21 @@ class PostService:
|
||||
if content_type == "text":
|
||||
original_raw_text = message.text or ""
|
||||
elif content_type == "media_group":
|
||||
original_raw_text = album[0].caption or "" if album and album[0].caption else ""
|
||||
original_raw_text = (
|
||||
album[0].caption or "" if album and album[0].caption else ""
|
||||
)
|
||||
else:
|
||||
original_raw_text = message.caption or ""
|
||||
|
||||
# Получаем скоры с обработкой ошибок
|
||||
deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, error_message = \
|
||||
await self._get_scores_with_error_handling(original_raw_text)
|
||||
(
|
||||
deepseek_score,
|
||||
rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
error_message,
|
||||
) = await self._get_scores_with_error_handling(original_raw_text)
|
||||
|
||||
# Формируем текст для поста (с сообщением об ошибке если есть)
|
||||
text_for_post = original_raw_text
|
||||
@@ -287,37 +364,65 @@ class PostService:
|
||||
)
|
||||
elif content_type == "photo":
|
||||
sent_message = await send_photo_message(
|
||||
self.settings.group_for_posts, message, message.photo[-1].file_id, post_text, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.photo[-1].file_id,
|
||||
post_text,
|
||||
markup,
|
||||
)
|
||||
elif content_type == "video":
|
||||
sent_message = await send_video_message(
|
||||
self.settings.group_for_posts, message, message.video.file_id, post_text, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.video.file_id,
|
||||
post_text,
|
||||
markup,
|
||||
)
|
||||
elif content_type == "audio":
|
||||
sent_message = await send_audio_message(
|
||||
self.settings.group_for_posts, message, message.audio.file_id, post_text, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.audio.file_id,
|
||||
post_text,
|
||||
markup,
|
||||
)
|
||||
elif content_type == "voice":
|
||||
sent_message = await send_voice_message(
|
||||
self.settings.group_for_posts, message, message.voice.file_id, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.voice.file_id,
|
||||
markup,
|
||||
)
|
||||
elif content_type == "video_note":
|
||||
sent_message = await send_video_note_message(
|
||||
self.settings.group_for_posts, message, message.video_note.file_id, markup
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
message.video_note.file_id,
|
||||
markup,
|
||||
)
|
||||
elif content_type == "media_group":
|
||||
# Для медиагруппы используем специальную обработку
|
||||
# Передаем ml_scores_json для сохранения в БД
|
||||
await self._process_media_group_background(
|
||||
message, album, first_name, post_text, is_anonymous, original_raw_text, ml_scores_json
|
||||
message,
|
||||
album,
|
||||
first_name,
|
||||
post_text,
|
||||
is_anonymous,
|
||||
original_raw_text,
|
||||
ml_scores_json,
|
||||
)
|
||||
return
|
||||
else:
|
||||
logger.error(f"PostService: Неподдерживаемый тип контента: {content_type}")
|
||||
logger.error(
|
||||
f"PostService: Неподдерживаемый тип контента: {content_type}"
|
||||
)
|
||||
return
|
||||
|
||||
if not sent_message:
|
||||
logger.error(f"PostService: Не удалось отправить пост типа {content_type}")
|
||||
logger.error(
|
||||
f"PostService: Не удалось отправить пост типа {content_type}"
|
||||
)
|
||||
return
|
||||
|
||||
# Сохраняем пост в БД (сохраняем исходный текст, без сообщения об ошибке)
|
||||
@@ -326,19 +431,27 @@ class PostService:
|
||||
text=original_raw_text,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
# Сохраняем медиа и скоры в фоне
|
||||
if content_type in ("photo", "video", "audio", "voice", "video_note"):
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(sent_message.message_id, ml_scores_json))
|
||||
asyncio.create_task(
|
||||
self._save_scores_background(
|
||||
sent_message.message_id, ml_scores_json
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Критическая ошибка в _process_post_background для {content_type}: {e}")
|
||||
logger.error(
|
||||
f"PostService: Критическая ошибка в _process_post_background для {content_type}: {e}"
|
||||
)
|
||||
|
||||
async def _process_media_group_background(
|
||||
self,
|
||||
@@ -348,14 +461,21 @@ class PostService:
|
||||
post_caption: str,
|
||||
is_anonymous: bool,
|
||||
original_raw_text: str,
|
||||
ml_scores_json: str = None
|
||||
ml_scores_json: str = None,
|
||||
) -> None:
|
||||
"""Обрабатывает медиагруппу в фоне"""
|
||||
try:
|
||||
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_ids = await send_media_group_message_to_private_chat(
|
||||
self.settings.group_for_posts, message, media_group, self.db, None, self.s3_storage
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
media_group,
|
||||
self.db,
|
||||
None,
|
||||
self.s3_storage,
|
||||
)
|
||||
|
||||
main_post_id = media_group_message_ids[-1]
|
||||
@@ -365,13 +485,15 @@ class PostService:
|
||||
text=original_raw_text,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(main_post)
|
||||
|
||||
# Сохраняем скоры в фоне (если они были получены)
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(main_post_id, ml_scores_json))
|
||||
asyncio.create_task(
|
||||
self._save_scores_background(main_post_id, ml_scores_json)
|
||||
)
|
||||
|
||||
for msg_id in media_group_message_ids:
|
||||
await self.db.add_message_link(main_post_id, msg_id)
|
||||
@@ -380,10 +502,7 @@ class PostService:
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
helper_message = await send_text_message(
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
"^",
|
||||
markup
|
||||
self.settings.group_for_posts, message, "^", markup
|
||||
)
|
||||
helper_message_id = helper_message.message_id
|
||||
|
||||
@@ -392,13 +511,12 @@ class PostService:
|
||||
text="^",
|
||||
author_id=message.from_user.id,
|
||||
helper_text_message_id=main_post_id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
)
|
||||
await self.db.add_post(helper_post)
|
||||
|
||||
await self.db.update_helper_message(
|
||||
message_id=main_post_id,
|
||||
helper_message_id=helper_message_id
|
||||
message_id=main_post_id, helper_message_id=helper_message_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Ошибка в _process_media_group_background: {e}")
|
||||
@@ -411,7 +529,13 @@ class PostService:
|
||||
raw_text = message.text or ""
|
||||
|
||||
# Получаем скоры для текста
|
||||
deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json = await self._get_scores(raw_text)
|
||||
(
|
||||
deepseek_score,
|
||||
rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
) = await self._get_scores(raw_text)
|
||||
|
||||
logger.debug(
|
||||
f"PostService.handle_text_post: Передача скоров в get_text_message - "
|
||||
@@ -433,7 +557,9 @@ class PostService:
|
||||
)
|
||||
markup = get_reply_keyboard_for_post()
|
||||
|
||||
sent_message = await send_text_message(self.settings.group_for_posts, message, post_text, markup)
|
||||
sent_message = await send_text_message(
|
||||
self.settings.group_for_posts, message, post_text, markup
|
||||
)
|
||||
|
||||
# Определяем анонимность
|
||||
is_anonymous = determine_anonymity(raw_text)
|
||||
@@ -443,13 +569,15 @@ class PostService:
|
||||
text=raw_text,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
# Сохраняем скоры в фоне
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(sent_message.message_id, ml_scores_json))
|
||||
asyncio.create_task(
|
||||
self._save_scores_background(sent_message.message_id, ml_scores_json)
|
||||
)
|
||||
|
||||
@track_time("handle_photo_post", "post_service")
|
||||
@track_errors("post_service", "handle_photo_post")
|
||||
@@ -459,7 +587,13 @@ class PostService:
|
||||
raw_caption = message.caption or ""
|
||||
|
||||
# Получаем скоры для текста
|
||||
deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json = await self._get_scores(raw_caption)
|
||||
(
|
||||
deepseek_score,
|
||||
rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
) = await self._get_scores(raw_caption)
|
||||
|
||||
logger.debug(
|
||||
f"PostService.handle_photo_post: Передача скоров в get_text_message - "
|
||||
@@ -483,7 +617,11 @@ class PostService:
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
sent_message = await send_photo_message(
|
||||
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,
|
||||
)
|
||||
|
||||
# Определяем анонимность
|
||||
@@ -494,14 +632,18 @@ class PostService:
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
# Сохраняем медиа и скоры в фоне
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(sent_message.message_id, ml_scores_json))
|
||||
asyncio.create_task(
|
||||
self._save_scores_background(sent_message.message_id, ml_scores_json)
|
||||
)
|
||||
|
||||
@track_time("handle_video_post", "post_service")
|
||||
@track_errors("post_service", "handle_video_post")
|
||||
@@ -511,7 +653,13 @@ class PostService:
|
||||
raw_caption = message.caption or ""
|
||||
|
||||
# Получаем скоры для текста
|
||||
deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json = await self._get_scores(raw_caption)
|
||||
(
|
||||
deepseek_score,
|
||||
rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
) = await self._get_scores(raw_caption)
|
||||
|
||||
logger.debug(
|
||||
f"PostService.handle_video_post: Передача скоров в get_text_message - "
|
||||
@@ -535,7 +683,11 @@ class PostService:
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
sent_message = await send_video_message(
|
||||
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,
|
||||
)
|
||||
|
||||
# Определяем анонимность
|
||||
@@ -546,14 +698,18 @@ class PostService:
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
# Сохраняем медиа и скоры в фоне
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(sent_message.message_id, ml_scores_json))
|
||||
asyncio.create_task(
|
||||
self._save_scores_background(sent_message.message_id, ml_scores_json)
|
||||
)
|
||||
|
||||
@track_time("handle_video_note_post", "post_service")
|
||||
@track_errors("post_service", "handle_video_note_post")
|
||||
@@ -574,11 +730,13 @@ class PostService:
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
@track_time("handle_audio_post", "post_service")
|
||||
@track_errors("post_service", "handle_audio_post")
|
||||
@@ -588,7 +746,13 @@ class PostService:
|
||||
raw_caption = message.caption or ""
|
||||
|
||||
# Получаем скоры для текста
|
||||
deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json = await self._get_scores(raw_caption)
|
||||
(
|
||||
deepseek_score,
|
||||
rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
) = await self._get_scores(raw_caption)
|
||||
|
||||
logger.debug(
|
||||
f"PostService.handle_audio_post: Передача скоров в get_text_message - "
|
||||
@@ -612,7 +776,11 @@ class PostService:
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
sent_message = await send_audio_message(
|
||||
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,
|
||||
)
|
||||
|
||||
# Определяем анонимность
|
||||
@@ -623,14 +791,18 @@ class PostService:
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
# Сохраняем медиа и скоры в фоне
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(sent_message.message_id, ml_scores_json))
|
||||
asyncio.create_task(
|
||||
self._save_scores_background(sent_message.message_id, ml_scores_json)
|
||||
)
|
||||
|
||||
@track_time("handle_voice_post", "post_service")
|
||||
@track_errors("post_service", "handle_voice_post")
|
||||
@@ -651,17 +823,21 @@ class PostService:
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
asyncio.create_task(
|
||||
self._save_media_background(sent_message, self.db, self.s3_storage)
|
||||
)
|
||||
|
||||
@track_time("handle_media_group_post", "post_service")
|
||||
@track_errors("post_service", "handle_media_group_post")
|
||||
@db_query_time("handle_media_group_post", "posts", "insert")
|
||||
@track_media_processing("media_group")
|
||||
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"""
|
||||
post_caption = " "
|
||||
raw_caption = ""
|
||||
@@ -671,7 +847,13 @@ class PostService:
|
||||
raw_caption = album[0].caption or ""
|
||||
|
||||
# Получаем скоры для текста
|
||||
deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json = await self._get_scores(raw_caption)
|
||||
(
|
||||
deepseek_score,
|
||||
rag_score,
|
||||
rag_confidence,
|
||||
rag_score_pos_only,
|
||||
ml_scores_json,
|
||||
) = await self._get_scores(raw_caption)
|
||||
|
||||
logger.debug(
|
||||
f"PostService.handle_media_group_post: Передача скоров в get_text_message - "
|
||||
@@ -695,7 +877,12 @@ class PostService:
|
||||
media_group = await prepare_media_group_from_middlewares(album, post_caption)
|
||||
|
||||
media_group_message_ids = await send_media_group_message_to_private_chat(
|
||||
self.settings.group_for_posts, message, media_group, self.db, None, self.s3_storage
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
media_group,
|
||||
self.db,
|
||||
None,
|
||||
self.s3_storage,
|
||||
)
|
||||
|
||||
main_post_id = media_group_message_ids[-1]
|
||||
@@ -705,13 +892,15 @@ class PostService:
|
||||
text=raw_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
await self.db.add_post(main_post)
|
||||
|
||||
# Сохраняем скоры в фоне
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(main_post_id, ml_scores_json))
|
||||
asyncio.create_task(
|
||||
self._save_scores_background(main_post_id, ml_scores_json)
|
||||
)
|
||||
|
||||
for msg_id in media_group_message_ids:
|
||||
await self.db.add_message_link(main_post_id, msg_id)
|
||||
@@ -720,10 +909,7 @@ class PostService:
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
helper_message = await send_text_message(
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
"^",
|
||||
markup
|
||||
self.settings.group_for_posts, message, "^", markup
|
||||
)
|
||||
helper_message_id = helper_message.message_id
|
||||
|
||||
@@ -732,19 +918,20 @@ class PostService:
|
||||
text="^",
|
||||
author_id=message.from_user.id,
|
||||
helper_text_message_id=main_post_id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
)
|
||||
await self.db.add_post(helper_post)
|
||||
|
||||
await self.db.update_helper_message(
|
||||
message_id=main_post_id,
|
||||
helper_message_id=helper_message_id
|
||||
message_id=main_post_id, helper_message_id=helper_message_id
|
||||
)
|
||||
|
||||
@track_time("process_post", "post_service")
|
||||
@track_errors("post_service", "process_post")
|
||||
@track_media_processing("media_group")
|
||||
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:
|
||||
"""
|
||||
Запускает обработку поста в фоне.
|
||||
Не блокирует выполнение - сразу возвращает управление.
|
||||
@@ -752,7 +939,11 @@ class PostService:
|
||||
first_name = get_first_name(message)
|
||||
|
||||
# Определяем тип контента
|
||||
content_type = "media_group" if message.media_group_id is not None else message.content_type
|
||||
content_type = (
|
||||
"media_group"
|
||||
if message.media_group_id is not None
|
||||
else message.content_type
|
||||
)
|
||||
|
||||
# Запускаем фоновую обработку
|
||||
asyncio.create_task(
|
||||
@@ -771,7 +962,7 @@ class StickerService:
|
||||
@track_file_operations("sticker")
|
||||
async def send_random_hello_sticker(self, message: types.Message) -> None:
|
||||
"""Send random hello sticker with metrics tracking"""
|
||||
name_stick_hello = list(Path('Stick').rglob('Hello_*'))
|
||||
name_stick_hello = list(Path("Stick").rglob("Hello_*"))
|
||||
if not name_stick_hello:
|
||||
return
|
||||
random_stick_hello = random.choice(name_stick_hello)
|
||||
@@ -784,7 +975,7 @@ class StickerService:
|
||||
@track_file_operations("sticker")
|
||||
async def send_random_goodbye_sticker(self, message: types.Message) -> None:
|
||||
"""Send random goodbye sticker with metrics tracking"""
|
||||
name_stick_bye = list(Path('Stick').rglob('Universal_*'))
|
||||
name_stick_bye = list(Path("Stick").rglob("Universal_*"))
|
||||
if not name_stick_bye:
|
||||
return
|
||||
random_stick_bye = random.choice(name_stick_bye)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Утилиты для очистки и диагностики проблем с голосовыми файлами
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -24,15 +25,19 @@ class VoiceFileCleanupUtils:
|
||||
orphaned_records = []
|
||||
|
||||
for record in all_audio_records:
|
||||
file_name = record.get('file_name', '')
|
||||
user_id = record.get('author_id', 0)
|
||||
file_name = record.get("file_name", "")
|
||||
user_id = record.get("author_id", 0)
|
||||
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||||
if not os.path.exists(file_path):
|
||||
orphaned_records.append((file_name, user_id))
|
||||
logger.warning(f"Найдена запись в БД без файла: {file_name} (user_id: {user_id})")
|
||||
logger.warning(
|
||||
f"Найдена запись в БД без файла: {file_name} (user_id: {user_id})"
|
||||
)
|
||||
|
||||
logger.info(f"Найдено {len(orphaned_records)} записей в БД без соответствующих файлов")
|
||||
logger.info(
|
||||
f"Найдено {len(orphaned_records)} записей в БД без соответствующих файлов"
|
||||
)
|
||||
return orphaned_records
|
||||
|
||||
except Exception as e:
|
||||
@@ -52,7 +57,9 @@ class VoiceFileCleanupUtils:
|
||||
|
||||
# Получаем все записи из БД
|
||||
all_audio_records = await self.bot_db.get_all_audio_records()
|
||||
db_file_names = {record.get('file_name', '') for record in all_audio_records}
|
||||
db_file_names = {
|
||||
record.get("file_name", "") for record in all_audio_records
|
||||
}
|
||||
|
||||
for file_path in ogg_files:
|
||||
file_name = file_path.stem # Имя файла без расширения
|
||||
@@ -77,9 +84,13 @@ class VoiceFileCleanupUtils:
|
||||
return 0
|
||||
|
||||
if dry_run:
|
||||
logger.info(f"DRY RUN: Найдено {len(orphaned_records)} записей для удаления")
|
||||
logger.info(
|
||||
f"DRY RUN: Найдено {len(orphaned_records)} записей для удаления"
|
||||
)
|
||||
for file_name, user_id in orphaned_records:
|
||||
logger.info(f"DRY RUN: Будет удалена запись: {file_name} (user_id: {user_id})")
|
||||
logger.info(
|
||||
f"DRY RUN: Будет удалена запись: {file_name} (user_id: {user_id})"
|
||||
)
|
||||
return len(orphaned_records)
|
||||
|
||||
# Удаляем записи
|
||||
@@ -88,7 +99,9 @@ class VoiceFileCleanupUtils:
|
||||
try:
|
||||
await self.bot_db.delete_audio_record_by_file_name(file_name)
|
||||
deleted_count += 1
|
||||
logger.info(f"Удалена запись в БД: {file_name} (user_id: {user_id})")
|
||||
logger.info(
|
||||
f"Удалена запись в БД: {file_name} (user_id: {user_id})"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении записи {file_name}: {e}")
|
||||
|
||||
@@ -109,7 +122,9 @@ class VoiceFileCleanupUtils:
|
||||
return 0
|
||||
|
||||
if dry_run:
|
||||
logger.info(f"DRY RUN: Найдено {len(orphaned_files)} файлов для удаления")
|
||||
logger.info(
|
||||
f"DRY RUN: Найдено {len(orphaned_files)} файлов для удаления"
|
||||
)
|
||||
for file_path in orphaned_files:
|
||||
logger.info(f"DRY RUN: Будет удален файл: {file_path}")
|
||||
return len(orphaned_files)
|
||||
@@ -149,7 +164,7 @@ class VoiceFileCleanupUtils:
|
||||
"total_files": file_count,
|
||||
"total_size_bytes": total_size,
|
||||
"total_size_mb": round(total_size / (1024 * 1024), 2),
|
||||
"directory": VOICE_USERS_DIR
|
||||
"directory": VOICE_USERS_DIR,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -179,9 +194,15 @@ class VoiceFileCleanupUtils:
|
||||
"db_records_count": db_records_count,
|
||||
"orphaned_db_records_count": len(orphaned_db_records),
|
||||
"orphaned_files_count": len(orphaned_files),
|
||||
"orphaned_db_records": orphaned_db_records[:10], # Первые 10 для примера
|
||||
"orphaned_db_records": orphaned_db_records[
|
||||
:10
|
||||
], # Первые 10 для примера
|
||||
"orphaned_files": orphaned_files[:10], # Первые 10 для примера
|
||||
"status": "healthy" if len(orphaned_db_records) == 0 and len(orphaned_files) == 0 else "issues_found"
|
||||
"status": (
|
||||
"healthy"
|
||||
if len(orphaned_db_records) == 0 and len(orphaned_files) == 0
|
||||
else "issues_found"
|
||||
),
|
||||
}
|
||||
|
||||
logger.info(f"Диагностика завершена. Статус: {diagnostic_result['status']}")
|
||||
|
||||
@@ -20,7 +20,7 @@ COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"help": "voice_help",
|
||||
"restart": "voice_restart",
|
||||
"emoji": "voice_emoji",
|
||||
"refresh": "voice_refresh"
|
||||
"refresh": "voice_refresh",
|
||||
}
|
||||
|
||||
# Button texts
|
||||
@@ -33,7 +33,7 @@ BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"🎧Послушать": "voice_listen",
|
||||
"Отменить": "voice_cancel",
|
||||
"🔄Сбросить прослушивания": "voice_refresh_listen",
|
||||
"😊Узнать эмодзи": "voice_emoji"
|
||||
"😊Узнать эмодзи": "voice_emoji",
|
||||
}
|
||||
|
||||
# Callback data
|
||||
@@ -43,7 +43,7 @@ CALLBACK_DELETE = "delete"
|
||||
# Callback to command mapping for metrics
|
||||
CALLBACK_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"save": "voice_save",
|
||||
"delete": "voice_delete"
|
||||
"delete": "voice_delete",
|
||||
}
|
||||
|
||||
# File paths
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
class VoiceBotError(Exception):
|
||||
"""Базовое исключение для voice_bot"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VoiceMessageError(VoiceBotError):
|
||||
"""Ошибка при работе с голосовыми сообщениями"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AudioProcessingError(VoiceBotError):
|
||||
"""Ошибка при обработке аудио"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(VoiceBotError):
|
||||
"""Ошибка базы данных"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FileOperationError(VoiceBotError):
|
||||
"""Ошибка при работе с файлами"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -7,16 +7,24 @@ from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from aiogram.types import FSInputFile
|
||||
from helper_bot.handlers.voice.constants import (MESSAGE_DELAY_1,
|
||||
MESSAGE_DELAY_2,
|
||||
MESSAGE_DELAY_3,
|
||||
MESSAGE_DELAY_4, STICK_DIR,
|
||||
STICK_PATTERN, STICKER_DELAY,
|
||||
VOICE_USERS_DIR)
|
||||
from helper_bot.handlers.voice.exceptions import (AudioProcessingError,
|
||||
DatabaseError,
|
||||
FileOperationError,
|
||||
VoiceMessageError)
|
||||
|
||||
from helper_bot.handlers.voice.constants import (
|
||||
MESSAGE_DELAY_1,
|
||||
MESSAGE_DELAY_2,
|
||||
MESSAGE_DELAY_3,
|
||||
MESSAGE_DELAY_4,
|
||||
STICK_DIR,
|
||||
STICK_PATTERN,
|
||||
STICKER_DELAY,
|
||||
VOICE_USERS_DIR,
|
||||
)
|
||||
from helper_bot.handlers.voice.exceptions import (
|
||||
AudioProcessingError,
|
||||
DatabaseError,
|
||||
FileOperationError,
|
||||
VoiceMessageError,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
@@ -24,12 +32,16 @@ from logs.custom_logger import logger
|
||||
|
||||
class VoiceMessage:
|
||||
"""Модель голосового сообщения"""
|
||||
def __init__(self, file_name: str, user_id: int, date_added: datetime, file_id: int):
|
||||
|
||||
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:
|
||||
"""Сервис для работы с голосовыми сообщениями"""
|
||||
|
||||
@@ -48,12 +60,16 @@ class VoiceBotService:
|
||||
|
||||
random_stick_hello = random.choice(name_stick_hello)
|
||||
random_stick_hello = FSInputFile(path=random_stick_hello)
|
||||
logger.info(f"Стикер успешно получен. Наименование стикера: {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}')
|
||||
if self.settings["Settings"]["logs"]:
|
||||
await self._send_error_to_logs(
|
||||
f"Отправка приветственных стикеров лажает. Ошибка: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
@track_time("send_welcome_messages", "voice_bot_service")
|
||||
@@ -71,86 +87,88 @@ class VoiceBotService:
|
||||
markup = self._get_main_keyboard()
|
||||
await message.answer(
|
||||
text="<b>Привет.</b>",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(STICKER_DELAY)
|
||||
|
||||
# Отправляем описание
|
||||
await message.answer(
|
||||
text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска</i>",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_1)
|
||||
|
||||
# Отправляем аналогию
|
||||
await message.answer(
|
||||
text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
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',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_3)
|
||||
|
||||
# Отправляем информацию об анонимности
|
||||
await message.answer(
|
||||
text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
# Отправляем предложения
|
||||
await message.answer(
|
||||
text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
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',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||||
|
||||
# Отправляем информацию о помощи
|
||||
await message.answer(
|
||||
text="Так же можешь ознакомиться с инструкцией к боту по команде /help",
|
||||
parse_mode='html',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
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',
|
||||
parse_mode="html",
|
||||
reply_markup=markup,
|
||||
disable_web_page_preview=not self.settings['Telegram']['preview_link']
|
||||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке приветственных сообщений: {e}")
|
||||
raise VoiceMessageError(f"Не удалось отправить приветственные сообщения: {e}")
|
||||
raise VoiceMessageError(
|
||||
f"Не удалось отправить приветственные сообщения: {e}"
|
||||
)
|
||||
|
||||
@track_time("get_random_audio", "voice_bot_service")
|
||||
@track_errors("voice_bot_service", "get_random_audio")
|
||||
@@ -215,6 +233,7 @@ class VoiceBotService:
|
||||
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")
|
||||
@@ -223,11 +242,9 @@ class VoiceBotService:
|
||||
"""Отправить ошибку в логи"""
|
||||
try:
|
||||
from helper_bot.utils.helper_func import send_voice_message
|
||||
|
||||
await send_voice_message(
|
||||
self.settings['Telegram']['important_logs'],
|
||||
None,
|
||||
None,
|
||||
None
|
||||
self.settings["Telegram"]["important_logs"], None, None, None
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить ошибку в логи: {e}")
|
||||
@@ -245,25 +262,27 @@ class AudioFileService:
|
||||
"""Сгенерировать имя файла для аудио"""
|
||||
try:
|
||||
# Проверяем есть ли запись о файле в базе данных
|
||||
user_audio_count = await self.bot_db.get_user_audio_records_count(user_id=user_id)
|
||||
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'
|
||||
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])
|
||||
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}'
|
||||
file_name = f"message_from_{user_id}_number_{new_number}"
|
||||
|
||||
return file_name
|
||||
|
||||
@@ -273,7 +292,9 @@ class AudioFileService:
|
||||
|
||||
@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:
|
||||
async def save_audio_file(
|
||||
self, file_name: str, user_id: int, date_added: datetime, file_id: str
|
||||
) -> None:
|
||||
"""Сохранить информацию об аудио файле в базу данных"""
|
||||
try:
|
||||
# Проверяем существование файла перед сохранением в БД
|
||||
@@ -283,14 +304,18 @@ class AudioFileService:
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
|
||||
logger.info(f"Информация об аудио файле успешно сохранена в БД: {file_name}")
|
||||
logger.info(
|
||||
f"Информация об аудио файле успешно сохранена в БД: {file_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении аудио файла в БД: {e}")
|
||||
raise DatabaseError(f"Не удалось сохранить аудио файл в БД: {e}")
|
||||
|
||||
@track_time("save_audio_file_with_transaction", "audio_file_service")
|
||||
@track_errors("audio_file_service", "save_audio_file_with_transaction")
|
||||
async def save_audio_file_with_transaction(self, file_name: str, user_id: int, date_added: datetime, file_id: str) -> None:
|
||||
async def save_audio_file_with_transaction(
|
||||
self, file_name: str, user_id: int, date_added: datetime, file_id: str
|
||||
) -> None:
|
||||
"""Сохранить информацию об аудио файле в базу данных с транзакцией"""
|
||||
try:
|
||||
# Проверяем существование файла перед сохранением в БД
|
||||
@@ -301,20 +326,28 @@ class AudioFileService:
|
||||
|
||||
# Используем транзакцию для атомарности операции
|
||||
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
|
||||
logger.info(f"Информация об аудио файле успешно сохранена в БД с транзакцией: {file_name}")
|
||||
logger.info(
|
||||
f"Информация об аудио файле успешно сохранена в БД с транзакцией: {file_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении аудио файла в БД с транзакцией: {e}")
|
||||
raise DatabaseError(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, max_retries: int = 3) -> None:
|
||||
async def download_and_save_audio(
|
||||
self, bot, message, file_name: str, max_retries: int = 3
|
||||
) -> None:
|
||||
"""Скачать и сохранить аудио файл с retry механизмом"""
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logger.info(f"Попытка {attempt + 1}/{max_retries} скачивания и сохранения аудио: {file_name}")
|
||||
logger.info(
|
||||
f"Попытка {attempt + 1}/{max_retries} скачивания и сохранения аудио: {file_name}"
|
||||
)
|
||||
|
||||
# Проверяем наличие голосового сообщения
|
||||
if not message or not message.voice:
|
||||
@@ -331,11 +364,15 @@ class AudioFileService:
|
||||
logger.info(f"Получена информация о файле: {file_info.file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении информации о файле: {e}")
|
||||
raise FileOperationError(f"Не удалось получить информацию о файле: {e}")
|
||||
raise FileOperationError(
|
||||
f"Не удалось получить информацию о файле: {e}"
|
||||
)
|
||||
|
||||
# Скачиваем файл
|
||||
try:
|
||||
downloaded_file = await bot.download_file(file_path=file_info.file_path)
|
||||
downloaded_file = await bot.download_file(
|
||||
file_path=file_info.file_path
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при скачивании файла: {e}")
|
||||
raise FileOperationError(f"Не удалось скачать файл: {e}")
|
||||
@@ -368,7 +405,7 @@ class AudioFileService:
|
||||
logger.error(f"Ошибка при создании директории: {e}")
|
||||
raise FileOperationError(f"Не удалось создать директорию: {e}")
|
||||
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||||
logger.info(f"Сохраняем файл по пути: {file_path}")
|
||||
|
||||
# Сбрасываем позицию в файле перед сохранением
|
||||
@@ -376,7 +413,7 @@ class AudioFileService:
|
||||
|
||||
# Сохраняем файл
|
||||
try:
|
||||
with open(file_path, 'wb') as new_file:
|
||||
with open(file_path, "wb") as new_file:
|
||||
new_file.write(downloaded_file.read())
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при записи файла на диск: {e}")
|
||||
@@ -399,7 +436,9 @@ class AudioFileService:
|
||||
pass
|
||||
raise FileOperationError(error_msg)
|
||||
|
||||
logger.info(f"Файл успешно сохранен: {file_path}, размер: {saved_file_size} bytes")
|
||||
logger.info(
|
||||
f"Файл успешно сохранен: {file_path}, размер: {saved_file_size} bytes"
|
||||
)
|
||||
return # Успешное завершение
|
||||
|
||||
except Exception as e:
|
||||
@@ -407,22 +446,30 @@ class AudioFileService:
|
||||
logger.error(f"Попытка {attempt + 1}/{max_retries} неудачна: {e}")
|
||||
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = (attempt + 1) * 2 # Экспоненциальная задержка: 2, 4, 6 секунд
|
||||
logger.info(f"Ожидание {wait_time} секунд перед следующей попыткой...")
|
||||
wait_time = (
|
||||
attempt + 1
|
||||
) * 2 # Экспоненциальная задержка: 2, 4, 6 секунд
|
||||
logger.info(
|
||||
f"Ожидание {wait_time} секунд перед следующей попыткой..."
|
||||
)
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Все {max_retries} попыток скачивания неудачны")
|
||||
logger.error(f"Traceback последней ошибки: {traceback.format_exc()}")
|
||||
logger.error(
|
||||
f"Traceback последней ошибки: {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
# Если все попытки неудачны
|
||||
raise FileOperationError(f"Не удалось скачать и сохранить аудио после {max_retries} попыток. Последняя ошибка: {last_exception}")
|
||||
raise FileOperationError(
|
||||
f"Не удалось скачать и сохранить аудио после {max_retries} попыток. Последняя ошибка: {last_exception}"
|
||||
)
|
||||
|
||||
@track_time("verify_file_exists", "audio_file_service")
|
||||
@track_errors("audio_file_service", "verify_file_exists")
|
||||
async def verify_file_exists(self, file_name: str) -> bool:
|
||||
"""Проверить существование и валидность файла"""
|
||||
try:
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"Файл не существует: {file_path}")
|
||||
@@ -434,10 +481,14 @@ class AudioFileService:
|
||||
return False
|
||||
|
||||
if file_size < 100: # Минимальный размер для аудио файла
|
||||
logger.warning(f"Файл слишком маленький: {file_path}, размер: {file_size} bytes")
|
||||
logger.warning(
|
||||
f"Файл слишком маленький: {file_path}, размер: {file_size} bytes"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(f"Файл проверен и валиден: {file_path}, размер: {file_size} bytes")
|
||||
logger.info(
|
||||
f"Файл проверен и валиден: {file_path}, размер: {file_size} bytes"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -24,22 +24,28 @@ def format_time_ago(date_from_db: str) -> Optional[str]:
|
||||
much_hour_ago = round(date_difference / 3600, 0)
|
||||
much_days_ago = int(round(much_hour_ago / 24, 0))
|
||||
|
||||
message_with_date = ''
|
||||
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>'
|
||||
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>'
|
||||
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>'
|
||||
message_with_date = (
|
||||
f"<b>Последнее сообщение было записано {word_day_escaped} назад</b>"
|
||||
)
|
||||
|
||||
return message_with_date
|
||||
|
||||
@@ -52,11 +58,11 @@ def plural_time(type: int, n: float) -> str:
|
||||
"""Форматировать множественное число для времени"""
|
||||
word = []
|
||||
if type == 1:
|
||||
word = ['минуту', 'минуты', 'минут']
|
||||
word = ["минуту", "минуты", "минут"]
|
||||
elif type == 2:
|
||||
word = ['час', 'часа', 'часов']
|
||||
word = ["час", "часа", "часов"]
|
||||
elif type == 3:
|
||||
word = ['день', 'дня', 'дней']
|
||||
word = ["день", "дня", "дней"]
|
||||
else:
|
||||
return str(int(n))
|
||||
|
||||
@@ -68,7 +74,8 @@ def plural_time(type: int, n: float) -> str:
|
||||
p = 2
|
||||
|
||||
new_number = int(n)
|
||||
return str(new_number) + ' ' + word[p]
|
||||
return str(new_number) + " " + word[p]
|
||||
|
||||
|
||||
@track_time("get_last_message_text", "voice_utils")
|
||||
@track_errors("voice_utils", "get_last_message_text")
|
||||
@@ -89,7 +96,8 @@ async def get_last_message_text(bot_db) -> Optional[str]:
|
||||
|
||||
async def validate_voice_message(message) -> bool:
|
||||
"""Проверить валидность голосового сообщения"""
|
||||
return message.content_type == 'voice'
|
||||
return message.content_type == "voice"
|
||||
|
||||
|
||||
@track_time("get_user_emoji_safe", "voice_utils")
|
||||
@track_errors("voice_utils", "get_user_emoji_safe")
|
||||
@@ -98,7 +106,11 @@ 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 "😊"
|
||||
return (
|
||||
user_emoji
|
||||
if user_emoji and user_emoji != "Смайл еще не определен"
|
||||
else "😊"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении эмодзи пользователя {user_id}: {e}")
|
||||
return "😊"
|
||||
|
||||
@@ -6,31 +6,44 @@ from aiogram import F, Router, types
|
||||
from aiogram.filters import Command, MagicData, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.handlers.private.constants import BUTTON_TEXTS, FSM_STATES
|
||||
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,
|
||||
get_user_emoji_safe,
|
||||
validate_voice_message)
|
||||
from helper_bot.handlers.voice.utils import (
|
||||
get_last_message_text,
|
||||
get_user_emoji_safe,
|
||||
validate_voice_message,
|
||||
)
|
||||
from helper_bot.keyboards import get_reply_keyboard
|
||||
from helper_bot.keyboards.keyboards import (get_main_keyboard,
|
||||
get_reply_keyboard_for_voice)
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
get_main_keyboard,
|
||||
get_reply_keyboard_for_voice,
|
||||
)
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import \
|
||||
DependenciesMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
from helper_bot.utils import messages
|
||||
from helper_bot.utils.helper_func import (check_user_emoji, get_first_name,
|
||||
send_voice_message, update_user_info)
|
||||
from helper_bot.utils.helper_func import (
|
||||
check_user_emoji,
|
||||
get_first_name,
|
||||
send_voice_message,
|
||||
update_user_info,
|
||||
)
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (db_query_time, track_errors,
|
||||
track_file_operations, track_time)
|
||||
from helper_bot.utils.metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_file_operations,
|
||||
track_time,
|
||||
)
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class VoiceHandlers:
|
||||
def __init__(self, db, settings):
|
||||
self.db = db.get_db() if hasattr(db, 'get_db') else db
|
||||
self.db = db.get_db() if hasattr(db, "get_db") else db
|
||||
self.settings = settings
|
||||
self.router = Router()
|
||||
self._setup_handlers()
|
||||
@@ -44,46 +57,42 @@ class VoiceHandlers:
|
||||
self.router.message.register(
|
||||
self.cancel_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "Отменить"
|
||||
F.text == "Отменить",
|
||||
)
|
||||
|
||||
# Обработчик кнопки "Голосовой бот"
|
||||
self.router.message.register(
|
||||
self.voice_bot_button_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BUTTON_TEXTS["VOICE_BOT"]
|
||||
F.text == BUTTON_TEXTS["VOICE_BOT"],
|
||||
)
|
||||
|
||||
# Команды
|
||||
self.router.message.register(
|
||||
self.restart_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_RESTART)
|
||||
Command(CMD_RESTART),
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_EMOJI)
|
||||
Command(CMD_EMOJI),
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.help_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_HELP)
|
||||
self.help_function, ChatTypeFilter(chat_type=["private"]), Command(CMD_HELP)
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.start,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_START)
|
||||
self.start, ChatTypeFilter(chat_type=["private"]), Command(CMD_START)
|
||||
)
|
||||
|
||||
# Дополнительные команды
|
||||
self.router.message.register(
|
||||
self.refresh_listen_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command(CMD_REFRESH)
|
||||
Command(CMD_REFRESH),
|
||||
)
|
||||
|
||||
# Обработчики состояний и кнопок
|
||||
@@ -91,7 +100,7 @@ class VoiceHandlers:
|
||||
self.standup_write,
|
||||
StateFilter(STATE_START),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BTN_SPEAK
|
||||
F.text == BTN_SPEAK,
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
@@ -104,42 +113,58 @@ class VoiceHandlers:
|
||||
self.standup_listen_audio,
|
||||
StateFilter(STATE_START),
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BTN_LISTEN
|
||||
F.text == BTN_LISTEN,
|
||||
)
|
||||
|
||||
# Новые обработчики кнопок
|
||||
self.router.message.register(
|
||||
self.refresh_listen_function,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "🔄Сбросить прослушивания"
|
||||
F.text == "🔄Сбросить прослушивания",
|
||||
)
|
||||
|
||||
self.router.message.register(
|
||||
self.handle_emoji_message,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "😊Узнать эмодзи"
|
||||
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")):
|
||||
async def voice_bot_button_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) нажал кнопку 'Голосовой бот'")
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) нажал кнопку 'Голосовой бот'"
|
||||
)
|
||||
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}")
|
||||
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")
|
||||
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"Ошибка при проверке приветственного сообщения для пользователя {message.from_user.id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при проверке приветственного сообщения для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
# В случае ошибки вызываем start
|
||||
await self.start(message, state, bot_db, settings)
|
||||
|
||||
@@ -150,46 +175,46 @@ class VoiceHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызывается функция restart_function")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
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 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")
|
||||
self, message: types.Message, state: FSMContext, settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил информацию об эмодзи")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил информацию об эмодзи"
|
||||
)
|
||||
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')
|
||||
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")
|
||||
self, message: types.Message, state: FSMContext, settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию help_function")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию help_function"
|
||||
)
|
||||
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')
|
||||
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']
|
||||
disable_web_page_preview=not settings["Telegram"]["preview_link"],
|
||||
)
|
||||
await state.set_state(STATE_START)
|
||||
|
||||
@@ -201,25 +226,33 @@ class VoiceHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}): вызывается функция start")
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}): вызывается функция start"
|
||||
)
|
||||
await state.set_state(STATE_START)
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
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)
|
||||
logger.info(f"Приветственные сообщения отправлены пользователю {message.from_user.id}")
|
||||
logger.info(
|
||||
f"Приветственные сообщения отправлены пользователю {message.from_user.id}"
|
||||
)
|
||||
|
||||
# Отмечаем, что пользователь получил приветственное сообщение
|
||||
try:
|
||||
await bot_db.mark_voice_bot_welcome_received(message.from_user.id)
|
||||
logger.info(f"Пользователь {message.from_user.id}: отмечен как получивший приветствие")
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id}: отмечен как получивший приветствие"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отметке получения приветствия для пользователя {message.from_user.id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при отметке получения приветствия для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
|
||||
@track_time("cancel_handler", "voice_handlers")
|
||||
@track_errors("voice_handlers", "cancel_handler")
|
||||
@@ -228,13 +261,15 @@ class VoiceHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
"""Обработчик кнопки 'Отменить' - возвращает в начальное состояние"""
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
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 message.answer(
|
||||
text="Добро пожаловать в меню!", reply_markup=markup, parse_mode="HTML"
|
||||
)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
logger.info(f"Пользователь {message.from_user.id} возвращен в главное меню")
|
||||
|
||||
@@ -245,10 +280,12 @@ class VoiceHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию refresh_listen_function")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию refresh_listen_function"
|
||||
)
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
await update_user_info(VOICE_BOT_NAME, message)
|
||||
markup = get_main_keyboard()
|
||||
|
||||
@@ -256,15 +293,16 @@ class VoiceHandlers:
|
||||
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')
|
||||
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
|
||||
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(
|
||||
@@ -272,12 +310,16 @@ class VoiceHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию standup_write")
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию standup_write"
|
||||
)
|
||||
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')
|
||||
record_voice_message = messages.get_message(
|
||||
get_first_name(message), "RECORD_VOICE_MESSAGE"
|
||||
)
|
||||
await message.answer(text=record_voice_message, reply_markup=markup)
|
||||
|
||||
try:
|
||||
@@ -285,11 +327,12 @@ class VoiceHandlers:
|
||||
if message_with_date:
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить дату последнего сообщения для пользователя {message.from_user.id}: {e}')
|
||||
logger.error(
|
||||
f"Не удалось получить дату последнего сообщения для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
|
||||
await state.set_state(STATE_STANDUP_WRITE)
|
||||
|
||||
|
||||
@track_time("suggest_voice", "voice_handlers")
|
||||
@track_errors("voice_handlers", "suggest_voice")
|
||||
async def suggest_voice(
|
||||
@@ -297,12 +340,12 @@ class VoiceHandlers:
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
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'])
|
||||
await message.forward(chat_id=settings["Telegram"]["group_for_logs"])
|
||||
markup = get_main_keyboard()
|
||||
|
||||
if await validate_voice_message(message):
|
||||
@@ -310,28 +353,37 @@ class VoiceHandlers:
|
||||
|
||||
# Отправляем аудио в приватный канал
|
||||
sent_message = await send_voice_message(
|
||||
settings['Telegram']['group_for_posts'],
|
||||
settings["Telegram"]["group_for_posts"],
|
||||
message,
|
||||
message.voice.file_id,
|
||||
markup_for_voice
|
||||
markup_for_voice,
|
||||
)
|
||||
logger.info(
|
||||
f"Голосовое сообщение пользователя {message.from_user.id} отправлено в группу постов (message_id: {sent_message.message_id})"
|
||||
)
|
||||
logger.info(f"Голосовое сообщение пользователя {message.from_user.id} отправлено в группу постов (message_id: {sent_message.message_id})")
|
||||
|
||||
# Сохраняем в базу инфо о посте
|
||||
await bot_db.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id)
|
||||
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')
|
||||
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:
|
||||
logger.warning(f"Голосовое сообщение пользователя {message.from_user.id} не прошло валидацию")
|
||||
unknown_content_message = messages.get_message(get_first_name(message), 'UNKNOWN_CONTENT_MESSAGE')
|
||||
await message.forward(chat_id=settings['Telegram']['group_for_logs'])
|
||||
logger.warning(
|
||||
f"Голосовое сообщение пользователя {message.from_user.id} не прошло валидацию"
|
||||
)
|
||||
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")
|
||||
@track_file_operations("voice")
|
||||
@@ -340,57 +392,73 @@ class VoiceHandlers:
|
||||
self,
|
||||
message: types.Message,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил прослушивание аудио")
|
||||
settings: MagicData("settings"),
|
||||
):
|
||||
logger.info(
|
||||
f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил прослушивание аудио"
|
||||
)
|
||||
markup = get_main_keyboard()
|
||||
|
||||
# Создаем сервис для работы с аудио
|
||||
voice_service = VoiceBotService(bot_db, settings)
|
||||
|
||||
try:
|
||||
#TODO: удалить логику из хендлера
|
||||
# TODO: удалить логику из хендлера
|
||||
# Получаем случайное аудио
|
||||
audio_data = await voice_service.get_random_audio(message.from_user.id)
|
||||
|
||||
if not audio_data:
|
||||
logger.warning(f"Для пользователя {message.from_user.id} не найдено доступных аудио для прослушивания")
|
||||
no_audio_message = messages.get_message(get_first_name(message), 'NO_AUDIO_MESSAGE')
|
||||
logger.warning(
|
||||
f"Для пользователя {message.from_user.id} не найдено доступных аудио для прослушивания"
|
||||
)
|
||||
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'Не удалось получить последнюю дату для пользователя {message.from_user.id}: {e}')
|
||||
logger.error(
|
||||
f"Не удалось получить последнюю дату для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
return
|
||||
|
||||
audio_for_user, date_added, user_emoji = audio_data
|
||||
|
||||
# Получаем путь к файлу
|
||||
path = Path(f'{VOICE_USERS_DIR}/{audio_for_user}.ogg')
|
||||
path = Path(f"{VOICE_USERS_DIR}/{audio_for_user}.ogg")
|
||||
|
||||
# Проверяем существование файла
|
||||
if not path.exists():
|
||||
logger.error(f"Файл не найден: {path} для пользователя {message.from_user.id}")
|
||||
logger.error(
|
||||
f"Файл не найден: {path} для пользователя {message.from_user.id}"
|
||||
)
|
||||
# Дополнительная диагностика
|
||||
logger.error(f"Директория {VOICE_USERS_DIR} существует: {Path(VOICE_USERS_DIR).exists()}")
|
||||
logger.error(
|
||||
f"Директория {VOICE_USERS_DIR} существует: {Path(VOICE_USERS_DIR).exists()}"
|
||||
)
|
||||
if Path(VOICE_USERS_DIR).exists():
|
||||
files_in_dir = list(Path(VOICE_USERS_DIR).glob("*.ogg"))
|
||||
logger.error(f"Файлы в директории: {[f.name for f in files_in_dir]}")
|
||||
logger.error(
|
||||
f"Файлы в директории: {[f.name for f in files_in_dir]}"
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
text="Файл аудио не найден. Обратитесь к администратору.",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем размер файла
|
||||
if path.stat().st_size == 0:
|
||||
logger.error(f"Файл пустой: {path} для пользователя {message.from_user.id}")
|
||||
logger.error(
|
||||
f"Файл пустой: {path} для пользователя {message.from_user.id}"
|
||||
)
|
||||
await message.answer(
|
||||
text="Файл аудио поврежден. Обратитесь к администратору.",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -398,11 +466,13 @@ class VoiceHandlers:
|
||||
|
||||
# Формируем подпись
|
||||
if user_emoji:
|
||||
caption = f'{user_emoji}\nДата записи: {date_added}'
|
||||
caption = f"{user_emoji}\nДата записи: {date_added}"
|
||||
else:
|
||||
caption = f'Дата записи: {date_added}'
|
||||
caption = f"Дата записи: {date_added}"
|
||||
|
||||
logger.info(f"Подготовлено голосовое сообщение для пользователя {message.from_user.id}: {caption}")
|
||||
logger.info(
|
||||
f"Подготовлено голосовое сообщение для пользователя {message.from_user.id}: {caption}"
|
||||
)
|
||||
|
||||
try:
|
||||
from helper_bot.utils.rate_limiter import send_with_rate_limit
|
||||
@@ -412,25 +482,31 @@ class VoiceHandlers:
|
||||
chat_id=message.chat.id,
|
||||
voice=voice,
|
||||
caption=caption,
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
|
||||
await send_with_rate_limit(_send_voice, message.chat.id)
|
||||
|
||||
# Маркируем сообщение как прослушанное только после успешной отправки
|
||||
await voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
|
||||
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)
|
||||
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
|
||||
text=f"Осталось непрослушанных: <b>{remaining_count}</b>",
|
||||
reply_markup=markup,
|
||||
)
|
||||
|
||||
except Exception as voice_error:
|
||||
if "VOICE_MESSAGES_FORBIDDEN" in str(voice_error):
|
||||
# Если голосовые сообщения запрещены, отправляем информативное сообщение
|
||||
logger.warning(f"Пользователь {message.from_user.id} запретил получение голосовых сообщений")
|
||||
logger.warning(
|
||||
f"Пользователь {message.from_user.id} запретил получение голосовых сообщений"
|
||||
)
|
||||
|
||||
privacy_message = "🔇 К сожалению, у тебя закрыт доступ к получению голосовых сообщений.\n\nДля продолжения взаимодействия с ботом необходимо дать возможность мне присылать войсы в настройках приватности Telegram.\n\n💡 Как это сделать:\n1. Открой настройки Telegram\n2. Перейди в 'Конфиденциальность и безопасность'\n3. Выбери 'Голосовые сообщения'\n4. Разреши получение от 'Всех' или добавь меня в исключения"
|
||||
|
||||
@@ -438,12 +514,16 @@ class VoiceHandlers:
|
||||
return # Выходим без записи о прослушивании
|
||||
|
||||
else:
|
||||
logger.error(f"Ошибка при отправке голосового сообщения пользователю {message.from_user.id}: {voice_error}")
|
||||
logger.error(
|
||||
f"Ошибка при отправке голосового сообщения пользователю {message.from_user.id}: {voice_error}"
|
||||
)
|
||||
raise voice_error
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при прослушивании аудио для пользователя {message.from_user.id}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при прослушивании аудио для пользователя {message.from_user.id}: {e}"
|
||||
)
|
||||
await message.answer(
|
||||
text="Произошла ошибка при получении аудио. Попробуйте позже.",
|
||||
reply_markup=markup
|
||||
reply_markup=markup,
|
||||
)
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
from aiogram import types
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import track_errors, track_time
|
||||
|
||||
|
||||
def get_reply_keyboard_for_post():
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(types.InlineKeyboardButton(
|
||||
text="Опубликовать", callback_data="publish"),
|
||||
types.InlineKeyboardButton(
|
||||
text="Отклонить", callback_data="decline")
|
||||
)
|
||||
builder.row(types.InlineKeyboardButton(
|
||||
text="👮♂️ Забанить", callback_data="ban")
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(text="Опубликовать", callback_data="publish"),
|
||||
types.InlineKeyboardButton(text="Отклонить", callback_data="decline"),
|
||||
)
|
||||
builder.row(types.InlineKeyboardButton(text="👮♂️ Забанить", callback_data="ban"))
|
||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||
return markup
|
||||
|
||||
|
||||
|
||||
async def get_reply_keyboard(db, user_id):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
||||
@@ -43,21 +40,22 @@ def get_reply_keyboard_admin():
|
||||
builder.row(
|
||||
types.KeyboardButton(text="Бан (Список)"),
|
||||
types.KeyboardButton(text="Бан по нику"),
|
||||
types.KeyboardButton(text="Бан по ID")
|
||||
types.KeyboardButton(text="Бан по ID"),
|
||||
)
|
||||
builder.row(
|
||||
types.KeyboardButton(text="Разбан (список)"),
|
||||
types.KeyboardButton(text="📊 ML Статистика")
|
||||
)
|
||||
builder.row(
|
||||
types.KeyboardButton(text="Вернуться в бота")
|
||||
types.KeyboardButton(text="📊 ML Статистика"),
|
||||
)
|
||||
builder.row(types.KeyboardButton(text="Вернуться в бота"))
|
||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||
return markup
|
||||
|
||||
|
||||
@track_time("create_keyboard_with_pagination", "keyboard_service")
|
||||
@track_errors("keyboard_service", "create_keyboard_with_pagination")
|
||||
def create_keyboard_with_pagination(page: int, total_items: int, array_items: list, callback: str):
|
||||
def create_keyboard_with_pagination(
|
||||
page: int, total_items: int, array_items: list, callback: str
|
||||
):
|
||||
"""
|
||||
Создает клавиатуру с пагинацией для заданного набора элементов и устанавливает необходимый callback
|
||||
|
||||
@@ -77,7 +75,9 @@ def create_keyboard_with_pagination(page: int, total_items: int, array_items: li
|
||||
if not array_items:
|
||||
# Если нет элементов, возвращаем только кнопку "Назад"
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
home_button = types.InlineKeyboardButton(text="🏠 Назад", callback_data="return")
|
||||
home_button = types.InlineKeyboardButton(
|
||||
text="🏠 Назад", callback_data="return"
|
||||
)
|
||||
keyboard.row(home_button)
|
||||
return keyboard.as_markup()
|
||||
|
||||
@@ -100,9 +100,12 @@ def create_keyboard_with_pagination(page: int, total_items: int, array_items: li
|
||||
current_row = []
|
||||
|
||||
for i in range(start_index, end_index):
|
||||
current_row.append(types.InlineKeyboardButton(
|
||||
text=f"{array_items[i][0]}", callback_data=f"{callback}_{array_items[i][1]}"
|
||||
))
|
||||
current_row.append(
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{array_items[i][0]}",
|
||||
callback_data=f"{callback}_{array_items[i][1]}",
|
||||
)
|
||||
)
|
||||
|
||||
# Когда набирается 3 кнопки, добавляем ряд
|
||||
if len(current_row) == 3:
|
||||
@@ -146,7 +149,11 @@ def create_keyboard_for_ban_reason():
|
||||
builder.add(types.KeyboardButton(text="Спам"))
|
||||
builder.add(types.KeyboardButton(text="Заебал стикерами"))
|
||||
builder.row(types.KeyboardButton(text="Реклама здесь: @kerrad1 "))
|
||||
builder.row(types.KeyboardButton(text="Тема с лагерями: https://vk.com/topic-75343895_50049913"))
|
||||
builder.row(
|
||||
types.KeyboardButton(
|
||||
text="Тема с лагерями: https://vk.com/topic-75343895_50049913"
|
||||
)
|
||||
)
|
||||
builder.row(types.KeyboardButton(text="Отменить"))
|
||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||
return markup
|
||||
@@ -176,12 +183,12 @@ def get_main_keyboard():
|
||||
# Первая строка: Высказаться и послушать
|
||||
builder.row(
|
||||
types.KeyboardButton(text="🎤Высказаться"),
|
||||
types.KeyboardButton(text="🎧Послушать")
|
||||
types.KeyboardButton(text="🎧Послушать"),
|
||||
)
|
||||
# Вторая строка: сбросить прослушивания и узнать эмодзи
|
||||
builder.row(
|
||||
types.KeyboardButton(text="🔄Сбросить прослушивания"),
|
||||
types.KeyboardButton(text="😊Узнать эмодзи")
|
||||
types.KeyboardButton(text="😊Узнать эмодзи"),
|
||||
)
|
||||
# Третья строка: Вернуться в меню
|
||||
builder.row(types.KeyboardButton(text="Отменить"))
|
||||
@@ -191,11 +198,7 @@ def get_main_keyboard():
|
||||
|
||||
def get_reply_keyboard_for_voice():
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(types.InlineKeyboardButton(
|
||||
text="Сохранить", callback_data="save")
|
||||
)
|
||||
builder.row(types.InlineKeyboardButton(
|
||||
text="Удалить", callback_data="delete")
|
||||
)
|
||||
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
|
||||
|
||||
@@ -6,22 +6,25 @@ from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.fsm.strategy import FSMStrategy
|
||||
|
||||
from helper_bot.handlers.admin import admin_router
|
||||
from helper_bot.handlers.callback import callback_router
|
||||
from helper_bot.handlers.group import group_router
|
||||
from helper_bot.handlers.private import private_router
|
||||
from helper_bot.handlers.voice import VoiceHandlers
|
||||
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
|
||||
from helper_bot.middlewares.dependencies_middleware import \
|
||||
DependenciesMiddleware
|
||||
from helper_bot.middlewares.metrics_middleware import (ErrorMetricsMiddleware,
|
||||
MetricsMiddleware)
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
from helper_bot.middlewares.metrics_middleware import (
|
||||
ErrorMetricsMiddleware,
|
||||
MetricsMiddleware,
|
||||
)
|
||||
from helper_bot.middlewares.rate_limit_middleware import RateLimitMiddleware
|
||||
from helper_bot.server_prometheus import (start_metrics_server,
|
||||
stop_metrics_server)
|
||||
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):
|
||||
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:
|
||||
@@ -30,14 +33,21 @@ async def start_bot_with_retry(bot: Bot, dp: Dispatcher, max_retries: int = 5, b
|
||||
break
|
||||
except Exception as e:
|
||||
error_msg = str(e).lower()
|
||||
if any(keyword in error_msg for keyword in ['network', 'disconnected', 'timeout', 'connection']):
|
||||
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})")
|
||||
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}")
|
||||
logging.error(
|
||||
f"Превышено максимальное количество попыток запуска бота: {e}"
|
||||
)
|
||||
raise
|
||||
else:
|
||||
logging.error(f"Критическая ошибка при запуске бота: {e}")
|
||||
@@ -45,11 +55,15 @@ async def start_bot_with_retry(bot: Bot, dp: Dispatcher, max_retries: int = 5, b
|
||||
|
||||
|
||||
async def start_bot(bdf):
|
||||
token = bdf.settings['Telegram']['bot_token']
|
||||
bot = Bot(token=token, default=DefaultBotProperties(
|
||||
parse_mode='HTML',
|
||||
link_preview_is_disabled=bdf.settings['Telegram']['preview_link']
|
||||
), timeout=60.0) # Увеличиваем timeout для стабильности
|
||||
token = bdf.settings["Telegram"]["bot_token"]
|
||||
bot = Bot(
|
||||
token=token,
|
||||
default=DefaultBotProperties(
|
||||
parse_mode="HTML",
|
||||
link_preview_is_disabled=bdf.settings["Telegram"]["preview_link"],
|
||||
),
|
||||
timeout=60.0,
|
||||
) # Увеличиваем timeout для стабильности
|
||||
|
||||
dp = Dispatcher(storage=MemoryStorage(), fsm_strategy=FSMStrategy.GLOBAL_USER)
|
||||
|
||||
@@ -64,7 +78,9 @@ async def start_bot(bdf):
|
||||
voice_router = voice_handlers.router
|
||||
|
||||
# Middleware уже добавлены на уровне dispatcher
|
||||
dp.include_routers(admin_router, private_router, callback_router, group_router, voice_router)
|
||||
dp.include_routers(
|
||||
admin_router, private_router, callback_router, group_router, voice_router
|
||||
)
|
||||
|
||||
# Получаем scoring_manager для использования в shutdown
|
||||
scoring_manager = bdf.get_scoring_manager()
|
||||
@@ -90,8 +106,8 @@ async def start_bot(bdf):
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
|
||||
# Запускаем HTTP сервер для метрик параллельно с ботом
|
||||
metrics_host = bdf.settings.get('Metrics', {}).get('host', '0.0.0.0')
|
||||
metrics_port = bdf.settings.get('Metrics', {}).get('port', 8080)
|
||||
metrics_host = bdf.settings.get("Metrics", {}).get("host", "0.0.0.0")
|
||||
metrics_port = bdf.settings.get("Metrics", {}).get("port", 8080)
|
||||
|
||||
try:
|
||||
# Запускаем метрики сервер
|
||||
|
||||
@@ -8,7 +8,9 @@ from aiogram.types import Message
|
||||
class AlbumGetter:
|
||||
"""Вспомогательный класс для получения полной медиагруппы из middleware"""
|
||||
|
||||
def __init__(self, album_data: Dict[str, Any], media_group_id: str, event: asyncio.Event):
|
||||
def __init__(
|
||||
self, album_data: Dict[str, Any], media_group_id: str, event: asyncio.Event
|
||||
):
|
||||
self.album_data = album_data
|
||||
self.media_group_id = media_group_id
|
||||
self.event = event
|
||||
@@ -141,7 +143,7 @@ class AlbumMiddleware(BaseMiddleware):
|
||||
"messages": [],
|
||||
"event": album_event,
|
||||
"task": None,
|
||||
"first_message_id": message_id
|
||||
"first_message_id": message_id,
|
||||
}
|
||||
# Запускаем фоновую задачу для сбора медиагруппы
|
||||
task = asyncio.create_task(self._collect_album_background(media_group_id))
|
||||
@@ -157,9 +159,7 @@ class AlbumMiddleware(BaseMiddleware):
|
||||
|
||||
# Передаем объект-геттер в data, чтобы handler мог получить полную медиагруппу
|
||||
album_getter = AlbumGetter(
|
||||
self.album_data,
|
||||
media_group_id,
|
||||
self.album_data[media_group_id]["event"]
|
||||
self.album_data, media_group_id, self.album_data[media_group_id]["event"]
|
||||
)
|
||||
data["album_getter"] = album_getter
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, Dict
|
||||
|
||||
from aiogram import BaseMiddleware, types
|
||||
from aiogram.types import CallbackQuery, Message, TelegramObject
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from logs.custom_logger import logger
|
||||
|
||||
@@ -12,7 +13,9 @@ BotDB = bdf.get_db()
|
||||
|
||||
|
||||
class BlacklistMiddleware(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:
|
||||
# Проверяем тип события и получаем пользователя
|
||||
user = None
|
||||
if isinstance(event, Message):
|
||||
@@ -24,20 +27,28 @@ class BlacklistMiddleware(BaseMiddleware):
|
||||
if not user:
|
||||
return await handler(event, data)
|
||||
|
||||
logger.info(f'Вызов BlacklistMiddleware для пользователя {user.username}')
|
||||
logger.info(f"Вызов BlacklistMiddleware для пользователя {user.username}")
|
||||
|
||||
# Используем асинхронную версию для предотвращения блокировки
|
||||
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(user.id)
|
||||
# Экранируем потенциально проблемные символы
|
||||
reason = html.escape(str(user_info[1])) if user_info and user_info[1] else "Не указана"
|
||||
reason = (
|
||||
html.escape(str(user_info[1]))
|
||||
if user_info and user_info[1]
|
||||
else "Не указана"
|
||||
)
|
||||
|
||||
# Преобразуем timestamp в человекочитаемый формат
|
||||
if user_info and user_info[2]:
|
||||
try:
|
||||
timestamp = int(user_info[2])
|
||||
date_unban = datetime.fromtimestamp(timestamp).strftime("%d-%m-%Y %H:%M")
|
||||
date_unban = datetime.fromtimestamp(timestamp).strftime(
|
||||
"%d-%m-%Y %H:%M"
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
date_unban = "Не указана"
|
||||
else:
|
||||
@@ -46,13 +57,17 @@ class BlacklistMiddleware(BaseMiddleware):
|
||||
# Отправляем сообщение в зависимости от типа события
|
||||
if isinstance(event, Message):
|
||||
await event.answer(
|
||||
f"<b>Ты заблокирован.</b>\n<b>Причина блокировки:</b> {reason}\n<b>Дата разбана:</b> {date_unban}")
|
||||
f"<b>Ты заблокирован.</b>\n<b>Причина блокировки:</b> {reason}\n<b>Дата разбана:</b> {date_unban}"
|
||||
)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.answer(
|
||||
f"<b>Ты заблокирован.</b>\n<b>Причина блокировки:</b> {reason}\n<b>Дата разбана:</b> {date_unban}",
|
||||
show_alert=True)
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
logger.info(f'BlacklistMiddleware результат для пользователя: {user.username} доступ разрешен')
|
||||
logger.info(
|
||||
f"BlacklistMiddleware результат для пользователя: {user.username} доступ разрешен"
|
||||
)
|
||||
return await handler(event, data)
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Any, Dict
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from logs.custom_logger import logger
|
||||
|
||||
@@ -9,20 +10,24 @@ from logs.custom_logger import logger
|
||||
class DependenciesMiddleware(BaseMiddleware):
|
||||
"""Универсальная middleware для внедрения зависимостей во все хендлеры"""
|
||||
|
||||
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||
async def __call__(
|
||||
self, handler, event: TelegramObject, data: Dict[str, Any]
|
||||
) -> Any:
|
||||
try:
|
||||
# Получаем глобальные зависимости
|
||||
bdf = get_global_instance()
|
||||
|
||||
# Внедряем зависимости в data для MagicData
|
||||
if 'bot_db' not in data:
|
||||
data['bot_db'] = bdf.get_db()
|
||||
if 'settings' not in data:
|
||||
data['settings'] = bdf.settings
|
||||
data['bot'] = data.get('bot')
|
||||
data['dp'] = data.get('dp')
|
||||
if "bot_db" not in data:
|
||||
data["bot_db"] = bdf.get_db()
|
||||
if "settings" not in data:
|
||||
data["settings"] = bdf.settings
|
||||
data["bot"] = data.get("bot")
|
||||
data["dp"] = data.get("dp")
|
||||
|
||||
logger.debug(f"DependenciesMiddleware: внедрены зависимости для {type(event).__name__}")
|
||||
logger.debug(
|
||||
f"DependenciesMiddleware: внедрены зависимости для {type(event).__name__}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в DependenciesMiddleware: {e}")
|
||||
|
||||
@@ -16,16 +16,16 @@ from ..utils.metrics import metrics
|
||||
|
||||
# Import button command mapping
|
||||
try:
|
||||
from ..handlers.admin.constants import (ADMIN_BUTTON_COMMAND_MAPPING,
|
||||
ADMIN_COMMANDS)
|
||||
from ..handlers.admin.constants import ADMIN_BUTTON_COMMAND_MAPPING, ADMIN_COMMANDS
|
||||
from ..handlers.callback.constants import CALLBACK_COMMAND_MAPPING
|
||||
from ..handlers.private.constants import BUTTON_COMMAND_MAPPING
|
||||
from ..handlers.voice.constants import \
|
||||
BUTTON_COMMAND_MAPPING as VOICE_BUTTON_COMMAND_MAPPING
|
||||
from ..handlers.voice.constants import \
|
||||
CALLBACK_COMMAND_MAPPING as VOICE_CALLBACK_COMMAND_MAPPING
|
||||
from ..handlers.voice.constants import \
|
||||
COMMAND_MAPPING as VOICE_COMMAND_MAPPING
|
||||
from ..handlers.voice.constants import (
|
||||
BUTTON_COMMAND_MAPPING as VOICE_BUTTON_COMMAND_MAPPING,
|
||||
)
|
||||
from ..handlers.voice.constants import (
|
||||
CALLBACK_COMMAND_MAPPING as VOICE_CALLBACK_COMMAND_MAPPING,
|
||||
)
|
||||
from ..handlers.voice.constants import COMMAND_MAPPING as VOICE_COMMAND_MAPPING
|
||||
except ImportError:
|
||||
# Fallback if constants not available
|
||||
BUTTON_COMMAND_MAPPING = {}
|
||||
@@ -52,13 +52,16 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
"""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:
|
||||
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
|
||||
|
||||
@@ -67,12 +70,18 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
event_metrics = {}
|
||||
|
||||
# Process event based on type
|
||||
if hasattr(event, 'message') and event.message:
|
||||
event_metrics = await self._record_comprehensive_message_metrics(event.message)
|
||||
if hasattr(event, "message") and event.message:
|
||||
event_metrics = await self._record_comprehensive_message_metrics(
|
||||
event.message
|
||||
)
|
||||
command_info = self._extract_command_info_with_fallback(event.message)
|
||||
elif hasattr(event, 'callback_query') and event.callback_query:
|
||||
event_metrics = await self._record_comprehensive_callback_metrics(event.callback_query)
|
||||
command_info = self._extract_callback_command_info_with_fallback(event.callback_query)
|
||||
elif hasattr(event, "callback_query") and event.callback_query:
|
||||
event_metrics = await self._record_comprehensive_callback_metrics(
|
||||
event.callback_query
|
||||
)
|
||||
command_info = self._extract_callback_command_info_with_fallback(
|
||||
event.callback_query
|
||||
)
|
||||
elif isinstance(event, Message):
|
||||
event_metrics = await self._record_comprehensive_message_metrics(event)
|
||||
command_info = self._extract_command_info_with_fallback(event)
|
||||
@@ -85,7 +94,9 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
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__}")
|
||||
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()
|
||||
@@ -96,22 +107,19 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
# Record successful execution metrics
|
||||
handler_name = self._get_handler_name(handler)
|
||||
|
||||
metrics.record_method_duration(
|
||||
handler_name,
|
||||
duration,
|
||||
"handler",
|
||||
"success"
|
||||
)
|
||||
metrics.record_method_duration(handler_name, duration, "handler", "success")
|
||||
|
||||
if command_info:
|
||||
metrics.record_command(
|
||||
command_info['command'],
|
||||
command_info['handler_type'],
|
||||
command_info['user_type'],
|
||||
"success"
|
||||
command_info["command"],
|
||||
command_info["handler_type"],
|
||||
command_info["user_type"],
|
||||
"success",
|
||||
)
|
||||
|
||||
await self._record_additional_success_metrics(event, event_metrics, handler_name)
|
||||
await self._record_additional_success_metrics(
|
||||
event, event_metrics, handler_name
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -122,40 +130,36 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
handler_name = self._get_handler_name(handler)
|
||||
error_type = type(e).__name__
|
||||
|
||||
metrics.record_method_duration(
|
||||
handler_name,
|
||||
duration,
|
||||
"handler",
|
||||
"error"
|
||||
)
|
||||
metrics.record_method_duration(handler_name, duration, "handler", "error")
|
||||
|
||||
metrics.record_error(
|
||||
error_type,
|
||||
"handler",
|
||||
handler_name
|
||||
)
|
||||
metrics.record_error(error_type, "handler", handler_name)
|
||||
|
||||
if command_info:
|
||||
metrics.record_command(
|
||||
command_info['command'],
|
||||
command_info['handler_type'],
|
||||
command_info['user_type'],
|
||||
"error"
|
||||
command_info["command"],
|
||||
command_info["handler_type"],
|
||||
command_info["user_type"],
|
||||
"error",
|
||||
)
|
||||
|
||||
await self._record_additional_error_metrics(event, event_metrics, handler_name, error_type)
|
||||
await self._record_additional_error_metrics(
|
||||
event, event_metrics, handler_name, error_type
|
||||
)
|
||||
|
||||
raise
|
||||
finally:
|
||||
# Record middleware execution time
|
||||
middleware_duration = time.time() - start_time
|
||||
metrics.record_middleware("MetricsMiddleware", middleware_duration, "success")
|
||||
metrics.record_middleware(
|
||||
"MetricsMiddleware", middleware_duration, "success"
|
||||
)
|
||||
|
||||
async def _update_active_users_metric(self):
|
||||
"""Periodically update active users metric from database."""
|
||||
try:
|
||||
#TODO: Должна подключаться к базе данных, а не к глобальному экземпляру
|
||||
# TODO: Должна подключаться к базе данных, а не к глобальному экземпляру
|
||||
from ..utils.base_dependency_factory import get_global_instance
|
||||
|
||||
bdf = get_global_instance()
|
||||
bot_db = bdf.get_db()
|
||||
|
||||
@@ -163,17 +167,19 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
# Простой подсчет всех пользователей в базе
|
||||
total_users_query = "SELECT COUNT(DISTINCT user_id) as total FROM our_users"
|
||||
total_users_result = await bot_db.fetch_one(total_users_query)
|
||||
total_users = total_users_result['total'] if total_users_result else 1
|
||||
total_users = total_users_result["total"] if total_users_result else 1
|
||||
|
||||
# Подсчет активных за день пользователей (date_changed - это Unix timestamp)
|
||||
daily_users_query = "SELECT COUNT(DISTINCT user_id) as daily FROM our_users WHERE date_changed > (strftime('%s', 'now', '-1 day'))"
|
||||
daily_users_result = await bot_db.fetch_one(daily_users_query)
|
||||
daily_users = daily_users_result['daily'] if daily_users_result else 1
|
||||
daily_users = daily_users_result["daily"] if daily_users_result else 1
|
||||
|
||||
# Устанавливаем метрики с правильными лейблами
|
||||
metrics.set_active_users(daily_users, "daily")
|
||||
metrics.set_total_users(total_users)
|
||||
self.logger.info(f"📊 Active users metric updated: {daily_users} (daily), {total_users} (total)")
|
||||
self.logger.info(
|
||||
f"📊 Active users metric updated: {daily_users} (daily), {total_users} (total)"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"❌ Failed to update users metric: {e}")
|
||||
@@ -181,7 +187,9 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
metrics.set_active_users(1, "daily")
|
||||
metrics.set_total_users(1)
|
||||
|
||||
async def _record_comprehensive_message_metrics(self, message: Message) -> Dict[str, Any]:
|
||||
async def _record_comprehensive_message_metrics(
|
||||
self, message: Message
|
||||
) -> Dict[str, Any]:
|
||||
"""Record comprehensive message metrics."""
|
||||
# Determine message type
|
||||
message_type = "text"
|
||||
@@ -213,118 +221,128 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
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
|
||||
"message_type": message_type,
|
||||
"chat_type": chat_type,
|
||||
"user_id": message.from_user.id if message.from_user else None,
|
||||
"is_bot": message.from_user.is_bot if message.from_user else False,
|
||||
}
|
||||
|
||||
async def _record_comprehensive_callback_metrics(self, callback: CallbackQuery) -> Dict[str, Any]:
|
||||
async def _record_comprehensive_callback_metrics(
|
||||
self, callback: CallbackQuery
|
||||
) -> Dict[str, Any]:
|
||||
"""Record comprehensive callback metrics."""
|
||||
# Record callback message
|
||||
metrics.record_message("callback_query", "callback", "callback_handler")
|
||||
|
||||
return {
|
||||
'callback_data': callback.data,
|
||||
'user_id': callback.from_user.id if callback.from_user else None,
|
||||
'is_bot': callback.from_user.is_bot if callback.from_user else False
|
||||
"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]:
|
||||
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"
|
||||
"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]]:
|
||||
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
|
||||
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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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"
|
||||
"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]]:
|
||||
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)
|
||||
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"
|
||||
"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"
|
||||
"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
|
||||
@@ -343,34 +361,42 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
command = f"callback_{callback_data[:20]}"
|
||||
|
||||
return {
|
||||
'command': command,
|
||||
'user_type': "user" if callback.from_user else "unknown",
|
||||
'handler_type': "unknown_callback_handler"
|
||||
"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):
|
||||
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:
|
||||
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'):
|
||||
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):
|
||||
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'):
|
||||
if event_metrics.get("user_id"):
|
||||
# You can add user-specific error tracking here
|
||||
pass
|
||||
|
||||
@@ -380,21 +406,22 @@ class MetricsMiddleware(BaseMiddleware):
|
||||
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>':
|
||||
if hasattr(handler, "__name__") and handler.__name__ != "<lambda>":
|
||||
return handler.__name__
|
||||
elif hasattr(handler, '__qualname__') and handler.__qualname__ != '<lambda>':
|
||||
elif hasattr(handler, "__qualname__") and handler.__qualname__ != "<lambda>":
|
||||
return handler.__qualname__
|
||||
elif hasattr(handler, 'callback') and hasattr(handler.callback, '__name__'):
|
||||
elif hasattr(handler, "callback") and hasattr(handler.callback, "__name__"):
|
||||
return handler.callback.__name__
|
||||
elif hasattr(handler, 'view') and hasattr(handler.view, '__name__'):
|
||||
elif hasattr(handler, "view") and hasattr(handler.view, "__name__"):
|
||||
return handler.view.__name__
|
||||
else:
|
||||
# Пытаемся получить имя из строкового представления
|
||||
handler_str = str(handler)
|
||||
if 'function' in handler_str:
|
||||
if "function" in handler_str:
|
||||
# Извлекаем имя функции из строки
|
||||
import re
|
||||
match = re.search(r'function\s+(\w+)', handler_str)
|
||||
|
||||
match = re.search(r"function\s+(\w+)", handler_str)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return "unknown"
|
||||
@@ -411,12 +438,12 @@ class DatabaseMetricsMiddleware(BaseMiddleware):
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
"""Process event and collect database metrics."""
|
||||
|
||||
# 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"
|
||||
|
||||
# Record middleware start
|
||||
start_time = time.time()
|
||||
@@ -434,11 +461,7 @@ class DatabaseMetricsMiddleware(BaseMiddleware):
|
||||
# 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
|
||||
)
|
||||
metrics.record_error(type(e).__name__, "database_middleware", handler_name)
|
||||
raise
|
||||
|
||||
|
||||
@@ -453,7 +476,7 @@ class ErrorMetricsMiddleware(BaseMiddleware):
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
"""Process event and collect error metrics."""
|
||||
|
||||
@@ -472,13 +495,11 @@ class ErrorMetricsMiddleware(BaseMiddleware):
|
||||
except Exception as e:
|
||||
# Record error metrics
|
||||
duration = time.time() - start_time
|
||||
handler_name = handler.__name__ if hasattr(handler, '__name__') else "unknown"
|
||||
|
||||
metrics.record_middleware("ErrorMetricsMiddleware", duration, "error")
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"error_middleware",
|
||||
handler_name
|
||||
handler_name = (
|
||||
handler.__name__ if hasattr(handler, "__name__") else "unknown"
|
||||
)
|
||||
|
||||
metrics.record_middleware("ErrorMetricsMiddleware", duration, "error")
|
||||
metrics.record_error(type(e).__name__, "error_middleware", handler_name)
|
||||
|
||||
raise
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""
|
||||
Middleware для автоматического применения rate limiting ко всем входящим сообщениям
|
||||
"""
|
||||
|
||||
from typing import Any, Awaitable, Callable, Dict, Union
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
from aiogram.types import (CallbackQuery, ChatMemberUpdated, InlineQuery,
|
||||
Message, Update)
|
||||
from aiogram.types import CallbackQuery, ChatMemberUpdated, InlineQuery, Message, Update
|
||||
|
||||
from helper_bot.utils.rate_limiter import telegram_rate_limiter
|
||||
from logs.custom_logger import logger
|
||||
|
||||
@@ -22,7 +23,7 @@ class RateLimitMiddleware(BaseMiddleware):
|
||||
self,
|
||||
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
|
||||
event: Union[Update, Message, CallbackQuery, InlineQuery, ChatMemberUpdated],
|
||||
data: Dict[str, Any]
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
"""Обрабатывает событие с rate limiting"""
|
||||
|
||||
@@ -49,10 +50,8 @@ class RateLimitMiddleware(BaseMiddleware):
|
||||
|
||||
# Применяем rate limiting к handler
|
||||
return await self.rate_limiter.send_with_rate_limit(
|
||||
rate_limited_handler,
|
||||
chat_id
|
||||
rate_limited_handler, chat_id
|
||||
)
|
||||
else:
|
||||
# Для других типов событий просто вызываем handler
|
||||
return await handler(event, data)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ class BulkTextMiddleware(BaseMiddleware):
|
||||
self.latency = latency
|
||||
self.texts = defaultdict(list)
|
||||
|
||||
|
||||
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Main middleware logic.
|
||||
@@ -37,10 +36,9 @@ class BulkTextMiddleware(BaseMiddleware):
|
||||
# # Sort the album messages by message_id and add to data
|
||||
msg_texts = self.texts[key]
|
||||
msg_texts.sort(key=lambda x: x.message_id)
|
||||
data["texts"] = ''.join([msg.text for msg in msg_texts])
|
||||
data["texts"] = "".join([msg.text for msg in msg_texts])
|
||||
#
|
||||
# Remove the media group from tracking to free up memory
|
||||
del self.texts[key]
|
||||
# # Call the original event handler
|
||||
return await handler(event, data)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
"""
|
||||
HTTP server for metrics endpoint integration with centralized Prometheus monitoring.
|
||||
Provides /metrics endpoint and health check for the bot.
|
||||
@@ -17,13 +16,14 @@ try:
|
||||
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):
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8080):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = web.Application()
|
||||
@@ -31,8 +31,8 @@ class MetricsServer:
|
||||
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("/metrics", self.metrics_handler)
|
||||
self.app.router.add_get("/health", self.health_handler)
|
||||
|
||||
async def metrics_handler(self, request: web.Request) -> web.Response:
|
||||
"""Handle /metrics endpoint for Prometheus scraping."""
|
||||
@@ -42,27 +42,21 @@ class MetricsServer:
|
||||
# Проверяем, что metrics доступен
|
||||
if not metrics:
|
||||
logger.error("Metrics object is not available")
|
||||
return web.Response(
|
||||
text="Metrics not available",
|
||||
status=500
|
||||
)
|
||||
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'
|
||||
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
|
||||
)
|
||||
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."""
|
||||
@@ -71,8 +65,8 @@ class MetricsServer:
|
||||
if not metrics:
|
||||
return web.Response(
|
||||
text="ERROR: Metrics not available",
|
||||
content_type='text/plain',
|
||||
status=503
|
||||
content_type="text/plain",
|
||||
status=503,
|
||||
)
|
||||
|
||||
# Проверяем, что можем получить метрики
|
||||
@@ -81,30 +75,25 @@ class MetricsServer:
|
||||
if not metrics_data:
|
||||
return web.Response(
|
||||
text="ERROR: Empty metrics",
|
||||
content_type='text/plain',
|
||||
status=503
|
||||
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
|
||||
content_type="text/plain",
|
||||
status=503,
|
||||
)
|
||||
|
||||
return web.Response(
|
||||
text="OK",
|
||||
content_type='text/plain',
|
||||
status=200
|
||||
)
|
||||
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
|
||||
content_type="text/plain",
|
||||
status=500,
|
||||
)
|
||||
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the HTTP server."""
|
||||
try:
|
||||
@@ -151,7 +140,9 @@ class MetricsServer:
|
||||
metrics_server: Optional[MetricsServer] = None
|
||||
|
||||
|
||||
async def start_metrics_server(host: str = '0.0.0.0', port: int = 8080) -> MetricsServer:
|
||||
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)
|
||||
|
||||
@@ -9,9 +9,14 @@
|
||||
|
||||
from .base import CombinedScore, ScoringResult, ScoringServiceProtocol
|
||||
from .deepseek_service import DeepSeekService
|
||||
from .exceptions import (DeepSeekAPIError, InsufficientExamplesError,
|
||||
ModelNotLoadedError, ScoringError, TextTooShortError,
|
||||
VectorStoreError)
|
||||
from .exceptions import (
|
||||
DeepSeekAPIError,
|
||||
InsufficientExamplesError,
|
||||
ModelNotLoadedError,
|
||||
ScoringError,
|
||||
TextTooShortError,
|
||||
VectorStoreError,
|
||||
)
|
||||
from .rag_client import RagApiClient
|
||||
from .scoring_manager import ScoringManager
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class ScoringResult:
|
||||
timestamp: Время получения оценки
|
||||
metadata: Дополнительные данные
|
||||
"""
|
||||
|
||||
score: float
|
||||
source: str
|
||||
model: str
|
||||
@@ -30,7 +31,9 @@ class ScoringResult:
|
||||
def __post_init__(self):
|
||||
"""Валидация score в диапазоне [0.0, 1.0]."""
|
||||
if not 0.0 <= self.score <= 1.0:
|
||||
raise ValueError(f"Score должен быть в диапазоне [0.0, 1.0], получено: {self.score}")
|
||||
raise ValueError(
|
||||
f"Score должен быть в диапазоне [0.0, 1.0], получено: {self.score}"
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Преобразует результат в словарь для сохранения в JSON."""
|
||||
@@ -68,6 +71,7 @@ class CombinedScore:
|
||||
rag: Результат от RAG сервиса (None если отключен/ошибка)
|
||||
errors: Словарь с ошибками по источникам
|
||||
"""
|
||||
|
||||
deepseek: Optional[ScoringResult] = None
|
||||
rag: Optional[ScoringResult] = None
|
||||
errors: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from helper_bot.utils.metrics import track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
|
||||
@@ -180,7 +181,7 @@ class DeepSeekService:
|
||||
text = response_text.strip()
|
||||
|
||||
# Убираем возможные обрамления
|
||||
text = text.strip('"\'`')
|
||||
text = text.strip("\"'`")
|
||||
|
||||
# Пробуем распарсить как число
|
||||
score = float(text)
|
||||
@@ -193,13 +194,18 @@ class DeepSeekService:
|
||||
except ValueError:
|
||||
# Пробуем найти число в тексте
|
||||
import re
|
||||
matches = re.findall(r'0\.\d+|1\.0|0|1', text)
|
||||
|
||||
matches = re.findall(r"0\.\d+|1\.0|0|1", text)
|
||||
if matches:
|
||||
score = float(matches[0])
|
||||
return max(0.0, min(1.0, score))
|
||||
|
||||
logger.error(f"DeepSeekService: Не удалось распарсить ответ: {response_text}")
|
||||
raise DeepSeekAPIError(f"Не удалось распарсить скор из ответа: {response_text}")
|
||||
logger.error(
|
||||
f"DeepSeekService: Не удалось распарсить ответ: {response_text}"
|
||||
)
|
||||
raise DeepSeekAPIError(
|
||||
f"Не удалось распарсить скор из ответа: {response_text}"
|
||||
)
|
||||
|
||||
@track_time("calculate_score", "deepseek_service")
|
||||
@track_errors("deepseek_service", "calculate_score")
|
||||
@@ -254,9 +260,11 @@ class DeepSeekService:
|
||||
)
|
||||
if attempt < self.max_retries - 1:
|
||||
# Экспоненциальная задержка
|
||||
await asyncio.sleep(2 ** attempt)
|
||||
await asyncio.sleep(2**attempt)
|
||||
|
||||
raise ScoringError(f"Все попытки запроса к DeepSeek API не удались: {last_error}")
|
||||
raise ScoringError(
|
||||
f"Все попытки запроса к DeepSeek API не удались: {last_error}"
|
||||
)
|
||||
|
||||
async def _make_api_request(self, prompt: str) -> float:
|
||||
"""
|
||||
@@ -282,7 +290,7 @@ class DeepSeekService:
|
||||
}
|
||||
],
|
||||
"temperature": 0.1, # Низкая температура для детерминированности
|
||||
"max_tokens": 10, # Ожидаем только число
|
||||
"max_tokens": 10, # Ожидаем только число
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -5,29 +5,35 @@
|
||||
|
||||
class ScoringError(Exception):
|
||||
"""Базовое исключение для ошибок скоринга."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ModelNotLoadedError(ScoringError):
|
||||
"""Модель не загружена или недоступна."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VectorStoreError(ScoringError):
|
||||
"""Ошибка при работе с хранилищем векторов."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DeepSeekAPIError(ScoringError):
|
||||
"""Ошибка при обращении к DeepSeek API."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientExamplesError(ScoringError):
|
||||
"""Недостаточно примеров для расчета скора."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TextTooShortError(ScoringError):
|
||||
"""Текст слишком короткий для векторизации."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -7,12 +7,12 @@ HTTP клиент для взаимодействия с внешним RAG се
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from helper_bot.utils.metrics import track_errors, track_time
|
||||
from logs.custom_logger import logger
|
||||
|
||||
from .base import ScoringResult
|
||||
from .exceptions import (InsufficientExamplesError, ScoringError,
|
||||
TextTooShortError)
|
||||
from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError
|
||||
|
||||
|
||||
class RagApiClient:
|
||||
@@ -52,7 +52,7 @@ class RagApiClient:
|
||||
enabled: Включен ли клиент
|
||||
"""
|
||||
# Убираем trailing slash если есть
|
||||
self.api_url = api_url.rstrip('/')
|
||||
self.api_url = api_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.timeout = timeout
|
||||
self.test_mode = test_mode
|
||||
@@ -64,10 +64,12 @@ class RagApiClient:
|
||||
headers={
|
||||
"X-API-Key": api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(f"RagApiClient инициализирован (url={self.api_url}, enabled={enabled})")
|
||||
logger.info(
|
||||
f"RagApiClient инициализирован (url={self.api_url}, enabled={enabled})"
|
||||
)
|
||||
|
||||
@property
|
||||
def source_name(self) -> str:
|
||||
@@ -108,8 +110,7 @@ class RagApiClient:
|
||||
|
||||
try:
|
||||
response = await self._client.post(
|
||||
f"{self.api_url}/score",
|
||||
json={"text": text.strip()}
|
||||
f"{self.api_url}/score", json={"text": text.strip()}
|
||||
)
|
||||
|
||||
# Обрабатываем различные статусы
|
||||
@@ -122,7 +123,10 @@ class RagApiClient:
|
||||
|
||||
logger.warning(f"RagApiClient: Ошибка валидации запроса: {error_msg}")
|
||||
|
||||
if "недостаточно" in error_msg.lower() or "insufficient" in error_msg.lower():
|
||||
if (
|
||||
"недостаточно" in error_msg.lower()
|
||||
or "insufficient" in error_msg.lower()
|
||||
):
|
||||
raise InsufficientExamplesError(error_msg)
|
||||
if "коротк" in error_msg.lower() or "short" in error_msg.lower():
|
||||
raise TextTooShortError(error_msg)
|
||||
@@ -137,7 +141,9 @@ class RagApiClient:
|
||||
raise ScoringError("RAG API endpoint не найден")
|
||||
|
||||
if response.status_code >= 500:
|
||||
logger.error(f"RagApiClient: Ошибка сервера RAG API: {response.status_code}")
|
||||
logger.error(
|
||||
f"RagApiClient: Ошибка сервера RAG API: {response.status_code}"
|
||||
)
|
||||
raise ScoringError(f"Ошибка сервера RAG API: {response.status_code}")
|
||||
|
||||
# Проверяем успешный статус
|
||||
@@ -148,7 +154,11 @@ class RagApiClient:
|
||||
|
||||
# Парсим ответ
|
||||
score = float(data.get("rag_score", 0.0))
|
||||
confidence = float(data.get("rag_confidence", 0.0)) if data.get("rag_confidence") is not None else None
|
||||
confidence = (
|
||||
float(data.get("rag_confidence", 0.0))
|
||||
if data.get("rag_confidence") is not None
|
||||
else None
|
||||
)
|
||||
rag_score_pos_only_raw = data.get("rag_score_pos_only")
|
||||
rag_score_pos_only = float(rag_score_pos_only_raw) if rag_score_pos_only_raw is not None else None
|
||||
|
||||
@@ -171,10 +181,14 @@ class RagApiClient:
|
||||
model=data.get("meta", {}).get("model", "rag-service"),
|
||||
confidence=confidence,
|
||||
metadata={
|
||||
"rag_score_pos_only": rag_score_pos_only,
|
||||
"rag_score_pos_only": (
|
||||
float(data.get("rag_score_pos_only", 0.0))
|
||||
if data.get("rag_score_pos_only") is not None
|
||||
else None
|
||||
),
|
||||
"positive_examples": data.get("meta", {}).get("positive_examples"),
|
||||
"negative_examples": data.get("meta", {}).get("negative_examples"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
@@ -184,7 +198,9 @@ class RagApiClient:
|
||||
logger.error(f"RagApiClient: Ошибка подключения к RAG API: {e}")
|
||||
raise ScoringError(f"Ошибка подключения к RAG API: {e}")
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
logger.error(f"RagApiClient: Ошибка парсинга ответа: {e}, response: {response.text if 'response' in locals() else 'N/A'}")
|
||||
logger.error(
|
||||
f"RagApiClient: Ошибка парсинга ответа: {e}, response: {response.text if 'response' in locals() else 'N/A'}"
|
||||
)
|
||||
raise ScoringError(f"Ошибка парсинга ответа от RAG API: {e}")
|
||||
except InsufficientExamplesError:
|
||||
raise
|
||||
@@ -195,7 +211,10 @@ class RagApiClient:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Только действительно неожиданные ошибки логируем здесь
|
||||
logger.error(f"RagApiClient: Неожиданная ошибка при расчете скора: {e}", exc_info=True)
|
||||
logger.error(
|
||||
f"RagApiClient: Неожиданная ошибка при расчете скора: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise ScoringError(f"Неожиданная ошибка: {e}")
|
||||
|
||||
@track_time("add_positive_example", "rag_client")
|
||||
@@ -221,20 +240,28 @@ class RagApiClient:
|
||||
response = await self._client.post(
|
||||
f"{self.api_url}/examples/positive",
|
||||
json={"text": text.strip()},
|
||||
headers=headers
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if response.status_code == 200 or response.status_code == 201:
|
||||
logger.info("RagApiClient: Положительный пример успешно добавлен")
|
||||
elif response.status_code == 400:
|
||||
logger.warning(f"RagApiClient: Ошибка валидации при добавлении положительного примера: {response.text}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка валидации при добавлении положительного примера: {response.text}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"RagApiClient: Неожиданный статус при добавлении положительного примера: {response.status_code}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Неожиданный статус при добавлении положительного примера: {response.status_code}"
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"RagApiClient: Таймаут при добавлении положительного примера")
|
||||
logger.warning(
|
||||
f"RagApiClient: Таймаут при добавлении положительного примера"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.warning(f"RagApiClient: Ошибка подключения при добавлении положительного примера: {e}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка подключения при добавлении положительного примера: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"RagApiClient: Ошибка добавления положительного примера: {e}")
|
||||
|
||||
@@ -261,20 +288,28 @@ class RagApiClient:
|
||||
response = await self._client.post(
|
||||
f"{self.api_url}/examples/negative",
|
||||
json={"text": text.strip()},
|
||||
headers=headers
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
if response.status_code == 200 or response.status_code == 201:
|
||||
logger.info("RagApiClient: Отрицательный пример успешно добавлен")
|
||||
elif response.status_code == 400:
|
||||
logger.warning(f"RagApiClient: Ошибка валидации при добавлении отрицательного примера: {response.text}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка валидации при добавлении отрицательного примера: {response.text}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"RagApiClient: Неожиданный статус при добавлении отрицательного примера: {response.status_code}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Неожиданный статус при добавлении отрицательного примера: {response.status_code}"
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"RagApiClient: Таймаут при добавлении отрицательного примера")
|
||||
logger.warning(
|
||||
f"RagApiClient: Таймаут при добавлении отрицательного примера"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.warning(f"RagApiClient: Ошибка подключения при добавлении отрицательного примера: {e}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка подключения при добавлении отрицательного примера: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"RagApiClient: Ошибка добавления отрицательного примера: {e}")
|
||||
|
||||
@@ -294,14 +329,18 @@ class RagApiClient:
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.warning(f"RagApiClient: Неожиданный статус при получении статистики: {response.status_code}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Неожиданный статус при получении статистики: {response.status_code}"
|
||||
)
|
||||
return {}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.warning(f"RagApiClient: Таймаут при получении статистики")
|
||||
return {}
|
||||
except httpx.RequestError as e:
|
||||
logger.warning(f"RagApiClient: Ошибка подключения при получении статистики: {e}")
|
||||
logger.warning(
|
||||
f"RagApiClient: Ошибка подключения при получении статистики: {e}"
|
||||
)
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"RagApiClient: Ошибка получения статистики: {e}")
|
||||
|
||||
@@ -13,8 +13,7 @@ from logs.custom_logger import logger
|
||||
|
||||
from .base import CombinedScore, ScoringResult
|
||||
from .deepseek_service import DeepSeekService
|
||||
from .exceptions import (InsufficientExamplesError, ScoringError,
|
||||
TextTooShortError)
|
||||
from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError
|
||||
from .rag_client import RagApiClient
|
||||
|
||||
|
||||
@@ -55,7 +54,9 @@ class ScoringManager:
|
||||
def is_any_enabled(self) -> bool:
|
||||
"""Проверяет, включен ли хотя бы один сервис."""
|
||||
rag_enabled = self.rag_client is not None and self.rag_client.is_enabled
|
||||
deepseek_enabled = self.deepseek_service is not None and self.deepseek_service.is_enabled
|
||||
deepseek_enabled = (
|
||||
self.deepseek_service is not None and self.deepseek_service.is_enabled
|
||||
)
|
||||
return rag_enabled or deepseek_enabled
|
||||
|
||||
@track_time("score_post", "scoring_manager")
|
||||
@@ -197,7 +198,6 @@ class ScoringManager:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
logger.info("ScoringManager: Добавлен отрицательный пример")
|
||||
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Закрывает ресурсы всех сервисов."""
|
||||
if self.deepseek_service:
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Optional
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from logs.custom_logger import logger
|
||||
|
||||
@@ -41,16 +42,22 @@ class AutoUnbanScheduler:
|
||||
# Получаем текущий UNIX timestamp
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
|
||||
logger.info(f"Поиск пользователей для разблокировки на timestamp: {current_timestamp}")
|
||||
logger.info(
|
||||
f"Поиск пользователей для разблокировки на timestamp: {current_timestamp}"
|
||||
)
|
||||
|
||||
# Получаем список пользователей для разблокировки
|
||||
users_to_unban = await self.bot_db.get_users_for_unblock_today(current_timestamp)
|
||||
users_to_unban = await self.bot_db.get_users_for_unblock_today(
|
||||
current_timestamp
|
||||
)
|
||||
|
||||
if not users_to_unban:
|
||||
logger.info("Нет пользователей для разблокировки сегодня")
|
||||
return
|
||||
|
||||
logger.info(f"Найдено {len(users_to_unban)} пользователей для разблокировки")
|
||||
logger.info(
|
||||
f"Найдено {len(users_to_unban)} пользователей для разблокировки"
|
||||
)
|
||||
|
||||
# Список для отслеживания результатов
|
||||
success_count = 0
|
||||
@@ -71,23 +78,30 @@ class AutoUnbanScheduler:
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
failed_users.append(f"{user_id}")
|
||||
logger.error(f"Исключение при разблокировке пользователя {user_id}: {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
|
||||
)
|
||||
|
||||
# Отправляем отчет в лог-канал
|
||||
await self._send_report(report)
|
||||
|
||||
logger.info(f"Автоматический разбан завершен. Успешно: {success_count}, Ошибок: {failed_count}")
|
||||
logger.info(
|
||||
f"Автоматический разбан завершен. Успешно: {success_count}, Ошибок: {failed_count}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Критическая ошибка в автоматическом разбане: {e}"
|
||||
logger.error(error_msg)
|
||||
await self._send_error_report(error_msg)
|
||||
|
||||
def _generate_report(self, success_count: int, failed_count: int,
|
||||
failed_users: list, all_users: dict) -> str:
|
||||
def _generate_report(
|
||||
self, success_count: int, failed_count: int, failed_users: list, all_users: dict
|
||||
) -> str:
|
||||
"""Генерирует отчет о результатах автоматического разбана"""
|
||||
report = f"🤖 <b>Отчет об автоматическом разбане</b>\n\n"
|
||||
report += f"📅 Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n"
|
||||
@@ -114,11 +128,9 @@ class AutoUnbanScheduler:
|
||||
"""Отправляет отчет в лог-канал"""
|
||||
try:
|
||||
if self.bot:
|
||||
group_for_logs = self.bdf.settings['Telegram']['group_for_logs']
|
||||
group_for_logs = self.bdf.settings["Telegram"]["group_for_logs"]
|
||||
await self.bot.send_message(
|
||||
chat_id=group_for_logs,
|
||||
text=report,
|
||||
parse_mode='HTML'
|
||||
chat_id=group_for_logs, text=report, parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке отчета: {e}")
|
||||
@@ -129,11 +141,11 @@ class AutoUnbanScheduler:
|
||||
"""Отправляет отчет об ошибке в важный лог-канал"""
|
||||
try:
|
||||
if self.bot:
|
||||
important_logs = self.bdf.settings['Telegram']['important_logs']
|
||||
important_logs = self.bdf.settings["Telegram"]["important_logs"]
|
||||
await self.bot.send_message(
|
||||
chat_id=important_logs,
|
||||
text=f"🚨 <b>Ошибка автоматического разбана</b>\n\n{error_msg}",
|
||||
parse_mode='HTML'
|
||||
parse_mode="HTML",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке отчета об ошибке: {e}")
|
||||
@@ -144,15 +156,17 @@ class AutoUnbanScheduler:
|
||||
# Добавляем задачу на ежедневное выполнение в 5:00 по Москве
|
||||
self.scheduler.add_job(
|
||||
self.auto_unban_users,
|
||||
CronTrigger(hour=5, minute=0, timezone='Europe/Moscow'),
|
||||
id='auto_unban_users',
|
||||
name='Автоматический разбан пользователей',
|
||||
replace_existing=True
|
||||
CronTrigger(hour=5, minute=0, timezone="Europe/Moscow"),
|
||||
id="auto_unban_users",
|
||||
name="Автоматический разбан пользователей",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
# Запускаем планировщик
|
||||
self.scheduler.start()
|
||||
logger.info("Планировщик автоматического разбана запущен. Задача запланирована на 5:00 по Москве")
|
||||
logger.info(
|
||||
"Планировщик автоматического разбана запущен. Задача запланирована на 5:00 по Москве"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при запуске планировщика: {e}")
|
||||
|
||||
@@ -2,23 +2,26 @@ import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from database.async_db import AsyncBotDB
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.utils.s3_storage import S3StorageService
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class BaseDependencyFactory:
|
||||
def __init__(self):
|
||||
project_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
env_path = os.path.join(project_dir, '.env')
|
||||
project_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
env_path = os.path.join(project_dir, ".env")
|
||||
if os.path.exists(env_path):
|
||||
load_dotenv(env_path)
|
||||
|
||||
self.settings = {}
|
||||
self._project_dir = project_dir
|
||||
|
||||
database_path = os.getenv('DATABASE_PATH', 'database/tg-bot-database.db')
|
||||
database_path = os.getenv("DATABASE_PATH", "database/tg-bot-database.db")
|
||||
if not os.path.isabs(database_path):
|
||||
database_path = os.path.join(project_dir, database_path)
|
||||
|
||||
@@ -32,72 +35,81 @@ class BaseDependencyFactory:
|
||||
|
||||
def _load_settings_from_env(self):
|
||||
"""Загружает настройки из переменных окружения."""
|
||||
self.settings['Telegram'] = {
|
||||
'bot_token': os.getenv('BOT_TOKEN', ''),
|
||||
'listen_bot_token': os.getenv('LISTEN_BOT_TOKEN', ''),
|
||||
'test_bot_token': os.getenv('TEST_BOT_TOKEN', ''),
|
||||
'preview_link': self._parse_bool(os.getenv('PREVIEW_LINK', 'false')),
|
||||
'main_public': os.getenv('MAIN_PUBLIC', ''),
|
||||
'group_for_posts': self._parse_int(os.getenv('GROUP_FOR_POSTS', '0')),
|
||||
'group_for_message': self._parse_int(os.getenv('GROUP_FOR_MESSAGE', '0')),
|
||||
'group_for_logs': self._parse_int(os.getenv('GROUP_FOR_LOGS', '0')),
|
||||
'important_logs': self._parse_int(os.getenv('IMPORTANT_LOGS', '0')),
|
||||
'archive': self._parse_int(os.getenv('ARCHIVE', '0')),
|
||||
'test_group': self._parse_int(os.getenv('TEST_GROUP', '0'))
|
||||
self.settings["Telegram"] = {
|
||||
"bot_token": os.getenv("BOT_TOKEN", ""),
|
||||
"listen_bot_token": os.getenv("LISTEN_BOT_TOKEN", ""),
|
||||
"test_bot_token": os.getenv("TEST_BOT_TOKEN", ""),
|
||||
"preview_link": self._parse_bool(os.getenv("PREVIEW_LINK", "false")),
|
||||
"main_public": os.getenv("MAIN_PUBLIC", ""),
|
||||
"group_for_posts": self._parse_int(os.getenv("GROUP_FOR_POSTS", "0")),
|
||||
"group_for_message": self._parse_int(os.getenv("GROUP_FOR_MESSAGE", "0")),
|
||||
"group_for_logs": self._parse_int(os.getenv("GROUP_FOR_LOGS", "0")),
|
||||
"important_logs": self._parse_int(os.getenv("IMPORTANT_LOGS", "0")),
|
||||
"archive": self._parse_int(os.getenv("ARCHIVE", "0")),
|
||||
"test_group": self._parse_int(os.getenv("TEST_GROUP", "0")),
|
||||
}
|
||||
|
||||
self.settings['Settings'] = {
|
||||
'logs': self._parse_bool(os.getenv('LOGS', 'false')),
|
||||
'test': self._parse_bool(os.getenv('TEST', 'false'))
|
||||
self.settings["Settings"] = {
|
||||
"logs": self._parse_bool(os.getenv("LOGS", "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'))
|
||||
self.settings["Metrics"] = {
|
||||
"host": os.getenv("METRICS_HOST", "0.0.0.0"),
|
||||
"port": self._parse_int(os.getenv("METRICS_PORT", "8080")),
|
||||
}
|
||||
|
||||
self.settings['S3'] = {
|
||||
'enabled': self._parse_bool(os.getenv('S3_ENABLED', 'false')),
|
||||
'endpoint_url': os.getenv('S3_ENDPOINT_URL', ''),
|
||||
'access_key': os.getenv('S3_ACCESS_KEY', ''),
|
||||
'secret_key': os.getenv('S3_SECRET_KEY', ''),
|
||||
'bucket_name': os.getenv('S3_BUCKET_NAME', ''),
|
||||
'region': os.getenv('S3_REGION', 'us-east-1')
|
||||
self.settings["S3"] = {
|
||||
"enabled": self._parse_bool(os.getenv("S3_ENABLED", "false")),
|
||||
"endpoint_url": os.getenv("S3_ENDPOINT_URL", ""),
|
||||
"access_key": os.getenv("S3_ACCESS_KEY", ""),
|
||||
"secret_key": os.getenv("S3_SECRET_KEY", ""),
|
||||
"bucket_name": os.getenv("S3_BUCKET_NAME", ""),
|
||||
"region": os.getenv("S3_REGION", "us-east-1"),
|
||||
}
|
||||
|
||||
# Настройки ML-скоринга
|
||||
self.settings['Scoring'] = {
|
||||
self.settings["Scoring"] = {
|
||||
# RAG API
|
||||
'rag_enabled': self._parse_bool(os.getenv('RAG_ENABLED', 'false')),
|
||||
'rag_api_url': os.getenv('RAG_API_URL', ''),
|
||||
'rag_api_key': os.getenv('RAG_API_KEY', ''),
|
||||
'rag_api_timeout': self._parse_int(os.getenv('RAG_API_TIMEOUT', '30')),
|
||||
'rag_test_mode': self._parse_bool(os.getenv('RAG_TEST_MODE', 'false')),
|
||||
"rag_enabled": self._parse_bool(os.getenv("RAG_ENABLED", "false")),
|
||||
"rag_api_url": os.getenv("RAG_API_URL", ""),
|
||||
"rag_api_key": os.getenv("RAG_API_KEY", ""),
|
||||
"rag_api_timeout": self._parse_int(os.getenv("RAG_API_TIMEOUT", "30")),
|
||||
"rag_test_mode": self._parse_bool(os.getenv("RAG_TEST_MODE", "false")),
|
||||
# DeepSeek
|
||||
'deepseek_enabled': self._parse_bool(os.getenv('DEEPSEEK_ENABLED', 'false')),
|
||||
'deepseek_api_key': os.getenv('DEEPSEEK_API_KEY', ''),
|
||||
'deepseek_api_url': os.getenv('DEEPSEEK_API_URL', 'https://api.deepseek.com/v1/chat/completions'),
|
||||
'deepseek_model': os.getenv('DEEPSEEK_MODEL', 'deepseek-chat'),
|
||||
'deepseek_timeout': self._parse_int(os.getenv('DEEPSEEK_TIMEOUT', '30')),
|
||||
"deepseek_enabled": self._parse_bool(
|
||||
os.getenv("DEEPSEEK_ENABLED", "false")
|
||||
),
|
||||
"deepseek_api_key": os.getenv("DEEPSEEK_API_KEY", ""),
|
||||
"deepseek_api_url": os.getenv(
|
||||
"DEEPSEEK_API_URL", "https://api.deepseek.com/v1/chat/completions"
|
||||
),
|
||||
"deepseek_model": os.getenv("DEEPSEEK_MODEL", "deepseek-chat"),
|
||||
"deepseek_timeout": self._parse_int(os.getenv("DEEPSEEK_TIMEOUT", "30")),
|
||||
}
|
||||
|
||||
def _init_s3_storage(self):
|
||||
"""Инициализирует S3StorageService если S3 включен."""
|
||||
self.s3_storage = None
|
||||
if self.settings['S3']['enabled']:
|
||||
s3_config = self.settings['S3']
|
||||
if s3_config['endpoint_url'] and s3_config['access_key'] and s3_config['secret_key'] and s3_config['bucket_name']:
|
||||
if self.settings["S3"]["enabled"]:
|
||||
s3_config = self.settings["S3"]
|
||||
if (
|
||||
s3_config["endpoint_url"]
|
||||
and s3_config["access_key"]
|
||||
and s3_config["secret_key"]
|
||||
and s3_config["bucket_name"]
|
||||
):
|
||||
self.s3_storage = S3StorageService(
|
||||
endpoint_url=s3_config['endpoint_url'],
|
||||
access_key=s3_config['access_key'],
|
||||
secret_key=s3_config['secret_key'],
|
||||
bucket_name=s3_config['bucket_name'],
|
||||
region=s3_config['region']
|
||||
endpoint_url=s3_config["endpoint_url"],
|
||||
access_key=s3_config["access_key"],
|
||||
secret_key=s3_config["secret_key"],
|
||||
bucket_name=s3_config["bucket_name"],
|
||||
region=s3_config["region"],
|
||||
)
|
||||
|
||||
def _parse_bool(self, value: str) -> bool:
|
||||
"""Парсит строковое значение в boolean."""
|
||||
return value.lower() in ('true', '1', 'yes', 'on')
|
||||
return value.lower() in ("true", "1", "yes", "on")
|
||||
|
||||
def _parse_int(self, value: str) -> int:
|
||||
"""Парсит строковое значение в integer."""
|
||||
@@ -130,16 +142,19 @@ class BaseDependencyFactory:
|
||||
|
||||
Вызывается лениво при первом обращении к get_scoring_manager().
|
||||
"""
|
||||
from helper_bot.services.scoring import (DeepSeekService, RagApiClient,
|
||||
ScoringManager)
|
||||
from helper_bot.services.scoring import (
|
||||
DeepSeekService,
|
||||
RagApiClient,
|
||||
ScoringManager,
|
||||
)
|
||||
|
||||
scoring_config = self.settings['Scoring']
|
||||
scoring_config = self.settings["Scoring"]
|
||||
|
||||
# Инициализация RAG API клиента
|
||||
rag_client = None
|
||||
if scoring_config['rag_enabled']:
|
||||
api_url = scoring_config['rag_api_url']
|
||||
api_key = scoring_config['rag_api_key']
|
||||
if scoring_config["rag_enabled"]:
|
||||
api_url = scoring_config["rag_api_url"]
|
||||
api_key = scoring_config["rag_api_key"]
|
||||
|
||||
if not api_url or not api_key:
|
||||
logger.warning("RAG включен, но не указаны RAG_API_URL или RAG_API_KEY")
|
||||
@@ -147,23 +162,27 @@ class BaseDependencyFactory:
|
||||
rag_client = RagApiClient(
|
||||
api_url=api_url,
|
||||
api_key=api_key,
|
||||
timeout=scoring_config['rag_api_timeout'],
|
||||
test_mode=scoring_config['rag_test_mode'],
|
||||
timeout=scoring_config["rag_api_timeout"],
|
||||
test_mode=scoring_config["rag_test_mode"],
|
||||
enabled=True,
|
||||
)
|
||||
logger.info(f"RagApiClient инициализирован: {api_url} (test_mode={scoring_config['rag_test_mode']})")
|
||||
logger.info(
|
||||
f"RagApiClient инициализирован: {api_url} (test_mode={scoring_config['rag_test_mode']})"
|
||||
)
|
||||
|
||||
# Инициализация DeepSeek сервиса
|
||||
deepseek_service = None
|
||||
if scoring_config['deepseek_enabled'] and scoring_config['deepseek_api_key']:
|
||||
if scoring_config["deepseek_enabled"] and scoring_config["deepseek_api_key"]:
|
||||
deepseek_service = DeepSeekService(
|
||||
api_key=scoring_config['deepseek_api_key'],
|
||||
api_url=scoring_config['deepseek_api_url'],
|
||||
model=scoring_config['deepseek_model'],
|
||||
timeout=scoring_config['deepseek_timeout'],
|
||||
api_key=scoring_config["deepseek_api_key"],
|
||||
api_url=scoring_config["deepseek_api_url"],
|
||||
model=scoring_config["deepseek_model"],
|
||||
timeout=scoring_config["deepseek_timeout"],
|
||||
enabled=True,
|
||||
)
|
||||
logger.info(f"DeepSeekService инициализирован: {scoring_config['deepseek_model']}")
|
||||
logger.info(
|
||||
f"DeepSeekService инициализирован: {scoring_config['deepseek_model']}"
|
||||
)
|
||||
|
||||
# Создаем менеджер
|
||||
self._scoring_manager = ScoringManager(
|
||||
@@ -183,11 +202,11 @@ class BaseDependencyFactory:
|
||||
ScoringManager или None если скоринг полностью отключен
|
||||
"""
|
||||
if self._scoring_manager is None:
|
||||
scoring_config = self.settings.get('Scoring', {})
|
||||
scoring_config = self.settings.get("Scoring", {})
|
||||
|
||||
# Проверяем, включен ли хотя бы один сервис
|
||||
rag_enabled = scoring_config.get('rag_enabled', False)
|
||||
deepseek_enabled = scoring_config.get('deepseek_enabled', False)
|
||||
rag_enabled = scoring_config.get("rag_enabled", False)
|
||||
deepseek_enabled = scoring_config.get("deepseek_enabled", False)
|
||||
|
||||
if not rag_enabled and not deepseek_enabled:
|
||||
logger.info("Scoring полностью отключен (RAG и DeepSeek disabled)")
|
||||
@@ -200,6 +219,7 @@ class BaseDependencyFactory:
|
||||
|
||||
_global_instance = None
|
||||
|
||||
|
||||
def get_global_instance():
|
||||
"""Возвращает глобальный экземпляр BaseDependencyFactory."""
|
||||
global _global_instance
|
||||
|
||||
@@ -10,35 +10,62 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||
|
||||
try:
|
||||
import emoji as _emoji_lib
|
||||
|
||||
_emoji_lib_available = True
|
||||
except ImportError:
|
||||
_emoji_lib = None
|
||||
_emoji_lib_available = False
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.types import (FSInputFile, InputMediaAudio, InputMediaDocument,
|
||||
InputMediaPhoto, InputMediaVideo)
|
||||
from aiogram.types import (
|
||||
FSInputFile,
|
||||
InputMediaAudio,
|
||||
InputMediaDocument,
|
||||
InputMediaPhoto,
|
||||
InputMediaVideo,
|
||||
)
|
||||
|
||||
from database.models import TelegramPost
|
||||
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
|
||||
|
||||
# Local imports - metrics
|
||||
from .metrics import (db_query_time, track_errors, track_file_operations,
|
||||
track_media_processing, track_time)
|
||||
from .metrics import (
|
||||
db_query_time,
|
||||
track_errors,
|
||||
track_file_operations,
|
||||
track_media_processing,
|
||||
track_time,
|
||||
)
|
||||
|
||||
bdf = get_global_instance()
|
||||
#TODO: поменять архитектуру и подключить правильный BotDB
|
||||
# TODO: поменять архитектуру и подключить правильный BotDB
|
||||
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_available and _emoji_lib is not None:
|
||||
emoji_list = list(_emoji_lib.EMOJI_DATA.keys())
|
||||
else:
|
||||
# Fallback minimal emoji set for environments without the 'emoji' package (e.g., CI tests)
|
||||
emoji_list = [
|
||||
"🙂", "😀", "😉", "😎", "🤖", "🦄", "🐱", "🐶", "🍀", "🔥",
|
||||
"🌟", "🎉", "💡", "🚀", "🌈"
|
||||
"🙂",
|
||||
"😀",
|
||||
"😉",
|
||||
"😎",
|
||||
"🤖",
|
||||
"🦄",
|
||||
"🐱",
|
||||
"🐶",
|
||||
"🍀",
|
||||
"🔥",
|
||||
"🌟",
|
||||
"🎉",
|
||||
"💡",
|
||||
"🚀",
|
||||
"🌈",
|
||||
]
|
||||
|
||||
|
||||
@@ -76,8 +103,8 @@ def get_first_name(message: types.Message) -> str:
|
||||
# Дополнительная проверка на специальные символы, которые могут вызвать проблемы в HTML
|
||||
first_name = str(message.from_user.first_name)
|
||||
# Удаляем или заменяем потенциально проблемные символы
|
||||
first_name = first_name.replace('\u0cc0', '') # Убираем символ "ೀ" (U+0CC0)
|
||||
first_name = first_name.replace('\u0cc1', '') # Убираем символ "ೀ" (U+0CC1)
|
||||
first_name = first_name.replace("\u0cc0", "") # Убираем символ "ೀ" (U+0CC0)
|
||||
first_name = first_name.replace("\u0cc1", "") # Убираем символ "ೀ" (U+0CC1)
|
||||
first_name = html.escape(first_name)
|
||||
return first_name
|
||||
return ""
|
||||
@@ -154,20 +181,24 @@ def get_text_message(
|
||||
# Если передан is_anonymous, используем его, иначе определяем по тексту (legacy)
|
||||
if is_anonymous is not None:
|
||||
if is_anonymous:
|
||||
final_text = f'{safe_post_text}\n\nПост опубликован анонимно'
|
||||
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
|
||||
else:
|
||||
final_text = f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
|
||||
else:
|
||||
# Legacy: определяем по тексту
|
||||
if "неанон" in post_text or "не анон" in post_text:
|
||||
final_text = f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
|
||||
elif "анон" in post_text:
|
||||
final_text = f'{safe_post_text}\n\nПост опубликован анонимно'
|
||||
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
|
||||
else:
|
||||
final_text = f'{safe_post_text}\n\nАвтор поста: {author_info}'
|
||||
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
|
||||
|
||||
# Добавляем блок со скорами если есть
|
||||
if deepseek_score is not None or rag_score is not None or rag_score_pos_only is not None:
|
||||
if (
|
||||
deepseek_score is not None
|
||||
or rag_score is not None
|
||||
or rag_score_pos_only is not None
|
||||
):
|
||||
scores_lines = ["\n📊 Уверенность в одобрении:"]
|
||||
if deepseek_score is not None:
|
||||
scores_lines.append(f"DeepSeek: {deepseek_score:.2f}")
|
||||
@@ -189,11 +220,13 @@ def get_text_message(
|
||||
|
||||
return final_text
|
||||
|
||||
|
||||
@track_time("download_file", "helper_func")
|
||||
@track_errors("helper_func", "download_file")
|
||||
@track_file_operations("unknown")
|
||||
async def download_file(message: types.Message, file_id: str, content_type: str = None,
|
||||
s3_storage = None) -> Optional[str]:
|
||||
async def download_file(
|
||||
message: types.Message, file_id: str, content_type: str = None, s3_storage=None
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Скачивает файл по file_id из Telegram и сохраняет в S3 или на локальный диск.
|
||||
|
||||
@@ -211,18 +244,22 @@ async def download_file(message: types.Message, file_id: str, content_type: str
|
||||
try:
|
||||
# Валидация параметров
|
||||
if not file_id or not message or not message.bot:
|
||||
logger.error("download_file: Неверные параметры - file_id, message или bot отсутствуют")
|
||||
logger.error(
|
||||
"download_file: Неверные параметры - file_id, message или bot отсутствуют"
|
||||
)
|
||||
return None
|
||||
|
||||
# Получаем информацию о файле
|
||||
file = await message.bot.get_file(file_id)
|
||||
if not file or not file.file_path:
|
||||
logger.error(f"download_file: Не удалось получить информацию о файле {file_id}")
|
||||
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'
|
||||
file_extension = os.path.splitext(original_filename)[1] or ".bin"
|
||||
|
||||
if s3_storage:
|
||||
# Сохраняем в S3
|
||||
@@ -233,7 +270,9 @@ async def download_file(message: types.Message, file_id: str, content_type: str
|
||||
|
||||
try:
|
||||
# Скачиваем из Telegram
|
||||
await message.bot.download_file(file_path=file.file_path, destination=temp_path)
|
||||
await message.bot.download_file(
|
||||
file_path=file.file_path, destination=temp_path
|
||||
)
|
||||
|
||||
# Генерируем S3 ключ
|
||||
s3_key = s3_storage.generate_s3_key(content_type, file_id)
|
||||
@@ -248,12 +287,16 @@ async def download_file(message: types.Message, file_id: str, content_type: str
|
||||
pass
|
||||
|
||||
if success:
|
||||
file_size = file.file_size if hasattr(file, 'file_size') else 0
|
||||
file_size = file.file_size if hasattr(file, "file_size") else 0
|
||||
download_time = time.time() - start_time
|
||||
logger.info(f"download_file: Файл загружен в S3 - {s3_key}, размер: {file_size} байт, время: {download_time:.2f}с")
|
||||
logger.info(
|
||||
f"download_file: Файл загружен в S3 - {s3_key}, размер: {file_size} байт, время: {download_time:.2f}с"
|
||||
)
|
||||
return s3_key
|
||||
else:
|
||||
logger.error(f"download_file: Не удалось загрузить файл в S3: {s3_key}")
|
||||
logger.error(
|
||||
f"download_file: Не удалось загрузить файл в S3: {s3_key}"
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
# Удаляем временный файл при ошибке
|
||||
@@ -262,20 +305,22 @@ async def download_file(message: types.Message, file_id: str, content_type: str
|
||||
except:
|
||||
pass
|
||||
download_time = time.time() - start_time
|
||||
logger.error(f"download_file: Ошибка загрузки файла в S3 {file_id}: {e}, время: {download_time:.2f}с")
|
||||
logger.error(
|
||||
f"download_file: Ошибка загрузки файла в S3 {file_id}: {e}, время: {download_time:.2f}с"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
# Старая логика - сохраняем на локальный диск
|
||||
# Определяем папку по типу контента
|
||||
type_folders = {
|
||||
'photo': 'photos',
|
||||
'video': 'videos',
|
||||
'audio': 'music',
|
||||
'voice': 'voice',
|
||||
'video_note': 'video_notes'
|
||||
"photo": "photos",
|
||||
"video": "videos",
|
||||
"audio": "music",
|
||||
"voice": "voice",
|
||||
"video_note": "video_notes",
|
||||
}
|
||||
|
||||
folder = type_folders.get(content_type, 'other')
|
||||
folder = type_folders.get(content_type, "other")
|
||||
base_path = "files"
|
||||
full_folder_path = os.path.join(base_path, folder)
|
||||
|
||||
@@ -283,14 +328,18 @@ async def download_file(message: types.Message, file_id: str, content_type: str
|
||||
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}")
|
||||
logger.debug(
|
||||
f"download_file: Начинаю скачивание файла {file_id} типа {content_type} в папку {folder}"
|
||||
)
|
||||
|
||||
# Генерируем уникальное имя файла
|
||||
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):
|
||||
@@ -300,19 +349,24 @@ async def download_file(message: types.Message, file_id: str, content_type: str
|
||||
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}с")
|
||||
logger.info(
|
||||
f"download_file: Файл успешно скачан - {file_path}, размер: {file_size} байт, время: {download_time:.2f}с"
|
||||
)
|
||||
|
||||
return file_path
|
||||
|
||||
except Exception as e:
|
||||
download_time = time.time() - start_time
|
||||
logger.error(f"download_file: Ошибка скачивания файла {file_id}: {e}, время: {download_time:.2f}с")
|
||||
logger.error(
|
||||
f"download_file: Ошибка скачивания файла {file_id}: {e}, время: {download_time:.2f}с"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@track_time("prepare_media_group_from_middlewares", "helper_func")
|
||||
@track_errors("helper_func", "prepare_media_group_from_middlewares")
|
||||
@track_media_processing("media_group")
|
||||
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
async def prepare_media_group_from_middlewares(album, post_caption: str = ""):
|
||||
"""
|
||||
Создает MediaGroup согласно best practices aiogram 3.x.
|
||||
|
||||
@@ -333,28 +387,36 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
file_id = message.photo[-1].file_id
|
||||
# Для фото используем InputMediaPhoto
|
||||
if i == 0: # Первое фото получает подпись
|
||||
media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption))
|
||||
media_group.append(
|
||||
InputMediaPhoto(media=file_id, caption=safe_post_caption)
|
||||
)
|
||||
else:
|
||||
media_group.append(InputMediaPhoto(media=file_id))
|
||||
elif message.video:
|
||||
file_id = message.video.file_id
|
||||
# Для видео используем InputMediaVideo
|
||||
if i == 0: # Первое видео получает подпись
|
||||
media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption))
|
||||
media_group.append(
|
||||
InputMediaVideo(media=file_id, caption=safe_post_caption)
|
||||
)
|
||||
else:
|
||||
media_group.append(InputMediaVideo(media=file_id))
|
||||
elif message.audio:
|
||||
file_id = message.audio.file_id
|
||||
# Для аудио используем InputMediaAudio
|
||||
if i == 0: # Первое аудио получает подпись
|
||||
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
||||
media_group.append(
|
||||
InputMediaAudio(media=file_id, caption=safe_post_caption)
|
||||
)
|
||||
else:
|
||||
media_group.append(InputMediaAudio(media=file_id))
|
||||
elif message.document:
|
||||
file_id = message.document.file_id
|
||||
# Для документов используем InputMediaDocument (если поддерживается)
|
||||
if i == 0: # Первый документ получает подпись
|
||||
media_group.append(InputMediaDocument(media=file_id, caption=safe_post_caption))
|
||||
media_group.append(
|
||||
InputMediaDocument(media=file_id, caption=safe_post_caption)
|
||||
)
|
||||
else:
|
||||
media_group.append(InputMediaDocument(media=file_id))
|
||||
else:
|
||||
@@ -363,21 +425,38 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
|
||||
return media_group
|
||||
|
||||
async def _save_media_group_background(sent_message: List[types.Message], bot_db: Any, main_post_id: Optional[int], s3_storage) -> None:
|
||||
|
||||
async def _save_media_group_background(
|
||||
sent_message: List[types.Message],
|
||||
bot_db: Any,
|
||||
main_post_id: Optional[int],
|
||||
s3_storage,
|
||||
) -> None:
|
||||
"""Сохраняет медиагруппу в фоне, чтобы не блокировать ответ пользователю"""
|
||||
try:
|
||||
success = await add_in_db_media_mediagroup(sent_message, bot_db, main_post_id, s3_storage)
|
||||
success = await add_in_db_media_mediagroup(
|
||||
sent_message, bot_db, main_post_id, s3_storage
|
||||
)
|
||||
if not success:
|
||||
logger.warning(f"_save_media_group_background: Не удалось сохранить медиа для медиагруппы {sent_message[-1].message_id}")
|
||||
logger.warning(
|
||||
f"_save_media_group_background: Не удалось сохранить медиа для медиагруппы {sent_message[-1].message_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"_save_media_group_background: Ошибка при сохранении медиа для медиагруппы {sent_message[-1].message_id}: {e}")
|
||||
logger.error(
|
||||
f"_save_media_group_background: Ошибка при сохранении медиа для медиагруппы {sent_message[-1].message_id}: {e}"
|
||||
)
|
||||
|
||||
|
||||
@track_time("add_in_db_media_mediagroup", "helper_func")
|
||||
@track_errors("helper_func", "add_in_db_media_mediagroup")
|
||||
@track_media_processing("media_group")
|
||||
@db_query_time("add_in_db_media_mediagroup", "posts", "insert")
|
||||
async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db: Any,
|
||||
main_post_id: Optional[int] = None, s3_storage = None) -> bool:
|
||||
async def add_in_db_media_mediagroup(
|
||||
sent_message: List[types.Message],
|
||||
bot_db: Any,
|
||||
main_post_id: Optional[int] = None,
|
||||
s3_storage=None,
|
||||
) -> bool:
|
||||
"""
|
||||
Добавляет контент медиа-группы в базу данных
|
||||
|
||||
@@ -394,7 +473,9 @@ async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db:
|
||||
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 не является списком")
|
||||
logger.error(
|
||||
"add_in_db_media_mediagroup: Неверные параметры - sent_message, bot_db или sent_message не является списком"
|
||||
)
|
||||
return False
|
||||
|
||||
if len(sent_message) == 0:
|
||||
@@ -412,27 +493,31 @@ async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db:
|
||||
file_id = None
|
||||
|
||||
if message.photo:
|
||||
content_type = 'photo'
|
||||
content_type = "photo"
|
||||
file_id = message.photo[-1].file_id
|
||||
elif message.video:
|
||||
content_type = 'video'
|
||||
content_type = "video"
|
||||
file_id = message.video.file_id
|
||||
elif message.audio:
|
||||
content_type = 'audio'
|
||||
content_type = "audio"
|
||||
file_id = message.audio.file_id
|
||||
elif message.voice:
|
||||
content_type = 'voice'
|
||||
content_type = "voice"
|
||||
file_id = message.voice.file_id
|
||||
elif message.video_note:
|
||||
content_type = '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)}")
|
||||
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)}")
|
||||
logger.error(
|
||||
f"add_in_db_media_mediagroup: file_id отсутствует в сообщении {i+1}/{len(sent_message)}"
|
||||
)
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
@@ -440,50 +525,74 @@ async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db:
|
||||
bdf = get_global_instance()
|
||||
s3_storage = bdf.get_s3_storage()
|
||||
|
||||
file_path = await download_file(message, file_id=file_id, content_type=content_type, s3_storage=s3_storage)
|
||||
file_path = await download_file(
|
||||
message,
|
||||
file_id=file_id,
|
||||
content_type=content_type,
|
||||
s3_storage=s3_storage,
|
||||
)
|
||||
if not file_path:
|
||||
logger.error(f"add_in_db_media_mediagroup: Не удалось скачать файл {file_id} в сообщении {i+1}/{len(sent_message)}")
|
||||
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, post_id, file_path, content_type)
|
||||
success = await bot_db.add_post_content(
|
||||
post_id, post_id, file_path, content_type
|
||||
)
|
||||
if not success:
|
||||
logger.error(f"add_in_db_media_mediagroup: Не удалось добавить контент в БД для сообщения {i+1}/{len(sent_message)}")
|
||||
if file_path.startswith('files/'):
|
||||
logger.error(
|
||||
f"add_in_db_media_mediagroup: Не удалось добавить контент в БД для сообщения {i+1}/{len(sent_message)}"
|
||||
)
|
||||
if file_path.startswith("files/"):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
logger.warning(f"add_in_db_media_mediagroup: Не удалось удалить файл {file_path}: {e}")
|
||||
logger.warning(
|
||||
f"add_in_db_media_mediagroup: Не удалось удалить файл {file_path}: {e}"
|
||||
)
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
processed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"add_in_db_media_mediagroup: Ошибка обработки сообщения {i+1}/{len(sent_message)}: {e}")
|
||||
logger.error(
|
||||
f"add_in_db_media_mediagroup: Ошибка обработки сообщения {i+1}/{len(sent_message)}: {e}"
|
||||
)
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
if processed_count == 0:
|
||||
logger.error(f"add_in_db_media_mediagroup: Не удалось обработать ни одного сообщения из медиагруппы {post_id}")
|
||||
logger.error(
|
||||
f"add_in_db_media_mediagroup: Не удалось обработать ни одного сообщения из медиагруппы {post_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
if failed_count > 0:
|
||||
logger.warning(f"add_in_db_media_mediagroup: Обработано {processed_count}/{len(sent_message)} сообщений медиагруппы {post_id}, ошибок: {failed_count}")
|
||||
logger.warning(
|
||||
f"add_in_db_media_mediagroup: Обработано {processed_count}/{len(sent_message)} сообщений медиагруппы {post_id}, ошибок: {failed_count}"
|
||||
)
|
||||
|
||||
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}с")
|
||||
logger.error(
|
||||
f"add_in_db_media_mediagroup: Критическая ошибка обработки медиагруппы: {e}, время: {processing_time:.2f}с"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@track_time("add_in_db_media", "helper_func")
|
||||
@track_errors("helper_func", "add_in_db_media")
|
||||
@track_media_processing("media_group")
|
||||
@db_query_time("add_in_db_media", "posts", "insert")
|
||||
@track_file_operations("media")
|
||||
async def add_in_db_media(sent_message: types.Message, bot_db: Any, s3_storage = None) -> bool:
|
||||
async def add_in_db_media(
|
||||
sent_message: types.Message, bot_db: Any, s3_storage=None
|
||||
) -> bool:
|
||||
"""
|
||||
Добавляет контент одиночного сообщения в базу данных
|
||||
|
||||
@@ -499,7 +608,9 @@ async def add_in_db_media(sent_message: types.Message, bot_db: Any, s3_storage =
|
||||
try:
|
||||
# Валидация параметров
|
||||
if not sent_message or not bot_db:
|
||||
logger.error("add_in_db_media: Неверные параметры - sent_message или bot_db отсутствуют")
|
||||
logger.error(
|
||||
"add_in_db_media: Неверные параметры - sent_message или bot_db отсутствуют"
|
||||
)
|
||||
return False
|
||||
|
||||
post_id = sent_message.message_id # ID поста (это же сообщение)
|
||||
@@ -508,29 +619,35 @@ async def add_in_db_media(sent_message: types.Message, bot_db: Any, s3_storage =
|
||||
|
||||
# Определяем тип контента и file_id
|
||||
if sent_message.photo:
|
||||
content_type = 'photo'
|
||||
content_type = "photo"
|
||||
file_id = sent_message.photo[-1].file_id
|
||||
elif sent_message.video:
|
||||
content_type = 'video'
|
||||
content_type = "video"
|
||||
file_id = sent_message.video.file_id
|
||||
elif sent_message.voice:
|
||||
content_type = 'voice'
|
||||
content_type = "voice"
|
||||
file_id = sent_message.voice.file_id
|
||||
elif sent_message.audio:
|
||||
content_type = 'audio'
|
||||
content_type = "audio"
|
||||
file_id = sent_message.audio.file_id
|
||||
elif sent_message.video_note:
|
||||
content_type = 'video_note'
|
||||
content_type = "video_note"
|
||||
file_id = sent_message.video_note.file_id
|
||||
else:
|
||||
logger.warning(f"add_in_db_media: Неподдерживаемый тип контента для сообщения {post_id}")
|
||||
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}")
|
||||
logger.error(
|
||||
f"add_in_db_media: file_id отсутствует для сообщения {post_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.debug(f"add_in_db_media: Обрабатываю {content_type} для сообщения {post_id}")
|
||||
logger.debug(
|
||||
f"add_in_db_media: Обрабатываю {content_type} для сообщения {post_id}"
|
||||
)
|
||||
|
||||
# Получаем s3_storage если не передан
|
||||
if s3_storage is None:
|
||||
@@ -538,40 +655,66 @@ async def add_in_db_media(sent_message: types.Message, bot_db: Any, s3_storage =
|
||||
s3_storage = bdf.get_s3_storage()
|
||||
|
||||
# Скачиваем файл (в S3 или на локальный диск)
|
||||
file_path = await download_file(sent_message, file_id=file_id, content_type=content_type, s3_storage=s3_storage)
|
||||
file_path = await download_file(
|
||||
sent_message,
|
||||
file_id=file_id,
|
||||
content_type=content_type,
|
||||
s3_storage=s3_storage,
|
||||
)
|
||||
if not file_path:
|
||||
logger.error(f"add_in_db_media: Не удалось скачать файл {file_id} для сообщения {post_id}")
|
||||
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)
|
||||
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}")
|
||||
logger.error(
|
||||
f"add_in_db_media: Не удалось добавить контент в БД для сообщения {post_id}"
|
||||
)
|
||||
# Удаляем скачанный файл при ошибке БД (только если это локальный файл, не S3)
|
||||
if file_path.startswith('files/'):
|
||||
if file_path.startswith("files/"):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.debug(f"add_in_db_media: Удален файл {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}")
|
||||
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}с")
|
||||
logger.info(
|
||||
f"add_in_db_media: Контент успешно добавлен для сообщения {post_id}, тип: {content_type}, время: {processing_time:.2f}с"
|
||||
)
|
||||
|
||||
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}с")
|
||||
logger.error(
|
||||
f"add_in_db_media: Ошибка обработки медиа для сообщения {post_id}: {e}, время: {processing_time:.2f}с"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@track_time("send_media_group_message_to_private_chat", "helper_func")
|
||||
@track_errors("helper_func", "send_media_group_message_to_private_chat")
|
||||
@track_media_processing("media_group")
|
||||
@db_query_time("send_media_group_message_to_private_chat", "posts", "insert")
|
||||
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
|
||||
media_group: List, bot_db: Any, main_post_id: Optional[int] = None, s3_storage=None) -> List[int]:
|
||||
async def send_media_group_message_to_private_chat(
|
||||
chat_id: int,
|
||||
message: types.Message,
|
||||
media_group: List,
|
||||
bot_db: Any,
|
||||
main_post_id: Optional[int] = None,
|
||||
s3_storage=None,
|
||||
) -> List[int]:
|
||||
"""
|
||||
Отправляет медиагруппу в чат и возвращает все message_id отправленных сообщений.
|
||||
|
||||
@@ -594,14 +737,19 @@ async def send_media_group_message_to_private_chat(chat_id: int, message: types.
|
||||
sent_message_ids = [msg.message_id for msg in sent_messages]
|
||||
main_message_id = sent_message_ids[-1]
|
||||
|
||||
asyncio.create_task(_save_media_group_background(sent_messages, bot_db, main_message_id, s3_storage))
|
||||
asyncio.create_task(
|
||||
_save_media_group_background(sent_messages, bot_db, main_message_id, s3_storage)
|
||||
)
|
||||
|
||||
return sent_message_ids
|
||||
|
||||
|
||||
@track_time("send_media_group_to_channel", "helper_func")
|
||||
@track_errors("helper_func", "send_media_group_to_channel")
|
||||
@track_media_processing("media_group")
|
||||
async def send_media_group_to_channel(bot, chat_id: int, post_content: List, post_text: str, s3_storage = None):
|
||||
async def send_media_group_to_channel(
|
||||
bot, chat_id: int, post_content: List, post_text: str, s3_storage=None
|
||||
):
|
||||
"""
|
||||
Отправляет медиа-группу с подписью к последнему файлу.
|
||||
|
||||
@@ -612,7 +760,9 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: List, pos
|
||||
post_text: Текст подписи.
|
||||
s3_storage: опциональный S3StorageService для работы с S3.
|
||||
"""
|
||||
logger.info(f"Начинаю отправку медиа-группы в чат {chat_id}, количество файлов: {len(post_content)}")
|
||||
logger.info(
|
||||
f"Начинаю отправку медиа-группы в чат {chat_id}, количество файлов: {len(post_content)}"
|
||||
)
|
||||
|
||||
# Получаем s3_storage если не передан
|
||||
if s3_storage is None:
|
||||
@@ -626,11 +776,17 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: List, pos
|
||||
for i, file_path_tuple in enumerate(post_content):
|
||||
try:
|
||||
file_path, content_type = file_path_tuple
|
||||
logger.debug(f"Обрабатываю файл {i+1}/{len(post_content)}: {file_path} (тип: {content_type})")
|
||||
logger.debug(
|
||||
f"Обрабатываю файл {i+1}/{len(post_content)}: {file_path} (тип: {content_type})"
|
||||
)
|
||||
|
||||
# Проверяем, это S3 ключ или локальный путь
|
||||
actual_path = file_path
|
||||
if s3_storage and not file_path.startswith('files/') and not os.path.exists(file_path):
|
||||
if (
|
||||
s3_storage
|
||||
and not file_path.startswith("files/")
|
||||
and not os.path.exists(file_path)
|
||||
):
|
||||
# Это S3 ключ, скачиваем во временный файл
|
||||
temp_path = await s3_storage.download_to_temp(file_path)
|
||||
if not temp_path:
|
||||
@@ -644,12 +800,14 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: List, pos
|
||||
|
||||
file = FSInputFile(path=actual_path)
|
||||
|
||||
if content_type == 'video':
|
||||
if content_type == "video":
|
||||
media.append(types.InputMediaVideo(media=file))
|
||||
elif content_type == 'photo':
|
||||
elif content_type == "photo":
|
||||
media.append(types.InputMediaPhoto(media=file))
|
||||
else:
|
||||
logger.warning(f"Неизвестный тип файла: {content_type} для {file_path}")
|
||||
logger.warning(
|
||||
f"Неизвестный тип файла: {content_type} для {file_path}"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Файл не найден: {file_path_tuple[0]}")
|
||||
continue
|
||||
@@ -664,11 +822,15 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: List, pos
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
media[-1].caption = safe_post_text
|
||||
logger.debug(f"Добавлена подпись к последнему файлу: {safe_post_text[:50]}{'...' if len(safe_post_text) > 50 else ''}")
|
||||
logger.debug(
|
||||
f"Добавлена подпись к последнему файлу: {safe_post_text[:50]}{'...' if len(safe_post_text) > 50 else ''}"
|
||||
)
|
||||
|
||||
try:
|
||||
sent_messages = await bot.send_media_group(chat_id=chat_id, media=media)
|
||||
logger.info(f"Медиа-группа успешно отправлена в чат {chat_id}, количество сообщений: {len(sent_messages)}")
|
||||
logger.info(
|
||||
f"Медиа-группа успешно отправлена в чат {chat_id}, количество сообщений: {len(sent_messages)}"
|
||||
)
|
||||
return sent_messages
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке медиа-группы в чат {chat_id}: {e}")
|
||||
@@ -681,9 +843,15 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: List, pos
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@track_time("send_text_message", "helper_func")
|
||||
@track_errors("helper_func", "send_text_message")
|
||||
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,
|
||||
):
|
||||
from .rate_limiter import send_with_rate_limit
|
||||
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
@@ -691,131 +859,132 @@ async def send_text_message(chat_id, message: types.Message, post_text: str, mar
|
||||
|
||||
async def _send_message():
|
||||
if markup is None:
|
||||
return await message.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=safe_post_text
|
||||
)
|
||||
return await message.bot.send_message(chat_id=chat_id, text=safe_post_text)
|
||||
else:
|
||||
return await message.bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=safe_post_text,
|
||||
reply_markup=markup
|
||||
chat_id=chat_id, text=safe_post_text, reply_markup=markup
|
||||
)
|
||||
|
||||
sent_message = await send_with_rate_limit(_send_message, chat_id)
|
||||
return sent_message
|
||||
|
||||
|
||||
@track_time("send_photo_message", "helper_func")
|
||||
@track_errors("helper_func", "send_photo_message")
|
||||
async def send_photo_message(chat_id, message: types.Message, photo: str, post_text: str,
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
async def send_photo_message(
|
||||
chat_id,
|
||||
message: types.Message,
|
||||
photo: str,
|
||||
post_text: str,
|
||||
markup: types.ReplyKeyboardMarkup = None,
|
||||
):
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
|
||||
if markup is None:
|
||||
sent_message = await message.bot.send_photo(
|
||||
chat_id=chat_id,
|
||||
caption=safe_post_text,
|
||||
photo=photo
|
||||
chat_id=chat_id, caption=safe_post_text, photo=photo
|
||||
)
|
||||
else:
|
||||
sent_message = await message.bot.send_photo(
|
||||
chat_id=chat_id,
|
||||
caption=safe_post_text,
|
||||
photo=photo,
|
||||
reply_markup=markup
|
||||
chat_id=chat_id, caption=safe_post_text, photo=photo, reply_markup=markup
|
||||
)
|
||||
return sent_message
|
||||
|
||||
|
||||
@track_time("send_video_message", "helper_func")
|
||||
@track_errors("helper_func", "send_video_message")
|
||||
async def send_video_message(chat_id, message: types.Message, video: str, post_text: str = "",
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
async def send_video_message(
|
||||
chat_id,
|
||||
message: types.Message,
|
||||
video: str,
|
||||
post_text: str = "",
|
||||
markup: types.ReplyKeyboardMarkup = None,
|
||||
):
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
|
||||
if markup is None:
|
||||
sent_message = await message.bot.send_video(
|
||||
chat_id=chat_id,
|
||||
caption=safe_post_text,
|
||||
video=video
|
||||
chat_id=chat_id, caption=safe_post_text, video=video
|
||||
)
|
||||
else:
|
||||
sent_message = await message.bot.send_video(
|
||||
chat_id=chat_id,
|
||||
caption=safe_post_text,
|
||||
video=video,
|
||||
reply_markup=markup
|
||||
chat_id=chat_id, caption=safe_post_text, video=video, reply_markup=markup
|
||||
)
|
||||
return sent_message
|
||||
|
||||
|
||||
@track_time("send_video_note_message", "helper_func")
|
||||
@track_errors("helper_func", "send_video_note_message")
|
||||
async def send_video_note_message(chat_id, message: types.Message, video_note: str,
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
async def send_video_note_message(
|
||||
chat_id,
|
||||
message: types.Message,
|
||||
video_note: str,
|
||||
markup: types.ReplyKeyboardMarkup = None,
|
||||
):
|
||||
if markup is None:
|
||||
sent_message = await message.bot.send_video_note(
|
||||
chat_id=chat_id,
|
||||
video_note=video_note
|
||||
chat_id=chat_id, video_note=video_note
|
||||
)
|
||||
else:
|
||||
sent_message = await message.bot.send_video_note(
|
||||
chat_id=chat_id,
|
||||
video_note=video_note,
|
||||
reply_markup=markup
|
||||
chat_id=chat_id, video_note=video_note, reply_markup=markup
|
||||
)
|
||||
return sent_message
|
||||
|
||||
|
||||
@track_time("send_audio_message", "helper_func")
|
||||
@track_errors("helper_func", "send_audio_message")
|
||||
async def send_audio_message(chat_id, message: types.Message, audio: str, post_text: str,
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
async def send_audio_message(
|
||||
chat_id,
|
||||
message: types.Message,
|
||||
audio: str,
|
||||
post_text: str,
|
||||
markup: types.ReplyKeyboardMarkup = None,
|
||||
):
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
|
||||
if markup is None:
|
||||
sent_message = await message.bot.send_audio(
|
||||
chat_id=chat_id,
|
||||
caption=safe_post_text,
|
||||
audio=audio
|
||||
chat_id=chat_id, caption=safe_post_text, audio=audio
|
||||
)
|
||||
else:
|
||||
sent_message = await message.bot.send_audio(
|
||||
chat_id=chat_id,
|
||||
caption=safe_post_text,
|
||||
audio=audio,
|
||||
reply_markup=markup
|
||||
chat_id=chat_id, caption=safe_post_text, audio=audio, reply_markup=markup
|
||||
)
|
||||
return sent_message
|
||||
|
||||
|
||||
@track_time("send_voice_message", "helper_func")
|
||||
@track_errors("helper_func", "send_voice_message")
|
||||
async def send_voice_message(chat_id, message: types.Message, voice: str,
|
||||
markup: types.ReplyKeyboardMarkup = None):
|
||||
async def send_voice_message(
|
||||
chat_id,
|
||||
message: types.Message,
|
||||
voice: str,
|
||||
markup: types.ReplyKeyboardMarkup = None,
|
||||
):
|
||||
from .rate_limiter import send_with_rate_limit
|
||||
|
||||
async def _send_voice():
|
||||
if markup is None:
|
||||
return await message.bot.send_voice(
|
||||
chat_id=chat_id,
|
||||
voice=voice
|
||||
)
|
||||
return await message.bot.send_voice(chat_id=chat_id, voice=voice)
|
||||
else:
|
||||
return await message.bot.send_voice(
|
||||
chat_id=chat_id,
|
||||
voice=voice,
|
||||
reply_markup=markup
|
||||
chat_id=chat_id, voice=voice, reply_markup=markup
|
||||
)
|
||||
|
||||
return await send_with_rate_limit(_send_voice, chat_id)
|
||||
|
||||
|
||||
@track_time("check_access", "helper_func")
|
||||
@track_errors("helper_func", "check_access")
|
||||
@db_query_time("check_access", "users", "select")
|
||||
async def check_access(user_id: int, bot_db):
|
||||
"""Проверка прав на совершение действий"""
|
||||
from logs.custom_logger import logger
|
||||
|
||||
result = await bot_db.is_admin(user_id)
|
||||
logger.info(f"check_access: пользователь {user_id} - результат: {result}")
|
||||
return result
|
||||
@@ -827,6 +996,7 @@ def add_days_to_date(days: int):
|
||||
future_date = current_date + timedelta(days=days)
|
||||
return int(future_date.timestamp())
|
||||
|
||||
|
||||
@track_time("get_banned_users_list", "helper_func")
|
||||
@track_errors("helper_func", "get_banned_users_list")
|
||||
@db_query_time("get_banned_users_list", "users", "select")
|
||||
@@ -854,7 +1024,9 @@ async def get_banned_users_list(offset: int, bot_db):
|
||||
|
||||
# Экранируем пользовательские данные для безопасного использования
|
||||
safe_user_name = html.escape(str(safe_user_name))
|
||||
safe_ban_reason = html.escape(str(ban_reason)) if ban_reason else "Причина не указана"
|
||||
safe_ban_reason = (
|
||||
html.escape(str(ban_reason)) if ban_reason else "Причина не указана"
|
||||
)
|
||||
|
||||
# Форматируем дату разбана в человекочитаемый формат
|
||||
if unban_date:
|
||||
@@ -873,7 +1045,7 @@ async def get_banned_users_list(offset: int, bot_db):
|
||||
except (ValueError, TypeError):
|
||||
# Если не удалось, показываем как есть
|
||||
safe_unban_date = html.escape(str(unban_date))
|
||||
elif hasattr(unban_date, 'strftime'):
|
||||
elif hasattr(unban_date, "strftime"):
|
||||
# Если это datetime объект
|
||||
safe_unban_date = unban_date.strftime("%d-%m-%Y %H:%M")
|
||||
else:
|
||||
@@ -890,6 +1062,7 @@ async def get_banned_users_list(offset: int, bot_db):
|
||||
message += f"**Дата разбана:** {safe_unban_date}\n\n"
|
||||
return message
|
||||
|
||||
|
||||
@track_time("get_banned_users_buttons", "helper_func")
|
||||
@track_errors("helper_func", "get_banned_users_buttons")
|
||||
@db_query_time("get_banned_users_buttons", "users", "select")
|
||||
@@ -919,6 +1092,7 @@ async def get_banned_users_buttons(bot_db):
|
||||
user_ids.append((safe_user_name, user_id))
|
||||
return user_ids
|
||||
|
||||
|
||||
@track_time("delete_user_blacklist", "helper_func")
|
||||
@track_errors("helper_func", "delete_user_blacklist")
|
||||
@db_query_time("delete_user_blacklist", "users", "delete")
|
||||
@@ -929,7 +1103,9 @@ async def delete_user_blacklist(user_id: int, bot_db):
|
||||
@track_time("check_username_and_full_name", "helper_func")
|
||||
@track_errors("helper_func", "check_username_and_full_name")
|
||||
@db_query_time("check_username_and_full_name", "users", "select")
|
||||
async def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
|
||||
async def check_username_and_full_name(
|
||||
user_id: int, username: str, full_name: str, bot_db
|
||||
):
|
||||
"""Проверяет, изменились ли username или full_name пользователя"""
|
||||
try:
|
||||
username_db = await bot_db.get_username(user_id)
|
||||
@@ -939,6 +1115,7 @@ async def check_username_and_full_name(user_id: int, username: str, full_name: s
|
||||
logger.error(f"Ошибка при проверке username и full_name: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@track_time("unban_notifier", "helper_func")
|
||||
@track_errors("helper_func", "unban_notifier")
|
||||
@db_query_time("unban_notifier", "users", "select")
|
||||
@@ -980,6 +1157,7 @@ async def update_user_info(source: str, message: types.Message):
|
||||
if not await BotDB.user_exists(user_id):
|
||||
# Create User object with current timestamp
|
||||
from database.models import User
|
||||
|
||||
current_timestamp = int(datetime.now().timestamp())
|
||||
user = User(
|
||||
user_id=user_id,
|
||||
@@ -992,18 +1170,23 @@ async def update_user_info(source: str, message: types.Message):
|
||||
has_stickers=False,
|
||||
date_added=current_timestamp,
|
||||
date_changed=current_timestamp,
|
||||
voice_bot_welcome_received=False
|
||||
voice_bot_welcome_received=False,
|
||||
)
|
||||
await BotDB.add_user(user)
|
||||
else:
|
||||
is_need_update = await 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:
|
||||
await BotDB.update_user_info(user_id, username, full_name)
|
||||
if source != 'voice':
|
||||
if source != "voice":
|
||||
await message.answer(
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}")
|
||||
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
|
||||
text=f'Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}')
|
||||
f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {full_name}"
|
||||
)
|
||||
await message.bot.send_message(
|
||||
chat_id=GROUP_FOR_LOGS,
|
||||
text=f"Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {full_name}\nНовый ник:{username}. Новый эмодзи:{user_emoji}",
|
||||
)
|
||||
sleep(1)
|
||||
await BotDB.update_user_date(user_id)
|
||||
|
||||
@@ -1014,7 +1197,11 @@ async def update_user_info(source: str, message: types.Message):
|
||||
async def check_user_emoji(message: types.Message):
|
||||
user_id = message.from_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 = await get_random_emoji()
|
||||
await BotDB.update_user_emoji(user_id=user_id, emoji=user_emoji)
|
||||
return user_emoji
|
||||
|
||||
@@ -4,55 +4,55 @@ import html
|
||||
from .metrics import metrics, track_errors, track_time
|
||||
|
||||
constants = {
|
||||
'HELLO_MESSAGE': "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
||||
"&Я был создан для того, чтобы помочь тебе выложить пост в наш канал и если это необходимо, связаться с админами ✍✉"
|
||||
"&Так же я могу выдать тебе набор стикеров, где я буду главным героем🦸♂"
|
||||
"&Наш бот голосового общения переехал ко мне! Доступен по кнопке 🎤Голосовой бот &Там можно послушать о чем говорит наш город🎧"
|
||||
"&Предлагай свой пост мне и я обязательно его опубликую😉"
|
||||
"&Для продолжения взаимодействия воспользуйся меню внизу твоего дисплея⬇"
|
||||
"&&Если что-то пошло не так: введи в чат команду /start или /restart, это перезапустит сценарий сначала."
|
||||
"Почитать инструкцию к боту можно по команде /help. Если есть вопросы, то пиши в личку: @Kerrad1"
|
||||
"&Не жми кнопку несколько раз если я не ответил с первого раза. Возможно ведутся тех.работы и я отвечу позже"
|
||||
"&&Группа в ВК: https://vk.com/love_bsk"
|
||||
"&Канал в ТГ: https://t.me/love_bsk",
|
||||
'SUGGEST_NEWS': "username, окей, жду от тебя текст поста🙌🏼"
|
||||
"&Обрати внимание, что я умный и смогу из твоего текста понять команды указанные ниже😉"
|
||||
"&Если хочешь чтобы пост был опубликован анонимно, напиши в любом месте своего поста слово 'анон'."
|
||||
"&Если хочешь опубликовать пост не анонимно, то напиши 'не анон', 'неанон' или не пиши ничего."
|
||||
"&&❗️❗️Я обучен только на команды, указанные мной выше👆"
|
||||
"&❗️❗️Проверь, чтобы указание авторства было выполнено так как я попросил, иначе пост будет выложен не корректно"
|
||||
"&Пост будет опубликован только в группе ТГ📩",
|
||||
"HELLO_MESSAGE": "Привет, username!👋🏼&Меня зовут Виби, я бот канала 'Влюбленный Бийск'❤🤖"
|
||||
"&Я был создан для того, чтобы помочь тебе выложить пост в наш канал и если это необходимо, связаться с админами ✍✉"
|
||||
"&Так же я могу выдать тебе набор стикеров, где я буду главным героем🦸♂"
|
||||
"&Наш бот голосового общения переехал ко мне! Доступен по кнопке 🎤Голосовой бот &Там можно послушать о чем говорит наш город🎧"
|
||||
"&Предлагай свой пост мне и я обязательно его опубликую😉"
|
||||
"&Для продолжения взаимодействия воспользуйся меню внизу твоего дисплея⬇"
|
||||
"&&Если что-то пошло не так: введи в чат команду /start или /restart, это перезапустит сценарий сначала."
|
||||
"Почитать инструкцию к боту можно по команде /help. Если есть вопросы, то пиши в личку: @Kerrad1"
|
||||
"&Не жми кнопку несколько раз если я не ответил с первого раза. Возможно ведутся тех.работы и я отвечу позже"
|
||||
"&&Группа в ВК: https://vk.com/love_bsk"
|
||||
"&Канал в ТГ: https://t.me/love_bsk",
|
||||
"SUGGEST_NEWS": "username, окей, жду от тебя текст поста🙌🏼"
|
||||
"&Обрати внимание, что я умный и смогу из твоего текста понять команды указанные ниже😉"
|
||||
"&Если хочешь чтобы пост был опубликован анонимно, напиши в любом месте своего поста слово 'анон'."
|
||||
"&Если хочешь опубликовать пост не анонимно, то напиши 'не анон', 'неанон' или не пиши ничего."
|
||||
"&&❗️❗️Я обучен только на команды, указанные мной выше👆"
|
||||
"&❗️❗️Проверь, чтобы указание авторства было выполнено так как я попросил, иначе пост будет выложен не корректно"
|
||||
"&Пост будет опубликован только в группе ТГ📩",
|
||||
"CONNECT_WITH_ADMIN": "username, напиши свое обращение или предложение✍️"
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺️❤️",
|
||||
"DEL_MESSAGE": "username, напиши свое обращение или предложение✍"
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺❤",
|
||||
"&Мы рассмотрим и ответим тебе в ближайшее время☺❤",
|
||||
"BYE_MESSAGE": "Если позднее захочешь предложить еще один пост или обратиться к админам с вопросом, то просто пришли в чат команду 👉 /restart"
|
||||
"&&И тебе пока!👋🏼❤️",
|
||||
"&&И тебе пока!👋🏼❤️",
|
||||
"USER_ERROR": "Увы, я не понимаю тебя😐💔 Выбери один из пунктов в нижнем меню, а затем пиши.",
|
||||
"QUESTION": "Сообщение успешно отправлено❤️ Ответим, как только сможем😉",
|
||||
"SUCCESS_SEND_MESSAGE": "Пост успешно отправлен❤️ Ожидай одобрения😊",
|
||||
# Voice handler messages
|
||||
"MESSAGE_FOR_STANDUP": "Отлично, ты вошел в режим стендапа 📣"
|
||||
"&Это свободное пространство, в котором может высказаться каждый житель нашего города, и он будет услышан🙌🏼"
|
||||
"&Для того чтобы высказаться, нажми кнопку: 'Высказаться' и запиши голосовое сообщение, оно выпадет анонимно кому-то другому🗣"
|
||||
"&Для того чтобы послушать о чем говорит наш город, нажми кнопку: 'Послушать'👂"
|
||||
"&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤"
|
||||
"&❗️Но пожалуйста не оскорбляй никого, и будь вежлив.",
|
||||
'WELCOME_MESSAGE': "<b>Привет.</b>",
|
||||
'DESCRIPTION_MESSAGE': "<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска</i>",
|
||||
'ANALOGY_MESSAGE': "Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
|
||||
'RULES_MESSAGE': "Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя бы на 5-10 секунд</i>",
|
||||
'ANONYMITY_MESSAGE': "Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)",
|
||||
'SUGGESTION_MESSAGE': "Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
|
||||
'EMOJI_INFO_MESSAGE': "Любые войсы будут помечены эмоджи. <b>Твой эмоджи - </b>{emoji}Таким эмоджи будут помечены твои сообщения для других Но другие люди не узнают кто за каким эмоджи скрывается:)",
|
||||
'HELP_INFO_MESSAGE': "Так же можешь ознакомиться с инструкцией к боту по команде /help",
|
||||
'FINAL_MESSAGE': "<b>Ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
|
||||
'HELP_MESSAGE': "Когда-нибудь здесь будет инструкция к боту. А пока по вопросам пиши в личку: @Kerrad1 или в Связаться с админами",
|
||||
'VOICE_SAVED_MESSAGE': "Окей, сохранил!👌",
|
||||
'LISTENINGS_CLEARED_MESSAGE': "Прослушивания очищены. Можешь начать слушать заново🤗",
|
||||
'NO_AUDIO_MESSAGE': "Прости, ты прослушал все аудио😔. Возвращайся позже, возможно наша база пополнится",
|
||||
'UNKNOWN_CONTENT_MESSAGE': "Я тебя не понимаю🤷♀️ запиши голосовое",
|
||||
'RECORD_VOICE_MESSAGE': "Хорошо, теперь пришли мне свое голосовое сообщение"
|
||||
"&Это свободное пространство, в котором может высказаться каждый житель нашего города, и он будет услышан🙌🏼"
|
||||
"&Для того чтобы высказаться, нажми кнопку: 'Высказаться' и запиши голосовое сообщение, оно выпадет анонимно кому-то другому🗣"
|
||||
"&Для того чтобы послушать о чем говорит наш город, нажми кнопку: 'Послушать'👂"
|
||||
"&Ты можешь анонимно пообщаться, поделиться чем-то важным, обратиться напрямую к жителям🤝 Также можешь выступить перед аудиторией (спеть песню, рассказать стихотворение, шутку)🎤"
|
||||
"&❗️Но пожалуйста не оскорбляй никого, и будь вежлив.",
|
||||
"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": "Хорошо, теперь пришли мне свое голосовое сообщение",
|
||||
}
|
||||
|
||||
|
||||
@@ -64,5 +64,5 @@ def get_message(username: str, type_message: str):
|
||||
raise TypeError("username is None")
|
||||
message = constants[type_message]
|
||||
# Экранируем потенциально проблемные символы для HTML
|
||||
message = message.replace('username', html.escape(username)).replace('&', '\n')
|
||||
message = message.replace("username", html.escape(username)).replace("&", "\n")
|
||||
return message
|
||||
|
||||
@@ -10,8 +10,13 @@ from contextlib import asynccontextmanager
|
||||
from functools import wraps
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from prometheus_client import (CONTENT_TYPE_LATEST, Counter, Gauge, Histogram,
|
||||
generate_latest)
|
||||
from prometheus_client import (
|
||||
CONTENT_TYPE_LATEST,
|
||||
Counter,
|
||||
Gauge,
|
||||
Histogram,
|
||||
generate_latest,
|
||||
)
|
||||
from prometheus_client.core import CollectorRegistry
|
||||
|
||||
# Метрики rate limiter теперь создаются в основном классе
|
||||
@@ -28,142 +33,140 @@ class BotMetrics:
|
||||
|
||||
# Bot commands counter
|
||||
self.bot_commands_total = Counter(
|
||||
'bot_commands_total',
|
||||
'Total number of bot commands processed',
|
||||
['command', 'status', 'handler_type', 'user_type'],
|
||||
registry=self.registry
|
||||
"bot_commands_total",
|
||||
"Total number of bot commands processed",
|
||||
["command", "status", "handler_type", "user_type"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Method execution time histogram
|
||||
self.method_duration_seconds = Histogram(
|
||||
'method_duration_seconds',
|
||||
'Time spent executing methods',
|
||||
['method_name', 'handler_type', 'status'],
|
||||
"method_duration_seconds",
|
||||
"Time spent executing methods",
|
||||
["method_name", "handler_type", "status"],
|
||||
# Оптимизированные buckets для Telegram API (обычно < 1 сек)
|
||||
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
|
||||
registry=self.registry
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Errors counter
|
||||
self.errors_total = Counter(
|
||||
'errors_total',
|
||||
'Total number of errors',
|
||||
['error_type', 'handler_type', 'method_name'],
|
||||
registry=self.registry
|
||||
"errors_total",
|
||||
"Total number of errors",
|
||||
["error_type", "handler_type", "method_name"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Active users gauge
|
||||
self.active_users = Gauge(
|
||||
'active_users',
|
||||
'Number of currently active users',
|
||||
['user_type'],
|
||||
registry=self.registry
|
||||
"active_users",
|
||||
"Number of currently active users",
|
||||
["user_type"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Total users gauge (отдельная метрика)
|
||||
self.total_users = Gauge(
|
||||
'total_users',
|
||||
'Total number of users in database',
|
||||
registry=self.registry
|
||||
"total_users", "Total number of users in database", registry=self.registry
|
||||
)
|
||||
|
||||
# Database query metrics
|
||||
self.db_query_duration_seconds = Histogram(
|
||||
'db_query_duration_seconds',
|
||||
'Time spent executing database queries',
|
||||
['query_type', 'table_name', 'operation'],
|
||||
"db_query_duration_seconds",
|
||||
"Time spent executing database queries",
|
||||
["query_type", "table_name", "operation"],
|
||||
# Оптимизированные buckets для SQLite/PostgreSQL
|
||||
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5],
|
||||
registry=self.registry
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Database queries counter
|
||||
self.db_queries_total = Counter(
|
||||
'db_queries_total',
|
||||
'Total number of database queries executed',
|
||||
['query_type', 'table_name', 'operation'],
|
||||
registry=self.registry
|
||||
"db_queries_total",
|
||||
"Total number of database queries executed",
|
||||
["query_type", "table_name", "operation"],
|
||||
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
|
||||
"db_errors_total",
|
||||
"Total number of database errors",
|
||||
["error_type", "query_type", "table_name", "operation"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Message processing metrics
|
||||
self.messages_processed_total = Counter(
|
||||
'messages_processed_total',
|
||||
'Total number of messages processed',
|
||||
['message_type', 'chat_type', 'handler_type'],
|
||||
registry=self.registry
|
||||
"messages_processed_total",
|
||||
"Total number of messages processed",
|
||||
["message_type", "chat_type", "handler_type"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Middleware execution metrics
|
||||
self.middleware_duration_seconds = Histogram(
|
||||
'middleware_duration_seconds',
|
||||
'Time spent in middleware execution',
|
||||
['middleware_name', 'status'],
|
||||
"middleware_duration_seconds",
|
||||
"Time spent in middleware execution",
|
||||
["middleware_name", "status"],
|
||||
# Middleware должен быть быстрым
|
||||
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.25],
|
||||
registry=self.registry
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Rate limiting metrics
|
||||
self.rate_limit_hits_total = Counter(
|
||||
'rate_limit_hits_total',
|
||||
'Total number of rate limit hits',
|
||||
['limit_type', 'user_id', 'action'],
|
||||
registry=self.registry
|
||||
"rate_limit_hits_total",
|
||||
"Total number of rate limit hits",
|
||||
["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
|
||||
"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
|
||||
"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'],
|
||||
"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
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.file_download_size_bytes = Histogram(
|
||||
'file_download_size_bytes',
|
||||
'Size of downloaded files in bytes',
|
||||
['content_type'],
|
||||
"file_download_size_bytes",
|
||||
"Size of downloaded files in bytes",
|
||||
["content_type"],
|
||||
buckets=[1024, 10240, 102400, 1048576, 10485760, 104857600, 1073741824],
|
||||
registry=self.registry
|
||||
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
|
||||
"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'],
|
||||
"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,
|
||||
)
|
||||
|
||||
def _create_rate_limit_metrics(self):
|
||||
@@ -171,96 +174,110 @@ class BotMetrics:
|
||||
try:
|
||||
# Создаем метрики rate limiter в том же registry
|
||||
self.rate_limit_requests_total = Counter(
|
||||
'rate_limit_requests_total',
|
||||
'Total number of rate limited requests',
|
||||
['chat_id', 'status', 'error_type'],
|
||||
registry=self.registry
|
||||
"rate_limit_requests_total",
|
||||
"Total number of rate limited requests",
|
||||
["chat_id", "status", "error_type"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_errors_total = Counter(
|
||||
'rate_limit_errors_total',
|
||||
'Total number of rate limit errors',
|
||||
['error_type', 'chat_id'],
|
||||
registry=self.registry
|
||||
"rate_limit_errors_total",
|
||||
"Total number of rate limit errors",
|
||||
["error_type", "chat_id"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_wait_duration_seconds = Histogram(
|
||||
'rate_limit_wait_duration_seconds',
|
||||
'Time spent waiting due to rate limiting',
|
||||
['chat_id'],
|
||||
"rate_limit_wait_duration_seconds",
|
||||
"Time spent waiting due to rate limiting",
|
||||
["chat_id"],
|
||||
buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0],
|
||||
registry=self.registry
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_active_chats = Gauge(
|
||||
'rate_limit_active_chats',
|
||||
'Number of active chats with rate limiting',
|
||||
registry=self.registry
|
||||
"rate_limit_active_chats",
|
||||
"Number of active chats with rate limiting",
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_success_rate = Gauge(
|
||||
'rate_limit_success_rate',
|
||||
'Success rate of rate limited requests',
|
||||
['chat_id'],
|
||||
registry=self.registry
|
||||
"rate_limit_success_rate",
|
||||
"Success rate of rate limited requests",
|
||||
["chat_id"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_requests_per_minute = Gauge(
|
||||
'rate_limit_requests_per_minute',
|
||||
'Requests per minute',
|
||||
['chat_id'],
|
||||
registry=self.registry
|
||||
"rate_limit_requests_per_minute",
|
||||
"Requests per minute",
|
||||
["chat_id"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_total_requests = Gauge(
|
||||
'rate_limit_total_requests',
|
||||
'Total number of requests',
|
||||
['chat_id'],
|
||||
registry=self.registry
|
||||
"rate_limit_total_requests",
|
||||
"Total number of requests",
|
||||
["chat_id"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_total_errors = Gauge(
|
||||
'rate_limit_total_errors',
|
||||
'Total number of errors',
|
||||
['chat_id', 'error_type'],
|
||||
registry=self.registry
|
||||
"rate_limit_total_errors",
|
||||
"Total number of errors",
|
||||
["chat_id", "error_type"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
self.rate_limit_avg_wait_time_seconds = Gauge(
|
||||
'rate_limit_avg_wait_time_seconds',
|
||||
'Average wait time in seconds',
|
||||
['chat_id'],
|
||||
registry=self.registry
|
||||
"rate_limit_avg_wait_time_seconds",
|
||||
"Average wait time in seconds",
|
||||
["chat_id"],
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Логируем ошибку, но не прерываем инициализацию
|
||||
import logging
|
||||
|
||||
logging.warning(f"Failed to create rate limit metrics: {e}")
|
||||
|
||||
def record_command(self, command_type: str, handler_type: str = "unknown", user_type: str = "unknown", status: str = "success"):
|
||||
def record_command(
|
||||
self,
|
||||
command_type: str,
|
||||
handler_type: str = "unknown",
|
||||
user_type: str = "unknown",
|
||||
status: str = "success",
|
||||
):
|
||||
"""Record a bot command execution."""
|
||||
self.bot_commands_total.labels(
|
||||
command=command_type,
|
||||
status=status,
|
||||
handler_type=handler_type,
|
||||
user_type=user_type
|
||||
user_type=user_type,
|
||||
).inc()
|
||||
|
||||
def record_error(self, error_type: str, handler_type: str = "unknown", method_name: str = "unknown"):
|
||||
def record_error(
|
||||
self,
|
||||
error_type: str,
|
||||
handler_type: str = "unknown",
|
||||
method_name: str = "unknown",
|
||||
):
|
||||
"""Record an error occurrence."""
|
||||
self.errors_total.labels(
|
||||
error_type=error_type,
|
||||
handler_type=handler_type,
|
||||
method_name=method_name
|
||||
error_type=error_type, handler_type=handler_type, method_name=method_name
|
||||
).inc()
|
||||
|
||||
def record_method_duration(self, method_name: str, duration: float, handler_type: str = "unknown", status: str = "success"):
|
||||
def record_method_duration(
|
||||
self,
|
||||
method_name: str,
|
||||
duration: float,
|
||||
handler_type: str = "unknown",
|
||||
status: str = "success",
|
||||
):
|
||||
"""Record method execution duration."""
|
||||
self.method_duration_seconds.labels(
|
||||
method_name=method_name,
|
||||
handler_type=handler_type,
|
||||
status=status
|
||||
method_name=method_name, handler_type=handler_type, status=status
|
||||
).observe(duration)
|
||||
|
||||
def set_active_users(self, count: int, user_type: str = "daily"):
|
||||
@@ -271,69 +288,74 @@ class BotMetrics:
|
||||
"""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."""
|
||||
self.db_query_duration_seconds.labels(
|
||||
query_type=query_type,
|
||||
table_name=table_name,
|
||||
operation=operation
|
||||
query_type=query_type, table_name=table_name, operation=operation
|
||||
).observe(duration)
|
||||
self.db_queries_total.labels(
|
||||
query_type=query_type,
|
||||
table_name=table_name,
|
||||
operation=operation
|
||||
query_type=query_type, table_name=table_name, operation=operation
|
||||
).inc()
|
||||
|
||||
def record_message(self, message_type: str, chat_type: str = "unknown", handler_type: str = "unknown"):
|
||||
def record_message(
|
||||
self,
|
||||
message_type: str,
|
||||
chat_type: str = "unknown",
|
||||
handler_type: str = "unknown",
|
||||
):
|
||||
"""Record a processed message."""
|
||||
self.messages_processed_total.labels(
|
||||
message_type=message_type,
|
||||
chat_type=chat_type,
|
||||
handler_type=handler_type
|
||||
message_type=message_type, chat_type=chat_type, handler_type=handler_type
|
||||
).inc()
|
||||
|
||||
def record_middleware(self, middleware_name: str, duration: float, status: str = "success"):
|
||||
def record_middleware(
|
||||
self, middleware_name: str, duration: float, status: str = "success"
|
||||
):
|
||||
"""Record middleware execution duration."""
|
||||
self.middleware_duration_seconds.labels(
|
||||
middleware_name=middleware_name,
|
||||
status=status
|
||||
middleware_name=middleware_name, status=status
|
||||
).observe(duration)
|
||||
|
||||
def record_file_download(self, content_type: str, file_size: int, duration: float):
|
||||
"""Record file download metrics."""
|
||||
self.file_downloads_total.labels(
|
||||
content_type=content_type,
|
||||
status="success"
|
||||
content_type=content_type, status="success"
|
||||
).inc()
|
||||
|
||||
self.file_download_duration_seconds.labels(
|
||||
content_type=content_type
|
||||
).observe(duration)
|
||||
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)
|
||||
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"
|
||||
content_type=content_type, status="error"
|
||||
).inc()
|
||||
|
||||
self.errors_total.labels(
|
||||
error_type="file_download_error",
|
||||
handler_type="media_processing",
|
||||
method_name="download_file"
|
||||
method_name="download_file",
|
||||
).inc()
|
||||
|
||||
def record_media_processing(self, content_type: str, duration: float, success: bool):
|
||||
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
|
||||
content_type=content_type, status=status
|
||||
).inc()
|
||||
|
||||
self.media_processing_duration_seconds.labels(
|
||||
@@ -344,19 +366,31 @@ class BotMetrics:
|
||||
self.errors_total.labels(
|
||||
error_type="media_processing_error",
|
||||
handler_type="media_processing",
|
||||
method_name="add_in_db_media"
|
||||
method_name="add_in_db_media",
|
||||
).inc()
|
||||
|
||||
def record_db_error(self, error_type: str, query_type: str = "unknown", table_name: str = "unknown", operation: str = "unknown"):
|
||||
def record_db_error(
|
||||
self,
|
||||
error_type: str,
|
||||
query_type: str = "unknown",
|
||||
table_name: str = "unknown",
|
||||
operation: str = "unknown",
|
||||
):
|
||||
"""Record database error occurrence."""
|
||||
self.db_errors_total.labels(
|
||||
error_type=error_type,
|
||||
query_type=query_type,
|
||||
table_name=table_name,
|
||||
operation=operation
|
||||
operation=operation,
|
||||
).inc()
|
||||
|
||||
def record_rate_limit_request(self, chat_id: int, success: bool, wait_time: float = 0.0, error_type: str = None):
|
||||
def record_rate_limit_request(
|
||||
self,
|
||||
chat_id: int,
|
||||
success: bool,
|
||||
wait_time: float = 0.0,
|
||||
error_type: str = None,
|
||||
):
|
||||
"""Record rate limit request metrics."""
|
||||
try:
|
||||
# Определяем статус
|
||||
@@ -364,9 +398,7 @@ class BotMetrics:
|
||||
|
||||
# Записываем счетчик запросов
|
||||
self.rate_limit_requests_total.labels(
|
||||
chat_id=str(chat_id),
|
||||
status=status,
|
||||
error_type=error_type or "none"
|
||||
chat_id=str(chat_id), status=status, error_type=error_type or "none"
|
||||
).inc()
|
||||
|
||||
# Записываем время ожидания
|
||||
@@ -378,11 +410,11 @@ class BotMetrics:
|
||||
# Записываем ошибки
|
||||
if not success and error_type:
|
||||
self.rate_limit_errors_total.labels(
|
||||
error_type=error_type,
|
||||
chat_id=str(chat_id)
|
||||
error_type=error_type, chat_id=str(chat_id)
|
||||
).inc()
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
logging.warning(f"Failed to record rate limit request: {e}")
|
||||
|
||||
def update_rate_limit_gauges(self):
|
||||
@@ -398,39 +430,38 @@ class BotMetrics:
|
||||
chat_id_str = str(chat_id)
|
||||
|
||||
# Процент успеха
|
||||
self.rate_limit_success_rate.labels(
|
||||
chat_id=chat_id_str
|
||||
).set(chat_stats.success_rate)
|
||||
self.rate_limit_success_rate.labels(chat_id=chat_id_str).set(
|
||||
chat_stats.success_rate
|
||||
)
|
||||
|
||||
# Запросов в минуту
|
||||
self.rate_limit_requests_per_minute.labels(
|
||||
chat_id=chat_id_str
|
||||
).set(chat_stats.requests_per_minute)
|
||||
self.rate_limit_requests_per_minute.labels(chat_id=chat_id_str).set(
|
||||
chat_stats.requests_per_minute
|
||||
)
|
||||
|
||||
# Общее количество запросов
|
||||
self.rate_limit_total_requests.labels(
|
||||
chat_id=chat_id_str
|
||||
).set(chat_stats.total_requests)
|
||||
self.rate_limit_total_requests.labels(chat_id=chat_id_str).set(
|
||||
chat_stats.total_requests
|
||||
)
|
||||
|
||||
# Среднее время ожидания
|
||||
self.rate_limit_avg_wait_time_seconds.labels(
|
||||
chat_id=chat_id_str
|
||||
).set(chat_stats.average_wait_time)
|
||||
self.rate_limit_avg_wait_time_seconds.labels(chat_id=chat_id_str).set(
|
||||
chat_stats.average_wait_time
|
||||
)
|
||||
|
||||
# Количество ошибок по типам
|
||||
if chat_stats.retry_after_errors > 0:
|
||||
self.rate_limit_total_errors.labels(
|
||||
chat_id=chat_id_str,
|
||||
error_type="RetryAfter"
|
||||
chat_id=chat_id_str, error_type="RetryAfter"
|
||||
).set(chat_stats.retry_after_errors)
|
||||
|
||||
if chat_stats.other_errors > 0:
|
||||
self.rate_limit_total_errors.labels(
|
||||
chat_id=chat_id_str,
|
||||
error_type="Other"
|
||||
chat_id=chat_id_str, error_type="Other"
|
||||
).set(chat_stats.other_errors)
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
logging.warning(f"Failed to update rate limit gauges: {e}")
|
||||
|
||||
def get_metrics(self) -> bytes:
|
||||
@@ -448,6 +479,7 @@ metrics = BotMetrics()
|
||||
# Decorators for easy metric collection
|
||||
def track_time(method_name: str = None, handler_type: str = "unknown"):
|
||||
"""Decorator to track execution time of functions."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
@@ -456,24 +488,16 @@ def track_time(method_name: str = None, handler_type: str = "unknown"):
|
||||
result = await func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"success"
|
||||
method_name or func.__name__, duration, handler_type, "success"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"error"
|
||||
method_name or func.__name__, duration, handler_type, "error"
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
type(e).__name__, handler_type, method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -484,35 +508,29 @@ def track_time(method_name: str = None, handler_type: str = "unknown"):
|
||||
result = func(*args, **kwargs)
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"success"
|
||||
method_name or func.__name__, duration, handler_type, "success"
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_method_duration(
|
||||
method_name or func.__name__,
|
||||
duration,
|
||||
handler_type,
|
||||
"error"
|
||||
method_name or func.__name__, duration, handler_type, "error"
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
type(e).__name__, handler_type, method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def track_errors(handler_type: str = "unknown", method_name: str = None):
|
||||
"""Decorator to track errors in functions."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
@@ -520,9 +538,7 @@ def track_errors(handler_type: str = "unknown", method_name: str = None):
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
type(e).__name__, handler_type, method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -532,20 +548,22 @@ def track_errors(handler_type: str = "unknown", method_name: str = None):
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
handler_type,
|
||||
method_name or func.__name__
|
||||
type(e).__name__, handler_type, method_name or func.__name__
|
||||
)
|
||||
raise
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def db_query_time(query_type: str = "unknown", table_name: str = "unknown", operation: str = "unknown"):
|
||||
def db_query_time(
|
||||
query_type: str = "unknown", table_name: str = "unknown", operation: str = "unknown"
|
||||
):
|
||||
"""Decorator to track database query execution time."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
@@ -559,16 +577,9 @@ def db_query_time(query_type: str = "unknown", table_name: str = "unknown", oper
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
metrics.record_db_error(
|
||||
type(e).__name__,
|
||||
query_type,
|
||||
table_name,
|
||||
operation
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"database",
|
||||
func.__name__
|
||||
type(e).__name__, query_type, table_name, operation
|
||||
)
|
||||
metrics.record_error(type(e).__name__, "database", func.__name__)
|
||||
raise
|
||||
|
||||
@wraps(func)
|
||||
@@ -583,21 +594,15 @@ def db_query_time(query_type: str = "unknown", table_name: str = "unknown", oper
|
||||
duration = time.time() - start_time
|
||||
metrics.record_db_query(query_type, duration, table_name, operation)
|
||||
metrics.record_db_error(
|
||||
type(e).__name__,
|
||||
query_type,
|
||||
table_name,
|
||||
operation
|
||||
)
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"database",
|
||||
func.__name__
|
||||
type(e).__name__, query_type, table_name, operation
|
||||
)
|
||||
metrics.record_error(type(e).__name__, "database", func.__name__)
|
||||
raise
|
||||
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@@ -612,16 +617,13 @@ async def track_middleware(middleware_name: str):
|
||||
except Exception as e:
|
||||
duration = time.time() - start_time
|
||||
metrics.record_middleware(middleware_name, duration, "error")
|
||||
metrics.record_error(
|
||||
type(e).__name__,
|
||||
"middleware",
|
||||
middleware_name
|
||||
)
|
||||
metrics.record_error(type(e).__name__, "middleware", middleware_name)
|
||||
raise
|
||||
|
||||
|
||||
def track_media_processing(content_type: str = "unknown"):
|
||||
"""Decorator to track media processing operations."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
@@ -652,11 +654,13 @@ def track_media_processing(content_type: str = "unknown"):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def track_file_operations(content_type: str = "unknown"):
|
||||
"""Decorator to track file download/upload operations."""
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
@@ -703,4 +707,5 @@ def track_file_operations(content_type: str = "unknown"):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Мониторинг и статистика rate limiting
|
||||
"""
|
||||
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from dataclasses import dataclass, field
|
||||
@@ -12,6 +13,7 @@ from logs.custom_logger import logger
|
||||
@dataclass
|
||||
class RateLimitStats:
|
||||
"""Статистика rate limiting для чата"""
|
||||
|
||||
chat_id: int
|
||||
total_requests: int = 0
|
||||
successful_requests: int = 0
|
||||
@@ -51,7 +53,9 @@ class RateLimitStats:
|
||||
minute_ago = current_time - 60
|
||||
|
||||
# Подсчитываем запросы за последнюю минуту
|
||||
recent_requests = sum(1 for req_time in self.request_times if req_time > minute_ago)
|
||||
recent_requests = sum(
|
||||
1 for req_time in self.request_times if req_time > minute_ago
|
||||
)
|
||||
return recent_requests
|
||||
|
||||
|
||||
@@ -64,7 +68,13 @@ class RateLimitMonitor:
|
||||
self.max_history_size = max_history_size
|
||||
self.error_history: deque = deque(maxlen=max_history_size)
|
||||
|
||||
def record_request(self, chat_id: int, success: bool, wait_time: float = 0.0, error_type: Optional[str] = None):
|
||||
def record_request(
|
||||
self,
|
||||
chat_id: int,
|
||||
success: bool,
|
||||
wait_time: float = 0.0,
|
||||
error_type: Optional[str] = None,
|
||||
):
|
||||
"""Записывает информацию о запросе"""
|
||||
current_time = time.time()
|
||||
|
||||
@@ -86,12 +96,14 @@ class RateLimitMonitor:
|
||||
chat_stats.other_errors += 1
|
||||
|
||||
# Записываем ошибку в историю
|
||||
self.error_history.append({
|
||||
'chat_id': chat_id,
|
||||
'error_type': error_type,
|
||||
'timestamp': current_time,
|
||||
'wait_time': wait_time
|
||||
})
|
||||
self.error_history.append(
|
||||
{
|
||||
"chat_id": chat_id,
|
||||
"error_type": error_type,
|
||||
"timestamp": current_time,
|
||||
"wait_time": wait_time,
|
||||
}
|
||||
)
|
||||
|
||||
# Обновляем глобальную статистику
|
||||
self.global_stats.total_requests += 1
|
||||
@@ -119,16 +131,15 @@ class RateLimitMonitor:
|
||||
def get_top_chats_by_requests(self, limit: int = 10) -> List[tuple]:
|
||||
"""Получает топ чатов по количеству запросов"""
|
||||
sorted_chats = sorted(
|
||||
self.stats.items(),
|
||||
key=lambda x: x[1].total_requests,
|
||||
reverse=True
|
||||
self.stats.items(), key=lambda x: x[1].total_requests, reverse=True
|
||||
)
|
||||
return sorted_chats[:limit]
|
||||
|
||||
def get_chats_with_high_error_rate(self, threshold: float = 0.1) -> List[tuple]:
|
||||
"""Получает чаты с высоким процентом ошибок"""
|
||||
high_error_chats = [
|
||||
(chat_id, stats) for chat_id, stats in self.stats.items()
|
||||
(chat_id, stats)
|
||||
for chat_id, stats in self.stats.items()
|
||||
if stats.error_rate > threshold and stats.total_requests > 5
|
||||
]
|
||||
return sorted(high_error_chats, key=lambda x: x[1].error_rate, reverse=True)
|
||||
@@ -139,8 +150,7 @@ class RateLimitMonitor:
|
||||
cutoff_time = current_time - (minutes * 60)
|
||||
|
||||
return [
|
||||
error for error in self.error_history
|
||||
if error['timestamp'] > cutoff_time
|
||||
error for error in self.error_history if error["timestamp"] > cutoff_time
|
||||
]
|
||||
|
||||
def get_error_summary(self, minutes: int = 60) -> Dict[str, int]:
|
||||
@@ -149,7 +159,7 @@ class RateLimitMonitor:
|
||||
error_summary = defaultdict(int)
|
||||
|
||||
for error in recent_errors:
|
||||
error_summary[error['error_type']] += 1
|
||||
error_summary[error["error_type"]] += 1
|
||||
|
||||
return dict(error_summary)
|
||||
|
||||
@@ -179,9 +189,13 @@ class RateLimitMonitor:
|
||||
# Логируем чаты с высоким процентом ошибок
|
||||
high_error_chats = self.get_chats_with_high_error_rate(0.2)
|
||||
if high_error_chats:
|
||||
logger.warning(f"Chats with high error rate (>20%): {len(high_error_chats)}")
|
||||
logger.warning(
|
||||
f"Chats with high error rate (>20%): {len(high_error_chats)}"
|
||||
)
|
||||
for chat_id, stats in high_error_chats[:5]: # Показываем только первые 5
|
||||
logger.warning(f" Chat {chat_id}: {stats.error_rate:.2%} error rate ({stats.failed_requests}/{stats.total_requests})")
|
||||
logger.warning(
|
||||
f" Chat {chat_id}: {stats.error_rate:.2%} error rate ({stats.failed_requests}/{stats.total_requests})"
|
||||
)
|
||||
|
||||
def reset_stats(self, chat_id: Optional[int] = None):
|
||||
"""Сбрасывает статистику"""
|
||||
@@ -200,7 +214,12 @@ class RateLimitMonitor:
|
||||
rate_limit_monitor = RateLimitMonitor()
|
||||
|
||||
|
||||
def record_rate_limit_request(chat_id: int, success: bool, wait_time: float = 0.0, error_type: Optional[str] = None):
|
||||
def record_rate_limit_request(
|
||||
chat_id: int,
|
||||
success: bool,
|
||||
wait_time: float = 0.0,
|
||||
error_type: Optional[str] = None,
|
||||
):
|
||||
"""Удобная функция для записи информации о запросе"""
|
||||
rate_limit_monitor.record_request(chat_id, success, wait_time, error_type)
|
||||
|
||||
@@ -211,11 +230,11 @@ def get_rate_limit_summary() -> Dict:
|
||||
recent_errors = rate_limit_monitor.get_recent_errors(60) # За последний час
|
||||
|
||||
return {
|
||||
'total_requests': global_stats.total_requests,
|
||||
'success_rate': global_stats.success_rate,
|
||||
'error_rate': global_stats.error_rate,
|
||||
'recent_errors_count': len(recent_errors),
|
||||
'active_chats': len(rate_limit_monitor.stats),
|
||||
'requests_per_minute': global_stats.requests_per_minute,
|
||||
'average_wait_time': global_stats.average_wait_time
|
||||
"total_requests": global_stats.total_requests,
|
||||
"success_rate": global_stats.success_rate,
|
||||
"error_rate": global_stats.error_rate,
|
||||
"recent_errors_count": len(recent_errors),
|
||||
"active_chats": len(rate_limit_monitor.stats),
|
||||
"requests_per_minute": global_stats.requests_per_minute,
|
||||
"average_wait_time": global_stats.average_wait_time,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""
|
||||
Rate limiter для предотвращения Flood control ошибок в Telegram Bot API
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
|
||||
from logs.custom_logger import logger
|
||||
|
||||
from .metrics import metrics
|
||||
@@ -15,6 +17,7 @@ from .metrics import metrics
|
||||
@dataclass
|
||||
class RateLimitConfig:
|
||||
"""Конфигурация для rate limiting"""
|
||||
|
||||
messages_per_second: float = 0.5 # Максимум 0.5 сообщений в секунду на чат
|
||||
burst_limit: int = 3 # Максимум 3 сообщения подряд
|
||||
retry_after_multiplier: float = 1.2 # Множитель для увеличения задержки при retry
|
||||
@@ -104,12 +107,7 @@ class RetryHandler:
|
||||
self.config = config
|
||||
|
||||
async def execute_with_retry(
|
||||
self,
|
||||
func: Callable,
|
||||
chat_id: int,
|
||||
*args,
|
||||
max_retries: int = 3,
|
||||
**kwargs
|
||||
self, func: Callable, chat_id: int, *args, max_retries: int = 3, **kwargs
|
||||
) -> Any:
|
||||
"""Выполняет функцию с повторными попытками при ошибках"""
|
||||
retry_count = 0
|
||||
@@ -127,7 +125,9 @@ class RetryHandler:
|
||||
retry_count += 1
|
||||
if retry_count > max_retries:
|
||||
logger.error(f"Max retries exceeded for RetryAfter: {e}")
|
||||
metrics.record_rate_limit_request(chat_id, False, total_wait_time, "RetryAfter")
|
||||
metrics.record_rate_limit_request(
|
||||
chat_id, False, total_wait_time, "RetryAfter"
|
||||
)
|
||||
raise
|
||||
|
||||
# Используем время ожидания от Telegram или наше увеличенное
|
||||
@@ -135,7 +135,9 @@ class RetryHandler:
|
||||
wait_time = min(wait_time, self.config.max_retry_delay)
|
||||
total_wait_time += wait_time
|
||||
|
||||
logger.warning(f"RetryAfter error, waiting {wait_time:.2f}s (attempt {retry_count}/{max_retries})")
|
||||
logger.warning(
|
||||
f"RetryAfter error, waiting {wait_time:.2f}s (attempt {retry_count}/{max_retries})"
|
||||
)
|
||||
await asyncio.sleep(wait_time)
|
||||
current_delay *= self.config.retry_after_multiplier
|
||||
|
||||
@@ -143,19 +145,25 @@ class RetryHandler:
|
||||
retry_count += 1
|
||||
if retry_count > max_retries:
|
||||
logger.error(f"Max retries exceeded for TelegramAPIError: {e}")
|
||||
metrics.record_rate_limit_request(chat_id, False, total_wait_time, "TelegramAPIError")
|
||||
metrics.record_rate_limit_request(
|
||||
chat_id, False, total_wait_time, "TelegramAPIError"
|
||||
)
|
||||
raise
|
||||
|
||||
wait_time = min(current_delay, self.config.max_retry_delay)
|
||||
total_wait_time += wait_time
|
||||
logger.warning(f"TelegramAPIError, waiting {wait_time:.2f}s (attempt {retry_count}/{max_retries}): {e}")
|
||||
logger.warning(
|
||||
f"TelegramAPIError, waiting {wait_time:.2f}s (attempt {retry_count}/{max_retries}): {e}"
|
||||
)
|
||||
await asyncio.sleep(wait_time)
|
||||
current_delay *= self.config.retry_after_multiplier
|
||||
|
||||
except Exception as e:
|
||||
# Для других ошибок не делаем retry
|
||||
logger.error(f"Non-retryable error: {e}")
|
||||
metrics.record_rate_limit_request(chat_id, False, total_wait_time, "Other")
|
||||
metrics.record_rate_limit_request(
|
||||
chat_id, False, total_wait_time, "Other"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -168,11 +176,7 @@ class TelegramRateLimiter:
|
||||
self.retry_handler = RetryHandler(self.config)
|
||||
|
||||
async def send_with_rate_limit(
|
||||
self,
|
||||
send_func: Callable,
|
||||
chat_id: int,
|
||||
*args,
|
||||
**kwargs
|
||||
self, send_func: Callable, chat_id: int, *args, **kwargs
|
||||
) -> Any:
|
||||
"""Отправляет сообщение с соблюдением rate limit и retry логики"""
|
||||
|
||||
@@ -184,8 +188,7 @@ class TelegramRateLimiter:
|
||||
|
||||
|
||||
# Глобальный экземпляр rate limiter
|
||||
from helper_bot.config.rate_limit_config import (RateLimitSettings,
|
||||
get_rate_limit_config)
|
||||
from helper_bot.config.rate_limit_config import RateLimitSettings, get_rate_limit_config
|
||||
|
||||
|
||||
def _create_rate_limit_config(settings: RateLimitSettings) -> RateLimitConfig:
|
||||
@@ -194,9 +197,10 @@ def _create_rate_limit_config(settings: RateLimitSettings) -> RateLimitConfig:
|
||||
messages_per_second=settings.messages_per_second,
|
||||
burst_limit=settings.burst_limit,
|
||||
retry_after_multiplier=settings.retry_after_multiplier,
|
||||
max_retry_delay=settings.max_retry_delay
|
||||
max_retry_delay=settings.max_retry_delay,
|
||||
)
|
||||
|
||||
|
||||
# Получаем конфигурацию из настроек
|
||||
_rate_limit_settings = get_rate_limit_config("production")
|
||||
_default_config = _create_rate_limit_config(_rate_limit_settings)
|
||||
@@ -204,7 +208,9 @@ _default_config = _create_rate_limit_config(_rate_limit_settings)
|
||||
telegram_rate_limiter = TelegramRateLimiter(_default_config)
|
||||
|
||||
|
||||
async def send_with_rate_limit(send_func: Callable, chat_id: int, *args, **kwargs) -> Any:
|
||||
async def send_with_rate_limit(
|
||||
send_func: Callable, chat_id: int, *args, **kwargs
|
||||
) -> Any:
|
||||
"""
|
||||
Удобная функция для отправки сообщений с rate limiting
|
||||
|
||||
@@ -216,4 +222,6 @@ async def send_with_rate_limit(send_func: Callable, chat_id: int, *args, **kwarg
|
||||
Returns:
|
||||
Результат выполнения функции отправки
|
||||
"""
|
||||
return await telegram_rate_limiter.send_with_rate_limit(send_func, chat_id, *args, **kwargs)
|
||||
return await telegram_rate_limiter.send_with_rate_limit(
|
||||
send_func, chat_id, *args, **kwargs
|
||||
)
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
"""
|
||||
Сервис для работы с S3 хранилищем.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import aioboto3
|
||||
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class S3StorageService:
|
||||
"""Сервис для работы с S3 хранилищем."""
|
||||
|
||||
def __init__(self, endpoint_url: str, access_key: str, secret_key: str,
|
||||
bucket_name: str, region: str = "us-east-1"):
|
||||
def __init__(
|
||||
self,
|
||||
endpoint_url: str,
|
||||
access_key: str,
|
||||
secret_key: str,
|
||||
bucket_name: str,
|
||||
region: str = "us-east-1",
|
||||
):
|
||||
self.endpoint_url = endpoint_url
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
@@ -22,26 +30,24 @@ class S3StorageService:
|
||||
self.region = region
|
||||
self.session = aioboto3.Session()
|
||||
|
||||
async def upload_file(self, file_path: str, s3_key: str,
|
||||
content_type: Optional[str] = None) -> bool:
|
||||
async def upload_file(
|
||||
self, file_path: str, s3_key: str, content_type: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Загружает файл в S3."""
|
||||
try:
|
||||
async with self.session.client(
|
||||
's3',
|
||||
"s3",
|
||||
endpoint_url=self.endpoint_url,
|
||||
aws_access_key_id=self.access_key,
|
||||
aws_secret_access_key=self.secret_key,
|
||||
region_name=self.region
|
||||
region_name=self.region,
|
||||
) as s3:
|
||||
extra_args = {}
|
||||
if content_type:
|
||||
extra_args['ContentType'] = content_type
|
||||
extra_args["ContentType"] = content_type
|
||||
|
||||
await s3.upload_file(
|
||||
file_path,
|
||||
self.bucket_name,
|
||||
s3_key,
|
||||
ExtraArgs=extra_args
|
||||
file_path, self.bucket_name, s3_key, ExtraArgs=extra_args
|
||||
)
|
||||
logger.info(f"Файл загружен в S3: {s3_key}")
|
||||
return True
|
||||
@@ -49,26 +55,24 @@ class S3StorageService:
|
||||
logger.error(f"Ошибка загрузки файла в S3 {s3_key}: {e}")
|
||||
return False
|
||||
|
||||
async def upload_fileobj(self, file_obj, s3_key: str,
|
||||
content_type: Optional[str] = None) -> bool:
|
||||
async def upload_fileobj(
|
||||
self, file_obj, s3_key: str, content_type: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Загружает файл из объекта в S3."""
|
||||
try:
|
||||
async with self.session.client(
|
||||
's3',
|
||||
"s3",
|
||||
endpoint_url=self.endpoint_url,
|
||||
aws_access_key_id=self.access_key,
|
||||
aws_secret_access_key=self.secret_key,
|
||||
region_name=self.region
|
||||
region_name=self.region,
|
||||
) as s3:
|
||||
extra_args = {}
|
||||
if content_type:
|
||||
extra_args['ContentType'] = content_type
|
||||
extra_args["ContentType"] = content_type
|
||||
|
||||
await s3.upload_fileobj(
|
||||
file_obj,
|
||||
self.bucket_name,
|
||||
s3_key,
|
||||
ExtraArgs=extra_args
|
||||
file_obj, self.bucket_name, s3_key, ExtraArgs=extra_args
|
||||
)
|
||||
logger.info(f"Файл загружен в S3 из объекта: {s3_key}")
|
||||
return True
|
||||
@@ -80,20 +84,16 @@ class S3StorageService:
|
||||
"""Скачивает файл из S3 на локальный диск."""
|
||||
try:
|
||||
async with self.session.client(
|
||||
's3',
|
||||
"s3",
|
||||
endpoint_url=self.endpoint_url,
|
||||
aws_access_key_id=self.access_key,
|
||||
aws_secret_access_key=self.secret_key,
|
||||
region_name=self.region
|
||||
region_name=self.region,
|
||||
) as s3:
|
||||
# Создаем директорию если её нет
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
|
||||
await s3.download_file(
|
||||
self.bucket_name,
|
||||
s3_key,
|
||||
local_path
|
||||
)
|
||||
await s3.download_file(self.bucket_name, s3_key, local_path)
|
||||
logger.info(f"Файл скачан из S3: {s3_key} -> {local_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -104,7 +104,7 @@ class S3StorageService:
|
||||
"""Скачивает файл из S3 во временный файл. Возвращает путь к временному файлу."""
|
||||
try:
|
||||
# Определяем расширение из ключа
|
||||
ext = Path(s3_key).suffix or '.bin'
|
||||
ext = Path(s3_key).suffix or ".bin"
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=ext)
|
||||
temp_path = temp_file.name
|
||||
temp_file.close()
|
||||
@@ -120,18 +120,20 @@ class S3StorageService:
|
||||
pass
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка скачивания файла из S3 во временный файл {s3_key}: {e}")
|
||||
logger.error(
|
||||
f"Ошибка скачивания файла из S3 во временный файл {s3_key}: {e}"
|
||||
)
|
||||
return None
|
||||
|
||||
async def file_exists(self, s3_key: str) -> bool:
|
||||
"""Проверяет существование файла в S3."""
|
||||
try:
|
||||
async with self.session.client(
|
||||
's3',
|
||||
"s3",
|
||||
endpoint_url=self.endpoint_url,
|
||||
aws_access_key_id=self.access_key,
|
||||
aws_secret_access_key=self.secret_key,
|
||||
region_name=self.region
|
||||
region_name=self.region,
|
||||
) as s3:
|
||||
await s3.head_object(Bucket=self.bucket_name, Key=s3_key)
|
||||
return True
|
||||
@@ -142,11 +144,11 @@ class S3StorageService:
|
||||
"""Удаляет файл из S3."""
|
||||
try:
|
||||
async with self.session.client(
|
||||
's3',
|
||||
"s3",
|
||||
endpoint_url=self.endpoint_url,
|
||||
aws_access_key_id=self.access_key,
|
||||
aws_secret_access_key=self.secret_key,
|
||||
region_name=self.region
|
||||
region_name=self.region,
|
||||
) as s3:
|
||||
await s3.delete_object(Bucket=self.bucket_name, Key=s3_key)
|
||||
logger.info(f"Файл удален из S3: {s3_key}")
|
||||
@@ -158,19 +160,31 @@ class S3StorageService:
|
||||
def generate_s3_key(self, content_type: str, file_id: str) -> str:
|
||||
"""Генерирует S3 ключ для файла. Один и тот же для всех постов с этим file_id."""
|
||||
type_folders = {
|
||||
'photo': 'photos',
|
||||
'video': 'videos',
|
||||
'audio': 'music',
|
||||
'voice': 'voice',
|
||||
'video_note': 'video_notes'
|
||||
"photo": "photos",
|
||||
"video": "videos",
|
||||
"audio": "music",
|
||||
"voice": "voice",
|
||||
"video_note": "video_notes",
|
||||
}
|
||||
|
||||
folder = type_folders.get(content_type, 'other')
|
||||
folder = type_folders.get(content_type, "other")
|
||||
# Определяем расширение из file_id или используем дефолтное
|
||||
ext = '.jpg' if content_type == 'photo' else \
|
||||
'.mp4' if content_type == 'video' else \
|
||||
'.mp3' if content_type == 'audio' else \
|
||||
'.ogg' if content_type == 'voice' else \
|
||||
'.mp4' if content_type == 'video_note' else '.bin'
|
||||
ext = (
|
||||
".jpg"
|
||||
if content_type == "photo"
|
||||
else (
|
||||
".mp4"
|
||||
if content_type == "video"
|
||||
else (
|
||||
".mp3"
|
||||
if content_type == "audio"
|
||||
else (
|
||||
".ogg"
|
||||
if content_type == "voice"
|
||||
else ".mp4" if content_type == "video_note" else ".bin"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return f"{folder}/{file_id}{ext}"
|
||||
|
||||
@@ -8,7 +8,7 @@ from loguru import logger
|
||||
logger.remove()
|
||||
|
||||
# Check if running in Docker/container
|
||||
is_container = os.path.exists('/.dockerenv') or os.getenv('DOCKER_CONTAINER') == 'true'
|
||||
is_container = os.path.exists("/.dockerenv") or os.getenv("DOCKER_CONTAINER") == "true"
|
||||
|
||||
if is_container:
|
||||
# In container: log to stdout/stderr
|
||||
@@ -16,13 +16,13 @@ if is_container:
|
||||
sys.stdout,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {line} | {message}",
|
||||
level=os.getenv("LOG_LEVEL", "INFO"),
|
||||
colorize=True
|
||||
colorize=True,
|
||||
)
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {line} | {message}",
|
||||
level="ERROR",
|
||||
colorize=True
|
||||
colorize=True,
|
||||
)
|
||||
else:
|
||||
# Local development: log to files
|
||||
@@ -30,8 +30,8 @@ else:
|
||||
if not os.path.exists(current_dir):
|
||||
os.makedirs(current_dir)
|
||||
|
||||
today = datetime.date.today().strftime('%Y-%m-%d')
|
||||
filename = f'{current_dir}/helper_bot_{today}.log'
|
||||
today = datetime.date.today().strftime("%Y-%m-%d")
|
||||
filename = f"{current_dir}/helper_bot_{today}.log"
|
||||
|
||||
logger.add(
|
||||
filename,
|
||||
@@ -42,4 +42,4 @@ else:
|
||||
)
|
||||
|
||||
# Bind logger name
|
||||
logger = logger.bind(name='main_log')
|
||||
logger = logger.bind(name="main_log")
|
||||
|
||||
@@ -4,6 +4,13 @@ version = "1.0.0"
|
||||
description = "Telegram bot with monitoring and metrics"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 88
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
|
||||
@@ -9,5 +9,6 @@ coverage>=7.0.0
|
||||
|
||||
# Development tools
|
||||
black>=23.0.0
|
||||
isort>=5.12.0
|
||||
flake8>=6.0.0
|
||||
mypy>=1.0.0
|
||||
|
||||
@@ -25,9 +25,9 @@ async def main():
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
|
||||
auto_unban_bot = Bot(
|
||||
token=bdf.settings['Telegram']['bot_token'],
|
||||
default=DefaultBotProperties(parse_mode='HTML'),
|
||||
timeout=30.0
|
||||
token=bdf.settings["Telegram"]["bot_token"],
|
||||
default=DefaultBotProperties(parse_mode="HTML"),
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
# Инициализируем планировщик автоматического разбана
|
||||
@@ -68,8 +68,8 @@ async def main():
|
||||
|
||||
# Останавливаем планировщик метрик
|
||||
try:
|
||||
from helper_bot.utils.metrics_scheduler import \
|
||||
stop_metrics_scheduler
|
||||
from helper_bot.utils.metrics_scheduler import stop_metrics_scheduler
|
||||
|
||||
stop_metrics_scheduler()
|
||||
logger.info("Планировщик метрик остановлен")
|
||||
except Exception as e:
|
||||
@@ -81,7 +81,6 @@ async def main():
|
||||
# Отменяем задачу бота
|
||||
bot_task.cancel()
|
||||
|
||||
|
||||
# Ждем завершения задачи бота и получаем результат main bot
|
||||
try:
|
||||
results = await asyncio.gather(bot_task, return_exceptions=True)
|
||||
@@ -92,7 +91,7 @@ async def main():
|
||||
logger.error(f"Ошибка при остановке задач: {e}")
|
||||
|
||||
# Закрываем сессию основного бота (если она еще не закрыта)
|
||||
if main_bot and hasattr(main_bot, 'session') and not main_bot.session.closed:
|
||||
if main_bot and hasattr(main_bot, "session") and not main_bot.session.closed:
|
||||
try:
|
||||
await main_bot.session.close()
|
||||
logger.info("Сессия основного бота корректно закрыта")
|
||||
@@ -105,27 +104,31 @@ async def main():
|
||||
await auto_unban_bot.session.close()
|
||||
logger.info("Сессия бота автоматического разбана корректно закрыта")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при закрытии сессии бота автоматического разбана: {e}")
|
||||
logger.error(
|
||||
f"Ошибка при закрытии сессии бота автоматического разбана: {e}"
|
||||
)
|
||||
|
||||
# Даем время на завершение всех aiohttp соединений
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
logger.info("Бот корректно остановлен")
|
||||
|
||||
|
||||
def init_db():
|
||||
db_path = '/app/database/tg-bot-database.db'
|
||||
schema_path = '/app/database/schema.sql'
|
||||
db_path = "/app/database/tg-bot-database.db"
|
||||
schema_path = "/app/database/schema.sql"
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print("Initializing database...")
|
||||
with open(schema_path, 'r') as f:
|
||||
with open(schema_path, "r") as f:
|
||||
schema = f.read()
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.executescript(schema)
|
||||
print("Database initialized successfully")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
init_db()
|
||||
asyncio.run(main())
|
||||
@@ -142,6 +145,8 @@ if __name__ == '__main__':
|
||||
|
||||
# Ждем завершения всех задач
|
||||
if pending:
|
||||
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||
loop.run_until_complete(
|
||||
asyncio.gather(*pending, return_exceptions=True)
|
||||
)
|
||||
|
||||
loop.close()
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"rag": {"score": 0.90, "model": "rubert-base-cased", "ts": 1706198400}
|
||||
}
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
@@ -28,7 +29,10 @@ try:
|
||||
from logs.custom_logger import logger
|
||||
except ImportError:
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DB_PATH = "database/tg-bot-database.db"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
Сканирует папку scripts/ и применяет все новые миграции, которые еще не были применены.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import importlib.util
|
||||
@@ -15,9 +16,9 @@ from typing import List, Tuple
|
||||
|
||||
# Исключаем служебные скрипты из миграций
|
||||
EXCLUDED_SCRIPTS = {
|
||||
'apply_migrations.py',
|
||||
'test_s3_connection.py',
|
||||
'voice_cleanup.py',
|
||||
"apply_migrations.py",
|
||||
"test_s3_connection.py",
|
||||
"voice_cleanup.py",
|
||||
}
|
||||
|
||||
DEFAULT_DB_PATH = "database/tg-bot-database.db"
|
||||
@@ -51,12 +52,13 @@ async def is_migration_script(script_path: Path) -> bool:
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Проверяем наличие функции main
|
||||
if hasattr(module, 'main'):
|
||||
if hasattr(module, "main"):
|
||||
import inspect
|
||||
|
||||
sig = inspect.signature(module.main)
|
||||
# Проверяем, что функция принимает db_path
|
||||
params = list(sig.parameters.keys())
|
||||
return 'db_path' in params
|
||||
return "db_path" in params
|
||||
return False
|
||||
except Exception:
|
||||
# Если не удалось проверить, считаем что это не миграция
|
||||
@@ -79,7 +81,7 @@ async def apply_migration(script_path: Path, db_path: str) -> bool:
|
||||
cwd=script_path.parent.parent,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5 минут максимум на миграцию
|
||||
timeout=300, # 5 минут максимум на миграцию
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
@@ -127,16 +129,23 @@ async def main(db_path: str, dry_run: bool = False) -> None:
|
||||
from logs.custom_logger import logger
|
||||
except ImportError:
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Импортируем MigrationRepository напрямую из файла
|
||||
migration_repo_path = project_root / "database" / "repositories" / "migration_repository.py"
|
||||
migration_repo_path = (
|
||||
project_root / "database" / "repositories" / "migration_repository.py"
|
||||
)
|
||||
if not migration_repo_path.exists():
|
||||
print(f"❌ Файл migration_repository.py не найден: {migration_repo_path}")
|
||||
sys.exit(1)
|
||||
|
||||
spec = importlib.util.spec_from_file_location("migration_repository", migration_repo_path)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"migration_repository", migration_repo_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
print("❌ Не удалось загрузить модуль migration_repository")
|
||||
sys.exit(1)
|
||||
@@ -178,7 +187,8 @@ async def main(db_path: str, dry_run: bool = False) -> None:
|
||||
|
||||
# Находим новые миграции
|
||||
new_migrations = [
|
||||
(name, path) for name, path in migration_scripts
|
||||
(name, path)
|
||||
for name, path in migration_scripts
|
||||
if name not in applied_migrations
|
||||
]
|
||||
|
||||
@@ -224,9 +234,7 @@ async def main(db_path: str, dry_run: bool = False) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Применение миграций базы данных"
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Применение миграций базы данных")
|
||||
parser.add_argument(
|
||||
"--db",
|
||||
default=os.environ.get("DATABASE_PATH", DEFAULT_DB_PATH),
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
SQLite не поддерживает DROP COLUMN напрямую (до версии 3.35.0),
|
||||
поэтому используем пересоздание таблицы.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
@@ -25,7 +26,10 @@ try:
|
||||
from logs.custom_logger import logger
|
||||
except ImportError:
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DB_PATH = "database/tg-bot-database.db"
|
||||
@@ -42,7 +46,7 @@ async def get_sqlite_version(conn: aiosqlite.Connection) -> tuple:
|
||||
"""Возвращает версию SQLite."""
|
||||
cursor = await conn.execute("SELECT sqlite_version()")
|
||||
version_str = (await cursor.fetchone())[0]
|
||||
return tuple(map(int, version_str.split('.')))
|
||||
return tuple(map(int, version_str.split(".")))
|
||||
|
||||
|
||||
async def main(db_path: str) -> None:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Скрипт для проверки подключения к S3 хранилищу.
|
||||
Читает настройки из .env файла или переменных окружения.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
@@ -14,7 +15,7 @@ sys.path.insert(0, str(project_root))
|
||||
# Загружаем .env файл
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_path = os.path.join(project_root, '.env')
|
||||
env_path = os.path.join(project_root, ".env")
|
||||
if os.path.exists(env_path):
|
||||
load_dotenv(env_path)
|
||||
|
||||
@@ -26,11 +27,12 @@ except ImportError:
|
||||
sys.exit(1)
|
||||
|
||||
# Данные для подключения из .env или переменных окружения
|
||||
S3_ACCESS_KEY = os.getenv('S3_ACCESS_KEY', 'j3tears100@gmail.com')
|
||||
S3_SECRET_KEY = os.getenv('S3_SECRET_KEY', 'wQ1-6sZEPs92sbZTSf96')
|
||||
S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', 'https://api.s3.miran.ru:443')
|
||||
S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME', 'telegram-helper-bot')
|
||||
S3_REGION = os.getenv('S3_REGION', 'us-east-1')
|
||||
S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY", "j3tears100@gmail.com")
|
||||
S3_SECRET_KEY = os.getenv("S3_SECRET_KEY", "wQ1-6sZEPs92sbZTSf96")
|
||||
S3_ENDPOINT_URL = os.getenv("S3_ENDPOINT_URL", "https://api.s3.miran.ru:443")
|
||||
S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME", "telegram-helper-bot")
|
||||
S3_REGION = os.getenv("S3_REGION", "us-east-1")
|
||||
|
||||
|
||||
async def test_s3_connection():
|
||||
"""Тестирует подключение к S3 хранилищу."""
|
||||
@@ -45,23 +47,25 @@ async def test_s3_connection():
|
||||
|
||||
try:
|
||||
async with session.client(
|
||||
's3',
|
||||
"s3",
|
||||
endpoint_url=S3_ENDPOINT_URL,
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name=S3_REGION
|
||||
region_name=S3_REGION,
|
||||
) as s3:
|
||||
# Пытаемся получить список бакетов (может не иметь прав, пропускаем если ошибка)
|
||||
print("📦 Получение списка бакетов...")
|
||||
try:
|
||||
response = await s3.list_buckets()
|
||||
buckets = response.get('Buckets', [])
|
||||
buckets = response.get("Buckets", [])
|
||||
print(f"✅ Подключение успешно! Найдено бакетов: {len(buckets)}")
|
||||
|
||||
if buckets:
|
||||
print("\n📋 Список бакетов:")
|
||||
for bucket in buckets:
|
||||
print(f" - {bucket['Name']} (создан: {bucket.get('CreationDate', 'неизвестно')})")
|
||||
print(
|
||||
f" - {bucket['Name']} (создан: {bucket.get('CreationDate', 'неизвестно')})"
|
||||
)
|
||||
else:
|
||||
print("\n⚠️ Бакеты не найдены.")
|
||||
except Exception as list_error:
|
||||
@@ -75,14 +79,16 @@ async def test_s3_connection():
|
||||
test_bucket = S3_BUCKET_NAME
|
||||
if buckets:
|
||||
# Проверяем, есть ли указанный бакет в списке
|
||||
bucket_names = [b['Name'] for b in buckets]
|
||||
bucket_names = [b["Name"] for b in buckets]
|
||||
if test_bucket not in bucket_names:
|
||||
print(f"⚠️ Бакет '{test_bucket}' не найден в списке.")
|
||||
print(f" Используем первый найденный бакет: '{buckets[0]['Name']}'")
|
||||
test_bucket = buckets[0]['Name']
|
||||
print(
|
||||
f" Используем первый найденный бакет: '{buckets[0]['Name']}'"
|
||||
)
|
||||
test_bucket = buckets[0]["Name"]
|
||||
|
||||
test_key = 'test-connection.txt'
|
||||
test_content = b'Test connection to S3 storage'
|
||||
test_key = "test-connection.txt"
|
||||
test_content = b"Test connection to S3 storage"
|
||||
|
||||
try:
|
||||
# Проверяем существование бакета
|
||||
@@ -94,17 +100,15 @@ async def test_s3_connection():
|
||||
print(" Проверьте права доступа к бакету")
|
||||
return False
|
||||
|
||||
await s3.put_object(
|
||||
Bucket=test_bucket,
|
||||
Key=test_key,
|
||||
Body=test_content
|
||||
await s3.put_object(Bucket=test_bucket, Key=test_key, Body=test_content)
|
||||
print(
|
||||
f"✅ Файл успешно записан в бакет '{test_bucket}' с ключом '{test_key}'"
|
||||
)
|
||||
print(f"✅ Файл успешно записан в бакет '{test_bucket}' с ключом '{test_key}'")
|
||||
|
||||
# Пытаемся прочитать файл
|
||||
print("🧪 Тестирование чтения файла...")
|
||||
response = await s3.get_object(Bucket=test_bucket, Key=test_key)
|
||||
content = await response['Body'].read()
|
||||
content = await response["Body"].read()
|
||||
|
||||
if content == test_content:
|
||||
print("✅ Файл успешно прочитан, содержимое совпадает")
|
||||
@@ -120,6 +124,7 @@ async def test_s3_connection():
|
||||
print(f"❌ Ошибка при тестировании записи/чтения: {e}")
|
||||
print(f" Тип ошибки: {type(e).__name__}")
|
||||
import traceback
|
||||
|
||||
print(f" Полный traceback:")
|
||||
traceback.print_exc()
|
||||
print("\nВозможные причины:")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""
|
||||
Скрипт для диагностики и очистки проблем с голосовыми файлами
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
@@ -44,22 +45,24 @@ async def main():
|
||||
|
||||
print(f"\n🗄️ База данных:")
|
||||
print(f" 📝 Записей в БД: {diagnostic_result['db_records_count']}")
|
||||
print(f" 🔍 Записей без файлов: {diagnostic_result['orphaned_db_records_count']}")
|
||||
print(
|
||||
f" 🔍 Записей без файлов: {diagnostic_result['orphaned_db_records_count']}"
|
||||
)
|
||||
print(f" 📁 Файлов без записей: {diagnostic_result['orphaned_files_count']}")
|
||||
|
||||
print(f"\n📋 Статус: {diagnostic_result['status']}")
|
||||
|
||||
if diagnostic_result['status'] == 'issues_found':
|
||||
if diagnostic_result["status"] == "issues_found":
|
||||
print("\n⚠️ Найдены проблемы!")
|
||||
|
||||
if diagnostic_result['orphaned_db_records_count'] > 0:
|
||||
if diagnostic_result["orphaned_db_records_count"] > 0:
|
||||
print(f"\n🗑️ Записи в БД без файлов (первые 10):")
|
||||
for file_name, user_id in diagnostic_result['orphaned_db_records']:
|
||||
for file_name, user_id in diagnostic_result["orphaned_db_records"]:
|
||||
print(f" - {file_name} (user_id: {user_id})")
|
||||
|
||||
if diagnostic_result['orphaned_files_count'] > 0:
|
||||
if diagnostic_result["orphaned_files_count"] > 0:
|
||||
print(f"\n📁 Файлы без записей в БД (первые 10):")
|
||||
for file_path in diagnostic_result['orphaned_files']:
|
||||
for file_path in diagnostic_result["orphaned_files"]:
|
||||
print(f" - {file_path}")
|
||||
|
||||
# Предлагаем очистку
|
||||
@@ -83,8 +86,12 @@ async def main():
|
||||
|
||||
elif choice == "3":
|
||||
print("\n🧹 Полная очистка...")
|
||||
db_deleted = await cleanup_utils.cleanup_orphaned_db_records(dry_run=False)
|
||||
files_deleted = await cleanup_utils.cleanup_orphaned_files(dry_run=False)
|
||||
db_deleted = await cleanup_utils.cleanup_orphaned_db_records(
|
||||
dry_run=False
|
||||
)
|
||||
files_deleted = await cleanup_utils.cleanup_orphaned_files(
|
||||
dry_run=False
|
||||
)
|
||||
print(f"✅ Удалено {db_deleted} записей в БД и {files_deleted} файлов")
|
||||
|
||||
elif choice == "4":
|
||||
|
||||
@@ -13,13 +13,13 @@ if str(_project_root) not in sys.path:
|
||||
import pytest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Chat, Message, User
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
# Импортируем моки в самом начале
|
||||
import tests.mocks
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
# Настройка pytest-asyncio
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
pytest_plugins = ("pytest_asyncio",)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -100,19 +100,16 @@ def mock_dispatcher():
|
||||
def test_settings():
|
||||
"""Возвращает тестовые настройки"""
|
||||
return {
|
||||
'Telegram': {
|
||||
'bot_token': 'test_token_123',
|
||||
'preview_link': False,
|
||||
'group_for_posts': '-1001234567890',
|
||||
'group_for_message': '-1001234567891',
|
||||
'main_public': '-1001234567892',
|
||||
'group_for_logs': '-1001234567893',
|
||||
'important_logs': '-1001234567894'
|
||||
"Telegram": {
|
||||
"bot_token": "test_token_123",
|
||||
"preview_link": False,
|
||||
"group_for_posts": "-1001234567890",
|
||||
"group_for_message": "-1001234567891",
|
||||
"main_public": "-1001234567892",
|
||||
"group_for_logs": "-1001234567893",
|
||||
"important_logs": "-1001234567894",
|
||||
},
|
||||
'Settings': {
|
||||
'logs': True,
|
||||
'test': False
|
||||
}
|
||||
"Settings": {"logs": True, "test": False},
|
||||
}
|
||||
|
||||
|
||||
@@ -129,71 +126,71 @@ def mock_factory(test_settings, mock_db):
|
||||
@pytest.fixture
|
||||
def sample_photo_message(mock_message):
|
||||
"""Создает сообщение с фото для тестов"""
|
||||
mock_message.content_type = 'photo'
|
||||
mock_message.caption = 'Тестовое фото'
|
||||
mock_message.content_type = "photo"
|
||||
mock_message.caption = "Тестовое фото"
|
||||
mock_message.media_group_id = None
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = 'photo_file_id'
|
||||
mock_message.photo[-1].file_id = "photo_file_id"
|
||||
return mock_message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_video_message(mock_message):
|
||||
"""Создает сообщение с видео для тестов"""
|
||||
mock_message.content_type = 'video'
|
||||
mock_message.caption = 'Тестовое видео'
|
||||
mock_message.content_type = "video"
|
||||
mock_message.caption = "Тестовое видео"
|
||||
mock_message.media_group_id = None
|
||||
mock_message.video = Mock()
|
||||
mock_message.video.file_id = 'video_file_id'
|
||||
mock_message.video.file_id = "video_file_id"
|
||||
return mock_message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_audio_message(mock_message):
|
||||
"""Создает сообщение с аудио для тестов"""
|
||||
mock_message.content_type = 'audio'
|
||||
mock_message.caption = 'Тестовое аудио'
|
||||
mock_message.content_type = "audio"
|
||||
mock_message.caption = "Тестовое аудио"
|
||||
mock_message.media_group_id = None
|
||||
mock_message.audio = Mock()
|
||||
mock_message.audio.file_id = 'audio_file_id'
|
||||
mock_message.audio.file_id = "audio_file_id"
|
||||
return mock_message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_voice_message(mock_message):
|
||||
"""Создает голосовое сообщение для тестов"""
|
||||
mock_message.content_type = 'voice'
|
||||
mock_message.content_type = "voice"
|
||||
mock_message.media_group_id = None
|
||||
mock_message.voice = Mock()
|
||||
mock_message.voice.file_id = 'voice_file_id'
|
||||
mock_message.voice.file_id = "voice_file_id"
|
||||
return mock_message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_video_note_message(mock_message):
|
||||
"""Создает видеокружок для тестов"""
|
||||
mock_message.content_type = 'video_note'
|
||||
mock_message.content_type = "video_note"
|
||||
mock_message.media_group_id = None
|
||||
mock_message.video_note = Mock()
|
||||
mock_message.video_note.file_id = 'video_note_file_id'
|
||||
mock_message.video_note.file_id = "video_note_file_id"
|
||||
return mock_message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_media_group(mock_message):
|
||||
"""Создает медиагруппу для тестов"""
|
||||
mock_message.media_group_id = 'group_123'
|
||||
mock_message.content_type = 'photo'
|
||||
mock_message.media_group_id = "group_123"
|
||||
mock_message.content_type = "photo"
|
||||
album = [mock_message]
|
||||
album[0].caption = 'Подпись к медиагруппе'
|
||||
album[0].caption = "Подпись к медиагруппе"
|
||||
return album
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_text_message(mock_message):
|
||||
"""Создает текстовое сообщение для тестов"""
|
||||
mock_message.content_type = 'text'
|
||||
mock_message.text = 'Тестовое текстовое сообщение'
|
||||
mock_message.content_type = "text"
|
||||
mock_message.text = "Тестовое текстовое сообщение"
|
||||
mock_message.media_group_id = None
|
||||
return mock_message
|
||||
|
||||
@@ -201,7 +198,7 @@ def sample_text_message(mock_message):
|
||||
@pytest.fixture
|
||||
def sample_document_message(mock_message):
|
||||
"""Создает сообщение с документом для тестов"""
|
||||
mock_message.content_type = 'document'
|
||||
mock_message.content_type = "document"
|
||||
mock_message.media_group_id = None
|
||||
return mock_message
|
||||
|
||||
@@ -209,18 +206,10 @@ def sample_document_message(mock_message):
|
||||
# Маркеры для категоризации тестов
|
||||
def pytest_configure(config):
|
||||
"""Настройка маркеров pytest"""
|
||||
config.addinivalue_line(
|
||||
"markers", "asyncio: mark test as async"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "slow: mark test as slow"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "integration: mark test as integration test"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "unit: mark test as unit test"
|
||||
)
|
||||
config.addinivalue_line("markers", "asyncio: mark test as async")
|
||||
config.addinivalue_line("markers", "slow: mark test as slow")
|
||||
config.addinivalue_line("markers", "integration: mark test as integration test")
|
||||
config.addinivalue_line("markers", "unit: mark test as unit test")
|
||||
|
||||
|
||||
# Автоматическая маркировка тестов
|
||||
|
||||
@@ -3,6 +3,7 @@ import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import UserMessage
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
|
||||
@@ -10,7 +11,7 @@ from database.repositories.message_repository import MessageRepository
|
||||
@pytest.fixture(scope="session")
|
||||
def test_db_path():
|
||||
"""Фикстура для пути к тестовой БД (сессионная область)."""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
yield temp_path
|
||||
@@ -38,20 +39,20 @@ def sample_messages():
|
||||
message_text="Первое тестовое сообщение",
|
||||
user_id=1001,
|
||||
telegram_message_id=2001,
|
||||
date=base_timestamp
|
||||
date=base_timestamp,
|
||||
),
|
||||
UserMessage(
|
||||
message_text="Второе тестовое сообщение",
|
||||
user_id=1002,
|
||||
telegram_message_id=2002,
|
||||
date=base_timestamp + 1
|
||||
date=base_timestamp + 1,
|
||||
),
|
||||
UserMessage(
|
||||
message_text="Третье тестовое сообщение",
|
||||
user_id=1003,
|
||||
telegram_message_id=2003,
|
||||
date=base_timestamp + 2
|
||||
)
|
||||
date=base_timestamp + 2,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -62,7 +63,7 @@ def message_without_date():
|
||||
message_text="Сообщение без даты",
|
||||
user_id=1004,
|
||||
telegram_message_id=2004,
|
||||
date=None
|
||||
date=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -73,7 +74,7 @@ def message_with_zero_date():
|
||||
message_text="Сообщение с нулевой датой",
|
||||
user_id=1005,
|
||||
telegram_message_id=2005,
|
||||
date=0
|
||||
date=0,
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +85,7 @@ def message_with_special_chars():
|
||||
message_text="Сообщение с 'кавычками', \"двойными кавычками\" и эмодзи 😊\nНовая строка",
|
||||
user_id=1006,
|
||||
telegram_message_id=2006,
|
||||
date=int(datetime.now().timestamp())
|
||||
date=int(datetime.now().timestamp()),
|
||||
)
|
||||
|
||||
|
||||
@@ -96,7 +97,7 @@ def long_message():
|
||||
message_text=long_text,
|
||||
user_id=1007,
|
||||
telegram_message_id=2007,
|
||||
date=int(datetime.now().timestamp())
|
||||
date=int(datetime.now().timestamp()),
|
||||
)
|
||||
|
||||
|
||||
@@ -107,7 +108,7 @@ def message_with_unicode():
|
||||
message_text="Сообщение с Unicode: 你好世界 🌍 Привет мир",
|
||||
user_id=1008,
|
||||
telegram_message_id=2008,
|
||||
date=int(datetime.now().timestamp())
|
||||
date=int(datetime.now().timestamp()),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import MessageContentLink, PostContent, TelegramPost
|
||||
from database.repositories.post_repository import PostRepository
|
||||
|
||||
@@ -37,7 +38,7 @@ def sample_telegram_post():
|
||||
text="Тестовый пост для unit тестов",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +50,7 @@ def sample_telegram_post_with_helper():
|
||||
text="Тестовый пост с helper сообщением",
|
||||
author_id=67890,
|
||||
helper_text_message_id=99999,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
)
|
||||
|
||||
|
||||
@@ -61,7 +62,7 @@ def sample_telegram_post_no_date():
|
||||
text="Тестовый пост без даты",
|
||||
author_id=67890,
|
||||
helper_text_message_id=None,
|
||||
created_at=None
|
||||
created_at=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,19 +70,14 @@ def sample_telegram_post_no_date():
|
||||
def sample_post_content():
|
||||
"""Создает тестовый объект PostContent"""
|
||||
return PostContent(
|
||||
message_id=12345,
|
||||
content_name="/path/to/test/file.jpg",
|
||||
content_type="photo"
|
||||
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
|
||||
)
|
||||
return MessageContentLink(post_id=12345, message_id=67890)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -105,7 +101,7 @@ def mock_logger():
|
||||
@pytest.fixture
|
||||
def temp_db_file():
|
||||
"""Создает временный файл БД для интеграционных тестов"""
|
||||
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as tmp_file:
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp_file:
|
||||
db_path = tmp_file.name
|
||||
|
||||
yield db_path
|
||||
@@ -132,22 +128,22 @@ def sample_posts_batch():
|
||||
text="Первый тестовый пост",
|
||||
author_id=11111,
|
||||
helper_text_message_id=None,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
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())
|
||||
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())
|
||||
)
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -159,7 +155,7 @@ def sample_content_batch():
|
||||
(10002, "/path/to/video1.mp4", "video"),
|
||||
(10003, "/path/to/audio1.mp3", "audio"),
|
||||
(10004, "/path/to/photo2.jpg", "photo"),
|
||||
(10005, "/path/to/video2.mp4", "video")
|
||||
(10005, "/path/to/video2.mp4", "video"),
|
||||
]
|
||||
|
||||
|
||||
@@ -195,19 +191,19 @@ def sample_author_ids():
|
||||
def mock_sql_queries():
|
||||
"""Создает мок для SQL запросов"""
|
||||
return {
|
||||
'create_tables': [
|
||||
"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"
|
||||
"CREATE TABLE IF NOT EXISTS message_link_to_content",
|
||||
],
|
||||
'add_post': "INSERT INTO post_from_telegram_suggest",
|
||||
'add_post_status': "status",
|
||||
'update_helper': "UPDATE post_from_telegram_suggest SET helper_text_message_id",
|
||||
'update_status': "UPDATE post_from_telegram_suggest SET status = ?",
|
||||
'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"
|
||||
"add_post": "INSERT INTO post_from_telegram_suggest",
|
||||
"add_post_status": "status",
|
||||
"update_helper": "UPDATE post_from_telegram_suggest SET helper_text_message_id",
|
||||
"update_status": "UPDATE post_from_telegram_suggest SET status = ?",
|
||||
"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",
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Моки для тестового окружения
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import Mock, patch
|
||||
@@ -11,33 +12,34 @@ def setup_test_mocks():
|
||||
"""Настройка моков для тестов"""
|
||||
# Мокаем os.getenv
|
||||
mock_env_vars = {
|
||||
'BOT_TOKEN': 'test_token_123',
|
||||
'LISTEN_BOT_TOKEN': '',
|
||||
'TEST_BOT_TOKEN': '',
|
||||
'PREVIEW_LINK': 'False',
|
||||
'MAIN_PUBLIC': '@test',
|
||||
'GROUP_FOR_POSTS': '-1001234567890',
|
||||
'GROUP_FOR_MESSAGE': '-1001234567891',
|
||||
'GROUP_FOR_LOGS': '-1001234567893',
|
||||
'IMPORTANT_LOGS': '-1001234567894',
|
||||
'TEST_GROUP': '-1001234567895',
|
||||
'LOGS': 'True',
|
||||
'TEST': 'False',
|
||||
'DATABASE_PATH': 'database/test.db'
|
||||
"BOT_TOKEN": "test_token_123",
|
||||
"LISTEN_BOT_TOKEN": "",
|
||||
"TEST_BOT_TOKEN": "",
|
||||
"PREVIEW_LINK": "False",
|
||||
"MAIN_PUBLIC": "@test",
|
||||
"GROUP_FOR_POSTS": "-1001234567890",
|
||||
"GROUP_FOR_MESSAGE": "-1001234567891",
|
||||
"GROUP_FOR_LOGS": "-1001234567893",
|
||||
"IMPORTANT_LOGS": "-1001234567894",
|
||||
"TEST_GROUP": "-1001234567895",
|
||||
"LOGS": "True",
|
||||
"TEST": "False",
|
||||
"DATABASE_PATH": "database/test.db",
|
||||
}
|
||||
|
||||
def mock_getenv(key, default=None):
|
||||
return mock_env_vars.get(key, default)
|
||||
|
||||
env_patcher = patch('os.getenv', side_effect=mock_getenv)
|
||||
env_patcher = patch("os.getenv", side_effect=mock_getenv)
|
||||
env_patcher.start()
|
||||
|
||||
# Мокаем AsyncBotDB
|
||||
mock_db = Mock()
|
||||
db_patcher = patch('helper_bot.utils.base_dependency_factory.AsyncBotDB', mock_db)
|
||||
db_patcher = patch("helper_bot.utils.base_dependency_factory.AsyncBotDB", mock_db)
|
||||
db_patcher.start()
|
||||
|
||||
return env_patcher, db_patcher
|
||||
|
||||
|
||||
# Настраиваем моки при импорте модуля
|
||||
env_patcher, db_patcher = setup_test_mocks()
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import Admin
|
||||
from database.repositories.admin_repository import AdminRepository
|
||||
|
||||
@@ -23,28 +24,25 @@ class TestAdminRepository:
|
||||
def admin_repository(self, mock_db_connection):
|
||||
"""Экземпляр AdminRepository для тестов"""
|
||||
# Патчим наследование от DatabaseConnection
|
||||
with patch.object(AdminRepository, '__init__', return_value=None):
|
||||
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._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"
|
||||
)
|
||||
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
|
||||
user_id=12345, role="admin", created_at="1705312200" # UNIX timestamp
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -64,11 +62,19 @@ class TestAdminRepository:
|
||||
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]
|
||||
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("Таблица администраторов создана")
|
||||
admin_repository.logger.info.assert_called_once_with(
|
||||
"Таблица администраторов создана"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_admin(self, admin_repository, sample_admin):
|
||||
@@ -164,7 +170,10 @@ class TestAdminRepository:
|
||||
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][0]
|
||||
== "SELECT user_id, role, created_at FROM admins WHERE user_id = ?"
|
||||
)
|
||||
assert call_args[0][1] == (user_id,)
|
||||
|
||||
# Проверяем результат
|
||||
@@ -190,9 +199,7 @@ class TestAdminRepository:
|
||||
"""Тест получения информации об администраторе без даты создания"""
|
||||
user_id = 12345
|
||||
# Мокаем результат запроса без created_at
|
||||
admin_repository._execute_query_with_result.return_value = [
|
||||
(12345, "admin")
|
||||
]
|
||||
admin_repository._execute_query_with_result.return_value = [(12345, "admin")]
|
||||
|
||||
result = await admin_repository.get_admin(user_id)
|
||||
|
||||
@@ -224,7 +231,9 @@ class TestAdminRepository:
|
||||
async def test_is_admin_error_handling(self, admin_repository):
|
||||
"""Тест обработки ошибок при проверке администратора"""
|
||||
# Мокаем ошибку при выполнении запроса
|
||||
admin_repository._execute_query_with_result.side_effect = Exception("Database error")
|
||||
admin_repository._execute_query_with_result.side_effect = Exception(
|
||||
"Database error"
|
||||
)
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await admin_repository.is_admin(12345)
|
||||
@@ -233,7 +242,9 @@ class TestAdminRepository:
|
||||
async def test_get_admin_error_handling(self, admin_repository):
|
||||
"""Тест обработки ошибок при получении информации об администраторе"""
|
||||
# Мокаем ошибку при выполнении запроса
|
||||
admin_repository._execute_query_with_result.side_effect = Exception("Database error")
|
||||
admin_repository._execute_query_with_result.side_effect = Exception(
|
||||
"Database error"
|
||||
)
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await admin_repository.get_admin(12345)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
|
||||
@@ -27,7 +28,7 @@ class TestAsyncBotDB:
|
||||
@pytest.fixture
|
||||
def async_bot_db(self, mock_factory):
|
||||
"""Экземпляр AsyncBotDB для тестов"""
|
||||
with patch('database.async_db.RepositoryFactory') as mock_factory_class:
|
||||
with patch("database.async_db.RepositoryFactory") as mock_factory_class:
|
||||
mock_factory_class.return_value = mock_factory
|
||||
db = AsyncBotDB("test.db")
|
||||
return db
|
||||
@@ -40,39 +41,57 @@ class TestAsyncBotDB:
|
||||
await async_bot_db.delete_audio_moderate_record(message_id)
|
||||
|
||||
# Проверяем, что метод вызван в репозитории
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(message_id)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(
|
||||
message_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_audio_moderate_record_with_different_message_id(self, async_bot_db, mock_factory):
|
||||
async def test_delete_audio_moderate_record_with_different_message_id(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест метода delete_audio_moderate_record с разными message_id"""
|
||||
test_cases = [123, 456, 789, 99999]
|
||||
|
||||
for message_id in test_cases:
|
||||
await async_bot_db.delete_audio_moderate_record(message_id)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_with(message_id)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_with(
|
||||
message_id
|
||||
)
|
||||
|
||||
# Проверяем, что метод вызван для каждого message_id
|
||||
assert mock_factory.audio.delete_audio_moderate_record.call_count == len(test_cases)
|
||||
assert mock_factory.audio.delete_audio_moderate_record.call_count == len(
|
||||
test_cases
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_audio_moderate_record_exception_handling(self, async_bot_db, mock_factory):
|
||||
async def test_delete_audio_moderate_record_exception_handling(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест обработки исключений в delete_audio_moderate_record"""
|
||||
message_id = 12345
|
||||
mock_factory.audio.delete_audio_moderate_record.side_effect = Exception("Database error")
|
||||
mock_factory.audio.delete_audio_moderate_record.side_effect = Exception(
|
||||
"Database error"
|
||||
)
|
||||
|
||||
# Метод должен пробросить исключение
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await async_bot_db.delete_audio_moderate_record(message_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_audio_moderate_record_integration_with_other_methods(self, async_bot_db, mock_factory):
|
||||
async def test_delete_audio_moderate_record_integration_with_other_methods(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест интеграции delete_audio_moderate_record с другими методами"""
|
||||
message_id = 12345
|
||||
user_id = 67890
|
||||
|
||||
# Мокаем другие методы
|
||||
mock_factory.audio.get_user_id_by_message_id_for_voice_bot = AsyncMock(return_value=user_id)
|
||||
mock_factory.audio.set_user_id_and_message_id_for_voice_bot = AsyncMock(return_value=True)
|
||||
mock_factory.audio.get_user_id_by_message_id_for_voice_bot = AsyncMock(
|
||||
return_value=user_id
|
||||
)
|
||||
mock_factory.audio.set_user_id_and_message_id_for_voice_bot = AsyncMock(
|
||||
return_value=True
|
||||
)
|
||||
|
||||
# Тестируем последовательность операций
|
||||
await async_bot_db.get_user_id_by_message_id_for_voice_bot(message_id)
|
||||
@@ -80,36 +99,54 @@ class TestAsyncBotDB:
|
||||
await async_bot_db.delete_audio_moderate_record(message_id)
|
||||
|
||||
# Проверяем, что все методы вызваны
|
||||
mock_factory.audio.get_user_id_by_message_id_for_voice_bot.assert_called_once_with(message_id)
|
||||
mock_factory.audio.set_user_id_and_message_id_for_voice_bot.assert_called_once_with(message_id, user_id)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(message_id)
|
||||
mock_factory.audio.get_user_id_by_message_id_for_voice_bot.assert_called_once_with(
|
||||
message_id
|
||||
)
|
||||
mock_factory.audio.set_user_id_and_message_id_for_voice_bot.assert_called_once_with(
|
||||
message_id, user_id
|
||||
)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(
|
||||
message_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_audio_moderate_record_zero_message_id(self, async_bot_db, mock_factory):
|
||||
async def test_delete_audio_moderate_record_zero_message_id(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест delete_audio_moderate_record с message_id = 0"""
|
||||
message_id = 0
|
||||
|
||||
await async_bot_db.delete_audio_moderate_record(message_id)
|
||||
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(message_id)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(
|
||||
message_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_audio_moderate_record_negative_message_id(self, async_bot_db, mock_factory):
|
||||
async def test_delete_audio_moderate_record_negative_message_id(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест delete_audio_moderate_record с отрицательным message_id"""
|
||||
message_id = -12345
|
||||
|
||||
await async_bot_db.delete_audio_moderate_record(message_id)
|
||||
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(message_id)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(
|
||||
message_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_audio_moderate_record_large_message_id(self, async_bot_db, mock_factory):
|
||||
async def test_delete_audio_moderate_record_large_message_id(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест delete_audio_moderate_record с большим message_id"""
|
||||
message_id = 999999999
|
||||
|
||||
await async_bot_db.delete_audio_moderate_record(message_id)
|
||||
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(message_id)
|
||||
mock_factory.audio.delete_audio_moderate_record.assert_called_once_with(
|
||||
message_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_user_blacklist_calls_history(self, async_bot_db, mock_factory):
|
||||
@@ -124,7 +161,7 @@ class TestAsyncBotDB:
|
||||
user_name=None,
|
||||
message_for_user=message_for_user,
|
||||
date_to_unban=date_to_unban,
|
||||
ban_author=ban_author
|
||||
ban_author=ban_author,
|
||||
)
|
||||
|
||||
# Проверяем, что сначала добавлен в blacklist
|
||||
@@ -142,17 +179,21 @@ class TestAsyncBotDB:
|
||||
assert history_call.ban_author == ban_author
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_user_blacklist_history_error_does_not_fail(self, async_bot_db, mock_factory):
|
||||
async def test_set_user_blacklist_history_error_does_not_fail(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест что ошибка записи в историю не ломает процесс бана"""
|
||||
user_id = 12345
|
||||
mock_factory.blacklist_history.add_record_on_ban.side_effect = Exception("History error")
|
||||
mock_factory.blacklist_history.add_record_on_ban.side_effect = Exception(
|
||||
"History error"
|
||||
)
|
||||
|
||||
# Бан должен пройти успешно, несмотря на ошибку в истории
|
||||
await async_bot_db.set_user_blacklist(
|
||||
user_id=user_id,
|
||||
message_for_user="Тест",
|
||||
date_to_unban=None,
|
||||
ban_author=None
|
||||
ban_author=None,
|
||||
)
|
||||
|
||||
# Проверяем, что пользователь все равно добавлен в blacklist
|
||||
@@ -162,7 +203,9 @@ class TestAsyncBotDB:
|
||||
mock_factory.blacklist_history.add_record_on_ban.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_blacklist_calls_history(self, async_bot_db, mock_factory):
|
||||
async def test_delete_user_blacklist_calls_history(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест что delete_user_blacklist вызывает обновление истории"""
|
||||
user_id = 12345
|
||||
|
||||
@@ -181,10 +224,14 @@ class TestAsyncBotDB:
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_blacklist_history_error_does_not_fail(self, async_bot_db, mock_factory):
|
||||
async def test_delete_user_blacklist_history_error_does_not_fail(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест что ошибка обновления истории не ломает процесс разбана"""
|
||||
user_id = 12345
|
||||
mock_factory.blacklist_history.set_unban_date.side_effect = Exception("History error")
|
||||
mock_factory.blacklist_history.set_unban_date.side_effect = Exception(
|
||||
"History error"
|
||||
)
|
||||
|
||||
# Разбан должен пройти успешно, несмотря на ошибку в истории
|
||||
result = await async_bot_db.delete_user_blacklist(user_id)
|
||||
@@ -199,7 +246,9 @@ class TestAsyncBotDB:
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_blacklist_returns_false_on_blacklist_error(self, async_bot_db, mock_factory):
|
||||
async def test_delete_user_blacklist_returns_false_on_blacklist_error(
|
||||
self, async_bot_db, mock_factory
|
||||
):
|
||||
"""Тест что delete_user_blacklist возвращает False при ошибке удаления из blacklist"""
|
||||
user_id = 12345
|
||||
mock_factory.blacklist.remove_user.return_value = False
|
||||
|
||||
@@ -3,8 +3,8 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch
|
||||
|
||||
import pytest
|
||||
from helper_bot.handlers.voice.exceptions import (DatabaseError,
|
||||
FileOperationError)
|
||||
|
||||
from helper_bot.handlers.voice.exceptions import DatabaseError, FileOperationError
|
||||
from helper_bot.handlers.voice.services import AudioFileService
|
||||
|
||||
|
||||
@@ -17,16 +17,19 @@ def mock_bot_db():
|
||||
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():
|
||||
"""Мок для бота"""
|
||||
@@ -35,6 +38,7 @@ def mock_bot():
|
||||
bot.download_file = AsyncMock()
|
||||
return bot
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_message():
|
||||
"""Мок для сообщения"""
|
||||
@@ -43,6 +47,7 @@ def mock_message():
|
||||
message.voice.file_id = "test_file_id"
|
||||
return message
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_file_info():
|
||||
"""Мок для информации о файле"""
|
||||
@@ -65,10 +70,14 @@ class TestGenerateFileName:
|
||||
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):
|
||||
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"
|
||||
mock_bot_db.get_path_for_audio_record.return_value = (
|
||||
"message_from_12345_number_3"
|
||||
)
|
||||
|
||||
result = await audio_service.generate_file_name(12345)
|
||||
|
||||
@@ -87,7 +96,9 @@ class TestGenerateFileName:
|
||||
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):
|
||||
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"
|
||||
@@ -97,9 +108,13 @@ class TestGenerateFileName:
|
||||
assert result == "message_from_12345_number_3"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_file_name_exception_handling(self, audio_service, mock_bot_db):
|
||||
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")
|
||||
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)
|
||||
@@ -111,17 +126,23 @@ class TestSaveAudioFile:
|
||||
"""Тесты для метода save_audio_file"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_audio_file_success(self, audio_service, mock_bot_db, sample_datetime):
|
||||
async def test_save_audio_file_success(
|
||||
self, audio_service, mock_bot_db, sample_datetime
|
||||
):
|
||||
"""Тест успешного сохранения аудио файла"""
|
||||
file_name = "test_audio"
|
||||
user_id = 12345
|
||||
file_id = "test_file_id"
|
||||
|
||||
# Мокаем verify_file_exists чтобы он возвращал True
|
||||
with patch.object(audio_service, 'verify_file_exists', return_value=True):
|
||||
await audio_service.save_audio_file(file_name, user_id, sample_datetime, file_id)
|
||||
with patch.object(audio_service, "verify_file_exists", return_value=True):
|
||||
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)
|
||||
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):
|
||||
@@ -132,20 +153,28 @@ class TestSaveAudioFile:
|
||||
file_id = "test_file_id"
|
||||
|
||||
# Мокаем verify_file_exists чтобы он возвращал True
|
||||
with patch.object(audio_service, 'verify_file_exists', return_value=True):
|
||||
await audio_service.save_audio_file(file_name, user_id, date_string, file_id)
|
||||
with patch.object(audio_service, "verify_file_exists", return_value=True):
|
||||
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)
|
||||
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):
|
||||
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")
|
||||
|
||||
# Мокаем verify_file_exists чтобы он возвращал True
|
||||
with patch.object(audio_service, 'verify_file_exists', return_value=True):
|
||||
with patch.object(audio_service, "verify_file_exists", return_value=True):
|
||||
with pytest.raises(DatabaseError) as exc_info:
|
||||
await audio_service.save_audio_file("test", 12345, sample_datetime, "file_id")
|
||||
await audio_service.save_audio_file(
|
||||
"test", 12345, sample_datetime, "file_id"
|
||||
)
|
||||
|
||||
assert "Не удалось сохранить аудио файл в БД" in str(exc_info.value)
|
||||
|
||||
@@ -154,7 +183,9 @@ 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):
|
||||
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
|
||||
|
||||
@@ -167,63 +198,92 @@ class TestDownloadAndSaveAudio:
|
||||
# Настраиваем поведение tell() для получения размера файла
|
||||
def mock_tell():
|
||||
return 0 if mock_downloaded_file.seek.call_count == 0 else 1024
|
||||
|
||||
mock_downloaded_file.tell = Mock(side_effect=mock_tell)
|
||||
|
||||
mock_bot.download_file.return_value = mock_downloaded_file
|
||||
|
||||
with patch('builtins.open', mock_open()) as mock_file:
|
||||
with patch('os.makedirs'):
|
||||
with patch('os.path.exists', return_value=True):
|
||||
with patch('os.path.getsize', return_value=1024):
|
||||
await audio_service.download_and_save_audio(mock_bot, mock_message, "test_audio")
|
||||
with patch("builtins.open", mock_open()) as mock_file:
|
||||
with patch("os.makedirs"):
|
||||
with patch("os.path.exists", return_value=True):
|
||||
with patch("os.path.getsize", return_value=1024):
|
||||
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_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")
|
||||
"""Тест скачивания когда сообщение отсутствует."""
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||||
):
|
||||
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 атрибута"""
|
||||
"""Тест скачивания когда у сообщения нет 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")
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||||
):
|
||||
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):
|
||||
"""Тест скачивания когда загрузка не удалась"""
|
||||
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")
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||||
):
|
||||
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):
|
||||
"""Тест обработки исключений при скачивании"""
|
||||
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")
|
||||
with patch(
|
||||
"helper_bot.handlers.voice.services.asyncio.sleep", new_callable=AsyncMock
|
||||
):
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
class TestAudioFileServiceIntegration:
|
||||
"""Интеграционные тесты для AudioFileService"""
|
||||
|
||||
@@ -234,7 +294,9 @@ class TestAudioFileServiceIntegration:
|
||||
|
||||
# Настраиваем моки
|
||||
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.get_path_for_audio_record.return_value = (
|
||||
"message_from_12345_number_1"
|
||||
)
|
||||
mock_bot_db.add_audio_record_simple = AsyncMock()
|
||||
|
||||
# Тестируем генерацию имени файла
|
||||
@@ -243,13 +305,15 @@ class TestAudioFileServiceIntegration:
|
||||
|
||||
# Тестируем сохранение в БД
|
||||
test_date = datetime.now()
|
||||
with patch.object(service, 'verify_file_exists', return_value=True):
|
||||
with patch.object(service, "verify_file_exists", return_value=True):
|
||||
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)
|
||||
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):
|
||||
@@ -263,16 +327,20 @@ class TestAudioFileServiceIntegration:
|
||||
|
||||
# Вторая запись
|
||||
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.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"
|
||||
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__':
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import AudioListenRecord, AudioMessage, AudioModerate
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
|
||||
@@ -23,10 +24,12 @@ class TestAudioRepository:
|
||||
def audio_repository(self, mock_db_connection):
|
||||
"""Экземпляр AudioRepository для тестов"""
|
||||
# Патчим наследование от DatabaseConnection
|
||||
with patch.object(AudioRepository, '__init__', return_value=None):
|
||||
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._execute_query_with_result = (
|
||||
mock_db_connection._execute_query_with_result
|
||||
)
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@@ -38,7 +41,7 @@ class TestAudioRepository:
|
||||
author_id=12345,
|
||||
date_added="2025-01-15 14:30:00",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
@@ -56,7 +59,9 @@ class TestAudioRepository:
|
||||
"""Тест включения внешних ключей"""
|
||||
await audio_repository.enable_foreign_keys()
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with("PRAGMA foreign_keys = ON;")
|
||||
audio_repository._execute_query.assert_called_once_with(
|
||||
"PRAGMA foreign_keys = ON;"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tables(self, audio_repository):
|
||||
@@ -73,7 +78,9 @@ class TestAudioRepository:
|
||||
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):
|
||||
async def test_add_audio_record_with_string_date(
|
||||
self, audio_repository, sample_audio_message
|
||||
):
|
||||
"""Тест добавления аудио записи со строковой датой"""
|
||||
await audio_repository.add_audio_record(sample_audio_message)
|
||||
|
||||
@@ -97,7 +104,7 @@ class TestAudioRepository:
|
||||
author_id=67890,
|
||||
date_added=datetime(2025, 1, 20, 10, 15, 0),
|
||||
file_id="test_file_id_2",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
@@ -115,7 +122,7 @@ class TestAudioRepository:
|
||||
author_id=11111,
|
||||
date_added=timestamp,
|
||||
file_id="test_file_id_3",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
@@ -127,7 +134,9 @@ class TestAudioRepository:
|
||||
@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")
|
||||
await audio_repository.add_audio_record_simple(
|
||||
"test_audio.ogg", 12345, "2025-01-15 14:30:00"
|
||||
)
|
||||
|
||||
# Проверяем, что метод вызван
|
||||
audio_repository._execute_query.assert_called_once()
|
||||
@@ -137,9 +146,13 @@ class TestAudioRepository:
|
||||
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):
|
||||
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)
|
||||
await audio_repository.add_audio_record_simple(
|
||||
"test_audio.ogg", 12345, sample_datetime
|
||||
)
|
||||
|
||||
# Проверяем, что date_added преобразован в timestamp
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
@@ -149,7 +162,9 @@ class TestAudioRepository:
|
||||
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,)]
|
||||
audio_repository._execute_query_with_result.return_value = [
|
||||
(expected_timestamp,)
|
||||
]
|
||||
|
||||
result = await audio_repository.get_last_date_audio()
|
||||
|
||||
@@ -191,7 +206,8 @@ class TestAudioRepository:
|
||||
"""
|
||||
SELECT file_name FROM audio_message_reference
|
||||
WHERE author_id = ? ORDER BY date_added DESC LIMIT 1
|
||||
""", (12345,)
|
||||
""",
|
||||
(12345,),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -209,7 +225,7 @@ class TestAudioRepository:
|
||||
# Мокаем результаты запросов
|
||||
audio_repository._execute_query_with_result.side_effect = [
|
||||
[("audio1.ogg",), ("audio2.ogg",)], # прослушанные
|
||||
[("audio1.ogg",), ("audio2.ogg",), ("audio3.ogg",)] # все аудио
|
||||
[("audio1.ogg",), ("audio2.ogg",), ("audio3.ogg",)], # все аудио
|
||||
]
|
||||
|
||||
result = await audio_repository.check_listen_audio(12345)
|
||||
@@ -225,7 +241,7 @@ class TestAudioRepository:
|
||||
|
||||
audio_repository._execute_query.assert_called_once_with(
|
||||
"INSERT OR IGNORE INTO user_audio_listens (file_name, user_id) VALUES (?, ?)",
|
||||
("test_audio.ogg", 12345)
|
||||
("test_audio.ogg", 12345),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -237,7 +253,8 @@ class TestAudioRepository:
|
||||
|
||||
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",)
|
||||
"SELECT author_id FROM audio_message_reference WHERE file_name = ?",
|
||||
("test_audio.ogg",),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -251,16 +268,19 @@ class TestAudioRepository:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name(self, audio_repository):
|
||||
"""Тест получения даты по имени файла"""
|
||||
timestamp = 1642404600 # 2022-01-17 10:30:00
|
||||
"""Тест получения даты по имени файла (UTC, без зависимости от локали)."""
|
||||
timestamp = 1642404600 # 2022-01-17 10:30:00 UTC
|
||||
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"
|
||||
expected = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
assert result == expected
|
||||
audio_repository._execute_query_with_result.assert_called_once_with(
|
||||
"SELECT date_added FROM audio_message_reference WHERE file_name = ?", ("test_audio.ogg",)
|
||||
"SELECT date_added FROM audio_message_reference WHERE file_name = ?",
|
||||
("test_audio.ogg",),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -291,22 +311,30 @@ class TestAudioRepository:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_user_id_and_message_id_for_voice_bot_success(self, audio_repository):
|
||||
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)
|
||||
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)
|
||||
(456, 123),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_user_id_and_message_id_for_voice_bot_exception(self, audio_repository):
|
||||
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)
|
||||
result = await audio_repository.set_user_id_and_message_id_for_voice_bot(
|
||||
123, 456
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
@@ -323,7 +351,9 @@ class TestAudioRepository:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_id_by_message_id_for_voice_bot_not_found(self, audio_repository):
|
||||
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 = []
|
||||
|
||||
@@ -346,7 +376,9 @@ class TestAudioRepository:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_logging(self, audio_repository, sample_audio_message):
|
||||
async def test_add_audio_record_logging(
|
||||
self, audio_repository, sample_audio_message
|
||||
):
|
||||
"""Тест логирования при добавлении аудио записи"""
|
||||
await audio_repository.add_audio_record(sample_audio_message)
|
||||
|
||||
@@ -360,7 +392,9 @@ class TestAudioRepository:
|
||||
@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")
|
||||
await audio_repository.add_audio_record_simple(
|
||||
"test_audio.ogg", 12345, "2025-01-15 14:30:00"
|
||||
)
|
||||
|
||||
# Проверяем, что лог записан
|
||||
audio_repository.logger.info.assert_called_once()
|
||||
@@ -371,17 +405,19 @@ class TestAudioRepository:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_logging(self, audio_repository):
|
||||
"""Тест логирования при получении даты по имени файла"""
|
||||
timestamp = 1642404600 # 2022-01-17 10:30:00
|
||||
"""Тест логирования при получении даты по имени файла (UTC)."""
|
||||
timestamp = 1642404600 # 2022-01-17 10:30:00 UTC
|
||||
audio_repository._execute_query_with_result.return_value = [(timestamp,)]
|
||||
|
||||
await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
# Проверяем, что лог записан
|
||||
expected = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
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 expected in log_message
|
||||
assert "test_audio.ogg" in log_message
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
|
||||
|
||||
@@ -21,10 +22,12 @@ class TestAudioRepositoryNewSchema:
|
||||
@pytest.fixture
|
||||
def audio_repository(self, mock_db_connection):
|
||||
"""Экземпляр AudioRepository для тестов"""
|
||||
with patch.object(AudioRepository, '__init__', return_value=None):
|
||||
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._execute_query_with_result = (
|
||||
mock_db_connection._execute_query_with_result
|
||||
)
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@@ -40,26 +43,41 @@ class TestAudioRepositoryNewSchema:
|
||||
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))
|
||||
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)
|
||||
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))
|
||||
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)
|
||||
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))
|
||||
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)
|
||||
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):
|
||||
@@ -71,7 +89,7 @@ class TestAudioRepositoryNewSchema:
|
||||
author_id=12345,
|
||||
date_added="2025-01-15 14:30:00",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
@@ -100,7 +118,7 @@ class TestAudioRepositoryNewSchema:
|
||||
author_id=12345,
|
||||
date_added=test_datetime,
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
@@ -123,7 +141,7 @@ class TestAudioRepositoryNewSchema:
|
||||
author_id=12345,
|
||||
date_added=test_timestamp,
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
@@ -137,7 +155,9 @@ class TestAudioRepositoryNewSchema:
|
||||
@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")
|
||||
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
|
||||
@@ -155,7 +175,9 @@ class TestAudioRepositoryNewSchema:
|
||||
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)
|
||||
await audio_repository.add_audio_record_simple(
|
||||
"test_audio.ogg", 12345, test_datetime
|
||||
)
|
||||
|
||||
# Проверяем параметры
|
||||
call_args = audio_repository._execute_query.call_args
|
||||
@@ -166,52 +188,65 @@ class TestAudioRepositoryNewSchema:
|
||||
|
||||
@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
|
||||
"""Тест преобразования UNIX timestamp в читаемую дату (UTC)."""
|
||||
test_timestamp = 1642248600 # 2022-01-15 12:10:00 UTC
|
||||
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"
|
||||
expected = datetime.fromtimestamp(test_timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
assert result == expected
|
||||
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
|
||||
"""Тест преобразования другого timestamp в читаемую дату (UTC)."""
|
||||
test_timestamp = 1705312800 # 2024-01-16 12:00:00 UTC
|
||||
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"
|
||||
expected = datetime.fromtimestamp(test_timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
@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
|
||||
"""Тест преобразования timestamp для полуночи (UTC)."""
|
||||
test_timestamp = 1705190400 # 2024-01-14 00:00:00 UTC
|
||||
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"
|
||||
expected = datetime.fromtimestamp(test_timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
@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
|
||||
"""Тест преобразования timestamp для конца года (UTC)."""
|
||||
test_timestamp = 1704067200 # 2023-12-31 00:00:00 UTC
|
||||
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"
|
||||
expected = datetime.fromtimestamp(test_timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
@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._execute_query.assert_called_once_with(
|
||||
"PRAGMA foreign_keys = ON;"
|
||||
)
|
||||
audio_repository.logger.info.assert_not_called() # Этот метод не логирует
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -220,7 +255,9 @@ class TestAudioRepositoryNewSchema:
|
||||
await audio_repository.create_tables()
|
||||
|
||||
# Проверяем, что лог записан
|
||||
audio_repository.logger.info.assert_called_once_with("Таблицы для аудио созданы")
|
||||
audio_repository.logger.info.assert_called_once_with(
|
||||
"Таблицы для аудио созданы"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_audio_record_logging_format(self, audio_repository):
|
||||
@@ -232,7 +269,7 @@ class TestAudioRepositoryNewSchema:
|
||||
author_id=12345,
|
||||
date_added="2025-01-15 14:30:00",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
await audio_repository.add_audio_record(audio_msg)
|
||||
@@ -248,7 +285,9 @@ class TestAudioRepositoryNewSchema:
|
||||
@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")
|
||||
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
|
||||
@@ -260,18 +299,20 @@ class TestAudioRepositoryNewSchema:
|
||||
|
||||
@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
|
||||
"""Тест формата лога при получении даты (UTC)."""
|
||||
test_timestamp = 1642248600 # 2022-01-15 12:10:00 UTC
|
||||
audio_repository._execute_query_with_result.return_value = [(test_timestamp,)]
|
||||
|
||||
await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
# Проверяем формат лога
|
||||
expected = datetime.fromtimestamp(test_timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
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 expected in log_message
|
||||
assert "test_audio.ogg" in log_message
|
||||
|
||||
|
||||
@@ -281,7 +322,7 @@ class TestAudioRepositoryEdgeCases:
|
||||
@pytest.fixture
|
||||
def audio_repository(self):
|
||||
"""Экземпляр AudioRepository для тестов"""
|
||||
with patch.object(AudioRepository, '__init__', return_value=None):
|
||||
with patch.object(AudioRepository, "__init__", return_value=None):
|
||||
repo = AudioRepository()
|
||||
repo._execute_query = AsyncMock()
|
||||
repo._execute_query_with_result = AsyncMock()
|
||||
@@ -298,7 +339,7 @@ class TestAudioRepositoryEdgeCases:
|
||||
author_id=12345,
|
||||
date_added="",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
# Должно вызвать ValueError при парсинге пустой строки
|
||||
@@ -315,7 +356,7 @@ class TestAudioRepositoryEdgeCases:
|
||||
author_id=12345,
|
||||
date_added="invalid_date",
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
# Должно вызвать ValueError при парсинге некорректной даты
|
||||
@@ -332,7 +373,7 @@ class TestAudioRepositoryEdgeCases:
|
||||
author_id=12345,
|
||||
date_added=None,
|
||||
file_id="test_file_id",
|
||||
listen_count=0
|
||||
listen_count=0,
|
||||
)
|
||||
|
||||
# Метод обрабатывает None как timestamp без преобразования
|
||||
@@ -355,7 +396,9 @@ class TestAudioRepositoryEdgeCases:
|
||||
"""Тест упрощенного добавления с некорректной строковой датой"""
|
||||
# Должно вызвать ValueError при парсинге некорректной даты
|
||||
with pytest.raises(ValueError):
|
||||
await audio_repository.add_audio_record_simple("test_audio.ogg", 12345, "invalid_date")
|
||||
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):
|
||||
@@ -370,28 +413,38 @@ class TestAudioRepositoryEdgeCases:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_date_by_file_name_zero_timestamp(self, audio_repository):
|
||||
"""Тест получения даты для timestamp = 0 (1970-01-01)"""
|
||||
"""Тест получения даты для timestamp = 0 (1970-01-01 UTC)."""
|
||||
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"
|
||||
expected = datetime.fromtimestamp(0, tz=timezone.utc).strftime("%d.%m.%Y %H:%M")
|
||||
assert result == expected
|
||||
|
||||
@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
|
||||
"""Тест получения даты для отрицательного timestamp (UTC)."""
|
||||
ts = -3600 # 1969-12-31 23:00:00 UTC
|
||||
audio_repository._execute_query_with_result.return_value = [(ts,)]
|
||||
|
||||
result = await audio_repository.get_date_by_file_name("test_audio.ogg")
|
||||
|
||||
assert result == "01.01.1970 02:00"
|
||||
expected = datetime.fromtimestamp(ts, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
@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())
|
||||
"""Тест получения даты для будущего timestamp (UTC, без зависимости от локали)."""
|
||||
future_timestamp = int(
|
||||
datetime(2030, 12, 31, 23, 59, 59, tzinfo=timezone.utc).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"
|
||||
expected = datetime.fromtimestamp(future_timestamp, tz=timezone.utc).strftime(
|
||||
"%d.%m.%Y %H:%M"
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.utils.auto_unban_scheduler import AutoUnbanScheduler
|
||||
|
||||
|
||||
@@ -13,7 +14,7 @@ class TestAutoUnbanIntegration:
|
||||
@pytest.fixture
|
||||
def test_db_path(self):
|
||||
"""Путь к тестовой базе данных"""
|
||||
return 'database/test_auto_unban.db'
|
||||
return "database/test_auto_unban.db"
|
||||
|
||||
@pytest.fixture
|
||||
def setup_test_db(self, test_db_path):
|
||||
@@ -30,7 +31,7 @@ class TestAutoUnbanIntegration:
|
||||
cursor.execute("PRAGMA foreign_keys = ON")
|
||||
|
||||
# Создаем таблицу our_users (нужна для внешних ключей)
|
||||
cursor.execute('''
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
@@ -44,10 +45,10 @@ class TestAutoUnbanIntegration:
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
)
|
||||
''')
|
||||
""")
|
||||
|
||||
# Создаем таблицу blacklist
|
||||
cursor.execute('''
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS blacklist (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
message_for_user TEXT,
|
||||
@@ -56,10 +57,10 @@ class TestAutoUnbanIntegration:
|
||||
ban_author INTEGER,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
""")
|
||||
|
||||
# Создаем таблицу blacklist_history
|
||||
cursor.execute('''
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS blacklist_history (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
@@ -72,66 +73,168 @@ class TestAutoUnbanIntegration:
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ban_author) REFERENCES our_users(user_id) ON DELETE SET NULL
|
||||
)
|
||||
''')
|
||||
""")
|
||||
|
||||
# Создаем индексы для blacklist_history
|
||||
cursor.execute('''
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blacklist_history_user_id ON blacklist_history(user_id)
|
||||
''')
|
||||
cursor.execute('''
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_ban ON blacklist_history(date_ban)
|
||||
''')
|
||||
cursor.execute('''
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_unban ON blacklist_history(date_unban)
|
||||
''')
|
||||
""")
|
||||
|
||||
# Добавляем тестовых пользователей в our_users
|
||||
current_time = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
users_data = [
|
||||
(123, "Test", "Test User 1", "test_user1", 0, "ru", 0, "😊", current_time, current_time, 0),
|
||||
(456, "Test", "Test User 2", "test_user2", 0, "ru", 0, "😊", current_time, current_time, 0),
|
||||
(789, "Test", "Test User 3", "test_user3", 0, "ru", 0, "😊", current_time, current_time, 0),
|
||||
(999, "Test", "Test User 4", "test_user4", 0, "ru", 0, "😊", current_time, current_time, 0),
|
||||
(
|
||||
123,
|
||||
"Test",
|
||||
"Test User 1",
|
||||
"test_user1",
|
||||
0,
|
||||
"ru",
|
||||
0,
|
||||
"😊",
|
||||
current_time,
|
||||
current_time,
|
||||
0,
|
||||
),
|
||||
(
|
||||
456,
|
||||
"Test",
|
||||
"Test User 2",
|
||||
"test_user2",
|
||||
0,
|
||||
"ru",
|
||||
0,
|
||||
"😊",
|
||||
current_time,
|
||||
current_time,
|
||||
0,
|
||||
),
|
||||
(
|
||||
789,
|
||||
"Test",
|
||||
"Test User 3",
|
||||
"test_user3",
|
||||
0,
|
||||
"ru",
|
||||
0,
|
||||
"😊",
|
||||
current_time,
|
||||
current_time,
|
||||
0,
|
||||
),
|
||||
(
|
||||
999,
|
||||
"Test",
|
||||
"Test User 4",
|
||||
"test_user4",
|
||||
0,
|
||||
"ru",
|
||||
0,
|
||||
"😊",
|
||||
current_time,
|
||||
current_time,
|
||||
0,
|
||||
),
|
||||
]
|
||||
cursor.executemany(
|
||||
"""INSERT INTO our_users (user_id, first_name, full_name, username, is_bot,
|
||||
language_code, has_stickers, emoji, date_added, date_changed, voice_bot_welcome_received)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
users_data
|
||||
users_data,
|
||||
)
|
||||
|
||||
# Добавляем тестовые данные в blacklist
|
||||
today_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
tomorrow_timestamp = int((datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).timestamp())
|
||||
tomorrow_timestamp = int(
|
||||
(datetime.now(timezone(timedelta(hours=3))) + timedelta(days=1)).timestamp()
|
||||
)
|
||||
|
||||
blacklist_data = [
|
||||
(123, "Test ban 1", today_timestamp, current_time, None), # Разблокируется сегодня
|
||||
(456, "Test ban 2", today_timestamp, current_time, None), # Разблокируется сегодня
|
||||
(789, "Test ban 3", tomorrow_timestamp, current_time, None), # Разблокируется завтра
|
||||
(999, "Test ban 4", None, current_time, None), # Навсегда заблокирован
|
||||
(
|
||||
123,
|
||||
"Test ban 1",
|
||||
today_timestamp,
|
||||
current_time,
|
||||
None,
|
||||
), # Разблокируется сегодня
|
||||
(
|
||||
456,
|
||||
"Test ban 2",
|
||||
today_timestamp,
|
||||
current_time,
|
||||
None,
|
||||
), # Разблокируется сегодня
|
||||
(
|
||||
789,
|
||||
"Test ban 3",
|
||||
tomorrow_timestamp,
|
||||
current_time,
|
||||
None,
|
||||
), # Разблокируется завтра
|
||||
(999, "Test ban 4", None, current_time, None), # Навсегда заблокирован
|
||||
]
|
||||
|
||||
cursor.executemany(
|
||||
"INSERT INTO blacklist (user_id, message_for_user, date_to_unban, created_at, ban_author) VALUES (?, ?, ?, ?, ?)",
|
||||
blacklist_data
|
||||
blacklist_data,
|
||||
)
|
||||
|
||||
# Добавляем тестовые данные в blacklist_history
|
||||
# Для пользователей 123 и 456 (которые будут разблокированы) создаем записи с date_unban = NULL
|
||||
yesterday_timestamp = int((datetime.now(timezone(timedelta(hours=3))) - timedelta(days=1)).timestamp())
|
||||
yesterday_timestamp = int(
|
||||
(datetime.now(timezone(timedelta(hours=3))) - timedelta(days=1)).timestamp()
|
||||
)
|
||||
|
||||
history_data = [
|
||||
(123, "Test ban 1", yesterday_timestamp, None, None, yesterday_timestamp, yesterday_timestamp), # Будет разблокирован
|
||||
(456, "Test ban 2", yesterday_timestamp, None, None, yesterday_timestamp, yesterday_timestamp), # Будет разблокирован
|
||||
(789, "Test ban 3", yesterday_timestamp, None, None, yesterday_timestamp, yesterday_timestamp), # Не будет разблокирован сегодня
|
||||
(999, "Test ban 4", yesterday_timestamp, None, None, yesterday_timestamp, yesterday_timestamp), # Навсегда заблокирован
|
||||
(
|
||||
123,
|
||||
"Test ban 1",
|
||||
yesterday_timestamp,
|
||||
None,
|
||||
None,
|
||||
yesterday_timestamp,
|
||||
yesterday_timestamp,
|
||||
), # Будет разблокирован
|
||||
(
|
||||
456,
|
||||
"Test ban 2",
|
||||
yesterday_timestamp,
|
||||
None,
|
||||
None,
|
||||
yesterday_timestamp,
|
||||
yesterday_timestamp,
|
||||
), # Будет разблокирован
|
||||
(
|
||||
789,
|
||||
"Test ban 3",
|
||||
yesterday_timestamp,
|
||||
None,
|
||||
None,
|
||||
yesterday_timestamp,
|
||||
yesterday_timestamp,
|
||||
), # Не будет разблокирован сегодня
|
||||
(
|
||||
999,
|
||||
"Test ban 4",
|
||||
yesterday_timestamp,
|
||||
None,
|
||||
None,
|
||||
yesterday_timestamp,
|
||||
yesterday_timestamp,
|
||||
), # Навсегда заблокирован
|
||||
]
|
||||
|
||||
cursor.executemany(
|
||||
"""INSERT INTO blacklist_history
|
||||
(user_id, message_for_user, date_ban, date_unban, ban_author, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
||||
history_data
|
||||
history_data,
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
@@ -148,9 +251,9 @@ class TestAutoUnbanIntegration:
|
||||
"""Создает мок фабрики зависимостей с тестовой базой"""
|
||||
mock_factory = Mock()
|
||||
mock_factory.settings = {
|
||||
'Telegram': {
|
||||
'group_for_logs': '-1001234567890',
|
||||
'important_logs': '-1001234567891'
|
||||
"Telegram": {
|
||||
"group_for_logs": "-1001234567890",
|
||||
"important_logs": "-1001234567891",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +261,7 @@ class TestAutoUnbanIntegration:
|
||||
import os
|
||||
|
||||
from database.async_db import AsyncBotDB
|
||||
|
||||
mock_factory.database = AsyncBotDB(test_db_path)
|
||||
|
||||
return mock_factory
|
||||
@@ -170,8 +274,10 @@ class TestAutoUnbanIntegration:
|
||||
return mock_bot
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_with_real_db(self, mock_get_instance, setup_test_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_with_real_db(
|
||||
self, mock_get_instance, setup_test_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест автоматического разбана с реальной базой данных"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
@@ -189,29 +295,39 @@ class TestAutoUnbanIntegration:
|
||||
assert initial_count == 4
|
||||
|
||||
# Проверяем начальное состояние истории: должно быть 2 записи с date_unban IS NULL для user_id 123 и 456
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist_history WHERE user_id IN (123, 456) AND date_unban IS NULL")
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM blacklist_history WHERE user_id IN (123, 456) AND date_unban IS NULL"
|
||||
)
|
||||
initial_open_history = cursor.fetchone()[0]
|
||||
assert initial_open_history == 2
|
||||
|
||||
# Запоминаем время до разбана для проверки updated_at
|
||||
before_unban_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
before_unban_timestamp = int(
|
||||
datetime.now(timezone(timedelta(hours=3))).timestamp()
|
||||
)
|
||||
|
||||
# Выполняем автоматический разбан
|
||||
await scheduler.auto_unban_users()
|
||||
|
||||
# Запоминаем время после разбана для проверки updated_at
|
||||
after_unban_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
after_unban_timestamp = int(
|
||||
datetime.now(timezone(timedelta(hours=3))).timestamp()
|
||||
)
|
||||
|
||||
# Проверяем, что пользователи с сегодняшней датой разблокированы
|
||||
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?",
|
||||
(current_timestamp,))
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?",
|
||||
(current_timestamp,),
|
||||
)
|
||||
today_count = cursor.fetchone()[0]
|
||||
assert today_count == 0
|
||||
|
||||
# Проверяем, что пользователи с завтрашней датой остались
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban > ?",
|
||||
(current_timestamp,))
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban > ?",
|
||||
(current_timestamp,),
|
||||
)
|
||||
tomorrow_count = cursor.fetchone()[0]
|
||||
assert tomorrow_count == 1
|
||||
|
||||
@@ -226,30 +342,46 @@ class TestAutoUnbanIntegration:
|
||||
assert final_count == 2 # Остались только завтрашние и навсегда заблокированные
|
||||
|
||||
# Проверяем историю банов: для user_id 123 и 456 должны быть установлены date_unban
|
||||
cursor.execute("SELECT user_id, date_unban, updated_at FROM blacklist_history WHERE user_id IN (123, 456) ORDER BY user_id")
|
||||
cursor.execute(
|
||||
"SELECT user_id, date_unban, updated_at FROM blacklist_history WHERE user_id IN (123, 456) ORDER BY user_id"
|
||||
)
|
||||
history_records = cursor.fetchall()
|
||||
|
||||
assert len(history_records) == 2
|
||||
|
||||
for user_id, date_unban, updated_at in history_records:
|
||||
# Проверяем, что date_unban установлен (не NULL)
|
||||
assert date_unban is not None, f"date_unban должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(date_unban, int), f"date_unban должен быть integer для user_id={user_id}"
|
||||
assert (
|
||||
date_unban is not None
|
||||
), f"date_unban должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(
|
||||
date_unban, int
|
||||
), f"date_unban должен быть integer для user_id={user_id}"
|
||||
|
||||
# Проверяем, что date_unban находится в разумных пределах (между before и after)
|
||||
assert before_unban_timestamp <= date_unban <= after_unban_timestamp, \
|
||||
f"date_unban для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}, получен {date_unban}"
|
||||
assert (
|
||||
before_unban_timestamp <= date_unban <= after_unban_timestamp
|
||||
), f"date_unban для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}, получен {date_unban}"
|
||||
|
||||
# Проверяем, что updated_at обновлен
|
||||
assert updated_at is not None, f"updated_at должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(updated_at, int), f"updated_at должен быть integer для user_id={user_id}"
|
||||
assert before_unban_timestamp <= updated_at <= after_unban_timestamp, \
|
||||
f"updated_at для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}, получен {updated_at}"
|
||||
assert (
|
||||
updated_at is not None
|
||||
), f"updated_at должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(
|
||||
updated_at, int
|
||||
), f"updated_at должен быть integer для user_id={user_id}"
|
||||
assert (
|
||||
before_unban_timestamp <= updated_at <= after_unban_timestamp
|
||||
), f"updated_at для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}, получен {updated_at}"
|
||||
|
||||
# Проверяем, что для user_id 789 и 999 записи в истории остались без изменений (date_unban все еще NULL)
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist_history WHERE user_id IN (789, 999) AND date_unban IS NULL")
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM blacklist_history WHERE user_id IN (789, 999) AND date_unban IS NULL"
|
||||
)
|
||||
unchanged_history = cursor.fetchone()[0]
|
||||
assert unchanged_history == 2, "Записи для user_id 789 и 999 должны остаться с date_unban = NULL"
|
||||
assert (
|
||||
unchanged_history == 2
|
||||
), "Записи для user_id 789 и 999 должны остаться с date_unban = NULL"
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -257,8 +389,10 @@ class TestAutoUnbanIntegration:
|
||||
mock_bot.send_message.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_no_users_today(self, mock_get_instance, setup_test_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_no_users_today(
|
||||
self, mock_get_instance, setup_test_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест разбана когда нет пользователей для разблокировки сегодня"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
@@ -267,10 +401,15 @@ class TestAutoUnbanIntegration:
|
||||
conn = sqlite3.connect(setup_test_db)
|
||||
cursor = conn.cursor()
|
||||
current_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
cursor.execute("DELETE FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?", (current_timestamp,))
|
||||
cursor.execute(
|
||||
"DELETE FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?",
|
||||
(current_timestamp,),
|
||||
)
|
||||
|
||||
# Проверяем начальное состояние истории: все записи должны иметь date_unban = NULL
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist_history WHERE date_unban IS NULL")
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM blacklist_history WHERE date_unban IS NULL"
|
||||
)
|
||||
initial_open_history = cursor.fetchone()[0]
|
||||
assert initial_open_history == 4 # Все 4 записи должны быть открытыми
|
||||
|
||||
@@ -288,17 +427,23 @@ class TestAutoUnbanIntegration:
|
||||
# Проверяем, что история не изменилась (все записи все еще с date_unban = NULL)
|
||||
conn = sqlite3.connect(setup_test_db)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM blacklist_history WHERE date_unban IS NULL")
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM blacklist_history WHERE date_unban IS NULL"
|
||||
)
|
||||
final_open_history = cursor.fetchone()[0]
|
||||
assert final_open_history == 4, "История не должна изменяться, если нет пользователей для разблокировки"
|
||||
assert (
|
||||
final_open_history == 4
|
||||
), "История не должна изменяться, если нет пользователей для разблокировки"
|
||||
conn.close()
|
||||
|
||||
# Проверяем, что отчет не был отправлен (нет пользователей для разблокировки)
|
||||
mock_bot.send_message.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_database_error(self, mock_get_instance, setup_test_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_database_error(
|
||||
self, mock_get_instance, setup_test_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест обработки ошибок базы данных"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
@@ -321,12 +466,14 @@ class TestAutoUnbanIntegration:
|
||||
# Проверяем, что отчет об ошибке был отправлен
|
||||
mock_bot.send_message.assert_called_once()
|
||||
call_args = mock_bot.send_message.call_args
|
||||
assert call_args[1]['chat_id'] == '-1001234567891' # important_logs
|
||||
assert "Ошибка автоматического разбана" in call_args[1]['text']
|
||||
assert call_args[1]["chat_id"] == "-1001234567891" # important_logs
|
||||
assert "Ошибка автоматического разбана" in call_args[1]["text"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_updates_history(self, mock_get_instance, setup_test_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_updates_history(
|
||||
self, mock_get_instance, setup_test_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест что автоматический разбан обновляет историю банов"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
@@ -347,13 +494,17 @@ class TestAutoUnbanIntegration:
|
||||
ORDER BY user_id
|
||||
""")
|
||||
initial_records = cursor.fetchall()
|
||||
assert len(initial_records) == 2, "Должно быть 2 открытые записи для user_id 123 и 456"
|
||||
assert (
|
||||
len(initial_records) == 2
|
||||
), "Должно быть 2 открытые записи для user_id 123 и 456"
|
||||
|
||||
# Запоминаем ID записей и их начальные значения updated_at
|
||||
record_ids = {row[0]: (row[1], row[4]) for row in initial_records}
|
||||
|
||||
# Запоминаем время до разбана
|
||||
before_unban_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
before_unban_timestamp = int(
|
||||
datetime.now(timezone(timedelta(hours=3))).timestamp()
|
||||
)
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -361,7 +512,9 @@ class TestAutoUnbanIntegration:
|
||||
await scheduler.auto_unban_users()
|
||||
|
||||
# Запоминаем время после разбана
|
||||
after_unban_timestamp = int(datetime.now(timezone(timedelta(hours=3))).timestamp())
|
||||
after_unban_timestamp = int(
|
||||
datetime.now(timezone(timedelta(hours=3))).timestamp()
|
||||
)
|
||||
|
||||
# Проверяем, что записи обновлены
|
||||
conn = sqlite3.connect(setup_test_db)
|
||||
@@ -379,26 +532,39 @@ class TestAutoUnbanIntegration:
|
||||
|
||||
for record_id, user_id, date_ban, date_unban, updated_at in updated_records:
|
||||
# Проверяем, что это одна из наших записей
|
||||
assert record_id in record_ids, f"Запись с id={record_id} должна быть в исходных записях"
|
||||
assert (
|
||||
record_id in record_ids
|
||||
), f"Запись с id={record_id} должна быть в исходных записях"
|
||||
|
||||
# Проверяем, что date_unban установлен
|
||||
assert date_unban is not None, f"date_unban должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(date_unban, int), f"date_unban должен быть integer для user_id={user_id}"
|
||||
assert (
|
||||
date_unban is not None
|
||||
), f"date_unban должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(
|
||||
date_unban, int
|
||||
), f"date_unban должен быть integer для user_id={user_id}"
|
||||
|
||||
# Проверяем, что date_unban находится в разумных пределах
|
||||
assert before_unban_timestamp <= date_unban <= after_unban_timestamp, \
|
||||
f"date_unban для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}"
|
||||
assert (
|
||||
before_unban_timestamp <= date_unban <= after_unban_timestamp
|
||||
), f"date_unban для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}"
|
||||
|
||||
# Проверяем, что updated_at обновлен (должен быть больше начального значения)
|
||||
assert updated_at is not None, f"updated_at должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(updated_at, int), f"updated_at должен быть integer для user_id={user_id}"
|
||||
assert before_unban_timestamp <= updated_at <= after_unban_timestamp, \
|
||||
f"updated_at для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}"
|
||||
assert (
|
||||
updated_at is not None
|
||||
), f"updated_at должен быть установлен для user_id={user_id}"
|
||||
assert isinstance(
|
||||
updated_at, int
|
||||
), f"updated_at должен быть integer для user_id={user_id}"
|
||||
assert (
|
||||
before_unban_timestamp <= updated_at <= after_unban_timestamp
|
||||
), f"updated_at для user_id={user_id} должен быть между {before_unban_timestamp} и {after_unban_timestamp}"
|
||||
|
||||
# Проверяем, что updated_at действительно обновлен (больше начального значения)
|
||||
initial_updated_at = record_ids[record_id][1]
|
||||
assert updated_at >= initial_updated_at, \
|
||||
f"updated_at для user_id={user_id} должен быть больше или равен начальному значению"
|
||||
assert (
|
||||
updated_at >= initial_updated_at
|
||||
), f"updated_at для user_id={user_id} должен быть больше или равен начальному значению"
|
||||
|
||||
# Проверяем, что обновлена только последняя запись для каждого пользователя
|
||||
# (если бы было несколько записей, обновилась бы только последняя)
|
||||
@@ -407,14 +573,18 @@ class TestAutoUnbanIntegration:
|
||||
WHERE user_id IN (123, 456) AND date_unban IS NOT NULL
|
||||
""")
|
||||
closed_records = cursor.fetchone()[0]
|
||||
assert closed_records == 2, "Должно быть закрыто 2 записи (по одной для каждого пользователя)"
|
||||
assert (
|
||||
closed_records == 2
|
||||
), "Должно быть закрыто 2 записи (по одной для каждого пользователя)"
|
||||
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM blacklist_history
|
||||
WHERE user_id IN (123, 456) AND date_unban IS NULL
|
||||
""")
|
||||
open_records = cursor.fetchone()[0]
|
||||
assert open_records == 0, "Не должно быть открытых записей для user_id 123 и 456"
|
||||
assert (
|
||||
open_records == 0
|
||||
), "Не должно быть открытых записей для user_id 123 и 456"
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -426,7 +596,9 @@ class TestAutoUnbanIntegration:
|
||||
# Проверяем, что дата в базе соответствует ожидаемому формату (timestamp)
|
||||
conn = sqlite3.connect(setup_test_db)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT date_to_unban FROM blacklist WHERE date_to_unban IS NOT NULL LIMIT 1")
|
||||
cursor.execute(
|
||||
"SELECT date_to_unban FROM blacklist WHERE date_to_unban IS NOT NULL LIMIT 1"
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
@@ -459,7 +631,7 @@ class TestSchedulerLifecycle:
|
||||
"""Тест создания задачи в планировщике"""
|
||||
scheduler = AutoUnbanScheduler()
|
||||
|
||||
with patch.object(scheduler.scheduler, 'add_job') as mock_add_job:
|
||||
with patch.object(scheduler.scheduler, "add_job") as mock_add_job:
|
||||
scheduler.start_scheduler()
|
||||
|
||||
# Проверяем, что задача была создана с правильными параметрами
|
||||
@@ -471,9 +643,10 @@ class TestSchedulerLifecycle:
|
||||
|
||||
# Проверяем триггер (должен быть CronTrigger)
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
assert isinstance(call_args[0][1], CronTrigger)
|
||||
|
||||
# Проверяем ID и имя задачи
|
||||
assert call_args[1]['id'] == 'auto_unban_users'
|
||||
assert call_args[1]['name'] == 'Автоматический разбан пользователей'
|
||||
assert call_args[1]['replace_existing'] is True
|
||||
assert call_args[1]["id"] == "auto_unban_users"
|
||||
assert call_args[1]["name"] == "Автоматический разбан пользователей"
|
||||
assert call_args[1]["replace_existing"] is True
|
||||
|
||||
@@ -3,8 +3,11 @@ from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from helper_bot.utils.auto_unban_scheduler import (AutoUnbanScheduler,
|
||||
get_auto_unban_scheduler)
|
||||
|
||||
from helper_bot.utils.auto_unban_scheduler import (
|
||||
AutoUnbanScheduler,
|
||||
get_auto_unban_scheduler,
|
||||
)
|
||||
|
||||
|
||||
class TestAutoUnbanScheduler:
|
||||
@@ -19,10 +22,9 @@ class TestAutoUnbanScheduler:
|
||||
def mock_bot_db(self):
|
||||
"""Создает мок базы данных"""
|
||||
mock_db = Mock()
|
||||
mock_db.get_users_for_unblock_today = AsyncMock(return_value={
|
||||
123: "test_user1",
|
||||
456: "test_user2"
|
||||
})
|
||||
mock_db.get_users_for_unblock_today = AsyncMock(
|
||||
return_value={123: "test_user1", 456: "test_user2"}
|
||||
)
|
||||
mock_db.delete_user_blacklist = AsyncMock(return_value=True)
|
||||
return mock_db
|
||||
|
||||
@@ -31,9 +33,9 @@ class TestAutoUnbanScheduler:
|
||||
"""Создает мок фабрики зависимостей"""
|
||||
mock_factory = Mock()
|
||||
mock_factory.settings = {
|
||||
'Telegram': {
|
||||
'group_for_logs': '-1001234567890',
|
||||
'important_logs': '-1001234567891'
|
||||
"Telegram": {
|
||||
"group_for_logs": "-1001234567890",
|
||||
"important_logs": "-1001234567891",
|
||||
}
|
||||
}
|
||||
return mock_factory
|
||||
@@ -57,8 +59,10 @@ class TestAutoUnbanScheduler:
|
||||
assert scheduler.bot == mock_bot
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_users_success(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_users_success(
|
||||
self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест успешного выполнения автоматического разбана"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
@@ -74,8 +78,10 @@ class TestAutoUnbanScheduler:
|
||||
mock_bot.send_message.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_users_no_users(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_users_no_users(
|
||||
self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест разбана когда нет пользователей для разблокировки"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
@@ -92,15 +98,16 @@ class TestAutoUnbanScheduler:
|
||||
mock_bot.send_message.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_users_partial_failure(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_users_partial_failure(
|
||||
self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест разбана с частичными ошибками"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(return_value={
|
||||
123: "test_user1",
|
||||
456: "test_user2"
|
||||
})
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(
|
||||
return_value={123: "test_user1", 456: "test_user2"}
|
||||
)
|
||||
# Первый вызов успешен, второй - ошибка
|
||||
mock_bot_db.delete_user_blacklist = AsyncMock(side_effect=[True, False])
|
||||
scheduler.bot_db = mock_bot_db
|
||||
@@ -114,12 +121,16 @@ class TestAutoUnbanScheduler:
|
||||
mock_bot.send_message.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_auto_unban_users_exception(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_auto_unban_users_exception(
|
||||
self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест разбана с исключением"""
|
||||
# Настройка моков
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(side_effect=Exception("Database error"))
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(
|
||||
side_effect=Exception("Database error")
|
||||
)
|
||||
scheduler.bot_db = mock_bot_db
|
||||
scheduler.set_bot(mock_bot)
|
||||
|
||||
@@ -130,7 +141,7 @@ class TestAutoUnbanScheduler:
|
||||
mock_bot.send_message.assert_called_once()
|
||||
# Проверяем, что сообщение об ошибке было отправлено
|
||||
call_args = mock_bot.send_message.call_args
|
||||
assert "Ошибка автоматического разбана" in call_args[1]['text']
|
||||
assert "Ошибка автоматического разбана" in call_args[1]["text"]
|
||||
|
||||
def test_generate_report(self, scheduler):
|
||||
"""Тест генерации отчета"""
|
||||
@@ -146,7 +157,7 @@ class TestAutoUnbanScheduler:
|
||||
assert "456 (test_user2)" in report
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_send_report(self, mock_get_instance, scheduler, mock_bdf, mock_bot):
|
||||
"""Тест отправки отчета"""
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
@@ -160,12 +171,14 @@ class TestAutoUnbanScheduler:
|
||||
|
||||
# Проверяем аргументы вызова
|
||||
call_args = mock_bot.send_message.call_args
|
||||
assert call_args[1]['text'] == report
|
||||
assert call_args[1]['parse_mode'] == 'HTML'
|
||||
assert call_args[1]["text"] == report
|
||||
assert call_args[1]["parse_mode"] == "HTML"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_send_error_report(self, mock_get_instance, scheduler, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_send_error_report(
|
||||
self, mock_get_instance, scheduler, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест отправки отчета об ошибке"""
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
scheduler.set_bot(mock_bot)
|
||||
@@ -178,14 +191,16 @@ class TestAutoUnbanScheduler:
|
||||
|
||||
# Проверяем аргументы вызова
|
||||
call_args = mock_bot.send_message.call_args
|
||||
assert "Ошибка автоматического разбана" in call_args[1]['text']
|
||||
assert error_msg in call_args[1]['text']
|
||||
assert call_args[1]['parse_mode'] == 'HTML'
|
||||
assert "Ошибка автоматического разбана" in call_args[1]["text"]
|
||||
assert error_msg in call_args[1]["text"]
|
||||
assert call_args[1]["parse_mode"] == "HTML"
|
||||
|
||||
def test_start_scheduler(self, scheduler):
|
||||
"""Тест запуска планировщика"""
|
||||
with patch.object(scheduler.scheduler, 'add_job') as mock_add_job, \
|
||||
patch.object(scheduler.scheduler, 'start') as mock_start:
|
||||
with (
|
||||
patch.object(scheduler.scheduler, "add_job") as mock_add_job,
|
||||
patch.object(scheduler.scheduler, "start") as mock_start,
|
||||
):
|
||||
|
||||
scheduler.start_scheduler()
|
||||
|
||||
@@ -207,8 +222,10 @@ class TestAutoUnbanScheduler:
|
||||
# APScheduler может не сразу остановиться, но это нормально
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
async def test_run_manual_unban(self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot):
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_run_manual_unban(
|
||||
self, mock_get_instance, scheduler, mock_bot_db, mock_bdf, mock_bot
|
||||
):
|
||||
"""Тест ручного запуска разбана"""
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
mock_bot_db.get_users_for_unblock_today.return_value = {}
|
||||
@@ -245,7 +262,7 @@ class TestDateHandling:
|
||||
today = datetime.now(moscow_tz).strftime("%Y-%m-%d")
|
||||
|
||||
assert len(today) == 10 # YYYY-MM-DD
|
||||
assert today.count('-') == 2
|
||||
assert today.count("-") == 2
|
||||
assert today[:4].isdigit() # Год
|
||||
assert today[5:7].isdigit() # Месяц
|
||||
assert today[8:10].isdigit() # День
|
||||
@@ -255,21 +272,23 @@ class TestDateHandling:
|
||||
class TestAsyncOperations:
|
||||
"""Тесты асинхронных операций"""
|
||||
|
||||
@patch('helper_bot.utils.auto_unban_scheduler.get_global_instance')
|
||||
@patch("helper_bot.utils.auto_unban_scheduler.get_global_instance")
|
||||
async def test_async_auto_unban_flow(self, mock_get_instance):
|
||||
"""Тест полного асинхронного потока разбана"""
|
||||
# Создаем моки
|
||||
mock_bdf = Mock()
|
||||
mock_bdf.settings = {
|
||||
'Telegram': {
|
||||
'group_for_logs': '-1001234567890',
|
||||
'important_logs': '-1001234567891'
|
||||
"Telegram": {
|
||||
"group_for_logs": "-1001234567890",
|
||||
"important_logs": "-1001234567891",
|
||||
}
|
||||
}
|
||||
mock_get_instance.return_value = mock_bdf
|
||||
|
||||
mock_bot_db = Mock()
|
||||
mock_bot_db.get_users_for_unblock_today = AsyncMock(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 = AsyncMock(return_value=True)
|
||||
|
||||
mock_bot = Mock()
|
||||
|
||||
@@ -3,9 +3,11 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import BlacklistHistoryRecord
|
||||
from database.repositories.blacklist_history_repository import \
|
||||
BlacklistHistoryRepository
|
||||
from database.repositories.blacklist_history_repository import (
|
||||
BlacklistHistoryRepository,
|
||||
)
|
||||
|
||||
|
||||
class TestBlacklistHistoryRepository:
|
||||
@@ -24,10 +26,12 @@ class TestBlacklistHistoryRepository:
|
||||
def blacklist_history_repository(self, mock_db_connection):
|
||||
"""Экземпляр BlacklistHistoryRepository для тестов"""
|
||||
# Патчим наследование от DatabaseConnection
|
||||
with patch.object(BlacklistHistoryRepository, '__init__', return_value=None):
|
||||
with patch.object(BlacklistHistoryRepository, "__init__", return_value=None):
|
||||
repo = BlacklistHistoryRepository()
|
||||
repo._execute_query = mock_db_connection._execute_query
|
||||
repo._execute_query_with_result = mock_db_connection._execute_query_with_result
|
||||
repo._execute_query_with_result = (
|
||||
mock_db_connection._execute_query_with_result
|
||||
)
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@@ -71,16 +75,30 @@ class TestBlacklistHistoryRepository:
|
||||
# Проверяем, что создается таблица с правильной структурой
|
||||
create_table_call = calls[0]
|
||||
assert "CREATE TABLE IF NOT EXISTS blacklist_history" in create_table_call[0][0]
|
||||
assert "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT" in create_table_call[0][0]
|
||||
assert (
|
||||
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT" in create_table_call[0][0]
|
||||
)
|
||||
assert "user_id INTEGER NOT NULL" in create_table_call[0][0]
|
||||
assert "message_for_user TEXT" in create_table_call[0][0]
|
||||
assert "date_ban INTEGER NOT NULL" in create_table_call[0][0]
|
||||
assert "date_unban INTEGER" in create_table_call[0][0]
|
||||
assert "ban_author INTEGER" in create_table_call[0][0]
|
||||
assert "created_at INTEGER DEFAULT (strftime('%s', 'now'))" in create_table_call[0][0]
|
||||
assert "updated_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]
|
||||
assert "FOREIGN KEY (ban_author) REFERENCES our_users(user_id) ON DELETE SET NULL" in create_table_call[0][0]
|
||||
assert (
|
||||
"created_at INTEGER DEFAULT (strftime('%s', 'now'))"
|
||||
in create_table_call[0][0]
|
||||
)
|
||||
assert (
|
||||
"updated_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]
|
||||
)
|
||||
assert (
|
||||
"FOREIGN KEY (ban_author) REFERENCES our_users(user_id) ON DELETE SET NULL"
|
||||
in create_table_call[0][0]
|
||||
)
|
||||
|
||||
# Проверяем создание индексов
|
||||
index_calls = calls[1:4]
|
||||
@@ -95,7 +113,9 @@ class TestBlacklistHistoryRepository:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_record_on_ban(self, blacklist_history_repository, sample_history_record):
|
||||
async def test_add_record_on_ban(
|
||||
self, blacklist_history_repository, sample_history_record
|
||||
):
|
||||
"""Тест добавления записи о бане в историю"""
|
||||
await blacklist_history_repository.add_record_on_ban(sample_history_record)
|
||||
|
||||
@@ -104,9 +124,12 @@ class TestBlacklistHistoryRepository:
|
||||
call_args = blacklist_history_repository._execute_query.call_args
|
||||
|
||||
# Проверяем SQL запрос
|
||||
sql_query = call_args[0][0].replace('\n', ' ').replace(' ', ' ').strip()
|
||||
sql_query = call_args[0][0].replace("\n", " ").replace(" ", " ").strip()
|
||||
assert "INSERT INTO blacklist_history" in sql_query
|
||||
assert "user_id, message_for_user, date_ban, date_unban, ban_author, created_at, updated_at" in sql_query
|
||||
assert (
|
||||
"user_id, message_for_user, date_ban, date_unban, ban_author, created_at, updated_at"
|
||||
in sql_query
|
||||
)
|
||||
|
||||
# Проверяем параметры
|
||||
params = call_args[0][1]
|
||||
@@ -157,7 +180,9 @@ class TestBlacklistHistoryRepository:
|
||||
date_unban = int(time.time())
|
||||
|
||||
# Мокируем результат проверки - находим открытую запись
|
||||
blacklist_history_repository._execute_query_with_result.return_value = [(100,)] # id записи
|
||||
blacklist_history_repository._execute_query_with_result.return_value = [
|
||||
(100,)
|
||||
] # id записи
|
||||
|
||||
result = await blacklist_history_repository.set_unban_date(user_id, date_unban)
|
||||
|
||||
@@ -170,7 +195,9 @@ class TestBlacklistHistoryRepository:
|
||||
# Проверяем, что затем обновляется запись
|
||||
assert blacklist_history_repository._execute_query.call_count == 1
|
||||
update_call = blacklist_history_repository._execute_query.call_args
|
||||
update_query = update_call[0][0].replace('\n', ' ').replace(' ', ' ').strip()
|
||||
update_query = (
|
||||
update_call[0][0].replace("\n", " ").replace(" ", " ").strip()
|
||||
)
|
||||
assert "UPDATE blacklist_history" in update_query
|
||||
assert "SET date_unban = ?" in update_query
|
||||
assert "updated_at = ?" in update_query
|
||||
@@ -224,7 +251,9 @@ class TestBlacklistHistoryRepository:
|
||||
date_unban = int(time.time())
|
||||
|
||||
# Мокируем исключение при проверке
|
||||
blacklist_history_repository._execute_query_with_result.side_effect = Exception("Database error")
|
||||
blacklist_history_repository._execute_query_with_result.side_effect = Exception(
|
||||
"Database error"
|
||||
)
|
||||
|
||||
result = await blacklist_history_repository.set_unban_date(user_id, date_unban)
|
||||
|
||||
@@ -245,7 +274,9 @@ class TestBlacklistHistoryRepository:
|
||||
|
||||
# Мокируем успешную проверку, но ошибку при обновлении
|
||||
blacklist_history_repository._execute_query_with_result.return_value = [(100,)]
|
||||
blacklist_history_repository._execute_query.side_effect = Exception("Update error")
|
||||
blacklist_history_repository._execute_query.side_effect = Exception(
|
||||
"Update error"
|
||||
)
|
||||
|
||||
result = await blacklist_history_repository.set_unban_date(user_id, date_unban)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import BlacklistUser
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
|
||||
@@ -23,10 +24,12 @@ class TestBlacklistRepository:
|
||||
def blacklist_repository(self, mock_db_connection):
|
||||
"""Экземпляр BlacklistRepository для тестов"""
|
||||
# Патчим наследование от DatabaseConnection
|
||||
with patch.object(BlacklistRepository, '__init__', return_value=None):
|
||||
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._execute_query_with_result = (
|
||||
mock_db_connection._execute_query_with_result
|
||||
)
|
||||
repo.logger = mock_db_connection.logger
|
||||
return repo
|
||||
|
||||
@@ -67,11 +70,19 @@ class TestBlacklistRepository:
|
||||
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]
|
||||
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("Таблица черного списка создана")
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
"Таблица черного списка создана"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_user(self, blacklist_repository, sample_blacklist_user):
|
||||
@@ -83,12 +94,23 @@ class TestBlacklistRepository:
|
||||
call_args = blacklist_repository._execute_query.call_args
|
||||
|
||||
# Проверяем SQL запрос (учитываем форматирование)
|
||||
sql_query = call_args[0][0].replace('\n', ' ').replace(' ', ' ').replace(' ', ' ').strip()
|
||||
sql_query = (
|
||||
call_args[0][0]
|
||||
.replace("\n", " ")
|
||||
.replace(" ", " ")
|
||||
.replace(" ", " ")
|
||||
.strip()
|
||||
)
|
||||
expected_sql = "INSERT INTO blacklist (user_id, message_for_user, date_to_unban, ban_author) VALUES (?, ?, ?, ?)"
|
||||
assert sql_query == expected_sql
|
||||
|
||||
# Проверяем параметры
|
||||
assert call_args[0][1] == (12345, "Нарушение правил", sample_blacklist_user.date_to_unban, 999)
|
||||
assert call_args[0][1] == (
|
||||
12345,
|
||||
"Нарушение правил",
|
||||
sample_blacklist_user.date_to_unban,
|
||||
999,
|
||||
)
|
||||
|
||||
# Проверяем логирование
|
||||
blacklist_repository.logger.info.assert_called_once_with(
|
||||
@@ -96,7 +118,9 @@ class TestBlacklistRepository:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_user_permanent_ban(self, blacklist_repository, sample_blacklist_user_permanent):
|
||||
async def test_add_user_permanent_ban(
|
||||
self, blacklist_repository, sample_blacklist_user_permanent
|
||||
):
|
||||
"""Тест добавления пользователя с постоянным баном"""
|
||||
await blacklist_repository.add_user(sample_blacklist_user_permanent)
|
||||
|
||||
@@ -184,7 +208,13 @@ class TestBlacklistRepository:
|
||||
async def test_get_user_success(self, blacklist_repository):
|
||||
"""Тест успешного получения пользователя по ID"""
|
||||
# Симулируем результат запроса
|
||||
mock_row = (12345, "Нарушение правил", int(time.time()) + 86400, int(time.time()), 111)
|
||||
mock_row = (
|
||||
12345,
|
||||
"Нарушение правил",
|
||||
int(time.time()) + 86400,
|
||||
int(time.time()),
|
||||
111,
|
||||
)
|
||||
blacklist_repository._execute_query_with_result.return_value = [mock_row]
|
||||
|
||||
result = await blacklist_repository.get_user(12345)
|
||||
@@ -201,7 +231,10 @@ class TestBlacklistRepository:
|
||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
assert "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author" in call_args[0][0]
|
||||
assert (
|
||||
"SELECT user_id, message_for_user, date_to_unban, created_at, ban_author"
|
||||
in call_args[0][0]
|
||||
)
|
||||
assert call_args[0][1] == (12345,)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -221,7 +254,7 @@ class TestBlacklistRepository:
|
||||
# Симулируем результат запроса
|
||||
mock_rows = [
|
||||
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time())),
|
||||
(67890, "Постоянный бан", None, int(time.time()) - 86400)
|
||||
(67890, "Постоянный бан", None, int(time.time()) - 86400),
|
||||
]
|
||||
blacklist_repository._execute_query_with_result.return_value = mock_rows
|
||||
|
||||
@@ -240,7 +273,7 @@ class TestBlacklistRepository:
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
||||
actual_query = ' '.join(call_args[0][0].split())
|
||||
actual_query = " ".join(call_args[0][0].split())
|
||||
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist LIMIT ?, ?"
|
||||
assert actual_query == expected_query
|
||||
assert call_args[0][1] == (0, 10)
|
||||
@@ -255,8 +288,14 @@ class TestBlacklistRepository:
|
||||
"""Тест получения всех пользователей без лимитов"""
|
||||
# Симулируем результат запроса (теперь включает ban_author)
|
||||
mock_rows = [
|
||||
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time()), 999),
|
||||
(67890, "Постоянный бан", None, int(time.time()) - 86400, None)
|
||||
(
|
||||
12345,
|
||||
"Нарушение правил",
|
||||
int(time.time()) + 86400,
|
||||
int(time.time()),
|
||||
999,
|
||||
),
|
||||
(67890, "Постоянный бан", None, int(time.time()) - 86400, None),
|
||||
]
|
||||
blacklist_repository._execute_query_with_result.return_value = mock_rows
|
||||
|
||||
@@ -270,7 +309,7 @@ class TestBlacklistRepository:
|
||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||
|
||||
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
||||
actual_query = ' '.join(call_args[0][0].split())
|
||||
actual_query = " ".join(call_args[0][0].split())
|
||||
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist"
|
||||
assert actual_query == expected_query
|
||||
# Проверяем, что параметры пустые (без лимитов)
|
||||
@@ -290,7 +329,9 @@ class TestBlacklistRepository:
|
||||
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)
|
||||
result = await blacklist_repository.get_users_for_unblock_today(
|
||||
current_timestamp
|
||||
)
|
||||
|
||||
# Проверяем, что возвращается правильный словарь
|
||||
assert len(result) == 2
|
||||
@@ -303,7 +344,10 @@ class TestBlacklistRepository:
|
||||
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][0]
|
||||
== "SELECT user_id FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?"
|
||||
)
|
||||
assert call_args[0][1] == (current_timestamp,)
|
||||
|
||||
# Проверяем логирование
|
||||
@@ -319,7 +363,9 @@ class TestBlacklistRepository:
|
||||
# Симулируем пустой результат запроса
|
||||
blacklist_repository._execute_query_with_result.return_value = []
|
||||
|
||||
result = await blacklist_repository.get_users_for_unblock_today(current_timestamp)
|
||||
result = await blacklist_repository.get_users_for_unblock_today(
|
||||
current_timestamp
|
||||
)
|
||||
|
||||
# Проверяем, что возвращается пустой словарь
|
||||
assert result == {}
|
||||
@@ -374,7 +420,9 @@ class TestBlacklistRepository:
|
||||
async def test_error_handling_in_get_user(self, blacklist_repository):
|
||||
"""Тест обработки ошибок при получении пользователя"""
|
||||
# Симулируем ошибку базы данных
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception(
|
||||
"Database connection failed"
|
||||
)
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
@@ -386,7 +434,9 @@ class TestBlacklistRepository:
|
||||
async def test_error_handling_in_get_all_users(self, blacklist_repository):
|
||||
"""Тест обработки ошибок при получении всех пользователей"""
|
||||
# Симулируем ошибку базы данных
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception(
|
||||
"Database connection failed"
|
||||
)
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
@@ -398,7 +448,9 @@ class TestBlacklistRepository:
|
||||
async def test_error_handling_in_get_count(self, blacklist_repository):
|
||||
"""Тест обработки ошибок при получении количества"""
|
||||
# Симулируем ошибку базы данных
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception("Database connection failed")
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception(
|
||||
"Database connection failed"
|
||||
)
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
@@ -407,10 +459,14 @@ class TestBlacklistRepository:
|
||||
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):
|
||||
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")
|
||||
blacklist_repository._execute_query_with_result.side_effect = Exception(
|
||||
"Database connection failed"
|
||||
)
|
||||
|
||||
# Проверяем, что исключение пробрасывается
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
|
||||
@@ -3,13 +3,16 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from helper_bot.handlers.callback.callback_handlers import (
|
||||
change_page,
|
||||
delete_voice_message,
|
||||
process_ban_user,
|
||||
process_unlock_user,
|
||||
return_to_main_menu,
|
||||
|
||||
save_voice_message,
|
||||
,
|
||||
)
|
||||
from helper_bot.handlers.voice.constants import CALLBACK_DELETE, CALLBACK_SAVE
|
||||
|
||||
@@ -27,6 +30,7 @@ def mock_call():
|
||||
call.answer = AsyncMock()
|
||||
return call
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_bot_db():
|
||||
"""Мок для базы данных"""
|
||||
@@ -35,20 +39,20 @@ def mock_bot_db():
|
||||
mock_db.delete_audio_moderate_record = AsyncMock()
|
||||
return mock_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings():
|
||||
"""Мок для настроек"""
|
||||
return {
|
||||
'Telegram': {
|
||||
'group_for_posts': 'test_group_id'
|
||||
}
|
||||
}
|
||||
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.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
|
||||
@@ -58,15 +62,23 @@ 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):
|
||||
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:
|
||||
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)
|
||||
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_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(
|
||||
@@ -75,23 +87,28 @@ class TestSaveVoiceMessage:
|
||||
|
||||
# Проверяем удаление сообщения из чата
|
||||
mock_call.bot.delete_message.assert_called_once_with(
|
||||
chat_id='test_group_id',
|
||||
message_id=12345
|
||||
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)
|
||||
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):
|
||||
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:
|
||||
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)
|
||||
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
|
||||
@@ -101,97 +118,146 @@ class TestSaveVoiceMessage:
|
||||
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):
|
||||
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")
|
||||
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)
|
||||
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):
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
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")
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
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
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
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")
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
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:
|
||||
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.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)
|
||||
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
|
||||
@@ -203,9 +269,13 @@ class TestCallbackHandlersIntegration:
|
||||
assert mock_call.answer.called
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_voice_message_full_workflow(self, mock_call, mock_bot_db, mock_settings):
|
||||
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)
|
||||
await delete_voice_message(
|
||||
mock_call, bot_db=mock_bot_db, settings=mock_settings
|
||||
)
|
||||
|
||||
# Проверяем последовательность вызовов
|
||||
assert mock_call.bot.delete_message.called
|
||||
@@ -213,32 +283,44 @@ class TestCallbackHandlersIntegration:
|
||||
assert mock_call.answer.called
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_audio_moderate_cleanup_consistency(self, mock_call, mock_bot_db, mock_settings):
|
||||
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.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:
|
||||
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.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)
|
||||
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)
|
||||
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
|
||||
|
||||
# Проверяем, что в обоих случаях вызывается очистка
|
||||
@@ -250,7 +332,9 @@ class TestCallbackHandlersEdgeCases:
|
||||
"""Тесты граничных случаев для callback handlers"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_voice_message_no_voice_attribute(self, mock_bot_db, mock_settings):
|
||||
async def test_save_voice_message_no_voice_attribute(
|
||||
self, mock_bot_db, mock_settings
|
||||
):
|
||||
"""Тест сохранения когда у сообщения нет voice атрибута"""
|
||||
call = Mock()
|
||||
call.message = Mock()
|
||||
@@ -260,25 +344,35 @@ class TestCallbackHandlersEdgeCases:
|
||||
call.bot.delete_message = AsyncMock()
|
||||
call.answer = AsyncMock()
|
||||
|
||||
with patch('helper_bot.handlers.callback.callback_handlers.AudioFileService'):
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
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)
|
||||
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):
|
||||
async def test_delete_voice_message_with_different_message_id(
|
||||
self, mock_bot_db, mock_settings
|
||||
):
|
||||
"""Тест удаления с другим message_id"""
|
||||
call = Mock()
|
||||
call.message = Mock()
|
||||
@@ -291,8 +385,7 @@ class TestCallbackHandlersEdgeCases:
|
||||
|
||||
# Проверяем, что используется правильный message_id
|
||||
call.bot.delete_message.assert_called_once_with(
|
||||
chat_id='test_group_id',
|
||||
message_id=99999
|
||||
chat_id="test_group_id", message_id=99999
|
||||
)
|
||||
mock_bot_db.delete_audio_moderate_record.assert_called_once_with(99999)
|
||||
|
||||
@@ -536,5 +629,5 @@ class TestProcessUnlockUser:
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
||||
@@ -8,9 +8,13 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram import types
|
||||
|
||||
from helper_bot.utils.helper_func import (
|
||||
add_in_db_media, add_in_db_media_mediagroup, download_file,
|
||||
send_media_group_message_to_private_chat)
|
||||
add_in_db_media,
|
||||
add_in_db_media_mediagroup,
|
||||
download_file,
|
||||
send_media_group_message_to_private_chat,
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadFile:
|
||||
@@ -21,38 +25,48 @@ class TestDownloadFile:
|
||||
"""Тест успешного скачивания фото"""
|
||||
# Создаем временную директорию
|
||||
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')):
|
||||
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_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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
result = await download_file(mock_message, "test_file_id", "photo")
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -62,7 +76,7 @@ class TestDownloadFile:
|
||||
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')
|
||||
result = await download_file(mock_message, "test_file_id", "photo")
|
||||
assert result is None
|
||||
|
||||
|
||||
@@ -76,7 +90,7 @@ class TestAddInDbMedia:
|
||||
mock_message = Mock()
|
||||
mock_message.message_id = 123
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = 'photo_123'
|
||||
mock_message.photo[-1].file_id = "photo_123"
|
||||
mock_message.video = None
|
||||
mock_message.voice = None
|
||||
mock_message.audio = None
|
||||
@@ -86,11 +100,16 @@ class TestAddInDbMedia:
|
||||
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'):
|
||||
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')
|
||||
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):
|
||||
@@ -98,7 +117,7 @@ class TestAddInDbMedia:
|
||||
mock_message = Mock()
|
||||
mock_message.message_id = 123
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = 'photo_123'
|
||||
mock_message.photo[-1].file_id = "photo_123"
|
||||
mock_message.video = None
|
||||
mock_message.voice = None
|
||||
mock_message.audio = None
|
||||
@@ -106,7 +125,7 @@ class TestAddInDbMedia:
|
||||
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.download_file', return_value=None):
|
||||
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
|
||||
@@ -118,7 +137,7 @@ class TestAddInDbMedia:
|
||||
mock_message = Mock()
|
||||
mock_message.message_id = 123
|
||||
mock_message.photo = [Mock()]
|
||||
mock_message.photo[-1].file_id = 'photo_123'
|
||||
mock_message.photo[-1].file_id = "photo_123"
|
||||
mock_message.video = None
|
||||
mock_message.voice = None
|
||||
mock_message.audio = None
|
||||
@@ -127,8 +146,13 @@ class TestAddInDbMedia:
|
||||
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'):
|
||||
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)
|
||||
|
||||
@@ -164,7 +188,7 @@ class TestAddInDbMediaMediagroup:
|
||||
mock_message1 = Mock()
|
||||
mock_message1.message_id = 1
|
||||
mock_message1.photo = [Mock()]
|
||||
mock_message1.photo[-1].file_id = 'photo_1'
|
||||
mock_message1.photo[-1].file_id = "photo_1"
|
||||
mock_message1.video = None
|
||||
mock_message1.voice = None
|
||||
mock_message1.audio = None
|
||||
@@ -174,7 +198,7 @@ class TestAddInDbMediaMediagroup:
|
||||
mock_message2.message_id = 2
|
||||
mock_message2.photo = None
|
||||
mock_message2.video = Mock()
|
||||
mock_message2.video.file_id = 'video_1'
|
||||
mock_message2.video.file_id = "video_1"
|
||||
mock_message2.voice = None
|
||||
mock_message2.audio = None
|
||||
mock_message2.video_note = None
|
||||
@@ -185,8 +209,12 @@ class TestAddInDbMediaMediagroup:
|
||||
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)
|
||||
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
|
||||
@@ -208,7 +236,7 @@ class TestAddInDbMediaMediagroup:
|
||||
mock_message1 = Mock()
|
||||
mock_message1.message_id = 1
|
||||
mock_message1.photo = [Mock()]
|
||||
mock_message1.photo[-1].file_id = 'photo_1'
|
||||
mock_message1.photo[-1].file_id = "photo_1"
|
||||
mock_message1.video = None
|
||||
mock_message1.voice = None
|
||||
mock_message1.audio = None
|
||||
@@ -228,7 +256,9 @@ class TestAddInDbMediaMediagroup:
|
||||
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'):
|
||||
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, так как есть ошибки (второе сообщение не поддерживается)
|
||||
@@ -256,8 +286,12 @@ class TestSendMediaGroupMessageToPrivateChat:
|
||||
# Мокаем БД
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=True):
|
||||
with patch('asyncio.create_task'): # Мокаем create_task, чтобы фоновая задача не выполнялась
|
||||
with patch(
|
||||
"helper_bot.utils.helper_func.add_in_db_media_mediagroup", return_value=True
|
||||
):
|
||||
with patch(
|
||||
"asyncio.create_task"
|
||||
): # Мокаем create_task, чтобы фоновая задача не выполнялась
|
||||
result = await send_media_group_message_to_private_chat(
|
||||
100, mock_message, [], mock_db, main_post_id=789
|
||||
)
|
||||
@@ -282,8 +316,13 @@ class TestSendMediaGroupMessageToPrivateChat:
|
||||
# Мокаем БД
|
||||
mock_db = AsyncMock()
|
||||
|
||||
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=False):
|
||||
with patch('asyncio.create_task'): # Мокаем create_task, чтобы фоновая задача не выполнялась
|
||||
with patch(
|
||||
"helper_bot.utils.helper_func.add_in_db_media_mediagroup",
|
||||
return_value=False,
|
||||
):
|
||||
with patch(
|
||||
"asyncio.create_task"
|
||||
): # Мокаем create_task, чтобы фоновая задача не выполнялась
|
||||
result = await send_media_group_message_to_private_chat(
|
||||
100, mock_message, [], mock_db, main_post_id=789
|
||||
)
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from aiogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
|
||||
KeyboardButton, ReplyKeyboardMarkup)
|
||||
from aiogram.types import (
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
ReplyKeyboardMarkup,
|
||||
)
|
||||
|
||||
from database.async_db import AsyncBotDB
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.keyboards.keyboards import (create_keyboard_with_pagination,
|
||||
get_reply_keyboard,
|
||||
get_reply_keyboard_admin,
|
||||
get_reply_keyboard_for_post,
|
||||
get_reply_keyboard_leave_chat)
|
||||
from helper_bot.keyboards.keyboards import (
|
||||
create_keyboard_with_pagination,
|
||||
get_reply_keyboard,
|
||||
get_reply_keyboard_admin,
|
||||
get_reply_keyboard_for_post,
|
||||
get_reply_keyboard_leave_chat,
|
||||
)
|
||||
|
||||
|
||||
class TestKeyboards:
|
||||
@@ -19,10 +26,7 @@ class TestKeyboards:
|
||||
def mock_db(self):
|
||||
"""Создает мок базы данных"""
|
||||
db = Mock(spec=AsyncBotDB)
|
||||
db.get_user_info = Mock(return_value={
|
||||
'stickers': True,
|
||||
'admin': False
|
||||
})
|
||||
db.get_user_info = Mock(return_value={"stickers": True, "admin": False})
|
||||
return db
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -48,9 +52,9 @@ class TestKeyboards:
|
||||
all_buttons.append(button.text)
|
||||
|
||||
# Проверяем наличие основных кнопок
|
||||
assert '📢Предложить свой пост' in all_buttons
|
||||
assert '👋🏼Сказать пока!' in all_buttons
|
||||
assert '📩Связаться с админами' in all_buttons
|
||||
assert "📢Предложить свой пост" in all_buttons
|
||||
assert "👋🏼Сказать пока!" in all_buttons
|
||||
assert "📩Связаться с админами" in all_buttons
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_reply_keyboard_with_stickers(self, mock_db):
|
||||
@@ -67,7 +71,7 @@ class TestKeyboards:
|
||||
all_buttons.append(button.text)
|
||||
|
||||
# Проверяем наличие кнопки стикеров
|
||||
assert '🤪Хочу стикеры' in all_buttons
|
||||
assert "🤪Хочу стикеры" in all_buttons
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_reply_keyboard_without_stickers(self, mock_db):
|
||||
@@ -84,7 +88,7 @@ class TestKeyboards:
|
||||
all_buttons.append(button.text)
|
||||
|
||||
# Проверяем отсутствие кнопки стикеров
|
||||
assert '🤪Хочу стикеры' not in all_buttons
|
||||
assert "🤪Хочу стикеры" not in all_buttons
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_reply_keyboard_admin(self, mock_db):
|
||||
@@ -101,9 +105,9 @@ class TestKeyboards:
|
||||
all_buttons.append(button.text)
|
||||
|
||||
# Проверяем наличие основных кнопок
|
||||
assert '📢Предложить свой пост' in all_buttons
|
||||
assert '👋🏼Сказать пока!' in all_buttons
|
||||
assert '📩Связаться с админами' in all_buttons
|
||||
assert "📢Предложить свой пост" in all_buttons
|
||||
assert "👋🏼Сказать пока!" in all_buttons
|
||||
assert "📩Связаться с админами" in all_buttons
|
||||
|
||||
def test_get_reply_keyboard_admin_keyboard(self):
|
||||
"""Тест админской клавиатуры"""
|
||||
@@ -145,8 +149,8 @@ class TestKeyboards:
|
||||
all_buttons.append(button.text)
|
||||
|
||||
# Проверяем наличие кнопок для постов
|
||||
assert 'Опубликовать' in all_buttons
|
||||
assert 'Отклонить' in all_buttons
|
||||
assert "Опубликовать" in all_buttons
|
||||
assert "Отклонить" in all_buttons
|
||||
|
||||
def test_get_reply_keyboard_leave_chat(self):
|
||||
"""Тест клавиатуры для выхода из чата"""
|
||||
@@ -162,7 +166,7 @@ class TestKeyboards:
|
||||
all_buttons.append(button.text)
|
||||
|
||||
# Проверяем наличие кнопки выхода
|
||||
assert 'Выйти из чата' in all_buttons
|
||||
assert "Выйти из чата" in all_buttons
|
||||
|
||||
def test_keyboard_resize(self):
|
||||
"""Тест настройки resize клавиатуры"""
|
||||
@@ -177,7 +181,7 @@ class TestKeyboards:
|
||||
keyboard = get_reply_keyboard_leave_chat()
|
||||
|
||||
# Проверяем, что клавиатура настроена правильно
|
||||
assert hasattr(keyboard, 'one_time_keyboard')
|
||||
assert hasattr(keyboard, "one_time_keyboard")
|
||||
assert keyboard.one_time_keyboard is True
|
||||
|
||||
|
||||
@@ -304,17 +308,17 @@ class TestKeyboardIntegration:
|
||||
|
||||
# Проверяем первую клавиатуру (ReplyKeyboardMarkup)
|
||||
assert isinstance(keyboard1, ReplyKeyboardMarkup)
|
||||
assert hasattr(keyboard1, 'keyboard')
|
||||
assert hasattr(keyboard1, "keyboard")
|
||||
assert isinstance(keyboard1.keyboard, list)
|
||||
|
||||
# Проверяем вторую клавиатуру (InlineKeyboardMarkup)
|
||||
assert isinstance(keyboard2, InlineKeyboardMarkup)
|
||||
assert hasattr(keyboard2, 'inline_keyboard')
|
||||
assert hasattr(keyboard2, "inline_keyboard")
|
||||
assert isinstance(keyboard2.inline_keyboard, list)
|
||||
|
||||
# Проверяем третью клавиатуру (ReplyKeyboardMarkup)
|
||||
assert isinstance(keyboard3, ReplyKeyboardMarkup)
|
||||
assert hasattr(keyboard3, 'keyboard')
|
||||
assert hasattr(keyboard3, "keyboard")
|
||||
assert isinstance(keyboard3.keyboard, list)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -345,17 +349,17 @@ class TestKeyboardIntegration:
|
||||
leave_buttons.append(button.text)
|
||||
|
||||
# Проверяем наличие основных кнопок
|
||||
assert '📢Предложить свой пост' in main_buttons
|
||||
assert '👋🏼Сказать пока!' in main_buttons
|
||||
assert '📩Связаться с админами' in main_buttons
|
||||
assert '🤪Хочу стикеры' in main_buttons
|
||||
assert "📢Предложить свой пост" in main_buttons
|
||||
assert "👋🏼Сказать пока!" in main_buttons
|
||||
assert "📩Связаться с админами" in main_buttons
|
||||
assert "🤪Хочу стикеры" in main_buttons
|
||||
|
||||
# Проверяем кнопки для постов
|
||||
assert 'Опубликовать' in post_buttons
|
||||
assert 'Отклонить' in post_buttons
|
||||
assert "Опубликовать" in post_buttons
|
||||
assert "Отклонить" in post_buttons
|
||||
|
||||
# Проверяем кнопку выхода
|
||||
assert 'Выйти из чата' in leave_buttons
|
||||
assert "Выйти из чата" in leave_buttons
|
||||
|
||||
|
||||
class TestPagination:
|
||||
@@ -363,7 +367,7 @@ class TestPagination:
|
||||
|
||||
def test_pagination_empty_list(self):
|
||||
"""Тест с пустым списком элементов"""
|
||||
keyboard = create_keyboard_with_pagination(1, 0, [], 'test')
|
||||
keyboard = create_keyboard_with_pagination(1, 0, [], "test")
|
||||
assert keyboard is not None
|
||||
# Проверяем, что есть только кнопка "Назад"
|
||||
assert len(keyboard.inline_keyboard) == 1
|
||||
@@ -372,10 +376,12 @@ class TestPagination:
|
||||
def test_pagination_single_page(self):
|
||||
"""Тест с одной страницей"""
|
||||
items = [("User1", 1), ("User2", 2), ("User3", 3)]
|
||||
keyboard = create_keyboard_with_pagination(1, 3, items, 'test')
|
||||
keyboard = create_keyboard_with_pagination(1, 3, items, "test")
|
||||
|
||||
# Проверяем количество кнопок (3 пользователя + кнопка "Назад")
|
||||
assert len(keyboard.inline_keyboard) == 2 # 1 ряд с пользователями + 1 ряд с "Назад"
|
||||
assert (
|
||||
len(keyboard.inline_keyboard) == 2
|
||||
) # 1 ряд с пользователями + 1 ряд с "Назад"
|
||||
assert len(keyboard.inline_keyboard[0]) == 3 # 3 пользователя в первом ряду
|
||||
assert keyboard.inline_keyboard[1][0].text == "🏠 Назад"
|
||||
|
||||
@@ -385,10 +391,12 @@ class TestPagination:
|
||||
def test_pagination_multiple_pages(self):
|
||||
"""Тест с несколькими страницами"""
|
||||
items = [("User" + str(i), i) for i in range(1, 15)] # 14 пользователей
|
||||
keyboard = create_keyboard_with_pagination(1, 14, items, 'test')
|
||||
keyboard = create_keyboard_with_pagination(1, 14, items, "test")
|
||||
|
||||
# На первой странице должно быть 9 пользователей (3 ряда по 3) + кнопка "Следующая" + "Назад"
|
||||
assert len(keyboard.inline_keyboard) == 5 # 3 ряда пользователей + навигация + назад
|
||||
assert (
|
||||
len(keyboard.inline_keyboard) == 5
|
||||
) # 3 ряда пользователей + навигация + назад
|
||||
assert len(keyboard.inline_keyboard[0]) == 3 # первый ряд: 3 пользователя
|
||||
assert len(keyboard.inline_keyboard[1]) == 3 # второй ряд: 3 пользователя
|
||||
assert len(keyboard.inline_keyboard[2]) == 3 # третий ряд: 3 пользователя
|
||||
@@ -398,10 +406,12 @@ class TestPagination:
|
||||
def test_pagination_second_page(self):
|
||||
"""Тест второй страницы"""
|
||||
items = [("User" + str(i), i) for i in range(1, 15)] # 14 пользователей
|
||||
keyboard = create_keyboard_with_pagination(2, 14, items, 'test')
|
||||
keyboard = create_keyboard_with_pagination(2, 14, items, "test")
|
||||
|
||||
# На второй странице должно быть 5 пользователей (2 ряда: 3+2) + кнопки "Предыдущая" и "Назад"
|
||||
assert len(keyboard.inline_keyboard) == 4 # 2 ряда пользователей + навигация + назад
|
||||
assert (
|
||||
len(keyboard.inline_keyboard) == 4
|
||||
) # 2 ряда пользователей + навигация + назад
|
||||
assert len(keyboard.inline_keyboard[0]) == 3 # первый ряд: 3 пользователя
|
||||
assert len(keyboard.inline_keyboard[1]) == 2 # второй ряд: 2 пользователя
|
||||
assert keyboard.inline_keyboard[2][0].text == "⬅️ Предыдущая"
|
||||
@@ -410,10 +420,12 @@ class TestPagination:
|
||||
def test_pagination_middle_page(self):
|
||||
"""Тест средней страницы"""
|
||||
items = [("User" + str(i), i) for i in range(1, 25)] # 24 пользователя
|
||||
keyboard = create_keyboard_with_pagination(2, 24, items, 'test')
|
||||
keyboard = create_keyboard_with_pagination(2, 24, items, "test")
|
||||
|
||||
# На второй странице должно быть 9 пользователей (3 ряда по 3) + кнопки "Предыдущая" и "Следующая"
|
||||
assert len(keyboard.inline_keyboard) == 5 # 3 ряда пользователей + навигация + назад
|
||||
assert (
|
||||
len(keyboard.inline_keyboard) == 5
|
||||
) # 3 ряда пользователей + навигация + назад
|
||||
assert len(keyboard.inline_keyboard[0]) == 3 # первый ряд: 3 пользователя
|
||||
assert len(keyboard.inline_keyboard[1]) == 3 # второй ряд: 3 пользователя
|
||||
assert len(keyboard.inline_keyboard[2]) == 3 # третий ряд: 3 пользователя
|
||||
@@ -423,7 +435,7 @@ class TestPagination:
|
||||
def test_pagination_invalid_page_number(self):
|
||||
"""Тест с некорректным номером страницы"""
|
||||
items = [("User" + str(i), i) for i in range(1, 10)] # 9 пользователей
|
||||
keyboard = create_keyboard_with_pagination(0, 9, items, 'test') # страница 0
|
||||
keyboard = create_keyboard_with_pagination(0, 9, items, "test") # страница 0
|
||||
|
||||
# Должна вернуться первая страница
|
||||
assert len(keyboard.inline_keyboard) == 4 # 3 ряда пользователей + назад
|
||||
@@ -434,7 +446,9 @@ class TestPagination:
|
||||
def test_pagination_page_out_of_range(self):
|
||||
"""Тест с номером страницы больше максимального"""
|
||||
items = [("User" + str(i), i) for i in range(1, 10)] # 9 пользователей
|
||||
keyboard = create_keyboard_with_pagination(5, 9, items, 'test') # страница 5 при 1 странице
|
||||
keyboard = create_keyboard_with_pagination(
|
||||
5, 9, items, "test"
|
||||
) # страница 5 при 1 странице
|
||||
|
||||
# Должна вернуться первая страница
|
||||
assert len(keyboard.inline_keyboard) == 4 # 3 ряда пользователей + назад
|
||||
@@ -445,7 +459,7 @@ class TestPagination:
|
||||
def test_pagination_callback_data_format(self):
|
||||
"""Тест формата callback_data"""
|
||||
items = [("User1", 123), ("User2", 456)]
|
||||
keyboard = create_keyboard_with_pagination(1, 2, items, 'ban')
|
||||
keyboard = create_keyboard_with_pagination(1, 2, items, "ban")
|
||||
|
||||
# Проверяем формат callback_data для пользователей
|
||||
assert keyboard.inline_keyboard[0][0].callback_data == "ban_123"
|
||||
@@ -457,7 +471,7 @@ class TestPagination:
|
||||
def test_pagination_navigation_callback_data(self):
|
||||
"""Тест callback_data для кнопок навигации"""
|
||||
items = [("User" + str(i), i) for i in range(1, 15)] # 14 пользователей
|
||||
keyboard = create_keyboard_with_pagination(2, 14, items, 'test')
|
||||
keyboard = create_keyboard_with_pagination(2, 14, items, "test")
|
||||
|
||||
# Проверяем callback_data для кнопки "Предыдущая"
|
||||
assert keyboard.inline_keyboard[2][0].callback_data == "page_1"
|
||||
@@ -468,7 +482,7 @@ class TestPagination:
|
||||
def test_pagination_exactly_items_per_page(self):
|
||||
"""Тест когда количество элементов точно равно items_per_page"""
|
||||
items = [("User" + str(i), i) for i in range(1, 10)] # ровно 9 пользователей
|
||||
keyboard = create_keyboard_with_pagination(1, 9, items, 'test')
|
||||
keyboard = create_keyboard_with_pagination(1, 9, items, "test")
|
||||
|
||||
# Должна быть только одна страница без кнопок навигации
|
||||
assert len(keyboard.inline_keyboard) == 4 # 3 ряда пользователей + назад
|
||||
@@ -478,5 +492,5 @@ class TestPagination:
|
||||
assert keyboard.inline_keyboard[3][0].text == "🏠 Назад"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v'])
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from database.models import UserMessage
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
|
||||
@@ -27,7 +28,7 @@ class TestMessageRepository:
|
||||
message_text="Тестовое сообщение",
|
||||
user_id=12345,
|
||||
telegram_message_id=67890,
|
||||
date=int(datetime.now().timestamp())
|
||||
date=int(datetime.now().timestamp()),
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
@@ -37,7 +38,7 @@ class TestMessageRepository:
|
||||
message_text="Тестовое сообщение без даты",
|
||||
user_id=12345,
|
||||
telegram_message_id=67891,
|
||||
date=None
|
||||
date=None,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -53,7 +54,10 @@ class TestMessageRepository:
|
||||
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
|
||||
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):
|
||||
@@ -74,11 +78,13 @@ class TestMessageRepository:
|
||||
sample_message.message_text,
|
||||
sample_message.user_id,
|
||||
sample_message.telegram_message_id,
|
||||
sample_message.date
|
||||
sample_message.date,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_without_date(self, message_repository, sample_message_no_date):
|
||||
async def test_add_message_without_date(
|
||||
self, message_repository, sample_message_no_date
|
||||
):
|
||||
"""Тест добавления сообщения без даты (должна генерироваться автоматически)."""
|
||||
# Мокаем _execute_query
|
||||
message_repository._execute_query = AsyncMock()
|
||||
@@ -107,7 +113,9 @@ class TestMessageRepository:
|
||||
|
||||
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
|
||||
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):
|
||||
@@ -125,7 +133,7 @@ class TestMessageRepository:
|
||||
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,)
|
||||
(message_id,),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -141,7 +149,7 @@ class TestMessageRepository:
|
||||
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,)
|
||||
(message_id,),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -157,10 +165,14 @@ class TestMessageRepository:
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_message_handles_exception(self, message_repository, sample_message):
|
||||
async def test_add_message_handles_exception(
|
||||
self, message_repository, sample_message
|
||||
):
|
||||
"""Тест обработки исключений при добавлении сообщения."""
|
||||
# Мокаем _execute_query для вызова исключения
|
||||
message_repository._execute_query = AsyncMock(side_effect=Exception("Database error"))
|
||||
message_repository._execute_query = AsyncMock(
|
||||
side_effect=Exception("Database error")
|
||||
)
|
||||
|
||||
with pytest.raises(Exception, match="Database error"):
|
||||
await message_repository.add_message(sample_message)
|
||||
@@ -183,7 +195,7 @@ class TestMessageRepository:
|
||||
message_text="Тестовое сообщение с нулевой датой",
|
||||
user_id=12345,
|
||||
telegram_message_id=67892,
|
||||
date=0
|
||||
date=0,
|
||||
)
|
||||
|
||||
# Мокаем _execute_query
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user