prosolis 7051e8ffff adventure: give every dispatch a card that looks like what it is
Every dispatch rendered the same image: one violet gradient, a swapped
emoji, a label. A death, a first-ever clear and a legendary hoard were
visually identical — and this image is not decoration. It is the
og:image on every link Pete puts in Matrix and the thumbnail on every
feed card. The most interesting thing that has ever happened in the
realm looked exactly like the most routine.

The card is now keyed on the dispatch GUID instead of the event type,
so the renderer can read the fact behind it and put the actual nouns on
it — the boss's name, the zone, the item, the level. Each family gets
its own palette, and treasure is tinted by the rarity gogobee already
computes and currently spends on an adjective in a sentence.

A realm-first gets visible ceremony. The priority/bulletin split is
already computed upstream and until now it only decided whether Matrix
got pinged; a thing nobody has ever done should also look different
from the ninth time somebody did it. It earns a ribbon on the card and
a badge and a heavier ring in the feed.

Siege cards carry the HP bar, which is the W1 deferral landing. The
siege fact has the boss and the defender count but never the HP, so the
bar comes from the war-room snapshot: the live row while that boss is
still camped, the history once it has closed. When neither has it yet —
the real two-minute window between a win being filed and the push that
explains it — the card renders barless rather than wrong, and caches
for five minutes instead of a day so the unfurl isn't pinned that way.

Nothing needs backfilling. A story with a type-keyed image_url still
serves, and a dispatch with no stored fact degrades to the old emblem.

Two things learned by looking at the rendered output rather than at the
tests, both of which unit tests would never have caught: the feed
thumbnail is a 16/10 crop of a 1200x630 card and was eating the chip
("EGENDARY"), hence advSafeX; and an empty bar under a victory headline
reads at a glance as a wipe, hence the event-aware caption.

Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
2026-07-24 15:47:44 -07:00
2026-05-24 22:11:36 -07:00
2026-05-22 17:25:27 -07:00
2026-05-24 21:51:49 -07:00
2026-05-24 21:51:49 -07:00
2026-05-24 21:51:49 -07:00

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_route channel; 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
  • !post on demand — type !post in 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 ffmpeg for video sources; also falls back to ffmpeg when 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 a news.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)
  • ffmpeg on PATH — 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 room
  • posting — rate limiting (min interval, burst cap, daily cap), optional round_robin block
  • storage — database path, retention windows
  • sources — RSS feeds with tier, polling interval, and required direct_route (must match a key in matrix.channels). Optional language = "en" drops items whose per-item <language> tag doesn't prefix-match — handy for multilingual feeds like Politico Europe
  • web — 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 ./...
S
Description
A Matrix news bot that ingests RSS feeds from curated sources, classifies stories using a local LLM, deduplicates semantically, and routes posts to the appropriate channel.
Readme
7.7 MiB
Languages
Go 69.1%
JavaScript 16.1%
HTML 9.9%
CSS 4.8%