#!/usr/bin/env python3
"""
tg_to_rss.py — собирает посты из публичных превью-страниц Telegram-каналов
(https://t.me/s/<username>) и генерирует один общий RSS 2.0 файл с картинками.

Работает БЕЗ токена бота и БЕЗ авторизации — использует ту же публичную
HTML-страницу, что открывается в браузере без входа в Telegram.

Ограничения:
  - t.me/s/<канал> отдаёт только последние ~20 постов на страницу
    (для RSS-ленты этого обычно достаточно; для полной истории нужен
    Telethon/Pyrogram — это отдельная история).
  - Канал должен быть ПУБЛИЧНЫМ (иметь username, а не только invite-ссылку).
  - Telegram может отдавать чуть разное количество постов в зависимости
    от региона/кэша, это нормально.

Использование:
  python3 tg_to_rss.py --out rss.xml
  (список каналов см. в CHANNELS ниже, либо через --channels)

Затем закиньте rss.xml на files.lainlife.org (по крону, см. пример внизу).
"""

import argparse
import hashlib
import html
import re
import sys
from datetime import datetime, timezone
from xml.sax.saxutils import escape

import requests
from bs4 import BeautifulSoup

# ─── Настройки ────────────────────────────────────────────────────────────

# Ваши 5 каналов, без @ и без ссылки — только username
CHANNELS = [
    "korbal028psychiatry",
    "verrotten_dev",
    "simplekancer",
    "lainlifesoc",
    "kp228",
    "metafifik",
    "tusovka_krevedki",
    "zuniqwq"
]

FEED_TITLE = "Мудец.рф — сборная лента Telegram-каналов"
FEED_LINK = "https://мудец.рф"
FEED_DESCRIPTION = "Автособранная лента постов из наших телеграм-каналов"

HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
    )
}

POSTS_PER_CHANNEL = 20  # сколько последних постов брать с каждого канала


# ─── Парсинг одного канала ────────────────────────────────────────────────

def fetch_channel_posts(username: str) -> list[dict]:
    """Возвращает список постов канала: dict с полями
    id, channel, url, date (datetime), text (html), images (list[str]).
    """
    url = f"https://t.me/s/{username}"
    resp = requests.get(url, headers=HEADERS, timeout=15)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "lxml")

    posts = []
    for msg in soup.select("div.tgme_widget_message"):
        # ID поста и постоянная ссылка
        post_url = msg.get("data-post")  # вида "username/123"
        if not post_url:
            continue
        post_link = f"https://t.me/{post_url}"
        post_id = post_url.split("/")[-1]

        # Дата публикации
        time_tag = msg.select_one("time.time")
        if time_tag and time_tag.get("datetime"):
            dt = datetime.fromisoformat(time_tag["datetime"])
        else:
            dt = datetime.now(timezone.utc)

        # Текст поста
        text_tag = msg.select_one("div.tgme_widget_message_text")
        text_html = str(text_tag) if text_tag else ""
        text_html = _clean_telegram_html(text_html)

        # Картинки: обычные фото-посты
        images = []
        for wrap in msg.select(".tgme_widget_message_photo_wrap"):
            style = wrap.get("style", "")
            m = re.search(r"background-image:url\('([^']+)'\)", style)
            if m:
                images.append(m.group(1))

        # Видео-превью тоже отдают "постер"-картинку — забираем и её
        for wrap in msg.select(".tgme_widget_message_video_thumb"):
            style = wrap.get("style", "")
            m = re.search(r"background-image:url\('([^']+)'\)", style)
            if m:
                images.append(m.group(1))

        # Пропускаем полностью пустые служебные посты (реакции/сервисные)
        if not text_html and not images:
            continue

        posts.append(
            {
                "id": f"{username}_{post_id}",
                "channel": username,
                "url": post_link,
                "date": dt,
                "text": text_html,
                "images": images,
            }
        )

    return posts


def _clean_telegram_html(raw: str) -> str:
    """Убирает обёртку div и приводит ссылки/переносы строк к приличному виду."""
    if not raw:
        return ""
    soup = BeautifulSoup(raw, "lxml")
    # <br> -> перенос строки
    for br in soup.find_all("br"):
        br.replace_with("\n")
    text = soup.get_text()
    text = html.escape(text.strip())
    text = text.replace("\n", "<br/>")
    return text


# ─── Генерация RSS ─────────────────────────────────────────────────────────

def build_rss(all_posts: list[dict]) -> str:
    items_xml = []
    for post in all_posts:
        title = f"[@{post['channel']}] {_make_title(post['text'], post['channel'])}"
        pub_date = post["date"].strftime("%a, %d %b %Y %H:%M:%S %z")
        guid = hashlib.md5(post["id"].encode()).hexdigest()

        description_parts = []
        if post["images"]:
            for img in post["images"]:
                description_parts.append(f'<img src="{escape(img)}"/><br/>')
        description_parts.append(post["text"])
        description = "".join(description_parts)

        enclosure_xml = ""
        if post["images"]:
            # RSS enclosure поддерживает только одну картинку — берём первую,
            # остальные (если есть) остаются внутри description как <img>
            enclosure_xml = (
                f'<enclosure url="{escape(post["images"][0])}" type="image/jpeg"/>'
            )

        items_xml.append(f"""    <item>
      <title>{escape(title)}</title>
      <link>{escape(post['url'])}</link>
      <guid isPermaLink="false">{guid}</guid>
      <pubDate>{pub_date}</pubDate>
      <source url="https://t.me/s/{escape(post['channel'])}">{escape(post['channel'])}</source>
      {enclosure_xml}
      <description><![CDATA[{description}]]></description>
    </item>""")

    now = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")

    rss = f"""<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{escape(FEED_TITLE)}</title>
    <link>{escape(FEED_LINK)}</link>
    <description>{escape(FEED_DESCRIPTION)}</description>
    <lastBuildDate>{now}</lastBuildDate>
    <atom:link href="https://files.lainlife.org/rss.xml" rel="self" type="application/rss+xml"/>
{chr(10).join(items_xml)}
  </channel>
</rss>
"""
    return rss


def _make_title(text_html: str, channel: str) -> str:
    plain = re.sub(r"<[^>]+>", " ", text_html)
    plain = html.unescape(plain).strip()
    plain = re.sub(r"\s+", " ", plain)
    if not plain:
        return f"Пост в @{channel}"
    return (plain[:80] + "…") if len(plain) > 80 else plain


# ─── main ───────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--out", default="rss.xml", help="куда сохранить итоговый файл")
    parser.add_argument(
        "--channels",
        nargs="+",
        default=None,
        help="список username каналов через пробел (без @). По умолчанию — список CHANNELS в коде",
    )
    args = parser.parse_args()

    channels = args.channels or CHANNELS
    all_posts = []
    for ch in channels:
        try:
            posts = fetch_channel_posts(ch)
            print(f"[ok] @{ch}: {len(posts)} постов", file=sys.stderr)
            all_posts.extend(posts[:POSTS_PER_CHANNEL])
        except Exception as e:
            print(f"[fail] @{ch}: {e}", file=sys.stderr)

    all_posts.sort(key=lambda p: p["date"], reverse=True)

    rss_xml = build_rss(all_posts)
    with open(args.out, "w", encoding="utf-8") as f:
        f.write(rss_xml)

    print(f"Готово: {args.out} ({len(all_posts)} постов)", file=sys.stderr)


if __name__ == "__main__":
    main()
