def subscribe_sms_dr_endpoint(endpoint_url, silent_failure=False): sender_address = get_config('sender_address') payload = { 'deliveryReceiptSubscription': { 'callbackReference': { 'notifyURL': endpoint_url } } } url = "{api}/outbound/{addr}/subscriptions".format( api=get_config('smsmt_url'), addr=quote(sender_address)) headers = get_standard_header() req = requests.post(url, headers=headers, json=payload) try: assert req.status_code == 201 resp = req.json() except AssertionError: exp = OrangeAPIError.from_request(req) logger.error("Unable to subscribe an SMS-DR endpoint. {exp}" .format(exp=exp)) logger.exception(exp) if not silent_failure: raise exp return False subscription_id = resp['deliveryReceiptSubscription'] \ .get('resourceURL', '').rsplit('/', 1)[-1] if subscription_id: update_config({'smsmtdr_subsription_id': subscription_id}, save=True) return bool(subscription_id)
def request_token(silent_failure=False): url = "{oauth_url}/token".format(oauth_url=get_config('oauth_url')) basic_header = b64encode( "{client_id}:{client_secret}".format( client_id=get_config('client_id'), client_secret=get_config('client_secret'))) headers = {'Authorization': "Basic {b64}".format(b64=basic_header)} payload = {'grant_type': 'client_credentials'} req = requests.post(url, headers=headers, data=payload) resp = req.json() if "token_type" in resp: expire_in = int(resp['expires_in']) token_data = { 'token': resp['access_token'], 'token_expiry': datetime.datetime.now() + datetime.timedelta( days=expire_in / ONE_DAY, seconds=expire_in % ONE_DAY) - datetime.timedelta( days=0, seconds=60)} update_config(token_data, save=True) return token_data else: exp = OrangeAPIError.from_request(req) logger.error("Unable to retrieve token. {}".format(exp)) logger.exception(exp) if not silent_failure: raise exp return False
def submit_sms_mt(address, message, sender_name=get_config('default_sender_name'), callback_data=None): return submit_sms_mt_request( mt_payload(dest_addr=cleaned_msisdn(address), message=message, sender_address=get_config('sender_address'), sender_name=sender_name))
def send_sms(to_addr, message, as_addr=get_config('default_sender_name'), db_save=get_config('use_db')): ''' SMS-MT shortcut function ''' to_addr = cleaned_msisdn(to_addr) if not db_save: return submit_sms_mt(to_addr, message, as_addr) msg = SMSMessage.create_mt(to_addr, message, as_addr, SMSMessage.PENDING) success = submit_sms_mt_request(msg.to_mt(), msg) msg.sending_status = msg.SENT if success else msg.FAILED_TO_SEND msg.save() return success, msg
def record_dr_from_payload(cls, payload): # no DR support in non-DB mode if not get_config('use_db'): return uuid = payload.get('callbackData') msg = cls.get_or_none(uuid) if msg is None: raise ValueError( "SMS-DR reference unreachable SMS-MT `{uuid}`".format( uuid=uuid)) kwargs = { 'sms_type': cls.DR, 'delivery_status_on': aware_datetime_from_iso( payload.get('delivery_status_on', datetime_to_iso(timezone.now()))), 'status': cls.DELIVERY_STATUS_MATRIX.get( payload.get('deliveryInfo', {}).get('deliveryStatus', cls.NOT_DELIVERED)) } msg.update(**kwargs) msg.save() return msg
def home(request): context = {'page': 'home', 'disable_tester': not get_config('enable_tester', False)} from django.core.urlresolvers import reverse url_mo = request.build_absolute_uri(reverse('oapisms_mo')) url_mtdr = request.build_absolute_uri(reverse('oapisms_dr')) try: smsmt_dr_endpoint = get_sms_dr_endpoint(False) except Exception as exp: messages.error(request, exp) smsmt_dr_endpoint = None context.update({ 'endpoints': [ ("SMS-MO", url_mo, None), ("SMS-DR", url_mtdr, ('oapisms_register_smsdr_endpoint', "register")), ("Registered SMS-DR", smsmt_dr_endpoint, ('oapisms_unregister_smsdr_endpoint', "unregister")) ] }) return render(request, 'orangeapisms/home.html', context)
def get_handler(slug): stub = 'orangeapisms.stub' mod = get_config('handler_module') if mod is None: mod = stub return import_path('handle_{}'.format(slug), module=mod, fallback=stub)
def get_sms_dr_subscriptions(silent_failure=False): subscription_id = get_config('smsmtdr_subsription_id') if not subscription_id: return {} url = "{api}/outbound/subscriptions/{subscription}".format( api=get_config('smsmt_url'), subscription=subscription_id) headers = get_standard_header() req = requests.get(url, headers=headers) try: assert req.status_code == 200 return req.json() except AssertionError: exp = OrangeAPIError.from_request(req) logger.error("Unable to retrieve contracts. {exp}".format(exp=exp)) logger.exception(exp) if not silent_failure: raise exp
def get_contracts(silent_failure=False): url = "{api}/contracts".format(api=get_config('smsadmin_url')) headers = get_standard_header() req = requests.get(url, headers=headers) try: assert req.status_code == 200 return req.json() except AssertionError: exp = OrangeAPIError.from_request(req) logger.error("Unable to retrieve contracts. {exp}".format(exp=exp)) logger.exception(exp) if not silent_failure: raise exp
def unsubscribe_sms_dr_endpoint(subscription_id, silent_failure=False): sender_address = get_config('sender_address') url = "{api}/outbound/{addr}/subscriptions/{sub}".format( api=get_config('smsmt_url'), addr=quote(sender_address), sub=subscription_id) headers = get_standard_header() req = requests.delete(url, headers=headers) try: assert req.status_code == 204 update_config({'smsmtdr_subsription_id': None}, save=True) except AssertionError: exp = OrangeAPIError.from_request(req) logger.error("Unable to unsubscribe an SMS-DR endpoint. {exp}" .format(exp=exp)) logger.exception(exp) if not silent_failure: raise exp return False return True
def cleaned_msisdn(to_addr): """ fixes common mistakes to make to_addr a MSISDN - removes extra chars - starts with a + - adds prefix if it seems missing """ if not get_config('fix_msisdn'): return to_addr # harmonize intl. number format (00xxx) to +xxx to_addr = re.sub(r"^00", "+", to_addr) # if a suffix was supplied, fix chars only if to_addr.startswith('+'): return "+{addr}".format(addr=re.sub(r"\D", "", to_addr)) # no prefix, make sure to remove default prefix if present and fix chars prefix = get_config('country_prefix') to_addr = re.sub(r"\D", "", to_addr) if to_addr.startswith(prefix): to_addr = re.sub(r"^{prefix}".format(prefix=prefix), "", to_addr) return "+{prefix}{addr}".format(prefix=prefix, addr=to_addr)
def create_mt(cls, destination_address, content, sender_address=None, sending_status=SENT): kwargs = { 'direction': cls.OUTGOING, 'sms_type': cls.MT, 'created_on': timezone.now(), 'sender_address': sender_address, 'destination_address': destination_address, 'content': content, 'status': sending_status, } if not get_config('use_db'): return cls(**kwargs) return cls.objects.create(**kwargs)
def create_mo_from_payload(cls, payload): kwargs = { 'direction': cls.INCOMING, 'sms_type': cls.MO, 'status': cls.RECEIVED, 'sender_address': cls.clean_address(payload.get('senderAddress')), 'destination_address': cls.clean_address(payload.get('destinationAddress')), 'message_id': payload.get('messageId'), 'content': payload.get('message'), 'created_on': aware_datetime_from_iso(payload.get('dateTime')) } if not get_config('use_db'): return cls(**kwargs) return cls.objects.create(**kwargs)
def home(request): context = {'page': 'home', 'disable_tester': not get_config('enable_tester', False)} from django.core.urlresolvers import reverse url_mo = request.build_absolute_uri(reverse('oapisms_mo')) url_mt = request.build_absolute_uri(reverse('oapisms_dr')) context.update({ 'endpoints': [ ('SMS-MO', url_mo), ('SMS-DR', url_mt), ] }) return render(request, 'orangeapisms/home.html', context)
def handle_request(active_form, form): if active_form == 'smsmt': success, msg = send_sms( to_addr=form.cleaned_data.get('destination_address'), message=form.cleaned_data.get('content'), as_addr=form.cleaned_data.get('sender_name') or get_config('default_sender_name')) handle_smsmt(msg) return success, "Sent {}".format(msg) elif active_form == 'fsmsmt': msg = SMSMessage.create_mt( destination_address=form.cleaned_data.get( 'destination_address'), content=form.cleaned_data.get('content'), sender_address=form.cleaned_data.get('sender_address'), sending_status=form.cleaned_data.get('status')) handle_smsmo(msg) return True, "Sent {}".format(msg) elif active_form == 'fsmsmo': msg = SMSMessage.create_mo_from_payload({ 'senderAddress': "tel:{}".format( form.cleaned_data.get('sender_address')), 'destinationAddress': form.cleaned_data.get( 'destination_address'), 'messageId': form.cleaned_data.get('message_id') or None, 'message': form.cleaned_data.get('content'), 'dateTime': datetime_to_iso( form.cleaned_data.get('created_on')) }) handle_smsmo(msg) return True, "Received {}".format(msg) elif active_form == 'fsmsdr': msg = SMSMessage.record_dr_from_payload({ 'callbackData': form.cleaned_data.get('uuid'), 'delivery_status_on': datetime_to_iso( form.cleaned_data.get('delivery_on')), 'deliveryInfo': { 'deliveryStatus': form.cleaned_data.get('status') } }) return True, "Updated {}".format(msg) else: return False, "Unknown action `{}`".format(active_form)
def register_smsdr_endpoint(request): from django.core.urlresolvers import reverse endpoint_url = request.build_absolute_uri(reverse('oapisms_dr')) try: assert subscribe_sms_dr_endpoint(endpoint_url) is True feedback = "Successfuly set {url} as SMS-DR endpoint. " \ "Subscription ID: {sub}" \ .format(url=endpoint_url, sub=get_config('smsmtdr_subsription_id')) lvl = messages.SUCCESS except AssertionError: feedback = "Unable to subscribe SMS-DR endpoint." lvl = messages.WARNING except Exception as e: feedback = e.__str__() lvl = messages.WARNING messages.add_message(request, lvl, feedback) return redirect('oapisms_home')
def check_balance(request): now = timezone.now() balance, expiry = get_sms_balance() if expiry <= now: balance_msg = "{balance} remaining SMS expired on {date}. Top-up " \ "account to extend expiry date ({country})" else: balance_msg = "{balance} SMS remaining until {date} ({country})" try: feedback = balance_msg.format(balance=balance, country=get_config('country'), date=expiry.strftime('%c')) lvl = messages.INFO except Exception as e: feedback = e.__str__() lvl = messages.WARNING messages.add_message(request, lvl, feedback) return redirect('oapisms_tester')
def unregister_smsdr_endpoint(request): subscription_id = get_config('smsmtdr_subsription_id') if subscription_id is not None: try: assert unsubscribe_sms_dr_endpoint(subscription_id) is True feedback = "Successfuly unsubscribed SMS-DR endpoint." lvl = messages.SUCCESS except AssertionError: feedback = "Unable to unsubscribe SMS-DR endpoint." lvl = messages.WARNING except Exception as e: feedback = e.__str__() lvl = messages.WARNING else: lvl = messages.WARNING feedback = "No registered SMS-DR endpoint to unsubscribe." messages.add_message(request, lvl, feedback) return redirect('oapisms_home')
class SMSMTForm(forms.Form): destination_address = forms.CharField( widget=forms.TextInput(attrs={'required': True, 'max_length': 255})) sender_name = forms.CharField( required=False, widget=forms.TextInput(attrs={ 'placeholder': get_config('default_sender_name'), 'max_length': 255})) content = forms.CharField(widget=forms.Textarea( attrs={'rows': 3, 'max_length': 1600, 'required': True})) @classmethod def get_initial(cls): return {} def clean_destination_address(self): return cleaned_msisdn(self.cleaned_data.get('destination_address'))
def check_balance(request): now = timezone.now() balance, expiry = get_sms_balance() if expiry <= now: balance_msg = "{balance} remaining SMS expired on {date}. Top-up " \ "account to extend expiry date ({country})" else: balance_msg = "{balance} SMS remaining until {date} ({country})" try: feedback = balance_msg.format( balance=balance, country=get_config('country'), date=expiry.strftime('%c')) lvl = messages.INFO except Exception as e: feedback = e.__str__() lvl = messages.WARNING messages.add_message(request, lvl, feedback) return redirect('oapisms_tester')
def record_dr_from_payload(cls, payload): # no DR support in non-DB mode if not get_config('use_db'): return uuid = payload.get('callbackData') msg = cls.get_or_none(uuid) if msg is None: raise ValueError("SMS-DR reference unreachable SMS-MT `{uuid}`" .format(uuid=uuid)) kwargs = { 'sms_type': cls.DR, 'delivery_status_on': aware_datetime_from_iso( payload.get('delivery_status_on', datetime_to_iso(timezone.now()))), 'status': cls.DELIVERY_STATUS_MATRIX.get( payload.get('deliveryInfo', {}) .get('deliveryStatus', cls.NOT_DELIVERED)) } msg.update(**kwargs) msg.save() return msg
def get_sms_balance(country=get_config('country')): contracts = get_contracts() expiry = None balance = 0 for contract in contracts.get('partnerContracts', {}).get('contracts', []): if not contract['service'] == SMS_SERVICE: continue for sc in contract.get('serviceContracts', []): if not sc['country'] == country: continue if not sc['service'] == SMS_SERVICE: continue balance += sc['availableUnits'] expires = datetime_from_iso(sc['expires']) if expiry is None or expiry < expires: expiry = expires if expiry is not None: expiry = API_TZ.localize(expiry) return balance, expiry
class FSMSMOForm(forms.Form): sender_address = forms.CharField( widget=forms.TextInput(attrs={'required': True, 'max_length': 255})) created_on = forms.DateTimeField() destination_address = forms.CharField( required=False, widget=forms.TextInput(attrs={ 'placeholder': get_config('sender_address'), 'max_length': 255})) message_id = forms.CharField( required=False, widget=forms.TextInput(attrs={'max_length': 64, 'required': False})) content = forms.CharField(widget=forms.Textarea( attrs={'rows': 3, 'max_length': 1600, 'required': True})) @classmethod def get_initial(cls): return {'created_on': timezone.now()} def clean_destination_address(self): return cleaned_msisdn(self.cleaned_data.get('destination_address'))
def do_submit_sms_mt_request(payload, message=None, silent_failure=False): ''' Use submit_sms_mt_request actual submission of API request for SMS-MT ''' def update_status(msg, success): if msg is None: return msg.update_status(msg.SENT if success else msg.FAILED_TO_SEND) return success sender_address = payload['outboundSMSMessageRequest']['senderAddress'] url = "{api}/outbound/{addr}/requests".format( api=get_config('smsmt_url'), addr=quote(sender_address)) headers = get_standard_header() req = requests.post(url, headers=headers, json=payload) try: assert req.status_code == 201 resp = req.json() except AssertionError: exp = OrangeAPIError.from_request(req) logger.error("Unable to transmit SMS-MT. {exp}".format(exp=exp)) logger.exception(exp) update_status(message, False) if not silent_failure: raise exp return False rurl = resp['outboundSMSMessageRequest'] \ .get('resourceURL', '').rsplit('/', 1)[-1] if message is not None and rurl: message.update_reference(rurl) return update_status(message, True) return bool(rurl)
def to_mt(self): from orangeapisms.utils import mt_payload return mt_payload(dest_addr=self.destination_address, message=self.content, sender_address=get_config('sender_address'), sender_name=self.sender_address)
def update_status(self, status): self.status = status if not get_config('use_db'): return self.save()
def update_reference(self, reference_code): self.reference_code = reference_code if not get_config('use_db'): return self.save()
def _decorated(*args, **kwargs): if not get_config('enable_tester', False): raise PermissionDenied return aview(*args, **kwargs)
modname, __, attr = name.rpartition('.') if not modname: # single module name return __import__(attr) m = __import__(modname, fromlist=[attr]) return getattr(m, attr) def ret(mod, call): return do_import('{module}.{callable}' .format(module=mod, callable=call)) try: return ret(module, callable_name) except (ImportError, AttributeError): if fallback is None: return None return ret(fallback, callable_name) SEND_ASYNC = get_config('send_async') CELERY_TASK = import_path('submit_sms_mt_request_task', get_config('celery_module')) def async_check(func): ''' decorator to route API-call request to celery depending on config ''' def _decorated(*args, **kwargs): if SEND_ASYNC and CELERY_TASK: return CELERY_TASK.apply_async(args) return func(*args, **kwargs) return _decorated
def get_token(): token_expiry = get_config('token_expiry') if token_expiry is None or token_expiry < datetime.datetime.now() \ + datetime.timedelta(days=0, seconds=60): assert request_token() return get_config('token')
# single module name return __import__(attr) m = __import__(modname, fromlist=[attr]) return getattr(m, attr) def ret(mod, call): return do_import('{module}.{callable}'.format(module=mod, callable=call)) try: return ret(module, callable_name) except (ImportError, AttributeError): if fallback is None: return None return ret(fallback, callable_name) SEND_ASYNC = get_config('send_async') CELERY_TASK = import_path('submit_sms_mt_request_task', get_config('celery_module')) def async_check(func): ''' decorator to route API-call request to celery depending on config ''' def _decorated(*args, **kwargs): if SEND_ASYNC and CELERY_TASK: return CELERY_TASK.apply_async(args) return func(*args, **kwargs) return _decorated