Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 45 additions & 12 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 Message.fid instead.",
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,24 +70,46 @@ 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.
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).
android: An instance of ``messaging.AndroidConfig`` (optional).
webpush: An instance of ``messaging.WebpushConfig`` (optional).
apns: An instance of ``messaging.ApnsConfig`` (optional).
fcm_options: An instance of ``messaging.FCMOptions`` (optional).
fids: A list of Firebase Installation IDs of targeted app instances (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.')
def __init__(
self, tokens=None, data=None, notification=None, android=None,
webpush=None, apns=None, fcm_options=None, fids=None):
if tokens is not None:
warnings.warn(
"MulticastMessage.tokens is deprecated. Use MulticastMessage.fids instead.",
DeprecationWarning,
stacklevel=2
)

if tokens is None and fids is None:
raise ValueError(
"Must specify at least one of MulticastMessage.tokens or MulticastMessage.fids.")

if tokens is not None:
_Validators.check_string_list('MulticastMessage.tokens', tokens)
if fids is not None:
_Validators.check_string_list('MulticastMessage.fids', fids)

tokens_len = len(tokens) if tokens is not None else 0
fids_len = len(fids) if fids is not None else 0
if tokens_len + fids_len > 500:
raise ValueError(
'Total number of tokens and fids must not exceed 500.')

self.tokens = tokens
self.fids = fids
self.data = data
self.notification = notification
self.android = android
Expand Down Expand Up @@ -695,16 +727,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
63 changes: 38 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,45 @@ 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.')

messages = []
if multicast_message.tokens is not None:
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
messages.extend([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])

if multicast_message.fids is not None:
messages.extend([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])

return messages

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 +228,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 +249,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
23 changes: 23 additions & 0 deletions integration/test_messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ def test_send_malformed_token():
with pytest.raises(exceptions.InvalidArgumentError):
messaging.send(msg, dry_run=True)

def test_send_invalid_fid():
msg = messaging.Message(
fid='not-a-fid',
notification=messaging.Notification('test-title', 'test-body')
)
with pytest.raises(exceptions.InvalidArgumentError):
messaging.send(msg, dry_run=True)

def test_send_each():
messages = [
messaging.Message(
Expand Down Expand Up @@ -149,6 +157,21 @@ def test_send_each_for_multicast():
assert response.exception is not None
assert response.message_id is None

def test_send_each_for_multicast_fids():
multicast = messaging.MulticastMessage(
notification=messaging.Notification('Title', 'Body'),
fids=['not-a-fid', 'also-not-a-fid'])

batch_response = messaging.send_each_for_multicast(multicast)

assert batch_response.success_count == 0
assert batch_response.failure_count == 2
assert len(batch_response.responses) == 2
for response in batch_response.responses:
assert response.success is False
assert response.exception is not None
assert response.message_id is None

def test_subscribe():
resp = messaging.subscribe_to_topic(_REGISTRATION_TOKEN, 'mock-topic')
assert resp.success_count + resp.failure_count == 1
Expand Down
Loading
Loading