Add YAML config loading with validation, env overrides for secrets, and a web dashboard exposing health, stats, and recent events.
153 lines
4.2 KiB
V
153 lines
4.2 KiB
V
module main
|
|
|
|
import os
|
|
import yaml
|
|
import sources
|
|
|
|
pub struct ServerConfig {
|
|
pub mut:
|
|
url string @[json: 'url']
|
|
auth_token string @[json: 'auth_token']
|
|
listen string @[json: 'listen']
|
|
hmac_secret string @[json: 'hmac_secret'] // shared secret for webhook HMAC-SHA256 validation
|
|
}
|
|
|
|
pub struct DefaultConfig {
|
|
pub mut:
|
|
priority int @[json: 'priority']
|
|
tags []string @[json: 'tags']
|
|
}
|
|
|
|
pub struct RateLimitConfig {
|
|
pub mut:
|
|
max_per_minute int @[json: 'max_per_minute']
|
|
max_per_source int @[json: 'max_per_source']
|
|
}
|
|
|
|
pub struct DedupConfig {
|
|
pub mut:
|
|
enabled bool @[json: 'enabled']
|
|
ttl_seconds int @[json: 'ttl_seconds']
|
|
rate_limit RateLimitConfig @[json: 'rate_limit']
|
|
}
|
|
|
|
pub struct SourcesConfig {
|
|
pub mut:
|
|
gitea []sources.GiteaSource @[json: 'gitea']
|
|
uptime_kuma []sources.UptimeKumaSource @[json: 'uptime_kuma']
|
|
docker []sources.DockerSource @[json: 'docker']
|
|
http_poll []sources.HttpPollSource @[json: 'http_poll']
|
|
cron []sources.CronSource @[json: 'cron']
|
|
generic []sources.GenericSource @[json: 'generic']
|
|
}
|
|
|
|
pub struct Config {
|
|
pub mut:
|
|
server ServerConfig @[json: 'server']
|
|
defaults DefaultConfig @[json: 'defaults']
|
|
sources SourcesConfig @[json: 'sources']
|
|
dedup DedupConfig @[json: 'dedup']
|
|
}
|
|
|
|
pub fn load_config(path string) !Config {
|
|
if !os.exists(path) {
|
|
return error('config file not found: ${path}')
|
|
}
|
|
mut cfg := yaml.decode_file[Config](path)!
|
|
|
|
// Env override for auth token
|
|
env_token := os.getenv('NTFY_TOKEN')
|
|
if env_token != '' {
|
|
cfg.server.auth_token = env_token
|
|
}
|
|
|
|
// Env override for HMAC secret
|
|
env_hmac := os.getenv('NTFY_HMAC_SECRET')
|
|
if env_hmac != '' {
|
|
cfg.server.hmac_secret = env_hmac
|
|
}
|
|
|
|
validate_config(cfg)!
|
|
return cfg
|
|
}
|
|
|
|
pub fn validate_config(cfg Config) ! {
|
|
if cfg.server.url == '' {
|
|
return error('server.url is required')
|
|
}
|
|
if cfg.server.listen == '' {
|
|
return error('server.listen is required')
|
|
}
|
|
if cfg.defaults.priority < 1 || cfg.defaults.priority > 5 {
|
|
return error('defaults.priority must be between 1 and 5')
|
|
}
|
|
|
|
// Collect all webhook paths and detect duplicates
|
|
mut seen_paths := map[string]string{} // path -> source description
|
|
|
|
for src in cfg.sources.gitea {
|
|
if src.webhook_path == '' {
|
|
return error('gitea source "${src.name}" missing webhook_path')
|
|
}
|
|
if src.topic == '' {
|
|
return error('gitea source "${src.name}" missing topic')
|
|
}
|
|
check_duplicate_path(mut seen_paths, src.webhook_path, 'gitea:${src.name}')!
|
|
}
|
|
for src in cfg.sources.uptime_kuma {
|
|
if src.webhook_path == '' {
|
|
return error('uptime_kuma source "${src.name}" missing webhook_path')
|
|
}
|
|
if src.topic == '' {
|
|
return error('uptime_kuma source "${src.name}" missing topic')
|
|
}
|
|
check_duplicate_path(mut seen_paths, src.webhook_path, 'uptime_kuma:${src.name}')!
|
|
}
|
|
for src in cfg.sources.docker {
|
|
if src.topic == '' {
|
|
return error('docker source "${src.name}" missing topic')
|
|
}
|
|
if src.hosts.len == 0 {
|
|
return error('docker source "${src.name}" requires at least one host')
|
|
}
|
|
}
|
|
for src in cfg.sources.http_poll {
|
|
if src.interval <= 0 {
|
|
return error('http_poll source "${src.name}" interval must be > 0')
|
|
}
|
|
for chk in src.checks {
|
|
if chk.url == '' {
|
|
return error('http_poll check missing url')
|
|
}
|
|
if chk.topic == '' {
|
|
return error('http_poll check for "${chk.url}" missing topic')
|
|
}
|
|
}
|
|
}
|
|
for src in cfg.sources.cron {
|
|
if src.webhook_path == '' {
|
|
return error('cron source "${src.name}" missing webhook_path')
|
|
}
|
|
if src.topic == '' {
|
|
return error('cron source "${src.name}" missing topic')
|
|
}
|
|
check_duplicate_path(mut seen_paths, src.webhook_path, 'cron:${src.name}')!
|
|
}
|
|
for src in cfg.sources.generic {
|
|
if src.webhook_path == '' {
|
|
return error('generic source "${src.name}" missing webhook_path')
|
|
}
|
|
if src.topic == '' {
|
|
return error('generic source "${src.name}" missing topic')
|
|
}
|
|
check_duplicate_path(mut seen_paths, src.webhook_path, 'generic:${src.name}')!
|
|
}
|
|
}
|
|
|
|
fn check_duplicate_path(mut seen map[string]string, path string, source_name string) ! {
|
|
if existing := seen[path] {
|
|
return error('duplicate webhook path "${path}" used by ${existing} and ${source_name}')
|
|
}
|
|
seen[path] = source_name
|
|
}
|