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