1414
1515"""Firebase Cloud Messaging module.""" 
1616
17+ import  json 
1718import  requests 
1819import  six 
1920
21+ import  googleapiclient 
22+ from  googleapiclient  import  http 
23+ from  googleapiclient  import  _auth 
24+ 
2025import  firebase_admin 
2126from  firebase_admin  import  _http_client 
2227from  firebase_admin  import  _messaging_utils 
3439    'ApiCallError' ,
3540    'Aps' ,
3641    'ApsAlert' ,
42+     'BatchResponse' ,
3743    'CriticalSound' ,
3844    'ErrorInfo' ,
3945    'Message' ,
46+     'MulticastMessage' ,
4047    'Notification' ,
48+     'SendResponse' ,
4149    'TopicManagementResponse' ,
4250    'WebpushConfig' ,
4351    'WebpushFcmOptions' ,
4452    'WebpushNotification' ,
4553    'WebpushNotificationAction' ,
4654
4755    'send' ,
56+     'send_all' ,
57+     'send_multicast' ,
4858    'subscribe_to_topic' ,
4959    'unsubscribe_from_topic' ,
5060]
5868ApsAlert  =  _messaging_utils .ApsAlert 
5969CriticalSound  =  _messaging_utils .CriticalSound 
6070Message  =  _messaging_utils .Message 
71+ MulticastMessage  =  _messaging_utils .MulticastMessage 
6172Notification  =  _messaging_utils .Notification 
6273WebpushConfig  =  _messaging_utils .WebpushConfig 
6374WebpushFcmOptions  =  _messaging_utils .WebpushFcmOptions 
@@ -88,6 +99,56 @@ def send(message, dry_run=False, app=None):
8899    """ 
89100    return  _get_messaging_service (app ).send (message , dry_run )
90101
102+ def  send_all (messages , dry_run = False , app = None ):
103+     """Sends the given list of messages via Firebase Cloud Messaging as a single batch. 
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 all 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+         multicast_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+     if  not  isinstance (multicast_message , MulticastMessage ):
141+         raise  ValueError ('Message must be an instance of messaging.MulticastMessage class.' )
142+     messages  =  [Message (
143+         data = multicast_message .data ,
144+         notification = multicast_message .notification ,
145+         android = multicast_message .android ,
146+         webpush = multicast_message .webpush ,
147+         apns = multicast_message .apns ,
148+         token = token 
149+     ) for  token  in  multicast_message .tokens ]
150+     return  _get_messaging_service (app ).send_all (messages , dry_run )
151+ 
91152def  subscribe_to_topic (tokens , topic , app = None ):
92153    """Subscribes a list of registration tokens to an FCM topic. 
93154
@@ -192,10 +253,57 @@ def __init__(self, code, message, detail=None):
192253        self .detail  =  detail 
193254
194255
256+ class  BatchResponse (object ):
257+     """The response received from a batch request to the FCM API.""" 
258+ 
259+     def  __init__ (self , responses ):
260+         self ._responses  =  responses 
261+         self ._success_count  =  len ([resp  for  resp  in  responses  if  resp .success ])
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+     """The response received from an individual batched request to the FCM API.""" 
279+ 
280+     def  __init__ (self , resp , exception ):
281+         self ._exception  =  exception 
282+         self ._message_id  =  None 
283+         if  resp :
284+             self ._message_id  =  resp .get ('name' , None )
285+ 
286+     @property  
287+     def  message_id (self ):
288+         """A message ID string that uniquely identifies the sent the message.""" 
289+         return  self ._message_id 
290+ 
291+     @property  
292+     def  success (self ):
293+         """A boolean indicating if the request was successful.""" 
294+         return  self ._message_id  is  not None  and  not  self ._exception 
295+ 
296+     @property  
297+     def  exception (self ):
298+         """A ApiCallError if an error occurs while sending the message to FCM service.""" 
299+         return  self ._exception 
300+ 
301+ 
195302class  _MessagingService (object ):
196303    """Service class that implements Firebase Cloud Messaging (FCM) functionality.""" 
197304
198305    FCM_URL  =  'https://fcm.googleapis.com/v1/projects/{0}/messages:send' 
306+     FCM_BATCH_URL  =  'https://fcm.googleapis.com/batch' 
199307    IID_URL  =  'https://iid.googleapis.com' 
200308    IID_HEADERS  =  {'access_token_auth' : 'true' }
201309    JSON_ENCODER  =  _messaging_utils .MessageEncoder ()
@@ -234,9 +342,13 @@ def __init__(self, app):
234342                'projectId option, or use service account credentials. Alternatively, set the ' 
235343                'GOOGLE_CLOUD_PROJECT environment variable.' )
236344        self ._fcm_url  =  _MessagingService .FCM_URL .format (project_id )
345+         self ._fcm_headers  =  {
346+             'X-GOOG-API-FORMAT-VERSION' : '2' ,
347+             'X-FIREBASE-CLIENT' : 'fire-admin-python/{0}' .format (firebase_admin .__version__ ),
348+         }
237349        self ._client  =  _http_client .JsonHttpClient (credential = app .credential .get_credential ())
238350        self ._timeout  =  app .options .get ('httpTimeout' )
239-         self ._client_version  =  'fire-admin-python/{0}' . format ( firebase_admin . __version__ )
351+         self ._transport  =  _auth . authorized_http ( app . credential . get_credential () )
240352
241353    @classmethod  
242354    def  encode_message (cls , message ):
@@ -245,16 +357,15 @@ def encode_message(cls, message):
245357        return  cls .JSON_ENCODER .default (message )
246358
247359    def  send (self , message , dry_run = False ):
248-         data  =  {'message' : _MessagingService .encode_message (message )}
249-         if  dry_run :
250-             data ['validate_only' ] =  True 
360+         data  =  self ._message_data (message , dry_run )
251361        try :
252-             headers  =  {
253-                 'X-GOOG-API-FORMAT-VERSION' : '2' ,
254-                 'X-FIREBASE-CLIENT' : self ._client_version ,
255-             }
256362            resp  =  self ._client .body (
257-                 'post' , url = self ._fcm_url , headers = headers , json = data , timeout = self ._timeout )
363+                 'post' ,
364+                 url = self ._fcm_url ,
365+                 headers = self ._fcm_headers ,
366+                 json = data ,
367+                 timeout = self ._timeout 
368+             )
258369        except  requests .exceptions .RequestException  as  error :
259370            if  error .response  is  not None :
260371                self ._handle_fcm_error (error )
@@ -264,6 +375,42 @@ def send(self, message, dry_run=False):
264375        else :
265376            return  resp ['name' ]
266377
378+     def  send_all (self , messages , dry_run = False ):
379+         """Sends the given messages to FCM via the batch API.""" 
380+         if  not  isinstance (messages , list ):
381+             raise  ValueError ('Messages must be an list of messaging.Message instances.' )
382+         if  len (messages ) >  100 :
383+             raise  ValueError ('send_all messages must not contain more than 100 messages.' )
384+ 
385+         responses  =  []
386+ 
387+         def  batch_callback (_ , response , error ):
388+             exception  =  None 
389+             if  error :
390+                 exception  =  self ._parse_batch_error (error )
391+             send_response  =  SendResponse (response , exception )
392+             responses .append (send_response )
393+ 
394+         batch  =  http .BatchHttpRequest (batch_callback , _MessagingService .FCM_BATCH_URL )
395+         for  message  in  messages :
396+             body  =  json .dumps (self ._message_data (message , dry_run ))
397+             req  =  http .HttpRequest (
398+                 http = self ._transport ,
399+                 postproc = self ._postproc ,
400+                 uri = self ._fcm_url ,
401+                 method = 'POST' ,
402+                 body = body ,
403+                 headers = self ._fcm_headers 
404+             )
405+             batch .add (req )
406+ 
407+         try :
408+             batch .execute ()
409+         except  googleapiclient .http .HttpError  as  error :
410+             raise  self ._parse_batch_error (error )
411+         else :
412+             return  BatchResponse (responses )
413+ 
267414    def  make_topic_management_request (self , tokens , topic , operation ):
268415        """Invokes the IID service for topic management functionality.""" 
269416        if  isinstance (tokens , six .string_types ):
@@ -299,6 +446,17 @@ def make_topic_management_request(self, tokens, topic, operation):
299446        else :
300447            return  TopicManagementResponse (resp )
301448
449+     def  _message_data (self , message , dry_run ):
450+         data  =  {'message' : _MessagingService .encode_message (message )}
451+         if  dry_run :
452+             data ['validate_only' ] =  True 
453+         return  data 
454+ 
455+     def  _postproc (self , _ , body ):
456+         """Handle response from batch API request.""" 
457+         # This only gets called for 2xx responses. 
458+         return  json .loads (body .decode ())
459+ 
302460    def  _handle_fcm_error (self , error ):
303461        """Handles errors received from the FCM API.""" 
304462        data  =  {}
@@ -309,20 +467,8 @@ def _handle_fcm_error(self, error):
309467        except  ValueError :
310468            pass 
311469
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 ())
470+         code , msg  =  _MessagingService ._parse_fcm_error (
471+             data , error .response .content , error .response .status_code )
326472        raise  ApiCallError (code , msg , error )
327473
328474    def  _handle_iid_error (self , error ):
@@ -342,3 +488,39 @@ def _handle_iid_error(self, error):
342488            msg  =  'Unexpected HTTP response with status: {0}; body: {1}' .format (
343489                error .response .status_code , error .response .content .decode ())
344490        raise  ApiCallError (code , msg , error )
491+ 
492+     def  _parse_batch_error (self , error ):
493+         """Parses a googleapiclient.http.HttpError content in to an ApiCallError.""" 
494+         if  error .content  is  None :
495+             msg  =  'Failed to call messaging API: {0}' .format (error )
496+             return  ApiCallError (self .INTERNAL_ERROR , msg , error )
497+ 
498+         data  =  {}
499+         try :
500+             parsed_body  =  json .loads (error .content .decode ())
501+             if  isinstance (parsed_body , dict ):
502+                 data  =  parsed_body 
503+         except  ValueError :
504+             pass 
505+ 
506+         code , msg  =  _MessagingService ._parse_fcm_error (data , error .content , error .resp .status )
507+         return  ApiCallError (code , msg , error )
508+ 
509+     @classmethod  
510+     def  _parse_fcm_error (cls , data , content , status_code ):
511+         """Parses an error response from the FCM API to a ApiCallError.""" 
512+         error_dict  =  data .get ('error' , {})
513+         server_code  =  None 
514+         for  detail  in  error_dict .get ('details' , []):
515+             if  detail .get ('@type' ) ==  'type.googleapis.com/google.firebase.fcm.v1.FcmError' :
516+                 server_code  =  detail .get ('errorCode' )
517+                 break 
518+         if  not  server_code :
519+             server_code  =  error_dict .get ('status' )
520+         code  =  _MessagingService .FCM_ERROR_CODES .get (server_code , _MessagingService .UNKNOWN_ERROR )
521+ 
522+         msg  =  error_dict .get ('message' )
523+         if  not  msg :
524+             msg  =  'Unexpected HTTP response with status: {0}; body: {1}' .format (
525+                 status_code , content .decode ())
526+         return  code , msg 
0 commit comments