ntfy/cmd/subscribe.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

335 lines
11 KiB
Go
Raw Normal View History

2021-12-17 02:33:01 +01:00
package cmd
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
2021-12-17 02:33:01 +01:00
"heckel.io/ntfy/util"
"os"
2022-05-10 03:25:00 +02:00
"os/exec"
"os/user"
"path/filepath"
"sort"
2021-12-17 02:33:01 +01:00
"strings"
)
2022-05-09 17:03:40 +02:00
func init() {
commands = append(commands, cmdSubscribe)
}
2022-05-10 03:25:00 +02:00
const (
clientRootConfigFileUnixAbsolute = "/etc/ntfy/client.yml"
clientUserConfigFileUnixRelative = "ntfy/client.yml"
clientUserConfigFileWindowsRelative = "ntfy\\client.yml"
)
2022-05-30 04:14:14 +02:00
var flagsSubscribe = append(
2023-02-06 05:34:27 +01:00
append([]cli.Flag{}, flagsDefault...),
2022-05-30 04:14:14 +02:00
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
&cli.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token used to auth against the server"},
&cli.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
2022-05-30 04:14:14 +02: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 02:33:01 +01:00
var cmdSubscribe = &cli.Command{
Name: "subscribe",
Aliases: []string{"sub"},
Usage: "Subscribe to one or more topics on a ntfy server",
2021-12-18 20:43:27 +01:00
UsageText: "ntfy subscribe [OPTIONS..] [TOPIC]",
2021-12-17 02:33:01 +01:00
Action: execSubscribe,
2022-01-23 07:00:38 +01:00
Category: categoryClient,
2022-05-30 04:14:14 +02:00
Flags: flagsSubscribe,
2022-06-01 22:57:35 +02:00
Before: initLogFunc,
2021-12-18 20:43:27 +01: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 02:33:01 +01:00
2021-12-18 20:43:27 +01: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 02:33:01 +01:00
2021-12-18 20:43:27 +01: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
2022-02-17 19:12:20 +01:00
ntfy sub -u phil:mypass secret # Subscribe with username/password
2021-12-18 20:43:27 +01:00
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 02:33:01 +01:00
2021-12-23 00:16:28 +01:00
Variable Aliases Description
--------------- --------------------- -----------------------------------
$NTFY_ID $id Unique message ID
$NTFY_TIME $time Unix timestamp of the message delivery
$NTFY_TOPIC $topic Topic name
$NTFY_MESSAGE $message, $m Message body
$NTFY_TITLE $title, $t Message title
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
2023-06-01 20:08:51 +02:00
$NTFY_RAW $raw Raw JSON message
2021-12-17 02:33:01 +01:00
2021-12-18 20:43:27 +01:00
Examples:
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
2022-05-09 22:22:52 +02:00
ntfy sub topic1 myscript.sh # Execute script for incoming messages
2021-12-18 20:43:27 +01:00
ntfy subscribe --from-config
2022-05-09 22:22:52 +02:00
Service mode (used in ntfy-client.service). This reads the config file and sets up
subscriptions for every topic in the "subscribe:" block (see config file).
2021-12-18 20:43:27 +01:00
Examples:
ntfy sub --from-config # Read topics from config file
2022-05-09 22:22:52 +02:00
ntfy sub --config=myclient.yml --from-config # Read topics from alternate config file
2022-05-10 03:25:00 +02:00
` + clientCommandDescriptionSuffix,
2021-12-17 02:33:01 +01:00
}
func execSubscribe(c *cli.Context) error {
2021-12-18 22:12:36 +01:00
// Read config and options
2021-12-18 20:43:27 +01:00
conf, err := loadConfig(c)
if err != nil {
return err
}
cl := client.New(conf)
2021-12-17 15:32:59 +01:00
since := c.String("since")
2022-02-17 19:12:20 +01:00
user := c.String("user")
token := c.String("token")
2021-12-17 15:32:59 +01:00
poll := c.Bool("poll")
scheduled := c.Bool("scheduled")
2021-12-18 22:12:36 +01:00
fromConfig := c.Bool("from-config")
2021-12-18 20:43:27 +01:00
topic := c.Args().Get(0)
command := c.Args().Get(1)
// Checks
if user != "" && token != "" {
return errors.New("cannot set both --user and --token")
}
2021-12-18 22:12:36 +01:00
if !fromConfig {
conf.Subscribe = nil // wipe if --from-config not passed
}
2021-12-17 15:32:59 +01:00
var options []client.SubscribeOption
if since != "" {
options = append(options, client.WithSince(since))
2021-12-17 02:33:01 +01:00
}
if token != "" {
options = append(options, client.WithBearerAuth(token))
2023-04-09 05:20:21 +02:00
} else if user != "" {
2022-02-17 19:12:20 +01:00
var pass string
parts := strings.SplitN(user, ":", 2)
if len(parts) == 2 {
user = parts[0]
pass = parts[1]
} else {
fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
p, err := util.ReadPassword(c.App.Reader)
if err != nil {
return err
}
pass = string(p)
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
}
options = append(options, client.WithBasicAuth(user, pass))
2023-04-09 05:20:21 +02:00
} else if conf.DefaultToken != "" {
options = append(options, client.WithBearerAuth(conf.DefaultToken))
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
2022-02-17 19:12:20 +01:00
}
2021-12-17 15:32:59 +01:00
if scheduled {
options = append(options, client.WithScheduled())
}
2021-12-18 22:12:36 +01:00
if topic == "" && len(conf.Subscribe) == 0 {
2021-12-19 04:02:36 +01:00
return errors.New("must specify topic, type 'ntfy subscribe --help' for help")
2021-12-18 22:12:36 +01:00
}
// Execute poll or subscribe
2021-12-17 15:32:59 +01:00
if poll {
2021-12-21 02:46:51 +01:00
return doPoll(c, cl, conf, topic, command, options...)
2021-12-18 22:12:36 +01:00
}
2021-12-21 02:46:51 +01:00
return doSubscribe(c, cl, conf, topic, command, options...)
2021-12-18 22:12:36 +01:00
}
2021-12-21 02:46:51 +01:00
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
2021-12-18 22:12:36 +01:00
for _, s := range conf.Subscribe { // may be nil
if auth := maybeAddAuthHeader(s, conf); auth != nil {
options = append(options, auth)
}
2021-12-21 02:46:51 +01:00
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
2021-12-18 20:43:27 +01:00
return err
2021-12-17 15:32:59 +01:00
}
2021-12-18 22:12:36 +01:00
}
if topic != "" {
2021-12-21 02:46:51 +01:00
if err := doPollSingle(c, cl, topic, command, options...); err != nil {
2021-12-18 22:12:36 +01:00
return err
2021-12-17 15:32:59 +01:00
}
2021-12-17 02:33:01 +01:00
}
return nil
}
2021-12-21 02:46:51 +01:00
func doPollSingle(c *cli.Context, cl *client.Client, topic, command string, options ...client.SubscribeOption) error {
2021-12-18 22:12:36 +01:00
messages, err := cl.Poll(topic, options...)
if err != nil {
return err
}
for _, m := range messages {
printMessageOrRunCommand(c, m, command)
2021-12-17 02:33:01 +01:00
}
return nil
}
2021-12-21 02:46:51 +01:00
func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
2022-05-09 22:22:52 +02:00
cmds := make(map[string]string) // Subscription ID -> command
for _, s := range conf.Subscribe { // May be nil
2021-12-21 21:22:27 +01:00
topicOptions := append(make([]client.SubscribeOption, 0), options...)
for filter, value := range s.If {
topicOptions = append(topicOptions, client.WithFilter(filter, value))
}
if auth := maybeAddAuthHeader(s, conf); auth != nil {
topicOptions = append(topicOptions, auth)
2022-02-17 19:16:01 +01:00
}
2023-06-01 22:01:39 +02:00
subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...)
if err != nil {
return err
}
if s.Command != "" {
cmds[subscriptionID] = s.Command
} else if conf.DefaultCommand != "" {
cmds[subscriptionID] = conf.DefaultCommand
} else {
cmds[subscriptionID] = ""
}
2021-12-18 22:12:36 +01:00
}
if topic != "" {
2023-06-01 22:01:39 +02:00
subscriptionID, err := cl.Subscribe(topic, options...)
if err != nil {
return err
}
2022-05-09 22:22:52 +02:00
cmds[subscriptionID] = command
2021-12-18 22:12:36 +01:00
}
for m := range cl.Messages {
2022-05-09 22:22:52 +02:00
cmd, ok := cmds[m.SubscriptionID]
2021-12-18 22:12:36 +01:00
if !ok {
continue
2021-12-17 15:32:59 +01:00
}
log.Debug("%s Dispatching received message: %s", logMessagePrefix(m), m.Raw)
2022-05-09 22:22:52 +02:00
printMessageOrRunCommand(c, m, cmd)
2021-12-17 02:33:01 +01:00
}
return nil
}
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
// check for subscription token then subscription user:pass
if s.Token != "" {
return client.WithBearerAuth(s.Token)
}
if s.User != "" && s.Password != nil {
return client.WithBasicAuth(s.User, *s.Password)
}
// if no subscription token nor subscription user:pass, check for default token then default user:pass
if conf.DefaultToken != "" {
return client.WithBearerAuth(conf.DefaultToken)
}
if conf.DefaultUser != "" && conf.DefaultPassword != nil {
return client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)
}
return nil
}
2021-12-18 22:12:36 +01:00
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
if command != "" {
runCommand(c, command, m)
} else {
log.Debug("%s Printing raw message", logMessagePrefix(m))
2021-12-18 22:12:36 +01:00
fmt.Fprintln(c.App.Writer, m.Raw)
}
}
func runCommand(c *cli.Context, command string, m *client.Message) {
if err := runCommandInternal(c, command, m); err != nil {
log.Warn("%s Command failed: %s", logMessagePrefix(m), err.Error())
2021-12-18 22:12:36 +01:00
}
}
2022-05-10 03:25:00 +02:00
func runCommandInternal(c *cli.Context, script string, m *client.Message) error {
scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.%s", os.TempDir(), util.RandomString(10), scriptExt)
log.Debug("%s Running command '%s' via temporary script %s", logMessagePrefix(m), script, scriptFile)
script = scriptHeader + script
if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
2022-05-10 03:25:00 +02:00
return err
}
defer os.Remove(scriptFile)
log.Debug("%s Executing script %s", logMessagePrefix(m), scriptFile)
2022-05-10 03:25:00 +02:00
cmd := exec.Command(scriptLauncher[0], append(scriptLauncher[1:], scriptFile)...)
cmd.Stdin = c.App.Reader
cmd.Stdout = c.App.Writer
cmd.Stderr = c.App.ErrWriter
cmd.Env = envVars(m)
return cmd.Run()
}
2021-12-17 02:33:01 +01:00
func envVars(m *client.Message) []string {
env := make([]string, 0)
2021-12-17 02:33:01 +01:00
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")...)
2021-12-22 10:21:59 +01:00
env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "tag", "ta")...)
2021-12-23 00:16:28 +01:00
env = append(env, envVar(m.Raw, "NTFY_RAW", "raw")...)
sort.Strings(env)
if log.IsTrace() {
log.Trace("%s With environment:\n%s", logMessagePrefix(m), strings.Join(env, "\n"))
}
return append(os.Environ(), env...)
2021-12-17 02:33:01 +01:00
}
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-18 20:43:27 +01:00
func loadConfig(c *cli.Context) (*client.Config, error) {
filename := c.String("config")
if filename != "" {
2021-12-22 13:46:17 +01:00
return client.LoadConfig(filename)
2021-12-18 20:43:27 +01:00
}
2022-06-01 22:57:35 +02:00
configFile := defaultClientConfigFile()
2021-12-18 20:43:27 +01:00
if s, _ := os.Stat(configFile); s != nil {
2021-12-22 13:46:17 +01:00
return client.LoadConfig(configFile)
2021-12-18 20:43:27 +01:00
}
return client.NewConfig(), nil
}
2022-05-10 03:25:00 +02:00
//lint:ignore U1000 Conditionally used in different builds
2022-06-01 22:57:35 +02:00
func defaultClientConfigFileUnix() string {
2022-05-10 03:25:00 +02:00
u, _ := user.Current()
configFile := clientRootConfigFileUnixAbsolute
if u.Uid != "0" {
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileUnixRelative)
}
return configFile
}
//lint:ignore U1000 Conditionally used in different builds
2022-06-01 22:57:35 +02:00
func defaultClientConfigFileWindows() string {
2022-05-10 03:25:00 +02:00
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative)
}
func logMessagePrefix(m *client.Message) string {
return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID)
}