diff options
Diffstat (limited to 'libs/apprise/plugins/NotifyFCM')
-rw-r--r-- | libs/apprise/plugins/NotifyFCM/__init__.py | 181 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyFCM/color.py | 127 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyFCM/common.py | 42 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyFCM/priority.py | 255 |
4 files changed, 573 insertions, 32 deletions
diff --git a/libs/apprise/plugins/NotifyFCM/__init__.py b/libs/apprise/plugins/NotifyFCM/__init__.py index 9269ea3b0..a6f801c8b 100644 --- a/libs/apprise/plugins/NotifyFCM/__init__.py +++ b/libs/apprise/plugins/NotifyFCM/__init__.py @@ -52,8 +52,13 @@ from ..NotifyBase import NotifyBase from ...common import NotifyType from ...utils import validate_regex from ...utils import parse_list +from ...utils import parse_bool +from ...common import NotifyImageSize from ...AppriseAttachment import AppriseAttachment from ...AppriseLocale import gettext_lazy as _ +from .common import (FCMMode, FCM_MODES) +from .priority import (FCM_PRIORITIES, FCMPriorityManager) +from .color import FCMColorManager # Default our global support flag NOTIFY_FCM_SUPPORT_ENABLED = False @@ -80,26 +85,6 @@ FCM_HTTP_ERROR_MAP = { } -class FCMMode(object): - """ - Define the Firebase Cloud Messaging Modes - """ - # The legacy way of sending a message - Legacy = "legacy" - - # The new API - OAuth2 = "oauth2" - - -# FCM Modes -FCM_MODES = ( - # Legacy API - FCMMode.Legacy, - # HTTP v1 URL - FCMMode.OAuth2, -) - - class NotifyFCM(NotifyBase): """ A wrapper for Google's Firebase Cloud Messaging Notifications @@ -136,13 +121,12 @@ class NotifyFCM(NotifyBase): # If it is more than this, then it is not accepted. max_fcm_keyfile_size = 5000 + # Allows the user to specify the NotifyImageSize object + image_size = NotifyImageSize.XY_256 + # The maximum length of the body body_maxlen = 1024 - # A title can not be used for SMS Messages. Setting this to zero will - # cause any title (if defined) to get placed into the message body. - title_maxlen = 0 - # Define object templates templates = ( # OAuth2 @@ -163,12 +147,6 @@ class NotifyFCM(NotifyBase): 'type': 'string', 'private': True, }, - 'mode': { - 'name': _('Mode'), - 'type': 'choice:string', - 'values': FCM_MODES, - 'default': FCMMode.Legacy, - }, 'project': { 'name': _('Project ID'), 'type': 'string', @@ -195,10 +173,47 @@ class NotifyFCM(NotifyBase): 'to': { 'alias_of': 'targets', }, + 'mode': { + 'name': _('Mode'), + 'type': 'choice:string', + 'values': FCM_MODES, + 'default': FCMMode.Legacy, + }, + 'priority': { + 'name': _('Mode'), + 'type': 'choice:string', + 'values': FCM_PRIORITIES, + }, + 'image_url': { + 'name': _('Custom Image URL'), + 'type': 'string', + }, + 'image': { + 'name': _('Include Image'), + 'type': 'bool', + 'default': False, + 'map_to': 'include_image', + }, + # Color can either be yes, no, or a #rrggbb ( + # rrggbb without hashtag is accepted to) + 'color': { + 'name': _('Notification Color'), + 'type': 'string', + 'default': 'yes', + }, }) + # Define our data entry + template_kwargs = { + 'data_kwargs': { + 'name': _('Data Entries'), + 'prefix': '+', + }, + } + def __init__(self, project, apikey, targets=None, mode=None, keyfile=None, - **kwargs): + data_kwargs=None, image_url=None, include_image=False, + color=None, priority=None, **kwargs): """ Initialize Firebase Cloud Messaging @@ -214,7 +229,7 @@ class NotifyFCM(NotifyBase): self.mode = NotifyFCM.template_tokens['mode']['default'] \ if not isinstance(mode, six.string_types) else mode.lower() if self.mode and self.mode not in FCM_MODES: - msg = 'The mode specified ({}) is invalid.'.format(mode) + msg = 'The FCM mode specified ({}) is invalid.'.format(mode) self.logger.warning(msg) raise TypeError(msg) @@ -267,6 +282,29 @@ class NotifyFCM(NotifyBase): # Acquire Device IDs to notify self.targets = parse_list(targets) + + # Our data Keyword/Arguments to include in our outbound payload + self.data_kwargs = {} + if isinstance(data_kwargs, dict): + self.data_kwargs.update(data_kwargs) + + # Include the image as part of the payload + self.include_image = include_image + + # A Custom Image URL + # FCM allows you to provide a remote https?:// URL to an image_url + # located on the internet that it will download and include in the + # payload. + # + # self.image_url() is reserved as an internal function name; so we + # jsut store it into a different variable for now + self.image_src = image_url + + # Initialize our priority + self.priority = FCMPriorityManager(self.mode, priority) + + # Initialize our color + self.color = FCMColorManager(color, asset=self.asset) return @property @@ -335,6 +373,10 @@ class NotifyFCM(NotifyBase): # Prepare our notify URL notify_url = self.notify_legacy_url + # Acquire image url + image = self.image_url(notify_type) \ + if not self.image_src else self.image_src + has_error = False # Create a copy of the targets list targets = list(self.targets) @@ -352,6 +394,17 @@ class NotifyFCM(NotifyBase): } } + if self.color: + # Acquire our color + payload['message']['android'] = { + 'notification': {'color': self.color.get(notify_type)}} + + if self.include_image and image: + payload['message']['notification']['image'] = image + + if self.data_kwargs: + payload['message']['data'] = self.data_kwargs + if recipient[0] == '#': payload['message']['topic'] = recipient[1:] self.logger.debug( @@ -373,6 +426,18 @@ class NotifyFCM(NotifyBase): } } } + + if self.color: + # Acquire our color + payload['notification']['notification']['color'] = \ + self.color.get(notify_type) + + if self.include_image and image: + payload['notification']['notification']['image'] = image + + if self.data_kwargs: + payload['data'] = self.data_kwargs + if recipient[0] == '#': payload['to'] = '/topics/{}'.format(recipient) self.logger.debug( @@ -385,6 +450,18 @@ class NotifyFCM(NotifyBase): "FCM recipient %s parsed as a device token", recipient) + # + # Apply our priority configuration (if set) + # + def merge(d1, d2): + for k in d2: + if k in d1 and isinstance(d1[k], dict) \ + and isinstance(d2[k], dict): + merge(d1[k], d2[k]) + else: + d1[k] = d2[k] + merge(payload, self.priority.payload()) + self.logger.debug( 'FCM %s POST URL: %s (cert_verify=%r)', self.mode, notify_url, self.verify_certificate, @@ -443,16 +520,30 @@ class NotifyFCM(NotifyBase): # Define any URL parameters params = { 'mode': self.mode, + 'image': 'yes' if self.include_image else 'no', + 'color': str(self.color), } + if self.priority: + # Store our priority if one was defined + params['priority'] = str(self.priority) + if self.keyfile: # Include our keyfile if specified params['keyfile'] = NotifyFCM.quote( self.keyfile[0].url(privacy=privacy), safe='') + if self.image_src: + # Include our image path as part of our URL payload + params['image_url'] = self.image_src + # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + # Add our data keyword/args into our URL response + params.update( + {'+{}'.format(k): v for k, v in self.data_kwargs.items()}) + reference = NotifyFCM.quote(self.project) \ if self.mode == FCMMode.OAuth2 \ else self.pprint(self.apikey, privacy, safe='') @@ -507,4 +598,30 @@ class NotifyFCM(NotifyBase): results['keyfile'] = \ NotifyFCM.unquote(results['qsd']['keyfile']) + # Our Priority + if 'priority' in results['qsd'] and results['qsd']['priority']: + results['priority'] = \ + NotifyFCM.unquote(results['qsd']['priority']) + + # Our Color + if 'color' in results['qsd'] and results['qsd']['color']: + results['color'] = \ + NotifyFCM.unquote(results['qsd']['color']) + + # Boolean to include an image or not + results['include_image'] = parse_bool(results['qsd'].get( + 'image', NotifyFCM.template_args['image']['default'])) + + # Extract image_url if it was specified + if 'image_url' in results['qsd']: + results['image_url'] = \ + NotifyFCM.unquote(results['qsd']['image_url']) + if 'image' not in results['qsd']: + # Toggle default behaviour if a custom image was provided + # but ONLY if the `image` boolean was not set + results['include_image'] = True + + # Store our data keyword/args if specified + results['data_kwargs'] = results['qsd+'] + return results diff --git a/libs/apprise/plugins/NotifyFCM/color.py b/libs/apprise/plugins/NotifyFCM/color.py new file mode 100644 index 000000000..a2fcfd662 --- /dev/null +++ b/libs/apprise/plugins/NotifyFCM/color.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Chris Caron <[email protected]> +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# New priorities are defined here: +# - https://firebase.google.com/docs/reference/fcm/rest/v1/\ +# projects.messages#NotificationPriority + +# Legacy color payload example here: +# https://firebase.google.com/docs/reference/fcm/rest/v1/\ +# projects.messages#androidnotification +import re +import six +from ...utils import parse_bool +from ...common import NotifyType +from ...AppriseAsset import AppriseAsset + + +class FCMColorManager(object): + """ + A Simple object to accept either a boolean value + - True: Use colors provided by Apprise + - False: Do not use colors at all + - rrggbb: where you provide the rgb values (hence #333333) + - rgb: is also accepted as rgb values (hence #333) + + For RGB colors, the hashtag is optional + """ + + __color_rgb = re.compile( + r'#?((?P<r1>[0-9A-F]{2})(?P<g1>[0-9A-F]{2})(?P<b1>[0-9A-F]{2})' + r'|(?P<r2>[0-9A-F])(?P<g2>[0-9A-F])(?P<b2>[0-9A-F]))', re.IGNORECASE) + + def __init__(self, color, asset=None): + """ + Parses the color object accordingly + """ + + # Initialize an asset object if one isn't otherwise defined + self.asset = asset \ + if isinstance(asset, AppriseAsset) else AppriseAsset() + + # Prepare our color + self.color = color + if isinstance(color, six.string_types): + self.color = self.__color_rgb.match(color) + if self.color: + # Store our RGB value as #rrggbb + self.color = '{red}{green}{blue}'.format( + red=self.color.group('r1'), + green=self.color.group('g1'), + blue=self.color.group('b1')).lower() \ + if self.color.group('r1') else \ + '{red1}{red2}{green1}{green2}{blue1}{blue2}'.format( + red1=self.color.group('r2'), + red2=self.color.group('r2'), + green1=self.color.group('g2'), + green2=self.color.group('g2'), + blue1=self.color.group('b2'), + blue2=self.color.group('b2')).lower() + + if self.color is None: + # Color not determined, so base it on boolean parser + self.color = parse_bool(color) + + def get(self, notify_type=NotifyType.INFO): + """ + Returns color or true/false value based on configuration + """ + + if isinstance(self.color, bool) and self.color: + # We want to use the asset value + return self.asset.color(notify_type=notify_type) + + elif self.color: + # return our color as is + return '#' + self.color + + # No color to return + return None + + def __str__(self): + """ + our color representation + """ + if isinstance(self.color, bool): + return 'yes' if self.color else 'no' + + # otherwise return our color + return self.color + + def __bool__(self): + """ + Allows this object to be wrapped in an Python 3.x based 'if + statement'. True is returned if a color was loaded + """ + return True if self.color is True or \ + isinstance(self.color, six.string_types) else False + + def __nonzero__(self): + """ + Allows this object to be wrapped in an Python 2.x based 'if + statement'. True is returned if a color was loaded + """ + return True if self.color is True or \ + isinstance(self.color, six.string_types) else False diff --git a/libs/apprise/plugins/NotifyFCM/common.py b/libs/apprise/plugins/NotifyFCM/common.py new file mode 100644 index 000000000..39765ff0b --- /dev/null +++ b/libs/apprise/plugins/NotifyFCM/common.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Chris Caron <[email protected]> +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +class FCMMode(object): + """ + Define the Firebase Cloud Messaging Modes + """ + # The legacy way of sending a message + Legacy = "legacy" + + # The new API + OAuth2 = "oauth2" + + +# FCM Modes +FCM_MODES = ( + # Legacy API + FCMMode.Legacy, + # HTTP v1 URL + FCMMode.OAuth2, +) diff --git a/libs/apprise/plugins/NotifyFCM/priority.py b/libs/apprise/plugins/NotifyFCM/priority.py new file mode 100644 index 000000000..7c8ab1ccf --- /dev/null +++ b/libs/apprise/plugins/NotifyFCM/priority.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Chris Caron <[email protected]> +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# New priorities are defined here: +# - https://firebase.google.com/docs/reference/fcm/rest/v1/\ +# projects.messages#NotificationPriority + +# Legacy priorities are defined here: +# - https://firebase.google.com/docs/cloud-messaging/http-server-ref +from .common import (FCMMode, FCM_MODES) +from ...logger import logger + + +class NotificationPriority(object): + """ + Defines the Notification Priorities as described on: + https://firebase.google.com/docs/reference/fcm/rest/v1/\ + projects.messages#androidmessagepriority + + NORMAL: + Default priority for data messages. Normal priority messages won't + open network connections on a sleeping device, and their delivery + may be delayed to conserve the battery. For less time-sensitive + messages, such as notifications of new email or other data to sync, + choose normal delivery priority. + + HIGH: + Default priority for notification messages. FCM attempts to + deliver high priority messages immediately, allowing the FCM + service to wake a sleeping device when possible and open a network + connection to your app server. Apps with instant messaging, chat, + or voice call alerts, for example, generally need to open a + network connection and make sure FCM delivers the message to the + device without delay. Set high priority if the message is + time-critical and requires the user's immediate interaction, but + beware that setting your messages to high priority contributes + more to battery drain compared with normal priority messages. + """ + + NORMAL = 'NORMAL' + HIGH = 'HIGH' + + +class FCMPriority(object): + """ + Defines our accepted priorites + """ + MIN = "min" + + LOW = "low" + + NORMAL = "normal" + + HIGH = "high" + + MAX = "max" + + +FCM_PRIORITIES = ( + FCMPriority.MIN, + FCMPriority.LOW, + FCMPriority.NORMAL, + FCMPriority.HIGH, + FCMPriority.MAX, +) + + +class FCMPriorityManager(object): + """ + A Simple object to make it easier to work with FCM set priorities + """ + + priority_map = { + FCMPriority.MIN: { + FCMMode.OAuth2: { + 'message': { + 'android': { + 'priority': NotificationPriority.NORMAL + }, + 'apns': { + 'headers': { + 'apns-priority': "5" + } + }, + 'webpush': { + 'headers': { + 'Urgency': 'very-low' + } + }, + } + }, + FCMMode.Legacy: { + 'priority': 'normal', + } + }, + FCMPriority.LOW: { + FCMMode.OAuth2: { + 'message': { + 'android': { + 'priority': NotificationPriority.NORMAL + }, + 'apns': { + 'headers': { + 'apns-priority': "5" + } + }, + 'webpush': { + 'headers': { + 'Urgency': 'low' + } + } + } + }, + FCMMode.Legacy: { + 'priority': 'normal', + } + }, + FCMPriority.NORMAL: { + FCMMode.OAuth2: { + 'message': { + 'android': { + 'priority': NotificationPriority.NORMAL + }, + 'apns': { + 'headers': { + 'apns-priority': "5" + } + }, + 'webpush': { + 'headers': { + 'Urgency': 'normal' + } + } + } + }, + FCMMode.Legacy: { + 'priority': 'normal', + } + }, + FCMPriority.HIGH: { + FCMMode.OAuth2: { + 'message': { + 'android': { + 'priority': NotificationPriority.HIGH + }, + 'apns': { + 'headers': { + 'apns-priority': "10" + } + }, + 'webpush': { + 'headers': { + 'Urgency': 'high' + } + } + } + }, + FCMMode.Legacy: { + 'priority': 'high', + } + }, + FCMPriority.MAX: { + FCMMode.OAuth2: { + 'message': { + 'android': { + 'priority': NotificationPriority.HIGH + }, + 'apns': { + 'headers': { + 'apns-priority': "10" + } + }, + 'webpush': { + 'headers': { + 'Urgency': 'high' + } + } + } + }, + FCMMode.Legacy: { + 'priority': 'high', + } + } + } + + def __init__(self, mode, priority=None): + """ + Takes a FCMMode and Priority + """ + + self.mode = mode + if self.mode not in FCM_MODES: + msg = 'The FCM mode specified ({}) is invalid.'.format(mode) + logger.warning(msg) + raise TypeError(msg) + + self.priority = None + if priority: + self.priority = \ + next((p for p in FCM_PRIORITIES + if p.startswith(priority[:2].lower())), None) + if not self.priority: + msg = 'An invalid FCM Priority ' \ + '({}) was specified.'.format(priority) + logger.warning(msg) + raise TypeError(msg) + + def payload(self): + """ + Returns our payload depending on our mode + """ + return self.priority_map[self.priority][self.mode] \ + if self.priority else {} + + def __str__(self): + """ + our priority representation + """ + return self.priority if self.priority else '' + + def __bool__(self): + """ + Allows this object to be wrapped in an Python 3.x based 'if + statement'. True is returned if a priority was loaded + """ + return True if self.priority else False + + def __nonzero__(self): + """ + Allows this object to be wrapped in an Python 2.x based 'if + statement'. True is returned if a priority was loaded + """ + return True if self.priority else False |