diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d9658b3..e1558c5f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - [added] The `db.reference()` method now optionally takes a `url` parameter. This can be used to access multiple Firebase Databases in the same project more easily. +- [added] The `messaging.WebpushNotification` type now supports + additional parameters. # v2.12.0 diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index e9e941c48..596449ebc 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -283,21 +283,76 @@ def __init__(self, headers=None, data=None, notification=None): self.notification = notification +class WebpushNotificationAction(object): + """An action available to the users when the notification is presented. + + Args: + action: Action string. + title: Title string. + icon: Icon URL for the action (optional). + """ + + def __init__(self, action, title, icon=None): + self.action = action + self.title = title + self.icon = icon + + class WebpushNotification(object): """Webpush-specific notification parameters. + Refer to the `Notification Reference`_ for more information. + Args: title: Title of the notification (optional). If specified, overrides the title set via ``messaging.Notification``. body: Body of the notification (optional). If specified, overrides the body set via ``messaging.Notification``. icon: Icon URL of the notification (optional). + actions: A list of ``messaging.WebpushNotificationAction`` instances (optional). + badge: URL of the image used to represent the notification when there is + not enough space to display the notification itself (optional). + data: Any arbitrary JSON data that should be associated with the notification (optional). + direction: The direction in which to display the notification (optional). Must be either + 'auto', 'ltr' or 'rtl'. + image: The URL of an image to be displayed in the notification (optional). + language: Notification language (optional). + renotify: A boolean indicating whether the user should be notified after a new + notification replaces an old one (optional). + require_interaction: A boolean indicating whether a notification should remain active + until the user clicks or dismisses it, rather than closing automatically (optional). + silent: True to indicate that the notification should be silent (optional). + tag: An identifying tag on the notification (optional). + timestamp_millis: A timestamp value in milliseconds on the notification (optional). + vibrate: A vibration pattern for the device's vibration hardware to emit when the + notification fires (optional). THe pattern is specified as an integer array. + custom_data: A dict of custom key-value pairs to be included in the notification + (optional) + + .. _Notification Reference: https://developer.mozilla.org/en-US/docs/Web/API\ + /notification/Notification """ - def __init__(self, title=None, body=None, icon=None): + def __init__(self, title=None, body=None, icon=None, actions=None, badge=None, data=None, + direction=None, image=None, language=None, renotify=None, + require_interaction=None, silent=None, tag=None, timestamp_millis=None, + vibrate=None, custom_data=None): self.title = title self.body = body self.icon = icon + self.actions = actions + self.badge = badge + self.data = data + self.direction = direction + self.image = image + self.language = language + self.renotify = renotify + self.require_interaction = require_interaction + self.silent = silent + self.tag = tag + self.timestamp_millis = timestamp_millis + self.vibrate = vibrate + self.custom_data = custom_data class APNSConfig(object): @@ -579,15 +634,68 @@ def encode_webpush_notification(cls, notification): raise ValueError('WebpushConfig.notification must be an instance of ' 'WebpushNotification class.') result = { + 'actions': cls.encode_webpush_notification_actions(notification.actions), + 'badge': _Validators.check_string( + 'WebpushNotification.badge', notification.badge), 'body': _Validators.check_string( 'WebpushNotification.body', notification.body), + 'data': notification.data, + 'dir': _Validators.check_string( + 'WebpushNotification.direction', notification.direction), 'icon': _Validators.check_string( 'WebpushNotification.icon', notification.icon), + 'image': _Validators.check_string( + 'WebpushNotification.image', notification.image), + 'lang': _Validators.check_string( + 'WebpushNotification.language', notification.language), + 'renotify': notification.renotify, + 'requireInteraction': notification.require_interaction, + 'silent': notification.silent, + 'tag': _Validators.check_string( + 'WebpushNotification.tag', notification.tag), + 'timestamp': _Validators.check_number( + 'WebpushNotification.timestamp_millis', notification.timestamp_millis), 'title': _Validators.check_string( 'WebpushNotification.title', notification.title), + 'vibrate': notification.vibrate, } + direction = result.get('dir') + if direction and direction not in ('auto', 'ltr', 'rtl'): + raise ValueError('WebpushNotification.direction must be "auto", "ltr" or "rtl".') + if notification.custom_data is not None: + if not isinstance(notification.custom_data, dict): + raise ValueError('WebpushNotification.custom_data must be a dict.') + for key, value in notification.custom_data.items(): + if key in result: + raise ValueError( + 'Multiple specifications for {0} in WebpushNotification.'.format(key)) + result[key] = value return cls.remove_null_values(result) + @classmethod + def encode_webpush_notification_actions(cls, actions): + """Encodes a list of WebpushNotificationActions into JSON.""" + if actions is None: + return None + if not isinstance(actions, list): + raise ValueError('WebpushConfig.notification.actions must be a list of ' + 'WebpushNotificationAction instances.') + results = [] + for action in actions: + if not isinstance(action, WebpushNotificationAction): + raise ValueError('WebpushConfig.notification.actions must be a list of ' + 'WebpushNotificationAction instances.') + result = { + 'action': _Validators.check_string( + 'WebpushNotificationAction.action', action.action), + 'title': _Validators.check_string( + 'WebpushNotificationAction.title', action.title), + 'icon': _Validators.check_string( + 'WebpushNotificationAction.icon', action.icon), + } + results.append(cls.remove_null_values(result)) + return results + @classmethod def encode_apns(cls, apns): """Encodes an APNSConfig instance into JSON.""" diff --git a/tests/test_messaging.py b/tests/test_messaging.py index a36f49c77..31c08c856 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -447,25 +447,174 @@ def test_invalid_icon(self, data): excinfo = self._check_notification(notification) assert str(excinfo.value) == 'WebpushNotification.icon must be a string.' + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_badge(self, data): + notification = messaging.WebpushNotification(badge=data) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotification.badge must be a string.' + + @pytest.mark.parametrize('data', NON_STRING_ARGS + ['foo']) + def test_invalid_direction(self, data): + notification = messaging.WebpushNotification(direction=data) + excinfo = self._check_notification(notification) + if isinstance(data, six.string_types): + assert str(excinfo.value) == ('WebpushNotification.direction must be "auto", ' + '"ltr" or "rtl".') + else: + assert str(excinfo.value) == 'WebpushNotification.direction must be a string.' + + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_image(self, data): + notification = messaging.WebpushNotification(image=data) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotification.image must be a string.' + + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_language(self, data): + notification = messaging.WebpushNotification(language=data) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotification.language must be a string.' + + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_tag(self, data): + notification = messaging.WebpushNotification(tag=data) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotification.tag must be a string.' + + @pytest.mark.parametrize('data', ['', 'foo', list(), tuple(), dict()]) + def test_invalid_timestamp(self, data): + notification = messaging.WebpushNotification(timestamp_millis=data) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotification.timestamp_millis must be a number.' + + @pytest.mark.parametrize('data', ['', list(), tuple(), True, False, 1, 0]) + def test_invalid_custom_data(self, data): + notification = messaging.WebpushNotification(custom_data=data) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotification.custom_data must be a dict.' + + @pytest.mark.parametrize('data', ['', dict(), tuple(), True, False, 1, 0, [1, 2]]) + def test_invalid_actions(self, data): + notification = messaging.WebpushNotification(actions=data) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == ('WebpushConfig.notification.actions must be a list of ' + 'WebpushNotificationAction instances.') + def test_webpush_notification(self): msg = messaging.Message( topic='topic', webpush=messaging.WebpushConfig( - notification=messaging.WebpushNotification(title='t', body='b', icon='i') + notification=messaging.WebpushNotification( + badge='badge', + body='body', + data={'foo': 'bar'}, + icon='icon', + image='image', + language='language', + renotify=True, + require_interaction=True, + silent=True, + tag='tag', + timestamp_millis=100, + title='title', + vibrate=[100, 200, 100], + custom_data={'k1': 'v1', 'k2': 'v2'}, + ), ) ) expected = { 'topic': 'topic', 'webpush': { 'notification': { - 'title': 't', - 'body': 'b', - 'icon': 'i', + 'badge': 'badge', + 'body': 'body', + 'data': {'foo': 'bar'}, + 'icon': 'icon', + 'image': 'image', + 'lang': 'language', + 'renotify': True, + 'requireInteraction': True, + 'silent': True, + 'tag': 'tag', + 'timestamp': 100, + 'vibrate': [100, 200, 100], + 'title': 'title', + 'k1': 'v1', + 'k2': 'v2', }, }, } check_encoding(msg, expected) + def test_multiple_field_specifications(self): + notification = messaging.WebpushNotification( + badge='badge', + custom_data={'badge': 'other badge'}, + ) + excinfo = self._check_notification(notification) + expected = 'Multiple specifications for badge in WebpushNotification.' + assert str(excinfo.value) == expected + + def test_webpush_notification_action(self): + msg = messaging.Message( + topic='topic', + webpush=messaging.WebpushConfig( + notification=messaging.WebpushNotification( + actions=[ + messaging.WebpushNotificationAction( + action='a1', + title='t1', + ), + messaging.WebpushNotificationAction( + action='a2', + title='t2', + icon='i2', + ), + ], + ), + ) + ) + expected = { + 'topic': 'topic', + 'webpush': { + 'notification': { + 'actions': [ + { + 'action': 'a1', + 'title': 't1', + }, + { + 'action': 'a2', + 'title': 't2', + 'icon': 'i2', + }, + ], + }, + }, + } + check_encoding(msg, expected) + + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_action_name(self, data): + action = messaging.WebpushNotificationAction(action=data, title='title') + notification = messaging.WebpushNotification(actions=[action]) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotificationAction.action must be a string.' + + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_action_title(self, data): + action = messaging.WebpushNotificationAction(action='action', title=data) + notification = messaging.WebpushNotification(actions=[action]) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotificationAction.title must be a string.' + + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_action_icon(self, data): + action = messaging.WebpushNotificationAction(action='action', title='title', icon=data) + notification = messaging.WebpushNotification(actions=[action]) + excinfo = self._check_notification(notification) + assert str(excinfo.value) == 'WebpushNotificationAction.icon must be a string.' + class TestAPNSConfigEncoder(object):