A multi-session build turning Pete's read-only web UI into something people return to. Five phases, signed-in features keyed off the OIDC subject; anonymous visitors keep the reverse-chron feed and localStorage-only state. Phase 1 — per-user read + bookmark state: user_story_state table + storage/userstate.go; auth-gated /api/read, /api/bookmark, /api/state and a /bookmarks page; reader.js syncs state server-side for signed-in users. Also hides the Matrix-posting UI when posting.enabled=false (web-only mode). Phase 2 — outbound feeds: storage.ListForFeed + web/feed.go hand-build RSS 2.0 (content:encoded) and JSON Feed 1.1 (no new dep); /feed.xml, /feed.json and per-channel variants; <link rel=alternate> discovery tags. Phase 3 — "For you" + related: storage/rank.go scores recent unread candidates by channel/source affinity + recency decay; RelatedStories via FTS5. ForYou rail + /for-you page; public /api/related feeds the reader's "You might also like". Phase 4 — source-health dashboard: source_health table + storage/sourcehealth.go (RecordPollResult, ListSourceHealth, SourceContentStats), written by the poller; admin-gated /status page behind web.admin_subs. Phase 5 — PWA + offline reader + Web Push: root-scoped manifest.webmanifest and sw.js (app-shell precache, /api/article runtime cache for offline reading, offline fallback, push/notificationclick handlers); PNG icons from pete.avif; pwa.js registers the SW and drives a notifications toggle. Web Push adds webpush-go, a [web.push] config block (pete -genvapid mints VAPID keys), a push_subscriptions table, auth-gated subscribe/unsubscribe endpoints, and a digest sender that pings each subscriber "N new stories" past their watermark, honoring disabled-sources and pruning gone endpoints. Tests added beside each new storage/web file; go test ./... and go vet clean.
256 lines
7.3 KiB
TOML
256 lines
7.3 KiB
TOML
[matrix]
|
|
homeserver = "https://matrix.example.org"
|
|
user_id = "@pete:matrix.example.org"
|
|
password = "${PETE_PASSWORD}"
|
|
pickle_key = "${PETE_PICKLE_KEY}"
|
|
display_name = "Pete"
|
|
data_dir = "./data"
|
|
admin_room = "!adminroomid:matrix.example.org"
|
|
# Matrix user IDs allowed to run in-room commands like !post. Empty disables
|
|
# commands entirely (channel rooms may be public, so empty != "anyone").
|
|
admins = ["@you:matrix.example.org"]
|
|
|
|
[matrix.channels]
|
|
tech = "!techroomid:matrix.example.org"
|
|
politics = "!politicsroomid:matrix.example.org"
|
|
gaming = "!gamingroomid:matrix.example.org"
|
|
lego = "!legoroomid:matrix.example.org"
|
|
|
|
[posting]
|
|
enabled = true # master switch for auto-posting news to Matrix; false = web-only (commands still work)
|
|
min_interval_seconds = 300
|
|
burst_cap_count = 3
|
|
burst_cap_window_seconds = 1800
|
|
daily_cap_total = 5 # hard global cap across ALL channels (rolling 24h); 0 disables
|
|
|
|
[posting.round_robin]
|
|
enabled = false # when true, replaces immediate posting with paced rotation
|
|
interval_hours = 4 # one story per N hours, cycling through channels in sorted order
|
|
|
|
[storage]
|
|
db_path = "./data/pete.db"
|
|
recent_window_hours = 24
|
|
|
|
[web]
|
|
enabled = true
|
|
listen_addr = ":8080"
|
|
site_title = "Pete"
|
|
base_url = "https://news.parodia.dev"
|
|
|
|
# OIDC subjects allowed to view the owner-facing source-health dashboard at
|
|
# /status (per-feed poll status, failures, content stats). Requires web.auth
|
|
# below. Empty = /status returns 404 for everyone. Find a user's subject in the
|
|
# server logs ("auth: user signed in" sub=...) after they sign in once.
|
|
admin_subs = []
|
|
|
|
# Optional OIDC sign-in (Authentik). When enabled, signed-in users get their
|
|
# preferences (hidden feeds, weather location, toggles) stored server-side keyed
|
|
# by their OIDC subject and synced across devices. Anonymous visitors keep using
|
|
# browser localStorage — the site stays public. If the provider is unreachable
|
|
# at startup, Pete logs a warning and serves anonymously rather than refusing to
|
|
# boot. Create an OAuth2/OIDC provider + application in Authentik, set the
|
|
# redirect URI to <base_url>/auth/callback, then fill these in.
|
|
[web.auth]
|
|
enabled = false
|
|
issuer = "https://authentik.parodia.dev/application/o/pete/"
|
|
client_id = "${PETE_OIDC_CLIENT_ID}"
|
|
client_secret = "${PETE_OIDC_CLIENT_SECRET}"
|
|
redirect_url = "https://news.parodia.dev/auth/callback"
|
|
# HMAC key that signs the session cookie. Generate with: openssl rand -hex 32
|
|
session_secret = "${PETE_SESSION_SECRET}"
|
|
|
|
# Optional Web Push digests. When enabled, signed-in users can opt in (from the
|
|
# feed-settings panel) to a periodic "N new stories" notification, delivered via
|
|
# a service worker so it also works when the site is installed as a PWA. Push is
|
|
# signed-in only, so it needs web.auth above; it does nothing otherwise.
|
|
# Generate the VAPID keypair once with: pete -genvapid
|
|
# then paste the two keys here (the private key is a secret — treat it like a
|
|
# password). subject identifies you to the push service (a mailto: or https: URL).
|
|
[web.push]
|
|
enabled = false
|
|
vapid_public_key = "${PETE_VAPID_PUBLIC_KEY}"
|
|
vapid_private_key = "${PETE_VAPID_PRIVATE_KEY}"
|
|
subject = "mailto:admin@parodia.dev"
|
|
# How often the sender wakes to look for new stories per subscriber (minutes).
|
|
interval_minutes = 360
|
|
# Smallest number of new (non-hidden) stories that triggers a digest, so a lone
|
|
# item doesn't ping everyone.
|
|
min_stories = 3
|
|
|
|
# Every enabled source MUST set direct_route to a key from [matrix.channels] above.
|
|
# There is no automatic classification — Pete posts each story to its configured channel.
|
|
# Optional: language = "en" drops feed items whose per-item <language> tag is
|
|
# set and doesn't match (prefix). Useful for multilingual feeds like Politico
|
|
# Europe that publish the same story in en / fr / de side-by-side. Items with
|
|
# no language tag pass through unchanged.
|
|
[[sources]]
|
|
name = "The Guardian — World"
|
|
feed_url = "https://www.theguardian.com/world/rss"
|
|
tier = 1
|
|
poll_interval_minutes = 20
|
|
direct_route = "politics"
|
|
enabled = true
|
|
# Optional: override the User-Agent for this feed only. Leave unset to use
|
|
# Pete's honest bot UA; set a browser string for feeds whose WAF blocks bots.
|
|
# user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
|
|
|
|
[[sources]]
|
|
name = "The Guardian — Politics"
|
|
feed_url = "https://www.theguardian.com/politics/rss"
|
|
tier = 1
|
|
poll_interval_minutes = 20
|
|
direct_route = "politics"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "The Guardian — US News"
|
|
feed_url = "https://www.theguardian.com/us-news/rss"
|
|
tier = 1
|
|
poll_interval_minutes = 20
|
|
direct_route = "politics"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Ars Technica"
|
|
feed_url = "https://feeds.arstechnica.com/arstechnica/index"
|
|
tier = 1
|
|
poll_interval_minutes = 20
|
|
direct_route = "tech"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Time Extension"
|
|
feed_url = "https://www.timeextension.com/feeds/news"
|
|
tier = 1
|
|
poll_interval_minutes = 20
|
|
direct_route = "gaming"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Pitchfork"
|
|
feed_url = "https://pitchfork.com/feed/feed-news/rss"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "music"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "The Quietus"
|
|
feed_url = "https://thequietus.com/feed"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "music"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Consequence"
|
|
feed_url = "https://consequence.net/feed/"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "music"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Brooklyn Vegan"
|
|
feed_url = "https://www.brooklynvegan.com/feed/"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "music"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "NME — Music"
|
|
feed_url = "https://www.nme.com/news/music/feed"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "music"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "BBC Newsround"
|
|
feed_url = "http://feeds.bbci.co.uk/newsround/rss.xml"
|
|
tier = 1
|
|
poll_interval_minutes = 30
|
|
direct_route = "kids"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "DOGO News"
|
|
feed_url = "https://www.dogonews.com/articles.rss"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "kids"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Science News Explores"
|
|
feed_url = "https://www.snexplores.org/feed"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "kids"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "NASA for Students"
|
|
feed_url = "https://www.nasa.gov/rss/dyn/educationnews.rss"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "kids"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Post-Punk.com"
|
|
feed_url = "https://post-punk.com/feed/"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "music"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Naked Capitalism"
|
|
feed_url = "https://www.nakedcapitalism.com/feed"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "finance"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Wolf Street"
|
|
feed_url = "https://wolfstreet.com/feed/"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "finance"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "MarketBeat"
|
|
feed_url = "https://www.marketbeat.com/feed/"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "finance"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Seeking Alpha"
|
|
feed_url = "https://seekingalpha.com/feed.xml"
|
|
tier = 2
|
|
poll_interval_minutes = 60
|
|
direct_route = "finance"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "Brickset"
|
|
feed_url = "https://brickset.com/feed"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "lego"
|
|
enabled = true
|
|
|
|
[[sources]]
|
|
name = "The Brick Fan"
|
|
feed_url = "https://www.thebrickfan.com/feed/"
|
|
tier = 1
|
|
poll_interval_minutes = 60
|
|
direct_route = "lego"
|
|
enabled = true
|