Beispiel #1
0
    def __init__(self, name, sygnal, config):
        super().__init__(name, sygnal, config)

        nonunderstood = set(self.cfg.keys()).difference(
            self.UNDERSTOOD_CONFIG_FIELDS)
        if len(nonunderstood) > 0:
            logger.warning(
                "The following configuration fields are not understood: %s",
                nonunderstood,
            )

        platform = self.get_config("platform")
        if not platform or platform == "production" or platform == "prod":
            self.use_sandbox = False
        elif platform == "sandbox":
            self.use_sandbox = True
        else:
            raise PushkinSetupException(f"Invalid platform: {platform}")

        certfile = self.get_config("certfile")
        keyfile = self.get_config("keyfile")
        if not certfile and not keyfile:
            raise PushkinSetupException(
                "You must provide a path to an APNs certificate, or an APNs token."
            )

        if certfile:
            if not os.path.exists(certfile):
                raise PushkinSetupException(
                    f"The APNs certificate '{certfile}' does not exist.")
        else:
            # keyfile
            if not os.path.exists(keyfile):
                raise PushkinSetupException(
                    f"The APNs key file '{keyfile}' does not exist.")
            if not self.get_config("key_id"):
                raise PushkinSetupException("You must supply key_id.")
            if not self.get_config("team_id"):
                raise PushkinSetupException("You must supply team_id.")
            if not self.get_config("topic"):
                raise PushkinSetupException("You must supply topic.")

        if self.get_config("certfile") is not None:
            self.apns_client = APNs(client_cert=self.get_config("certfile"),
                                    use_sandbox=self.use_sandbox)
        else:
            self.apns_client = APNs(
                key=self.get_config("keyfile"),
                key_id=self.get_config("key_id"),
                team_id=self.get_config("team_id"),
                topic=self.get_config("topic"),
                use_sandbox=self.use_sandbox,
            )

        # without this, aioapns will retry every second forever.
        self.apns_client.pool.max_connection_attempts = 3
async def send_push(token, title, subtitle, priority, additionalInfo):
    apns_key_client = APNs(
        key="My_APNS_Auth_Key.p8",
        key_id="FromApplePortal<p8 key id>",
        team_id="FromApplePortal<team id>",
        topic="com.your.bundleId",
        use_sandbox=True,
    )
    request = NotificationRequest(
        device_token=token,
        message={
            "aps": {
                "sound": "",
                "content-available": 1,
                "alert": {
                    "title": title,
                    "body": subtitle
                },
            },
            "anyAdditionalData": additionalInfo,
        },
        priority=priority,
        push_type=PushType.BACKGROUND if priority == "5" else PushType.ALERT,
    )

    await apns_key_client.send_notification(request)
    print("Fired notification on device {} with priority {}".format(
        token, priority))
Beispiel #3
0
class APNsV3(AbstractAPNs):
    def __init__(
        self,
        p8_filename: str = '',
        p8_key_id: str = '',
        p8_team_id: str = '',
        p8_topic: str = '',
        pem_client_cert: str = '',
        cert_type: str = '',
    ):
        args = {
            'pem': {
                'client_cert': pem_client_cert
            },
            'p8': {
                'key': p8_filename,
                'key_id': p8_key_id,
                'team_id': p8_team_id,
                'topic': p8_topic,
            }
        }[cert_type]
        self.apns = APNs(
            use_sandbox=False,
            **args
        )

    async def send_data(
        self,
        targets: List[str],
        data: dict
    ) -> Tuple[int, int]:
        collapse_key = str(uuid4())
        results = await asyncio.gather(*[
            self.apns.send_notification(
                NotificationRequest(
                    device_token=target,
                    message=data,
                    collapse_key=collapse_key,
                    push_type=PushType.ALERT,
                )
            )
            for target in targets
        ])

        success = 0
        failed = len(targets)
        for response in results:
            if response.is_successful:
                success += 1
        failed -= success

        return success, failed
Beispiel #4
0
async def run():
    apns_cert_client = APNs(
        client_cert='/path/to/apns-cert.pem',  # 令牌
        use_sandbox=False,
    )
    apns_key_client = APNs(
        key='/path/to/apns-key.p8',  # 私钥
        key_id='<KEY_ID>',  # 10个字符的密钥标识符(kid)密钥
        team_id='<TEAM_ID>',  # 您的10个字符的团队ID
        topic=
        'com.okmusician.ios',  # 远程通知的主题,通常是应用程序的软件包ID。您在开发人员帐户中创建的证书必须包含此主题的功能。
        use_sandbox=False,
    )
    request = NotificationRequest(
        device_token='<DEVICE_TOKEN>',  # user token
        message={"aps": {
            "alert": "Hello from APNs",
            "badge": "1",
        }},
        notification_id=str(uuid4()),  # optional
        time_to_live=3,  # optional
    )
    await apns_cert_client.send_notification(request)
    await apns_key_client.send_notification(request)
Beispiel #5
0
 def __init__(
     self,
     p8_filename: str = '',
     p8_key_id: str = '',
     p8_team_id: str = '',
     p8_topic: str = '',
     pem_client_cert: str = '',
     cert_type: str = '',
 ):
     args = {
         'pem': {
             'client_cert': pem_client_cert
         },
         'p8': {
             'key': p8_filename,
             'key_id': p8_key_id,
             'team_id': p8_team_id,
             'topic': p8_topic,
         }
     }[cert_type]
     self.apns = APNs(
         use_sandbox=False,
         **args
     )
Beispiel #6
0
 def create_apns(self) -> APNs:
     apns_config = config.push_worker.apns
     args = {
         'pem': {
             'client_cert': apns_config.pem_cert.file_name
         },
         'p8': {
             'key': apns_config.p8_cert.file_name,
             'key_id': apns_config.p8_cert.key_id,
             'team_id': apns_config.p8_cert.team_id,
             'topic': apns_config.p8_cert.topic,
         }
     }[apns_config.cert_type]
     print(args)
     return APNs(
         use_sandbox=False,
         **args
     )
Beispiel #7
0
async def send_push_notification(device_token, badge, alert={}):
    apns_key_client = APNs(
        key=os.path.join(settings.BASE_DIR, 'logged_in_assets',
                         'AuthKey_X44S38MRBS.p8'),
        key_id=settings.IOS_KEY_ID,  # Key ID
        team_id=settings.IOS_TEAM_ID,  # Team ID
        topic=settings.IOS_APP_BUNDLE_ID,  # Bundle ID
        use_sandbox=True,
    )
    request = NotificationRequest(
        device_token=device_token,
        message={
            "aps": {
                "alert": {
                    "title": alert["title"],
                    "body": alert["body"],
                    "selectedIndex": alert["selected_index"]
                },
                "badge": badge,
            }
        },
    )
    await apns_key_client.send_notification(request)
Beispiel #8
0
if __name__ == "__main__":
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    setup_logger("INFO")

    client_cert = "apns-production-cert.pem"
    device_token = "<DEVICE_TOKEN>"
    message = {
        "aps": {
            "alert": "Hello from APNs Tester.",
            "badge": "1",
            "sound": "default",
        }
    }

    apns = APNs(client_cert)

    async def send_request():
        request = NotificationRequest(device_token=device_token,
                                      message=message)
        await apns.send_notification(request)

    async def main():
        send_requests = [send_request() for _ in range(1000)]
        import time

        t = time.time()
        await asyncio.wait(send_requests)
        print("Done: %s" % (time.time() - t))
        print()
Beispiel #9
0
class ApnsPushkin(ConcurrencyLimitedPushkin):
    """
    Relays notifications to the Apple Push Notification Service.
    """

    # Errors for which the token should be rejected and not reused
    # See https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/handling_notification_responses_from_apns  # noqa: E501
    # for the full list of possible errors.
    TOKEN_ERRORS = {
        # A client has uploaded an invalid token.
        (400, "BadDeviceToken"),
        # `DeviceTokenNotForTopic` may be due to a token for a different app or an
        # incorrect topic in the Sygnal configuration. In the event of a
        # misconfiguration, clients will need to reupload their tokens to their
        # homeserver.
        (400, "DeviceTokenNotForTopic"),
        (400, "TopicDisallowed"),
        # The token is no longer valid, probably because the app has been uninstalled.
        (410, "Unregistered"),
    }

    MAX_TRIES = 3
    RETRY_DELAY_BASE = 10

    MAX_FIELD_LENGTH = 1024
    MAX_JSON_BODY_SIZE = 4096

    UNDERSTOOD_CONFIG_FIELDS = {
        "type",
        "platform",
        "certfile",
        "team_id",
        "key_id",
        "keyfile",
        "topic",
    } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS

    def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str,
                                                                 Any]) -> None:
        super().__init__(name, sygnal, config)

        nonunderstood = set(self.cfg.keys()).difference(
            self.UNDERSTOOD_CONFIG_FIELDS)
        if len(nonunderstood) > 0:
            logger.warning(
                "The following configuration fields are not understood: %s",
                nonunderstood,
            )

        platform = self.get_config("platform", str)
        if not platform or platform == "production" or platform == "prod":
            self.use_sandbox = False
        elif platform == "sandbox":
            self.use_sandbox = True
        else:
            raise PushkinSetupException(f"Invalid platform: {platform}")

        certfile = self.get_config("certfile", str)
        keyfile = self.get_config("keyfile", str)
        if not certfile and not keyfile:
            raise PushkinSetupException(
                "You must provide a path to an APNs certificate, or an APNs token."
            )

        if certfile:
            if not os.path.exists(certfile):
                raise PushkinSetupException(
                    f"The APNs certificate '{certfile}' does not exist.")
        elif keyfile:
            # keyfile
            if not os.path.exists(keyfile):
                raise PushkinSetupException(
                    f"The APNs key file '{keyfile}' does not exist.")
            if not self.get_config("key_id", str):
                raise PushkinSetupException("You must supply key_id.")
            if not self.get_config("team_id", str):
                raise PushkinSetupException("You must supply team_id.")
            if not self.get_config("topic", str):
                raise PushkinSetupException("You must supply topic.")

        # use the Sygnal global proxy configuration
        proxy_url_str = sygnal.config.get("proxy")

        loop = asyncio.get_event_loop()
        if proxy_url_str:
            # this overrides the create_connection method to use a HTTP proxy
            loop = ProxyingEventLoopWrapper(loop,
                                            proxy_url_str)  # type: ignore

        if certfile is not None:
            # max_connection_attempts is actually the maximum number of
            # additional connection attempts, so =0 means try once only
            # (we will retry at a higher level so not worth doing more here)
            self.apns_client = APNs(
                client_cert=certfile,
                use_sandbox=self.use_sandbox,
                max_connection_attempts=0,
                loop=loop,
            )

            self._report_certificate_expiration(certfile)
        else:
            # max_connection_attempts is actually the maximum number of
            # additional connection attempts, so =0 means try once only
            # (we will retry at a higher level so not worth doing more here)
            self.apns_client = APNs(
                key=self.get_config("keyfile", str),
                key_id=self.get_config("key_id", str),
                team_id=self.get_config("team_id", str),
                topic=self.get_config("topic", str),
                use_sandbox=self.use_sandbox,
                max_connection_attempts=0,
                loop=loop,
            )

        # without this, aioapns will retry every second forever.
        self.apns_client.pool.max_connection_attempts = 3

    def _report_certificate_expiration(self, certfile: str) -> None:
        """Export the epoch time that the certificate expires as a metric."""
        with open(certfile, "rb") as f:
            cert_bytes = f.read()

        cert = load_pem_x509_certificate(cert_bytes, default_backend())
        # Report the expiration time as seconds since the epoch (in UTC time).
        CERTIFICATE_EXPIRATION_GAUGE.labels(pushkin=self.name).set(
            cert.not_valid_after.replace(tzinfo=timezone.utc).timestamp())

    async def _dispatch_request(
        self,
        log: NotificationLoggerAdapter,
        span: Span,
        device: Device,
        shaved_payload: Dict[str, Any],
        prio: int,
    ) -> List[str]:
        """
        Actually attempts to dispatch the notification once.
        """

        # this is no good: APNs expects ID to be in their format
        # so we can't just derive a
        # notif_id = context.request_id + f"-{n.devices.index(device)}"

        notif_id = str(uuid4())

        log.info(f"Sending as APNs-ID {notif_id}")
        span.set_tag("apns_id", notif_id)

        device_token = base64.b64decode(device.pushkey).hex()

        request = NotificationRequest(
            device_token=device_token,
            message=shaved_payload,
            priority=prio,
            notification_id=notif_id,
        )

        try:

            with ACTIVE_REQUESTS_GAUGE.track_inprogress():
                with SEND_TIME_HISTOGRAM.time():
                    response = await self._send_notification(request)
        except aioapns.ConnectionError:
            raise TemporaryNotificationDispatchException(
                "aioapns Connection Failure")

        code = int(response.status)

        span.set_tag(tags.HTTP_STATUS_CODE, code)

        RESPONSE_STATUS_CODES_COUNTER.labels(pushkin=self.name,
                                             code=code).inc()

        if response.is_successful:
            return []
        else:
            # .description corresponds to the 'reason' response field
            span.set_tag("apns_reason", response.description)
            if (code, response.description) in self.TOKEN_ERRORS:
                log.info(
                    "APNs token %s for pushkin %s was rejected: %d %s",
                    device_token,
                    self.name,
                    code,
                    response.description,
                )
                return [device.pushkey]
            else:
                if 500 <= code < 600:
                    raise TemporaryNotificationDispatchException(
                        f"{response.status} {response.description}")
                else:
                    raise NotificationDispatchException(
                        f"{response.status} {response.description}")

    async def _dispatch_notification_unlimited(
            self, n: Notification, device: Device,
            context: NotificationContext) -> List[str]:
        log = NotificationLoggerAdapter(logger,
                                        {"request_id": context.request_id})

        # The pushkey is kind of secret because you can use it to send push
        # to someone.
        # span_tags = {"pushkey": device.pushkey}
        span_tags: Dict[str, int] = {}

        with self.sygnal.tracer.start_span(
                "apns_dispatch", tags=span_tags,
                child_of=context.opentracing_span) as span_parent:
            # Before we build the payload, check that the default_payload is not
            # malformed and reject the pushkey if it is

            default_payload = {}

            if device.data:
                default_payload = device.data.get("default_payload", {})
                if not isinstance(default_payload, dict):
                    log.error(
                        "default_payload is malformed, this value must be a dict."
                    )
                    return [device.pushkey]

            if n.event_id and not n.type:
                payload: Optional[Dict[str,
                                       Any]] = self._get_payload_event_id_only(
                                           n, default_payload)
            else:
                payload = self._get_payload_full(n, device, log)

            if payload is None:
                # Nothing to do
                span_parent.log_kv({logs.EVENT: "apns_no_payload"})
                return []
            prio = 10
            if n.prio == "low":
                prio = 5

            shaved_payload = apnstruncate.truncate(
                payload, max_length=self.MAX_JSON_BODY_SIZE)

            for retry_number in range(self.MAX_TRIES):
                try:
                    span_tags = {"retry_num": retry_number}

                    with self.sygnal.tracer.start_span(
                            "apns_dispatch_try",
                            tags=span_tags,
                            child_of=span_parent) as span:
                        assert shaved_payload is not None
                        return await self._dispatch_request(
                            log, span, device, shaved_payload, prio)
                except TemporaryNotificationDispatchException as exc:
                    retry_delay = self.RETRY_DELAY_BASE * (2**retry_number)
                    if exc.custom_retry_delay is not None:
                        retry_delay = exc.custom_retry_delay

                    log.warning(
                        "Temporary failure, will retry in %d seconds",
                        retry_delay,
                        exc_info=True,
                    )

                    span_parent.log_kv({
                        "event": "temporary_fail",
                        "retrying_in": retry_delay
                    })
                    if retry_number < self.MAX_TRIES - 1:
                        await twisted_sleep(
                            retry_delay, twisted_reactor=self.sygnal.reactor)

            raise NotificationDispatchException("Retried too many times.")

    def _get_payload_event_id_only(
            self, n: Notification,
            default_payload: Dict[str, Any]) -> Dict[str, Any]:
        """
        Constructs a payload for a notification where we know only the event ID.
        Args:
            n: The notification to construct a payload for.
            device: Device information to which the constructed payload
            will be sent.

        Returns:
            The APNs payload as a nested dicts.
        """
        payload = {}

        payload.update(default_payload)

        if n.room_id:
            payload["room_id"] = n.room_id
        if n.event_id:
            payload["event_id"] = n.event_id

        if n.counts.unread is not None:
            payload["unread_count"] = n.counts.unread
        if n.counts.missed_calls is not None:
            payload["missed_calls"] = n.counts.missed_calls

        return payload

    def _get_payload_full(
            self, n: Notification, device: Device,
            log: NotificationLoggerAdapter) -> Optional[Dict[str, Any]]:
        """
        Constructs a payload for a notification.
        Args:
            n: The notification to construct a payload for.
            device: Device information to which the constructed payload
            will be sent.
            log: A logger.

        Returns:
            The APNs payload as nested dicts.
        """
        if not n.sender and not n.sender_display_name:
            from_display = " "
        elif n.sender_display_name is not None:
            from_display = n.sender_display_name
        elif n.sender is not None:
            from_display = n.sender
        from_display = from_display[0:self.MAX_FIELD_LENGTH]

        loc_key = None
        loc_args = None
        if n.type == "m.room.message" or n.type == "m.room.encrypted":
            room_display = None
            if n.room_name:
                room_display = n.room_name[0:self.MAX_FIELD_LENGTH]
            elif n.room_alias:
                room_display = n.room_alias[0:self.MAX_FIELD_LENGTH]

            content_display = None
            action_display = None
            is_image = False
            if n.content and "msgtype" in n.content and "body" in n.content:
                if "body" in n.content:
                    if n.content["msgtype"] == "m.text":
                        content_display = n.content["body"]
                    elif n.content["msgtype"] == "m.emote":
                        action_display = n.content["body"]
                    else:
                        # fallback: 'body' should always be user-visible text
                        # in an m.room.message
                        content_display = n.content["body"]
                if n.content["msgtype"] == "m.image":
                    is_image = True

            if room_display:
                if is_image:
                    loc_key = "IMAGE_FROM_USER_IN_ROOM"
                    loc_args = [from_display, content_display, room_display]
                elif content_display:
                    loc_key = "MSG_FROM_USER_IN_ROOM_WITH_CONTENT"
                    loc_args = [from_display, room_display, content_display]
                elif action_display:
                    loc_key = "ACTION_FROM_USER_IN_ROOM"
                    loc_args = [room_display, from_display, action_display]
                else:
                    loc_key = "MSG_FROM_USER_IN_ROOM"
                    loc_args = [from_display, room_display]
            else:
                if is_image:
                    loc_key = "IMAGE_FROM_USER"
                    loc_args = [from_display, content_display]
                elif content_display:
                    loc_key = "MSG_FROM_USER_WITH_CONTENT"
                    loc_args = [from_display, content_display]
                elif action_display:
                    loc_key = "ACTION_FROM_USER"
                    loc_args = [from_display, action_display]
                else:
                    loc_key = "MSG_FROM_USER"
                    loc_args = [from_display]

        elif n.type == "m.call.invite":
            is_video_call = False

            # This detection works only for hs that uses WebRTC for calls
            if n.content and "offer" in n.content and "sdp" in n.content[
                    "offer"]:
                sdp = n.content["offer"]["sdp"]
                if "m=video" in sdp:
                    is_video_call = True

            if is_video_call:
                loc_key = "VIDEO_CALL_FROM_USER"
            else:
                loc_key = "VOICE_CALL_FROM_USER"

            loc_args = [from_display]
        elif n.type == "m.room.member":
            if n.user_is_target:
                if n.membership == "invite":
                    if n.room_name:
                        loc_key = "USER_INVITE_TO_NAMED_ROOM"
                        loc_args = [
                            from_display,
                            n.room_name[0:self.MAX_FIELD_LENGTH],
                        ]
                    elif n.room_alias:
                        loc_key = "USER_INVITE_TO_NAMED_ROOM"
                        loc_args = [
                            from_display,
                            n.room_alias[0:self.MAX_FIELD_LENGTH],
                        ]
                    else:
                        loc_key = "USER_INVITE_TO_CHAT"
                        loc_args = [from_display]
        elif n.type:
            # A type of message was received that we don't know about
            # but it was important enough for a push to have got to us
            loc_key = "MSG_FROM_USER"
            loc_args = [from_display]

        badge = None
        if n.counts.unread is not None:
            badge = n.counts.unread
        if n.counts.missed_calls is not None:
            if badge is None:
                badge = 0
            badge += n.counts.missed_calls

        if loc_key is None and badge is None:
            log.info("Nothing to do for alert of type %s", n.type)
            return None

        payload = {}

        if n.type and device.data:
            payload = copy.deepcopy(device.data.get("default_payload", {}))

        payload.setdefault("aps", {})

        if loc_key:
            payload["aps"].setdefault("alert", {})["loc-key"] = loc_key

        if loc_args:
            payload["aps"].setdefault("alert", {})["loc-args"] = loc_args

        if badge is not None:
            payload["aps"]["badge"] = badge

        if loc_key and n.room_id:
            payload["room_id"] = n.room_id
        if loc_key and n.event_id:
            payload["event_id"] = n.event_id

        return payload

    async def _send_notification(
            self, request: NotificationRequest) -> NotificationResult:
        return await Deferred.fromFuture(
            asyncio.ensure_future(self.apns_client.send_notification(request)))
Beispiel #10
0
    def __init__(self, name: str, sygnal: "Sygnal", config: Dict[str,
                                                                 Any]) -> None:
        super().__init__(name, sygnal, config)

        nonunderstood = set(self.cfg.keys()).difference(
            self.UNDERSTOOD_CONFIG_FIELDS)
        if len(nonunderstood) > 0:
            logger.warning(
                "The following configuration fields are not understood: %s",
                nonunderstood,
            )

        platform = self.get_config("platform", str)
        if not platform or platform == "production" or platform == "prod":
            self.use_sandbox = False
        elif platform == "sandbox":
            self.use_sandbox = True
        else:
            raise PushkinSetupException(f"Invalid platform: {platform}")

        certfile = self.get_config("certfile", str)
        keyfile = self.get_config("keyfile", str)
        if not certfile and not keyfile:
            raise PushkinSetupException(
                "You must provide a path to an APNs certificate, or an APNs token."
            )

        if certfile:
            if not os.path.exists(certfile):
                raise PushkinSetupException(
                    f"The APNs certificate '{certfile}' does not exist.")
        elif keyfile:
            # keyfile
            if not os.path.exists(keyfile):
                raise PushkinSetupException(
                    f"The APNs key file '{keyfile}' does not exist.")
            if not self.get_config("key_id", str):
                raise PushkinSetupException("You must supply key_id.")
            if not self.get_config("team_id", str):
                raise PushkinSetupException("You must supply team_id.")
            if not self.get_config("topic", str):
                raise PushkinSetupException("You must supply topic.")

        # use the Sygnal global proxy configuration
        proxy_url_str = sygnal.config.get("proxy")

        loop = asyncio.get_event_loop()
        if proxy_url_str:
            # this overrides the create_connection method to use a HTTP proxy
            loop = ProxyingEventLoopWrapper(loop,
                                            proxy_url_str)  # type: ignore

        if certfile is not None:
            # max_connection_attempts is actually the maximum number of
            # additional connection attempts, so =0 means try once only
            # (we will retry at a higher level so not worth doing more here)
            self.apns_client = APNs(
                client_cert=certfile,
                use_sandbox=self.use_sandbox,
                max_connection_attempts=0,
                loop=loop,
            )

            self._report_certificate_expiration(certfile)
        else:
            # max_connection_attempts is actually the maximum number of
            # additional connection attempts, so =0 means try once only
            # (we will retry at a higher level so not worth doing more here)
            self.apns_client = APNs(
                key=self.get_config("keyfile", str),
                key_id=self.get_config("key_id", str),
                team_id=self.get_config("team_id", str),
                topic=self.get_config("topic", str),
                use_sandbox=self.use_sandbox,
                max_connection_attempts=0,
                loop=loop,
            )

        # without this, aioapns will retry every second forever.
        self.apns_client.pool.max_connection_attempts = 3
Beispiel #11
0
if __name__ == '__main__':
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    setup_logger('INFO')

    client_cert = '/Users/mac/musician/apps/push_serverice/cert/apns_dev.pem'
    device_token = '69-FC-04-3F-20-C6-02-91-2B-87-AA-B7-EA-19-BF-78-38-32-82-B6-0C-BF-33-A6-2C-CB-A1-EA-B2-79-0F-85'
    message = {
        "aps": {
            "alert": "Hello from APNs Tester.",
            "badge": "1",
            "sound": "default",
        }
    }

    apns = APNs(client_cert, use_sandbox=True)

    async def send_request():
        request = NotificationRequest(device_token=device_token,
                                      message=message)
        await apns.send_notification(request)

    async def main():
        send_requests = [send_request() for _ in range(1000)]
        import time
        t = time.time()
        ret = await asyncio.wait(send_requests)
        print(ret)
        print('Done: %s' % (time.time() - t))
        print()
Beispiel #12
0
class ApnsPushkin(Pushkin):
    """
    Relays notifications to the Apple Push Notification Service.
    """

    # Errors for which the token should be rejected
    TOKEN_ERROR_REASON = "Unregistered"
    TOKEN_ERROR_CODE = 410

    MAX_TRIES = 3
    RETRY_DELAY_BASE = 10

    MAX_FIELD_LENGTH = 1024
    MAX_JSON_BODY_SIZE = 4096

    UNDERSTOOD_CONFIG_FIELDS = {
        "type",
        "platform",
        "certfile",
        "team_id",
        "key_id",
        "keyfile",
        "topic",
    }

    def __init__(self, name, sygnal, config):
        super().__init__(name, sygnal, config)

        nonunderstood = set(self.cfg.keys()).difference(
            self.UNDERSTOOD_CONFIG_FIELDS)
        if len(nonunderstood) > 0:
            logger.warning(
                "The following configuration fields are not understood: %s",
                nonunderstood,
            )

        platform = self.get_config("platform")
        if not platform or platform == "production" or platform == "prod":
            self.use_sandbox = False
        elif platform == "sandbox":
            self.use_sandbox = True
        else:
            raise PushkinSetupException(f"Invalid platform: {platform}")

        certfile = self.get_config("certfile")
        keyfile = self.get_config("keyfile")
        if not certfile and not keyfile:
            raise PushkinSetupException(
                "You must provide a path to an APNs certificate, or an APNs token."
            )

        if certfile:
            if not os.path.exists(certfile):
                raise PushkinSetupException(
                    f"The APNs certificate '{certfile}' does not exist.")
        else:
            # keyfile
            if not os.path.exists(keyfile):
                raise PushkinSetupException(
                    f"The APNs key file '{keyfile}' does not exist.")
            if not self.get_config("key_id"):
                raise PushkinSetupException("You must supply key_id.")
            if not self.get_config("team_id"):
                raise PushkinSetupException("You must supply team_id.")
            if not self.get_config("topic"):
                raise PushkinSetupException("You must supply topic.")

        if self.get_config("certfile") is not None:
            self.apns_client = APNs(client_cert=self.get_config("certfile"),
                                    use_sandbox=self.use_sandbox)
        else:
            self.apns_client = APNs(
                key=self.get_config("keyfile"),
                key_id=self.get_config("key_id"),
                team_id=self.get_config("team_id"),
                topic=self.get_config("topic"),
                use_sandbox=self.use_sandbox,
            )

        # without this, aioapns will retry every second forever.
        self.apns_client.pool.max_connection_attempts = 3

    async def _dispatch_request(self, log, span, device, shaved_payload, prio):
        """
        Actually attempts to dispatch the notification once.
        """

        # this is no good: APNs expects ID to be in their format
        # so we can't just derive a
        # notif_id = context.request_id + f"-{n.devices.index(device)}"

        notif_id = str(uuid4())

        log.info(f"Sending as APNs-ID {notif_id}")
        span.set_tag("apns_id", notif_id)

        device_token = base64.b64decode(device.pushkey).hex()

        request = NotificationRequest(
            device_token=device_token,
            message=shaved_payload,
            priority=prio,
            notification_id=notif_id,
        )

        try:

            with ACTIVE_REQUESTS_GAUGE.track_inprogress():
                with SEND_TIME_HISTOGRAM.time():
                    response = await self._send_notification(request)
        except aioapns.ConnectionError:
            raise TemporaryNotificationDispatchException(
                "aioapns Connection Failure")

        code = int(response.status)

        span.set_tag(tags.HTTP_STATUS_CODE, code)

        RESPONSE_STATUS_CODES_COUNTER.labels(pushkin=self.name,
                                             code=code).inc()

        if response.is_successful:
            return []
        else:
            # .description corresponds to the 'reason' response field
            span.set_tag("apns_reason", response.description)
            if (code == self.TOKEN_ERROR_CODE
                    or response.description == self.TOKEN_ERROR_REASON):
                return [device.pushkey]
            else:
                if 500 <= code < 600:
                    raise TemporaryNotificationDispatchException(
                        f"{response.status} {response.description}")
                else:
                    raise NotificationDispatchException(
                        f"{response.status} {response.description}")

    async def dispatch_notification(self, n, device, context):
        log = NotificationLoggerAdapter(logger,
                                        {"request_id": context.request_id})

        # The pushkey is kind of secret because you can use it to send push
        # to someone.
        # span_tags = {"pushkey": device.pushkey}
        span_tags = {}

        with self.sygnal.tracer.start_span(
                "apns_dispatch", tags=span_tags,
                child_of=context.opentracing_span) as span_parent:

            if n.event_id and not n.type:
                payload = self._get_payload_event_id_only(n)
            else:
                payload = self._get_payload_full(n, log)

            if payload is None:
                # Nothing to do
                span_parent.log_kv({logs.EVENT: "apns_no_payload"})
                return
            prio = 10
            if n.prio == "low":
                prio = 5

            shaved_payload = apnstruncate.truncate(
                payload, max_length=self.MAX_JSON_BODY_SIZE)

            for retry_number in range(self.MAX_TRIES):
                try:
                    log.debug("Trying")

                    span_tags = {"retry_num": retry_number}

                    with self.sygnal.tracer.start_span(
                            "apns_dispatch_try",
                            tags=span_tags,
                            child_of=span_parent) as span:
                        return await self._dispatch_request(
                            log, span, device, shaved_payload, prio)
                except TemporaryNotificationDispatchException as exc:
                    retry_delay = self.RETRY_DELAY_BASE * (2**retry_number)
                    if exc.custom_retry_delay is not None:
                        retry_delay = exc.custom_retry_delay

                    log.warning(
                        "Temporary failure, will retry in %d seconds",
                        retry_delay,
                        exc_info=True,
                    )

                    span_parent.log_kv({
                        "event": "temporary_fail",
                        "retrying_in": retry_delay
                    })

                    if retry_number == self.MAX_TRIES - 1:
                        raise NotificationDispatchException(
                            "Retried too many times.") from exc
                    else:
                        await twisted_sleep(
                            retry_delay, twisted_reactor=self.sygnal.reactor)

    def _get_payload_event_id_only(self, n):
        """
        Constructs a payload for a notification where we know only the event ID.
        Args:
            n: The notification to construct a payload for.

        Returns:
            The APNs payload as a nested dicts.
        """
        payload = {}

        if n.room_id:
            payload["room_id"] = n.room_id
        if n.event_id:
            payload["event_id"] = n.event_id

        if n.counts.unread is not None:
            payload["unread_count"] = n.counts.unread
        if n.counts.missed_calls is not None:
            payload["missed_calls"] = n.counts.missed_calls

        return payload

    def _get_payload_full(self, n, log):
        """
        Constructs a payload for a notification.
        Args:
            n: The notification to construct a payload for.
            log: A logger.

        Returns:
            The APNs payload as nested dicts.
        """
        from_display = n.sender
        if n.sender_display_name is not None:
            from_display = n.sender_display_name
        from_display = from_display[0:self.MAX_FIELD_LENGTH]

        loc_key = None
        loc_args = None
        if n.type == "m.room.message" or n.type == "m.room.encrypted":
            room_display = None
            if n.room_name:
                room_display = n.room_name[0:self.MAX_FIELD_LENGTH]
            elif n.room_alias:
                room_display = n.room_alias[0:self.MAX_FIELD_LENGTH]

            content_display = None
            action_display = None
            is_image = False
            if n.content and "msgtype" in n.content and "body" in n.content:
                if "body" in n.content:
                    if n.content["msgtype"] == "m.text":
                        content_display = n.content["body"]
                    elif n.content["msgtype"] == "m.emote":
                        action_display = n.content["body"]
                    else:
                        # fallback: 'body' should always be user-visible text
                        # in an m.room.message
                        content_display = n.content["body"]
                if n.content["msgtype"] == "m.image":
                    is_image = True

            if room_display:
                if is_image:
                    loc_key = "IMAGE_FROM_USER_IN_ROOM"
                    loc_args = [from_display, content_display, room_display]
                elif content_display:
                    loc_key = "MSG_FROM_USER_IN_ROOM_WITH_CONTENT"
                    loc_args = [from_display, room_display, content_display]
                elif action_display:
                    loc_key = "ACTION_FROM_USER_IN_ROOM"
                    loc_args = [room_display, from_display, action_display]
                else:
                    loc_key = "MSG_FROM_USER_IN_ROOM"
                    loc_args = [from_display, room_display]
            else:
                if is_image:
                    loc_key = "IMAGE_FROM_USER"
                    loc_args = [from_display, content_display]
                elif content_display:
                    loc_key = "MSG_FROM_USER_WITH_CONTENT"
                    loc_args = [from_display, content_display]
                elif action_display:
                    loc_key = "ACTION_FROM_USER"
                    loc_args = [from_display, action_display]
                else:
                    loc_key = "MSG_FROM_USER"
                    loc_args = [from_display]

        elif n.type == "m.call.invite":
            is_video_call = False

            # This detection works only for hs that uses WebRTC for calls
            if n.content and "offer" in n.content and "sdp" in n.content[
                    "offer"]:
                sdp = n.content["offer"]["sdp"]
                if "m=video" in sdp:
                    is_video_call = True

            if is_video_call:
                loc_key = "VIDEO_CALL_FROM_USER"
            else:
                loc_key = "VOICE_CALL_FROM_USER"

            loc_args = [from_display]
        elif n.type == "m.room.member":
            if n.user_is_target:
                if n.membership == "invite":
                    if n.room_name:
                        loc_key = "USER_INVITE_TO_NAMED_ROOM"
                        loc_args = [
                            from_display,
                            n.room_name[0:self.MAX_FIELD_LENGTH],
                        ]
                    elif n.room_alias:
                        loc_key = "USER_INVITE_TO_NAMED_ROOM"
                        loc_args = [
                            from_display,
                            n.room_alias[0:self.MAX_FIELD_LENGTH],
                        ]
                    else:
                        loc_key = "USER_INVITE_TO_CHAT"
                        loc_args = [from_display]
        elif n.type:
            # A type of message was received that we don't know about
            # but it was important enough for a push to have got to us
            loc_key = "MSG_FROM_USER"
            loc_args = [from_display]

        aps = {}
        if loc_key:
            aps["alert"] = {"loc-key": loc_key}

        if loc_args:
            aps["alert"]["loc-args"] = loc_args

        badge = None
        if n.counts.unread is not None:
            badge = n.counts.unread
        if n.counts.missed_calls is not None:
            if badge is None:
                badge = 0
            badge += n.counts.missed_calls

        if badge is not None:
            aps["badge"] = badge

        if loc_key:
            aps["content-available"] = 1

        if loc_key is None and badge is None:
            log.info("Nothing to do for alert of type %s", n.type)
            return None

        payload = {}

        if loc_key and n.room_id:
            payload["room_id"] = n.room_id

        payload["aps"] = aps

        return payload

    async def _send_notification(self, request):
        return await Deferred.fromFuture(
            asyncio.ensure_future(self.apns_client.send_notification(request)))
Beispiel #13
0
    team_id = '<TEAM_ID>'
    bundle_id = '<BUNDLE_ID>'
    auth_key_id = '<AUTH_KEY_ID>'
    auth_key = '<AUTH_KEY>'
    message = {
        "aps": {
            "alert": "Hello from APNs tester.",
            "badge": "1",
            "sound": "default",
        }
    }

    apns = APNs(team_id=team_id,
                bundle_id=bundle_id,
                auth_key_id=auth_key_id,
                auth_key=auth_key,
                use_sandbox=True)

    async def send_request():
        request = NotificationRequest(device_token=device_token,
                                      message=message)
        await apns.send_notification(request)

    async def main():
        send_requests = [send_request() for _ in range(3)]
        import time
        t = time.time()
        await asyncio.wait(send_requests)
        print('Done: %s' % (time.time() - t))
        print()