Browse Source

initial commit

pull/5/head
Chris Caron 7 years ago
parent
commit
a8f57a8b6e
  1. 63
      .gitignore
  2. 15
      .travis.yml
  3. 6
      MANIFEST.in
  4. 49
      README.md
  5. 175
      apprise/Apprise.py
  6. 350
      apprise/Utils.py
  7. 30
      apprise/__init__.py
  8. 445
      apprise/plugins/NotifyBase.py
  9. 178
      apprise/plugins/NotifyBoxcar.py
  10. 317
      apprise/plugins/NotifyEmail.py
  11. 123
      apprise/plugins/NotifyFaast.py
  12. 193
      apprise/plugins/NotifyGrowl/NotifyGrowl.py
  13. 6
      apprise/plugins/NotifyGrowl/__init__.py
  14. 0
      apprise/plugins/NotifyGrowl/gntp/__init__.py
  15. 141
      apprise/plugins/NotifyGrowl/gntp/cli.py
  16. 77
      apprise/plugins/NotifyGrowl/gntp/config.py
  17. 511
      apprise/plugins/NotifyGrowl/gntp/core.py
  18. 25
      apprise/plugins/NotifyGrowl/gntp/errors.py
  19. 265
      apprise/plugins/NotifyGrowl/gntp/notifier.py
  20. 45
      apprise/plugins/NotifyGrowl/gntp/shim.py
  21. 4
      apprise/plugins/NotifyGrowl/gntp/version.py
  22. 136
      apprise/plugins/NotifyJSON.py
  23. 212
      apprise/plugins/NotifyJoin.py
  24. 172
      apprise/plugins/NotifyMatterMost.py
  25. 173
      apprise/plugins/NotifyMyAndroid.py
  26. 177
      apprise/plugins/NotifyProwl.py
  27. 167
      apprise/plugins/NotifyPushBullet.py
  28. 146
      apprise/plugins/NotifyPushalot.py
  29. 76
      apprise/plugins/NotifyPushjet.py
  30. 76
      apprise/plugins/NotifyPushjet/NotifyPushjet.py
  31. 6
      apprise/plugins/NotifyPushjet/__init__.py
  32. 6
      apprise/plugins/NotifyPushjet/pushjet/__init__.py
  33. 49
      apprise/plugins/NotifyPushjet/pushjet/errors.py
  34. 319
      apprise/plugins/NotifyPushjet/pushjet/pushjet.py
  35. 66
      apprise/plugins/NotifyPushjet/pushjet/utilities.py
  36. 222
      apprise/plugins/NotifyPushover.py
  37. 307
      apprise/plugins/NotifyRocketChat.py
  38. 287
      apprise/plugins/NotifySlack.py
  39. 412
      apprise/plugins/NotifyTelegram.py
  40. 155
      apprise/plugins/NotifyToasty.py
  41. 116
      apprise/plugins/NotifyTwitter/NotifyTwitter.py
  42. 6
      apprise/plugins/NotifyTwitter/__init__.py
  43. 25
      apprise/plugins/NotifyTwitter/tweepy/__init__.py
  44. 1348
      apprise/plugins/NotifyTwitter/tweepy/api.py
  45. 178
      apprise/plugins/NotifyTwitter/tweepy/auth.py
  46. 256
      apprise/plugins/NotifyTwitter/tweepy/binder.py
  47. 435
      apprise/plugins/NotifyTwitter/tweepy/cache.py
  48. 214
      apprise/plugins/NotifyTwitter/tweepy/cursor.py
  49. 34
      apprise/plugins/NotifyTwitter/tweepy/error.py
  50. 493
      apprise/plugins/NotifyTwitter/tweepy/models.py
  51. 109
      apprise/plugins/NotifyTwitter/tweepy/parsers.py
  52. 466
      apprise/plugins/NotifyTwitter/tweepy/streaming.py
  53. 58
      apprise/plugins/NotifyTwitter/tweepy/utils.py
  54. 221
      apprise/plugins/NotifyXBMC.py
  55. 154
      apprise/plugins/NotifyXML.py
  56. 50
      apprise/plugins/__init__.py
  57. 22
      apprise/var/NotifyXML-1.0.xsd
  58. BIN
      apprise/var/apprise-failure-128x128.png
  59. BIN
      apprise/var/apprise-failure-256x256.png
  60. BIN
      apprise/var/apprise-failure-72x72.png
  61. BIN
      apprise/var/apprise-info-128x128.png
  62. BIN
      apprise/var/apprise-info-256x256.png
  63. BIN
      apprise/var/apprise-info-72x72.png
  64. BIN
      apprise/var/apprise-success-128x128.png
  65. BIN
      apprise/var/apprise-success-256x256.png
  66. BIN
      apprise/var/apprise-success-72x72.png
  67. BIN
      apprise/var/apprise-warning-128x128.png
  68. BIN
      apprise/var/apprise-warning-256x256.png
  69. BIN
      apprise/var/apprise-warning-72x72.png
  70. 26
      bin/apprise.py
  71. 9
      requirements.txt
  72. 4
      setup.cfg
  73. 64
      setup.py
  74. 12
      test/test_api.py

63
.gitignore vendored

@ -0,0 +1,63 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# vi swap files
.*.sw?
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints

15
.travis.yml

@ -0,0 +1,15 @@
language: python
python:
- "2.7"
install:
- pip install .
- pip install coveralls
- pip install -r requirements.txt
after_success:
- coveralls
# run tests
script: nosetests --with-coverage --cover-package=apprise

6
MANIFEST.in

@ -0,0 +1,6 @@
include LICENSE
include README.md
include requirements.txt
recursive-include test *
global-exclude *.pyc
global-exclude __pycache__

49
README.md

@ -0,0 +1,49 @@
<hr/>
**ap·prise** / *verb*<br/>
To inform or tell (someone). To make one aware of something.
<hr/>
*Apprise* allows you to take advantage of *just about* every notification service available to us today. Send a notification to almost all of the most popular services out there today (such as Telegram, Slack, Twitter, etc). The ones that don't exist can be adapted and supported too!
## Supported Notifications
The section identifies all of the services supported by this script.
### Popular Notification Services
The table below identifies the services this tool supports and some example service urls you need to use in order to take advantage of it.
| Notification Service | Service ID | Default Port | Example Syntax |
| -------------------- | ---------- | ------------ | -------------- |
| [Boxcar](https://github.com/caronc/apprise/wiki/Notify_boxcar) | boxcar:// | (TCP) 443 | boxcar://hostname<br />boxcar://hostname/@tag<br/>boxcar://hostname/device_token<br />boxcar://hostname/device_token1/device_token2/device_tokenN<br />boxcar://hostname/alias<br />boxcar://hostname/@tag/@tag2/alias/device_token
| [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken
| [Growl](https://github.com/caronc/apprise/wiki/Notify_growl) | growl:// | (UDP) 23053 | growl://hostname<br />growl://hostname:portno<br />growl://password@hostname<br />growl://password@hostname:port</br>_Note: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
| [Mattermost](https://github.com/caronc/apprise/wiki/Notify_mattermost) | mmost:// | (TCP) 8065 | mmost://hostname/authkey<br />mmost://hostname:80/authkey<br />mmost://user@hostname:80/authkey<br />mmost://hostname/authkey?channel=channel<br />mmosts://hostname/authkey<br />mmosts://user@hostname/authkey<br />
| [Notify My Android](https://github.com/caronc/apprise/wiki/Notify_my_android) | nma:// | (TCP) 443 | nma://apikey
| [Prowl](https://github.com/caronc/apprise/wiki/Notify_prowl) | prowl:// | (TCP) 443 | prowl://apikey<br />prowl://apikey/providerkey
| [Pushalot](https://github.com/caronc/apprise/wiki/Notify_pushalot) | palot:// | (TCP) 443 | palot://authorizationtoken
| [PushBullet](https://github.com/caronc/apprise/wiki/Notify_pushbullet) | pbul:// | (TCP) 443 | pbul://accesstoken<br />pbul://accesstoken/#channel<br/>pbul://accesstoken/A_DEVICE_ID<br />pbul://accesstoken/email@address.com<br />pbul://accesstoken/#channel/#channel2/email@address.net/DEVICE
| [Pushjet](https://github.com/caronc/apprise/wiki/Notify_pushjet) | pjet:// | (TCP) 80 | pjet://secret<br />pjet://secret@hostname<br />pjet://secret@hostname:port<br />pjets://secret@hostname<br />pjets://secret@hostname:port<br /><i>Note: if no hostname defined https://api.pushjet.io will be used
| [Pushover](https://github.com/caronc/apprise/wiki/Notify_pushover) | pover:// | (TCP) 443 | pover://user@token<br />pover://user@token/DEVICE<br />pover://user@token/DEVICE1/DEVICE2/DEVICEN<br />_Note: you must specify both your user_id and token_
| [Rocket.Chat](https://github.com/caronc/apprise/wiki/Notify_rocketchat) | rocket:// or rockets:// | (TCP) 80 or 443 | rocket://user:password@hostname/RoomID/Channel<br />rockets://user:password@hostname:443/Channel1/Channel1/RoomID<br />rocket://user:password@hostname/Channel
| [Slack](https://github.com/caronc/apprise/wiki/Notify_slack) | slack:// | (TCP) 443 | slack://TokenA/TokenB/TokenC/Channel<br />slack://botname@TokenA/TokenB/TokenC/Channel<br />slack://user@TokenA/TokenB/TokenC/Channel1/Channel2/ChannelN
| [Super Toasty](https://github.com/caronc/apprise/wiki/Notify_toasty) | toasty:// | (TCP) 80 | toasty://user@DEVICE<br />toasty://user@DEVICE1/DEVICE2/DEVICEN<br />_Note: you must specify both your user_id and at least 1 device!_
| [Telegram](https://github.com/caronc/apprise/wiki/Notify_telegram) | tgram:// | (TCP) 443 | tgram://bottoken/ChatID<br />tgram://bottoken/ChatID1/ChatID2/ChatIDN
| [Twitter](https://github.com/caronc/apprise/wiki/Notify_twitter) | tweet:// | (TCP) 443 | tweet://user@CKey/CSecret/AKey/ASecret
| [XBMC](https://github.com/caronc/apprise/wiki/Notify_xbmc) | xbmc:// or xbmcs:// | (TCP) 8080 or 443 | xbmc://hostname<br />xbmc://user@hostname<br />xbmc://user:password@hostname:port
### Email Support
| Service ID | Default Port | Example Syntax |
| ---------- | ------------ | -------------- |
| [mailto://](https://github.com/caronc/apprise/wiki/Notify_email) | (TCP) 25 | mailto://userid:pass@domain.com<br />mailto://domain.com?user=userid&pass=password<br/>mailto://domain.com:2525?user=userid&pass=password<br />mailto://user@gmail.com&pass=password<br />mailto://userid:password@example.com?smtp=mail.example.com&from=noreply@example.com&name=no%20reply
| [mailtos//](https://github.com/caronc/apprise/wiki/Notify_email) | (TCP) 587 | mailtos://userid:pass@domain.com<br />mailtos://domain.com?user=userid&pass=password<br/>mailtos://domain.com:465?user=userid&pass=password<br />mailtos://user@hotmail.com&pass=password<br />mailtos://userid:password@example.com?smtp=mail.example.com&from=noreply@example.com&name=no%20reply
Apprise have some email services built right into it (such as hotmail, gmail, etc) that greatly simplify the mailto:// service. See more details [here](https://github.com/caronc/apprise/wiki/Notify_email).
### Custom Notifications
| Post Method | Service ID | Default Port | Example Syntax |
| -------------------- | ---------- | ------------ | -------------- |
| [JSON](https://github.com/caronc/apprise/wiki/Notify_Custom_JSON) | json:// or jsons:// | (TCP) 80 or 443 | json://hostname<br />json://user@hostname<br />json://user:password@hostname:port<br />json://hostname/a/path/to/post/to
| [XML](https://github.com/caronc/apprise/wiki/Notify_Custom_XML) | xml:// or xmls:// | (TCP) 80 or 443 | xml://hostname<br />xml://user@hostname<br />xml://user:password@hostname:port<br />xml://hostname/a/path/to/post/to

175
apprise/Apprise.py

@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
import re
import logging
from . import plugins
from .Utils import parse_url
from .Utils import parse_list
from .Utils import parse_bool
logger = logging.getLogger(__name__)
# Build a list of supported plugins
SCHEMA_MAP = {}
# Load our Lookup Matrix
def __load_matrix():
"""
Dynamically load our schema map; this allows us to gracefully
skip over plugins we simply don't have the dependecies for.
"""
# to add it's mapping to our hash table
for entry in dir(plugins):
# Get our plugin
plugin = getattr(plugins, entry)
proto = getattr(plugin, 'PROTOCOL', None)
protos = getattr(plugin, 'SECURE_PROTOCOL', None)
if not proto:
# Must have at least PROTOCOL defined
continue
if proto not in SCHEMA_MAP:
SCHEMA_MAP[proto] = plugin
if protos and protos not in SCHEMA_MAP:
SCHEMA_MAP[protos] = plugin
# Dynamically build our module
__load_matrix()
class Apprise(object):
"""
Our Notification Manager
"""
def __init__(self, servers=None):
"""
Loads a set of server urls
"""
# Initialize a server list of URLs
self.servers = list()
if servers:
self.add(servers)
def add(self, servers, include_image=True, image_url=None,
image_path=None):
"""
Adds one or more server URLs into our list.
"""
servers = parse_list(servers)
for _server in servers:
# swap hash (#) tag values with their html version
# This is useful for accepting channels (as arguments to
# pushbullet)
_server = _server.replace('/#', '/%23')
# Parse our url details
# the server object is a dictionary containing all of the
# information parsed from our URL
server = parse_url(_server, default_schema='unknown')
# Initialize our return status
return_status = True
if not server:
# This is a dirty hack; but it's the only work around to
# tgram:// messages since the bot_token has a colon in it.
# It invalidates an normal URL.
# This hack searches for this bogus URL and corrects it
# so we can properly load it further down. The other
# alternative is to ask users to actually change the colon
# into a slash (which will work too), but it's more likely
# to cause confusion... So this is the next best thing
tgram = re.match(
r'(?P<protocol>%s://)(bot)?(?P<prefix>([a-z0-9_-]+)'
r'(:[a-z0-9_-]+)?@)?(?P<btoken_a>[0-9]+):+'
r'(?P<remaining>.*)$' % 'tgram',
_server, re.I)
if tgram:
if tgram.group('prefix'):
server = self.parse_url('%s%s%s/%s' % (
tgram.group('protocol'),
tgram.group('prefix'),
tgram.group('btoken_a'),
tgram.group('remaining'),
),
default_schema='unknown',
)
else:
server = self.parse_url('%s%s/%s' % (
tgram.group('protocol'),
tgram.group('btoken_a'),
tgram.group('remaining'),
),
default_schema='unknown',
)
if not server:
# Failed to parse te server
self.logger.error('Could not parse URL: %s' % server)
return_status = False
continue
# Some basic validation
if server['schema'] not in SCHEMA_MAP:
self.logger.error(
'%s is not a supported server type.' %
server['schema'].upper(),
)
return_status = False
continue
notify_args = server.copy().items() + {
# Logger Details
'logger': self.logger,
# Base
'include_image': include_image,
'secure': (server['schema'][-1] == 's'),
# Support SSL Certificate 'verify' keyword
# Default to being enabled (True)
'verify': parse_bool(server['qsd'].get('verify', True)),
# Overrides
'override_image_url': image_url,
'override_image_path': image_path,
}.items()
# Grant our plugin access to manipulate the dictionary
if not SCHEMA_MAP[server['schema']].pre_parse(notify_args):
# the arguments are invalid or can not be used.
return_status = False
continue
# Add our entry to our list as it can be actioned at this point
self.servers.add(notify_args)
# Return our status
return return_status
def clear(self, urls):
"""
Empties our server list
"""
self.servers.clear()
def notify(self, title='', body=''):
"""
Notifies all loaded servers using the content provided.
"""
# TODO: iterate over server entries and execute notification

350
apprise/Utils.py

@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
#
# A simple collection of general functions
#
# Copyright (C) 2017 Chris Caron <lead2gold@gmail.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
import re
from os.path import expanduser
from urlparse import urlparse
from urlparse import parse_qsl
from urllib import quote
from urllib import unquote
import logging
logger = logging.getLogger(__name__)
# URL Indexing Table for returns via parse_url()
VALID_URL_RE = re.compile(r'^[\s]*([^:\s]+):[/\\]*([^?]+)(\?(.+))?[\s]*$')
VALID_HOST_RE = re.compile(r'^[\s]*([^:/\s]+)')
VALID_QUERY_RE = re.compile(r'^(.*[/\\])([^/\\]*)$')
# delimiters used to separate values when content is passed in by string.
# This is useful when turning a string into a list
STRING_DELIMITERS = r'[\[\]\;,\s]+'
# Pre-Escape content since we reference it so much
ESCAPED_PATH_SEPARATOR = re.escape('\\/')
ESCAPED_WIN_PATH_SEPARATOR = re.escape('\\')
ESCAPED_NUX_PATH_SEPARATOR = re.escape('/')
TIDY_WIN_PATH_RE = re.compile(
'(^[%s]{2}|[^%s\s][%s]|[\s][%s]{2}])([%s]+)' % (
ESCAPED_WIN_PATH_SEPARATOR,
ESCAPED_WIN_PATH_SEPARATOR,
ESCAPED_WIN_PATH_SEPARATOR,
ESCAPED_WIN_PATH_SEPARATOR,
ESCAPED_WIN_PATH_SEPARATOR,
),
)
TIDY_WIN_TRIM_RE = re.compile(
'^(.+[^:][^%s])[\s%s]*$' % (
ESCAPED_WIN_PATH_SEPARATOR,
ESCAPED_WIN_PATH_SEPARATOR,
),
)
TIDY_NUX_PATH_RE = re.compile(
'([%s])([%s]+)' % (
ESCAPED_NUX_PATH_SEPARATOR,
ESCAPED_NUX_PATH_SEPARATOR,
),
)
TIDY_NUX_TRIM_RE = re.compile(
'([^%s])[\s%s]+$' % (
ESCAPED_NUX_PATH_SEPARATOR,
ESCAPED_NUX_PATH_SEPARATOR,
),
)
def tidy_path(path):
"""take a filename and or directory and attempts to tidy it up by removing
trailing slashes and correcting any formatting issues.
For example: ////absolute//path// becomes:
/absolute/path
"""
# Windows
path = TIDY_WIN_PATH_RE.sub('\\1', path.strip())
# Linux
path = TIDY_NUX_PATH_RE.sub('\\1', path.strip())
# Linux Based Trim
path = TIDY_NUX_TRIM_RE.sub('\\1', path.strip())
# Windows Based Trim
path = expanduser(TIDY_WIN_TRIM_RE.sub('\\1', path.strip()))
return path
def parse_url(url, default_schema='http'):
"""A function that greatly simplifies the parsing of a url
specified by the end user.
Valid syntaxes are:
<schema>://<user>@<host>:<port>/<path>
<schema>://<user>:<passwd>@<host>:<port>/<path>
<schema>://<host>:<port>/<path>
<schema>://<host>/<path>
<schema>://<host>
Argument parsing is also supported:
<schema>://<user>@<host>:<port>/<path>?key1=val&key2=val2
<schema>://<user>:<passwd>@<host>:<port>/<path>?key1=val&key2=val2
<schema>://<host>:<port>/<path>?key1=val&key2=val2
<schema>://<host>/<path>?key1=val&key2=val2
<schema>://<host>?key1=val&key2=val2
The function returns a simple dictionary with all of
the parsed content within it and returns 'None' if the
content could not be extracted.
"""
if not isinstance(url, basestring):
# Simple error checking
return None
# Default Results
result = {
# The username (if specified)
'user': None,
# The password (if specified)
'password': None,
# The port (if specified)
'port': None,
# The hostname
'host': None,
# The full path (query + path)
'fullpath': None,
# The path
'path': None,
# The query
'query': None,
# The schema
'schema': None,
# The schema
'url': None,
# The arguments passed in (the parsed query)
# This is in a dictionary of {'key': 'val', etc }
# qsd = Query String Dictionary
'qsd': {}
}
qsdata = ''
match = VALID_URL_RE.search(url)
if match:
# Extract basic results
result['schema'] = match.group(1).lower().strip()
host = match.group(2).strip()
try:
qsdata = match.group(4).strip()
except AttributeError:
# No qsdata
pass
else:
match = VALID_HOST_RE.search(url)
if not match:
return None
result['schema'] = default_schema
host = match.group(1).strip()
if not result['schema']:
result['schema'] = default_schema
if not host:
# Invalid Hostname
return None
# Now do a proper extraction of data
parsed = urlparse('http://%s' % host)
# Parse results
result['host'] = parsed[1].strip()
result['fullpath'] = quote(unquote(tidy_path(parsed[2].strip())))
try:
# Handle trailing slashes removed by tidy_path
if result['fullpath'][-1] not in ('/', '\\') and \
url[-1] in ('/', '\\'):
result['fullpath'] += url.strip()[-1]
except IndexError:
# No problem, there simply isn't any returned results
# and therefore, no trailing slash
pass
# Parse Query Arugments ?val=key&key=val
# while ensureing that all keys are lowercase
if qsdata:
result['qsd'] = dict([(k.lower().strip(), v.strip())
for k, v in parse_qsl(
qsdata,
keep_blank_values=True,
strict_parsing=False,
)])
if not result['fullpath']:
# Default
result['fullpath'] = None
else:
# Using full path, extract query from path
match = VALID_QUERY_RE.search(result['fullpath'])
if match:
result['path'] = match.group(1)
result['query'] = match.group(2)
if not result['path']:
result['path'] = None
if not result['query']:
result['query'] = None
try:
(result['user'], result['host']) = \
re.split('[\s@]+', result['host'])[:2]
except ValueError:
# no problem then, host only exists
# and it's already assigned
pass
if result['user'] is not None:
try:
(result['user'], result['password']) = \
re.split('[:\s]+', result['user'])[:2]
except ValueError:
# no problem then, user only exists
# and it's already assigned
pass
try:
(result['host'], result['port']) = \
re.split('[\s:]+', result['host'])[:2]
except ValueError:
# no problem then, user only exists
# and it's already assigned
pass
if result['port']:
try:
result['port'] = int(result['port'])
except (ValueError, TypeError):
# Invalid Port Specified
return None
if result['port'] == 0:
result['port'] = None
# Re-assemble cleaned up version of the url
result['url'] = '%s://' % result['schema']
if isinstance(result['user'], basestring):
result['url'] += result['user']
if isinstance(result['password'], basestring):
result['url'] += ':%s@' % result['password']
else:
result['url'] += '@'
result['url'] += result['host']
if result['port']:
result['url'] += ':%d' % result['port']
if result['fullpath']:
result['url'] += result['fullpath']
return result
def parse_bool(arg, default=False):
"""
NZBGet uses 'yes' and 'no' as well as other strings such as 'on' or
'off' etch to handle boolean operations from it's control interface.
This method can just simplify checks to these variables.
If the content could not be parsed, then the default is returned.
"""
if isinstance(arg, basestring):
# no = no - False
# of = short for off - False
# 0 = int for False
# fa = short for False - False
# f = short for False - False
# n = short for No or Never - False
# ne = short for Never - False
# di = short for Disable(d) - False
# de = short for Deny - False
if arg.lower()[0:2] in (
'de', 'di', 'ne', 'f', 'n', 'no', 'of', '0', 'fa'):
return False
# ye = yes - True
# on = short for off - True
# 1 = int for True
# tr = short for True - True
# t = short for True - True
# al = short for Always (and Allow) - True
# en = short for Enable(d) - True
elif arg.lower()[0:2] in (
'en', 'al', 't', 'y', 'ye', 'on', '1', 'tr'):
return True
# otherwise
return default
# Handle other types
return bool(arg)
def parse_list(*args):
"""
Take a string list and break it into a delimited
list of arguments. This funciton also supports
the processing of a list of delmited strings and will
always return a unique set of arguments. Duplicates are
always combined in the final results.
You can append as many items to the argument listing for
parsing.
Hence: parse_list('.mkv, .iso, .avi') becomes:
['.mkv', '.iso', '.avi']
Hence: parse_list('.mkv, .iso, .avi', ['.avi', '.mp4']) becomes:
['.mkv', '.iso', '.avi', '.mp4']
The parsing is very forgiving and accepts spaces, slashes, commas
semicolons, and pipes as delimiters
"""
result = []
for arg in args:
if isinstance(arg, basestring):
result += re.split(STRING_DELIMITERS, arg)
elif isinstance(arg, (list, tuple)):
for _arg in arg:
if isinstance(arg, basestring):
result += re.split(STRING_DELIMITERS, arg)
# A list inside a list? - use recursion
elif isinstance(_arg, (list, tuple)):
result += parse_list(_arg)
else:
# Convert whatever it is to a string and work with it
result += parse_list(str(_arg))
else:
# Convert whatever it is to a string and work with it
result += parse_list(str(arg))
# apply as well as make the list unique by converting it
# to a set() first. filter() eliminates any empty entries
return filter(bool, list(set(result)))

30
apprise/__init__.py

@ -0,0 +1,30 @@
# -*- encoding: utf-8 -*-
#
# Supported Push Notifications Libraries
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from .Apprise import Apprise
__version__ = '0.0.1'
__author__ = 'Chris Caron <lead2gold@gmail.com>'
__all__ = [
# Core
'Apprise',
]

445
apprise/plugins/NotifyBase.py

@ -0,0 +1,445 @@
# -*- encoding: utf-8 -*-
#
# Base Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from time import sleep
import re
import markdown
import logging
from os.path import join
from os.path import dirname
from os.path import abspath
# For conversion
from chardet import detect as chardet_detect
# Define a general HTML Escaping
try:
# use sax first because it's faster
from xml.sax.saxutils import escape as sax_escape
def _escape(text):
"""
saxutil escape tool
"""
return sax_escape(text, {"'": "&apos;", "\"": "&quot;"})
except ImportError:
# if we can't, then fall back to cgi escape
from cgi import escape as cgi_escape
def _escape(text):
"""
cgi escape tool
"""
return cgi_escape(text, quote=True)
class NotifyType(object):
INFO = 'info'
SUCCESS = 'success'
FAILURE = 'failure'
WARNING = 'warning'
# Most Servers do not like more then 1 request per 5 seconds,
# so 5.5 gives us a safe play range...
NOTIFY_THROTTLE_SEC = 5.5
NOTIFY_TYPES = (
NotifyType.INFO,
NotifyType.SUCCESS,
NotifyType.FAILURE,
NotifyType.WARNING,
)
# A Simple Mapping of Colors; For every NOTIFY_TYPE identified,
# there should be a mapping to it's color here:
HTML_NOTIFY_MAP = {
NotifyType.INFO: '#3AA3E3',
NotifyType.SUCCESS: '#3AA337',
NotifyType.FAILURE: '#A32037',
NotifyType.WARNING: '#CACF29',
}
class NotifyImageSize(object):
XY_72 = '72x72'
XY_128 = '128x128'
XY_256 = '256x256'
NOTIFY_IMAGE_SIZES = (
NotifyImageSize.XY_72,
NotifyImageSize.XY_128,
NotifyImageSize.XY_256,
)
HTTP_ERROR_MAP = {
400: 'Bad Request - Unsupported Parameters.',
401: 'Verification Failed.',
404: 'Page not found.',
405: 'Method not allowed.',
500: 'Internal server error.',
503: 'Servers are overloaded.',
}
# Application Identifier
NOTIFY_APPLICATION_ID = 'apprise'
NOTIFY_APPLICATION_DESC = 'Apprise Notifications'
# Image Control
NOTIFY_IMAGE_URL = \
'http://nuxref.com/apprise/apprise-{TYPE}-{XY}.png'
NOTIFY_IMAGE_FILE = abspath(join(
dirname(__file__),
'var',
'apprise-{TYPE}-{XY}.png',
))
# HTML New Line Delimiter
NOTIFY_NEWLINE = '\r\n'
class NotifyFormat(object):
TEXT = 'text'
HTML = 'html'
NOTIFY_FORMATS = (
NotifyFormat.TEXT,
NotifyFormat.HTML,
)
# Regular expression retrieved from:
# http://www.regular-expressions.info/email.html
IS_EMAIL_RE = re.compile(
r"(?P<userid>[a-z0-9!#$%&'*+/=?^_`{|}~-]+"
r"(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)"
r"*)@(?P<domain>(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+"
r"[a-z0-9](?:[a-z0-9-]*"
r"[a-z0-9]))?",
re.IGNORECASE,
)
class NotifyBase(object):
"""
This is the base class for all notification services
"""
# The default simple (insecure) protocol
# all inheriting entries must provide their protocol lookup
# protocol:// (in this example they would specify 'protocol')
PROTOCOL = ''
# The default secure protocol
# all inheriting entries must provide their protocol lookup
# protocols:// (in this example they would specify 'protocols')
# This value can be the same as the defined PROTOCOL.
SECURE_PROTOCOL = ''
def __init__(self, title_maxlen=100, body_maxlen=512,
notify_format=NotifyFormat.TEXT, image_size=None,
include_image=False, override_image_path=None,
secure=False, **kwargs):
"""
Initialize some general logging and common server arguments
that will keep things consistent when working with the
notifiers that will inherit this class
"""
# Logging
self.logger = logging.getLogger(__name__)
if notify_format.lower() not in NOTIFY_FORMATS:
self.logger.error(
'Invalid notification format %s' % notify_format,
)
raise TypeError(
'Invalid notification format %s' % notify_format,
)
if image_size and image_size not in NOTIFY_IMAGE_SIZES:
self.logger.error(
'Invalid image size %s' % image_size,
)
raise TypeError(
'Invalid image size %s' % image_size,
)
self.app_id = NOTIFY_APPLICATION_ID
self.app_desc = NOTIFY_APPLICATION_DESC
self.notify_format = notify_format.lower()
self.title_maxlen = title_maxlen
self.body_maxlen = body_maxlen
self.image_size = image_size
self.include_image = include_image
self.secure = secure
# Certificate Verification (for SSL calls); default to being enabled
self.verify_certificate = kwargs.get('verify', True)
self.host = kwargs.get('host', '')
self.port = kwargs.get('port')
if self.port:
try:
self.port = int(self.port)
except (TypeError, ValueError):
self.port = None
self.user = kwargs.get('user')
self.password = kwargs.get('password')
# Over-rides
self.override_image_url = kwargs.get('override_image_url')
self.override_image_path = kwargs.get('override_image_path')
def throttle(self, throttle_time=NOTIFY_THROTTLE_SEC):
"""
A common throttle control
"""
self.logger.debug('Throttling...')
sleep(throttle_time)
return
def image_url(self, notify_type):
"""
Returns Image URL if possible
"""
if self.override_image_url:
# Over-ride
return self.override_image_url
if not self.image_size:
return None
if notify_type not in NOTIFY_TYPES:
return None
re_map = {
'{TYPE}': notify_type,
'{XY}': self.image_size,
}
# Iterate over above list and store content accordingly
re_table = re.compile(
r'(' + '|'.join(re_map.keys()) + r')',
re.IGNORECASE,
)
return re_table.sub(lambda x: re_map[x.group()], NOTIFY_IMAGE_URL)
def image_raw(self, notify_type):
"""
Returns the raw image if it can
"""
if not self.override_image_path:
if not self.image_size:
return None
if notify_type not in NOTIFY_TYPES:
return None
re_map = {
'{TYPE}': notify_type,
'{XY}': self.image_size,
}
# Iterate over above list and store content accordingly
re_table = re.compile(
r'(' + '|'.join(re_map.keys()) + r')',
re.IGNORECASE,
)
# Now we open and return the file
_file = re_table.sub(
lambda x: re_map[x.group()], NOTIFY_IMAGE_FILE)
else:
# Override Path Specified
_file = self.override_image_path
try:
fd = open(_file, 'rb')
except:
return None
try:
return fd.read()
except:
return None
finally:
fd.close()
def escape_html(self, html, convert_new_lines=False):
"""
Takes html text as input and escapes it so that it won't
conflict with any xml/html wrapping characters.
"""
escaped = _escape(html).\
replace(u'\t', u'&emsp;').\
replace(u' ', u' &nbsp;')
if convert_new_lines:
return escaped.replace(u'\n', u'<br />')
return escaped
def to_utf8(self, content):
"""
Attempts to convert non-utf8 content to... (you guessed it) utf8
"""
if not content:
return ''
if isinstance(content, unicode):
return content.encode('utf-8')
result = chardet_detect(content)
encoding = result['encoding']
try:
content = content.decode(
encoding,
errors='replace',
)
return content.encode('utf-8')
except UnicodeError:
raise ValueError(
'%s contains invalid characters' % (
content))
except KeyError:
raise ValueError(
'%s encoding could not be detected ' % (
content))
except TypeError:
try:
content = content.decode(
encoding,
'replace',
)
return content.encode('utf-8')
except UnicodeError:
raise ValueError(
'%s contains invalid characters' % (
content))
except KeyError:
raise ValueError(
'%s encoding could not be detected ' % (
content))
return ''
def to_html(self, body):
"""
Returns the specified title in an html format and factors
in a titles defined max length
"""
html = markdown.markdown(body)
# TODO:
# This function should return multiple messages if we exceed
# the maximum number of characters. the second message should
# The new message should factor in the title and add ' cont...'
# to the end of it. It should also include the added characters
# put in place by the html characters. So there is a little bit
# of math and manipulation that needs to go on here.
# we always return a list
return [html, ]
def notify(self, title, body, notify_type=NotifyType.SUCCESS,
**kwargs):
"""
This should be over-rided by the class that
inherits this one.
"""
if notify_type and notify_type not in NOTIFY_TYPES:
self.warning(
'An invalid notification type (%s) was specified.' % (
notify_type))
if not isinstance(body, basestring):
body = ''
if not isinstance(title, basestring):
title = ''
# Ensure we're set up as UTF-8
title = self.to_utf8(title)
body = self.to_utf8(body)
if title:
title = title[0:self.title_maxlen]
if self.notify_format == NotifyFormat.HTML:
bodies = self.to_html(body=body)
elif self.notify_format == NotifyFormat.TEXT:
# TODO: this should split the content into
# multiple messages
bodies = [body[0:self.body_maxlen], ]
while len(bodies):
b = bodies.pop(0)
# Send Message(s)
if not self._notify(
title=title, body=b,
notify_type=notify_type,
**kwargs):
return False
# If we got here, we sent part of the notification
# if there are any left, we should throttle so we
# don't overload the server with requests (they
# might not be happy with us otherwise)
if len(bodies):
self.throttle()
return True
def pre_parse(self, url, server_settings):
"""
grants the ability to manipulate or additionally parse the content
provided in the server_settings variable.
Return True if you're satisfied with them (and may have additionally
changed them) and False if the settings are not acceptable or useable
Since this is the base class, plugins are not requird to overload it
but have the option to. By default the configuration is always
accepted.
"""
return True

178
apprise/plugins/NotifyBoxcar.py

@ -0,0 +1,178 @@
# -*- encoding: utf-8 -*-
#
# Boxcar Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from json import dumps
import requests
import re
# Used to break apart list of potential tags by their delimiter
# into a usable list.
TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
# Used to validate Tags, Aliases and Devices
IS_TAG = re.compile(r'^[A-Za-z0-9]{1,63}$')
IS_ALIAS = re.compile(r'^[@]?[A-Za-z0-9]+$')
IS_DEVICETOKEN = re.compile(r'^[A-Za-z0-9]{64}$')
class NotifyBoxcar(NotifyBase):
"""
A wrapper for Boxcar Notifications
"""
# The default simple (insecure) protocol
PROTOCOL = 'boxcar'
# The default secure protocol
SECURE_PROTOCOL = 'boxcars'
def __init__(self, recipients=None, **kwargs):
"""
Initialize Boxcar Object
"""
super(NotifyBoxcar, self).__init__(
title_maxlen=250, body_maxlen=10000,
notify_format=NotifyFormat.TEXT,
**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
# Initialize tag list
self.tags = list()
# Initialize alias list
self.aliases = list()
# Initialize device_token list
self.device_tokens = list()
if recipients is None:
recipients = []
elif isinstance(recipients, basestring):
recipients = filter(bool, TAGS_LIST_DELIM.split(
recipients,
))
elif not isinstance(recipients, (tuple, list)):
recipients = []
# Validate recipients and drop bad ones:
for recipient in recipients:
if IS_DEVICETOKEN.match(recipient):
# store valid device
self.device_tokens.append(recipient)
elif IS_TAG.match(recipient):
# store valid tag
self.tags.append(recipient)
elif IS_ALIAS.match(recipient):
# store valid tag/alias
self.aliases.append(recipient)
else:
self.logger.warning(
'Dropped invalid tag/alias/device_token '
'(%s) specified.' % recipient,
)
continue
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Boxcar Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
# prepare Boxcar Object
payload = {
'badge': 'auto',
'alert': '%s:\r\n%s' % (title, body),
}
if self.tags:
payload['tags'] = self.tags
if self.aliases:
payload['aliases'] = self.aliases
if self.device_tokens:
payload['device_tokens'] = self.device_tokens
auth = None
if self.user:
auth = (self.user, self.password)
url = '%s://%s' % (self.schema, self.host)
if isinstance(self.port, int):
url += ':%d' % self.port
url += '/api/push'
self.logger.debug('Boxcar POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('Boxcar Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
auth=auth,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
try:
self.logger.warning(
'Failed to send Boxcar notification: '
'%s (error=%s).' % (
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send Boxcar notification '
'(error=%s).' % (
r.status_code))
# Return; we're done
return False
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Boxcar '
'notification to %s.' % (
self.host))
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

317
apprise/plugins/NotifyEmail.py

@ -0,0 +1,317 @@
# -*- encoding: utf-8 -*-
#
# Email Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
import re
from datetime import datetime
from smtplib import SMTP
from smtplib import SMTPException
from socket import error as SocketError
from email.mime.text import MIMEText
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import IS_EMAIL_RE
# Default Non-Encryption Port
EMAIL_SMTP_PORT = 25
# Default Secure Port
EMAIL_SMTPS_PORT = 587
# Default SMTP Timeout (in seconds)
SMTP_SERVER_TIMEOUT = 30
class WebBaseLogin(object):
"""
This class is just used in conjunction of the default emailers
to best formulate a login to it using the data detected
"""
# User Login must be Email Based
EMAIL = 'Email'
# User Login must UserID Based
USERID = 'UserID'
# To attempt to make this script stupid proof,
# if we detect an email address that is part of the
# this table, we can pre-use a lot more defaults if
# they aren't otherwise specified on the users
# input
WEBBASE_LOOKUP_TABLE = (
# Google GMail
(
'Google Mail',
re.compile('^(?P<id>[^@]+)@(?P<domain>gmail\.com)$', re.I),
{
'port': 587,
'smtp_host': 'smtp.gmail.com',
'secure': True,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Pronto Mail
(
'Pronto Mail',
re.compile('^(?P<id>[^@]+)@(?P<domain>prontomail\.com)$', re.I),
{
'port': 465,
'smtp_host': 'secure.emailsrvr.com',
'secure': True,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Microsoft Hotmail
(
'Microsoft Hotmail',
re.compile('^(?P<id>[^@]+)@(?P<domain>(hotmail|live)\.com)$', re.I),
{
'port': 587,
'smtp_host': 'smtp.live.com',
'secure': True,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Yahoo Mail
(
'Yahoo Mail',
re.compile('^(?P<id>[^@]+)@(?P<domain>yahoo\.(ca|com))$', re.I),
{
'port': 465,
'smtp_host': 'smtp.mail.yahoo.com',
'secure': True,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Catch All
(
'Custom',
re.compile('^(?P<id>[^@]+)@(?P<domain>.+)$', re.I),
{
# Setting smtp_host to None is a way of
# auto-detecting it based on other parameters
# specified. There is no reason to ever modify
# this Catch All
'smtp_host': None,
},
),
)
# Mail Prefix Servers (TODO)
MAIL_SERVER_PREFIXES = (
'smtp', 'mail', 'smtps', 'outgoing'
)
class NotifyEmail(NotifyBase):
"""
A wrapper to Email Notifications
"""
# The default simple (insecure) protocol
PROTOCOL = 'mailto'
# The default secure protocol
SECURE_PROTOCOL = 'mailtos'
def __init__(self, to, notify_format, **kwargs):
"""
Initialize Email Object
"""
super(NotifyEmail, self).__init__(
title_maxlen=250, body_maxlen=32768,
notify_format=notify_format,
**kwargs)
# Store To Addr
self.to_addr = to
# Handle SMTP vs SMTPS (Secure vs UnSecure)
if not self.port:
if self.secure:
self.port = EMAIL_SMTPS_PORT
else:
self.port = EMAIL_SMTP_PORT
# Email SMTP Server Timeout
try:
self.timeout = int(kwargs.get('timeout', SMTP_SERVER_TIMEOUT))
except (ValueError, TypeError):
self.timeout = SMTP_SERVER_TIMEOUT
# Now we want to construct the To and From email
# addresses from the URL provided
self.from_name = kwargs.get('name', 'NZB Notification')
self.from_addr = kwargs.get('from', None)
if not self.from_addr:
# Keep trying to be clever and make it equal to the to address
self.from_addr = self.to_addr
if not isinstance(self.to_addr, basestring):
raise TypeError('No valid ~To~ email address specified.')
if not IS_EMAIL_RE.match(self.to_addr):
raise TypeError('Invalid ~To~ email format: %s' % self.to_addr)
if not isinstance(self.from_addr, basestring):
raise TypeError('No valid ~From~ email address specified.')
match = IS_EMAIL_RE.match(self.from_addr)
if not match:
# Parse Source domain based on from_addr
raise TypeError('Invalid ~From~ email format: %s' % self.to_addr)
# Now detect the SMTP Server
self.smtp_host = kwargs.get('smtp_host', None)
# Apply any defaults based on certain known configurations
self.NotifyEmailDefaults()
# Using the match, we want to extract the user id and domain
return
def NotifyEmailDefaults(self):
"""
A function that prefills defaults based on the email
it was provided.
"""
if self.smtp_host:
# SMTP Server was explicitly specified, therefore it
# is assumed the caller knows what he's doing and
# is intentionally over-riding any smarts to be
# applied
return
for i in range(len(WEBBASE_LOOKUP_TABLE)):
self.logger.debug('Scanning %s against %s' % (
self.to_addr, WEBBASE_LOOKUP_TABLE[i][0]
))
match = WEBBASE_LOOKUP_TABLE[i][1].match(self.to_addr)
if match:
self.logger.info(
'Applying %s Defaults' %
WEBBASE_LOOKUP_TABLE[i][0],
)
self.port = WEBBASE_LOOKUP_TABLE[i][2]\
.get('port', self.port)
self.secure = WEBBASE_LOOKUP_TABLE[i][2]\
.get('secure', self.secure)
self.smtp_host = WEBBASE_LOOKUP_TABLE[i][2]\
.get('smtp_host', self.smtp_host)
if self.smtp_host is None:
# Detect Server if possible
self.smtp_host = re.split('[\s@]+', self.from_addr)[-1]
# Adjust email login based on the defined
# usertype
login_type = WEBBASE_LOOKUP_TABLE[i][2]\
.get('login_type', [])
if IS_EMAIL_RE.match(self.user) and \
WebBaseLogin.EMAIL not in login_type:
# Email specified but login type
# not supported; switch it to user id
self.user = match.group('id')
elif WebBaseLogin.USERID not in login_type:
# user specified but login type
# not supported; switch it to email
self.user = '%s@%s' % (self.user, self.host)
break
def _notify(self, title, body, **kwargs):
"""
Perform Email Notification
"""
self.logger.debug('Email From: %s <%s>' % (
self.from_addr, self.from_name))
self.logger.debug('Email To: %s' % (self.to_addr))
self.logger.debug('Login ID: %s' % (self.user))
self.logger.debug('Delivery: %s:%d' % (self.smtp_host, self.port))
# Prepare Email Message
if self.notify_format == NotifyFormat.HTML:
email = MIMEText(body, 'html')
email['Content-Type'] = 'text/html'
else:
email = MIMEText(body, 'text')
email['Content-Type'] = 'text/plain'
email['Subject'] = title
email['From'] = '%s <%s>' % (self.from_name, self.from_addr)
email['To'] = self.to_addr
email['Date'] = datetime.utcnow()\
.strftime("%a, %d %b %Y %H:%M:%S +0000")
email['X-Application'] = self.app_id
try:
self.logger.debug('Connecting to remote SMTP server...')
socket = SMTP(
self.smtp_host,
self.port,
None,
timeout=self.timeout,
)
if self.secure:
# Handle Secure Connections
self.logger.debug('Securing connection with TLS...')
socket.starttls()
if self.user and self.password:
# Apply Login credetials
self.logger.debug('Applying user credentials...')
socket.login(self.user, self.password)
# Send the email
socket.sendmail(self.from_addr, self.to_addr, email.as_string())
self.logger.info('Sent Email notification to "%s".' % (
self.to_addr,
))
except (SocketError, SMTPException), e:
self.logger.warning(
'A Connection error occured sending Email '
'notification to %s.' % self.smtp_host)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
try:
socket.quit()
except:
# no problem
pass
return True

123
apprise/plugins/NotifyFaast.py

@ -0,0 +1,123 @@
# -*- encoding: utf-8 -*-
#
# Faast Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
import requests
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import NotifyImageSize
from .NotifyBase import HTTP_ERROR_MAP
# Faast uses the http protocol with JSON requests
FAAST_URL = 'https://www.appnotifications.com/account/notifications.json'
# Image Support (72x72)
FAAST_IMAGE_XY = NotifyImageSize.XY_72
class NotifyFaast(NotifyBase):
"""
A wrapper for Faast Notifications
"""
# The default protocol (this is secure for faast)
PROTOCOL = 'faast'
# The default secure protocol
SECURE_PROTOCOL = 'faast'
def __init__(self, authtoken, **kwargs):
"""
Initialize Faast Object
"""
super(NotifyFaast, self).__init__(
title_maxlen=250, body_maxlen=32768,
image_size=FAAST_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
self.authtoken = authtoken
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Faast Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'multipart/form-data'
}
# prepare JSON Object
payload = {
'user_credentials': self.authtoken,
'title': title,
'message': body,
}
if self.include_image:
image_url = self.image_url(
notify_type,
)
if image_url:
payload['icon_url'] = image_url
self.logger.debug('Faast POST URL: %s (cert_verify=%r)' % (
FAAST_URL, self.verify_certificate,
))
self.logger.debug('Faast Payload: %s' % str(payload))
try:
r = requests.post(
FAAST_URL,
data=payload,
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Faast notification: '
'%s (error=%s).' % (
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Faast notification '
'(error=%s).' % (
r.status_code))
# Return; we're done
return False
else:
self.logger.info('Sent Faast notification.')
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Faast notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

193
apprise/plugins/NotifyGrowl/NotifyGrowl.py

@ -0,0 +1,193 @@
# -*- encoding: utf-8 -*-
#
# Growl Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from ..NotifyBase import NotifyBase
from ..NotifyBase import NotifyFormat
from ..NotifyBase import NotifyImageSize
from .gntp.notifier import GrowlNotifier
from .gntp.errors import NetworkError as GrowlNetworkError
from .gntp.errors import AuthError as GrowlAuthenticationError
# Default Growl Port
GROWL_UDP_PORT = 23053
# Image Support (72x72)
GROWL_IMAGE_XY = NotifyImageSize.XY_72
# Priorities
class GrowlPriority(object):
VERY_LOW = -2
MODERATE = -1
NORMAL = 0
HIGH = 1
EMERGENCY = 2
GROWL_PRIORITIES = (
GrowlPriority.VERY_LOW,
GrowlPriority.MODERATE,
GrowlPriority.NORMAL,
GrowlPriority.HIGH,
GrowlPriority.EMERGENCY,
)
GROWL_NOTIFICATION_TYPE = "New Messages"
class NotifyGrowl(NotifyBase):
"""
A wrapper to Growl Notifications
"""
# The default protocol
PROTOCOL = 'growl'
# The default secure protocol
SECURE_PROTOCOL = 'growl'
def __init__(self, priority=GrowlPriority.NORMAL, version=2, **kwargs):
"""
Initialize Growl Object
"""
super(NotifyGrowl, self).__init__(
title_maxlen=250, body_maxlen=32768,
image_size=GROWL_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
# A Global flag that tracks registration
self.is_registered = False
if not self.port:
self.port = GROWL_UDP_PORT
# The Priority of the message
if priority not in GROWL_PRIORITIES:
self.priority = GrowlPriority.NORMAL
else:
self.priority = priority
# Always default the sticky flag to False
self.sticky = False
# Store Version
self.version = version
payload = {
'applicationName': self.app_id,
'notifications': [GROWL_NOTIFICATION_TYPE, ],
'defaultNotifications': [GROWL_NOTIFICATION_TYPE, ],
'hostname': self.host,
'port': self.port,
}
if self.password is not None:
payload['password'] = self.password
self.logger.debug('Growl Registration Payload: %s' % str(payload))
self.growl = GrowlNotifier(**payload)
try:
self.growl.register()
# Toggle our flag
self.is_registered = True
self.logger.debug(
'Growl server registration completed successfully.'
)
except GrowlNetworkError:
self.logger.warning(
'A network error occured sending Growl '
'notification to %s.' % self.host)
return
except GrowlAuthenticationError:
self.logger.warning(
'An authentication error occured sending Growl '
'notification to %s.' % self.host)
return
return
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Growl Notification
"""
if not self.is_registered:
# We can't do anything
return None
icon = None
if self.include_image:
if self.version >= 2:
# URL Based
icon = self.image_url(notify_type)
else:
# Raw
icon = self.image_raw(notify_type)
payload = {
'noteType': GROWL_NOTIFICATION_TYPE,
'title': title,
'description': body,
'icon': icon is not None,
'sticky': False,
'priority': self.priority,
}
self.logger.debug('Growl Payload: %s' % str(payload))
# Update icon of payload to be raw data
payload['icon'] = icon
try:
response = self.growl.notify(**payload)
if not isinstance(response, bool):
self.logger.warning(
'Growl notification failed to send with response: %s' %
str(response),
)
else:
self.logger.debug(
'Growl notification sent successfully.'
)
except GrowlNetworkError as e:
# Since Growl servers listen for UDP broadcasts,
# it's possible that you will never get to this part
# of the code since there is no acknowledgement as to
# whether it accepted what was sent to it or not.
# however, if the host/server is unavailable, you will
# get to this point of the code.
self.logger.warning(
'A Connection error occured sending Growl '
'notification to %s.' % self.host)
self.logger.debug('Growl Exception: %s' % str(e))
# Return; we're done
return False
return True

6
apprise/plugins/NotifyGrowl/__init__.py

@ -0,0 +1,6 @@
# -*- encoding: utf-8 -*-
from . import NotifyGrowl
__all__ = [
'NotifyGrowl',
]

0
apprise/plugins/NotifyGrowl/gntp/__init__.py

141
apprise/plugins/NotifyGrowl/gntp/cli.py

@ -0,0 +1,141 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
import logging
import os
import sys
from optparse import OptionParser, OptionGroup
from .notifier import GrowlNotifier
from .shim import RawConfigParser
from .version import __version__
DEFAULT_CONFIG = os.path.expanduser('~/.gntp')
config = RawConfigParser({
'hostname': 'localhost',
'password': None,
'port': 23053,
})
config.read([DEFAULT_CONFIG])
if not config.has_section('gntp'):
config.add_section('gntp')
class ClientParser(OptionParser):
def __init__(self):
OptionParser.__init__(self, version="%%prog %s" % __version__)
group = OptionGroup(self, "Network Options")
group.add_option("-H", "--host",
dest="host", default=config.get('gntp', 'hostname'),
help="Specify a hostname to which to send a remote notification. [%default]")
group.add_option("--port",
dest="port", default=config.getint('gntp', 'port'), type="int",
help="port to listen on [%default]")
group.add_option("-P", "--password",
dest='password', default=config.get('gntp', 'password'),
help="Network password")
self.add_option_group(group)
group = OptionGroup(self, "Notification Options")
group.add_option("-n", "--name",
dest="app", default='Python GNTP Test Client',
help="Set the name of the application [%default]")
group.add_option("-s", "--sticky",
dest='sticky', default=False, action="store_true",
help="Make the notification sticky [%default]")
group.add_option("--image",
dest="icon", default=None,
help="Icon for notification (URL or /path/to/file)")
group.add_option("-m", "--message",
dest="message", default=None,
help="Sets the message instead of using stdin")
group.add_option("-p", "--priority",
dest="priority", default=0, type="int",
help="-2 to 2 [%default]")
group.add_option("-d", "--identifier",
dest="identifier",
help="Identifier for coalescing")
group.add_option("-t", "--title",
dest="title", default=None,
help="Set the title of the notification [%default]")
group.add_option("-N", "--notification",
dest="name", default='Notification',
help="Set the notification name [%default]")
group.add_option("--callback",
dest="callback",
help="URL callback")
self.add_option_group(group)
# Extra Options
self.add_option('-v', '--verbose',
dest='verbose', default=0, action='count',
help="Verbosity levels")
def parse_args(self, args=None, values=None):
values, args = OptionParser.parse_args(self, args, values)
if values.message is None:
print('Enter a message followed by Ctrl-D')
try:
message = sys.stdin.read()
except KeyboardInterrupt:
exit()
else:
message = values.message
if values.title is None:
values.title = ' '.join(args)
# If we still have an empty title, use the
# first bit of the message as the title
if values.title == '':
values.title = message[:20]
values.verbose = logging.WARNING - values.verbose * 10
return values, message
def main():
(options, message) = ClientParser().parse_args()
logging.basicConfig(level=options.verbose)
if not os.path.exists(DEFAULT_CONFIG):
logging.info('No config read found at %s', DEFAULT_CONFIG)
growl = GrowlNotifier(
applicationName=options.app,
notifications=[options.name],
defaultNotifications=[options.name],
hostname=options.host,
password=options.password,
port=options.port,
)
result = growl.register()
if result is not True:
exit(result)
# This would likely be better placed within the growl notifier
# class but until I make _checkIcon smarter this is "easier"
if options.icon is not None and not options.icon.startswith('http'):
logging.info('Loading image %s', options.icon)
f = open(options.icon)
options.icon = f.read()
f.close()
result = growl.notify(
noteType=options.name,
title=options.title,
description=message,
icon=options.icon,
sticky=options.sticky,
priority=options.priority,
callback=options.callback,
identifier=options.identifier,
)
if result is not True:
exit(result)
if __name__ == "__main__":
main()

77
apprise/plugins/NotifyGrowl/gntp/config.py

@ -0,0 +1,77 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
The gntp.config module is provided as an extended GrowlNotifier object that takes
advantage of the ConfigParser module to allow us to setup some default values
(such as hostname, password, and port) in a more global way to be shared among
programs using gntp
"""
import logging
import os
from . import gntp.notifier
from . import gntp.shim
__all__ = [
'mini',
'GrowlNotifier'
]
logger = logging.getLogger(__name__)
class GrowlNotifier(gntp.notifier.GrowlNotifier):
"""
ConfigParser enhanced GrowlNotifier object
For right now, we are only interested in letting users overide certain
values from ~/.gntp
::
[gntp]
hostname = ?
password = ?
port = ?
"""
def __init__(self, *args, **kwargs):
config = gntp.shim.RawConfigParser({
'hostname': kwargs.get('hostname', 'localhost'),
'password': kwargs.get('password'),
'port': kwargs.get('port', 23053),
})
config.read([os.path.expanduser('~/.gntp')])
# If the file does not exist, then there will be no gntp section defined
# and the config.get() lines below will get confused. Since we are not
# saving the config, it should be safe to just add it here so the
# code below doesn't complain
if not config.has_section('gntp'):
logger.info('Error reading ~/.gntp config file')
config.add_section('gntp')
kwargs['password'] = config.get('gntp', 'password')
kwargs['hostname'] = config.get('gntp', 'hostname')
kwargs['port'] = config.getint('gntp', 'port')
super(GrowlNotifier, self).__init__(*args, **kwargs)
def mini(description, **kwargs):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
"""
kwargs['notifierFactory'] = GrowlNotifier
gntp.notifier.mini(description, **kwargs)
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
# so extra debugging is useful
logging.basicConfig(level=logging.INFO)
mini('Testing mini notification')

511
apprise/plugins/NotifyGrowl/gntp/core.py

@ -0,0 +1,511 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
import hashlib
import re
import time
from . import shim
from . import errors as errors
__all__ = [
'GNTPRegister',
'GNTPNotice',
'GNTPSubscribe',
'GNTPOK',
'GNTPError',
'parse_gntp',
]
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
GNTP_INFO_LINE = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
re.IGNORECASE
)
GNTP_INFO_LINE_SHORT = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
re.IGNORECASE
)
GNTP_HEADER = re.compile('([\w-]+):(.+)')
GNTP_EOL = shim.b('\r\n')
GNTP_SEP = shim.b(': ')
class _GNTPBuffer(shim.StringIO):
"""GNTP Buffer class"""
def writeln(self, value=None):
if value:
self.write(shim.b(value))
self.write(GNTP_EOL)
def writeheader(self, key, value):
if not isinstance(value, str):
value = str(value)
self.write(shim.b(key))
self.write(GNTP_SEP)
self.write(shim.b(value))
self.write(GNTP_EOL)
class _GNTPBase(object):
"""Base initilization
:param string messagetype: GNTP Message type
:param string version: GNTP Protocol version
:param string encription: Encryption protocol
"""
def __init__(self, messagetype=None, version='1.0', encryption=None):
self.info = {
'version': version,
'messagetype': messagetype,
'encryptionAlgorithmID': encryption
}
self.hash_algo = {
'MD5': hashlib.md5,
'SHA1': hashlib.sha1,
'SHA256': hashlib.sha256,
'SHA512': hashlib.sha512,
}
self.headers = {}
self.resources = {}
def __str__(self):
return self.encode()
def _parse_info(self, data):
"""Parse the first line of a GNTP message to get security and other info values
:param string data: GNTP Message
:return dict: Parsed GNTP Info line
"""
match = GNTP_INFO_LINE.match(data)
if not match:
raise errors.ParseError('ERROR_PARSING_INFO_LINE')
info = match.groupdict()
if info['encryptionAlgorithmID'] == 'NONE':
info['encryptionAlgorithmID'] = None
return info
def set_password(self, password, encryptAlgo='MD5'):
"""Set a password for a GNTP Message
:param string password: Null to clear password
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
"""
if not password:
self.info['encryptionAlgorithmID'] = None
self.info['keyHashAlgorithm'] = None
return
self.password = shim.b(password)
self.encryptAlgo = encryptAlgo.upper()
if not self.encryptAlgo in self.hash_algo:
raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
hashfunction = self.hash_algo.get(self.encryptAlgo)
password = password.encode('utf8')
seed = time.ctime().encode('utf8')
salt = hashfunction(seed).hexdigest()
saltHash = hashfunction(seed).digest()
keyBasis = password + saltHash
key = hashfunction(keyBasis).digest()
keyHash = hashfunction(key).hexdigest()
self.info['keyHashAlgorithmID'] = self.encryptAlgo
self.info['keyHash'] = keyHash.upper()
self.info['salt'] = salt.upper()
def _decode_hex(self, value):
"""Helper function to decode hex string to `proper` hex string
:param string value: Human readable hex string
:return string: Hex string
"""
result = ''
for i in range(0, len(value), 2):
tmp = int(value[i:i + 2], 16)
result += chr(tmp)
return result
def _decode_binary(self, rawIdentifier, identifier):
rawIdentifier += '\r\n\r\n'
dataLength = int(identifier['Length'])
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
pointerEnd = pointerStart + dataLength
data = self.raw[pointerStart:pointerEnd]
if not len(data) == dataLength:
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
return data
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
if password is None:
raise errors.AuthError('Missing password')
keyHash = self.info.get('keyHash', None)
if keyHash is None and self.password is None:
return True
if keyHash is None:
raise errors.AuthError('Invalid keyHash')
if self.password is None:
raise errors.AuthError('Missing password')
keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5')
password = self.password.encode('utf8')
saltHash = self._decode_hex(self.info['salt'])
keyBasis = password + saltHash
self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest()
keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest()
if not keyHash.upper() == self.info['keyHash'].upper():
raise errors.AuthError('Invalid Hash')
return True
def validate(self):
"""Verify required headers"""
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise errors.ParseError('Missing Notification Header: ' + header)
def _format_info(self):
"""Generate info line for GNTP Message
:return string:
"""
info = 'GNTP/%s %s' % (
self.info.get('version'),
self.info.get('messagetype'),
)
if self.info.get('encryptionAlgorithmID', None):
info += ' %s:%s' % (
self.info.get('encryptionAlgorithmID'),
self.info.get('ivValue'),
)
else:
info += ' NONE'
if self.info.get('keyHashAlgorithmID', None):
info += ' %s:%s.%s' % (
self.info.get('keyHashAlgorithmID'),
self.info.get('keyHash'),
self.info.get('salt')
)
return info
def _parse_dict(self, data):
"""Helper function to parse blocks of GNTP headers into a dictionary
:param string data:
:return dict: Dictionary of parsed GNTP Headers
"""
d = {}
for line in data.split('\r\n'):
match = GNTP_HEADER.match(line)
if not match:
continue
key = match.group(1).strip()
val = match.group(2).strip()
d[key] = val
return d
def add_header(self, key, value):
self.headers[key] = value
def add_resource(self, data):
"""Add binary resource
:param string data: Binary Data
"""
data = shim.b(data)
identifier = hashlib.md5(data).hexdigest()
self.resources[identifier] = data
return 'x-growl-resource://%s' % identifier
def decode(self, data, password=None):
"""Decode GNTP Message
:param string data:
"""
self.password = password
self.raw = shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self.headers = self._parse_dict(parts[0])
def encode(self):
"""Encode a generic GNTP Message
:return string: GNTP Message ready to be sent. Returned as a byte string
"""
buff = _GNTPBuffer()
buff.writeln(self._format_info())
#Headers
for k, v in self.headers.items():
buff.writeheader(k, v)
buff.writeln()
#Resources
for resource, data in self.resources.items():
buff.writeheader('Identifier', resource)
buff.writeheader('Length', len(data))
buff.writeln()
buff.write(data)
buff.writeln()
buff.writeln()
return buff.getvalue()
class GNTPRegister(_GNTPBase):
"""Represents a GNTP Registration Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notifications-Count'
]
_requiredNotificationHeaders = ['Notification-Name']
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'REGISTER')
self.notifications = []
if data:
self.decode(data, password)
else:
self.set_password(password)
self.add_header('Application-Name', 'pygntp')
self.add_header('Notifications-Count', 0)
def validate(self):
'''Validate required headers and validate notification headers'''
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise errors.ParseError('Missing Registration Header: ' + header)
for notice in self.notifications:
for header in self._requiredNotificationHeaders:
if not notice.get(header, False):
raise errors.ParseError('Missing Notification Header: ' + header)
def decode(self, data, password):
"""Decode existing GNTP Registration message
:param string data: Message to decode
"""
self.raw = shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Notification-Name', False):
self.notifications.append(notice)
elif notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('register.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
def add_notification(self, name, enabled=True):
"""Add new Notification to Registration message
:param string name: Notification Name
:param boolean enabled: Enable this notification by default
"""
notice = {}
notice['Notification-Name'] = name
notice['Notification-Enabled'] = enabled
self.notifications.append(notice)
self.add_header('Notifications-Count', len(self.notifications))
def encode(self):
"""Encode a GNTP Registration Message
:return string: Encoded GNTP Registration message. Returned as a byte string
"""
buff = _GNTPBuffer()
buff.writeln(self._format_info())
#Headers
for k, v in self.headers.items():
buff.writeheader(k, v)
buff.writeln()
#Notifications
if len(self.notifications) > 0:
for notice in self.notifications:
for k, v in notice.items():
buff.writeheader(k, v)
buff.writeln()
#Resources
for resource, data in self.resources.items():
buff.writeheader('Identifier', resource)
buff.writeheader('Length', len(data))
buff.writeln()
buff.write(data)
buff.writeln()
buff.writeln()
return buff.getvalue()
class GNTPNotice(_GNTPBase):
"""Represents a GNTP Notification Command
:param string data: (Optional) See decode()
:param string app: (Optional) Set Application-Name
:param string name: (Optional) Set Notification-Name
:param string title: (Optional) Set Notification Title
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notification-Name',
'Notification-Title'
]
def __init__(self, data=None, app=None, name=None, title=None, password=None):
_GNTPBase.__init__(self, 'NOTIFY')
if data:
self.decode(data, password)
else:
self.set_password(password)
if app:
self.add_header('Application-Name', app)
if name:
self.add_header('Notification-Name', name)
if title:
self.add_header('Notification-Title', title)
def decode(self, data, password):
"""Decode existing GNTP Notification message
:param string data: Message to decode.
"""
self.raw = shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('notice.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
class GNTPSubscribe(_GNTPBase):
"""Represents a GNTP Subscribe Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Subscriber-ID',
'Subscriber-Name',
]
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'SUBSCRIBE')
if data:
self.decode(data, password)
else:
self.set_password(password)
class GNTPOK(_GNTPBase):
"""Represents a GNTP OK Response
:param string data: (Optional) See _GNTPResponse.decode()
:param string action: (Optional) Set type of action the OK Response is for
"""
_requiredHeaders = ['Response-Action']
def __init__(self, data=None, action=None):
_GNTPBase.__init__(self, '-OK')
if data:
self.decode(data)
if action:
self.add_header('Response-Action', action)
class GNTPError(_GNTPBase):
"""Represents a GNTP Error response
:param string data: (Optional) See _GNTPResponse.decode()
:param string errorcode: (Optional) Error code
:param string errordesc: (Optional) Error Description
"""
_requiredHeaders = ['Error-Code', 'Error-Description']
def __init__(self, data=None, errorcode=None, errordesc=None):
_GNTPBase.__init__(self, '-ERROR')
if data:
self.decode(data)
if errorcode:
self.add_header('Error-Code', errorcode)
self.add_header('Error-Description', errordesc)
def error(self):
return (self.headers.get('Error-Code', None),
self.headers.get('Error-Description', None))
def parse_gntp(data, password=None):
"""Attempt to parse a message as a GNTP message
:param string data: Message to be parsed
:param string password: Optional password to be used to verify the message
"""
data = shim.u(data)
match = GNTP_INFO_LINE_SHORT.match(data)
if not match:
raise errors.ParseError('INVALID_GNTP_INFO')
info = match.groupdict()
if info['messagetype'] == 'REGISTER':
return GNTPRegister(data, password=password)
elif info['messagetype'] == 'NOTIFY':
return GNTPNotice(data, password=password)
elif info['messagetype'] == 'SUBSCRIBE':
return GNTPSubscribe(data, password=password)
elif info['messagetype'] == '-OK':
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':
return GNTPError(data)
raise errors.ParseError('INVALID_GNTP_MESSAGE')

25
apprise/plugins/NotifyGrowl/gntp/errors.py

@ -0,0 +1,25 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
class BaseError(Exception):
pass
class ParseError(BaseError):
errorcode = 500
errordesc = 'Error parsing the message'
class AuthError(BaseError):
errorcode = 400
errordesc = 'Error with authorization'
class UnsupportedError(BaseError):
errorcode = 500
errordesc = 'Currently unsupported by gntp.py'
class NetworkError(BaseError):
errorcode = 500
errordesc = "Error connecting to growl server"

265
apprise/plugins/NotifyGrowl/gntp/notifier.py

@ -0,0 +1,265 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
The gntp.notifier module is provided as a simple way to send notifications
using GNTP
.. note::
This class is intended to mostly mirror the older Python bindings such
that you should be able to replace instances of the old bindings with
this class.
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
"""
import logging
import platform
import socket
import sys
from .version import __version__
from . import core
from . import errors as errors
from . import shim
__all__ = [
'mini',
'GrowlNotifier',
]
logger = logging.getLogger(__name__)
class GrowlNotifier(object):
"""Helper class to simplfy sending Growl messages
:param string applicationName: Sending application name
:param list notification: List of valid notifications
:param list defaultNotifications: List of notifications that should be enabled
by default
:param string applicationIcon: Icon URL
:param string hostname: Remote host
:param integer port: Remote port
"""
passwordHash = 'MD5'
socketTimeout = 3
def __init__(self, applicationName='Python GNTP', notifications=[],
defaultNotifications=None, applicationIcon=None, hostname='localhost',
password=None, port=23053):
self.applicationName = applicationName
self.notifications = list(notifications)
if defaultNotifications:
self.defaultNotifications = list(defaultNotifications)
else:
self.defaultNotifications = self.notifications
self.applicationIcon = applicationIcon
self.password = password
self.hostname = hostname
self.port = int(port)
def _checkIcon(self, data):
'''
Check the icon to see if it's valid
If it's a simple URL icon, then we return True. If it's a data icon
then we return False
'''
logger.info('Checking icon')
return shim.u(data).startswith('http')
def register(self):
"""Send GNTP Registration
.. warning::
Before sending notifications to Growl, you need to have
sent a registration message at least once
"""
logger.info('Sending registration to %s:%s', self.hostname, self.port)
register = core.GNTPRegister()
register.add_header('Application-Name', self.applicationName)
for notification in self.notifications:
enabled = notification in self.defaultNotifications
register.add_notification(notification, enabled)
if self.applicationIcon:
if self._checkIcon(self.applicationIcon):
register.add_header('Application-Icon', self.applicationIcon)
else:
resource = register.add_resource(self.applicationIcon)
register.add_header('Application-Icon', resource)
if self.password:
register.set_password(self.password, self.passwordHash)
self.add_origin_info(register)
self.register_hook(register)
return self._send('register', register)
def notify(self, noteType, title, description, icon=None, sticky=False,
priority=None, callback=None, identifier=None, custom={}):
"""Send a GNTP notifications
.. warning::
Must have registered with growl beforehand or messages will be ignored
:param string noteType: One of the notification names registered earlier
:param string title: Notification title (usually displayed on the notification)
:param string description: The main content of the notification
:param string icon: Icon URL path
:param boolean sticky: Sticky notification
:param integer priority: Message priority level from -2 to 2
:param string callback: URL callback
:param dict custom: Custom attributes. Key names should be prefixed with X-
according to the spec but this is not enforced by this class
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
assert noteType in self.notifications
notice = core.GNTPNotice()
notice.add_header('Application-Name', self.applicationName)
notice.add_header('Notification-Name', noteType)
notice.add_header('Notification-Title', title)
if self.password:
notice.set_password(self.password, self.passwordHash)
if sticky:
notice.add_header('Notification-Sticky', sticky)
if priority:
notice.add_header('Notification-Priority', priority)
if icon:
if self._checkIcon(icon):
notice.add_header('Notification-Icon', icon)
else:
resource = notice.add_resource(icon)
notice.add_header('Notification-Icon', resource)
if description:
notice.add_header('Notification-Text', description)
if callback:
notice.add_header('Notification-Callback-Target', callback)
if identifier:
notice.add_header('Notification-Coalescing-ID', identifier)
for key in custom:
notice.add_header(key, custom[key])
self.add_origin_info(notice)
self.notify_hook(notice)
return self._send('notify', notice)
def subscribe(self, id, name, port):
"""Send a Subscribe request to a remote machine"""
sub = core.GNTPSubscribe()
sub.add_header('Subscriber-ID', id)
sub.add_header('Subscriber-Name', name)
sub.add_header('Subscriber-Port', port)
if self.password:
sub.set_password(self.password, self.passwordHash)
self.add_origin_info(sub)
self.subscribe_hook(sub)
return self._send('subscribe', sub)
def add_origin_info(self, packet):
"""Add optional Origin headers to message"""
packet.add_header('Origin-Machine-Name', platform.node())
packet.add_header('Origin-Software-Name', 'gntp.py')
packet.add_header('Origin-Software-Version', __version__)
packet.add_header('Origin-Platform-Name', platform.system())
packet.add_header('Origin-Platform-Version', platform.platform())
def register_hook(self, packet):
pass
def notify_hook(self, packet):
pass
def subscribe_hook(self, packet):
pass
def _send(self, messagetype, packet):
"""Send the GNTP Packet"""
packet.validate()
data = packet.encode()
logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(self.socketTimeout)
try:
s.connect((self.hostname, self.port))
s.send(data)
recv_data = s.recv(1024)
while not recv_data.endswith(shim.b("\r\n\r\n")):
recv_data += s.recv(1024)
except socket.error:
# Python2.5 and Python3 compatibile exception
exc = sys.exc_info()[1]
raise errors.NetworkError(exc)
response = core.parse_gntp(recv_data)
s.close()
logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
if type(response) == core.GNTPOK:
return True
logger.error('Invalid response: %s', response.error())
return response.error()
def mini(description, applicationName='PythonMini', noteType="Message",
title="Mini Message", applicationIcon=None, hostname='localhost',
password=None, port=23053, sticky=False, priority=None,
callback=None, notificationIcon=None, identifier=None,
notifierFactory=GrowlNotifier):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
try:
growl = notifierFactory(
applicationName=applicationName,
notifications=[noteType],
defaultNotifications=[noteType],
applicationIcon=applicationIcon,
hostname=hostname,
password=password,
port=port,
)
result = growl.register()
if result is not True:
return result
return growl.notify(
noteType=noteType,
title=title,
description=description,
icon=notificationIcon,
sticky=sticky,
priority=priority,
callback=callback,
identifier=identifier,
)
except Exception:
# We want the "mini" function to be simple and swallow Exceptions
# in order to be less invasive
logger.exception("Growl error")
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
# so extra debugging is useful
logging.basicConfig(level=logging.INFO)
mini('Testing mini notification')

45
apprise/plugins/NotifyGrowl/gntp/shim.py

@ -0,0 +1,45 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
Python2.5 and Python3.3 compatibility shim
Heavily inspirted by the "six" library.
https://pypi.python.org/pypi/six
"""
import sys
PY3 = sys.version_info[0] == 3
if PY3:
def b(s):
if isinstance(s, bytes):
return s
return s.encode('utf8', 'replace')
def u(s):
if isinstance(s, bytes):
return s.decode('utf8', 'replace')
return s
from io import BytesIO as StringIO
from configparser import RawConfigParser
else:
def b(s):
if isinstance(s, unicode):
return s.encode('utf8', 'replace')
return s
def u(s):
if isinstance(s, unicode):
return s
if isinstance(s, int):
s = str(s)
return unicode(s, "utf8", "replace")
from StringIO import StringIO
from ConfigParser import RawConfigParser
b.__doc__ = "Ensure we have a byte string"
u.__doc__ = "Ensure we have a unicode string"

4
apprise/plugins/NotifyGrowl/gntp/version.py

@ -0,0 +1,4 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
__version__ = '1.0.2'

136
apprise/plugins/NotifyJSON.py

@ -0,0 +1,136 @@
# -*- encoding: utf-8 -*-
#
# JSON Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from json import dumps
import requests
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import NotifyImageSize
from .NotifyBase import HTTP_ERROR_MAP
# Image Support (128x128)
JSON_IMAGE_XY = NotifyImageSize.XY_128
class NotifyJSON(NotifyBase):
"""
A wrapper for JSON Notifications
"""
# The default protocol
PROTOCOL = 'json'
# The default secure protocol
SECURE_PROTOCOL = 'jsons'
def __init__(self, **kwargs):
"""
Initialize JSON Object
"""
super(NotifyJSON, self).__init__(
title_maxlen=250, body_maxlen=32768,
image_size=JSON_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, basestring):
self.fullpath = '/'
return
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform JSON Notification
"""
# prepare JSON Object
payload = {
# Version: Major.Minor, Major is only updated if the entire
# schema is changed. If just adding new items (or removing
# old ones, only increment the Minor!
'version': '1.0',
'title': title,
'message': body,
'type': notify_type,
}
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
auth = None
if self.user:
auth = (self.user, self.password)
url = '%s://%s' % (self.schema, self.host)
if isinstance(self.port, int):
url += ':%d' % self.port
url += self.fullpath
self.logger.debug('JSON POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('JSON Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
auth=auth,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
try:
self.logger.warning(
'Failed to send JSON notification: '
'%s (error=%s).' % (
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send JSON notification '
'(error=%s).' % (
r.status_code))
# Return; we're done
return False
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending JSON '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

212
apprise/plugins/NotifyJoin.py

@ -0,0 +1,212 @@
# -*- encoding: utf-8 -*-
#
# Join Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
# Join URL: http://joaoapps.com/join/
# To use this plugin, you need to first access (make sure your browser allows
# popups): https://joinjoaomgcd.appspot.com/
#
# To register you just need to allow it to connect to your Google Profile but
# the good news is it doesn't ask for anything too personal.
#
# You can download the app for your phone here:
# https://play.google.com/store/apps/details?id=com.joaomgcd.join
import requests
import re
from urllib import urlencode
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
from .NotifyBase import NotifyImageSize
# Join uses the http protocol with JSON requests
JOIN_URL = 'https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush'
# Token required as part of the API request
VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{32}')
# Default User
JOIN_DEFAULT_USER = 'apprise'
# Extend HTTP Error Messages
JOIN_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
401: 'Unauthorized - Invalid Token.',
}.items())
# Used to break path apart into list of devices
DEVICE_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
# Used to detect a device
IS_DEVICE_RE = re.compile(r'([A-Za-z0-9]{32})')
# Used to detect a device
IS_GROUP_RE = re.compile(
r'(group\.)?(?P<name>(all|android|chrome|windows10|phone|tablet|pc))',
re.IGNORECASE,
)
# Image Support (72x72)
JOIN_IMAGE_XY = NotifyImageSize.XY_72
class NotifyJoin(NotifyBase):
"""
A wrapper for Join Notifications
"""
# The default protocol
PROTOCOL = 'join'
# The default secure protocol
SECURE_PROTOCOL = 'join'
def __init__(self, apikey, devices, **kwargs):
"""
Initialize Join Object
"""
super(NotifyJoin, self).__init__(
title_maxlen=250, body_maxlen=1000,
image_size=JOIN_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if not VALIDATE_APIKEY.match(apikey.strip()):
self.logger.warning(
'The first API Token specified (%s) is invalid.' % apikey,
)
raise TypeError(
'The first API Token specified (%s) is invalid.' % apikey,
)
# The token associated with the account
self.apikey = apikey.strip()
if isinstance(devices, basestring):
self.devices = filter(bool, DEVICE_LIST_DELIM.split(
devices,
))
elif isinstance(devices, (tuple, list)):
self.devices = devices
else:
self.devices = list()
if len(self.devices) == 0:
self.logger.warning('No device(s) were specified.')
raise TypeError('No device(s) were specified.')
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Join Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/x-www-form-urlencoded',
}
# error tracking (used for function return)
has_error = False
# Create a copy of the devices list
devices = list(self.devices)
while len(devices):
device = devices.pop(0)
group_re = IS_GROUP_RE.match(device)
if group_re:
device = 'group.%s' % group_re.group('name').lower()
elif not IS_DEVICE_RE.match(device):
self.logger.warning(
"The specified device '%s' is invalid; skipping." % (
device,
)
)
continue
url_args = {
'apikey': self.apikey,
'deviceId': device,
'title': title,
'text': body,
}
if self.include_image:
image_url = self.image_url(
notify_type,
)
if image_url:
url_args['icon'] = image_url
# prepare payload
payload = {
}
# Prepare the URL
url = '%s?%s' % (JOIN_URL, urlencode(url_args))
self.logger.debug('Join POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('Join Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=payload,
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Join:%s '
'notification: %s (error=%s).' % (
device,
JOIN_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Join:%s '
'notification (error=%s).' % (
device,
r.status_code))
# self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done
has_error = True
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Join:%s '
'notification.' % device
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
if len(devices):
# Prevent thrashing requests
self.throttle()
return has_error

172
apprise/plugins/NotifyMatterMost.py

@ -0,0 +1,172 @@
# -*- encoding: utf-8 -*-
#
# MatterMost Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from json import dumps
import requests
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import NotifyImageSize
from .NotifyBase import HTTP_ERROR_MAP
from .NotifyBase import NOTIFY_APPLICATION_ID
import re
# Some Reference Locations:
# - https://docs.mattermost.com/developer/webhooks-incoming.html
# - https://docs.mattermost.com/administration/config-settings.html
# Used to validate Authorization Token
VALIDATE_AUTHTOKEN = re.compile(r'[A-Za-z0-9]{24,32}')
# Image Support (72x72)
MATTERMOST_IMAGE_XY = NotifyImageSize.XY_72
# MATTERMOST uses the http protocol with JSON requests
MATTERMOST_PORT = 8065
class NotifyMatterMost(NotifyBase):
"""
A wrapper for MatterMost Notifications
"""
# The default protocol
PROTOCOL = 'mmost'
# The default secure protocol
SECURE_PROTOCOL = 'mmosts'
def __init__(self, authtoken, channel=None, **kwargs):
"""
Initialize MatterMost Object
"""
super(NotifyMatterMost, self).__init__(
title_maxlen=250, body_maxlen=4000,
image_size=MATTERMOST_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
# Our API Key
self.authtoken = authtoken
# Validate authtoken
if not authtoken:
self.logger.warning(
'Missing MatterMost Authorization Token.'
)
raise TypeError(
'Missing MatterMost Authorization Token.'
)
if not VALIDATE_AUTHTOKEN.match(authtoken):
self.logger.warning(
'Invalid MatterMost Authorization Token Specified.'
)
raise TypeError(
'Invalid MatterMost Authorization Token Specified.'
)
# A Channel (optional)
self.channel = channel
if not self.port:
self.port = MATTERMOST_PORT
return
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform MatterMost Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
# prepare JSON Object
payload = {
'text': '###### %s\n%s' % (title, body),
'icon_url': self.image_url(notify_type),
}
if self.user:
payload['username'] = self.user
else:
payload['username'] = NOTIFY_APPLICATION_ID
if self.channel:
payload['channel'] = self.channel
url = '%s://%s' % (self.schema, self.host)
if isinstance(self.port, int):
url += ':%d' % self.port
url += '/hooks/%s' % self.authtoken
self.logger.debug('MatterMost POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('MatterMost Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send MatterMost notification:'
'%s (error=%s).' % (
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send MatterMost notification '
'(error=%s).' % (
r.status_code))
# Return; we're done
return False
else:
self.logger.info('Sent MatterMost notification.')
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending MatterMost '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

173
apprise/plugins/NotifyMyAndroid.py

@ -0,0 +1,173 @@
# -*- encoding: utf-8 -*-
#
# Notify My Android (NMA) Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
import requests
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
# Notify My Android uses the http protocol with JSON requests
NMA_URL = 'https://www.notifymyandroid.com/publicapi/notify'
# Extend HTTP Error Messages
NMA_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
400: 'Data is wrong format, invalid length or null.',
401: 'API Key provided is invalid',
402: 'Maximum number of API calls per hour reached.',
}.items())
# Used to validate Authorization Token
VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{48}')
# Priorities
class NotifyMyAndroidPriority(object):
VERY_LOW = -2
MODERATE = -1
NORMAL = 0
HIGH = 1
EMERGENCY = 2
NMA_PRIORITIES = (
NotifyMyAndroidPriority.VERY_LOW,
NotifyMyAndroidPriority.MODERATE,
NotifyMyAndroidPriority.NORMAL,
NotifyMyAndroidPriority.HIGH,
NotifyMyAndroidPriority.EMERGENCY,
)
class NotifyMyAndroid(NotifyBase):
"""
A wrapper for Notify My Android (NMA) Notifications
"""
# The default protocol
PROTOCOL = 'nma'
# The default secure protocol
SECURE_PROTOCOL = 'nma'
def __init__(self, apikey, priority=NotifyMyAndroidPriority.NORMAL,
devapikey=None, **kwargs):
"""
Initialize Notify My Android Object
"""
super(NotifyMyAndroid, self).__init__(
title_maxlen=1000, body_maxlen=10000,
notify_format=NotifyFormat.HTML,
**kwargs)
# The Priority of the message
if priority not in NMA_PRIORITIES:
self.priority = NotifyMyAndroidPriority.NORMAL
else:
self.priority = priority
# Validate apikey
if not VALIDATE_APIKEY.match(apikey):
self.logger.warning(
'Invalid NMA API Key specified.'
)
raise TypeError(
'Invalid NMA API Key specified.'
)
self.apikey = apikey
if devapikey:
# Validate apikey
if not VALIDATE_APIKEY.match(devapikey):
self.logger.warning(
'Invalid NMA DEV API Key specified.'
)
raise TypeError(
'Invalid NMA DEV API Key specified.'
)
self.devapikey = devapikey
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Notify My Android Notification
"""
headers = {
'User-Agent': self.app_id,
}
# prepare JSON Object
payload = {
'apikey': self.apikey,
'application': self.app_id,
'event': title,
'description': body,
'priority': self.priority,
}
if self.notify_format == NotifyFormat.HTML:
payload['content-type'] = 'text/html'
if self.devapikey:
payload['developerkey'] = self.devapikey
self.logger.debug('NMA POST URL: %s (cert_verify=%r)' % (
NMA_URL, self.verify_certificate,
))
self.logger.debug('NMA Payload: %s' % str(payload))
try:
r = requests.post(
NMA_URL,
data=payload,
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send NMA notification: %s (error=%s).' % (
NMA_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send NMA notification (error=%s).' % (
r.status_code))
# Return; we're done
return False
else:
self.logger.debug('NMA Server Response: %s.' % r.text)
self.logger.info('Sent NMA notification.')
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending NMA notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

177
apprise/plugins/NotifyProwl.py

@ -0,0 +1,177 @@
# -*- encoding: utf-8 -*-
#
# Prowl Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
import requests
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
# Prowl uses the http protocol with JSON requests
PROWL_URL = 'https://api.prowlapp.com/publicapi/add'
# Used to validate API Key
VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{40}')
# Used to validate Provider Key
VALIDATE_PROVIDERKEY = re.compile(r'[A-Za-z0-9]{40}')
# Priorities
class ProwlPriority(object):
VERY_LOW = -2
MODERATE = -1
NORMAL = 0
HIGH = 1
EMERGENCY = 2
PROWL_PRIORITIES = (
ProwlPriority.VERY_LOW,
ProwlPriority.MODERATE,
ProwlPriority.NORMAL,
ProwlPriority.HIGH,
ProwlPriority.EMERGENCY,
)
# Extend HTTP Error Messages
PROWL_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
406: 'IP address has exceeded API limit',
409: 'Request not aproved.',
}.items())
class NotifyProwl(NotifyBase):
"""
A wrapper for Prowl Notifications
"""
# The default protocol
PROTOCOL = 'prowl'
# The default secure protocol
SECURE_PROTOCOL = 'prowl'
def __init__(self, apikey, providerkey=None,
priority=ProwlPriority.NORMAL,
**kwargs):
"""
Initialize Prowl Object
"""
super(NotifyProwl, self).__init__(
title_maxlen=1024, body_maxlen=10000,
notify_format=NotifyFormat.TEXT,
**kwargs)
if priority not in PROWL_PRIORITIES:
self.priority = ProwlPriority.NORMAL
else:
self.priority = priority
if not VALIDATE_APIKEY.match(apikey):
self.logger.warning(
'The API key specified (%s) is invalid.' % apikey,
)
raise TypeError(
'The API key specified (%s) is invalid.' % apikey,
)
# Store the API key
self.apikey = apikey
# Store the provider key (if specified)
if providerkey:
if not VALIDATE_PROVIDERKEY.match(providerkey):
self.logger.warning(
'The Provider key specified (%s) '
'is invalid.' % providerkey)
raise TypeError(
'The Provider key specified (%s) '
'is invalid.' % providerkey)
# Store the Provider Key
self.providerkey = providerkey
def _notify(self, title, body, **kwargs):
"""
Perform Prowl Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-type': "application/x-www-form-urlencoded",
}
# prepare JSON Object
payload = {
'apikey': self.apikey,
'application': self.app_id,
'event': title,
'description': body,
'priority': self.priority,
}
if self.providerkey:
payload['providerkey'] = self.providerkey
self.logger.debug('Prowl POST URL: %s (cert_verify=%r)' % (
PROWL_URL, self.verify_certificate,
))
self.logger.debug('Prowl Payload: %s' % str(payload))
try:
r = requests.post(
PROWL_URL,
data=payload,
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Prowl notification: '
'%s (error=%s).' % (
PROWL_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Prowl notification '
'(error=%s).' % (
r.status_code))
self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done
return False
else:
self.logger.info('Sent Prowl notification.')
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Prowl notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

167
apprise/plugins/NotifyPushBullet.py

@ -0,0 +1,167 @@
# -*- encoding: utf-8 -*-
#
# PushBullet Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from json import dumps
import requests
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
from .NotifyBase import IS_EMAIL_RE
# Flag used as a placeholder to sending to all devices
PUSHBULLET_SEND_TO_ALL = 'ALL_DEVICES'
# PushBullet uses the http protocol with JSON requests
PUSHBULLET_URL = 'https://api.pushbullet.com/v2/pushes'
# Used to break apart list of potential recipients by their delimiter
# into a usable list.
RECIPIENTS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
# Extend HTTP Error Messages
PUSHBULLET_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
401: 'Unauthorized - Invalid Token.',
}.items())
class NotifyPushBullet(NotifyBase):
"""
A wrapper for PushBullet Notifications
"""
# The default protocol
PROTOCOL = 'pbul'
# The default secure protocol
SECURE_PROTOCOL = 'pbul'
def __init__(self, accesstoken, recipients=None, **kwargs):
"""
Initialize PushBullet Object
"""
super(NotifyPushBullet, self).__init__(
title_maxlen=250, body_maxlen=32768,
notify_format=NotifyFormat.TEXT,
**kwargs)
self.accesstoken = accesstoken
if isinstance(recipients, basestring):
self.recipients = filter(bool, RECIPIENTS_LIST_DELIM.split(
recipients,
))
elif isinstance(recipients, (tuple, list)):
self.recipients = recipients
else:
self.recipients = list()
if len(self.recipients) == 0:
self.recipients = (PUSHBULLET_SEND_TO_ALL, )
def _notify(self, title, body, **kwargs):
"""
Perform PushBullet Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
auth = (self.accesstoken, '')
# error tracking (used for function return)
has_error = False
# Create a copy of the recipients list
recipients = list(self.recipients)
while len(recipients):
recipient = recipients.pop(0)
# prepare JSON Object
payload = {
'type': 'note',
'title': title,
'body': body,
}
if recipient is PUSHBULLET_SEND_TO_ALL:
# Send to all
pass
elif IS_EMAIL_RE.match(recipient):
payload['email'] = recipient
self.logger.debug(
"Recipient '%s' is an email address" % recipient)
elif recipient[0] == '#':
payload['channel_tag'] = recipient[1:]
self.logger.debug("Recipient '%s' is a channel" % recipient)
else:
payload['device_iden'] = recipient
self.logger.debug(
"Recipient '%s' is a device" % recipient)
self.logger.debug('PushBullet POST URL: %s (cert_verify=%r)' % (
PUSHBULLET_URL, self.verify_certificate,
))
self.logger.debug('PushBullet Payload: %s' % str(payload))
try:
r = requests.post(
PUSHBULLET_URL,
data=dumps(payload),
headers=headers,
auth=auth,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send PushBullet notification: '
'%s (error=%s).' % (
PUSHBULLET_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send PushBullet notification '
'(error=%s).' % r.status_code)
# self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done
has_error = True
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending PushBullet '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
if len(recipients):
# Prevent thrashing requests
self.throttle()
return not has_error

146
apprise/plugins/NotifyPushalot.py

@ -0,0 +1,146 @@
# -*- encoding: utf-8 -*-
#
# Pushalot Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from json import dumps
import requests
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import NotifyImageSize
from .NotifyBase import HTTP_ERROR_MAP
# Pushalot uses the http protocol with JSON requests
PUSHALOT_URL = 'https://pushalot.com/api/sendmessage'
# Image Support (72x72)
PUSHALOT_IMAGE_XY = NotifyImageSize.XY_72
# Extend HTTP Error Messages
PUSHALOT_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
406: 'Message throttle limit hit.',
410: 'AuthorizedToken is no longer valid.',
}.items())
# Used to validate Authorization Token
VALIDATE_AUTHTOKEN = re.compile(r'[A-Za-z0-9]{32}')
class NotifyPushalot(NotifyBase):
"""
A wrapper for Pushalot Notifications
"""
# The default protocol
PROTOCOL = 'palot'
# The default secure protocol
SECURE_PROTOCOL = 'palot'
def __init__(self, authtoken, is_important=False, **kwargs):
"""
Initialize Pushalot Object
"""
super(NotifyPushalot, self).__init__(
title_maxlen=250, body_maxlen=32768,
image_size=PUSHALOT_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
# Is Important Flag
self.is_important = is_important
self.authtoken = authtoken
# Validate authtoken
if not VALIDATE_AUTHTOKEN.match(authtoken):
self.logger.warning(
'Invalid Pushalot Authorization Token Specified.'
)
raise TypeError(
'Invalid Pushalot Authorization Token Specified.'
)
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Pushalot Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
# prepare JSON Object
payload = {
'AuthorizationToken': self.authtoken,
'IsImportant': self.is_important,
'Title': title,
'Body': body,
'Source': self.app_id,
}
if self.include_image:
image_url = self.image_url(
notify_type,
)
if image_url:
payload['Image'] = image_url
self.logger.debug('Pushalot POST URL: %s (cert_verify=%r)' % (
PUSHALOT_URL, self.verify_certificate,
))
self.logger.debug('Pushalot Payload: %s' % str(payload))
try:
r = requests.post(
PUSHALOT_URL,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Pushalot notification: '
'%s (error=%s).' % (
PUSHALOT_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Pushalot notification '
'(error=%s).' % r.status_code)
# Return; we're done
return False
else:
self.logger.info('Sent Pushalot notification.')
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Pushalot notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

76
apprise/plugins/NotifyPushjet.py

@ -0,0 +1,76 @@
# -*- encoding: utf-8 -*-
#
# Pushjet Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from pushjet import errors
from pushjet import pushjet
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
class NotifyPushjet(NotifyBase):
"""
A wrapper for Pushjet Notifications
"""
# The default protocol
PROTOCOL = 'pjet'
# The default secure protocol
SECURE_PROTOCOL = 'pjets'
def __init__(self, **kwargs):
"""
Initialize Pushjet Object
"""
super(NotifyPushjet, self).__init__(
title_maxlen=250, body_maxlen=32768,
notify_format=NotifyFormat.TEXT,
**kwargs)
def _notify(self, title, body, notify_type):
"""
Perform Pushjet Notification
"""
try:
if self.user and self.host:
server = "http://"
if self.secure:
server = "https://"
server += self.host
if self.port:
server += ":" + str(self.port)
api = pushjet.Api(server)
service = api.Service(secret_key=self.user)
else:
api = pushjet.Api(pushjet.DEFAULT_API_URL)
service = api.Service(secret_key=self.host)
service.send(body, title)
except (errors.PushjetError, ValueError) as e:
self.logger.warning('Failed to send Pushjet notification.')
self.logger.debug('Pushjet Exception: %s' % str(e))
return False
return True

76
apprise/plugins/NotifyPushjet/NotifyPushjet.py

@ -0,0 +1,76 @@
# -*- encoding: utf-8 -*-
#
# Pushjet Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from .pushjet import errors
from .pushjet import pushjet
from ..NotifyBase import NotifyBase
from ..NotifyBase import NotifyFormat
class NotifyPushjet(NotifyBase):
"""
A wrapper for Pushjet Notifications
"""
# The default protocol
PROTOCOL = 'pjet'
# The default secure protocol
SECURE_PROTOCOL = 'pjets'
def __init__(self, **kwargs):
"""
Initialize Pushjet Object
"""
super(NotifyPushjet, self).__init__(
title_maxlen=250, body_maxlen=32768,
notify_format=NotifyFormat.TEXT,
**kwargs)
def _notify(self, title, body, notify_type):
"""
Perform Pushjet Notification
"""
try:
if self.user and self.host:
server = "http://"
if self.secure:
server = "https://"
server += self.host
if self.port:
server += ":" + str(self.port)
api = pushjet.Api(server)
service = api.Service(secret_key=self.user)
else:
api = pushjet.Api(pushjet.DEFAULT_API_URL)
service = api.Service(secret_key=self.host)
service.send(body, title)
except (errors.PushjetError, ValueError) as e:
self.logger.warning('Failed to send Pushjet notification.')
self.logger.debug('Pushjet Exception: %s' % str(e))
return False
return True

6
apprise/plugins/NotifyPushjet/__init__.py

@ -0,0 +1,6 @@
# -*- encoding: utf-8 -*-
from . import NotifyPushjet
__all__ = [
'NotifyPushjet',
]

6
apprise/plugins/NotifyPushjet/pushjet/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
"""A Python API for Pushjet. Send notifications to your phone from Python scripts!"""
from .pushjet import Service, Device, Subscription, Message, Api
from .errors import PushjetError, AccessError, NonexistentError, SubscriptionError, RequestError, ServerError

49
apprise/plugins/NotifyPushjet/pushjet/errors.py

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from requests import RequestException
import sys
if sys.version_info[0] < 3:
# This is built into Python 3.
class ConnectionError(Exception):
pass
class PushjetError(Exception):
"""All the errors inherit from this. Therefore, ``except PushjetError`` catches all errors."""
class AccessError(PushjetError):
"""Raised when a secret key is missing for a service method that needs one."""
class NonexistentError(PushjetError):
"""Raised when an attempt to access a nonexistent service is made."""
class SubscriptionError(PushjetError):
"""Raised when an attempt to subscribe to a service that's already subscribed to,
or to unsubscribe from a service that isn't subscribed to, is made."""
class RequestError(PushjetError, ConnectionError):
"""Raised if something goes wrong in the connection to the API server.
Inherits from ``ConnectionError`` on Python 3, and can therefore be caught
with ``except ConnectionError`` there.
:ivar requests_exception: The underlying `requests <http://docs.python-requests.org>`__
exception. Access this if you want to handle different HTTP request errors in different ways.
"""
def __str__(self):
return "requests.{error}: {description}".format(
error=self.requests_exception.__class__.__name__,
description=str(self.requests_exception)
)
def __init__(self, requests_exception):
self.requests_exception = requests_exception
class ServerError(PushjetError):
"""Raised if the API server has an error while processing your request.
This getting raised means there's a bug in the server! If you manage to
track down what caused it, you can `open an issue on Pushjet's GitHub page
<https://github.com/Pushjet/Pushjet-Server-Api/issues>`__.
"""

319
apprise/plugins/NotifyPushjet/pushjet/pushjet.py

@ -0,0 +1,319 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import requests
from functools import partial
from .utilities import (
NoNoneDict,
requires_secret_key, with_api_bound,
is_valid_uuid, is_valid_public_key, is_valid_secret_key, repr_format
)
from .errors import NonexistentError, SubscriptionError, RequestError, ServerError
import sys
if sys.version_info[0] >= 3:
from urllib.parse import urljoin
unicode_type = str
else:
from urlparse import urljoin
unicode_type = unicode
DEFAULT_API_URL = 'https://api.pushjet.io/'
class PushjetModel(object):
_api = None # This is filled in later.
class Service(PushjetModel):
"""A Pushjet service to send messages through. To receive messages, devices
subscribe to these.
:param secret_key: The service's API key for write access. If provided,
:func:`~pushjet.Service.send`, :func:`~pushjet.Service.edit`, and
:func:`~pushjet.Service.delete` become available.
Either this or the public key parameter must be present.
:param public_key: The service's public API key for read access only.
Either this or the secret key parameter must be present.
:ivar name: The name of the service.
:ivar icon_url: The URL to the service's icon. May be ``None``.
:ivar created: When the service was created, as seconds from epoch.
:ivar secret_key: The service's secret API key, or ``None`` if the service is read-only.
:ivar public_key: The service's public API key, to be used when subscribing to the service.
"""
def __repr__(self):
return "<Pushjet Service: \"{}\">".format(repr_format(self.name))
def __init__(self, secret_key=None, public_key=None):
if secret_key is None and public_key is None:
raise ValueError("Either a secret key or public key "
"must be provided.")
elif secret_key and not is_valid_secret_key(secret_key):
raise ValueError("Invalid secret key provided.")
elif public_key and not is_valid_public_key(public_key):
raise ValueError("Invalid public key provided.")
self.secret_key = unicode_type(secret_key) if secret_key else None
self.public_key = unicode_type(public_key) if public_key else None
self.refresh()
def _request(self, endpoint, method, is_secret, params=None, data=None):
params = params or {}
if is_secret:
params['secret'] = self.secret_key
else:
params['service'] = self.public_key
return self._api._request(endpoint, method, params, data)
@requires_secret_key
def send(self, message, title=None, link=None, importance=None):
"""Send a message to the service's subscribers.
:param message: The message body to be sent.
:param title: (optional) The message's title. Messages can be without title.
:param link: (optional) An URL to be sent with the message.
:param importance: (optional) The priority level of the message. May be
a number between 1 and 5, where 1 is least important and 5 is most.
"""
data = NoNoneDict({
'message': message,
'title': title,
'link': link,
'level': importance
})
self._request('message', 'POST', is_secret=True, data=data)
@requires_secret_key
def edit(self, name=None, icon_url=None):
"""Edit the service's attributes.
:param name: (optional) A new name to give the service.
:param icon_url: (optional) A new URL to use as the service's icon URL.
Set to an empty string to remove the service's icon entirely.
"""
data = NoNoneDict({
'name': name,
'icon': icon_url
})
if not data:
return
self._request('service', 'PATCH', is_secret=True, data=data)
self.name = unicode_type(name)
self.icon_url = unicode_type(icon_url)
@requires_secret_key
def delete(self):
"""Delete the service. Irreversible."""
self._request('service', 'DELETE', is_secret=True)
def _update_from_data(self, data):
self.name = data['name']
self.icon_url = data['icon'] or None
self.created = data['created']
self.public_key = data['public']
self.secret_key = data.get('secret', getattr(self, 'secret_key', None))
def refresh(self):
"""Refresh the server's information, in case it could be edited from elsewhere.
:raises: :exc:`~pushjet.NonexistentError` if the service was deleted before refreshing.
"""
key_name = 'public'
secret = False
if self.secret_key is not None:
key_name = 'secret'
secret = True
status, response = self._request('service', 'GET', is_secret=secret)
if status == requests.codes.NOT_FOUND:
raise NonexistentError("A service with the provided {} key "
"does not exist (anymore, at least).".format(key_name))
self._update_from_data(response['service'])
@classmethod
def _from_data(cls, data):
# This might be a no-no, but I see little alternative if
# different constructors with different parameters are needed,
# *and* a default __init__ constructor should be present.
# This, along with the subclassing for custom API URLs, may
# very well be one of those pieces of code you look back at
# years down the line - or maybe just a couple of weeks - and say
# "what the heck was I thinking"? I assure you, though, future me.
# This was the most reasonable thing to get the API + argspecs I wanted.
obj = cls.__new__(cls)
obj._update_from_data(data)
return obj
@classmethod
def create(cls, name, icon_url=None):
"""Create a new service.
:param name: The name of the new service.
:param icon_url: (optional) An URL to an image to be used as the service's icon.
:return: The newly-created :class:`~pushjet.Service`.
"""
data = NoNoneDict({
'name': name,
'icon': icon_url
})
_, response = cls._api._request('service', 'POST', data=data)
return cls._from_data(response['service'])
class Device(PushjetModel):
"""The "receiver" for messages. Subscribes to services and receives any
messages they send.
:param uuid: The device's unique ID as a UUID. Does not need to be registered
before using it. A UUID can be generated with ``uuid.uuid4()``, for example.
:ivar uuid: The UUID the device was initialized with.
"""
def __repr__(self):
return "<Pushjet Device: {}>".format(self.uuid)
def __init__(self, uuid):
uuid = unicode_type(uuid)
if not is_valid_uuid(uuid):
raise ValueError("Invalid UUID provided. Try uuid.uuid4().")
self.uuid = unicode_type(uuid)
def _request(self, endpoint, method, params=None, data=None):
params = (params or {})
params['uuid'] = self.uuid
return self._api._request(endpoint, method, params, data)
def subscribe(self, service):
"""Subscribe the device to a service.
:param service: The service to subscribe to. May be a public key or a :class:`~pushjet.Service`.
:return: The :class:`~pushjet.Service` subscribed to.
:raises: :exc:`~pushjet.NonexistentError` if the provided service does not exist.
:raises: :exc:`~pushjet.SubscriptionError` if the provided service is already subscribed to.
"""
data = {}
data['service'] = service.public_key if isinstance(service, Service) else service
status, response = self._request('subscription', 'POST', data=data)
if status == requests.codes.CONFLICT:
raise SubscriptionError("The device is already subscribed to that service.")
elif status == requests.codes.NOT_FOUND:
raise NonexistentError("A service with the provided public key "
"does not exist (anymore, at least).")
return self._api.Service._from_data(response['service'])
def unsubscribe(self, service):
"""Unsubscribe the device from a service.
:param service: The service to unsubscribe from. May be a public key or a :class:`~pushjet.Service`.
:raises: :exc:`~pushjet.NonexistentError` if the provided service does not exist.
:raises: :exc:`~pushjet.SubscriptionError` if the provided service isn't subscribed to.
"""
data = {}
data['service'] = service.public_key if isinstance(service, Service) else service
status, _ = self._request('subscription', 'DELETE', data=data)
if status == requests.codes.CONFLICT:
raise SubscriptionError("The device is not subscribed to that service.")
elif status == requests.codes.NOT_FOUND:
raise NonexistentError("A service with the provided public key "
"does not exist (anymore, at least).")
def get_subscriptions(self):
"""Get all the subscriptions the device has.
:return: A list of :class:`~pushjet.Subscription`\ s.
"""
_, response = self._request('subscription', 'GET')
subscriptions = []
for subscription_dict in response['subscriptions']:
subscriptions.append(Subscription(subscription_dict))
return subscriptions
def get_messages(self):
"""Get all new (that is, as of yet unretrieved) messages.
:return: A list of :class:`~pushjet.Message`\ s.
"""
_, response = self._request('message', 'GET')
messages = []
for message_dict in response['messages']:
messages.append(Message(message_dict))
return messages
class Subscription(object):
"""A subscription to a service, with the metadata that entails.
:ivar service: The service the subscription is to, as a :class:`~pushjet.Service`.
:ivar time_subscribed: When the subscription was made, as seconds from epoch.
:ivar last_checked: When the device last retrieved messages from the subscription,
as seconds from epoch.
:ivar device_uuid: The UUID of the device that owns the subscription.
"""
def __repr__(self):
return "<Pushjet Subscription to service \"{}\">".format(repr_format(self.service.name))
def __init__(self, subscription_dict):
self.service = Service._from_data(subscription_dict['service'])
self.time_subscribed = subscription_dict['timestamp']
self.last_checked = subscription_dict['timestamp_checked']
self.device_uuid = subscription_dict['uuid'] # Not sure this is needed, but...
class Message(object):
"""A message received from a service.
:ivar message: The message body.
:ivar title: The message title. May be ``None``.
:ivar link: The URL the message links to. May be ``None``.
:ivar time_sent: When the message was sent, as seconds from epoch.
:ivar importance: The message's priority level between 1 and 5, where 1 is
least important and 5 is most.
:ivar service: The :class:`~pushjet.Service` that sent the message.
"""
def __repr__(self):
return "<Pushjet Message: \"{}\">".format(repr_format(self.title or self.message))
def __init__(self, message_dict):
self.message = message_dict['message']
self.title = message_dict['title'] or None
self.link = message_dict['link'] or None
self.time_sent = message_dict['timestamp']
self.importance = message_dict['level']
self.service = Service._from_data(message_dict['service'])
class Api(object):
"""An API with a custom URL. Use this if you're connecting to a self-hosted
Pushjet API instance, or a non-standard one in general.
:param url: The URL to the API instance.
:ivar url: The URL to the API instance, as supplied.
"""
def __repr__(self):
return "<Pushjet Api: {}>".format(self.url).encode(sys.stdout.encoding, errors='replace')
def __init__(self, url):
self.url = unicode_type(url)
self.Service = with_api_bound(Service, self)
self.Device = with_api_bound(Device, self)
def _request(self, endpoint, method, params=None, data=None):
url = urljoin(self.url, endpoint)
try:
r = requests.request(method, url, params=params, data=data)
except requests.RequestException as e:
raise RequestError(e)
status = r.status_code
if status == requests.codes.INTERNAL_SERVER_ERROR:
raise ServerError(
"An error occurred in the server while processing your request. "
"This should probably be reported to: "
"https://github.com/Pushjet/Pushjet-Server-Api/issues"
)
try:
response = r.json()
except ValueError:
response = {}
return status, response

66
apprise/plugins/NotifyPushjet/pushjet/utilities.py

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import sys
from decorator import decorator
from .errors import AccessError
# Help class(...es? Nah. Just singular for now.)
class NoNoneDict(dict):
"""A dict that ignores values that are None. Not completely API-compatible
with dict, but contains all that's needed.
"""
def __repr__(self):
return "NoNoneDict({dict})".format(dict=dict.__repr__(self))
def __init__(self, initial={}):
self.update(initial)
def __setitem__(self, key, value):
if value is not None:
dict.__setitem__(self, key, value)
def update(self, data):
for key, value in data.items():
self[key] = value
# Decorators / factories
@decorator
def requires_secret_key(func, self, *args, **kwargs):
"""Raise an error if the method is called without a secret key."""
if self.secret_key is None:
raise AccessError("The Service doesn't have a secret "
"key provided, and therefore lacks write permission.")
return func(self, *args, **kwargs)
def with_api_bound(cls, api):
new_cls = type(cls.__name__, (cls,), {
'_api': api,
'__doc__': (
"Create a :class:`~pushjet.{name}` bound to the API. "
"See :class:`pushjet.{name}` for documentation."
).format(name=cls.__name__)
})
return new_cls
# Helper functions
UUID_RE = re.compile(r'^[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}$')
PUBLIC_KEY_RE = re.compile(r'^[A-Za-z0-9]{4}-[A-Za-z0-9]{6}-[A-Za-z0-9]{12}-[A-Za-z0-9]{5}-[A-Za-z0-9]{9}$')
SECRET_KEY_RE = re.compile(r'^[A-Za-z0-9]{32}$')
is_valid_uuid = lambda s: UUID_RE.match(s) is not None
is_valid_public_key = lambda s: PUBLIC_KEY_RE.match(s) is not None
is_valid_secret_key = lambda s: SECRET_KEY_RE.match(s) is not None
def repr_format(s):
s = s.replace('\n', ' ').replace('\r', '')
original_length = len(s)
s = s[:30]
s += '...' if len(s) != original_length else ''
s = s.encode(sys.stdout.encoding, errors='replace')
return s

222
apprise/plugins/NotifyPushover.py

@ -0,0 +1,222 @@
# -*- encoding: utf-8 -*-
#
# Pushover Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
import requests
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
# Flag used as a placeholder to sending to all devices
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
# Pushover uses the http protocol with JSON requests
PUSHOVER_URL = 'https://api.pushover.net/1/messages.json'
# Used to validate API Key
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{30}')
# Used to detect a User and/or Group
VALIDATE_USERGROUP = re.compile(r'[A-Za-z0-9]{30}')
# Used to detect a User and/or Group
VALIDATE_DEVICE = re.compile(r'[A-Za-z0-9_]{1,25}')
# Priorities
class PushoverPriority(object):
VERY_LOW = -2
MODERATE = -1
NORMAL = 0
HIGH = 1
EMERGENCY = 2
PUSHOVER_PRIORITIES = (
PushoverPriority.VERY_LOW,
PushoverPriority.MODERATE,
PushoverPriority.NORMAL,
PushoverPriority.HIGH,
PushoverPriority.EMERGENCY,
)
# Used to break path apart into list of devices
DEVICE_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
# Extend HTTP Error Messages
PUSHOVER_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
401: 'Unauthorized - Invalid Token.',
}.items())
class NotifyPushover(NotifyBase):
"""
A wrapper for Pushover Notifications
"""
# The default protocol
PROTOCOL = 'pover'
# The default secure protocol
SECURE_PROTOCOL = 'pover'
def __init__(self, token, devices=None,
priority=PushoverPriority.NORMAL,
**kwargs):
"""
Initialize Pushover Object
"""
super(NotifyPushover, self).__init__(
title_maxlen=250, body_maxlen=512,
notify_format=NotifyFormat.TEXT,
**kwargs)
if not VALIDATE_TOKEN.match(token.strip()):
self.logger.warning(
'The API Token specified (%s) is invalid.' % token,
)
raise TypeError(
'The API Token specified (%s) is invalid.' % token,
)
# The token associated with the account
self.token = token.strip()
if isinstance(devices, basestring):
self.devices = filter(bool, DEVICE_LIST_DELIM.split(
devices,
))
elif isinstance(devices, (tuple, list)):
self.devices = devices
else:
self.devices = list()
if len(self.devices) == 0:
self.devices = (PUSHOVER_SEND_TO_ALL, )
# The Priority of the message
if priority not in PUSHOVER_PRIORITIES:
self.priority = PushoverPriority.NORMAL
else:
self.priority = priority
if not self.user:
self.logger.warning('No user was specified.')
raise TypeError('No user was specified.')
if not self.token:
self.logger.warning('No token was specified.')
raise TypeError('No token was specified.')
if not VALIDATE_USERGROUP.match(self.user):
self.logger.warning(
'The user/group specified (%s) is invalid.' % self.user,
)
raise TypeError(
'The user/group specified (%s) is invalid.' % self.user,
)
def _notify(self, title, body, **kwargs):
"""
Perform Pushover Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/x-www-form-urlencoded'
}
auth = (self.token, '')
# error tracking (used for function return)
has_error = False
# Create a copy of the devices list
devices = list(self.devices)
while len(devices):
device = devices.pop(0)
# prepare JSON Object
payload = {
'token': self.token,
'user': self.user,
'priority': str(self.priority),
'title': title,
'message': body,
}
if device != PUSHOVER_SEND_TO_ALL:
if not VALIDATE_DEVICE.match(device):
self.logger.warning(
'The device specified (%s) is invalid.' % device,
)
has_error = True
continue
payload['device'] = device
self.logger.debug('Pushover POST URL: %s (cert_verify=%r)' % (
PUSHOVER_URL, self.verify_certificate,
))
self.logger.debug('Pushover Payload: %s' % str(payload))
try:
r = requests.post(
PUSHOVER_URL,
data=payload,
headers=headers,
auth=auth,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Pushover:%s '
'notification: %s (error=%s).' % (
device,
PUSHOVER_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Pushover:%s '
'notification (error=%s).' % (
device,
r.status_code))
# self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done
has_error = True
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Pushover:%s ' % (
device) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
if len(devices):
# Prevent thrashing requests
self.throttle()
return has_error

307
apprise/plugins/NotifyRocketChat.py

@ -0,0 +1,307 @@
# -*- encoding: utf-8 -*-
#
# Notify Rocket.Chat Notify Wrapper
#
# Copyright (C) 2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
import requests
import json
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$')
IS_ROOM_ID = re.compile(r'^(?P<name>[A-Za-z0-9]+)$')
# Extend HTTP Error Messages
RC_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
400: 'Channel/RoomId is wrong format, or missing from server.',
401: 'Authentication tokens provided is invalid or missing.',
}.items())
# Used to break apart list of potential tags by their delimiter
# into a usable list.
LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
class NotifyRocketChat(NotifyBase):
"""
A wrapper for Notify Rocket.Chat Notifications
"""
# The default protocol
PROTOCOL = 'rocket'
# The default secure protocol
SECURE_PROTOCOL = 'rockets'
def __init__(self, recipients=None, **kwargs):
"""
Initialize Notify Rocket.Chat Object
"""
super(NotifyRocketChat, self).__init__(
title_maxlen=200, body_maxlen=32768,
notify_format=NotifyFormat.TEXT,
**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
# Prepare our URL
self.api_url = '%s://%s' % (self.schema, self.host)
if isinstance(self.port, int):
self.api_url += ':%d' % self.port
self.api_url += '/api/v1/'
# Initialize channels list
self.channels = list()
# Initialize room_id list
self.room_ids = list()
if recipients is None:
recipients = []
elif isinstance(recipients, basestring):
recipients = filter(bool, LIST_DELIM.split(
recipients,
))
elif not isinstance(recipients, (tuple, list)):
recipients = []
# Validate recipients and drop bad ones:
for recipient in recipients:
result = IS_CHANNEL.match(recipient)
if result:
# store valid device
self.channels.append(result.group('name'))
continue
result = IS_ROOM_ID.match(recipient)
if result:
# store valid room_id
self.channels.append(result.group('name'))
continue
self.logger.warning(
'Dropped invalid channel/room_id ' +
'(%s) specified.' % recipient,
)
if len(self.room_ids) == 0 and len(self.channels) == 0:
raise TypeError(
'No Rocket.Chat room_id and/or channels specified to notify.'
)
# Used to track token headers upon authentication (if successful)
self.headers = {}
# Track whether we authenticated okay
self.authenticated = self.login()
if not self.authenticated:
raise TypeError(
'Authentication to Rocket.Chat server failed.'
)
def _notify(self, title, body, notify_type, **kwargs):
"""
wrapper to send_notification since we can alert more then one channel
"""
# Prepare our message
text = '*%s*\r\n%s' % (title.replace('*', '\*'), body)
# Send all our defined channels
for channel in self.channels:
self.send_notification({
'text': text,
'channel': channel,
}, notify_type=notify_type, **kwargs)
# Send all our defined room id's
for room_id in self.room_ids:
self.send_notification({
'text': text,
'roomId': room_id,
}, notify_type=notify_type, **kwargs)
def send_notification(self, payload, notify_type, **kwargs):
"""
Perform Notify Rocket.Chat Notification
"""
if not self.authenticated:
# We couldn't authenticate; we're done
return False
self.logger.debug('Rocket.Chat POST URL: %s (cert_verify=%r)' % (
self.api_url + 'chat.postMessage', self.verify_certificate,
))
self.logger.debug('Rocket.Chat Payload: %s' % str(payload))
try:
r = requests.post(
self.api_url + 'chat.postMessage',
data=payload,
headers=self.headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Rocket.Chat notification: ' +
'%s (error=%s).' % (
RC_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Rocket.Chat notification ' +
'(error=%s).' % (
r.status_code))
# Return; we're done
return False
else:
self.logger.debug('Rocket.Chat Server Response: %s.' % r.text)
self.logger.info('Sent Rocket.Chat notification.')
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Rocket.Chat ' +
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True
def login(self):
"""
login to our server
"""
payload = {
'username': self.user,
'password': self.password,
}
try:
r = requests.post(
self.api_url + 'login',
data=payload,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to authenticate with Rocket.Chat server: ' +
'%s (error=%s).' % (
RC_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to authenticate with Rocket.Chat server ' +
'(error=%s).' % (
r.status_code))
# Return; we're done
return False
else:
self.logger.debug('Rocket.Chat authentication successful')
response = json.loads(r.text)
if response.get('status') != "success":
self.logger.warning(
'Could not authenticate with Rocket.Chat server.')
return False
# Set our headers for further communication
self.headers['X-Auth-Token'] = \
response.get('data').get('authToken')
self.headers['X-User-Id'] = \
response.get('data').get('userId')
# We're authenticated now
self.authenticated = True
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured authenticating to the ' +
'Rocket.Chat server.')
self.logger.debug('Socket Exception: %s' % str(e))
return False
return True
def logout(self):
"""
logout of our server
"""
if not self.authenticated:
# Nothing to do
return True
try:
r = requests.post(
self.api_url + 'logout',
headers=self.headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to log off Rocket.Chat server: ' +
'%s (error=%s).' % (
RC_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to log off Rocket.Chat server ' +
'(error=%s).' % (
r.status_code))
# Return; we're done
return False
else:
self.logger.debug(
'Rocket.Chat log off successful; response %s.' % (
r.text))
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured logging off the ' +
'Rocket.Chat server')
self.logger.debug('Socket Exception: %s' % str(e))
return False
# We're no longer authenticated now
self.authenticated = False
return True

287
apprise/plugins/NotifySlack.py

@ -0,0 +1,287 @@
# -*- encoding: utf-8 -*-
#
# Slack Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
# To use this plugin, you need to first access https://api.slack.com
# Specifically https://my.slack.com/services/new/incoming-webhook/
# to create a new incoming webhook for your account. You'll need to
# follow the wizard to pre-determine the channel(s) you want your
# message to broadcast to, and when you're complete, you will
# recieve a URL that looks something like this:
# https://hooks.slack.com/services/T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7F
# ^ ^ ^
# | | |
# These are important <--------------^---------^---------------^
#
#
import requests
import re
from json import dumps
from time import time
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
from .NotifyBase import HTML_NOTIFY_MAP
from .NotifyBase import NotifyImageSize
# Slack uses the http protocol with JSON requests
SLACK_URL = 'https://hooks.slack.com/services'
# Token required as part of the API request
# /AAAAAAAAA/........./........................
VALIDATE_TOKEN_A = re.compile(r'[A-Z0-9]{9}')
# Token required as part of the API request
# /........./BBBBBBBBB/........................
VALIDATE_TOKEN_B = re.compile(r'[A-Z0-9]{9}')
# Token required as part of the API request
# /........./........./CCCCCCCCCCCCCCCCCCCCCCCC
VALIDATE_TOKEN_C = re.compile(r'[A-Za-z0-9]{24}')
# Default User
SLACK_DEFAULT_USER = 'apprise'
# Extend HTTP Error Messages
SLACK_HTTP_ERROR_MAP = dict(HTTP_ERROR_MAP.items() + {
401: 'Unauthorized - Invalid Token.',
}.items())
# Used to break path apart into list of devices
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
# Used to detect a device
IS_CHANNEL_RE = re.compile(r'#?([A-Za-z0-9_]{1,32})')
# Image Support (72x72)
SLACK_IMAGE_XY = NotifyImageSize.XY_72
class NotifySlack(NotifyBase):
"""
A wrapper for Slack Notifications
"""
# The default protocol
PROTOCOL = 'slack'
# The default secure protocol
SECURE_PROTOCOL = 'slack'
def __init__(self, token_a, token_b, token_c, channels, **kwargs):
"""
Initialize Slack Object
"""
super(NotifySlack, self).__init__(
title_maxlen=250, body_maxlen=1000,
image_size=SLACK_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if not VALIDATE_TOKEN_A.match(token_a.strip()):
self.logger.warning(
'The first API Token specified (%s) is invalid.' % token_a,
)
raise TypeError(
'The first API Token specified (%s) is invalid.' % token_a,
)
# The token associated with the account
self.token_a = token_a.strip()
if not VALIDATE_TOKEN_B.match(token_b.strip()):
self.logger.warning(
'The second API Token specified (%s) is invalid.' % token_b,
)
raise TypeError(
'The second API Token specified (%s) is invalid.' % token_b,
)
# The token associated with the account
self.token_b = token_b.strip()
if not VALIDATE_TOKEN_C.match(token_c.strip()):
self.logger.warning(
'The third API Token specified (%s) is invalid.' % token_c,
)
raise TypeError(
'The third API Token specified (%s) is invalid.' % token_c,
)
# The token associated with the account
self.token_c = token_c.strip()
if not self.user:
self.logger.warning(
'No user was specified; using %s.' % SLACK_DEFAULT_USER)
self.user = SLACK_DEFAULT_USER
if isinstance(channels, basestring):
self.channels = filter(bool, CHANNEL_LIST_DELIM.split(
channels,
))
elif isinstance(channels, (tuple, list)):
self.channels = channels
else:
self.channels = list()
if len(self.channels) == 0:
self.logger.warning('No channel(s) were specified.')
raise TypeError('No channel(s) were specified.')
# Formatting requirements are defined here:
# https://api.slack.com/docs/message-formatting
self._re_formatting_map = {
# New lines must become the string version
'\r\*\n': '\\n',
# Escape other special characters
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
}
# Iterate over above list and store content accordingly
self._re_formatting_rules = re.compile(
r'(' + '|'.join(self._re_formatting_map.keys()) + r')',
re.IGNORECASE,
)
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Slack Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
}
# error tracking (used for function return)
has_error = False
# Perform Formatting
title = self._re_formatting_rules.sub(
lambda x: self._re_formatting_map[x.group()], title,
)
body = self._re_formatting_rules.sub(
lambda x: self._re_formatting_map[x.group()], body,
)
url = '%s/%s/%s/%s' % (
SLACK_URL,
self.token_a,
self.token_b,
self.token_c,
)
image_url = None
if self.include_image:
image_url = self.image_url(
notify_type,
)
# Create a copy of the channel list
channels = list(self.channels)
while len(channels):
channel = channels.pop(0)
if not IS_CHANNEL_RE.match(channel):
self.logger.warning(
"The specified channel '%s' is invalid; skipping." % (
channel,
)
)
continue
if len(channel) > 1 and channel[0] == '+':
# Treat as encoded id if prefixed with a +
_channel = channel[1:]
elif len(channel) > 1 and channel[0] == '@':
# Treat @ value 'as is'
_channel = channel
else:
# Prefix with channel hash tag
_channel = '#%s' % channel
# prepare JSON Object
payload = {
'channel': _channel,
'username': self.user,
# Use Markdown language
'mrkdwn': True,
'attachments': [{
'title': title,
'text': body,
'color': HTML_NOTIFY_MAP[notify_type],
# Time
'ts': time(),
'footer': self.app_id,
}],
}
if image_url:
payload['attachments'][0]['footer_icon'] = image_url
self.logger.debug('Slack POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('Slack Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Slack:%s '
'notification: %s (error=%s).' % (
channel,
SLACK_HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Slack:%s '
'notification (error=%s).' % (
channel,
r.status_code))
# self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done
has_error = True
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Slack:%s ' % (
channel) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
if len(channels):
# Prevent thrashing requests
self.throttle()
return has_error

412
apprise/plugins/NotifyTelegram.py

@ -0,0 +1,412 @@
# -*- encoding: utf-8 -*-
#
# Telegram Notify Wrapper
#
# Copyright (C) 2016-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
# To use this plugin, you need to first access https://api.telegram.org
# You need to create a bot and acquire it's Token Identifier (bot_token)
#
# Basically you need to create a chat with a user called the 'BotFather'
# and type: /newbot
#
# Then follow through the wizard, it will provide you an api key
# that looks like this:123456789:alphanumeri_characters
#
# For each chat_id a bot joins will have a chat_id associated with it.
# You will need this value as well to send the notification.
#
# Log into the webpage version of the site if you like by accessing:
# https://web.telegram.org
#
# You can't check out to see if your entry is working using:
# https://api.telegram.org/botAPI_KEY/getMe
#
# Pay attention to the word 'bot' that must be present infront of your
# api key that the BotFather gave you.
#
# For example, a url might look like this:
# https://api.telegram.org/bot123456789:alphanumeri_characters/getMe
#
import requests
import re
from json import loads
from json import dumps
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import HTTP_ERROR_MAP
# Telegram uses the http protocol with JSON requests
TELEGRAM_BOT_URL = 'https://api.telegram.org/bot'
# Token required as part of the API request
# allow the word 'bot' infront
VALIDATE_BOT_TOKEN = re.compile(
r'(bot)?(?P<key>[0-9]+:[A-Za-z0-9_-]+)/*$',
re.IGNORECASE,
)
# Chat ID is required
# If the Chat ID is positive, then it's addressed to a single person
# If the Chat ID is negative, then it's targeting a group
IS_CHAT_ID_RE = re.compile(
r'(@*(?P<idno>-?[0-9]{1,32})|(?P<name>[a-z_-][a-z0-9_-]*))',
re.IGNORECASE,
)
# Disable image support for now
# The stickers/images are kind of big and consume a lot of space
# It's not as appealing as just having the post not contain
# an image at all.
TELEGRAM_IMAGE_XY = None
# Used to break path apart into list of chat identifiers
CHAT_ID_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
class NotifyTelegram(NotifyBase):
"""
A wrapper for Telegram Notifications
"""
# The default protocol
PROTOCOL = 'tgram'
# The default secure protocol
SECURE_PROTOCOL = 'tgram'
def __init__(self, bot_token, chat_ids, **kwargs):
"""
Initialize Telegram Object
"""
super(NotifyTelegram, self).__init__(
title_maxlen=250, body_maxlen=4096,
image_size=TELEGRAM_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if bot_token is None:
raise TypeError(
'The Bot Token specified is invalid.'
)
result = VALIDATE_BOT_TOKEN.match(bot_token.strip())
if not result:
raise TypeError(
'The Bot Token specified (%s) is invalid.' % bot_token,
)
# Store our API Key
self.bot_token = result.group('key')
if isinstance(chat_ids, basestring):
self.chat_ids = filter(bool, CHAT_ID_LIST_DELIM.split(
chat_ids,
))
elif isinstance(chat_ids, (tuple, list)):
self.chat_ids = list(chat_ids)
else:
self.chat_ids = list()
if self.user:
# Treat this as a channel too
self.chat_ids.append(self.user)
# Bot's can't send messages to themselves which is fair enough
# but if or when they can, this code will allow a default fallback
# solution if no chat_id and/or channel is specified
# if len(self.chat_ids) == 0:
#
# chat_id = self._get_chat_id()
# if chat_id is not None:
# self.logger.warning(
# 'No chat_id or @channel was specified; ' +\
# 'using detected bot_chat_id (%d).' % chat_id,
# )
# self.chat_ids.append(str(chat_id))
if len(self.chat_ids) == 0:
self.logger.warning('No chat_id(s) were specified.')
raise TypeError('No chat_id(s) were specified.')
def _get_chat_id(self):
"""
This function retrieves the chat id belonging to the key specified
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
}
url = '%s%s/%s' % (
TELEGRAM_BOT_URL,
self.bot_token,
'getMe'
)
self.logger.debug('Telegram (Detection) GET URL: %s' % url)
chat_id = None
try:
r = requests.post(url, headers=headers)
if r.status_code == requests.codes.ok:
# Extract our chat ID
result = loads(r.text)
if result.get('ok', False) is True:
chat_id = result['result'].get('id')
if chat_id <= 0:
chat_id = None
else:
# We had a problem
try:
# Try to get the error message if we can:
error_msg = loads(r.text)['description']
except:
error_msg = None
try:
if error_msg:
self.logger.warning(
'Failed to lookup Telegram chat_id from '
'apikey: (%s) %s.' % (r.status_code, error_msg))
else:
self.logger.warning(
'Failed to lookup Telegram chat_id from '
'apikey: %s (error=%s).' % (
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to lookup Telegram chat_id from '
'apikey: (error=%s).' % r.status_code)
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured looking up Telegram chat_id '
'from apikey.')
self.logger.debug('Socket Exception: %s' % str(e))
return chat_id
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Telegram Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
}
# error tracking (used for function return)
has_error = False
image_url = None
if self.include_image:
image_content = self.image_raw(
notify_type,
)
if image_content is not None:
# prepare our eimage URL
image_url = '%s%s/%s' % (
TELEGRAM_BOT_URL,
self.bot_token,
'sendPhoto'
)
# Set up our upload
files = {'photo': ('%s.png' % notify_type, image_content)}
url = '%s%s/%s' % (
TELEGRAM_BOT_URL,
self.bot_token,
'sendMessage'
)
payload = {}
if self.notify_format == NotifyFormat.HTML:
# HTML
payload['parse_mode'] = 'HTML'
payload['text'] = '<b>%s</b>\r\n%s' % (title, body)
else:
# Text
# payload['parse_mode'] = 'Markdown'
payload['parse_mode'] = 'HTML'
payload['text'] = '<b>%s</b>\r\n%s' % (
self.escape_html(title),
self.escape_html(body),
)
# Create a copy of the chat_ids list
chat_ids = list(self.chat_ids)
while len(chat_ids):
chat_id = chat_ids.pop(0)
chat_id = IS_CHAT_ID_RE.match(chat_id)
if not chat_id:
self.logger.warning(
"The specified chat_id '%s' is invalid; skipping." % (
chat_id,
)
)
continue
if chat_id.group('name') is not None:
# Name
payload['chat_id'] = '@%s' % chat_id.group('name')
else:
# ID
payload['chat_id'] = chat_id.group('idno')
if image_url is not None:
image_payload = {
'chat_id': payload['chat_id'],
'disable_notification': True,
}
self.logger.debug(
'Telegram (image) POST URL: %s (cert_verify=%r)' % (
image_url, self.verify_certificate))
self.logger.debug(
'Telegram (image) Payload: %s' % str(image_payload))
try:
r = requests.post(
image_url,
data=image_payload,
headers={
'User-Agent': self.app_id,
},
files=files,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
# Try to get the error message if we can:
error_msg = loads(r.text)['description']
except:
error_msg = None
try:
if error_msg:
self.logger.warning(
'Failed to send Telegram Image:%s '
'notification: (%s) %s.' % (
payload['chat_id'],
r.status_code, error_msg))
else:
self.logger.warning(
'Failed to send Telegram Image:%s '
'notification: %s (error=%s).' % (
payload['chat_id'],
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Telegram Image:%s '
'notification (error=%s).' % (
payload['chat_id'],
r.status_code))
has_error = True
continue
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Telegram:%s ' % (
payload['chat_id']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
continue
self.logger.debug('Telegram POST URL: %s' % url)
self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('Telegram Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
# Try to get the error message if we can:
error_msg = loads(r.text)['description']
except:
error_msg = None
try:
if error_msg:
self.logger.warning(
'Failed to send Telegram:%s '
'notification: (%s) %s.' % (
payload['chat_id'],
r.status_code, error_msg))
else:
self.logger.warning(
'Failed to send Telegram:%s '
'notification: %s (error=%s).' % (
payload['chat_id'],
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Telegram:%s '
'notification (error=%s).' % (
payload['chat_id'], r.status_code))
# self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done
has_error = True
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Telegram:%s ' % (
payload['chat_id']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
if len(chat_ids):
# Prevent thrashing requests
self.throttle()
return has_error

155
apprise/plugins/NotifyToasty.py

@ -0,0 +1,155 @@
# -*- encoding: utf-8 -*-
#
# (Super) Toasty Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from urllib import quote
import requests
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import NotifyImageSize
from .NotifyBase import HTTP_ERROR_MAP
# Toasty uses the http protocol with JSON requests
TOASTY_URL = 'http://api.supertoasty.com/notify/'
# Image Support (128x128)
TOASTY_IMAGE_XY = NotifyImageSize.XY_128
# Used to break apart list of potential devices by their delimiter
# into a usable list.
DEVICES_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
class NotifyToasty(NotifyBase):
"""
A wrapper for Toasty Notifications
"""
# The default protocol
PROTOCOL = 'toasty'
# The default secure protocol
SECURE_PROTOCOL = 'toasty'
def __init__(self, devices, **kwargs):
"""
Initialize Toasty Object
"""
super(NotifyToasty, self).__init__(
title_maxlen=250, body_maxlen=32768,
image_size=TOASTY_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if isinstance(devices, basestring):
self.devices = filter(bool, DEVICES_LIST_DELIM.split(
devices,
))
elif isinstance(devices, (tuple, list)):
self.devices = devices
else:
raise TypeError('You must specify at least 1 device.')
if not self.user:
raise TypeError('You must specify a username.')
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Toasty Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'multipart/form-data',
}
# error tracking (used for function return)
has_error = False
# Create a copy of the devices list
devices = list(self.devices)
while len(devices):
device = devices.pop(0)
# prepare JSON Object
payload = {
'sender': quote(self.user),
'title': quote(title),
'text': quote(body),
}
if self.include_image:
image_url = self.image_url(
notify_type,
)
if image_url:
payload['image'] = image_url
# URL to transmit content via
url = '%s%s' % (TOASTY_URL, device)
self.logger.debug('Toasty POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('Toasty Payload: %s' % str(payload))
try:
r = requests.get(
url,
data=payload,
headers=headers,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send Toasty:%s '
'notification: %s (error=%s).' % (
device,
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except IndexError:
self.logger.warning(
'Failed to send Toasty:%s '
'notification (error=%s).' % (
device,
r.status_code))
# self.logger.debug('Response Details: %s' % r.raw.read())
# Return; we're done
has_error = True
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending Toasty:%s ' % (
device) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
if len(devices):
# Prevent thrashing requests
self.throttle()
return has_error

116
apprise/plugins/NotifyTwitter/NotifyTwitter.py

@ -0,0 +1,116 @@
# -*- encoding: utf-8 -*-
#
# Twitter Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from . import tweepy
from ..NotifyBase import NotifyBase
from ..NotifyBase import NotifyFormat
# Direct Messages have not image support
TWITTER_IMAGE_XY = None
class NotifyTwitter(NotifyBase):
"""
A wrapper to Twitter Notifications
"""
# The default protocol
PROTOCOL = 'tweet'
# The default secure protocol
SECURE_PROTOCOL = 'tweet'
def __init__(self, ckey, csecret, akey, asecret, **kwargs):
"""
Initialize Twitter Object
Tweets are restriced to 140 (soon to be 240), but DM messages
do not have any restriction on them
"""
super(NotifyTwitter, self).__init__(
title_maxlen=250, body_maxlen=4096,
image_size=TWITTER_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if not ckey:
raise TypeError(
'An invalid Consumer API Key was specified.'
)
if not csecret:
raise TypeError(
'An invalid Consumer Secret API Key was specified.'
)
if not akey:
raise TypeError(
'An invalid Acess Token API Key was specified.'
)
if not asecret:
raise TypeError(
'An invalid Acess Token Secret API Key was specified.'
)
if not self.user:
raise TypeError(
'No user was specified.'
)
try:
# Attempt to Establish a connection to Twitter
self.auth = tweepy.OAuthHandler(ckey, csecret)
# Apply our Access Tokens
self.auth.set_access_token(akey, asecret)
except Exception:
raise TypeError(
'Twitter authentication failed; '
'please verify your configuration.'
)
return
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform Twitter Notification
"""
text = '%s\r\n%s' % (title, body)
try:
# Get our API
api = tweepy.API(self.auth)
# Send our Direct Message
api.send_direct_message(self.user, text=text)
except Exception as e:
self.logger.warning(
'A Connection error occured sending Twitter '
'direct message to %s.' % self.user)
self.logger.debug('Twitter Exception: %s' % str(e))
# Return; we're done
return False
return True

6
apprise/plugins/NotifyTwitter/__init__.py

@ -0,0 +1,6 @@
# -*- encoding: utf-8 -*-
from . import NotifyTwitter
__all__ = [
'NotifyTwitter',
]

25
apprise/plugins/NotifyTwitter/tweepy/__init__.py

@ -0,0 +1,25 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
"""
Tweepy Twitter API library
"""
__version__ = '3.5.0'
__author__ = 'Joshua Roesslein'
__license__ = 'MIT'
from .models import Status, User, DirectMessage, Friendship, SavedSearch, SearchResults, ModelFactory, Category
from .error import TweepError, RateLimitError
from .api import API
from .cache import Cache, MemoryCache, FileCache
from .auth import OAuthHandler, AppAuthHandler
from .streaming import Stream, StreamListener
from .cursor import Cursor
# Global, unauthenticated instance of API
api = API()
def debug(enable=True, level=1):
from six.moves.http_client import HTTPConnection
HTTPConnection.debuglevel = level

1348
apprise/plugins/NotifyTwitter/tweepy/api.py

File diff suppressed because it is too large Load Diff

178
apprise/plugins/NotifyTwitter/tweepy/auth.py

@ -0,0 +1,178 @@
from __future__ import print_function
import six
import logging
from .error import TweepError
from .api import API
import requests
from requests_oauthlib import OAuth1Session, OAuth1
from requests.auth import AuthBase
from six.moves.urllib.parse import parse_qs
WARNING_MESSAGE = """Warning! Due to a Twitter API bug, signin_with_twitter
and access_type don't always play nice together. Details
https://dev.twitter.com/discussions/21281"""
class AuthHandler(object):
def apply_auth(self, url, method, headers, parameters):
"""Apply authentication headers to request"""
raise NotImplementedError
def get_username(self):
"""Return the username of the authenticated user"""
raise NotImplementedError
class OAuthHandler(AuthHandler):
"""OAuth authentication handler"""
OAUTH_HOST = 'api.twitter.com'
OAUTH_ROOT = '/oauth/'
def __init__(self, consumer_key, consumer_secret, callback=None):
if type(consumer_key) == six.text_type:
consumer_key = consumer_key.encode('ascii')
if type(consumer_secret) == six.text_type:
consumer_secret = consumer_secret.encode('ascii')
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
self.access_token = None
self.access_token_secret = None
self.callback = callback
self.username = None
self.oauth = OAuth1Session(consumer_key,
client_secret=consumer_secret,
callback_uri=self.callback)
def _get_oauth_url(self, endpoint):
return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
def apply_auth(self):
return OAuth1(self.consumer_key,
client_secret=self.consumer_secret,
resource_owner_key=self.access_token,
resource_owner_secret=self.access_token_secret,
decoding=None)
def _get_request_token(self, access_type=None):
try:
url = self._get_oauth_url('request_token')
if access_type:
url += '?x_auth_access_type=%s' % access_type
return self.oauth.fetch_request_token(url)
except Exception as e:
raise TweepError(e)
def set_access_token(self, key, secret):
self.access_token = key
self.access_token_secret = secret
def get_authorization_url(self,
signin_with_twitter=False,
access_type=None):
"""Get the authorization URL to redirect the user"""
try:
if signin_with_twitter:
url = self._get_oauth_url('authenticate')
if access_type:
logging.warning(WARNING_MESSAGE)
else:
url = self._get_oauth_url('authorize')
self.request_token = self._get_request_token(access_type=access_type)
return self.oauth.authorization_url(url)
except Exception as e:
raise TweepError(e)
def get_access_token(self, verifier=None):
"""
After user has authorized the request token, get access token
with user supplied verifier.
"""
try:
url = self._get_oauth_url('access_token')
self.oauth = OAuth1Session(self.consumer_key,
client_secret=self.consumer_secret,
resource_owner_key=self.request_token['oauth_token'],
resource_owner_secret=self.request_token['oauth_token_secret'],
verifier=verifier, callback_uri=self.callback)
resp = self.oauth.fetch_access_token(url)
self.access_token = resp['oauth_token']
self.access_token_secret = resp['oauth_token_secret']
return self.access_token, self.access_token_secret
except Exception as e:
raise TweepError(e)
def get_xauth_access_token(self, username, password):
"""
Get an access token from an username and password combination.
In order to get this working you need to create an app at
http://twitter.com/apps, after that send a mail to api@twitter.com
and request activation of xAuth for it.
"""
try:
url = self._get_oauth_url('access_token')
oauth = OAuth1(self.consumer_key,
client_secret=self.consumer_secret)
r = requests.post(url=url,
auth=oauth,
headers={'x_auth_mode': 'client_auth',
'x_auth_username': username,
'x_auth_password': password})
credentials = parse_qs(r.content)
return credentials.get('oauth_token')[0], credentials.get('oauth_token_secret')[0]
except Exception as e:
raise TweepError(e)
def get_username(self):
if self.username is None:
api = API(self)
user = api.verify_credentials()
if user:
self.username = user.screen_name
else:
raise TweepError('Unable to get username,'
' invalid oauth token!')
return self.username
class OAuth2Bearer(AuthBase):
def __init__(self, bearer_token):
self.bearer_token = bearer_token
def __call__(self, request):
request.headers['Authorization'] = 'Bearer ' + self.bearer_token
return request
class AppAuthHandler(AuthHandler):
"""Application-only authentication handler"""
OAUTH_HOST = 'api.twitter.com'
OAUTH_ROOT = '/oauth2/'
def __init__(self, consumer_key, consumer_secret):
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
self._bearer_token = ''
resp = requests.post(self._get_oauth_url('token'),
auth=(self.consumer_key,
self.consumer_secret),
data={'grant_type': 'client_credentials'})
data = resp.json()
if data.get('token_type') != 'bearer':
raise TweepError('Expected token_type to equal "bearer", '
'but got %s instead' % data.get('token_type'))
self._bearer_token = data['access_token']
def _get_oauth_url(self, endpoint):
return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
def apply_auth(self):
return OAuth2Bearer(self._bearer_token)

256
apprise/plugins/NotifyTwitter/tweepy/binder.py

@ -0,0 +1,256 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
from __future__ import print_function
import time
import re
from six.moves.urllib.parse import quote
import requests
import logging
from .error import TweepError, RateLimitError, is_rate_limit_error_message
from .utils import convert_to_utf8_str
from .models import Model
re_path_template = re.compile('{\w+}')
log = logging.getLogger('tweepy.binder')
def bind_api(**config):
class APIMethod(object):
api = config['api']
path = config['path']
payload_type = config.get('payload_type', None)
payload_list = config.get('payload_list', False)
allowed_param = config.get('allowed_param', [])
method = config.get('method', 'GET')
require_auth = config.get('require_auth', False)
search_api = config.get('search_api', False)
upload_api = config.get('upload_api', False)
use_cache = config.get('use_cache', True)
session = requests.Session()
def __init__(self, args, kwargs):
api = self.api
# If authentication is required and no credentials
# are provided, throw an error.
if self.require_auth and not api.auth:
raise TweepError('Authentication required!')
self.post_data = kwargs.pop('post_data', None)
self.retry_count = kwargs.pop('retry_count',
api.retry_count)
self.retry_delay = kwargs.pop('retry_delay',
api.retry_delay)
self.retry_errors = kwargs.pop('retry_errors',
api.retry_errors)
self.wait_on_rate_limit = kwargs.pop('wait_on_rate_limit',
api.wait_on_rate_limit)
self.wait_on_rate_limit_notify = kwargs.pop('wait_on_rate_limit_notify',
api.wait_on_rate_limit_notify)
self.parser = kwargs.pop('parser', api.parser)
self.session.headers = kwargs.pop('headers', {})
self.build_parameters(args, kwargs)
# Pick correct URL root to use
if self.search_api:
self.api_root = api.search_root
elif self.upload_api:
self.api_root = api.upload_root
else:
self.api_root = api.api_root
# Perform any path variable substitution
self.build_path()
if self.search_api:
self.host = api.search_host
elif self.upload_api:
self.host = api.upload_host
else:
self.host = api.host
# Manually set Host header to fix an issue in python 2.5
# or older where Host is set including the 443 port.
# This causes Twitter to issue 301 redirect.
# See Issue https://github.com/tweepy/tweepy/issues/12
self.session.headers['Host'] = self.host
# Monitoring rate limits
self._remaining_calls = None
self._reset_time = None
def build_parameters(self, args, kwargs):
self.session.params = {}
for idx, arg in enumerate(args):
if arg is None:
continue
try:
self.session.params[self.allowed_param[idx]] = convert_to_utf8_str(arg)
except IndexError:
raise TweepError('Too many parameters supplied!')
for k, arg in kwargs.items():
if arg is None:
continue
if k in self.session.params:
raise TweepError('Multiple values for parameter %s supplied!' % k)
self.session.params[k] = convert_to_utf8_str(arg)
log.info("PARAMS: %r", self.session.params)
def build_path(self):
for variable in re_path_template.findall(self.path):
name = variable.strip('{}')
if name == 'user' and 'user' not in self.session.params and self.api.auth:
# No 'user' parameter provided, fetch it from Auth instead.
value = self.api.auth.get_username()
else:
try:
value = quote(self.session.params[name])
except KeyError:
raise TweepError('No parameter value found for path variable: %s' % name)
del self.session.params[name]
self.path = self.path.replace(variable, value)
def execute(self):
self.api.cached_result = False
# Build the request URL
url = self.api_root + self.path
full_url = 'https://' + self.host + url
# Query the cache if one is available
# and this request uses a GET method.
if self.use_cache and self.api.cache and self.method == 'GET':
cache_result = self.api.cache.get(url)
# if cache result found and not expired, return it
if cache_result:
# must restore api reference
if isinstance(cache_result, list):
for result in cache_result:
if isinstance(result, Model):
result._api = self.api
else:
if isinstance(cache_result, Model):
cache_result._api = self.api
self.api.cached_result = True
return cache_result
# Continue attempting request until successful
# or maximum number of retries is reached.
retries_performed = 0
while retries_performed < self.retry_count + 1:
# handle running out of api calls
if self.wait_on_rate_limit:
if self._reset_time is not None:
if self._remaining_calls is not None:
if self._remaining_calls < 1:
sleep_time = self._reset_time - int(time.time())
if sleep_time > 0:
if self.wait_on_rate_limit_notify:
print("Rate limit reached. Sleeping for:", sleep_time)
time.sleep(sleep_time + 5) # sleep for few extra sec
# if self.wait_on_rate_limit and self._reset_time is not None and \
# self._remaining_calls is not None and self._remaining_calls < 1:
# sleep_time = self._reset_time - int(time.time())
# if sleep_time > 0:
# if self.wait_on_rate_limit_notify:
# print("Rate limit reached. Sleeping for: " + str(sleep_time))
# time.sleep(sleep_time + 5) # sleep for few extra sec
# Apply authentication
if self.api.auth:
auth = self.api.auth.apply_auth()
# Request compression if configured
if self.api.compression:
self.session.headers['Accept-encoding'] = 'gzip'
# Execute request
try:
resp = self.session.request(self.method,
full_url,
data=self.post_data,
timeout=self.api.timeout,
auth=auth,
proxies=self.api.proxy)
except Exception as e:
raise TweepError('Failed to send request: %s' % e)
rem_calls = resp.headers.get('x-rate-limit-remaining')
if rem_calls is not None:
self._remaining_calls = int(rem_calls)
elif isinstance(self._remaining_calls, int):
self._remaining_calls -= 1
reset_time = resp.headers.get('x-rate-limit-reset')
if reset_time is not None:
self._reset_time = int(reset_time)
if self.wait_on_rate_limit and self._remaining_calls == 0 and (
# if ran out of calls before waiting switching retry last call
resp.status_code == 429 or resp.status_code == 420):
continue
retry_delay = self.retry_delay
# Exit request loop if non-retry error code
if resp.status_code == 200:
break
elif (resp.status_code == 429 or resp.status_code == 420) and self.wait_on_rate_limit:
if 'retry-after' in resp.headers:
retry_delay = float(resp.headers['retry-after'])
elif self.retry_errors and resp.status_code not in self.retry_errors:
break
# Sleep before retrying request again
time.sleep(retry_delay)
retries_performed += 1
# If an error was returned, throw an exception
self.api.last_response = resp
if resp.status_code and not 200 <= resp.status_code < 300:
try:
error_msg, api_error_code = \
self.parser.parse_error(resp.text)
except Exception:
error_msg = "Twitter error response: status code = %s" % resp.status_code
api_error_code = None
if is_rate_limit_error_message(error_msg):
raise RateLimitError(error_msg, resp)
else:
raise TweepError(error_msg, resp, api_code=api_error_code)
# Parse the response payload
result = self.parser.parse(self, resp.text)
# Store result into cache if one is available.
if self.use_cache and self.api.cache and self.method == 'GET' and result:
self.api.cache.store(url, result)
return result
def _call(*args, **kwargs):
method = APIMethod(args, kwargs)
if kwargs.get('create'):
return method
else:
return method.execute()
# Set pagination mode
if 'cursor' in APIMethod.allowed_param:
_call.pagination_mode = 'cursor'
elif 'max_id' in APIMethod.allowed_param:
if 'since_id' in APIMethod.allowed_param:
_call.pagination_mode = 'id'
elif 'page' in APIMethod.allowed_param:
_call.pagination_mode = 'page'
return _call

435
apprise/plugins/NotifyTwitter/tweepy/cache.py

@ -0,0 +1,435 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
from __future__ import print_function
import time
import datetime
import threading
import os
try:
import cPickle as pickle
except ImportError:
import pickle
try:
import hashlib
except ImportError:
# python 2.4
import md5 as hashlib
try:
import fcntl
except ImportError:
# Probably on a windows system
# TODO: use win32file
pass
class Cache(object):
"""Cache interface"""
def __init__(self, timeout=60):
"""Initialize the cache
timeout: number of seconds to keep a cached entry
"""
self.timeout = timeout
def store(self, key, value):
"""Add new record to cache
key: entry key
value: data of entry
"""
raise NotImplementedError
def get(self, key, timeout=None):
"""Get cached entry if exists and not expired
key: which entry to get
timeout: override timeout with this value [optional]
"""
raise NotImplementedError
def count(self):
"""Get count of entries currently stored in cache"""
raise NotImplementedError
def cleanup(self):
"""Delete any expired entries in cache."""
raise NotImplementedError
def flush(self):
"""Delete all cached entries"""
raise NotImplementedError
class MemoryCache(Cache):
"""In-memory cache"""
def __init__(self, timeout=60):
Cache.__init__(self, timeout)
self._entries = {}
self.lock = threading.Lock()
def __getstate__(self):
# pickle
return {'entries': self._entries, 'timeout': self.timeout}
def __setstate__(self, state):
# unpickle
self.lock = threading.Lock()
self._entries = state['entries']
self.timeout = state['timeout']
def _is_expired(self, entry, timeout):
return timeout > 0 and (time.time() - entry[0]) >= timeout
def store(self, key, value):
self.lock.acquire()
self._entries[key] = (time.time(), value)
self.lock.release()
def get(self, key, timeout=None):
self.lock.acquire()
try:
# check to see if we have this key
entry = self._entries.get(key)
if not entry:
# no hit, return nothing
return None
# use provided timeout in arguments if provided
# otherwise use the one provided during init.
if timeout is None:
timeout = self.timeout
# make sure entry is not expired
if self._is_expired(entry, timeout):
# entry expired, delete and return nothing
del self._entries[key]
return None
# entry found and not expired, return it
return entry[1]
finally:
self.lock.release()
def count(self):
return len(self._entries)
def cleanup(self):
self.lock.acquire()
try:
for k, v in dict(self._entries).items():
if self._is_expired(v, self.timeout):
del self._entries[k]
finally:
self.lock.release()
def flush(self):
self.lock.acquire()
self._entries.clear()
self.lock.release()
class FileCache(Cache):
"""File-based cache"""
# locks used to make cache thread-safe
cache_locks = {}
def __init__(self, cache_dir, timeout=60):
Cache.__init__(self, timeout)
if os.path.exists(cache_dir) is False:
os.mkdir(cache_dir)
self.cache_dir = cache_dir
if cache_dir in FileCache.cache_locks:
self.lock = FileCache.cache_locks[cache_dir]
else:
self.lock = threading.Lock()
FileCache.cache_locks[cache_dir] = self.lock
if os.name == 'posix':
self._lock_file = self._lock_file_posix
self._unlock_file = self._unlock_file_posix
elif os.name == 'nt':
self._lock_file = self._lock_file_win32
self._unlock_file = self._unlock_file_win32
else:
print('Warning! FileCache locking not supported on this system!')
self._lock_file = self._lock_file_dummy
self._unlock_file = self._unlock_file_dummy
def _get_path(self, key):
md5 = hashlib.md5()
md5.update(key.encode('utf-8'))
return os.path.join(self.cache_dir, md5.hexdigest())
def _lock_file_dummy(self, path, exclusive=True):
return None
def _unlock_file_dummy(self, lock):
return
def _lock_file_posix(self, path, exclusive=True):
lock_path = path + '.lock'
if exclusive is True:
f_lock = open(lock_path, 'w')
fcntl.lockf(f_lock, fcntl.LOCK_EX)
else:
f_lock = open(lock_path, 'r')
fcntl.lockf(f_lock, fcntl.LOCK_SH)
if os.path.exists(lock_path) is False:
f_lock.close()
return None
return f_lock
def _unlock_file_posix(self, lock):
lock.close()
def _lock_file_win32(self, path, exclusive=True):
# TODO: implement
return None
def _unlock_file_win32(self, lock):
# TODO: implement
return
def _delete_file(self, path):
os.remove(path)
if os.path.exists(path + '.lock'):
os.remove(path + '.lock')
def store(self, key, value):
path = self._get_path(key)
self.lock.acquire()
try:
# acquire lock and open file
f_lock = self._lock_file(path)
datafile = open(path, 'wb')
# write data
pickle.dump((time.time(), value), datafile)
# close and unlock file
datafile.close()
self._unlock_file(f_lock)
finally:
self.lock.release()
def get(self, key, timeout=None):
return self._get(self._get_path(key), timeout)
def _get(self, path, timeout):
if os.path.exists(path) is False:
# no record
return None
self.lock.acquire()
try:
# acquire lock and open
f_lock = self._lock_file(path, False)
datafile = open(path, 'rb')
# read pickled object
created_time, value = pickle.load(datafile)
datafile.close()
# check if value is expired
if timeout is None:
timeout = self.timeout
if timeout > 0:
if (time.time() - created_time) >= timeout:
# expired! delete from cache
value = None
self._delete_file(path)
# unlock and return result
self._unlock_file(f_lock)
return value
finally:
self.lock.release()
def count(self):
c = 0
for entry in os.listdir(self.cache_dir):
if entry.endswith('.lock'):
continue
c += 1
return c
def cleanup(self):
for entry in os.listdir(self.cache_dir):
if entry.endswith('.lock'):
continue
self._get(os.path.join(self.cache_dir, entry), None)
def flush(self):
for entry in os.listdir(self.cache_dir):
if entry.endswith('.lock'):
continue
self._delete_file(os.path.join(self.cache_dir, entry))
class MemCacheCache(Cache):
"""Cache interface"""
def __init__(self, client, timeout=60):
"""Initialize the cache
client: The memcache client
timeout: number of seconds to keep a cached entry
"""
self.client = client
self.timeout = timeout
def store(self, key, value):
"""Add new record to cache
key: entry key
value: data of entry
"""
self.client.set(key, value, time=self.timeout)
def get(self, key, timeout=None):
"""Get cached entry if exists and not expired
key: which entry to get
timeout: override timeout with this value [optional].
DOES NOT WORK HERE
"""
return self.client.get(key)
def count(self):
"""Get count of entries currently stored in cache. RETURN 0"""
raise NotImplementedError
def cleanup(self):
"""Delete any expired entries in cache. NO-OP"""
raise NotImplementedError
def flush(self):
"""Delete all cached entries. NO-OP"""
raise NotImplementedError
class RedisCache(Cache):
"""Cache running in a redis server"""
def __init__(self, client,
timeout=60,
keys_container='tweepy:keys',
pre_identifier='tweepy:'):
Cache.__init__(self, timeout)
self.client = client
self.keys_container = keys_container
self.pre_identifier = pre_identifier
def _is_expired(self, entry, timeout):
# Returns true if the entry has expired
return timeout > 0 and (time.time() - entry[0]) >= timeout
def store(self, key, value):
"""Store the key, value pair in our redis server"""
# Prepend tweepy to our key,
# this makes it easier to identify tweepy keys in our redis server
key = self.pre_identifier + key
# Get a pipe (to execute several redis commands in one step)
pipe = self.client.pipeline()
# Set our values in a redis hash (similar to python dict)
pipe.set(key, pickle.dumps((time.time(), value)))
# Set the expiration
pipe.expire(key, self.timeout)
# Add the key to a set containing all the keys
pipe.sadd(self.keys_container, key)
# Execute the instructions in the redis server
pipe.execute()
def get(self, key, timeout=None):
"""Given a key, returns an element from the redis table"""
key = self.pre_identifier + key
# Check to see if we have this key
unpickled_entry = self.client.get(key)
if not unpickled_entry:
# No hit, return nothing
return None
entry = pickle.loads(unpickled_entry)
# Use provided timeout in arguments if provided
# otherwise use the one provided during init.
if timeout is None:
timeout = self.timeout
# Make sure entry is not expired
if self._is_expired(entry, timeout):
# entry expired, delete and return nothing
self.delete_entry(key)
return None
# entry found and not expired, return it
return entry[1]
def count(self):
"""Note: This is not very efficient,
since it retreives all the keys from the redis
server to know how many keys we have"""
return len(self.client.smembers(self.keys_container))
def delete_entry(self, key):
"""Delete an object from the redis table"""
pipe = self.client.pipeline()
pipe.srem(self.keys_container, key)
pipe.delete(key)
pipe.execute()
def cleanup(self):
"""Cleanup all the expired keys"""
keys = self.client.smembers(self.keys_container)
for key in keys:
entry = self.client.get(key)
if entry:
entry = pickle.loads(entry)
if self._is_expired(entry, self.timeout):
self.delete_entry(key)
def flush(self):
"""Delete all entries from the cache"""
keys = self.client.smembers(self.keys_container)
for key in keys:
self.delete_entry(key)
class MongodbCache(Cache):
"""A simple pickle-based MongoDB cache sytem."""
def __init__(self, db, timeout=3600, collection='tweepy_cache'):
"""Should receive a "database" cursor from pymongo."""
Cache.__init__(self, timeout)
self.timeout = timeout
self.col = db[collection]
self.col.create_index('created', expireAfterSeconds=timeout)
def store(self, key, value):
from bson.binary import Binary
now = datetime.datetime.utcnow()
blob = Binary(pickle.dumps(value))
self.col.insert({'created': now, '_id': key, 'value': blob})
def get(self, key, timeout=None):
if timeout:
raise NotImplementedError
obj = self.col.find_one({'_id': key})
if obj:
return pickle.loads(obj['value'])
def count(self):
return self.col.find({}).count()
def delete_entry(self, key):
return self.col.remove({'_id': key})
def cleanup(self):
"""MongoDB will automatically clear expired keys."""
pass
def flush(self):
self.col.drop()
self.col.create_index('created', expireAfterSeconds=self.timeout)

214
apprise/plugins/NotifyTwitter/tweepy/cursor.py

@ -0,0 +1,214 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
from __future__ import print_function
from .error import TweepError
from .parsers import ModelParser, RawParser
class Cursor(object):
"""Pagination helper class"""
def __init__(self, method, *args, **kargs):
if hasattr(method, 'pagination_mode'):
if method.pagination_mode == 'cursor':
self.iterator = CursorIterator(method, args, kargs)
elif method.pagination_mode == 'id':
self.iterator = IdIterator(method, args, kargs)
elif method.pagination_mode == 'page':
self.iterator = PageIterator(method, args, kargs)
else:
raise TweepError('Invalid pagination mode.')
else:
raise TweepError('This method does not perform pagination')
def pages(self, limit=0):
"""Return iterator for pages"""
if limit > 0:
self.iterator.limit = limit
return self.iterator
def items(self, limit=0):
"""Return iterator for items in each page"""
i = ItemIterator(self.iterator)
i.limit = limit
return i
class BaseIterator(object):
def __init__(self, method, args, kargs):
self.method = method
self.args = args
self.kargs = kargs
self.limit = 0
def __next__(self):
return self.next()
def next(self):
raise NotImplementedError
def prev(self):
raise NotImplementedError
def __iter__(self):
return self
class CursorIterator(BaseIterator):
def __init__(self, method, args, kargs):
BaseIterator.__init__(self, method, args, kargs)
start_cursor = kargs.pop('cursor', None)
self.next_cursor = start_cursor or -1
self.prev_cursor = start_cursor or 0
self.num_tweets = 0
def next(self):
if self.next_cursor == 0 or (self.limit and self.num_tweets == self.limit):
raise StopIteration
data, cursors = self.method(cursor=self.next_cursor,
*self.args,
**self.kargs)
self.prev_cursor, self.next_cursor = cursors
if len(data) == 0:
raise StopIteration
self.num_tweets += 1
return data
def prev(self):
if self.prev_cursor == 0:
raise TweepError('Can not page back more, at first page')
data, self.next_cursor, self.prev_cursor = self.method(cursor=self.prev_cursor,
*self.args,
**self.kargs)
self.num_tweets -= 1
return data
class IdIterator(BaseIterator):
def __init__(self, method, args, kargs):
BaseIterator.__init__(self, method, args, kargs)
self.max_id = kargs.pop('max_id', None)
self.num_tweets = 0
self.results = []
self.model_results = []
self.index = 0
def next(self):
"""Fetch a set of items with IDs less than current set."""
if self.limit and self.limit == self.num_tweets:
raise StopIteration
if self.index >= len(self.results) - 1:
data = self.method(max_id=self.max_id, parser=RawParser(), *self.args, **self.kargs)
if hasattr(self.method, '__self__'):
old_parser = self.method.__self__.parser
# Hack for models which expect ModelParser to be set
self.method.__self__.parser = ModelParser()
# This is a special invocation that returns the underlying
# APIMethod class
model = ModelParser().parse(self.method(create=True), data)
if hasattr(self.method, '__self__'):
self.method.__self__.parser = old_parser
result = self.method.__self__.parser.parse(self.method(create=True), data)
else:
result = model
if len(self.results) != 0:
self.index += 1
self.results.append(result)
self.model_results.append(model)
else:
self.index += 1
result = self.results[self.index]
model = self.model_results[self.index]
if len(result) == 0:
raise StopIteration
# TODO: Make this not dependant on the parser making max_id and
# since_id available
self.max_id = model.max_id
self.num_tweets += 1
return result
def prev(self):
"""Fetch a set of items with IDs greater than current set."""
if self.limit and self.limit == self.num_tweets:
raise StopIteration
self.index -= 1
if self.index < 0:
# There's no way to fetch a set of tweets directly 'above' the
# current set
raise StopIteration
data = self.results[self.index]
self.max_id = self.model_results[self.index].max_id
self.num_tweets += 1
return data
class PageIterator(BaseIterator):
def __init__(self, method, args, kargs):
BaseIterator.__init__(self, method, args, kargs)
self.current_page = 0
def next(self):
if self.limit > 0:
if self.current_page > self.limit:
raise StopIteration
items = self.method(page=self.current_page, *self.args, **self.kargs)
if len(items) == 0:
raise StopIteration
self.current_page += 1
return items
def prev(self):
if self.current_page == 1:
raise TweepError('Can not page back more, at first page')
self.current_page -= 1
return self.method(page=self.current_page, *self.args, **self.kargs)
class ItemIterator(BaseIterator):
def __init__(self, page_iterator):
self.page_iterator = page_iterator
self.limit = 0
self.current_page = None
self.page_index = -1
self.num_tweets = 0
def next(self):
if self.limit > 0:
if self.num_tweets == self.limit:
raise StopIteration
if self.current_page is None or self.page_index == len(self.current_page) - 1:
# Reached end of current page, get the next page...
self.current_page = self.page_iterator.next()
self.page_index = -1
self.page_index += 1
self.num_tweets += 1
return self.current_page[self.page_index]
def prev(self):
if self.current_page is None:
raise TweepError('Can not go back more, at first page')
if self.page_index == 0:
# At the beginning of the current page, move to next...
self.current_page = self.page_iterator.prev()
self.page_index = len(self.current_page)
if self.page_index == 0:
raise TweepError('No more items')
self.page_index -= 1
self.num_tweets -= 1
return self.current_page[self.page_index]

34
apprise/plugins/NotifyTwitter/tweepy/error.py

@ -0,0 +1,34 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
from __future__ import print_function
import six
class TweepError(Exception):
"""Tweepy exception"""
def __init__(self, reason, response=None, api_code=None):
self.reason = six.text_type(reason)
self.response = response
self.api_code = api_code
Exception.__init__(self, reason)
def __str__(self):
return self.reason
def is_rate_limit_error_message(message):
"""Check if the supplied error message belongs to a rate limit error."""
return isinstance(message, list) \
and len(message) > 0 \
and 'code' in message[0] \
and message[0]['code'] == 88
class RateLimitError(TweepError):
"""Exception for Tweepy hitting the rate limit."""
# RateLimitError has the exact same properties and inner workings
# as TweepError for backwards compatibility reasons.
pass

493
apprise/plugins/NotifyTwitter/tweepy/models.py

@ -0,0 +1,493 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
from __future__ import absolute_import, print_function
from .utils import parse_datetime, parse_html_value, parse_a_href
class ResultSet(list):
"""A list like object that holds results from a Twitter API query."""
def __init__(self, max_id=None, since_id=None):
super(ResultSet, self).__init__()
self._max_id = max_id
self._since_id = since_id
@property
def max_id(self):
if self._max_id:
return self._max_id
ids = self.ids()
# Max_id is always set to the *smallest* id, minus one, in the set
return (min(ids) - 1) if ids else None
@property
def since_id(self):
if self._since_id:
return self._since_id
ids = self.ids()
# Since_id is always set to the *greatest* id in the set
return max(ids) if ids else None
def ids(self):
return [item.id for item in self if hasattr(item, 'id')]
class Model(object):
def __init__(self, api=None):
self._api = api
def __getstate__(self):
# pickle
pickle = dict(self.__dict__)
try:
del pickle['_api'] # do not pickle the API reference
except KeyError:
pass
return pickle
@classmethod
def parse(cls, api, json):
"""Parse a JSON object into a model instance."""
raise NotImplementedError
@classmethod
def parse_list(cls, api, json_list):
"""
Parse a list of JSON objects into
a result set of model instances.
"""
results = ResultSet()
for obj in json_list:
if obj:
results.append(cls.parse(api, obj))
return results
def __repr__(self):
state = ['%s=%s' % (k, repr(v)) for (k, v) in vars(self).items()]
return '%s(%s)' % (self.__class__.__name__, ', '.join(state))
class Status(Model):
@classmethod
def parse(cls, api, json):
status = cls(api)
setattr(status, '_json', json)
for k, v in json.items():
if k == 'user':
user_model = getattr(api.parser.model_factory, 'user') if api else User
user = user_model.parse(api, v)
setattr(status, 'author', user)
setattr(status, 'user', user) # DEPRECIATED
elif k == 'created_at':
setattr(status, k, parse_datetime(v))
elif k == 'source':
if '<' in v:
setattr(status, k, parse_html_value(v))
setattr(status, 'source_url', parse_a_href(v))
else:
setattr(status, k, v)
setattr(status, 'source_url', None)
elif k == 'retweeted_status':
setattr(status, k, Status.parse(api, v))
elif k == 'place':
if v is not None:
setattr(status, k, Place.parse(api, v))
else:
setattr(status, k, None)
else:
setattr(status, k, v)
return status
def destroy(self):
return self._api.destroy_status(self.id)
def retweet(self):
return self._api.retweet(self.id)
def retweets(self):
return self._api.retweets(self.id)
def favorite(self):
return self._api.create_favorite(self.id)
def __eq__(self, other):
if isinstance(other, Status):
return self.id == other.id
return NotImplemented
def __ne__(self, other):
result = self == other
if result is NotImplemented:
return result
return not result
class User(Model):
@classmethod
def parse(cls, api, json):
user = cls(api)
setattr(user, '_json', json)
for k, v in json.items():
if k == 'created_at':
setattr(user, k, parse_datetime(v))
elif k == 'status':
setattr(user, k, Status.parse(api, v))
elif k == 'following':
# twitter sets this to null if it is false
if v is True:
setattr(user, k, True)
else:
setattr(user, k, False)
else:
setattr(user, k, v)
return user
@classmethod
def parse_list(cls, api, json_list):
if isinstance(json_list, list):
item_list = json_list
else:
item_list = json_list['users']
results = ResultSet()
for obj in item_list:
results.append(cls.parse(api, obj))
return results
def timeline(self, **kargs):
return self._api.user_timeline(user_id=self.id, **kargs)
def friends(self, **kargs):
return self._api.friends(user_id=self.id, **kargs)
def followers(self, **kargs):
return self._api.followers(user_id=self.id, **kargs)
def follow(self):
self._api.create_friendship(user_id=self.id)
self.following = True
def unfollow(self):
self._api.destroy_friendship(user_id=self.id)
self.following = False
def lists_memberships(self, *args, **kargs):
return self._api.lists_memberships(user=self.screen_name,
*args,
**kargs)
def lists_subscriptions(self, *args, **kargs):
return self._api.lists_subscriptions(user=self.screen_name,
*args,
**kargs)
def lists(self, *args, **kargs):
return self._api.lists_all(user=self.screen_name,
*args,
**kargs)
def followers_ids(self, *args, **kargs):
return self._api.followers_ids(user_id=self.id,
*args,
**kargs)
class DirectMessage(Model):
@classmethod
def parse(cls, api, json):
dm = cls(api)
for k, v in json.items():
if k == 'sender' or k == 'recipient':
setattr(dm, k, User.parse(api, v))
elif k == 'created_at':
setattr(dm, k, parse_datetime(v))
else:
setattr(dm, k, v)
return dm
def destroy(self):
return self._api.destroy_direct_message(self.id)
class Friendship(Model):
@classmethod
def parse(cls, api, json):
relationship = json['relationship']
# parse source
source = cls(api)
for k, v in relationship['source'].items():
setattr(source, k, v)
# parse target
target = cls(api)
for k, v in relationship['target'].items():
setattr(target, k, v)
return source, target
class Category(Model):
@classmethod
def parse(cls, api, json):
category = cls(api)
for k, v in json.items():
setattr(category, k, v)
return category
class SavedSearch(Model):
@classmethod
def parse(cls, api, json):
ss = cls(api)
for k, v in json.items():
if k == 'created_at':
setattr(ss, k, parse_datetime(v))
else:
setattr(ss, k, v)
return ss
def destroy(self):
return self._api.destroy_saved_search(self.id)
class SearchResults(ResultSet):
@classmethod
def parse(cls, api, json):
metadata = json['search_metadata']
results = SearchResults()
results.refresh_url = metadata.get('refresh_url')
results.completed_in = metadata.get('completed_in')
results.query = metadata.get('query')
results.count = metadata.get('count')
results.next_results = metadata.get('next_results')
status_model = getattr(api.parser.model_factory, 'status') if api else Status
for status in json['statuses']:
results.append(status_model.parse(api, status))
return results
class List(Model):
@classmethod
def parse(cls, api, json):
lst = List(api)
for k, v in json.items():
if k == 'user':
setattr(lst, k, User.parse(api, v))
elif k == 'created_at':
setattr(lst, k, parse_datetime(v))
else:
setattr(lst, k, v)
return lst
@classmethod
def parse_list(cls, api, json_list, result_set=None):
results = ResultSet()
if isinstance(json_list, dict):
json_list = json_list['lists']
for obj in json_list:
results.append(cls.parse(api, obj))
return results
def update(self, **kargs):
return self._api.update_list(self.slug, **kargs)
def destroy(self):
return self._api.destroy_list(self.slug)
def timeline(self, **kargs):
return self._api.list_timeline(self.user.screen_name,
self.slug,
**kargs)
def add_member(self, id):
return self._api.add_list_member(self.slug, id)
def remove_member(self, id):
return self._api.remove_list_member(self.slug, id)
def members(self, **kargs):
return self._api.list_members(self.user.screen_name,
self.slug,
**kargs)
def is_member(self, id):
return self._api.is_list_member(self.user.screen_name,
self.slug,
id)
def subscribe(self):
return self._api.subscribe_list(self.user.screen_name, self.slug)
def unsubscribe(self):
return self._api.unsubscribe_list(self.user.screen_name, self.slug)
def subscribers(self, **kargs):
return self._api.list_subscribers(self.user.screen_name,
self.slug,
**kargs)
def is_subscribed(self, id):
return self._api.is_subscribed_list(self.user.screen_name,
self.slug,
id)
class Relation(Model):
@classmethod
def parse(cls, api, json):
result = cls(api)
for k, v in json.items():
if k == 'value' and json['kind'] in ['Tweet', 'LookedupStatus']:
setattr(result, k, Status.parse(api, v))
elif k == 'results':
setattr(result, k, Relation.parse_list(api, v))
else:
setattr(result, k, v)
return result
class Relationship(Model):
@classmethod
def parse(cls, api, json):
result = cls(api)
for k, v in json.items():
if k == 'connections':
setattr(result, 'is_following', 'following' in v)
setattr(result, 'is_followed_by', 'followed_by' in v)
else:
setattr(result, k, v)
return result
class JSONModel(Model):
@classmethod
def parse(cls, api, json):
return json
class IDModel(Model):
@classmethod
def parse(cls, api, json):
if isinstance(json, list):
return json
else:
return json['ids']
class BoundingBox(Model):
@classmethod
def parse(cls, api, json):
result = cls(api)
if json is not None:
for k, v in json.items():
setattr(result, k, v)
return result
def origin(self):
"""
Return longitude, latitude of southwest (bottom, left) corner of
bounding box, as a tuple.
This assumes that bounding box is always a rectangle, which
appears to be the case at present.
"""
return tuple(self.coordinates[0][0])
def corner(self):
"""
Return longitude, latitude of northeast (top, right) corner of
bounding box, as a tuple.
This assumes that bounding box is always a rectangle, which
appears to be the case at present.
"""
return tuple(self.coordinates[0][2])
class Place(Model):
@classmethod
def parse(cls, api, json):
place = cls(api)
for k, v in json.items():
if k == 'bounding_box':
# bounding_box value may be null (None.)
# Example: "United States" (id=96683cc9126741d1)
if v is not None:
t = BoundingBox.parse(api, v)
else:
t = v
setattr(place, k, t)
elif k == 'contained_within':
# contained_within is a list of Places.
setattr(place, k, Place.parse_list(api, v))
else:
setattr(place, k, v)
return place
@classmethod
def parse_list(cls, api, json_list):
if isinstance(json_list, list):
item_list = json_list
else:
item_list = json_list['result']['places']
results = ResultSet()
for obj in item_list:
results.append(cls.parse(api, obj))
return results
class Media(Model):
@classmethod
def parse(cls, api, json):
media = cls(api)
for k, v in json.items():
setattr(media, k, v)
return media
class ModelFactory(object):
"""
Used by parsers for creating instances
of models. You may subclass this factory
to add your own extended models.
"""
status = Status
user = User
direct_message = DirectMessage
friendship = Friendship
saved_search = SavedSearch
search_results = SearchResults
category = Category
list = List
relation = Relation
relationship = Relationship
media = Media
json = JSONModel
ids = IDModel
place = Place
bounding_box = BoundingBox

109
apprise/plugins/NotifyTwitter/tweepy/parsers.py

@ -0,0 +1,109 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
from __future__ import print_function
from .models import ModelFactory
from .utils import import_simplejson
from .error import TweepError
class Parser(object):
def parse(self, method, payload):
"""
Parse the response payload and return the result.
Returns a tuple that contains the result data and the cursors
(or None if not present).
"""
raise NotImplementedError
def parse_error(self, payload):
"""
Parse the error message and api error code from payload.
Return them as an (error_msg, error_code) tuple. If unable to parse the
message, throw an exception and default error message will be used.
"""
raise NotImplementedError
class RawParser(Parser):
def __init__(self):
pass
def parse(self, method, payload):
return payload
def parse_error(self, payload):
return payload
class JSONParser(Parser):
payload_format = 'json'
def __init__(self):
self.json_lib = import_simplejson()
def parse(self, method, payload):
try:
json = self.json_lib.loads(payload)
except Exception as e:
raise TweepError('Failed to parse JSON payload: %s' % e)
needs_cursors = 'cursor' in method.session.params
if needs_cursors and isinstance(json, dict):
if 'previous_cursor' in json:
if 'next_cursor' in json:
cursors = json['previous_cursor'], json['next_cursor']
return json, cursors
else:
return json
def parse_error(self, payload):
error_object = self.json_lib.loads(payload)
if 'error' in error_object:
reason = error_object['error']
api_code = error_object.get('code')
else:
reason = error_object['errors']
api_code = [error.get('code') for error in
reason if error.get('code')]
api_code = api_code[0] if len(api_code) == 1 else api_code
return reason, api_code
class ModelParser(JSONParser):
def __init__(self, model_factory=None):
JSONParser.__init__(self)
self.model_factory = model_factory or ModelFactory
def parse(self, method, payload):
try:
if method.payload_type is None:
return
model = getattr(self.model_factory, method.payload_type)
except AttributeError:
raise TweepError('No model for this payload type: '
'%s' % method.payload_type)
json = JSONParser.parse(self, method, payload)
if isinstance(json, tuple):
json, cursors = json
else:
cursors = None
if method.payload_list:
result = model.parse_list(method.api, json)
else:
result = model.parse(method.api, json)
if cursors:
return result, cursors
else:
return result

466
apprise/plugins/NotifyTwitter/tweepy/streaming.py

@ -0,0 +1,466 @@
# Tweepy
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
# Appengine users: https://developers.google.com/appengine/docs/python/sockets/#making_httplib_use_sockets
from __future__ import absolute_import, print_function
import logging
import re
import requests
from requests.exceptions import Timeout
from threading import Thread
from time import sleep
import six
import ssl
from .models import Status
from .api import API
from .error import TweepError
from .utils import import_simplejson
json = import_simplejson()
STREAM_VERSION = '1.1'
class StreamListener(object):
def __init__(self, api=None):
self.api = api or API()
def on_connect(self):
"""Called once connected to streaming server.
This will be invoked once a successful response
is received from the server. Allows the listener
to perform some work prior to entering the read loop.
"""
pass
def on_data(self, raw_data):
"""Called when raw data is received from connection.
Override this method if you wish to manually handle
the stream data. Return False to stop stream and close connection.
"""
data = json.loads(raw_data)
if 'in_reply_to_status_id' in data:
status = Status.parse(self.api, data)
if self.on_status(status) is False:
return False
elif 'delete' in data:
delete = data['delete']['status']
if self.on_delete(delete['id'], delete['user_id']) is False:
return False
elif 'event' in data:
status = Status.parse(self.api, data)
if self.on_event(status) is False:
return False
elif 'direct_message' in data:
status = Status.parse(self.api, data)
if self.on_direct_message(status) is False:
return False
elif 'friends' in data:
if self.on_friends(data['friends']) is False:
return False
elif 'limit' in data:
if self.on_limit(data['limit']['track']) is False:
return False
elif 'disconnect' in data:
if self.on_disconnect(data['disconnect']) is False:
return False
elif 'warning' in data:
if self.on_warning(data['warning']) is False:
return False
else:
logging.error("Unknown message type: " + str(raw_data))
def keep_alive(self):
"""Called when a keep-alive arrived"""
return
def on_status(self, status):
"""Called when a new status arrives"""
return
def on_exception(self, exception):
"""Called when an unhandled exception occurs."""
return
def on_delete(self, status_id, user_id):
"""Called when a delete notice arrives for a status"""
return
def on_event(self, status):
"""Called when a new event arrives"""
return
def on_direct_message(self, status):
"""Called when a new direct message arrives"""
return
def on_friends(self, friends):
"""Called when a friends list arrives.
friends is a list that contains user_id
"""
return
def on_limit(self, track):
"""Called when a limitation notice arrives"""
return
def on_error(self, status_code):
"""Called when a non-200 status code is returned"""
return False
def on_timeout(self):
"""Called when stream connection times out"""
return
def on_disconnect(self, notice):
"""Called when twitter sends a disconnect notice
Disconnect codes are listed here:
https://dev.twitter.com/docs/streaming-apis/messages#Disconnect_messages_disconnect
"""
return
def on_warning(self, notice):
"""Called when a disconnection warning message arrives"""
return
class ReadBuffer(object):
"""Buffer data from the response in a smarter way than httplib/requests can.
Tweets are roughly in the 2-12kb range, averaging around 3kb.
Requests/urllib3/httplib/socket all use socket.read, which blocks
until enough data is returned. On some systems (eg google appengine), socket
reads are quite slow. To combat this latency we can read big chunks,
but the blocking part means we won't get results until enough tweets
have arrived. That may not be a big deal for high throughput systems.
For low throughput systems we don't want to sacrafice latency, so we
use small chunks so it can read the length and the tweet in 2 read calls.
"""
def __init__(self, stream, chunk_size, encoding='utf-8'):
self._stream = stream
self._buffer = six.b('')
self._chunk_size = chunk_size
self._encoding = encoding
def read_len(self, length):
while not self._stream.closed:
if len(self._buffer) >= length:
return self._pop(length)
read_len = max(self._chunk_size, length - len(self._buffer))
self._buffer += self._stream.read(read_len)
def read_line(self, sep=six.b('\n')):
"""Read the data stream until a given separator is found (default \n)
:param sep: Separator to read until. Must by of the bytes type (str in python 2,
bytes in python 3)
:return: The str of the data read until sep
"""
start = 0
while not self._stream.closed:
loc = self._buffer.find(sep, start)
if loc >= 0:
return self._pop(loc + len(sep))
else:
start = len(self._buffer)
self._buffer += self._stream.read(self._chunk_size)
def _pop(self, length):
r = self._buffer[:length]
self._buffer = self._buffer[length:]
return r.decode(self._encoding)
class Stream(object):
host = 'stream.twitter.com'
def __init__(self, auth, listener, **options):
self.auth = auth
self.listener = listener
self.running = False
self.timeout = options.get("timeout", 300.0)
self.retry_count = options.get("retry_count")
# values according to
# https://dev.twitter.com/docs/streaming-apis/connecting#Reconnecting
self.retry_time_start = options.get("retry_time", 5.0)
self.retry_420_start = options.get("retry_420", 60.0)
self.retry_time_cap = options.get("retry_time_cap", 320.0)
self.snooze_time_step = options.get("snooze_time", 0.25)
self.snooze_time_cap = options.get("snooze_time_cap", 16)
# The default socket.read size. Default to less than half the size of
# a tweet so that it reads tweets with the minimal latency of 2 reads
# per tweet. Values higher than ~1kb will increase latency by waiting
# for more data to arrive but may also increase throughput by doing
# fewer socket read calls.
self.chunk_size = options.get("chunk_size", 512)
self.verify = options.get("verify", True)
self.api = API()
self.headers = options.get("headers") or {}
self.new_session()
self.body = None
self.retry_time = self.retry_time_start
self.snooze_time = self.snooze_time_step
def new_session(self):
self.session = requests.Session()
self.session.headers = self.headers
self.session.params = None
def _run(self):
# Authenticate
url = "https://%s%s" % (self.host, self.url)
# Connect and process the stream
error_counter = 0
resp = None
exception = None
while self.running:
if self.retry_count is not None:
if error_counter > self.retry_count:
# quit if error count greater than retry count
break
try:
auth = self.auth.apply_auth()
resp = self.session.request('POST',
url,
data=self.body,
timeout=self.timeout,
stream=True,
auth=auth,
verify=self.verify)
if resp.status_code != 200:
if self.listener.on_error(resp.status_code) is False:
break
error_counter += 1
if resp.status_code == 420:
self.retry_time = max(self.retry_420_start,
self.retry_time)
sleep(self.retry_time)
self.retry_time = min(self.retry_time * 2,
self.retry_time_cap)
else:
error_counter = 0
self.retry_time = self.retry_time_start
self.snooze_time = self.snooze_time_step
self.listener.on_connect()
self._read_loop(resp)
except (Timeout, ssl.SSLError) as exc:
# This is still necessary, as a SSLError can actually be
# thrown when using Requests
# If it's not time out treat it like any other exception
if isinstance(exc, ssl.SSLError):
if not (exc.args and 'timed out' in str(exc.args[0])):
exception = exc
break
if self.listener.on_timeout() is False:
break
if self.running is False:
break
sleep(self.snooze_time)
self.snooze_time = min(self.snooze_time + self.snooze_time_step,
self.snooze_time_cap)
except Exception as exc:
exception = exc
# any other exception is fatal, so kill loop
break
# cleanup
self.running = False
if resp:
resp.close()
self.new_session()
if exception:
# call a handler first so that the exception can be logged.
self.listener.on_exception(exception)
raise exception
def _data(self, data):
if self.listener.on_data(data) is False:
self.running = False
def _read_loop(self, resp):
charset = resp.headers.get('content-type', default='')
enc_search = re.search('charset=(?P<enc>\S*)', charset)
if enc_search is not None:
encoding = enc_search.group('enc')
else:
encoding = 'utf-8'
buf = ReadBuffer(resp.raw, self.chunk_size, encoding=encoding)
while self.running and not resp.raw.closed:
length = 0
while not resp.raw.closed:
line = buf.read_line().strip()
if not line:
self.listener.keep_alive() # keep-alive new lines are expected
elif line.isdigit():
length = int(line)
break
else:
raise TweepError('Expecting length, unexpected value found')
next_status_obj = buf.read_len(length)
if self.running:
self._data(next_status_obj)
# # Note: keep-alive newlines might be inserted before each length value.
# # read until we get a digit...
# c = b'\n'
# for c in resp.iter_content(decode_unicode=True):
# if c == b'\n':
# continue
# break
#
# delimited_string = c
#
# # read rest of delimiter length..
# d = b''
# for d in resp.iter_content(decode_unicode=True):
# if d != b'\n':
# delimited_string += d
# continue
# break
#
# # read the next twitter status object
# if delimited_string.decode('utf-8').strip().isdigit():
# status_id = int(delimited_string)
# next_status_obj = resp.raw.read(status_id)
# if self.running:
# self._data(next_status_obj.decode('utf-8'))
if resp.raw.closed:
self.on_closed(resp)
def _start(self, async):
self.running = True
if async:
self._thread = Thread(target=self._run)
self._thread.start()
else:
self._run()
def on_closed(self, resp):
""" Called when the response has been closed by Twitter """
pass
def userstream(self,
stall_warnings=False,
_with=None,
replies=None,
track=None,
locations=None,
async=False,
encoding='utf8'):
self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/%s/user.json' % STREAM_VERSION
self.host = 'userstream.twitter.com'
if stall_warnings:
self.session.params['stall_warnings'] = stall_warnings
if _with:
self.session.params['with'] = _with
if replies:
self.session.params['replies'] = replies
if locations and len(locations) > 0:
if len(locations) % 4 != 0:
raise TweepError("Wrong number of locations points, "
"it has to be a multiple of 4")
self.session.params['locations'] = ','.join(['%.2f' % l for l in locations])
if track:
self.session.params['track'] = u','.join(track).encode(encoding)
self._start(async)
def firehose(self, count=None, async=False):
self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/%s/statuses/firehose.json' % STREAM_VERSION
if count:
self.url += '&count=%s' % count
self._start(async)
def retweet(self, async=False):
self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/%s/statuses/retweet.json' % STREAM_VERSION
self._start(async)
def sample(self, async=False, languages=None):
self.session.params = {'delimited': 'length'}
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/%s/statuses/sample.json' % STREAM_VERSION
if languages:
self.session.params['language'] = ','.join(map(str, languages))
self._start(async)
def filter(self, follow=None, track=None, async=False, locations=None,
stall_warnings=False, languages=None, encoding='utf8', filter_level=None):
self.body = {}
self.session.headers['Content-type'] = "application/x-www-form-urlencoded"
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/%s/statuses/filter.json' % STREAM_VERSION
if follow:
self.body['follow'] = u','.join(follow).encode(encoding)
if track:
self.body['track'] = u','.join(track).encode(encoding)
if locations and len(locations) > 0:
if len(locations) % 4 != 0:
raise TweepError("Wrong number of locations points, "
"it has to be a multiple of 4")
self.body['locations'] = u','.join(['%.4f' % l for l in locations])
if stall_warnings:
self.body['stall_warnings'] = stall_warnings
if languages:
self.body['language'] = u','.join(map(str, languages))
if filter_level:
self.body['filter_level'] = unicode(filter_level, encoding)
self.session.params = {'delimited': 'length'}
self.host = 'stream.twitter.com'
self._start(async)
def sitestream(self, follow, stall_warnings=False,
with_='user', replies=False, async=False):
self.body = {}
if self.running:
raise TweepError('Stream object already connected!')
self.url = '/%s/site.json' % STREAM_VERSION
self.body['follow'] = u','.join(map(six.text_type, follow))
self.body['delimited'] = 'length'
if stall_warnings:
self.body['stall_warnings'] = stall_warnings
if with_:
self.body['with'] = with_
if replies:
self.body['replies'] = replies
self._start(async)
def disconnect(self):
if self.running is False:
return
self.running = False

58
apprise/plugins/NotifyTwitter/tweepy/utils.py

@ -0,0 +1,58 @@
# Tweepy
# Copyright 2010 Joshua Roesslein
# See LICENSE for details.
from __future__ import print_function
from datetime import datetime
import six
from six.moves.urllib.parse import quote
from email.utils import parsedate
def parse_datetime(string):
return datetime(*(parsedate(string)[:6]))
def parse_html_value(html):
return html[html.find('>')+1:html.rfind('<')]
def parse_a_href(atag):
start = atag.find('"') + 1
end = atag.find('"', start)
return atag[start:end]
def convert_to_utf8_str(arg):
# written by Michael Norton (http://docondev.blogspot.com/)
if isinstance(arg, six.text_type):
arg = arg.encode('utf-8')
elif not isinstance(arg, bytes):
arg = six.text_type(arg).encode('utf-8')
return arg
def import_simplejson():
try:
import simplejson as json
except ImportError:
try:
import json # Python 2.6+
except ImportError:
try:
# Google App Engine
from django.utils import simplejson as json
except ImportError:
raise ImportError("Can't load a json library")
return json
def list_to_csv(item_list):
if item_list:
return ','.join([str(i) for i in item_list])

221
apprise/plugins/NotifyXBMC.py

@ -0,0 +1,221 @@
# -*- encoding: utf-8 -*-
#
# XBMC Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from json import dumps
import requests
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import NotifyType
from .NotifyBase import NotifyImageSize
from .NotifyBase import HTTP_ERROR_MAP
# Image Support (128x128)
XBMC_IMAGE_XY = NotifyImageSize.XY_128
# XBMC uses the http protocol with JSON requests
XBMC_PORT = 8080
XBMC_PROTOCOL_V2 = 2
XBMC_PROTOCOL_V6 = 6
SUPPORTED_XBMC_PROTOCOLS = (
XBMC_PROTOCOL_V2,
XBMC_PROTOCOL_V6,
)
class NotifyXBMC(NotifyBase):
"""
A wrapper for XBMC/KODI Notifications
"""
# The default protocol
PROTOCOL = ('xbmc', 'kodi')
# The default secure protocol
SECURE_PROTOCOL = ('xbmc', 'kodis')
def __init__(self, **kwargs):
"""
Initialize XBMC/KODI Object
"""
super(NotifyXBMC, self).__init__(
title_maxlen=250, body_maxlen=32768,
image_size=XBMC_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
if not self.port:
self.port = XBMC_PORT
self.protocol = kwargs.get('protocol', XBMC_PROTOCOL_V2)
if self.protocol not in SUPPORTED_XBMC_PROTOCOLS:
raise TypeError("Invalid protocol specified.")
return
def _payload_60(self, title, body, notify_type, **kwargs):
"""
Builds payload for KODI API v6.0
Returns (headers, payload)
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
# prepare JSON Object
payload = {
'jsonrpc': '6.0',
'method': 'GUI.ShowNotification',
'params': {
'title': title,
'message': body,
# displaytime is defined in microseconds
'displaytime': 12000,
},
'id': 1,
}
if self.include_image:
image_url = self.image_url(
notify_type,
)
if image_url:
payload['image'] = image_url
if notify_type is NotifyType.Error:
payload['type'] = 'error'
elif notify_type is NotifyType.Warning:
payload['type'] = 'warning'
else:
payload['type'] = 'info'
return (headers, dumps(payload))
def _payload_20(self, title, body, notify_type, **kwargs):
"""
Builds payload for XBMC API v2.0
Returns (headers, payload)
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
# prepare JSON Object
payload = {
'jsonrpc': '2.0',
'method': 'GUI.ShowNotification',
'params': {
'title': title,
'message': body,
# displaytime is defined in microseconds
'displaytime': 12000,
},
'id': 1,
}
if self.include_image:
image_url = self.image_url(
notify_type,
)
if image_url:
payload['image'] = image_url
return (headers, dumps(payload))
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform XBMC Notification
"""
if self.protocol == XBMC_PROTOCOL_V2:
# XBMC v2.0
(headers, payload) = self._payload_20(
title, body, notify_type, **kwargs)
else:
# XBMC v6.0
(headers, payload) = self._payload_60(
title, body, notify_type, **kwargs)
auth = None
if self.user:
auth = (self.user, self.password)
url = '%s://%s' % (self.schema, self.host)
if isinstance(self.port, int):
url += ':%d' % self.port
url += '/jsonrpc'
self.logger.debug('XBMC/KODI POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('XBMC/KODI Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=payload,
headers=headers,
auth=auth,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send XBMC/KODI notification:'
'%s (error=%s).' % (
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send XBMC/KODI notification '
'(error=%s).' % r.status_code)
# Return; we're done
return False
else:
self.logger.info('Sent XBMC/KODI notification.')
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending XBMC/KODI '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

154
apprise/plugins/NotifyXML.py

@ -0,0 +1,154 @@
# -*- encoding: utf-8 -*-
#
# XML Notify Wrapper
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from urllib import quote
import requests
import re
from .NotifyBase import NotifyBase
from .NotifyBase import NotifyFormat
from .NotifyBase import NotifyImageSize
from .NotifyBase import HTTP_ERROR_MAP
# Image Support (128x128)
XML_IMAGE_XY = NotifyImageSize.XY_128
class NotifyXML(NotifyBase):
"""
A wrapper for XML Notifications
"""
# The default protocol
PROTOCOL = 'xml'
# The default secure protocol
SECURE_PROTOCOL = 'xmls'
def __init__(self, **kwargs):
"""
Initialize XML Object
"""
super(NotifyXML, self).__init__(
title_maxlen=250, body_maxlen=32768,
image_size=XML_IMAGE_XY,
notify_format=NotifyFormat.TEXT,
**kwargs)
self.payload = """<?xml version='1.0' encoding='utf-8'?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<Notification xmlns:xsi="http://nuxref.com/apprise/NotifyXML-1.0.xsd">
<Version>1.0</Version>
<Subject>{SUBJECT}</Subject>
<MessageType>{MESSAGE_TYPE}</MessageType>
<Message>{MESSAGE}</Message>
</Notification>
</soapenv:Body>
</soapenv:Envelope>"""
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, basestring):
self.fullpath = '/'
return
def _notify(self, title, body, notify_type, **kwargs):
"""
Perform XML Notification
"""
# prepare XML Object
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/xml'
}
re_map = {
'{MESSAGE_TYPE}': quote(notify_type),
'{SUBJECT}': quote(title),
'{MESSAGE}': quote(body),
}
# Iterate over above list and store content accordingly
re_table = re.compile(
r'(' + '|'.join(re_map.keys()) + r')',
re.IGNORECASE,
)
auth = None
if self.user:
auth = (self.user, self.password)
url = '%s://%s' % (self.schema, self.host)
if isinstance(self.port, int):
url += ':%d' % self.port
url += self.fullpath
payload = re_table.sub(lambda x: re_map[x.group()], self.payload)
self.logger.debug('XML POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('XML Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=payload,
headers=headers,
auth=auth,
verify=self.verify_certificate,
)
if r.status_code != requests.codes.ok:
try:
self.logger.warning(
'Failed to send XML notification: '
'%s (error=%s).' % (
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send XML notification '
'(error=%s).' % r.status_code)
# Return; we're done
return False
except requests.ConnectionError as e:
self.logger.warning(
'A Connection error occured sending XML '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True

50
apprise/plugins/__init__.py

@ -0,0 +1,50 @@
# -*- encoding: utf-8 -*-
#
# Our service wrappers
#
# Copyright (C) 2014-2017 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# apprise is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# apprise is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with apprise. If not, see <http://www.gnu.org/licenses/>.
from .NotifyBoxcar import NotifyBoxcar
from .NotifyEmail import NotifyEmail
from .NotifyFaast import NotifyFaast
from .NotifyGrowl import NotifyGrowl
from .NotifyJSON import NotifyJSON
from .NotifyMyAndroid import NotifyMyAndroid
from .NotifyProwl import NotifyProwl
from .NotifyPushalot import NotifyPushalot
from .NotifyPushBullet import NotifyPushBullet
from .NotifyPushover import NotifyPushover
from .NotifyRocketChat import NotifyRocketChat
from .NotifyToasty import NotifyToasty
from .NotifyTwitter import NotifyTwitter
from .NotifyXBMC import NotifyXBMC
from .NotifyXML import NotifyXML
from .NotifySlack import NotifySlack
from .NotifyJoin import NotifyJoin
from .NotifyTelegram import NotifyTelegram
from .NotifyMatterMost import NotifyMatterMost
from .NotifyPushjet import NotifyPushjet
__all__ = [
# Notification Services
'NotifyBoxcar', 'NotifyEmail', 'NotifyFaast', 'NotifyGrowl', 'NotifyJSON',
'NotifyMyAndroid', 'NotifyProwl', 'NotifyPushalot', 'NotifyPushBullet',
'NotifyPushover', 'NotifyRocketChat', 'NotifyToasty', 'NotifyTwitter',
'NotifyXBMC', 'NotifyXML', 'NotifySlack', 'NotifyJoin', 'NotifyTelegram',
'NotifyMatterMost', 'NotifyPushjet'
]

22
apprise/var/NotifyXML-1.0.xsd

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Notification">
<xs:complexType>
<xs:sequence>
<xs:element name="Version" type="xs:string" />
<xs:element name="MessageType" type="xs:string" />
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="success" />
<xs:enumeration value="failure" />
<xs:enumeration value="info" />
<xs:enumeration value="warning" />
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="Subject" type="xs:string" />
<xs:element name="Message" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

BIN
apprise/var/apprise-failure-128x128.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
apprise/var/apprise-failure-256x256.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
apprise/var/apprise-failure-72x72.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
apprise/var/apprise-info-128x128.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
apprise/var/apprise-info-256x256.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
apprise/var/apprise-info-72x72.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
apprise/var/apprise-success-128x128.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
apprise/var/apprise-success-256x256.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
apprise/var/apprise-success-72x72.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
apprise/var/apprise-warning-128x128.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
apprise/var/apprise-warning-256x256.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
apprise/var/apprise-warning-72x72.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

26
bin/apprise.py

@ -0,0 +1,26 @@
#!/usr/bin/env python
def _main():
"""\
Usage: apprise [options] [URL ...]
Send notifications to a variety of different supported services.
See also https://github.com/caronc/apprise
URL The notification service URL
Options:
-h, --help show this message
-t TITLE, --title TITLE Specify a notification title.
-b BODY, --body BODY Specify a notification body.
-i IMGURL, --image IMGURL Specify an image to send with the notification.
The image should be in the format of a URL
string such as file:///local/path/to/file.png or
a remote site like: http://my.host/my.image.png.
"""
if __name__ == '__main__':
_main()

9
requirements.txt

@ -0,0 +1,9 @@
chardet
markdown
decorator
requests
requests-oauthlib
oauthlib
urllib3
six
click

4
setup.cfg

@ -0,0 +1,4 @@
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

64
setup.py

@ -0,0 +1,64 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# SetupTools Script
#
# Copyright (C) 2017 Chris Caron <lead2gold@gmail.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
import os
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
from setuptools import find_packages
install_options = os.environ.get("APPRISE_INSTALL", "").split(",")
libonly_flags = set(["lib-only", "libonly", "no-cli", "without-cli"])
if libonly_flags.intersection(install_options):
console_scripts = []
else:
console_scripts = ['apprise = apprise:_main']
setup(
name='apprise',
version='0.0.1',
description='A friendly notification hub',
license='GPLv3',
long_description=open('README.md').read(),
url='https://github.com/caronc/apprise',
keywords='push notifications email boxcar faast growl Join KODI '
'Mattermost NotifyMyAndroid Prowl Pushalot PushBullet Pushjet '
'Pushover Rocket.Chat Slack Toasty Telegram Twitter XBMC ',
author='Chris Caron',
author_email='lead2gold@gmail.com',
packages=find_packages(),
package_data={
'apprise': ['var/*'],
},
include_package_data=True,
scripts=['bin/apprise.py', ],
install_requires=open('requirements.txt').readlines(),
classifiers=(
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Natural Language :: English',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
),
entry_points={'console_scripts': console_scripts},
python_requires='>=2.7, <3',
)

12
test/test_api.py

@ -0,0 +1,12 @@
"""API properties.
"""
from __future__ import print_function
from __future__ import unicode_literals
from apprise import Apprise
def test_initialization():
"API: apprise() test initialization"""
a = Apprise()
Loading…
Cancel
Save