def rent_number( sms_callback_url: str, client: nexmo.Client, country_code: str = "US" ) -> dict: """Rents a number for the given country. NOTE: This immediately charges us for the number (for at least a month). """ numbers = client.get_available_numbers( country_code, {"features": "SMS,VOICE", "type": "mobile-lvn"} ) error = RuntimeError("No numbers available.") for number in numbers["numbers"]: try: client.buy_number( {"country": number["country"], "msisdn": number["msisdn"]} ) setup_number( number=number["msisdn"], country=number["country"], sms_callback_url=sms_callback_url, client=client, ) # Normalize the number. number["msisdn"] = normalize_number(number["msisdn"]) return number except nexmo.Error as error: continue raise error
class Nexmo(object): """ Gateway for sending text messages and making phone calls using Nexmo_. You need your Nexmo account credentials, and a registered app account dashboard. ``NEXMO_API_KEY`` Should be set to your account's API key from the Dashboard. ``NEXMO_API_SECRET`` Should be set to your account's authorization token. ``NEXMO_CALLER_ID`` The name of the service to text through. .. _Nexmo: https://www.nexmo.com/ """ def __init__(self): self.client = Client(getattr(settings, 'NEXMO_API_KEY'), getattr(settings, 'NEXMO_API_SECRET')) # def make_call(self, device, token): def send_sms(self, device, token): body = ugettext('Your authentication token is %s') % token opts = {} opts['to'] = device.number.as_e164, opts['from'] = getattr(settings, 'NEXMO_CALLER_ID') or 'DjangoApp' opts['body'] = body self.client.send_message(opts)
def setup_number(number: str, country: str, sms_callback_url: str, client: nexmo.Client): client.update_number({ "msisdn": number, "country": country, "moHttpUrl": sms_callback_url, "voiceCallbackType": "app", "voiceCallbackValue": client.application_id, })
def handle_member_answer( event_number: str, member_number: str, origin_conversation_uuid: str, origin_call_uuid: str, client: nexmo.Client, ): """Connects an organizer to a call-in-progress when they answer.""" # Members can actually be part of multiple events, so look up the event # separately. member = db.get_member_by_number(member_number) event = db.get_event_by_number(event_number) if member is None or event is None: error_ncco = [{ "action": "talk", "text": common_text.voice_answer_error }] return error_ncco client.send_speech( origin_call_uuid, text=common_text.voice_answer_announce.format(member=member)) ncco = [ { "action": "talk", "text": common_text.voice_answer_greeting.format(member=member, event=event), }, { "action": "conversation", "name": origin_conversation_uuid, "startOnEnter": True, "endOnExit": True, }, ] audit_log.log( audit_log.Kind.VOICE_CONVERSATION_ANSWERED, description=f"{member.name} answered {origin_conversation_uuid[-12:]}.", user="******", event=event, ) return ncco
def rent_number(sms_callback_url: str, client: nexmo.Client, country_code: str = "US") -> dict: """Rents a number for the given country. NOTE: This immediately charges us for the number (for at least a month). """ # Try to get SMS and VOICE numbers first. numbers = client.get_available_numbers(country_code, { "features": "SMS,VOICE", "type": "mobile-lvn" }) # If that fails, get a VOICE-only number. if not numbers.get("numbers", []): numbers = client.get_available_numbers(country_code, { "features": "VOICE", "type": "mobile-lvn" }) error = RuntimeError("No numbers available.") for number in numbers.get("numbers", []): try: client.buy_number({ "country": number["country"], "msisdn": number["msisdn"] }) setup_number( number=number["msisdn"], country=number["country"], sms_callback_url=sms_callback_url, client=client, ) # normalize the number. Nexmo sends it back in E164 format *without* the leading + number["msisdn"] = normalize_e164_number(number["msisdn"]) return number except nexmo.Error as nexmo_error: error = nexmo_error continue raise error
def handle_member_answer( event_number: str, member_number: str, origin_conversation_uuid: str, origin_call_uuid: str, client: nexmo.Client, ): """Connects an organizer to a call-in-progress when they answer.""" # Members can actually be part of multiple events, so look up the event # separately. member = db.get_member_by_number(member_number) event = db.get_event_by_number(event_number) if member is None or event is None: error_ncco = [{ "action": "talk", "text": ("Oh no, an error occurred and we couldn't find the event or " "member entry for this call."), }] return error_ncco client.send_speech(origin_call_uuid, text=f"{member.name} is joining this call.") ncco = [ { "action": "talk", "text": f"Hello {member.name}, connecting you to {event.name}.", }, { "action": "conversation", "name": origin_conversation_uuid, "startOnEnter": True, "endOnExit": True, }, ] return ncco
def send_sms(sender: str, to: str, message: str, client: nexmo.Client) -> dict: """Sends an SMS. ``sender`` and ``to`` must be in proper long form. """ # Nexmo is apparently picky about + being in the sender. sender = sender.strip("+") resp = client.send_message({"from": sender, "to": to, "text": message}) # Nexmo client incorrectly treats failed messages as successful error_text = resp["messages"][0].get("error-text") if error_text: raise nexmo.ClientError(error_text) return resp
def send_sms(sender: str, to: str, message: str, client: nexmo.Client) -> dict: """Sends an SMS. ``sender`` and ``to`` must be in proper long form. """ # This has to be removed at some point. Sleep a little to avoid hitting rate limits. time.sleep(0.3) # Nexmo is apparently picky about + being in the sender. sender = sender.strip("+") logging.info(f"Sending from {sender} to {to} message length {len(message)}") resp = client.send_message({"from": sender, "to": to, "text": message}) # Nexmo client incorrectly treats failed messages as successful error_text = resp["messages"][0].get("error-text") if error_text: raise nexmo.ClientError(error_text) return resp
def handle_inbound_call( reporter_number: str, event_number: str, conversation_uuid: str, call_uuid: str, host: str, client: nexmo.Client, ) -> List[dict]: # Get the event. If there's no event, tell the user that something went # wrong. event = db.get_event_by_number(event_number) if event is None: error_ncco = [{"action": "talk", "text": common_text.voice_no_event}] return error_ncco # Make sure the number isn't blocked. if db.check_if_blocked(event=event, number=reporter_number): error_ncco = [{"action": "talk", "text": common_text.voice_blocked}] return error_ncco # Get the members for the event. If there are no members, tell the user. :( event_members = list(db.get_verified_event_members(event)) if not event_members: error_ncco = [{"action": "talk", "text": common_text.voice_no_members}] return error_ncco # Make sure that the user is a verified member of this hotline. # If not, bounce them. if not db.get_verified_member_for_event_by_number(event, reporter_number): error_ncco = [{"action": "talk", "text": common_text.voice_non_member}] return error_ncco # Great, we have an event. Greet the user. if event.voice_greeting is not None and event.voice_greeting.strip(): greeting = event.voice_greeting else: greeting = common_text.voice_default_greeting.format(event=event) # NCCOs to be given to the caller. reporter_nccos: List[dict] = [] # Greet the reporter. reporter_nccos.append({"action": "talk", "text": greeting}) # Start a "conversation" (conference call) reporter_nccos.append({ "action": "conversation", "name": conversation_uuid, "eventMethod": "POST", "musicOnHoldUrl": [HOLD_MUSIC], "endOnExit": False, "startOnEnter": False, }) # Add all of the event members to the conference call. for member in event_members: client.create_call({ "to": [{ "type": "phone", "number": member.number }], "from": { "type": "phone", "number": event.primary_number }, "answer_url": [ f"https://{host}/telephony/connect-to-conference/{conversation_uuid}/{call_uuid}" ], "answer_method": "POST", }) # TODO NZ: log name instead of number. # Last four digits of number is {reporter_number[-4:]} audit_log.log( audit_log.Kind.VOICE_CONVERSATION_STARTED, description= f"A new voice conversation was started. UUID is {conversation_uuid[-12:]}.", event=event, reporter_number=reporter_number, ) return reporter_nccos
def get_number_info(number: str, client: nexmo.Client) -> dict: return client.get_account_numbers(pattern=number)["numbers"][0]
def handle_inbound_call( event_number: str, conversation_uuid: str, call_uuid: str, host: str, client: nexmo.Client, ) -> List[dict]: # Get the event. If there's no event, tell the user that something went # wrong. event = db.get_event_by_number(event_number) if event is None: error_ncco = [{ "action": "talk", "text": "No event was found for this number. Please reach out to the event staff directly for assistance.", }] return error_ncco # Get the members for the event. If there are no members, tell the user. :( event_members = list(db.get_verified_event_members(event)) if not event_members: error_ncco = [{ "action": "talk", "text": ("Unfortunately, there are no verified members for this event's hotline. " "Please reach out to the event staff directly for assistance."), }] return error_ncco # Great, we have an event. Greet the user. greeting = ( f"Thank you for calling the Code of Conduct hotline for {event.name}. This will dial all " f"of the hotline members and put you on hold until one is able to answer." ) # NCCOs to be given to the caller. reporter_nccos: List[dict] = [] # Greet the reporter. reporter_nccos.append({"action": "talk", "text": greeting}) # Start a "conversation" (conference call) reporter_nccos.append({ "action": "conversation", "name": conversation_uuid, "eventMethod": "POST", "musicOnHoldUrl": [HOLD_MUSIC], "endOnExit": False, "startOnEnter": False, }) # Add all of the event members to the conference call. for member in event_members: client.create_call({ "to": [{ "type": "phone", "number": member.number }], "from": { "type": "phone", "number": event.primary_number }, "answer_url": [ f"https://{host}/telephony/connect-to-conference/{conversation_uuid}/{call_uuid}" ], "answer_method": "POST", }) audit_log.log( audit_log.Kind.VOICE_CONVERSATION_STARTED, description= f"A new voice conversation was started, uuid is {conversation_uuid}", event=event, ) return reporter_nccos
def __init__(self): self.client = Client(getattr(settings, 'NEXMO_API_KEY'), getattr(settings, 'NEXMO_API_SECRET'))
def client(self): api_key = self.config.get('key') api_secret = self.config.get('secret') return Client(key=api_key, secret=api_secret)