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,72 @@ 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
+
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
+
195
316
class _MessagingService (object ):
196
317
"""Service class that implements Firebase Cloud Messaging (FCM) functionality."""
197
318
198
- FCM_URL = 'https://fcm.googleapis.com/v1/projects/{0}/messages:send'
199
319
IID_URL = 'https://iid.googleapis.com'
200
320
IID_HEADERS = {'access_token_auth' : 'true' }
201
321
JSON_ENCODER = _messaging_utils .MessageEncoder ()
@@ -233,10 +353,14 @@ def __init__(self, app):
233
353
'Project ID is required to access Cloud Messaging service. Either set the '
234
354
'projectId option, or use service account credentials. Alternatively, set the '
235
355
'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
+ }
237
362
self ._client = _http_client .JsonHttpClient (credential = app .credential .get_credential ())
238
363
self ._timeout = app .options .get ('httpTimeout' )
239
- self ._client_version = 'fire-admin-python/{0}' .format (firebase_admin .__version__ )
240
364
241
365
@classmethod
242
366
def encode_message (cls , message ):
@@ -245,25 +369,37 @@ def encode_message(cls, message):
245
369
return cls .JSON_ENCODER .default (message )
246
370
247
371
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 )
251
373
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 )
261
378
else :
262
379
msg = 'Failed to call messaging API: {0}' .format (error )
263
380
raise ApiCallError (self .INTERNAL_ERROR , msg , error )
264
381
else :
265
382
return resp ['name' ]
266
383
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
+
267
403
def make_topic_management_request (self , tokens , topic , operation ):
268
404
"""Invokes the IID service for topic management functionality."""
269
405
if isinstance (tokens , six .string_types ):
@@ -299,11 +435,21 @@ def make_topic_management_request(self, tokens, topic, operation):
299
435
else :
300
436
return TopicManagementResponse (resp )
301
437
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 ):
303
448
"""Handles errors received from the FCM API."""
304
449
data = {}
305
450
try :
306
- parsed_body = error .response .json ()
451
+ import json
452
+ parsed_body = json .loads (error .content )
307
453
if isinstance (parsed_body , dict ):
308
454
data = parsed_body
309
455
except ValueError :
@@ -322,8 +468,8 @@ def _handle_fcm_error(self, error):
322
468
msg = error_dict .get ('message' )
323
469
if not msg :
324
470
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 )
327
473
328
474
def _handle_iid_error (self , error ):
329
475
"""Handles errors received from the Instance ID API."""
0 commit comments