├── LICENSE ├── README.md ├── gotify-example.yaml ├── gotify-push └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Marcel Schwarz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gotify-push - A simple CLI client for the Gotify REST-API 2 | 3 | `gotify-push` is a simple command line client, written in python, that pushes notifications to the awesome [Gotify](https://github.com/gotify/server) REST-API. It works by setting default values for all required notification parameters, allowing you to override single parameters. This means you don't need to remember the syntax of `curl` or your server's URL and application tokens. You can even run `gotify-push` without any parameters to send a predefined notification with default parameters. 4 | 5 | ## Installation 6 | 7 | `gotify-push` requires `python3` to run. 8 | 9 | First install its dependencies by running: 10 | 11 | ```bash 12 | pip3 install -r requirements.txt 13 | ``` 14 | 15 | Then copy and rename the `gotify-example.yaml` file to `gotify.yaml`. Edit the configuation file to suit your needs and place it in one of the locations described in the [configuration](#default-config-file-locations) section. 16 | 17 | Install the script by copying it to a folder in your system's path, such as `/usr/local/bin`. Feel free to rename the script in this step if you wish to do so. 18 | 19 | ```bash 20 | cp gotify-push /usr/local/bin/. 21 | ``` 22 | 23 | Make sure that the script is executable for all required users. For example, to make `gotify-push` executable by all users: 24 | 25 | ```bash 26 | chmod a+x /usr/local/bin/gotify-push 27 | ``` 28 | 29 | ## Configuration 30 | 31 | The configuration file is a simple YAML file with the following structure: 32 | 33 | ```yaml 34 | # Url of the Gotify server (including the protocol to use) 35 | url: 'https://notify.example.com' 36 | # Default notification parameters 37 | default: 38 | token: '' 39 | title: 'Server' 40 | message: 'Hey! Listen!' 41 | priority: 10 42 | # Token map to map keys to tokens 43 | tokenMap: 44 | cron: '' 45 | ssh: '' 46 | ``` 47 | 48 | ### Default config file locations 49 | 50 | A configuration file needs to be placed in one of the following locations: 51 | 52 | ```bash 53 | ./gotify.yaml 54 | ./gotify.yml 55 | ~/.gotify.yaml 56 | ~/.gotify.yml 57 | ~/.gotify-push/gotify.yaml 58 | ~/.gotify-push/gotify.yml 59 | ~/.config/gotify-push/gotify.yaml 60 | ~/.config/gotify-push/gotify.yml 61 | /etc/gotify-push/gotify.yaml 62 | /etc/gotify-push/gotify.yml 63 | ``` 64 | 65 | Entries that are higher up in this list will overwrite entries below it if multiple configuration files exist. 66 | 67 | ### tokenMap 68 | 69 | One feature of `gotify-push` is the `tokenMap` defined in the configuration file. This file assigns human readable names (known as `key`s in `gotify-push`) to application tokens. This allows you to send notifications to different Gotify apps without having to remember their application tokens. 70 | 71 | An entry of the `tokenMap` looks like this: 72 | 73 | ```yaml 74 | ssh: ' 75 | ``` 76 | 77 | and can be used like so: 78 | 79 | ```bash 80 | gotify-push -k ssh -t "SSH login" -m "A user has logged in per SSH" 81 | ``` 82 | 83 | ### Custom config file location 84 | 85 | To load a config file from a custom directory the `-c/--config` argument may be used: 86 | 87 | ```bash 88 | gotify-push -c /path/to/config-file.yaml 89 | ``` 90 | 91 | ### Running gotify-push without a config file 92 | 93 | To run `gotify-push` without a config file the following arguments must be provided: 94 | 95 | ```bash 96 | gotify-push -u https://notify.example.com -a -t "Hello" -m "Hello World" -p 10 97 | ``` 98 | 99 | ## Usage 100 | 101 | * Running `gotify-push` with all default values read from a config file is as simple as running: 102 | 103 | ```bash 104 | gotify-push 105 | ``` 106 | 107 | * To list all possible arguments use the built-in help function: 108 | 109 | ```bash 110 | gotify-push --help 111 | ``` 112 | 113 | * To set a custom title and message: 114 | 115 | ```bash 116 | gotify-push -t "Hello" -m "Hello World" 117 | ``` 118 | 119 | * Pipe a message from `stdin` (notice the `-` following the `-m/--message` argument): 120 | 121 | ```bash 122 | echo "Hello World" | gotify-push -t "Hello" -m - 123 | ``` 124 | 125 | * Running `gotify-push` with a different notification priority: 126 | 127 | ```bash 128 | gotify-push -t "Hello" -m "Hello World" -p 10 129 | ``` 130 | 131 | * To run `gotify-push` with a different application token run: 132 | 133 | ```bash 134 | gotify-push -a -t "Hello" -m "Hello World" 135 | ``` 136 | 137 | where `` is the gotify app token. 138 | 139 | * To use the `tokenMap` defined in the configuration file run: 140 | 141 | ```bash 142 | gotify-push -k -t "Hello" -m "Hello World" 143 | ``` 144 | 145 | where `` is the name of the application provided in the configuration file. 146 | -------------------------------------------------------------------------------- /gotify-example.yaml: -------------------------------------------------------------------------------- 1 | # Url of the Gotify server (including the protocol to use) 2 | url: 'https://notify.example.com' 3 | # Default notification parameters 4 | default: 5 | token: '' 6 | title: 'Server' 7 | message: 'Hey! Listen!' 8 | priority: 10 9 | # Token map to map keys to tokens 10 | tokenMap: 11 | cron: '' 12 | ssh: '' 13 | -------------------------------------------------------------------------------- /gotify-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os.path 4 | import yaml 5 | import argparse 6 | import fileinput 7 | from urllib.parse import urljoin 8 | import requests 9 | 10 | APP_NAME = 'gotify-push' 11 | CONFIG_NAME = 'gotify' 12 | 13 | config = None 14 | 15 | def main(): 16 | # Parse and declare arguments 17 | args = parseArgs() 18 | 19 | # Check if url argument supplied (if so, don't load config) 20 | url = parseURLAndConfig(args) 21 | # Load app token (either from map, from argument, or default from config) 22 | token = parseToken(args) 23 | # Load title (either from argument, or default from config) 24 | title = parseTitle(args) 25 | # Load message (either from argument, from stdin, or default from config) 26 | message = parseMessage(args) 27 | # Load priority (either from argument, or default from config) 28 | priority = parsePriority(args) 29 | 30 | # Perform request 31 | doRequest(url, token, title, message, priority) 32 | 33 | def parseArgs(): 34 | # Argument parser declaration 35 | parser = argparse.ArgumentParser(description='A simple CLI client that pushes notifications to the Gotify REST-API. Can also read and use default values from config files.') 36 | groupUrlOrConfig = parser.add_mutually_exclusive_group() 37 | groupUrlOrConfig.add_argument('-u', '--url', help='url of Gotify server (overrides config)') 38 | groupUrlOrConfig.add_argument('-c', '--config', help='file path of a yaml config file') 39 | groupTokenOrKey = parser.add_mutually_exclusive_group() 40 | groupTokenOrKey.add_argument('-a', '--app-token', help='gotify app token to which to send the notification') 41 | groupTokenOrKey.add_argument('-k', '--key', help='lookup token from key in the config file') 42 | parser.add_argument('-t', '--title', help='the title of the notification') 43 | parser.add_argument('-m', '--message', help='the message of the notification (use \'-\' to read from stdin)') 44 | parser.add_argument('-p', '--priority', type=int, help='the priority of the notification') 45 | return parser.parse_args() 46 | 47 | def parseURLAndConfig(args): 48 | # Declare variable config as reference to global config variable in this function 49 | global config 50 | if args.url is not None: 51 | # This check is necessary since at this point in time argparse does not 52 | # yet support "necessarily inclusive" groups 53 | # (https://bugs.python.org/issue11588) 54 | if not (args.app_token is not None and args.title is not None and args.message is not None and args.priority is not None) or args.key is not None: 55 | sys.exit('The following arguments are required when using url argument:\n-a/--app-token -t/--title -m/--message -p/--priority\nAlso -k/--key argument may not be used with url argument') 56 | # Load url from argument 57 | return args.url 58 | # Load config 59 | if args.config is not None: 60 | # Load config from path provided as argument 61 | config = loadConfig(args.config) 62 | else: 63 | # Load config file from predefined locations 64 | config = loadDefaultConfig() 65 | # Load url from config 66 | return config['url'] 67 | 68 | def parseToken(args): 69 | if args.key is not None: 70 | return loadTokenFromKey(config, args.key) 71 | if args.app_token is not None: 72 | return args.app_token 73 | return config['default']['token'] 74 | 75 | def parseTitle(args): 76 | if args.title is not None: 77 | return args.title 78 | return config['default']['title'] 79 | 80 | def parseMessage(args): 81 | if args.message is not None: 82 | if args.message is '-': 83 | return bytes.decode(sys.stdin.buffer.read()) 84 | return args.message 85 | return config['default']['message'] 86 | 87 | def parsePriority(args): 88 | if args.priority is not None: 89 | return args.priority 90 | return config['default']['priority'] 91 | 92 | def loadConfig(configFile): 93 | if os.path.isfile(configFile): 94 | file = open(configFile, 'r') 95 | yamlString = file.read() 96 | file.close() 97 | else: 98 | sys.exit('Could not load config: ' + path) 99 | return yaml.load(yamlString, Loader=yaml.SafeLoader) 100 | 101 | def loadDefaultConfig(): 102 | configFileLocations = getDefaultConfigFileLocations() 103 | 104 | configLoaded = False 105 | 106 | # Load config files in array order (multiple configs will override each other) 107 | for configFile in configFileLocations: 108 | if os.path.isfile(configFile): 109 | file = open(configFile, 'r') 110 | yamlString = file.read() 111 | file.close() 112 | configLoaded = True 113 | 114 | # Exit with error if no config was loaded 115 | if not configLoaded: 116 | configFileLocations = '\n'.join(map(str, getDefaultConfigFileLocations())) 117 | sys.exit('No config file found! Possible locations are:\n' + configFileLocations) 118 | 119 | return yaml.load(yamlString, Loader=yaml.SafeLoader) 120 | 121 | def getDefaultConfigFileLocations(): 122 | HOME = os.path.expanduser('~') 123 | configFileLocations = [ 124 | '/etc/' + APP_NAME + '/' + CONFIG_NAME + '.yml', 125 | '/etc/' + APP_NAME + '/' + CONFIG_NAME + '.yaml', 126 | HOME + '/.' + APP_NAME + '/' + CONFIG_NAME + '.yml', 127 | HOME + '/.' + APP_NAME + '/' + CONFIG_NAME + '.yaml', 128 | HOME + '/.config/' + APP_NAME + '/' + CONFIG_NAME + '.yml', 129 | HOME + '/.config/' + APP_NAME + '/' + CONFIG_NAME + '.yaml', 130 | HOME + '.' + CONFIG_NAME + '.yml', 131 | HOME + '.' + CONFIG_NAME + '.yaml', 132 | './' + CONFIG_NAME + '.yml', 133 | './' + CONFIG_NAME + '.yaml' 134 | ] 135 | return configFileLocations 136 | 137 | 138 | def loadTokenFromKey(config, key): 139 | # Lookup if key exists in token Map 140 | if config['tokenMap'] is not None and key in config['tokenMap']: 141 | return config['tokenMap'][key] 142 | else: 143 | sys.exit('No such key found in config: ' + key) 144 | 145 | def doRequest(url, token, title, message, priority): 146 | requestURL = urljoin(url, '/message?token=' + token) 147 | try: 148 | resp = requests.post(requestURL, json={ 149 | 'title': title, 150 | 'message': message, 151 | 'priority': priority 152 | }) 153 | except requests.exceptions.RequestException as e: 154 | # Print exception if reqeuest fails 155 | sys.exit('Could not connect to Gotify server:\n' + str(e)) 156 | 157 | # Print request result if server returns http error code 158 | if resp.status_code is not requests.codes.ok: 159 | sys.exit(bytes.decode(resp.content)) 160 | 161 | if __name__ == "__main__": 162 | main() 163 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.20.0 2 | PyYAML>=4.2b1 3 | --------------------------------------------------------------------------------