Example #1
0
class TwilioAPIWorker(ApplicationWorker):
    """Emulates the Twilio API to use vumi as if it was Twilio"""
    CONFIG_CLASS = TwilioAPIConfig

    @inlineCallbacks
    def setup_application(self):
        """Application specific setup"""
        self.app_config = self.get_static_config()
        self.server = TwilioAPIServer(self, self.app_config.api_version)
        path = os.path.join(
            self.app_config.web_path, self.app_config.api_version)
        self.webserver = self.start_web_resources([
            (self.server.app.resource(), path)],
            self.app_config.web_port)
        redis = yield TxRedisManager.from_config(self.app_config.redis_manager)
        self.session_manager = SessionManager(
            redis, self.app_config.redis_timeout)
        self.session_lookup = SessionIDLookup(
            redis, self.app_config.redis_timeout,
            self.app_config.session_lookup_namespace)

    @inlineCallbacks
    def teardown_application(self):
        """Clean-up of setup done in `setup_application`"""
        yield self.webserver.loseConnection()
        yield self.session_manager.stop()

    def _http_request(self, url='', method='GET', data={}):
        return treq.request(method, url, persistent=False, data=data)

    def _request_data_from_session(self, session):
        return {
            'CallSid': session['CallId'],
            'AccountSid': session['AccountSid'],
            'From': session['From'],
            'To': session['To'],
            'CallStatus': session['Status'],
            'ApiVersion': self.app_config.api_version,
            'Direction': session['Direction'],
        }

    @inlineCallbacks
    def _get_twiml_from_client(self, session, data=None):
        if data is None:
            data = self._request_data_from_session(session)
        twiml_raw = yield self._http_request(
            session['Url'], session['Method'], data)
        if twiml_raw.code < 200 or twiml_raw.code >= 300:
            twiml_raw = yield self._http_request(
                session['FallbackUrl'], session['FallbackMethod'], data)
        twiml_raw = yield twiml_raw.content()
        twiml_parser = TwiMLParser(session['Url'])
        returnValue(twiml_parser.parse(twiml_raw))

    @inlineCallbacks
    def _handle_connected_call(
            self, session_id, session, status='in-progress', twiml=None):
        # TODO: Support sending ForwardedFrom parameter
        # TODO: Support sending CallerName parameter
        # TODO: Support sending geographic data parameters
        session['Status'] = status
        self.session_manager.save_session(session_id, session)
        if twiml is None:
            twiml = yield self._get_twiml_from_client(session)
        for verb in twiml:
            if verb.name == "Play":
                # TODO: Support loop and digit attributes
                yield self._send_message(verb.nouns[0], session)
            elif verb.name == "Hangup":
                yield self._send_message(
                    None, session, TransportUserMessage.SESSION_CLOSE)
                yield self.session_manager.clear_session(session_id)
                break
            elif verb.name == "Gather":
                # TODO: Support timeout and numDigits attributes
                msgs = []
                for subverb in verb.nouns:
                    # TODO: Support Say and Pause subverbs
                    if subverb.name == "Play":
                        msgs.append({'speech_url': subverb.nouns[0]})
                session['Gather_Action'] = verb.attributes['action']
                session['Gather_Method'] = verb.attributes['method']
                yield self.session_manager.save_session(session_id, session)
                if len(msgs) == 0:
                    msgs.append({'speech_url': None})
                msgs[-1]['wait_for'] = verb.attributes['finishOnKey']
                for msg in msgs:
                    yield self._send_message(
                        msg['speech_url'], session,
                        wait_for=msg.get('wait_for'))
                break

    def _send_message(self, url, session, session_event=None, wait_for=None):
        helper_metadata = {'voice': {}}
        if url is not None:
            helper_metadata['voice']['speech_url'] = url
        if wait_for is not None:
            helper_metadata['voice']['wait_for'] = wait_for

        return self.send_to(
            session['To'], None,
            from_addr=session['From'],
            session_event=session_event,
            to_addr_type=TransportUserMessage.AT_MSISDN,
            from_addr_type=TransportUserMessage.AT_MSISDN,
            helper_metadata=helper_metadata)

    @inlineCallbacks
    def consume_user_message(self, message):
        # At the moment there is no way to determine whether or not a message
        # is the result of a wait_for or just a single digit, so if the Gather
        # data exists inside the current session data, then we assume that it
        # is the result of a Gather
        # TODO: Fix this
        session = yield self.session_manager.load_session(message['from_addr'])
        if session.get('Gather_Action') and session.get('Gather_Method'):
            data = self._request_data_from_session(session)
            data['Digits'] = message['content']
            twiml = yield self._get_twiml_from_client({
                'Url': session['Gather_Action'],
                'Method': session['Gather_Method'],
                'Fallback_Url': None,
                'Fallback_Method': None, },
                data=data)
            yield self._handle_connected_call(
                message['from_addr'], session, twiml=twiml)

    @inlineCallbacks
    def consume_ack(self, event):
        message_id = event['user_message_id']
        session_id = yield self.session_lookup.get_address(message_id)
        yield self.session_lookup.delete_id(message_id)
        session = yield self.session_manager.load_session(session_id)

        if session['Status'] == 'queued':
            yield self._handle_connected_call(session_id, session)

    @inlineCallbacks
    def consume_nack(self, event):
        message_id = event['user_message_id']
        session_id = yield self.session_lookup.get_address(message_id)
        yield self.session_lookup.delete_id(message_id)
        session = yield self.session_manager.load_session(session_id)

        if session['Status'] == 'queued':
            yield self._handle_connected_call(
                session_id, session, status='failed')

    @inlineCallbacks
    def new_session(self, message):
        yield self.session_lookup.set_id(
            message['message_id'], message['from_addr'])
        config = yield self.get_config(message)
        session = {
            'CallId': self.server._get_sid(),
            'AccountSid': self.server._get_sid(),
            'From': message['from_addr'],
            'To': message['to_addr'],
            'Status': 'in-progress',
            'Direction': 'inbound',
            'Url': config.client_path,
            'Method': config.client_method,
            'StatusCallback': config.status_callback_path,
            'StatusCallbackMethod': config.status_callback_method,
        }
        yield self.session_manager.create_session(
            message['from_addr'], **session)

        twiml = yield self._get_twiml_from_client(session)
        for verb in twiml:
            if verb.name == "Play":
                yield self.reply_to(message, None, helper_metadata={
                    'voice': {
                        'speech_url': verb.nouns[0],
                        }
                    })
            elif verb.name == "Hangup":
                yield self.reply_to(
                    message, None,
                    session_event=TransportUserMessage.SESSION_CLOSE)
                yield self.session_manager.clear_session(message['from_addr'])
                break
            elif verb.name == "Gather":
                # TODO: Support timeout and numDigits attributes
                msgs = []
                for subverb in verb.nouns:
                    # TODO: Support Say and Pause subverbs
                    if subverb.name == "Play":
                        msgs.append({'speech_url': subverb.nouns[0]})
                session['Gather_Action'] = verb.attributes['action']
                session['Gather_Method'] = verb.attributes['method']
                yield self.session_manager.save_session(
                    message['from_addr'], session)
                if len(msgs) == 0:
                    msgs.append({'speech_url': None})
                msgs[-1]['wait_for'] = verb.attributes['finishOnKey']
                for msg in msgs:
                    yield self.reply_to(message, None, helper_metadata={
                        'voice': {
                            'speech_url': msg.get('speech_url'),
                            'wait_for': msg.get('wait_for'),
                        }})
                break

    @inlineCallbacks
    def close_session(self, message):
        # TODO: Implement call duration parameters
        # TODO: Implement recording parameters
        session = yield self.session_manager.load_session(message['from_addr'])
        yield self.session_manager.clear_session(message['from_addr'])
        url = session.get('StatusCallback')

        if url and url != 'None':
            session['Status'] = 'completed'
            data = self._request_data_from_session(session)
            yield self._http_request(
                session['StatusCallback'], session['StatusCallbackMethod'],
                data)