Ejemplo n.º 1
0
def form_session_handler(v, text, msg):
    """
    The form session handler will use the inbound text to answer the next question
    in the open SQLXformsSession for the associated contact. If no session is open,
    the handler passes. If multiple sessions are open, they are all closed and an
    error message is displayed to the user.
    """
    with critical_section_for_smsforms_sessions(v.owner_id):
        if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(v.domain):
            channel = get_channel_for_contact(v.owner_id, v.phone_number)
            running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
                channel)
            if running_session_info.session_id:
                session = SQLXFormsSession.by_session_id(
                    running_session_info.session_id)
                if not session.session_is_open:
                    # This should never happen. But if it does we should set the channel free
                    # and act like there was no available session
                    notify_error(
                        "The supposedly running session was not open and was released. "
                        'No known way for this to happen, so worth investigating.'
                    )
                    XFormsSessionSynchronization.clear_stale_channel_claim(
                        channel)
                    session = None
            else:
                session = None
        else:
            multiple, session = get_single_open_session_or_close_multiple(
                v.domain, v.owner_id)
            if multiple:
                send_sms_to_verified_number(
                    v, get_message(MSG_MULTIPLE_SESSIONS, v))
                return True

        if session:
            session.phone_number = v.phone_number
            session.modified_time = datetime.utcnow()
            session.save()

            # Metadata to be applied to the inbound message
            inbound_metadata = MessageMetadata(
                workflow=session.workflow,
                reminder_id=session.reminder_id,
                xforms_session_couch_id=session._id,
            )
            add_msg_tags(msg, inbound_metadata)

            try:
                answer_next_question(v, text, msg, session)
            except Exception:
                # Catch any touchforms errors
                log_sms_exception(msg)
                send_sms_to_verified_number(
                    v, get_message(MSG_TOUCHFORMS_DOWN, v))
            return True
        else:
            return False
Ejemplo n.º 2
0
def get_inbound_phone_entry(msg):
    if msg.backend_id:
        backend = SQLMobileBackend.load(msg.backend_id, is_couch_id=True)
        if toggles.INBOUND_SMS_LENIENCY.enabled(backend.domain):
            p = None
            if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(
                    backend.domain):
                running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
                    SMSChannel(backend_id=msg.backend_id,
                               phone_number=msg.phone_number))
                contact_id = running_session_info.contact_id
                if contact_id:
                    p = PhoneNumber.get_phone_number_for_owner(
                        contact_id, msg.phone_number)
                if p is not None:
                    return (p, True)
                elif running_session_info.session_id:
                    # This would be very unusual, as it would mean the supposedly running form session
                    # is linked to a phone number, contact pair that doesn't exist in the PhoneNumber table
                    notify_error(
                        "Contact from running session has no match in PhoneNumber table. "
                        "Only known way for this to happen is if you "
                        "unregister a phone number for a contact "
                        "while they are in an active session.",
                        details={'running_session_info': running_session_info})

            if not backend.is_global:
                p = PhoneNumber.get_two_way_number_with_domain_scope(
                    msg.phone_number, backend.domains_with_access)
                return (p, p is not None)

    return (PhoneNumber.get_reserved_number(msg.phone_number), False)
    def test_get_inbound_phone_entry__one_phone_number_multiple_contacts_on__session_no_contact(
            self):
        """Fall back to lenient query if we can't find the contact returned by the sticky session"""
        channel = SMSChannel(self.domain2_backend.couch_id, self.phone_number)
        info = RunningSessionInfo(None, "fake-owner-missing")
        XFormsSessionSynchronization._set_running_session_info_for_channel(
            channel, info, 60)
        self.addCleanup(
            XFormsSessionSynchronization.
            _release_running_session_info_for_channel, info, channel)
        phone_number, has_domain_two_way_scope = self._get_inbound_phone_entry__backend_domain_2(
        )

        self.assertEqual(phone_number.owner_id, "fake-owner-3")
        self.assertTrue(has_domain_two_way_scope)
        self.assertTrue(
            self.domain2_backend.domain_is_authorized(phone_number.domain))
    def test_get_inbound_phone_entry__one_phone_number_multiple_contacts_on__with_session(
            self):
        """Should return the phone number of the contact associated with the session"""
        channel = SMSChannel(self.domain2_backend.couch_id, self.phone_number)
        info = RunningSessionInfo(uuid.uuid4().hex, "fake-owner-4")
        XFormsSessionSynchronization._set_running_session_info_for_channel(
            channel, info, 60)
        self.addCleanup(
            XFormsSessionSynchronization.
            _release_running_session_info_for_channel, info, channel)
        phone_number, has_domain_two_way_scope = self._get_inbound_phone_entry__backend_domain_2(
        )

        self.assertEqual(phone_number.owner_id, "fake-owner-4")
        self.assertTrue(has_domain_two_way_scope)
        self.assertTrue(
            self.domain2_backend.domain_is_authorized(phone_number.domain))
Ejemplo n.º 5
0
def send_first_message(domain, recipient, phone_entry_or_number, session, responses, logged_subevent, workflow):
    # This try/except section is just here (temporarily) to support future refactors
    # If any of these notify, they should be replaced with a comment as to why the two are different
    # so that someone refactoring in the future will know that this or that param is necessary.
    try:
        if session.workflow != workflow:
            # see if we can eliminate the workflow arg
            notify_error('Exploratory: session.workflow != workflow', details={
                'session.workflow': session.workflow, 'workflow': workflow})
        if session.connection_id != recipient.get_id:
            # see if we can eliminate the recipient arg
            notify_error('Exploratory: session.connection_id != recipient.get_id', details={
                'session.connection_id': session.connection_id, 'recipient.get_id': recipient.get_id,
                'recipient': recipient
            })
        if session.related_subevent != logged_subevent:
            # see if we can eliminate the logged_subevent arg
            notify_error('Exploratory: session.related_subevent != logged_subevent', details={
                'session.connection_id': session.connection_id, 'logged_subevent': logged_subevent})
    except Exception:
        # The above running is not mission critical, so if it errors just leave a message in the log
        # for us to follow up on.
        # Absence of the message below and messages above ever notifying
        # will indicate that we can remove these args.
        notify_exception(None, "Error in section of code that's just supposed help inform future refactors")

    if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain):
        if not XFormsSessionSynchronization.claim_channel_for_session(session):
            send_first_message.apply_async(
                args=(domain, recipient, phone_entry_or_number, session, responses, logged_subevent, workflow),
                countdown=60
            )
            return

    metrics_counter('commcare.smsforms.session_started', 1, tags={'domain': domain, 'workflow': workflow})

    if len(responses) > 0:
        message = format_message_list(responses)
        metadata = MessageMetadata(
            workflow=workflow,
            xforms_session_couch_id=session.couch_id,
        )
        if isinstance(phone_entry_or_number, PhoneNumber):
            send_sms_to_verified_number(
                phone_entry_or_number,
                message,
                metadata,
                logged_subevent=logged_subevent
            )
        else:
            send_sms(
                domain,
                recipient,
                phone_entry_or_number,
                message,
                metadata
            )
    logged_subevent.completed()
    def test_get_inbound_phone_entry__one_phone_number_multiple_contacts_on__session_different_domain(
            self):
        """Phone number returned belongs to a domain which does not have access to this backend.
        TODO: This should probably be an error or it should fall back to 'lenient' query"""
        channel = SMSChannel(self.domain2_backend.couch_id, self.phone_number)
        info = RunningSessionInfo(uuid.uuid4().hex, "fake-owner-2")
        XFormsSessionSynchronization._set_running_session_info_for_channel(
            channel, info, 60)
        self.addCleanup(
            XFormsSessionSynchronization.
            _release_running_session_info_for_channel, info, channel)
        phone_number, has_domain_two_way_scope = self._get_inbound_phone_entry__backend_domain_2(
        )

        self.assertEqual(phone_number.owner_id, "fake-owner-2")
        self.assertTrue(has_domain_two_way_scope)
        self.assertFalse(
            self.domain2_backend.domain_is_authorized(
                phone_number.domain))  # bad
Ejemplo n.º 7
0
    def create_tasks(self):
        survey_sessions_due_for_action = self.get_survey_sessions_due_for_action(
        )
        all_open_session_ids = self.get_open_session_ids()
        for domain, connection_id, session_id, current_action_due, phone_number in survey_sessions_due_for_action:
            if skip_domain(domain):
                continue

            if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain):
                fake_session = SQLXFormsSession(
                    session_id=session_id,
                    connection_id=connection_id,
                    phone_number=phone_number,
                )
                if not XFormsSessionSynchronization.channel_is_available_for_session(
                        fake_session):
                    running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
                        fake_session.get_channel())
                    # First confirm the supposedly running session is even open
                    # and if it's not (should be exceedingly rare) release it and act like it wasn't there
                    if running_session_info.session_id \
                            and running_session_info.session_id not in all_open_session_ids:
                        notify_error(
                            "The supposedly running session was not open and was released. "
                            "No known way for this to happen, so worth investigating.",
                            details={
                                'running_session_info': running_session_info
                            })
                        XFormsSessionSynchronization.clear_stale_channel_claim(
                            fake_session.get_channel())
                    # This is the 99% case: there's a running session for the channel
                    # so leave this session/action in the queue for later and move on to the next one
                    else:
                        continue

            enqueue_lock = self.get_enqueue_lock(session_id,
                                                 current_action_due)
            if enqueue_lock.acquire(blocking=False):
                handle_due_survey_action.delay(domain, connection_id,
                                               session_id)
Ejemplo n.º 8
0
def handle_due_survey_action(domain, contact_id, session_id):
    with critical_section_for_smsforms_sessions(contact_id):
        session = SQLXFormsSession.by_session_id(session_id)
        if (not session or not session.session_is_open
                or session.current_action_due > utcnow()):
            return

        if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain):
            if not XFormsSessionSynchronization.claim_channel_for_session(
                    session):
                from .management.commands import handle_survey_actions
                # Unless we release this lock, handle_survey_actions will be unable to requeue this task
                # for the default duration of 1h, which we don't want
                handle_survey_actions.Command.get_enqueue_lock(
                    session_id, session.current_action_due).release()
                return

        if session_is_stale(session):
            # If a session is having some unrecoverable errors that aren't benefitting from
            # being retried, those errors should show up in sentry log and the fix should
            # be dealt with. In terms of the current session itself, we just close it out
            # to allow new sessions to start.
            session.mark_completed(False)
            session.save()
            return

        if session.current_action_is_a_reminder:
            # Resend the current question in the open survey to the contact
            p = PhoneNumber.get_phone_number_for_owner(session.connection_id,
                                                       session.phone_number)
            if p:
                metadata = MessageMetadata(
                    workflow=session.workflow,
                    xforms_session_couch_id=session._id,
                )
                resp = FormplayerInterface(session.session_id,
                                           domain).current_question()
                send_sms_to_verified_number(
                    p,
                    resp.event.text_prompt,
                    metadata,
                    logged_subevent=session.related_subevent)

            session.move_to_next_action()
            session.save()
        else:
            # Close the session
            session.close()
            session.save()
    def test_incoming_sms_linked_form_session__session_contact_matches_incoming(
            self):
        session = self._make_session(self.number1)
        self._claim_channel(session)

        msg = self._get_sms()
        handled = form_session_handler(self.number1, msg.text, msg)
        self.assertTrue(handled)

        # message gets attached to the correct session
        self.assertEqual(msg.xforms_session_couch_id, session.couch_id)

        # sticky session is still 'open'
        session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
            session.get_channel())
        self.assertEqual(session_info.session_id, session.session_id)
def test_auto_clear_stale_session_on_claim():
    phone_number_a = _clean_up_number('15555555555')
    session_a_1 = FakeSession(session_id=str(uuid4()),
                              phone_number=phone_number_a,
                              connection_id='Alpha')
    session_a_2 = FakeSession(session_id=str(uuid4()),
                              phone_number=phone_number_a,
                              connection_id='Beta')

    # Nothing set yet, so it can be claimed
    assert XFormsSessionSynchronization.channel_is_available_for_session(
        session_a_1)
    # And so can the other one
    assert XFormsSessionSynchronization.channel_is_available_for_session(
        session_a_2)
    # Claim succeeds
    assert XFormsSessionSynchronization.claim_channel_for_session(session_a_1)
    # Claim for same channel fails
    assert not XFormsSessionSynchronization.channel_is_available_for_session(
        session_a_2)
    assert not XFormsSessionSynchronization.claim_channel_for_session(
        session_a_2)
    # Set the current active session to closed manually, leaving a dangling/stale session claim
    session_a_1.session_is_open = False
    # Channel is now available
    assert XFormsSessionSynchronization.channel_is_available_for_session(
        session_a_2)
    # And the call above cleared the channel claim but left the contact
    assert (XFormsSessionSynchronization.get_running_session_info_for_channel(
        session_a_2.get_channel()) == RunningSessionInfo(session_id=None,
                                                         contact_id='Alpha'))
    # Claim for the channel now succeeds
    assert XFormsSessionSynchronization.claim_channel_for_session(session_a_2)
    # And it is now the active session for the channel
    assert (XFormsSessionSynchronization.get_running_session_info_for_channel(
        session_a_2.get_channel()).session_id == session_a_2.session_id)
Ejemplo n.º 11
0
    def test_incoming_sms_linked_form_session__session_contact_different_from_incoming(
            self):
        session = self._make_session(self.number2)
        self._claim_channel(session)

        msg = self._get_sms()
        # session belongs to `number2` but message comes from `number1`
        handled = form_session_handler(self.number1, msg.text, msg)
        self.assertTrue(handled)

        # msg does not get updated
        self.assertEqual(msg.xforms_session_couch_id, None)

        # session should be closed
        session.refresh_from_db()
        self.assertFalse(session.session_is_open)
        self.assertFalse(session.completed)

        # sticky session is removed
        session_info = XFormsSessionSynchronization.get_running_session_info_for_channel(
            session.get_channel())
        self.assertIsNone(session_info.session_id)
Ejemplo n.º 12
0
 def _claim_channel(self, session):
     XFormsSessionSynchronization.claim_channel_for_session(session)
     self.addCleanup(
         XFormsSessionSynchronization.release_channel_for_session, session)
def _clean_up_number(phone_number):
    XFormsSessionSynchronization._set_running_session_info_for_channel(
        SMSChannel(BACKEND_ID, phone_number),
        RunningSessionInfo(None, None),
        expiry=10)
    return phone_number
def test_session_synchronization():
    phone_number_a = _clean_up_number('15555555555')
    phone_number_b = _clean_up_number('15555555554')
    session_a_1 = FakeSession(session_id=str(uuid4()),
                              phone_number=phone_number_a,
                              connection_id='Alpha')
    session_a_2 = FakeSession(session_id=str(uuid4()),
                              phone_number=phone_number_a,
                              connection_id='Beta')
    session_b_1 = FakeSession(session_id=str(uuid4()),
                              phone_number=phone_number_b,
                              connection_id='Kappa')

    # Nothing set yet, so it can be claimed
    assert XFormsSessionSynchronization.channel_is_available_for_session(
        session_a_1)
    # And so can the other one
    assert XFormsSessionSynchronization.channel_is_available_for_session(
        session_a_2)
    # Claim succeeds
    assert XFormsSessionSynchronization.claim_channel_for_session(session_a_1)
    # Claim for same channel fails
    assert not XFormsSessionSynchronization.channel_is_available_for_session(
        session_a_2)
    assert not XFormsSessionSynchronization.claim_channel_for_session(
        session_a_2)
    # But same session can re-claim it
    assert XFormsSessionSynchronization.claim_channel_for_session(session_a_1)
    # And another session can claim another channel
    assert XFormsSessionSynchronization.claim_channel_for_session(session_b_1)
    # And if the first session releases the channel
    XFormsSessionSynchronization.release_channel_for_session(session_a_1)
    # Then the contact is still set
    assert XFormsSessionSynchronization.get_running_session_info_for_channel(
        SMSChannel(BACKEND_ID, phone_number_a)).contact_id == 'Alpha'
    # But the other session (that couldn't before) can claim it now
    assert XFormsSessionSynchronization.claim_channel_for_session(session_a_2)
    # Trying to clear the channel claim will fail because the session is still open
    assert not XFormsSessionSynchronization.clear_stale_channel_claim(
        SMSChannel(BACKEND_ID, phone_number_a))
    # But if we close the session first
    session_a_2.close()
    # The session is now "stale" so we can clear that stale channel claim
    assert XFormsSessionSynchronization.clear_stale_channel_claim(
        SMSChannel(BACKEND_ID, phone_number_a))
    # If we try to clear it again it'll be a no-op and return false, since it's already cleared
    assert not XFormsSessionSynchronization.clear_stale_channel_claim(
        SMSChannel(BACKEND_ID, phone_number_a))