Esempio n. 1
0
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
Esempio n. 2
0
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,
    })
Esempio n. 4
0
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
Esempio n. 5
0
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
Esempio n. 6
0
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
Esempio n. 8
0
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
Esempio n. 9
0
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]
Esempio n. 11
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
Esempio n. 12
0
 def __init__(self):
     self.client = Client(getattr(settings, 'NEXMO_API_KEY'),
                          getattr(settings, 'NEXMO_API_SECRET'))
Esempio n. 13
0
    def client(self):
        api_key = self.config.get('key')
        api_secret = self.config.get('secret')

        return Client(key=api_key, secret=api_secret)