diff --git a/.cursor/rules/version-and-docs.mdc b/.cursor/rules/version-and-docs.mdc index 6a742f0..8316739 100644 --- a/.cursor/rules/version-and-docs.mdc +++ b/.cursor/rules/version-and-docs.mdc @@ -20,4 +20,4 @@ alwaysApply: true 5. Не поднимать версию за косметические правки без изменения поведения. -Текущая базовая версия: **1.1.0**. +Текущая базовая версия: **1.1.1**. diff --git a/README.md b/README.md index 207a290..5b01d4d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Zilant / VOLK — Telegram poster и подписки -**Версия:** 1.1.0 ([`VERSION`](VERSION)) +**Версия:** 1.1.1 ([`VERSION`](VERSION)) Автоматическая публикация постов и событий в Telegram-канал, веб-редактор, бот подписок с сезонными deep link. diff --git a/VERSION b/VERSION index 9084fa2..524cb55 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.1.1 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e18f6ad..0a901cb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,12 @@ Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/). Версия проекта — в файле [`VERSION`](../VERSION) (SemVer). +## [1.1.1] - 2026-09-02 + +### Fixed + +- Публикация постов и событий: второй шаг (edit со строкой ссылок) повторяется до появления `href=` в ответе API; без подтверждённых ссылок запись не помечается опубликованной. Устраняет редкие случаи, когда edit логировался как успех, а ссылки в канале отсутствовали. + ## [1.1.0] - 2026-08-30 ### Added diff --git a/evtg_publish.py b/evtg_publish.py index 8627bc3..d159516 100644 --- a/evtg_publish.py +++ b/evtg_publish.py @@ -9,7 +9,13 @@ from datetime import datetime, timezone, timedelta from urllib.parse import urlparse, unquote from dotenv import load_dotenv 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 # Загрузка переменных окружения @@ -126,6 +132,114 @@ async def download_image(image_url, httpx_client): 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): """Публикация одного события""" 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_message_url = bot_api_method_url(BOT_TOKEN, "sendMessage") + message_is_photo = False if has_image: try: @@ -255,6 +370,7 @@ async def tg_post_event(httpx_client, event_data): if photo_result.get('ok'): message_data = photo_result.get('result', {}) message_id = message_data.get('message_id') + message_is_photo = True logger.info(f"Фото успешно отправлено для события {event_data['number']}, message_id={message_id}") else: # Ошибка в ответе API - пробуем отправить текстовое сообщение @@ -418,203 +534,20 @@ async def tg_post_event(httpx_client, event_data): logger.info(f"Финальный текст идентичен исходному для события {event_data['number']}, редактирование не требуется") return message_id - await asyncio.sleep(2) - - # Редактируем сообщение, добавляя ссылки - 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") - edit_text_url = bot_api_method_url(BOT_TOKEN, "editMessageText") - - if has_image: - try: - 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') - 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 + entity_label = f"события {event_data['number']}" + links_ok = await _edit_event_add_links( + httpx_client, message_is_photo, message_id, final_text, entity_label + ) + if not links_ok and message_is_photo: + links_ok = await _edit_event_add_links( + httpx_client, False, message_id, final_text, entity_label + ) + if not links_ok: + logger.error( + f"Событие {event_data['number']}: сообщение отправлено (id={message_id}), " + f"но ссылки не добавлены" + ) + return None return message_id # Возвращаем только message_id diff --git a/telegram_relay.py b/telegram_relay.py index 1396af8..26f5063 100644 --- a/telegram_relay.py +++ b/telegram_relay.py @@ -50,3 +50,14 @@ def get_ptb_base_urls() -> Tuple[str, str]: def bot_api_method_url(token: str, method: str) -> str: """Полный URL метода Bot API через relay (для сырых httpx/requests вызовов).""" 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 diff --git a/tg_publish.py b/tg_publish.py index b6ac609..6114b12 100644 --- a/tg_publish.py +++ b/tg_publish.py @@ -16,7 +16,14 @@ from formatter import get_event_text from telegram import Bot from telegram.error import TelegramError, TimedOut 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 # Настройка anyio для правильной работы в отдельном потоке @@ -146,6 +153,88 @@ async def download_image(image_url, httpx_client): logger.error(f"Ошибка при скачивании изображения {image_url}: {e}") 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): """Публикация одной записи по 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) final_text = f"{text}\n\n{links_line}" - await asyncio.sleep(2) # задержка между обращениями к телеграм - - # Редактируем сообщение, добавляя ссылки - # Используем message_has_image для определения типа сообщения - # Если было отправлено изображение, редактируем caption, иначе - текст - if message_has_image: - # Сообщение с изображением - редактируем 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 - 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 + if not await _edit_post_add_links( + bot, message_id, message_has_image, final_text, vk_post_id + ): + logger.error( + f"Запись VK ID {vk_post_id}: сообщение отправлено (id={message_id}), " + f"но ссылки не добавлены — published_in_tg не обновляем" + ) + return False await asyncio.sleep(2) # задержка между обращениями к телеграм