реструктуризация файлов проекта
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-08-30 10:08:43 +03:00
parent adc49c285c
commit 251fa0be11
21 changed files with 619 additions and 333 deletions
+41
View File
@@ -0,0 +1,41 @@
# Вспомогательные скрипты
Скрипты вне основного пайплайна (`runner.sh`) и systemd-сервисов. Запускать из **корня проекта** с активированным venv.
## ops/ — эксплуатация
| Файл | Назначение |
|------|------------|
| `genkey.py` | Генерация hex-ключа для `SECRET_KEY` / `WEBHOOK_SECRET` |
| `register_tg_webhook.py` | Ручная регистрация webhook (в норме — `wsgi_bot.py` при старте) |
| `copy_databases.sh` | Клонирование MariaDB между базами |
```bash
python tools/ops/genkey.py
./venv/bin/python tools/ops/register_tg_webhook.py
bash tools/ops/copy_databases.sh <source_db> <target_db> <username>
```
## dev/ — отладка и примеры
| Файл | Назначение |
|------|------------|
| `send_telegram_example.py` | CLI-пример API `/send_message` |
| `volk_telegram_testing.py` | Отладочный тест публикации в Telegram |
| `volk_category_testing.py` | Отладочный тест загрузки категории VOLK |
```bash
python tools/dev/send_telegram_example.py <user_id> "<message>"
python tools/dev/volk_telegram_testing.py
python tools/dev/volk_category_testing.py <category_id>
```
## legacy/ — снятые с пайплайна
| Файл | Назначение |
|------|------------|
| `evt_prefetch.py` | Массовая пометка видимых событий `marked_to_publication=1`. Убран из `runner.sh` в 1.0.0 |
```bash
./venv/bin/python tools/legacy/evt_prefetch.py
```
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Пример скрипта для отправки сообщений через Telegram бота.
Использование (из корня проекта):
python tools/dev/send_telegram_example.py <user_id> <message>
"""
import sys
import requests
import json
import os
from dotenv import load_dotenv
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
load_dotenv(os.path.join(_PROJECT_ROOT, ".env"))
load_dotenv()
def send_message(user_id, message_text, bot_url=None, api_token=None):
"""
Отправляет сообщение пользователю через Telegram бота
Args:
user_id (int): ID пользователя в Telegram
message_text (str): Текст сообщения
bot_url (str): URL бота (если не указан, берется из переменных окружения)
api_token (str): API токен для аутентификации (если не указан, берется из переменных окружения)
Returns:
tuple: (success: bool, message: str)
"""
if bot_url is None:
# Пытаемся получить URL из переменных окружения
webhook_url = os.getenv('WEBHOOK_URL')
webhook_port = os.getenv('WEBHOOK_PORT', '5005')
if webhook_url:
# Используем внешний URL через reverse proxy
bot_url = f"{webhook_url}/send_message"
else:
bot_url = f"http://localhost:{webhook_port}/send_message"
if api_token is None:
api_token = os.getenv('WEBHOOK_SECRET')
try:
url = f"{bot_url}/send_message"
data = {
"user_id": user_id,
"message": message_text
}
headers = {
'Content-Type': 'application/json',
'X-API-Secret-Token': api_token
}
response = requests.post(url, json=data, headers=headers, timeout=10)
result = response.json()
if response.status_code == 200 and result.get("success"):
return True, result.get("message", "Сообщение отправлено успешно")
else:
return False, result.get("error", "Неизвестная ошибка")
except requests.exceptions.RequestException as e:
return False, f"Ошибка сети: {e}"
except json.JSONDecodeError as e:
return False, f"Ошибка парсинга ответа: {e}"
except Exception as e:
return False, f"Неожиданная ошибка: {e}"
def main():
if len(sys.argv) != 3:
print("Использование: python tools/dev/send_telegram_example.py <user_id> <message>")
print("Пример: python tools/dev/send_telegram_example.py 123456789 'Привет! Это тестовое сообщение.'")
sys.exit(1)
try:
user_id = int(sys.argv[1])
except ValueError:
print("Ошибка: user_id должен быть числом")
sys.exit(1)
message_text = sys.argv[2]
print(f"Отправка сообщения пользователю {user_id}: {message_text}")
success, result_message = send_message(user_id, message_text)
if success:
print(f"{result_message}")
else:
print(f"{result_message}")
sys.exit(1)
if __name__ == "__main__":
main()
+360
View File
@@ -0,0 +1,360 @@
#!/usr/bin/env python3
"""
Тестовый модуль для проверки загрузки описания категории.
Использование (из корня проекта):
python tools/dev/volk_category_testing.py <category_id>
"""
import os
import sys
import json
import time
import traceback
import socket
import argparse
from datetime import datetime
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
from dotenv import load_dotenv
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
load_dotenv(os.path.join(_PROJECT_ROOT, ".env"))
load_dotenv()
# Параметры из .env
VOLK_CATEGORY_DESC_PREFIX = os.getenv('VOLK_CATEGORY_DESC_PREFIX')
VOLK_API_VERSION = os.getenv('VOLK_API_VERSION')
# Лог-файл для тестирования
TEST_LOG_FILE = 'volk_category_testing.log'
def log_message(message, category="TEST"):
"""Функция для записи в лог-файл с повторными попытками при блокировке"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [{category}] {message}\n"
# До 5 попыток записи с увеличением задержки
for attempt in range(5):
try:
with open(TEST_LOG_FILE, 'a', encoding='utf-8') as f:
f.write(log_entry)
return True
except (IOError, OSError) as e:
if attempt < 4: # Не последняя попытка
time.sleep(0.4) # Задержка 400ms между попытками
else:
return False
return False
def safe_log_direct(message, category="TEST"):
"""Безопасная функция для прямой записи в лог-файл без обработки исключений
Используется в критических ситуациях, когда обычный log_message может не сработать"""
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] [{category}] {message}\n"
# Прямая запись без контекстного менеджера для максимальной надежности
f = open(TEST_LOG_FILE, 'a', encoding='utf-8')
try:
f.write(log_entry)
f.flush() # Принудительно сбрасываем буфер
finally:
f.close()
except:
# Игнорируем все исключения - эта функция должна быть максимально безопасной
pass
def test_category_description_load(category_id, category_number=None, total_count=None):
"""Тестирует загрузку описания категории
Args:
category_id: ID категории для тестирования
category_number: Номер категории в последовательности (опционально)
total_count: Общее количество категорий (опционально)
"""
if category_number and total_count:
header = f"НАЧАЛО ТЕСТИРОВАНИЯ [{category_number}/{total_count}] КАТЕГОРИИ: {category_id}"
else:
header = f"НАЧАЛО ТЕСТИРОВАНИЯ ЗАГРУЗКИ ОПИСАНИЯ КАТЕГОРИИ: {category_id}"
log_message("=" * 80)
log_message(header)
log_message("=" * 80)
# Проверка наличия префикса URL
if not VOLK_CATEGORY_DESC_PREFIX:
error_msg = "Переменная окружения VOLK_CATEGORY_DESC_PREFIX не установлена"
log_message(error_msg, "ERROR")
print(f"ОШИБКА: {error_msg}")
return False
# Формирование URL
desc_url = VOLK_CATEGORY_DESC_PREFIX + category_id
log_message(f"URL для запроса: {desc_url}")
log_message(f"Префикс URL: {VOLK_CATEGORY_DESC_PREFIX}")
log_message(f"ID категории: {category_id}")
desc_response = None
# Шаг 1: Запрос через urlopen
log_message("ШАГ 1: Вызов urlopen()")
try:
req = Request(desc_url)
log_message(f"Request объект создан: {req}")
safe_log_direct("Выполняется urlopen с таймаутом 30 секунд", "DEBUG")
response = urlopen(req, timeout=30)
safe_log_direct("urlopen успешно выполнен", "DEBUG")
log_message("urlopen успешно выполнен")
except (URLError, HTTPError, OSError, socket.gaierror, socket.timeout, ConnectionError) as e:
# Используем безопасное логирование для критических ошибок
safe_log_direct("ПЕРЕХВАЧЕНА СЕТЕВАЯ ОШИБКА urlopen", "ERROR")
safe_log_direct(f"Категория: {category_id}, URL: {desc_url}", "ERROR")
safe_log_direct(f"Тип ошибки: {type(e).__name__}, Сообщение: {str(e)}", "ERROR")
try:
traceback_str = traceback.format_exc()
safe_log_direct(f"Трассировка:\n{traceback_str}", "ERROR")
log_message(f"Трассировка сетевой ошибки:\n{traceback_str}", "ERROR")
except:
pass
try:
error_msg = f"Ошибка сетевого подключения при запросе описания категории {category_id} из {desc_url}: {type(e).__name__}: {e}"
log_message(error_msg, "ERROR")
except:
pass
print(f"ОШИБКА: Сетевая ошибка при запросе - {type(e).__name__}: {e}")
return False
except Exception as e:
# Перехватываем любые другие исключения, которые могут возникнуть при urlopen
safe_log_direct("ПЕРЕХВАЧЕНА НЕОЖИДАННАЯ ОШИБКА urlopen", "ERROR")
safe_log_direct(f"Категория: {category_id}, URL: {desc_url}", "ERROR")
safe_log_direct(f"Тип ошибки: {type(e).__name__}, Сообщение: {str(e)}", "ERROR")
try:
traceback_str = traceback.format_exc()
safe_log_direct(f"Трассировка:\n{traceback_str}", "ERROR")
log_message(f"Трассировка неожиданной ошибки:\n{traceback_str}", "ERROR")
except:
pass
try:
error_msg = f"Неожиданная ошибка при запросе описания категории {category_id} из {desc_url}: {type(e).__name__}: {e}"
log_message(error_msg, "ERROR")
except:
pass
print(f"ОШИБКА: Неожиданная ошибка - {type(e).__name__}: {e}")
return False
# Шаг 2: Чтение и декодирование данных
log_message("ШАГ 2: Чтение и декодирование данных")
try:
with response:
# Явно указываем кодировку UTF-8 при декодировании
raw_data = response.read()
log_message(f"raw data read, размер: {len(raw_data)} байт")
safe_log_direct(f"Размер raw_data: {len(raw_data)} байт", "DEBUG")
try:
decoded_data = raw_data.decode('utf-8')
log_message("data decoded (UTF-8)")
safe_log_direct("Декодирование UTF-8 успешно", "DEBUG")
except UnicodeDecodeError as e:
error_msg = f"Ошибка декодирования ответа для категории {category_id}: {e}. Попытка декодирования как latin-1."
log_message(error_msg, "WARNING")
decoded_data = raw_data.decode('latin-1')
log_message("data decoded (latin-1 fallback)")
log_message(f"Длина декодированной строки: {len(decoded_data)} символов")
log_message(f"Первые 200 символов ответа:\n{decoded_data[:200]}")
# Шаг 3: Парсинг JSON
log_message("ШАГ 3: Парсинг JSON")
try:
desc_response = json.loads(decoded_data)
log_message("json loaded успешно")
safe_log_direct("JSON успешно распарсен", "DEBUG")
except json.JSONDecodeError as e:
error_msg = f"Ошибка парсинга JSON для категории {category_id}: {e}. Первые 500 символов ответа: {decoded_data[:500]}"
log_message(error_msg, "ERROR")
log_message(f"Полный ответ (первые 1000 символов):\n{decoded_data[:1000]}", "ERROR")
print(f"ОШИБКА: Не удалось распарсить JSON - {e}")
return False
except Exception as e:
error_msg = f"Ошибка при чтении ответа для категории {category_id}: {type(e).__name__}: {e}"
log_message(error_msg, "ERROR")
traceback_str = traceback.format_exc()
log_message(f"Трассировка ошибки чтения:\n{traceback_str}", "ERROR")
print(f"ОШИБКА: Ошибка чтения ответа - {type(e).__name__}: {e}")
return False
# Проверяем, что desc_response был успешно получен
if desc_response is None:
error_msg = f"Не удалось получить ответ для категории {category_id}"
log_message(error_msg, "ERROR")
print(f"ОШИБКА: {error_msg}")
return False
log_message(f"Получен JSON ответ: {json.dumps(desc_response, ensure_ascii=False, indent=2)}")
# Шаг 4: Проверка версии API
log_message("ШАГ 4: Проверка версии API")
api_version = desc_response.get('api_version')
log_message(f"Получена версия API: {api_version}")
log_message(f"Ожидаемая версия API: {VOLK_API_VERSION}")
if api_version != VOLK_API_VERSION:
error_msg = f"Несовпадение версии API при загрузке описания категории {category_id}: получена версия '{api_version}', ожидается '{VOLK_API_VERSION}'."
log_message(error_msg, "ERROR")
print(f"ОШИБКА: {error_msg}")
return False
log_message("Версия API соответствует ожидаемой")
# Шаг 5: Проверка наличия блока data
log_message("ШАГ 5: Проверка наличия блока data")
if 'data' not in desc_response:
error_msg = f"В JSON ответе для категории {category_id} отсутствует блок 'data'."
log_message(error_msg, "ERROR")
log_message(f"Структура JSON ответа: {list(desc_response.keys())}", "ERROR")
print(f"ОШИБКА: {error_msg}")
return False
log_message("Блок 'data' найден в ответе")
desc_data = desc_response['data']
log_message(f"Тип desc_data: {type(desc_data)}")
# Шаг 6: Проверка типа desc_data
log_message("ШАГ 6: Проверка типа desc_data")
if not isinstance(desc_data, dict):
error_msg = f"Блок 'data' для категории {category_id} не является словарём (тип: {type(desc_data)})."
log_message(error_msg, "ERROR")
print(f"ОШИБКА: {error_msg}")
return False
log_message("desc_data является словарём")
log_message(f"Ключи в desc_data: {list(desc_data.keys())}")
# Шаг 7: Получение description
log_message("ШАГ 7: Получение description из блока data")
description = desc_data.get('description')
log_message(f"description получен, тип: {type(description)}")
if description:
log_message(f"Длина description: {len(description)} символов")
log_message(f"Первые 500 символов description:\n{description[:500]}")
log_message("Описание успешно получено")
else:
log_message("description пустое или отсутствует (будет установлено в None)")
description = None
# Итоговый результат
if category_number and total_count:
footer = f"ТЕСТИРОВАНИЕ ЗАВЕРШЕНО УСПЕШНО [{category_number}/{total_count}] КАТЕГОРИИ: {category_id}"
else:
footer = f"ТЕСТИРОВАНИЕ ЗАВЕРШЕНО УСПЕШНО ДЛЯ КАТЕГОРИИ: {category_id}"
log_message("=" * 80)
log_message(footer)
log_message("=" * 80)
return True
def main():
"""Главная функция"""
parser = argparse.ArgumentParser(
description='Тестирование загрузки описания категории из API VOLK',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Примеры использования:
python tools/dev/volk_category_testing.py volk26_nri
python tools/dev/volk_category_testing.py volk26_nri volk26_other volk26_third
"""
)
parser.add_argument(
'category_ids',
type=str,
nargs='+',
help='ID категорий для тестирования (от 1 до 10 штук, например: volk26_nri volk26_other)'
)
args = parser.parse_args()
category_ids = args.category_ids
# Проверка количества ID
if len(category_ids) == 0:
print("ОШИБКА: Не указан ни один ID категории")
parser.print_help()
sys.exit(1)
if len(category_ids) > 10:
print(f"ОШИБКА: Указано слишком много ID категорий ({len(category_ids)}). Максимум 10.")
sys.exit(1)
# Проверка наличия необходимых переменных окружения
if not VOLK_CATEGORY_DESC_PREFIX:
print("ОШИБКА: Переменная окружения VOLK_CATEGORY_DESC_PREFIX не установлена")
print("Проверьте файл .env")
sys.exit(1)
if not VOLK_API_VERSION:
print("ОШИБКА: Переменная окружения VOLK_API_VERSION не установлена")
print("Проверьте файл .env")
sys.exit(1)
log_message("=" * 80)
log_message("ЗАПУСК ТЕСТИРОВАНИЯ ЗАГРУЗКИ ОПИСАНИЯ КАТЕГОРИЙ")
log_message(f"Время запуска: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
log_message(f"Количество категорий для обработки: {len(category_ids)}")
log_message(f"ID категорий: {', '.join(category_ids)}")
log_message(f"Префикс URL: {VOLK_CATEGORY_DESC_PREFIX}")
log_message(f"Версия API: {VOLK_API_VERSION}")
log_message(f"Лог-файл: {TEST_LOG_FILE}")
log_message("=" * 80)
# Обработка каждой категории последовательно
total_count = len(category_ids)
success_count = 0
fail_count = 0
for index, category_id in enumerate(category_ids, start=1):
log_message("")
log_message("")
log_message(f">>> ОБРАБОТКА КАТЕГОРИИ {index} из {total_count}: {category_id}")
log_message("")
print(f"\n[{index}/{total_count}] Обработка категории: {category_id}")
success = test_category_description_load(category_id, category_number=index, total_count=total_count)
if success:
success_count += 1
print(f"[{index}/{total_count}] ✓ УСПЕХ: Категория {category_id} обработана успешно")
else:
fail_count += 1
print(f"[{index}/{total_count}] ✗ ОШИБКА: Категория {category_id} не обработана")
# Итоговая статистика
log_message("")
log_message("")
log_message("=" * 80)
log_message("ИТОГОВАЯ СТАТИСТИКА")
log_message("=" * 80)
log_message(f"Всего категорий обработано: {total_count}")
log_message(f"Успешно: {success_count}")
log_message(f"С ошибками: {fail_count}")
log_message(f"Время завершения: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
log_message("=" * 80)
print(f"\n{'='*60}")
print(f"ИТОГИ:")
print(f" Всего категорий: {total_count}")
print(f" Успешно: {success_count}")
print(f" С ошибками: {fail_count}")
print(f" Лог сохранен в: {TEST_LOG_FILE}")
print(f"{'='*60}")
# Возвращаем код ошибки, если были неудачи
if fail_count > 0:
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()
+603
View File
@@ -0,0 +1,603 @@
import os
import logging
import httpx
import asyncio
import time
import sys
import json
from datetime import datetime
from dotenv import load_dotenv
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if _PROJECT_ROOT not in sys.path:
sys.path.insert(0, _PROJECT_ROOT)
load_dotenv(os.path.join(_PROJECT_ROOT, ".env"))
load_dotenv()
from telegram_relay import bot_api_method_url, get_relay_url
from season_links import subscription_start_link
# Настройки из переменных окружения
POSTER_BOT_TOKEN = os.getenv('POSTER_BOT_TOKEN')
RESPONDER_BOT_NAME = os.getenv('RESPONDER_BOT_NAME')
CHANNEL_ID = os.getenv('CHANNEL_ID')
DESC_PREFIX = os.getenv('DESC_PREFIX', '')
PZK_PREFIX = os.getenv('PZK_PREFIX', '')
MAX_CAPTION_LENGTH = int(os.getenv('MAX_CAPTION_LENGTH', 1000))
MAX_TEXT_LENGTH = int(os.getenv('MAX_TEXT_LENGTH', 4000))
PUBLISH_SILENTLY = os.getenv('PUBLISH_SILENTLY', 'false').lower() in ('true', '1', 'yes', 'on')
USE_SUBSCRIPTION_BOT = os.getenv('USE_SUBSCRIPTION_BOT', 'true').lower() in ('true', '1', 'yes', 'on')
# Настройка логирования
LOG_FILE = 'volk_telegram_testing.log'
logger = logging.getLogger('VOLK_TG_Test')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
# Консольный обработчик
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# Файловый обработчик с повторными попытками
class RetryFileHandler(logging.FileHandler):
def emit(self, record):
for _ in range(5):
try:
super().emit(record)
return
except (IOError, PermissionError):
time.sleep(0.5)
print(f"Failed to write to log file after 5 attempts: {record.msg}")
file_handler = RetryFileHandler(LOG_FILE, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
async def test_telegram_publish():
"""Тестовая публикация сообщения в Telegram"""
logger.info("=" * 80)
logger.info("НАЧАЛО ТЕСТОВОЙ ПУБЛИКАЦИИ В TELEGRAM")
logger.info("=" * 80)
# Логируем загруженные параметры
logger.info("Загруженные параметры из .env:")
logger.info(f" POSTER_BOT_TOKEN: {'*' * 20 if POSTER_BOT_TOKEN else 'НЕ ЗАДАН'}")
logger.info(f" RESPONDER_BOT_NAME: {RESPONDER_BOT_NAME}")
logger.info(f" CHANNEL_ID: {CHANNEL_ID}")
logger.info(f" DESC_PREFIX: {DESC_PREFIX}")
logger.info(f" PZK_PREFIX: {PZK_PREFIX}")
logger.info(f" MAX_CAPTION_LENGTH: {MAX_CAPTION_LENGTH}")
logger.info(f" MAX_TEXT_LENGTH: {MAX_TEXT_LENGTH}")
logger.info(f" PUBLISH_SILENTLY: {PUBLISH_SILENTLY}")
logger.info(f" USE_SUBSCRIPTION_BOT: {USE_SUBSCRIPTION_BOT}")
logger.info(f" RELAY_URL: {get_relay_url()}")
# Проверка обязательных параметров
if not POSTER_BOT_TOKEN:
logger.error("ОШИБКА: POSTER_BOT_TOKEN не задан в .env")
return False
if not CHANNEL_ID:
logger.error("ОШИБКА: CHANNEL_ID не задан в .env")
return False
# Формируем тестовое сообщение
test_number = "TEST001"
test_tags = "#тест #volk_testing"
test_unit_name = "Тестовая площадка"
test_name = "Тестовое событие для проверки публикации"
test_about = "Это тестовое сообщение для проверки работы публикации в Telegram. " * 5
# Формируем базовый текст
base_text = f"{test_tags}\n{test_unit_name}\n<b>{test_name}</b>"
base_text_length = len(base_text)
# Формируем строку ссылок
placeholder_message_id = '1234567890'
if USE_SUBSCRIPTION_BOT:
links_line = (
f'🔎 <a href="{DESC_PREFIX}{test_number}/">Инфо</a> | '
f'📌 <a href="{PZK_PREFIX}{test_number}/">Иду</a> | '
f'🔜 <a href="{subscription_start_link(RESPONDER_BOT_NAME, "event", placeholder_message_id)}">Подписка</a>'
)
else:
links_line = f'🔎 <a href="{DESC_PREFIX}{test_number}/">Инфо</a>'
links_line_length = len(links_line)
# Определяем максимальную длину
max_length = MAX_CAPTION_LENGTH # Используем caption для теста
# Вычисляем доступную длину для текста
available_length = max_length - base_text_length - links_line_length - 3
if available_length < 0:
available_length = 0
# Обрезаем about до доступной длины
if len(test_about) > available_length:
test_about = test_about[:available_length-3] + "..."
# Формируем финальный текст
final_text = f"{base_text}\n\n{test_about}\n\n{links_line}"
# Логируем детали сообщения
logger.info("=" * 80)
logger.info("ДЕТАЛИ ТЕСТОВОГО СООБЩЕНИЯ:")
logger.info("=" * 80)
logger.info(f"Длина base_text: {base_text_length} символов")
logger.info(f"Длина test_about: {len(test_about)} символов")
logger.info(f"Длина links_line: {links_line_length} символов")
logger.info(f"Максимальная длина (max_length): {max_length} символов")
logger.info(f"Доступная длина для about: {available_length} символов")
logger.info(f"Длина final_text: {len(final_text)} символов")
logger.info(f"Используется изображение: НЕТ")
logger.info("=" * 80)
logger.info("ТЕКСТ СООБЩЕНИЯ:")
logger.info("=" * 80)
logger.info(final_text)
logger.info("=" * 80)
# Создаем httpx клиент для прямых запросов к Telegram Bot API
httpx_client = None
try:
# Создаём клиент БЕЗ HTTP/2 с указанными параметрами
logger.info("Создание httpx клиента с отключенным HTTP/2...")
httpx_client = httpx.AsyncClient(
http2=False, # ← отключаем HTTP/2
timeout=20.0, # явный таймаут
follow_redirects=True
)
logger.info("httpx клиент успешно создан с http2=False, timeout=20.0")
# Получаем информацию о боте через Telegram Bot API
try:
logger.info("=" * 80)
logger.info("ПОПЫТКА ПОЛУЧЕНИЯ ИНФОРМАЦИИ О БОТЕ")
logger.info("=" * 80)
get_me_url = bot_api_method_url(POSTER_BOT_TOKEN, "getMe")
logger.info(f"URL запроса: {get_me_url}")
logger.info(f"Метод: GET")
start_time = datetime.now()
response = await httpx_client.get(get_me_url)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
logger.info(f"Статус ответа: {response.status_code}")
logger.info(f"Время запроса: {duration:.2f} секунд")
if response.status_code == 200:
bot_info = response.json()
if bot_info.get('ok'):
result = bot_info.get('result', {})
logger.info("=" * 80)
logger.info("ИНФОРМАЦИЯ О БОТЕ УСПЕШНО ПОЛУЧЕНА!")
logger.info("=" * 80)
logger.info(f"ID: {result.get('id')}")
logger.info(f"Username: @{result.get('username')}")
logger.info(f"First name: {result.get('first_name')}")
logger.info(f"Is bot: {result.get('is_bot')}")
logger.info("=" * 80)
else:
logger.error(f"Ошибка в ответе API: {bot_info}")
else:
logger.error(f"HTTP ошибка: {response.status_code}")
logger.error(f"Текст ответа: {response.text}")
except httpx.TimeoutException as e:
logger.error("=" * 80)
logger.error("ТАЙМАУТ при получении информации о боте!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
except httpx.HTTPError as e:
logger.error("=" * 80)
logger.error("ОШИБКА HTTP при получении информации о боте!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
except Exception as e:
logger.error("=" * 80)
logger.error("НЕОЖИДАННАЯ ОШИБКА при получении информации о боте!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}")
logger.error(f"Полное представление: {repr(e)}")
import traceback
logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
# Публикуем тестовое сообщение через Telegram Bot API
try:
logger.info("=" * 80)
logger.info("ПОПЫТКА ОТПРАВКИ ТЕСТОВОГО СООБЩЕНИЯ")
logger.info("=" * 80)
send_message_url = bot_api_method_url(POSTER_BOT_TOKEN, "sendMessage")
logger.info(f"URL запроса: {send_message_url}")
logger.info(f"Метод: POST")
logger.info(f"Параметры запроса:")
logger.info(f" chat_id: {CHANNEL_ID}")
logger.info(f" text_length: {len(final_text)}")
logger.info(f" parse_mode: HTML")
logger.info(f" disable_notification: {PUBLISH_SILENTLY}")
logger.info(f" disable_web_page_preview: True")
payload = {
'chat_id': CHANNEL_ID,
'text': final_text,
'parse_mode': 'HTML',
'disable_notification': PUBLISH_SILENTLY,
'disable_web_page_preview': True
}
start_time = datetime.now()
response = await httpx_client.post(send_message_url, json=payload)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
logger.info(f"Статус ответа: {response.status_code}")
logger.info(f"Время запроса: {duration:.2f} секунд")
if response.status_code == 200:
result = response.json()
if result.get('ok'):
message_data = result.get('result', {})
message_id = message_data.get('message_id')
chat_data = message_data.get('chat', {})
logger.info("=" * 80)
logger.info("СООБЩЕНИЕ УСПЕШНО ОТПРАВЛЕНО!")
logger.info("=" * 80)
logger.info(f"Message ID: {message_id}")
logger.info(f"Chat ID: {chat_data.get('id')}")
logger.info(f"Chat type: {chat_data.get('type')}")
logger.info(f"Chat title: {chat_data.get('title', 'N/A')}")
logger.info(f"Время отправки: {duration:.2f} секунд")
logger.info(f"Дата сообщения: {message_data.get('date')}")
logger.info("=" * 80)
# Редактируем сообщение, добавляя информацию о редактировании
try:
logger.info("=" * 80)
logger.info("ПОПЫТКА РЕДАКТИРОВАНИЯ СООБЩЕНИЯ")
logger.info("=" * 80)
edit_message_url = bot_api_method_url(POSTER_BOT_TOKEN, "editMessageText")
logger.info(f"URL запроса: {edit_message_url}")
logger.info(f"Метод: POST")
logger.info(f"Параметры запроса:")
logger.info(f" chat_id: {CHANNEL_ID}")
logger.info(f" message_id: {message_id}")
# Добавляем информацию о редактировании в конец сообщения
edited_text = f"{final_text}\n\n<i>Сообщение отредактировано через httpx клиент</i>"
logger.info(f" text_length (после редактирования): {len(edited_text)}")
logger.info(f" parse_mode: HTML")
edit_payload = {
'chat_id': CHANNEL_ID,
'message_id': message_id,
'text': edited_text,
'parse_mode': 'HTML',
'disable_web_page_preview': True
}
edit_start_time = datetime.now()
edit_response = await httpx_client.post(edit_message_url, json=edit_payload)
edit_end_time = datetime.now()
edit_duration = (edit_end_time - edit_start_time).total_seconds()
logger.info(f"Статус ответа: {edit_response.status_code}")
logger.info(f"Время запроса: {edit_duration:.2f} секунд")
if edit_response.status_code == 200:
edit_result = edit_response.json()
if edit_result.get('ok'):
logger.info("=" * 80)
logger.info("СООБЩЕНИЕ УСПЕШНО ОТРЕДАКТИРОВАНО!")
logger.info("=" * 80)
logger.info(f"Время редактирования: {edit_duration:.2f} секунд")
logger.info("=" * 80)
else:
logger.error(f"Ошибка в ответе API при редактировании: {edit_result}")
else:
logger.error(f"HTTP ошибка при редактировании: {edit_response.status_code}")
logger.error(f"Текст ответа: {edit_response.text}")
except httpx.TimeoutException as e:
logger.error("=" * 80)
logger.error("ТАЙМАУТ ПРИ РЕДАКТИРОВАНИИ СООБЩЕНИЯ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
logger.error(f"Параметры запроса:")
logger.error(f" chat_id: {CHANNEL_ID}")
logger.error(f" message_id: {message_id}")
logger.error(f" text_length: {len(edited_text)}")
except httpx.HTTPError as e:
logger.error("=" * 80)
logger.error("ОШИБКА HTTP ПРИ РЕДАКТИРОВАНИИ СООБЩЕНИЯ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
except Exception as e:
logger.error("=" * 80)
logger.error("НЕОЖИДАННАЯ ОШИБКА ПРИ РЕДАКТИРОВАНИИ СООБЩЕНИЯ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}")
logger.error(f"Полное представление: {repr(e)}")
import traceback
logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
# Публикуем второе сообщение с картинкой
try:
logger.info("=" * 80)
logger.info("ПОПЫТКА ОТПРАВКИ ТЕСТОВОГО СООБЩЕНИЯ С КАРТИНКОЙ")
logger.info("=" * 80)
send_photo_url = bot_api_method_url(POSTER_BOT_TOKEN, "sendPhoto")
image_url = "https://base.volk-nn.ru/uploads/events/volk26/volk26_event/20251210_175059_0.jpg"
# Формируем текст для сообщения с картинкой
photo_caption = f"{test_tags}\n{test_unit_name}\n<b>Тестовое сообщение с картинкой</b>\n\nЭто тестовое сообщение с изображением для проверки работы публикации фото в Telegram."
logger.info(f"URL запроса: {send_photo_url}")
logger.info(f"Метод: POST")
logger.info(f"Параметры запроса:")
logger.info(f" chat_id: {CHANNEL_ID}")
logger.info(f" photo_url: {image_url}")
logger.info(f" photo_url_length: {len(image_url)}")
logger.info(f" caption_length: {len(photo_caption)}")
logger.info(f" parse_mode: HTML")
logger.info(f" disable_notification: {PUBLISH_SILENTLY}")
photo_payload = {
'chat_id': CHANNEL_ID,
'photo': image_url,
'caption': photo_caption,
'parse_mode': 'HTML',
'disable_notification': PUBLISH_SILENTLY
}
photo_start_time = datetime.now()
photo_response = await httpx_client.post(send_photo_url, json=photo_payload)
photo_end_time = datetime.now()
photo_duration = (photo_end_time - photo_start_time).total_seconds()
logger.info(f"Статус ответа: {photo_response.status_code}")
logger.info(f"Время запроса: {photo_duration:.2f} секунд")
if photo_response.status_code == 200:
photo_result = photo_response.json()
if photo_result.get('ok'):
photo_message_data = photo_result.get('result', {})
photo_message_id = photo_message_data.get('message_id')
photo_chat_data = photo_message_data.get('chat', {})
logger.info("=" * 80)
logger.info("СООБЩЕНИЕ С КАРТИНКОЙ УСПЕШНО ОТПРАВЛЕНО!")
logger.info("=" * 80)
logger.info(f"Message ID: {photo_message_id}")
logger.info(f"Chat ID: {photo_chat_data.get('id')}")
logger.info(f"Chat type: {photo_chat_data.get('type')}")
logger.info(f"Chat title: {photo_chat_data.get('title', 'N/A')}")
logger.info(f"Время отправки: {photo_duration:.2f} секунд")
logger.info(f"Дата сообщения: {photo_message_data.get('date')}")
logger.info("=" * 80)
# Редактируем сообщение с картинкой, добавляя информацию о редактировании
try:
logger.info("=" * 80)
logger.info("ПОПЫТКА РЕДАКТИРОВАНИЯ СООБЩЕНИЯ С КАРТИНКОЙ")
logger.info("=" * 80)
edit_photo_caption_url = bot_api_method_url(POSTER_BOT_TOKEN, "editMessageCaption")
logger.info(f"URL запроса: {edit_photo_caption_url}")
logger.info(f"Метод: POST")
logger.info(f"Параметры запроса:")
logger.info(f" chat_id: {CHANNEL_ID}")
logger.info(f" message_id: {photo_message_id}")
# Добавляем информацию о редактировании в конец подписи
edited_photo_caption = f"{photo_caption}\n\n<i>Сообщение с картинкой отредактировано через httpx клиент</i>"
logger.info(f" caption_length (после редактирования): {len(edited_photo_caption)}")
logger.info(f" parse_mode: HTML")
edit_photo_payload = {
'chat_id': CHANNEL_ID,
'message_id': photo_message_id,
'caption': edited_photo_caption,
'parse_mode': 'HTML'
}
edit_photo_start_time = datetime.now()
edit_photo_response = await httpx_client.post(edit_photo_caption_url, json=edit_photo_payload)
edit_photo_end_time = datetime.now()
edit_photo_duration = (edit_photo_end_time - edit_photo_start_time).total_seconds()
logger.info(f"Статус ответа: {edit_photo_response.status_code}")
logger.info(f"Время запроса: {edit_photo_duration:.2f} секунд")
if edit_photo_response.status_code == 200:
edit_photo_result = edit_photo_response.json()
if edit_photo_result.get('ok'):
logger.info("=" * 80)
logger.info("СООБЩЕНИЕ С КАРТИНКОЙ УСПЕШНО ОТРЕДАКТИРОВАНО!")
logger.info("=" * 80)
logger.info(f"Время редактирования: {edit_photo_duration:.2f} секунд")
logger.info("=" * 80)
else:
logger.error(f"Ошибка в ответе API при редактировании подписи: {edit_photo_result}")
logger.error(f"Описание ошибки: {edit_photo_result.get('description', 'N/A')}")
logger.error(f"Код ошибки: {edit_photo_result.get('error_code', 'N/A')}")
else:
logger.error(f"HTTP ошибка при редактировании подписи: {edit_photo_response.status_code}")
logger.error(f"Текст ответа: {edit_photo_response.text}")
except httpx.TimeoutException as e:
logger.error("=" * 80)
logger.error("ТАЙМАУТ ПРИ РЕДАКТИРОВАНИИ СООБЩЕНИЯ С КАРТИНКОЙ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
logger.error(f"Параметры запроса:")
logger.error(f" chat_id: {CHANNEL_ID}")
logger.error(f" message_id: {photo_message_id}")
logger.error(f" caption_length: {len(edited_photo_caption)}")
except httpx.HTTPError as e:
logger.error("=" * 80)
logger.error("ОШИБКА HTTP ПРИ РЕДАКТИРОВАНИИ СООБЩЕНИЯ С КАРТИНКОЙ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
except Exception as e:
logger.error("=" * 80)
logger.error("НЕОЖИДАННАЯ ОШИБКА ПРИ РЕДАКТИРОВАНИИ СООБЩЕНИЯ С КАРТИНКОЙ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}")
logger.error(f"Полное представление: {repr(e)}")
import traceback
logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
else:
logger.error(f"Ошибка в ответе API при отправке фото: {photo_result}")
logger.error(f"Описание ошибки: {photo_result.get('description', 'N/A')}")
logger.error(f"Код ошибки: {photo_result.get('error_code', 'N/A')}")
else:
logger.error(f"HTTP ошибка при отправке фото: {photo_response.status_code}")
logger.error(f"Текст ответа: {photo_response.text}")
except httpx.TimeoutException as e:
logger.error("=" * 80)
logger.error("ТАЙМАУТ ПРИ ОТПРАВКЕ СООБЩЕНИЯ С КАРТИНКОЙ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
logger.error(f"Параметры запроса:")
logger.error(f" chat_id: {CHANNEL_ID}")
logger.error(f" photo_url: {image_url}")
logger.error(f" caption_length: {len(photo_caption)}")
except httpx.HTTPError as e:
logger.error("=" * 80)
logger.error("ОШИБКА HTTP ПРИ ОТПРАВКЕ СООБЩЕНИЯ С КАРТИНКОЙ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
except Exception as e:
logger.error("=" * 80)
logger.error("НЕОЖИДАННАЯ ОШИБКА ПРИ ОТПРАВКЕ СООБЩЕНИЯ С КАРТИНКОЙ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}")
logger.error(f"Полное представление: {repr(e)}")
import traceback
logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
return True
else:
logger.error(f"Ошибка в ответе API: {result}")
logger.error(f"Описание ошибки: {result.get('description', 'N/A')}")
logger.error(f"Код ошибки: {result.get('error_code', 'N/A')}")
return False
else:
logger.error(f"HTTP ошибка: {response.status_code}")
logger.error(f"Текст ответа: {response.text}")
return False
except httpx.TimeoutException as e:
logger.error("=" * 80)
logger.error("ТАЙМАУТ ПРИ ОТПРАВКЕ СООБЩЕНИЯ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
logger.error(f"Параметры запроса:")
logger.error(f" chat_id: {CHANNEL_ID}")
logger.error(f" text_length: {len(final_text)}")
logger.error(f" parse_mode: HTML")
return False
except httpx.HTTPError as e:
logger.error("=" * 80)
logger.error("ОШИБКА HTTP ПРИ ОТПРАВКЕ СООБЩЕНИЯ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e)}")
logger.error(f"Полное представление: {repr(e)}")
return False
except Exception as e:
logger.error("=" * 80)
logger.error("НЕОЖИДАННАЯ ОШИБКА ПРИ ОТПРАВКЕ СООБЩЕНИЯ!")
logger.error("=" * 80)
logger.error(f"Тип ошибки: {type(e).__name__}")
logger.error(f"Сообщение: {str(e)}")
logger.error(f"Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}")
logger.error(f"Полное представление: {repr(e)}")
import traceback
logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
return False
finally:
# Закрываем httpx клиент
if httpx_client is not None:
try:
logger.info("Закрытие httpx клиента...")
await httpx_client.aclose()
logger.info("httpx клиент успешно закрыт")
except Exception as e:
logger.error(f"Ошибка при закрытии httpx клиента: {e}")
logger.error(f" Тип ошибки: {type(e).__name__}")
import traceback
logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
async def main():
"""Главная функция"""
try:
success = await test_telegram_publish()
if success:
logger.info("=" * 80)
logger.info("ТЕСТ ЗАВЕРШЕН УСПЕШНО")
logger.info("=" * 80)
sys.exit(0)
else:
logger.error("=" * 80)
logger.error("ТЕСТ ЗАВЕРШЕН С ОШИБКОЙ")
logger.error("=" * 80)
sys.exit(1)
except KeyboardInterrupt:
logger.warning("Прервано пользователем")
sys.exit(1)
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
import traceback
logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""LEGACY — не входит в пайплайн runner.sh с версии 1.0.0.
Проставляет marked_to_publication = 1 видимым событиям (is_visible = 1),
у которых флаг ещё не установлен.
Запуск вручную из корня проекта:
./venv/bin/python tools/legacy/evt_prefetch.py
"""
import os
import mysql.connector
from mysql.connector import Error
from dotenv import load_dotenv
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
load_dotenv(os.path.join(_PROJECT_ROOT, ".env"))
load_dotenv()
MDB_HOST = os.getenv('MDB_HOST')
MDB_USER = os.getenv('MDB_USER')
MDB_PW = os.getenv('MDB_PW')
MDBASE = os.getenv('MDBASE')
def prefetch_all_events():
"""
Проверяет записи в таблице events и устанавливает флаг marked_to_publication
для записей, которые:
- имеют флаг is_visible = 1
- не имеют флага marked_to_publication = 1
"""
try:
connection = mysql.connector.connect(
host=MDB_HOST,
user=MDB_USER,
password=MDB_PW,
database=MDBASE
)
cursor_temp = connection.cursor()
cursor_temp.execute("SET time_zone = '+00:00'")
cursor_temp.close()
cursor = connection.cursor()
update_query = """
UPDATE events
SET marked_to_publication = 1
WHERE is_visible = 1
AND marked_to_publication = 0
"""
cursor.execute(update_query)
updated_count = cursor.rowcount
connection.commit()
print(f"Обновлено записей: {updated_count}")
return updated_count
except Error as e:
print(f"Ошибка базы данных: {e}")
if 'connection' in locals() and connection.is_connected():
connection.rollback()
return 0
except Exception as e:
print(f"Общая ошибка: {e}")
return 0
finally:
if 'connection' in locals() and connection.is_connected():
cursor.close()
connection.close()
if __name__ == "__main__":
prefetch_all_events()
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
# Использование (из корня проекта):
# bash tools/ops/copy_databases.sh <source_db> <target_db> <username>
# Пример:
# bash tools/ops/copy_databases.sh old_db new_db appuser
set -e # Завершать выполнение при любой ошибке
if [ "$#" -ne 3 ]; then
echo "Использование: $0 <исходная_база> <целевая_база> <имя_пользователя>"
exit 1
fi
SOURCE_DB="$1"
TARGET_DB="$2"
DB_USER="$3"
BACKUP_DIR="."
# Функция для запроса пароля от MariaDB один раз
read_mysql_password() {
if [ -z "$MYSQL_PWD" ]; then
read -s -p "Введите пароль MariaDB для пользователя root: " MYSQL_PWD
echo
export MYSQL_PWD
fi
}
# Функция проверки существования базы данных
db_exists() {
local db_name="$1"
if mysql -u root -e "SHOW DATABASES LIKE '$db_name';" | grep -q "^$db_name$"; then
return 0
else
return 1
fi
}
# Функция резервного копирования базы
backup_db() {
local db_name="$1"
local backup_file="${BACKUP_DIR}/${db_name}.backup.sql"
echo "Создаётся резервная копия базы '$db_name' в файл '$backup_file'..."
mysqldump -u root \
--routines \
--triggers \
--events \
--single-transaction \
"$db_name" > "$backup_file"
echo "Резервная копия '$db_name' успешно создана."
}
# === НАЧАЛО ОСНОВНОГО СЦЕНАРИЯ ===
read_mysql_password
echo "Проверка существования исходной базы '$SOURCE_DB'..."
if ! db_exists "$SOURCE_DB"; then
echo "Ошибка: исходная база данных '$SOURCE_DB' не существует."
exit 1
fi
# Резервное копирование исходной базы (всегда)
backup_db "$SOURCE_DB"
# Проверка существования целевой базы
if db_exists "$TARGET_DB"; then
echo "Целевая база '$TARGET_DB' уже существует."
backup_db "$TARGET_DB"
echo "Удаление существующей базы '$TARGET_DB'..."
mysql -u root -e "DROP DATABASE \`$TARGET_DB\`;"
else
echo "Целевая база '$TARGET_DB' не существует — будет создана."
fi
# Создание новой целевой базы
echo "Создание новой базы '$TARGET_DB'..."
mysql -u root -e "CREATE DATABASE \`$TARGET_DB\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Создание временного дампа без указания базы (без CREATE DATABASE и USE)
TEMP_DUMP=$(mktemp)
echo "Создание временного дампа из '$SOURCE_DB'..."
mysqldump -u root \
--routines \
--triggers \
--events \
--single-transaction \
--no-create-db \
"$SOURCE_DB" > "$TEMP_DUMP"
# Восстановление в целевую базу
echo "Восстановление данных в '$TARGET_DB'..."
mysql -u root "$TARGET_DB" < "$TEMP_DUMP"
# Удаление временного файла
rm -f "$TEMP_DUMP"
# Настройка прав пользователя
echo "Настройка прав для пользователя '$DB_USER'@'%' на базу '$TARGET_DB'..."
mysql -u root -e "CREATE USER IF NOT EXISTS \`$DB_USER\`@'%';"
mysql -u root -e "GRANT ALL PRIVILEGES ON \`$TARGET_DB\`.* TO \`$DB_USER\`@'%';"
mysql -u root -e "FLUSH PRIVILEGES;"
echo "✅ Копирование завершено успешно!"
echo "Исходная база: $SOURCE_DB → Целевая база: $TARGET_DB"
echo "Пользователь: $DB_USER@% имеет полные права на целевую базу."
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
"""Генерация случайного hex-ключа для SECRET_KEY или WEBHOOK_SECRET.
Запуск из корня проекта:
python tools/ops/genkey.py
"""
import secrets
if __name__ == "__main__":
print(secrets.token_hex(32))
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Ручная регистрация Telegram webhook через internal_relay (с secret_token).
Запуск из корня проекта:
./venv/bin/python tools/ops/register_tg_webhook.py
Печатает полный ответ setWebhook — по нему видно, принял ли Telegram секрет.
"""
import json
import sys
import os
_PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
if _PROJECT_ROOT not in sys.path:
sys.path.insert(0, _PROJECT_ROOT)
from dotenv import load_dotenv
import requests
load_dotenv(os.path.join(_PROJECT_ROOT, ".env"))
load_dotenv()
from telegram_relay import bot_api_method_url, get_relay_url
TOKEN = os.getenv("RESPONDER_BOT_TOKEN")
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
WEBHOOK_SECRET = (os.getenv("WEBHOOK_SECRET") or "").strip().strip('"').strip("'")
def main() -> int:
if not TOKEN:
print("ERROR: RESPONDER_BOT_TOKEN не задан")
return 1
if not WEBHOOK_URL:
print("ERROR: WEBHOOK_URL не задан")
return 1
if not WEBHOOK_SECRET:
print("ERROR: WEBHOOK_SECRET не задан")
return 1
api_url = bot_api_method_url(TOKEN, "setWebhook")
form = {
"url": WEBHOOK_URL,
"secret_token": WEBHOOK_SECRET,
"max_connections": "40",
# Иначе Telegram может сохранить старый список без callback_query
"allowed_updates": json.dumps([
"message",
"edited_message",
"callback_query",
]),
}
print(f"RELAY_URL = {get_relay_url()}")
print(f"WEBHOOK_URL = {WEBHOOK_URL}")
print(f"secret_len = {len(WEBHOOK_SECRET)}")
print(f"POST = {api_url.replace(TOKEN, '<TOKEN>')}")
print(f"form keys = {list(form.keys())}")
resp = requests.post(api_url, data=form, timeout=60)
print(f"HTTP status = {resp.status_code}")
try:
data = resp.json()
print("JSON response:")
print(json.dumps(data, ensure_ascii=False, indent=2))
except Exception:
print("Raw response:")
print(resp.text[:1000])
return 1
if not data.get("ok"):
print("FAILED: Telegram/relay вернул ok=false")
return 1
print("OK: setWebhook принят. Нажмите «Подписка» / отправьте /start боту.")
print("В логе бота не должно быть «Секретный токен отсутствует».")
return 0
if __name__ == "__main__":
sys.exit(main())