Switch config from YAML to TOML

This commit is contained in:
prosolis
2026-05-24 22:11:36 -07:00
parent e88483526d
commit 9d5db63c56
11 changed files with 280 additions and 253 deletions

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@ data/
.env .env
pete pete
config.yaml config.yaml
config.toml
node_modules/ node_modules/

View File

@@ -20,4 +20,4 @@ WORKDIR /app
COPY --from=builder /app/pete . COPY --from=builder /app/pete .
VOLUME /app/data VOLUME /app/data
EXPOSE 8080 EXPOSE 8080
CMD ["./pete", "-config", "/app/config.yaml"] CMD ["./pete", "-config", "/app/config.toml"]

View File

@@ -39,17 +39,17 @@ git clone <repo-url> && cd pete
go build -tags goolm . go build -tags goolm .
# Create config from example # Create config from example
cp config.example.yaml config.yaml cp config.example.yaml config.toml
# Edit config.yaml — fill in Matrix credentials, room IDs, and a direct_route for every source # 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) # First run: seed current feed items as seen (prevents flood)
./pete -config config.yaml -seed ./pete -config config.toml -seed
# Post one story to verify the pipeline # Post one story to verify the pipeline
./pete -config config.yaml -test ./pete -config config.toml -test
# Run # Run
./pete -config config.yaml ./pete -config config.toml
``` ```
## Configuration ## Configuration
@@ -62,7 +62,7 @@ See `config.example.yaml` for the full structure. Key sections:
- `sources` — RSS feeds with tier, polling interval, and **required** `direct_route` (must match a key in `matrix.channels`) - `sources` — RSS feeds with tier, polling interval, and **required** `direct_route` (must match a key in `matrix.channels`)
- `web` — read-only HTTP UI (enabled toggle, listen address, site title, public base URL) - `web` — read-only HTTP UI (enabled toggle, listen address, site title, public base URL)
Environment variables can be referenced with `${VAR}` syntax in the YAML. Environment variables can be referenced with `${VAR}` syntax in the TOML.
### Web UI ### Web UI
@@ -91,7 +91,7 @@ With a 4-hour cadence Pete will post at most 6 stories/day, so set `posting.dail
| Flag | Description | | Flag | Description |
|---|---| |---|---|
| `-config <path>` | Path to config file (default: `config.yaml`) | | `-config <path>` | Path to config file (default: `config.toml`) |
| `-seed` | Ingest all current feed items as seen without posting, then exit | | `-seed` | Ingest all current feed items as seen without posting, then exit |
| `-test` | Post one story to verify the full pipeline, then exit | | `-test` | Post one story to verify the full pipeline, then exit |

75
config.example.toml Normal file
View File

@@ -0,0 +1,75 @@
[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.channels]
tech = "!techroomid:matrix.example.org"
politics = "!politicsroomid:matrix.example.org"
gaming = "!gamingroomid:matrix.example.org"
[posting]
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"
# 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.
[[sources]]
name = "The Guardian — World"
feed_url = "https://www.theguardian.com/world/rss"
tier = 1
poll_interval_minutes = 20
direct_route = "politics"
enabled = true
[[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

View File

@@ -1,69 +0,0 @@
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"
channels:
tech: "!techroomid:matrix.example.org"
politics: "!politicsroomid:matrix.example.org"
gaming: "!gamingroomid:matrix.example.org"
posting:
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
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"
# 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.
sources:
- name: "The Guardian — World"
feed_url: "https://www.theguardian.com/world/rss"
tier: 1
poll_interval_minutes: 20
direct_route: "politics"
enabled: true
- name: "The Guardian — Politics"
feed_url: "https://www.theguardian.com/politics/rss"
tier: 1
poll_interval_minutes: 20
direct_route: "politics"
enabled: true
- 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
- name: "Ars Technica"
feed_url: "https://feeds.arstechnica.com/arstechnica/index"
tier: 1
poll_interval_minutes: 20
direct_route: "tech"
enabled: true
- name: "Time Extension"
feed_url: "https://www.timeextension.com/feeds/news"
tier: 1
poll_interval_minutes: 20
direct_route: "gaming"
enabled: true

View File

@@ -8,4 +8,4 @@ services:
- "${PETE_WEB_PORT:-8080}:8080" - "${PETE_WEB_PORT:-8080}:8080"
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./config.yaml:/app/config.yaml:ro - ./config.toml:/app/config.toml:ro

2
go.mod
View File

@@ -3,9 +3,9 @@ module pete
go 1.25.0 go 1.25.0
require ( require (
github.com/BurntSushi/toml v1.6.0
github.com/PuerkitoBio/goquery v1.12.0 github.com/PuerkitoBio/goquery v1.12.0
github.com/mmcdole/gofeed v1.3.0 github.com/mmcdole/gofeed v1.3.0
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.28.0 maunium.net/go/mautrix v0.28.0
modernc.org/sqlite v1.50.1 modernc.org/sqlite v1.50.1
) )

4
go.sum
View File

@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
@@ -143,8 +145,6 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.28.0 h1:vBakLzf8MAdfED3NzAKiMeKQbc3AQ4EAS03NC+TVMXQ= maunium.net/go/mautrix v0.28.0 h1:vBakLzf8MAdfED3NzAKiMeKQbc3AQ4EAS03NC+TVMXQ=

View File

@@ -6,70 +6,70 @@ import (
"os" "os"
"regexp" "regexp"
"gopkg.in/yaml.v3" "github.com/BurntSushi/toml"
) )
// envBracketRe matches only ${VAR} style env references, not bare $VAR. // envBracketRe matches only ${VAR} style env references, not bare $VAR.
var envBracketRe = regexp.MustCompile(`\$\{([^}]+)\}`) var envBracketRe = regexp.MustCompile(`\$\{([^}]+)\}`)
type Config struct { type Config struct {
Matrix MatrixConfig `yaml:"matrix"` Matrix MatrixConfig `toml:"matrix"`
Posting PostingConfig `yaml:"posting"` Posting PostingConfig `toml:"posting"`
Storage StorageConfig `yaml:"storage"` Storage StorageConfig `toml:"storage"`
Web WebConfig `yaml:"web"` Web WebConfig `toml:"web"`
Sources []SourceConfig `yaml:"sources"` Sources []SourceConfig `toml:"sources"`
} }
// WebConfig controls the read-only HTTP interface (news.parodia.dev style). // WebConfig controls the read-only HTTP interface (news.parodia.dev style).
type WebConfig struct { type WebConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `toml:"enabled"`
ListenAddr string `yaml:"listen_addr"` // e.g. ":8080" or "127.0.0.1:8080" ListenAddr string `toml:"listen_addr"` // e.g. ":8080" or "127.0.0.1:8080"
SiteTitle string `yaml:"site_title"` // display name in the header SiteTitle string `toml:"site_title"` // display name in the header
BaseURL string `yaml:"base_url"` // public URL (used in metadata only) BaseURL string `toml:"base_url"` // public URL (used in metadata only)
} }
type MatrixConfig struct { type MatrixConfig struct {
Homeserver string `yaml:"homeserver"` Homeserver string `toml:"homeserver"`
UserID string `yaml:"user_id"` UserID string `toml:"user_id"`
Password string `yaml:"password"` Password string `toml:"password"`
PickleKey string `yaml:"pickle_key"` PickleKey string `toml:"pickle_key"`
DisplayName string `yaml:"display_name"` DisplayName string `toml:"display_name"`
DataDir string `yaml:"data_dir"` DataDir string `toml:"data_dir"`
AdminRoom string `yaml:"admin_room"` AdminRoom string `toml:"admin_room"`
Channels map[string]string `yaml:"channels"` Channels map[string]string `toml:"channels"`
} }
type PostingConfig struct { type PostingConfig struct {
MinIntervalSeconds int `yaml:"min_interval_seconds"` MinIntervalSeconds int `toml:"min_interval_seconds"`
BurstCapCount int `yaml:"burst_cap_count"` BurstCapCount int `toml:"burst_cap_count"`
BurstCapWindowSeconds int `yaml:"burst_cap_window_seconds"` BurstCapWindowSeconds int `toml:"burst_cap_window_seconds"`
DedupCooldownHours int `yaml:"dedup_cooldown_hours"` DedupCooldownHours int `toml:"dedup_cooldown_hours"`
// DailyCapTotal is the hard global cap on posts across ALL channels in a // DailyCapTotal is the hard global cap on posts across ALL channels in a
// rolling 24-hour window. 0 disables the cap. // rolling 24-hour window. 0 disables the cap.
DailyCapTotal int `yaml:"daily_cap_total"` DailyCapTotal int `toml:"daily_cap_total"`
RoundRobin RoundRobinConfig `yaml:"round_robin"` RoundRobin RoundRobinConfig `toml:"round_robin"`
} }
// RoundRobinConfig switches Pete from immediate-on-classify posting to a // RoundRobinConfig switches Pete from immediate-on-classify posting to a
// paced rotation: one story per IntervalHours, picking the next channel in // paced rotation: one story per IntervalHours, picking the next channel in
// rotation order that has a postable story (skip-and-advance, newest-first). // rotation order that has a postable story (skip-and-advance, newest-first).
type RoundRobinConfig struct { type RoundRobinConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `toml:"enabled"`
IntervalHours int `yaml:"interval_hours"` IntervalHours int `toml:"interval_hours"`
} }
type StorageConfig struct { type StorageConfig struct {
DBPath string `yaml:"db_path"` DBPath string `toml:"db_path"`
RecentWindowHours int `yaml:"recent_window_hours"` RecentWindowHours int `toml:"recent_window_hours"`
} }
type SourceConfig struct { type SourceConfig struct {
Name string `yaml:"name"` Name string `toml:"name"`
FeedURL string `yaml:"feed_url"` FeedURL string `toml:"feed_url"`
Tier int `yaml:"tier"` Tier int `toml:"tier"`
PollIntervalMinutes int `yaml:"poll_interval_minutes"` PollIntervalMinutes int `toml:"poll_interval_minutes"`
DirectRoute string `yaml:"direct_route"` DirectRoute string `toml:"direct_route"`
Enabled bool `yaml:"enabled"` Enabled bool `toml:"enabled"`
} }
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
@@ -90,7 +90,7 @@ func Load(path string) (*Config, error) {
}) })
var cfg Config var cfg Config
if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil { if _, err := toml.Decode(expanded, &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err) return nil, fmt.Errorf("parse config: %w", err)
} }

View File

@@ -8,28 +8,29 @@ import (
// validMatrix is a reusable matrix config block for tests. // validMatrix is a reusable matrix config block for tests.
const validMatrix = ` const validMatrix = `
matrix: [matrix]
homeserver: "https://matrix.example.org" homeserver = "https://matrix.example.org"
user_id: "@pete:example.org" user_id = "@pete:example.org"
password: "testpass" password = "testpass"
channels: [matrix.channels]
tech: "!tech:example.org" tech = "!tech:example.org"
politics: "!politics:example.org" politics = "!politics:example.org"
` `
func TestLoadValid(t *testing.T) { func TestLoadValid(t *testing.T) {
yaml := validMatrix + ` toml := validMatrix + `
storage: [storage]
db_path: "/tmp/test.db" db_path = "/tmp/test.db"
sources:
- name: "Test Source" [[sources]]
feed_url: "https://example.com/rss" name = "Test Source"
tier: 1 feed_url = "https://example.com/rss"
poll_interval_minutes: 20 tier = 1
direct_route: "tech" poll_interval_minutes = 20
enabled: true direct_route = "tech"
enabled = true
` `
cfg := loadFromString(t, yaml) cfg := loadFromString(t, toml)
if cfg.Matrix.Homeserver != "https://matrix.example.org" { if cfg.Matrix.Homeserver != "https://matrix.example.org" {
t.Errorf("homeserver = %q", cfg.Matrix.Homeserver) t.Errorf("homeserver = %q", cfg.Matrix.Homeserver)
@@ -51,18 +52,18 @@ sources:
func TestEnvVarExpansion(t *testing.T) { func TestEnvVarExpansion(t *testing.T) {
t.Setenv("TEST_PETE_PASS", "secret_pass_123") t.Setenv("TEST_PETE_PASS", "secret_pass_123")
yaml := ` toml := `
matrix: [matrix]
homeserver: "https://matrix.example.org" homeserver = "https://matrix.example.org"
user_id: "@pete:example.org" user_id = "@pete:example.org"
password: "${TEST_PETE_PASS}" password = "${TEST_PETE_PASS}"
channels: [matrix.channels]
tech: "!tech:example.org" tech = "!tech:example.org"
storage:
db_path: "/tmp/test.db" [storage]
sources: [] db_path = "/tmp/test.db"
` `
cfg := loadFromString(t, yaml) cfg := loadFromString(t, toml)
if cfg.Matrix.Password != "secret_pass_123" { if cfg.Matrix.Password != "secret_pass_123" {
t.Errorf("password = %q, want secret_pass_123", cfg.Matrix.Password) t.Errorf("password = %q, want secret_pass_123", cfg.Matrix.Password)
@@ -70,18 +71,19 @@ sources: []
} }
func TestDefaults(t *testing.T) { func TestDefaults(t *testing.T) {
yaml := validMatrix + ` toml := validMatrix + `
storage: [storage]
db_path: "/tmp/test.db" db_path = "/tmp/test.db"
sources:
- name: "S" [[sources]]
feed_url: "https://example.com/rss" name = "S"
tier: 1 feed_url = "https://example.com/rss"
poll_interval_minutes: 20 tier = 1
direct_route: "tech" poll_interval_minutes = 20
enabled: true direct_route = "tech"
enabled = true
` `
cfg := loadFromString(t, yaml) cfg := loadFromString(t, toml)
if cfg.Matrix.DataDir != "./data" { if cfg.Matrix.DataDir != "./data" {
t.Errorf("data_dir default = %q, want ./data", cfg.Matrix.DataDir) t.Errorf("data_dir default = %q, want ./data", cfg.Matrix.DataDir)
@@ -106,109 +108,125 @@ sources:
func TestValidationErrors(t *testing.T) { func TestValidationErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
yaml string toml string
}{ }{
{"missing homeserver", ` {"missing homeserver", `
matrix: [matrix]
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
[storage]
db_path = "/tmp/t.db"
`}, `},
{"missing user_id", ` {"missing user_id", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
[storage]
db_path = "/tmp/t.db"
`}, `},
{"missing password", ` {"missing password", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
[storage]
db_path = "/tmp/t.db"
`}, `},
{"no channels", ` {"no channels", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
storage: {db_path: "/tmp/t.db"} [storage]
db_path = "/tmp/t.db"
`}, `},
{"missing db_path", ` {"missing db_path", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {} tech = "!t:e"
[storage]
`}, `},
{"invalid tier", ` {"invalid tier", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
sources: [storage]
- name: "S" db_path = "/tmp/t.db"
feed_url: "https://e.com/rss" [[sources]]
tier: 5 name = "S"
poll_interval_minutes: 20 feed_url = "https://e.com/rss"
direct_route: "tech" tier = 5
enabled: true poll_interval_minutes = 20
direct_route = "tech"
enabled = true
`}, `},
{"missing source name", ` {"missing source name", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
sources: [storage]
- feed_url: "https://e.com/rss" db_path = "/tmp/t.db"
tier: 1 [[sources]]
poll_interval_minutes: 20 feed_url = "https://e.com/rss"
direct_route: "tech" tier = 1
enabled: true poll_interval_minutes = 20
direct_route = "tech"
enabled = true
`}, `},
{"enabled source missing direct_route", ` {"enabled source missing direct_route", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
sources: [storage]
- name: "S" db_path = "/tmp/t.db"
feed_url: "https://e.com/rss" [[sources]]
tier: 1 name = "S"
poll_interval_minutes: 20 feed_url = "https://e.com/rss"
enabled: true tier = 1
poll_interval_minutes = 20
enabled = true
`}, `},
{"direct_route not in channels", ` {"direct_route not in channels", `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
sources: [storage]
- name: "S" db_path = "/tmp/t.db"
feed_url: "https://e.com/rss" [[sources]]
tier: 1 name = "S"
poll_interval_minutes: 20 feed_url = "https://e.com/rss"
direct_route: "gaming" tier = 1
enabled: true poll_interval_minutes = 20
direct_route = "gaming"
enabled = true
`}, `},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "config.yaml") path := filepath.Join(dir, "config.toml")
os.WriteFile(path, []byte(tt.yaml), 0o644) os.WriteFile(path, []byte(tt.toml), 0o644)
_, err := Load(path) _, err := Load(path)
if err == nil { if err == nil {
t.Error("expected validation error, got nil") t.Error("expected validation error, got nil")
@@ -219,31 +237,33 @@ sources:
func TestDisabledSourceSkipsDirectRouteCheck(t *testing.T) { func TestDisabledSourceSkipsDirectRouteCheck(t *testing.T) {
// A disabled source without direct_route must not fail validation. // A disabled source without direct_route must not fail validation.
yaml := ` toml := `
matrix: [matrix]
homeserver: "https://h" homeserver = "https://h"
user_id: "@p:h" user_id = "@p:h"
password: "pw" password = "pw"
channels: {tech: "!t:e"} [matrix.channels]
storage: {db_path: "/tmp/t.db"} tech = "!t:e"
sources: [storage]
- name: "Off" db_path = "/tmp/t.db"
feed_url: "https://e.com/rss" [[sources]]
tier: 1 name = "Off"
poll_interval_minutes: 20 feed_url = "https://e.com/rss"
enabled: false tier = 1
poll_interval_minutes = 20
enabled = false
` `
cfg := loadFromString(t, yaml) cfg := loadFromString(t, toml)
if cfg.Sources[0].DirectRoute != "" { if cfg.Sources[0].DirectRoute != "" {
t.Errorf("expected empty direct_route, got %q", cfg.Sources[0].DirectRoute) t.Errorf("expected empty direct_route, got %q", cfg.Sources[0].DirectRoute)
} }
} }
func loadFromString(t *testing.T, yamlContent string) *Config { func loadFromString(t *testing.T, tomlContent string) *Config {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "config.yaml") path := filepath.Join(dir, "config.toml")
if err := os.WriteFile(path, []byte(yamlContent), 0o644); err != nil { if err := os.WriteFile(path, []byte(tomlContent), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
cfg, err := Load(path) cfg, err := Load(path)

View File

@@ -22,7 +22,7 @@ import (
) )
func main() { func main() {
configPath := flag.String("config", "config.yaml", "path to config file") configPath := flag.String("config", "config.toml", "path to config file")
seed := flag.Bool("seed", false, "ingest current feed items as seen without posting, then exit") seed := flag.Bool("seed", false, "ingest current feed items as seen without posting, then exit")
test := flag.Bool("test", false, "post one story to verify the full pipeline, then exit") test := flag.Bool("test", false, "post one story to verify the full pipeline, then exit")
testSource := flag.String("test-source", "", "source name to use for -test (default: first enabled)") testSource := flag.String("test-source", "", "source name to use for -test (default: first enabled)")