CI / test (push) Successful in 2m34s
- Filtres avancés: drop, set_priority, add_tag avec wildcard * (filter_outgoing.v) - Notifications structurées: click_url + actions (ntfy.v + sources/event.v) - Webhook sortant: Slack, Discord, JSON (filter_outgoing.v) - Fichier d'état: persistance poll_state + silence_until (filter_outgoing.v) - Config: sections filters, outgoing, state (config.v) - Tests: 9 nouveaux tests unitaires (filter_outgoing_test.v) - Docs: ROADMAP Phase 5 4/8, version 0.5.0
89 lines
2.3 KiB
V
89 lines
2.3 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}')
|
|
}
|
|
// 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
|
|
}
|