Switch config from YAML to TOML
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,5 @@ data/
|
|||||||
.env
|
.env
|
||||||
pete
|
pete
|
||||||
config.yaml
|
config.yaml
|
||||||
|
config.toml
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -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
75
config.example.toml
Normal 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
|
||||||
@@ -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
|
|
||||||
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -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)")
|
||||||
|
|||||||
Reference in New Issue
Block a user