mirror of
				https://github.com/binwiederhier/ntfy.git
				synced 2025-10-31 13:02:24 +01:00 
			
		
		
		
	Docs, LoadConfig, config test
This commit is contained in:
		
							parent
							
								
									66c749d5f0
								
							
						
					
					
						commit
						68d881291c
					
				
					 11 changed files with 130 additions and 41 deletions
				
			
		client
cmd
docs
scripts
server
util
|  | @ -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) | | ||||||
|  |  | ||||||
|  | @ -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: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Philipp Heckel
						Philipp Heckel