diff --git a/evtg_publish.py b/evtg_publish.py index c8237dd..f55220f 100644 --- a/evtg_publish.py +++ b/evtg_publish.py @@ -4,9 +4,8 @@ import asyncio import pymysql import time import html +import httpx from datetime import datetime, timezone, timedelta -from telegram import Bot, InlineKeyboardMarkup, InlineKeyboardButton -from telegram.error import TelegramError, RetryAfter, TimedOut from dotenv import load_dotenv from formatter import get_event_text @@ -61,6 +60,13 @@ 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 format_tags(tags_str): """Форматирование тегов с добавлением #""" if not tags_str: @@ -84,7 +90,7 @@ def prepare_text(text): return text -async def tg_post_event(bot, event_data): +async def tg_post_event(httpx_client, event_data): """Публикация одного события""" try: # Форматирование текста @@ -148,18 +154,48 @@ async def tg_post_event(bot, event_data): 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"Попытка отправки фото для события {event_data['number']}: chat_id={CHANNEL_ID}, caption_length={len(initial_text)}, image_url_length={len(image_url)}") - message = await bot.send_photo( - chat_id=CHANNEL_ID, - photo=image_url, - caption=initial_text, - parse_mode="HTML", - disable_notification=PUBLISH_SILENTLY - ) - logger.info(f"Фото успешно отправлено для события {event_data['number']}, message_id={message.message_id}") - except TimedOut as e: + 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"Фото успешно отправлено для события {event_data['number']}, 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"ТАЙМАУТ при отправке изображения для события номер {event_data['number']}") logger.error(f"Полная диагностическая информация об ошибке TimedOut:") logger.error(f" Тип ошибки: {type(e).__name__}") @@ -172,24 +208,51 @@ async def tg_post_event(bot, event_data): logger.error(f" caption_length: {len(initial_text)}") logger.error(f" parse_mode: HTML") raise - except TelegramError as e: + 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"Не удалось отправить изображение для события номер {event_data['number']}: {e}. Отправляем текстовое сообщение.") logger.error(f"Полная информация об ошибке Telegram при отправке изображения для события {event_data['number']}:") logger.error(f" Тип ошибки: {type(e).__name__}") logger.error(f" Сообщение: {str(e)}") - logger.error(f" Все атрибуты ошибки: {vars(e)}") + logger.error(f" Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}") logger.error(f" Полное представление: {repr(e)}") logger.info(f"Попытка отправки текстового сообщения вместо фото для события {event_data['number']}") try: - message = await bot.send_message( - chat_id=CHANNEL_ID, - text=initial_text, - parse_mode="HTML", - disable_notification=PUBLISH_SILENTLY, - disable_web_page_preview=True # Отключаем предпросмотр ссылок - ) - logger.info(f"Текстовое сообщение успешно отправлено для события {event_data['number']}, message_id={message.message_id}") - except TimedOut as e: + 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"Текстовое сообщение успешно отправлено для события {event_data['number']}, 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"ТАЙМАУТ при отправке текстового сообщения для события номер {event_data['number']}") logger.error(f"Полная диагностическая информация об ошибке TimedOut:") logger.error(f" Тип ошибки: {type(e).__name__}") @@ -201,18 +264,46 @@ async def tg_post_event(bot, event_data): 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"Попытка отправки текстового сообщения для события {event_data['number']}: chat_id={CHANNEL_ID}, text_length={len(initial_text)}") try: - message = await bot.send_message( - chat_id=CHANNEL_ID, - text=initial_text, - parse_mode="HTML", - disable_notification=PUBLISH_SILENTLY, - disable_web_page_preview=True # Отключаем предпросмотр ссылок - ) - logger.info(f"Текстовое сообщение успешно отправлено для события {event_data['number']}, message_id={message.message_id}") - except TimedOut as e: + 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"Текстовое сообщение успешно отправлено для события {event_data['number']}, 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"ТАЙМАУТ при отправке текстового сообщения для события номер {event_data['number']}") logger.error(f"Полная диагностическая информация об ошибке TimedOut:") logger.error(f" Тип ошибки: {type(e).__name__}") @@ -225,9 +316,6 @@ async def tg_post_event(bot, event_data): logger.error(f" parse_mode: HTML") raise - # Получаем ID сообщения для использования в ссылке подписки - message_id = message.message_id - # Формируем финальный текст с ссылками if USE_SUBSCRIPTION_BOT: links_line = f'🔎 Инфо | 📌 Иду | 🔜 Подписка' @@ -239,16 +327,42 @@ async def tg_post_event(bot, event_data): # Редактируем сообщение, добавляя ссылки logger.info(f"Попытка редактирования сообщения для события {event_data['number']}: message_id={message_id}, final_text_length={len(final_text)}") + edit_caption_url = f"https://api.telegram.org/bot{BOT_TOKEN}/editMessageCaption" + edit_text_url = f"https://api.telegram.org/bot{BOT_TOKEN}/editMessageText" + if has_image: try: - await bot.edit_message_caption( - chat_id=CHANNEL_ID, - message_id=message_id, - caption=final_text, - parse_mode="HTML" - ) - logger.info(f"Подпись успешно отредактирована для события {event_data['number']}") - except TimedOut as e: + edit_caption_payload = { + 'chat_id': CHANNEL_ID, + 'message_id': message_id, + 'caption': final_text, + 'parse_mode': 'HTML' + } + edit_caption_response = await httpx_client.post(edit_caption_url, json=edit_caption_payload) + + if edit_caption_response.status_code == 200: + edit_result = edit_caption_response.json() + if edit_result.get('ok'): + logger.info(f"Подпись успешно отредактирована для события {event_data['number']}") + else: + # Ошибка при редактировании подписи - пробуем редактировать текст + error_code = edit_result.get('error_code') + if error_code == 429: + retry_after = edit_result.get('parameters', {}).get('retry_after', 60) + raise Exception(f"RetryAfter:{retry_after}") + raise Exception(f"API error: {edit_result.get('description', 'Unknown error')}") + else: + if edit_caption_response.status_code == 429: + retry_after = 60 + try: + error_data = edit_caption_response.json() + retry_after = error_data.get('parameters', {}).get('retry_after', 60) + except: + pass + raise Exception(f"RetryAfter:{retry_after}") + raise Exception(f"HTTP {edit_caption_response.status_code}: {edit_caption_response.text}") + + except httpx.TimeoutException as e: logger.error(f"ТАЙМАУТ при редактировании подписи для события номер {event_data['number']}") logger.error(f"Полная диагностическая информация об ошибке TimedOut:") logger.error(f" Тип ошибки: {type(e).__name__}") @@ -261,24 +375,48 @@ async def tg_post_event(bot, event_data): logger.error(f" caption_length: {len(final_text)}") logger.error(f" parse_mode: HTML") raise - except TelegramError as e: + except Exception as e: + error_msg = str(e) + if error_msg.startswith("RetryAfter:"): + raise logger.warning(f"Не удалось отредактировать подпись для события номер {event_data['number']}: {e}. Пробуем отредактировать текстовое сообщение.") logger.error(f"Полная информация об ошибке Telegram при редактировании подписи для события {event_data['number']}:") logger.error(f" Тип ошибки: {type(e).__name__}") logger.error(f" Сообщение: {str(e)}") - logger.error(f" Все атрибуты ошибки: {vars(e)}") + logger.error(f" Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}") logger.error(f" Полное представление: {repr(e)}") logger.info(f"Попытка редактирования текста вместо подписи для события {event_data['number']}") try: - await bot.edit_message_text( - chat_id=CHANNEL_ID, - message_id=message_id, - text=final_text, - parse_mode="HTML", - disable_web_page_preview=True # Отключаем предпросмотр ссылок - ) - logger.info(f"Текст успешно отредактирован для события {event_data['number']}") - except TimedOut as e: + edit_text_payload = { + 'chat_id': CHANNEL_ID, + 'message_id': message_id, + 'text': final_text, + 'parse_mode': 'HTML', + 'disable_web_page_preview': True + } + edit_text_response = await httpx_client.post(edit_text_url, json=edit_text_payload) + + if edit_text_response.status_code == 200: + edit_result = edit_text_response.json() + if edit_result.get('ok'): + logger.info(f"Текст успешно отредактирован для события {event_data['number']}") + else: + error_code = edit_result.get('error_code') + if error_code == 429: + retry_after = edit_result.get('parameters', {}).get('retry_after', 60) + raise Exception(f"RetryAfter:{retry_after}") + raise Exception(f"API error: {edit_result.get('description', 'Unknown error')}") + else: + if edit_text_response.status_code == 429: + retry_after = 60 + try: + error_data = edit_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 {edit_text_response.status_code}: {edit_text_response.text}") + except httpx.TimeoutException as e: logger.error(f"ТАЙМАУТ при редактировании текста для события номер {event_data['number']}") logger.error(f"Полная диагностическая информация об ошибке TimedOut:") logger.error(f" Тип ошибки: {type(e).__name__}") @@ -291,17 +429,43 @@ async def tg_post_event(bot, event_data): logger.error(f" text_length: {len(final_text)}") logger.error(f" parse_mode: HTML") raise + except Exception as e: + error_msg = str(e) + if error_msg.startswith("RetryAfter:"): + raise + raise else: try: - await bot.edit_message_text( - chat_id=CHANNEL_ID, - message_id=message_id, - text=final_text, - parse_mode="HTML", - disable_web_page_preview=True # Отключаем предпросмотр ссылок - ) - logger.info(f"Текст успешно отредактирован для события {event_data['number']}") - except TimedOut as e: + edit_text_payload = { + 'chat_id': CHANNEL_ID, + 'message_id': message_id, + 'text': final_text, + 'parse_mode': 'HTML', + 'disable_web_page_preview': True + } + edit_text_response = await httpx_client.post(edit_text_url, json=edit_text_payload) + + if edit_text_response.status_code == 200: + edit_result = edit_text_response.json() + if edit_result.get('ok'): + logger.info(f"Текст успешно отредактирован для события {event_data['number']}") + else: + error_code = edit_result.get('error_code') + if error_code == 429: + retry_after = edit_result.get('parameters', {}).get('retry_after', 60) + raise Exception(f"RetryAfter:{retry_after}") + raise Exception(f"API error: {edit_result.get('description', 'Unknown error')}") + else: + if edit_text_response.status_code == 429: + retry_after = 60 + try: + error_data = edit_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 {edit_text_response.status_code}: {edit_text_response.text}") + except httpx.TimeoutException as e: logger.error(f"ТАЙМАУТ при редактировании текста для события номер {event_data['number']}") logger.error(f"Полная диагностическая информация об ошибке TimedOut:") logger.error(f" Тип ошибки: {type(e).__name__}") @@ -317,17 +481,7 @@ async def tg_post_event(bot, event_data): return message_id # Возвращаем только message_id - except RetryAfter as e: - # Обработка ошибки FloodWait (429) - logger.error(f"Получена ошибка FloodWait (429) при публикации события номер {event_data['number']}: {e}") - logger.error(f"Необходимо подождать {e.retry_after} секунд перед следующей попыткой") - logger.error(f"Полная информация об ошибке RetryAfter для события {event_data['number']}:") - logger.error(f" Тип ошибки: {type(e).__name__}") - logger.error(f" Сообщение: {str(e)}") - logger.error(f" Все атрибуты ошибки: {vars(e)}") - logger.error(f" Полное представление: {repr(e)}") - raise # Пробрасываем исключение выше для обработки на уровне цикла - except TimedOut as e: + except httpx.TimeoutException as e: logger.error(f"ТАЙМАУТ при публикации события номер {event_data['number']}") logger.error(f"Полная диагностическая информация об ошибке TimedOut:") logger.error(f" Тип ошибки: {type(e).__name__}") @@ -341,12 +495,23 @@ async def tg_post_event(bot, event_data): logger.error(f" initial_text_length: {len(initial_text) if 'initial_text' in locals() else 'N/A'}") logger.error(f" final_text_length: {len(final_text) if 'final_text' in locals() else 'N/A'}") return None - except TelegramError as e: + 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) при публикации события номер {event_data['number']}") + logger.error(f"Необходимо подождать {retry_after} секунд перед следующей попыткой") + logger.error(f"Полная информация об ошибке RetryAfter для события {event_data['number']}:") + logger.error(f" Тип ошибки: RetryAfter") + logger.error(f" Сообщение: {error_msg}") + logger.error(f" retry_after: {retry_after}") + raise RetryAfterException(retry_after) logger.error(f"Ошибка Telegram при публикации события номер {event_data['number']}: {e}") logger.error(f"Полная информация об ошибке Telegram для события {event_data['number']}:") logger.error(f" Тип ошибки: {type(e).__name__}") logger.error(f" Сообщение: {str(e)}") - logger.error(f" Все атрибуты ошибки: {vars(e)}") + logger.error(f" Все атрибуты ошибки: {vars(e) if hasattr(e, '__dict__') else 'N/A'}") logger.error(f" Полное представление: {repr(e)}") return None except Exception as e: @@ -402,8 +567,16 @@ async def tg_post_event_by_id(id_event): return old_message_id = event.get('tg_message_id') # Сохраняем старый ID сообщения - bot = Bot(token=BOT_TOKEN) - tg_message_id = await tg_post_event(bot, event) # Получаем новый message_id + # Создаем httpx клиент с отключенным HTTP/2 + httpx_client = httpx.AsyncClient( + http2=False, + timeout=20.0, + follow_redirects=True + ) + try: + tg_message_id = await tg_post_event(httpx_client, event) # Получаем новый message_id + finally: + await httpx_client.aclose() if tg_message_id: # Используем UTC время для записи в базу данных @@ -430,13 +603,25 @@ async def tg_post_event_by_id(id_event): else: logger.error(f"Не удалось опубликовать событие ID {id_event}") - except RetryAfter as e: - logger.error(f"Полная информация об ошибке RetryAfter в tg_post_event_by_id для события ID {id_event}:") - logger.error(f" Тип ошибки: {type(e).__name__}") - logger.error(f" Сообщение: {str(e)}") - logger.error(f" Все атрибуты ошибки: {vars(e)}") - logger.error(f" Полное представление: {repr(e)}") - raise e + 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_event_by_id для события ID {id_event}:") + 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_event_by_id для события ID {id_event}:") + 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_event_by_id для события ID {id_event}:") @@ -497,14 +682,22 @@ async def tg_post_all_events(): if event_count < len(events): await asyncio.sleep(2) - except RetryAfter as e: + except RetryAfterException as e: logger.error(f"Прерываем публикацию из-за ошибки FloodWait. Ожидание: {e.retry_after} секунд") logger.error(f"Полная информация об ошибке RetryAfter в tg_post_all_events для события ID {event['id_event']}:") - logger.error(f" Тип ошибки: {type(e).__name__}") + logger.error(f" Тип ошибки: RetryAfter") logger.error(f" Сообщение: {str(e)}") - logger.error(f" Все атрибуты ошибки: {vars(e)}") + logger.error(f" retry_after: {e.retry_after}") logger.error(f" Полное представление: {repr(e)}") break + except Exception as e: + logger.error(f"Ошибка при публикации события ID {event['id_event']}: {e}") + logger.error(f"Полная информация об ошибке в tg_post_all_events для события ID {event['id_event']}:") + 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 {event['id_event']}: {e}") logger.error(f"Полная информация об ошибке в tg_post_all_events для события ID {event['id_event']}:")