# Публикация карточек в Telegram через Bot API. # Бот: @dedinit_vesti___bot (шаблон из конфига .env). # Режим подтверждения: веб вызывает publish() только после явного клика. # Метрики: getChatMemberCount / getMessage (views) для опубликованных. import os from pathlib import Path import httpx BASE_DIR = Path(__file__).resolve().parent.parent BOT_TOKEN = os.getenv("VESTI_BOT_TOKEN", "") API = "https://api.telegram.org" TIMEOUT = 20.0 def _bot_url(method: str) -> str: if not BOT_TOKEN: raise RuntimeError("VESTI_BOT_TOKEN не задан (dry-run недоступен для сети)") return f"{API}/bot{BOT_TOKEN}/{method}" def channel_username(direction: str, lang: str) -> str: """@dedinit_vesti___bot — по спеке; канал — тот же без _bot.""" return f"@dedinit_vesti_{direction}_{lang}_bot" def _upload_photo(chat_id: str, photo_path: str, caption: str = "") -> int: """Отправляет фото (медиа первым). Возвращает message_id.""" with open(photo_path, "rb") as f: r = httpx.post( _bot_url("sendPhoto"), data={"chat_id": chat_id, "caption": caption[:1024]}, files={"photo": f}, timeout=TIMEOUT, ) r.raise_for_status() return r.json()["result"]["message_id"] def _send_message(chat_id: str, text: str, parse_mode: str = "HTML") -> int: r = httpx.post( _bot_url("sendMessage"), json={"chat_id": chat_id, "text": text, "parse_mode": parse_mode}, timeout=TIMEOUT, ) r.raise_for_status() return r.json()["result"]["message_id"] def publish_card(card: dict, direction: str = "linux", lang: str = "ru", chat_id: str | None = None) -> dict: """Публикует карточку {media, text} в канал. Возвращает {message_id, media_message_id}. dry-run: если VESTI_BOT_TOKEN не задан — возвращает плейсхолдер (не отправляет). """ if not BOT_TOKEN: return {"message_id": 0, "media_message_id": 0, "dry_run": True} chat = chat_id or channel_username(direction, lang) media_msg = None if card.get("media"): media_msg = _upload_photo(chat, card["media"], caption=card["text"][:1024]) msg = _send_message(chat, card["text"]) return {"message_id": msg, "media_message_id": media_msg, "dry_run": False} def publish_multi(card: dict, directions: list[str], lang: str = "ru") -> dict: """Публикует карточку во ВСЕ каналы направлений. Возвращает {direction: {message_id, media_message_id, dry_run}}. directions: список направлений (напр. ['linux', 'ai']). При пустом списке — публикация в направление по умолчанию (card['direction']). """ if not directions: directions = [card.get("direction") or "linux"] out = {} for d in directions: out[d] = publish_card(card, direction=d, lang=lang) return out def get_views(chat_id: str, message_id: int) -> int: """Возвращает просмотры опубликованного сообщения через Bot API (getMessage → views).""" if not BOT_TOKEN: return 0 try: r = httpx.get(_bot_url("getMessage"), params={"chat_id": chat_id, "message_id": message_id}, timeout=TIMEOUT) r.raise_for_status() m = r.json()["result"] return int(m.get("views") or m.get("forwards") or 0) except Exception as e: print(f" ! get_views failed {chat_id}/{message_id}: {e}") return 0 def get_views_multi(directions: list[str], message_ids: dict, lang: str = "ru") -> dict: """Собирает просмотры по каждому направлению. Возвращает {direction: views}.""" out = {} for d in directions: mid = (message_ids.get(d) or {}).get("message_id") or 0 if mid: out[d] = get_views(channel_username(d, lang), mid) else: out[d] = 0 return out