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 | ||||
| #       - 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: | ||||
|  |  | |||
|  | @ -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
									
								
								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" | ||||
| 	"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
									
										
									
									
										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 | ||||
| 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) | | ||||
|  |  | |||
|  | @ -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: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
							
								
								
									
										12
									
								
								util/util.go
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								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) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue