Docs, LoadConfig, config test

This commit is contained in:
Philipp Heckel 2021-12-22 13:46:17 +01:00
parent 66c749d5f0
commit 68d881291c
11 changed files with 130 additions and 41 deletions

View file

@ -14,6 +14,8 @@
# command: /usr/local/bin/mytopic-triggered.sh # command: /usr/local/bin/mytopic-triggered.sh
# - topic: myserver.com/anothertopic # - topic: myserver.com/anothertopic
# command: 'echo "$message"' # command: 'echo "$message"'
# if:
# priority: high,urgent
# #
# Variables: # Variables:
# Variable Aliases Description # Variable Aliases Description
@ -26,4 +28,8 @@
# $NTFY_PRIORITY $priority, $p Message priority (1=min, 5=max) # $NTFY_PRIORITY $priority, $p Message priority (1=min, 5=max)
# $NTFY_TAGS $tags, $ta Message tags (comma separated list) # $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: # subscribe:

View file

@ -1,5 +1,10 @@
package client package client
import (
"gopkg.in/yaml.v2"
"os"
)
const ( const (
// DefaultBaseURL is the base URL used to expand short topic names // DefaultBaseURL is the base URL used to expand short topic names
DefaultBaseURL = "https://ntfy.sh" DefaultBaseURL = "https://ntfy.sh"
@ -22,3 +27,16 @@ func NewConfig() *Config {
Subscribe: nil, 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
client/config_test.go Normal file
View file

@ -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"])
}

View file

@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
"heckel.io/ntfy/client" "heckel.io/ntfy/client"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
"log" "log"
@ -225,7 +224,7 @@ func envVar(value string, vars ...string) []string {
func loadConfig(c *cli.Context) (*client.Config, error) { func loadConfig(c *cli.Context) (*client.Config, error) {
filename := c.String("config") filename := c.String("config")
if filename != "" { if filename != "" {
return loadConfigFromFile(filename) return client.LoadConfig(filename)
} }
u, _ := user.Current() u, _ := user.Current()
configFile := defaultClientRootConfigFile configFile := defaultClientRootConfigFile
@ -233,19 +232,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
configFile = util.ExpandHome(defaultClientUserConfigFile) configFile = util.ExpandHome(defaultClientUserConfigFile)
} }
if s, _ := os.Stat(configFile); s != nil { if s, _ := os.Stat(configFile); s != nil {
return loadConfigFromFile(configFile) return client.LoadConfig(configFile)
} }
return client.NewConfig(), nil 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
}

Binary file not shown.

View file

@ -217,16 +217,25 @@ curl -s "ntfy.sh/mytopic/json?poll=1&sched=1"
### Filter messages ### Filter messages
You can filter which messages are returned based on the well-known message fields `message`, `title`, `priority` and 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 `tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags
with the tag "zfs-error": "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" $ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error"
{"id":"0TIkJpBcxR","time":1640122627,"event":"open","topic":"alerts"} {"id":"0TIkJpBcxR","time":1640122627,"event":"open","topic":"alerts"}
{"id":"X3Uzz9O1sM","time":1640122674,"event":"message","topic":"alerts","priority":4,"tags":["zfs-error"], {"id":"X3Uzz9O1sM","time":1640122674,"event":"message","topic":"alerts","priority":4,
"message":"ZFS pool corruption detected"} "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 ### Subscribe to multiple topics
It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of 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: 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 | | `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 | | `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 | | `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 | | `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 all listed tags (comma-separated) | | `tags` | `X-Tags`, `tag`, `ta` | Filter: Only return messages that match *all listed tags* (comma-separated) |

View file

@ -125,25 +125,31 @@ Here's an example config file that subscribes to three different topics, executi
=== "~/.config/ntfy/client.yml" === "~/.config/ntfy/client.yml"
```yaml ```yaml
subscribe: subscribe:
- topic: echo-this - topic: echo-this
command: 'echo "Message received: $message"' 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: alerts - topic: alerts
command: notify-send "$m" command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m"
if:
priority: high,urgent
- topic: calc - topic: calc
command: 'gnome-calculator 2>/dev/null &' 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: In this example, when `ntfy subscribe --from-config` is executed:
* Messages to topic `echo-this` will be simply echoed to standard out * Messages to `echo-this` simply echos to standard out
* Messages to topic `get-temp` will publish the CPU core temperature to topic `temp` * Messages to `alerts` display as desktop notification for high priority messages using `notify-send`
* Messages to topic `alerts` will be displayed as desktop notification using `notify-send` * Messages to `calc` open the gnome calculator 😀 (*because, why not*)
* And messages to topic `calc` will 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: I hope this shows how powerful this command is. Here's a short video that demonstrates the above example:

View file

@ -22,7 +22,7 @@ if [ "$1" = "configure" ] && [ -d /run/systemd/system ]; then
fi fi
fi fi
# Restart service # Restart services
systemctl --system daemon-reload >/dev/null || true systemctl --system daemon-reload >/dev/null || true
if systemctl is-active -q ntfy.service; then if systemctl is-active -q ntfy.service; then
echo "Restarting ntfy.service ..." echo "Restarting ntfy.service ..."
@ -32,4 +32,12 @@ if [ "$1" = "configure" ] && [ -d /run/systemd/system ]; then
systemctl restart ntfy.service >/dev/null || true systemctl restart ntfy.service >/dev/null || true
fi fi
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 fi

View file

@ -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") messageFilter = readParam(r, "x-message", "message", "m")
titleFilter = readParam(r, "x-title", "title", "t") titleFilter = readParam(r, "x-title", "title", "t")
tagsFilter = util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",") tagsFilter = util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
priorityFilter, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) priorityFilter = make([]int, 0)
return // may be err! 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 { if msg.Event != messageEvent {
return true // filters only apply to messages return true // filters only apply to messages
} }
@ -502,7 +509,7 @@ func passesQueryFilter(msg *message, messageFilter string, titleFilter string, p
if messagePriority == 0 { if messagePriority == 0 {
messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (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 return false
} }
if len(tagsFilter) > 0 && !util.InStringListAll(msg.Tags, tagsFilter) { if len(tagsFilter) > 0 && !util.InStringListAll(msg.Tags, tagsFilter) {

View file

@ -408,6 +408,9 @@ func TestServer_PollWithQueryFilters(t *testing.T) {
queriesThatShouldReturnMessageOne := []string{ queriesThatShouldReturnMessageOne := []string{
"/mytopic/json?poll=1&priority=1", "/mytopic/json?poll=1&priority=1",
"/mytopic/json?poll=1&priority=min", "/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",
"/mytopic/json?poll=1&tags=tag1,tag2", "/mytopic/json?poll=1&tags=tag1,tag2",
"/mytopic/json?poll=1&message=my+first+message", "/mytopic/json?poll=1&message=my+first+message",

View file

@ -18,7 +18,7 @@ var (
random = rand.New(rand.NewSource(time.Now().UnixNano())) random = rand.New(rand.NewSource(time.Now().UnixNano()))
randomMutex = sync.Mutex{} 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 // 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) 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 // SplitNoEmpty splits a string using strings.Split, but filters out empty strings
func SplitNoEmpty(s string, sep string) []string { func SplitNoEmpty(s string, sep string) []string {
res := make([]string, 0) res := make([]string, 0)