14
14
15
15
"""Firebase Cloud Messaging module."""
16
16
17
+ import json
17
18
import requests
18
19
import six
19
20
45
46
'WebpushNotificationAction' ,
46
47
47
48
'send' ,
49
+ 'sendAll' ,
48
50
'subscribe_to_topic' ,
49
51
'unsubscribe_from_topic' ,
50
52
]
@@ -88,6 +90,26 @@ def send(message, dry_run=False, app=None):
88
90
"""
89
91
return _get_messaging_service (app ).send (message , dry_run )
90
92
93
+ def sendAll (messages , dry_run = False , app = None ):
94
+ """Batch sends the given messages via Firebase Cloud Messaging (FCM).
95
+
96
+ If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
97
+ recipients. Instead FCM performs all the usual validations, and emulates the send operation.
98
+
99
+ Args:
100
+ messages: A list of ``messaging.Message`` instances.
101
+ dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
102
+ app: An App instance (optional).
103
+
104
+ Returns:
105
+ list: A list of message ID string that uniquely identifies the sent the messages.
106
+
107
+ Raises:
108
+ ApiCallError: If an error occurs while sending the message to FCM service.
109
+ ValueError: If the input arguments are invalid.
110
+ """
111
+ return _get_messaging_service (app ).sendAll (messages , dry_run )
112
+
91
113
def subscribe_to_topic (tokens , topic , app = None ):
92
114
"""Subscribes a list of registration tokens to an FCM topic.
93
115
@@ -196,10 +218,13 @@ class _MessagingService(object):
196
218
"""Service class that implements Firebase Cloud Messaging (FCM) functionality."""
197
219
198
220
FCM_URL = 'https://fcm.googleapis.com/v1/projects/{0}/messages:send'
221
+ FCM_BATCH_URL = 'https://fcm.googleapis.com/batch'
199
222
IID_URL = 'https://iid.googleapis.com'
200
223
IID_HEADERS = {'access_token_auth' : 'true' }
201
224
JSON_ENCODER = _messaging_utils .MessageEncoder ()
202
225
226
+ BATCH_PART_BOUNDARY = '__END_OF_PART__'
227
+
203
228
INTERNAL_ERROR = 'internal-error'
204
229
UNKNOWN_ERROR = 'unknown-error'
205
230
FCM_ERROR_CODES = {
@@ -235,26 +260,25 @@ def __init__(self, app):
235
260
'GOOGLE_CLOUD_PROJECT environment variable.' )
236
261
self ._fcm_url = _MessagingService .FCM_URL .format (project_id )
237
262
self ._client = _http_client .JsonHttpClient (credential = app .credential .get_credential ())
263
+ self ._fcm_headers = {
264
+ 'X-GOOG-API-FORMAT-VERSION' : '2' ,
265
+ 'X-FIREBASE-CLIENT' : 'fire-admin-python/{0}' .format (firebase_admin .__version__ )
266
+ }
238
267
self ._timeout = app .options .get ('httpTimeout' )
239
- self ._client_version = 'fire-admin-python/{0}' .format (firebase_admin .__version__ )
240
268
241
269
@classmethod
242
270
def encode_message (cls , message ):
243
271
if not isinstance (message , Message ):
244
272
raise ValueError ('Message must be an instance of messaging.Message class.' )
245
273
return cls .JSON_ENCODER .default (message )
246
274
275
+ # TODO: sendMulticast
276
+
247
277
def send (self , message , dry_run = False ):
248
- data = {'message' : _MessagingService .encode_message (message )}
249
- if dry_run :
250
- data ['validate_only' ] = True
278
+ data = self ._message_data (message , dry_run )
251
279
try :
252
- headers = {
253
- 'X-GOOG-API-FORMAT-VERSION' : '2' ,
254
- 'X-FIREBASE-CLIENT' : self ._client_version ,
255
- }
256
280
resp = self ._client .body (
257
- 'post' , url = self ._fcm_url , headers = headers , json = data , timeout = self ._timeout )
281
+ 'post' , url = self ._fcm_url , headers = self . _fcm_headers , json = data , timeout = self ._timeout )
258
282
except requests .exceptions .RequestException as error :
259
283
if error .response is not None :
260
284
self ._handle_fcm_error (error )
@@ -264,6 +288,35 @@ def send(self, message, dry_run=False):
264
288
else :
265
289
return resp ['name' ]
266
290
291
+ def sendAll (self , messages , dry_run = False ):
292
+ data = self ._batch_message_data (messages , dry_run )
293
+ headers = {
294
+ 'Content-Type' : 'multipart/mixed; boundary={}' .format (_MessagingService .BATCH_PART_BOUNDARY )
295
+ }
296
+ try :
297
+ response = self ._client .request (
298
+ 'post' , url = _MessagingService .FCM_BATCH_URL , headers = headers , data = data , timeout = self ._timeout
299
+ )
300
+ except requests .exceptions .RequestException as error :
301
+ if error .response is not None :
302
+ self ._handle_fcm_error (error )
303
+ else :
304
+ msg = 'Failed to call messaging API: {0}' .format (error )
305
+ raise ApiCallError (self .INTERNAL_ERROR , msg , error )
306
+ else :
307
+ import re
308
+ # Split our batches responses in to groups
309
+ response_regex = re .compile ('--batch.*?$\n (.*?)(?=--batch)' , re .DOTALL | re .MULTILINE )
310
+ responses = response_regex .findall (response .content )
311
+
312
+ # First section is headers related to the batch request
313
+ # Second section is headers related to the batched request
314
+ # Third section is data related to the batched request
315
+ response_sections = [r .split ('\r \n \r \n ' ) for r in responses ]
316
+ response_json = [json .loads (r [2 ]) for r in response_sections ]
317
+
318
+ return response_json
319
+
267
320
def make_topic_management_request (self , tokens , topic , operation ):
268
321
"""Invokes the IID service for topic management functionality."""
269
322
if isinstance (tokens , six .string_types ):
@@ -299,6 +352,39 @@ def make_topic_management_request(self, tokens, topic, operation):
299
352
else :
300
353
return TopicManagementResponse (resp )
301
354
355
+ def _message_data (self , message , dry_run ):
356
+ data = {'message' : _MessagingService .encode_message (message )}
357
+ if dry_run :
358
+ data ['validate_only' ] = True
359
+ return data
360
+
361
+ def _batch_message_data (self , messages , dry_run ):
362
+ parts = [self ._batch_request_part (request , dry_run , _MessagingService .BATCH_PART_BOUNDARY , index )\
363
+ for (index , request ) in enumerate (messages )]
364
+ return '{}--{}--\r \n ' .format ('' .join (parts ), _MessagingService .BATCH_PART_BOUNDARY )
365
+
366
+ def _batch_request_part (self , request , dry_run , boundary , index ):
367
+ data = self ._batch_request_part_data (request , dry_run )
368
+ part = '--{}\r \n ' .format (boundary )
369
+ part += 'Content-Length: {}\r \n ' .format (len (data ))
370
+ part += 'Content-Type: application/http\r \n '
371
+ part += 'content-id: {}\r \n ' .format (index + 1 )
372
+ part += 'content-transfer-encoding: binary\r \n '
373
+ part += '\r \n '
374
+ part += '{}\r \n ' .format (data )
375
+ return part
376
+
377
+ def _batch_request_part_data (self , request , dry_run ):
378
+ body = json .dumps (self ._message_data (request , dry_run ))
379
+ data = 'POST {} HTTP/1.1\r \n ' .format (self ._fcm_url )
380
+ data += 'Content-Length: {}\r \n ' .format (len (body ))
381
+ data += 'Content-Type: application/json; charset=UTF-8\r \n '
382
+ data += '\r \n ' .join (['{}: {}' .format (k , v ) for k , v in self ._fcm_headers .items ()])
383
+ data += '\r \n \r \n '
384
+ data += body
385
+ return data
386
+
387
+
302
388
def _handle_fcm_error (self , error ):
303
389
"""Handles errors received from the FCM API."""
304
390
data = {}
0 commit comments