diff --git a/db_edit.py b/db_edit.py index 723f66f..cb4e6c2 100644 --- a/db_edit.py +++ b/db_edit.py @@ -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 zk_load import load_json_all from volk_load import load_categories, load_all_events +from volk_cat_publish import tg_post_category_by_id, tg_post_all_categories # Загружаем настройки из .env load_dotenv() @@ -1072,6 +1073,92 @@ def api_publish_all_events(): log_event(error_msg) 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/', 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']) @login_required def api_load_categories(): diff --git a/templates/index.html b/templates/index.html index 90e3dd5..88eead9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -668,6 +668,9 @@
+ @@ -1007,6 +1010,10 @@ {% endif %} document.getElementById('publish-all-events-btn').addEventListener('click', publishAllEvents); {% 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'); if (loadCategoriesBtn) { loadCategoriesBtn.addEventListener('click', loadCategories); @@ -2050,6 +2057,9 @@
+
`; 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'); if (togglePubBtn) { @@ -2226,6 +2244,66 @@ 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() { const tableBody = document.getElementById('posts-table'); diff --git a/volk_cat_publish.py b/volk_cat_publish.py new file mode 100644 index 0000000..b2edf64 --- /dev/null +++ b/volk_cat_publish.py @@ -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"{title}") + 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()) +