14
14
15
15
"""Firebase Cloud Messaging module."""
16
16
17
+ import json
17
18
import requests
18
19
import six
19
20
21
+ import googleapiclient
22
+ from googleapiclient import http
23
+ from googleapiclient import _auth
24
+
20
25
import firebase_admin
21
26
from firebase_admin import _http_client
22
27
from firebase_admin import _messaging_utils
34
39
'ApiCallError' ,
35
40
'Aps' ,
36
41
'ApsAlert' ,
42
+ 'BatchResponse' ,
37
43
'CriticalSound' ,
38
44
'ErrorInfo' ,
39
45
'Message' ,
46
+ 'MulticastMessage' ,
40
47
'Notification' ,
48
+ 'SendResponse' ,
41
49
'TopicManagementResponse' ,
42
50
'WebpushConfig' ,
43
51
'WebpushFcmOptions' ,
44
52
'WebpushNotification' ,
45
53
'WebpushNotificationAction' ,
46
54
47
55
'send' ,
56
+ 'send_all' ,
57
+ 'send_multicast' ,
48
58
'subscribe_to_topic' ,
49
59
'unsubscribe_from_topic' ,
50
60
]
58
68
ApsAlert = _messaging_utils .ApsAlert
59
69
CriticalSound = _messaging_utils .CriticalSound
60
70
Message = _messaging_utils .Message
71
+ MulticastMessage = _messaging_utils .MulticastMessage
61
72
Notification = _messaging_utils .Notification
62
73
WebpushConfig = _messaging_utils .WebpushConfig
63
74
WebpushFcmOptions = _messaging_utils .WebpushFcmOptions
@@ -88,6 +99,54 @@ def send(message, dry_run=False, app=None):
88
99
"""
89
100
return _get_messaging_service (app ).send (message , dry_run )
90
101
102
+ def send_all (messages , dry_run = False , app = None ):
103
+ """Batch sends the given messages via Firebase Cloud Messaging (FCM).
104
+
105
+ If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
106
+ recipients. Instead FCM performs all the usual validations, and emulates the send operation.
107
+
108
+ Args:
109
+ messages: A list of ``messaging.Message`` instances.
110
+ dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
111
+ app: An App instance (optional).
112
+
113
+ Returns:
114
+ BatchResponse: A ``messaging.BatchResponse`` instance.
115
+
116
+ Raises:
117
+ ApiCallError: If an error occurs while sending the message to FCM service.
118
+ ValueError: If the input arguments are invalid.
119
+ """
120
+ return _get_messaging_service (app ).send_all (messages , dry_run )
121
+
122
+ def send_multicast (multicast_message , dry_run = False , app = None ):
123
+ """Sends the given mutlicast message to the mutlicast message tokens via Firebase Cloud Messaging (FCM).
124
+
125
+ If the ``dry_run`` mode is enabled, the message will not be actually delivered to the
126
+ recipients. Instead FCM performs all the usual validations, and emulates the send operation.
127
+
128
+ Args:
129
+ message: An instance of ``messaging.MulticastMessage``.
130
+ dry_run: A boolean indicating whether to run the operation in dry run mode (optional).
131
+ app: An App instance (optional).
132
+
133
+ Returns:
134
+ BatchResponse: A ``messaging.BatchResponse`` instance.
135
+
136
+ Raises:
137
+ ApiCallError: If an error occurs while sending the message to FCM service.
138
+ ValueError: If the input arguments are invalid.
139
+ """
140
+ messages = map (lambda token : Message (
141
+ data = multicast_message .data ,
142
+ notification = multicast_message .notification ,
143
+ android = multicast_message .android ,
144
+ webpush = multicast_message .webpush ,
145
+ apns = multicast_message .apns ,
146
+ token = token
147
+ ), multicast_message .tokens )
148
+ return _get_messaging_service (app ).send_all (messages , dry_run )
149
+
91
150
def subscribe_to_topic (tokens , topic , app = None ):
92
151
"""Subscribes a list of registration tokens to an FCM topic.
93
152
@@ -192,10 +251,67 @@ def __init__(self, code, message, detail=None):
192
251
self .detail = detail
193
252
194
253
254
+ class BatchResponse (object ):
255
+
256
+ def __init__ (self , responses ):
257
+ self ._responses = responses
258
+ self ._success_count = 0
259
+ for response in responses :
260
+ if response .success :
261
+ self ._success_count += 1
262
+
263
+ @property
264
+ def responses (self ):
265
+ """A list of ``messaging.SendResponse`` objects (possibly empty)."""
266
+ return self ._responses
267
+
268
+ @property
269
+ def success_count (self ):
270
+ return self ._success_count
271
+
272
+ @property
273
+ def failure_count (self ):
274
+ return len (self .responses ) - self .success_count
275
+
276
+
277
+ class SendResponse (object ):
278
+
279
+ def __init__ (self , resp , exception ):
280
+ self ._message_id = None
281
+ self ._exception = None
282
+ if resp :
283
+ self ._message_id = resp .get ('name' , None )
284
+ if exception :
285
+ data = {}
286
+ try :
287
+ parsed_body = json .loads (exception .content )
288
+ if isinstance (parsed_body , dict ):
289
+ data = parsed_body
290
+ except ValueError :
291
+ pass
292
+ self ._exception = _MessagingService ._parse_fcm_error (data , exception .content , exception .resp .status , exception )
293
+
294
+ @property
295
+ def message_id (self ):
296
+ """A message ID string that uniquely identifies the sent the message."""
297
+ return self ._message_id
298
+
299
+ @property
300
+ def success (self ):
301
+ """A boolean indicating if the request was successful."""
302
+ return self ._message_id is not None and not self ._exception
303
+
304
+ @property
305
+ def exception (self ):
306
+ """A ApiCallError if an error occurs while sending the message to FCM service."""
307
+ return self ._exception
308
+
309
+
195
310
class _MessagingService (object ):
196
311
"""Service class that implements Firebase Cloud Messaging (FCM) functionality."""
197
312
198
313
FCM_URL = 'https://fcm.googleapis.com/v1/projects/{0}/messages:send'
314
+ FCM_BATCH_URL = 'https://fcm.googleapis.com/batch'
199
315
IID_URL = 'https://iid.googleapis.com'
200
316
IID_HEADERS = {'access_token_auth' : 'true' }
201
317
JSON_ENCODER = _messaging_utils .MessageEncoder ()
@@ -234,9 +350,13 @@ def __init__(self, app):
234
350
'projectId option, or use service account credentials. Alternatively, set the '
235
351
'GOOGLE_CLOUD_PROJECT environment variable.' )
236
352
self ._fcm_url = _MessagingService .FCM_URL .format (project_id )
353
+ self ._fcm_headers = {
354
+ 'X-GOOG-API-FORMAT-VERSION' : '2' ,
355
+ 'X-FIREBASE-CLIENT' : 'fire-admin-python/{0}' .format (firebase_admin .__version__ ),
356
+ }
237
357
self ._client = _http_client .JsonHttpClient (credential = app .credential .get_credential ())
238
358
self ._timeout = app .options .get ('httpTimeout' )
239
- self ._client_version = 'fire-admin-python/{0}' . format ( firebase_admin . __version__ )
359
+ self ._transport = _auth . authorized_http ( app . credential . get_credential () )
240
360
241
361
@classmethod
242
362
def encode_message (cls , message ):
@@ -245,16 +365,10 @@ def encode_message(cls, message):
245
365
return cls .JSON_ENCODER .default (message )
246
366
247
367
def send (self , message , dry_run = False ):
248
- data = {'message' : _MessagingService .encode_message (message )}
249
- if dry_run :
250
- data ['validate_only' ] = True
368
+ data = self ._message_data (message , dry_run )
251
369
try :
252
- headers = {
253
- 'X-GOOG-API-FORMAT-VERSION' : '2' ,
254
- 'X-FIREBASE-CLIENT' : self ._client_version ,
255
- }
256
370
resp = self ._client .body (
257
- 'post' , url = self ._fcm_url , headers = headers , json = data , timeout = self ._timeout )
371
+ 'post' , url = self ._fcm_url , headers = self . _fcm_headers , json = data , timeout = self ._timeout )
258
372
except requests .exceptions .RequestException as error :
259
373
if error .response is not None :
260
374
self ._handle_fcm_error (error )
@@ -264,6 +378,23 @@ def send(self, message, dry_run=False):
264
378
else :
265
379
return resp ['name' ]
266
380
381
+ def send_all (self , messages , dry_run = False ):
382
+ responses = []
383
+
384
+ def batch_callback (request_id , response , exception ):
385
+ send_response = SendResponse (response , exception )
386
+ responses .append (send_response )
387
+
388
+ batch = http .BatchHttpRequest (batch_callback , _MessagingService .FCM_BATCH_URL )
389
+ for message in messages :
390
+ body = json .dumps (self ._message_data (message , dry_run ))
391
+ req = http .HttpRequest (
392
+ http = self ._transport , postproc = self ._postproc , uri = self ._fcm_url , method = 'POST' , body = body , headers = self ._fcm_headers )
393
+ batch .add (req )
394
+
395
+ batch .execute ()
396
+ return BatchResponse (responses )
397
+
267
398
def make_topic_management_request (self , tokens , topic , operation ):
268
399
"""Invokes the IID service for topic management functionality."""
269
400
if isinstance (tokens , six .string_types ):
@@ -299,6 +430,18 @@ def make_topic_management_request(self, tokens, topic, operation):
299
430
else :
300
431
return TopicManagementResponse (resp )
301
432
433
+ def _message_data (self , message , dry_run ):
434
+ data = {'message' : _MessagingService .encode_message (message )}
435
+ if dry_run :
436
+ data ['validate_only' ] = True
437
+ return data
438
+
439
+ def _postproc (self , resp , body ):
440
+ if resp .status == 200 :
441
+ return json .loads (body )
442
+ else :
443
+ raise Exception ('unexpected response' )
444
+
302
445
def _handle_fcm_error (self , error ):
303
446
"""Handles errors received from the FCM API."""
304
447
data = {}
@@ -309,21 +452,7 @@ def _handle_fcm_error(self, error):
309
452
except ValueError :
310
453
pass
311
454
312
- error_dict = data .get ('error' , {})
313
- server_code = None
314
- for detail in error_dict .get ('details' , []):
315
- if detail .get ('@type' ) == 'type.googleapis.com/google.firebase.fcm.v1.FcmError' :
316
- server_code = detail .get ('errorCode' )
317
- break
318
- if not server_code :
319
- server_code = error_dict .get ('status' )
320
- code = _MessagingService .FCM_ERROR_CODES .get (server_code , _MessagingService .UNKNOWN_ERROR )
321
-
322
- msg = error_dict .get ('message' )
323
- if not msg :
324
- 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 )
455
+ raise _MessagingService ._parse_fcm_error (data , error .response .content , error .response .status_code , error )
327
456
328
457
def _handle_iid_error (self , error ):
329
458
"""Handles errors received from the Instance ID API."""
@@ -342,3 +471,22 @@ def _handle_iid_error(self, error):
342
471
msg = 'Unexpected HTTP response with status: {0}; body: {1}' .format (
343
472
error .response .status_code , error .response .content .decode ())
344
473
raise ApiCallError (code , msg , error )
474
+
475
+ @classmethod
476
+ def _parse_fcm_error (cls , data , content , status_code , error ):
477
+ """Parses an error response from the FCM API to a ApiCallError."""
478
+ error_dict = data .get ('error' , {})
479
+ server_code = None
480
+ for detail in error_dict .get ('details' , []):
481
+ if detail .get ('@type' ) == 'type.googleapis.com/google.firebase.fcm.v1.FcmError' :
482
+ server_code = detail .get ('errorCode' )
483
+ break
484
+ if not server_code :
485
+ server_code = error_dict .get ('status' )
486
+ code = _MessagingService .FCM_ERROR_CODES .get (server_code , _MessagingService .UNKNOWN_ERROR )
487
+
488
+ msg = error_dict .get ('message' )
489
+ if not msg :
490
+ msg = 'Unexpected HTTP response with status: {0}; body: {1}' .format (status_code , content .decode ())
491
+
492
+ return ApiCallError (code , msg , error )
0 commit comments