Phase D backend: UNO is now a session like hold'em, not a single stake. You sit
with a buy-in stack, ante into a pot each hand, and leave with what's in front of
you. The engine lost its `You` constant and its measured multiples: ApplyMove
takes the acting seat, New takes a seat list, a Tier carries an ante instead of a
Base, and a hand settles by moving the pot to the winner (less rake, and never
when a bot takes it) rather than paying a multiple. A mercy kill puts a seat out
of the hand, not out of the game — the last one standing takes the pot.
The redaction moved to the web layer, where hold'em's already lives: the engine
now stamps every seat's hand onto its events, and viewUno/viewUnoEvents strip
everything that isn't the viewer's own. TestUnoViewNeverLeaksAnotherSeatsCards is
the wall. unoTable implements tableGame; /uno/{sit,move,leave,tables,stream,chat,
say} mirror hold'em, with stream/chat/say now shared game-agnostic handlers.
The frontend is not done: uno.js still calls the retired solo endpoint, so the
page renders but is not yet playable. All engine and web tests are green.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
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 ./...