summaryrefslogtreecommitdiffhomepage
path: root/libs/apprise/plugins/NotifyFCM
diff options
context:
space:
mode:
Diffstat (limited to 'libs/apprise/plugins/NotifyFCM')
-rw-r--r--libs/apprise/plugins/NotifyFCM/__init__.py181
-rw-r--r--libs/apprise/plugins/NotifyFCM/color.py127
-rw-r--r--libs/apprise/plugins/NotifyFCM/common.py42
-rw-r--r--libs/apprise/plugins/NotifyFCM/priority.py255
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