Skip to content

Commit 844207e

Browse files
committed
Add messaging send_all and send_multicast functions
1 parent 3da3b5a commit 844207e

File tree

3 files changed

+186
-19
lines changed

3 files changed

+186
-19
lines changed

firebase_admin/_messaging_utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ def __init__(self, data=None, notification=None, android=None, webpush=None, apn
5454
self.condition = condition
5555

5656

57+
class MulticastMessage(Message):
58+
"""A message that can be sent to multiple tokens via Firebase Cloud Messaging.
59+
60+
Contains payload information as well as recipient information. In particular, the message must
61+
contain exactly one of token, topic or condition fields.
62+
63+
Args:
64+
tokens: A list of registration token of the device to which the message should be sent (optional).
65+
data: A dictionary of data fields (optional). All keys and values in the dictionary must be
66+
strings.
67+
notification: An instance of ``messaging.Notification`` (optional).
68+
android: An instance of ``messaging.AndroidConfig`` (optional).
69+
webpush: An instance of ``messaging.WebpushConfig`` (optional).
70+
apns: An instance of ``messaging.ApnsConfig`` (optional).
71+
"""
72+
def __init__(self, tokens=[], data=None, notification=None, android=None, webpush=None, apns=None):
73+
super(MulticastMessage, self).__init__(data=data, notification=notification, android=android, webpush=webpush, apns=apns)
74+
self.tokens = tokens
75+
76+
5777
class Notification(object):
5878
"""A notification that can be included in a message.
5979

firebase_admin/messaging.py

Lines changed: 165 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
import requests
1818
import six
19+
import threading
20+
21+
import googleapiclient
22+
from googleapiclient.discovery import build
1923

2024
import firebase_admin
2125
from firebase_admin import _http_client
@@ -34,17 +38,22 @@
3438
'ApiCallError',
3539
'Aps',
3640
'ApsAlert',
41+
'BatchResponse',
3742
'CriticalSound',
3843
'ErrorInfo',
3944
'Message',
45+
'MulticastMessage',
4046
'Notification',
47+
'SendResponse',
4148
'TopicManagementResponse',
4249
'WebpushConfig',
4350
'WebpushFcmOptions',
4451
'WebpushNotification',
4552
'WebpushNotificationAction',
4653

4754
'send',
55+
'send_all',
56+
'send_multicast',
4857
'subscribe_to_topic',
4958
'unsubscribe_from_topic',
5059
]
@@ -58,6 +67,7 @@
5867
ApsAlert = _messaging_utils.ApsAlert
5968
CriticalSound = _messaging_utils.CriticalSound
6069
Message = _messaging_utils.Message
70+
MulticastMessage = _messaging_utils.MulticastMessage
6171
Notification = _messaging_utils.Notification
6272
WebpushConfig = _messaging_utils.WebpushConfig
6373
WebpushFcmOptions = _messaging_utils.WebpushFcmOptions
@@ -88,6 +98,54 @@ def send(message, dry_run=False, app=None):
8898
"""
8999
return _get_messaging_service(app).send(message, dry_run)
90100

101+
def send_all(messages, dry_run=False, app=None):
102+
"""Batch sends the given messages via Firebase Cloud Messaging (FCM).
103+
104+
If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
105+
recipients. Instead FCM performs all the usual validations, and emulates the send operation.
106+
107+
Args:
108+
messages: A list of ``messaging.Message`` instances.
109+
dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
110+
app: An App instance (optional).
111+
112+
Returns:
113+
BatchResponse: A ``messaging.BatchResponse`` instance.
114+
115+
Raises:
116+
ApiCallError: If an error occurs while sending the message to FCM service.
117+
ValueError: If the input arguments are invalid.
118+
"""
119+
return _get_messaging_service(app).send_all(messages, dry_run)
120+
121+
def send_multicast(multicast_message, dry_run=False, app=None):
122+
"""Sends the given mutlicast message to the mutlicast message tokens via Firebase Cloud Messaging (FCM).
123+
124+
If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
125+
recipients. Instead FCM performs all the usual validations, and emulates the send operation.
126+
127+
Args:
128+
message: An instance of ``messaging.MulticastMessage``.
129+
dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
130+
app: An App instance (optional).
131+
132+
Returns:
133+
BatchResponse: A ``messaging.BatchResponse`` instance.
134+
135+
Raises:
136+
ApiCallError: If an error occurs while sending the message to FCM service.
137+
ValueError: If the input arguments are invalid.
138+
"""
139+
messages = map(lambda token: Message(
140+
data=multicast_message.data,
141+
notification=multicast_message.notification,
142+
android=multicast_message.android,
143+
webpush=multicast_message.webpush,
144+
apns=multicast_message.apns,
145+
token=token
146+
), multicast_message.tokens)
147+
return _get_messaging_service(app).send_all(messages, dry_run)
148+
91149
def subscribe_to_topic(tokens, topic, app=None):
92150
"""Subscribes a list of registration tokens to an FCM topic.
93151
@@ -192,10 +250,72 @@ def __init__(self, code, message, detail=None):
192250
self.detail = detail
193251

194252

253+
class BatchResponse(object):
254+
255+
def __init__(self, responses):
256+
if not isinstance(responses, list):
257+
raise ValueError('Unexpected responses: {0}.'.format(responses))
258+
self._responses = responses
259+
self._success_count = 0
260+
self._failure_count = 0
261+
for response in responses:
262+
if response.success:
263+
self._success_count += 1
264+
else:
265+
self._failure_count += 1
266+
267+
@property
268+
def responses(self):
269+
"""A list of ``messaging.SendResponse`` objects (possibly empty)."""
270+
return self._responses
271+
272+
@property
273+
def success_count(self):
274+
return self._success_count
275+
276+
@property
277+
def failure_count(self):
278+
return self._failure_count
279+
280+
281+
class SendResponse(object):
282+
283+
def __init__(self, resp, exception):
284+
if resp and not isinstance(resp, dict):
285+
raise ValueError('Unexpected response: {0}.'.format(resp))
286+
287+
self._message_id = None
288+
self._exception = None
289+
290+
if resp:
291+
self._message_id = resp.get('name', None)
292+
293+
if exception:
294+
if exception.content is not None:
295+
self._exception = _MessagingService._parse_fcm_error(exception)
296+
else:
297+
msg = 'Failed to call messaging API: {0}'.format(exception)
298+
self._exception = ApiCallError(_MessagingService.INTERNAL_ERROR, msg, exception)
299+
300+
@property
301+
def message_id(self):
302+
"""A message ID string that uniquely identifies the sent the message."""
303+
return self._message_id
304+
305+
@property
306+
def success(self):
307+
"""A boolean indicating if the request was successful."""
308+
return self._message_id is not None and not self._exception
309+
310+
@property
311+
def exception(self):
312+
"""A ApiCallError if an error occurs while sending the message to FCM service."""
313+
return self._exception
314+
315+
195316
class _MessagingService(object):
196317
"""Service class that implements Firebase Cloud Messaging (FCM) functionality."""
197318

198-
FCM_URL = 'https://fcm.googleapis.com/v1/projects/{0}/messages:send'
199319
IID_URL = 'https://iid.googleapis.com'
200320
IID_HEADERS = {'access_token_auth': 'true'}
201321
JSON_ENCODER = _messaging_utils.MessageEncoder()
@@ -233,10 +353,14 @@ def __init__(self, app):
233353
'Project ID is required to access Cloud Messaging service. Either set the '
234354
'projectId option, or use service account credentials. Alternatively, set the '
235355
'GOOGLE_CLOUD_PROJECT environment variable.')
236-
self._fcm_url = _MessagingService.FCM_URL.format(project_id)
356+
self._fcm_service = build('fcm', 'v1', credentials=app.credential.get_credential())
357+
self._fcm_parent = 'projects/{}'.format(project_id)
358+
self._fcm_headers = {
359+
'X-GOOG-API-FORMAT-VERSION': '2',
360+
'X-FIREBASE-CLIENT': 'fire-admin-python/{0}'.format(firebase_admin.__version__)
361+
}
237362
self._client = _http_client.JsonHttpClient(credential=app.credential.get_credential())
238363
self._timeout = app.options.get('httpTimeout')
239-
self._client_version = 'fire-admin-python/{0}'.format(firebase_admin.__version__)
240364

241365
@classmethod
242366
def encode_message(cls, message):
@@ -245,25 +369,37 @@ def encode_message(cls, message):
245369
return cls.JSON_ENCODER.default(message)
246370

247371
def send(self, message, dry_run=False):
248-
data = {'message': _MessagingService.encode_message(message)}
249-
if dry_run:
250-
data['validate_only'] = True
372+
request = self._message_request(message, dry_run)
251373
try:
252-
headers = {
253-
'X-GOOG-API-FORMAT-VERSION': '2',
254-
'X-FIREBASE-CLIENT': self._client_version,
255-
}
256-
resp = self._client.body(
257-
'post', url=self._fcm_url, headers=headers, json=data, timeout=self._timeout)
258-
except requests.exceptions.RequestException as error:
259-
if error.response is not None:
260-
self._handle_fcm_error(error)
374+
resp = request.execute()
375+
except googleapiclient.errors.HttpError as error:
376+
if error.content is not None:
377+
raise _MessagingService._parse_fcm_error(error)
261378
else:
262379
msg = 'Failed to call messaging API: {0}'.format(error)
263380
raise ApiCallError(self.INTERNAL_ERROR, msg, error)
264381
else:
265382
return resp['name']
266383

384+
def send_all(self, messages, dry_run=False):
385+
message_count = len(messages)
386+
send_all_complete = threading.Event()
387+
responses = []
388+
389+
def send_all_callback(request_id, response, exception):
390+
send_response = SendResponse(response, exception)
391+
responses.append(send_response)
392+
if len(responses) == message_count:
393+
send_all_complete.set()
394+
395+
batch = self._fcm_service.new_batch_http_request(callback=send_all_callback)
396+
for message in messages:
397+
batch.add(self._message_request(message, dry_run))
398+
batch.execute()
399+
400+
send_all_complete.wait()
401+
return BatchResponse(responses)
402+
267403
def make_topic_management_request(self, tokens, topic, operation):
268404
"""Invokes the IID service for topic management functionality."""
269405
if isinstance(tokens, six.string_types):
@@ -299,11 +435,21 @@ def make_topic_management_request(self, tokens, topic, operation):
299435
else:
300436
return TopicManagementResponse(resp)
301437

302-
def _handle_fcm_error(self, error):
438+
def _message_request(self, message, dry_run):
439+
data = {'message': _MessagingService.encode_message(message)}
440+
if dry_run:
441+
data['validate_only'] = True
442+
request = self._fcm_service.projects().messages().send(parent=self._fcm_parent, body=data)
443+
request.headers.update(self._fcm_headers)
444+
return request
445+
446+
@classmethod
447+
def _parse_fcm_error(cls, error):
303448
"""Handles errors received from the FCM API."""
304449
data = {}
305450
try:
306-
parsed_body = error.response.json()
451+
import json
452+
parsed_body = json.loads(error.content)
307453
if isinstance(parsed_body, dict):
308454
data = parsed_body
309455
except ValueError:
@@ -322,8 +468,8 @@ def _handle_fcm_error(self, error):
322468
msg = error_dict.get('message')
323469
if not msg:
324470
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(
325-
error.response.status_code, error.response.content.decode())
326-
raise ApiCallError(code, msg, error)
471+
error.resp.status, error.content)
472+
return ApiCallError(code, msg, error)
327473

328474
def _handle_iid_error(self, error):
329475
"""Handles errors received from the Instance ID API."""

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ tox >= 3.6.0
66

77
cachecontrol >= 0.12.4
88
google-api-core[grpc] >= 1.7.0, < 2.0.0dev; platform.python_implementation != 'PyPy'
9+
google-api-python-client >= 1.7.8
910
google-cloud-firestore >= 0.31.0; platform.python_implementation != 'PyPy'
1011
google-cloud-storage >= 1.13.0
1112
six >= 1.6.1

0 commit comments

Comments
 (0)