[ZILANT] отладка до-редактирования ссылок в пост
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-09-02 21:40:29 +03:00
parent 62a77dba78
commit ac17043a08
7 changed files with 75 additions and 38 deletions
+1 -1
View File
@@ -20,4 +20,4 @@ alwaysApply: true
5. Не поднимать версию за косметические правки без изменения поведения. 5. Не поднимать версию за косметические правки без изменения поведения.
Текущая базовая версия: **1.1.1**. Текущая базовая версия: **1.1.2**.
+1 -1
View File
@@ -1,6 +1,6 @@
# Zilant / VOLK — Telegram poster и подписки # Zilant / VOLK — Telegram poster и подписки
**Версия:** 1.1.1 ([`VERSION`](VERSION)) **Версия:** 1.1.2 ([`VERSION`](VERSION))
Автоматическая публикация постов и событий в Telegram-канал, веб-редактор, бот подписок с сезонными deep link. Автоматическая публикация постов и событий в Telegram-канал, веб-редактор, бот подписок с сезонными deep link.
+1 -1
View File
@@ -1 +1 @@
1.1.1 1.1.2
+6
View File
@@ -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.2] - 2026-09-02
### Fixed
- Проверка успешного edit ссылок: Telegram отдаёт plain text и `text_link` entities, а не `href=` в caption; `message is not modified` трактуется как успех (ссылки уже применены). Устраняет ложные ошибки при успешной публикации.
## [1.1.1] - 2026-09-02 ## [1.1.1] - 2026-09-02
### Fixed ### Fixed
+24 -21
View File
@@ -14,7 +14,7 @@ from telegram_relay import (
EDIT_LINKS_INITIAL_DELAY_SEC, EDIT_LINKS_INITIAL_DELAY_SEC,
EDIT_LINKS_MAX_ATTEMPTS, EDIT_LINKS_MAX_ATTEMPTS,
EDIT_LINKS_RETRY_INTERVAL_SEC, EDIT_LINKS_RETRY_INTERVAL_SEC,
published_text_has_links, published_message_has_links,
) )
from season_links import subscription_start_link from season_links import subscription_start_link
@@ -133,10 +133,11 @@ async def download_image(image_url, httpx_client):
async def _edit_event_add_links(httpx_client, has_image, message_id, final_text, entity_label): async def _edit_event_add_links(httpx_client, has_image, message_id, final_text, entity_label):
"""Edit caption/text со ссылками; повтор до появления href= в ответе API.""" """Edit caption/text со ссылками; повтор до подтверждения text_link в ответе API."""
await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC) await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC)
method = "editMessageCaption" if has_image else "editMessageText" method = "editMessageCaption" if has_image else "editMessageText"
field = "caption" if has_image else "text" field = "caption" if has_image else "text"
entities_field = "caption_entities" if has_image else "entities"
api_url = bot_api_method_url(BOT_TOKEN, method) api_url = bot_api_method_url(BOT_TOKEN, method)
logger.info( logger.info(
@@ -172,15 +173,17 @@ async def _edit_event_add_links(httpx_client, has_image, message_id, final_text,
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
if data.get('ok'): if data.get('ok'):
result_text = (data.get('result') or {}).get(field) or "" result = data.get('result') or {}
if published_text_has_links(result_text): result_text = result.get(field) or ""
entities = result.get(entities_field) or []
if published_message_has_links(result_text, entities):
logger.info( logger.info(
f"Ссылки добавлены для {entity_label} " f"Ссылки добавлены для {entity_label} "
f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})" f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})"
) )
return True return True
logger.warning( logger.warning(
f"edit ok, но href= не найден (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) " f"edit ok, но ссылки не обнаружены (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
f"для {entity_label}" f"для {entity_label}"
) )
else: else:
@@ -190,15 +193,15 @@ async def _edit_event_add_links(httpx_client, has_image, message_id, final_text,
retry_after = data.get('parameters', {}).get('retry_after', 60) retry_after = data.get('parameters', {}).get('retry_after', 60)
raise Exception(f"RetryAfter:{retry_after}") raise Exception(f"RetryAfter:{retry_after}")
if error_code == 400 and "not modified" in error_description: if error_code == 400 and "not modified" in error_description:
logger.warning( logger.info(
f"message is not modified (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) " f"Caption уже содержит ссылки для {entity_label} "
f"для {entity_label} — повтор" f"(message is not modified, попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})"
)
else:
logger.warning(
f"API error edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
f"для {entity_label}: {data.get('description')}"
) )
return True
logger.warning(
f"API error edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
f"для {entity_label}: {data.get('description')}"
)
elif response.status_code == 429: elif response.status_code == 429:
retry_after = 60 retry_after = 60
try: try:
@@ -210,15 +213,15 @@ async def _edit_event_add_links(httpx_client, has_image, message_id, final_text,
try: try:
error_description = (response.json().get('description') or '').lower() error_description = (response.json().get('description') or '').lower()
if "not modified" in error_description: if "not modified" in error_description:
logger.warning( logger.info(
f"message is not modified HTTP 400 (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) " f"Caption уже содержит ссылки для {entity_label} "
f"для {entity_label} — повтор" f"(message is not modified HTTP 400, попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})"
)
else:
logger.warning(
f"HTTP 400 edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
f"для {entity_label}: {response.text[:300]}"
) )
return True
logger.warning(
f"HTTP 400 edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
f"для {entity_label}: {response.text[:300]}"
)
except Exception: except Exception:
logger.warning( logger.warning(
f"HTTP 400 edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) " f"HTTP 400 edit (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
+28 -2
View File
@@ -58,6 +58,32 @@ EDIT_LINKS_MAX_ATTEMPTS = 5
EDIT_LINKS_RETRY_INTERVAL_SEC = 2 EDIT_LINKS_RETRY_INTERVAL_SEC = 2
def published_message_has_links(text: str, entities=None) -> bool:
"""
Проверка, что в опубликованном caption/text есть кликабельные ссылки.
Telegram в ответе editMessage* отдаёт plain text (без href=) и entities типа text_link.
"""
if entities:
for ent in entities:
if isinstance(ent, dict):
if ent.get("type") == "text_link" and ent.get("url"):
return True
elif getattr(ent, "type", None) == "text_link" and getattr(ent, "url", None):
return True
if not text:
return False
link_markers = (
"Оригинал в ВК",
"Подписка",
"Инфо",
"Иду",
)
return any(marker in text for marker in link_markers)
def published_text_has_links(text: str) -> bool: def published_text_has_links(text: str) -> bool:
"""В caption/text после edit должны появиться HTML-ссылки.""" """Обратная совместимость; предпочтительно published_message_has_links."""
return bool(text) and "href=" in text return published_message_has_links(text)
+14 -12
View File
@@ -22,7 +22,7 @@ from telegram_relay import (
EDIT_LINKS_INITIAL_DELAY_SEC, EDIT_LINKS_INITIAL_DELAY_SEC,
EDIT_LINKS_MAX_ATTEMPTS, EDIT_LINKS_MAX_ATTEMPTS,
EDIT_LINKS_RETRY_INTERVAL_SEC, EDIT_LINKS_RETRY_INTERVAL_SEC,
published_text_has_links, published_message_has_links,
) )
from season_links import subscription_start_link from season_links import subscription_start_link
@@ -157,7 +157,7 @@ async def download_image(image_url, httpx_client):
async def _edit_post_add_links(bot, message_id, message_has_image, final_text, vk_post_id): async def _edit_post_add_links(bot, message_id, message_has_image, final_text, vk_post_id):
""" """
Второй шаг публикации: editMessageCaption/Text со ссылками. Второй шаг публикации: editMessageCaption/Text со ссылками.
Повторяет попытку, пока в ответе API нет href= (защита от гонки после sendPhoto). Повторяет попытку, пока в ответе API нет text_link / меток ссылок.
""" """
await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC) await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC)
@@ -171,6 +171,7 @@ async def _edit_post_add_links(bot, message_id, message_has_image, final_text, v
parse_mode='HTML', parse_mode='HTML',
) )
published_text = edited.caption or "" published_text = edited.caption or ""
entities = edited.caption_entities or []
else: else:
edited = await bot.edit_message_text( edited = await bot.edit_message_text(
chat_id=CHANNEL_ID, chat_id=CHANNEL_ID,
@@ -180,8 +181,9 @@ async def _edit_post_add_links(bot, message_id, message_has_image, final_text, v
disable_web_page_preview=True, disable_web_page_preview=True,
) )
published_text = edited.text or "" published_text = edited.text or ""
entities = edited.entities or []
if published_text_has_links(published_text): if published_message_has_links(published_text, entities):
logger.info( logger.info(
f"Ссылки добавлены для записи VK ID {vk_post_id} " f"Ссылки добавлены для записи VK ID {vk_post_id} "
f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})" f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})"
@@ -189,7 +191,7 @@ async def _edit_post_add_links(bot, message_id, message_has_image, final_text, v
return True return True
logger.warning( logger.warning(
f"editMessage* ok, но href= не найден в ответе " f"editMessage* ok, но ссылки не обнаружены в ответе "
f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) для VK ID {vk_post_id}" f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) для VK ID {vk_post_id}"
) )
except TimedOut as e: except TimedOut as e:
@@ -208,15 +210,15 @@ async def _edit_post_add_links(bot, message_id, message_has_image, final_text, v
raise RetryAfterException(getattr(e, 'retry_after', 60)) raise RetryAfterException(getattr(e, 'retry_after', 60))
err = str(e).lower() err = str(e).lower()
if "not modified" in err: if "not modified" in err:
logger.warning( logger.info(
f"message is not modified (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) " f"Caption уже содержит ссылки для VK ID {vk_post_id} "
f"для VK ID {vk_post_id} — повтор" f"(message is not modified, попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS})"
)
else:
logger.warning(
f"Ошибка edit ссылок (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
f"для VK ID {vk_post_id}: {e}"
) )
return True
logger.warning(
f"Ошибка edit ссылок (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) "
f"для VK ID {vk_post_id}: {e}"
)
except RetryAfterException: except RetryAfterException:
raise raise
except Exception as e: except Exception as e: