// 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) }