Skip to content

Instantly share code, notes, and snippets.

@mskian
Last active March 28, 2022 11:38
Show Gist options
  • Save mskian/e7b4a6e2b068bd1e6605547f498e3ed6 to your computer and use it in GitHub Desktop.
Save mskian/e7b4a6e2b068bd1e6605547f498e3ed6 to your computer and use it in GitHub Desktop.
Gotify Python CLI for Termux - Send Messages to the Gotify/server
# Url of the Gotify server (including the protocol to use)
url: 'https://push.example.com'
# Default notification parameters
default:
token: 'XXXXXXXXXXXXXX'
title: 'Hello World'
message: 'Hello Push From Python CLI'
priority: 5
#!/usr/bin/env python
import sys
import time
import os.path
from urllib.parse import urljoin
import argparse
import requests
import yaml
from halo import Halo
APP_NAME = 'gotify'
CONFIG_NAME = 'gfycli'
config = None
def main():
# Parse and declare arguments
args = parseArgs()
# Check if url argument supplied (if so, don't load config)
url = parseURLAndConfig(args)
# Load app token (either from map, from argument, or default from config)
token = parseToken(args)
# Load title (either from argument, or default from config)
title = parseTitle(args)
# Load message (either from argument, from stdin, or default from config)
message = parseMessage(args)
# Load priority (either from argument, or default from config)
priority = parsePriority(args)
# Perform request
doRequest(url, token, title, message, priority)
def parseArgs():
# Argument parser declaration
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.')
groupUrlOrConfig = parser.add_mutually_exclusive_group()
groupUrlOrConfig.add_argument('-u', '--url', help='url of Gotify server (overrides config)')
groupUrlOrConfig.add_argument('-c', '--config', help='file path of a yaml config file')
groupTokenOrKey = parser.add_mutually_exclusive_group()
groupTokenOrKey.add_argument('-a', '--app-token', help='gotify app token to which to send the notification')
parser.add_argument('-t', '--title', help='the title of the notification')
parser.add_argument('-m', '--message', help='the message of the notification (use \'-\' to read from stdin)')
parser.add_argument('-p', '--priority', type=int, help='the priority of the notification')
return parser.parse_args()
def parseURLAndConfig(args):
# Declare variable config as reference to global config variable in this function
global config
if args.url is not None:
# This check is necessary since at this point in time argparse does not
# yet support "necessarily inclusive" groups
# (https://bugs.python.org/issue11588)
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:
sys.exit('The following arguments are required when using url argument:\n-a/--app-token -t/--title -m/--message -p/--priority\n')
# Load url from argument
return args.url
# Load config
if args.config is not None:
# Load config from path provided as argument
config = loadConfig(args.config)
else:
# Load config file from predefined locations
config = loadDefaultConfig()
# Load url from config
return config['url']
def parseToken(args):
if args.app_token is not None:
return args.app_token
return config['default']['token']
def parseTitle(args):
if args.title is not None:
return args.title
return config['default']['title']
def parseMessage(args):
if args.message is not None:
if args.message is '-':
return bytes.decode(sys.stdin.buffer.read())
return args.message
return config['default']['message']
def parsePriority(args):
if args.priority is not None:
return args.priority
return config['default']['priority']
def loadConfig(configFile):
if os.path.isfile(configFile):
file = open(configFile, 'r')
yamlString = file.read()
file.close()
else:
sys.exit('Could not load config: ' + os.path)
return yaml.load(yamlString, Loader=yaml.SafeLoader)
def loadDefaultConfig():
configFileLocations = getDefaultConfigFileLocations()
configLoaded = False
# Load config files in array order (multiple configs will override each other)
for configFile in configFileLocations:
if os.path.isfile(configFile):
file = open(configFile, 'r')
yamlString = file.read()
file.close()
configLoaded = True
# Exit with error if no config was loaded
if not configLoaded:
configFileLocations = '\n'.join(map(str, getDefaultConfigFileLocations()))
sys.exit('No config file found! Possible locations are:\n' + configFileLocations)
return yaml.load(yamlString, Loader=yaml.SafeLoader)
def getDefaultConfigFileLocations():
HOME = os.path.expanduser('~')
configFileLocations = [
'/etc/' + APP_NAME + '/' + CONFIG_NAME + '.yaml',
'/etc/' + APP_NAME + '/' + CONFIG_NAME + '.yml',
HOME + '/.' + APP_NAME + '/' + CONFIG_NAME + '.yaml',
HOME + '/.' + APP_NAME + '/' + CONFIG_NAME + '.yml',
HOME + '/.config/' + APP_NAME + '/' + CONFIG_NAME + '.yaml',
HOME + '/.config/' + APP_NAME + '/' + CONFIG_NAME + '.yml',
HOME + '/' + CONFIG_NAME + '.yaml',
HOME + '/' + CONFIG_NAME + '.yml',
'./' + CONFIG_NAME + '.yaml',
'./' + CONFIG_NAME + '.yml'
]
return configFileLocations
def doRequest(url, token, title, message, priority):
try:
spinner = Halo(text='Connected...', color='cyan')
spinner.start()
time.sleep(1)
spinner.text = 'Sending Message...'
requestURL = urljoin(url, '/message?token=' + token)
time.sleep(2)
resp = requests.post(requestURL, json={
'title': title,
'message': message,
'priority': priority
})
spinner.stop()
except requests.ConnectionError as e:
spinner.stop()
print('Oops - Enter a Valid URL')
except requests.exceptions.RequestException as e:
spinner.stop()
# Print exception if reqeuest fails
sys.exit('Could not connect to Gotify server:\n' + str(e))
except (KeyboardInterrupt, SystemExit):
spinner.stop()
print("Ok ok, quitting")
sys.exit(1)
else:
# Print request result if server returns http error code
if resp.status_code is not requests.codes['ok']:
sys.exit(bytes.decode(resp.content))
else:
print('Message Sent Successfully')
if __name__ == "__main__":
main()
requests>=2.20.0
PyYAML>=5.1
halo>=0.0.23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment