send (title/priority/tags), read (poll mode, greppable one-liners), topics (known homelab buses from config). Config with homelab defaults on first run. Unit tests against httptest; smoked against the real ntfy on topic agent-tools-test. Spec: doc/tool-parity.md 3.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
133 lines
3.5 KiB
Go
133 lines
3.5 KiB
Go
// notifyr sends and reads notifications on the homelab's ntfy bus, so
|
|
// any agent can alert a human and check recent infra alerts the same
|
|
// way. See doc/tool-parity.md §3.3.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.brasse-pc.eu/brasse/agent-tools/notifyr/notify"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
const usage = `notifyr - ntfy client for agents (send and read notifications)
|
|
|
|
Usage:
|
|
notifyr send --msg "text" [--topic T] [--title X]
|
|
[--priority min|low|default|high|urgent] [--tags a,b]
|
|
notifyr read [--topic T] [--since 10m|2h|all] [--limit N]
|
|
notifyr topics list known topics (from the config)
|
|
notifyr version
|
|
|
|
Config: ~/.config/notifyr/config.json (created on first run; override
|
|
path with NOTIFYR_CONFIG). Holds server URL, optional token, the
|
|
default topic and the known-topics table.
|
|
|
|
Examples:
|
|
notifyr send --topic ci-fel --title "Build failed" --msg "agent-tools arm64: FAIL" --priority high
|
|
notifyr read --topic pi5-server-fel --since 2h # what has alerted lately?
|
|
`
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Print(usage)
|
|
os.Exit(2)
|
|
}
|
|
cfgPath := notify.ConfigPath()
|
|
switch os.Args[1] {
|
|
case "send":
|
|
cmdSend(cfgPath, os.Args[2:])
|
|
case "read":
|
|
cmdRead(cfgPath, os.Args[2:])
|
|
case "topics":
|
|
cmdTopics(cfgPath)
|
|
case "version", "--version", "-v":
|
|
fmt.Println("notifyr", version)
|
|
case "help", "--help", "-h":
|
|
fmt.Print(usage)
|
|
default:
|
|
die("unknown command %q — run 'notifyr help'", os.Args[1])
|
|
}
|
|
}
|
|
|
|
func cmdSend(cfgPath string, args []string) {
|
|
fs := flag.NewFlagSet("send", flag.ExitOnError)
|
|
topic := fs.String("topic", "", "topic (default: default_topic from config)")
|
|
title := fs.String("title", "", "notification title")
|
|
msg := fs.String("msg", "", "message text (required)")
|
|
priority := fs.String("priority", "", "min|low|default|high|urgent or 1-5")
|
|
tags := fs.String("tags", "", "comma-separated tags/emoji shortcodes")
|
|
fs.Parse(args)
|
|
cfg, err := notify.LoadConfig(cfgPath)
|
|
if err != nil {
|
|
die("%v", err)
|
|
}
|
|
if *topic == "" {
|
|
*topic = cfg.DefaultTopic
|
|
}
|
|
var tagList []string
|
|
if *tags != "" {
|
|
tagList = strings.Split(*tags, ",")
|
|
}
|
|
if err := notify.New(cfg.Server, cfg.Token).Send(*topic, *title, *msg, *priority, tagList); err != nil {
|
|
die("%v", err)
|
|
}
|
|
fmt.Printf("sent to %s/%s\n", cfg.Server, *topic)
|
|
}
|
|
|
|
func cmdRead(cfgPath string, args []string) {
|
|
fs := flag.NewFlagSet("read", flag.ExitOnError)
|
|
topic := fs.String("topic", "", "topic (default: default_topic from config)")
|
|
since := fs.String("since", "12h", "how far back: 10m, 2h, unix timestamp or all")
|
|
limit := fs.Int("limit", 0, "print at most N (newest) messages")
|
|
fs.Parse(args)
|
|
cfg, err := notify.LoadConfig(cfgPath)
|
|
if err != nil {
|
|
die("%v", err)
|
|
}
|
|
if *topic == "" {
|
|
*topic = cfg.DefaultTopic
|
|
}
|
|
msgs, err := notify.New(cfg.Server, cfg.Token).Read(*topic, *since, *limit)
|
|
if err != nil {
|
|
die("%v", err)
|
|
}
|
|
if len(msgs) == 0 {
|
|
fmt.Printf("no messages on %s since %s\n", *topic, *since)
|
|
return
|
|
}
|
|
for _, m := range msgs {
|
|
fmt.Println(notify.Format(m))
|
|
}
|
|
}
|
|
|
|
func cmdTopics(cfgPath string) {
|
|
cfg, err := notify.LoadConfig(cfgPath)
|
|
if err != nil {
|
|
die("%v", err)
|
|
}
|
|
names := make([]string, 0, len(cfg.Topics))
|
|
for n := range cfg.Topics {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for _, n := range names {
|
|
mark := " "
|
|
if n == cfg.DefaultTopic {
|
|
mark = "* "
|
|
}
|
|
fmt.Printf("%s%-18s %s\n", mark, n, cfg.Topics[n])
|
|
}
|
|
fmt.Fprintln(os.Stderr, "(* = default topic)")
|
|
}
|
|
|
|
func die(format string, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, "notifyr: "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|