Docs, LoadConfig, config test
This commit is contained in:
parent
66c749d5f0
commit
68d881291c
11 changed files with 130 additions and 41 deletions
|
@ -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:
|
||||||
|
|
|
@ -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
35
client/config_test.go
Normal 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"])
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
BIN
docs/static/img/cli-subscribe-video-3.webm
vendored
BIN
docs/static/img/cli-subscribe-video-3.webm
vendored
Binary file not shown.
|
@ -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) |
|
||||||
|
|
|
@ -127,23 +127,29 @@ Here's an example config file that subscribes to three different topics, executi
|
||||||
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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
12
util/util.go
12
util/util.go
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue