Commit Graph

82 Commits

Author SHA1 Message Date
prosolis
83b1216541 Drop LWN subscriber-only articles instead of stamping paywalled
LWN's subscriber-only articles have no public version reachable by
archive snapshots or bypass UAs, so stamping them as paywalled produces
clicks that always dead-end. Detect the marker text and skip ingestion
entirely.
2026-05-25 14:31:08 -07:00
prosolis
dd99f2c7ab Fix paywall false positive on Anime News Network
ANN wraps article bodies in <div class="KonaBody"> with no semantic
<article>/<main> tags, so body-length extraction fell below the 500-char
threshold and the poller flagged stories as gated. Broaden the container
fallback to also try itemprop="articleBody", common content-container
classes, and ANN's KonaBody.
2026-05-25 13:12:50 -07:00
prosolis
9e5ba5aafc Fix SSRF, XSS, dedup, force-post, and DB hot-path issues
- Add internal/safehttp: hardened HTTP client (DNS-resolved dial guard
  blocking loopback/RFC1918/CGNAT/link-local, redirect re-validation,
  body-size cap) and rewire article/feed/thumb clients through it
- Cap goquery body at 5 MiB so a hostile origin can't OOM the process
- search.js: reject non-http(s) hrefs to block stored XSS via javascript:
- dedup: tracking-param key "CMP" was unreachable (lookup lowercases);
  fixed to "cmp" so CMP= is actually stripped from canonical URLs
- ForcePost: postItem now returns bool; on dedup-skip ForcePost returns
  false so !post falls back to DB lookup instead of silently consuming
- Bound reaction callbacks behind an 8-slot semaphore; drop overflow
- Add stories indexes on (channel, classified, seen_at DESC),
  (classified, seen_at DESC), and partial image_url to kill full scans
  in IsKnownImageURL and ORDER BY seen_at hot paths
- Surface FTS5 probe Scan error instead of swallowing it
2026-05-25 12:23:37 -07:00
prosolis
e428f37c9e Add music sources to example config 2026-05-25 12:08:57 -07:00
prosolis
b8dcaa2aa1 Fix paywall false positives + add og:image fallbacks
- Body extractor falls back to <article>/<main> container text when
  <p> extraction is sparse, catching <br>-separated bodies (Phoronix).
- Detect Cloudflare bot-block / JS-challenge pages on 403/503 and
  treat them as transport failures rather than paywalls (Brooklyn Vegan).
- og:image extractor falls back to img.wp-post-image and the first
  content <img> in <article>/<main>, with lazy-load placeholder
  handling via data-src / data-lazy-src (Hardcore Gaming 101).
- New -backfill-paywall flag re-checks paywalled=1 rows with the
  current logic, clearing false positives and filling missing thumbs.
2026-05-25 12:06:29 -07:00
prosolis
71dc6d77ab Add anime and foss channel themes
Two new web-only channels alongside EU: anime (🌸 sakura pink #ec5e8a)
and foss (🐧 amber #d97706). Both get the full bg/text/decoration/
border/glow theme classes.
2026-05-25 11:48:33 -07:00
prosolis
ec1f130ed5 Add full-text search, EU channel theme, and web-only sources
Search: new FTS5-backed SearchStories query, /search JSON endpoint,
client-side overlay (search.js) wired into the layout header. EU
channel gets its own theme color (#003399) across bg/text/border/glow
classes. Sources can now route to non-Matrix channels without
validation error (web-only mode); a warning still flags typos.
2026-05-25 11:23:52 -07:00
prosolis
509a0fc7a7 Harden paywall detection + render paywalled stamp on cards
Bypass-UA retry (Googlebot + Google referer) for soft paywalls, JSON-LD
gating scoped to Article-typed nodes, HTTP 402 treated as explicit
paywall, Wayback freshness filter (30d cap), archive.today as secondary
archive fallback, and transport failures no longer trigger snapshot
swaps. When gating is detected and no archive workaround succeeds, the
story is stored with paywalled=1 and the web card renders a diagonal
red rubber-stamp overlay so readers know the link is gated.
2026-05-25 11:23:22 -07:00
prosolis
9bc8743b5e Handle AVIF source images in thumb pipeline
Decode AVIF via avifdec when Go's image.Decode can't, and pass-through
small AVIFs (<=800px wide) by caching the original bytes instead of
re-encoding.
2026-05-25 09:27:15 -07:00
prosolis
24103394ae Canvas-based seasonal weather effects + demo page
Server picks variant from Lisbon-local date (rain/petals/jacaranda/motes/leaves)
and renders behind content via a canvas particle layer. Each variant has a
hand-drawn silhouette so shapes are recognizable. /weather demo route exposes
variant + intensity + phase pickers, locking the time-of-day phase override.
2026-05-25 09:20:35 -07:00
prosolis
5ea64c1d32 Paywall signal detection, sqlite driver registration, /img route fix
- Detect explicit paywall markers (article:content_tier meta, JSON-LD
  isAccessibleForFree) so metered articles fall back to Wayback even
  when body length is above threshold
- Blank-import go.mau.fi/util/dbutil/litestream so the sqlite3-fk-wal
  driver mautrix's cryptohelper depends on is registered
- Fix /img route: Go ServeMux requires {wildcard} to be a whole segment,
  so capture {name} and strip the .avif suffix in the handler
2026-05-25 07:50:36 -07:00
prosolis
bddd15f7d1 Web UI: music channel, posted-to-Matrix glow, redesigned index, AVIF thumbnails
- Add music as a fourth channel (nav + theme color)
- Glowing themed border on cards that have been posted to Matrix
- Replace per-channel index sections with: "Pete just posted" strip,
  channel dashboard (last post, 24h count, totals), unified latest feed
- /img proxy: SSRF-guarded thumbnail re-encoder that resizes to 800px
  and runs avifenc -q 45, cached under data/img-cache
2026-05-24 23:07:17 -07:00
prosolis
0111a1b06d Local mode, Makefile, image fixes, Pete avatar
- Add -local flag: web/RSS-only mode that skips Matrix login and posting,
  so the web UI can be exercised against live feeds without credentials
- Add Makefile (build/local/seed/test/clean) that handles Tailwind +
  go build in one shot
- Fix Guardian thumbnails: NormalizeImageURL was rewriting width=1200
  onto signed i.guim.co.uk URLs, invalidating the s= signature and
  returning 401. Leave signed URLs alone and pick the widest
  media:content variant up front instead
- Use pete.avif as the header logo, favicon, and footer mark; drop the
  unused leaf.svg
2026-05-24 22:43:03 -07:00
prosolis
9d5db63c56 Switch config from YAML to TOML 2026-05-24 22:11:36 -07:00
prosolis
e88483526d Rip out Ollama: direct_route only, no LLM layer
Pete moves to a remote host without Ollama access. Every source must
declare a direct_route channel; the classifier, explainer, semantic
dedup, !explain summaries, feed_hint, and the recent_headlines /
classification_log tables are gone. Deterministic dedup (canonical URL,
headline_norm, per-channel cooldown) remains.
2026-05-24 22:07:13 -07:00
prosolis
fbd4b84eaf Add read-only web UI
Serves Pete's classified-story archive over HTTP alongside the Matrix
bot. Three sections (gaming/tech/politics), Animal-Crossing-vibe Tailwind
templates, day/night palette driven by the visitor's browser clock.
Web port configurable via web.listen_addr and ${PETE_WEB_PORT} in
docker-compose. Tailwind built in a node stage in the Dockerfile so
deployments don't need node at runtime.
2026-05-24 21:51:49 -07:00
prosolis
87906719fa Round-robin: rotate by channel instead of by source
Source-keyed rotation skewed toward whichever channel had the most
feeds (4 of 7 sources routed to politics, so politics dominated the
rotation). Channel-keyed rotation guarantees variety regardless of
feed counts.

Schema: round_robin_state.last_source -> last_channel, added via
addColumnIfMissing so existing DBs migrate in place.
2026-05-24 19:31:27 -07:00
prosolis
afe2ef996b Fix !post in round-robin mode, reaction VS16, image label
- !post falls back to newest unposted story for the channel when the
  in-memory queue is empty (the steady state under round-robin).
- Accept ️/️ (U+FE0F variation selector) as question reactions —
  the bare codepoints alone missed clients that render the colored emoji.
- Rewrite Guardian i.guim.co.uk thumbnails to width=1200 so we stop
  rejecting real images as "tracking pixels"; relabel the size warning.
- Log decrypt failures and reactions on events not in post_log so future
  silent drops surface instead of vanishing.
2026-05-24 09:14:37 -07:00
prosolis
23fffdda3c README: document !post on-demand command 2026-05-23 15:09:10 -07:00
prosolis
689dc4ef92 Add !post command to force-publish next queued story
Typed in a configured channel room, !post pops the head of that channel's
queue and sends immediately, bypassing min-interval, burst cap, and daily
cap. Canonical-URL dedup still applies. Empty queue gets a threaded reply.
2026-05-23 14:47:37 -07:00
prosolis
f29c056171 README: document round-robin posting mode 2026-05-22 19:59:29 -07:00
prosolis
3baec4c8bc Add round-robin posting mode
One story per interval_hours (default 4), cycling through enabled sources
in config order. Empty sources are skipped and the pointer advances to
whichever source actually posted. State persists across restarts.

Duplicate-flagged stories now get a _duplicate sentinel channel so they
stay out of the rotation pool alongside _discarded.
2026-05-22 19:58:46 -07:00
prosolis
69967b25c6 Plumb ctx through classifier and Ollama HTTP path
OllamaClient.Generate/GenerateText/call now take ctx and build the HTTP
request via NewRequestWithContext, so an in-flight LLM call is aborted
when the parent context is cancelled (Ctrl-C). Classifier.Classify and
its tier helpers take ctx too. ProcessFunc gets a ctx parameter so the
poller can forward its cancellable context down to classification.

Explainer.summarize manages its own 60s context since reaction-driven
flow has no parent ctx to inherit.
2026-05-22 18:55:43 -07:00
prosolis
c9318d7bb0 Hard daily cap, no-flood shutdown, ctx-aware poller, double-image fix
Four related fixes after Pete flooded a channel and ignored Ctrl-C:

1. Global daily cap (posting.daily_cap_total, default 5): hard ceiling on
   posts across ALL channels in a rolling 24h window. Checked before the
   per-channel min-interval and burst-cap.

2. Shutdown no longer flushes the queue. Previous drainAll posted every
   remaining item with rate limits disabled — which was literally the
   flood. Replaced with dropOnShutdown that clears queues and logs
   the count.

3. Poller respects ctx mid-loop. pollOnceWithErr now takes ctx and bails
   between items, so Ctrl-C doesn't have to wait for ~30s of network
   per pending story before shutdown can complete.

4. Double-image fix. PostStory now reports imageSent; the queue clears
   ImageURL before retry so a text-send failure after a successful
   image upload doesn't re-post the image.
2026-05-22 18:51:33 -07:00
prosolis
8d1e6ed568 Update README: !explain, paywall/Wayback fallback, explainer package 2026-05-22 18:38:37 -07:00
prosolis
d41eaa6504 Test bullet parser; fix marker-less passthrough
The previous parser treated any non-empty non-header line as a bullet,
so 'plain paragraph' answers were silently rewrapped as <ul><li>. Now
we only count a line if it had a recognized bullet marker (- * • ·);
if none did, fall back to raw + <pre> as originally intended.

Tests cover dash/asterisk/unicode bullets, blank-line tolerance,
Summary:/TL;DR header stripping, HTML escaping, single-bullet output,
the no-marker passthrough path, and the IsQuestionReaction set.
2026-05-22 18:31:26 -07:00
prosolis
dd324c0317 Accept more question-mark reactions as explain triggers
Now triggers on  ⁉️ 🤔 ? ? — covers the obvious red/white/thinking
variants, the exclamation-question combo (with and without VS16), and
plain ascii / fullwidth question marks for keyboard users.
2026-05-22 18:29:32 -07:00
prosolis
3537e073e9 !explain via reaction: thread-reply with a 3-bullet TL;DR
When a user reacts  on one of Pete's posts, fetch the article body,
ask Ollama for a 3-bullet summary, and post it as a threaded reply
rooted at the original story event. Per-process cooldown of 5min per
story keeps repeated reactions from re-summarizing.

- ingestion.FetchArticleBody: visible <p> text capped at 8000 chars
- classifier.OllamaClient.GenerateText: non-JSON variant
- storage.GetStoryByGUID: full row lookup
- matrix.PostThreadedReply: m.thread + m.in_reply_to fallback
- poster.SetReactionCallback: optional hook fired after recording
- New package: internal/explainer
2026-05-22 18:27:11 -07:00
prosolis
e26b69e43f Paywall detection: swap to Wayback snapshot when article body is gated
Fetch each new article once and measure visible <p> text. If body is below
500 chars (or fetch fails), resolve a Wayback snapshot via the
archive.org/wayback/available API and use that URL for both the og:image
fallback and the posted link. Dedup keys stay derived from the original
URL so paywalled/non-paywalled hits collide as before.

- New: internal/ingestion/article.go (FetchArticleMeta via goquery)
- New: internal/ingestion/wayback.go (ResolveWayback)
- Removed: internal/ingestion/og.go (folded into article.go)
- poller.go: dedup first, then one article fetch, then snapshot fallback
2026-05-22 17:53:54 -07:00
prosolis
ca6663c051 Update mautrix version reference in README 2026-05-22 17:27:40 -07:00
prosolis
310950e722 Bump indirect deps: x/crypto 0.52, x/net 0.55, x/sys 0.45 2026-05-22 17:27:00 -07:00
prosolis
652d6dfa38 Initial commit 2026-05-22 17:25:27 -07:00