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