Philipp Heckel 4 лет назад
Родитель
Сommit
68d881291c

+ 6 - 0
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:

+ 18 - 0
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
+}

+ 35 - 0
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"])
+}

+ 2 - 15
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
-}

BIN
docs/static/img/cli-subscribe-video-3.webm


+ 15 - 6
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) |

+ 19 - 13
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:
 

+ 9 - 1
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

+ 12 - 5
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) {

+ 3 - 0
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",

+ 11 - 1
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)