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}') } // v0.5 — Click URL (opens when notification is tapped) if event.click_url != '' { header.add_custom('X-Click', event.click_url)! } // v0.5 — Action buttons if event.actions.len > 0 { mut action_strs := []string{} for a in event.actions { action_strs << '${a.action}, ${a.label}, ${a.url}${if a.clear { ', clear=true' } else { '' }}' } header.add_custom('X-Actions', action_strs.join('; '))! } 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 }