diff --git a/astro.config.mjs b/astro.config.mjs index b58a652..39d10a0 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -34,12 +34,19 @@ export default defineConfig({ routing: { prefixDefaultLocale: false }, }, - // /stats merged into /community-size. Keep the old URLs working, for every locale this build - // actually has -- hardcoding /nl/stats would 404 on the .eu build, which is English-only. - redirects: Object.fromEntries([ - ['/stats', '/community-size'], - ...scope.locales.slice(1).map((l) => [`/${l}/stats`, `/${l}/community-size`]), - ]), + // Redirects: .eu renamed /countries to /stats; national scopes keep /stats → /community-size. + // Each scope builds separately, so the conditional keeps the right redirect per build. + redirects: Object.fromEntries( + scope.aggregate + ? [ + ['/countries', '/stats'], + ...scope.locales.slice(1).map((l) => [`/${l}/countries`, `/${l}/stats`]), + ] + : [ + ['/stats', '/community-size'], + ...scope.locales.slice(1).map((l) => [`/${l}/stats`, `/${l}/community-size`]), + ], + ), integrations: [ sitemap(), diff --git a/scripts/eu-infra-pds.py b/scripts/eu-infra-pds.py index ee60e69..5fd11ce 100644 --- a/scripts/eu-infra-pds.py +++ b/scripts/eu-infra-pds.py @@ -2,10 +2,10 @@ """Count accounts hosted on European AT Protocol infrastructure, merged into europe-stats.json as `europeanInfra`. -Being hosted on a European sovereign PDS (Eurosky and peers) is a strong signal that an account is -European, but it says nothing about WHICH country, so this is a separate top-level number and is -never folded into the per-country counts or the language estimate. It answers a different question: -how many people run on European infrastructure rather than Bluesky's US hosting. +Being hosted on a European sovereign PDS (Eurosky, w-social, and other EU-operated PDSes) is a strong +signal that an account is European, but it says nothing about WHICH country, so this is a separate +top-level number and is never folded into the per-country counts or the language estimate. It answers +a different question: how many people run on European infrastructure rather than Bluesky's US hosting. Given --placed (the confirmed, country-placed DIDs from eu-stats-aggregate.py --emit-placed) it also reports how much of that overlaps the per-country counts, so the site can publish a European @@ -14,6 +14,7 @@ total that adds up instead of counting a Dutch account on Eurosky twice. python3 scripts/eu-infra-pds.py --stats europe-stats.json --placed eu-placed.dids python3 scripts/eu-infra-pds.py --self-test """ + from __future__ import annotations import argparse @@ -23,10 +24,43 @@ import sys import time import urllib.request -# European-operated PDS hosts. Start with Eurosky; add peers here as they appear. `endpoint` is the -# PDS service URL that answers com.atproto.sync.listRepos. +# European-operated PDS hosts that expose com.atproto.sync.listRepos on the open network. A PDS +# being here means accounts on it are confirmed European — a strong signal, just not which country. +# Only include PDSes whose listRepos endpoint is publicly accessible; private/closed PDSes and bot +# networks (e.g. blacksky.app) are excluded. Unreachable PDSes are harmless here: the script's +# except-Exception swallows the failure so a down host never blocks the deploy. EUROPEAN_PDS = [ - {"host": "eurosky.social", "endpoint": "https://eurosky.social", "label": "Eurosky"}, + { + "host": "eurosky.social", + "endpoint": "https://eurosky.social", + "label": "Eurosky", + }, + { + "host": "pds.wsocial.network", + "endpoint": "https://pds.wsocial.network", + "label": "w-social", + }, + { + "host": "forum-hietzing.at", + "endpoint": "https://forum-hietzing.at", + "label": "Hietzing", + }, + { + "host": "pds.bowverse.fr", + "endpoint": "https://pds.bowverse.fr", + "label": "Bowverse", + }, + {"host": "pds.sved.be", "endpoint": "https://pds.sved.be", "label": "Sved"}, + { + "host": "pds.kelosocial.eu", + "endpoint": "https://pds.kelosocial.eu", + "label": "Kelosocial", + }, + { + "host": "pds.jdkserver.nl", + "endpoint": "https://pds.jdkserver.nl", + "label": "JDK", + }, ] @@ -63,7 +97,12 @@ def build(hosts=EUROPEAN_PDS, placed: set[str] | None = None) -> dict: for h in hosts: try: total, active, placed_active = count_repos(h["endpoint"], placed) - row = {"host": h["host"], "label": h["label"], "accounts": active, "reposTotal": total} + row = { + "host": h["host"], + "label": h["label"], + "accounts": active, + "reposTotal": total, + } if placed: row["placedInCountry"] = placed_active out.append(row) @@ -91,12 +130,14 @@ def read_dids(path: str) -> set[str]: def _self_test(): # Fake one page of listRepos so the de-duplication is tested, not just asserted. - page = {"repos": [ - {"did": "did:plc:a", "active": True}, # placed under a country already - {"did": "did:plc:b", "active": True}, # European infra only - {"did": "did:plc:c", "active": True}, # European infra only - {"did": "did:plc:d", "active": False}, # deactivated: not an account - ]} + page = { + "repos": [ + {"did": "did:plc:a", "active": True}, # placed under a country already + {"did": "did:plc:b", "active": True}, # European infra only + {"did": "did:plc:c", "active": True}, # European infra only + {"did": "did:plc:d", "active": False}, # deactivated: not an account + ] + } import io import contextlib @@ -109,13 +150,19 @@ def _self_test(): try: hosts = [{"host": "x", "label": "X", "endpoint": "https://x"}] plain = build(hosts) - assert plain == {"hosts": [{"host": "x", "label": "X", "accounts": 3, "reposTotal": 4}], - "total": 3}, plain - assert "countryless" not in plain, plain # no --placed: the site must fall back, not guess + assert plain == { + "hosts": [{"host": "x", "label": "X", "accounts": 3, "reposTotal": 4}], + "total": 3, + }, plain + assert "countryless" not in plain, ( + plain + ) # no --placed: the site must fall back, not guess deduped = build(hosts, placed={"did:plc:a", "did:plc:zz"}) assert deduped["total"] == 3, deduped - assert deduped["placedInCountry"] == 1, deduped # only a, and only because it is active - assert deduped["countryless"] == 2, deduped # b + c: the number safe to add + assert deduped["placedInCountry"] == 1, ( + deduped + ) # only a, and only because it is active + assert deduped["countryless"] == 2, deduped # b + c: the number safe to add finally: urllib.request.urlopen = real print("eu-infra self-test OK") @@ -125,9 +172,12 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("--stats", default="europe-stats.json") ap.add_argument("--out", default=None) - ap.add_argument("--placed", default=None, - help="eu-placed.dids from eu-stats-aggregate.py --emit-placed; without it " - "the overlap is unknown and the site keeps the two numbers separate") + ap.add_argument( + "--placed", + default=None, + help="eu-placed.dids from eu-stats-aggregate.py --emit-placed; without it " + "the overlap is unknown and the site keeps the two numbers separate", + ) ap.add_argument("--self-test", action="store_true") args = ap.parse_args() @@ -144,13 +194,20 @@ def main(): f.write(json.dumps(stats, indent=2) + "\n") os.replace(tmp, out) hosts = ", ".join(f"{h['label']}={h['accounts']:,}" for h in infra["hosts"]) - print(f"europeanInfra: {infra['total']:,} active on {len(infra['hosts'])} European PDS host(s) ({hosts})") + print( + f"europeanInfra: {infra['total']:,} active on {len(infra['hosts'])} European PDS host(s) ({hosts})" + ) if "countryless" in infra: - print(f" of those, {infra['placedInCountry']:,} are already counted under a country; " - f"{infra['countryless']:,} are European infrastructure only") + print( + f" of those, {infra['placedInCountry']:,} are already counted under a country; " + f"{infra['countryless']:,} are European infrastructure only" + ) else: - print(" no --placed given: overlap with the per-country counts unknown, " - "so the site will keep the numbers separate", file=sys.stderr) + print( + " no --placed given: overlap with the per-country counts unknown, " + "so the site will keep the numbers separate", + file=sys.stderr, + ) if __name__ == "__main__": diff --git a/scripts/nl-handle-class.py b/scripts/nl-handle-class.py index 5ee20f5..2b97020 100644 --- a/scripts/nl-handle-class.py +++ b/scripts/nl-handle-class.py @@ -40,6 +40,12 @@ PDS_SUFFIXES = ( ".bsky.team", ".blacksky.community", ".pds.witchcraft.systems", + ".wsocial.eu", + ".kelosocial.eu", + ".pds.bowverse.fr", + ".pds.sved.be", + ".pds.jdkserver.nl", + ".forum-hietzing.at", ) # Bridges: the handle describes an account on another network. @@ -129,7 +135,9 @@ def self_test() -> int: assert is_dutch_domain("smb.phys.tue.nl") assert is_dutch_domain("gemeente.amsterdam") assert not is_dutch_domain("someone.bsky.social") - assert not is_dutch_domain("mastodon.nl.ap.brid.gy"), "bridge domain is not the holder's" + assert not is_dutch_domain("mastodon.nl.ap.brid.gy"), ( + "bridge domain is not the holder's" + ) assert not is_dutch_domain("example.com") print("self-test ok") return 0 diff --git a/src/components/pages/EuropeFacts.astro b/src/components/pages/EuropeFacts.astro index 6ed8372..55ae5bd 100644 --- a/src/components/pages/EuropeFacts.astro +++ b/src/components/pages/EuropeFacts.astro @@ -18,10 +18,18 @@ const lexDetail = europeFacts.lexicons.byNsid.slice(0, 30); const beyondPct = confirmedTotal ? Math.round((europeFacts.lexicons.accountsUsingSomething / confirmedTotal) * 100) : 0; -// Eurosky is shown in full as the European-infrastructure stat, so drop it from this "other hosts" -// list — otherwise the page shows two different Eurosky numbers (the full count vs the small -// country-attributed subset that happens to fall in this scanned set). -const otherHosts = europeFacts.pds.topOtherHosts.filter((h) => h.host !== 'eurosky.social'); +// European PDSes are counted in full in the European-infrastructure stat, so drop them from this +// "other hosts" list — otherwise the page shows two different numbers for the same hosts. +const EU_PDS_HOSTS = new Set([ + 'eurosky.social', + 'pds.wsocial.network', + 'forum-hietzing.at', + 'pds.bowverse.fr', + 'pds.sved.be', + 'pds.kelosocial.eu', + 'pds.jdkserver.nl', +]); +const otherHosts = europeFacts.pds.topOtherHosts.filter((h) => !EU_PDS_HOSTS.has(h.host)); const pdsTotal = europeFacts.pds.blueskyHosted + europeFacts.pds.elsewhere; const elsewherePct = pdsTotal ? Math.round((europeFacts.pds.elsewhere / pdsTotal) * 1000) / 10 : 0; const bskyPct = pdsTotal ? Math.round((europeFacts.pds.blueskyHosted / pdsTotal) * 1000) / 10 : 0; @@ -69,7 +77,7 @@ const bskyPct = pdsTotal ? Math.round((europeFacts.pds.blueskyHosted / pdsTotal) Bluesky hosted {bskyPct}% elsewhere {elsewherePct}% -

{nf(europeFacts.pds.elsewhere)} accounts ({elsewherePct}%) live on a PDS outside Bluesky's own hosting. Eurosky is counted in full in the European-infrastructure figure above, so it is left out of this list to avoid showing two different numbers for it.

+

{nf(europeFacts.pds.elsewhere)} accounts ({elsewherePct}%) live on a PDS outside Bluesky's own hosting. European PDSes are counted in full in the European-infrastructure figure above, so they are left out of this list to avoid showing two different numbers.

{otherHosts.length > 0 && (