CI / test (push) Failing after 1m18s
**ACLs (Access Control Lists):** - Ajout du type AclConfig dans sources/event.v (allowed_ips CIDR + allowed_tokens) - Chaque source webhook (Gitea, Uptime Kuma, Cron, Generic) supporte les ACLs - Validation IP via X-Forwarded-For / X-Real-IP avant HMAC - Validation Bearer token via Authorization header - Tests: 7 tests ACL (IP exact, CIDR, parse IPv4, ACL vide, IP+token combiné) **Plugin system:** - sources/plugin.v: runner exécutable externe, stdout JSON → Event - Exit 0 = publish, exit ≠ 0 = skip. Timeout configurable - Plugin loop dans server.v (goroutine, toutes les 60s) - Example: scripts/example-plugin-disk.sh (vérifie espace disque) **Docs:** - README.md: ajout source Plugin + section Features complète - ARCHITECTURE.md: flux Plugin, flux ACL, endpoints /metrics /api/silence - ROADMAP.md: Phase 5 → 8/8 complet, ajout v0.6.0 - ntfy-bridge.example.yaml: sections ACLs et Plugins commentées - Version bump: 0.5.0 → 0.6.0
87 lines
2.2 KiB
V
87 lines
2.2 KiB
V
module sources
|
|
|
|
import os
|
|
import time
|
|
import x.json2 as json
|
|
|
|
// transform_plugin runs an external executable, passes the raw event on stdin,
|
|
// and expects a JSON Event on stdout. Exit code 0 = success, non-zero = drop.
|
|
// timeout defaults to 10s if not set.
|
|
pub fn transform_plugin(raw string, source PluginSource) ?Event {
|
|
timeout := if source.timeout > 0 { source.timeout } else { 10 }
|
|
|
|
// Write raw input to a temp file for the plugin to read (optional)
|
|
// We use os.execute with timeout via a wrapper
|
|
start := time.now()
|
|
|
|
// Run the plugin command. The plugin receives nothing on stdin;
|
|
// it's a polling model — the plugin is called periodically and
|
|
// produces output when it has something to report.
|
|
result := os.execute(source.command)
|
|
|
|
elapsed := time.now().unix() - start.unix()
|
|
if elapsed > timeout {
|
|
return none
|
|
}
|
|
|
|
// Exit code 0 = event, non-zero = skip
|
|
if result.exit_code != 0 {
|
|
return none
|
|
}
|
|
|
|
output := result.output.trim_space()
|
|
if output == '' {
|
|
return none
|
|
}
|
|
|
|
// Parse plugin output as JSON
|
|
doc := json.decode[json.Any](output, json.DecoderOptions{}) or { return none }
|
|
obj := doc.as_map()
|
|
|
|
priority := obj['priority'] or { json.Any(3) }.int()
|
|
|
|
mut tags := []string{}
|
|
if tags_any := obj['tags'] {
|
|
for tag in tags_any.as_array() {
|
|
tags << tag.str()
|
|
}
|
|
}
|
|
|
|
message := obj['message'] or { json.Any(output) }.str()
|
|
click_url := obj['click_url'] or { json.Any('') }.str()
|
|
|
|
mut actions := []Action{}
|
|
if actions_any := obj['actions'] {
|
|
for a in actions_any.as_array() {
|
|
am := a.as_map()
|
|
actions << Action{
|
|
action: am['action'] or { json.Any('view') }.str()
|
|
label: am['label'] or { json.Any('') }.str()
|
|
url: am['url'] or { json.Any('') }.str()
|
|
clear: am['clear'] or { json.Any(false) }.bool()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve topic: plugin output can override, or fall back to source config
|
|
mut topic := source.topic
|
|
if plugin_topic := obj['topic'] {
|
|
t := plugin_topic.str()
|
|
if t != '' {
|
|
topic = t
|
|
}
|
|
}
|
|
|
|
return Event{
|
|
source: 'plugin'
|
|
name: source.name
|
|
topic: topic
|
|
priority: int(priority)
|
|
tags: tags
|
|
message: message
|
|
raw: output
|
|
click_url: click_url
|
|
actions: actions
|
|
}
|
|
}
|