Enhance bot functionality and refactor database interactions
- Added `ca-certificates` installation to Dockerfile for improved network security. - Updated health check command in Dockerfile to include better timeout handling. - Refactored `run_helper.py` to implement proper signal handling and logging during shutdown. - Transitioned database operations to an asynchronous model in `async_db.py`, improving performance and responsiveness. - Updated database schema to support new foreign key relationships and optimized indexing for better query performance. - Enhanced various bot handlers to utilize async database methods, improving overall efficiency and user experience. - Removed obsolete database and fix scripts to streamline the project structure.
This commit is contained in:
26
database/__init__.py
Normal file
26
database/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Пакет для работы с базой данных.
|
||||
|
||||
Содержит:
|
||||
- models: модели данных
|
||||
- base: базовый класс для работы с БД
|
||||
- repositories: репозитории для разных сущностей
|
||||
- repository_factory: фабрика репозиториев
|
||||
- async_db: основной класс AsyncBotDB
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
User, BlacklistUser, UserMessage, TelegramPost, PostContent,
|
||||
MessageContentLink, Admin, Migration, AudioMessage, AudioListenRecord, AudioModerate
|
||||
)
|
||||
from .repository_factory import RepositoryFactory
|
||||
from .base import DatabaseConnection
|
||||
from .async_db import AsyncBotDB
|
||||
|
||||
# Для обратной совместимости экспортируем старый интерфейс
|
||||
__all__ = [
|
||||
'User', 'BlacklistUser', 'UserMessage', 'TelegramPost', 'PostContent',
|
||||
'MessageContentLink', 'Admin', 'Migration', 'AudioMessage', 'AudioListenRecord', 'AudioModerate',
|
||||
'RepositoryFactory', 'DatabaseConnection', 'AsyncBotDB'
|
||||
]
|
||||
|
||||
1225
database/async_db.py
1225
database/async_db.py
File diff suppressed because it is too large
Load Diff
114
database/base.py
Normal file
114
database/base.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import aiosqlite
|
||||
from typing import Optional
|
||||
from logs.custom_logger import logger
|
||||
|
||||
|
||||
class DatabaseConnection:
|
||||
"""Базовый класс для работы с базой данных."""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = os.path.abspath(db_path)
|
||||
self.logger = logger
|
||||
self.logger.info(f'Инициация базы данных: {self.db_path}')
|
||||
|
||||
async def _get_connection(self):
|
||||
"""Получение асинхронного соединения с базой данных."""
|
||||
try:
|
||||
conn = await aiosqlite.connect(self.db_path)
|
||||
# Включаем поддержку внешних ключей
|
||||
await conn.execute("PRAGMA foreign_keys = ON")
|
||||
# Включаем WAL режим для лучшей производительности
|
||||
await conn.execute("PRAGMA journal_mode = WAL")
|
||||
await conn.execute("PRAGMA synchronous = NORMAL")
|
||||
await conn.execute("PRAGMA cache_size = 10000")
|
||||
await conn.execute("PRAGMA temp_store = MEMORY")
|
||||
return conn
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при получении соединения: {e}")
|
||||
raise
|
||||
|
||||
async def _execute_query(self, query: str, params: tuple = ()):
|
||||
"""Выполнение запроса с автоматическим закрытием соединения."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
result = await conn.execute(query, params)
|
||||
await conn.commit()
|
||||
return result
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при выполнении запроса: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def _execute_query_with_result(self, query: str, params: tuple = ()):
|
||||
"""Выполнение запроса с результатом и автоматическим закрытием соединения."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
result = await conn.execute(query, params)
|
||||
# Получаем все результаты сразу, чтобы можно было закрыть соединение
|
||||
rows = await result.fetchall()
|
||||
return rows
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при выполнении запроса: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def _execute_transaction(self, queries: list):
|
||||
"""Выполнение транзакции с несколькими запросами."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
for query, params in queries:
|
||||
await conn.execute(query, params)
|
||||
await conn.commit()
|
||||
except Exception as e:
|
||||
if conn:
|
||||
await conn.rollback()
|
||||
self.logger.error(f"Ошибка при выполнении транзакции: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def check_database_integrity(self):
|
||||
"""Проверяет целостность базы данных и очищает WAL файлы."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
result = await conn.execute("PRAGMA integrity_check")
|
||||
integrity_result = await result.fetchone()
|
||||
|
||||
if integrity_result and integrity_result[0] == "ok":
|
||||
self.logger.info("Проверка целостности базы данных прошла успешно")
|
||||
await conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
self.logger.info("WAL файлы очищены")
|
||||
else:
|
||||
self.logger.warning(f"Проблемы с целостностью базы данных: {integrity_result}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при проверке целостности базы данных: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def cleanup_wal_files(self):
|
||||
"""Очищает WAL файлы и переключает на DELETE режим для предотвращения проблем с I/O."""
|
||||
conn = None
|
||||
try:
|
||||
conn = await self._get_connection()
|
||||
await conn.execute("PRAGMA journal_mode=DELETE")
|
||||
await conn.execute("PRAGMA journal_mode=WAL")
|
||||
self.logger.info("WAL файлы очищены и режим восстановлен")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при очистке WAL файлов: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
await conn.close()
|
||||
1551
database/db.py
1551
database/db.py
File diff suppressed because it is too large
Load Diff
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Скрипт для диагностики и исправления проблем с базой данных Telegram бота.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
def check_database_file(db_path):
|
||||
"""Проверяет состояние файла базы данных."""
|
||||
print(f"Проверка файла: {db_path}")
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ Файл базы данных не найден: {db_path}")
|
||||
return False
|
||||
|
||||
# Проверяем права доступа
|
||||
if not os.access(db_path, os.R_OK | os.W_OK):
|
||||
print(f"❌ Нет прав доступа к файлу: {db_path}")
|
||||
return False
|
||||
|
||||
# Проверяем размер файла
|
||||
file_size = os.path.getsize(db_path)
|
||||
print(f"✅ Размер файла: {file_size} байт")
|
||||
|
||||
return True
|
||||
|
||||
def check_wal_files(db_path):
|
||||
"""Проверяет WAL файлы."""
|
||||
db_dir = os.path.dirname(db_path)
|
||||
db_name = os.path.basename(db_path)
|
||||
base_name = os.path.splitext(db_name)[0]
|
||||
|
||||
wal_file = os.path.join(db_dir, f"{base_name}.db-wal")
|
||||
shm_file = os.path.join(db_dir, f"{base_name}.db-shm")
|
||||
|
||||
print(f"\nПроверка WAL файлов:")
|
||||
|
||||
if os.path.exists(wal_file):
|
||||
wal_size = os.path.getsize(wal_file)
|
||||
print(f"✅ WAL файл найден: {wal_file} ({wal_size} байт)")
|
||||
else:
|
||||
print(f"ℹ️ WAL файл не найден: {wal_file}")
|
||||
|
||||
if os.path.exists(shm_file):
|
||||
shm_size = os.path.getsize(shm_file)
|
||||
print(f"✅ SHM файл найден: {shm_file} ({shm_size} байт)")
|
||||
else:
|
||||
print(f"ℹ️ SHM файл не найден: {shm_file}")
|
||||
|
||||
return wal_file, shm_file
|
||||
|
||||
def test_database_connection(db_path):
|
||||
"""Тестирует подключение к базе данных."""
|
||||
print(f"\nТестирование подключения к базе данных...")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path, timeout=10.0)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверяем версию SQLite
|
||||
cursor.execute("SELECT sqlite_version()")
|
||||
version = cursor.fetchone()[0]
|
||||
print(f"✅ SQLite версия: {version}")
|
||||
|
||||
# Проверяем таблицы
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = cursor.fetchall()
|
||||
print(f"✅ Найдено таблиц: {len(tables)}")
|
||||
|
||||
# Проверяем целостность
|
||||
cursor.execute("PRAGMA integrity_check")
|
||||
integrity = cursor.fetchone()[0]
|
||||
if integrity == "ok":
|
||||
print("✅ Целостность базы данных: OK")
|
||||
else:
|
||||
print(f"⚠️ Проблемы с целостностью: {integrity}")
|
||||
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ Ошибка SQLite: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Неожиданная ошибка: {e}")
|
||||
return False
|
||||
|
||||
def cleanup_wal_files(db_path):
|
||||
"""Очищает WAL файлы."""
|
||||
print(f"\nОчистка WAL файлов...")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path, timeout=10.0)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Переключаем на DELETE режим для очистки WAL
|
||||
cursor.execute("PRAGMA journal_mode=DELETE")
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
|
||||
# Принудительно создаем checkpoint
|
||||
cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
|
||||
conn.close()
|
||||
print("✅ WAL файлы очищены")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при очистке WAL файлов: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Основная функция."""
|
||||
print("🔧 Диагностика базы данных Telegram бота")
|
||||
print("=" * 50)
|
||||
|
||||
# Определяем путь к базе данных
|
||||
current_dir = os.getcwd()
|
||||
db_path = os.path.join(current_dir, 'database', 'tg-bot-database.db')
|
||||
|
||||
print(f"Текущая директория: {current_dir}")
|
||||
print(f"Путь к базе данных: {db_path}")
|
||||
|
||||
# Проверяем файл базы данных
|
||||
if not check_database_file(db_path):
|
||||
print("\n❌ Файл базы данных недоступен. Проверьте права доступа и существование файла.")
|
||||
return
|
||||
|
||||
# Проверяем WAL файлы
|
||||
wal_file, shm_file = check_wal_files(db_path)
|
||||
|
||||
# Тестируем подключение
|
||||
if not test_database_connection(db_path):
|
||||
print("\n❌ Не удалось подключиться к базе данных.")
|
||||
return
|
||||
|
||||
# Очищаем WAL файлы
|
||||
if cleanup_wal_files(db_path):
|
||||
print("\n✅ База данных проверена и исправлена.")
|
||||
else:
|
||||
print("\n⚠️ База данных проверена, но не удалось очистить WAL файлы.")
|
||||
|
||||
print("\n📋 Рекомендации:")
|
||||
print("1. Убедитесь, что у процесса есть права на запись в директорию database/")
|
||||
print("2. Проверьте свободное место на диске")
|
||||
print("3. Если проблемы продолжаются, попробуйте перезапустить бота")
|
||||
print("4. В крайнем случае, создайте резервную копию и пересоздайте базу данных")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
103
database/models.py
Normal file
103
database/models.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""Модель пользователя."""
|
||||
user_id: int
|
||||
first_name: str
|
||||
full_name: str
|
||||
username: Optional[str] = None
|
||||
is_bot: bool = False
|
||||
language_code: str = "ru"
|
||||
emoji: str = "😊"
|
||||
has_stickers: bool = False
|
||||
date_added: Optional[str] = None
|
||||
date_changed: Optional[str] = None
|
||||
voice_bot_welcome_received: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlacklistUser:
|
||||
"""Модель пользователя в черном списке."""
|
||||
user_id: int
|
||||
message_for_user: Optional[str] = None
|
||||
date_to_unban: Optional[int] = None
|
||||
created_at: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserMessage:
|
||||
"""Модель сообщения пользователя."""
|
||||
message_text: str
|
||||
user_id: int
|
||||
telegram_message_id: int
|
||||
date: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TelegramPost:
|
||||
"""Модель поста из Telegram."""
|
||||
message_id: int
|
||||
text: str
|
||||
author_id: int
|
||||
helper_text_message_id: Optional[int] = None
|
||||
created_at: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PostContent:
|
||||
"""Модель контента поста."""
|
||||
message_id: int
|
||||
content_name: str
|
||||
content_type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageContentLink:
|
||||
"""Модель связи сообщения с контентом."""
|
||||
post_id: int
|
||||
message_id: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Admin:
|
||||
"""Модель администратора."""
|
||||
user_id: int
|
||||
role: str = "admin"
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Migration:
|
||||
"""Модель миграции."""
|
||||
version: int
|
||||
script_name: str
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioMessage:
|
||||
"""Модель аудио сообщения."""
|
||||
file_name: str
|
||||
author_id: int
|
||||
date_added: str
|
||||
file_id: str
|
||||
listen_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioListenRecord:
|
||||
"""Модель записи прослушивания аудио."""
|
||||
file_name: str
|
||||
user_id: int
|
||||
is_listen: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioModerate:
|
||||
"""Модель для voice bot."""
|
||||
message_id: int
|
||||
user_id: int
|
||||
23
database/repositories/__init__.py
Normal file
23
database/repositories/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Пакет репозиториев для работы с базой данных.
|
||||
|
||||
Содержит репозитории для разных сущностей:
|
||||
- user_repository: работа с пользователями
|
||||
- blacklist_repository: работа с черным списком
|
||||
- message_repository: работа с сообщениями
|
||||
- post_repository: работа с постами
|
||||
- admin_repository: работа с администраторами
|
||||
- audio_repository: работа с аудио
|
||||
"""
|
||||
|
||||
from .user_repository import UserRepository
|
||||
from .blacklist_repository import BlacklistRepository
|
||||
from .message_repository import MessageRepository
|
||||
from .post_repository import PostRepository
|
||||
from .admin_repository import AdminRepository
|
||||
from .audio_repository import AudioRepository
|
||||
|
||||
__all__ = [
|
||||
'UserRepository', 'BlacklistRepository', 'MessageRepository', 'PostRepository',
|
||||
'AdminRepository', 'AudioRepository'
|
||||
]
|
||||
74
database/repositories/admin_repository.py
Normal file
74
database/repositories/admin_repository.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from typing import Optional
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import Admin
|
||||
|
||||
|
||||
class AdminRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с администраторами."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы администраторов."""
|
||||
# Включаем поддержку внешних ключей
|
||||
await self._execute_query("PRAGMA foreign_keys = ON")
|
||||
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
role TEXT DEFAULT 'admin',
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица администраторов создана")
|
||||
|
||||
async def add_admin(self, admin: Admin) -> None:
|
||||
"""Добавление администратора."""
|
||||
query = "INSERT INTO admins (user_id, role) VALUES (?, ?)"
|
||||
params = (admin.user_id, admin.role)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Администратор добавлен: user_id={admin.user_id}, role={admin.role}")
|
||||
|
||||
async def remove_admin(self, user_id: int) -> None:
|
||||
"""Удаление администратора."""
|
||||
query = "DELETE FROM admins WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Администратор удален: user_id={user_id}")
|
||||
|
||||
async def is_admin(self, user_id: int) -> bool:
|
||||
"""Проверка, является ли пользователь администратором."""
|
||||
query = "SELECT 1 FROM admins WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return bool(row)
|
||||
|
||||
async def get_admin(self, user_id: int) -> Optional[Admin]:
|
||||
"""Получение информации об администраторе."""
|
||||
query = "SELECT user_id, role, created_at FROM admins WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return Admin(
|
||||
user_id=row[0],
|
||||
role=row[1],
|
||||
created_at=row[2] if len(row) > 2 else None
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_all_admins(self) -> list[Admin]:
|
||||
"""Получение всех администраторов."""
|
||||
query = "SELECT user_id, role, created_at FROM admins ORDER BY created_at DESC"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
|
||||
admins = []
|
||||
for row in rows:
|
||||
admin = Admin(
|
||||
user_id=row[0],
|
||||
role=row[1],
|
||||
created_at=row[2] if len(row) > 2 else None
|
||||
)
|
||||
admins.append(admin)
|
||||
|
||||
return admins
|
||||
210
database/repositories/audio_repository.py
Normal file
210
database/repositories/audio_repository.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from typing import Optional, List
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import AudioMessage, AudioListenRecord, AudioModerate
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AudioRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с аудио сообщениями."""
|
||||
|
||||
async def enable_foreign_keys(self):
|
||||
"""Включает поддержку внешних ключей."""
|
||||
await self._execute_query("PRAGMA foreign_keys = ON;")
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблиц для аудио."""
|
||||
# Таблица аудио сообщений
|
||||
audio_query = '''
|
||||
CREATE TABLE IF NOT EXISTS audio_message_reference (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
file_name TEXT NOT NULL UNIQUE,
|
||||
author_id INTEGER NOT NULL,
|
||||
date_added INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(audio_query)
|
||||
|
||||
# Таблица прослушивания аудио
|
||||
listen_query = '''
|
||||
CREATE TABLE IF NOT EXISTS user_audio_listens (
|
||||
file_name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (file_name, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(listen_query)
|
||||
|
||||
# Таблица для voice bot
|
||||
voice_query = '''
|
||||
CREATE TABLE IF NOT EXISTS audio_moderate (
|
||||
user_id INTEGER NOT NULL,
|
||||
message_id INTEGER,
|
||||
PRIMARY KEY (user_id, message_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(voice_query)
|
||||
|
||||
self.logger.info("Таблицы для аудио созданы")
|
||||
|
||||
async def add_audio_record(self, audio: AudioMessage) -> None:
|
||||
"""Добавляет информацию о войсе пользователя."""
|
||||
query = """
|
||||
INSERT INTO audio_message_reference (file_name, author_id, date_added)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
# Преобразуем datetime в UNIX timestamp если нужно
|
||||
if isinstance(audio.date_added, str):
|
||||
date_timestamp = int(datetime.fromisoformat(audio.date_added).timestamp())
|
||||
elif isinstance(audio.date_added, datetime):
|
||||
date_timestamp = int(audio.date_added.timestamp())
|
||||
else:
|
||||
date_timestamp = audio.date_added
|
||||
|
||||
params = (audio.file_name, audio.author_id, date_timestamp)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Аудио добавлено: file_name={audio.file_name}, author_id={audio.author_id}")
|
||||
|
||||
async def add_audio_record_simple(self, file_name: str, user_id: int, date_added) -> None:
|
||||
"""Добавляет информацию о войсе пользователя (упрощенная версия)."""
|
||||
query = """
|
||||
INSERT INTO audio_message_reference (file_name, author_id, date_added)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
# Преобразуем datetime в UNIX timestamp если нужно
|
||||
if isinstance(date_added, str):
|
||||
date_timestamp = int(datetime.fromisoformat(date_added).timestamp())
|
||||
elif isinstance(date_added, datetime):
|
||||
date_timestamp = int(date_added.timestamp())
|
||||
else:
|
||||
date_timestamp = date_added
|
||||
|
||||
params = (file_name, user_id, date_timestamp)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Аудио добавлено: file_name={file_name}, user_id={user_id}")
|
||||
|
||||
async def get_last_date_audio(self) -> Optional[int]:
|
||||
"""Получает дату последнего войса."""
|
||||
query = "SELECT date_added FROM audio_message_reference ORDER BY date_added DESC LIMIT 1"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
self.logger.info(f"Последняя дата аудио: {row[0]}")
|
||||
return row[0]
|
||||
return None
|
||||
|
||||
async def get_user_audio_records_count(self, user_id: int) -> int:
|
||||
"""Получает количество записей пользователя."""
|
||||
query = "SELECT COUNT(*) FROM audio_message_reference WHERE author_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else 0
|
||||
|
||||
async def get_path_for_audio_record(self, user_id: int) -> Optional[str]:
|
||||
"""Получает название последнего файла пользователя."""
|
||||
query = """
|
||||
SELECT file_name FROM audio_message_reference
|
||||
WHERE author_id = ? ORDER BY date_added DESC LIMIT 1
|
||||
"""
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else None
|
||||
|
||||
async def check_listen_audio(self, user_id: int) -> List[str]:
|
||||
"""Проверяет непрослушанные аудио для пользователя."""
|
||||
query = """
|
||||
SELECT l.file_name
|
||||
FROM audio_message_reference a
|
||||
LEFT JOIN user_audio_listens l ON l.file_name = a.file_name
|
||||
WHERE l.user_id = ? AND l.file_name IS NOT NULL
|
||||
"""
|
||||
listened_files = await self._execute_query_with_result(query, (user_id,))
|
||||
|
||||
# Получаем все аудио, кроме созданных пользователем
|
||||
all_audio_query = 'SELECT file_name FROM audio_message_reference WHERE author_id <> ?'
|
||||
all_files = await self._execute_query_with_result(all_audio_query, (user_id,))
|
||||
|
||||
# Находим непрослушанные
|
||||
listened_set = {row[0] for row in listened_files}
|
||||
all_set = {row[0] for row in all_files}
|
||||
new_files = list(all_set - listened_set)
|
||||
|
||||
self.logger.info(f"Найдено {len(new_files)} непрослушанных аудио для пользователя {user_id}")
|
||||
return new_files
|
||||
|
||||
async def mark_listened_audio(self, file_name: str, user_id: int) -> None:
|
||||
"""Отмечает аудио прослушанным для пользователя."""
|
||||
query = "INSERT OR IGNORE INTO user_audio_listens (file_name, user_id) VALUES (?, ?)"
|
||||
params = (file_name, user_id)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Аудио {file_name} отмечено как прослушанное для пользователя {user_id}")
|
||||
|
||||
async def get_user_id_by_file_name(self, file_name: str) -> Optional[int]:
|
||||
"""Получает user_id пользователя по имени файла."""
|
||||
query = "SELECT author_id FROM audio_message_reference WHERE file_name = ?"
|
||||
rows = await self._execute_query_with_result(query, (file_name,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
user_id = row[0]
|
||||
self.logger.info(f"Получен user_id {user_id} для файла {file_name}")
|
||||
return user_id
|
||||
return None
|
||||
|
||||
async def get_date_by_file_name(self, file_name: str) -> Optional[str]:
|
||||
"""Получает дату добавления файла."""
|
||||
query = "SELECT date_added FROM audio_message_reference WHERE file_name = ?"
|
||||
rows = await self._execute_query_with_result(query, (file_name,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
date_added = row[0]
|
||||
# Преобразуем UNIX timestamp в читаемую дату
|
||||
readable_date = datetime.fromtimestamp(date_added).strftime('%d.%m.%Y %H:%M')
|
||||
self.logger.info(f"Получена дата {readable_date} для файла {file_name}")
|
||||
return readable_date
|
||||
return None
|
||||
|
||||
async def refresh_listen_audio(self, user_id: int) -> None:
|
||||
"""Очищает всю информацию о прослушанных аудио пользователем."""
|
||||
query = "DELETE FROM user_audio_listens WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Очищены записи прослушивания для пользователя {user_id}")
|
||||
|
||||
async def delete_listen_count_for_user(self, user_id: int) -> None:
|
||||
"""Удаляет данные о прослушанных пользователем аудио."""
|
||||
query = "DELETE FROM user_audio_listens WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Удалены записи прослушивания для пользователя {user_id}")
|
||||
|
||||
# Методы для voice bot
|
||||
async def set_user_id_and_message_id_for_voice_bot(self, message_id: int, user_id: int) -> bool:
|
||||
"""Устанавливает связь между message_id и user_id для voice bot."""
|
||||
try:
|
||||
query = "INSERT OR IGNORE INTO audio_moderate (user_id, message_id) VALUES (?, ?)"
|
||||
params = (user_id, message_id)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Связь установлена: message_id={message_id}, user_id={user_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка установки связи: {e}")
|
||||
return False
|
||||
|
||||
async def get_user_id_by_message_id_for_voice_bot(self, message_id: int) -> Optional[int]:
|
||||
"""Получает user_id пользователя по message_id для voice bot."""
|
||||
query = "SELECT user_id FROM audio_moderate WHERE message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
user_id = row[0]
|
||||
self.logger.info(f"Получен user_id {user_id} для message_id {message_id}")
|
||||
return user_id
|
||||
return None
|
||||
116
database/repositories/blacklist_repository.py
Normal file
116
database/repositories/blacklist_repository.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from typing import Optional, List, Dict
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import BlacklistUser
|
||||
|
||||
|
||||
class BlacklistRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с черным списком."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы черного списка."""
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS blacklist (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
message_for_user TEXT,
|
||||
date_to_unban INTEGER,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица черного списка создана")
|
||||
|
||||
async def add_user(self, blacklist_user: BlacklistUser) -> None:
|
||||
"""Добавляет пользователя в черный список."""
|
||||
query = """
|
||||
INSERT INTO blacklist (user_id, message_for_user, date_to_unban)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
params = (blacklist_user.user_id, blacklist_user.message_for_user, blacklist_user.date_to_unban)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Пользователь добавлен в черный список: user_id={blacklist_user.user_id}")
|
||||
|
||||
async def remove_user(self, user_id: int) -> bool:
|
||||
"""Удаляет пользователя из черного списка."""
|
||||
try:
|
||||
query = "DELETE FROM blacklist WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Пользователь с идентификатором {user_id} успешно удален из черного списка.")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка удаления пользователя с идентификатором {user_id} "
|
||||
f"из таблицы blacklist. Ошибка: {str(e)}")
|
||||
return False
|
||||
|
||||
async def user_exists(self, user_id: int) -> bool:
|
||||
"""Проверяет, существует ли запись с данным user_id в blacklist."""
|
||||
query = "SELECT 1 FROM blacklist WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
self.logger.info(f"Существует ли пользователь: user_id={user_id} Итог: {rows}")
|
||||
return bool(rows)
|
||||
|
||||
async def get_user(self, user_id: int) -> Optional[BlacklistUser]:
|
||||
"""Возвращает информацию о пользователе в черном списке по user_id."""
|
||||
query = "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return BlacklistUser(
|
||||
user_id=row[0],
|
||||
message_for_user=row[1],
|
||||
date_to_unban=row[2],
|
||||
created_at=row[3]
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_all_users(self, offset: int = 0, limit: int = 10) -> List[BlacklistUser]:
|
||||
"""Возвращает список пользователей в черном списке."""
|
||||
query = "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist LIMIT ?, ?"
|
||||
rows = await self._execute_query_with_result(query, (offset, limit))
|
||||
|
||||
users = []
|
||||
for row in rows:
|
||||
users.append(BlacklistUser(
|
||||
user_id=row[0],
|
||||
message_for_user=row[1],
|
||||
date_to_unban=row[2],
|
||||
created_at=row[3]
|
||||
))
|
||||
|
||||
self.logger.info(f"Получен список пользователей в черном списке (offset={offset}, limit={limit}): {len(users)}")
|
||||
return users
|
||||
|
||||
async def get_all_users_no_limit(self) -> List[BlacklistUser]:
|
||||
"""Возвращает список всех пользователей в черном списке без лимитов."""
|
||||
query = "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
|
||||
users = []
|
||||
for row in rows:
|
||||
users.append(BlacklistUser(
|
||||
user_id=row[0],
|
||||
message_for_user=row[1],
|
||||
date_to_unban=row[2],
|
||||
created_at=row[3]
|
||||
))
|
||||
|
||||
self.logger.info(f"Получен список всех пользователей в черном списке: {len(users)}")
|
||||
return users
|
||||
|
||||
async def get_users_for_unblock_today(self, current_timestamp: int) -> Dict[int, int]:
|
||||
"""Возвращает список пользователей, у которых истек срок блокировки."""
|
||||
query = "SELECT user_id FROM blacklist WHERE date_to_unban IS NOT NULL AND date_to_unban <= ?"
|
||||
rows = await self._execute_query_with_result(query, (current_timestamp,))
|
||||
|
||||
users = {user_id: user_id for user_id, in rows}
|
||||
self.logger.info(f"Получен список пользователей для разблокировки: {users}")
|
||||
return users
|
||||
|
||||
async def get_count(self) -> int:
|
||||
"""Получение количества пользователей в черном списке."""
|
||||
query = "SELECT COUNT(*) FROM blacklist"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else 0
|
||||
44
database/repositories/message_repository.py
Normal file
44
database/repositories/message_repository.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import UserMessage
|
||||
|
||||
|
||||
class MessageRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с сообщениями пользователей."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы сообщений пользователей."""
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT,
|
||||
user_id INTEGER,
|
||||
telegram_message_id INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица сообщений пользователей создана")
|
||||
|
||||
async def add_message(self, message: UserMessage) -> None:
|
||||
"""Добавление сообщения пользователя."""
|
||||
if message.date is None:
|
||||
message.date = int(datetime.now().timestamp())
|
||||
|
||||
query = """
|
||||
INSERT INTO user_messages (message_text, user_id, telegram_message_id, date)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
params = (message.message_text, message.user_id, message.telegram_message_id, message.date)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Новое сообщение добавлено: telegram_message_id={message.telegram_message_id}")
|
||||
|
||||
async def get_user_by_message_id(self, message_id: int) -> Optional[int]:
|
||||
"""Получение пользователя по message_id."""
|
||||
query = "SELECT user_id FROM user_messages WHERE telegram_message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (message_id,))
|
||||
row = rows[0] if rows else None
|
||||
return row[0] if row else None
|
||||
150
database/repositories/post_repository.py
Normal file
150
database/repositories/post_repository.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Tuple
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import TelegramPost, PostContent, MessageContentLink
|
||||
|
||||
|
||||
class PostRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с постами из Telegram."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблиц для постов."""
|
||||
# Таблица постов из Telegram
|
||||
post_query = '''
|
||||
CREATE TABLE IF NOT EXISTS post_from_telegram_suggest (
|
||||
message_id INTEGER NOT NULL PRIMARY KEY,
|
||||
text TEXT,
|
||||
helper_text_message_id INTEGER,
|
||||
author_id INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(post_query)
|
||||
|
||||
# Таблица контента постов
|
||||
content_query = '''
|
||||
CREATE TABLE IF NOT EXISTS content_post_from_telegram (
|
||||
message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
content_type TEXT,
|
||||
PRIMARY KEY (message_id, content_name),
|
||||
FOREIGN KEY (message_id) REFERENCES post_from_telegram_suggest (message_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(content_query)
|
||||
|
||||
# Таблица связи сообщений с контентом
|
||||
link_query = '''
|
||||
CREATE TABLE IF NOT EXISTS message_link_to_content (
|
||||
post_id INTEGER NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, message_id),
|
||||
FOREIGN KEY (post_id) REFERENCES post_from_telegram_suggest (message_id) ON DELETE CASCADE
|
||||
)
|
||||
'''
|
||||
await self._execute_query(link_query)
|
||||
|
||||
self.logger.info("Таблицы для постов созданы")
|
||||
|
||||
async def add_post(self, post: TelegramPost) -> None:
|
||||
"""Добавление поста."""
|
||||
if not post.created_at:
|
||||
post.created_at = int(datetime.now().timestamp())
|
||||
|
||||
query = """
|
||||
INSERT INTO post_from_telegram_suggest (message_id, text, author_id, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
params = (post.message_id, post.text, post.author_id, post.created_at)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Пост добавлен: message_id={post.message_id}")
|
||||
|
||||
async def update_helper_message(self, message_id: int, helper_message_id: int) -> None:
|
||||
"""Обновление helper сообщения."""
|
||||
query = "UPDATE post_from_telegram_suggest SET helper_text_message_id = ? WHERE message_id = ?"
|
||||
await self._execute_query(query, (helper_message_id, message_id))
|
||||
|
||||
async def add_post_content(self, post_id: int, message_id: int, content_name: str, content_type: str) -> bool:
|
||||
"""Добавление контента поста."""
|
||||
try:
|
||||
# Сначала добавляем связь
|
||||
link_query = "INSERT OR IGNORE INTO message_link_to_content (post_id, message_id) VALUES (?, ?)"
|
||||
await self._execute_query(link_query, (post_id, message_id))
|
||||
|
||||
# Затем добавляем контент
|
||||
content_query = """
|
||||
INSERT OR IGNORE INTO content_post_from_telegram (message_id, content_name, content_type)
|
||||
VALUES (?, ?, ?)
|
||||
"""
|
||||
await self._execute_query(content_query, (message_id, content_name, content_type))
|
||||
|
||||
self.logger.info(f"Контент поста добавлен: post_id={post_id}, message_id={message_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при добавлении контента поста: {e}")
|
||||
return False
|
||||
|
||||
async def get_post_content_by_helper_id(self, helper_message_id: int) -> List[Tuple[str, str]]:
|
||||
"""Получает контент поста по helper_text_message_id."""
|
||||
query = """
|
||||
SELECT cpft.content_name, cpft.content_type
|
||||
FROM post_from_telegram_suggest pft
|
||||
JOIN message_link_to_content mltc ON pft.message_id = mltc.post_id
|
||||
JOIN content_post_from_telegram cpft ON cpft.message_id = mltc.message_id
|
||||
WHERE pft.helper_text_message_id = ?
|
||||
"""
|
||||
post_content = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
|
||||
self.logger.info(f"Получен контент поста: {len(post_content)} элементов")
|
||||
return post_content
|
||||
|
||||
async def get_post_text_by_helper_id(self, helper_message_id: int) -> Optional[str]:
|
||||
"""Получает текст поста по helper_text_message_id."""
|
||||
query = "SELECT text FROM post_from_telegram_suggest WHERE helper_text_message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
self.logger.info(f"Получен текст поста для helper_message_id={helper_message_id}")
|
||||
return row[0]
|
||||
return None
|
||||
|
||||
async def get_post_ids_by_helper_id(self, helper_message_id: int) -> List[int]:
|
||||
"""Получает ID сообщений по helper_text_message_id."""
|
||||
query = """
|
||||
SELECT mltc.message_id
|
||||
FROM post_from_telegram_suggest pft
|
||||
JOIN message_link_to_content mltc ON pft.message_id = mltc.post_id
|
||||
WHERE pft.helper_text_message_id = ?
|
||||
"""
|
||||
rows = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
|
||||
post_ids = [row[0] for row in rows]
|
||||
self.logger.info(f"Получены ID сообщений: {len(post_ids)} элементов")
|
||||
return post_ids
|
||||
|
||||
async def get_author_id_by_message_id(self, message_id: int) -> Optional[int]:
|
||||
"""Получает ID автора по message_id."""
|
||||
query = "SELECT author_id FROM post_from_telegram_suggest WHERE message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
author_id = row[0]
|
||||
self.logger.info(f"Получен author_id: {author_id} для message_id={message_id}")
|
||||
return author_id
|
||||
return None
|
||||
|
||||
async def get_author_id_by_helper_message_id(self, helper_message_id: int) -> Optional[int]:
|
||||
"""Получает ID автора по helper_text_message_id."""
|
||||
query = "SELECT author_id FROM post_from_telegram_suggest WHERE helper_text_message_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (helper_message_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
author_id = row[0]
|
||||
self.logger.info(f"Получен author_id: {author_id} для helper_message_id={helper_message_id}")
|
||||
return author_id
|
||||
return None
|
||||
258
database/repositories/user_repository.py
Normal file
258
database/repositories/user_repository.py
Normal file
@@ -0,0 +1,258 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from database.base import DatabaseConnection
|
||||
from database.models import User
|
||||
|
||||
|
||||
class UserRepository(DatabaseConnection):
|
||||
"""Репозиторий для работы с пользователями."""
|
||||
|
||||
async def create_tables(self):
|
||||
"""Создание таблицы пользователей."""
|
||||
query = '''
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
full_name TEXT,
|
||||
username TEXT,
|
||||
is_bot BOOLEAN DEFAULT 0,
|
||||
language_code TEXT,
|
||||
has_stickers BOOLEAN DEFAULT 0 NOT NULL,
|
||||
emoji TEXT,
|
||||
date_added INTEGER NOT NULL,
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
)
|
||||
'''
|
||||
await self._execute_query(query)
|
||||
self.logger.info("Таблица пользователей создана")
|
||||
|
||||
async def user_exists(self, user_id: int) -> bool:
|
||||
"""Проверяет, существует ли пользователь в базе данных."""
|
||||
query = "SELECT user_id FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
self.logger.info(f"Проверка существования пользователя: user_id={user_id}, результат={rows}")
|
||||
return bool(len(rows))
|
||||
|
||||
async def add_user(self, user: User) -> None:
|
||||
"""Добавление нового пользователя."""
|
||||
if not user.date_added:
|
||||
user.date_added = int(datetime.now().timestamp())
|
||||
if not user.date_changed:
|
||||
user.date_changed = int(datetime.now().timestamp())
|
||||
|
||||
query = """
|
||||
INSERT INTO our_users (user_id, first_name, full_name, username, is_bot,
|
||||
language_code, emoji, has_stickers, date_added, date_changed, voice_bot_welcome_received)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
params = (user.user_id, user.first_name, user.full_name, user.username,
|
||||
user.is_bot, user.language_code, user.emoji, user.has_stickers,
|
||||
user.date_added, user.date_changed, user.voice_bot_welcome_received)
|
||||
|
||||
await self._execute_query(query, params)
|
||||
self.logger.info(f"Новый пользователь добавлен: {user.user_id}")
|
||||
|
||||
async def get_user_info(self, user_id: int) -> Optional[User]:
|
||||
"""Получение информации о пользователе."""
|
||||
query = "SELECT username, full_name, has_stickers, emoji FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return User(
|
||||
user_id=user_id,
|
||||
first_name="", # Не получаем из этого запроса
|
||||
full_name=row[1],
|
||||
username=row[0],
|
||||
has_stickers=bool(row[2]) if row[2] is not None else False,
|
||||
emoji=row[3]
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Получение пользователя по ID."""
|
||||
query = "SELECT * FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
return User(
|
||||
user_id=row[0],
|
||||
first_name=row[1],
|
||||
full_name=row[2],
|
||||
username=row[3],
|
||||
is_bot=bool(row[4]),
|
||||
language_code=row[5],
|
||||
has_stickers=bool(row[6]),
|
||||
emoji=row[7],
|
||||
date_added=row[8],
|
||||
date_changed=row[9],
|
||||
voice_bot_welcome_received=bool(row[10]) if len(row) > 10 else False
|
||||
)
|
||||
return None
|
||||
|
||||
async def get_username(self, user_id: int) -> Optional[str]:
|
||||
"""Возвращает username пользователя."""
|
||||
query = "SELECT username FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
username = row[0]
|
||||
self.logger.info(f"Username пользователя найден: user_id={user_id}, username={username}")
|
||||
return username
|
||||
return None
|
||||
|
||||
async def get_user_id_by_username(self, username: str) -> Optional[int]:
|
||||
"""Возвращает user_id пользователя по username."""
|
||||
query = "SELECT user_id FROM our_users WHERE username = ?"
|
||||
rows = await self._execute_query_with_result(query, (username,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
user_id = row[0]
|
||||
self.logger.info(f"User_id пользователя найден: username={username}, user_id={user_id}")
|
||||
return user_id
|
||||
return None
|
||||
|
||||
async def get_full_name_by_id(self, user_id: int) -> Optional[str]:
|
||||
"""Возвращает full_name пользователя."""
|
||||
query = "SELECT full_name FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
full_name = row[0]
|
||||
self.logger.info(f"Full_name пользователя найден: user_id={user_id}, full_name={full_name}")
|
||||
return full_name
|
||||
return None
|
||||
|
||||
async def get_user_first_name(self, user_id: int) -> Optional[str]:
|
||||
"""Возвращает first_name пользователя."""
|
||||
query = "SELECT first_name FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
first_name = row[0]
|
||||
self.logger.info(f"First_name пользователя найден: user_id={user_id}, first_name={first_name}")
|
||||
return first_name
|
||||
return None
|
||||
|
||||
async def get_all_user_ids(self) -> List[int]:
|
||||
"""Возвращает список всех user_id."""
|
||||
query = "SELECT user_id FROM our_users"
|
||||
rows = await self._execute_query_with_result(query)
|
||||
user_ids = [row[0] for row in rows]
|
||||
self.logger.info(f"Получен список всех user_id: {user_ids}")
|
||||
return user_ids
|
||||
|
||||
async def get_last_users(self, limit: int = 30) -> List[tuple]:
|
||||
"""Получение последних пользователей."""
|
||||
query = "SELECT full_name, user_id FROM our_users ORDER BY date_changed DESC LIMIT ?"
|
||||
rows = await self._execute_query_with_result(query, (limit,))
|
||||
return rows
|
||||
|
||||
async def update_user_date(self, user_id: int) -> None:
|
||||
"""Обновление даты последнего изменения пользователя."""
|
||||
date_changed = int(datetime.now().timestamp())
|
||||
query = "UPDATE our_users SET date_changed = ? WHERE user_id = ?"
|
||||
await self._execute_query(query, (date_changed, user_id))
|
||||
|
||||
async def update_user_info(self, user_id: int, username: str = None, full_name: str = None) -> None:
|
||||
"""Обновление информации о пользователе."""
|
||||
if username and full_name:
|
||||
query = "UPDATE our_users SET username = ?, full_name = ? WHERE user_id = ?"
|
||||
params = (username, full_name, user_id)
|
||||
elif username:
|
||||
query = "UPDATE our_users SET username = ? WHERE user_id = ?"
|
||||
params = (username, user_id)
|
||||
elif full_name:
|
||||
query = "UPDATE our_users SET full_name = ? WHERE user_id = ?"
|
||||
params = (full_name, user_id)
|
||||
else:
|
||||
return
|
||||
|
||||
await self._execute_query(query, params)
|
||||
|
||||
async def update_user_emoji(self, user_id: int, emoji: str) -> None:
|
||||
"""Обновление эмодзи пользователя."""
|
||||
query = "UPDATE our_users SET emoji = ? WHERE user_id = ?"
|
||||
await self._execute_query(query, (emoji, user_id))
|
||||
|
||||
async def update_stickers_info(self, user_id: int) -> None:
|
||||
"""Обновление информации о стикерах."""
|
||||
query = "UPDATE our_users SET has_stickers = 1 WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
|
||||
async def get_stickers_info(self, user_id: int) -> bool:
|
||||
"""Получение информации о стикерах."""
|
||||
query = "SELECT has_stickers FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
return bool(row[0]) if row and row[0] is not None else False
|
||||
|
||||
async def check_emoji_exists(self, emoji: str) -> bool:
|
||||
"""Проверка существования эмодзи."""
|
||||
query = "SELECT 1 FROM our_users WHERE emoji = ?"
|
||||
rows = await self._execute_query_with_result(query, (emoji,))
|
||||
row = rows[0] if rows else None
|
||||
return bool(row)
|
||||
|
||||
async def get_user_emoji(self, user_id: int) -> str:
|
||||
"""
|
||||
Получает эмодзи пользователя.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя.
|
||||
|
||||
Returns:
|
||||
str: Эмодзи пользователя или "Смайл еще не определен" если не установлен.
|
||||
"""
|
||||
query = "SELECT emoji FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row and row[0]:
|
||||
emoji = row[0]
|
||||
self.logger.info(f"Эмодзи пользователя найден: user_id={user_id}, emoji={emoji}")
|
||||
return str(emoji)
|
||||
else:
|
||||
self.logger.info(f"Эмодзи пользователя не найден: user_id={user_id}")
|
||||
return "Смайл еще не определен"
|
||||
|
||||
async def check_emoji_for_user(self, user_id: int) -> str:
|
||||
"""
|
||||
Проверяет, есть ли уже у пользователя назначенный emoji.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя.
|
||||
|
||||
Returns:
|
||||
str: Эмодзи пользователя или "Смайл еще не определен" если не установлен.
|
||||
"""
|
||||
return await self.get_user_emoji(user_id)
|
||||
|
||||
async def check_voice_bot_welcome_received(self, user_id: int) -> bool:
|
||||
"""Проверяет, получал ли пользователь приветственное сообщение от voice_bot."""
|
||||
query = "SELECT voice_bot_welcome_received FROM our_users WHERE user_id = ?"
|
||||
rows = await self._execute_query_with_result(query, (user_id,))
|
||||
row = rows[0] if rows else None
|
||||
|
||||
if row:
|
||||
welcome_received = bool(row[0])
|
||||
self.logger.info(f"Пользователь {user_id} получал приветствие: {welcome_received}")
|
||||
return welcome_received
|
||||
return False
|
||||
|
||||
async def mark_voice_bot_welcome_received(self, user_id: int) -> bool:
|
||||
"""Отмечает, что пользователь получил приветственное сообщение от voice_bot."""
|
||||
try:
|
||||
query = "UPDATE our_users SET voice_bot_welcome_received = 1 WHERE user_id = ?"
|
||||
await self._execute_query(query, (user_id,))
|
||||
self.logger.info(f"Пользователь {user_id} отмечен как получивший приветствие")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка при отметке получения приветствия: {e}")
|
||||
return False
|
||||
79
database/repository_factory.py
Normal file
79
database/repository_factory.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from typing import Optional
|
||||
from database.repositories.user_repository import UserRepository
|
||||
from database.repositories.blacklist_repository import BlacklistRepository
|
||||
from database.repositories.message_repository import MessageRepository
|
||||
from database.repositories.post_repository import PostRepository
|
||||
from database.repositories.admin_repository import AdminRepository
|
||||
from database.repositories.audio_repository import AudioRepository
|
||||
|
||||
|
||||
class RepositoryFactory:
|
||||
"""Фабрика для создания репозиториев."""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self._user_repo: Optional[UserRepository] = None
|
||||
self._blacklist_repo: Optional[BlacklistRepository] = None
|
||||
self._message_repo: Optional[MessageRepository] = None
|
||||
self._post_repo: Optional[PostRepository] = None
|
||||
self._admin_repo: Optional[AdminRepository] = None
|
||||
self._audio_repo: Optional[AudioRepository] = None
|
||||
|
||||
@property
|
||||
def users(self) -> UserRepository:
|
||||
"""Возвращает репозиторий пользователей."""
|
||||
if self._user_repo is None:
|
||||
self._user_repo = UserRepository(self.db_path)
|
||||
return self._user_repo
|
||||
|
||||
@property
|
||||
def blacklist(self) -> BlacklistRepository:
|
||||
"""Возвращает репозиторий черного списка."""
|
||||
if self._blacklist_repo is None:
|
||||
self._blacklist_repo = BlacklistRepository(self.db_path)
|
||||
return self._blacklist_repo
|
||||
|
||||
@property
|
||||
def messages(self) -> MessageRepository:
|
||||
"""Возвращает репозиторий сообщений."""
|
||||
if self._message_repo is None:
|
||||
self._message_repo = MessageRepository(self.db_path)
|
||||
return self._message_repo
|
||||
|
||||
@property
|
||||
def posts(self) -> PostRepository:
|
||||
"""Возвращает репозиторий постов."""
|
||||
if self._post_repo is None:
|
||||
self._post_repo = PostRepository(self.db_path)
|
||||
return self._post_repo
|
||||
|
||||
@property
|
||||
def admins(self) -> AdminRepository:
|
||||
"""Возвращает репозиторий администраторов."""
|
||||
if self._admin_repo is None:
|
||||
self._admin_repo = AdminRepository(self.db_path)
|
||||
return self._admin_repo
|
||||
|
||||
@property
|
||||
def audio(self) -> AudioRepository:
|
||||
"""Возвращает репозиторий аудио."""
|
||||
if self._audio_repo is None:
|
||||
self._audio_repo = AudioRepository(self.db_path)
|
||||
return self._audio_repo
|
||||
|
||||
async def create_all_tables(self):
|
||||
"""Создает все таблицы в базе данных."""
|
||||
await self.users.create_tables()
|
||||
await self.blacklist.create_tables()
|
||||
await self.messages.create_tables()
|
||||
await self.posts.create_tables()
|
||||
await self.admins.create_tables()
|
||||
await self.audio.create_tables()
|
||||
|
||||
async def check_database_integrity(self):
|
||||
"""Проверяет целостность базы данных."""
|
||||
await self.users.check_database_integrity()
|
||||
|
||||
async def cleanup_wal_files(self):
|
||||
"""Очищает WAL файлы."""
|
||||
await self.users.cleanup_wal_files()
|
||||
@@ -1,14 +1,18 @@
|
||||
-- Telegram Helper Bot Database Schema
|
||||
-- Compatible with Docker container deployment
|
||||
|
||||
-- IMPORTANT: Enable foreign key support after each database connection
|
||||
-- PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Note: sqlite_sequence table is automatically created by SQLite for AUTOINCREMENT fields
|
||||
-- No need to create it manually
|
||||
|
||||
-- Users who have listened to audio messages
|
||||
CREATE TABLE IF NOT EXISTS listen_audio_users (
|
||||
CREATE TABLE IF NOT EXISTS user_audio_listens (
|
||||
file_name TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
is_listen BOOLEAN NOT NULL DEFAULT 0
|
||||
PRIMARY KEY (file_name, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Reference table for audio messages
|
||||
@@ -16,29 +20,24 @@ CREATE TABLE IF NOT EXISTS audio_message_reference (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
file_name TEXT NOT NULL UNIQUE,
|
||||
author_id INTEGER NOT NULL,
|
||||
date_added DATE NOT NULL,
|
||||
listen_count INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- Database migrations tracking
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
version INTEGER NOT NULL PRIMARY KEY,
|
||||
script_name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
date_added INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Bot administrators
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
role TEXT
|
||||
role TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- User blacklist for banned users
|
||||
CREATE TABLE IF NOT EXISTS blacklist (
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
user_name TEXT,
|
||||
message_for_user TEXT,
|
||||
date_to_unban INTEGER
|
||||
date_to_unban INTEGER,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- User message history
|
||||
@@ -46,8 +45,9 @@ CREATE TABLE IF NOT EXISTS user_messages (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
message_text TEXT,
|
||||
user_id INTEGER,
|
||||
message_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL
|
||||
telegram_message_id INTEGER NOT NULL,
|
||||
date INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Suggested posts from Telegram
|
||||
@@ -56,14 +56,16 @@ CREATE TABLE IF NOT EXISTS post_from_telegram_suggest (
|
||||
text TEXT,
|
||||
helper_text_message_id INTEGER,
|
||||
author_id INTEGER,
|
||||
created_at TEXT NOT NULL
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Links between posts and content
|
||||
CREATE TABLE IF NOT EXISTS message_link_to_content (
|
||||
post_id INTEGER NOT NULL,
|
||||
message_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (post_id, message_id)
|
||||
PRIMARY KEY (post_id, message_id),
|
||||
FOREIGN KEY (post_id) REFERENCES post_from_telegram_suggest(message_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Content associated with Telegram posts
|
||||
@@ -71,22 +73,22 @@ CREATE TABLE IF NOT EXISTS content_post_from_telegram (
|
||||
message_id INTEGER NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
content_type TEXT,
|
||||
PRIMARY KEY (message_id, content_name)
|
||||
PRIMARY KEY (message_id, content_name),
|
||||
FOREIGN KEY (message_id) REFERENCES post_from_telegram_suggest(message_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Bot users information
|
||||
-- Bot users information (user_id is now PRIMARY KEY)
|
||||
CREATE TABLE IF NOT EXISTS our_users (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||
first_name TEXT,
|
||||
full_name TEXT,
|
||||
username TEXT,
|
||||
is_bot BOOLEAN DEFAULT 0,
|
||||
language_code TEXT,
|
||||
has_stickers INTEGER DEFAULT 0 NOT NULL,
|
||||
has_stickers BOOLEAN DEFAULT 0 NOT NULL,
|
||||
emoji TEXT,
|
||||
date_added DATE NOT NULL,
|
||||
date_changed DATE NOT NULL,
|
||||
date_added INTEGER NOT NULL,
|
||||
date_changed INTEGER NOT NULL,
|
||||
voice_bot_welcome_received BOOLEAN DEFAULT 0
|
||||
);
|
||||
|
||||
@@ -94,14 +96,18 @@ CREATE TABLE IF NOT EXISTS our_users (
|
||||
CREATE TABLE IF NOT EXISTS audio_moderate (
|
||||
user_id INTEGER NOT NULL,
|
||||
message_id INTEGER,
|
||||
PRIMARY KEY (user_id, message_id)
|
||||
PRIMARY KEY (user_id, message_id),
|
||||
FOREIGN KEY (user_id) REFERENCES our_users(user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_listen_audio_users_file_name ON listen_audio_users(file_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_listen_audio_users_user_id ON listen_audio_users(user_id);
|
||||
-- Optimized index for user_audio_listens - only user_id for "show all audio listened by user X"
|
||||
CREATE INDEX IF NOT EXISTS idx_user_audio_listens_user_id ON user_audio_listens(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_message_reference_author_id ON audio_message_reference(author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_messages_user_id ON user_messages(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_author_id ON post_from_telegram_suggest(author_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_our_users_user_id ON our_users(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_moderate_user_id ON audio_moderate(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_blacklist_date_to_unban ON blacklist(date_to_unban);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_messages_date ON user_messages(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_audio_message_reference_date ON audio_message_reference(date_added);
|
||||
CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_date ON post_from_telegram_suggest(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_our_users_date_changed ON our_users(date_changed);
|
||||
|
||||
Reference in New Issue
Block a user