This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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@% имеет полные права на целевую базу."
|
||||
@@ -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))
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user