1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2020 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# For LaMetric to work, you need to first setup a custom application on their 27# website. it can be done as follows: 28 29# Cloud Mode: 30# - Sign Up and login to the developer webpage https://developer.lametric.com 31# 32# - Create a **Indicator App** if you haven't already done so from here: 33# https://developer.lametric.com/applications/sources 34# 35# There is a great official tutorial on how to do this here: 36# https://lametric-documentation.readthedocs.io/en/latest/\ 37# guides/first-steps/first-lametric-indicator-app.html 38# 39# - Make sure to set the **Communication Type** to **PUSH**. 40# 41# - You will be able to **Publish** your app once you've finished setting it 42# up. This will allow it to be accessible from the internet using the 43# `cloud` mode of this Apprise Plugin. The **Publish** button shows up 44# from within the settings of your Lametric App upon clicking on the 45# **Draft Vx** folder (where `x` is the version - usually a 1) 46# 47# When you've completed, the site would have provided you a **PUSH URL** that 48# looks like this: 49# https://developer.lametric.com/api/v1/dev/widget/update/\ 50# com.lametric.{app_id}/{app_ver} 51# 52# You will need to record the `{app_id}` and `{app_ver}` to use the `cloud` 53# mode. 54# 55# The same page should also provide you with an **Access Token**. It's 56# approximately 86 characters with two equal (`=`) characters at the end of it. 57# This becomes your `{app_token}`. Here is an example of what one might 58# look like: 59# K2MxWI0NzU0ZmI2NjJlZYTgViMDgDRiN8YjlmZjRmNTc4NDVhJzk0RiNjNh0EyKWW==` 60# 61# The syntax for the cloud mode is: 62# * `lametric://{app_token}@{app_id}/{app_ver}?mode=cloud` 63 64# Device Mode: 65# - Sign Up and login to the developer webpage https://developer.lametric.com 66# - Locate your Device API Key; you can find it here: 67# https://developer.lametric.com/user/devices 68# - From here you can get your your API Key for the device you plan to notify. 69# - Your devices IP Address can be found in LaMetric Time app at: 70# Settings -> Wi-Fi -> IP Address 71# 72# The syntax for the device mode is: 73# * `lametric://{apikey}@{host}` 74 75# A great source for API examples (Device Mode): 76# - https://lametric-documentation.readthedocs.io/en/latest/reference-docs\ 77# /device-notifications.html 78# 79# A great source for API examples (Cloud Mode): 80# - https://lametric-documentation.readthedocs.io/en/latest/reference-docs\ 81# /lametric-cloud-reference.html 82 83# A great source for the icon reference: 84# - https://developer.lametric.com/icons 85 86 87import re 88import six 89import requests 90from json import dumps 91from .NotifyBase import NotifyBase 92from ..common import NotifyType 93from ..utils import validate_regex 94from ..AppriseLocale import gettext_lazy as _ 95from ..utils import is_hostname 96from ..utils import is_ipaddr 97 98# A URL Parser to detect App ID 99LAMETRIC_APP_ID_DETECTOR_RE = re.compile( 100 r'(com\.lametric\.)?(?P<app_id>[0-9a-z.-]{1,64})' 101 r'(/(?P<app_ver>[1-9][0-9]*))?', re.I) 102 103# Tokens are huge 104LAMETRIC_IS_APP_TOKEN = re.compile(r'^[a-z0-9]{80,}==$', re.I) 105 106 107class LametricMode(object): 108 """ 109 Define Lametric Notification Modes 110 """ 111 # App posts upstream to the developer API on Lametric's website 112 CLOUD = "cloud" 113 114 # Device mode posts directly to the device that you identify 115 DEVICE = "device" 116 117 118LAMETRIC_MODES = ( 119 LametricMode.CLOUD, 120 LametricMode.DEVICE, 121) 122 123 124class LametricPriority(object): 125 """ 126 Priority of the message 127 """ 128 129 # info: this priority means that notification will be displayed on the 130 # same “level” as all other notifications on the device that come 131 # from apps (for example facebook app). This notification will not 132 # be shown when screensaver is active. By default message is sent 133 # with "info" priority. This level of notification should be used 134 # for notifications like news, weather, temperature, etc. 135 INFO = 'info' 136 137 # warning: notifications with this priority will interrupt ones sent with 138 # lower priority (“info”). Should be used to notify the user 139 # about something important but not critical. For example, 140 # events like “someone is coming home” should use this priority 141 # when sending notifications from smart home. 142 WARNING = 'warning' 143 144 # critical: the most important notifications. Interrupts notification 145 # with priority info or warning and is displayed even if 146 # screensaver is active. Use with care as these notifications 147 # can pop in the middle of the night. Must be used only for 148 # really important notifications like notifications from smoke 149 # detectors, water leak sensors, etc. Use it for events that 150 # require human interaction immediately. 151 CRITICAL = 'critical' 152 153 154LAMETRIC_PRIORITIES = ( 155 LametricPriority.INFO, 156 LametricPriority.WARNING, 157 LametricPriority.CRITICAL, 158) 159 160 161class LametricIconType(object): 162 """ 163 Represents the nature of notification. 164 """ 165 166 # info - "i" icon will be displayed prior to the notification. Means that 167 # notification contains information, no need to take actions on it. 168 INFO = 'info' 169 170 # alert: "!!!" icon will be displayed prior to the notification. Use it 171 # when you want the user to pay attention to that notification as 172 # it indicates that something bad happened and user must take 173 # immediate action. 174 ALERT = 'alert' 175 176 # none: no notification icon will be shown. 177 NONE = 'none' 178 179 180LAMETRIC_ICON_TYPES = ( 181 LametricIconType.INFO, 182 LametricIconType.ALERT, 183 LametricIconType.NONE, 184) 185 186 187class LametricSoundCategory(object): 188 """ 189 Define Sound Categories 190 """ 191 NOTIFICATIONS = "notifications" 192 ALARMS = "alarms" 193 194 195class LametricSound(object): 196 """ 197 There are 2 categories of sounds, to make things simple we just lump them 198 all togther in one class object. 199 200 Syntax is (Category, (AlarmID, Alias1, Alias2, ...)) 201 """ 202 203 # Alarm Category Sounds 204 ALARM01 = (LametricSoundCategory.ALARMS, ('alarm1', 'a1', 'a01')) 205 ALARM02 = (LametricSoundCategory.ALARMS, ('alarm2', 'a2', 'a02')) 206 ALARM03 = (LametricSoundCategory.ALARMS, ('alarm3', 'a3', 'a03')) 207 ALARM04 = (LametricSoundCategory.ALARMS, ('alarm4', 'a4', 'a04')) 208 ALARM05 = (LametricSoundCategory.ALARMS, ('alarm5', 'a5', 'a05')) 209 ALARM06 = (LametricSoundCategory.ALARMS, ('alarm6', 'a6', 'a06')) 210 ALARM07 = (LametricSoundCategory.ALARMS, ('alarm7', 'a7', 'a07')) 211 ALARM08 = (LametricSoundCategory.ALARMS, ('alarm8', 'a8', 'a08')) 212 ALARM09 = (LametricSoundCategory.ALARMS, ('alarm9', 'a9', 'a09')) 213 ALARM10 = (LametricSoundCategory.ALARMS, ('alarm10', 'a10')) 214 ALARM11 = (LametricSoundCategory.ALARMS, ('alarm11', 'a11')) 215 ALARM12 = (LametricSoundCategory.ALARMS, ('alarm12', 'a12')) 216 ALARM13 = (LametricSoundCategory.ALARMS, ('alarm13', 'a13')) 217 218 # Notification Category Sounds 219 BICYCLE = (LametricSoundCategory.NOTIFICATIONS, ('bicycle', 'bike')) 220 CAR = (LametricSoundCategory.NOTIFICATIONS, ('car', )) 221 CASH = (LametricSoundCategory.NOTIFICATIONS, ('cash', )) 222 CAT = (LametricSoundCategory.NOTIFICATIONS, ('cat', )) 223 DOG01 = (LametricSoundCategory.NOTIFICATIONS, ('dog', 'dog1', 'dog01')) 224 DOG02 = (LametricSoundCategory.NOTIFICATIONS, ('dog2', 'dog02')) 225 ENERGY = (LametricSoundCategory.NOTIFICATIONS, ('energy', )) 226 KNOCK = (LametricSoundCategory.NOTIFICATIONS, ('knock-knock', 'knock')) 227 EMAIL = (LametricSoundCategory.NOTIFICATIONS, ( 228 'letter_email', 'letter', 'email')) 229 LOSE01 = (LametricSoundCategory.NOTIFICATIONS, ('lose1', 'lose01', 'lose')) 230 LOSE02 = (LametricSoundCategory.NOTIFICATIONS, ('lose2', 'lose02')) 231 NEGATIVE01 = (LametricSoundCategory.NOTIFICATIONS, ( 232 'negative1', 'negative01', 'neg01', 'neg1', '-')) 233 NEGATIVE02 = (LametricSoundCategory.NOTIFICATIONS, ( 234 'negative2', 'negative02', 'neg02', 'neg2', '--')) 235 NEGATIVE03 = (LametricSoundCategory.NOTIFICATIONS, ( 236 'negative3', 'negative03', 'neg03', 'neg3', '---')) 237 NEGATIVE04 = (LametricSoundCategory.NOTIFICATIONS, ( 238 'negative4', 'negative04', 'neg04', 'neg4', '----')) 239 NEGATIVE05 = (LametricSoundCategory.NOTIFICATIONS, ( 240 'negative5', 'negative05', 'neg05', 'neg5', '-----')) 241 NOTIFICATION01 = (LametricSoundCategory.NOTIFICATIONS, ( 242 'notification', 'notification1', 'notification01', 'not01', 'not1')) 243 NOTIFICATION02 = (LametricSoundCategory.NOTIFICATIONS, ( 244 'notification2', 'notification02', 'not02', 'not2')) 245 NOTIFICATION03 = (LametricSoundCategory.NOTIFICATIONS, ( 246 'notification3', 'notification03', 'not03', 'not3')) 247 NOTIFICATION04 = (LametricSoundCategory.NOTIFICATIONS, ( 248 'notification4', 'notification04', 'not04', 'not4')) 249 OPEN_DOOR = (LametricSoundCategory.NOTIFICATIONS, ( 250 'open_door', 'open', 'door')) 251 POSITIVE01 = (LametricSoundCategory.NOTIFICATIONS, ( 252 'positive1', 'positive01', 'pos01', 'p1', '+')) 253 POSITIVE02 = (LametricSoundCategory.NOTIFICATIONS, ( 254 'positive2', 'positive02', 'pos02', 'p2', '++')) 255 POSITIVE03 = (LametricSoundCategory.NOTIFICATIONS, ( 256 'positive3', 'positive03', 'pos03', 'p3', '+++')) 257 POSITIVE04 = (LametricSoundCategory.NOTIFICATIONS, ( 258 'positive4', 'positive04', 'pos04', 'p4', '++++')) 259 POSITIVE05 = (LametricSoundCategory.NOTIFICATIONS, ( 260 'positive5', 'positive05', 'pos05', 'p5', '+++++')) 261 POSITIVE06 = (LametricSoundCategory.NOTIFICATIONS, ( 262 'positive6', 'positive06', 'pos06', 'p6', '++++++')) 263 STATISTIC = (LametricSoundCategory.NOTIFICATIONS, ('statistic', 'stat')) 264 THUNDER = (LametricSoundCategory.NOTIFICATIONS, ('thunder')) 265 WATER01 = (LametricSoundCategory.NOTIFICATIONS, ('water1', 'water01')) 266 WATER02 = (LametricSoundCategory.NOTIFICATIONS, ('water2', 'water02')) 267 WIN01 = (LametricSoundCategory.NOTIFICATIONS, ('win', 'win01', 'win1')) 268 WIN02 = (LametricSoundCategory.NOTIFICATIONS, ('win2', 'win02')) 269 WIND = (LametricSoundCategory.NOTIFICATIONS, ('wind', )) 270 WIND_SHORT = (LametricSoundCategory.NOTIFICATIONS, ('wind_short', )) 271 272 273# A listing of all the sounds; the order DOES matter, content is read from 274# top down and then right to left (over aliases). Longer similar sounding 275# elements should be placed higher in the list over others. for example 276# ALARM10 should come before ALARM01 (because ALARM01 can match on 'alarm1' 277# which is very close to 'alarm10' 278LAMETRIC_SOUNDS = ( 279 # Alarm Category Entries 280 LametricSound.ALARM13, LametricSound.ALARM12, LametricSound.ALARM11, 281 LametricSound.ALARM10, LametricSound.ALARM09, LametricSound.ALARM08, 282 LametricSound.ALARM07, LametricSound.ALARM06, LametricSound.ALARM05, 283 LametricSound.ALARM04, LametricSound.ALARM03, LametricSound.ALARM02, 284 LametricSound.ALARM01, 285 286 # Notification Category Entries 287 LametricSound.BICYCLE, LametricSound.CAR, LametricSound.CASH, 288 LametricSound.CAT, LametricSound.DOG02, LametricSound.DOG01, 289 LametricSound.ENERGY, LametricSound.KNOCK, LametricSound.EMAIL, 290 LametricSound.LOSE02, LametricSound.LOSE01, LametricSound.NEGATIVE01, 291 LametricSound.NEGATIVE02, LametricSound.NEGATIVE03, 292 LametricSound.NEGATIVE04, LametricSound.NEGATIVE05, 293 LametricSound.NOTIFICATION04, LametricSound.NOTIFICATION03, 294 LametricSound.NOTIFICATION02, LametricSound.NOTIFICATION01, 295 LametricSound.OPEN_DOOR, LametricSound.POSITIVE01, 296 LametricSound.POSITIVE02, LametricSound.POSITIVE03, 297 LametricSound.POSITIVE04, LametricSound.POSITIVE05, 298 LametricSound.POSITIVE01, LametricSound.STATISTIC, LametricSound.THUNDER, 299 LametricSound.WATER02, LametricSound.WATER01, LametricSound.WIND, 300 LametricSound.WIND_SHORT, LametricSound.WIN01, LametricSound.WIN02, 301) 302 303 304class NotifyLametric(NotifyBase): 305 """ 306 A wrapper for LaMetric Notifications 307 """ 308 309 # The default descriptive name associated with the Notification 310 service_name = 'LaMetric' 311 312 # The services URL 313 service_url = 'https://lametric.com' 314 315 # The default protocol 316 protocol = 'lametric' 317 318 # The default secure protocol 319 secure_protocol = 'lametrics' 320 321 # Allow 300 requests per minute. 322 # 60/300 = 0.2 323 request_rate_per_sec = 0.20 324 325 # A URL that takes you to the setup/help of the specific protocol 326 setup_url = 'https://github.com/caronc/apprise/wiki/Notify_lametric' 327 328 # Lametric does have titles when creating a message 329 title_maxlen = 0 330 331 # URL used for notifying Lametric App's created in the Dev Portal 332 cloud_notify_url = 'https://developer.lametric.com/api/v1' \ 333 '/dev/widget/update/com.lametric.{app_id}/{app_ver}' 334 335 # URL used for local notifications directly to the device 336 device_notify_url = '{schema}://{host}{port}/api/v2/device/notifications' 337 338 # The Device User ID 339 default_device_user = 'dev' 340 341 # Track all icon mappings back to Apprise Icon NotifyType's 342 # See: https://developer.lametric.com/icons 343 # Icon ID looks like <prefix>XXX, where <prefix> is: 344 # - "i" (for static icon) 345 # - "a" (for animation) 346 # - XXX - is the number of the icon and can be found at: 347 # https://developer.lametric.com/icons 348 lametric_icon_id_mapping = { 349 # 620/Info 350 NotifyType.INFO: 'i620', 351 # 9182/info_good 352 NotifyType.SUCCESS: 'i9182', 353 # 9183/info_caution 354 NotifyType.WARNING: 'i9183', 355 # 9184/info_error 356 NotifyType.FAILURE: 'i9184', 357 } 358 359 # Define object templates 360 templates = ( 361 # Cloud (App) Mode 362 '{schema}://{app_token}@{app_id}', 363 '{schema}://{app_token}@{app_id}/{app_ver}', 364 365 # Device Mode 366 '{schema}://{apikey}@{host}', 367 '{schema}://{apikey}@{host}:{port}', 368 '{schema}://{user}:{apikey}@{host}:{port}', 369 ) 370 371 # Define our template tokens 372 template_tokens = dict(NotifyBase.template_tokens, **{ 373 # Used for Local Device mode 374 'apikey': { 375 'name': _('Device API Key'), 376 'type': 'string', 377 'private': True, 378 }, 379 # Used for Cloud mode 380 'app_id': { 381 'name': _('App ID'), 382 'type': 'string', 383 'private': True, 384 }, 385 # Used for Cloud mode 386 'app_ver': { 387 'name': _('App Version'), 388 'type': 'string', 389 'regex': (r'^[1-9][0-9]*$', ''), 390 'default': '1', 391 }, 392 # Used for Cloud mode 393 'app_token': { 394 'name': _('App Access Token'), 395 'type': 'string', 396 'regex': (r'^[A-Z0-9]{80,}==$', 'i'), 397 }, 398 'host': { 399 'name': _('Hostname'), 400 'type': 'string', 401 'required': True, 402 }, 403 'port': { 404 'name': _('Port'), 405 'type': 'int', 406 'min': 1, 407 'max': 65535, 408 'default': 8080, 409 }, 410 'user': { 411 'name': _('Username'), 412 'type': 'string', 413 }, 414 }) 415 416 # Define our template arguments 417 template_args = dict(NotifyBase.template_args, **{ 418 'apikey': { 419 'alias_of': 'apikey', 420 }, 421 'app_id': { 422 'alias_of': 'app_id', 423 }, 424 'app_ver': { 425 'alias_of': 'app_ver', 426 }, 427 'app_token': { 428 'alias_of': 'app_token', 429 }, 430 'priority': { 431 'name': _('Priority'), 432 'type': 'choice:string', 433 'values': LAMETRIC_PRIORITIES, 434 'default': LametricPriority.INFO, 435 }, 436 'icon': { 437 'name': _('Custom Icon'), 438 'type': 'string', 439 }, 440 'icon_type': { 441 'name': _('Icon Type'), 442 'type': 'choice:string', 443 'values': LAMETRIC_ICON_TYPES, 444 'default': LametricIconType.NONE, 445 }, 446 'mode': { 447 'name': _('Mode'), 448 'type': 'choice:string', 449 'values': LAMETRIC_MODES, 450 'default': LametricMode.DEVICE, 451 }, 452 'sound': { 453 'name': _('Sound'), 454 'type': 'string', 455 }, 456 # Lifetime is in seconds 457 'cycles': { 458 'name': _('Cycles'), 459 'type': 'int', 460 'min': 0, 461 'default': 1, 462 }, 463 }) 464 465 def __init__(self, apikey=None, app_token=None, app_id=None, 466 app_ver=None, priority=None, icon=None, icon_type=None, 467 sound=None, mode=None, cycles=None, **kwargs): 468 """ 469 Initialize LaMetric Object 470 """ 471 super(NotifyLametric, self).__init__(**kwargs) 472 473 self.mode = mode.strip().lower() \ 474 if isinstance(mode, six.string_types) \ 475 else self.template_args['mode']['default'] 476 477 # Default Cloud Argument 478 self.lametric_app_id = None 479 self.lametric_app_ver = None 480 self.lametric_app_access_token = None 481 482 # Default Device/Cloud Argument 483 self.lametric_apikey = None 484 485 if self.mode not in LAMETRIC_MODES: 486 msg = 'An invalid LaMetric Mode ({}) was specified.'.format(mode) 487 self.logger.warning(msg) 488 raise TypeError(msg) 489 490 if self.mode == LametricMode.CLOUD: 491 try: 492 results = LAMETRIC_APP_ID_DETECTOR_RE.match(app_id) 493 except TypeError: 494 msg = 'An invalid LaMetric Application ID ' \ 495 '({}) was specified.'.format(app_id) 496 self.logger.warning(msg) 497 raise TypeError(msg) 498 499 # Detect our Access Token 500 self.lametric_app_access_token = validate_regex( 501 app_token, 502 *self.template_tokens['app_token']['regex']) 503 if not self.lametric_app_access_token: 504 msg = 'An invalid LaMetric Application Access Token ' \ 505 '({}) was specified.'.format(app_token) 506 self.logger.warning(msg) 507 raise TypeError(msg) 508 509 # If app_ver is specified, it over-rides all 510 if app_ver: 511 self.lametric_app_ver = validate_regex( 512 app_ver, *self.template_tokens['app_ver']['regex']) 513 if not self.lametric_app_ver: 514 msg = 'An invalid LaMetric Application Version ' \ 515 '({}) was specified.'.format(app_ver) 516 self.logger.warning(msg) 517 raise TypeError(msg) 518 519 else: 520 # If app_ver wasn't specified, we parse it from the 521 # Application ID 522 self.lametric_app_ver = results.group('app_ver') \ 523 if results.group('app_ver') else \ 524 self.template_tokens['app_ver']['default'] 525 526 # Store our Application ID 527 self.lametric_app_id = results.group('app_id') 528 529 if self.mode == LametricMode.DEVICE: 530 self.lametric_apikey = validate_regex(apikey) 531 if not self.lametric_apikey: 532 msg = 'An invalid LaMetric Device API Key ' \ 533 '({}) was specified.'.format(apikey) 534 self.logger.warning(msg) 535 raise TypeError(msg) 536 537 if priority not in LAMETRIC_PRIORITIES: 538 self.priority = self.template_args['priority']['default'] 539 540 else: 541 self.priority = priority 542 543 # assign our icon (if it was defined); we also eliminate 544 # any hashtag (#) entries that might be present 545 self.icon = re.search(r'[#\s]*(?P<value>.+?)\s*$', icon) \ 546 .group('value') if isinstance(icon, six.string_types) else None 547 548 if icon_type not in LAMETRIC_ICON_TYPES: 549 self.icon_type = self.template_args['icon_type']['default'] 550 551 else: 552 self.icon_type = icon_type 553 554 # The number of times the message should be displayed 555 self.cycles = self.template_args['cycles']['default'] \ 556 if not (isinstance(cycles, int) and 557 cycles > self.template_args['cycles']['min']) else cycles 558 559 self.sound = None 560 if isinstance(sound, six.string_types): 561 # If sound is set, get it's match 562 self.sound = self.sound_lookup(sound.strip().lower()) 563 if self.sound is None: 564 self.logger.warning( 565 'An invalid LaMetric sound ({}) was specified.'.format( 566 sound)) 567 return 568 569 @staticmethod 570 def sound_lookup(lookup): 571 """ 572 A simple match function that takes string and returns the 573 LametricSound object it was found in. 574 575 """ 576 577 for x in LAMETRIC_SOUNDS: 578 match = next((f for f in x[1] if f.startswith(lookup)), None) 579 if match: 580 # We're done 581 return x 582 583 # No match was found 584 return None 585 586 def _cloud_notification_payload(self, body, notify_type, headers): 587 """ 588 Return URL and payload for cloud directed requests 589 """ 590 591 # Update header entries 592 headers.update({ 593 'X-Access-Token': self.lametric_apikey, 594 }) 595 596 if self.sound: 597 self.logger.warning( 598 'LaMetric sound setting is unavailable in Cloud mode') 599 600 if self.priority != self.template_args['priority']['default']: 601 self.logger.warning( 602 'LaMetric priority setting is unavailable in Cloud mode') 603 604 if self.icon_type != self.template_args['icon_type']['default']: 605 self.logger.warning( 606 'LaMetric icon_type setting is unavailable in Cloud mode') 607 608 if self.cycles != self.template_args['cycles']['default']: 609 self.logger.warning( 610 'LaMetric cycle settings is unavailable in Cloud mode') 611 612 # Assign our icon if the user specified a custom one, otherwise 613 # choose from our pre-set list (based on notify_type) 614 icon = self.icon if self.icon \ 615 else self.lametric_icon_id_mapping[notify_type] 616 617 # Our Payload 618 # Cloud Notifications don't have as much functionality 619 # You can not set priority and/or sound 620 payload = { 621 "frames": [ 622 { 623 "icon": icon, 624 "text": body, 625 "index": 0, 626 } 627 ] 628 } 629 630 # Prepare our Cloud Notify URL 631 notify_url = self.cloud_notify_url.format( 632 app_id=self.lametric_app_id, app_ver=self.lametric_app_ver) 633 634 # Return request parameters 635 return (notify_url, None, payload) 636 637 def _device_notification_payload(self, body, notify_type, headers): 638 """ 639 Return URL and Payload for Device directed requests 640 """ 641 642 # Assign our icon if the user specified a custom one, otherwise 643 # choose from our pre-set list (based on notify_type) 644 icon = self.icon if self.icon \ 645 else self.lametric_icon_id_mapping[notify_type] 646 647 # Our Payload 648 payload = { 649 # Priority of the message 650 "priority": self.priority, 651 652 # Icon Type: Represents the nature of notification 653 "icon_type": self.icon_type, 654 655 # The time notification lives in queue to be displayed in 656 # milliseconds (ms). The default lifetime is 2 minutes (120000ms). 657 # If notification stayed in queue for longer than lifetime 658 # milliseconds - it will not be displayed. 659 "lifetime": 120000, 660 661 "model": { 662 # cycles - the number of times message should be displayed. If 663 # cycles is set to 0, notification will stay on the screen 664 # until user dismisses it manually. By default it is set to 1. 665 "cycles": self.cycles, 666 "frames": [ 667 { 668 "icon": icon, 669 "text": body, 670 } 671 ] 672 } 673 } 674 675 if self.sound: 676 # Sound was set, so add it to the payload 677 payload["model"]["sound"] = { 678 # The sound category 679 "category": self.sound[0], 680 681 # The first element of our tuple is always the id 682 "id": self.sound[1][0], 683 684 # repeat - defines the number of times sound must be played. 685 # If set to 0 sound will be played until notification is 686 # dismissed. By default the value is set to 1. 687 "repeat": 1, 688 } 689 690 if not self.user: 691 # Use default user if there wasn't one otherwise specified 692 self.user = self.default_device_user 693 694 # Prepare our authentication 695 auth = (self.user, self.password) 696 697 # Prepare our Direct Access Notify URL 698 notify_url = self.device_notify_url.format( 699 schema="https" if self.secure else "http", 700 host=self.host, 701 port=':{}'.format( 702 self.port if self.port 703 else self.template_tokens['port']['default'])) 704 705 # Return request parameters 706 return (notify_url, auth, payload) 707 708 def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): 709 """ 710 Perform LaMetric Notification 711 """ 712 713 # Prepare our headers: 714 headers = { 715 'User-Agent': self.app_id, 716 'Content-Type': 'application/json', 717 'Accept': 'application/json', 718 'Cache-Control': 'no-cache', 719 } 720 721 # Depending on the mode, the payload is gathered by 722 # - _device_notification_payload() 723 # - _cloud_notification_payload() 724 (notify_url, auth, payload) = getattr( 725 self, '_{}_notification_payload'.format(self.mode))( 726 body=body, notify_type=notify_type, headers=headers) 727 728 self.logger.debug('LaMetric POST URL: %s (cert_verify=%r)' % ( 729 notify_url, self.verify_certificate, 730 )) 731 self.logger.debug('LaMetric Payload: %s' % str(payload)) 732 733 # Always call throttle before any remote server i/o is made 734 self.throttle() 735 736 try: 737 r = requests.post( 738 notify_url, 739 data=dumps(payload), 740 headers=headers, 741 auth=auth, 742 verify=self.verify_certificate, 743 timeout=self.request_timeout, 744 ) 745 # An ideal response would be: 746 # { 747 # "success": { 748 # "id": "<notification id>" 749 # } 750 # } 751 752 if r.status_code not in ( 753 requests.codes.created, requests.codes.ok): 754 # We had a problem 755 status_str = \ 756 NotifyLametric.http_response_code_lookup(r.status_code) 757 758 self.logger.warning( 759 'Failed to send LaMetric notification: ' 760 '{}{}error={}.'.format( 761 status_str, 762 ', ' if status_str else '', 763 r.status_code)) 764 765 self.logger.debug('Response Details:\r\n{}'.format(r.content)) 766 767 # Return; we're done 768 return False 769 770 else: 771 self.logger.info('Sent LaMetric notification.') 772 773 except requests.RequestException as e: 774 self.logger.warning( 775 'A Connection error occurred sending LaMetric ' 776 'notification to %s.' % self.host) 777 self.logger.debug('Socket Exception: %s' % str(e)) 778 779 # Return; we're done 780 return False 781 782 return True 783 784 def url(self, privacy=False, *args, **kwargs): 785 """ 786 Returns the URL built dynamically based on specified arguments. 787 """ 788 789 # Define any URL parameters 790 params = { 791 'mode': self.mode, 792 } 793 794 # Extend our parameters 795 params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) 796 797 if self.icon: 798 # Assign our icon IF one was specified 799 params['icon'] = self.icon 800 801 if self.mode == LametricMode.CLOUD: 802 # Upstream/LaMetric App Return 803 return '{schema}://{token}@{app_id}/{app_ver}/?{params}'.format( 804 schema=self.protocol, 805 token=self.pprint( 806 self.lametric_app_access_token, privacy, safe=''), 807 app_id=self.pprint(self.lametric_app_id, privacy, safe=''), 808 app_ver=NotifyLametric.quote(self.lametric_app_ver, safe=''), 809 params=NotifyLametric.urlencode(params)) 810 811 # 812 # If we reach here then we're dealing with LametricMode.DEVICE 813 # 814 if self.priority != self.template_args['priority']['default']: 815 params['priority'] = self.priority 816 817 if self.icon_type != self.template_args['icon_type']['default']: 818 params['icon_type'] = self.icon_type 819 820 if self.cycles != self.template_args['cycles']['default']: 821 params['cycles'] = self.cycles 822 823 if self.sound: 824 # Store our sound entry 825 # The first element of our tuple is always the id 826 params['sound'] = self.sound[1][0] 827 828 auth = '' 829 if self.user and self.password: 830 auth = '{user}:{apikey}@'.format( 831 user=NotifyLametric.quote(self.user, safe=''), 832 apikey=self.pprint(self.lametric_apikey, privacy, safe=''), 833 ) 834 else: # self.apikey is set 835 auth = '{apikey}@'.format( 836 apikey=self.pprint(self.lametric_apikey, privacy, safe=''), 837 ) 838 839 # Local Return 840 return '{schema}://{auth}{hostname}{port}/?{params}'.format( 841 schema=self.secure_protocol if self.secure else self.protocol, 842 auth=auth, 843 # never encode hostname since we're expecting it to be a valid one 844 hostname=self.host, 845 port='' if self.port is None 846 or self.port == self.template_tokens['port']['default'] 847 else ':{}'.format(self.port), 848 params=NotifyLametric.urlencode(params), 849 ) 850 851 @staticmethod 852 def parse_url(url): 853 """ 854 Parses the URL and returns enough arguments that can allow 855 us to re-instantiate this object. 856 857 """ 858 859 results = NotifyBase.parse_url(url, verify_host=False) 860 if not results: 861 # We're done early as we couldn't load the results 862 return results 863 864 if results.get('user') and not results.get('password'): 865 # Handle URL like: 866 # schema://user@host 867 868 # This becomes the password 869 results['password'] = results['user'] 870 results['user'] = None 871 872 # Priority Handling 873 if 'priority' in results['qsd'] and results['qsd']['priority']: 874 results['priority'] = NotifyLametric.unquote( 875 results['qsd']['priority'].strip().lower()) 876 877 # Icon Type 878 if 'icon' in results['qsd'] and results['qsd']['icon']: 879 results['icon'] = NotifyLametric.unquote( 880 results['qsd']['icon'].strip().lower()) 881 882 # Icon Type 883 if 'icon_type' in results['qsd'] and results['qsd']['icon_type']: 884 results['icon_type'] = NotifyLametric.unquote( 885 results['qsd']['icon_type'].strip().lower()) 886 887 # Sound 888 if 'sound' in results['qsd'] and results['qsd']['sound']: 889 results['sound'] = NotifyLametric.unquote( 890 results['qsd']['sound'].strip().lower()) 891 892 # API Key (Device Mode) 893 if 'apikey' in results['qsd'] and results['qsd']['apikey']: 894 # Extract API Key from an argument 895 results['apikey'] = \ 896 NotifyLametric.unquote(results['qsd']['apikey']) 897 898 # App ID 899 if 'app' in results['qsd'] \ 900 and results['qsd']['app']: 901 902 # Extract the App ID from an argument 903 results['app_id'] = \ 904 NotifyLametric.unquote(results['qsd']['app']) 905 906 # App Version 907 if 'app_ver' in results['qsd'] \ 908 and results['qsd']['app_ver']: 909 910 # Extract the App ID from an argument 911 results['app_ver'] = \ 912 NotifyLametric.unquote(results['qsd']['app_ver']) 913 914 if 'token' in results['qsd'] and results['qsd']['token']: 915 # Extract Application Access Token from an argument 916 results['app_token'] = \ 917 NotifyLametric.unquote(results['qsd']['token']) 918 919 # Mode override 920 if 'mode' in results['qsd'] and results['qsd']['mode']: 921 results['mode'] = NotifyLametric.unquote( 922 results['qsd']['mode'].strip().lower()) 923 else: 924 # We can try to detect the mode based on the validity of the 925 # hostname. We can also scan the validity of the Application 926 # Access token 927 # 928 # This isn't a surfire way to do things though; it's best to 929 # specify the mode= flag 930 results['mode'] = LametricMode.DEVICE \ 931 if ((is_hostname(results['host']) or 932 is_ipaddr(results['host'])) and 933 934 # make sure password is not an Access Token 935 (results['password'] and not 936 LAMETRIC_IS_APP_TOKEN.match(results['password'])) and 937 938 # Scan for app_ flags 939 next((f for f in results.keys() \ 940 if f.startswith('app_')), None) is None) \ 941 else LametricMode.CLOUD 942 943 # Handle defaults if not set 944 if results['mode'] == LametricMode.DEVICE: 945 # Device Mode Defaults 946 if 'apikey' not in results: 947 results['apikey'] = \ 948 NotifyLametric.unquote(results['password']) 949 950 else: 951 # CLOUD Mode Defaults 952 if 'app_id' not in results: 953 results['app_id'] = \ 954 NotifyLametric.unquote(results['host']) 955 if 'app_token' not in results: 956 results['app_token'] = \ 957 NotifyLametric.unquote(results['password']) 958 959 # Set cycles 960 try: 961 results['cycles'] = abs(int(results['qsd'].get('cycles'))) 962 963 except (TypeError, ValueError): 964 # Not a valid integer; ignore entry 965 pass 966 967 return results 968 969 @staticmethod 970 def parse_native_url(url): 971 """ 972 Support 973 https://developer.lametric.com/api/v1/dev/\ 974 widget/update/com.lametric.{APP_ID}/1 975 976 https://developer.lametric.com/api/v1/dev/\ 977 widget/update/com.lametric.{APP_ID}/{APP_VER} 978 """ 979 980 # If users do provide the Native URL they wll also want to add 981 # ?token={APP_ACCESS_TOKEN} to the parameters at the end or the 982 # URL will fail to load in later stages. 983 result = re.match( 984 r'^http(?P<secure>s)?://(?P<host>[^/]+)' 985 r'/api/(?P<api_ver>v[1-9]*[0-9]+)' 986 r'/dev/widget/update/' 987 r'com\.lametric\.(?P<app_id>[0-9a-z.-]{1,64})' 988 r'(/(?P<app_ver>[1-9][0-9]*))?/?' 989 r'(?P<params>\?.+)?$', url, re.I) 990 991 if result: 992 return NotifyLametric.parse_url( 993 '{schema}://{app_id}{app_ver}/{params}'.format( 994 schema=NotifyLametric.secure_protocol 995 if result.group('secure') else NotifyLametric.protocol, 996 app_id=result.group('app_id'), 997 app_ver='/{}'.format(result.group('app_ver')) 998 if result.group('app_ver') else '', 999 params='' if not result.group('params') 1000 else result.group('params'))) 1001 1002 return None 1003