Compare commits
2
Commits
685938d9f7
...
179d78a750
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
179d78a750 | ||
|
|
837d5a171f |
+114
-4
@@ -25,6 +25,7 @@ from db_update_shortname import ai_shortname_all, generate_ai_shortname
|
|||||||
from evtg_publish import tg_post_event_by_id, tg_post_all_events
|
from evtg_publish import tg_post_event_by_id, tg_post_all_events
|
||||||
from zk_load import load_json_all
|
from zk_load import load_json_all
|
||||||
from volk_load import load_categories, load_all_events
|
from volk_load import load_categories, load_all_events
|
||||||
|
from volk_cat_publish import tg_post_category_by_id, tg_post_all_categories
|
||||||
|
|
||||||
# Загружаем настройки из .env
|
# Загружаем настройки из .env
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -1072,6 +1073,92 @@ def api_publish_all_events():
|
|||||||
log_event(error_msg)
|
log_event(error_msg)
|
||||||
return jsonify({'success': False, 'error': error_msg}), 500
|
return jsonify({'success': False, 'error': error_msg}), 500
|
||||||
|
|
||||||
|
@bp.route('/api/publish_all_categories', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def api_publish_all_categories():
|
||||||
|
try:
|
||||||
|
log_event("Запуск публикации всех площадок...")
|
||||||
|
|
||||||
|
# Создаем асинхронную обертку для запуска в отдельном потоке
|
||||||
|
import asyncio
|
||||||
|
from threading import Thread
|
||||||
|
import queue
|
||||||
|
|
||||||
|
def run_async_func():
|
||||||
|
try:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
result = loop.run_until_complete(tg_post_all_categories())
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
|
||||||
|
# Создаем очередь для передачи результата
|
||||||
|
q = queue.Queue()
|
||||||
|
# Запускаем в отдельном потоке
|
||||||
|
thread = Thread(target=lambda: q.put(run_async_func()))
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
result = q.get()
|
||||||
|
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
raise result
|
||||||
|
|
||||||
|
log_event("Публикация всех площадок выполнена успешно")
|
||||||
|
return jsonify({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Ошибка при публикации всех площадок: {str(e)}"
|
||||||
|
log_event(error_msg)
|
||||||
|
return jsonify({'success': False, 'error': error_msg}), 500
|
||||||
|
|
||||||
|
@bp.route('/api/publish_category/<category_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def api_publish_category(category_id):
|
||||||
|
try:
|
||||||
|
# Сначала проверяем существование категории
|
||||||
|
db = get_db()
|
||||||
|
with db.conn.cursor() as cursor:
|
||||||
|
cursor.execute("SELECT ID, TITLE FROM categories WHERE ID = %s", (category_id,))
|
||||||
|
category = cursor.fetchone()
|
||||||
|
if not category:
|
||||||
|
return jsonify({'success': False, 'error': 'Category not found'}), 404
|
||||||
|
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
log_event(f"Запуск публикации площадки ID: {category_id}")
|
||||||
|
|
||||||
|
# Создаем асинхронную обертку для запуска в отдельном потоке
|
||||||
|
import asyncio
|
||||||
|
from threading import Thread
|
||||||
|
import queue
|
||||||
|
|
||||||
|
def run_async_func():
|
||||||
|
try:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
result = loop.run_until_complete(tg_post_category_by_id(category_id))
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
|
||||||
|
# Создаем очередь для передачи результата
|
||||||
|
q = queue.Queue()
|
||||||
|
# Запускаем в отдельном потоке
|
||||||
|
thread = Thread(target=lambda: q.put(run_async_func()))
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
result = q.get()
|
||||||
|
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
raise result
|
||||||
|
|
||||||
|
log_event(f"Публикация площадки ID {category_id} выполнена успешно")
|
||||||
|
return jsonify({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Ошибка при публикации площадки: {str(e)}"
|
||||||
|
log_event(error_msg)
|
||||||
|
return jsonify({'success': False, 'error': error_msg}), 500
|
||||||
|
|
||||||
@bp.route('/api/load_categories', methods=['POST'])
|
@bp.route('/api/load_categories', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def api_load_categories():
|
def api_load_categories():
|
||||||
@@ -1201,23 +1288,46 @@ def api_update_category(category_id):
|
|||||||
db = get_db()
|
db = get_db()
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
|
new_id = data.get('id', '').strip()
|
||||||
|
if not new_id:
|
||||||
|
return jsonify({'success': False, 'error': 'ID is required'}), 400
|
||||||
|
|
||||||
with db.conn.cursor() as cursor:
|
with db.conn.cursor() as cursor:
|
||||||
|
# Сначала проверяем существование категории по старому ID
|
||||||
|
cursor.execute("SELECT AUTO_ID FROM categories WHERE ID = %s", (category_id,))
|
||||||
|
category = cursor.fetchone()
|
||||||
|
|
||||||
|
if not category:
|
||||||
|
return jsonify({'success': False, 'error': 'Category not found'}), 404
|
||||||
|
|
||||||
|
auto_id = category['AUTO_ID']
|
||||||
|
|
||||||
|
# Если новый ID отличается от старого, проверяем, не занят ли он
|
||||||
|
if new_id != category_id:
|
||||||
|
cursor.execute("SELECT AUTO_ID FROM categories WHERE ID = %s", (new_id,))
|
||||||
|
existing = cursor.fetchone()
|
||||||
|
if existing and existing['AUTO_ID'] != auto_id:
|
||||||
|
return jsonify({'success': False, 'error': f'Category with ID "{new_id}" already exists'}), 400
|
||||||
|
|
||||||
|
# Обновляем категорию, включая ID если он изменился
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
UPDATE categories
|
UPDATE categories
|
||||||
SET TITLE = %s, description = %s, image_url = %s, tg_id = %s, marked_for_publication = %s
|
SET ID = %s, TITLE = %s, description = %s, image_url = %s, tg_id = %s, marked_for_publication = %s
|
||||||
WHERE ID = %s
|
WHERE AUTO_ID = %s
|
||||||
""", (
|
""", (
|
||||||
|
new_id,
|
||||||
data.get('title', ''),
|
data.get('title', ''),
|
||||||
data.get('description', ''),
|
data.get('description', ''),
|
||||||
data.get('image_url', ''),
|
data.get('image_url', ''),
|
||||||
data.get('tg_id'),
|
data.get('tg_id'),
|
||||||
bool(data.get('marked_for_publication', False)),
|
bool(data.get('marked_for_publication', False)),
|
||||||
category_id
|
auto_id
|
||||||
))
|
))
|
||||||
db.conn.commit()
|
db.conn.commit()
|
||||||
|
|
||||||
if cursor.rowcount > 0:
|
if cursor.rowcount > 0:
|
||||||
client_ip = get_client_ip()
|
client_ip = get_client_ip()
|
||||||
log_event(f"Обновлена площадка: {category_id}, IP {client_ip}")
|
log_event(f"Обновлена площадка: {category_id} -> {new_id}, IP {client_ip}")
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
else:
|
else:
|
||||||
return jsonify({'success': False, 'error': 'Category not found'}), 404
|
return jsonify({'success': False, 'error': 'Category not found'}), 404
|
||||||
|
|||||||
@@ -668,6 +668,9 @@
|
|||||||
<!-- Вкладка площадок -->
|
<!-- Вкладка площадок -->
|
||||||
<div class="tab-pane fade" id="categories-tab-pane" role="tabpanel" aria-labelledby="categories-tab" tabindex="0">
|
<div class="tab-pane fade" id="categories-tab-pane" role="tabpanel" aria-labelledby="categories-tab" tabindex="0">
|
||||||
<div class="top-action-buttons d-flex align-items-center">
|
<div class="top-action-buttons d-flex align-items-center">
|
||||||
|
<button class="btn btn-success me-2" id="publish-all-categories-btn">
|
||||||
|
<i class="fas fa-paper-plane me-1"></i> Опубликовать все
|
||||||
|
</button>
|
||||||
<button class="btn btn-info" id="load-categories-btn">
|
<button class="btn btn-info" id="load-categories-btn">
|
||||||
<i class="fas fa-sync-alt me-1"></i> Площадки
|
<i class="fas fa-sync-alt me-1"></i> Площадки
|
||||||
</button>
|
</button>
|
||||||
@@ -1007,6 +1010,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
document.getElementById('publish-all-events-btn').addEventListener('click', publishAllEvents);
|
document.getElementById('publish-all-events-btn').addEventListener('click', publishAllEvents);
|
||||||
{% if g.workmode == 'VOLK' %}
|
{% if g.workmode == 'VOLK' %}
|
||||||
|
const publishAllCategoriesBtn = document.getElementById('publish-all-categories-btn');
|
||||||
|
if (publishAllCategoriesBtn) {
|
||||||
|
publishAllCategoriesBtn.addEventListener('click', publishAllCategories);
|
||||||
|
}
|
||||||
const loadCategoriesBtn = document.getElementById('load-categories-btn');
|
const loadCategoriesBtn = document.getElementById('load-categories-btn');
|
||||||
if (loadCategoriesBtn) {
|
if (loadCategoriesBtn) {
|
||||||
loadCategoriesBtn.addEventListener('click', loadCategories);
|
loadCategoriesBtn.addEventListener('click', loadCategories);
|
||||||
@@ -2050,6 +2057,9 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div>
|
<div>
|
||||||
|
<button class="btn btn-sm btn-success me-2" id="publish-category-btn" ${!category.marked_for_publication || category.is_tg_published ? 'style="display: none;"' : ''}>
|
||||||
|
<i class="fas fa-paper-plane me-1"></i>Опубликовать
|
||||||
|
</button>
|
||||||
<button class="btn btn-sm btn-info me-2" id="toggle-category-pub-btn">
|
<button class="btn btn-sm btn-info me-2" id="toggle-category-pub-btn">
|
||||||
${category.marked_for_publication ?
|
${category.marked_for_publication ?
|
||||||
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
|
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
|
||||||
@@ -2103,6 +2113,14 @@
|
|||||||
detailsHtml += `</div>`;
|
detailsHtml += `</div>`;
|
||||||
detailsPanel.innerHTML = detailsHtml;
|
detailsPanel.innerHTML = detailsHtml;
|
||||||
|
|
||||||
|
// Обработчик для кнопки публикации категории
|
||||||
|
const publishBtn = document.getElementById('publish-category-btn');
|
||||||
|
if (publishBtn) {
|
||||||
|
publishBtn.addEventListener('click', () => {
|
||||||
|
publishCategory(category.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Обработчик для кнопки переключения публикации категории
|
// Обработчик для кнопки переключения публикации категории
|
||||||
const togglePubBtn = document.getElementById('toggle-category-pub-btn');
|
const togglePubBtn = document.getElementById('toggle-category-pub-btn');
|
||||||
if (togglePubBtn) {
|
if (togglePubBtn) {
|
||||||
@@ -2226,6 +2244,66 @@
|
|||||||
alert('Произошла ошибка при удалении площадки');
|
alert('Произошла ошибка при удалении площадки');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Публикация категории
|
||||||
|
async function publishCategory(categoryId) {
|
||||||
|
showProcessingMessage("Публикация площадки...");
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${PREFIX}/api/publish_category/${encodeURIComponent(categoryId)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
hideProcessingMessage();
|
||||||
|
if (data.success) {
|
||||||
|
alert('Площадка успешно опубликована!');
|
||||||
|
// Обновляем детали категории
|
||||||
|
showCategoryDetails(categoryId);
|
||||||
|
// Обновляем список категорий
|
||||||
|
loadCategoriesList();
|
||||||
|
} else {
|
||||||
|
alert('Ошибка публикации площадки: ' + (data.error || 'неизвестная ошибка'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
hideProcessingMessage();
|
||||||
|
console.error('Ошибка публикации площадки:', error);
|
||||||
|
alert('Произошла ошибка при публикации площадки');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Публикация всех категорий
|
||||||
|
function publishAllCategories() {
|
||||||
|
const currentCategoryId = selectedCategoryId;
|
||||||
|
showProcessingMessage("Публикация всех площадок...");
|
||||||
|
fetch(`${PREFIX}/api/publish_all_categories`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
hideProcessingMessage();
|
||||||
|
if (data.success) {
|
||||||
|
alert('Публикация всех площадок выполнена успешно!');
|
||||||
|
loadCategoriesList().then(() => {
|
||||||
|
if (currentCategoryId) {
|
||||||
|
highlightSelectedCategory(currentCategoryId);
|
||||||
|
showCategoryDetails(currentCategoryId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert('Ошибка публикации всех площадок: ' + (data.error || 'неизвестная ошибка'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
hideProcessingMessage();
|
||||||
|
console.error('Ошибка публикации всех площадок:', error);
|
||||||
|
alert('Произошла ошибка при публикации всех площадок');
|
||||||
|
});
|
||||||
|
}
|
||||||
// Функция загрузки постов
|
// Функция загрузки постов
|
||||||
async function loadPosts() {
|
async function loadPosts() {
|
||||||
const tableBody = document.getElementById('posts-table');
|
const tableBody = document.getElementById('posts-table');
|
||||||
|
|||||||
@@ -0,0 +1,472 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import pymysql
|
||||||
|
import time
|
||||||
|
import html
|
||||||
|
import httpx
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from formatter import get_event_text
|
||||||
|
|
||||||
|
# Загрузка переменных окружения
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Настройки из переменных окружения
|
||||||
|
BOT_TOKEN = os.getenv('POSTER_BOT_TOKEN')
|
||||||
|
CHANNEL_ID = os.getenv('CHANNEL_ID')
|
||||||
|
MDB_HOST = os.getenv('MDB_HOST')
|
||||||
|
MDB_USER = os.getenv('MDB_USER')
|
||||||
|
MDB_PW = os.getenv('MDB_PW')
|
||||||
|
MDBASE = os.getenv('MDBASE')
|
||||||
|
LOG_FILE = os.getenv('LOG_FILE', 'volk_cat_publisher.log')
|
||||||
|
|
||||||
|
# Параметры длины сообщений
|
||||||
|
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')
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logger = logging.getLogger('TG_CAT_post')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
formatter = logging.Formatter('[%(asctime)s] [%(name)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
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.setFormatter(formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Класс для обработки RetryAfter (FloodWait 429)
|
||||||
|
class RetryAfterException(Exception):
|
||||||
|
"""Исключение для обработки FloodWait (429) от Telegram API"""
|
||||||
|
def __init__(self, retry_after):
|
||||||
|
self.retry_after = retry_after
|
||||||
|
super().__init__(f"RetryAfter: {retry_after}")
|
||||||
|
|
||||||
|
def prepare_text(text):
|
||||||
|
"""Подготовка текстовых полей с экранированием HTML-сущностей"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Экранируем специальные символы HTML
|
||||||
|
text = html.escape(str(text))
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
async def tg_post_category(httpx_client, category_data):
|
||||||
|
"""Публикация одной категории"""
|
||||||
|
try:
|
||||||
|
# Форматирование текста
|
||||||
|
title = prepare_text(category_data['TITLE'] or "")
|
||||||
|
description_raw = category_data['description']
|
||||||
|
|
||||||
|
# Формируем базовый текст (название категории)
|
||||||
|
caption_parts = []
|
||||||
|
if title:
|
||||||
|
caption_parts.append(f"<b>{title}</b>")
|
||||||
|
base_text = "\n".join(caption_parts)
|
||||||
|
base_text_length = len(base_text)
|
||||||
|
|
||||||
|
# Определяем максимальную длину в зависимости от типа сообщения
|
||||||
|
image_url = category_data['image_url']
|
||||||
|
has_image = image_url and image_url.strip()
|
||||||
|
max_length = MAX_CAPTION_LENGTH if has_image else MAX_TEXT_LENGTH
|
||||||
|
|
||||||
|
# Вычисляем доступную длину для описания категории
|
||||||
|
available_length = max_length - base_text_length - 3 # -3 для символов переноса строки
|
||||||
|
if available_length < 0:
|
||||||
|
available_length = 0
|
||||||
|
|
||||||
|
# Получаем обработанный текст с учетом доступной длины
|
||||||
|
description = get_event_text(description_raw, available_length) if description_raw else ""
|
||||||
|
|
||||||
|
# Формируем первоначальный текст
|
||||||
|
initial_text = f"{base_text}\n\n{description}" if description else base_text
|
||||||
|
|
||||||
|
# Логируем детальную информацию о данных для публикации
|
||||||
|
logger.info(f"=== Детали публикации категории ID {category_data['ID']} ===")
|
||||||
|
logger.info(f"Длина initial_text: {len(initial_text)} символов")
|
||||||
|
logger.info(f"Длина base_text: {base_text_length} символов")
|
||||||
|
logger.info(f"Длина description: {len(description)} символов")
|
||||||
|
logger.info(f"Максимальная длина (max_length): {max_length} символов")
|
||||||
|
logger.info(f"Доступная длина для description: {available_length} символов")
|
||||||
|
logger.info(f"Есть изображение: {has_image}")
|
||||||
|
if has_image:
|
||||||
|
logger.info(f"URL изображения: {image_url}")
|
||||||
|
logger.info(f"Длина URL изображения: {len(image_url)} символов")
|
||||||
|
logger.info(f"Текст для публикации: {initial_text[:200]}..." if len(initial_text) > 200 else f"Текст для публикации: {initial_text}")
|
||||||
|
|
||||||
|
# Публикация сообщения
|
||||||
|
send_photo_url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
|
||||||
|
send_message_url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||||
|
|
||||||
|
if has_image:
|
||||||
|
try:
|
||||||
|
logger.info(f"Попытка отправки фото для категории {category_data['ID']}: chat_id={CHANNEL_ID}, caption_length={len(initial_text)}, image_url_length={len(image_url)}")
|
||||||
|
photo_payload = {
|
||||||
|
'chat_id': CHANNEL_ID,
|
||||||
|
'photo': image_url,
|
||||||
|
'caption': initial_text,
|
||||||
|
'parse_mode': 'HTML',
|
||||||
|
'disable_notification': PUBLISH_SILENTLY
|
||||||
|
}
|
||||||
|
photo_response = await httpx_client.post(send_photo_url, json=photo_payload)
|
||||||
|
|
||||||
|
if photo_response.status_code == 200:
|
||||||
|
photo_result = photo_response.json()
|
||||||
|
if photo_result.get('ok'):
|
||||||
|
message_data = photo_result.get('result', {})
|
||||||
|
message_id = message_data.get('message_id')
|
||||||
|
logger.info(f"Фото успешно отправлено для категории {category_data['ID']}, message_id={message_id}")
|
||||||
|
else:
|
||||||
|
# Ошибка в ответе API - пробуем отправить текстовое сообщение
|
||||||
|
error_code = photo_result.get('error_code')
|
||||||
|
if error_code == 429:
|
||||||
|
# FloodWait - пробрасываем как RetryAfter
|
||||||
|
retry_after = photo_result.get('parameters', {}).get('retry_after', 60)
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
raise Exception(f"API error: {photo_result.get('description', 'Unknown error')}")
|
||||||
|
else:
|
||||||
|
if photo_response.status_code == 429:
|
||||||
|
# FloodWait
|
||||||
|
retry_after = 60
|
||||||
|
try:
|
||||||
|
error_data = photo_response.json()
|
||||||
|
retry_after = error_data.get('parameters', {}).get('retry_after', 60)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
raise Exception(f"HTTP {photo_response.status_code}: {photo_response.text}")
|
||||||
|
|
||||||
|
except httpx.TimeoutException as e:
|
||||||
|
logger.error(f"ТАЙМАУТ при отправке изображения для категории ID {category_data['ID']}")
|
||||||
|
logger.error(f"Полная диагностическая информация об ошибке TimedOut:")
|
||||||
|
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" image_url: {image_url}")
|
||||||
|
logger.error(f" caption_length: {len(initial_text)}")
|
||||||
|
logger.error(f" parse_mode: HTML")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
if error_msg.startswith("RetryAfter:"):
|
||||||
|
retry_after = int(error_msg.split(":")[1])
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
logger.warning(f"Не удалось отправить изображение для категории ID {category_data['ID']}: {e}. Отправляем текстовое сообщение.")
|
||||||
|
logger.error(f"Полная информация об ошибке Telegram при отправке изображения для категории {category_data['ID']}:")
|
||||||
|
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)}")
|
||||||
|
logger.info(f"Попытка отправки текстового сообщения вместо фото для категории {category_data['ID']}")
|
||||||
|
try:
|
||||||
|
text_payload = {
|
||||||
|
'chat_id': CHANNEL_ID,
|
||||||
|
'text': initial_text,
|
||||||
|
'parse_mode': 'HTML',
|
||||||
|
'disable_notification': PUBLISH_SILENTLY,
|
||||||
|
'disable_web_page_preview': True
|
||||||
|
}
|
||||||
|
text_response = await httpx_client.post(send_message_url, json=text_payload)
|
||||||
|
|
||||||
|
if text_response.status_code == 200:
|
||||||
|
text_result = text_response.json()
|
||||||
|
if text_result.get('ok'):
|
||||||
|
message_data = text_result.get('result', {})
|
||||||
|
message_id = message_data.get('message_id')
|
||||||
|
logger.info(f"Текстовое сообщение успешно отправлено для категории {category_data['ID']}, message_id={message_id}")
|
||||||
|
else:
|
||||||
|
error_code = text_result.get('error_code')
|
||||||
|
if error_code == 429:
|
||||||
|
retry_after = text_result.get('parameters', {}).get('retry_after', 60)
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
raise Exception(f"API error: {text_result.get('description', 'Unknown error')}")
|
||||||
|
else:
|
||||||
|
if text_response.status_code == 429:
|
||||||
|
retry_after = 60
|
||||||
|
try:
|
||||||
|
error_data = text_response.json()
|
||||||
|
retry_after = error_data.get('parameters', {}).get('retry_after', 60)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
raise Exception(f"HTTP {text_response.status_code}: {text_response.text}")
|
||||||
|
except httpx.TimeoutException as e:
|
||||||
|
logger.error(f"ТАЙМАУТ при отправке текстового сообщения для категории ID {category_data['ID']}")
|
||||||
|
logger.error(f"Полная диагностическая информация об ошибке TimedOut:")
|
||||||
|
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(initial_text)}")
|
||||||
|
logger.error(f" parse_mode: HTML")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
if error_msg.startswith("RetryAfter:"):
|
||||||
|
raise
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
logger.info(f"Попытка отправки текстового сообщения для категории {category_data['ID']}: chat_id={CHANNEL_ID}, text_length={len(initial_text)}")
|
||||||
|
try:
|
||||||
|
text_payload = {
|
||||||
|
'chat_id': CHANNEL_ID,
|
||||||
|
'text': initial_text,
|
||||||
|
'parse_mode': 'HTML',
|
||||||
|
'disable_notification': PUBLISH_SILENTLY,
|
||||||
|
'disable_web_page_preview': True
|
||||||
|
}
|
||||||
|
text_response = await httpx_client.post(send_message_url, json=text_payload)
|
||||||
|
|
||||||
|
if text_response.status_code == 200:
|
||||||
|
text_result = text_response.json()
|
||||||
|
if text_result.get('ok'):
|
||||||
|
message_data = text_result.get('result', {})
|
||||||
|
message_id = message_data.get('message_id')
|
||||||
|
logger.info(f"Текстовое сообщение успешно отправлено для категории {category_data['ID']}, message_id={message_id}")
|
||||||
|
else:
|
||||||
|
error_code = text_result.get('error_code')
|
||||||
|
if error_code == 429:
|
||||||
|
retry_after = text_result.get('parameters', {}).get('retry_after', 60)
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
raise Exception(f"API error: {text_result.get('description', 'Unknown error')}")
|
||||||
|
else:
|
||||||
|
if text_response.status_code == 429:
|
||||||
|
retry_after = 60
|
||||||
|
try:
|
||||||
|
error_data = text_response.json()
|
||||||
|
retry_after = error_data.get('parameters', {}).get('retry_after', 60)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
raise Exception(f"HTTP {text_response.status_code}: {text_response.text}")
|
||||||
|
except httpx.TimeoutException as e:
|
||||||
|
logger.error(f"ТАЙМАУТ при отправке текстового сообщения для категории ID {category_data['ID']}")
|
||||||
|
logger.error(f"Полная диагностическая информация об ошибке TimedOut:")
|
||||||
|
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(initial_text)}")
|
||||||
|
logger.error(f" parse_mode: HTML")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return message_id # Возвращаем только message_id
|
||||||
|
|
||||||
|
except httpx.TimeoutException as e:
|
||||||
|
logger.error(f"ТАЙМАУТ при публикации категории ID {category_data['ID']}")
|
||||||
|
logger.error(f"Полная диагностическая информация об ошибке TimedOut:")
|
||||||
|
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" ID: {category_data.get('ID')}")
|
||||||
|
logger.error(f" has_image: {bool(category_data.get('image_url'))}")
|
||||||
|
logger.error(f" image_url_length: {len(category_data.get('image_url', '') or '')}")
|
||||||
|
logger.error(f" initial_text_length: {len(initial_text) if 'initial_text' in locals() else 'N/A'}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
if error_msg.startswith("RetryAfter:"):
|
||||||
|
# Обработка ошибки FloodWait (429)
|
||||||
|
retry_after = int(error_msg.split(":")[1])
|
||||||
|
logger.error(f"Получена ошибка FloodWait (429) при публикации категории ID {category_data['ID']}")
|
||||||
|
logger.error(f"Необходимо подождать {retry_after} секунд перед следующей попыткой")
|
||||||
|
logger.error(f"Полная информация об ошибке RetryAfter для категории {category_data['ID']}:")
|
||||||
|
logger.error(f" Тип ошибки: RetryAfter")
|
||||||
|
logger.error(f" Сообщение: {error_msg}")
|
||||||
|
logger.error(f" retry_after: {retry_after}")
|
||||||
|
raise RetryAfterException(retry_after)
|
||||||
|
logger.error(f"Ошибка Telegram при публикации категории ID {category_data['ID']}: {e}")
|
||||||
|
logger.error(f"Полная информация об ошибке Telegram для категории {category_data['ID']}:")
|
||||||
|
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)}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Неожиданная ошибка при публикации категории ID {category_data['ID']}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def tg_post_category_by_id(id_category):
|
||||||
|
"""Публикация категории по ID"""
|
||||||
|
logger.info(f"Запуск публикации для категории ID {id_category}")
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host=MDB_HOST,
|
||||||
|
user=MDB_USER,
|
||||||
|
password=MDB_PW,
|
||||||
|
database=MDBASE,
|
||||||
|
charset='utf8mb4',
|
||||||
|
cursorclass=pymysql.cursors.DictCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT AUTO_ID, ID, TITLE, description, image_url, tg_id
|
||||||
|
FROM categories
|
||||||
|
WHERE ID = %s
|
||||||
|
AND marked_for_publication = True
|
||||||
|
AND is_tg_published = False
|
||||||
|
""", (id_category,))
|
||||||
|
category = cursor.fetchone()
|
||||||
|
|
||||||
|
if not category:
|
||||||
|
logger.info(f"Категория ID {id_category} не найдена или уже опубликована")
|
||||||
|
return
|
||||||
|
|
||||||
|
old_tg_id = category.get('tg_id') # Сохраняем старый ID сообщения
|
||||||
|
# Создаем httpx клиент с отключенным HTTP/2
|
||||||
|
httpx_client = httpx.AsyncClient(
|
||||||
|
http2=False,
|
||||||
|
timeout=20.0,
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
tg_id = await tg_post_category(httpx_client, category) # Получаем новый message_id
|
||||||
|
finally:
|
||||||
|
await httpx_client.aclose()
|
||||||
|
|
||||||
|
if tg_id:
|
||||||
|
# Используем UTC время для записи в базу данных
|
||||||
|
current_time_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE categories
|
||||||
|
SET is_tg_published = True,
|
||||||
|
tg_id = %s,
|
||||||
|
tg_published_date = %s
|
||||||
|
WHERE ID = %s
|
||||||
|
""", (tg_id, current_time_utc, id_category))
|
||||||
|
conn.commit()
|
||||||
|
logger.info(f"Категория ID {category['ID']} успешно опубликована")
|
||||||
|
else:
|
||||||
|
logger.error(f"Не удалось опубликовать категорию ID {id_category}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Проверяем, не RetryAfter ли это
|
||||||
|
error_msg = str(e)
|
||||||
|
if error_msg.startswith("RetryAfter:") or (hasattr(e, 'retry_after')):
|
||||||
|
retry_after = getattr(e, 'retry_after', int(error_msg.split(":")[1]) if ":" in error_msg else 60)
|
||||||
|
logger.error(f"Полная информация об ошибке RetryAfter в tg_post_category_by_id для категории ID {id_category}:")
|
||||||
|
logger.error(f" Тип ошибки: RetryAfter")
|
||||||
|
logger.error(f" Сообщение: {error_msg}")
|
||||||
|
logger.error(f" retry_after: {retry_after}")
|
||||||
|
logger.error(f" Полное представление: {repr(e)}")
|
||||||
|
raise RetryAfterException(retry_after)
|
||||||
|
# Если это не RetryAfter, обрабатываем как обычную ошибку
|
||||||
|
if "RetryAfter" not in error_msg:
|
||||||
|
logger.error(f"Ошибка при публикации: {e}")
|
||||||
|
logger.error(f"Полная информация об ошибке в tg_post_category_by_id для категории ID {id_category}:")
|
||||||
|
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)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при публикации: {e}")
|
||||||
|
logger.error(f"Полная информация об ошибке в tg_post_category_by_id для категории ID {id_category}:")
|
||||||
|
logger.error(f" Тип ошибки: {type(e).__name__}")
|
||||||
|
logger.error(f" Сообщение: {str(e)}")
|
||||||
|
logger.error(f" Все атрибуты ошибки: {vars(e)}")
|
||||||
|
logger.error(f" Полное представление: {repr(e)}")
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
async def tg_post_all_categories():
|
||||||
|
"""Публикация всех неопубликованных категорий (максимум 5 за один вызов)"""
|
||||||
|
logger.info("Запуск скрипта публикации всех категорий")
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host=MDB_HOST,
|
||||||
|
user=MDB_USER,
|
||||||
|
password=MDB_PW,
|
||||||
|
database=MDBASE,
|
||||||
|
charset='utf8mb4',
|
||||||
|
cursorclass=pymysql.cursors.DictCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT ID
|
||||||
|
FROM categories
|
||||||
|
WHERE marked_for_publication = True
|
||||||
|
AND is_tg_published = False
|
||||||
|
LIMIT 5
|
||||||
|
""")
|
||||||
|
categories = cursor.fetchall()
|
||||||
|
|
||||||
|
category_count = 0
|
||||||
|
for category in categories:
|
||||||
|
try:
|
||||||
|
await tg_post_category_by_id(category['ID'])
|
||||||
|
category_count += 1
|
||||||
|
|
||||||
|
# Задержка между публикациями разных категорий
|
||||||
|
if category_count < len(categories):
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
except RetryAfterException as e:
|
||||||
|
logger.error(f"Прерываем публикацию из-за ошибки FloodWait. Ожидание: {e.retry_after} секунд")
|
||||||
|
logger.error(f"Полная информация об ошибке RetryAfter в tg_post_all_categories для категории ID {category['ID']}:")
|
||||||
|
logger.error(f" Тип ошибки: RetryAfter")
|
||||||
|
logger.error(f" Сообщение: {str(e)}")
|
||||||
|
logger.error(f" retry_after: {e.retry_after}")
|
||||||
|
logger.error(f" Полное представление: {repr(e)}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при публикации категории ID {category['ID']}: {e}")
|
||||||
|
logger.error(f"Полная информация об ошибке в tg_post_all_categories для категории ID {category['ID']}:")
|
||||||
|
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)}")
|
||||||
|
# Продолжаем публикацию следующих категорий, несмотря на ошибку
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при публикации категории ID {category['ID']}: {e}")
|
||||||
|
logger.error(f"Полная информация об ошибке в tg_post_all_categories для категории ID {category['ID']}:")
|
||||||
|
logger.error(f" Тип ошибки: {type(e).__name__}")
|
||||||
|
logger.error(f" Сообщение: {str(e)}")
|
||||||
|
logger.error(f" Все атрибуты ошибки: {vars(e)}")
|
||||||
|
logger.error(f" Полное представление: {repr(e)}")
|
||||||
|
# Продолжаем публикацию следующих категорий, несмотря на ошибку
|
||||||
|
|
||||||
|
logger.info(f"Опубликовано категорий в этом запуске: {category_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при публикации: {e}")
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
logger.info("Завершение работы скрипта")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(tg_post_all_categories())
|
||||||
|
|
||||||
Reference in New Issue
Block a user