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
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)
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))
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
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))
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"], )
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))
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))
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 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"], )
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"], )
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)
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)
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)
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
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
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
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 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