- A room opened with Play now never loads /join: the API's answer to POST /api/rooms is counted as rooms opened here. - Link previews (Slack, Discord, Facebook, iMessage, WhatsApp and the rest) are reported as where the link was shared, since the people who click them arrive with no referrer. WhatsApp's fetcher is a bot now. - An address that asks for a scanner's paths, or keeps coming back without opening a game, is a bot's all day; so is a browser that loads pages at a steady interval from a new address each time, never playing. A declared bot is still judged request by request. - The site's own addresses, sslip.io included, are not referrers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
82 lines
4.3 KiB
Bash
Executable File
82 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# The visitors digest: who is at the table right now and who came today,
|
|
# the rooms opened and how far each got, with today's traffic so far.
|
|
# Runs ON the droplet: hnefatafl-visitors.sh [YYYY-MM-DD] (default: today, UTC)
|
|
# Every game, bot games included, is a room with a ledger, so all show in full.
|
|
set -u
|
|
DAY="${1:-$(date -u +%F)}"
|
|
python3 - "$DAY" <<'PY'
|
|
import datetime, glob, json, subprocess, sys
|
|
sys.path.insert(0, "/usr/local/lib/hnefatafl")
|
|
import traffic
|
|
|
|
day = sys.argv[1]
|
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
print("as of %s UTC, day %s" % (now.strftime("%H:%M"), day))
|
|
|
|
def sh(cmd):
|
|
try:
|
|
return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=20).stdout.strip()
|
|
except Exception:
|
|
return "?"
|
|
|
|
print("live sockets: %s | rooms touched in the last hour: %s" % (
|
|
sh("ss -tn state established '( sport = :8789 )' | tail -n +2 | wc -l"),
|
|
sh("find /var/lib/hnefatafl/rooms -name '*.jsonl' -mmin -60 2>/dev/null | wc -l")))
|
|
|
|
# --- traffic: Caddy's access log, people and bots apart ---
|
|
t = traffic.scan(day)
|
|
print("traffic: %d addresses of people, %d page requests (%d bot addresses, of them %d scanners, %d monitors); hall %d, rules %d, guide %d; rooms opened here %d, room links %s; shared on %s; referrers %s; campaigns %s" % (
|
|
len(t["addresses"]), t["human"], len(t["botAddresses"]), t["scanners"], t["monitors"], t["pages"]["hall"], t["pages"]["rules"], t["pages"]["guide"],
|
|
t["opened"], dict(t["joins"]) or "none", dict(t["shared"]) or "none", dict(t["referrers"]) or "none", dict(t["campaigns"]) or "none"))
|
|
|
|
# --- rooms: every ledger, today's in detail ---
|
|
first_seen = {}
|
|
rooms = []
|
|
for f in glob.glob("/var/lib/hnefatafl/rooms/*.jsonl"):
|
|
try:
|
|
lines = [json.loads(l) for l in open(f) if l.strip()]
|
|
meta = lines[0]
|
|
assert meta["t"] == "room"
|
|
except Exception:
|
|
continue
|
|
created = datetime.datetime.fromtimestamp(meta["createdAt"] / 1000, datetime.timezone.utc)
|
|
humans, bots_seated, started, turns, last, over, talk = [], [], False, 0, created, None, 0
|
|
names = {}
|
|
for x in lines[1:]:
|
|
if x["t"] == "seat":
|
|
(bots_seated if x["bot"] else humans).append(x["name"])
|
|
names[x["id"]] = x["name"]
|
|
elif x["t"] == "unseat":
|
|
if x["id"] in names: bots_seated.remove(names.pop(x["id"]))
|
|
elif x["t"] == "start":
|
|
started = True
|
|
elif x["t"] == "turn":
|
|
turns += 1
|
|
last = datetime.datetime.fromtimestamp(x["at"] / 1000, datetime.timezone.utc)
|
|
elif x["t"] == "chat":
|
|
talk += 1
|
|
elif x["t"] == "over":
|
|
over = "a draw" if x["winner"] is None else "%s won" % names.get(x["winner"], x["winner"])
|
|
for h in humans:
|
|
if h not in first_seen or created < first_seen[h]:
|
|
first_seen[h] = created
|
|
rooms.append(dict(id=meta["id"], size=meta["seats"], created=created, humans=humans, bots=bots_seated, started=started, turns=turns, last=last, over=over, talk=talk))
|
|
|
|
today = sorted([r for r in rooms if r["created"].strftime("%F") == day], key=lambda r: r["created"])
|
|
names = sorted({h for r in today for h in r["humans"]})
|
|
new = [n for n in names if first_seen[n].strftime("%F") == day]
|
|
print("players today: %s | NEW today: %s" % (", ".join(names) or "none", ", ".join(new) or "none"))
|
|
print("rooms today: %d (of %d ever)" % (len(today), len(rooms)))
|
|
for r in today:
|
|
idle_h = (now - r["last"]).total_seconds() / 3600
|
|
state = "lobby only" if not r["started"] else ("started, no turns" if not r["turns"] else (r["over"] or ("abandoned" if idle_h > 12 else "in play")))
|
|
span = ("%d min" % round((r["last"] - r["created"]).total_seconds() / 60)) if r["turns"] else ""
|
|
seated = "+".join(r["humans"]) + ((" vs %d bot%s" % (len(r["bots"]), "" if len(r["bots"]) == 1 else "s")) if r["bots"] else "")
|
|
empty = r["size"] - len(r["humans"]) - len(r["bots"])
|
|
print(" %s %s %s%s %s %d turns %s last turn %s%s" % (
|
|
r["created"].strftime("%H:%M"), r["id"], seated, (" (%d seat%s empty)" % (empty, "" if empty == 1 else "s")) if empty else "",
|
|
state, r["turns"], span, r["last"].strftime("%H:%M") if r["turns"] else "never",
|
|
(" %d line%s of talk" % (r["talk"], "" if r["talk"] == 1 else "s")) if r["talk"] else ""))
|
|
PY
|