gogobee already sends boss/opponent/zone/outcome/level on every dispatch. renderAdventure melted them into prose and only the prose was persisted, so "how many bosses has she downed" was answerable only by parsing English back out of a headline. adventure_events keeps the fact as fact. It's the one adventure table that's a log rather than a snapshot: the roster answers where Josie is now and is replaced every tick; this answers what she has ever done, which no snapshot can. INSERT OR IGNORE on the guid because gogobee retries a fact whose ack it lost, and this is the only adventure store where a duplicate is permanently wrong — the roster forgives one by replacing itself, a double-counted kill is in the tally forever. On top of it the who page grows two public sections, counted from dispatches that were already public: the record (tallies, per-boss and per-zone, realm firsts, milestones) and the trail (the last 40 facts, each linking to the dispatch that told it). One read feeds both — the pool is MaxOpenConns(1), so six COUNT queries would serialize for an answer that fits in memory. Only the trail is capped. A limit on the read would truncate a *tally* rather than a list, and a veteran's kill count frozen at 40 reads as a fact instead of a missing page. Only the subject of a fact earns a trophy: a duel you lost still names you, and it belongs on your trail but not in your record. Trophies count forward only. Every adventurer's past is prose in the feed and can't be counted back out, so they show no record until they next do something — a clean absence rather than a wall of zeroes. No treasures: there's no loot fact on the wire, and inventory is current-state with no history, so counting the vault would score a bought sword as a trophy. Needs a fact type upstream.
Pete
A Matrix news bot that ingests RSS feeds from curated sources and routes each story to a configured channel.
Features
- RSS ingestion from configurable sources (Guardian, Ars Technica, Time Extension, …) with per-source polling intervals
- Direct routing — every source declares a
direct_routechannel; Pete posts there. No LLM, no classification step - Deduplication — GUID exact match, canonical-URL match, normalized-headline match, and per-channel canonical-URL cooldown
- Metered release — per-channel post queues with minimum interval (5min) and burst cap (3/30min)
- Round-robin mode — opt-in pacing: one story per N hours (default 4), cycling through channels in sorted order, skip-and-advance over empty channels, state persists across restarts
- Reaction tracking — records emoji reactions on posts
!poston demand — type!postin any configured channel room and Pete force-publishes the next queued story for that channel, bypassing min-interval, burst cap, and daily cap (canonical-URL dedup still applies); replies in-thread if the queue is empty- Paywall detection — if an article's visible body text is below threshold, Pete swaps in a Wayback Machine snapshot URL for both the lead image and the posted link
- FTS5 search — full-text search across headlines and ledes
- Image validation — HEAD-based checks filter tracking pixels, uploads valid images via MXC
- Video thumbnails — extracts a frame via
ffmpegfor video sources; also falls back toffmpegwhen Go's stdlib JPEG decoder rejects a source image - Seasonal weather — canvas-based ambient overlay (snow, leaves, rain, etc.) driven by a Portugal/Lisbon calendar; visitors can toggle it with the ⭐ star button in the nav
- Web UI — read-only browser interface (Tailwind, Animal-Crossing-ish vibe, day/night palette that follows the visitor's clock) at
/plus a section per channel. Designed for anews.parodia.dev-style deployment
Channels
| Channel | Purpose |
|---|---|
gaming |
Gaming news, releases, platform announcements |
tech |
Technology news, product/industry stories |
politics |
Political, policy, current events |
eu |
Portugal and the wider European beat (web-only — does not post to Matrix) |
music |
Records, scenes, artists |
anime |
Series, studios, manga |
foss |
Kernel, distros, free/open source |
kids |
World news written for younger readers (web-only) |
Requirements
- Go 1.25+
- A Matrix account for Pete with access to target rooms
- SQLite (bundled via pure Go driver, no CGo)
ffmpegonPATH— used for video-frame thumbnails and as a fallback decoder for JPEGs the Go stdlib rejects- No CGo dependencies — E2EE uses pure Go crypto (goolm) via mautrix v0.28
Setup
# Clone and build
git clone <repo-url> && cd pete
go build -tags goolm .
# Create config from example
cp config.example.toml config.toml
# Edit config.toml — fill in Matrix credentials, room IDs, and a direct_route for every source
# First run: seed current feed items as seen (prevents flood)
./pete -config config.toml -seed
# Post one story to verify the pipeline
./pete -config config.toml -test
# Run
./pete -config config.toml
Configuration
See config.example.toml for the full structure. Key sections:
matrix— homeserver, credentials, channel room IDs, optional admin roomposting— rate limiting (min interval, burst cap, daily cap), optionalround_robinblockstorage— database path, retention windowssources— RSS feeds with tier, polling interval, and requireddirect_route(must match a key inmatrix.channels). Optionallanguage = "en"drops items whose per-item<language>tag doesn't prefix-match — handy for multilingual feeds like Politico Europeweb— read-only HTTP UI (enabled toggle, listen address, site title, public base URL)
Environment variables can be referenced with ${VAR} syntax in the TOML.
Web UI
Set web.enabled: true (default port :8080) to expose Pete's classified-story archive over HTTP. A landing page at / plus one section per channel (/gaming, /tech, /politics, /eu, /music, /anime, /foss, /kids), all pulling from the stories table. A /weather page demos the seasonal canvas overlay in isolation. Adding a section is two lines in internal/web/server.go (the channels slice) plus a matching theme color in internal/web/static/css/input.css.
The frontend uses a small Tailwind build:
npm install
npm run build:css # produces internal/web/static/css/output.css
# or for live editing:
npm run watch:css
The Docker image runs the build automatically as a first stage, so deployments don't need Node.
Front the server with a reverse proxy (Caddy, nginx, Traefik) terminating TLS for e.g. news.parodia.dev → 127.0.0.1:8080. The day/night palette is driven by the visitor's browser clock — no server-side timezone needed.
Round-robin mode
Set posting.round_robin.enabled: true to switch Pete from "post immediately" to a paced rotation. On each tick (interval_hours, default 4) Pete picks the newest unposted story routed to the next channel in sorted order, posting through the existing queue. Empty channels are skipped; the rotation pointer advances to whichever channel actually posted, and last_channel / last_tick_at are persisted so restarts don't reset the cycle. Rotating by channel (not by source) guarantees variety even when one channel has many more feeds than the others.
With a 4-hour cadence Pete will post at most 6 stories/day, so set posting.daily_cap_total to 0 (or ≥6) when enabling this — otherwise the daily cap can silently swallow a tick.
Flags
| Flag | Description |
|---|---|
-config <path> |
Path to config file (default: config.toml) |
-seed |
Ingest all current feed items as seen without posting, then exit |
-test |
Post one story to verify the full pipeline, then exit |
-local |
Web/RSS-only mode: poll feeds and serve the web UI on web.listen_addr. Skips Matrix login and posting — useful for local testing |
Docker
docker compose up -d
Set PETE_PASSWORD in your environment or a .env file for the Matrix password.
Architecture
RSS Feed → Poller → GUID/Canonical/Headline Dedup → Article Fetch → Store → Route by direct_route → Image Validate → Queue → Matrix
↓
Paywall? → Wayback snapshot
Packages
| Package | Purpose |
|---|---|
internal/config |
TOML config loading with ${ENV} expansion |
internal/storage |
SQLite with WAL, FTS5, all queries |
internal/ingestion |
Per-source RSS polling, feed parsing, image validation, paywall detection |
internal/dedup |
Canonical URL + headline normalization helpers |
internal/matrix |
Password auth with device persistence, posting, threaded replies, reaction + message listener |
internal/poster |
Per-channel metered release queue, reaction tracking |
internal/scheduler |
Round-robin posting scheduler: paced rotation across channels when enabled |
internal/web |
Read-only HTTP UI: Tailwind templates, day/night palette, per-channel feed pages |
Post Format
[lead image if valid]
**Headline** → article_url
Lede text from feed
`source name`
Testing
go test ./...