1616
1717import  requests 
1818import  six 
19+ import  threading 
20+ 
21+ import  googleapiclient 
22+ from  googleapiclient .discovery  import  build 
1923
2024import  firebase_admin 
2125from  firebase_admin  import  _http_client 
3438    'ApiCallError' ,
3539    'Aps' ,
3640    'ApsAlert' ,
41+     'BatchResponse' ,
3742    'CriticalSound' ,
3843    'ErrorInfo' ,
3944    'Message' ,
45+     'MulticastMessage' ,
4046    'Notification' ,
47+     'SendResponse' ,
4148    'TopicManagementResponse' ,
4249    'WebpushConfig' ,
4350    'WebpushFcmOptions' ,
4451    'WebpushNotification' ,
4552    'WebpushNotificationAction' ,
4653
4754    'send' ,
55+     'send_all' ,
56+     'send_multicast' ,
4857    'subscribe_to_topic' ,
4958    'unsubscribe_from_topic' ,
5059]
5867ApsAlert  =  _messaging_utils .ApsAlert 
5968CriticalSound  =  _messaging_utils .CriticalSound 
6069Message  =  _messaging_utils .Message 
70+ MulticastMessage  =  _messaging_utils .MulticastMessage 
6171Notification  =  _messaging_utils .Notification 
6272WebpushConfig  =  _messaging_utils .WebpushConfig 
6373WebpushFcmOptions  =  _messaging_utils .WebpushFcmOptions 
@@ -88,6 +98,54 @@ def send(message, dry_run=False, app=None):
8898    """ 
8999    return  _get_messaging_service (app ).send (message , dry_run )
90100
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+ 
91149def  subscribe_to_topic (tokens , topic , app = None ):
92150    """Subscribes a list of registration tokens to an FCM topic. 
93151
@@ -192,10 +250,72 @@ def __init__(self, code, message, detail=None):
192250        self .detail  =  detail 
193251
194252
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+ 
195316class  _MessagingService (object ):
196317    """Service class that implements Firebase Cloud Messaging (FCM) functionality.""" 
197318
198-     FCM_URL  =  'https://fcm.googleapis.com/v1/projects/{0}/messages:send' 
199319    IID_URL  =  'https://iid.googleapis.com' 
200320    IID_HEADERS  =  {'access_token_auth' : 'true' }
201321    JSON_ENCODER  =  _messaging_utils .MessageEncoder ()
@@ -233,10 +353,14 @@ def __init__(self, app):
233353                'Project ID is required to access Cloud Messaging service. Either set the ' 
234354                'projectId option, or use service account credentials. Alternatively, set the ' 
235355                '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+         }
237362        self ._client  =  _http_client .JsonHttpClient (credential = app .credential .get_credential ())
238363        self ._timeout  =  app .options .get ('httpTimeout' )
239-         self ._client_version  =  'fire-admin-python/{0}' .format (firebase_admin .__version__ )
240364
241365    @classmethod  
242366    def  encode_message (cls , message ):
@@ -245,25 +369,37 @@ def encode_message(cls, message):
245369        return  cls .JSON_ENCODER .default (message )
246370
247371    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 )
251373        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 )
261378            else :
262379                msg  =  'Failed to call messaging API: {0}' .format (error )
263380                raise  ApiCallError (self .INTERNAL_ERROR , msg , error )
264381        else :
265382            return  resp ['name' ]
266383
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+ 
267403    def  make_topic_management_request (self , tokens , topic , operation ):
268404        """Invokes the IID service for topic management functionality.""" 
269405        if  isinstance (tokens , six .string_types ):
@@ -299,11 +435,21 @@ def make_topic_management_request(self, tokens, topic, operation):
299435        else :
300436            return  TopicManagementResponse (resp )
301437
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 ):
303448        """Handles errors received from the FCM API.""" 
304449        data  =  {}
305450        try :
306-             parsed_body  =  error .response .json ()
451+             import  json 
452+             parsed_body  =  json .loads (error .content )
307453            if  isinstance (parsed_body , dict ):
308454                data  =  parsed_body 
309455        except  ValueError :
@@ -322,8 +468,8 @@ def _handle_fcm_error(self, error):
322468        msg  =  error_dict .get ('message' )
323469        if  not  msg :
324470            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 )
327473
328474    def  _handle_iid_error (self , error ):
329475        """Handles errors received from the Instance ID API.""" 
0 commit comments