ntfy/cmd/subscribe.go

234 lines
7.3 KiB
Go
Raw Normal View History

2021-12-17 14:33:01 +13:00
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
2021-12-19 08:43:27 +13:00
"gopkg.in/yaml.v2"
2021-12-17 14:33:01 +13:00
"heckel.io/ntfy/client"
"heckel.io/ntfy/util"
"log"
"os"
"os/exec"
2021-12-19 08:43:27 +13:00
"os/user"
2021-12-17 14:33:01 +13:00
"strings"
)
var cmdSubscribe = &cli.Command{
Name: "subscribe",
Aliases: []string{"sub"},
Usage: "Subscribe to one or more topics on a ntfy server",
2021-12-19 08:43:27 +13:00
UsageText: "ntfy subscribe [OPTIONS..] [TOPIC]",
2021-12-17 14:33:01 +13:00
Action: execSubscribe,
Flags: []cli.Flag{
2021-12-19 08:43:27 +13:00
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "config file"},
2021-12-17 14:33:01 +13:00
&cli.StringFlag{Name: "exec", Aliases: []string{"e"}, Usage: "execute command for each message event"},
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since (Unix timestamp, or all)"},
2021-12-19 08:43:27 +13:00
&cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"},
2021-12-18 03:32:59 +13:00
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
2021-12-17 14:33:01 +13:00
},
2021-12-19 08:43:27 +13:00
Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for
every arriving message. There are 3 modes in which the command can be run:
2021-12-17 14:33:01 +13:00
2021-12-19 08:43:27 +13:00
ntfy subscribe TOPIC
This prints the JSON representation of every incoming message. It is useful when you
have a command that wants to stream-read incoming JSON messages. Unless --poll is passed,
this command stays open forever.
2021-12-17 14:33:01 +13:00
2021-12-19 08:43:27 +13:00
Examples:
ntfy subscribe mytopic # Prints JSON for incoming messages for ntfy.sh/mytopic
ntfy sub home.lan/backups # Subscribe to topic on different server
ntfy sub --poll home.lan/backups # Just query for latest messages and exit
ntfy subscribe TOPIC COMMAND
This executes COMMAND for every incoming messages. The message fields are passed to the
command as environment variables:
2021-12-17 14:33:01 +13:00
Variable Aliases Description
--------------- --------------- -----------------------------------
2021-12-19 08:43:27 +13:00
$NTFY_ID $id Unique message ID
$NTFY_TIME $time Unix timestamp of the message delivery
$NTFY_TOPIC $topic Topic name
2021-12-17 14:33:01 +13:00
$NTFY_MESSAGE $message, $m Message body
$NTFY_TITLE $title, $t Message title
$NTFY_PRIORITY $priority, $p Message priority (1=min, 5=max)
$NTFY_TAGS $tags, $ta Message tags (comma separated list)
2021-12-19 08:43:27 +13:00
Examples:
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
ntfy sub topic1 /my/script.sh # Execute script for incoming messages
ntfy subscribe --from-config
Service mode (used in ntfy-client.service). This reads the config file (/etc/ntfy/client.yml
or ~/.config/ntfy/client.yml) and sets up subscriptions for every topic in the "subscribe:"
block (see config file).
Examples:
ntfy sub --from-config # Read topics from config file
ntfy sub --config=/my/client.yml --from-config # Read topics from alternate config file
2021-12-17 14:33:01 +13:00
`,
}
func execSubscribe(c *cli.Context) error {
2021-12-19 08:43:27 +13:00
fromConfig := c.Bool("from-config")
if fromConfig {
return execSubscribeFromConfig(c)
}
return execSubscribeWithoutConfig(c)
}
func execSubscribeFromConfig(c *cli.Context) error {
conf, err := loadConfig(c)
if err != nil {
return err
}
cl := client.New(conf)
commands := make(map[string]string)
for _, s := range conf.Subscribe {
topicURL := cl.Subscribe(s.Topic)
commands[topicURL] = s.Exec
}
for m := range cl.Messages {
command, ok := commands[m.TopicURL]
if !ok {
continue
}
_ = dispatchMessage(c, command, m)
}
return nil
}
func execSubscribeWithoutConfig(c *cli.Context) error {
2021-12-17 14:33:01 +13:00
if c.NArg() < 1 {
return errors.New("topic missing")
}
2021-12-18 16:38:29 +13:00
fmt.Fprintln(c.App.ErrWriter, "\x1b[1;33mThis command is incubating. The interface may change without notice.\x1b[0m")
2021-12-19 08:43:27 +13:00
conf, err := loadConfig(c)
if err != nil {
return err
}
cl := client.New(conf)
2021-12-18 03:32:59 +13:00
since := c.String("since")
poll := c.Bool("poll")
scheduled := c.Bool("scheduled")
2021-12-19 08:43:27 +13:00
topic := c.Args().Get(0)
command := c.Args().Get(1)
2021-12-18 03:32:59 +13:00
var options []client.SubscribeOption
if since != "" {
options = append(options, client.WithSince(since))
2021-12-17 14:33:01 +13:00
}
2021-12-18 03:32:59 +13:00
if poll {
options = append(options, client.WithPoll())
}
if scheduled {
options = append(options, client.WithScheduled())
}
if poll {
2021-12-19 08:43:27 +13:00
messages, err := cl.Poll(topic, options...)
if err != nil {
return err
2021-12-18 03:32:59 +13:00
}
2021-12-19 08:43:27 +13:00
for _, m := range messages {
_ = dispatchMessage(c, command, m)
2021-12-18 03:32:59 +13:00
}
2021-12-19 08:43:27 +13:00
} else {
cl.Subscribe(topic, options...)
2021-12-18 03:32:59 +13:00
for m := range cl.Messages {
_ = dispatchMessage(c, command, m)
}
2021-12-17 14:33:01 +13:00
}
return nil
}
func dispatchMessage(c *cli.Context, command string, m *client.Message) error {
if command != "" {
return execCommand(c, command, m)
}
fmt.Println(m.Raw)
return nil
}
func execCommand(c *cli.Context, command string, m *client.Message) error {
if m.Event == client.OpenEvent {
log.Printf("[%s] Connection opened, subscribed to topic", collapseTopicURL(m.TopicURL))
} else if m.Event == client.MessageEvent {
2021-12-18 03:32:59 +13:00
if err := runCommandInternal(c, command, m); err != nil {
log.Printf("[%s] Command failed: %s", collapseTopicURL(m.TopicURL), err.Error())
}
2021-12-17 14:33:01 +13:00
}
return nil
}
func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
scriptFile, err := createTmpScript(command)
if err != nil {
return err
}
defer os.Remove(scriptFile)
log.Printf("[%s] Executing: %s (for message: %s)", collapseTopicURL(m.TopicURL), command, m.Raw)
cmd := exec.Command("sh", "-c", scriptFile)
cmd.Stdin = c.App.Reader
cmd.Stdout = c.App.Writer
cmd.Stderr = c.App.ErrWriter
cmd.Env = envVars(m)
return cmd.Run()
}
func createTmpScript(command string) (string, error) {
scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
script := fmt.Sprintf("#!/bin/sh\n%s", command)
if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
return "", err
}
return scriptFile, nil
}
func envVars(m *client.Message) []string {
env := os.Environ()
env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
env = append(env, envVar(m.Topic, "NTFY_TOPIC", "topic")...)
env = append(env, envVar(fmt.Sprintf("%d", m.Time), "NTFY_TIME", "time")...)
env = append(env, envVar(m.Message, "NTFY_MESSAGE", "message", "m")...)
env = append(env, envVar(m.Title, "NTFY_TITLE", "title", "t")...)
env = append(env, envVar(fmt.Sprintf("%d", m.Priority), "NTFY_PRIORITY", "priority", "prio", "p")...)
env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "ta")...)
return env
}
func envVar(value string, vars ...string) []string {
env := make([]string, 0)
for _, v := range vars {
env = append(env, fmt.Sprintf("%s=%s", v, value))
}
return env
}
2021-12-19 08:43:27 +13:00
func loadConfig(c *cli.Context) (*client.Config, error) {
filename := c.String("config")
if filename != "" {
return loadConfigFromFile(filename)
}
u, _ := user.Current()
configFile := defaultClientRootConfigFile
if u.Uid != "0" {
configFile = util.ExpandHome(defaultClientUserConfigFile)
}
if s, _ := os.Stat(configFile); s != nil {
return loadConfigFromFile(configFile)
}
return client.NewConfig(), nil
}
func loadConfigFromFile(filename string) (*client.Config, error) {
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
c := client.NewConfig()
if err := yaml.Unmarshal(b, c); err != nil {
return nil, err
}
return c, nil
}