Skip to content
58 changes: 45 additions & 13 deletions firebase_admin/_messaging_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import math
import numbers
import re
import warnings

from firebase_admin import _messaging_utils

Expand All @@ -27,7 +28,7 @@ class Message:
"""A message that can be sent via Firebase Cloud Messaging.

Contains payload information as well as recipient information. In particular, the message must
contain exactly one of token, topic or condition fields.
contain exactly one of fid, token, topic or condition fields.

Args:
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
Expand All @@ -37,20 +38,29 @@ class Message:
webpush: An instance of ``messaging.WebpushConfig`` (optional).
apns: An instance of ``messaging.ApnsConfig`` (optional).
fcm_options: An instance of ``messaging.FCMOptions`` (optional).
token: The registration token of the device to which the message should be sent (optional).
fid: The Firebase installation ID of an FCM registered app instance to which the
message should be sent (optional).
token: Deprecated. Use ``fid`` instead.
topic: Name of the FCM topic to which the message should be sent (optional). Topic name
may contain the ``/topics/`` prefix.
condition: The FCM condition to which the message should be sent (optional).
"""

def __init__(self, data=None, notification=None, android=None, webpush=None, apns=None,
fcm_options=None, token=None, topic=None, condition=None):
fcm_options=None, token=None, topic=None, condition=None, fid=None):
if token is not None:
warnings.warn(
"Message.token is deprecated. Use fid instead.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should it be Message.fid to be aligned with Message.token?

DeprecationWarning,
stacklevel=2
)
self.data = data
self.notification = notification
self.android = android
self.webpush = webpush
self.apns = apns
self.fcm_options = fcm_options
self.fid = fid
self.token = token
self.topic = topic
self.condition = condition
Expand All @@ -60,10 +70,11 @@ def __str__(self):


class MulticastMessage:
"""A message that can be sent to multiple tokens via Firebase Cloud Messaging.
"""A message that can be sent to multiple tokens or fids via Firebase Cloud Messaging.

Args:
tokens: A list of registration tokens of targeted devices.
fids: A list of Firebase Installation IDs of targeted app instances (optional).
tokens: Deprecated. Use ``fids`` instead (optional).
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
strings.
notification: An instance of ``messaging.Notification`` (optional).
Expand All @@ -72,12 +83,32 @@ class MulticastMessage:
apns: An instance of ``messaging.ApnsConfig`` (optional).
fcm_options: An instance of ``messaging.FCMOptions`` (optional).
"""
def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None,
fcm_options=None):
_Validators.check_string_list('MulticastMessage.tokens', tokens)
if len(tokens) > 500:
raise ValueError('MulticastMessage.tokens must not contain more than 500 tokens.')
self.tokens = tokens
def __init__(
self, tokens=None, fids=None, data=None, notification=None, android=None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does fids need to be placed at the end?

webpush=None, apns=None, fcm_options=None):
if tokens is not None:
warnings.warn(
"MulticastMessage.tokens is deprecated. Use fids instead.",
DeprecationWarning,
stacklevel=2
)

if (tokens is None and fids is None) or (tokens is not None and fids is not None):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to support the case where the tokens is not None and fids is not None? We can raise an error if len(tokens) + len(fids) > 500 in this case.

Supporting this case will allow people to start using fids even when not all of their tokens have been switched to fids.

With that being said, this is just a nice-to-have. It's unnecessary if it requires too many changes.

raise ValueError("Must specify either 'tokens' or 'fids'.")

if tokens is not None:
_Validators.check_string_list('MulticastMessage.tokens', tokens)
if len(tokens) > 500:
raise ValueError('MulticastMessage.tokens must not contain more than 500 tokens.')
self.tokens = tokens
self.fids = None
else:
_Validators.check_string_list('MulticastMessage.fids', fids)
if len(fids) > 500:
raise ValueError('MulticastMessage.fids must not contain more than 500 fids.')
self.fids = fids
self.tokens = None

self.data = data
self.notification = notification
self.android = android
Expand Down Expand Up @@ -695,16 +726,17 @@ def default(self, o): # pylint: disable=method-hidden
'Message.condition', o.condition, non_empty=True),
'data': _Validators.check_string_dict('Message.data', o.data),
'notification': MessageEncoder.encode_notification(o.notification),
'fid': _Validators.check_string('Message.fid', o.fid, non_empty=True),
'token': _Validators.check_string('Message.token', o.token, non_empty=True),
'topic': _Validators.check_string('Message.topic', o.topic, non_empty=True),
'webpush': MessageEncoder.encode_webpush(o.webpush),
'fcm_options': MessageEncoder.encode_fcm_options(o.fcm_options),
}
result['topic'] = MessageEncoder.sanitize_topic_name(result.get('topic'))
result = MessageEncoder.remove_null_values(result)
target_count = sum(t in result for t in ['token', 'topic', 'condition'])
target_count = sum(t in result for t in ['fid', 'token', 'topic', 'condition'])
if target_count != 1:
raise ValueError('Exactly one of token, topic or condition must be specified.')
raise ValueError('Exactly one of fid, token, topic or condition must be specified.')
return result

@classmethod
Expand Down
58 changes: 33 additions & 25 deletions firebase_admin/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import json
import asyncio
import logging
import warnings
import requests
import httpx

Expand Down Expand Up @@ -172,13 +173,40 @@ async def send_each_async(
"""
return await _get_messaging_service(app).send_each_async(messages, dry_run)

def _get_messages_from_multicast(multicast_message: MulticastMessage) -> List[Message]:
"""Extracts individual Message objects from a MulticastMessage."""
if not isinstance(multicast_message, MulticastMessage):
raise ValueError('Message must be an instance of messaging.MulticastMessage class.')
if multicast_message.tokens is not None:
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
return [Message(
data=multicast_message.data,
notification=multicast_message.notification,
android=multicast_message.android,
webpush=multicast_message.webpush,
apns=multicast_message.apns,
fcm_options=multicast_message.fcm_options,
token=token
) for token in multicast_message.tokens]

return [Message(
data=multicast_message.data,
notification=multicast_message.notification,
android=multicast_message.android,
webpush=multicast_message.webpush,
apns=multicast_message.apns,
fcm_options=multicast_message.fcm_options,
fid=fid
) for fid in multicast_message.fids]

async def send_each_for_multicast_async(
multicast_message: MulticastMessage,
dry_run: bool = False,
app: Optional[App] = None
) -> BatchResponse:
"""Sends the given mutlicast message to each token asynchronously via Firebase Cloud Messaging
(FCM).
"""Sends the given multicast message to each token or fid asynchronously via
Firebase Cloud Messaging (FCM).

If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
recipients. Instead, FCM performs all the usual validations and emulates the send operation.
Expand All @@ -195,21 +223,11 @@ async def send_each_for_multicast_async(
FirebaseError: If an error occurs while sending the message to the FCM service.
ValueError: If the input arguments are invalid.
"""
if not isinstance(multicast_message, MulticastMessage):
raise ValueError('Message must be an instance of messaging.MulticastMessage class.')
messages = [Message(
data=multicast_message.data,
notification=multicast_message.notification,
android=multicast_message.android,
webpush=multicast_message.webpush,
apns=multicast_message.apns,
fcm_options=multicast_message.fcm_options,
token=token
) for token in multicast_message.tokens]
messages = _get_messages_from_multicast(multicast_message)
return await _get_messaging_service(app).send_each_async(messages, dry_run)

def send_each_for_multicast(multicast_message, dry_run=False, app=None):
"""Sends the given mutlicast message to each token via Firebase Cloud Messaging (FCM).
"""Sends the given multicast message to each token or fid via Firebase Cloud Messaging (FCM).

If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
recipients. Instead, FCM performs all the usual validations and emulates the send operation.
Expand All @@ -226,17 +244,7 @@ def send_each_for_multicast(multicast_message, dry_run=False, app=None):
FirebaseError: If an error occurs while sending the message to the FCM service.
ValueError: If the input arguments are invalid.
"""
if not isinstance(multicast_message, MulticastMessage):
raise ValueError('Message must be an instance of messaging.MulticastMessage class.')
messages = [Message(
data=multicast_message.data,
notification=multicast_message.notification,
android=multicast_message.android,
webpush=multicast_message.webpush,
apns=multicast_message.apns,
fcm_options=multicast_message.fcm_options,
token=token
) for token in multicast_message.tokens]
messages = _get_messages_from_multicast(multicast_message)
return _get_messaging_service(app).send_each(messages, dry_run)

def subscribe_to_topic(tokens, topic, app=None):
Expand Down
Loading
Loading