From 770159cebdb3b650653fc2e26e9e718d73e5c88e Mon Sep 17 00:00:00 2001 From: Archos Date: Sat, 18 Apr 2026 15:12:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20oprava=20=C4=8Dist=C4=9Bn=C3=AD=20HTML,?= =?UTF-8?q?=20STATS=5FTOKEN,=20p=C5=99esko=C4=8Den=C3=AD=20hashtag=20toot?= =?UTF-8?q?=C5=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- weekly_report.py | 213 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 weekly_report.py diff --git a/weekly_report.py b/weekly_report.py new file mode 100644 index 0000000..599e682 --- /dev/null +++ b/weekly_report.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +import html +import json +import os +import re +import sys +import argparse +import urllib.request +import urllib.error +from datetime import datetime, timezone, timedelta + +TIPS = [ + "Hashtagy fungují jako klíčová slova – používej je a ostatní tě snáz najdou.", + "Pomocí seznamů si můžeš organizovat sledované účty do tematických skupin.", + "Příspěvky s viditelností \"Pouze sledující\" vidí jen tvoji sledující, ne celý fediverse.", + "Filtrovat nežádoucí obsah lze přes Nastavení → Filtry – hodí se na citlivá témata.", + "Zmínit někoho funguje i napříč instancemi – stačí napsat @uzivatel@instance.tld.", +] + +def load_env(path=".env"): + env = {} + try: + with open(path) as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, val = line.partition("=") + env[key.strip()] = val.strip().strip('"').strip("'") + except FileNotFoundError: + pass + return env + +def api_get(url, token): + req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"}) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as e: + print(f"HTTP {e.code} při volání {url}: {e.read().decode()}", file=sys.stderr) + raise + except urllib.error.URLError as e: + print(f"Chyba sítě při volání {url}: {e.reason}", file=sys.stderr) + raise + +def api_post(url, token, data): + body = json.dumps(data).encode() + req = urllib.request.Request( + url, + data=body, + headers={ + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as e: + print(f"HTTP {e.code} při odesílání tootu: {e.read().decode()}", file=sys.stderr) + raise + except urllib.error.URLError as e: + print(f"Chyba sítě při odesílání tootu: {e.reason}", file=sys.stderr) + raise + +def get_measures(base_url, admin_token, date_from, date_to): + url = f"{base_url}/api/v1/admin/measures" + payload = { + "keys": ["new_users", "active_users", "interactions"], + "start_at": date_from.isoformat(), + "end_at": date_to.isoformat(), + } + body = json.dumps(payload).encode() + req = urllib.request.Request( + url, + data=body, + headers={ + "Authorization": f"Bearer {admin_token}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as e: + print(f"HTTP {e.code} při volání measures API: {e.read().decode()}", file=sys.stderr) + raise + except urllib.error.URLError as e: + print(f"Chyba sítě při volání measures API: {e.reason}", file=sys.stderr) + raise + +def truncate(text, max_chars=100): + text = re.sub(r']*class="[^"]*hashtag[^"]*"[^>]*>.*?', "",text, flags=re.IGNORECASE) + text = re.sub(r"<[^>]+>", " ", text) + text = html.unescape(text) + text = re.sub(r"\s+", " ", text).strip() + if len(text) <= max_chars: + return text + cut = text[:max_chars].rsplit(" ", 1)[0].rstrip(".,!?;:") + return cut + "…" + +def format_date_cs(dt): + months = [ + "ledna", "února", "března", "dubna", "května", "června", + "července", "srpna", "září", "října", "listopadu", "prosince", + ] + return f"{dt.day}. {months[dt.month - 1]}" + +def build_toot(measures_data, tags, trend_status, date_from, date_to, week_number): + stats = {m["key"]: int(m["total"]) for m in measures_data} + new_users = stats.get("new_users", 0) + active_users = stats.get("active_users", 0) + interactions = stats.get("interactions", 0) + + hashtags = " ".join(f"#{t['name']}" for t in tags[:3]) if tags else "(žádné)" + + tip = TIPS[week_number % len(TIPS)] + + if trend_status: + acct = trend_status.get("account", {}).get("acct", "?") + content = truncate(trend_status.get("content", ""), 100) + boosts = trend_status.get("reblogs_count", 0) + favs = trend_status.get("favourites_count", 0) + toot_tyden = f"🌟 Toot týdne od @{acct}:\n\"{content}\"\n🔁 {boosts} ⭐ {favs}\n\n" + else: + toot_tyden = "" + + date_from_str = format_date_cs(date_from) + date_to_str = format_date_cs(date_to) + year = date_to.year + + return ( + f"🐘 Týdenní přehled Mamutovo.cz\n" + f"📅 {date_from_str} – {date_to_str} {year}\n" + f"\n" + f"👥 Noví uživatelé: {new_users}\n" + f"✅ Aktivní uživatelé: {active_users}\n" + f"📝 Interakce: {interactions}\n" + f"\n" + f"🔥 Populární hashtagy:\n" + f"{hashtags}\n" + f"\n" + f"{toot_tyden}" + f"💡 Tip týdne: {tip}\n" + f"\n" + f"🔗 fedi.mamutovo.cz" + ) + +def main(): + parser = argparse.ArgumentParser(description="Týdenní statistiky Mamutovo.cz") + parser.add_argument("--dry-run", action="store_true", help="Pouze vypíše toot, neodešle") + args = parser.parse_args() + + env = {**load_env(), **os.environ} + + for var in ("NOVINKY_TOKEN", "INSTANCE_URL", "STATS_TOKEN"): + if not env.get(var): + print(f"Chybí proměnná prostředí: {var}", file=sys.stderr) + sys.exit(1) + + novinky_token = env["NOVINKY_TOKEN"] + admin_token = env["STATS_TOKEN"] + base_url = env["INSTANCE_URL"].rstrip("/") + + now = datetime.now(timezone.utc) + date_to = now.replace(hour=0, minute=0, second=0, microsecond=0) + date_from = date_to - timedelta(days=7) + week_number = now.isocalendar()[1] + + try: + measures_data = get_measures(base_url, admin_token, date_from, date_to) + except Exception: + sys.exit(1) + + try: + tags = api_get(f"{base_url}/api/v1/trends/tags?limit=3", admin_token) + except Exception: + tags = [] + + try: + statuses = api_get(f"{base_url}/api/v1/trends/statuses?limit=5", admin_token) + trend_status = None + for s in statuses: + clean = re.sub(r']*class="[^"]*hashtag[^"]*"[^>]*>.*?', "",s.get("content", ""), flags=re.IGNORECASE) + clean = re.sub(r"<[^>]+>", " ", clean) + clean = html.unescape(clean) + clean = re.sub(r"\s+", " ", clean).strip() + if len(clean) >= 20: + trend_status = s + break + except Exception: + trend_status = None + + toot = build_toot(measures_data, tags, trend_status, date_from, date_to, week_number) + + if args.dry_run: + print(toot) + return + + try: + result = api_post( + f"{base_url}/api/v1/statuses", + novinky_token, + {"status": toot, "visibility": "public"}, + ) + print(f"Toot odeslán: {result.get('url', '(bez URL)')}") + except Exception: + sys.exit(1) + +if __name__ == "__main__": + main()