Skip to content

Commit 8cf7291

Browse files
willawang8908hiranya911
authored andcommitted
Analytics label based on @chemidy (#310)
* add analytics_label in FcmOptions * fix errors * fix errors * add analytics_label encoders * fix line-too-long * fix lint errors
1 parent f2dd24e commit 8cf7291

File tree

3 files changed

+174
-8
lines changed

3 files changed

+174
-8
lines changed

firebase_admin/_messaging_utils.py

+104-5
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,21 @@ class Message(object):
3636
android: An instance of ``messaging.AndroidConfig`` (optional).
3737
webpush: An instance of ``messaging.WebpushConfig`` (optional).
3838
apns: An instance of ``messaging.ApnsConfig`` (optional).
39+
fcm_options: An instance of ``messaging.FcmOptions`` (optional).
3940
token: The registration token of the device to which the message should be sent (optional).
4041
topic: Name of the FCM topic to which the message should be sent (optional). Topic name
4142
may contain the ``/topics/`` prefix.
4243
condition: The FCM condition to which the message should be sent (optional).
4344
"""
4445

4546
def __init__(self, data=None, notification=None, android=None, webpush=None, apns=None,
46-
token=None, topic=None, condition=None):
47+
fcm_options=None, token=None, topic=None, condition=None):
4748
self.data = data
4849
self.notification = notification
4950
self.android = android
5051
self.webpush = webpush
5152
self.apns = apns
53+
self.fcm_options = fcm_options
5254
self.token = token
5355
self.topic = topic
5456
self.condition = condition
@@ -65,8 +67,10 @@ class MulticastMessage(object):
6567
android: An instance of ``messaging.AndroidConfig`` (optional).
6668
webpush: An instance of ``messaging.WebpushConfig`` (optional).
6769
apns: An instance of ``messaging.ApnsConfig`` (optional).
70+
fcm_options: An instance of ``messaging.FcmOptions`` (optional).
6871
"""
69-
def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None):
72+
def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None,
73+
fcm_options=None):
7074
_Validators.check_string_list('MulticastMessage.tokens', tokens)
7175
if len(tokens) > 100:
7276
raise ValueError('MulticastMessage.tokens must not contain more than 100 tokens.')
@@ -76,6 +80,7 @@ def __init__(self, tokens, data=None, notification=None, android=None, webpush=N
7680
self.android = android
7781
self.webpush = webpush
7882
self.apns = apns
83+
self.fcm_options = fcm_options
7984

8085

8186
class Notification(object):
@@ -107,16 +112,18 @@ class AndroidConfig(object):
107112
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
108113
strings. When specified, overrides any data fields set via ``Message.data``.
109114
notification: A ``messaging.AndroidNotification`` to be included in the message (optional).
115+
fcm_options: A ``messaging.AndroidFcmOptions`` to be included in the message (optional).
110116
"""
111117

112118
def __init__(self, collapse_key=None, priority=None, ttl=None, restricted_package_name=None,
113-
data=None, notification=None):
119+
data=None, notification=None, fcm_options=None):
114120
self.collapse_key = collapse_key
115121
self.priority = priority
116122
self.ttl = ttl
117123
self.restricted_package_name = restricted_package_name
118124
self.data = data
119125
self.notification = notification
126+
self.fcm_options = fcm_options
120127

121128

122129
class AndroidNotification(object):
@@ -165,6 +172,18 @@ def __init__(self, title=None, body=None, icon=None, color=None, sound=None, tag
165172
self.channel_id = channel_id
166173

167174

175+
class AndroidFcmOptions(object):
176+
"""Options for features provided by the FCM SDK for Android.
177+
178+
Args:
179+
analytics_label: contains additional options for features provided by the FCM Android SDK
180+
(optional).
181+
"""
182+
183+
def __init__(self, analytics_label=None):
184+
self.analytics_label = analytics_label
185+
186+
168187
class WebpushConfig(object):
169188
"""Webpush-specific options that can be included in a message.
170189
@@ -279,14 +298,17 @@ class APNSConfig(object):
279298
Args:
280299
headers: A dictionary of headers (optional).
281300
payload: A ``messaging.APNSPayload`` to be included in the message (optional).
301+
fcm_options: A ``messaging.APNSFcmOptions`` instance to be included in the message
302+
(optional).
282303
283304
.. _APNS Documentation: https://developer.apple.com/library/content/documentation\
284305
/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html
285306
"""
286307

287-
def __init__(self, headers=None, payload=None):
308+
def __init__(self, headers=None, payload=None, fcm_options=None):
288309
self.headers = headers
289310
self.payload = payload
311+
self.fcm_options = fcm_options
290312

291313

292314
class APNSPayload(object):
@@ -387,6 +409,29 @@ def __init__(self, title=None, subtitle=None, body=None, loc_key=None, loc_args=
387409
self.launch_image = launch_image
388410

389411

412+
class APNSFcmOptions(object):
413+
"""Options for features provided by the FCM SDK for iOS.
414+
415+
Args:
416+
analytics_label: contains additional options for features provided by the FCM iOS SDK
417+
(optional).
418+
"""
419+
420+
def __init__(self, analytics_label=None):
421+
self.analytics_label = analytics_label
422+
423+
424+
class FcmOptions(object):
425+
"""Options for features provided by SDK.
426+
427+
Args:
428+
analytics_label: contains additional options to use across all platforms (optional).
429+
"""
430+
431+
def __init__(self, analytics_label=None):
432+
self.analytics_label = analytics_label
433+
434+
390435
class _Validators(object):
391436
"""A collection of data validation utilities.
392437
@@ -442,6 +487,14 @@ def check_string_list(cls, label, value):
442487
raise ValueError('{0} must not contain non-string values.'.format(label))
443488
return value
444489

490+
@classmethod
491+
def check_analytics_label(cls, label, value):
492+
"""Checks if the given value is a valid analytics label."""
493+
value = _Validators.check_string(label, value)
494+
if value is not None and not re.match(r'^[a-zA-Z0-9-_.~%]{1,50}$', value):
495+
raise ValueError('Malformed {}.'.format(label))
496+
return value
497+
445498

446499
class MessageEncoder(json.JSONEncoder):
447500
"""A custom JSONEncoder implementation for serializing Message instances into JSON."""
@@ -468,13 +521,29 @@ def encode_android(cls, android):
468521
'restricted_package_name': _Validators.check_string(
469522
'AndroidConfig.restricted_package_name', android.restricted_package_name),
470523
'ttl': cls.encode_ttl(android.ttl),
524+
'fcm_options': cls.encode_android_fcm_options(android.fcm_options),
471525
}
472526
result = cls.remove_null_values(result)
473527
priority = result.get('priority')
474528
if priority and priority not in ('high', 'normal'):
475529
raise ValueError('AndroidConfig.priority must be "high" or "normal".')
476530
return result
477531

532+
@classmethod
533+
def encode_android_fcm_options(cls, fcm_options):
534+
"""Encodes a AndroidFcmOptions instance into a json."""
535+
if fcm_options is None:
536+
return None
537+
if not isinstance(fcm_options, AndroidFcmOptions):
538+
raise ValueError('AndroidConfig.fcm_options must be an instance of '
539+
'AndroidFcmOptions class.')
540+
result = {
541+
'analytics_label': _Validators.check_analytics_label(
542+
'AndroidFcmOptions.analytics_label', fcm_options.analytics_label),
543+
}
544+
result = cls.remove_null_values(result)
545+
return result
546+
478547
@classmethod
479548
def encode_ttl(cls, ttl):
480549
"""Encodes a AndroidConfig TTL duration into a string."""
@@ -553,7 +622,7 @@ def encode_webpush(cls, webpush):
553622
'headers': _Validators.check_string_dict(
554623
'WebpushConfig.headers', webpush.headers),
555624
'notification': cls.encode_webpush_notification(webpush.notification),
556-
'fcmOptions': cls.encode_webpush_fcm_options(webpush.fcm_options),
625+
'fcm_options': cls.encode_webpush_fcm_options(webpush.fcm_options),
557626
}
558627
return cls.remove_null_values(result)
559628

@@ -653,6 +722,7 @@ def encode_apns(cls, apns):
653722
'headers': _Validators.check_string_dict(
654723
'APNSConfig.headers', apns.headers),
655724
'payload': cls.encode_apns_payload(apns.payload),
725+
'fcm_options': cls.encode_apns_fcm_options(apns.fcm_options),
656726
}
657727
return cls.remove_null_values(result)
658728

@@ -670,6 +740,20 @@ def encode_apns_payload(cls, payload):
670740
result[key] = value
671741
return cls.remove_null_values(result)
672742

743+
@classmethod
744+
def encode_apns_fcm_options(cls, fcm_options):
745+
"""Encodes an APNSFcmOptions instance into JSON."""
746+
if fcm_options is None:
747+
return None
748+
if not isinstance(fcm_options, APNSFcmOptions):
749+
raise ValueError('APNSConfig.fcm_options must be an instance of APNSFcmOptions class.')
750+
result = {
751+
'analytics_label': _Validators.check_analytics_label(
752+
'APNSFcmOptions.analytics_label', fcm_options.analytics_label),
753+
}
754+
result = cls.remove_null_values(result)
755+
return result
756+
673757
@classmethod
674758
def encode_aps(cls, aps):
675759
"""Encodes an Aps instance into JSON."""
@@ -790,10 +874,25 @@ def default(self, obj): # pylint: disable=method-hidden
790874
'token': _Validators.check_string('Message.token', obj.token, non_empty=True),
791875
'topic': _Validators.check_string('Message.topic', obj.topic, non_empty=True),
792876
'webpush': MessageEncoder.encode_webpush(obj.webpush),
877+
'fcm_options': MessageEncoder.encode_fcm_options(obj.fcm_options),
793878
}
794879
result['topic'] = MessageEncoder.sanitize_topic_name(result.get('topic'))
795880
result = MessageEncoder.remove_null_values(result)
796881
target_count = sum([t in result for t in ['token', 'topic', 'condition']])
797882
if target_count != 1:
798883
raise ValueError('Exactly one of token, topic or condition must be specified.')
799884
return result
885+
886+
@classmethod
887+
def encode_fcm_options(cls, fcm_options):
888+
"""Encodes an FcmOptions instance into JSON."""
889+
if fcm_options is None:
890+
return None
891+
if not isinstance(fcm_options, FcmOptions):
892+
raise ValueError('Message.fcm_options must be an instance of FcmOptions class.')
893+
result = {
894+
'analytics_label': _Validators.check_analytics_label(
895+
'FcmOptions.analytics_label', fcm_options.analytics_label),
896+
}
897+
result = cls.remove_null_values(result)
898+
return result

firebase_admin/messaging.py

+7
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@
3333

3434
__all__ = [
3535
'AndroidConfig',
36+
'AndroidFcmOptions',
3637
'AndroidNotification',
3738
'APNSConfig',
39+
'APNSFcmOptions',
3840
'APNSPayload',
3941
'ApiCallError',
4042
'Aps',
4143
'ApsAlert',
4244
'BatchResponse',
4345
'CriticalSound',
4446
'ErrorInfo',
47+
'FcmOptions',
4548
'Message',
4649
'MulticastMessage',
4750
'Notification',
@@ -61,12 +64,15 @@
6164

6265

6366
AndroidConfig = _messaging_utils.AndroidConfig
67+
AndroidFcmOptions = _messaging_utils.AndroidFcmOptions
6468
AndroidNotification = _messaging_utils.AndroidNotification
6569
APNSConfig = _messaging_utils.APNSConfig
70+
APNSFcmOptions = _messaging_utils.APNSFcmOptions
6671
APNSPayload = _messaging_utils.APNSPayload
6772
Aps = _messaging_utils.Aps
6873
ApsAlert = _messaging_utils.ApsAlert
6974
CriticalSound = _messaging_utils.CriticalSound
75+
FcmOptions = _messaging_utils.FcmOptions
7076
Message = _messaging_utils.Message
7177
MulticastMessage = _messaging_utils.MulticastMessage
7278
Notification = _messaging_utils.Notification
@@ -145,6 +151,7 @@ def send_multicast(multicast_message, dry_run=False, app=None):
145151
android=multicast_message.android,
146152
webpush=multicast_message.webpush,
147153
apns=multicast_message.apns,
154+
fcm_options=multicast_message.fcm_options,
148155
token=token
149156
) for token in multicast_message.tokens]
150157
return _get_messaging_service(app).send_all(messages, dry_run)

tests/test_messaging.py

+63-3
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ def test_data_message(self):
120120
def test_prefixed_topic(self):
121121
check_encoding(messaging.Message(topic='/topics/topic'), {'topic': 'topic'})
122122

123+
def test_fcm_options(self):
124+
check_encoding(
125+
messaging.Message(
126+
topic='topic', fcm_options=messaging.FcmOptions('analytics_label_v1')),
127+
{'topic': 'topic', 'fcm_options': {'analytics_label': 'analytics_label_v1'}})
128+
check_encoding(
129+
messaging.Message(topic='topic', fcm_options=messaging.FcmOptions()),
130+
{'topic': 'topic'})
131+
123132

124133
class TestNotificationEncoder(object):
125134

@@ -157,6 +166,47 @@ def test_notification_message(self):
157166
{'topic': 'topic', 'notification': {'title': 't'}})
158167

159168

169+
class TestFcmOptionEncoder(object):
170+
171+
@pytest.mark.parametrize('label', [
172+
'!',
173+
'THIS_IS_LONGER_THAN_50_CHARACTERS_WHICH_IS_NOT_ALLOWED',
174+
'',
175+
])
176+
def test_invalid_fcm_options(self, label):
177+
with pytest.raises(ValueError) as excinfo:
178+
check_encoding(messaging.Message(
179+
topic='topic',
180+
fcm_options=messaging.FcmOptions(label)
181+
))
182+
expected = 'Malformed FcmOptions.analytics_label.'
183+
assert str(excinfo.value) == expected
184+
185+
def test_fcm_options(self):
186+
check_encoding(
187+
messaging.Message(
188+
topic='topic',
189+
fcm_options=messaging.FcmOptions(),
190+
android=messaging.AndroidConfig(fcm_options=messaging.AndroidFcmOptions()),
191+
apns=messaging.APNSConfig(fcm_options=messaging.APNSFcmOptions())
192+
),
193+
{'topic': 'topic'})
194+
check_encoding(
195+
messaging.Message(
196+
topic='topic',
197+
fcm_options=messaging.FcmOptions('message-label'),
198+
android=messaging.AndroidConfig(
199+
fcm_options=messaging.AndroidFcmOptions('android-label')),
200+
apns=messaging.APNSConfig(fcm_options=messaging.APNSFcmOptions('apns-label'))
201+
),
202+
{
203+
'topic': 'topic',
204+
'fcm_options': {'analytics_label': 'message-label'},
205+
'android': {'fcm_options': {'analytics_label': 'android-label'}},
206+
'apns': {'fcm_options': {'analytics_label': 'apns-label'}},
207+
})
208+
209+
160210
class TestAndroidConfigEncoder(object):
161211

162212
@pytest.mark.parametrize('data', NON_OBJECT_ARGS)
@@ -216,7 +266,8 @@ def test_android_config(self):
216266
restricted_package_name='package',
217267
priority='high',
218268
ttl=123,
219-
data={'k1': 'v1', 'k2': 'v2'}
269+
data={'k1': 'v1', 'k2': 'v2'},
270+
fcm_options=messaging.AndroidFcmOptions('analytics_label_v1')
220271
)
221272
)
222273
expected = {
@@ -230,6 +281,9 @@ def test_android_config(self):
230281
'k1': 'v1',
231282
'k2': 'v2',
232283
},
284+
'fcm_options': {
285+
'analytics_label': 'analytics_label_v1',
286+
},
233287
},
234288
}
235289
check_encoding(msg, expected)
@@ -484,7 +538,7 @@ def test_webpush_notification(self):
484538
expected = {
485539
'topic': 'topic',
486540
'webpush': {
487-
'fcmOptions': {
541+
'fcm_options': {
488542
'link': 'https://example',
489543
},
490544
},
@@ -714,7 +768,10 @@ def test_invalid_headers(self, data):
714768
def test_apns_config(self):
715769
msg = messaging.Message(
716770
topic='topic',
717-
apns=messaging.APNSConfig(headers={'h1': 'v1', 'h2': 'v2'})
771+
apns=messaging.APNSConfig(
772+
headers={'h1': 'v1', 'h2': 'v2'},
773+
fcm_options=messaging.APNSFcmOptions('analytics_label_v1')
774+
),
718775
)
719776
expected = {
720777
'topic': 'topic',
@@ -723,6 +780,9 @@ def test_apns_config(self):
723780
'h1': 'v1',
724781
'h2': 'v2',
725782
},
783+
'fcm_options': {
784+
'analytics_label': 'analytics_label_v1',
785+
},
726786
},
727787
}
728788
check_encoding(msg, expected)

0 commit comments

Comments
 (0)