diff --git a/client/client.yml b/client/client.yml index 2fbe17a..74be0f7 100644 --- a/client/client.yml +++ b/client/client.yml @@ -14,6 +14,8 @@ # command: /usr/local/bin/mytopic-triggered.sh # - topic: myserver.com/anothertopic # command: 'echo "$message"' +# if: +# priority: high,urgent # # Variables: # Variable Aliases Description @@ -26,4 +28,8 @@ # $NTFY_PRIORITY $priority, $p Message priority (1=min, 5=max) # $NTFY_TAGS $tags, $ta Message tags (comma separated list) # +# Filters ('if:'): +# You can filter 'message', 'title', 'priority' (comma-separated list, logical OR) +# and 'tags' (comma-separated list, logical AND). See https://ntfy.sh/docs/subscribe/api/#filter-messages. +# # subscribe: diff --git a/client/config.go b/client/config.go index c2a40d8..c44fac6 100644 --- a/client/config.go +++ b/client/config.go @@ -1,5 +1,10 @@ package client +import ( + "gopkg.in/yaml.v2" + "os" +) + const ( // DefaultBaseURL is the base URL used to expand short topic names DefaultBaseURL = "https://ntfy.sh" @@ -22,3 +27,16 @@ func NewConfig() *Config { Subscribe: nil, } } + +// LoadConfig loads the Client config from a yaml file +func LoadConfig(filename string) (*Config, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + c := NewConfig() + if err := yaml.Unmarshal(b, c); err != nil { + return nil, err + } + return c, nil +} diff --git a/client/config_test.go b/client/config_test.go new file mode 100644 index 0000000..67b69be --- /dev/null +++ b/client/config_test.go @@ -0,0 +1,35 @@ +package client + +import ( + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +func TestConfig_Load(t *testing.T) { + filename := filepath.Join(t.TempDir(), "client.yml") + require.Nil(t, os.WriteFile(filename, []byte(` +default-host: http://localhost +subscribe: + - topic: no-command + - topic: echo-this + command: 'echo "Message received: $message"' + - topic: alerts + command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m" + if: + priority: high,urgent +`), 0600)) + + conf, err := LoadConfig(filename) + require.Nil(t, err) + require.Equal(t, "http://localhost", conf.DefaultHost) + require.Equal(t, 3, len(conf.Subscribe)) + require.Equal(t, "no-command", conf.Subscribe[0].Topic) + require.Equal(t, "", conf.Subscribe[0].Command) + require.Equal(t, "echo-this", conf.Subscribe[1].Topic) + require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command) + require.Equal(t, "alerts", conf.Subscribe[2].Topic) + require.Equal(t, `notify-send -i /usr/share/ntfy/logo.png "Important" "$m"`, conf.Subscribe[2].Command) + require.Equal(t, "high,urgent", conf.Subscribe[2].If["priority"]) +} diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 7ba2152..12db37c 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "github.com/urfave/cli/v2" - "gopkg.in/yaml.v2" "heckel.io/ntfy/client" "heckel.io/ntfy/util" "log" @@ -225,7 +224,7 @@ func envVar(value string, vars ...string) []string { func loadConfig(c *cli.Context) (*client.Config, error) { filename := c.String("config") if filename != "" { - return loadConfigFromFile(filename) + return client.LoadConfig(filename) } u, _ := user.Current() configFile := defaultClientRootConfigFile @@ -233,19 +232,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) { configFile = util.ExpandHome(defaultClientUserConfigFile) } if s, _ := os.Stat(configFile); s != nil { - return loadConfigFromFile(configFile) + return client.LoadConfig(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 -} diff --git a/docs/static/img/cli-subscribe-video-3.webm b/docs/static/img/cli-subscribe-video-3.webm index 8a07f41..a286e2b 100644 Binary files a/docs/static/img/cli-subscribe-video-3.webm and b/docs/static/img/cli-subscribe-video-3.webm differ diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md index 92c171d..5cf25ae 100644 --- a/docs/subscribe/api.md +++ b/docs/subscribe/api.md @@ -217,16 +217,25 @@ curl -s "ntfy.sh/mytopic/json?poll=1&sched=1" ### Filter messages You can filter which messages are returned based on the well-known message fields `message`, `title`, `priority` and -`tags`. Currently, only exact matches are supported. Here's an example that only returns messages of high priority -with the tag "zfs-error": +`tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags +"zfs-error" and "error". Note that the `priority` filter is a logical OR and the `tags` filter is a logical AND. ``` $ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error" {"id":"0TIkJpBcxR","time":1640122627,"event":"open","topic":"alerts"} -{"id":"X3Uzz9O1sM","time":1640122674,"event":"message","topic":"alerts","priority":4,"tags":["zfs-error"], - "message":"ZFS pool corruption detected"} +{"id":"X3Uzz9O1sM","time":1640122674,"event":"message","topic":"alerts","priority":4, + "tags":["error", "zfs-error"], "message":"ZFS pool corruption detected"} ``` +Available filters (all case-insensitive): + +| Filter variable | Alias | Example | Description | +|---|---|---|---| +| `message` | `X-Message`, `m` | `ntfy.sh/mytopic?some_message` | Only return messages that match this exact message string | +| `title` | `X-Title`, `t` | `ntfy.sh/mytopic?title=some+title` | Only return messages that match this exact title string | +| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) | +| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) | + ### Subscribe to multiple topics It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics in the URL. This allows you to reduce the number of connections you have to maintain: @@ -314,5 +323,5 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a | `scheduled` | `X-Scheduled`, `sched` | Include scheduled/delayed messages in message list | | `message` | `X-Message`, `m` | Filter: Only return messages that match this exact message string | | `title` | `X-Title`, `t` | Filter: Only return messages that match this exact title string | -| `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match this priority | -| `tags` | `X-Tags`, `tag`, `ta` | Filter: Only return messages that all listed tags (comma-separated) | +| `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match *any priority listed* (comma-separated) | +| `tags` | `X-Tags`, `tag`, `ta` | Filter: Only return messages that match *all listed tags* (comma-separated) | diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md index 07e6f28..2889273 100644 --- a/docs/subscribe/cli.md +++ b/docs/subscribe/cli.md @@ -125,25 +125,31 @@ Here's an example config file that subscribes to three different topics, executi === "~/.config/ntfy/client.yml" ```yaml subscribe: - - topic: echo-this - command: 'echo "Message received: $message"' - - topic: get-temp - command: | - temp="$(sensors | awk '/Package/ { print $4 }')" - ntfy publish --quiet temp "$temp"; - echo "CPU temp is $temp; published to topic 'temp'" + - topic: echo-this + command: 'echo "Message received: $message"' - topic: alerts - command: notify-send "$m" + command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m" + if: + priority: high,urgent - topic: calc command: 'gnome-calculator 2>/dev/null &' - ``` + - topic: print-temp + command: | + echo "You can easily run inline scripts, too." + temp="$(sensors | awk '/Pack/ { print substr($4,2,2) }')" + if [ $temp -gt 80 ]; then + echo "Warning: CPU temperature is $temp. Too high." + else + echo "CPU temperature is $temp. That's alright." + fi + ``` In this example, when `ntfy subscribe --from-config` is executed: -* Messages to topic `echo-this` will be simply echoed to standard out -* Messages to topic `get-temp` will publish the CPU core temperature to topic `temp` -* Messages to topic `alerts` will be displayed as desktop notification using `notify-send` -* And messages to topic `calc` will open the gnome calculator 😀 (*because, why not*) +* Messages to `echo-this` simply echos to standard out +* Messages to `alerts` display as desktop notification for high priority messages using `notify-send` +* Messages to `calc` open the gnome calculator 😀 (*because, why not*) +* Messages to `print-temp` execute an inline script and print the CPU temperature I hope this shows how powerful this command is. Here's a short video that demonstrates the above example: diff --git a/scripts/postinst.sh b/scripts/postinst.sh index dabc0a1..4287e0c 100755 --- a/scripts/postinst.sh +++ b/scripts/postinst.sh @@ -22,7 +22,7 @@ if [ "$1" = "configure" ] && [ -d /run/systemd/system ]; then fi fi - # Restart service + # Restart services systemctl --system daemon-reload >/dev/null || true if systemctl is-active -q ntfy.service; then echo "Restarting ntfy.service ..." @@ -32,4 +32,12 @@ if [ "$1" = "configure" ] && [ -d /run/systemd/system ]; then systemctl restart ntfy.service >/dev/null || true fi fi + if systemctl is-active -q ntfy-client.service; then + echo "Restarting ntfy-client.service ..." + if [ -x /usr/bin/deb-systemd-invoke ]; then + deb-systemd-invoke try-restart ntfy-client.service >/dev/null || true + else + systemctl restart ntfy-client.service >/dev/null || true + fi + fi fi diff --git a/server/server.go b/server/server.go index 47b3e0f..a2bff6c 100644 --- a/server/server.go +++ b/server/server.go @@ -480,15 +480,22 @@ func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visi } } -func parseQueryFilters(r *http.Request) (messageFilter string, titleFilter string, priorityFilter int, tagsFilter []string, err error) { +func parseQueryFilters(r *http.Request) (messageFilter string, titleFilter string, priorityFilter []int, tagsFilter []string, err error) { messageFilter = readParam(r, "x-message", "message", "m") titleFilter = readParam(r, "x-title", "title", "t") tagsFilter = util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",") - priorityFilter, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) - return // may be err! + priorityFilter = make([]int, 0) + for _, p := range util.SplitNoEmpty(readParam(r, "x-priority", "priority", "prio", "p"), ",") { + priority, err := util.ParsePriority(p) + if err != nil { + return "", "", nil, nil, err + } + priorityFilter = append(priorityFilter, priority) + } + return } -func passesQueryFilter(msg *message, messageFilter string, titleFilter string, priorityFilter int, tagsFilter []string) bool { +func passesQueryFilter(msg *message, messageFilter string, titleFilter string, priorityFilter []int, tagsFilter []string) bool { if msg.Event != messageEvent { return true // filters only apply to messages } @@ -502,7 +509,7 @@ func passesQueryFilter(msg *message, messageFilter string, titleFilter string, p if messagePriority == 0 { messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0) } - if priorityFilter > 0 && messagePriority != priorityFilter { + if len(priorityFilter) > 0 && !util.InIntList(priorityFilter, messagePriority) { return false } if len(tagsFilter) > 0 && !util.InStringListAll(msg.Tags, tagsFilter) { diff --git a/server/server_test.go b/server/server_test.go index a1c995d..715b416 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -408,6 +408,9 @@ func TestServer_PollWithQueryFilters(t *testing.T) { queriesThatShouldReturnMessageOne := []string{ "/mytopic/json?poll=1&priority=1", "/mytopic/json?poll=1&priority=min", + "/mytopic/json?poll=1&priority=min,low", + "/mytopic/json?poll=1&priority=1,2", + "/mytopic/json?poll=1&p=2,min", "/mytopic/json?poll=1&tags=tag1", "/mytopic/json?poll=1&tags=tag1,tag2", "/mytopic/json?poll=1&message=my+first+message", diff --git a/util/util.go b/util/util.go index 8243bcc..cb4ffbb 100644 --- a/util/util.go +++ b/util/util.go @@ -18,7 +18,7 @@ var ( random = rand.New(rand.NewSource(time.Now().UnixNano())) randomMutex = sync.Mutex{} - errInvalidPriority = errors.New("unknown priority") + errInvalidPriority = errors.New("invalid priority") ) // FileExists checks if a file exists, and returns true if it does @@ -50,6 +50,16 @@ func InStringListAll(haystack []string, needles []string) bool { return matches == len(needles) } +// InIntList returns true if needle is contained in haystack +func InIntList(haystack []int, needle int) bool { + for _, s := range haystack { + if s == needle { + return true + } + } + return false +} + // SplitNoEmpty splits a string using strings.Split, but filters out empty strings func SplitNoEmpty(s string, sep string) []string { res := make([]string, 0)