Files
ntfy-bridge/ntfy.v
T
bruno 4e1d7cd94a Add config validation and dashboard
Add YAML config loading with validation, env overrides for secrets, and
a web dashboard exposing health, stats, and recent events.
2026-08-03 12:12:12 -04:00

77 lines
1.9 KiB
V

module main
import net.http
import time
import sources
pub struct NtfyClient {
pub mut:
base_url string
auth_token string
logger Logger
}
pub fn new_ntfy_client(cfg ServerConfig, logger Logger) NtfyClient {
return NtfyClient{
base_url: cfg.url.trim_right('/')
auth_token: cfg.auth_token
logger: logger
}
}
fn join_tags(tags []string) string {
return tags.join(',')
}
// publish sends an event to the Ntfy server with exponential backoff retry (3 attempts).
pub fn (c NtfyClient) publish(event sources.Event) ! {
url := '${c.base_url}/${event.topic}'
mut header := http.Header{}
header.add_custom('Content-Type', 'text/plain')!
header.add_custom('X-Priority', event.priority.str())!
if event.tags.len > 0 {
header.add_custom('X-Tags', join_tags(event.tags))!
}
if c.auth_token != '' {
header.add(.authorization, 'Bearer ${c.auth_token}')
}
c.logger.debug('ntfy', 'publish',
'POST ${url} priority=${event.priority} tags=${join_tags(event.tags)}')
// Retry with exponential backoff: 3 attempts, 1s / 2s / 4s delays
mut last_err := error('unknown')
for attempt := 1; attempt <= 3; attempt++ {
resp := http.fetch(http.FetchConfig{
url: url
method: .post
header: header
data: event.message
}) or {
last_err = err
if attempt < 3 {
delay := time.second * (1 << (attempt - 1))
c.logger.warn('ntfy', 'publish',
'attempt ${attempt}/3 failed: ${err}, retrying in ${delay}')
time.sleep(delay)
}
continue
}
if resp.status_code >= 200 && resp.status_code < 300 {
c.logger.debug('ntfy', 'publish', 'success ${resp.status_code}')
return
}
last_err = error('ntfy returned ${resp.status_code}: ${resp.body}')
if attempt < 3 {
delay := time.second * (1 << (attempt - 1))
c.logger.warn('ntfy', 'publish',
'attempt ${attempt}/3 failed: ${last_err}, retrying in ${delay}')
time.sleep(delay)
}
}
return last_err
}