diff --git a/pushserver/applications/apple.py b/pushserver/applications/apple.py index 3cccf77..c127b2b 100644 --- a/pushserver/applications/apple.py +++ b/pushserver/applications/apple.py @@ -1,143 +1,147 @@ __all__ = ['AppleHeaders', 'ApplePayload'] class AppleHeaders(object): """ Apple headers structure for a push notification """ def __init__(self, app_id: str, event: str, token: str, call_id: str, sip_from: str, from_display_name: str, sip_to: str, media_type: str, silent: bool, reason: str, - badge: int): + badge: int, filename: str, filetype: str): """ :param app_id: `str` id provided by the mobile application (bundle id) :param event: `str` 'incoming_session', 'incoming_conference', 'cancel' or 'message' :param token: `str` destination device token. :param call_id: `str` unique sip parameter. :param sip_from: `str` SIP URI for who is calling :param from_display_name: `str` display name of the caller :param sip_to: `str` SIP URI for who is called :param media_type: `str` 'audio', 'video', 'chat', 'sms' or 'file-transfer' :param silent: `bool` True for silent notification :param reason: `str` Cancel reason :param badge: `int` Number to display as badge """ self.app_id = app_id self.token = token self.call_id = call_id self.sip_from = sip_from self.sip_to = sip_to self.from_display_name = from_display_name self.media_type = media_type self.silent = silent self.event = event self.reason = reason self.badge = badge + self.filename = filename + self.filetype = filetype self.apns_push_type = self.create_push_type() self.apns_expiration = self.create_expiration() self.apns_topic = self.create_topic() self.apns_priority = self.create_priority() 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'. """ return 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 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. """ return 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. """ return @property def headers(self) -> dict: """ Generate apple notification headers :return: a `dict` object with headers. """ headers = { 'apns-push-type': self.apns_push_type, 'apns-expiration': self.apns_expiration, 'apns-priority': self.apns_priority, 'apns-topic': self.apns_topic, 'authorization': f"bearer {self.token}"} if self.apns_push_type == 'background': headers['content-available'] = '1' return headers class ApplePayload(object): """ Apple payload structure for a push notification """ def __init__(self, app_id: str, event: str, token: str, call_id: str, sip_from: str, from_display_name: str, sip_to: str, media_type, silent: bool, reason: str, - badge: int): + badge: int, filename: str, filetype: str): """ :param app_id: `str` id provided by the mobile application (bundle id) :param event: `str` 'incoming_session', 'incoming_conference', 'cancel' or 'message' :param token: `str` destination device token. :param call_id: `str` unique sip parameter. :param sip_from: `str` SIP URI for who is calling :param from_display_name: `str` display name of the caller :param sip_to: `str` SIP URI for who is called :param media_type: `str` 'audio', 'video', 'chat', 'sms' or 'file-transfer' :param silent: `bool` True for silent notification :param reason: `str` Cancel reason :param badge: `int` Number to display as badge """ self.app_id = app_id self.token = token self.call_id = call_id self.sip_from = sip_from self.sip_to = sip_to self.from_display_name = from_display_name self.media_type = media_type self.silent = silent self.event = event self.reason = reason self.badge = badge + self.filename = filename + self.filetype = filetype @property def payload(self) -> dict: """ logic to define apple payload using request parameters """ payload = {} return payload diff --git a/pushserver/applications/firebase.py b/pushserver/applications/firebase.py index 46743d1..c240d5c 100644 --- a/pushserver/applications/firebase.py +++ b/pushserver/applications/firebase.py @@ -1,122 +1,126 @@ from pushserver.resources import settings from oauth2client.service_account import ServiceAccountCredentials __all__ = ['FirebaseHeaders', 'FirebasePayload'] class FirebaseHeaders(object): def __init__(self, app_id: str, event: str, token: str, call_id: str, sip_from: str, from_display_name: str, sip_to: str, media_type: str, silent: bool, reason: str, - badge: int): + badge: int, filename: str, filetype: str): """ :param app_id: `str` id provided by the mobile application (bundle id) :param event: `str` 'incoming_session', 'incoming_conference', 'cancel' or 'message' :param token: `str` destination device token. :param call_id: `str` unique sip parameter. :param sip_from: `str` SIP URI for who is calling :param from_display_name: `str` display name of the caller :param sip_to: `str` SIP URI for who is called :param media_type: `str` 'audio', 'video', 'chat', 'sms' or 'file-transfer' :param silent: `bool` True for silent notification :param reason: `str` Cancel reason :param badge: `int` Number to display as badge """ self.app_id = app_id self.token = token self.call_id = call_id self.sip_from = sip_from self.sip_to = sip_to self.from_display_name = from_display_name self.media_type = media_type self.silent = silent self.event = event self.reason = reason self.badge = badge + self.filename = filename + self.filetype = filetype try: self.auth_key = settings.params.pns_register[(self.app_id, 'firebase')]['auth_key'] except KeyError: self.auth_key = None try: self.auth_file = settings.params.pns_register[(self.app_id, 'firebase')]['auth_file'] except KeyError: self.auth_file = None @property def access_token(self) -> str: # https://github.com/firebase/quickstart-python/blob/909f39e77395cb0682108184ba565150caa68a31/messaging/messaging.py#L25-L33 """ Retrieve a valid access token that can be used to authorize requests. :return: `str` Access token. """ scopes = ['https://www.googleapis.com/auth/firebase.messaging'] try: credentials = ServiceAccountCredentials.from_json_keyfile_name(self.auth_file, scopes) access_token_info = credentials.get_access_token() return access_token_info.access_token except Exception as e: self.error = f"Error: cannot generated Firebase access token: {e}" return '' @property def headers(self): """ Generate Firebase headers structure for a push notification :return: a firebase push notification header. """ if self.auth_key: headers = {'Content-Type': 'application/json', 'Authorization': f"key={self.auth_key}"} else: headers = { 'Authorization': f"Bearer {self.access_token}", 'Content-Type': 'application/json; UTF-8', } return headers class FirebasePayload(object): def __init__(self, app_id: str, event: str, token: str, call_id: str, sip_from: str, from_display_name: str, sip_to: str, media_type: str, silent: bool, reason: str, - badge: int): + badge: int, filename: str, filetype: str): """ :param app_id: `str` id provided by the mobile application (bundle id) :param event: `str` 'incoming_session', 'incoming_conference', 'cancel' or 'message' :param token: `str` destination device token. :param call_id: `str` unique sip parameter. :param sip_from: `str` SIP URI for who is calling :param from_display_name: `str` display name of the caller :param sip_to: `str` SIP URI for who is called :param media_type: `str` 'audio', 'video', 'chat', 'sms' or 'file-transfer' :param silent: `bool` True for silent notification :param reason: `str` Cancel reason :param badge: `int` Number to display as badge """ self.app_id = app_id self.token = token self.call_id = call_id # corresponds to session_id in the output self.event = event self.media_type = media_type self.sip_from = sip_from self.from_display_name = from_display_name self.sip_to = sip_to self.silent = silent self.reason = reason self.badge = badge + self.filename = filename + self.filetype = filetype @property def payload(self) -> dict: """ Generate a Firebase payload for a push notification :return a Firebase payload: """ payload = {} return payload diff --git a/pushserver/applications/sylk.py b/pushserver/applications/sylk.py index 140e1cb..c2d3bb4 100644 --- a/pushserver/applications/sylk.py +++ b/pushserver/applications/sylk.py @@ -1,208 +1,269 @@ # 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 = 'alert' if self.event in ('incoming_session', 'incoming_conference_request'): push_type = 'voip' elif self.event == 'cancel': push_type = '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 } elif self.event == 'message': payload = { 'aps': { 'alert': { 'title': 'New message', 'body': 'From %s' % self.sip_from, }, 'message_id': self.call_id, "sound": "default", "badge": self.badge, } } + elif self.event == 'transfer': + payload = { + 'aps': { + 'alert': { + 'title': f'New {self.media_type} message' if self.media_type in ['audio', 'video'] else 'New file', + 'body': 'From %s' % self.sip_from, + }, + "sound": "default", + "badge": self.badge, + }, + 'data': { + 'event': self.event, + 'from_uri': self.sip_form, + 'to_uri': self.sip_to, + 'file-id': self.call_id, + 'media-type': self.media_type, + 'metadata': { + 'filename': self.filename, + 'filetype': self.filetype + } + } + } 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 } elif self.event == 'message': data = { 'event': self.event, 'from_uri': self.sip_from, 'to_uri': self.sip_to } + elif self.event == 'transfer': + data = { + 'event': self.event, + 'from_uri': self.sip_form, + 'to_uri': self.sip_to, + 'media-type': self.media_type, + 'file-id': self.call_id, + 'metadata': { + 'filename': self.filename, + 'filetype': self.filetype + } + } 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' } } } - if (self.event == 'message'): + if self.event == 'message': http_payload = { 'message': { 'token': self.token, 'data': data, 'notification': { 'title': 'New message', 'body': 'From %s' % self.sip_from, 'image': 'https://icanblink.com/apple-touch-icon-180x180.png' }, 'apns': { 'headers': { 'apns-priority': '5', } }, 'android': { 'priority': 'high', 'ttl': '60s', 'notification': { 'channel_id': 'sylk-messages-sound', 'sound': 'default', 'default_sound': True, 'notification_priority': 'PRIORITY_HIGH' } } } } + elif self.event == 'transfer': + http_payload = { + 'message': { + 'token': self.token, + 'data': data, + 'notification': { + 'title': f'New {self.media_type} message' if self.media_type in ['audio', 'video'] else 'New file', + 'body': 'From %s' % self.sip_from, + 'image': 'https://icanblink.com/apple-touch-icon-180x180.png' + }, + 'apns': { + 'headers': { + 'apns-priority': '5', + } + }, + 'android': { + 'priority': 'high', + 'ttl': '60s', + 'notification': { + 'channel_id': 'sylk-messages-sound', + 'sound': 'default', + 'default_sound': True, + 'notification_priority': 'PRIORITY_HIGH' + } + } + } + } # fcm_payload = messaging.Message( # token=self.token, # data=data, # android=messaging.AndroidConfig( # ttl=datetime.timedelta(seconds=60), # priority='high' # ) # ) return http_payload diff --git a/pushserver/models/requests.py b/pushserver/models/requests.py index e6aa83f..92a53d9 100644 --- a/pushserver/models/requests.py +++ b/pushserver/models/requests.py @@ -1,223 +1,227 @@ from pydantic import BaseModel, root_validator, validator from pushserver.resources import settings from pushserver.resources.utils import fix_platform_name def gen_validator_items() -> tuple: """ Generate some dicts according to minimum required parameters, and each app required paramaters, usefull for request validation. :return: two `dict` objects with common items and apps items. """ common_items = {'app-id', 'call-id', 'platform', 'from', 'token'} only_sylk_items = {'silent', 'to', 'event'} only_linphone_items = set() apps_items = {'sylk': common_items | only_sylk_items, # union 'linphone': common_items | only_linphone_items} return common_items, apps_items common_items, apps_items = gen_validator_items() def alias_rename(attribute: str) -> str: """ Rename request name attribute, replacing '_' by '_' and removing 'sip_' characters. :param attribute: `str` from request :return: a `str` corresponding to alias. """ if attribute.startswith('sip_'): return attribute.split('_', maxsplit=1)[1] return attribute.replace('_', '-') class AddRequest(BaseModel): app_id: str # id provided by the mobile application (bundle id) platform: str # 'firebase', 'android', 'apple' or 'ios' token: str # destination device token in hex device_id: str # the device-id that owns the token (used for logging purposes) silent: bool = True user_agent: str = None class Config: alias_generator = alias_rename @root_validator(pre=True) def check_required_items_for_add(cls, values): app_id, platform = values.get('app-id'), values.get('platform') if not app_id: raise ValueError("Field 'app-id' required") if not platform: raise ValueError("Field 'platform' required") platform = fix_platform_name(platform) if platform not in ('firebase', 'apple'): raise ValueError(f"The '{platform}' platform is not configured") pns_register = settings.params.pns_register if (app_id, platform) not in pns_register.keys(): raise ValueError(f"{platform.capitalize()} {app_id} app " f"is not configured") return values @validator('platform') def platform_valid_values(cls, v): if v not in ('apple', 'ios', 'android', 'firebase', 'fcm', 'apns'): raise ValueError("platform must be 'apple', 'android' or 'firebase'") return v class AddResponse(BaseModel): app_id: str # id provided by the mobile application (bundle id) platform: str # 'firebase', 'android', 'apple' or 'ios' token: str # destination device token in hex device_id: str # the device-id that owns the token (used for logging purposes) silent: bool = True user_agent: str = None class Config: allow_population_by_field_name = True alias_generator = alias_rename class RemoveRequest(BaseModel): app_id: str # id provided by the mobile application (bundle id) device_id: str = None # the device-id that owns the token (used for logging purposes) class Config: alias_generator = alias_rename @root_validator(pre=True) def check_required_items_for_add(cls, values): app_id = values.get('app-id') if not app_id: raise ValueError("Field 'app-id' required") return values class RemoveResponse(BaseModel): app_id: str # id provided by the mobile application (bundle id) device_id: str = None # the device-id that owns the token (used for logging purposes) class Config: allow_population_by_field_name = True alias_generator = alias_rename class PushRequest(BaseModel): event: str = None # (required for sylk) 'incoming_session', 'incoming_conference' or 'cancel' call_id: str # (required for apple) unique sip parameter sip_from: str # (required for firebase) SIP URI for who is calling from_display_name: str = None # (required for sylk) display name of the caller to: str # SIP URI for who is called media_type: str = None # 'audio', 'video', 'chat', 'sms' or 'file-transfer' reason: str = None # Cancel reason badge: int = 1 + filename: str = None + filetype: str = None class Config: alias_generator = alias_rename class WakeUpRequest(BaseModel): # API expects a json object like: app_id: str # id provided by the mobile application (bundle id) platform: str # 'firebase', 'android', 'apple' or 'ios' event: str = None # (required for sylk) 'incoming_session', 'incoming_conference', 'cancel' or 'message' token: str # destination device token in hex device_id: str = None # the device-id that owns the token (used for logging purposes) call_id: str # (required for apple) unique sip parameter sip_from: str # (required for firebase) SIP URI for who is calling from_display_name: str = None # (required for sylk) display name of the caller sip_to: str # SIP URI for who is called media_type: str = None # 'audio', 'video', 'chat', 'sms' or 'file-transfer' silent: bool = True # True for silent notification reason: str = None # Cancel reason badge: int = 1 + filename: str = None + filetype: str = None class Config: alias_generator = alias_rename @root_validator(pre=True) def check_required_items_by_app(cls, values): app_id, platform = values.get('app-id'), values.get('platform') if not app_id: raise ValueError("Field 'app-id' required") if not platform: raise ValueError("Field 'platform' required") platform = fix_platform_name(platform) if platform not in ('firebase', 'apple'): raise ValueError(f"'{platform}' platform is not configured") pns_register = settings.params.pns_register if (app_id, platform) not in pns_register.keys(): raise ValueError(f"{platform.capitalize()} {app_id} app " f"is not configured") try: name = pns_register[(app_id, platform)]['name'] check_items = apps_items[name] missing_items = [] for item in check_items: if values.get(item) is None: missing_items.append(item) if missing_items: missing_items_show = [] for item in missing_items: if item in ('sip_to', 'sip_from', 'device_id'): item = item.split('_')[1] else: item = item.replace('-', '_') missing_items_show.append(item) raise ValueError(f"'{' ,'.join(missing_items)}' " f"item(s) missing.") except KeyError: pass event = values.get('event') if event != 'cancel': media_type = values.get('media-type') if not media_type: raise ValueError("Field media-type required") - if media_type not in ('audio', 'video', 'chat', 'sms', 'file-transfer'): + if media_type not in ('audio', 'video', 'image', 'chat', 'sms', 'file-transfer'): raise ValueError("media-type must be 'audio', 'video', " - "'chat', 'sms', 'file-transfer'") + "'chat', 'sms', 'file-transfer', 'image'") if 'linphone' in name: if event: if event != 'incoming_session': raise ValueError('event not found (must be incoming_sesion)') else: values['event'] = 'incoming_session' return values @validator('platform') def platform_valid_values(cls, v): - if v not in ('apple', 'ios', 'android', 'firebase','fcm', 'apns'): + if v not in ('apple', 'ios', 'android', 'firebase', 'fcm', 'apns'): raise ValueError("platform must be 'apple', 'android' or 'firebase'") return v @validator('event') def event_valid_values(cls, v): - if v not in ('incoming_session', 'incoming_conference_request', 'cancel', 'message'): - raise ValueError("event must be 'incoming_session', 'incoming_conference_request', 'cancel' or 'message'") + if v not in ('incoming_session', 'incoming_conference_request', 'cancel', 'message', 'transfer'): + raise ValueError("event must be 'incoming_session', 'incoming_conference_request', 'cancel' or 'message', 'transfer'") return v diff --git a/pushserver/resources/notification.py b/pushserver/resources/notification.py index e95ab7c..5f1cecb 100644 --- a/pushserver/resources/notification.py +++ b/pushserver/resources/notification.py @@ -1,97 +1,98 @@ 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, self.wp_request.badge] + self.wp_request.reason, self.wp_request.badge, + self.wp_request.filename, self.wp_request.filetype] @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_dict = payload_class(*self.args).payload try: payload = json.dumps(payload_dict) except Exception: payload = None 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 diff --git a/scripts/sylk-pushclient-v2 b/scripts/sylk-pushclient-v2 index d6204f6..56aedf3 100755 --- a/scripts/sylk-pushclient-v2 +++ b/scripts/sylk-pushclient-v2 @@ -1,133 +1,137 @@ #!/usr/bin/python # import json # import logging import re import sys import requests # try: # import pymysql # except ImportError: # pass from argparse import ArgumentParser if __name__ == '__main__': parser = ArgumentParser() subparsers = parser.add_subparsers(dest='action') parser.add_argument('--url', dest='url', required=False, default='http://localhost:8400', help='Base push URL') parser.add_argument('--account', dest='account', required=True, help='Account') subparserA = subparsers.add_parser('push', help='Send push request') subparserA.add_argument('--mediatype', dest='media_type', default="audio", required=False, help='Audio, Video or Message') subparserA.add_argument('--callid', dest='call_id', required=True, help='Call ID') subparserA.add_argument('--event', dest='event', required=False, help='Event', default='incoming_session') subparserA.add_argument('--from', dest='from_uri', required=True, help='From') subparserA.add_argument('--from_name', dest='from_name', required=False, help='From name') subparserA.add_argument('--to', dest='to_uri', required=True, help='To') subparserA.add_argument('--reason', dest='reason', required=False, help='Reason') subparserA.add_argument('--badge', dest='badge', default=1, required=False, help='Badge to display') subparserA.add_argument('--deviceid', dest='device_id', default=None, required=False, help='Device Id/Sip instance') + subparserA.add_argument('--filename', dest='filename', default=None, required=False, help='Filetype') + subparserA.add_argument('--filetype', dest='filetype', default=None, required=False, help='Filetype') subparserB = subparsers.add_parser('add', help='Add a push token') subparserB.add_argument('--platform', dest='platform', help='Platform') subparserB.add_argument('--appid', dest='appid', required=True, help='App ID') subparserB.add_argument('--token', dest='device_token', required=True, help='Device token') subparserB.add_argument('--deviceid', dest='device_id', required=True, help='Device Id') subparserB.add_argument('--silent', dest='silent', default="1", required=False, help='Silent') subparserB.add_argument('--user_agent', dest='user_agent', default="None", required=False, help='User Agent') subparserC = subparsers.add_parser('remove', help='Remove a push token') subparserC.add_argument('--appid', dest='appid', required=True, help='App ID') subparserC.add_argument('--deviceid', dest='device_id', required=True, help='Device Id') options = parser.parse_args() try: from_uri = re.sub(r'^"|"$', '', options.from_uri) except AttributeError: pass try: from_name = options.from_name.strip('\"') if options.from_name else None except AttributeError: pass try: media_type = options.media_type if ("video" in options.media_type): media_type = 'video' elif ("audio" in options.media_type): media_type = 'audio' except AttributeError: pass if options.url[-1] == '/': options.url = options.url[:-1] url = '{}/{}/{}'.format(options.url, 'v2/tokens', options.account) if options.action == 'add': log_params = {'platform': options.platform, 'app-id': options.appid, 'token': options.device_token, 'device-id': options.device_id, 'silent': options.silent, 'user-agent': options.user_agent} elif options.action == 'remove': log_params = {'app-id': options.appid, 'device-id': options.device_id} else: log_params = {'media-type': media_type, 'event': options.event, 'from': from_uri, 'from-display-name': from_name or from_uri, 'to': options.to_uri, 'call-id': options.call_id, 'badge': options.badge, - 'reason': options.reason} + 'reason': options.reason, + 'filename': options.filename, + 'filetype': options.filetype} if options.device_id is None: url = '{}/{}/{}/push'.format(options.url, 'v2/tokens', options.account) else: url = '{}/{}/{}/push/{}'.format(options.url, 'v2/tokens', options.account, options.device_id) def getMethod(*args, **kwargs): if options.action == 'remove': return requests.delete(*args, **kwargs) else: return requests.post(*args, **kwargs) action = options.action.title() try: r = getMethod(url, timeout=5, json=log_params) print("%s request to %s - %s: %s" % (action, url, r.status_code, r.text)) if r.status_code >= 200 and r.status_code < 300: sys.exit(0) elif r.status_code == 410: body = r.json() try: for result in body['data']: failure = result['body']['_content']['failure'] if failure == 1: # A push client may want to act based on various response codes # https://firebase.google.com/docs/cloud-messaging/http-server-ref#error-codes reason = result['body']['_content']['results'][0]['error'] if reason == 'NotRegistered': print("Token %s must be purged" % token) # q = "delete from push_tokens where token = '%s'" % token # con = pymysql.connect('localhost', 'opensips', 'XYZ', 'opensips') # with con: # cur = con.cursor() # cur.execute(q) except KeyError: pass sys.exit(0) else: print("%s request to %s failed: %d: %s" % (action, url, r.status_code, r.text)) sys.exit(1) except Exception as e: print("%s request to %s failed: connection error" % (action, url)) sys.exit(1)