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
package shared

import (
	"encoding/json"
	"github.com/tailscale/hujson"
	"log"
	"os"
	"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.jsonc"  // Path to config.json in development environment
	devSecretsPath = "../dev/secrets.dev.jsonc" // Path to config.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"`
	BlockedFeedsFile string  `json:"blocked_feeds_file"`
	// RestrictRequests, when true, closes new-feed requests by default: only
	// fediverse monikers (@user@host) listed in AllowedRequestersFile (one per
	// line) may request feeds; everyone else is refused, and an empty or missing
	// list denies everyone. When false (default) anyone may request feeds.
	RestrictRequests      bool           `json:"restrict_requests"`
	AllowedRequestersFile string         `json:"allowed_requesters_file"`
	ProfileDir            string         `json:"profile_dir"`
	ProfileKeepDays       int            `json:"profile_keep_days"`
	CachePageTemplates    bool           `json:"cache_page_templates"`
	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"`
	MetricsAuth     string   `json:"metrics_auth"`
}

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
	mustDeserializeFile(cfgPath, &config)
	// Read secrets member from secrets file
	mustDeserializeFile(secretsPath, &config.Secrets)
	return &config
}

func mustDeserializeFile[T any](fileName string, obj *T) {
	var err error
	var cfgJson []byte
	cfgJson, err = os.ReadFile(fileName)
	if err != nil {
		log.Fatal(err)
	}
	// JSONC => JSON
	cfgJson, err = standardizeJSON(cfgJson)
	if err != nil {
		log.Fatal(err)
	}
	// Parse
	if err := json.Unmarshal(cfgJson, obj); err != nil {
		log.Fatal(err)
	}
}

func standardizeJSON(b []byte) ([]byte, error) {
	ast, err := hujson.Parse(b)
	if err != nil {
		return b, err
	}
	ast.Standardize()
	return ast.Pack(), nil
}