Добавлен функционал для отслеживания истории банов пользователей.

- Введена новая модель `BlacklistHistoryRecord` для хранения информации о банах и разблокировках.
- Обновлены методы `set_user_blacklist` и `delete_user_blacklist` в `AsyncBotDB` для логирования событий в историю.
- Обновлена схема базы данных для создания таблицы `blacklist_history` и соответствующих индексов.
- Обновлены тесты для проверки нового функционала и обработки ошибок при записи в историю.
This commit is contained in:
2026-01-23 16:23:27 +03:00
parent 7269130777
commit 3b841fcbfa
10 changed files with 904 additions and 20 deletions

View File

@@ -3,7 +3,7 @@ from datetime import datetime
from typing import Optional, List, Dict, Any, Tuple
from database.repository_factory import RepositoryFactory
from database.models import (
User, BlacklistUser, UserMessage, TelegramPost, PostContent,
User, BlacklistUser, BlacklistHistoryRecord, UserMessage, TelegramPost, PostContent,
Admin, AudioMessage
)
@@ -196,7 +196,10 @@ class AsyncBotDB:
date_to_unban: int = None,
ban_author: Optional[int] = None,
):
"""Добавляет пользователя в черный список."""
"""
Добавляет пользователя в черный список.
Также создает запись в истории банов для отслеживания.
"""
blacklist_user = BlacklistUser(
user_id=user_id,
message_for_user=message_for_user,
@@ -204,9 +207,40 @@ class AsyncBotDB:
ban_author=ban_author,
)
await self.factory.blacklist.add_user(blacklist_user)
# Логируем в историю банов
try:
date_ban = int(datetime.now().timestamp())
history_record = BlacklistHistoryRecord(
user_id=user_id,
message_for_user=message_for_user,
date_ban=date_ban,
date_unban=None, # Будет установлено при разбане
ban_author=ban_author,
)
await self.factory.blacklist_history.add_record_on_ban(history_record)
except Exception as e:
# Ошибка записи в историю не должна ломать процесс бана
self.logger.error(
f"Ошибка записи в историю банов для user_id={user_id}: {e}"
)
async def delete_user_blacklist(self, user_id: int) -> bool:
"""Удаляет пользователя из черного списка."""
"""
Удаляет пользователя из черного списка.
Также обновляет запись в истории банов, устанавливая date_unban.
"""
# Сначала обновляем историю (если есть открытая запись)
try:
date_unban = int(datetime.now().timestamp())
await self.factory.blacklist_history.set_unban_date(user_id, date_unban)
except Exception as e:
# Ошибка записи в историю не должна ломать критический путь разбана
self.logger.error(
f"Ошибка обновления истории при разбане для user_id={user_id}: {e}"
)
# Удаляем из черного списка (критический путь)
return await self.factory.blacklist.remove_user(user_id)
async def check_user_in_blacklist(self, user_id: int) -> bool:

View File

@@ -29,6 +29,19 @@ class BlacklistUser:
ban_author: Optional[int] = None
@dataclass
class BlacklistHistoryRecord:
"""Модель записи истории банов/разбанов."""
user_id: int
message_for_user: Optional[str] = None
date_ban: int = 0
date_unban: Optional[int] = None
ban_author: Optional[int] = None
id: Optional[int] = None
created_at: Optional[int] = None
updated_at: Optional[int] = None
@dataclass
class UserMessage:
"""Модель сообщения пользователя."""

View File

@@ -4,6 +4,7 @@
Содержит репозитории для разных сущностей:
- user_repository: работа с пользователями
- blacklist_repository: работа с черным списком
- blacklist_history_repository: работа с историей банов/разбанов
- message_repository: работа с сообщениями
- post_repository: работа с постами
- admin_repository: работа с администраторами
@@ -12,12 +13,13 @@
from .user_repository import UserRepository
from .blacklist_repository import BlacklistRepository
from .blacklist_history_repository import BlacklistHistoryRepository
from .message_repository import MessageRepository
from .post_repository import PostRepository
from .admin_repository import AdminRepository
from .audio_repository import AudioRepository
__all__ = [
'UserRepository', 'BlacklistRepository', 'MessageRepository', 'PostRepository',
'AdminRepository', 'AudioRepository'
'UserRepository', 'BlacklistRepository', 'BlacklistHistoryRepository',
'MessageRepository', 'PostRepository', 'AdminRepository', 'AudioRepository'
]

View File

@@ -0,0 +1,119 @@
from typing import Optional
from database.base import DatabaseConnection
from database.models import BlacklistHistoryRecord
class BlacklistHistoryRepository(DatabaseConnection):
"""Репозиторий для работы с историей банов/разбанов."""
async def create_tables(self):
"""Создание таблицы истории банов/разбанов."""
query = '''
CREATE TABLE IF NOT EXISTS blacklist_history (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
message_for_user TEXT,
date_ban INTEGER NOT NULL,
date_unban INTEGER,
ban_author INTEGER,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
updated_at INTEGER DEFAULT (strftime('%s', 'now')),
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)
# Создаем индексы
await self._execute_query(
"CREATE INDEX IF NOT EXISTS idx_blacklist_history_user_id ON blacklist_history(user_id)"
)
await self._execute_query(
"CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_ban ON blacklist_history(date_ban)"
)
await self._execute_query(
"CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_unban ON blacklist_history(date_unban)"
)
self.logger.info("Таблица истории банов/разбанов создана")
async def add_record_on_ban(self, record: BlacklistHistoryRecord) -> None:
"""Добавляет запись о бане в историю."""
query = """
INSERT INTO blacklist_history (
user_id, message_for_user, date_ban, date_unban, ban_author, created_at, updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?)
"""
# Используем текущее время, если не указано
from datetime import datetime
current_timestamp = int(datetime.now().timestamp())
params = (
record.user_id,
record.message_for_user,
record.date_ban,
record.date_unban,
record.ban_author,
record.created_at if record.created_at is not None else current_timestamp,
record.updated_at if record.updated_at is not None else current_timestamp,
)
await self._execute_query(query, params)
self.logger.info(
f"Запись о бане добавлена в историю: user_id={record.user_id}, "
f"date_ban={record.date_ban}"
)
async def set_unban_date(self, user_id: int, date_unban: int) -> bool:
"""
Обновляет date_unban и updated_at в последней записи (date_unban IS NULL) для пользователя.
Args:
user_id: ID пользователя
date_unban: Timestamp даты разбана
Returns:
True если запись обновлена, False если не найдена открытая запись
"""
try:
from datetime import datetime
current_timestamp = int(datetime.now().timestamp())
# SQLite не поддерживает ORDER BY в UPDATE, поэтому используем подзапрос
# Сначала проверяем, есть ли открытая запись
check_query = """
SELECT id FROM blacklist_history
WHERE user_id = ? AND date_unban IS NULL
ORDER BY id DESC
LIMIT 1
"""
rows = await self._execute_query_with_result(check_query, (user_id,))
if not rows:
self.logger.warning(
f"Не найдена открытая запись в истории для обновления: user_id={user_id}"
)
return False
# Обновляем найденную запись
update_query = """
UPDATE blacklist_history
SET date_unban = ?,
updated_at = ?
WHERE id = ?
"""
record_id = rows[0][0]
params = (date_unban, current_timestamp, record_id)
await self._execute_query(update_query, params)
self.logger.info(
f"Дата разбана обновлена в истории: user_id={user_id}, date_unban={date_unban}"
)
return True
except Exception as e:
self.logger.error(
f"Ошибка обновления даты разбана в истории для user_id={user_id}: {str(e)}"
)
return False

View File

@@ -1,6 +1,7 @@
from typing import Optional
from database.repositories.user_repository import UserRepository
from database.repositories.blacklist_repository import BlacklistRepository
from database.repositories.blacklist_history_repository import BlacklistHistoryRepository
from database.repositories.message_repository import MessageRepository
from database.repositories.post_repository import PostRepository
from database.repositories.admin_repository import AdminRepository
@@ -14,6 +15,7 @@ class RepositoryFactory:
self.db_path = db_path
self._user_repo: Optional[UserRepository] = None
self._blacklist_repo: Optional[BlacklistRepository] = None
self._blacklist_history_repo: Optional[BlacklistHistoryRepository] = None
self._message_repo: Optional[MessageRepository] = None
self._post_repo: Optional[PostRepository] = None
self._admin_repo: Optional[AdminRepository] = None
@@ -33,6 +35,13 @@ class RepositoryFactory:
self._blacklist_repo = BlacklistRepository(self.db_path)
return self._blacklist_repo
@property
def blacklist_history(self) -> BlacklistHistoryRepository:
"""Возвращает репозиторий истории банов/разбанов."""
if self._blacklist_history_repo is None:
self._blacklist_history_repo = BlacklistHistoryRepository(self.db_path)
return self._blacklist_history_repo
@property
def messages(self) -> MessageRepository:
"""Возвращает репозиторий сообщений."""
@@ -65,6 +74,7 @@ class RepositoryFactory:
"""Создает все таблицы в базе данных."""
await self.users.create_tables()
await self.blacklist.create_tables()
await self.blacklist_history.create_tables()
await self.messages.create_tables()
await self.posts.create_tables()
await self.admins.create_tables()

View File

@@ -40,6 +40,20 @@ CREATE TABLE IF NOT EXISTS blacklist (
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
);
-- Blacklist history for tracking all ban/unban events
CREATE TABLE IF NOT EXISTS blacklist_history (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
message_for_user TEXT,
date_ban INTEGER NOT NULL,
date_unban INTEGER,
ban_author INTEGER,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
updated_at INTEGER DEFAULT (strftime('%s', 'now')),
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
);
-- User message history
CREATE TABLE IF NOT EXISTS user_messages (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -109,6 +123,9 @@ CREATE INDEX IF NOT EXISTS idx_audio_message_reference_author_id ON audio_messag
CREATE INDEX IF NOT EXISTS idx_user_messages_user_id ON user_messages(user_id);
CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_author_id ON post_from_telegram_suggest(author_id);
CREATE INDEX IF NOT EXISTS idx_blacklist_date_to_unban ON blacklist(date_to_unban);
CREATE INDEX IF NOT EXISTS idx_blacklist_history_user_id ON blacklist_history(user_id);
CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_ban ON blacklist_history(date_ban);
CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_unban ON blacklist_history(date_unban);
CREATE INDEX IF NOT EXISTS idx_user_messages_date ON user_messages(date);
CREATE INDEX IF NOT EXISTS idx_audio_message_reference_date ON audio_message_reference(date_added);
CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_date ON post_from_telegram_suggest(created_at);