# -*- coding: utf-8 -*- # Copyright (C) 2019 Chris Caron # All rights reserved. # # This code is licensed under the MIT License. # # 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 : # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # 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. import click import logging import platform import sys from os.path import isfile from os.path import expanduser from os.path import expandvars from . import NotifyType from . import Apprise from . import AppriseAsset from . import AppriseConfig from .utils import parse_list from .common import NOTIFY_TYPES from .logger import logger from . import __title__ from . import __version__ from . import __license__ from . import __copywrite__ # Defines our click context settings adding -h to the additional options that # can be specified to get the help menu to come up CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) # Define our default configuration we use if nothing is otherwise specified DEFAULT_SEARCH_PATHS = ( '~/.apprise', '~/.apprise.yml', '~/.config/apprise', '~/.config/apprise.yml', ) # Detect Windows if platform.system() == 'Windows': # Default Search Path for Windows Users DEFAULT_SEARCH_PATHS = ( expandvars('%APPDATA%/Apprise/apprise'), expandvars('%APPDATA%/Apprise/apprise.yml'), expandvars('%LOCALAPPDATA%/Apprise/apprise'), expandvars('%LOCALAPPDATA%/Apprise/apprise.yml'), ) def print_help_msg(command): """ Prints help message when -h or --help is specified. """ with click.Context(command) as ctx: click.echo(command.get_help(ctx)) def print_version_msg(): """ Prints version message when -V or --version is specified. """ result = list() result.append('{} v{}'.format(__title__, __version__)) result.append(__copywrite__) result.append( 'This code is licensed under the {} License.'.format(__license__)) click.echo('\n'.join(result)) @click.command(context_settings=CONTEXT_SETTINGS) @click.option('--body', '-b', default=None, type=str, help='Specify the message body. If no body is specified then ' 'content is read from .') @click.option('--title', '-t', default=None, type=str, help='Specify the message title. This field is complete ' 'optional.') @click.option('--config', '-c', default=None, type=str, multiple=True, metavar='CONFIG_URL', help='Specify one or more configuration locations.') @click.option('--attach', '-a', default=None, type=str, multiple=True, metavar='ATTACHMENT_URL', help='Specify one or more configuration locations.') @click.option('--notification-type', '-n', default=NotifyType.INFO, type=str, metavar='TYPE', help='Specify the message type (default=info). Possible values' ' are "{}", and "{}".'.format( '", "'.join(NOTIFY_TYPES[:-1]), NOTIFY_TYPES[-1])) @click.option('--theme', '-T', default='default', type=str, metavar='THEME', help='Specify the default theme.') @click.option('--tag', '-g', default=None, type=str, multiple=True, metavar='TAG', help='Specify one or more tags to filter ' 'which services to notify. Use multiple --tag (-g) entries to ' '"OR" the tags together and comma separated to "AND" them. ' 'If no tags are specified then all services are notified.') @click.option('--dry-run', '-d', is_flag=True, help='Perform a trial run but only prints the notification ' 'services to-be triggered to stdout. Notifications are never ' 'sent using this mode.') @click.option('--verbose', '-v', count=True, help='Makes the operation more talkative. Use multiple v to ' 'increase the verbosity. I.e.: -vvvv') @click.option('--version', '-V', is_flag=True, help='Display the apprise version and exit.') @click.argument('urls', nargs=-1, metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',) def main(body, title, config, attach, urls, notification_type, theme, tag, dry_run, verbose, version): """ Send a notification to all of the specified servers identified by their URLs the content provided within the title, body and notification-type. For a list of all of the supported services and information on how to use them, check out at https://github.com/caronc/apprise """ # Note: Click ignores the return values of functions it wraps, If you # want to return a specific error code, you must call sys.exit() # as you will see below. # Logging ch = logging.StreamHandler(sys.stdout) if verbose > 3: # -vvvv: Most Verbose Debug Logging logger.setLevel(logging.TRACE) elif verbose > 2: # -vvv: Debug Logging logger.setLevel(logging.DEBUG) elif verbose > 1: # -vv: INFO Messages logger.setLevel(logging.INFO) elif verbose > 0: # -v: WARNING Messages logger.setLevel(logging.WARNING) else: # No verbosity means we display ERRORS only AND any deprecation # warnings logger.setLevel(logging.ERROR) # Format our logger formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) if version: print_version_msg() sys.exit(0) # Prepare our asset asset = AppriseAsset(theme=theme) # Create our object a = Apprise(asset=asset) # Load our configuration if no URLs or specified configuration was # identified on the command line a.add(AppriseConfig( paths=[f for f in DEFAULT_SEARCH_PATHS if isfile(expanduser(f))] if not (config or urls) else config), asset=asset) # Load our inventory up for url in urls: a.add(url) if len(a) == 0: logger.error( 'You must specify at least one server URL or populated ' 'configuration file.') print_help_msg(main) sys.exit(1) # each --tag entry comprises of a comma separated 'and' list # we or each of of the --tag and sets specified. tags = None if not tag else [parse_list(t) for t in tag] if not dry_run: if body is None: logger.trace('No --body (-b) specified; reading from stdin') # if no body was specified, then read from STDIN body = click.get_text_stream('stdin').read() # now print it out result = a.notify( body=body, title=title, notify_type=notification_type, tag=tags, attach=attach) else: # Number of rows to assume in the terminal. In future, maybe this can # be detected and made dynamic. The actual row count is 80, but 5 # characters are already reserved for the counter on the left rows = 75 # Initialize our URL response; This is populated within the for/loop # below; but plays a factor at the end when we need to determine if # we iterated at least once in the loop. url = None for idx, server in enumerate(a.find(tag=tags)): url = server.url(privacy=True) click.echo("{: 3d}. {}".format( idx + 1, url if len(url) <= rows else '{}...'.format(url[:rows - 3]))) if server.tags: click.echo("{} - {}".format(' ' * 5, ', '.join(server.tags))) # Initialize a default response of nothing matched, otherwise # if we matched at least one entry, we can return True result = None if url is None else True if result is None: # There were no notifications set. This is a result of just having # empty configuration files and/or being to restrictive when filtering # by specific tag(s) sys.exit(2) elif result is False: # At least 1 notification service failed to send sys.exit(1) # else: We're good! sys.exit(0)