diff --git a/pushserver/applications/sylk.py b/pushserver/applications/sylk.py index bab2824..dfa88d2 100644 --- a/pushserver/applications/sylk.py +++ b/pushserver/applications/sylk.py @@ -1,159 +1,159 @@ import datetime from pushserver.applications.apple import * from pushserver.applications.firebase import * from pushserver.resources.utils import callid_to_uuid from firebase_admin import messaging __all__ = ['AppleSylkHeaders', 'AppleSylkPayload', 'FirebaseSylkHeaders', 'FirebaseSylkPayload'] class AppleSylkHeaders(AppleHeaders): """ An Apple headers structure for a push notification """ def create_push_type(self) -> str: """ logic to define apns_push_type value using request parameters apns_push_type reflect the contents of the notification’s payload, it can be: 'alert', 'background', 'voip', 'complication', 'fileprovider' or 'mdm'. """ push_type = 'voip' if self.event in ('incoming_session', 'incoming_conference_request') else 'background' return push_type def create_expiration(self) -> int: """ logic to define apns_expiration value using request parameters apns_expiration is the date at which the notification expires, (UNIX epoch expressed in seconds UTC). """ return '120' def create_topic(self) -> str: """ logic to define apns_topic value using request parameters apns_topic is in general is the app’s bundle ID and may have a suffix based on the notification’s type. """ apns_topic = self.app_id if self.app_id.endswith('.dev') or self.app_id.endswith('.prod'): apns_topic = '.'.join(self.app_id.split('.')[:-1]) if self.event in ('incoming_session', 'incoming_conference_request'): apns_topic = f"{apns_topic}.voip" return apns_topic def create_priority(self) -> int: """ logic to define apns_priority value using request parameters Notification priority, apns_prioriy 10 o send the notification immediately, 5 to send the notification based on power considerations on the user’s device. """ apns_priority = '10' if self.event in ('incoming_session', 'incoming_conference_request') else '5' return apns_priority class FirebaseSylkHeaders(FirebaseHeaders): """ Firebase headers for a push notification """ class AppleSylkPayload(ApplePayload): """ A payload for a Apple Sylk push notification """ @property def payload(self) -> str: """ Generate an AppleSylk notification payload """ if self.event == 'cancel': payload = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'reason': self.reason } else: payload = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'media-type': self.media_type, 'from_uri': self.sip_from, 'from_display_name': self.from_display_name, 'to_uri': self.sip_to } return payload class FirebaseSylkPayload(FirebasePayload): """ A payload for a Firebase Sylk push notification """ @property def payload(self) -> str: """ Generate a Sylk payload and extra Firebase parameters """ if not self.from_display_name: from_display_name = self.sip_from else: from_display_name = self.from_display_name if self.event == 'cancel': data = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'reason': self.reason } else: data = { 'event': self.event, 'call-id': self.call_id, 'session-id': callid_to_uuid(self.call_id), 'media-type': self.media_type, 'from_uri': self.sip_from, 'from_display_name': from_display_name, 'to_uri': self.sip_to } http_payload = { 'message': { 'token': self.token, 'data': data, 'android': { 'priority': 'high', 'ttl': '60s' } } } fcm_payload = messaging.Message( token=self.token, data=data, android=messaging.AndroidConfig( ttl=datetime.timedelta(seconds=60), priority='high' ) ) - return {'http': http_payload, 'fcm': fcm_payload} + return http_payload diff --git a/pushserver/pns/apple.py b/pushserver/pns/apple.py index 6572b7c..d7b70ab 100644 --- a/pushserver/pns/apple.py +++ b/pushserver/pns/apple.py @@ -1,400 +1,404 @@ import json import os import socket import ssl import time import hyper from hyper import HTTP20Connection, tls from pushserver.models.requests import WakeUpRequest from pushserver.resources.utils import log_event, ssl_cert from pushserver.pns.base import PNS, PushRequest, PlatformRegister class ApplePNS(PNS): """ An Apple Push Notification service """ def __init__(self, app_id: str, app_name: str, url_push: str, voip: bool, cert_file: str, key_file: str): """ :param app_id: `str`, blunde id provided by application. :param url_push: `str`, URI to push a notification (from applications.ini) :param cert_file `str`: path to APNS certificate (provided by dev app kit) :param key_file `str`: path to APNS key (provided by dev app kit) :param voip: `bool`, Required for apple, `True` for voip push notification type. """ self.app_id = app_id self.app_name = app_name self.url_push = url_push self.voip = voip self.key_file = key_file self.cert_file = cert_file class AppleConn(ApplePNS): """ An Apple connection """ def __init__(self, app_id: str, app_name: str, url_push: str, voip: bool, cert_file: str, key_file: str, apple_pns: PNS, loggers: dict, port: int = 443): """ :param apple_pns `ApplePNS`: Apple Push Notification Service. :param port `int`: 443 or 2197 to allow APNS traffic but block other HTTP traffic. :param loggers: `dict` global logging instances to write messages (params.loggers) :attribute ssl_context `ssl.SSLContext`: generated with a valid apple certificate. :attribute connection `HTTP20Connection`: related to an app and its corresponding certificate. """ self.app_id = app_id self.app_name = app_name self.url_push = url_push self.voip = voip self.key_file = key_file self.cert_file = cert_file self.apple_pns = apple_pns self.port = port self.loggers = loggers @property def ssl_context(self) -> ssl.SSLContext: """ Define a ssl context using a cert_file to open a connection requires a valid certificate file :return: a ssl.SSLContext object """ cert_file = self.cert_file key_file = self.key_file if self.key_file else self.cert_file ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE ssl_context.load_cert_chain(keyfile=key_file, certfile=cert_file) return ssl_context @property def connection(self) -> HTTP20Connection: """ Open an apple connection requires a ssl context :return: an hyper.http20.connection.HTTP20Connection object """ host = self.url_push port = self.port ssl_context = self.ssl_context connection = HTTP20Connection(host=host, port=port, ssl_context=ssl_context, force_proto=tls.H2C_PROTOCOL) cert_file_name = self.cert_file.split('/')[-1] key_file_name = self.key_file.split('/')[-1] if self.key_file else None if self.loggers['debug']: if key_file_name: msg = f'{self.app_name.capitalize()} app: Connecting to {host}:{port} ' \ f'using {cert_file_name} certificate ' \ f'and {key_file_name} key files' else: msg = f'{self.app_name.capitalize()} app: Connecting to {host}:{port} ' \ f'using {cert_file_name} certificate' log_event(loggers=self.loggers, msg=msg, level='deb') return connection class AppleRegister(PlatformRegister): def __init__(self, app_id: str, app_name:str, voip: bool, credentials_path: str, config_dict: dict, loggers: dict): self.app_id = app_id self.app_name = app_name self.voip = voip self.credentials_path = credentials_path self.config_dict = config_dict self.loggers = loggers self.error = '' @property def url_push(self) -> str: try: return self.config_dict['apple_push_url'] except KeyError: self.error = 'apple_push_url not found in applications.ini' return None @property def certificate(self) -> dict: if self.error: return {} else: try: cert_file = f"{self.credentials_path}/" \ f"{self.config_dict['apple_certificate']}" cert_exists = os.path.exists(cert_file) if not cert_exists: self.error = f"{cert_file} - no such file." return {} else: return {'cert_file': cert_file, 'cert_exists': cert_exists} except KeyError: self.error = 'apple_certificate not found in applications.ini' return {} @property def key(self) -> dict: if self.error: return {} try: key_file = f"{self.credentials_path}/" \ f"{self.config_dict['apple_key']}" key_exists = os.path.exists(key_file) if not key_exists: self.error = f"{key_file} - no such file." return {} except KeyError: return {} return {'key_file': key_file, 'key_exists': key_exists} @property def ssl_valid_cert(self) -> bool: if self.error: return else: try: cert_file = self.certificate.get('cert_file') key_file = self.key.get('key_file') if not (cert_file or key_file): self.error = 'An apple certificate/key is needed to open a connection' elif not ssl_cert(cert_file, key_file): self.error = f"{cert_file} - bad ssl certificate." return else: return True except FileNotFoundError as exc: self.error = exc return @property def apple_pns(self) -> ApplePNS: if self.error: return if self.ssl_valid_cert: cert_file = self.certificate.get('cert_file') key_file = self.key.get('key_file') return ApplePNS(app_id=self.app_id, app_name=self.app_name, url_push=self.url_push, voip=self.voip, cert_file=cert_file, key_file=key_file) @property def apple_conn(self): if self.error: return return AppleConn(app_id=self.app_id, app_name=self.app_name, url_push=self.url_push, voip=self.voip, cert_file=self.certificate.get('cert_file'), key_file=self.key.get('key_file'), apple_pns=self.apple_pns, loggers=self.loggers).connection @property def register_entries(self): if self.error: return {} return {'pns': self.apple_pns, 'conn': self.apple_conn} class ApplePushRequest(PushRequest): """ Apple push notification request """ def __init__(self, error: str, app_name: str, app_id: str, request_id: str, headers: str, payload: dict, loggers: dict, log_remote: dict, wp_request: WakeUpRequest, register: dict): """ :param error: `str` :param app_name: `str` 'linphone' or 'payload' :param app_id: `str` bundle id :param headers: `AppleHeaders` Apple push notification headers :param payload: `ApplePayload` Apple push notification payload :param wp_request: `WakeUpRequest` :param loggers: `dict` global logging instances to write messages (params.loggers) """ self.error = error self.app_name = app_name self.app_id = app_id self.platform = 'apple' self.request_id = request_id self.headers = headers self.payload = payload self.token = wp_request.token self.call_id = wp_request.call_id self.media_type = wp_request.media_type self.wp_request = wp_request self.loggers = loggers self.log_remote = log_remote self.apple_pns = register['pns'] self.connection = register['conn'] self.path = f'/3/device/{self.token}' self.results = self.send_notification() def send_notification(self) -> dict: """ Send an apple push requests to a single device. If status of response is like 5xx, an exponential backoff factor is implemented to retry the notification according to media type. :param `hstr` token: destination device. :param `str` method: HTTP request method, must be 'POST'. :param `AppleHeaders` headers: Apple push notification headers. :param `ApplePayload` payload: Apple push notification payload. """ if self.error: self.log_error() return {'code': 500, 'body': {}, 'reason': 'Internal server error'} n_retries, backoff_factor = self.retries_params(self.media_type) log_path = f'http://{self.apple_pns.url_push}{self.path}' status_forcelist = tuple([status for status in range(500, 600)]) counter = 0 status = 500 reason = '' body = {} while counter <= n_retries: if self.connection: try: self.log_request(path=log_path) self.connection.request('POST', self.path, self.payload, self.headers) response = self.connection.get_response() reason_str = response.read().decode('utf8').replace("'", '"') if reason_str: reason_json = json.loads(reason_str) reason = reason_json.get('reason') else: reason = reason_str status = response.status if status not in status_forcelist: break except socket.gaierror: reason = 'socket error' except hyper.http20.exceptions.StreamResetError: reason = 'stream error' except ValueError as err: reason = f'Bad type of object in headers or payload: {err}' break else: reason = 'no connection' counter += 1 timer = backoff_factor * (2 ** (counter - 1)) time.sleep(timer) if counter == n_retries: reason = 'max retries reached' url = f'https:{self.connection.host}:{self.connection.port}{self.path}' if status != 200: details = self.apple_error_info(reason) if details: reason = f'{reason} - {details}' + + if status == 400 and 'BadDeviceToken' in reason: + status = 410 results = {'body': body, 'code': status, 'reason': reason, 'url': url, + 'platform': 'apple', 'call_id': self.call_id, 'token': self.token } self.results = results self.log_results() return results def apple_error_info(self, reason): """ Give a human readable message according to 'reason' from apple APN. :returns : a string with message according to reason """ description_codes = {'ConnectionFailed': 'There was an error connecting to APNs.', 'InternalException': 'This exception should not be raised. If it is, please report this as a bug.', 'BadPayloadException': 'Something bad with the payload.', 'BadCollapseId': 'The collapse identifier exceeds the maximum allowed size', 'BadDeviceToken': 'The specified device token was bad. Verify that the request contains a valid token and that the token matches the environment.', 'BadExpirationDate:': 'The apns-expiration value is bad.', 'BadMessageId': 'The apns-id value is bad.', 'BadPriority': 'The apns-priority value is bad.', 'BadTopic': 'The apns-topic was invalid.', 'DeviceTokenNotForTopic': 'The device token does not match the specified topic.', 'DuplicateHeaders': 'One or more headers were repeated.', 'IdleTimeout': 'Idle time out.', 'MissingDeviceToken': 'The device token is not specified in the request :path. Verify that the :path header contains the device token.', 'MissingTopic': 'The apns-topic header of the request was not specified and was required. The apns-topic header is mandatory when the client is connected using a certificate that supports multiple topics.', 'PayloadEmpty': 'The message payload was empty.', 'TopicDisallowed': 'Pushing to this topic is not allowed.', 'BadCertificate': 'The certificate was bad.', 'BadCertificateEnvironment': 'The client certificate was for the wrong environment.', 'ExpiredProviderToken': 'The provider token is stale and a new token should be generated.', 'Forbidden': 'The specified action is not allowed.', 'InvalidProviderToken': 'The provider token is not valid or the token signature could not be verified.', 'MissingProviderToken': 'No provider certificate was used to connect to APNs and Authorization header was missing or no provider token was specified.', 'BadPath': 'The request contained a bad :path value.', 'MethodNotAllowed': 'The specified :method was not POST.', 'Unregistered': 'The device token is inactive for the specified topic.', 'PayloadTooLarge': 'The message payload was too large. The maximum payload size is 4096 bytes.', 'TooManyProviderTokenUpdates': 'The provider token is being updated too often.', 'TooManyRequests': 'Too many requests were made consecutively to the same device token.', 'InternalServerError': 'An internal server error occurred.', 'ServiceUnavailable': 'The service is unavailable.', 'Shutdown': 'The server is shutting down.', 'InvalidPushType': 'The apns-push-type value is invalid.'} try: message = description_codes[reason] return message except KeyError: return None diff --git a/pushserver/pns/firebase.py b/pushserver/pns/firebase.py index aa7773e..165c430 100644 --- a/pushserver/pns/firebase.py +++ b/pushserver/pns/firebase.py @@ -1,381 +1,404 @@ import json import os import time from datetime import datetime import oauth2client import requests from pushserver.models.requests import WakeUpRequest from requests.adapters import HTTPAdapter from urllib3 import Retry from pushserver.pns.base import PNS, PushRequest, PlatformRegister from pushserver.resources.utils import log_event, fix_non_serializable_types import firebase_admin from firebase_admin import messaging default_app = firebase_admin.initialize_app() class FirebasePNS(PNS): """ A Firebase Push Notification service """ def __init__(self, app_id: str, app_name: str, url_push: str, voip: bool, auth_key: str = None, auth_file: str = None): """ :param app_id `str`: Application ID. :param url_push `str`: URI to push a notification. :param voip `bool`: required for apple, `True` for voip push notification type. :param auth_key `str`: A Firebase credential for push notifications. :param auth_file `str`: A Firebase credential for push notifications. """ self.app_id = app_id self.app_name = app_name self.url_push = url_push self.voip = voip self.auth_key = auth_key self.auth_file = auth_file self.error = '' class FirebaseRegister(PlatformRegister): def __init__(self, app_id: str, app_name: str, voip: bool, config_dict: dict, credentials_path: str, loggers: dict): self.app_id = app_id self.app_name = app_name self.voip = voip self.credentials_path = credentials_path self.config_dict = config_dict self.loggers = loggers self.error = '' self.auth_key, self.auth_file = self.set_auths() @property def url_push(self): try: return self.config_dict['firebase_push_url'] except KeyError: self.error = 'firebase_push_url not found in applications.ini' return None def set_auths(self): auth_key = None auth_file = None try: auth_key = self.config_dict['firebase_authorization_key'] except KeyError: try: auth_file = self.config_dict['firebase_authorization_file'] if self.credentials_path: auth_file = f"{self.credentials_path}/" \ f"{auth_file}" else: pass if not os.path.exists(auth_file): self.error = f'{auth_file} - no such file' except KeyError: self.error = 'not firebase_authorization_key or ' \ 'firebase_authorization_file found in applications.ini' return auth_key, auth_file @property def pns(self) -> FirebasePNS: pns = None if self.auth_key: auth_file = '' pns = FirebasePNS(app_id=self.app_id, app_name=self.app_name, url_push=self.url_push, voip=self.voip, auth_key=self.auth_key, auth_file=auth_file) elif self.auth_file: pns = FirebasePNS(app_id=self.app_id, app_name=self.app_name, url_push=self.url_push, voip=self.voip, auth_file=self.auth_file) self.error = pns.error if pns.error else '' return pns @property def register_entries(self): if self.error: return {} return {'pns': self.pns, 'auth_key': self.auth_key, 'auth_file': self.auth_file, 'refreshed_token': False} class FirebasePushRequest(PushRequest): """ Firebase push notification request """ def __init__(self, error: str, app_name: str, app_id: str, request_id: str, headers: str, payload: dict, loggers: dict, log_remote: dict, wp_request: WakeUpRequest, register: dict): """ :param error: `str` :param app_name: `str` 'linphone' or 'payload' :param app_id: `str` bundle id :param headers: `FirebaseHeaders` Firebase push notification headers :param payload: `FirebasePayload`Firebase push notification payload :param wp_request: `WakeUpRequest` :param loggers: `dict` global logging instances to write messages (params.loggers) """ self.error = error self.app_name = app_name self.app_id = app_id self.platform = 'firebase' self.request_id = request_id self.headers = headers self.payload = payload self.token = wp_request.token self.wp_request = wp_request self.loggers = loggers self.log_remote = log_remote self.pns = register['pns'] self.path = self.pns.url_push self.results = self.send_http_notification() # self.results = self.send_fcm_notification() def requests_retry_session(self, counter=0): """ Define parameters to retry a push notification according to media_type. :param counter: `int` (optional) if retries was necessary because of connection fails Following rfc3261 specification, an exponential backoff factor is used. More specifically: backoff = 0.5 T1 = 500ms max_retries_call = 7 time_to_live_call = 64 seconds max_retries_sms = 11 time_to_live_sms ~ 2 hours """ retries = self.retries_params[self.media_type] - counter backoff_factor = self.retries_params['bo_factor'] * 0.5 * 2 ** counter status_forcelist = tuple([status for status in range(500, 600)]) session = None session = session or requests.Session() retry = Retry( total=retries, read=retries, connect=retries, backoff_factor=backoff_factor, status_forcelist=status_forcelist) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) return session def send_http_notification(self) -> dict: """ Send a Firebase push notification over HTTP """ if self.error: self.log_error() return {'code': 500, 'body': {}, 'reason': 'Internal server error'} n_retries, backoff_factor = self.retries_params(self.wp_request.media_type) counter = 0 error = False code = 500 reason = "" body = None response = None while counter <= n_retries: self.log_request(path=self.pns.url_push) try: response = requests.post(self.pns.url_push, - self.payload['http'], + self.payload, headers=self.headers) break except requests.exceptions.RequestException as e: error = True reason = f'connection failed: {e}' counter += 1 timer = backoff_factor * (2 ** (counter - 1)) time.sleep(timer) if counter == n_retries: reason = "maximum retries reached" elif error: try: response = self.requests_retry_session(counter). \ post(self.pns.url_push, self.payload, headers=self.headers) except Exception as x: level = 'error' msg = f"outgoing {self.platform.title()} response for " \ f"{self.request_id}, push failed: " \ f"an error occurred in {x.__class__.__name__}" log_event(loggers=self.loggers, msg=msg, level=level) try: body = response.__dict__ except (TypeError, ValueError): code = 500 reason = 'cannot parse response body' body = {} else: reason = body.get('reason') code = response.status_code for k in ('raw', 'request', 'connection', 'cookies', 'elapsed'): try: del body[k] except KeyError: pass except TypeError: break body = json.dumps(fix_non_serializable_types(body)) if isinstance(body, str): body = json.loads(body) if code == 200: + description = 'OK' try: failure = body['_content']['failure'] except KeyError: pass else: if failure == 1: - reason = body['_content']['results'][0]['error'] + description = body['_content']['results'][0]['error'] code = 410 - elif code == 404: + + else: try: - payload_code = body['_content']['error']['code'] + reason = body['reason'] except KeyError: - pass + reason = None + + try: + details = body['_content']['error']['message'] + except KeyError: + details = None + + try: + internal_code = body['_content']['error']['code'] + except KeyError: + internal_code = None + + if internal_code == 400 and 'not a valid FCM registration token' in details: + code = 410 + + if reason and details: + description = "%s %s" % (reason, details) + elif reason: + description = reason + elif details: + error_description = details else: - if payload_code == 404: - code = 410 - + description = 'unknown failure reason' + keys = list(body.keys()) for key in keys: if not body[key]: del body[key] results = {'body': body, 'code': code, - 'reason': reason, + 'reason': description, 'url': self.pns.url_push, + 'platform': 'firebase', 'call_id': self.wp_request.call_id, 'token': self.token } self.results = results self.log_results() return results def send_fcm_notification(self) -> dict: """ Send a native Firebase push notification """ if self.error: self.log_error() return {'code': 500, 'body': {}, 'reason': 'Internal server error'} n_retries, backoff_factor = self.retries_params(self.wp_request.media_type) counter = 0 error = False code = 200 response = None body = None reason = None while counter <= n_retries: self.log_request(path=self.pns.url_push) try: response = messaging.send(self.payload['fcm']) break except Exception as e: error = True response = f'connection failed: {e}' counter += 1 timer = backoff_factor * (2 ** (counter - 1)) conde = 500 time.sleep(timer) if counter == n_retries: reason = "maximum retries reached" elif error: try: response = self.requests_retry_session(counter). \ post(self.pns.url_push, self.payload, headers=self.headers) except Exception as x: level = 'error' msg = f"outgoing {self.platform.title()} response for " \ f"{self.request_id}, push failed: " \ f"an error occurred in {x.__class__.__name__}" log_event(loggers=self.loggers, msg=msg, level=level) body = {'response': response} results = {'body': body, 'code': code, 'reason': reason, 'url': self.pns.url_push, + 'platform': 'firebase', 'call_id': self.wp_request.call_id, 'token': self.token } self.results = results self.log_results() # Request is missing required authentication credential. # Expected OAuth 2 access token, login cookie or other valid authentication # credential. UNAUTHENTICATED code = results.get('code') reason = results.get('reason') if not got401 and code == 401 and reason == 'Unauthorized': if not self.pns.get('refreshed_token'): level = 'warn' msg = f"outgoing {self.platform.title()} response for request " \ f"{self.request_id} need a new access token - " \ f"server will refresh it and try again" log_event(loggers=self.loggers, msg=msg, level=level, to_file=True) # retry with a new Fireplace access token self.pns.access_token = self.pns.set_access_token() level = 'warn' msg = f"outgoing {self.platform.title()} response for request " \ f"{self.request_id} a new access token was generated - " \ f"trying again" log_event(loggers=self.loggers, msg=msg, level=level, to_file=True) self.results = self.send_notification() return results diff --git a/pushserver/resources/notification.py b/pushserver/resources/notification.py index d7a4ccb..a791293 100644 --- a/pushserver/resources/notification.py +++ b/pushserver/resources/notification.py @@ -1,93 +1,100 @@ import importlib import json from pushserver.models.requests import WakeUpRequest from pushserver.resources import settings def handle_request(wp_request, request_id: str) -> dict: """ Create a PushNotification object, and call methods to send the notification. :param wp_request: `WakeUpRequest', received from /push route. :param loggers: `dict` global logging instances to write messages (params.loggers) :param request_id: `str`, request ID generated on request event. :return: a `dict` with push notification results """ push_notification = PushNotification(wp_request=wp_request, request_id=request_id) results = push_notification.send_notification() return results class PushNotification(object): """ Push Notification actions from wake up request """ def __init__(self, wp_request: WakeUpRequest, request_id: str): """ :param wp_request: `WakeUpRequest`, from http request :param request_id: `str`, request ID generated on request event. """ self.wp_request = wp_request self.app_id = self.wp_request.app_id self.platform = self.wp_request.platform self.pns_register = settings.params.pns_register self.request_id = request_id self.loggers = settings.params.loggers self.log_remote = self.pns_register[(self.app_id, self.platform)].get('log_remote') self.config_dict = self.pns_register[(self.app_id, self.platform)] self.app_name = self.pns_register[(self.app_id, self.platform)]['name'] self.args = [self.app_id, self.wp_request.event, self.wp_request.token, self.wp_request.call_id, self.wp_request.sip_from, self.wp_request.from_display_name, self.wp_request.sip_to, self.wp_request.media_type, self.wp_request.silent, self.wp_request.reason] @property def custom_apps(self): apps = [self.pns_register[key]['name'] for key in self.pns_register.keys()] custom_apps = set(app for app in apps if app not in ('sylk', 'linphone')) return custom_apps def send_notification(self) -> dict: """ Send a push notification according to wakeup request params. """ error = '' headers_class = self.pns_register[(self.app_id, self.platform)]['headers_class'] headers = headers_class(*self.args).headers payload_class = self.pns_register[(self.app_id, self.platform)]['payload_class'] - payload = payload_class(*self.args).payload - payload = json.dumps(payload) + try: + payload_dict = payload_class(*self.args).payload['http'] + except KeyError: + payload_dict = payload_class(*self.args).payload + + try: + payload = json.dumps(payload) + except Exception as e: + pass if not (headers and payload): error = f'{headers_class.__name__} and {payload_class.__name__} ' \ f'returned bad objects:' \ f'{headers}, {payload}' if not isinstance(headers, dict) or not isinstance(payload, str): error = f'{headers_class.__name__} and {payload_class.__name__} ' \ f'returned bad objects:' \ f'{headers}, {payload}' register = self.pns_register[(self.app_id, self.platform)] platform_module = importlib.import_module(f'pushserver.pns.{self.platform}') push_request_class = getattr(platform_module, f'{self.platform.capitalize()}PushRequest') push_request = push_request_class(error=error, app_name=self.app_name, app_id=self.app_id, request_id=self.request_id, headers=headers, payload=payload, loggers=self.loggers, log_remote=self.log_remote, wp_request=self.wp_request, register=register) return push_request.results