示例#1
0
def maybe_handle_verification(member_number: str, message: str):
    """Checks if the message is a verification message for the given number."""
    pending_member_record = db.find_pending_member_by_number(member_number)

    if not pending_member_record:
        return False

    if message.strip().lower() not in ("ok", "yes", "okay"):
        print(f"Verification message was not okay, was {message}")
        # This was "handled", even though verification wasn't approved.
        return True

    pending_member_record.verified = True
    pending_member_record.save()

    audit_log.log(
        audit_log.Kind.MEMBER_NUMBER_VERIFIED,
        description=f"{pending_member_record.name} verified their number.",
        event=pending_member_record.event,
    )

    sender = _get_sender_for_member(pending_member_record)
    reply = "Thank you, your number is confirmed."
    lowlevel.send_sms(sender, member_number, reply)

    return True
示例#2
0
def numbers(event, user):
    members = db.get_event_members(event)
    form = forms.AddMemberForm(flask.request.form)

    if flask.request.method == "POST" and form.validate():
        member = db.new_event_member(event)
        form.populate_obj(member)
        member.save()

        audit_log.log(
            audit_log.Kind.MEMBER_ADDED,
            description=f"{flask.g.user['name']} added {member.name}.",
            event=event,
            user=user["user_id"],
        )

        # Start the verification process.
        hotline.telephony.verification.start_member_verification(member)

        return flask.redirect(
            flask.url_for(flask.request.endpoint, event_slug=event.slug))

    return flask.render_template("events/numbers.html",
                                 event=event,
                                 members=members,
                                 form=form)
示例#3
0
def acquire(event, user):
    new_number = db.acquire_number(event)

    audit_log.log(
        audit_log.Kind.NUMBER_ACQUIRED,
        description=f"{flask.g.user['name']} acquired the number {new_number}",
        event=event,
        user=user["user_id"],
    )

    return flask.redirect(flask.url_for(".numbers", event_slug=event.slug))
示例#4
0
def manually_verify(member):
    member.verified = True
    member.save()

    audit_log.log(
        audit_log.Kind.MEMBER_NUMBER_VERIFIED,
        description=f"An admin manually verified {member.name}'s number.",
        event=member.event,
    )

    return True
示例#5
0
def remove_member(member_id, event, user):
    member = db.get_member(member_id)

    db.remove_event_member(member_id)

    audit_log.log(
        audit_log.Kind.MEMBER_REMOVED,
        description=f"{flask.g.user['name']} removed {member.name}.",
        event=event,
        user=user["user_id"],
    )

    return flask.redirect(flask.url_for(".numbers", event_slug=event.slug))
示例#6
0
def remove_blocklist_item(event: models.Event, blocklist_id: str, user: dict):
    item = models.BlockList.get(models.BlockList.event == event,
                                models.BlockList.id == int(blocklist_id))

    item.delete_instance()

    audit_log.log(
        kind=audit_log.Kind.NUMBER_UNBLOCKED,
        description=
        f"{user['name']} unblocked the number ending in {item.number[-4:]}.",
        event=event,
        user=user["user_id"],
    )
示例#7
0
def remove_organizer(organizer_id, event, user):
    organizer = db.get_event_organizer(organizer_id)

    db.remove_event_organizer(organizer_id)

    audit_log.log(
        audit_log.Kind.ORGANIZER_REMOVED,
        description=f"{flask.g.user['name']} removed {organizer.user_email}.",
        event=event,
        user=user["user_id"],
    )

    return flask.redirect(flask.url_for(".organizers", event_slug=event.slug))
示例#8
0
def release(event, user):
    previous_number = event.primary_number
    event.primary_number = None
    event.primary_number_id = None
    event.save()

    audit_log.log(
        audit_log.Kind.NUMBER_RELEASED,
        description=f"{flask.g.user['name']} released the number {previous_number}",
        event=event,
        user=user["user_id"],
    )

    return flask.redirect(flask.url_for(".numbers", event_slug=event.slug))
示例#9
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
示例#10
0
def create_blocklist_item(event: models.Event, log_id: str, user: dict):
    log = models.AuditLog.get(models.AuditLog.event == event,
                              models.AuditLog.id == int(log_id))

    models.BlockList.create(event=event,
                            number=log.reporter_number,
                            blocked_by=user["name"])

    audit_log.log(
        kind=audit_log.Kind.NUMBER_BLOCKED,
        description=
        f"{user['name']} blocked the number ending in {log.reporter_number[-4:]}.",
        event=event,
        user=user["user_id"],
    )
示例#11
0
def remove_event_chat(event: models.Event, chat_id: str, user: dict):
    item = models.SmsChat.get(models.SmsChat.event == event,
                              models.SmsChat.id == int(chat_id))

    item.delete_instance()

    # Delete all connections
    models.SmsChatConnection.delete().where(
        models.SmsChatConnection.smschat == item).execute()

    audit_log.log(
        kind=audit_log.Kind.CHAT_DELETED,
        description=
        f"{user['name']} deleted the chat with the relay number {item.relay_number}.",
        event=event,
        user=user["user_id"],
    )
示例#12
0
def details(event, user):
    form = forms.EventEditForm(flask.request.form, event)

    if flask.request.method == "POST" and form.validate():
        form.populate_obj(event)
        event.save()

        audit_log.log(
            audit_log.Kind.EVENT_MODIFIED,
            description=f"{flask.g.user['name']} updated the event details.",
            event=event,
            user=user["user_id"],
        )

        return flask.redirect(
            flask.url_for(flask.request.endpoint, event_slug=event.slug))

    return flask.render_template("events/edit.html", event=event, form=form)
示例#13
0
def add():
    user = flask.g.user
    form = forms.EventEditForm(flask.request.form)

    if flask.request.method == "POST" and form.validate():
        event = db.new_event()
        form.populate_obj(event)
        event.save()
        db.add_event_organizer(event, user)

        audit_log.log(
            audit_log.Kind.EVENT_MODIFIED,
            description=f"{flask.g.user['name']} created the event.",
            event=event,
            user=user["user_id"],
        )

        return flask.redirect(flask.url_for(".numbers", event_slug=event.slug))

    return flask.render_template("events/add.html", form=form)
示例#14
0
def organizers(event, user):
    organizers = db.get_event_organizers(event)

    form = forms.AddOrganizerForm(flask.request.form)

    if flask.request.method == "POST" and form.validate():
        db.add_pending_event_organizer(event, form.email.data)

        audit_log.log(
            audit_log.Kind.ORGANIZER_ADDED,
            description=f"{flask.g.user['name']} invited {form.email.data}.",
            event=event,
            user=user["user_id"],
        )

        return flask.redirect(
            flask.url_for(flask.request.endpoint, event_slug=event.slug))

    return flask.render_template("events/organizers.html",
                                 event=event,
                                 organizers=organizers,
                                 form=form)
示例#15
0
def maybe_handle_stop(
    sender: str, relay: str, message: str, smschat: models.SmsChat
) -> bool:
    """Handle a potential stop request for a given number and SmsChat."""
    if message.strip().lower() != "stop":
        return False

    # Notify other chatroom members.
    room = smschat.room
    room.relay(sender, common_text.sms_left_chat, _send_sms_no_fail)

    # Remove the sender from the chat room.
    removed_user = room.remove_user(sender)
    smschat.save(only=[models.SmsChat.room])

    # Break the chat connection.
    models.SmsChatConnection.delete().where(
        models.SmsChatConnection.smschat == smschat,
        models.SmsChatConnection.user_number == sender,
        models.SmsChatConnection.relay_number == relay,
    ).execute()

    audit_log.log(
        audit_log.Kind.PARTICIPANT_LEFT_CHAT,
        description=f"{removed_user.name} has left the chat room "
        "with relay number"
        "{removed_user.relay}. "
        "The last 4 digits of the their number is {removed_user.number[-4:]}",
        event=smschat.event,
    )

    # Notify the sender they will no longer get messages.
    lowlevel.send_sms(
        sender=relay, to=sender, message=common_text.sms_stop_request_completed
    )

    return True
示例#16
0
def _create_room(event_number: str, reporter_number: str) -> hotline.chatroom.Chatroom:
    """Creates a room for the event with the given primary number.

    The alogrithm is a little tricky here. The event organizers can not use
    the primary number as the chat relay for this chat, so a new number must be
    used.
    """
    # Find the event.
    event = db.get_event_by_number(event_number)

    if not event:
        raise EventDoesNotExist(f"No event for number {event_number}.")

    # Create a chatroom
    chatroom = hotline.chatroom.Chatroom()
    chatroom.add_user(name="Reporter", number=reporter_number, relay=event_number)

    # Find all organizers.
    organizers = list(db.get_verified_event_members(event))

    if not organizers:
        raise NoOrganizersAvailable(f"No organizers found for {event.name}. :/")

    # Find an unused number to use for the organizers' relay.
    # Use the first organizer's number here, as all organizers should be
    # assigned the same relay anyway.
    organizer_number = organizers[0].number
    relay_number = db.find_unused_relay_number(event.primary_number, organizer_number)

    if not relay_number:
        raise NoRelaysAvailable()

    # Now add the organizers and their relay.
    for organizer in organizers:
        chatroom.add_user(
            name=organizer.name, number=organizer.number, relay=relay_number
        )

    # Save the chatroom.
    db.save_room(chatroom, event=event)

    audit_log.log(
        audit_log.Kind.SMS_CONVERSATION_STARTED,
        description=f"A new sms conversation was started last 4 digits of number is {reporter_number[-4:]}",
        event=event,
    )

    # Send welcome messages.
    lowlevel.send_sms(
        sender=event_number,
        to=reporter_number,
        message=f"You have started a new chat with the organizers of {event.name}.",
    )

    for organizer in organizers:
        lowlevel.send_sms(
            sender=relay_number,
            to=organizer.number,
            message=f"This is the beginning of a new chat for {event.name}, the last 4 digits of the reporters number are {reporter_number[-4:]}.",
        )

    return chatroom
示例#17
0
def _create_room(event_number: str,
                 reporter_number: str) -> hotline.chatroom.Chatroom:
    """Creates a room for the event with the given primary number.

    The alogrithm is a little tricky here. The event organizers can not use
    the primary number as the chat relay for this chat, so a new number must be
    used.
    """
    # Find the event.
    event = db.get_event_by_number(event_number)

    if not event:
        raise EventDoesNotExist()

    # Make sure the number isn't blocked.
    if db.check_if_blocked(event=event, number=reporter_number):
        raise NumberBlocked()

    # Create a chatroom
    chatroom = hotline.chatroom.Chatroom()
    chatroom.add_user(name="Reporter",
                      number=reporter_number,
                      relay=event_number)

    # Find all organizers.
    organizers = list(db.get_verified_event_members(event))

    if not organizers:
        raise NoOrganizersAvailable()

    # Find an unused number to use for the organizers' relay.
    relay_number = db.find_unused_relay_number(event)

    if not relay_number:
        raise NoRelaysAvailable()

    # Now add the organizers and their relay.
    for organizer in organizers:
        chatroom.add_user(name=organizer.name,
                          number=organizer.number,
                          relay=relay_number)

    # Save the chatroom.
    db.save_room(chatroom, relay_number=relay_number, event=event)

    audit_log.log(
        audit_log.Kind.SMS_CONVERSATION_STARTED,
        description=
        f"A new sms conversation was started. Last 4 digits of number is {reporter_number[-4:]}",
        event=event,
        reporter_number=reporter_number,
    )

    # Determine the greeting.
    if event.sms_greeting is not None and event.sms_greeting.strip():
        greeting = event.sms_greeting
    else:
        greeting = common_text.sms_default_greeting.format(event=event)

    # Send welcome messages.
    lowlevel.send_sms(sender=event_number,
                      to=reporter_number,
                      message=greeting)

    for organizer in organizers:
        lowlevel.send_sms(
            sender=relay_number,
            to=organizer.number,
            message=common_text.sms_introduction.format(
                event=event, reporter_number=reporter_number[-4:]),
        )

    return chatroom
示例#18
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
示例#19
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