# Форматирование карточки поста для Telegram (MUST: ≤4096 символов). # «Богатый репост»: [комментарий модератора] + полный текст поста + ссылка на оригинал. # Без служебной информации (направление/источник/просмотры) — она в вебе и бандле. # Текст — plain (не HTML): исходный текст поста не перекодируем, ссылки Telegram # распознаёт сам; предпросмотр ссылки отключается на уровне sendMessage. import re MAX_TEXT = 4096 # фото/видео, поддерживаемые Bot API sendPhoto/sendVideo MEDIA_EXTS = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".mp4", ".mov", ".avi", ".mkv") def make_card(post: dict, comment: str | None = None) -> dict: """Собирает карточку «богатого репоста». Возвращает {"media": path|None, "text": str}. post: dict из БД (id, text, url, media_path, is_own, tg_channel, tg_post_id, ...). Формат текста: [комментарий модератора] — если задан, первым блоком [полный текст исходного поста] — без сниппета/суммари [ссылка на оригинал] — последней строкой (атрибуция для своих) Текст НЕ содержит служебки (📁 направление/язык, 📰 источник, 👁 просмотры) — это был «агрегаторный» вид карточки. """ comment = (comment or "").strip() # тело: пересказ (rewritten_text) приоритетнее исходного текста body = (post.get("rewritten_text") or post.get("text") or "").strip() rewritten = bool((post.get("rewritten_text") or "").strip()) # ссылка на оригинал / атрибуция своего контента is_own = int(post.get("is_own") or 0) == 1 orig = None if post.get("tg_channel") == "dedinit" and post.get("tg_post_id"): orig = f"https://t.me/dedinit/{post['tg_post_id']}" elif post.get("url"): orig = post.get("url") if rewritten: # пересказ — публикация от имени автора канала (не пересылка) tail = "✍️ Дед в АйТи (@dedinit)" + (f" · Источник: {orig}" if orig else "") elif is_own: tail = "✍️ Дед в АйТи (@dedinit)" + (f" · {orig}" if orig else "") elif orig: tail = f"🔗 Оригинал: {orig}" else: tail = None parts = [] if comment: parts.append(comment) if body: parts.append(body) if tail: parts.append(tail) if not parts: parts = ["(пост без текста)"] text = "\n\n".join(parts) if len(text) > MAX_TEXT: # обрезаем полный текст, но сохраняем ссылку/атрибуцию в конце tail_block = ("\n\n" + tail) if tail else "" budget = MAX_TEXT - len(tail_block) - 3 body_cut = text[:budget].rstrip() + "…" text = body_cut + tail_block if len(text) > MAX_TEXT: text = text[: MAX_TEXT - 3] + "…" # медиа: фото И видео (раньше отбрасывалось всё кроме картинок) media = post.get("media_path") or post.get("media_local") or None if media and not str(media).lower().endswith(MEDIA_EXTS): media = None if media: # нормализация пути: media_path в БД = 'media/', но старые файлы # физически лежат в media/media/ (баг краулера до 2026-09-12). # Пробуем оба варианта, отдаём тот, что существует. import os as _os cands = [ str(media), str(_os.path.join("media", str(media).replace("media/", "", 1))) if str(media).startswith("media/") else None, ] found = next((c for c in cands if c and _os.path.exists(c)), None) media = found or media # если ни один не найден — оставляем как есть (publisher retry) return {"media": media, "text": text}