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 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 = current_question(session.session_id, domain) 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 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): 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
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
def sms_keyword_handler(v, text, msg): with critical_section_for_smsforms_sessions(v.owner_id): text = text.strip() if text == "": return False sessions = SQLXFormsSession.get_all_open_sms_sessions(v.domain, v.owner_id) text_words = text.upper().split() if text.startswith("#"): return handle_global_keywords(v, text, msg, text_words, sessions) else: return handle_domain_keywords(v, text, msg, text_words, sessions)
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 close_session(self, contact_id, session_id): with critical_section_for_smsforms_sessions(contact_id): session = SQLXFormsSession.by_session_id(session_id) try: session.close(force=False) except TouchformsError as e: try: self.retry(exc=e) except TouchformsError as e: raise e finally: # Eventually the session needs to get closed session.mark_completed(False) session.save() return session.save()
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 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 = current_question(session.session_id, domain) 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 get_critical_section(self, recipient): if self.critical_section_already_acquired: return no_op_context_manager() return critical_section_for_smsforms_sessions(recipient.get_id)
def fire_sms_survey_event(reminder, handler, recipients, verified_numbers, logged_event): current_event = reminder.current_event if reminder.callback_try_count > 0: # Leaving this as an explicit reminder that all survey related actions now happen # in a different process. Eventually all of this code will be removed when we move # to the new reminders framework. pass else: reminder.xforms_session_ids = [] domain_obj = Domain.get_by_name(reminder.domain, strict=True) # Get the app, module, and form try: form_unique_id = current_event.form_unique_id form = Form.get_form(form_unique_id) app = form.get_app() module = form.get_module() except Exception: logged_event.error(MessagingEvent.ERROR_CANNOT_FIND_FORM) return # Start a touchforms session for each recipient for recipient in recipients: logged_subevent = logged_event.create_subevent( handler, reminder, recipient) verified_number, unverified_number = get_recipient_phone_number( reminder, recipient, verified_numbers) no_verified_number = verified_number is None cant_use_unverified_number = (unverified_number is None or form_requires_input(form)) if no_verified_number and cant_use_unverified_number: logged_subevent.error( MessagingEvent.ERROR_NO_TWO_WAY_PHONE_NUMBER) continue if verified_number: pb = PhoneBlacklist.get_by_phone_number_or_none( verified_number.phone_number) else: pb = PhoneBlacklist.get_by_phone_number_or_none( unverified_number) if pb and not pb.send_sms: logged_subevent.error(MessagingEvent.ERROR_PHONE_OPTED_OUT) continue with critical_section_for_smsforms_sessions(recipient.get_id): # Get the case to submit the form against, if any if (is_commcarecase(recipient) and not handler.force_surveys_to_use_triggered_case): case_id = recipient.case_id else: case_id = reminder.case_id if form.requires_case() and not case_id: logged_subevent.error(MessagingEvent.ERROR_NO_CASE_GIVEN) continue # Close all currently open sessions SQLXFormsSession.close_all_open_sms_sessions( reminder.domain, recipient.get_id) # Start the new session try: if current_event.callback_timeout_intervals: if handler.submit_partial_forms: expire_after = sum( current_event.callback_timeout_intervals) reminder_intervals = current_event.callback_timeout_intervals[: -1] else: expire_after = SQLXFormsSession.MAX_SESSION_LENGTH reminder_intervals = current_event.callback_timeout_intervals submit_partially_completed_forms = handler.submit_partial_forms include_case_updates_in_partial_submissions = handler.include_case_side_effects else: expire_after = SQLXFormsSession.MAX_SESSION_LENGTH reminder_intervals = [] submit_partially_completed_forms = False include_case_updates_in_partial_submissions = False session, responses = start_session( SQLXFormsSession.create_session_object( reminder.domain, recipient, verified_number.phone_number if verified_number else unverified_number, app, form, expire_after=expire_after, reminder_intervals=reminder_intervals, submit_partially_completed_forms= submit_partially_completed_forms, include_case_updates_in_partial_submissions= include_case_updates_in_partial_submissions), reminder.domain, recipient, app, module, form, case_id, case_for_case_submission=handler. force_surveys_to_use_triggered_case) except TouchformsError as e: human_readable_message = get_formplayer_exception( reminder.domain, e) logged_subevent.error( MessagingEvent.ERROR_TOUCHFORMS_ERROR, additional_error_text=human_readable_message) if touchforms_error_is_config_error(reminder.domain, e): # Don't reraise the exception because this means there are configuration # issues with the form that need to be fixed continue else: # Reraise the exception so that the framework retries it again later raise except Exception as e: logged_subevent.error( MessagingEvent.ERROR_TOUCHFORMS_ERROR) # Reraise the exception so that the framework retries it again later raise session.survey_incentive = handler.survey_incentive session.workflow = get_workflow(handler) session.reminder_id = reminder._id session.save() reminder.xforms_session_ids.append(session.session_id) logged_subevent.xforms_session = session logged_subevent.save() # Send out first message if len(responses) > 0: message = format_message_list(responses) metadata = MessageMetadata( workflow=get_workflow(handler), reminder_id=reminder._id, xforms_session_couch_id=session._id, ) if verified_number: send_sms_to_verified_number( verified_number, message, metadata, logged_subevent=logged_subevent) else: send_sms(reminder.domain, recipient, unverified_number, message, metadata) logged_subevent.completed()
def send(self, recipient, logged_event): app, module, form, requires_input = self.get_memoized_app_module_form( logged_event.domain) if any([o is None for o in (app, module, form)]): logged_event.error(MessagingEvent.ERROR_CANNOT_FIND_FORM) return logged_subevent = logged_event.create_subevent_from_contact_and_content( recipient, self, case_id=self.case.case_id if self.case else None, ) # We don't try to look up the phone number from the user case in this scenario # because this use case involves starting a survey session, which can be # very different if the contact is a user or is a case. So here if recipient # is a user we only allow them to fill out the survey as the user contact, and # not the user case contact. phone_entry_or_number = self.get_two_way_entry_or_phone_number( recipient, try_user_case=False) if phone_entry_or_number is None: logged_subevent.error(MessagingEvent.ERROR_NO_PHONE_NUMBER) return if requires_input and not isinstance(phone_entry_or_number, PhoneNumber): logged_subevent.error(MessagingEvent.ERROR_NO_TWO_WAY_PHONE_NUMBER) return # The SMS framework already checks if the number has opted out before sending to # it. But for this use case we check for it here because we don't want to start # the survey session if they've opted out. if self.phone_has_opted_out(phone_entry_or_number): logged_subevent.error(MessagingEvent.ERROR_PHONE_OPTED_OUT) return with critical_section_for_smsforms_sessions(recipient.get_id): # Get the case to submit the form against, if any case_id = None if is_commcarecase(recipient): case_id = recipient.case_id elif self.case: case_id = self.case.case_id if form.requires_case() and not case_id: logged_subevent.error(MessagingEvent.ERROR_NO_CASE_GIVEN) return session, responses = self.start_smsforms_session( logged_event.domain, recipient, case_id, phone_entry_or_number, logged_subevent, self.get_workflow(logged_event), app, module, form) if session: logged_subevent.xforms_session = session logged_subevent.save() self.send_first_message(logged_event.domain, recipient, phone_entry_or_number, session, responses, logged_subevent, self.get_workflow(logged_event)) logged_subevent.completed()