mirror of
https://github.com/caronc/apprise.git
synced 2025-02-16 18:21:01 +01:00
FCM Plugin Supports more Payload Options (#489)
This commit is contained in:
parent
6fbe23832b
commit
5e2a293195
@ -52,8 +52,13 @@ from ..NotifyBase import NotifyBase
|
|||||||
from ...common import NotifyType
|
from ...common import NotifyType
|
||||||
from ...utils import validate_regex
|
from ...utils import validate_regex
|
||||||
from ...utils import parse_list
|
from ...utils import parse_list
|
||||||
|
from ...utils import parse_bool
|
||||||
|
from ...common import NotifyImageSize
|
||||||
from ...AppriseAttachment import AppriseAttachment
|
from ...AppriseAttachment import AppriseAttachment
|
||||||
from ...AppriseLocale import gettext_lazy as _
|
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
|
# Default our global support flag
|
||||||
NOTIFY_FCM_SUPPORT_ENABLED = False
|
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):
|
class NotifyFCM(NotifyBase):
|
||||||
"""
|
"""
|
||||||
A wrapper for Google's Firebase Cloud Messaging Notifications
|
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.
|
# If it is more than this, then it is not accepted.
|
||||||
max_fcm_keyfile_size = 5000
|
max_fcm_keyfile_size = 5000
|
||||||
|
|
||||||
|
# Allows the user to specify the NotifyImageSize object
|
||||||
|
image_size = NotifyImageSize.XY_256
|
||||||
|
|
||||||
# The maximum length of the body
|
# The maximum length of the body
|
||||||
body_maxlen = 1024
|
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
|
# Define object templates
|
||||||
templates = (
|
templates = (
|
||||||
# OAuth2
|
# OAuth2
|
||||||
@ -163,12 +147,6 @@ class NotifyFCM(NotifyBase):
|
|||||||
'type': 'string',
|
'type': 'string',
|
||||||
'private': True,
|
'private': True,
|
||||||
},
|
},
|
||||||
'mode': {
|
|
||||||
'name': _('Mode'),
|
|
||||||
'type': 'choice:string',
|
|
||||||
'values': FCM_MODES,
|
|
||||||
'default': FCMMode.Legacy,
|
|
||||||
},
|
|
||||||
'project': {
|
'project': {
|
||||||
'name': _('Project ID'),
|
'name': _('Project ID'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
@ -195,10 +173,47 @@ class NotifyFCM(NotifyBase):
|
|||||||
'to': {
|
'to': {
|
||||||
'alias_of': 'targets',
|
'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,
|
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
|
Initialize Firebase Cloud Messaging
|
||||||
|
|
||||||
@ -214,7 +229,7 @@ class NotifyFCM(NotifyBase):
|
|||||||
self.mode = NotifyFCM.template_tokens['mode']['default'] \
|
self.mode = NotifyFCM.template_tokens['mode']['default'] \
|
||||||
if not isinstance(mode, six.string_types) else mode.lower()
|
if not isinstance(mode, six.string_types) else mode.lower()
|
||||||
if self.mode and self.mode not in FCM_MODES:
|
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)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
@ -267,6 +282,29 @@ class NotifyFCM(NotifyBase):
|
|||||||
|
|
||||||
# Acquire Device IDs to notify
|
# Acquire Device IDs to notify
|
||||||
self.targets = parse_list(targets)
|
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
|
return
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -335,6 +373,10 @@ class NotifyFCM(NotifyBase):
|
|||||||
# Prepare our notify URL
|
# Prepare our notify URL
|
||||||
notify_url = self.notify_legacy_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
|
has_error = False
|
||||||
# Create a copy of the targets list
|
# Create a copy of the targets list
|
||||||
targets = list(self.targets)
|
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] == '#':
|
if recipient[0] == '#':
|
||||||
payload['message']['topic'] = recipient[1:]
|
payload['message']['topic'] = recipient[1:]
|
||||||
self.logger.debug(
|
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] == '#':
|
if recipient[0] == '#':
|
||||||
payload['to'] = '/topics/{}'.format(recipient)
|
payload['to'] = '/topics/{}'.format(recipient)
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
@ -385,6 +450,18 @@ class NotifyFCM(NotifyBase):
|
|||||||
"FCM recipient %s parsed as a device token",
|
"FCM recipient %s parsed as a device token",
|
||||||
recipient)
|
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(
|
self.logger.debug(
|
||||||
'FCM %s POST URL: %s (cert_verify=%r)',
|
'FCM %s POST URL: %s (cert_verify=%r)',
|
||||||
self.mode, notify_url, self.verify_certificate,
|
self.mode, notify_url, self.verify_certificate,
|
||||||
@ -443,16 +520,30 @@ class NotifyFCM(NotifyBase):
|
|||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'mode': self.mode,
|
'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:
|
if self.keyfile:
|
||||||
# Include our keyfile if specified
|
# Include our keyfile if specified
|
||||||
params['keyfile'] = NotifyFCM.quote(
|
params['keyfile'] = NotifyFCM.quote(
|
||||||
self.keyfile[0].url(privacy=privacy), safe='')
|
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
|
# Extend our parameters
|
||||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
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) \
|
reference = NotifyFCM.quote(self.project) \
|
||||||
if self.mode == FCMMode.OAuth2 \
|
if self.mode == FCMMode.OAuth2 \
|
||||||
else self.pprint(self.apikey, privacy, safe='')
|
else self.pprint(self.apikey, privacy, safe='')
|
||||||
@ -507,4 +598,30 @@ class NotifyFCM(NotifyBase):
|
|||||||
results['keyfile'] = \
|
results['keyfile'] = \
|
||||||
NotifyFCM.unquote(results['qsd']['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
|
return results
|
||||||
|
127
apprise/plugins/NotifyFCM/color.py
Normal file
127
apprise/plugins/NotifyFCM/color.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
|
||||||
|
# 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
|
42
apprise/plugins/NotifyFCM/common.py
Normal file
42
apprise/plugins/NotifyFCM/common.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
|
||||||
|
# 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,
|
||||||
|
)
|
255
apprise/plugins/NotifyFCM/priority.py
Normal file
255
apprise/plugins/NotifyFCM/priority.py
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
|
||||||
|
# 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
|
@ -22,8 +22,16 @@
|
|||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
# 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
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
#
|
||||||
|
# Great Resources:
|
||||||
|
# - Dev/Legacy API:
|
||||||
|
# https://firebase.google.com/docs/cloud-messaging/http-server-ref
|
||||||
|
# - Legacy API (v1) -> OAuth
|
||||||
|
# - https://firebase.google.com/docs/cloud-messaging/migrate-v1
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import six
|
||||||
import sys
|
import sys
|
||||||
import mock
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
@ -35,6 +43,10 @@ from helpers import AppriseURLTester
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from apprise.plugins.NotifyFCM.oauth import GoogleOAuth
|
from apprise.plugins.NotifyFCM.oauth import GoogleOAuth
|
||||||
|
from apprise.plugins.NotifyFCM.common import FCM_MODES
|
||||||
|
from apprise.plugins.NotifyFCM.priority import (
|
||||||
|
FCMPriorityManager, FCM_PRIORITIES)
|
||||||
|
from apprise.plugins.NotifyFCM.color import FCMColorManager
|
||||||
from cryptography.exceptions import UnsupportedAlgorithm
|
from cryptography.exceptions import UnsupportedAlgorithm
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -101,6 +113,30 @@ apprise_url_tests = (
|
|||||||
# Test apikey= to=
|
# Test apikey= to=
|
||||||
'instance': plugins.NotifyFCM,
|
'instance': plugins.NotifyFCM,
|
||||||
}),
|
}),
|
||||||
|
('fcm://?apikey=abc123&to=device&image=yes', {
|
||||||
|
# Test image boolean
|
||||||
|
'instance': plugins.NotifyFCM,
|
||||||
|
}),
|
||||||
|
('fcm://?apikey=abc123&to=device&color=no', {
|
||||||
|
# Disable colors
|
||||||
|
'instance': plugins.NotifyFCM,
|
||||||
|
}),
|
||||||
|
('fcm://?apikey=abc123&to=device&color=aabbcc', {
|
||||||
|
# custom colors
|
||||||
|
'instance': plugins.NotifyFCM,
|
||||||
|
}),
|
||||||
|
('fcm://?apikey=abc123&to=device'
|
||||||
|
'&image_url=http://example.com/interesting.jpg', {
|
||||||
|
# Test image_url
|
||||||
|
'instance': plugins.NotifyFCM}),
|
||||||
|
('fcm://?apikey=abc123&to=device'
|
||||||
|
'&image_url=http://example.com/interesting.jpg&image=no', {
|
||||||
|
# Test image_url but set to no
|
||||||
|
'instance': plugins.NotifyFCM}),
|
||||||
|
('fcm://?apikey=abc123&to=device&+key=value&+key2=value2', {
|
||||||
|
# Test apikey= to= and data arguments
|
||||||
|
'instance': plugins.NotifyFCM,
|
||||||
|
}),
|
||||||
('fcm://%20?to=device&keyfile=/invalid/path', {
|
('fcm://%20?to=device&keyfile=/invalid/path', {
|
||||||
# invalid Project ID
|
# invalid Project ID
|
||||||
'instance': TypeError,
|
'instance': TypeError,
|
||||||
@ -176,9 +212,136 @@ def test_plugin_fcm_urls():
|
|||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
'cryptography' not in sys.modules, reason="Requires cryptography")
|
'cryptography' not in sys.modules, reason="Requires cryptography")
|
||||||
@mock.patch('requests.post')
|
@mock.patch('requests.post')
|
||||||
def test_plugin_fcm_general(mock_post):
|
def test_plugin_fcm_general_legacy(mock_post):
|
||||||
"""
|
"""
|
||||||
NotifyFCM() General Checks
|
NotifyFCM() General Legacy/APIKey Checks
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Disable Throttling to speed testing
|
||||||
|
plugins.NotifyBase.request_rate_per_sec = 0
|
||||||
|
|
||||||
|
# Prepare a good response
|
||||||
|
response = mock.Mock()
|
||||||
|
response.status_code = requests.codes.ok
|
||||||
|
mock_post.return_value = response
|
||||||
|
|
||||||
|
# A valid Legacy URL
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://abc123/device/'
|
||||||
|
'?+key=value&+key2=value2'
|
||||||
|
'&image_url=https://example.com/interesting.png')
|
||||||
|
|
||||||
|
# Send our notification
|
||||||
|
assert obj.notify("test") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/fcm/send'
|
||||||
|
|
||||||
|
payload = mock_post.mock_calls[0][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'data' in data
|
||||||
|
assert isinstance(data, dict)
|
||||||
|
assert 'key' in data['data']
|
||||||
|
assert data['data']['key'] == 'value'
|
||||||
|
assert 'key2' in data['data']
|
||||||
|
assert data['data']['key2'] == 'value2'
|
||||||
|
|
||||||
|
assert 'notification' in data
|
||||||
|
assert isinstance(data['notification'], dict)
|
||||||
|
assert 'notification' in data['notification']
|
||||||
|
assert isinstance(data['notification']['notification'], dict)
|
||||||
|
assert 'image' in data['notification']['notification']
|
||||||
|
assert data['notification']['notification']['image'] == \
|
||||||
|
'https://example.com/interesting.png'
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test priorities
|
||||||
|
#
|
||||||
|
mock_post.reset_mock()
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://abc123/device/?priority=low')
|
||||||
|
assert mock_post.call_count == 0
|
||||||
|
|
||||||
|
# Send our notification
|
||||||
|
assert obj.notify(title="title", body="body") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/fcm/send'
|
||||||
|
|
||||||
|
payload = mock_post.mock_calls[0][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'data' not in data
|
||||||
|
assert 'notification' in data
|
||||||
|
assert isinstance(data['notification'], dict)
|
||||||
|
assert 'notification' in data['notification']
|
||||||
|
assert isinstance(data['notification']['notification'], dict)
|
||||||
|
assert 'image' not in data['notification']['notification']
|
||||||
|
assert 'priority' in data
|
||||||
|
|
||||||
|
# legacy can only switch between high/low
|
||||||
|
assert data['priority'] == "normal"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test colors
|
||||||
|
#
|
||||||
|
mock_post.reset_mock()
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://abc123/device/?color=no')
|
||||||
|
assert mock_post.call_count == 0
|
||||||
|
|
||||||
|
# Send our notification
|
||||||
|
assert obj.notify(title="title", body="body") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/fcm/send'
|
||||||
|
|
||||||
|
payload = mock_post.mock_calls[0][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'data' not in data
|
||||||
|
assert 'notification' in data
|
||||||
|
assert isinstance(data['notification'], dict)
|
||||||
|
assert 'notification' in data['notification']
|
||||||
|
assert isinstance(data['notification']['notification'], dict)
|
||||||
|
assert 'image' not in data['notification']['notification']
|
||||||
|
assert 'color' not in data['notification']['notification']
|
||||||
|
|
||||||
|
mock_post.reset_mock()
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://abc123/device/?color=AA001b')
|
||||||
|
assert mock_post.call_count == 0
|
||||||
|
|
||||||
|
# Send our notification
|
||||||
|
assert obj.notify(title="title", body="body") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/fcm/send'
|
||||||
|
|
||||||
|
payload = mock_post.mock_calls[0][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'data' not in data
|
||||||
|
assert 'notification' in data
|
||||||
|
assert isinstance(data['notification'], dict)
|
||||||
|
assert 'notification' in data['notification']
|
||||||
|
assert isinstance(data['notification']['notification'], dict)
|
||||||
|
assert 'image' not in data['notification']['notification']
|
||||||
|
assert 'color' in data['notification']['notification']
|
||||||
|
assert data['notification']['notification']['color'] == '#aa001b'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
'cryptography' not in sys.modules, reason="Requires cryptography")
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_plugin_fcm_general_oauth(mock_post):
|
||||||
|
"""
|
||||||
|
NotifyFCM() General OAuth Checks
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -222,7 +385,7 @@ def test_plugin_fcm_general(mock_post):
|
|||||||
obj = Apprise.instantiate(
|
obj = Apprise.instantiate(
|
||||||
'fcm://mock-project-id/device/#topic/?keyfile={}'.format(str(path)))
|
'fcm://mock-project-id/device/#topic/?keyfile={}'.format(str(path)))
|
||||||
|
|
||||||
# we'll fail as a result
|
# send our notification
|
||||||
assert obj.notify("test") is True
|
assert obj.notify("test") is True
|
||||||
|
|
||||||
# Test our call count
|
# Test our call count
|
||||||
@ -234,6 +397,146 @@ def test_plugin_fcm_general(mock_post):
|
|||||||
assert mock_post.call_args_list[2][0][0] == \
|
assert mock_post.call_args_list[2][0][0] == \
|
||||||
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
|
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
|
||||||
|
|
||||||
|
mock_post.reset_mock()
|
||||||
|
# Now we test using a valid Project ID and data parameters
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://mock-project-id/device/#topic/?keyfile={}'
|
||||||
|
'&+key=value&+key2=value2'
|
||||||
|
'&image_url=https://example.com/interesting.png'.format(str(path)))
|
||||||
|
assert mock_post.call_count == 0
|
||||||
|
|
||||||
|
# send our notification
|
||||||
|
assert obj.notify("test") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 3
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://accounts.google.com/o/oauth2/token'
|
||||||
|
|
||||||
|
assert mock_post.call_args_list[1][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
|
||||||
|
payload = mock_post.mock_calls[1][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'message' in data
|
||||||
|
assert isinstance(data['message'], dict)
|
||||||
|
assert 'data' in data['message']
|
||||||
|
assert isinstance(data['message']['data'], dict)
|
||||||
|
assert 'key' in data['message']['data']
|
||||||
|
assert data['message']['data']['key'] == 'value'
|
||||||
|
assert 'key2' in data['message']['data']
|
||||||
|
assert data['message']['data']['key2'] == 'value2'
|
||||||
|
|
||||||
|
assert 'notification' in data['message']
|
||||||
|
assert isinstance(data['message']['notification'], dict)
|
||||||
|
assert 'image' in data['message']['notification']
|
||||||
|
assert data['message']['notification']['image'] == \
|
||||||
|
'https://example.com/interesting.png'
|
||||||
|
|
||||||
|
assert mock_post.call_args_list[2][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
|
||||||
|
|
||||||
|
payload = mock_post.mock_calls[2][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'message' in data
|
||||||
|
assert isinstance(data['message'], dict)
|
||||||
|
assert 'data' in data['message']
|
||||||
|
assert isinstance(data['message']['data'], dict)
|
||||||
|
assert 'key' in data['message']['data']
|
||||||
|
assert data['message']['data']['key'] == 'value'
|
||||||
|
assert 'key2' in data['message']['data']
|
||||||
|
assert data['message']['data']['key2'] == 'value2'
|
||||||
|
|
||||||
|
assert 'notification' in data['message']
|
||||||
|
assert isinstance(data['message']['notification'], dict)
|
||||||
|
assert 'image' in data['message']['notification']
|
||||||
|
assert data['message']['notification']['image'] == \
|
||||||
|
'https://example.com/interesting.png'
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test priorities
|
||||||
|
#
|
||||||
|
mock_post.reset_mock()
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://mock-project-id/device/?keyfile={}'
|
||||||
|
'&priority=high'.format(str(path)))
|
||||||
|
assert mock_post.call_count == 0
|
||||||
|
|
||||||
|
# Send our notification
|
||||||
|
assert obj.notify(title="title", body="body") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 2
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://accounts.google.com/o/oauth2/token'
|
||||||
|
|
||||||
|
assert mock_post.call_args_list[1][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
|
||||||
|
payload = mock_post.mock_calls[1][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'message' in data
|
||||||
|
assert isinstance(data['message'], dict)
|
||||||
|
assert 'data' not in data['message']
|
||||||
|
assert 'notification' in data['message']
|
||||||
|
assert isinstance(data['message']['notification'], dict)
|
||||||
|
assert 'image' not in data['message']['notification']
|
||||||
|
assert data['message']['apns']['headers']['apns-priority'] == "10"
|
||||||
|
assert data['message']['webpush']['headers']['Urgency'] == "high"
|
||||||
|
assert data['message']['android']['priority'] == "HIGH"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test colors
|
||||||
|
#
|
||||||
|
mock_post.reset_mock()
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://mock-project-id/device/?keyfile={}'
|
||||||
|
'&color=no'.format(str(path)))
|
||||||
|
assert mock_post.call_count == 0
|
||||||
|
|
||||||
|
# Send our notification
|
||||||
|
assert obj.notify(title="title", body="body") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 2
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://accounts.google.com/o/oauth2/token'
|
||||||
|
|
||||||
|
assert mock_post.call_args_list[1][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
|
||||||
|
payload = mock_post.mock_calls[1][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'message' in data
|
||||||
|
assert isinstance(data['message'], dict)
|
||||||
|
assert 'data' not in data['message']
|
||||||
|
assert 'notification' in data['message']
|
||||||
|
assert isinstance(data['message']['notification'], dict)
|
||||||
|
assert 'color' not in data['message']['notification']
|
||||||
|
|
||||||
|
mock_post.reset_mock()
|
||||||
|
obj = Apprise.instantiate(
|
||||||
|
'fcm://mock-project-id/device/?keyfile={}'
|
||||||
|
'&color=#12AAbb'.format(str(path)))
|
||||||
|
assert mock_post.call_count == 0
|
||||||
|
|
||||||
|
# Send our notification
|
||||||
|
assert obj.notify(title="title", body="body") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 2
|
||||||
|
assert mock_post.call_args_list[0][0][0] == \
|
||||||
|
'https://accounts.google.com/o/oauth2/token'
|
||||||
|
|
||||||
|
assert mock_post.call_args_list[1][0][0] == \
|
||||||
|
'https://fcm.googleapis.com/v1/projects/mock-project-id/messages:send'
|
||||||
|
payload = mock_post.mock_calls[1][2]
|
||||||
|
data = json.loads(payload['data'])
|
||||||
|
assert 'message' in data
|
||||||
|
assert isinstance(data['message'], dict)
|
||||||
|
assert 'data' not in data['message']
|
||||||
|
assert 'notification' in data['message']
|
||||||
|
assert isinstance(data['message']['notification'], dict)
|
||||||
|
assert 'color' in data['message']['android']['notification']
|
||||||
|
assert data['message']['android']['notification']['color'] == '#12aabb'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
'cryptography' not in sys.modules, reason="Requires cryptography")
|
'cryptography' not in sys.modules, reason="Requires cryptography")
|
||||||
@ -443,6 +746,96 @@ def test_plugin_fcm_keyfile_missing_entries_parse(tmpdir):
|
|||||||
assert oauth.load(str(path)) is False
|
assert oauth.load(str(path)) is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
'cryptography' not in sys.modules, reason="Requires cryptography")
|
||||||
|
def test_plugin_fcm_priorities():
|
||||||
|
"""
|
||||||
|
NotifyFCM() FCMPriorityManager() Testing
|
||||||
|
"""
|
||||||
|
|
||||||
|
for mode in FCM_MODES:
|
||||||
|
for priority in FCM_PRIORITIES:
|
||||||
|
instance = FCMPriorityManager(mode, priority)
|
||||||
|
assert isinstance(instance.payload(), dict)
|
||||||
|
# Verify it's not empty
|
||||||
|
assert bool(instance)
|
||||||
|
assert instance.payload()
|
||||||
|
assert str(instance) == priority
|
||||||
|
|
||||||
|
# We do not have to set a priority
|
||||||
|
instance = FCMPriorityManager(mode)
|
||||||
|
assert isinstance(instance.payload(), dict)
|
||||||
|
|
||||||
|
# Dictionary is empty though
|
||||||
|
assert not bool(instance)
|
||||||
|
assert not instance.payload()
|
||||||
|
assert str(instance) == ''
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
instance = FCMPriorityManager(mode, 'invalid')
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
instance = FCMPriorityManager('invald', 'high')
|
||||||
|
|
||||||
|
# mode validation is done at the higher NotifyFCM() level so
|
||||||
|
# it is not tested here (not required)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
'cryptography' not in sys.modules, reason="Requires cryptography")
|
||||||
|
def test_plugin_fcm_colors():
|
||||||
|
"""
|
||||||
|
NotifyFCM() FCMColorManager() Testing
|
||||||
|
"""
|
||||||
|
|
||||||
|
# No colors
|
||||||
|
instance = FCMColorManager('no')
|
||||||
|
assert bool(instance) is False
|
||||||
|
assert instance.get() is None
|
||||||
|
# We'll return that we are not defined
|
||||||
|
assert str(instance) == 'no'
|
||||||
|
|
||||||
|
# Asset colors
|
||||||
|
instance = FCMColorManager('yes')
|
||||||
|
assert isinstance(instance.get(), six.string_types)
|
||||||
|
# Output: #rrggbb
|
||||||
|
assert len(instance.get()) == 7
|
||||||
|
# Starts with has symbol
|
||||||
|
assert instance.get()[0] == '#'
|
||||||
|
# We'll return that we are defined but using default configuration
|
||||||
|
assert str(instance) == 'yes'
|
||||||
|
|
||||||
|
# We will be `true` because we can acquire a color based on what was
|
||||||
|
# passed in
|
||||||
|
assert bool(instance) is True
|
||||||
|
|
||||||
|
# Custom color
|
||||||
|
instance = FCMColorManager('#A2B3A4')
|
||||||
|
assert isinstance(instance.get(), six.string_types)
|
||||||
|
assert instance.get() == '#a2b3a4'
|
||||||
|
assert bool(instance) is True
|
||||||
|
# str() response does not include hashtag
|
||||||
|
assert str(instance) == 'a2b3a4'
|
||||||
|
|
||||||
|
# Custom color (no hashtag)
|
||||||
|
instance = FCMColorManager('A2B3A4')
|
||||||
|
assert isinstance(instance.get(), six.string_types)
|
||||||
|
# Hashtag is always part of output
|
||||||
|
assert instance.get() == '#a2b3a4'
|
||||||
|
assert bool(instance) is True
|
||||||
|
# str() response does not include hashtag
|
||||||
|
assert str(instance) == 'a2b3a4'
|
||||||
|
|
||||||
|
# Custom color (no hashtag) but only using 3 letter rgb values
|
||||||
|
instance = FCMColorManager('AC4')
|
||||||
|
assert isinstance(instance.get(), six.string_types)
|
||||||
|
# Hashtag is always part of output
|
||||||
|
assert instance.get() == '#aacc44'
|
||||||
|
assert bool(instance) is True
|
||||||
|
# str() response does not include hashtag
|
||||||
|
assert str(instance) == 'aacc44'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
'cryptography' in sys.modules,
|
'cryptography' in sys.modules,
|
||||||
reason="Requires that cryptography NOT be installed")
|
reason="Requires that cryptography NOT be installed")
|
||||||
@ -460,3 +853,26 @@ def test_plugin_fcm_cryptography_import_error():
|
|||||||
|
|
||||||
# It's not possible because our cryptography depedancy is missing
|
# It's not possible because our cryptography depedancy is missing
|
||||||
assert obj is None
|
assert obj is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
'cryptography' not in sys.modules, reason="Requires cryptography")
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_plugin_fcm_edge_cases(mock_post):
|
||||||
|
"""
|
||||||
|
NotifyFCM() Edge Cases
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Disable Throttling to speed testing
|
||||||
|
plugins.NotifyBase.request_rate_per_sec = 0
|
||||||
|
|
||||||
|
# Prepare a good response
|
||||||
|
response = mock.Mock()
|
||||||
|
response.status_code = requests.codes.ok
|
||||||
|
mock_post.return_value = response
|
||||||
|
|
||||||
|
# this tests an edge case where verify if the data_kwargs is a dictionary
|
||||||
|
# or not. Below, we don't even define it, so it will be None (causing
|
||||||
|
# the check to go). We'll still correctly instantiate a plugin:
|
||||||
|
obj = plugins.NotifyFCM("project", "api:123", targets='device')
|
||||||
|
assert obj is not None
|
||||||
|
Loading…
Reference in New Issue
Block a user