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()