Added IFTTT support; fixes #7

This commit is contained in:
Chris Caron 2018-03-08 20:20:51 -05:00
parent 3057077451
commit afddd9e897
4 changed files with 327 additions and 1 deletions

View File

@ -26,6 +26,7 @@ The table below identifies the services this tool supports and some example serv
| [Emby](https://github.com/caronc/apprise/wiki/Notify_emby) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/<br />emby://user:password@hostname
| [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
| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/EventToTrigger<br />ifttt://webhooksID/EventToTrigger/Value1/Value2/Value3<br />ifttt://webhooksID/EventToTrigger/?Value3=NewEntry&Value2=AnotherValue
| [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 />

View File

@ -0,0 +1,218 @@
# -*- coding: utf-8 -*-
#
# IFTTT (If-This-Then-That)
#
# Copyright (C) 2017-2018 Chris Caron <lead2gold@gmail.com>
#
# This file is part of apprise.
#
# 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.
#
# For this plugin to work, you need to add the Maker applet to your profile
# Simply visit https://ifttt.com/search and search for 'Webhooks'
# Or if you're signed in, click here: https://ifttt.com/maker_webhooks
# and click 'Connect'
#
# You'll want to visit the settings of this Applet and pay attention to the
# URL. For example, it might look like this:
# https://maker.ifttt.com/use/a3nHB7gA9TfBQSqJAHklod
#
# In the above example a3nHB7gA9TfBQSqJAHklod becomes your {apikey}
# You will need this to make this notification work correctly
#
# For each event you create you will assign it a name (this will be known as
# the {event} when building your URL.
import requests
from json import dumps
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
class NotifyIFTTT(NotifyBase):
"""
A wrapper for IFTTT Notifications
"""
# Even though you'll add 'Ingredients' as {{ Value1 }} to your Applets,
# you must use their lowercase value in the HTTP POST.
ifttt_default_key_prefix = 'value'
# The default IFTTT Key to use when mapping the title text to the IFTTT
# event. The idea here is if someone wants to over-ride the default and
# change it to another Ingredient Name (in 2018, you were limited to have
# value1, value2, and value3).
ifttt_default_title_key = 'value1'
# The default IFTTT Key to use when mapping the body text to the IFTTT
# event. The idea here is if someone wants to over-ride the default and
# change it to another Ingredient Name (in 2018, you were limited to have
# value1, value2, and value3).
ifttt_default_body_key = 'value2'
# The default IFTTT Key to use when mapping the body text to the IFTTT
# event. The idea here is if someone wants to over-ride the default and
# change it to another Ingredient Name (in 2018, you were limited to have
# value1, value2, and value3).
ifttt_default_type_key = 'value3'
# The default protocol
protocol = 'ifttt'
# IFTTT uses the http protocol with JSON requests
notify_url = 'https://maker.ifttt.com/trigger/{event}/with/key/{apikey}'
def __init__(self, apikey, event, event_args=None, **kwargs):
"""
Initialize IFTTT Object
"""
super(NotifyIFTTT, self).__init__(
title_maxlen=250, body_maxlen=32768, **kwargs)
if not apikey:
raise TypeError('You must specify the Webhooks apikey.')
if not event:
raise TypeError('You must specify the Event you wish to trigger.')
# Store our APIKey
self.apikey = apikey
# Store our Event we wish to trigger
self.event = event
if isinstance(event_args, dict):
# Make a copy of the arguments so that they can't change
# outside of this plugin
self.event_args = event_args.copy()
else:
# Force a dictionary
self.event_args = dict()
def notify(self, title, body, notify_type, **kwargs):
"""
Perform IFTTT Notification
"""
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
}
# prepare JSON Object
payload = {
self.ifttt_default_title_key: title,
self.ifttt_default_body_key: body,
self.ifttt_default_type_key: notify_type,
}
# Update our payload using any other event_args specified
payload.update(self.event_args)
# Eliminate empty fields; users wishing to cancel the use of the
# self.ifttt_default_ entries can preset these keys to being
# empty so that they get caught here and removed.
payload = {x: y for x, y in payload.items() if y}
# URL to transmit content via
url = self.notify_url.format(
apikey=self.apikey,
event=self.event,
)
self.logger.debug('IFTTT POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('IFTTT Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
)
self.logger.debug(
u"IFTTT HTTP response status: %r" % r.status_code)
self.logger.debug(
u"IFTTT HTTP response headers: %r" % r.headers)
self.logger.debug(
u"IFTTT HTTP response body: %r" % r.content)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send IFTTT:%s '
'notification: %s (error=%s).' % (
self.event,
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send IFTTT:%s '
'notification (error=%s).' % (
self.event,
r.status_code))
# self.logger.debug('Response Details: %s' % r.content)
return False
else:
self.logger.info(
'Sent IFTTT notification to Event %s.' % self.event)
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending IFTTT:%s ' % (
self.event) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
return False
return True
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to substantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results
# Our Event
results['event'] = results['host']
# Our API Key
results['apikey'] = results['user']
# Store ValueX entries based on each entry past the host
results['event_args'] = {
'{0}{1}'.format(NotifyIFTTT.ifttt_default_key_prefix, n + 1):
NotifyBase.unquote(x)
for n, x in enumerate(
NotifyBase.split_path(results['fullpath'])) if x}
# Allow users to set key=val parameters to specify more types
# of payload options
results['event_args'].update(
{k: NotifyBase.unquote(v)
for k, v in results['qsd'].items()})
return results

View File

@ -26,6 +26,7 @@ from .NotifyEmail import NotifyEmail
from .NotifyEmby import NotifyEmby
from .NotifyFaast import NotifyFaast
from .NotifyGrowl.NotifyGrowl import NotifyGrowl
from .NotifyIFTTT import NotifyIFTTT
from .NotifyJoin import NotifyJoin
from .NotifyJSON import NotifyJSON
from .NotifyMatterMost import NotifyMatterMost
@ -56,7 +57,7 @@ from ..common import NOTIFY_TYPES
__all__ = [
# Notification Services
'NotifyBoxcar', 'NotifyEmail', 'NotifyEmby', 'NotifyDiscord',
'NotifyFaast', 'NotifyGrowl', 'NotifyJoin', 'NotifyJSON',
'NotifyFaast', 'NotifyGrowl', 'NotifyIFTTT', 'NotifyJoin', 'NotifyJSON',
'NotifyMatterMost', 'NotifyMyAndroid', 'NotifyProwl', 'NotifyPushalot',
'NotifyPushBullet', 'NotifyPushjet', 'NotifyPushover', 'NotifyRocketChat',
'NotifySlack', 'NotifyStride', 'NotifyToasty', 'NotifyTwitter',

View File

@ -291,6 +291,72 @@ TEST_URLS = (
'test_requests_exceptions': True,
}),
##################################
# NotifyIFTTT - If This Than That
##################################
('ifttt://', {
'instance': None,
}),
# No User
('ifttt://EventID/', {
'instance': TypeError,
}),
# Value1 gets assigned Entry1
# Title = <assigned title>
# Body = <assigned body>
('ifttt://WebHookID@EventID/Entry1/', {
'instance': plugins.NotifyIFTTT,
}),
# Value1, Value2, and Value2, the below assigns:
# Value1 = Entry1
# Value2 = AnotherEntry
# Value3 = ThirdValue
# Title = <assigned title>
# Body = <assigned body>
('ifttt://WebHookID@EventID/Entry1/AnotherEntry/ThirdValue', {
'instance': plugins.NotifyIFTTT,
}),
# Mix and match content, the below assigns:
# Value1 = FirstValue
# AnotherKey = Hello
# Value5 = test
# Title = <assigned title>
# Body = <assigned body>
('ifttt://WebHookID@EventID/FirstValue/?AnotherKey=Hello&Value5=test', {
'instance': plugins.NotifyIFTTT,
}),
# This would assign:
# Value1 = FirstValue
# Title = <blank> - disable the one passed by the notify call
# Body = <blank> - disable the one passed by the notify call
# The idea here is maybe you just want to use the apprise IFTTTT hook
# to trigger something and not nessisarily pass text along to it
('ifttt://WebHookID@EventID/FirstValue/?Title=&Body=', {
'instance': plugins.NotifyIFTTT,
}),
('ifttt://:@/', {
'instance': None,
}),
# Test website connection failures
('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT,
# force a failure
'response': False,
'requests_response_code': requests.codes.internal_server_error,
}),
('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT,
# throw a bizzare code forcing us to fail to look it up
'response': False,
'requests_response_code': 999,
}),
('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT,
# Throws a series of connection and transfer exceptions when this flag
# is set and tests that we gracfully handle them
'test_requests_exceptions': True,
}),
##################################
# NotifyJoin
##################################
@ -1960,6 +2026,46 @@ def test_notify_emby_plugin_notify(mock_post, mock_get, mock_logout,
assert obj.notify('title', 'body', 'info') is True
@mock.patch('requests.get')
@mock.patch('requests.post')
def test_notify_ifttt_plugin(mock_post, mock_get):
"""
API: NotifyIFTTT() Extra Checks
"""
# Initialize some generic (but valid) tokens
apikey = 'webhookid'
event = 'event'
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
mock_post.return_value.status_code = requests.codes.ok
mock_get.return_value.status_code = requests.codes.ok
mock_get.return_value.content = '{}'
mock_post.return_value.content = '{}'
try:
obj = plugins.NotifyIFTTT(apikey=apikey, event=None, event_args=None)
# No token specified
assert(False)
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
obj = plugins.NotifyIFTTT(apikey=apikey, event=event, event_args=None)
assert(isinstance(obj, plugins.NotifyIFTTT))
assert(len(obj.event_args) == 0)
# Disable throttling to speed up unit tests
obj.throttle_attempt = 0
assert obj.notify(title='title', body='body',
notify_type=NotifyType.INFO) is True
def test_notify_stride_plugin():
"""
API: NotifyStride() Extra Checks