Merge remote-tracking branch 'origin/master' into dev-13

This commit is contained in:
2026-02-02 00:12:09 +03:00
105 changed files with 8845 additions and 8665 deletions

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт миграции для добавления колонки ban_author в таблицу blacklist.
Колонка хранит user_id администратора, инициировавшего бан.
"""
import argparse
import asyncio
import os
import sys
from pathlib import Path
import aiosqlite
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
from logs.custom_logger import logger # noqa: E402
DEFAULT_DB_PATH = "database/tg-bot-database.db"
def _column_exists(rows: list, name: str) -> bool:
"""PRAGMA table_info returns (cid, name, type, notnull, dflt_value, pk)."""
for row in rows:
if row[1] == name:
return True
return False
async def main(db_path: str) -> None:
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error("База данных не найдена: %s", db_path)
print(f"Ошибка: база данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
# Проверяем наличие колонки ban_author
cursor = await conn.execute("PRAGMA table_info(blacklist)")
rows = await cursor.fetchall()
await cursor.close()
if not _column_exists(rows, "ban_author"):
logger.info("Добавление колонки ban_author в blacklist")
await conn.execute(
"ALTER TABLE blacklist "
"ADD COLUMN ban_author INTEGER REFERENCES our_users (user_id) ON DELETE SET NULL"
)
await conn.commit()
print("Колонка ban_author добавлена в таблицу blacklist.")
else:
print("Колонка ban_author уже существует в таблице blacklist.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Добавление колонки ban_author в blacklist"
)
parser.add_argument(
"--db",
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или DB_PATH)",
)
args = parser.parse_args()
asyncio.run(main(args.db))

View File

@@ -1,139 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт миграции для добавления колонки is_anonymous в таблицу post_from_telegram_suggest.
Для существующих записей определяет is_anonymous на основе текста или устанавливает NULL.
"""
import argparse
import asyncio
import os
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
from helper_bot.utils.helper_func import determine_anonymity
from logs.custom_logger import logger
DEFAULT_DB_PATH = "database/tg-bot-database.db"
def _column_exists(rows: list, name: str) -> bool:
"""PRAGMA table_info returns (cid, name, type, notnull, dflt_value, pk)."""
for row in rows:
if row[1] == name:
return True
return False
async def main(db_path: str) -> None:
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error("База данных не найдена: %s", db_path)
print(f"Ошибка: база данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
# Проверяем наличие колонки is_anonymous
cursor = await conn.execute("PRAGMA table_info(post_from_telegram_suggest)")
rows = await cursor.fetchall()
await cursor.close()
if not _column_exists(rows, "is_anonymous"):
logger.info("Добавление колонки is_anonymous в post_from_telegram_suggest")
await conn.execute(
"ALTER TABLE post_from_telegram_suggest "
"ADD COLUMN is_anonymous INTEGER"
)
await conn.commit()
print("Колонка is_anonymous добавлена.")
else:
print("Колонка is_anonymous уже существует.")
# Получаем все записи с текстом для обновления
cursor = await conn.execute(
"SELECT message_id, text FROM post_from_telegram_suggest WHERE text IS NOT NULL"
)
posts = await cursor.fetchall()
await cursor.close()
updated_count = 0
null_count = 0
# Обновляем каждую запись
for message_id, text in posts:
try:
# Определяем is_anonymous на основе текста
# Если текст пустой или None, устанавливаем NULL (legacy)
if not text or not text.strip():
is_anonymous = None
else:
is_anonymous = determine_anonymity(text)
# Преобразуем bool в int для SQLite (True -> 1, False -> 0, None -> None)
is_anonymous_int = (
None if is_anonymous is None else (1 if is_anonymous else 0)
)
await conn.execute(
"UPDATE post_from_telegram_suggest SET is_anonymous = ? WHERE message_id = ?",
(is_anonymous_int, message_id),
)
if is_anonymous is not None:
updated_count += 1
else:
null_count += 1
except Exception as e:
logger.error(f"Ошибка при обработке поста message_id={message_id}: {e}")
# В случае ошибки устанавливаем NULL
await conn.execute(
"UPDATE post_from_telegram_suggest SET is_anonymous = NULL WHERE message_id = ?",
(message_id,),
)
null_count += 1
# Обновляем записи без текста (устанавливаем NULL)
cursor = await conn.execute(
"SELECT COUNT(*) FROM post_from_telegram_suggest WHERE text IS NULL"
)
row = await cursor.fetchone()
posts_without_text = row[0] if row else 0
await cursor.close()
if posts_without_text > 0:
await conn.execute(
"UPDATE post_from_telegram_suggest SET is_anonymous = NULL WHERE text IS NULL"
)
null_count += posts_without_text
await conn.commit()
total_updated = updated_count + null_count
logger.info(
f"Миграция завершена. Обновлено записей: {total_updated} "
f"(определено: {updated_count}, установлено NULL: {null_count})"
)
print(f"Миграция завершена.")
print(f"Обновлено записей: {total_updated}")
print(f" - Определено is_anonymous: {updated_count}")
print(f" - Установлено NULL: {null_count}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Добавление колонки is_anonymous в post_from_telegram_suggest"
)
parser.add_argument(
"--db",
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или DB_PATH)",
)
args = parser.parse_args()
asyncio.run(main(args.db))

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
Миграция: Добавление колонки для ML-скоринга постов.
Добавляет:
- ml_scores (TEXT/JSON) - JSON с результатами оценки от разных моделей
Структура ml_scores:
{
"deepseek": {"score": 0.75, "model": "deepseek-chat", "ts": 1706198400},
"rag": {"score": 0.90, "model": "rubert-base-cased", "ts": 1706198400}
}
"""
import argparse
import asyncio
import os
import sys
from pathlib import Path
# Добавляем корень проекта в путь
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
# Пытаемся импортировать logger, если не получается - используем стандартный
try:
from logs.custom_logger import logger
except ImportError:
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
DEFAULT_DB_PATH = "database/tg-bot-database.db"
async def column_exists(conn: aiosqlite.Connection, table: str, column: str) -> bool:
"""Проверяет существование колонки в таблице."""
cursor = await conn.execute(f"PRAGMA table_info({table})")
columns = await cursor.fetchall()
return any(col[1] == column for col in columns)
async def main(db_path: str) -> None:
"""
Основная функция миграции.
Добавляет колонку ml_scores в таблицу post_from_telegram_suggest.
Миграция идемпотентна - можно запускать повторно без ошибок.
"""
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error(f"База данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
# Проверяем и добавляем колонку ml_scores
if not await column_exists(conn, "post_from_telegram_suggest", "ml_scores"):
await conn.execute(
"ALTER TABLE post_from_telegram_suggest ADD COLUMN ml_scores TEXT"
)
logger.info("Колонка ml_scores добавлена в post_from_telegram_suggest")
else:
logger.info("Колонка ml_scores уже существует")
await conn.commit()
logger.info("Миграция add_ml_scores_columns завершена успешно")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Добавление колонки ml_scores для ML-скоринга"
)
parser.add_argument(
"--db",
default=os.environ.get("DATABASE_PATH", DEFAULT_DB_PATH),
help="Путь к БД",
)
args = parser.parse_args()
asyncio.run(main(args.db))

View File

@@ -1,181 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт миграции для добавления поддержки опубликованных постов:
1. Добавляет колонку published_message_id в таблицу post_from_telegram_suggest
2. Создает таблицу published_post_content для хранения медиафайлов опубликованных постов
3. Создает индексы для производительности
"""
import argparse
import asyncio
import os
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
from logs.custom_logger import logger
DEFAULT_DB_PATH = "database/tg-bot-database.db"
def _column_exists(rows: list, name: str) -> bool:
"""Проверяет существование колонки в таблице.
PRAGMA table_info returns (cid, name, type, notnull, dflt_value, pk)."""
for row in rows:
if row[1] == name:
return True
return False
async def main(db_path: str, dry_run: bool = False) -> None:
"""Выполняет миграцию БД для поддержки опубликованных постов."""
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error("База данных не найдена: %s", db_path)
print(f"Ошибка: база данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
changes_made = []
# 1. Проверяем и добавляем колонку published_message_id
cursor = await conn.execute("PRAGMA table_info(post_from_telegram_suggest)")
rows = await cursor.fetchall()
await cursor.close()
if not _column_exists(rows, "published_message_id"):
if dry_run:
print(
"DRY RUN: Будет добавлена колонка published_message_id в post_from_telegram_suggest"
)
changes_made.append("Добавление колонки published_message_id")
else:
logger.info(
"Добавление колонки published_message_id в post_from_telegram_suggest"
)
await conn.execute(
"ALTER TABLE post_from_telegram_suggest "
"ADD COLUMN published_message_id INTEGER"
)
await conn.commit()
print(
"✓ Колонка published_message_id добавлена в post_from_telegram_suggest"
)
changes_made.append("Добавлена колонка published_message_id")
else:
print(
"✓ Колонка published_message_id уже существует в post_from_telegram_suggest"
)
# 2. Проверяем и создаем таблицу published_post_content
cursor = await conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='published_post_content'"
)
table_exists = await cursor.fetchone()
await cursor.close()
if not table_exists:
if dry_run:
print("DRY RUN: Будет создана таблица published_post_content")
changes_made.append("Создание таблицы published_post_content")
else:
logger.info("Создание таблицы published_post_content")
await conn.execute("""
CREATE TABLE IF NOT EXISTS published_post_content (
published_message_id INTEGER NOT NULL,
content_name TEXT NOT NULL,
content_type TEXT,
published_at INTEGER NOT NULL,
PRIMARY KEY (published_message_id, content_name)
)
""")
await conn.commit()
print("✓ Таблица published_post_content создана")
changes_made.append("Создана таблица published_post_content")
else:
print("✓ Таблица published_post_content уже существует")
# 3. Проверяем и создаем индексы
indexes = [
(
"idx_published_post_content_message_id",
"CREATE INDEX IF NOT EXISTS idx_published_post_content_message_id "
"ON published_post_content(published_message_id)",
),
(
"idx_post_from_telegram_suggest_published",
"CREATE INDEX IF NOT EXISTS idx_post_from_telegram_suggest_published "
"ON post_from_telegram_suggest(published_message_id)",
),
]
for index_name, index_sql in indexes:
cursor = await conn.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
(index_name,),
)
index_exists = await cursor.fetchone()
await cursor.close()
if not index_exists:
if dry_run:
print(f"DRY RUN: Будет создан индекс {index_name}")
changes_made.append(f"Создание индекса {index_name}")
else:
logger.info(f"Создание индекса {index_name}")
await conn.execute(index_sql)
await conn.commit()
print(f"✓ Индекс {index_name} создан")
changes_made.append(f"Создан индекс {index_name}")
else:
print(f"✓ Индекс {index_name} уже существует")
# Финальная статистика
if dry_run:
if changes_made:
print("\n" + "=" * 60)
print("DRY RUN: Следующие изменения будут выполнены:")
for change in changes_made:
print(f" - {change}")
print("=" * 60)
else:
print(
"\nВсе необходимые изменения уже применены. Ничего делать не нужно."
)
else:
if changes_made:
logger.info(
f"Миграция завершена. Выполнено изменений: {len(changes_made)}"
)
print(f"\n✓ Миграция завершена успешно!")
print(f"Выполнено изменений: {len(changes_made)}")
for change in changes_made:
print(f" - {change}")
else:
print(
"\nВсе необходимые изменения уже применены. Ничего делать не нужно."
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Добавление поддержки опубликованных постов в БД"
)
parser.add_argument(
"--db",
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или переменная окружения DB_PATH)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Показать что будет сделано без выполнения изменений",
)
args = parser.parse_args()
asyncio.run(main(args.db, dry_run=args.dry_run))

241
scripts/apply_migrations.py Normal file
View File

@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
Скрипт для автоматического применения миграций базы данных.
Сканирует папку scripts/ и применяет все новые миграции, которые еще не были применены.
"""
import argparse
import asyncio
import importlib.util
import os
import subprocess
import sys
from pathlib import Path
from typing import List, Tuple
# Исключаем служебные скрипты из миграций
EXCLUDED_SCRIPTS = {
'apply_migrations.py',
'test_s3_connection.py',
'voice_cleanup.py',
}
DEFAULT_DB_PATH = "database/tg-bot-database.db"
def get_migration_scripts(scripts_dir: Path) -> List[Tuple[str, Path]]:
"""
Получает список скриптов миграций из папки scripts.
Возвращает список кортежей (имя_файла, путь_к_файлу), отсортированный по имени файла.
"""
scripts = []
for script_file in sorted(scripts_dir.glob("*.py")):
if script_file.name not in EXCLUDED_SCRIPTS:
scripts.append((script_file.name, script_file))
return scripts
async def is_migration_script(script_path: Path) -> bool:
"""
Проверяет, является ли скрипт миграцией.
Миграция должна иметь функцию main() с параметром db_path.
"""
try:
spec = importlib.util.spec_from_file_location("migration_script", script_path)
if spec is None or spec.loader is None:
return False
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(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 False
except Exception:
# Если не удалось проверить, считаем что это не миграция
return False
async def apply_migration(script_path: Path, db_path: str) -> bool:
"""
Применяет миграцию, запуская скрипт.
Returns:
True если миграция применена успешно, False в противном случае.
"""
script_name = script_path.name
try:
# Запускаем скрипт как отдельный процесс
result = subprocess.run(
[sys.executable, str(script_path), "--db", db_path],
cwd=script_path.parent.parent,
capture_output=True,
text=True,
timeout=300 # 5 минут максимум на миграцию
)
if result.returncode == 0:
if result.stdout:
print(f" {result.stdout.strip()}")
return True
else:
print(f" ❌ Ошибка:")
if result.stdout:
print(f" STDOUT: {result.stdout}")
if result.stderr:
print(f" STDERR: {result.stderr}")
return False
except subprocess.TimeoutExpired:
print(f" ❌ Превышен лимит времени (5 минут)")
return False
except Exception as e:
print(f" ❌ Ошибка: {e}")
return False
async def main(db_path: str, dry_run: bool = False) -> None:
"""
Основная функция для применения миграций.
Args:
db_path: Путь к базе данных
dry_run: Если True, только показывает какие миграции будут применены
"""
# Импортируем зависимости только когда они действительно нужны
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
# Проверяем наличие необходимых зависимостей
try:
import aiosqlite
except ImportError:
print("❌ Ошибка: модуль aiosqlite не установлен.")
print("💡 Установите зависимости: pip install -r requirements.txt")
sys.exit(1)
# Импортируем logger
try:
from logs.custom_logger import logger
except ImportError:
import logging
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"
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)
if spec is None or spec.loader is None:
print("Не удалось загрузить модуль migration_repository")
sys.exit(1)
migration_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(migration_module)
MigrationRepository = migration_module.MigrationRepository
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error(f"База данных не найдена: {db_path}")
print(f"❌ Ошибка: база данных не найдена: {db_path}")
return
scripts_dir = project_root / "scripts"
if not scripts_dir.exists():
logger.error(f"Папка scripts не найдена: {scripts_dir}")
print(f"❌ Ошибка: папка scripts не найдена: {scripts_dir}")
return
# Инициализируем репозиторий миграций напрямую
migration_repo = MigrationRepository(db_path)
await migration_repo.create_table()
# Получаем список примененных миграций
applied_migrations = await migration_repo.get_applied_migrations()
logger.info(f"Примененных миграций: {len(applied_migrations)}")
# Получаем все скрипты миграций
all_scripts = get_migration_scripts(scripts_dir)
# Фильтруем только миграции
migration_scripts = []
for script_name, script_path in all_scripts:
if await is_migration_script(script_path):
migration_scripts.append((script_name, script_path))
else:
logger.debug(f"Скрипт {script_name} не является миграцией, пропускаем")
# Находим новые миграции
new_migrations = [
(name, path) for name, path in migration_scripts
if name not in applied_migrations
]
if not new_migrations:
print("Все миграции уже применены")
logger.info("Новых миграций не найдено")
return
print(f"📋 Найдено новых миграций: {len(new_migrations)}")
for name, _ in new_migrations:
print(f" - {name}")
if dry_run:
print("\n🔍 DRY RUN: миграции не будут применены")
return
# Применяем миграции по порядку
print("\n🚀 Применение миграций...")
failed_migrations = []
for script_name, script_path in new_migrations:
print(f"📝 {script_name}...", end=" ", flush=True)
success = await apply_migration(script_path, db_path)
if success:
# Отмечаем миграцию как примененную
await migration_repo.mark_migration_applied(script_name)
print("")
else:
failed_migrations.append(script_name)
print("")
logger.error(f"Не удалось применить миграцию: {script_name}")
# Прерываем выполнение при ошибке
print(f"\n⚠️ Прерывание: миграция {script_name} завершилась с ошибкой")
break
if failed_migrations:
print(f"\nНе удалось применить {len(failed_migrations)} миграций:")
for name in failed_migrations:
print(f" - {name}")
sys.exit(1)
else:
print(f"\nВсе миграции применены успешно ({len(new_migrations)} шт.)")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Применение миграций базы данных"
)
parser.add_argument(
"--db",
default=os.environ.get("DATABASE_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или DATABASE_PATH из env)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Показать какие миграции будут применены без фактического применения",
)
args = parser.parse_args()
asyncio.run(main(args.db, args.dry_run))

View File

@@ -1,79 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт для проставления status='legacy' всем существующим записям в post_from_telegram_suggest.
Добавляет колонку status, если её нет, затем обновляет все строки.
"""
import argparse
import asyncio
import os
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
from logs.custom_logger import logger
DEFAULT_DB_PATH = "database/tg-bot-database.db"
def _column_exists(rows: list, name: str) -> bool:
"""PRAGMA table_info returns (cid, name, type, notnull, dflt_value, pk)."""
for row in rows:
if row[1] == name:
return True
return False
async def main(db_path: str) -> None:
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error("База данных не найдена: %s", db_path)
print(f"Ошибка: база данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
# Проверяем наличие колонки status
cursor = await conn.execute("PRAGMA table_info(post_from_telegram_suggest)")
rows = await cursor.fetchall()
await cursor.close()
if not _column_exists(rows, "status"):
logger.info("Добавление колонки status в post_from_telegram_suggest")
await conn.execute(
"ALTER TABLE post_from_telegram_suggest "
"ADD COLUMN status TEXT NOT NULL DEFAULT 'suggest'"
)
await conn.commit()
print("Колонка status добавлена.")
else:
print("Колонка status уже существует.")
# Обновляем все существующие записи на legacy
await conn.execute("UPDATE post_from_telegram_suggest SET status = 'legacy'")
await conn.commit()
cursor = await conn.execute("SELECT changes()")
row = await cursor.fetchone()
updated = row[0] if row else 0
await cursor.close()
logger.info("Обновлено записей в post_from_telegram_suggest: %d", updated)
print(f"Обновлено записей: {updated}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Backfill status='legacy' для post_from_telegram_suggest"
)
parser.add_argument(
"--db",
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или DB_PATH)",
)
args = parser.parse_args()
asyncio.run(main(args.db))

View File

@@ -1,149 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт для приведения текста постов к "сырому" виду.
Удаляет форматирование, добавленное функцией get_text_message(), оставляя только исходный текст.
"""
import argparse
import asyncio
import html
import os
import re
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
from logs.custom_logger import logger
DEFAULT_DB_PATH = "database/tg-bot-database.db"
# Паттерны для определения форматированного текста
PREFIX = "Пост из ТГ:\n"
ANONYMOUS_SUFFIX = "\n\nПост опубликован анонимно"
AUTHOR_SUFFIX_PATTERN = re.compile(r"\n\nАвтор поста: .+$")
def extract_raw_text(formatted_text: str) -> str:
"""
Извлекает сырой текст из форматированного текста поста.
Args:
formatted_text: Форматированный текст поста
Returns:
str: Сырой текст или исходный текст, если форматирование не обнаружено
"""
if not formatted_text:
return ""
# Проверяем, начинается ли текст с префикса
if not formatted_text.startswith(PREFIX):
# Текст уже в сыром виде или имеет другой формат
return formatted_text
# Извлекаем текст после префикса
text_after_prefix = formatted_text[len(PREFIX) :]
# Проверяем, заканчивается ли текст на "Пост опубликован анонимно"
if text_after_prefix.endswith(ANONYMOUS_SUFFIX):
raw_text = text_after_prefix[: -len(ANONYMOUS_SUFFIX)]
# Проверяем, заканчивается ли текст на "Автор поста: ..."
elif AUTHOR_SUFFIX_PATTERN.search(text_after_prefix):
raw_text = AUTHOR_SUFFIX_PATTERN.sub("", text_after_prefix)
else:
# Не удалось определить формат, возвращаем текст без префикса
raw_text = text_after_prefix
# Декодируем HTML-экранирование
raw_text = html.unescape(raw_text)
return raw_text
async def main(db_path: str, dry_run: bool = False) -> None:
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error("База данных не найдена: %s", db_path)
print(f"Ошибка: база данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
# Получаем все записи с текстом
cursor = await conn.execute(
"SELECT message_id, text FROM post_from_telegram_suggest WHERE text IS NOT NULL AND text != ''"
)
posts = await cursor.fetchall()
await cursor.close()
updated_count = 0
skipped_count = 0
error_count = 0
print(f"Найдено записей для обработки: {len(posts)}")
if dry_run:
print("РЕЖИМ ПРОВЕРКИ (dry-run): изменения не будут сохранены")
# Обрабатываем каждую запись
for message_id, formatted_text in posts:
try:
# Извлекаем сырой текст
raw_text = extract_raw_text(formatted_text)
# Проверяем, изменился ли текст
if raw_text == formatted_text:
skipped_count += 1
continue
if dry_run:
print(f"\n[DRY-RUN] message_id={message_id}:")
print(f" Было: {formatted_text[:100]}...")
print(f" Станет: {raw_text[:100]}...")
else:
# Обновляем запись
await conn.execute(
"UPDATE post_from_telegram_suggest SET text = ? WHERE message_id = ?",
(raw_text, message_id),
)
updated_count += 1
except Exception as e:
logger.error(f"Ошибка при обработке поста message_id={message_id}: {e}")
error_count += 1
if not dry_run:
await conn.commit()
total_processed = updated_count + skipped_count + error_count
logger.info(
f"Обработка завершена. Всего записей: {total_processed}, "
f"обновлено: {updated_count}, пропущено: {skipped_count}, ошибок: {error_count}"
)
print(f"\nОбработка завершена:")
print(f" - Всего записей: {total_processed}")
print(f" - Обновлено: {updated_count}")
print(f" - Пропущено (уже в сыром виде): {skipped_count}")
print(f" - Ошибок: {error_count}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Приведение текста постов к 'сырому' виду"
)
parser.add_argument(
"--db",
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или DB_PATH)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Режим проверки без сохранения изменений",
)
args = parser.parse_args()
asyncio.run(main(args.db, args.dry_run))

View File

@@ -1,95 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт миграции для создания таблицы blacklist_history.
Таблица хранит историю всех операций бана/разбана пользователей.
"""
import argparse
import asyncio
import os
import sys
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
from logs.custom_logger import logger
DEFAULT_DB_PATH = "database/tg-bot-database.db"
def _table_exists(rows: list, table_name: str) -> bool:
"""Проверяет существование таблицы по результатам PRAGMA table_list."""
for row in rows:
if row[1] == table_name: # name column
return True
return False
async def main(db_path: str) -> None:
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error("База данных не найдена: %s", db_path)
print(f"Ошибка: база данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
# Проверяем наличие таблицы blacklist_history
cursor = await conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='blacklist_history'"
)
rows = await cursor.fetchall()
await cursor.close()
if not rows:
logger.info("Создание таблицы blacklist_history")
# Создаем таблицу
await conn.execute("""
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 conn.execute(
"CREATE INDEX IF NOT EXISTS idx_blacklist_history_user_id ON blacklist_history(user_id)"
)
await conn.execute(
"CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_ban ON blacklist_history(date_ban)"
)
await conn.execute(
"CREATE INDEX IF NOT EXISTS idx_blacklist_history_date_unban ON blacklist_history(date_unban)"
)
await conn.commit()
logger.info("Таблица blacklist_history и индексы успешно созданы")
print("Таблица blacklist_history и индексы успешно созданы.")
else:
print("Таблица blacklist_history уже существует.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Создание таблицы blacklist_history для истории банов/разбанов"
)
parser.add_argument(
"--db",
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или DB_PATH)",
)
args = parser.parse_args()
asyncio.run(main(args.db))

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Миграция: Удаление колонки vector_hash из таблицы post_from_telegram_suggest.
Колонка больше не нужна, т.к. RAG сервис вынесен в отдельный микросервис
и хранит векторы самостоятельно.
SQLite не поддерживает DROP COLUMN напрямую (до версии 3.35.0),
поэтому используем пересоздание таблицы.
"""
import argparse
import asyncio
import os
import sys
from pathlib import Path
# Добавляем корень проекта в путь
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
# Пытаемся импортировать logger, если не получается - используем стандартный
try:
from logs.custom_logger import logger
except ImportError:
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
DEFAULT_DB_PATH = "database/tg-bot-database.db"
async def column_exists(conn: aiosqlite.Connection, table: str, column: str) -> bool:
"""Проверяет существование колонки в таблице."""
cursor = await conn.execute(f"PRAGMA table_info({table})")
columns = await cursor.fetchall()
return any(col[1] == column for col in columns)
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('.')))
async def main(db_path: str) -> None:
"""
Удаляет колонку vector_hash из таблицы post_from_telegram_suggest.
"""
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error(f"База данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
# Проверяем существует ли колонка
if not await column_exists(conn, "post_from_telegram_suggest", "vector_hash"):
logger.info("Колонка vector_hash не существует, миграция не требуется")
return
# Проверяем версию SQLite
version = await get_sqlite_version(conn)
logger.info(f"Версия SQLite: {'.'.join(map(str, version))}")
# SQLite 3.35.0+ поддерживает DROP COLUMN
if version >= (3, 35, 0):
logger.info("Используем ALTER TABLE DROP COLUMN")
await conn.execute(
"ALTER TABLE post_from_telegram_suggest DROP COLUMN vector_hash"
)
else:
# Для старых версий пересоздаём таблицу
logger.info("Используем пересоздание таблицы (SQLite < 3.35.0)")
# Получаем список колонок без vector_hash
cursor = await conn.execute("PRAGMA table_info(post_from_telegram_suggest)")
columns = await cursor.fetchall()
column_names = [col[1] for col in columns if col[1] != "vector_hash"]
columns_str = ", ".join(column_names)
logger.info(f"Колонки для сохранения: {columns_str}")
# Пересоздаём таблицу
await conn.execute("BEGIN TRANSACTION")
try:
# Создаём временную таблицу
await conn.execute(
f"CREATE TABLE post_from_telegram_suggest_backup AS "
f"SELECT {columns_str} FROM post_from_telegram_suggest"
)
# Удаляем старую таблицу
await conn.execute("DROP TABLE post_from_telegram_suggest")
# Переименовываем временную
await conn.execute(
"ALTER TABLE post_from_telegram_suggest_backup "
"RENAME TO post_from_telegram_suggest"
)
await conn.execute("COMMIT")
except Exception as e:
await conn.execute("ROLLBACK")
raise e
await conn.commit()
logger.info("Колонка vector_hash успешно удалена")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Удаление колонки vector_hash из post_from_telegram_suggest"
)
parser.add_argument(
"--db",
default=os.environ.get("DATABASE_PATH", DEFAULT_DB_PATH),
help="Путь к БД",
)
args = parser.parse_args()
asyncio.run(main(args.db))

View File

@@ -1,138 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт миграции для переноса записей из blacklist в blacklist_history.
Переносит все существующие записи из таблицы blacklist в таблицу blacklist_history.
"""
import argparse
import asyncio
import os
import sys
from datetime import datetime
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
import aiosqlite
from logs.custom_logger import logger
DEFAULT_DB_PATH = "database/tg-bot-database.db"
async def main(db_path: str) -> None:
db_path = os.path.abspath(db_path)
if not os.path.exists(db_path):
logger.error("База данных не найдена: %s", db_path)
print(f"Ошибка: база данных не найдена: {db_path}")
return
async with aiosqlite.connect(db_path) as conn:
await conn.execute("PRAGMA foreign_keys = ON")
# Проверяем наличие таблицы blacklist_history
cursor = await conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='blacklist_history'"
)
rows = await cursor.fetchall()
await cursor.close()
if not rows:
logger.error(
"Таблица blacklist_history не найдена. Сначала запустите create_blacklist_history_table.py"
)
print(
"Ошибка: таблица blacklist_history не найдена. Сначала запустите create_blacklist_history_table.py"
)
return
# Получаем все записи из blacklist
cursor = await conn.execute(
"SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist"
)
blacklist_records = await cursor.fetchall()
await cursor.close()
if not blacklist_records:
print("В таблице blacklist нет записей для переноса.")
logger.info("В таблице blacklist нет записей для переноса")
return
logger.info(
"Найдено записей в blacklist для переноса: %d", len(blacklist_records)
)
print(f"Найдено записей в blacklist для переноса: {len(blacklist_records)}")
# Получаем текущее время в Unix timestamp
current_time = int(datetime.now().timestamp())
# Переносим записи в blacklist_history
migrated_count = 0
skipped_count = 0
for record in blacklist_records:
user_id, message_for_user, date_to_unban, created_at, ban_author = record
# Проверяем, нет ли уже записи для этого user_id с таким же date_ban
# (чтобы избежать дубликатов при повторном запуске)
date_ban = created_at if created_at is not None else current_time
check_cursor = await conn.execute(
"SELECT id FROM blacklist_history WHERE user_id = ? AND date_ban = ?",
(user_id, date_ban),
)
existing = await check_cursor.fetchone()
await check_cursor.close()
if existing:
logger.debug(
"Запись для user_id=%d с date_ban=%d уже существует, пропускаем",
user_id,
date_ban,
)
skipped_count += 1
continue
# Вставляем запись в blacklist_history
await conn.execute(
"""
INSERT INTO blacklist_history
(user_id, message_for_user, date_ban, date_unban, ban_author, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
user_id,
message_for_user,
date_ban,
date_to_unban,
ban_author,
created_at if created_at is not None else current_time,
current_time,
),
)
migrated_count += 1
await conn.commit()
logger.info(
"Миграция завершена. Перенесено записей: %d, пропущено (дубликаты): %d",
migrated_count,
skipped_count,
)
print(
f"Миграция завершена. Перенесено записей: {migrated_count}, пропущено (дубликаты): {skipped_count}"
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Перенос записей из blacklist в blacklist_history"
)
parser.add_argument(
"--db",
default=os.environ.get("DB_PATH", DEFAULT_DB_PATH),
help="Путь к БД (или DB_PATH)",
)
args = parser.parse_args()
asyncio.run(main(args.db))

View File

@@ -3,7 +3,6 @@
Скрипт для проверки подключения к S3 хранилищу.
Читает настройки из .env файла или переменных окружения.
"""
import asyncio
import os
import sys
@@ -15,7 +14,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)
@@ -27,12 +26,11 @@ 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 хранилищу."""
@@ -42,54 +40,50 @@ async def test_s3_connection():
print(f"Region: {S3_REGION}")
print(f"Access Key: {S3_ACCESS_KEY}")
print()
session = aioboto3.Session()
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:
print(f"⚠️ Не удалось получить список бакетов: {list_error}")
print(" Это нормально, если нет прав на list_buckets")
print(" Продолжаем тестирование с указанным бакетом...")
# Пытаемся создать тестовый файл в указанном бакете
print("\n🧪 Тестирование записи файла...")
# Используем первый найденный бакет, если указанный не найден
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"]
test_key = "test-connection.txt"
test_content = b"Test connection to S3 storage"
print(f" Используем первый найденный бакет: '{buckets[0]['Name']}'")
test_bucket = buckets[0]['Name']
test_key = 'test-connection.txt'
test_content = b'Test connection to S3 storage'
try:
# Проверяем существование бакета
try:
@@ -99,32 +93,33 @@ async def test_s3_connection():
print(f"❌ Бакет '{test_bucket}' недоступен: {head_error}")
print(" Проверьте права доступа к бакету")
return False
await s3.put_object(Bucket=test_bucket, Key=test_key, Body=test_content)
print(
f"✅ Файл успешно записан в бакет '{test_bucket}' с ключом '{test_key}'"
await s3.put_object(
Bucket=test_bucket,
Key=test_key,
Body=test_content
)
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("✅ Файл успешно прочитан, содержимое совпадает")
else:
print("⚠️ Файл прочитан, но содержимое не совпадает")
# Удаляем тестовый файл
print("🧹 Удаление тестового файла...")
await s3.delete_object(Bucket=test_bucket, Key=test_key)
print("✅ Тестовый файл удален")
except Exception as e:
print(f"❌ Ошибка при тестировании записи/чтения: {e}")
print(f" Тип ошибки: {type(e).__name__}")
import traceback
print(f" Полный traceback:")
traceback.print_exc()
print("\nВозможные причины:")
@@ -132,9 +127,9 @@ async def test_s3_connection():
print(" 2. Нет прав на запись в бакет")
print(" 3. Неверный endpoint URL или регион")
print(" 4. Проблемы с форматом endpoint (попробуйте без :443)")
return True
except Exception as e:
print(f"❌ Ошибка подключения к S3: {e}")
print("\nВозможные причины:")