20135263c4
Posts can now be sourced from multiple VK groups, each routed to its own Telegram/MAX destination(s) with independent on/off switches, configured via data/routes.json (supports // line comments). Falls back to a single route auto-generated from the legacy VK_SOURCE/TG_CHAT_ID/MAX_CHAT_ID env vars if routes.json doesn't exist yet, so existing deployments keep working. Routes are processed strictly sequentially within a cycle (no concurrency) to keep flood control on VK/TG/MAX correct, since bot tokens are shared across routes. DB schema gains route_id in the posts uniqueness key so the same VK donor can safely feed multiple routes without status collisions. Also removes the trailing-hashtag-stripping logic in text_formatter, which was silently deleting VK posts' own hashtags whenever COMMON_TAGS wasn't configured (it always wasn't) - posts are now forwarded unchanged.
103 lines
4.1 KiB
Python
103 lines
4.1 KiB
Python
from __future__ import annotations
|
||
from loguru import logger
|
||
|
||
from datetime import datetime
|
||
from typing import Any, Optional
|
||
from aiogram import Bot
|
||
try:
|
||
from .config import settings
|
||
from .text_formatter import split_message_chunks
|
||
except (ImportError, ValueError):
|
||
from config import settings
|
||
from text_formatter import split_message_chunks
|
||
|
||
|
||
class AdminNotifier:
|
||
def __init__(self, bot: Bot) -> None:
|
||
self.bot = bot
|
||
self.admin_ids = settings.admin_id_list
|
||
|
||
async def send_to_all(self, text: str) -> None:
|
||
if not self.admin_ids:
|
||
return
|
||
|
||
for chunk in split_message_chunks(text, 4000):
|
||
for admin_id in self.admin_ids:
|
||
try:
|
||
await self.bot.send_message(
|
||
chat_id=admin_id,
|
||
text=chunk,
|
||
parse_mode="HTML",
|
||
disable_web_page_preview=True,
|
||
)
|
||
except Exception as exc:
|
||
logger.warning("Failed to send report to admin {}: {}", admin_id, exc)
|
||
|
||
def _route_section(self, route_name: str, vk_url: str, found_posts: list[dict[str, Any]], error: Optional[str]) -> Optional[str]:
|
||
if error:
|
||
return (
|
||
f"⚠️ <b>{route_name}</b> (<a href=\"{vk_url}\">VK</a>) — ошибка проверки:\n"
|
||
f"<code>{error}</code>"
|
||
)
|
||
|
||
if not found_posts:
|
||
if not settings.report_empty_runs:
|
||
return None
|
||
return f"ℹ️ <b>{route_name}</b> (<a href=\"{vk_url}\">VK</a>) — новых постов не обнаружено."
|
||
|
||
lines = [f"🚀 <b>{route_name}</b> (<a href=\"{vk_url}\">VK</a>) — опубликовано: {len(found_posts)}"]
|
||
for idx, p in enumerate(found_posts, 1):
|
||
post_id = p.get("vk_post_id")
|
||
vk_post_url = p.get("vk_post_url")
|
||
tg_url = p.get("tg_url")
|
||
max_url = p.get("max_url")
|
||
tg_status = p.get("tg_status")
|
||
max_status = p.get("max_status")
|
||
tg_err = p.get("tg_error")
|
||
max_err = p.get("max_error")
|
||
|
||
lines.append(f"\n<b>{idx}. Пост #{post_id}</b>")
|
||
lines.append(f"• <a href=\"{vk_post_url}\">Оригинал в VK</a>")
|
||
|
||
if tg_status == "published":
|
||
tg_link = f"<a href=\"{tg_url}\">Ссылка</a>" if tg_url else "Опубликовано"
|
||
lines.append(f"• Telegram: ✅ {tg_link}")
|
||
elif tg_err:
|
||
lines.append(f"• Telegram: ❌ Ошибка: <code>{tg_err[:100]}</code>")
|
||
|
||
if max_status == "published":
|
||
max_link = f"<a href=\"{max_url}\">Ссылка</a>" if max_url else "Опубликовано"
|
||
lines.append(f"• MAX: ✅ {max_link}")
|
||
elif max_err:
|
||
lines.append(f"• MAX: ❌ Ошибка: <code>{max_err[:100]}</code>")
|
||
|
||
return "\n".join(lines)
|
||
|
||
async def notify_cycle_result(self, route_results: list[dict[str, Any]]) -> None:
|
||
"""route_results: one entry per route this cycle -
|
||
{"route_name": str, "vk_url": str, "reports": list[dict], "error": Optional[str]}
|
||
Sent as a single aggregated report per cycle (chunked if too long) rather
|
||
than one message per route, to avoid flooding admins when there are several
|
||
donor->recipient routes.
|
||
"""
|
||
if not self.admin_ids:
|
||
return
|
||
|
||
sections: list[str] = []
|
||
for rr in route_results:
|
||
section = self._route_section(
|
||
rr.get("route_name", rr.get("route_id", "?")),
|
||
rr.get("vk_url", ""),
|
||
rr.get("reports", []),
|
||
rr.get("error"),
|
||
)
|
||
if section:
|
||
sections.append(section)
|
||
|
||
if not sections:
|
||
return
|
||
|
||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
text = f"<b>[VK Poster] Итоги проверки</b> — <code>{now_str}</code>\n\n" + "\n\n".join(sections)
|
||
await self.send_to_all(text)
|