16
16
17
17
import requests
18
18
import six
19
+ import threading
20
+
21
+ import googleapiclient
22
+ from googleapiclient .discovery import build
19
23
20
24
import firebase_admin
21
25
from firebase_admin import _http_client
34
38
'ApiCallError' ,
35
39
'Aps' ,
36
40
'ApsAlert' ,
41
+ 'BatchResponse' ,
37
42
'CriticalSound' ,
38
43
'ErrorInfo' ,
39
44
'Message' ,
45
+ 'MulticastMessage' ,
40
46
'Notification' ,
47
+ 'SendResponse' ,
41
48
'TopicManagementResponse' ,
42
49
'WebpushConfig' ,
43
50
'WebpushFcmOptions' ,
44
51
'WebpushNotification' ,
45
52
'WebpushNotificationAction' ,
46
53
47
54
'send' ,
55
+ 'send_all' ,
56
+ 'send_multicast' ,
48
57
'subscribe_to_topic' ,
49
58
'unsubscribe_from_topic' ,
50
59
]
58
67
ApsAlert = _messaging_utils .ApsAlert
59
68
CriticalSound = _messaging_utils .CriticalSound
60
69
Message = _messaging_utils .Message
70
+ MulticastMessage = _messaging_utils .MulticastMessage
61
71
Notification = _messaging_utils .Notification
62
72
WebpushConfig = _messaging_utils .WebpushConfig
63
73
WebpushFcmOptions = _messaging_utils .WebpushFcmOptions
@@ -88,6 +98,54 @@ def send(message, dry_run=False, app=None):
88
98
"""
89
99
return _get_messaging_service (app ).send (message , dry_run )
90
100
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
+
91
149
def subscribe_to_topic (tokens , topic , app = None ):
92
150
"""Subscribes a list of registration tokens to an FCM topic.
93
151
@@ -192,10 +250,65 @@ def __init__(self, code, message, detail=None):
192
250
self .detail = detail
193
251
194
252
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
+ self ._message_id = None
287
+ self ._exception = None
288
+ if resp :
289
+ self ._message_id = resp .get ('name' , None )
290
+ if exception :
291
+ self ._exception = _MessagingService ._parse_fcm_error (exception )
292
+
293
+ @property
294
+ def message_id (self ):
295
+ """A message ID string that uniquely identifies the sent the message."""
296
+ return self ._message_id
297
+
298
+ @property
299
+ def success (self ):
300
+ """A boolean indicating if the request was successful."""
301
+ return self ._message_id is not None and not self ._exception
302
+
303
+ @property
304
+ def exception (self ):
305
+ """A ApiCallError if an error occurs while sending the message to FCM service."""
306
+ return self ._exception
307
+
308
+
195
309
class _MessagingService (object ):
196
310
"""Service class that implements Firebase Cloud Messaging (FCM) functionality."""
197
311
198
- FCM_URL = 'https://fcm.googleapis.com/v1/projects/{0}/messages:send'
199
312
IID_URL = 'https://iid.googleapis.com'
200
313
IID_HEADERS = {'access_token_auth' : 'true' }
201
314
JSON_ENCODER = _messaging_utils .MessageEncoder ()
@@ -233,7 +346,8 @@ def __init__(self, app):
233
346
'Project ID is required to access Cloud Messaging service. Either set the '
234
347
'projectId option, or use service account credentials. Alternatively, set the '
235
348
'GOOGLE_CLOUD_PROJECT environment variable.' )
236
- self ._fcm_url = _MessagingService .FCM_URL .format (project_id )
349
+ self ._fcm_service = build ('fcm' , 'v1' , credentials = app .credential .get_credential ())
350
+ self ._fcm_parent = 'projects/{}' .format (project_id )
237
351
self ._client = _http_client .JsonHttpClient (credential = app .credential .get_credential ())
238
352
self ._timeout = app .options .get ('httpTimeout' )
239
353
self ._client_version = 'fire-admin-python/{0}' .format (firebase_admin .__version__ )
@@ -245,25 +359,33 @@ def encode_message(cls, message):
245
359
return cls .JSON_ENCODER .default (message )
246
360
247
361
def send (self , message , dry_run = False ):
248
- data = {'message' : _MessagingService .encode_message (message )}
249
- if dry_run :
250
- data ['validate_only' ] = True
362
+ request = self ._message_request (message , dry_run )
251
363
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 )
261
- else :
262
- msg = 'Failed to call messaging API: {0}' .format (error )
263
- raise ApiCallError (self .INTERNAL_ERROR , msg , error )
364
+ resp = request .execute ()
365
+ except googleapiclient .errors .HttpError as error :
366
+ raise _MessagingService ._parse_fcm_error (error )
264
367
else :
265
368
return resp ['name' ]
266
369
370
+ def send_all (self , messages , dry_run = False ):
371
+ message_count = len (messages )
372
+ send_all_complete = threading .Event ()
373
+ responses = []
374
+
375
+ def send_all_callback (request_id , response , exception ):
376
+ send_response = SendResponse (response , exception )
377
+ responses .append (send_response )
378
+ if len (responses ) == message_count :
379
+ send_all_complete .set ()
380
+
381
+ batch = self ._fcm_service .new_batch_http_request (callback = send_all_callback )
382
+ for message in messages :
383
+ batch .add (self ._message_request (message , dry_run ))
384
+ batch .execute ()
385
+
386
+ send_all_complete .wait ()
387
+ return BatchResponse (responses )
388
+
267
389
def make_topic_management_request (self , tokens , topic , operation ):
268
390
"""Invokes the IID service for topic management functionality."""
269
391
if isinstance (tokens , six .string_types ):
@@ -299,11 +421,29 @@ def make_topic_management_request(self, tokens, topic, operation):
299
421
else :
300
422
return TopicManagementResponse (resp )
301
423
302
- def _handle_fcm_error (self , error ):
424
+ def _message_request (self , message , dry_run ):
425
+ data = {'message' : _MessagingService .encode_message (message )}
426
+ if dry_run :
427
+ data ['validate_only' ] = True
428
+ request = self ._fcm_service .projects ().messages ().send (parent = self ._fcm_parent , body = data )
429
+ headers = {
430
+ 'X-GOOG-API-FORMAT-VERSION' : '2' ,
431
+ 'X-FIREBASE-CLIENT' : self ._client_version ,
432
+ }
433
+ request .headers .update (headers )
434
+ return request
435
+
436
+ @classmethod
437
+ def _parse_fcm_error (cls , error ):
303
438
"""Handles errors received from the FCM API."""
439
+ if error .content is None :
440
+ msg = 'Failed to call messaging API: {0}' .format (error )
441
+ return ApiCallError (_MessagingService .INTERNAL_ERROR , msg , error )
442
+
304
443
data = {}
305
444
try :
306
- parsed_body = error .response .json ()
445
+ import json
446
+ parsed_body = json .loads (error .content )
307
447
if isinstance (parsed_body , dict ):
308
448
data = parsed_body
309
449
except ValueError :
@@ -322,8 +462,8 @@ def _handle_fcm_error(self, error):
322
462
msg = error_dict .get ('message' )
323
463
if not msg :
324
464
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 )
465
+ error .resp . status , error .content )
466
+ return ApiCallError (code , msg , error )
327
467
328
468
def _handle_iid_error (self , error ):
329
469
"""Handles errors received from the Instance ID API."""
0 commit comments