diff --git a/src/vk_parser_app/workers/tg_poster.py b/src/vk_parser_app/workers/tg_poster.py
index aa7690b..82bc008 100644
--- a/src/vk_parser_app/workers/tg_poster.py
+++ b/src/vk_parser_app/workers/tg_poster.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
+import aiohttp
import json
import os
import re
@@ -12,7 +13,7 @@ from zoneinfo import ZoneInfo
from aiogram import Bot
from aiogram.exceptions import TelegramRetryAfter
-from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaVideo, LinkPreviewOptions
+from aiogram.types import FSInputFile, InputMediaPhoto, InputMediaVideo
from loguru import logger
from ..config import settings
@@ -29,6 +30,8 @@ from ..text_utils import build_publication_text
LOCAL_TZ = ZoneInfo("Asia/Yekaterinburg")
MAX_MEDIA_GROUP = 10
+MAX_RICH_MEDIA = 50
+MAX_RICH_TEXT = 32768
PROJECT_ROOT = Path(__file__).resolve().parents[3]
UPLOAD_ROOT = PROJECT_ROOT / "uploads"
@@ -85,6 +88,10 @@ def local_upload_path(url: str) -> Path | None:
return path if path.is_file() else None
+class RichMessageUnavailable(RuntimeError):
+ pass
+
+
class TelegramPoster:
def __init__(self) -> None:
self.pool = None
@@ -353,59 +360,77 @@ class TelegramPoster:
messages = await self.tg_retry(lambda: self.bot.send_media_group(media=group, **self.chat_kwargs()))
return [int(message.message_id) for message in messages]
- async def send_post(self, post: dict[str, Any]) -> list[int]:
- text = self.build_text(post)
- media_rows = await self.load_media(int(post["id"]))
-
- # If no media, just send text
- if not media_rows:
- return await self.send_text(text)
+ def rich_media_value(self, item: dict[str, Any]) -> str | None:
+ stored = str(item.get("tg_file_id") or item.get("storage_attachment_id") or "").strip()
+ return stored or None
- # Telegram Rich Message (Single message, block layout, up to 4096 chars)
- # We use LinkPreviewOptions with the first media URL to show it above the text.
- first_url = None
- for row in media_rows:
- url = str(row.get("original_url") or "").strip()
- if url.startswith("http"):
- first_url = url
- break
+ def rich_text_html(self, text: str) -> str:
+ paragraphs = []
+ for paragraph in str(text or "").strip().split("\n\n"):
+ body = "
".join(line for line in paragraph.splitlines() if line.strip())
+ if body:
+ paragraphs.append(f"
{body}
") + return "\n".join(paragraphs) - if first_url: - # Send as a Rich Message with Link Preview - message = await self.tg_retry( - lambda: self.bot.send_message( - text=text, - link_preview_options=LinkPreviewOptions( - url=first_url, - show_above_text=True - ), - **self.chat_kwargs() - ) - ) - message_ids = [int(message.message_id)] - - # If it's an album (multiple media), we can send the rest as a media group without caption - if len(media_rows) > 1: - media_items = [] - for item in media_rows[1:]: - media = self.media_value(item) - if not media: continue - media_type = str(item.get("media_type") or "") - if media_type == "photo": - media_items.append({"type": "photo", "media": media}) - elif media_type == "video": - media_items.append({"type": "video", "media": media}) - - rest = media_items - for start in range(0, len(rest), self.media_group_max_items): - chunk = rest[start : start + self.media_group_max_items] - message_ids.extend(await self.send_media_items(chunk, caption=None)) - if self.send_delay_sec: - await asyncio.sleep(self.send_delay_sec) - - return message_ids + def build_rich_message(self, text: str, media_rows: list[dict[str, Any]]) -> dict[str, Any] | None: + rich_media = [] + media_tags = [] + for idx, item in enumerate(media_rows[:MAX_RICH_MEDIA]): + media_type = str(item.get("media_type") or "") + if media_type not in ("photo", "video"): + continue + media = self.rich_media_value(item) + if not media: + return None + media_id = f"m{idx}" + input_type = "photo" if media_type == "photo" else "video" + rich_media.append({"id": media_id, "media": {"type": input_type, "media": media}}) + if media_type == "photo": + media_tags.append(f'