From 60ad4f0114038b5e49df237a937ed0949fd0f4e7 Mon Sep 17 00:00:00 2001 From: Archos Date: Tue, 31 Mar 2026 20:44:13 +0200 Subject: [PATCH] =?UTF-8?q?init:=20onboarding=20syst=C3=A9m=20pro=20CZ=20M?= =?UTF-8?q?astodon=20komunitu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + README.md | 29 ++ accounts.html | 1011 +++++++++++++++++++++++++++++++++++++++ accounts.json | 110 +++++ mastodon_cz_accounts.py | 243 ++++++++++ start.html | 380 +++++++++++++++ starter-general.csv | 16 + 7 files changed, 1794 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 accounts.html create mode 100644 accounts.json create mode 100644 mastodon_cz_accounts.py create mode 100644 start.html create mode 100644 starter-general.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..202ae5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +*.log +.env +test-output/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c32333e --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# fedi_start + +Onboarding systém pro nové uživatele Mastodonu (CZ/SK komunita). + +## Soubory + +| Soubor | Popis | +|---|---| +| `start.html` | Úvodní onboarding stránka | +| `accounts.html` | Interaktivní seznam CZ účtů s filtry | +| `accounts.json` | Data účtů (generováno skriptem) | +| `starter-general.csv` | Starter pack pro import do Mastodonu | +| `mastodon_cz_accounts.py` | Automatický sběr CZ/SK účtů | + +## Lokální spuštění +```bash +python3 -m http.server 8080 +# http://localhost:8080/accounts.html +``` + +## Generování dat +```bash +python3 mastodon_cz_accounts.py --output . +``` + +## Cron +``` +0 3 * * * /usr/bin/python3 /opt/fedi_start/mastodon_cz_accounts.py --output /var/www/fedi_start/ +``` diff --git a/accounts.html b/accounts.html new file mode 100644 index 0000000..b43f247 --- /dev/null +++ b/accounts.html @@ -0,0 +1,1011 @@ + + + + + +CZ účty na Mastodonu – Mamutovo + + + + + + +
+

CZ účty na Mastodonu

+

Automaticky aktualizovaný seznam aktivních českých a slovenských účtů. Aktualizace každý den.

+ +
+
+ 🔍 + +
+ + +
+
+ +
+ +
+ +
+ Zobrazuji 0 z 0 účtů +
+ + +
+
+ +
+ + + { } JSON +
+ +
+
+ Načítám účty... +
+ +
+
Žádné účty neodpovídají filtru. Zkus jiný výraz.
+ + + + diff --git a/accounts.json b/accounts.json new file mode 100644 index 0000000..14dee6d --- /dev/null +++ b/accounts.json @@ -0,0 +1,110 @@ +{ + "generated_at": "2025-03-29T03:00:00Z", + "count": 8, + "accounts": [ + { + "name": "Mamutovo", + "handle": "mamutovo@mamutovo.cz", + "bio": "Oficiální účet české Mastodon instance Mamutovo. Novinky, tipy a komunita pro CZ/SK uživatele.", + "avatar": "", + "followers": 1200, + "statuses": 340, + "score": 95, + "tags": ["česky", "komunita", "mastodon"], + "category": "ostatni", + "last_active": "2025-03-28", + "url": "https://mamutovo.cz/@mamutovo" + }, + { + "name": "Linux CZ", + "handle": "linuxcz@fosstodon.org", + "bio": "Česká komunita okolo Linuxu a open source. Tipy, návody, diskuse. 🇨🇿", + "avatar": "", + "followers": 870, + "statuses": 1200, + "score": 88, + "tags": ["linux", "opensource", "tech"], + "category": "tech", + "last_active": "2025-03-29", + "url": "https://fosstodon.org/@linuxcz" + }, + { + "name": "Fedi.Tips", + "handle": "feditips@mstdn.social", + "bio": "Tipy jak používat Mastodon a fediverse. V češtině i angličtině.", + "avatar": "", + "followers": 650, + "statuses": 980, + "score": 82, + "tags": ["mastodon", "tipy", "fediverse"], + "category": "tech", + "last_active": "2025-03-27", + "url": "https://mstdn.social/@feditips" + }, + { + "name": "Open Source CZ", + "handle": "oscz@mastodon.social", + "bio": "Open source projekty, svobodný software a decentralizace po česku. 🇨🇿", + "avatar": "", + "followers": 540, + "statuses": 760, + "score": 79, + "tags": ["opensource", "tech", "svobodný software"], + "category": "tech", + "last_active": "2025-03-26", + "url": "https://mastodon.social/@oscz" + }, + { + "name": "Česká věda", + "handle": "ceskaveda@scholar.social", + "bio": "Popularizace vědy a výzkumu v češtině. Biologie, fyzika, astronomie. 🇨🇿", + "avatar": "", + "followers": 430, + "statuses": 520, + "score": 74, + "tags": ["věda", "vzdělávání", "česky"], + "category": "veda", + "last_active": "2025-03-25", + "url": "https://scholar.social/@ceskaveda" + }, + { + "name": "Foto CZ", + "handle": "fotocz@mastodon.social", + "bio": "Česká fotografická komunita. Krajiny, portréty, street foto. #fotografie 🇨🇿", + "avatar": "", + "followers": 390, + "statuses": 890, + "score": 71, + "tags": ["fotografie", "foto", "umění"], + "category": "foto", + "last_active": "2025-03-28", + "url": "https://mastodon.social/@fotocz" + }, + { + "name": "Gaming CZ", + "handle": "gamingcz@mastodon.social", + "bio": "Videohry po česku. Recenze, novinky, diskuse bez korporátního hype.", + "avatar": "", + "followers": 310, + "statuses": 430, + "score": 65, + "tags": ["gaming", "hry", "česky"], + "category": "gaming", + "last_active": "2025-03-20", + "url": "https://mastodon.social/@gamingcz" + }, + { + "name": "Kultura CZ", + "handle": "kulturacz@mastodon.social", + "bio": "Kultura, knihy, filmy a hudba v češtině. 🇨🇿", + "avatar": "", + "followers": 280, + "statuses": 350, + "score": 61, + "tags": ["kultura", "knihy", "hudba"], + "category": "kultura", + "last_active": "2025-03-22", + "url": "https://mastodon.social/@kulturacz" + } + ] +} diff --git a/mastodon_cz_accounts.py b/mastodon_cz_accounts.py new file mode 100644 index 0000000..0c6f8fa --- /dev/null +++ b/mastodon_cz_accounts.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +""" +mastodon_cz_accounts.py +Sbírá CZ/SK účty z Mastodonu přes /api/v1/directory?language=cs +– stejná logika jako mstdn.cz od @adent. + +Kritéria: + - discoverable=true (uživatel chce být nalezen) + - jazyk příspěvků nastaven na cs nebo sk + - aktivní za posledních 30 dní + - min. 10 příspěvků + +Použití: + python3 mastodon_cz_accounts.py + python3 mastodon_cz_accounts.py --output /var/www/start/ + +Cron (každý den v 3:00): + 0 3 * * * /usr/bin/python3 /opt/mastodon-start/mastodon_cz_accounts.py --output /var/www/start/ >> /var/log/mastodon-start.log 2>&1 +""" + +import json, csv, time, re, argparse, logging +from datetime import datetime, timezone, timedelta +from pathlib import Path +import urllib.request, urllib.error, urllib.parse + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S") +log = logging.getLogger(__name__) + +# ── CONFIG ──────────────────────────────────── +QUERY_INSTANCES = [ + "mastodon.social", + "mstdn.social", + "mastodon.online", + "fosstodon.org", + "chaos.social", + "mastodon.cloud", + "infosec.exchange", + "scholar.social", + "mamutovo.cz", +] +TARGET_LANGUAGES = ["cs", "sk"] +MIN_STATUSES = 10 +MIN_FOLLOWERS = 10 +MAX_DAYS_INACTIVE = 30 +TOP_N = 60 +RATE_LIMIT_DELAY = 1.2 +PAGE_LIMIT = 80 +MAX_PAGES = 10 + +# ── HTTP ────────────────────────────────────── +def api_get(url, timeout=12): + try: + req = urllib.request.Request(url, headers={"User-Agent": "MamutovoStarterBot/1.0 (+https://mamutovo.cz)"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + except urllib.error.HTTPError as e: + if e.code == 429: + log.warning("Rate limit – čekám 60s"); time.sleep(60) + elif e.code not in (404, 410): + log.debug(f"HTTP {e.code} {url}") + return None + except Exception as e: + log.debug(f"Chyba {url}: {e}"); return None + +# ── SBĚR ───────────────────────────────────── +def fetch_directory(instance, language, order="active"): + accounts = [] + seen_ids = set() + for page in range(MAX_PAGES): + offset = page * PAGE_LIMIT + url = (f"https://{instance}/api/v1/directory" + f"?language={language}&local=false&order={order}" + f"&limit={PAGE_LIMIT}&offset={offset}") + batch = api_get(url) + if not batch or not isinstance(batch, list): + break + new = 0 + for acc in batch: + aid = acc.get("id") + if aid and aid not in seen_ids: + seen_ids.add(aid) + acc["_source_instance"] = instance + acc["_language"] = language + accounts.append(acc) + new += 1 + log.debug(f" {instance} lang={language} offset={offset}: {new} nových") + if len(batch) < PAGE_LIMIT: + break + time.sleep(RATE_LIMIT_DELAY) + return accounts + +def fetch_all(): + seen_handles = set() + all_accounts = [] + for instance in QUERY_INSTANCES: + for lang in TARGET_LANGUAGES: + log.info(f"directory {instance} lang={lang} ...") + batch = fetch_directory(instance, lang) + added = 0 + for acc in batch: + handle = acc.get("acct", "") + if "@" not in handle: + handle = f"{handle}@{instance}" + if handle in seen_handles: + continue + seen_handles.add(handle) + acc["_handle"] = handle + all_accounts.append(acc) + added += 1 + log.info(f" → {added} nových (celkem {len(all_accounts)})") + time.sleep(RATE_LIMIT_DELAY) + log.info(f"Sběr hotov: {len(all_accounts)} unikátních účtů") + return all_accounts + +# ── FILTRY ──────────────────────────────────── +def passes_quality(acc): + if acc.get("suspended") or acc.get("limited"): + return False + if (acc.get("statuses_count") or 0) < MIN_STATUSES: return False + if (acc.get("followers_count") or 0) < MIN_FOLLOWERS: return False + last = acc.get("last_status_at") + if not last: + return False + try: + dt = datetime.fromisoformat(last.replace("Z", "+00:00")) + if dt < datetime.now(timezone.utc) - timedelta(days=MAX_DAYS_INACTIVE): + return False + except Exception: + pass + return True + +# ── SCORING ─────────────────────────────────── +def score(acc): + followers = acc.get("followers_count", 0) or 0 + statuses = acc.get("statuses_count", 0) or 0 + following = acc.get("following_count", 1) or 1 + f = min(40, int(40 * min(followers, 2000) / 2000)) + a = min(30, int(30 * min(statuses, 2000) / 2000)) + r = min(20, int(min(followers / max(following, 1), 4) * 5)) + handle = acc.get("_handle", "") + instance = handle.split("@")[-1] if "@" in handle else "" + b = 10 if any(x in instance for x in ("mamutovo", "czech")) else 0 + return min(100, f + a + r + b) + +# ── KATEGORIE ───────────────────────────────── +CATEGORIES = { + "tech": ["linux", "python", "programov", "software", "opensource", "developer", "sysadmin", "git"], + "foto": ["fotografi", "foto", "photograph", "objektiv", "kamera"], + "veda": ["věda", "fyzika", "biologi", "astronom", "výzkum", "science", "matematik"], + "kultura": ["knihy", "literatura", "film", "hudba", "divadlo", "umění"], + "gaming": ["gaming", "hry", "videohry", "steam", "gamer"], + "zpravy": ["novinář", "zprávy", "politik", "média", "journalist"], +} + +def categorize(acc): + text = re.sub(r"<[^>]+>", " ", acc.get("note", "") or "").lower() + text += " " + (acc.get("display_name", "") or "").lower() + for cat, kws in CATEGORIES.items(): + if any(kw in text for kw in kws): + return cat + return "ostatni" + +def extract_tags(acc): + text = re.sub(r"<[^>]+>", " ", acc.get("note", "") or "").lower() + found = [] + for kws in CATEGORIES.values(): + for kw in kws: + if kw in text and kw not in found and len(kw) > 3: + found.append(kw.strip()) + return found[:4] + +# ── VÝSTUP ──────────────────────────────────── +def build_output(raw): + results = [] + for acc in raw: + if not passes_quality(acc): + continue + handle = acc.get("_handle", acc.get("acct", "")) + bio = re.sub(r"<[^>]+>", " ", acc.get("note", "") or "").strip() + results.append({ + "name": acc.get("display_name") or acc.get("username", ""), + "handle": handle, + "bio": bio[:220], + "avatar": acc.get("avatar", ""), + "followers": acc.get("followers_count", 0), + "statuses": acc.get("statuses_count", 0), + "score": score(acc), + "tags": extract_tags(acc), + "category": categorize(acc), + "last_active": acc.get("last_status_at", ""), + "url": acc.get("url", ""), + "language": acc.get("_language", "cs"), + }) + seen = set() + unique = [] + for r in sorted(results, key=lambda x: x["score"], reverse=True): + if r["handle"] not in seen: + seen.add(r["handle"]) + unique.append(r) + return unique[:TOP_N] + +def write_json(accounts, output_dir): + data = {"generated_at": datetime.now(timezone.utc).isoformat(), "count": len(accounts), "accounts": accounts} + p = output_dir / "accounts.json" + p.write_text(json.dumps(data, ensure_ascii=False, indent=2)) + log.info(f"JSON: {p} ({len(accounts)} účtů)") + +def write_csv(accounts, output_dir): + p = output_dir / "accounts.csv" + with open(p, "w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(["Account address", "Show boosts"]) + for a in accounts: + w.writerow([a["handle"], "true"]) + log.info(f"CSV: {p}") + +# ── MAIN ────────────────────────────────────── +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--output", default=".", help="Výstupní adresář") + parser.add_argument("--top", default=TOP_N, type=int) + parser.add_argument("--debug", action="store_true") + args = parser.parse_args() + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + global TOP_N + TOP_N = args.top + output_dir = Path(args.output) + output_dir.mkdir(parents=True, exist_ok=True) + log.info(f"Startuji – {len(QUERY_INSTANCES)} instancí × {len(TARGET_LANGUAGES)} jazyků") + raw = fetch_all() + accounts = build_output(raw) + if not accounts: + log.error("Žádné účty! Zkontroluj připojení.") + return 1 + log.info(f"Po filtraci: {len(accounts)} účtů") + write_json(accounts, output_dir) + write_csv(accounts, output_dir) + log.info("Hotovo.") + return 0 + +if __name__ == "__main__": + exit(main()) diff --git a/start.html b/start.html new file mode 100644 index 0000000..9de07d2 --- /dev/null +++ b/start.html @@ -0,0 +1,380 @@ + + + + + +Začni na Mastodonu – Mamutovo + + + + +
+
🦣 Mamutovo · Průvodce pro nováčky
+

Twitter bez korporátu.
Mastodon za 3 minuty.

+

+ Žádné algoritmy. Žádné reklamy. Patří komunitě.
+ Tyhle 4 kroky ti zaplní feed a pomůžou udělat první post. +

+
+ +
+ + +
+
01
+
+

Stáhni starter pack

+

CSV soubor s účty, které stojí za sledování. Nahraješ ho do Mastodonu a feed se okamžitě zaplní.

+ +
+
+ + +
+
02
+
+

Nahraj CSV do Mastodonu

+

Přejdi do nastavení a importuj soubor. Trvá to 30 sekund.

+
+ Nastavení → Import a export → Import
+ Typ: Sledovaní → Vyber soubor → Nahrát +
+
+
+ + +
+
03
+
+

Pošli první post

+

Zkopíruj, uprav, odešli. Komunita reaguje na #Představení.

+ Klikni pro zkopírování +
+Ahoj Mastodon! 👋 Jsem tu nový/nová, přišel/přišla jsem z [Twitteru / Facebooku]. +Zajímá mě [Linux / příroda / fotografie / ...]. Rád/a se seznámím! + +#Představení #novácek #Mamutovo +
+ ✏️ Napsat post +
+
+ + +
+
04
+
+

Sleduj hashtagy, co tě zajímají

+

Klikni na hashtag → „Sledovat hashtag". Příspěvky se ti začnou objevovat v timeline.

+ +
+
+ +
+ + + +
Zkopírováno! Vlož do pole pro post ✓
+ + + + + diff --git a/starter-general.csv b/starter-general.csv new file mode 100644 index 0000000..15d3daa --- /dev/null +++ b/starter-general.csv @@ -0,0 +1,16 @@ +Account address,Show boosts +FediFollows@mastodon.online,true +Mastodon@mastodon.social,true +feditips@mstdn.social,true +fosstodon@fosstodon.org,true +climateaction@climatejustice.social,false +writingexchange@writing.exchange,true +photography@mastodon.social,true +gamedev@mastodon.gamedev.place,true +scicomm@scholar.social,false +openstreetmap@en.osm.town,false +techbots@botsin.space,false +privacyguides@mastodon.social,false +linuxrocks@linuxrocks.online,true +bookwyrm@bookwyrm.social,false +prague@mastodon.social,true