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

This commit is contained in:
2026-09-02 21:51:18 +03:00
parent ac17043a08
commit 709178d9ea
7 changed files with 106 additions and 38 deletions
+1 -1
View File
@@ -20,4 +20,4 @@ alwaysApply: true
5. Не поднимать версию за косметические правки без изменения поведения. 5. Не поднимать версию за косметические правки без изменения поведения.
Текущая базовая версия: **1.1.2**. Текущая базовая версия: **1.1.3**.
+1 -1
View File
@@ -1,6 +1,6 @@
# Zilant / VOLK — Telegram poster и подписки # Zilant / VOLK — Telegram poster и подписки
**Версия:** 1.1.2 ([`VERSION`](VERSION)) **Версия:** 1.1.3 ([`VERSION`](VERSION))
Автоматическая публикация постов и событий в Telegram-канал, веб-редактор, бот подписок с сезонными deep link. Автоматическая публикация постов и событий в Telegram-канал, веб-редактор, бот подписок с сезонными deep link.
+1 -1
View File
@@ -1 +1 @@
1.1.2 1.1.3
+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.3] - 2026-09-02
### Fixed
- Проверка footer-ссылок после edit: сравнение по конкретным URL (Инфо/Иду/Подписка, ВК), а не по любому `text_link` или словам в тексте тела события — устраняет ложный «успех» при пакетной публикации.
## [1.1.2] - 2026-09-02 ## [1.1.2] - 2026-09-02
### Fixed ### Fixed
+28 -8
View File
@@ -14,7 +14,8 @@ 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_message_has_links, published_message_has_expected_links,
extract_text_link_urls,
) )
from season_links import subscription_start_link from season_links import subscription_start_link
@@ -132,8 +133,10 @@ 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): async def _edit_event_add_links(
"""Edit caption/text со ссылками; повтор до подтверждения text_link в ответе API.""" httpx_client, has_image, message_id, final_text, entity_label, expected_urls, min_length
):
"""Edit caption/text со ссылками; повтор до подтверждения всех expected_urls в text_link."""
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"
@@ -176,15 +179,20 @@ async def _edit_event_add_links(httpx_client, has_image, message_id, final_text,
result = data.get('result') or {} result = data.get('result') or {}
result_text = result.get(field) or "" result_text = result.get(field) or ""
entities = result.get(entities_field) or [] entities = result.get(entities_field) or []
if published_message_has_links(result_text, entities): if published_message_has_expected_links(
entities, expected_urls, result_text=result_text, min_length=min_length
):
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
found = extract_text_link_urls(entities)
logger.warning( logger.warning(
f"edit ok, но ссылки не обнаружены (попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) " f"edit ok, но footer-ссылки не подтверждены "
f"для {entity_label}" f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) для {entity_label}; "
f"ожидались {expected_urls}, найдены text_link: {found}, "
f"len(text)={len(result_text)}, min_len={min_length}"
) )
else: else:
error_code = data.get('error_code') error_code = data.get('error_code')
@@ -532,6 +540,16 @@ async def tg_post_event(httpx_client, event_data):
else: else:
final_text = initial_text final_text = initial_text
expected_urls = []
if DESC_PREFIX:
expected_urls.append(f"{DESC_PREFIX}{number}/")
if USE_SUBSCRIPTION_BOT:
expected_urls.append(f"{PZK_PREFIX}{number}/")
expected_urls.append(
subscription_start_link(RESPONDER_BOT_NAME, "event", message_id)
)
min_length = len(final_text) - 15
# Проверяем, изменился ли текст. Если нет - не редактируем сообщение # Проверяем, изменился ли текст. Если нет - не редактируем сообщение
if final_text == initial_text: if final_text == initial_text:
logger.info(f"Финальный текст идентичен исходному для события {event_data['number']}, редактирование не требуется") logger.info(f"Финальный текст идентичен исходному для события {event_data['number']}, редактирование не требуется")
@@ -539,11 +557,13 @@ async def tg_post_event(httpx_client, event_data):
entity_label = f"события {event_data['number']}" entity_label = f"события {event_data['number']}"
links_ok = await _edit_event_add_links( links_ok = await _edit_event_add_links(
httpx_client, message_is_photo, message_id, final_text, entity_label httpx_client, message_is_photo, message_id, final_text, entity_label,
expected_urls, min_length,
) )
if not links_ok and message_is_photo: if not links_ok and message_is_photo:
links_ok = await _edit_event_add_links( links_ok = await _edit_event_add_links(
httpx_client, False, message_id, final_text, entity_label httpx_client, False, message_id, final_text, entity_label,
expected_urls, min_length,
) )
if not links_ok: if not links_ok:
logger.error( logger.error(
+43 -18
View File
@@ -58,32 +58,57 @@ 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: def extract_text_link_urls(entities) -> list:
""" """URL из entities типа text_link (ответ Telegram Bot API или python-telegram-bot)."""
Проверка, что в опубликованном caption/text есть кликабельные ссылки. urls = []
for ent in entities or []:
Telegram в ответе editMessage* отдаёт plain text (без href=) и entities типа text_link.
"""
if entities:
for ent in entities:
if isinstance(ent, dict): if isinstance(ent, dict):
if ent.get("type") == "text_link" and ent.get("url"): if ent.get("type") == "text_link" and ent.get("url"):
return True urls.append(ent["url"])
elif getattr(ent, "type", None) == "text_link" and getattr(ent, "url", None): elif getattr(ent, "type", None) == "text_link":
url = getattr(ent, "url", None)
if url:
urls.append(url)
return urls
def published_message_has_expected_links(
entities,
expected_urls,
*,
result_text: str | None = None,
min_length: int | None = None,
) -> bool:
"""
Проверка, что в сообщении есть именно footer-ссылки публикации.
Нельзя проверять «любой text_link» или слова «Инфо»/«Подписка» в тексте:
в теле события могут быть свои <a href> из replace_vk_links.
"""
expected_urls = [u for u in (expected_urls or []) if u]
if not expected_urls:
return True return True
if not text: if min_length is not None and result_text is not None:
if len(result_text) < min_length:
return False return False
link_markers = ( found = set(extract_text_link_urls(entities))
"Оригинал в ВК", return all(url in found for url in expected_urls)
"Подписка",
"Инфо",
"Иду", def published_message_has_links(text: str, entities=None, expected_urls=None) -> bool:
"""Проверка footer-ссылок; expected_urls обязателен при вызове из publisher."""
if expected_urls is not None:
return published_message_has_expected_links(
entities,
expected_urls,
result_text=text,
min_length=None,
) )
return any(marker in text for marker in link_markers) return bool(extract_text_link_urls(entities))
def published_text_has_links(text: str) -> bool: def published_text_has_links(text: str) -> bool:
"""Обратная совместимость; предпочтительно published_message_has_links.""" """Устаревший вызов без expected_urls — только для совместимости."""
return published_message_has_links(text) return published_message_has_links(text)
+24 -7
View File
@@ -22,7 +22,8 @@ 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_message_has_links, published_message_has_expected_links,
extract_text_link_urls,
) )
from season_links import subscription_start_link from season_links import subscription_start_link
@@ -154,10 +155,12 @@ async def download_image(image_url, httpx_client):
raise raise
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, expected_urls, min_length
):
""" """
Второй шаг публикации: editMessageCaption/Text со ссылками. Второй шаг публикации: editMessageCaption/Text со ссылками.
Повторяет попытку, пока в ответе API нет text_link / меток ссылок. Повторяет попытку, пока в ответе API нет всех expected_urls в text_link entities.
""" """
await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC) await asyncio.sleep(EDIT_LINKS_INITIAL_DELAY_SEC)
@@ -183,16 +186,21 @@ async def _edit_post_add_links(bot, message_id, message_has_image, final_text, v
published_text = edited.text or "" published_text = edited.text or ""
entities = edited.entities or [] entities = edited.entities or []
if published_message_has_links(published_text, entities): if published_message_has_expected_links(
entities, expected_urls, result_text=published_text, min_length=min_length
):
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})"
) )
return True return True
found = extract_text_link_urls(entities)
logger.warning( logger.warning(
f"editMessage* ok, но ссылки не обнаружены в ответе " f"editMessage* ok, но footer-ссылки не подтверждены "
f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) для VK ID {vk_post_id}" f"(попытка {attempt}/{EDIT_LINKS_MAX_ATTEMPTS}) для VK ID {vk_post_id}; "
f"ожидались {expected_urls}, найдены text_link: {found}, "
f"len(text)={len(published_text)}, min_len={min_length}"
) )
except TimedOut as e: except TimedOut as e:
if attempt >= EDIT_LINKS_MAX_ATTEMPTS: if attempt >= EDIT_LINKS_MAX_ATTEMPTS:
@@ -519,9 +527,18 @@ 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}"
expected_urls = []
if post['vk_post_url'] and (post['vk_post_url'].startswith('http://') or post['vk_post_url'].startswith('https://')):
expected_urls.append(post['vk_post_url'])
if post['is_event'] and USE_SUBSCRIPTION_BOT:
expected_urls.append(
subscription_start_link(RESPONDER_BOT_NAME, "post", message_id)
)
min_length = len(final_text) - 15
if not await _edit_post_add_links( if not await _edit_post_add_links(
bot, message_id, message_has_image, final_text, vk_post_id bot, message_id, message_has_image, final_text, vk_post_id,
expected_urls, min_length,
): ):
logger.error( logger.error(
f"Запись VK ID {vk_post_id}: сообщение отправлено (id={message_id}), " f"Запись VK ID {vk_post_id}: сообщение отправлено (id={message_id}), "