1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
4# All rights reserved.
5#
6# This code is licensed under the MIT License.
7#
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files(the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions :
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24# THE SOFTWARE.
25
26# Sources
27# - https://dreambox.de/en/
28# - https://dream.reichholf.net/wiki/Hauptseite
29# - https://dream.reichholf.net/wiki/Enigma2:WebInterface#Message
30# - https://github.com/E2OpenPlugins/e2openplugin-OpenWebif
31# - https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/\
32#       OpenWebif-API-documentation#message
33
34import six
35import requests
36from json import loads
37
38from .NotifyBase import NotifyBase
39from ..URLBase import PrivacyMode
40from ..common import NotifyType
41from ..AppriseLocale import gettext_lazy as _
42
43
44class Enigma2MessageType(object):
45    # Defines the Enigma2 notification types Apprise can map to
46    INFO = 1
47    WARNING = 2
48    ERROR = 3
49
50
51# If a mapping fails, the default of Enigma2MessageType.INFO is used
52MESSAGE_MAPPING = {
53    NotifyType.INFO: Enigma2MessageType.INFO,
54    NotifyType.SUCCESS: Enigma2MessageType.INFO,
55    NotifyType.WARNING: Enigma2MessageType.WARNING,
56    NotifyType.FAILURE: Enigma2MessageType.ERROR,
57}
58
59
60class NotifyEnigma2(NotifyBase):
61    """
62    A wrapper for Enigma2 Notifications
63    """
64
65    # The default descriptive name associated with the Notification
66    service_name = 'Enigma2'
67
68    # The services URL
69    service_url = 'https://dreambox.de/'
70
71    # The default protocol
72    protocol = 'enigma2'
73
74    # The default secure protocol
75    secure_protocol = 'enigma2s'
76
77    # A URL that takes you to the setup/help of the specific protocol
78    setup_url = 'https://github.com/caronc/apprise/wiki/Notify_enigma2'
79
80    # Enigma2 does not support a title
81    title_maxlen = 0
82
83    # The maximum allowable characters allowed in the body per message
84    body_maxlen = 1000
85
86    # Throttle a wee-bit to avoid thrashing
87    request_rate_per_sec = 0.5
88
89    # Define object templates
90    templates = (
91        '{schema}://{host}',
92        '{schema}://{host}:{port}',
93        '{schema}://{user}@{host}',
94        '{schema}://{user}@{host}:{port}',
95        '{schema}://{user}:{password}@{host}',
96        '{schema}://{user}:{password}@{host}:{port}',
97        '{schema}://{host}/{fullpath}',
98        '{schema}://{host}:{port}/{fullpath}',
99        '{schema}://{user}@{host}/{fullpath}',
100        '{schema}://{user}@{host}:{port}/{fullpath}',
101        '{schema}://{user}:{password}@{host}/{fullpath}',
102        '{schema}://{user}:{password}@{host}:{port}/{fullpath}',
103    )
104
105    # Define our template tokens
106    template_tokens = dict(NotifyBase.template_tokens, **{
107        'host': {
108            'name': _('Hostname'),
109            'type': 'string',
110            'required': True,
111        },
112        'port': {
113            'name': _('Port'),
114            'type': 'int',
115            'min': 1,
116            'max': 65535,
117        },
118        'user': {
119            'name': _('Username'),
120            'type': 'string',
121        },
122        'password': {
123            'name': _('Password'),
124            'type': 'string',
125            'private': True,
126        },
127        'fullpath': {
128            'name': _('Path'),
129            'type': 'string',
130        },
131    })
132
133    template_args = dict(NotifyBase.template_args, **{
134        'timeout': {
135            'name': _('Server Timeout'),
136            'type': 'int',
137            # The number of seconds to display the message for
138            'default': 13,
139            # -1 means infinit
140            'min': -1,
141        },
142    })
143
144    # Define any kwargs we're using
145    template_kwargs = {
146        'headers': {
147            'name': _('HTTP Header'),
148            'prefix': '+',
149        },
150    }
151
152    def __init__(self, timeout=None, headers=None, **kwargs):
153        """
154        Initialize Enigma2 Object
155
156        headers can be a dictionary of key/value pairs that you want to
157        additionally include as part of the server headers to post with
158        """
159        super(NotifyEnigma2, self).__init__(**kwargs)
160
161        try:
162            self.timeout = int(timeout)
163            if self.timeout < self.template_args['timeout']['min']:
164                # Bulletproof; can't go lower then min value
165                self.timeout = self.template_args['timeout']['min']
166
167        except (ValueError, TypeError):
168            # Use default timeout
169            self.timeout = self.template_args['timeout']['default']
170
171        self.fullpath = kwargs.get('fullpath')
172        if not isinstance(self.fullpath, six.string_types):
173            self.fullpath = '/'
174
175        self.headers = {}
176        if headers:
177            # Store our extra headers
178            self.headers.update(headers)
179
180        return
181
182    def url(self, privacy=False, *args, **kwargs):
183        """
184        Returns the URL built dynamically based on specified arguments.
185        """
186
187        # Define any URL parameters
188        params = {
189            'timeout': str(self.timeout),
190        }
191
192        # Append our headers into our parameters
193        params.update({'+{}'.format(k): v for k, v in self.headers.items()})
194
195        # Extend our parameters
196        params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
197
198        # Determine Authentication
199        auth = ''
200        if self.user and self.password:
201            auth = '{user}:{password}@'.format(
202                user=NotifyEnigma2.quote(self.user, safe=''),
203                password=self.pprint(
204                    self.password, privacy, mode=PrivacyMode.Secret, safe=''),
205            )
206        elif self.user:
207            auth = '{user}@'.format(
208                user=NotifyEnigma2.quote(self.user, safe=''),
209            )
210
211        default_port = 443 if self.secure else 80
212
213        return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
214            schema=self.secure_protocol if self.secure else self.protocol,
215            auth=auth,
216            # never encode hostname since we're expecting it to be a valid one
217            hostname=self.host,
218            port='' if self.port is None or self.port == default_port
219                 else ':{}'.format(self.port),
220            fullpath=NotifyEnigma2.quote(self.fullpath, safe='/'),
221            params=NotifyEnigma2.urlencode(params),
222        )
223
224    def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
225        """
226        Perform Enigma2 Notification
227        """
228
229        # prepare Enigma2 Object
230        headers = {
231            'User-Agent': self.app_id,
232        }
233
234        params = {
235            'text': body,
236            'type': MESSAGE_MAPPING.get(
237                notify_type, Enigma2MessageType.INFO),
238            'timeout': self.timeout,
239        }
240
241        # Apply any/all header over-rides defined
242        headers.update(self.headers)
243
244        auth = None
245        if self.user:
246            auth = (self.user, self.password)
247
248        # Set our schema
249        schema = 'https' if self.secure else 'http'
250
251        url = '%s://%s' % (schema, self.host)
252        if isinstance(self.port, int):
253            url += ':%d' % self.port
254
255        # Prepare our message URL
256        url += self.fullpath.rstrip('/') + '/api/message'
257
258        self.logger.debug('Enigma2 POST URL: %s (cert_verify=%r)' % (
259            url, self.verify_certificate,
260        ))
261        self.logger.debug('Enigma2 Parameters: %s' % str(params))
262
263        # Always call throttle before any remote server i/o is made
264        self.throttle()
265
266        try:
267            r = requests.get(
268                url,
269                params=params,
270                headers=headers,
271                auth=auth,
272                verify=self.verify_certificate,
273                timeout=self.request_timeout,
274            )
275
276            if r.status_code != requests.codes.ok:
277                # We had a problem
278                status_str = \
279                    NotifyEnigma2.http_response_code_lookup(r.status_code)
280
281                self.logger.warning(
282                    'Failed to send Enigma2 notification: '
283                    '{}{}error={}.'.format(
284                        status_str,
285                        ', ' if status_str else '',
286                        r.status_code))
287
288                self.logger.debug('Response Details:\r\n{}'.format(r.content))
289
290                # Return; we're done
291                return False
292
293            # We were able to post our message; now lets evaluate the response
294            try:
295                # Acquire our result
296                result = loads(r.content).get('result', False)
297
298            except (AttributeError, TypeError, ValueError):
299                # ValueError = r.content is Unparsable
300                # TypeError = r.content is None
301                # AttributeError = r is None
302
303                # We could not parse JSON response.
304                result = False
305
306            if not result:
307                self.logger.warning(
308                    'Failed to send Enigma2 notification: '
309                    'There was no server acknowledgement.')
310                self.logger.debug('Response Details:\r\n{}'.format(r.content))
311                # Return; we're done
312                return False
313
314            self.logger.info('Sent Enigma2 notification.')
315
316        except requests.RequestException as e:
317            self.logger.warning(
318                'A Connection error occurred sending Enigma2 '
319                'notification to %s.' % self.host)
320            self.logger.debug('Socket Exception: %s' % str(e))
321
322            # Return; we're done
323            return False
324
325        return True
326
327    @staticmethod
328    def parse_url(url):
329        """
330        Parses the URL and returns enough arguments that can allow
331        us to re-instantiate this object.
332
333        """
334        results = NotifyBase.parse_url(url)
335        if not results:
336            # We're done early as we couldn't load the results
337            return results
338
339        # Add our headers that the user can potentially over-ride if they wish
340        # to to our returned result set
341        results['headers'] = results['qsd+']
342        if results['qsd-']:
343            results['headers'].update(results['qsd-'])
344            NotifyBase.logger.deprecate(
345                "minus (-) based Enigma header tokens are being "
346                " removed; use the plus (+) symbol instead.")
347
348        # Tidy our header entries by unquoting them
349        results['headers'] = {
350            NotifyEnigma2.unquote(x): NotifyEnigma2.unquote(y)
351            for x, y in results['headers'].items()}
352
353        # Save timeout value (if specified)
354        if 'timeout' in results['qsd'] and len(results['qsd']['timeout']):
355            results['timeout'] = results['qsd']['timeout']
356
357        return results
358