1# -*- coding: utf-8 -*- 2# 3# IFTTT (If-This-Then-That) 4# 5# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> 6# All rights reserved. 7# 8# This code is licensed under the MIT License. 9# 10# Permission is hereby granted, free of charge, to any person obtaining a copy 11# of this software and associated documentation files(the "Software"), to deal 12# in the Software without restriction, including without limitation the rights 13# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 14# copies of the Software, and to permit persons to whom the Software is 15# furnished to do so, subject to the following conditions : 16# 17# The above copyright notice and this permission notice shall be included in 18# all copies or substantial portions of the Software. 19# 20# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 23# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26# THE SOFTWARE. 27# 28# For this plugin to work, you need to add the Maker applet to your profile 29# Simply visit https://ifttt.com/search and search for 'Webhooks' 30# Or if you're signed in, click here: https://ifttt.com/maker_webhooks 31# and click 'Connect' 32# 33# You'll want to visit the settings of this Applet and pay attention to the 34# URL. For example, it might look like this: 35# https://maker.ifttt.com/use/a3nHB7gA9TfBQSqJAHklod 36# 37# In the above example a3nHB7gA9TfBQSqJAHklod becomes your {webhook_id} 38# You will need this to make this notification work correctly 39# 40# For each event you create you will assign it a name (this will be known as 41# the {event} when building your URL. 42import re 43import requests 44from json import dumps 45 46from .NotifyBase import NotifyBase 47from ..common import NotifyType 48from ..utils import parse_list 49from ..utils import validate_regex 50from ..AppriseLocale import gettext_lazy as _ 51 52 53class NotifyIFTTT(NotifyBase): 54 """ 55 A wrapper for IFTTT Notifications 56 57 """ 58 59 # The default descriptive name associated with the Notification 60 service_name = 'IFTTT' 61 62 # The services URL 63 service_url = 'https://ifttt.com/' 64 65 # The default protocol 66 secure_protocol = 'ifttt' 67 68 # A URL that takes you to the setup/help of the specific protocol 69 setup_url = 'https://github.com/caronc/apprise/wiki/Notify_ifttt' 70 71 # Even though you'll add 'Ingredients' as {{ Value1 }} to your Applets, 72 # you must use their lowercase value in the HTTP POST. 73 ifttt_default_key_prefix = 'value' 74 75 # The default IFTTT Key to use when mapping the title text to the IFTTT 76 # event. The idea here is if someone wants to over-ride the default and 77 # change it to another Ingredient Name (in 2018, you were limited to have 78 # value1, value2, and value3). 79 ifttt_default_title_key = 'value1' 80 81 # The default IFTTT Key to use when mapping the body text to the IFTTT 82 # event. The idea here is if someone wants to over-ride the default and 83 # change it to another Ingredient Name (in 2018, you were limited to have 84 # value1, value2, and value3). 85 ifttt_default_body_key = 'value2' 86 87 # The default IFTTT Key to use when mapping the body text to the IFTTT 88 # event. The idea here is if someone wants to over-ride the default and 89 # change it to another Ingredient Name (in 2018, you were limited to have 90 # value1, value2, and value3). 91 ifttt_default_type_key = 'value3' 92 93 # IFTTT uses the http protocol with JSON requests 94 notify_url = 'https://maker.ifttt.com/' \ 95 'trigger/{event}/with/key/{webhook_id}' 96 97 # Define object templates 98 templates = ( 99 '{schema}://{webhook_id}/{events}', 100 ) 101 102 # Define our template tokens 103 template_tokens = dict(NotifyBase.template_tokens, **{ 104 'webhook_id': { 105 'name': _('Webhook ID'), 106 'type': 'string', 107 'private': True, 108 'required': True, 109 }, 110 'events': { 111 'name': _('Events'), 112 'type': 'list:string', 113 'required': True, 114 }, 115 }) 116 117 # Define our template arguments 118 template_args = dict(NotifyBase.template_args, **{ 119 'to': { 120 'alias_of': 'events', 121 }, 122 }) 123 124 # Define our token control 125 template_kwargs = { 126 'add_tokens': { 127 'name': _('Add Tokens'), 128 'prefix': '+', 129 }, 130 'del_tokens': { 131 'name': _('Remove Tokens'), 132 'prefix': '-', 133 }, 134 } 135 136 def __init__(self, webhook_id, events, add_tokens=None, del_tokens=None, 137 **kwargs): 138 """ 139 Initialize IFTTT Object 140 141 add_tokens can optionally be a dictionary of key/value pairs 142 that you want to include in the IFTTT post to the server. 143 144 del_tokens can optionally be a list/tuple/set of tokens 145 that you want to eliminate from the IFTTT post. There isn't 146 much real functionality to this one unless you want to remove 147 reference to Value1, Value2, and/or Value3 148 149 """ 150 super(NotifyIFTTT, self).__init__(**kwargs) 151 152 # Webhook ID (associated with project) 153 self.webhook_id = validate_regex(webhook_id) 154 if not self.webhook_id: 155 msg = 'An invalid IFTTT Webhook ID ' \ 156 '({}) was specified.'.format(webhook_id) 157 self.logger.warning(msg) 158 raise TypeError(msg) 159 160 # Store our Events we wish to trigger 161 self.events = parse_list(events) 162 if not self.events: 163 msg = 'You must specify at least one event you wish to trigger on.' 164 self.logger.warning(msg) 165 raise TypeError(msg) 166 167 # Tokens to include in post 168 self.add_tokens = {} 169 if add_tokens: 170 self.add_tokens.update(add_tokens) 171 172 # Tokens to remove 173 self.del_tokens = [] 174 if del_tokens is not None: 175 if isinstance(del_tokens, (list, tuple, set)): 176 self.del_tokens = del_tokens 177 178 elif isinstance(del_tokens, dict): 179 # Convert the dictionary into a list 180 self.del_tokens = set(del_tokens.keys()) 181 182 else: 183 msg = 'del_token must be a list; {} was provided'.format( 184 str(type(del_tokens))) 185 self.logger.warning(msg) 186 raise TypeError(msg) 187 188 def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): 189 """ 190 Perform IFTTT Notification 191 """ 192 193 headers = { 194 'User-Agent': self.app_id, 195 'Content-Type': 'application/json', 196 } 197 198 # prepare JSON Object 199 payload = { 200 self.ifttt_default_title_key: title, 201 self.ifttt_default_body_key: body, 202 self.ifttt_default_type_key: notify_type, 203 } 204 205 # Add any new tokens expected (this can also potentially override 206 # any entries defined above) 207 payload.update(self.add_tokens) 208 209 # Eliminate fields flagged for removal otherwise ensure all tokens are 210 # lowercase since that is what the IFTTT server expects from us. 211 payload = {x.lower(): y for x, y in payload.items() 212 if x not in self.del_tokens} 213 214 # error tracking (used for function return) 215 has_error = False 216 217 # Create a copy of our event lit 218 events = list(self.events) 219 220 while len(events): 221 222 # Retrive an entry off of our event list 223 event = events.pop(0) 224 225 # URL to transmit content via 226 url = self.notify_url.format( 227 webhook_id=self.webhook_id, 228 event=event, 229 ) 230 231 self.logger.debug('IFTTT POST URL: %s (cert_verify=%r)' % ( 232 url, self.verify_certificate, 233 )) 234 self.logger.debug('IFTTT Payload: %s' % str(payload)) 235 236 # Always call throttle before any remote server i/o is made 237 self.throttle() 238 239 try: 240 r = requests.post( 241 url, 242 data=dumps(payload), 243 headers=headers, 244 verify=self.verify_certificate, 245 timeout=self.request_timeout, 246 ) 247 self.logger.debug( 248 u"IFTTT HTTP response headers: %r" % r.headers) 249 self.logger.debug( 250 u"IFTTT HTTP response body: %r" % r.content) 251 252 if r.status_code != requests.codes.ok: 253 # We had a problem 254 status_str = \ 255 NotifyIFTTT.http_response_code_lookup(r.status_code) 256 257 self.logger.warning( 258 'Failed to send IFTTT notification to {}: ' 259 '{}{}error={}.'.format( 260 event, 261 status_str, 262 ', ' if status_str else '', 263 r.status_code)) 264 265 self.logger.debug( 266 'Response Details:\r\n{}'.format(r.content)) 267 268 # Mark our failure 269 has_error = True 270 continue 271 272 else: 273 self.logger.info( 274 'Sent IFTTT notification to %s.' % event) 275 276 except requests.RequestException as e: 277 self.logger.warning( 278 'A Connection error occurred sending IFTTT:%s ' % ( 279 event) + 'notification.' 280 ) 281 self.logger.debug('Socket Exception: %s' % str(e)) 282 283 # Mark our failure 284 has_error = True 285 continue 286 287 return not has_error 288 289 def url(self, privacy=False, *args, **kwargs): 290 """ 291 Returns the URL built dynamically based on specified arguments. 292 """ 293 294 # Our URL parameters 295 params = self.url_parameters(privacy=privacy, *args, **kwargs) 296 297 # Store any new key/value pairs added to our list 298 params.update({'+{}'.format(k): v for k, v in self.add_tokens}) 299 params.update({'-{}'.format(k): '' for k in self.del_tokens}) 300 301 return '{schema}://{webhook_id}@{events}/?{params}'.format( 302 schema=self.secure_protocol, 303 webhook_id=self.pprint(self.webhook_id, privacy, safe=''), 304 events='/'.join([NotifyIFTTT.quote(x, safe='') 305 for x in self.events]), 306 params=NotifyIFTTT.urlencode(params), 307 ) 308 309 @staticmethod 310 def parse_url(url): 311 """ 312 Parses the URL and returns enough arguments that can allow 313 us to re-instantiate this object. 314 315 """ 316 results = NotifyBase.parse_url(url, verify_host=False) 317 if not results: 318 # We're done early as we couldn't load the results 319 return results 320 321 # Our API Key is the hostname if no user is specified 322 results['webhook_id'] = \ 323 results['user'] if results['user'] else results['host'] 324 325 # Unquote our API Key 326 results['webhook_id'] = NotifyIFTTT.unquote(results['webhook_id']) 327 328 # Parse our add_token and del_token arguments (if specified) 329 results['add_token'] = results['qsd+'] 330 results['del_token'] = results['qsd-'] 331 332 # Our Event 333 results['events'] = list() 334 if results['user']: 335 # If a user was defined, then the hostname is actually a event 336 # too 337 results['events'].append(NotifyIFTTT.unquote(results['host'])) 338 339 # Now fetch the remaining tokens 340 results['events'].extend(NotifyIFTTT.split_path(results['fullpath'])) 341 342 # The 'to' makes it easier to use yaml configuration 343 if 'to' in results['qsd'] and len(results['qsd']['to']): 344 results['events'] += \ 345 NotifyIFTTT.parse_list(results['qsd']['to']) 346 347 return results 348 349 @staticmethod 350 def parse_native_url(url): 351 """ 352 Support https://maker.ifttt.com/use/WEBHOOK_ID/EVENT_ID 353 """ 354 355 result = re.match( 356 r'^https?://maker\.ifttt\.com/use/' 357 r'(?P<webhook_id>[A-Z0-9_-]+)' 358 r'((?P<events>(/[A-Z0-9_-]+)+))?' 359 r'/?(?P<params>\?.+)?$', url, re.I) 360 361 if result: 362 return NotifyIFTTT.parse_url( 363 '{schema}://{webhook_id}{events}{params}'.format( 364 schema=NotifyIFTTT.secure_protocol, 365 webhook_id=result.group('webhook_id'), 366 events='' if not result.group('events') 367 else '@{}'.format(result.group('events')), 368 params='' if not result.group('params') 369 else result.group('params'))) 370 371 return None 372