Коррекция логики добавления ссылок при [ZILANT] постинге в ТГ
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -20,4 +20,4 @@ alwaysApply: true
|
|||||||
|
|
||||||
5. Не поднимать версию за косметические правки без изменения поведения.
|
5. Не поднимать версию за косметические правки без изменения поведения.
|
||||||
|
|
||||||
Текущая базовая версия: **1.1.0**.
|
Текущая базовая версия: **1.1.1**.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Zilant / VOLK — Telegram poster и подписки
|
# Zilant / VOLK — Telegram poster и подписки
|
||||||
|
|
||||||
**Версия:** 1.1.0 ([`VERSION`](VERSION))
|
**Версия:** 1.1.1 ([`VERSION`](VERSION))
|
||||||
|
|
||||||
Автоматическая публикация постов и событий в Telegram-канал, веб-редактор, бот подписок с сезонными deep link.
|
Автоматическая публикация постов и событий в Telegram-канал, веб-редактор, бот подписок с сезонными deep link.
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/).
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/).
|
||||||
Версия проекта — в файле [`VERSION`](../VERSION) (SemVer).
|
Версия проекта — в файле [`VERSION`](../VERSION) (SemVer).
|
||||||
|
|
||||||
|
## [1.1.1] - 2026-09-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Публикация постов и событий: второй шаг (edit со строкой ссылок) повторяется до появления `href=` в ответе API; без подтверждённых ссылок запись не помечается опубликованной. Устраняет редкие случаи, когда edit логировался как успех, а ссылки в канале отсутствовали.
|
||||||
|
|
||||||
## [1.1.0] - 2026-08-30
|
## [1.1.0] - 2026-08-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+131
-198
@@ -9,7 +9,13 @@ from datetime import datetime, timezone, timedelta
|
|||||||
from urllib.parse import urlparse, unquote
|
from urllib.parse import urlparse, unquote
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from formatter import get_event_text
|
from formatter import get_event_text
|
||||||
from telegram_relay import bot_api_method_url
|
from telegram_relay import (
|
||||||
|
bot_api_method_url,
|
||||||
|
EDIT_LINKS_INITIAL_DELAY_SEC,
|
||||||
|
EDIT_LINKS_MAX_ATTEMPTS,
|
||||||
|
EDIT_LINKS_RETRY_INTERVAL_SEC,
|
||||||
|
published_text_has_links,
|
||||||
|
)
|
||||||
from season_links import subscription_start_link
|
from season_links import subscription_start_link
|
||||||
|
|
||||||
# Загрузка переменных окружения
|
# Загрузка переменных окружения
|
||||||
@@ -126,6 +132,114 @@ async def download_image(image_url, httpx_client):
|
|||||||
return image_data, content_type, _filename_from_image_url(image_url)
|
return image_data, content_type, _filename_from_image_url(image_url)
|
||||||
|
|
||||||
|
|
||||||
|
async def _edit_event_add_links(httpx_client, has_image, message_id, final_text, entity_label):
|
||||||
|
"""Edit caption/text со ссылками; повтор до появления href= в ответе API."""
|
||||||
|
await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC)
|
||||||
|
method = "editMessageCaption" if has_image else "editMessageText"
|
||||||
|
field = "caption" if has_image else "text"
|
||||||
|
api_url = bot_api_method_url(BOT_TOKEN, method)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Попытка редактирования {entity_label}: message_id={message_id}, "
|
||||||
|
f"final_text_length={len(final_text)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for attempt in range(1, EDIT_LINKS_MAX_ATTEMPTS + 1):
|
||||||
|
payload = {
|
||||||
|
'chat_id': CHANNEL_ID,
|
||||||
|
'message_id': message_id,
|
||||||
|
field: final_text,
|
||||||
|
'parse_mode': 'HTML',
|
||||||
|
}
|
||||||
|
if not has_image:
|
||||||
|
payload['disable_web_page_preview'] = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await httpx_client.post(api_url, json=payload)
|
||||||
|
except httpx.TimeoutException as e:
|
||||||
|
if attempt >= EDIT_LINKS_MAX_ATTEMPTS:
|
||||||
|
logger.error(
|
||||||
|
f"Таймаут edit ссылок для {entity_label} после "
|
||||||
|
f"{EDIT_LINKS_MAX_ATTEMPTS} попыток: {e}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
logger.warning(
|
||||||
|
f"Таймаут edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) для {entity_label}"
|
||||||
|
)
|
||||||
|
await asyncio.sleep(EDIT_LINKS_RETRY_INTERVAL_SEC)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('ok'):
|
||||||
|
result_text = (data.get('result') or {}).get(field) or ""
|
||||||
|
if published_text_has_links(result_text):
|
||||||
|
logger.info(
|
||||||
|
f"Ссылки добавлены для {entity_label} "
|
||||||
|
f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
logger.warning(
|
||||||
|
f"edit ok, но href= не найден (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для {entity_label}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error_code = data.get('error_code')
|
||||||
|
error_description = (data.get('description') or '').lower()
|
||||||
|
if error_code == 429:
|
||||||
|
retry_after = data.get('parameters', {}).get('retry_after', 60)
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
if error_code == 400 and "not modified" in error_description:
|
||||||
|
logger.warning(
|
||||||
|
f"message is not modified (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для {entity_label} — повтор"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"API error edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для {entity_label}: {data.get('description')}"
|
||||||
|
)
|
||||||
|
elif response.status_code == 429:
|
||||||
|
retry_after = 60
|
||||||
|
try:
|
||||||
|
retry_after = response.json().get('parameters', {}).get('retry_after', 60)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise Exception(f"RetryAfter:{retry_after}")
|
||||||
|
elif response.status_code == 400:
|
||||||
|
try:
|
||||||
|
error_description = (response.json().get('description') or '').lower()
|
||||||
|
if "not modified" in error_description:
|
||||||
|
logger.warning(
|
||||||
|
f"message is not modified HTTP 400 (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для {entity_label} — повтор"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"HTTP 400 edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для {entity_label}: {response.text[:300]}"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
f"HTTP 400 edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для {entity_label}: {response.text[:300]}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"HTTP {response.status_code} edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для {entity_label}: {response.text[:300]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if attempt < EDIT_LINKS_MAX_ATTEMPTS:
|
||||||
|
await asyncio.sleep(EDIT_LINKS_RETRY_INTERVAL_SEC)
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"Не удалось добавить строку ссылок для {entity_label} "
|
||||||
|
f"после {EDIT_LINKS_MAX_ATTEMPTS} попыток"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def tg_post_event(httpx_client, event_data):
|
async def tg_post_event(httpx_client, event_data):
|
||||||
"""Публикация одного события"""
|
"""Публикация одного события"""
|
||||||
try:
|
try:
|
||||||
@@ -228,6 +342,7 @@ async def tg_post_event(httpx_client, event_data):
|
|||||||
# Публикация первоначального сообщения без ссылок
|
# Публикация первоначального сообщения без ссылок
|
||||||
send_photo_url = bot_api_method_url(BOT_TOKEN, "sendPhoto")
|
send_photo_url = bot_api_method_url(BOT_TOKEN, "sendPhoto")
|
||||||
send_message_url = bot_api_method_url(BOT_TOKEN, "sendMessage")
|
send_message_url = bot_api_method_url(BOT_TOKEN, "sendMessage")
|
||||||
|
message_is_photo = False
|
||||||
|
|
||||||
if has_image:
|
if has_image:
|
||||||
try:
|
try:
|
||||||
@@ -255,6 +370,7 @@ async def tg_post_event(httpx_client, event_data):
|
|||||||
if photo_result.get('ok'):
|
if photo_result.get('ok'):
|
||||||
message_data = photo_result.get('result', {})
|
message_data = photo_result.get('result', {})
|
||||||
message_id = message_data.get('message_id')
|
message_id = message_data.get('message_id')
|
||||||
|
message_is_photo = True
|
||||||
logger.info(f"Фото успешно отправлено для события {event_data['number']}, message_id={message_id}")
|
logger.info(f"Фото успешно отправлено для события {event_data['number']}, message_id={message_id}")
|
||||||
else:
|
else:
|
||||||
# Ошибка в ответе API - пробуем отправить текстовое сообщение
|
# Ошибка в ответе API - пробуем отправить текстовое сообщение
|
||||||
@@ -418,203 +534,20 @@ async def tg_post_event(httpx_client, event_data):
|
|||||||
logger.info(f"Финальный текст идентичен исходному для события {event_data['number']}, редактирование не требуется")
|
logger.info(f"Финальный текст идентичен исходному для события {event_data['number']}, редактирование не требуется")
|
||||||
return message_id
|
return message_id
|
||||||
|
|
||||||
await asyncio.sleep(2)
|
entity_label = f"события {event_data['number']}"
|
||||||
|
links_ok = await _edit_event_add_links(
|
||||||
# Редактируем сообщение, добавляя ссылки
|
httpx_client, message_is_photo, message_id, final_text, entity_label
|
||||||
logger.info(f"Попытка редактирования сообщения для события {event_data['number']}: message_id={message_id}, final_text_length={len(final_text)}")
|
)
|
||||||
edit_caption_url = bot_api_method_url(BOT_TOKEN, "editMessageCaption")
|
if not links_ok and message_is_photo:
|
||||||
edit_text_url = bot_api_method_url(BOT_TOKEN, "editMessageText")
|
links_ok = await _edit_event_add_links(
|
||||||
|
httpx_client, False, message_id, final_text, entity_label
|
||||||
if has_image:
|
)
|
||||||
try:
|
if not links_ok:
|
||||||
edit_caption_payload = {
|
logger.error(
|
||||||
'chat_id': CHANNEL_ID,
|
f"Событие {event_data['number']}: сообщение отправлено (id={message_id}), "
|
||||||
'message_id': message_id,
|
f"но ссылки не добавлены"
|
||||||
'caption': final_text,
|
)
|
||||||
'parse_mode': 'HTML'
|
return None
|
||||||
}
|
|
||||||
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')
|
|
||||||
error_description = edit_result.get('description', 'Unknown error')
|
|
||||||
# Обрабатываем ошибку 400 (сообщение не изменено) как предупреждение
|
|
||||||
if error_code == 400 and "not modified" in error_description.lower():
|
|
||||||
logger.info(f"Сообщение для события {event_data['number']} не требует редактирования (текст идентичен)")
|
|
||||||
return message_id
|
|
||||||
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: {error_description}")
|
|
||||||
else:
|
|
||||||
if edit_caption_response.status_code == 400:
|
|
||||||
try:
|
|
||||||
error_data = edit_caption_response.json()
|
|
||||||
error_description = error_data.get('description', '')
|
|
||||||
if "not modified" in error_description.lower():
|
|
||||||
logger.info(f"Сообщение для события {event_data['number']} не требует редактирования (текст идентичен)")
|
|
||||||
return message_id
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
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__}")
|
|
||||||
logger.error(f" Сообщение: {str(e)}")
|
|
||||||
logger.error(f" Все атрибуты ошибки: {vars(e)}")
|
|
||||||
logger.error(f" Полное представление: {repr(e)}")
|
|
||||||
logger.error(f" Параметры запроса:")
|
|
||||||
logger.error(f" chat_id: {CHANNEL_ID}")
|
|
||||||
logger.error(f" message_id: {message_id}")
|
|
||||||
logger.error(f" caption_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
|
|
||||||
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) if hasattr(e, '__dict__') else 'N/A'}")
|
|
||||||
logger.error(f" Полное представление: {repr(e)}")
|
|
||||||
logger.info(f"Попытка редактирования текста вместо подписи для события {event_data['number']}")
|
|
||||||
try:
|
|
||||||
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')
|
|
||||||
error_description = edit_result.get('description', 'Unknown error')
|
|
||||||
# Обрабатываем ошибку 400 (сообщение не изменено) как предупреждение
|
|
||||||
if error_code == 400 and "not modified" in error_description.lower():
|
|
||||||
logger.info(f"Сообщение для события {event_data['number']} не требует редактирования (текст идентичен)")
|
|
||||||
return message_id
|
|
||||||
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: {error_description}")
|
|
||||||
else:
|
|
||||||
if edit_text_response.status_code == 400:
|
|
||||||
try:
|
|
||||||
error_data = edit_text_response.json()
|
|
||||||
error_description = error_data.get('description', '')
|
|
||||||
if "not modified" in error_description.lower():
|
|
||||||
logger.info(f"Сообщение для события {event_data['number']} не требует редактирования (текст идентичен)")
|
|
||||||
return message_id
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
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__}")
|
|
||||||
logger.error(f" Сообщение: {str(e)}")
|
|
||||||
logger.error(f" Все атрибуты ошибки: {vars(e)}")
|
|
||||||
logger.error(f" Полное представление: {repr(e)}")
|
|
||||||
logger.error(f" Параметры запроса:")
|
|
||||||
logger.error(f" chat_id: {CHANNEL_ID}")
|
|
||||||
logger.error(f" message_id: {message_id}")
|
|
||||||
logger.error(f" text_length: {len(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:
|
|
||||||
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')
|
|
||||||
error_description = edit_result.get('description', 'Unknown error')
|
|
||||||
# Обрабатываем ошибку 400 (сообщение не изменено) как предупреждение
|
|
||||||
if error_code == 400 and "not modified" in error_description.lower():
|
|
||||||
logger.info(f"Сообщение для события {event_data['number']} не требует редактирования (текст идентичен)")
|
|
||||||
return message_id
|
|
||||||
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: {error_description}")
|
|
||||||
else:
|
|
||||||
if edit_text_response.status_code == 400:
|
|
||||||
try:
|
|
||||||
error_data = edit_text_response.json()
|
|
||||||
error_description = error_data.get('description', '')
|
|
||||||
if "not modified" in error_description.lower():
|
|
||||||
logger.info(f"Сообщение для события {event_data['number']} не требует редактирования (текст идентичен)")
|
|
||||||
return message_id
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
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__}")
|
|
||||||
logger.error(f" Сообщение: {str(e)}")
|
|
||||||
logger.error(f" Все атрибуты ошибки: {vars(e)}")
|
|
||||||
logger.error(f" Полное представление: {repr(e)}")
|
|
||||||
logger.error(f" Параметры запроса:")
|
|
||||||
logger.error(f" chat_id: {CHANNEL_ID}")
|
|
||||||
logger.error(f" message_id: {message_id}")
|
|
||||||
logger.error(f" text_length: {len(final_text)}")
|
|
||||||
logger.error(f" parse_mode: HTML")
|
|
||||||
raise
|
|
||||||
|
|
||||||
return message_id # Возвращаем только message_id
|
return message_id # Возвращаем только message_id
|
||||||
|
|
||||||
|
|||||||
@@ -50,3 +50,14 @@ def get_ptb_base_urls() -> Tuple[str, str]:
|
|||||||
def bot_api_method_url(token: str, method: str) -> str:
|
def bot_api_method_url(token: str, method: str) -> str:
|
||||||
"""Полный URL метода Bot API через relay (для сырых httpx/requests вызовов)."""
|
"""Полный URL метода Bot API через relay (для сырых httpx/requests вызовов)."""
|
||||||
return f"{get_relay_url()}/bot{token}/{method}"
|
return f"{get_relay_url()}/bot{token}/{method}"
|
||||||
|
|
||||||
|
|
||||||
|
# Публикация в два шага (send → edit ссылок): пауза и повторы при гонке с Telegram
|
||||||
|
EDIT_LINKS_INITIAL_DELAY_SEC = 3
|
||||||
|
EDIT_LINKS_MAX_ATTEMPTS = 5
|
||||||
|
EDIT_LINKS_RETRY_INTERVAL_SEC = 2
|
||||||
|
|
||||||
|
|
||||||
|
def published_text_has_links(text: str) -> bool:
|
||||||
|
"""В caption/text после edit должны появиться HTML-ссылки."""
|
||||||
|
return bool(text) and "href=" in text
|
||||||
|
|||||||
+97
-72
@@ -16,7 +16,14 @@ from formatter import get_event_text
|
|||||||
from telegram import Bot
|
from telegram import Bot
|
||||||
from telegram.error import TelegramError, TimedOut
|
from telegram.error import TelegramError, TimedOut
|
||||||
from telegram.request import HTTPXRequest
|
from telegram.request import HTTPXRequest
|
||||||
from telegram_relay import get_ptb_base_urls, get_relay_url
|
from telegram_relay import (
|
||||||
|
get_ptb_base_urls,
|
||||||
|
get_relay_url,
|
||||||
|
EDIT_LINKS_INITIAL_DELAY_SEC,
|
||||||
|
EDIT_LINKS_MAX_ATTEMPTS,
|
||||||
|
EDIT_LINKS_RETRY_INTERVAL_SEC,
|
||||||
|
published_text_has_links,
|
||||||
|
)
|
||||||
from season_links import subscription_start_link
|
from season_links import subscription_start_link
|
||||||
|
|
||||||
# Настройка anyio для правильной работы в отдельном потоке
|
# Настройка anyio для правильной работы в отдельном потоке
|
||||||
@@ -146,6 +153,88 @@ async def download_image(image_url, httpx_client):
|
|||||||
logger.error(f"Ошибка при скачивании изображения {image_url}: {e}")
|
logger.error(f"Ошибка при скачивании изображения {image_url}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def _edit_post_add_links(bot, message_id, message_has_image, final_text, vk_post_id):
|
||||||
|
"""
|
||||||
|
Второй шаг публикации: editMessageCaption/Text со ссылками.
|
||||||
|
Повторяет попытку, пока в ответе API нет href= (защита от гонки после sendPhoto).
|
||||||
|
"""
|
||||||
|
await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC)
|
||||||
|
|
||||||
|
for attempt in range(1, EDIT_LINKS_MAX_ATTEMPTS + 1):
|
||||||
|
try:
|
||||||
|
if message_has_image:
|
||||||
|
edited = await bot.edit_message_caption(
|
||||||
|
chat_id=CHANNEL_ID,
|
||||||
|
message_id=message_id,
|
||||||
|
caption=final_text,
|
||||||
|
parse_mode='HTML',
|
||||||
|
)
|
||||||
|
published_text = edited.caption or ""
|
||||||
|
else:
|
||||||
|
edited = await bot.edit_message_text(
|
||||||
|
chat_id=CHANNEL_ID,
|
||||||
|
message_id=message_id,
|
||||||
|
text=final_text,
|
||||||
|
parse_mode='HTML',
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
||||||
|
published_text = edited.text or ""
|
||||||
|
|
||||||
|
if published_text_has_links(published_text):
|
||||||
|
logger.info(
|
||||||
|
f"Ссылки добавлены для записи VK ID {vk_post_id} "
|
||||||
|
f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"editMessage* ok, но href= не найден в ответе "
|
||||||
|
f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) для VK ID {vk_post_id}"
|
||||||
|
)
|
||||||
|
except TimedOut as e:
|
||||||
|
if attempt >= EDIT_LINKS_MAX_ATTEMPTS:
|
||||||
|
logger.error(
|
||||||
|
f"Таймаут редактирования ссылок для VK ID {vk_post_id} "
|
||||||
|
f"после {EDIT_LINKS_MAX_ATTEMPTS} попыток: {e}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
logger.warning(
|
||||||
|
f"Таймаут edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для VK ID {vk_post_id}, повтор"
|
||||||
|
)
|
||||||
|
except TelegramError as e:
|
||||||
|
if hasattr(e, 'retry_after') or '429' in str(e):
|
||||||
|
raise RetryAfterException(getattr(e, 'retry_after', 60))
|
||||||
|
err = str(e).lower()
|
||||||
|
if "not modified" in err:
|
||||||
|
logger.warning(
|
||||||
|
f"message is not modified (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для VK ID {vk_post_id} — повтор"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Ошибка edit ссылок (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для VK ID {vk_post_id}: {e}"
|
||||||
|
)
|
||||||
|
except RetryAfterException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Неожиданная ошибка edit ссылок (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
|
||||||
|
f"для VK ID {vk_post_id}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if attempt < EDIT_LINKS_MAX_ATTEMPTS:
|
||||||
|
await asyncio.sleep(EDIT_LINKS_RETRY_INTERVAL_SEC)
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"Не удалось добавить строку ссылок для VK ID {vk_post_id} "
|
||||||
|
f"после {EDIT_LINKS_MAX_ATTEMPTS} попыток"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def publish_to_tg(vk_post_id):
|
async def publish_to_tg(vk_post_id):
|
||||||
"""Публикация одной записи по VK post ID"""
|
"""Публикация одной записи по VK post ID"""
|
||||||
logger.info(f"Запуск публикации для записи VK ID {vk_post_id}")
|
logger.info(f"Запуск публикации для записи VK ID {vk_post_id}")
|
||||||
@@ -429,78 +518,14 @@ async def publish_to_tg(vk_post_id):
|
|||||||
links_line = " | ".join(updated_links)
|
links_line = " | ".join(updated_links)
|
||||||
final_text = f"{text}\n\n{links_line}"
|
final_text = f"{text}\n\n{links_line}"
|
||||||
|
|
||||||
await asyncio.sleep(2) # задержка между обращениями к телеграм
|
if not await _edit_post_add_links(
|
||||||
|
bot, message_id, message_has_image, final_text, vk_post_id
|
||||||
# Редактируем сообщение, добавляя ссылки
|
):
|
||||||
# Используем message_has_image для определения типа сообщения
|
logger.error(
|
||||||
# Если было отправлено изображение, редактируем caption, иначе - текст
|
f"Запись VK ID {vk_post_id}: сообщение отправлено (id={message_id}), "
|
||||||
if message_has_image:
|
f"но ссылки не добавлены — published_in_tg не обновляем"
|
||||||
# Сообщение с изображением - редактируем caption с повторными попытками
|
|
||||||
edit_success = False
|
|
||||||
for attempt in range(1, max_retries + 1):
|
|
||||||
try:
|
|
||||||
await bot.edit_message_caption(
|
|
||||||
chat_id=CHANNEL_ID,
|
|
||||||
message_id=message_id,
|
|
||||||
caption=final_text,
|
|
||||||
parse_mode='HTML'
|
|
||||||
)
|
)
|
||||||
edit_success = True
|
return False
|
||||||
logger.info(f"Caption успешно отредактирован для записи VK ID {vk_post_id}")
|
|
||||||
break # Успешное редактирование, выходим из цикла
|
|
||||||
except TimedOut as e:
|
|
||||||
if attempt < max_retries:
|
|
||||||
logger.warning(f"Таймаут при редактировании caption (попытка {attempt}/{max_retries}) для записи VK ID {vk_post_id}. Повтор через {retry_interval} секунд...")
|
|
||||||
await asyncio.sleep(retry_interval)
|
|
||||||
else:
|
|
||||||
logger.warning(f"Таймаут при редактировании caption после {max_retries} попыток для записи VK ID {vk_post_id}. Сообщение опубликовано, но без ссылок.")
|
|
||||||
# Не критичная ошибка - сообщение уже опубликовано
|
|
||||||
break
|
|
||||||
except TelegramError as e:
|
|
||||||
if hasattr(e, 'retry_after') or '429' in str(e):
|
|
||||||
retry_after = getattr(e, 'retry_after', 60)
|
|
||||||
raise RetryAfterException(retry_after)
|
|
||||||
# Не критичная ошибка - сообщение уже опубликовано
|
|
||||||
logger.warning(f"Не удалось отредактировать caption для записи VK ID {vk_post_id}: {type(e).__name__}: {e}. Сообщение опубликовано, но без ссылок.")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
# Не критичная ошибка - сообщение уже опубликовано
|
|
||||||
logger.warning(f"Неожиданная ошибка при редактировании caption для записи VK ID {vk_post_id}: {type(e).__name__}: {e}. Сообщение опубликовано, но без ссылок.")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Текстовое сообщение - редактируем текст с повторными попытками
|
|
||||||
edit_success = False
|
|
||||||
for attempt in range(1, max_retries + 1):
|
|
||||||
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
|
|
||||||
)
|
|
||||||
edit_success = True
|
|
||||||
logger.info(f"Текст успешно отредактирован для записи VK ID {vk_post_id}")
|
|
||||||
break # Успешное редактирование, выходим из цикла
|
|
||||||
except TimedOut as e:
|
|
||||||
if attempt < max_retries:
|
|
||||||
logger.warning(f"Таймаут при редактировании текста (попытка {attempt}/{max_retries}) для записи VK ID {vk_post_id}. Повтор через {retry_interval} секунд...")
|
|
||||||
await asyncio.sleep(retry_interval)
|
|
||||||
else:
|
|
||||||
logger.warning(f"Таймаут при редактировании текста после {max_retries} попыток для записи VK ID {vk_post_id}. Сообщение опубликовано, но без ссылок.")
|
|
||||||
# Не критичная ошибка - сообщение уже опубликовано
|
|
||||||
break
|
|
||||||
except TelegramError as e:
|
|
||||||
if hasattr(e, 'retry_after') or '429' in str(e):
|
|
||||||
retry_after = getattr(e, 'retry_after', 60)
|
|
||||||
raise RetryAfterException(retry_after)
|
|
||||||
# Не критичная ошибка - сообщение уже опубликовано
|
|
||||||
logger.warning(f"Не удалось отредактировать текст для записи VK ID {vk_post_id}: {type(e).__name__}: {e}. Сообщение опубликовано, но без ссылок.")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
# Не критичная ошибка - сообщение уже опубликовано
|
|
||||||
logger.warning(f"Неожиданная ошибка при редактировании текста для записи VK ID {vk_post_id}: {type(e).__name__}: {e}. Сообщение опубликовано, но без ссылок.")
|
|
||||||
break
|
|
||||||
|
|
||||||
await asyncio.sleep(2) # задержка между обращениями к телеграм
|
await asyncio.sleep(2) # задержка между обращениями к телеграм
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user