def dispatchNotification(self, n): pushkeys = [device.pushkey for device in n.devices if device.app_id == self.name] # Resolve canonical IDs for all pushkeys pushkeys = [canonical_reg_id or reg_id for (reg_id, canonical_reg_id) in self.canonical_reg_id_store.get_canonical_ids(pushkeys).items()] data = GcmPushkin.build_data(n) # TODO: Implement collapse_key to queue only one message per room. request = gcmclient.JSONMessage(pushkeys, data) failed = [] logger.info("%r => %r", data, pushkeys); for retry in range(0, MAX_TRIES): response = self.gcm.send(request) for reg_id, msg_id in response.success.items(): logger.debug( "Successfully sent notification %s to %s as %s", n.id, reg_id, msg_id) for reg_id, canonical_reg_id in response.canonical.items(): self.canonical_reg_id_store.set_canonical_id(reg_id, canonical_reg_id) # gcm-client extracts the NotRegistered errors and puts the reg_ids in # the not_registered list. failed.extend(response.not_registered) logger.info("Reg IDs Not Registered: %r", response.not_registered); # Other errors live in the `failed` dict, but some of those mean # the reg_id is permanently dead too and we should remove it. for failed_reg_id,error_code in response.failed.items(): if error_code in PERMANENT_FAILURE_CODES: logger.info( "Reg ID %r has permanently failed with code %r", failed_reg_id, error_code ) failed.append(failed_reg_id) else: logger.info( "Reg ID %r has temporarily failed with code %r", failed_reg_id, error_code ) if not response.needs_retry(): break request = response.retry() gevent.wait(timeout=response.delay(retry)) else: # response.unavailable is a list of reg IDs that failed temporarily # but is undocumented in gcmclient's API logger.info("Gave up retrying reg IDs: %r", response.unavailable); return failed
def send_android_push_notification(user, data): # type: (UserProfile, Dict[str, Any]) -> None if not gcm: logging.error("Attempting to send a GCM push notification, but no API key was configured") return reg_ids = [device.token for device in PushDeviceToken.objects.filter(user=user, kind=PushDeviceToken.GCM)] msg = gcmclient.JSONMessage(reg_ids, data) res = gcm.send(msg) for reg_id, msg_id in res.success.items(): logging.info("GCM: Sent %s as %s" % (reg_id, msg_id)) # res.canonical will contain results when there are duplicate registrations for the same # device. The "canonical" registration is the latest registration made by the device. # Ref: http://developer.android.com/google/gcm/adv.html#canonical for reg_id, new_reg_id in res.canonical.items(): if reg_id == new_reg_id: # I'm not sure if this should happen. In any case, not really actionable. logging.warning("GCM: Got canonical ref but it already matches our ID %s!" % (reg_id,)) elif not PushDeviceToken.objects.filter(token=new_reg_id, kind=PushDeviceToken.GCM).count(): # This case shouldn't happen; any time we get a canonical ref it should have been # previously registered in our system. # # That said, recovery is easy: just update the current PDT object to use the new ID. logging.warning( "GCM: Got canonical ref %s replacing %s but new ID not registered! Updating." % (new_reg_id, reg_id)) PushDeviceToken.objects.filter( token=reg_id, kind=PushDeviceToken.GCM).update(token=new_reg_id) else: # Since we know the new ID is registered in our system we can just drop the old one. logging.info("GCM: Got canonical ref %s, dropping %s" % (new_reg_id, reg_id)) PushDeviceToken.objects.filter(token=reg_id, kind=PushDeviceToken.GCM).delete() for reg_id in res.not_registered: logging.info("GCM: Removing %s" % (reg_id,)) device = PushDeviceToken.objects.get(token=reg_id, kind=PushDeviceToken.GCM) device.delete() for reg_id, err_code in res.failed.items(): logging.warning("GCM: Delivery to %s failed: %s" % (reg_id, err_code)) if res.needs_retry(): # TODO logging.warning("GCM: delivery needs a retry but ignoring")
def _route(self, notification, router_data): """Blocking GCM call to route the notification""" data = {"chid": notification.channel_id} # Payload data is optional. The endpoint handler validates that the # correct encryption headers are included with the data. if notification.data: mdata = self.config.get('max_data', 4096) if len(notification.data) > mdata: raise self._error("This message is intended for a " + "constrained device and is limited " + "to 3070 bytes. Converted buffer too " + "long by %d bytes" % (len(notification.data) - mdata), 413, errno=104) data['body'] = notification.data data['con'] = notification.headers['content-encoding'] data['enc'] = notification.headers['encryption'] if 'crypto-key' in notification.headers: data['cryptokey'] = notification.headers['crypto-key'] elif 'encryption-key' in notification.headers: data['enckey'] = notification.headers['encryption-key'] # registration_ids are the GCM instance tokens (specified during # registration. router_ttl = notification.ttl or 0 payload = gcmclient.JSONMessage( registration_ids=[router_data.get("token")], collapse_key=self.collapseKey, time_to_live=max(self.min_ttl, router_ttl), dry_run=self.dryRun or ("dryrun" in router_data), data=data, ) creds = router_data.get("creds", {"senderID": "missing id"}) try: self.gcm.api_key = creds["auth"] result = self.gcm.send(payload) except KeyError: raise self._error( "Server error, missing bridge credentials " + "for %s" % creds.get("senderID"), 500) except gcmclient.GCMAuthenticationError, e: raise self._error("Authentication Error: %s" % e, 500)
def _route(self, notification, router_data): """Blocking GCM call to route the notification""" payload = gcmclient.JSONMessage( registration_ids=[router_data["token"]], collapse_key=self.collapseKey, time_to_live=self.ttl, dry_run=self.dryRun, data={"Msg": notification.data, "Chid": notification.channel_id, "Ver": notification.version} ) creds = router_data.get("creds", {"senderID": "missing id"}) try: self.gcm.api_key = creds["auth"] result = self.gcm.send(payload) except KeyError: self._error("Server error, missing bridge credentials for %s" % creds.get("senderID"), 500) except gcmclient.GCMAuthenticationError, e: self._error("Authentication Error: %s" % e, 500)
def _route(self, notification, uaid_data): """Blocking GCM call to route the notification""" router_data = uaid_data["router_data"] # THIS MUST MATCH THE CHANNELID GENERATED BY THE REGISTRATION SERVICE # Currently this value is in hex form. data = {"chid": notification.channel_id.hex} # Payload data is optional. The endpoint handler validates that the # correct encryption headers are included with the data. if notification.data: mdata = self.config.get('max_data', 4096) if notification.data_length > mdata: raise self._error("This message is intended for a " + "constrained device and is limited " + "to 3070 bytes. Converted buffer too " + "long by %d bytes" % (notification.data_length - mdata), 413, errno=104, log_exception=False) data['body'] = notification.data data['con'] = notification.headers['encoding'] if 'encryption' in notification.headers: data['enc'] = notification.headers.get('encryption') if 'crypto_key' in notification.headers: data['cryptokey'] = notification.headers['crypto_key'] elif 'encryption_key' in notification.headers: data['enckey'] = notification.headers['encryption_key'] # registration_ids are the GCM instance tokens (specified during # registration. router_ttl = min(self.MAX_TTL, max(notification.ttl or 0, self.min_ttl)) payload = gcmclient.JSONMessage( registration_ids=[router_data.get("token")], collapse_key=self.collapseKey, time_to_live=router_ttl, dry_run=self.dryRun or ("dryrun" in router_data), data=data, ) try: gcm = self.gcm[router_data['creds']['senderID']] result = gcm.send(payload) except KeyError: self.log.critical("Missing GCM bridge credentials") raise RouterException("Server error", status_code=500) except gcmclient.GCMAuthenticationError as e: self.log.error("GCM Authentication Error: %s" % e) raise RouterException("Server error", status_code=500) except ConnectionError as e: self.log.warn("GCM Unavailable: %s" % e) self.metrics.increment("notification.bridge.error", tags=make_tags( self._base_tags, reason="connection_unavailable")) raise RouterException("Server error", status_code=502, log_exception=False) except Exception as e: self.log.error("Unhandled exception in GCM Routing: %s" % e) raise RouterException("Server error", status_code=500) return self._process_reply(result, uaid_data, ttl=router_ttl, notification=notification)