[ВОЛК] переход tg_publish на httpx_client
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
2025-12-21 19:26:09 +03:00
parent 5c46c0bcf0
commit 0d411bb5ce
+254 -63
View File
@@ -6,9 +6,8 @@ import time
import html import html
import json import json
import re import re
import httpx
from datetime import datetime, timezone from datetime import datetime, timezone
from telegram import Bot, PollOption
from telegram.error import TelegramError, RetryAfter
from dotenv import load_dotenv from dotenv import load_dotenv
from formatter import get_event_text from formatter import get_event_text
@@ -63,6 +62,13 @@ file_handler = RetryFileHandler(LOG_FILE, encoding='utf-8')
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
logger.addHandler(file_handler) 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): def prepare_text(text):
"""Подготовка текстовых полей с экранированием HTML-сущностей""" """Подготовка текстовых полей с экранированием HTML-сущностей"""
if not text: if not text:
@@ -111,8 +117,14 @@ async def publish_to_tg(vk_post_id):
logger.info(f"Запись VK ID {vk_post_id} не найдена или уже опубликована") logger.info(f"Запись VK ID {vk_post_id} не найдена или уже опубликована")
return False return False
bot = Bot(token=BOT_TOKEN) # Создаем httpx клиент с отключенным HTTP/2
httpx_client = httpx.AsyncClient(
http2=False,
timeout=20.0,
follow_redirects=True
)
try:
# Проверяем наличие организационного хештега # Проверяем наличие организационного хештега
is_org_message = has_org_tag(post['text']) is_org_message = has_org_tag(post['text'])
@@ -161,36 +173,108 @@ async def publish_to_tg(vk_post_id):
logger.info(f"=== Конец отформатированного текста ===") logger.info(f"=== Конец отформатированного текста ===")
# Публикация первоначального сообщения без ссылок # Публикация первоначального сообщения без ссылок
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 and not is_org_message: if has_image and not is_org_message:
try: try:
message = await bot.send_photo( photo_payload = {
chat_id=CHANNEL_ID, 'chat_id': CHANNEL_ID,
photo=image_url, 'photo': image_url,
caption=text, 'caption': text,
parse_mode="HTML", 'parse_mode': 'HTML',
disable_notification=PUBLISH_SILENTLY 'disable_notification': PUBLISH_SILENTLY
) }
except TelegramError as e: 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')
else:
# Ошибка в ответе API - пробуем отправить текстовое сообщение
error_code = photo_result.get('error_code')
if error_code == 429:
retry_after = photo_result.get('parameters', {}).get('retry_after', 60)
raise RetryAfterException(retry_after)
raise Exception(f"API error: {photo_result.get('description', 'Unknown error')}")
else:
if photo_response.status_code == 429:
retry_after = 60
try:
error_data = photo_response.json()
retry_after = error_data.get('parameters', {}).get('retry_after', 60)
except:
pass
raise RetryAfterException(retry_after)
raise Exception(f"HTTP {photo_response.status_code}: {photo_response.text}")
except RetryAfterException:
raise
except Exception as e:
logger.warning(f"Не удалось отправить изображение для записи VK ID {vk_post_id}: {e}. Отправляем текстовое сообщение.") logger.warning(f"Не удалось отправить изображение для записи VK ID {vk_post_id}: {e}. Отправляем текстовое сообщение.")
message = await bot.send_message( text_payload = {
chat_id=CHANNEL_ID, 'chat_id': CHANNEL_ID,
text=text, 'text': text,
parse_mode="HTML", 'parse_mode': 'HTML',
disable_notification=PUBLISH_SILENTLY, 'disable_notification': PUBLISH_SILENTLY,
disable_web_page_preview=True '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')
else:
error_code = text_result.get('error_code')
if error_code == 429:
retry_after = text_result.get('parameters', {}).get('retry_after', 60)
raise RetryAfterException(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 RetryAfterException(retry_after)
raise Exception(f"HTTP {text_response.status_code}: {text_response.text}")
else: else:
# Если это организационное сообщение или нет изображения, отправляем текстовое сообщение # Если это организационное сообщение или нет изображения, отправляем текстовое сообщение
message = await bot.send_message( text_payload = {
chat_id=CHANNEL_ID, 'chat_id': CHANNEL_ID,
text=text, 'text': text,
parse_mode="HTML", 'parse_mode': 'HTML',
disable_notification=PUBLISH_SILENTLY, 'disable_notification': PUBLISH_SILENTLY,
disable_web_page_preview=True 'disable_web_page_preview': True
) }
text_response = await httpx_client.post(send_message_url, json=text_payload)
# Получаем ID сообщения для использования в ссылке подписки if text_response.status_code == 200:
message_id = message.message_id text_result = text_response.json()
if text_result.get('ok'):
message_data = text_result.get('result', {})
message_id = message_data.get('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 RetryAfterException(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 RetryAfterException(retry_after)
raise Exception(f"HTTP {text_response.status_code}: {text_response.text}")
# Формируем финальный текст с ссылками # Формируем финальный текст с ссылками
if links: if links:
@@ -208,31 +292,96 @@ async def publish_to_tg(vk_post_id):
await asyncio.sleep(2) # задержка между обращениями к телеграм await asyncio.sleep(2) # задержка между обращениями к телеграм
# Редактируем сообщение, добавляя ссылки # Редактируем сообщение, добавляя ссылки
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 and not is_org_message: if has_image and not is_org_message:
try: try:
await bot.edit_message_caption( edit_caption_payload = {
chat_id=CHANNEL_ID, 'chat_id': CHANNEL_ID,
message_id=message_id, 'message_id': message_id,
caption=final_text, 'caption': final_text,
parse_mode="HTML" 'parse_mode': 'HTML'
) }
except TelegramError as e: edit_caption_response = await httpx_client.post(edit_caption_url, json=edit_caption_payload)
logger.warning(f"Не удалось отредактировать подпись для записи VK ID {vk_post_id}: {e}. Пробуем отредактировать текстовое сообщение.")
await bot.edit_message_text( if edit_caption_response.status_code == 200:
chat_id=CHANNEL_ID, edit_result = edit_caption_response.json()
message_id=message_id, if not edit_result.get('ok'):
text=final_text, error_code = edit_result.get('error_code')
parse_mode="HTML", if error_code == 429:
disable_web_page_preview=True retry_after = edit_result.get('parameters', {}).get('retry_after', 60)
) raise RetryAfterException(retry_after)
raise Exception(f"API error: {edit_result.get('description', 'Unknown error')}")
else: else:
await bot.edit_message_text( if edit_caption_response.status_code == 429:
chat_id=CHANNEL_ID, retry_after = 60
message_id=message_id, try:
text=final_text, error_data = edit_caption_response.json()
parse_mode="HTML", retry_after = error_data.get('parameters', {}).get('retry_after', 60)
disable_web_page_preview=True except:
) pass
raise RetryAfterException(retry_after)
raise Exception(f"HTTP {edit_caption_response.status_code}: {edit_caption_response.text}")
except RetryAfterException:
raise
except Exception as e:
logger.warning(f"Не удалось отредактировать подпись для записи VK ID {vk_post_id}: {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 not edit_result.get('ok'):
error_code = edit_result.get('error_code')
if error_code == 429:
retry_after = edit_result.get('parameters', {}).get('retry_after', 60)
raise RetryAfterException(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 RetryAfterException(retry_after)
raise Exception(f"HTTP {edit_text_response.status_code}: {edit_text_response.text}")
else:
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 not edit_result.get('ok'):
error_code = edit_result.get('error_code')
if error_code == 429:
retry_after = edit_result.get('parameters', {}).get('retry_after', 60)
raise RetryAfterException(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 RetryAfterException(retry_after)
raise Exception(f"HTTP {edit_text_response.status_code}: {edit_text_response.text}")
await asyncio.sleep(2) # задержка между обращениями к телеграм await asyncio.sleep(2) # задержка между обращениями к телеграм
@@ -242,22 +391,53 @@ async def publish_to_tg(vk_post_id):
try: try:
# Парсим варианты ответа # Парсим варианты ответа
options = json.loads(post['poll_options']) options = json.loads(post['poll_options'])
poll_options = [PollOption(option, 0) for option in options]
# Отправляем опрос (в каналах можно отправлять только анонимные опросы) # Отправляем опрос (в каналах можно отправлять только анонимные опросы)
poll_message = await bot.send_poll( send_poll_url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPoll"
chat_id=CHANNEL_ID, poll_payload = {
question=post['poll_question'], 'chat_id': CHANNEL_ID,
options=poll_options, 'question': post['poll_question'],
is_anonymous=True, # В каналах можно отправлять только анонимные опросы 'options': options,
allows_multiple_answers=post['poll_multiple'], 'is_anonymous': True, # В каналах можно отправлять только анонимные опросы
close_date=post['poll_end_date'] if post['poll_end_date'] else None, 'allows_multiple_answers': post['poll_multiple'],
disable_notification=PUBLISH_SILENTLY 'disable_notification': PUBLISH_SILENTLY
) }
poll_message_id = poll_message.message_id # Добавляем close_date, если указан
if post['poll_end_date']:
# Преобразуем datetime в Unix timestamp
if isinstance(post['poll_end_date'], datetime):
poll_payload['close_date'] = int(post['poll_end_date'].timestamp())
else:
poll_payload['close_date'] = post['poll_end_date']
poll_response = await httpx_client.post(send_poll_url, json=poll_payload)
if poll_response.status_code == 200:
poll_result = poll_response.json()
if poll_result.get('ok'):
poll_message_data = poll_result.get('result', {})
poll_message_id = poll_message_data.get('message_id')
logger.info(f"Опубликован опрос для записи VK ID {vk_post_id}") logger.info(f"Опубликован опрос для записи VK ID {vk_post_id}")
else:
error_code = poll_result.get('error_code')
if error_code == 429:
retry_after = poll_result.get('parameters', {}).get('retry_after', 60)
raise RetryAfterException(retry_after)
raise Exception(f"API error: {poll_result.get('description', 'Unknown error')}")
else:
if poll_response.status_code == 429:
retry_after = 60
try:
error_data = poll_response.json()
retry_after = error_data.get('parameters', {}).get('retry_after', 60)
except:
pass
raise RetryAfterException(retry_after)
raise Exception(f"HTTP {poll_response.status_code}: {poll_response.text}")
except RetryAfterException:
raise
except Exception as e: except Exception as e:
logger.error(f"Ошибка при публикации опроса для записи VK ID {vk_post_id}: {e}") logger.error(f"Ошибка при публикации опроса для записи VK ID {vk_post_id}: {e}")
@@ -275,12 +455,23 @@ async def publish_to_tg(vk_post_id):
logger.info(f"Запись VK ID {vk_post_id} успешно опубликована") logger.info(f"Запись VK ID {vk_post_id} успешно опубликована")
return True return True
finally:
await httpx_client.aclose()
except RetryAfter as e: except RetryAfterException as e:
logger.error(f"Получена ошибка FloodWait (429) при публикации записи VK ID {vk_post_id}: {e}") logger.error(f"Получена ошибка FloodWait (429) при публикации записи VK ID {vk_post_id}: {e}")
logger.error(f"Необходимо подождать {e.retry_after} секунд перед следующей попыткой") logger.error(f"Необходимо подождать {e.retry_after} секунд перед следующей попыткой")
raise raise
except TelegramError as e: except httpx.TimeoutException as e:
logger.error(f"ТАЙМАУТ при публикации записи VK ID {vk_post_id}: {e}")
return False
except Exception as e:
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"Получена ошибка FloodWait (429) при публикации записи VK ID {vk_post_id}")
logger.error(f"Необходимо подождать {retry_after} секунд перед следующей попыткой")
raise RetryAfterException(retry_after)
logger.error(f"Ошибка Telegram при публикации записи VK ID {vk_post_id}: {e}") logger.error(f"Ошибка Telegram при публикации записи VK ID {vk_post_id}: {e}")
return False return False
except Exception as e: except Exception as e:
@@ -325,7 +516,7 @@ async def publish_to_tg_all():
if post_count < len(posts): if post_count < len(posts):
await asyncio.sleep(2) await asyncio.sleep(2)
except RetryAfter as e: except RetryAfterException as e:
logger.error(f"Прерываем публикацию из-за ошибки FloodWait. Ожидание: {e.retry_after} секунд") logger.error(f"Прерываем публикацию из-за ошибки FloodWait. Ожидание: {e.retry_after} секунд")
break break
except Exception as e: except Exception as e: