diff --git a/README.md b/README.md
index 8efeb3ac..71c265ee 100644
--- a/README.md
+++ b/README.md
@@ -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/
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
growl://hostname:portno
growl://password@hostname
growl://password@hostname:port**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
ifttt://webhooksID/EventToTrigger/Value1/Value2/Value3
ifttt://webhooksID/EventToTrigger/?Value3=NewEntry&Value2=AnotherValue
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device
join://apikey/device1/device2/deviceN/
join://apikey/group
join://apikey/groupA/groupB/groupN
join://apikey/DeviceA/groupA/groupN/DeviceN/
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname
kodi://user@hostname
kodi://user:password@hostname:port
| [Mattermost](https://github.com/caronc/apprise/wiki/Notify_mattermost) | mmost:// | (TCP) 8065 | mmost://hostname/authkey
mmost://hostname:80/authkey
mmost://user@hostname:80/authkey
mmost://hostname/authkey?channel=channel
mmosts://hostname/authkey
mmosts://user@hostname/authkey
diff --git a/apprise/plugins/NotifyIFTTT.py b/apprise/plugins/NotifyIFTTT.py
new file mode 100644
index 00000000..02c941dc
--- /dev/null
+++ b/apprise/plugins/NotifyIFTTT.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+#
+# IFTTT (If-This-Then-That)
+#
+# Copyright (C) 2017-2018 Chris Caron
+#
+# 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
diff --git a/apprise/plugins/__init__.py b/apprise/plugins/__init__.py
index e6f4f62f..873dcbf8 100644
--- a/apprise/plugins/__init__.py
+++ b/apprise/plugins/__init__.py
@@ -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',
diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py
index 4e1264c7..e45d2c7a 100644
--- a/test/test_rest_plugins.py
+++ b/test/test_rest_plugins.py
@@ -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 =
+ # Body =
+ ('ifttt://WebHookID@EventID/Entry1/', {
+ 'instance': plugins.NotifyIFTTT,
+ }),
+ # Value1, Value2, and Value2, the below assigns:
+ # Value1 = Entry1
+ # Value2 = AnotherEntry
+ # Value3 = ThirdValue
+ # Title =
+ # Body =
+ ('ifttt://WebHookID@EventID/Entry1/AnotherEntry/ThirdValue', {
+ 'instance': plugins.NotifyIFTTT,
+ }),
+ # Mix and match content, the below assigns:
+ # Value1 = FirstValue
+ # AnotherKey = Hello
+ # Value5 = test
+ # Title =
+ # Body =
+ ('ifttt://WebHookID@EventID/FirstValue/?AnotherKey=Hello&Value5=test', {
+ 'instance': plugins.NotifyIFTTT,
+ }),
+ # This would assign:
+ # Value1 = FirstValue
+ # Title = - disable the one passed by the notify call
+ # Body = - 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