1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
package shared
import (
"encoding/json"
"fmt"
"log"
"os"
"sort"
"time"
)
const (
configVarName = "CONFIG" // If set, will load config.json from this path and not from devConfigPath
secretsVarName = "SECRETS" // If set, will load secrets.json from this path and not from devSecretsPath
devConfigPath = "dev/config.dev.json" // Path to config.json in development environment
devSecretsPath = "dev/secrets.dev.json" // Path to secrets.json in development environment
)
type Config struct {
Secrets Secrets `json:"-"`
LogFile string `json:"log_file"`
LogLevel string `json:"log_level"`
ServicePort uint `json:"service_port"`
Host string `json:"host"`
DbFile string `json:"db_file"`
// RestrictRequests, when true, closes new-feed requests by default: only
// fediverse monikers (@user@host) in the allowlist, which is kept in the
// database and edited with `rss-kakapo allow`, may request feeds; everyone
// else is refused, and an empty allowlist denies everyone. When false
// (default) anyone may request feeds.
RestrictRequests bool `json:"restrict_requests"`
ProfileDir string `json:"profile_dir"`
ProfileKeepDays int `json:"profile_keep_days"`
UpdateSchedule UpdateSchedule `json:"update_schedule"`
// MaxRetryDelayHours caps how long a feed is left alone after a failed
// check, whatever the backoff or the server's Retry-After say, unless its
// normal interval is longer. 0 means the default of 24.
MaxRetryDelayHours int `json:"max_retry_delay_hours"`
// BackdateGraceHours: an item never seen before is tooted only if it is
// dated after the newest item seen so far, or at most this many hours
// before it (for posts published with an earlier date, or a wrong time
// zone). nil means the default of 48.
BackdateGraceHours *int `json:"backdate_grace_hours"`
// MaxNewItemsPerFetch, if above 0, limits how many new items one fetch of
// a feed may toot: if a feed suddenly shows more, only the newest that
// many are tooted, and the rest are remembered as seen. 0 (default) is
// no limit.
MaxNewItemsPerFetch int `json:"max_new_items_per_fetch"`
PostsMinCountKept int `json:"posts_min_count_kept"`
PostsMinDaysKept int `json:"posts_min_days_kept"`
PurgeWaitSec int `json:"purge_wait_sec"`
FallbackProfilePic string `json:"fallback_profile_pic"`
Birb *UserInfo `json:"birb"`
}
// UpdateSchedule is how often a feed is checked, in hours, by how long ago it
// last had a new post: within a day, a week, four weeks, or longer.
type UpdateSchedule struct {
Day int `json:"day"`
Week int `json:"week"`
Weeks4 int `json:"weeks4"`
Older int `json:"older"`
}
// DefaultUpdateSchedule is upstream's documented schedule.
var DefaultUpdateSchedule = UpdateSchedule{Day: 1, Week: 3, Weeks4: 6, Older: 12}
// WithDefaults fills in every interval that is not set (or not positive) from
// DefaultUpdateSchedule. An interval of 0 would schedule the next check right
// away, so the feed would be checked as fast as the loop runs.
func (s UpdateSchedule) WithDefaults() UpdateSchedule {
def := DefaultUpdateSchedule
if s.Day <= 0 {
s.Day = def.Day
}
if s.Week <= 0 {
s.Week = def.Week
}
if s.Weeks4 <= 0 {
s.Weeks4 = def.Weeks4
}
if s.Older <= 0 {
s.Older = def.Older
}
return s
}
type UserInfo struct {
User string `json:"user"`
Published time.Time `json:"published"`
ManuallyApprovesFollows bool `json:"manually_approves_follows"`
ProfilePic string `json:"profile_pic"`
HeaderPic string `json:"header_pic"`
PubKey string `json:"pub_key"`
PrivKey string `json:"priv_key"`
}
type Secrets struct {
BirdPrivKeyPass string `json:"birb_privkey_passphrase"`
ApiKeys []string `json:"api_keys"`
}
func LoadConfig() *Config {
// Where are our config and secrets files?
cfgPath := os.Getenv(configVarName)
if len(cfgPath) == 0 {
cfgPath = devConfigPath
}
secretsPath := os.Getenv(secretsVarName)
if len(secretsPath) == 0 {
secretsPath = devSecretsPath
}
// Read config file
var config Config
cfgJson := mustDeserializeFile(cfgPath, &config)
for _, w := range removedKeyWarnings(cfgJson) {
fmt.Fprintf(os.Stderr, "rss-kakapo: warning: %s\n", w)
}
// Read secrets member from secrets file
mustDeserializeFile(secretsPath, &config.Secrets)
return &config
}
// removedKeys are config keys that once had an effect and no longer do, with
// what replaced them. Like any unknown key they are ignored, but with a
// warning, since whoever set one expects it to do something.
var removedKeys = map[string]string{
"blocked_feeds_file": "the blocklist is kept in the database; see rss-kakapo block",
"allowed_requesters_file": "the allowlist is kept in the database; see rss-kakapo allow",
"cache_page_templates": "the page templates are compiled into the binary",
}
// removedKeyWarnings returns a warning for every key of removedKeys that the
// config JSON sets, sorted by key.
func removedKeyWarnings(cfgJson []byte) []string {
var keys map[string]json.RawMessage
if json.Unmarshal(cfgJson, &keys) != nil {
return nil
}
var res []string
for key := range keys {
if instead, ok := removedKeys[key]; ok {
res = append(res, fmt.Sprintf("config key %s is no longer supported and is ignored: %s", key, instead))
}
}
sort.Strings(res)
return res
}
// mustDeserializeFile parses the JSON file into obj and returns its contents.
func mustDeserializeFile[T any](fileName string, obj *T) []byte {
cfgJson, err := os.ReadFile(fileName)
if err != nil {
log.Fatal(err)
}
if err := json.Unmarshal(cfgJson, obj); err != nil {
log.Fatalf("%s: %v", fileName, err)
}
return cfgJson
}
|