Exemple #1
0
class ACTR():

    def __init__(self, config: dict, dotbot: dict) -> None:
        """Initializes class"""
        self.config = config
        self.dotbot = dotbot
        self.logger_level = ''

        self.visemes = None
        
        self.logger = BBotLoggerAdapter(logging.getLogger('actr'), self, self, 'actr')        

    def init(self, core):
        """Initializes some values"""
        pass
        

    def get_actr(self, text: str, locale: str, voice_id: str, time_scale: int):
        """
        """
        
        # get visemes
        self.visemes.voice_locale = locale
        self.visemes.voice_id = voice_id
        visemes = self.visemes.get_visemes(text, time_scale)
        
        # fixes amazon polly response
        # adds a start delay and provides duration for each viseme
        first = True 
        new_visemes = []
        start_delay = 0       
        for idx, v in enumerate(visemes):                                    
            # if this is the first viseme then time is start_delay value                
            if first: 
                start_delay = v['time'] 
                first = False
                        
            if idx == len(visemes) - 1:
                # this is the last viseme. let duration as the median of previous viseme duration
                # @TODO
                duration = 100
            else:                
                # get duration by the substraction of next viseme time and current time
                next_t = visemes[idx + 1]['time']
                duration = next_t - v['time']
                        
            new_visemes.append({'value': v['value'], 'duration': duration})

        new_visemes.insert(0,{'value': 'sil', 'duration': start_delay})
        response = {
           'visemes': new_visemes
        }

        self.logger.debug('Visemes:' + str(response))
        return response
Exemple #2
0
class WolframAlphaShortAnswers(ChatbotEngine):
    """
    """
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

    def init(self, core: BBotCore):
        """
        Initializes bot
        """
        super().init(core)
        self.logger = BBotLoggerAdapter(logging.getLogger('wolframsa_cbe'),
                                        self, self.core)

    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """
        super().get_response(request)

        appid = self.dotbot.chatbot_engine['appId']
        query = self.request['input']['text']
        self.logger.debug(
            'Querying to Wolfram Alpha Short Answers API with query: ' + query)
        r = requests.get(
            f'http://api.wolframalpha.com/v1/result?appid={appid}&i={query}')
        self.logger.debug('Wolfram Alpha Short Answers API response code: ' +
                          str(r.status_code) + ' - message: ' +
                          str(r.text)[0:300])
        if r.status_code == 200:
            aw = r.text
        if r.status_code == 501:
            aw = r.text  # Because this bot is designed to return a single result, this message may appear if no sufficiently short result can be found. You may occasionally receive this answer when requesting information on topics that are restricted or not covered.

        self.core.bbot.text(aw)
        return self.response
class TokenManagerParityETH():

    web3 = None

    def __init__(self, config: dict, dotbot: dict) -> None:
        """Initializes class"""
        self.config = config
        self.dotbot = dotbot
        
        self.logger_level = ''

        self.core = None
        self.parity_node_rpc_url = ''
                
        self.logger = BBotLoggerAdapter(logging.getLogger('token_parityeth'), self, self, '')        

    def init(self, core):
        """Initializes some values"""
        pass

    def connect(self):
        if not TokenManagerParityETH.web3:
            self.logger.debug('Connection to node ' + self.parity_node_rpc_url)
            if 'http' in self.parity_node_rpc_url:
                provider = Web3.HTTPProvider(self.parity_node_rpc_url)
            if 'ws://' in self.parity_node_rpc_url:
                provider = Web3.WebsocketProvider(self.parity_node_rpc_url)
            if 'file://' in self.parity_node_rpc_url:
                url = self.parity_node_rpc_url.replace('file://', '') #web3 does not recognize file:// scheme
                provider = Web3.IPCProvider(url)

            TokenManagerParityETH.web3 = Web3(provider)

    def transfer(self, fromAddress: str, toAddress: str, amount: float, credential: str=''):
        self.connect()
        self.logger.debug('Transfering ' + str(amount) + 'ETH from ' + fromAddress + ' to ' + toAddress)
        response = TokenManagerParityETH.web3.parity.personal.sendTransaction( 
            {
                'to': Web3.toChecksumAddress(toAddress), 
                'from': Web3.toChecksumAddress(fromAddress), 
                'value': Web3.toWei(amount, 'ether'),
                'gas': 21000,
                'gasPrice': 0 
            }, 
            credential)
        self.logger.debug('Transaction hash: ' + response.hex())
        return response
Exemple #4
0
class Restful:
    """"""
    def __init__(self, config: dict, dotbot: dict = None) -> None:
        """

        """
        self.config = config
        self.dotbot = dotbot
        self.dotdb = None  #
        self.tts = None
        self.actr = None
        self.logger_level = ''

        self.params = {}

    def init(self, core):
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('channel_restful'),
                                        self, self.core, 'ChannelRestful')

    def endpoint(self, request=dict):
        try:
            print(
                '--------------------------------------------------------------------------------------------------------------------------------'
            )
            self.params = request.get_json(force=True)
            self.logger.debug("Received request " + str(self.params))

            user_id = self.params.get('userId')
            bot_id = self.params.get('botId')
            org_id = self.params.get('orgId')
            pub_token = self.params.get('pubToken')
            channel_id = self.params.get('channelId')
            input_params = self.params['input']

            input_params['channelPlatform'] = 'bbot_restful_channel'

            # get publisher user id from token
            pub_bot = self.dotdb.find_publisherbot_by_publisher_token(
                pub_token)
            if not pub_bot:
                raise Exception('Publisher not found')
            self.logger.debug('Found subscription id: ' + str(pub_bot.id) +
                              ' - publisher name: ' + pub_bot.publisher_name +
                              ' - for bot name: ' + pub_bot.bot_name +
                              ' - bot id:' + pub_bot.bot_id)

            pub_id = pub_bot.publisher_name

            print("1")

            # if 'runBot' in params:
            #    run_bot = self.params['runBot']

            dotbot = self.dotdb.find_dotbot_by_bot_id(pub_bot.bot_id)
            print("2")
            if not dotbot:
                raise Exception('Bot not found')
            bot_id = dotbot.bot_id
            # build extended dotbot
            dotbot.services = pub_bot.services
            dotbot.channels = pub_bot.channels
            dotbot.botsubscription = pub_bot
            print("3")
            self.dotbot = dotbot  # needed for methods below
            config = load_configuration(
                os.path.abspath(
                    os.path.dirname(__file__) + "../../../instance"),
                "BBOT_ENV")
            print("4")

            bot = BBotCore.create_bot(config['bbot_core'], dotbot)
            self.core = bot
            input_text = ""
            #for input_type, input_value in input_params.items():
            # bot.get_response(input_type, input_value)
            #    _ = input_type
            #    input_text = input_text + input_value
            req = bot.create_request(input_params, user_id, bot_id, org_id,
                                     pub_id, channel_id)
            bbot_response = {}
            http_code = 500
            bbot_response = bot.get_response(req)

            #response = defaultdict(lambda: defaultdict(dict))    # create a response dict with autodict property
            #for br in bot_response.keys():
            #   response[br] = bot_response[br]

            #response['output'] = self.escape_html_from_text(response['output'])
            #logger.debug('Escaped response text: ' + str(response['output']))

            if self.params.get('ttsEnabled'):
                bbot_response['tts'] = {}
                if not self.tts:
                    bbot_response['errors'].append(
                        {'message': 'No TTS engine configured for this bot.'})
                else:
                    #retrieve TTS audio generated from all texts from bbot output
                    self.tts.voice_locale = self.get_tts_locale()
                    self.tts.voice_id = self.get_tts_voice_id()
                    all_texts = BBotCore.get_all_texts_from_output(
                        bbot_response['output'])
                    bbot_response['tts'][
                        'url'] = self.tts.get_speech_audio_url(
                            all_texts, self.get_tts_timescale())

            if self.params.get('actrEnabled', None):
                bbot_response['actr'] = {}
                if not self.tts:
                    response['errors'].append(
                        {'message': 'No ACTR engine configured for this bot.'})
                else:
                    all_texts = BBotCore.get_all_texts_from_output(
                        bbot_response['output'])
                    bbot_response['actr'] = self.actr.get_actr(
                        all_texts, self.get_tts_locale(),
                        self.get_tts_voice_id(), self.get_tts_timescale())

            if self.params.get('debugEnabled') is None:
                if 'debug' in bbot_response:
                    del bbot_response['debug']

            http_code = 200

        except Exception as e:
            if isinstance(
                    e, BBotException
            ):  # BBotException means the issue is in bot userland, not rhizome
                http_code = 200
            else:
                self.logger.critical(
                    str(e) + "\n" + str(traceback.format_exc()))
                http_code = 500

            if os.environ['BBOT_ENV'] == 'development':
                bbot_response = {
                    'output': [{
                        'type': 'message',
                        'text': cgi.escape(str(e))
                    }],  #@TODO use bbot.text() 
                    'error': {
                        'traceback': str(traceback.format_exc())
                    }
                }
            else:
                bbot_response = {
                    'output': [{
                        'type':
                        'message',
                        'text':
                        'An error happened. Please try again later.'
                    }]
                }
                # @TODO this should be configured in dotbot
                # @TODO let bot engine decide what to do?

        self.logger.debug("Response from restful channel: " +
                          str(bbot_response))
        return {
            'response': json.dumps(bbot_response),
            'status': http_code,
            'mimetype': 'application/json'
        }

    def get_endpoint_path(self) -> str:
        return self.config['endpoint_path']

    def get_tts_locale(self) -> str:
        """Returns locale for tts service"""
        if self.dotbot.tts.get('locale') is not None:
            return self.dotbot.tts['locale']
        # from request
        if self.params.get('ttsLocale') is not None:
            return self.params['ttsLocale']
        # If there is no voice set, check fo default in DotBot
        if self.dotbot.tts.get('defaultLocale') is not None:
            return self.dotbot.tts['defaultLocale']
        # If there is not even defaultVoiceId, try with hardcoded default value
        return 'en_US'

    def get_tts_voice_id(self) -> str:
        """Returns bot voice id"""
        # DotBot VoiceId has higher priority. external configurations can't change this
        if self.dotbot.tts.get('voiceId') is not None:
            return self.dotbot.tts['voiceId']
        # from request
        if self.params.get('ttsVoiceId') is not None:
            return self.params['ttsVoiceId']
        # If there is no voice set, check fo default in DotBot
        if self.dotbot.tts.get('defaultVoiceId') is not None:
            return self.dotbot.tts['defaultVoiceId']
        # If there is not even defaultVoiceId, try with hardcoded default value
        return 0

    def get_tts_timescale(
            self
    ) -> str:  #@TODO we might need a method to get values like this
        """Returns bot tts time scale"""
        # DotBot timeScale has higher priority. external configurations can't change this
        if self.dotbot.tts.get('timeScale') is not None:
            return self.dotbot.tts['timeScale']
        # from request
        if self.params.get('ttsTimeScale') is not None:
            return self.params['ttsTimeScale']
        # If there is no time scale set, check fo default in DotBot
        if self.dotbot.tts.get('defaultTimeScale') is not None:
            return self.dotbot.tts['defaultTimeScale']
        # If there is not even default set, try with hardcoded default value
        return 100

    def get_http_locale(self) -> str:
        """ @TODO """
        return None

    def escape_html_from_text(self, bbot_response: list) -> list:
        """Escape HTML chars from text objects"""
        response = []
        for br in bbot_response:
            response_type = list(br.keys())[0]
            if response_type == 'text':
                br['text'] = html.escape(br['text'])
            response.append(br)

        return response
Exemple #5
0
class TokenManager():
    """Executes payments on a private development Ethereum parity node using personal module for seed token demo page"""

    SUBSCRIPTION_TYPE_FREE = 'free'
    SUBSCRIPTION_TYPE_PER_USE = 'perUse'
    SUBSCRIPTION_TYPE_MONTHLY = 'perMonth'

    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.
        """
        self.config = config
        self.dotbot = dotbot
        
        self.logger_level = ''        
        self.minimum_accepted_balance = 5
        self.core = None    
        self.token_manager = None
        self.greenhousedb = None

    def init(self, core: BBotCore):
        """
        :param bot:
        :return:
        """
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('ext.token_mgnt'), self, self.core.bot, '$token')                

        smokesignal.on(BBotCore.SIGNAL_CALL_BBOT_FUNCTION_BEFORE, self.function_payment)
        smokesignal.on(BBotCore.SIGNAL_GET_RESPONSE_AFTER, self.volley_payment)
        smokesignal.on(BBotCore.SIGNAL_GET_RESPONSE_BEFORE, self.payment_check)

    def payment_check(self, data):
        """
        Check before anything if the payment for the service is paid for current period
        """
        
        if self.core.get_publisher_subscription_type() == TokenManager.SUBSCRIPTION_TYPE_MONTHLY:
            self.logger.debug('Monthly suscription. Will check payment status')
            paid = self.check_period_payment_status(self.dotbot.botsubscription.id)
            if not paid:
                self.logger.debug('Publisher last payment is more than a month ago.')
                self.insufficient_funds()
        """
        Disabling this for now. We need to make tx async in order to stop wait for a response we dont need at the moment
        elif self.core.get_publisher_subscription_type() == TokenManager.SUBSCRIPTION_TYPE_PER_USE and self.dotbot.per_use_cost > 0:
            self.logger.debug('Per use suscription. Will check balance first')
            if not self.previous_checkings():
                return

            pbalance = self.token_manager.get_balance(self.core.get_publisher_name())
            if pbalance < self.minimum_accepted_balance:
                self.logger.debug('Publisher balance is less than ' + str(self.minimum_accepted_balance))                
                self.insufficient_funds()
        """        
    def check_period_payment_status(self, subscription_id):
        self.logger.debug('Checking payment of subscriptionId ' + str(subscription_id))
        lastPaymentDate = self.greenhousedb.get_last_payment_date_by_subscription_id(subscription_id)
        if not lastPaymentDate:
            self.logger.debug('There is no payments')
            return False
        delta = (datetime.datetime.now() - lastPaymentDate).days
        self.logger.debug('Last payment date is ' + str(lastPaymentDate) + ' - days ago: ' + str(delta))
        if delta >= 30:
            return False
        return True
        
    def volley_payment(self, data):
        """
        Volley payment from publisher to bot owner
        """   

        if self.core.get_publisher_subscription_type() == TokenManager.SUBSCRIPTION_TYPE_FREE:
            self.logger.debug('Free suscription. No payment needed.')
            return
        
        # It's not free. so we need publisher id
        if not self.core.get_publisher_name():
            self.core.reset_output()
            self.core.bbot.text('This bot is not free. Please, set publisher token.')
            raise BBotCoreHalt('Bot halted. Missing publisher id')    

        if self.dotbot.owner_name == self.core.get_publisher_name():
            self.logger.debug('Publisher is at the same time the bot owner. No need to do payment.')
            return
        
        if self.core.get_publisher_subscription_type() == TokenManager.SUBSCRIPTION_TYPE_MONTHLY:
            self.logger.debug('Monthly suscription. No volley payment needed.') # It's assumed there is a previous check for monthly payment
            return

        if self.core.get_publisher_subscription_type() == TokenManager.SUBSCRIPTION_TYPE_PER_USE:
            volley_cost = self.dotbot.per_use_cost

            if volley_cost is not None: # register bot volleys only if it has declared volley cost (can be 0)
                self.logger.debug('Paying volley activity')            
                try:
                    self.token_manager.transfer(self.core.get_publisher_name(), self.dotbot.owner_name, volley_cost)
                except TokenManagerInsufficientFundsException as e:
                    self.insufficient_funds()

    def function_payment(self, data):
        """
        Function payment from bot owner to service owner
        """
        if data['register_enabled'] is True: #@TODO we should have a flag for regiter and another for payment
            self.logger.debug('Paying function activity: ' + str(data))        

            service_owner_name = data['data']['owner_name']

            if data['data'].get('subscription_type') == TokenManager.SUBSCRIPTION_TYPE_FREE:
                self.logger.debug('Free component suscription. No payment needed.')
                return
            
            if service_owner_name == self.dotbot.owner_name:
                self.logger.debug('Bot owner is at the same time the component owner. No need to do payment.')
                return
            
            if data['data'].get('subscription_type') == TokenManager.SUBSCRIPTION_TYPE_MONTHLY:
                self.logger.debug('Monthly component suscription. Will check payment status')
                paid = self.check_period_payment_status(data['data']['subscription_id'])
                if not paid:
                    self.logger.debug('Bot owner last payment is more than a month ago.')
                    self.insufficient_funds()
                else:
                    self.logger.debug('Subscription payment ok')
                    return
                
            if data['data'].get('subscription_type') == TokenManager.SUBSCRIPTION_TYPE_PER_USE:                
                try:
                    self.token_manager.transfer(self.dotbot.owner_name, service_owner_name, data['data']['cost'])
                except TokenManagerInsufficientFundsException as e:
                    self.insufficient_funds()
            
    def previous_checkings(self, owner_name, publisher_name):
        """
        Some previous checks before payment
        """
        if not publisher_name:
            self.core.reset_output()
            self.core.bbot.text('This bot is not free. Please, set publisher token.') # @TODO ?
            raise BBotCoreHalt('Bot halted missing publisher token')    

        if owner_name == publisher_name:
            self.logger.debug('Publisher is at the same time the bot owner. No need to do payment.')
            return False
        
        return True
        
    def insufficient_funds(self):
        """
        When there is insufficient funds we reset output, send error message and halt bot 
        """
        self.core.reset_output()
        self.core.bbot.text('Sorry, the bot is not available at this moment. Please try again later.') # @TODO chec environment to show different message
        raise BBotCoreHalt('Bot halted by insufficient funds')    
Exemple #6
0
class FallbackBots():
    """."""
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.
        """
        self.config = config
        self.dotbot = dotbot

        self.logger_level = ''

        self.core = None
        self.logger = None

    def init(self, core: BBotCore):
        """

        :param bot:
        :return:
        """
        self.core = core
        self.logger = BBotLoggerAdapter(
            logging.getLogger('pipeline.fallbackbots'), self, self.core,
            'FallbackBots')

    def process(self):
        """
        @TODO this will be called from a pubsub event, so args might change
        Call to fallback bots defined in dotbot when the main bot has a no match o
        or when it doesnt answer or has an invalid response
        @TODO this might be replaced by a conditional pipeline

        :param bot:
        :param response:
        :return:
        """
        if not self.core.bot.is_fallback and (self.core.response.get('noMatch')
                                              or
                                              self.core.response.get('error')):
            self.logger.debug(
                'Bot engine has a no match. Looking fallback bots')

            # try import bots
            fbbs = self.core.dotbot.get('fallbackBots', [])
            for bot_name in fbbs:
                self.logger.debug(f'Trying with bot {bot_name}')
                bot_dotbot_container = self.core.dotdb.find_dotbot_by_idname(
                    bot_name)
                if not bot_dotbot_container:
                    raise Exception(f'Fallback bot not found {bot_name}')
                else:
                    bot_dotbot = bot_dotbot_container.dotbot

                config_path = os.path.abspath(
                    os.path.dirname(__file__) + "/../instance")
                config = load_configuration(config_path, "BBOT_ENV")
                bbot = create_bot(config, bot_dotbot)
                bbot.is_fallback = True
                req = ChatbotEngine.create_request(core.request['input'],
                                                   core.user_id, 1, 1)
                fallback_response = bbot.get_response(req)
                if fallback_response.get('error'):
                    self.logger.error(
                        'Fallback bot returned an invalid response. Discarding.'
                    )
                    continue
                if not fallback_response.get('noMatch'):
                    self.logger.debug(
                        'Fallback bot has a response. Returning this to channel.'
                    )
                    self.core.response = fallback_response
                    return
            if fbbs:
                self.logger.debug(
                    'Fallback bot don\'t have a response either. Sending original main bot response if any'
                )
            else:
                self.logger.debug(
                    'No fallback defined for this bot. Sending original main bot response if any'
                )

        self.logger.debug('Bot responded with a match. No fallback needed.')
        return
Exemple #7
0
class SendEmail():
    """Sends email"""
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.
        """
        self.config = config
        self.dotbot = dotbot

        self.logger_level = ''

        self.core = None
        self.logger = None

    def init(self, core: ChatbotEngine):
        """

        :param bot:
        :return:
        """
        self.core = core
        self.logger = BBotLoggerAdapter(
            logging.getLogger('core_ext.send_email'), self, self.core,
            '$sendEmail')
        core.register_function(
            'sendEmail', {
                'object': self,
                'method': 'sendEmail',
                'cost': 0.001,
                'register_enabled': True
            })

    def sendEmail(self, args, f_type):
        """
        Sends email

        :param args:
        :param f_type:
        :return:
        """
        try:
            recipient = self.core.resolve_arg(args[0], f_type)
        except IndexError:
            raise BBotException({
                'code': 250,
                'function': 'sendEmail',
                'arg': 0,
                'message': 'Recipient address is missing.'
            })

        try:
            sender = self.core.resolve_arg(args[1], f_type)
        except IndexError:
            raise BBotException({
                'code': 251,
                'function': 'sendEmail',
                'arg': 1,
                'message': 'Sender address is missing.'
            })
        if type(recipient) is not str:
            raise BBotException({
                'code': 251,
                'function': 'sendEmail',
                'arg': 0,
                'message': 'Recipient has to be string.'
            })

        try:
            subject = self.core.resolve_arg(args[2], f_type)
        except IndexError:
            raise BBotException({
                'code': 252,
                'function': 'sendEmail',
                'arg': 2,
                'message': 'Subject is missing.'
            })
        if type(subject) is not str:
            raise BBotException({
                'code': 251,
                'function': 'sendEmail',
                'arg': 2,
                'message': 'Subject has to be string.'
            })

        try:
            body = self.core.resolve_arg(args[3], f_type)
        except IndexError:
            raise BBotException({
                'code': 253,
                'function': 'sendEmail',
                'arg': 3,
                'message': 'Email body is missing.'
            })

        smtp_config = self.core.dotbot['smtp']

        msg = EmailMessage()
        msg['Subject'] = subject
        msg['From'] = sender
        msg['To'] = recipient

        body = body.replace('\n', '<br />')

        msg.set_content(
            "Please see this email with an html compatible email client\n")
        msg.add_alternative(f"""\
                    <html>
                    <head></head>
                        <body>
                            {body}
                        </body>
                    </html>
                    """,
                            subtype='html')

        self.logger.debug(
            f"Sending email through {smtp_config['server_host']}:{smtp_config['server_port']} to {recipient}"
        )

        smtp = smtplib.SMTP(smtp_config['server_host'],
                            smtp_config['server_port'])
        smtp.set_debuglevel(1)
        if smtp_config.get('server_username', "") and smtp_config.get(
                'serer_password', ""):
            smtp.login(smtp_config['server_username'],
                       smtp_config['server_password'])
        smtp.send_message(msg)
        smtp.quit()
Exemple #8
0
class TemplateEngineTemplator():
    """."""
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.
        """
        self.config = config
        self.dotbot = dotbot

        self.logger_level = ''

        self.core = None
        self.logger = None

    def init(self, core: BBotCore):
        """

        :param bot:
        :return:
        """
        self.core = core
        self.logger = BBotLoggerAdapter(
            logging.getLogger('pipeline.templator'), self, self.core,
            'Templetor')

    def get_functions(self):
        """
        Adds custom functions to template @TODO this should be initialized just once at runtime, not at every render!!

        :return:
        """
        c_functions = {}
        for f_name in self.core.functions_map:  # register template functions from extensions
            self.logger.debug('Adding template custom function "' + f_name +
                              '"')
            c_functions[f_name] = getattr(self.core.bbot, f_name)
        return c_functions

    def render(self, string: str) -> str:
        """
        Renders any string with configured bbot custom functions and bot session vars
        """
        # We still need a way to know if a string is a template or not, but Templator don't need enclosing
        # So for Templator, just enclose the whole string with {{ }} for BBot to know it is a template
        if re.search('({%).*(%})|({{.*}})', string) is None:
            self.logger.debug('Nothing to render')
            return string
        string = string.replace('{{', '')
        string = string.replace('}}', '')
        string = string.replace('{%', '')
        string = string.replace('%}', '')

        session_vars = {}
        if hasattr(self.core.bot, 'session'):
            session_vars = self.core.bot.session.get_var(self.core.bot.user_id)

        t_globals = session_vars
        # add predefined vars from publishers
        if hasattr(self.dotbot, 'botsubscription'):
            if hasattr(self.dotbot.botsubscription, 'predefined_vars'):
                t_globals = {
                    **t_globals,
                    **self.dotbot.botsubscription.predefined_vars
                }

        # get custom functions from extensions
        c_functions = self.get_functions()
        t_globals = {**t_globals, **c_functions}

        self.logger.debug('Rendering template: "' + str(string) + '"')
        templator_obj = Template(string, globals=t_globals)
        try:
            response = str(templator_obj())
        except NameError as e:
            err_msg = 'Template error: ' + str(e)
            self.core.logger.debug(err_msg)
            raise BBotException({'message': err_msg})

        if response[
                -1:] == '\n':  # Templator seems to add a trailing \n, remove it
            response = response[:-1]

        self.logger.debug('Template response: "' + response + '"')

        if response.find('<function BBotFunctionsProxy') is not -1:
            self.logger.error(
                'Templator returned an invalid response. Botdev forgot to escape $?'
            )
            response = '<TEMPLATE RENDERING ERROR. CHECK DEBUG DATA>'  # @TODO add debug data in context to the instruction executed

        return response

    def process(self):
        """
        Runs as pipeline process
        """
        for k, r in enumerate(self.core.response['output']):
            response_type = list(r)[0]
            if response_type == 'text':  #@TODO this should traverse the whole dict not just text
                response = r['text']
                self.core.response['output'][k]['text'] = self.render(response)
class TokenManagerSeedWallet():

    web3 = None

    def __init__(self, config: dict, dotbot: dict) -> None:
        """Initializes class"""
        self.config = config
        self.dotbot = dotbot

        self.logger_level = ''

        self.core = None
        self.seed_wallet_api_key = ''
        self.seed_wallet_url = ''

        self.logger = BBotLoggerAdapter(logging.getLogger('token_seedwallet'),
                                        self, self, '')

    def init(self, core):
        """Initializes some values"""
        pass

    def transfer(self,
                 fromUsername: str,
                 toUsername: str,
                 amount: float,
                 credential: str = ''):
        """
        Transfers tokens from one user to another

        :param fromUsername: (string) Username source
        :param toUsername: (string) Username destination
        :param amount: (float) amount of tokens to be transfered
        :param credential: (string) optional. Credentials needed to move funds from source (usually passphrase or private key)
        :returns: (string) TX hash
        """
        self.logger.debug('Transfering ' + str(amount) + ' SEED from user ' +
                          fromUsername + ' to user ' + toUsername)
        payload = {
            'username': fromUsername,
            'to': toUsername,
            'amount': str(amount)
        }
        r = self.do_request('post', 'send', payload)

        try:
            aw = r.json()
        except json.decoder.JSONDecodeError:
            raise Exception('Seed Wallet error response: ' + r.text)

        if r.status_code != 200:
            # check if the error is insufficient funds
            if r.status_code == 422 and aw['errors'].get(
                    'amount', {}
            ).get('message') == 'domain.wallet.validation.insufficient_funds':
                raise TokenManagerInsufficientFundsException()

            raise Exception('Seed Wallet error response: ' + str(aw))

        self.logger.debug('Transaction hash: ' + str(aw['transactionId']))
        return aw['transactionId']

    def get_balance(self, username: str):
        """
        """
        self.logger.debug('Getting user balance')
        r = self.do_request('get', 'balance', {'username': username})

        aw = r.json()
        if r.status_code != 200:
            raise Exception('Seed Wallet error response: ' + str(aw))

        return float(aw['balance'])

    def do_request(self, type: str, method: str, payload: str = dict):
        """
        """
        headers = {
            'API-INTEGRATION-KEY': self.seed_wallet_api_key,
            "Content-type": "application/json"
        }
        self.logger.debug('Requesting to Seed Wallet at url ' +
                          self.seed_wallet_url + method)
        if type == 'post':
            r = requests.post(self.seed_wallet_url + method,
                              json=payload,
                              headers=headers)
        if type == 'get':
            r = requests.get(self.seed_wallet_url + method,
                             params=payload,
                             headers=headers)
        self.logger.debug('Seed Wallet response: Code: ' + str(r.status_code) +
                          ': ' + str(r.text[0:300]))
        return r
class WeatherReport():
    """Returns Weather Report"""
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.
        """
        self.config = config
        self.dotbot = dotbot

        # vars set from plugins
        self.accuweather_api_key = ''
        self.logger_level = ''

        self.core = None
        self.logger = None

    def init(self, core: BBotCore):
        """

        :param bot:
        :return:
        """
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('core_fnc.weather'),
                                        self, self.core.bot, '$weather')

        self.method_name = 'weather'
        self.accuweather_text = 'Weather forecast provided by Accuweather'
        self.accuweather_image_url = 'https://static.seedtoken.io/AW_RGB.png'

        core.register_function(
            'weather', {
                'object': self,
                'method': self.method_name,
                'cost': 0.1,
                'register_enabled': True
            })
        # we register this to add accuweather text even when result is cached from extensions_cache decorator
        smokesignal.on(BBotCore.SIGNAL_CALL_BBOT_FUNCTION_AFTER,
                       self.add_accuweather_text)

    @BBotCore.extensions_cache
    def weather(self, args, f_type):
        """
        Returns weather report
        @TODO return forecast based on args[1] date

        :param args:
        :param f_type:
        :return:
        """
        try:
            location = self.core.resolve_arg(args[0], f_type)
        except IndexError:
            raise BBotException({
                'code': 0,
                'function': 'weather',
                'arg': 0,
                'message': 'Location is missing.'
            })

        try:
            date = args[1]
        except IndexError:
            date = 'today'  # optional. default 'today'

        self.logger.info(f'Retrieving weather for {location}')
        st = self.search_text(location)

        if not st:
            self.logger.info("Location not found. Invalid location")
            return {
                'text':
                '<No weather data or invalid location>',  #@TODO should raise a custom exception which will be used for flow exceptions
                'canonicalLocation': location
            }

        location_key = st[0].get('Key', None)

        self.logger.debug("Accuweather Location Key: " + location_key)

        canonical_location = st[0]['LocalizedName'] + ', ' + st[0]['Country'][
            'LocalizedName']
        self.logger.debug('Canonical location: ' + canonical_location)
        self.logger.debug('Requeting Accuweather current conditions...')
        r = requests.get(
            f'http://dataservice.accuweather.com/currentconditions/v1/{location_key}?apikey={self.accuweather_api_key}&details=false'
        )
        self.logger.debug('Accuweather response: ' + str(r.json())[0:300])
        if r.status_code == 200:
            aw = r.json()

            return {
                'text': aw[0]['WeatherText'],
                'temperature': {
                    'metric': str(aw[0]['Temperature']['Metric']['Value']),
                    'imperial': str(aw[0]['Temperature']['Imperial']['Value'])
                },
                'canonicalLocation': canonical_location
            }

        err_msg = r.json()['fault']['faultstring']
        self.logger.critical(err_msg)
        raise BBotExtensionException(err_msg, BBotCore.FNC_RESPONSE_ERROR)

    def search_text(self, location):
        # get locationkey based on provided location
        self.logger.info(f'Requesting Accuweather location key...')
        r = requests.get(
            f'http://dataservice.accuweather.com/locations/v1/search?apikey={self.accuweather_api_key}&q={location}&details=false'
        )
        self.logger.debug('Accuweather response: ' + str(r.json())[0:300])
        if r.status_code == 200:
            return r.json()

        err_msg = r.json()['fault']['faultstring']
        self.logger.critical(err_msg)
        raise BBotExtensionException(err_msg, BBotCore.FNC_RESPONSE_ERROR)

    def add_accuweather_text(self, data):
        # check if call is made from a weather call
        if data['name'] is self.method_name:
            # check if the call was successful
            if data['response_code'] is BBotCore.FNC_RESPONSE_OK:
                # check if text is already added
                if not self.core.bbot.outputHasText(self.accuweather_text):
                    # adds accuweather logo to the bots response
                    self.core.bbot.text(self.accuweather_text)
                    self.core.bbot.image(self.accuweather_image_url)
Exemple #11
0
class BotFramework:
    
    def __init__(self, config: dict, dotbot: dict) -> None:
        """        
        """
        self.config = config
        self.dotbot = dotbot
        self.dotdb = None #        
        self.logger_level = ''
        self.access_token = None
        self.dotbot = None

    def init(self, core):
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('channel_botframerwork'), self, self.core, 'ChannelBotFramework')        

        self.logger.debug("Listening BotFramework from path: " + self.get_webhook_path())

    def endpoint(self, request=dict, publisherbot_token=str):
        print('------------------------------------------------------------------------------------------------------------------------')
        self.logger.debug('Received request: ' + str(request.data))
        self.logger.debug(f'Received a BotFramework webhook request for publisher token {publisherbot_token}')        

        try:
            params = request.get_json(force=True)
            org_id = 1

            # get publisher user id from token
            pub_bot = self.dotdb.find_publisherbot_by_publisher_token(publisherbot_token)
            if not pub_bot:
                raise Exception('Publisher not found')
            self.logger.debug('Found publisher: ' + pub_bot.publisher_name + ' - for bot id: ' + pub_bot.bot_id)
            pub_id = pub_bot.publisher_name
                    
            dotbot = self.dotdb.find_dotbot_by_bot_id(pub_bot.bot_id)                    
            self.dotbot = dotbot
            if not dotbot:
                raise Exception('Bot not found')
            bot_id = dotbot.bot_id
            # build extended dotbot 
            dotbot.services = pub_bot.services
            dotbot.channels = pub_bot.channels
            dotbot.botsubscription = pub_bot

            if 'botframework' not in dotbot.channels.keys():
                raise BBotException("Botframework chanel in not enabled")

            self.app_id = pub_bot.channels['botframework']['app_id']
            self.app_password = pub_bot.channels['botframework']['app_password']

            self.service_url = params['serviceUrl']
            user_id = params['from']['id']
           
            bbot_request = params
            if not bbot_request.get('text'):
                bbot_request['text'] = 'hello'

            self.response_payload = {
                'channelId': params['channelId'],
                'conversation': params['conversation'],
                'from': params['recipient'],
                'id': params['id'],                
                'replyToId': params['id'],                            
                #'inputHint': 'acceptingInput',
                #'localTimestamp': params['localTimestamp'],
                #'locale': params['locale'],
                #'serviceUrl': params['serviceUrl'],
                #'timestamp': datetime.datetime.now().isoformat(),
            }

            channel_id = params['channelId']
            
            config = load_configuration(os.path.abspath(os.path.dirname(__file__) + "../../../instance"), "BBOT_ENV")
            bbot = BBotCore.create_bot(config['bbot_core'], dotbot)
            self.logger.debug('User id: ' + user_id)

            # authenticate
            self.authenticate()

            req = bbot.create_request(bbot_request, user_id, bot_id, org_id, pub_id, channel_id)                           
            bbot_response = bbot.get_response(req)
            http_code = 200
            
        except Exception as e:          
            if isinstance(e, BBotException): # BBotException means the issue is in bot userland, not rhizome
                http_code = 200                                                
            else:
                self.logger.critical(str(e) + "\n" + str(traceback.format_exc()))            
                http_code = 500            
                
            if os.environ['BBOT_ENV'] == 'development':                
                bbot_response = {                    
                    'output': [{'type': 'message', 'text': cgi.escape(str(e))}], #@TODO use bbot.text() 
                    'error': {'traceback': str(traceback.format_exc())}
                    }
            else:
                bbot_response = {'output': [{'type': 'message', 'text': 'An error happened. Please try again later.'}]}
                # @TODO this should be configured in dotbot
                # @TODO let bot engine decide what to do?
            
        self.logger.debug("Response from restful channel: " + str(bbot_response))
        self.to_botframework(bbot_response)

    def get_webhook_url(self) -> str:
        return self.config['webhook_uri']

    def get_webhook_path(self) -> str:
        parsed_url = urlparse(self.config['webhook_uri'])
        return parsed_url.path 

    def to_botframework(self, bbot_response):
        
        response_payload = copy.deepcopy(self.response_payload)

        for br in bbot_response['output']:
            
            r = {**response_payload, **br}

            self.logger.debug("Response sent back to BotFramework: " + str(r))        
            url = self.service_url + 'v3/conversations/' + r['conversation']['id'] + '/activities/' + r['id']
            self.logger.debug("To url: " + url)
            response = requests.post(url, headers=self.directline_get_headers(), json=r)
            msg = "BotFramework response: http code: " + str(response.status_code) + " message: " + str(response.text)
            if response.status_code != 200:
                raise BBotException(msg)
            self.logger.debug(msg)

    def directline_get_headers(self):
        headers = {
            'Content-Type': 'application/json',            
        }       
        if self.access_token:
            headers['Authorization'] = 'Bearer ' + self.access_token
        return headers

    def authenticate(self):
        # first check if we have access_token in database
        self.logger.debug("Looking for Azure AD access token in database...")
        stored_token = self.dotdb.get_azure_ad_access_token(self.dotbot.bot_id)        
        if stored_token:
            expire_date = stored_token['expire_date']
            if  expire_date >= datetime.datetime.utcnow():
                # got valid token
                self.access_token = stored_token['access_token']                
                self.logger.debug('Got valid token from db. Will expire in ' + str(stored_token['expire_date']))
                return
            else:
                self.logger.debug('Got expired token. Will request new one')
        else:
            self.logger.debug('There is no token in database. Will request one')

        url = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token"
        payload = {
            "grant_type": "client_credentials",
            "client_id": self.app_id,
            "client_secret": self.app_password,
            "scope": "https://api.botframework.com/.default"
        }
        self.logger.debug("Sending request to Microsoft OAuth with payload: " + str(payload))
        response = requests.post(url, data=payload)    
        msg = "Response from Microsoft OAuth: http code: " + str(response.status_code) + " message: " + str(response.text)
        if response.status_code != 200:
            raise BBotException(msg)
        self.logger.debug(msg)
        json_response = response.json()
        self.access_token = json_response['access_token']
        expire_date = datetime.datetime.utcnow() + datetime.timedelta(0, json_response['expires_in']) # now plus x seconds to get expire date 

        self.dotdb.set_azure_ad_access_token(self.dotbot.bot_id, self.access_token, expire_date)
Exemple #12
0
class DialogFlow(ChatbotEngine):
    """
    BBot engine that calls external program.
    """

    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

    def init(self, core: BBotCore):
        """
        Initializebot engine 
        """
        super().init(core)

        self.logger = BBotLoggerAdapter(logging.getLogger('dialogfl_cbe'), self, self.core)
        
        self.service_account = json.loads(self.dotbot.chatbot_engine['serviceAccount'])
        self.platform = None

        self.available_platforms = {
            'google_assistant': 'ACTIONS_ON_GOOGLE',
            'facebook': 'FACEBOOK',
            'slack': 'SLACK',
            'telegram': 'TELEGRAM',
            'skype': 'SKYPE'
        }
        
        credentials = Credentials.from_service_account_info(self.service_account)
        self.session_client = dialogflow.SessionsClient(credentials=credentials)
        
    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """
        
        super().get_response(request)

        self.platform = self.get_platform()

        input_text = request['input']['text']
        input_text.replace("\n", " ")
        
        session_id = request['user_id']
        language_code = 'en-US'
        
        """
        Returns the result of detect intent with texts as inputs.

        Using the same `session_id` between requests allows continuation
        of the conversaion.
        """
                
        session = self.session_client.session_path(self.service_account['project_id'], session_id)
        
        text_input = dialogflow.types.TextInput(
            text=input_text, language_code=language_code)

        query_input = dialogflow.types.QueryInput(text=text_input)

        response_class = self.session_client.detect_intent(
            session=session, query_input=query_input)

        response = MessageToDict(response_class)

        self.logger.debug('Response: ' + json.dumps(response, indent=4, sort_keys=True))
                
        self.logger.debug('Detected intent: {} (confidence: {})'.format(
            response['queryResult']['intent']['displayName'],
            response['queryResult']['intentDetectionConfidence']))
                    
        self.logger.debug('Looking for media cards for platform: ' + str(self.platform))
        found_text = False
        for fm in response['queryResult']['fulfillmentMessages']:
            # get text response
            text = None
            if fm.get('platform') == self.platform: # using get() because dialogflow is sending some objects without platform property...?(or is MessageToDict()?)
                if fm.get('text'):                    
                    text = fm['text']['text'][0]                    
                elif fm.get('simpleResponses'): # text for google assistant
                    text = fm['simpleResponses']['simpleResponses'][0]['textToSpeech']
                
                if text:
                    found_text = True
                    self.core.bbot.text(text)
                
                # get card/media and convert it to heroCard() arguments                            
                if fm.get('card'): # telegram, facebook
                    title = fm['card'].get('title')
                    image_url = fm['card'].get('imageUri')
                    subtitle = fm['card'].get('subtitle')
                    text = fm['card'].get('text')
                    buttons = []
                    if fm['card'].get('buttons'):                        
                        for b in fm['card']['buttons']:
                            buttons.append(self.core.bbot.imBack(b['text'], b.get('postback')))

                    self.core.bbot.heroCard(image_url, title, subtitle, text, buttons)
                                
                if fm.get('basicCard'): # google assistant
                    bcard = fm['basicCard']
                    title = bcard.get('title')
                    subtitle = bcard.get('subtitle')
                    text = bcard.get('formattedText')
                    image_url = bcard.get('image', {}).get('imageUri')
                    buttons = []
                    if bcard.get('buttons'):
                        for b in bcard['buttons']:
                            if 'openUriAction' in b:
                                buttons.append(self.core.bbot.openUrl(b['title'], b['openUriAction']['uri']))

                    self.core.bbot.heroCard(image_url, title, subtitle, text, buttons)
                    
                if fm.get('image'): #slackware
                    self.core.bbot.imageCard(fm['image']['imageUri'])
      
                # get suggestions (chips for google assistat)
                if fm.get('suggestions'):
                    suggested_actions = []
                    for sa in fm['suggestions']['suggestions']:
                        suggested_actions.append(self.core.bbot.imBack(sa['title']))
                    
                    self.core.bbot.suggestedActions(suggested_actions)

                # get quick replies (skype) -- it allows just one quick reply from gui??
                if fm.get('quickReplies'):
                    self.core.bbot.suggestedActions(self.core.bbot.imBack(fm['quickReplies']['title'], fm['quickReplies']['quickReplies'][0]))
                        
        if not found_text: # if specific platform text was not defined, show default 
            self.core.bbot.text(response['queryResult'].get('fulfillmentText', ''))

    def get_platform(self):
        # first check if there is a forcePlatform set. if not, take channelId from channel
        current_platform = self.get_dialogflow_platform_from_channel_id(self.channel_id)        
        force_platform = self.dotbot.chatbot_engine.get('forcePlatform')
        if force_platform:
            self.logger.debug('Platform should be "' + str(current_platform) + '" based in current channelId "' + self.channel_id + '" but...')
            self.logger.debug('Setting forced by config platform to: ' + str(force_platform))
            platform = force_platform
        else:            
            platform = current_platform
            self.logger.debug('Setting platform to "' + str(platform) + '" based on current channelId "' + str(self.channel_id) + '"')
                
        # if selected channelId is not supported (might be some channel from restful like hadron or webchat) will set platform with defaultPlatform value from dotbot chatbot_engine object 
        if platform not in self.available_platforms.values():                                        
            self.logger.debug('Platform "' + str(platform) + '" is invalid. Setting platform to default value: ' +str(self.dotbot.chatbot_engine.get('defaultPlatform')))
            platform = self.dotbot.chatbot_engine.get('defaultPlatform') 
        
        #if platform not in self.available_platforms.values():                                        
        #    raise Exception("Dialoflowg platform not supported: " + str(platform))

        return platform

    def get_dialogflow_platform_from_channel_id(self, channel_id: str):                
        return self.available_platforms.get(channel_id)
Exemple #13
0
class Telegram:
    """Translates telegram request/response to flow"""

    def __init__(self, config: dict, dotbot: dict) -> None:
        """        
        """
        self.config = config
        self.dotbot = dotbot
        self.dotdb = None #
        self.api = None
        self.logger_level = ''

        self.response_type_fnc = {
            'none': self.none,
            'text': self.send_text,
            'image': self.send_image,
            'video': self.send_video,
            'audio': self.send_audio,
            'buttons': self.send_buttons
        }
        self.default_text_encoding = 'HTML'

    def init(self, core):
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('channel_telegram'), self, self.core, 'ChannelTelegram')        

    def endpoint(self, request=dict, publisherbot_token=str):
        print('------------------------------------------------------------------------------------------------------------------------')
        self.logger.debug(f'Received a Telegram webhook request for publisher token {publisherbot_token}')

        enabled = self.webhook_check(publisherbot_token)
        if enabled:
            try:
                params = request.get_json(force=True)
                org_id = 1

                # checks if bot is telegram enabled
                # if not, it delete the webhook and throw an exception
                
                # get publisher user id from token
                pub_bot = self.dotdb.find_publisherbot_by_publisher_token(publisherbot_token)
                if not pub_bot:
                    raise Exception('Publisher not found')
                self.logger.debug('Found publisher: ' + pub_bot.publisher_name + ' - for bot id: ' + pub_bot.bot_id)
                pub_id = pub_bot.publisher_name
                
                # if 'runBot' in params:
                #    run_bot = self.params['runBot']
            
                dotbot = self.dotdb.find_dotbot_by_bot_id(pub_bot.bot_id)                    
                if not dotbot:
                    raise Exception('Bot not found')
                bot_id = dotbot.bot_id
                # build extended dotbot 
                dotbot.services = pub_bot.services
                dotbot.channels = pub_bot.channels
                dotbot.botsubscription = pub_bot
                
                token = pub_bot.channels['telegram']['token']
                self.set_api_token(token)

                user_id = self.get_user_id(params)
                telegram_recv = self.get_message(params)
                self.logger.debug('POST data from Telegram: ' + str(params))
                bbot_request = self.to_bbot_request(telegram_recv)

                config = load_configuration(os.path.abspath(os.path.dirname(__file__) + "../../../instance"), "BBOT_ENV")
                bbot = BBotCore.create_bot(config['bbot_core'], dotbot)
                self.logger.debug('User id: ' + user_id)
                req = bbot.create_request(bbot_request, user_id, bot_id, org_id, pub_id)                           
                bbot_response = bbot.get_response(req)
                
            except Exception as e:           
                self.logger.critical(str(e) + "\n" + str(traceback.format_exc()))            
                if os.environ['BBOT_ENV'] == 'development':
                    bbot_response = {
                        'output': [{'text': cgi.escape(str(e))}],
                        'error': {'traceback': str(traceback.format_exc())}
                        }
                else:
                    bbot_response = {'output': [{'text': 'An error happened. Please try again later.'}]}
                    # @TODO this should be configured in dotbot
                    # @TODO let bot engine decide what to do?

            self.logger.debug("Response from telegram channel: " + str(bbot_response))
            self.send_response(bbot_response)

    def webhook_check(self, publisherbot_token):

        pb = self.dotdb.find_publisherbot_by_publisher_token(publisherbot_token)

        if pb.channels.get('telegram'):
            return True

        self.logger.warning(f'Deleting invalid Telegram webhook for publisher bot token: {publisherbot_token} - publisher id: ' + pb.publisher_name)
        self.set_api_token(pb.channels['telegram']['token'])
        delete_ret = self.api.deleteWebhook()
        if delete_ret:
            self.logger.warning("Successfully deleted.")
            return False
            #raise Exception('Received a telegram webhook request on a telegram disabled bot. The webhook was deleted now.')
        else:
            error = "Received a telegram webhook request on a telegram disabled bot and couldn't delete the invalid webhook"
            self.logger.error(error)
            raise Exception(error)

    def set_api_token(self, token: str):
        self.api = telepot.Bot(token)

    def to_bbot_request(self, request: str) -> str:
        return {'text': request}

    def get_webhook_url(self) -> str:
        return self.config['webhook_uri']

    def get_webhook_path(self) -> str:
        return urlparse(self.config['webhook_uri']).path

    ### Responses

    def send_response(self, bbot_response: dict):
        """
        Parses BBot output response and sends content to telegram
        """
        # @TODO check if we can avoid sending separate api request for each text if there are more than one

        bbot_output = bbot_response['output']

        t_output = self.buttons_process(bbot_output)

        # Iterate through bbot responses
        for br in t_output:
            response_type = list(br.keys())[0]
            if callable(self.response_type_fnc.get(response_type)):
                self.response_type_fnc[response_type](br)
            else:
                self.logger.warning('Unrecognized BBot output response "' + response_type)

    def none(self, arg):
        """

        :return:
        """
        pass

    def send_text(self, text: list):
        """
        Sends text to telegram
        """
        text = text['text']
        if type(text) is str and text:
            self.api.sendMessage(self.user_id, text, parse_mode=self.default_text_encoding)
        else:
            self.logger.error("Trying to send empty message to Telegram")

    def send_image(self, image: dict):
        """
        Sends image to telegram
        """
        image = image['image']
        caption = None
        if image.get('title'):
            caption = f"*{image['title']}*"
            if image.get('subtitle'):
                caption += f"\n{image['subtitle']}"

        self.api.sendPhoto(self.user_id, image['url'], caption=caption, parse_mode=self.default_text_encoding,
                           disable_notification=None, reply_to_message_id=None, reply_markup=None)
        
    def send_video(self, video: dict):
        """
        Sends video to telegram
        """
        video = video['video']
        caption = None
        if video.get('title'):
            caption = f"*{video['title']}*"
            if video.get('subtitle'):
                caption += f"\n{video['subtitle']}"

        self.api.sendVideo(self.user_id, video['url'], duration=None, width=None, height=None,
                           caption=caption, parse_mode=self.default_text_encoding, supports_streaming=None, disable_notification=None, reply_to_message_id=None, reply_markup=None)

    def send_audio(self, audio: dict):
        """
        Sends audio to telegram
        """
        audio = audio['audio']
        caption = None
        if audio.get('title'):
            caption = f"*{audio['title']}*"
            if audio.get('subtitle'):
                caption += f"\n{audio['subtitle']}"

        #self.self.sendAudio(self.user_id, audio['uri'], caption=caption, parse_mode='Markdown', duration=None, performer=None,
        #   title="Title?", disable_notification=None, reply_to_message_id=None, reply_markup=None)
        self.api.sendVoice(self.user_id, audio['url'], caption=caption, parse_mode=self.default_text_encoding, duration=None,
                           disable_notification=None, reply_to_message_id=None, reply_markup=None)

    def send_buttons(self, buttons: dict):
        """
        Sends buttons to telegram
        """
        buttons = buttons['buttons']
        from telepot.namedtuple import InlineKeyboardMarkup, InlineKeyboardButton
        telegram_buttons = []
        for button in buttons['buttons']:
            telegram_buttons.append([InlineKeyboardButton(text=button['text'], callback_data=button['postback'])])
        keyboard = InlineKeyboardMarkup(inline_keyboard=telegram_buttons)        
        self.api.sendMessage(self.user_id, buttons['text'], reply_markup=keyboard)


    ### Request

    def get_user_id(self, request: dict):
        if request.get('message'): #regular text
            self.user_id = str(request['message']['from']['id'])
            return self.user_id

        if request.get('callback_query'): # callback from a button click
            return str(request['callback_query']['from']['id'])

    def get_message(self, request: dict):
        if request.get('message'): #regular text
            return request['message']['text']

        if request.get('callback_query'): # callback from a button click
            return request['callback_query']['data']


    ### Misc

    def webhooks_check(self):
        """
        This will check and start all webhooks for telegram enabled bots
        """

        sleep_time = 3 # 20 requests per minute is ok?

        # get all telegram enabled bots
        telegram_pubbots = self.dotdb.find_publisherbots_by_channel('telegram')
        
        if not telegram_pubbots:
            self.logger.debug('No telegram enabled bots')
            return

        # cert file only used on local machines with self-signed certificate
        cert_file = open(self.config['cert_filename'], 'rb') if self.config.get('cert_filename') else None

        for tpb in telegram_pubbots:                    
            if tpb.channels['telegram']['token']:
                self.logger.debug('---------------------------------------------------------------------------------------------------------------')
                self.logger.debug('Checking Telegram webhook for publisher name ' + tpb.publisher_name + ' publisher token: ' + tpb.token + ' - bot id: ' + tpb.bot_id + '...')
                self.logger.debug('Setting token: ' + tpb.channels['telegram']['token'])
                
                try:
                    self.set_api_token(tpb.channels['telegram']['token'])

                    # build webhook url
                    url = self.get_webhook_url().replace('<publisherbot_token>', tpb.token)

                    # check webhook current status (faster than overriding webhook)
                    webhook_info = self.api.getWebhookInfo()
                    self.logger.debug('WebHookInfo: ' + str(webhook_info))
                    webhook_notset = webhook_info['url'] == ''
                    if webhook_info['url'] != url and not webhook_notset: # webhook url is set and wrong
                        self.logger.warning('Telegram webhook set is invalid (' + webhook_info['url'] + '). Deleting webhook...')
                        delete_ret = self.api.deleteWebhook()
                        if delete_ret:
                            self.logger.warning("Successfully deleted.")
                        else:
                            error = "Couldn't delete the invalid webhook"
                            self.logger.error(error)
                            raise Exception(error)
                        webhook_notset = True
                    if webhook_notset: # webhook is not set
                        self.logger.info(f'Setting webhook for bot id ' + tpb.bot_id + f' with webhook url {url}')
                        set_ret = self.api.setWebhook(url=url, certificate=cert_file)
                        self.logger.debug("setWebHook response: " + str(set_ret))
                        if set_ret:
                            self.logger.info("Successfully set.")
                        else:
                            error = "Couldn't set the webhook"
                            self.logger.error(error)
                            raise Exception(error)
                    else:
                        self.logger.debug("Webhook is correct")
                except telepot.exception.TelegramError:
                    self.logger.debug('Invalid Telegram token') # This might happen when the token is invalid. We need to ignore and ontinue

                time.sleep(sleep_time)

    def buttons_process(self, bbot_output: dict) -> dict:
        """
        Groups text and buttons for Telegram API.
        BBot response specification do not groups buttons and texts, so his is a process to do it for self.

        Buttons needs special treatment because Telegram ask for mandatory text output with it
        so we need to find and send text output at the same time

        :param bbot_output: BBot response output
        :return: Telegram buttons object
        """
        for idx, br in enumerate(bbot_output):
            response_type = list(br.keys())[0]

            if response_type == 'button':
                # look for previous text
                if bbot_output[idx - 1].get('text'):
                    buttons_text = bbot_output[idx - 1]['text']
                    bbot_output[idx - 1] = {'none': []}  # will be send with buttons
                else:
                    buttons_text = ''
                # look for next buttons
                buttons = [br['button']]
                for idx2, next_btn in enumerate(bbot_output[idx + 1:len(bbot_output)]):
                    if next_btn.get('button'):
                        buttons.append(next_btn['button'])
                        bbot_output[idx2 + idx + 1] = {'none': []}  # will be added with the grouped buttons
                    elif next_btn.get('text'):  # when it founds a text, stops looking for more buttons
                        break

                bbot_output[idx] = {  # modifying button object for self.send_button()
                    'buttons': {
                        'text': buttons_text,
                        'buttons': buttons
                    }
                }
        return bbot_output
Exemple #14
0
class TTSAmazonPolly():

    def __init__(self, config: dict, dotbot: dict) -> None:
        """Initializes class"""
        self.config = config
        self.dotbot = dotbot
        self.logger_level = ''
        self.voice_id = 1
        self.voice_locale = 'en_US'
        self.tts_service_name = 'AmazonPolly'

        # https://docs.aws.amazon.com/polly/latest/dg/voices-in-polly.html
        self.voice_id_locale_map = {
            'en_US': ['Joanna', 'Ivy', 'Kendra', 'Kimberly', 'Salli', 'Joey', 'Justin', 'Matthew'],
            'en_GB': ['Emma', 'Amy', 'Brian']
        }

        self.logger = BBotLoggerAdapter(logging.getLogger('tts'), self, self, 'tts')        
    

    def init(self, core):
        pass

    def get_speech_audio_url(self, text: str, scale_time: int=100) -> str:
        """Returns audio file url with speech synthesis"""

        text = self.set_scaletime(text, scale_time)

        filename = self.get_audio_filename(text)

        self.logger.debug('Providing voice audio file "' + filename + '"')

        # check if audio file is available in cache folder
        exists = os.path.isfile(self.get_file_full_path(filename))
        if not exists:
            self.logger.debug('Not found in cache. Requesting audio file to amazon polly')
            self.gen_speech_audio(text, filename)
        else:
            self.logger.debug('Found audio file in cache')

        return self.config['cache_web_url'] + filename

    def gen_speech_audio(self, text: str, filename: str) -> bool:
        """Calls TTS service and places the audio file somewhere"""

        polly_client = boto3.Session(
                aws_access_key_id = self.config['aws_access_key_id'],                     
                aws_secret_access_key = self.config['aws_secret_access_key'],
                region_name = self.config['aws_region_name']).client('polly')

        voice_id = self.get_amazonpolly_voice_id_from_locale()
        
        
        text = self.process(text)

        self.logger.debug('Requesting speech audio to Amazon Polly voice id "' + voice_id + '" - Text: "' + text + '"')
        response = polly_client.synthesize_speech(                    
                    VoiceId = voice_id,
                    OutputFormat='mp3', 
                    TextType='ssml',
                    Text = text)

        self.logger.debug("Amazon Polly reponse: " + str(response))

        if response.get('AudioStream', None):
            file = open(self.get_file_full_path(filename), 'wb')
            file.write(response['AudioStream'].read())
            file.close()
            return True

        return False

    def get_filename(self, text: str) -> str:
        """Returns filename based on the text hash and prefixes tts service name and voice locale and voice id"""    
        hash_object = hashlib.md5(text.encode())
        return self.tts_service_name + '_' + self.voice_locale + '_' + str(self.voice_id) + '_' + hash_object.hexdigest()

    def get_audio_filename(self, text: str) -> str:
        """Returns filename based on the text hash and prefixes tts service name and voice locale and voice id and format type"""    
        return self.get_filename(text) + '.mp3'
        
    def get_file_full_path(self, filename: str) -> str:
        """Returns audio file full path"""
        return self.config['cache_local_path'] + '/' + filename
        
    def get_amazonpolly_voice_id_from_locale(self) -> str:
        """Returns voice id based on locale and bbot voice id
            If no valid voice id is provided will default to id 1"""
        return self.voice_id_locale_map[self.voice_locale][int(self.voice_id)]

    
    def set_scaletime(self, text, scale_time: int=100) -> str:
        """Parses the ssml to adjust all timescales on it"""
        #@TODO
        return '<speak><prosody rate="' + str(scale_time) + '%">' + text + '</prosody></speak>'

    
    def get_speechmark(self, text: str, speechmark_type: str, time_scale: int) -> str:
        """Returns speechmark and stores it in cache"""

        text = self.set_scaletime(text, time_scale)

        filename = self.get_speechmark_filename(text, speechmark_type)

        self.logger.debug('Checking if Amazon Polly word speechmark is in cache (file "' + filename + '"')

        # checking if it's already in the cache
        fullpath_filename = self.get_file_full_path(filename)
        try:
            with open(fullpath_filename, 'r') as myfile:
                speechmark = myfile.read()
                self.logger.debug('Found in cache')
                return json.loads(speechmark)
        except FileNotFoundError:
            self.logger.debug('It\'s not. Requsting it to Amazon Polly')

        # it's not, lets generate it
        polly_client = boto3.Session(
                aws_access_key_id = self.config['aws_access_key_id'],                     
                aws_secret_access_key = self.config['aws_secret_access_key'],
                region_name = self.config['aws_region_name']).client('polly')

        voice_id = self.get_amazonpolly_voice_id_from_locale()
        
        text = self.process(text)

        self.logger.debug('Requesting speechmark word to Amazon Polly')
        response = polly_client.synthesize_speech(
                    SpeechMarkTypes = [speechmark_type],
                    VoiceId = voice_id,
                    OutputFormat='json', 
                    TextType='ssml',
                    Text = text)

        self.logger.debug("Amazon Polly reponse: " + str(response))

        if response.get('AudioStream', None):   
            sm = response['AudioStream'].read().decode('utf8') # Polly provides a binary so convert it to utf8 string        
            # convert it to proper json list (still a json string anyway, and why is Polly sending json NL separated???)
            sm = '[' + sm.replace("\n", ",")[:-1] + ']'
            # now store to cache
            file = open(fullpath_filename, 'w')            
            file.write(sm)
            file.close()
            self.logger.debug('Providing and storing speechmark "' + filename + '"')
            self.logger.debug('Speechmark response:')
            self.logger.debug(sm)
            return json.loads(sm) # now convert json string to dict

        return False


    def get_speechmark_filename(self, text: str, speechmark_type: str) -> str:
        """Returns filename based on the text hash and prefixes tts service name and voice locale and voice id and format type"""    
        return self.get_filename(text) + '_' + speechmark_type + '_speechmark.txt'

    def get_visemes(self, text, time_scale: int) -> dict:
        """
        Returns a dict with visemes from Amazon Polly
        """        
        visemes = self.get_speechmark(text, 'viseme', time_scale)
        
        v_res = []
        for v in visemes:            
            v_res.append({
                'time': v['time'],
                'value': v['value'] 
            })            
        self.logger.debug('Visemes response: ' + str(v_res))
        return v_res
   
        
    def process(self, text: str) -> str:
        """
        """
        # convert symbols to text (< 'less than', > 'greater than', etc)
        text = self.convert_symbols(text)

        # convert html content
        text = self.convert_html(text)
        return text

    def convert_symbols(self, text: str) -> str:
        """Convert symbols to text (< 'less than', > 'greater than', etc) 
        @TODO multilanguage support
        @TODO check previous comas
        """
        text = text.replace('&lt;', ', less than symbol, ')
        text = text.replace('&gt;', ', greater than symbol, ')
        text = text.replace('[', ', open square brackets, ')
        text = text.replace(']', ', close sqare brackets, ')
        text = text.replace('{', ', open curly brackets, ')
        text = text.replace('}', ', close curly brackets, ')
        text = text.replace('(', ', ')
        text = text.replace(')', ', ')        
        text = text.replace('&quot', ', quotes, ')
        text = text.replace('&#x27;', '')        
        text = text.replace('$', ', $ symbol, ')        # @TODO this should convert values like $123 into '123 dollars'
        return text

    def convert_html(self, text: str) -> str:
        """
        """
        soup = BeautifulSoup(text, "html.parser") # using this parser prevents BeautifulSoup trying to rewrite a well-formated html
        
        # converts all anchor tags into its enclosed text
        all_anchors = soup('a')        
        for aa in all_anchors:
            aa.replace_with(aa.get_text())        

        return str(soup)
class DialogFlow(ChatbotEngine):
    """
    BBot engine that calls external program.
    """
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

    def init(self, core: BBotCore):
        """
        Initializebot engine 
        """
        super().init(core)

        self.logger = BBotLoggerAdapter(logging.getLogger('dialogfl_cbe'),
                                        self, self.core)
        self.chatbot_engine = self.dotbot.chatbot_engine

        self.service_account = json.loads(
            self.chatbot_engine['serviceAccount'])

        credentials = Credentials.from_service_account_info(
            self.service_account)
        self.session_client = dialogflow.SessionsClient(
            credentials=credentials)

    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """

        self.request = request

        input_text = request['input']['text']
        input_text.replace("\n", " ")

        session_id = request['user_id']
        language_code = 'en-US'
        """
        Returns the result of detect intent with texts as inputs.

        Using the same `session_id` between requests allows continuation
        of the conversaion.
        """

        session = self.session_client.session_path(
            self.service_account['project_id'], session_id)

        text_input = dialogflow.types.TextInput(text=input_text,
                                                language_code=language_code)

        query_input = dialogflow.types.QueryInput(text=text_input)

        response = self.session_client.detect_intent(session=session,
                                                     query_input=query_input)

        self.logger.debug('Detected intent: {} (confidence: {})'.format(
            response.query_result.intent.display_name,
            response.query_result.intent_detection_confidence))

        self.core.bbot.text(response.query_result.fulfillment_text)
Exemple #16
0
class ActivityLogger():
    """Registers in a DB some bot activity"""

    ACTIVITY_TYPE_VOLLEY = 1
    ACTIVITY_TYPE_FUNCTION = 2

    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.
        """
        self.config = config
        self.dotbot = dotbot

        self.logger_level = ''

        self.core = None
        self.logger = None

        self.logger = BBotLoggerAdapter(logging.getLogger('core_ext.reg_act'),
                                        self, self.core, 'RegisterActivity')

    def init(self, core: BBotCore):
        """
        :param bot:
        :return:
        """
        self.core = core

        # Initialize the connection @TODO improve this
        if 'mongodb_uri' not in self.config:
            raise RuntimeError("FATAL ERR: Missing config var uri")
        uri = self.config['mongodb_uri']
        client = MongoClient(uri)
        parts = uri.split("/")
        last_part = parts.pop()
        parts = last_part.split("?")
        database_name = parts[0]
        self.mongo = client[database_name]

        smokesignal.on(BBotCore.SIGNAL_CALL_BBOT_FUNCTION_AFTER,
                       self.register_function_call)
        smokesignal.on(BBotCore.SIGNAL_GET_RESPONSE_AFTER,
                       self.register_volley)

    def register_volley(self, data):
        """
        Register each volley
        """

        self.logger.debug('Registering volley activity')
        self.register_activity({
            'type': self.ACTIVITY_TYPE_VOLLEY,
            'code': BBotCore.FNC_RESPONSE_OK,
            'cost': self.dotbot.per_use_cost
        })

    def register_function_call(self, data):
        """
        Register each function call
        """
        if data['register_enabled'] is True:
            self.logger.debug(
                'Registering function call activity: function name "' +
                data['name'])

            if 'error_message' in data:
                data['error_message'] = data['error_message']

            self.register_activity({
                'data': '',  #@TDODO to define
                'type': self.ACTIVITY_TYPE_FUNCTION,
                'code': data['response_code'],
                'cost': data['data']['cost']
            })

    def register_activity(self, data):
        """
        Common register function
        """
        doc = {
            "_id": ObjectId(),
            "type": data['type'],
            "code": data['code'],
            "datetime": datetime.datetime.utcnow(),
            "botId": self.core.bot.bot_id,
            "userId": self.core.bot.user_id,
            'pubId': self.core.bot.pub_id,
            "cost": data['cost']
        }
        #if data.get('data') is not None:
        #    doc = {**doc, **data['data']}

        self.mongo.activity.insert(doc)
Exemple #17
0
class RasaServer(ChatbotEngine):
    """
    """
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

        self.recv = []

    def init(self, core: BBotCore):
        """
        Initializes bot
        """
        super().init(core)
        self.logger = BBotLoggerAdapter(logging.getLogger('rasaserver_cbe'),
                                        self, self.core)

    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """
        super().get_response(request)

        msg = self.request['input']['text']

        server_type = self.get_server_type()
        if server_type == 'socketio':
            response = self.socketio_server(msg)
        if server_type == 'rest':
            response = self.rest_server(msg)

        for r in response:
            if 'text' in r.keys():
                self.core.bbot.text(r['text'])
            if 'image' in r.keys():
                self.core.bbot.imageCard(r['image'])
            if 'quick_replies' in r.keys():
                quick_reply = []
                for qr in r['quick_replies']:
                    if qr['type'] == 'postback':
                        quick_reply.append(self.core.bbot.imBack(qr['title']))
                    if qr['type'] == 'web_url':
                        quick_reply.append(
                            self.core.bbot.openUrl(qr['title'], qr['payload']))

                self.core.bbot.suggestedActions(quick_reply)
        return self.response

    def get_server_type(self):
        if self.dotbot.chatbot_engine['serverUrl'].startswith('http'):
            return 'rest'
        elif self.dotbot.chatbot_engine['serverUrl'].startswith('ws'):
            return 'socketio'
        else:
            raise BBotException('Wrong Rasa Server url')

    def rest_server(self, msg):
        server_url = self.dotbot.chatbot_engine['serverUrl']
        self.logger.debug('Querying to Rasa Server ' + server_url)
        params = {"sender": self.user_id, "message": msg}
        r = requests.post(server_url + '/webhooks/rest/webhook', json=params)
        self.logger.debug('Rasa Server response code: ' + str(r.status_code) +
                          ' - message: ' + str(r.text)[0:300])
        if r.status_code == 200:
            aw = r.json()
        else:
            raise BBotException(r.text)
        return aw

    def socketio_server(self, msg):
        self.recv = []
        server_url = self.dotbot.chatbot_engine['serverUrl']
        user_message_evt = self.dotbot.chatbot_engine.get(
            'userMessageEvt') or 'user_uttered'
        bot_message_evt = self.dotbot.chatbot_engine.get(
            'botMessageEvt') or 'bot_uttered'

        sio = socketio.Client()

        @sio.on(bot_message_evt)
        def on_message(data):
            self.logger.debug("Received '%s'" % data)
            self.recv.append(data)

        @sio.on('session_confirm')
        def on_message(data):
            self.logger.debug("Session confirmed '%s'" % data)

        self.logger.debug('Querying to Rasa Server ' + server_url)
        sio.connect(server_url)
        sio.call('session_request', {"session_id": [self.user_id]})
        sio.call(user_message_evt,
                 data={
                     "message": msg,
                     "customData": {
                         "language": "en"
                     },
                     "session_id": self.user_id
                 })
        sio.disconnect()
        return self.recv
class ChatScript(ChatbotEngine):
    """BBot engine based on ChatScript."""
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

    def init(self, core: BBotCore):
        """
        Initializebot engine 
        """
        super().init(core)

        self.logger = BBotLoggerAdapter(logging.getLogger('chatscript_cbe'),
                                        self, self.core)

    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """
        super().get_response(request)

        self.request = request

        input_text = request['input']['text']
        chatbot_engine = self.dotbot.chatbot_engine

        cs_bot_id = chatbot_engine['botId']
        self.logger.debug('Request received for bot id "' + cs_bot_id +
                          '" with text: "' + str(input_text) + '"')

        if not input_text:
            input_text = " "  # at least one space, as per the required protocol
        msg_to_send = str.encode(
            u'%s\u0000%s\u0000%s\u0000' %
            (request["user_id"], chatbot_engine['botId'], input_text))
        response = {}  # type: dict
        self.logger.debug("Connecting to chatscript server host: " +
                          chatbot_engine['host'] + " - port: " +
                          str(chatbot_engine['port']) + " - botid: " +
                          chatbot_engine['botId'])
        try:
            # Connect, send, receive and close socket. Connections are not
            # persistent
            connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            connection.settimeout(10)  # in secs
            connection.connect(
                (chatbot_engine['host'], int(chatbot_engine['port'])))
            connection.sendall(msg_to_send)
            msg = ''
            while True:
                chunk = connection.recv(1024)
                if chunk == b'':
                    break
                msg = msg + chunk.decode("utf-8")
            connection.close()
            response = BBotCore.create_response(msg)

        except Exception as e:
            self.logger.critical(str(e) + "\n" + str(traceback.format_exc()))
            raise Exception(e)

        self.logger.debug("Chatscript response: " + str(response))

        # check if chatscript is an error, it should add obb flagging it
        if not len(response):
            msg = "Empty response from ChatScript server"
            self.logger.critical(msg)
            raise Exception(msg)
        if response == "No such bot.\r\n":
            msg = "There is no such bot on this ChatScript server"
            self.logger.critical(msg)
            raise Exception(msg)

        # convert chatscript response to bbot response specification
        self.to_bbot_response(response)

    def to_bbot_response(self, response: str) -> dict:
        """
        Converts Chatscript response to BBOT response speciication
        :param response:  Chatscript response
        :return: BBOT response specification dict
        """
        # split response and oob
        #response, oob = ChatScript.split_response(response)

        response_split = response.split('\\n')
        for rs in response_split:
            #rs = {**bbot_response, **oob} @TODO check oob support
            self.core.bbot.text(rs)

    @staticmethod
    def split_response(response: str) -> tuple:
        """
        Returns a splitted text response and OOB in a tuple
        :param response: Chatscript response
        :return: Tuple with text response and OOB
        """
        oob_json_re = re.search('^\[{.*\}\ ]', response)
        oob = {}
        if oob_json_re:
            oob = json.loads(oob_json_re.group(0).strip('[]'))
            response = response.replace(oob_json_re.group(0), '')
        return response, oob
Exemple #19
0
class RemoteAPIs():
    """Calls remote API endpoints"""
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.
        """
        self.config = config
        self.dotbot = dotbot

        # vars set from plugins
        self.logger_level = ''
        self.request_timeout = 1
        self.dotdb = None

        self.core = None

    def init(self, core: BBotCore):
        """

        :param bot:
        :return:
        """
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('ext.r_services'),
                                        self, self.core.bot, '$call_api')

        # get services for the bot
        services = self.get_bot_services()
        for serv in services:
            self.logger.debug('Register service ' + serv['name'])
            self.logger.debug(str(serv))

            fmap = {
                'owner_name': serv['ownerName'],
                'cost': serv['cost'],
                'subscription_type': serv['subscriptionType'],
                'subscription_id': serv['subscriptionId'],
                'register_enabled': True
            }

            if serv['url'] is not '':
                fmap = {
                    **fmap,
                    **{
                        'object': self,
                        'method': serv['function_name'],
                        'url': serv['url'],
                        'request_method': serv['method'],
                        'timeout': serv['timeout'],
                        'predefined_vars': serv['predefined_vars'],
                        'headers': serv['headers'],
                        'user': serv.get('user'),
                        'passwd': serv.get('passwd'),
                        'mapped_vars': serv['mapped_vars'],
                    }
                }

            core.register_function(serv['function_name'], fmap)

    def get_bot_services(self):
        return self.dotbot.services

    def __getattr__(self, fname):
        def function(*args, **kwargs):
            r_api_data = self.core.functions_map[fname]
            self.logger.debug('Calling remote API ' + fname + ' args: ' +
                              str(args) + ' - metadata: ' + str(r_api_data))
            auth = None
            if r_api_data.get('user'):
                auth = (r_api_data['user'], r_api_data['passwd'])

            # map args to variables
            c = 0
            params = {}
            for mv in r_api_data['mapped_vars']:
                try:
                    params[mv] = self.core.resolve_arg(args[c])
                    c += 1
                except IndexError:
                    raise BBotException({
                        'code':
                        250,
                        'function':
                        fname,
                        'arg':
                        c,
                        'message':
                        'Parameter ' + str(c) + ' is missing'
                    })

            params = {**params, **r_api_data['predefined_vars']}

            # interpolate args to url
            url = r_api_data['url']
            c = 0
            for arg in args[0]:
                url = url.replace('{{' + str(c) + '}}', str(args[c]))
                c += 1

            try:

                if r_api_data['request_method'] == 'get':
                    r = requests.get(
                        url,
                        params=params,
                        headers=r_api_data['headers'],
                        timeout=self.request_timeout
                        or r_api_data['timeout'],  # <<<< custom per service?
                        auth=auth,
                        allow_redirects=True,
                    )
                else:
                    r = requests.post(url,
                                      data=params,
                                      headers=r_api_data['headers'],
                                      timeout=self.request_timeout
                                      or r_api_data['timeout'],
                                      auth=auth,
                                      allow_redirects=True)

                self.logger.debug('Response:' + str(r))
                self.logger.debug('Headers: ' + str(r.headers))
                if r.status_code == requests.codes.ok:
                    if 'application/json' in r.headers.get(
                            'Content-Type'
                    ) or 'application/javascript' in r.headers.get(
                            'Content-Type'):
                        return r.json()
                    else:  # default
                        return r.text

                # On error
                r.raise_for_status()
            except Exception as e:
                self.logger.debug(
                    str(e) + "\n" + str(traceback.format_exc())
                )  # errors in remote apis are not critical for us. This should be alerted to service dev
                if os.environ['BBOT_ENV'] == 'development':
                    raise BBotExtensionException(str(e),
                                                 BBotCore.FNC_RESPONSE_ERROR)
                else:
                    return None

        return function
Exemple #20
0
class Telegram:
    """Translates telegram request/response to flow"""
    def __init__(self, config: dict, dotbot: dict) -> None:
        """        
        """
        self.config = config
        self.dotbot = dotbot
        self.dotdb = None  #
        self.api = None
        self.logger_level = ''

        self.default_text_encoding = 'HTML'  #@TODO move this to dotbot

        self.emOpen = "<b>"
        self.emClose = "</b>"
        if self.default_text_encoding == 'markdown':
            self.emOpen = "*"
            self.emClose = "*"

    def init(self, core):
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('channel_telegram'),
                                        self, self.core, 'ChannelTelegram')

        self.logger.debug("Listening Telegram from path: " +
                          self.get_webhook_path())

    def endpoint(self, request=dict, publisherbot_token=str):
        print(
            '------------------------------------------------------------------------------------------------------------------------'
        )
        self.logger.debug(
            f'Received a Telegram webhook request for publisher token {publisherbot_token}'
        )

        enabled = self.webhook_check(publisherbot_token)
        if enabled:
            try:
                params = request.get_json(force=True)
                org_id = 1

                # checks if bot is telegram enabled
                # if not, it delete the webhook and throw an exception

                # get publisher user id from token
                pub_bot = self.dotdb.find_publisherbot_by_publisher_token(
                    publisherbot_token)
                if not pub_bot:
                    raise Exception('Publisher not found')
                self.logger.debug('Found publisher: ' +
                                  pub_bot.publisher_name + ' - for bot id: ' +
                                  pub_bot.bot_id)
                pub_id = pub_bot.publisher_name

                # if 'runBot' in params:
                #    run_bot = self.params['runBot']

                dotbot = self.dotdb.find_dotbot_by_bot_id(pub_bot.bot_id)
                if not dotbot:
                    raise Exception('Bot not found')
                bot_id = dotbot.bot_id
                # build extended dotbot
                dotbot.services = pub_bot.services
                dotbot.channels = pub_bot.channels
                dotbot.botsubscription = pub_bot

                token = pub_bot.channels['telegram']['token']
                self.set_api_token(token)

                user_id = self.get_user_id(params)
                telegram_recv = self.get_message(params)
                self.logger.debug('POST data from Telegram: ' + str(params))
                bbot_request = self.to_bbot_request(telegram_recv)

                channel_id = 'telegram'

                config = load_configuration(
                    os.path.abspath(
                        os.path.dirname(__file__) + "../../../instance"),
                    "BBOT_ENV")
                bbot = BBotCore.create_bot(config['bbot_core'], dotbot)
                self.logger.debug('User id: ' + user_id)
                req = bbot.create_request(bbot_request, user_id, bot_id,
                                          org_id, pub_id, channel_id)
                bbot_response = bbot.get_response(req)

                self.send_response(bbot_response)
                self.logger.debug("Response from telegram channel: " +
                                  str(bbot_response))

            except Exception as e:
                self.logger.critical(
                    str(e) + "\n" + str(traceback.format_exc()))
                if os.environ['BBOT_ENV'] == 'development':
                    bbot_response = {
                        'output': [{
                            'type': 'message',
                            'text': cgi.escape(str(e))
                        }],
                        'error': {
                            'traceback': str(traceback.format_exc())
                        }
                    }
                else:
                    bbot_response = {
                        'output': [{
                            'type':
                            'message',
                            'text':
                            'An error happened. Please try again later.'
                        }]
                    }
                    # @TODO this should be configured in dotbot
                    # @TODO let bot engine decide what to do?

                self.logger.debug("Response from telegram channel: " +
                                  str(bbot_response))
                self.send_response(bbot_response)

    def webhook_check(self, publisherbot_token):

        pb = self.dotdb.find_publisherbot_by_publisher_token(
            publisherbot_token)

        if pb.channels.get('telegram'):
            return True

        self.logger.warning(
            f'Deleting invalid Telegram webhook for publisher bot token: {publisherbot_token} - publisher id: '
            + pb.publisher_name)
        self.set_api_token(pb.channels['telegram']['token'])
        delete_ret = self.api.deleteWebhook()
        if delete_ret:
            self.logger.warning("Successfully deleted.")
            return False
            #raise Exception('Received a telegram webhook request on a telegram disabled bot. The webhook was deleted now.')
        else:
            error = "Received a telegram webhook request on a telegram disabled bot and couldn't delete the invalid webhook"
            self.logger.error(error)
            raise Exception(error)

    def set_api_token(self, token: str):
        self.api = telepot.Bot(token)

    def to_bbot_request(self, request: str) -> str:
        return {'text': request}

    def get_webhook_url(self) -> str:
        return self.config['webhook_uri']

    def get_webhook_path(self) -> str:
        return urlparse(self.config['webhook_uri']).path

    ### Responses

    def send_response(self, bbot_response: dict):
        """
        Parses BBot output response and sends content to telegram
        """
        # @TODO check if we can avoid sending separate api request for each text if there are more than one

        bbot_output = bbot_response['output']

        # Iterate through bbot responses
        for br in bbot_output:
            buttons = None
            if 'suggestedActions' in br:  # this must be first
                buttons = br['suggestedActions']['actions']

            if 'text' in br:
                self.send_text(br['text'], buttons)
            if 'attachments' in br:
                for a in br['attachments']:
                    if a['contentType'] == 'application/vnd.microsoft.card.audio':
                        self.send_audio_card(a['content'], buttons)
                    if a['contentType'] == 'application/vnd.microsoft.card.video':
                        self.send_video_card(a['content'], buttons)
                    if a['contentType'] in [
                            'application/vnd.microsoft.card.hero',
                            'application/vnd.microsoft.card.thumbnail',
                            'image/png', 'image/jpeg'
                    ]:

                        self.send_image_hero_card(a['content'], buttons)

    def _get_keyboard(self, buttons: list):
        if not buttons or len(buttons) == 0:
            return None
        telegram_buttons = []
        for button in buttons:
            if button['type'] == 'imBack':
                telegram_buttons.append([
                    InlineKeyboardButton(text=button['title'],
                                         callback_data=button['value'])
                ])
            #@TODO add button with link
        keyboard = InlineKeyboardMarkup(inline_keyboard=telegram_buttons)
        return keyboard

    def send_text(self, text: list, buttons: list):
        """
        Sends text to telegram
        """
        if len(text) == 0:
            text = "..."

        keyboard = self._get_keyboard(buttons)
        self.api.sendMessage(self.user_id,
                             text,
                             parse_mode=self.default_text_encoding,
                             reply_markup=keyboard)

    def send_image_hero_card(self, card: dict, buttons: list):
        url = None
        if card.get('media'):
            url = card['media'][0]['url']
        elif card.get('images'):
            url = card['images'][0]['url']
        caption = self._common_media_caption(card)
        keyboard = self._get_keyboard(buttons)
        self.logger.debug('Sending image to Telegram: url: ' + url)
        self.api.sendPhoto(self.user_id,
                           url,
                           caption=caption,
                           parse_mode=self.default_text_encoding,
                           disable_notification=None,
                           reply_to_message_id=None,
                           reply_markup=keyboard)

    def send_audio_card(self, card: dict, buttons: list):
        url = card['media'][0]['url']
        caption = self._common_media_caption(card)
        keyboard = self._get_keyboard(buttons)
        self.logger.debug('Sending audio to Telegram: url: ' + url)
        self.api.sendAudio(self.user_id,
                           url,
                           caption=caption,
                           parse_mode=self.default_text_encoding,
                           duration=None,
                           performer=None,
                           title=None,
                           disable_notification=None,
                           reply_to_message_id=None,
                           reply_markup=keyboard)

    def send_video_card(self, card: dict, buttons: list):
        url = card['media'][0]['url']
        caption = self._common_media_caption(card)
        keyboard = self._get_keyboard(buttons)
        self.logger.debug('Sending video to Telegram: url: ' + url)
        self.api.sendVideo(self.user_id,
                           url,
                           duration=None,
                           width=None,
                           height=None,
                           caption=caption,
                           parse_mode=self.default_text_encoding,
                           supports_streaming=None,
                           disable_notification=None,
                           reply_to_message_id=None,
                           reply_markup=keyboard)

    def _common_media_caption(self, card: dict):

        title = None  # Seems title arg in sendAudio() is not working...? we add it in caption then
        caption = ""
        if card.get('title'):
            caption += f"{self.emOpen}{card['title']}{self.emClose}"
        if card.get('subtitle'):
            if len(caption) > 0:
                caption += "\n"
            caption += card['subtitle']
        if card.get('text'):
            if len(caption) > 0:
                caption += "\n\n"
            caption += card['text']

        if len(caption) == 0:
            caption = None
        return caption

    ### Request

    def get_user_id(self, request: dict):
        if request.get('message'):  #regular text
            self.user_id = str(request['message']['from']['id'])
            return self.user_id

        if request.get('callback_query'):  # callback from a button click
            self.user_id = str(request['callback_query']['from']['id'])
            return self.user_id

    def get_message(self, request: dict):
        if request.get('message'):  #regular text
            return request['message']['text']

        if request.get('callback_query'):  # callback from a button click
            return request['callback_query']['data']

    ### Misc

    def webhooks_check(self):
        """
        This will check and start all webhooks for telegram enabled bots
        """

        sleep_time = 3  # 20 requests per minute is ok?

        # get all telegram enabled bots
        telegram_pubbots = self.dotdb.find_publisherbots_by_channel('telegram')

        if not telegram_pubbots:
            self.logger.debug('No telegram enabled bots')
            return

        # cert file only used on local machines with self-signed certificate
        cert_file = open(self.config['cert_filename'],
                         'rb') if self.config.get('cert_filename') else None

        for tpb in telegram_pubbots:
            if tpb.channels['telegram']['token']:
                self.logger.debug(
                    '---------------------------------------------------------------------------------------------------------------'
                )
                self.logger.debug(
                    'Checking Telegram webhook for publisher name ' +
                    tpb.publisher_name + ' publisher token: ' + tpb.token +
                    ' - bot id: ' + tpb.bot_id + '...')
                self.logger.debug('Setting token: ' +
                                  tpb.channels['telegram']['token'])

                try:
                    self.set_api_token(tpb.channels['telegram']['token'])

                    # build webhook url
                    url = self.get_webhook_url().replace(
                        '<publisherbot_token>', tpb.token)

                    # check webhook current status (faster than overriding webhook)
                    webhook_info = self.api.getWebhookInfo()
                    self.logger.debug('WebHookInfo: ' + str(webhook_info))
                    webhook_notset = webhook_info['url'] == ''
                    if webhook_info[
                            'url'] != url and not webhook_notset:  # webhook url is set and wrong
                        self.logger.warning(
                            'Telegram webhook set is invalid (' +
                            webhook_info['url'] + '). Deleting webhook...')
                        delete_ret = self.api.deleteWebhook()
                        if delete_ret:
                            self.logger.warning("Successfully deleted.")
                        else:
                            error = "Couldn't delete the invalid webhook"
                            self.logger.error(error)
                            raise Exception(error)
                        webhook_notset = True
                    if webhook_notset:  # webhook is not set
                        self.logger.info(f'Setting webhook for bot id ' +
                                         tpb.bot_id +
                                         f' with webhook url {url}')
                        set_ret = self.api.setWebhook(url=url,
                                                      certificate=cert_file)
                        self.logger.debug("setWebHook response: " +
                                          str(set_ret))
                        if set_ret:
                            self.logger.info("Successfully set.")
                        else:
                            error = "Couldn't set the webhook"
                            self.logger.error(error)
                            raise Exception(error)
                    else:
                        self.logger.debug("Webhook is correct")
                except telepot.exception.TelegramError:
                    self.logger.debug(
                        'Invalid Telegram token'
                    )  # This might happen when the token is invalid. We need to ignore and ontinue

                time.sleep(sleep_time)
Exemple #21
0
class WatsonAssistant(ChatbotEngine):
    """
    BBot engine that calls external program.
    """
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

        self.dotdb = None

    def init(self, core: BBotCore):
        """
        Initializebot engine 
        """
        super().init(core)

        self.logger = BBotLoggerAdapter(logging.getLogger('watson_cbe'), self,
                                        self.core)

        # Set up Assistant service.
        authenticator = IAMAuthenticator(
            self.dotbot.chatbot_engine['iamApikey'])
        self.service = ibm_watson.AssistantV2(authenticator=authenticator,
                                              version='2019-02-28')
        self.service.set_service_url(self.dotbot.chatbot_engine['url'])

        self.logger.debug("Connection: " + str(self.service))

    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """

        self.request = request

        input_text = request['input']['text']
        user_id = request['user_id']

        session = self.get_bot_session(user_id)
        session_id = session['session_id']

        response = {}
        try:
            response = self.get_assistant_response(session_id,
                                                   request['input'])
        except ibm_cloud_sdk_core.api_exception.ApiException as e:  # This means we are trying to use an expired session
            # @TODO I don't know how to test this before. try to improve it.
            # create a new session (but dont delete context!) and try again
            self.logger.debug('Session has timed out. Try to create a new one')
            session = self.get_bot_session(user_id, True)
            session_id = session['session_id']
            response = self.get_assistant_response(session_id,
                                                   request['input'])

        self.logger.debug("Response: " + str(response))

        output = ""
        if response['output']['generic']:
            if response['output']['generic'][0]['response_type'] == 'text':
                output = response['output']['generic'][0]['text']

        self.core.bbot.text(output)

    def get_assistant_response(self, session_id: str, rinput: dict):
        """
        Returns response from bot engine

        :param session_id: The session id
        :param rinput: The request dict
        :return: A dict 
        """
        return self.service.message(self.dotbot.chatbot_engine['assistantId'],
                                    session_id,
                                    input={
                                        'text': rinput['text']
                                    }).get_result()

    def get_bot_session(self, user_id: str, renew: bool = False):
        """
        Returns session data both session id and context
        If there is no session on db we create one

        :param user_id: A string with the user id
        :param renew: A bool to indicate if we need to renew the session id (watson asisstant has a timeout and we have to get a new one when that happens)
        :return: A dict 
        """
        session = self.dotdb.get_watson_assistant_session(user_id) or {
            'session_id': None,
            'context': {}
        }

        if session:
            self.logger.debug("Found old session: " + str(session))

        if renew:
            session['session_id'] = None

        if not session.get('session_id'):
            session_id = self.service.create_session(
                assistant_id=self.dotbot.chatbot_engine['assistantId']
            ).get_result()['session_id']
            session['session_id'] = session_id
            self.logger.debug("Created new session: " + session_id)
            self.dotdb.set_watson_assistant_session(
                user_id, session_id, session['context']
            )  # context might have data if we are renewing session

        return session
Exemple #22
0
class PandoraBots(ChatbotEngine):
    """
    BBot engine that calls external program.
    """
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

        self.botkey = dotbot.chatbot_engine['botkey']
        self.dotdb = None

    def init(self, core: BBotCore):
        """
        Initializebot engine 
        """
        super().init(core)

        self.url = 'https://api.pandorabots.com'
        self.logger = BBotLoggerAdapter(logging.getLogger('pandora_cbe'), self,
                                        self.core)

    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """

        super().get_response(request)

        session = self.get_session()
        response = {}

        if not session:
            self.logger.debug(
                'There is no session for this user. Ask Pandorabots for a new one.'
            )
            response = self.atalk(self.request['input']['text'])
            self.logger.debug('Storing sessionid and client_name')
            self.set_session(response['sessionid'], response['client_name'])
        else:
            sessionid = session['sessionid']
            client_name = session['client_name']
            self.logger.debug('Requesting bot response with sessionid "' +
                              str(sessionid) + '" and client_name "' +
                              str(client_name) + '"')
            response = self.talk(self.request['input']['text'], sessionid,
                                 client_name)

        self.logger.debug("Response content: " + str(response))

        if response['status'] == 'ok':
            for msg in response['responses']:
                self.core.bbot.text(msg)
        else:
            raise BBotException(str(response))

    def atalk(self, input_txt: str):
        params = {
            'botkey': self.botkey,
            'input': input_txt,
        }
        response = requests.post(self.url + '/atalk', params)
        self.logger.debug("Response status code: " + str(response.status_code))
        if response.status_code == 200:
            return response.json()
        elif response.status_code == 401:
            raise BBotException(response.text)
        else:
            raise Exception(response.text)

    def talk(self, input_txt: str, sessionid: str, client_name: str):
        params = {
            'botkey': self.botkey,
            'input': input_txt,
            'extra': True,
            'sessionid': sessionid,
            'client_name': client_name
        }
        response = requests.post(self.url + '/talk', params)
        self.logger.debug("Response status code: " + str(response.status_code))
        if response.status_code == 200:
            return response.json()
        elif response.status_code == 401:
            raise BBotException(response.text)
        else:
            raise Exception(response.text)

    def get_session(self):
        return self.dotdb.get_pandorabots_session(self.bot_id, self.user_id)

    def set_session(self, sessionid, client_name):
        self.dotdb.set_pandorabots_session(self.bot_id, self.user_id,
                                           sessionid, client_name)
Exemple #23
0
class DotRepository():
    """MongoDB client."""

    mongo_clients = {}  # mongo clients cache

    def __init__(self, config: dict, dotbot: dict = None) -> None:
        """Initialize the connection."""

        self.config = config

        self.logger_level = ''

        self.connection_timeout = 5000

        if 'uri' not in config:
            raise RuntimeError("FATAL ERR: Missing config var uri")

    def init(self, core: BBotCore):
        """
        :param bot:
        :return:
        """
        self.core = core
        self.logger = BBotLoggerAdapter(logging.getLogger('dotrepository'),
                                        self, self.core, 'dotrepository')

        uri = self.config['uri']
        uri_parts = pymongo.uri_parser.parse_uri(uri)
        database_name = uri_parts['database']

        self.logger.debug("Initializing mongodb with URI " +
                          uri.replace(str(uri_parts['password']), '********'))
        if uri in DotRepository.mongo_clients.keys():  # look in cache
            self.logger.debug("Found mongo client already active in memory")
            self.mongo = DotRepository.mongo_clients[uri]
            return

        client = MongoClient(uri,
                             serverSelectionTimeoutMS=self.connection_timeout)

        self.mongo = client[database_name]

        DotRepository.mongo_clients[uri] = client[database_name]

    def restart_from_scratch(self):
        """Drop and recreate each collection in database."""
        collections = ['organizations', 'users', 'dotbot', 'dotflow']
        collections_in_db = self.mongo.list_collection_names()
        for collection_name in collections:
            if collection_name in collections_in_db:
                self.mongo.drop_collection(collection_name)
            self.mongo.create_collection(collection_name)
        # Unique organization name
        self.mongo.organizations.create_index('name', unique=True)
        # Unique username and token
        self.mongo.users.create_index('username', unique=True)
        self.mongo.users.create_index('token', unique=True)
        # Unique dotbot name
        #self.mongo.dotbot.create_index('dotbot.name', unique=True)
        # Unique dotflow name by dotbot
        #self.mongo.dotflow.create_index([('name', ASCENDING),
        #                                 ('dotbot_id', ASCENDING)],
        #                               unique=True)

    @staticmethod
    def get_projection_from_fields(fields: list = []):
        """Returns a mongodb projection based on a list of fieldnames in dot notation (note _id will be included always)"""
        return dict([field, 1] for field in fields
                    if len(field) > 0) if len(fields[0]) else None

### ORGANIZATIONS (deprecated?)

    def create_organization(self, name: str) -> Organization:
        """
        Create a new organization.

        :param name: Unique organization name.
        :return: The organization created.
        """
        params = {'name': name}
        organization_id = self.mongo.organizations.insert_one(
            params).inserted_id
        result = self.mongo.organizations.find_one(
            {"_id": ObjectId(str(organization_id))})
        organization = Organization()
        organization.id = str(organization_id)
        organization.name = result['name']
        return organization

    def find_one_organization(self, filters: dict) -> Organization:
        """
        Retrieve an organization by filters.

        :param filters: Dictionary with matching conditions.
        :return: Organization instance or None if not found.
        """
        result = self.mongo.organizations.find_one(filters)
        if not result:
            return None
        organization = Organization()
        organization.id = str(result['_id'])
        organization.name = result['name']
        return organization

### USERS/AUTH (deprecated?)

    def find_one_user(self, filters: dict) -> User:
        """
        Retrieve a user by filters.

        :param filters: Dictionary with matching conditions.
        :return: User instance or None if not found.
        """
        result = self.mongo.users.find_one(filters)
        if not result:
            return None
        user = User()
        user.id = str(result['_id'])
        user.username = result['username']
        user.hashed_password = result['hashed_password']
        organization_id = ObjectId(str(result['organization_id']))
        user.organization = self.find_one_organization(
            {'_id': organization_id})
        return user

    def create_user(self, username: str, plaintext_password: str,
                    organization: Organization, is_admin: int) -> User:
        """
        Create a new user.

        :param username: Unique username.
        :param plaintext_password: Password in plain text that won't be stored.
        :param organization: A valid organization.
        :param is_admin: If the user has administrative privileges: 1.
                         Default: 0.
        :return: User created.
        """
        param = {
            'username':
            username,
            'hashed_password':
            bcrypt.hashpw(
                plaintext_password,  # pylint: disable=no-member
                bcrypt.gensalt()),
            'organization_id':
            organization.id,
            'is_admin':
            is_admin,
            'token':
            '',
            'created_at':
            datetime.datetime.utcnow(),
            'updated_at':
            datetime.datetime.utcnow()
        }
        user_id = self.mongo.users.insert_one(param).inserted_id
        return self.find_one_user({"_id": ObjectId(str(user_id))})

    def login(self, username: str, plaintext_password: str) -> Token:
        """
        Try to authenticate a user.

        :param username: Unique username.
        :param plaintext_password: Password.
        :raise AuthenticationError: on failure.
        :return: Authentication token.
        """
        token = Token()
        user = self.find_one_user({"username": username})
        if user:
            if bcrypt.checkpw(
                    plaintext_password,  # pylint: disable=no-member
                    user.hashed_password):
                token.status = 'success'
                token.token = os.urandom(24).hex()
                self.mongo.users.update_one({"username": username}, {
                    "$set": {
                        "token": token.token,
                        'updated_at': datetime.datetime.utcnow()
                    }
                })
        if token.status != 'success':
            raise AuthenticationError()
        return token

    def logout(self, username: str) -> None:
        """
        Remove the authentication token from a user.

        :param username: A valid username
        :return: None
        """
        self.mongo.users.update_one({"username": username},
                                    {"$set": {"token": "", "updated_at": \
                                        datetime.datetime.utcnow()}})

    def find_user_by_token(self, token: str) -> User:
        """
        Retrieve a user by its authentication token.

        :param token: A valid authentication token.
        :return: User related to token.
        """
        return self.find_one_user({"token": token})

### DOTBOTCONTAINER

    def marshall_dotbot_container(self, result) -> DotBotContainer:
        """
        Marshall a dotbotcontainer.

        :param result: A mongodb document representing a dotbot.
        :return: DotBotContainer instance
        """
        dotbot_container = DotBotContainer()
        dotbot_container.dotbot = result['dotbot']
        dotbot_container.organization = self.find_one_organization(
            {'_id': ObjectId(str(result['organizationId']))})
        dotbot_container.deleted = result['deleted']
        dotbot_container.createdAt = result['createdAt']
        dotbot_container.updatedAt = result['updatedAt']
        return dotbot_container

    def find_dotbot_containers(self, filters: dict) -> list:
        """
        Retrieve a list of dotbots.

        :param filters: Dictionary with matching conditions.
        :return: List of dotbots
        """
        results = self.mongo.dotbot.find(filters)
        dotbots = []
        for result in results:
            dotbots.append(self.marshall_dotbot_container(result))
        return dotbots

    def find_one_dotbot_container(self, filters: dict) -> DotBotContainer:
        """
        Retrieve a dotbotContainer by filters.

        :param filters: Dictionary with matching conditions.
        :return: DotBotContainer instance or None if not found.
        """

        result = self.mongo.dotbot.find_one(filters)

        if not result:
            return None
        return self.marshall_dotbot_container(result)

    def find_dotbot_container_by_container_id(
            self, container_id: str) -> DotBotContainer:
        """
        Retrieve a dotbot by its container ID.

        :param container_id: DotBot container ID
        :return: DotBotContainer instance or None if not found.
        """
        return self.find_one_dotbot_container(
            {"_id": ObjectId(str(container_id))})

    def find_dotbot_container_by_idname(self,
                                        dotbot_idname: str) -> DotBotContainer:
        """
        Retrieve a dotbot by its id or name.

        :param dotbot_idname: DotBot id or name
        :return: DotBot instance or None if not found.
        """
        return self.find_one_dotbot_container({
            '$or': [{
                'dotbot.id': dotbot_idname
            }, {
                'dotbot.name': dotbot_idname
            }]
        })

    def find_dotbots_by_channel(self, channel: str) -> list:
        """
        Retrieve a dotbot list by its enabled channels

        :param channel: DotBot enabled channel
        :return: DotBot intance list
        """

        return self.find_dotbot_containers(
            {'dotbot.channels.' + channel + '.enabled': True})

    def create_dotbot(self, dotbot: dict,
                      organization: Organization) -> DotBotContainer:
        """
        Create a new DotBot.

        :param dotbot: DotBot data
        :param organization: A valid organization.
        :return: DotBot created.
        """
        oid = ObjectId()
        if not dotbot.get('id'):  # Insert oid on dotbot id if not set
            dotbot['id'] = str(oid)

        param = {
            '_id': oid,
            'dotbot': dotbot,
            'organizationId': organization.id,
            'deleted': '0',
            'createdAt': datetime.datetime.utcnow(),
            'updatedAt': datetime.datetime.utcnow()
        }
        self.mongo.dotbot.insert_one(param)
        return self.find_dotbot_container_by_container_id(oid)

    def update_dotbot_by_container_id(self, container_id: str,
                                      dotbot: dict) -> DotBotContainer:
        """
        Update a DotBot by its container id.

        :param container_id: DotBot container id
        :param dotbot: DotBotContainer object
        :return: Updated DotBot
        """
        self.mongo.dotbot.update_one({"_id": ObjectId(str(container_id))}, {
            "$set": {
                "dotbot": dotbot,
                "updatedAt": datetime.datetime.utcnow()
            }
        })
        return self.find_dotbot_container_by_container_id(container_id)

    def update_dotbot_by_idname(self, dotbot_idname: str,
                                dotbot: dict) -> DotBotContainer:
        """
        Update a DotBot by its id.

        :param dotbot_id: DotBot id
        :param dotbot: DotBot object
        :return: Updated DotBotContainer object
        """
        self.mongo.dotbot.update_one(
            {
                '$or': [{
                    'dotbot.id': dotbot_idname
                }, {
                    'dotbot.name': dotbot_idname
                }]
            }, {
                "$set": {
                    "dotbot": dotbot,
                    "updatedAt": datetime.datetime.utcnow()
                }
            })
        return self.find_dotbot_container_by_idname(dotbot_idname)

    def delete_dotbot_by_container_id(self, container_id: str) -> None:
        """
        Soft-delete a DotBot.

        :param container_id: DotBot container ID
        """
        self.mongo.dotbot.update_one(
            {"_id": ObjectId(str(container_id))},
            {"$set": {
                "deleted": 1,
                "updatedAt": datetime.datetime.utcnow()
            }})

    def delete_dotbot_by_idname(self, dotbot_idname: str) -> None:
        """
        Soft-delete a DotBot.

        :param dotbot_idname: DotBot ID
        """
        self.mongo.dotbot.update_one(
            {
                '$or': [{
                    'dotbot.id': dotbot_idname
                }, {
                    'dotbot.name': dotbot_idname
                }]
            },
            {"$set": {
                "deleted": 1,
                "updatedAt": datetime.datetime.utcnow()
            }})

    ### DOTBOT

    def marshall_dotbot(self, result) -> DotBot:
        """
        Marshall a dotbot.

        :param result: A mongodb document representing a dotbot.
        :return: DotBot instance
        """
        dotbot = DotBot()
        dotbot.owner_name = result['ownerName']
        dotbot.name = result['name']
        dotbot.bot_id = result['botId']
        dotbot.title = result['title']
        dotbot.chatbot_engine = result['chatbotEngine']
        dotbot.per_use_cost = result['perUseCost']
        dotbot.per_month_cost = result['perMonthCost']
        dotbot.updated_at = result['updatedAt']
        try:
            dotbot.tts = result['tts']
        except KeyError:
            dotbot.tts = {}
        return dotbot

    def find_one_dotbot(self, filters: dict) -> DotBot:
        """
        Retrieve a dotbot by filters.

        :param filters: Dictionary with matching conditions.
        :return: DotBot instance or None if not found.
        """
        result = self.mongo.greenhouse_dotbots.find_one(filters)
        if not result:
            return None
        return self.marshall_dotbot(result)

    def find_dotbots(self, filters: dict) -> list:
        """
        Retrieve a list of dotbots.

        :param filters: Dictionary with matching conditions.
        :return: List of dotbots
        """
        results = self.mongo.greenhouse_dotbots.find(filters)
        dotbots = []
        for result in results:
            dotbots.append(self.marshall_dotbot(result))
        return dotbots

    def find_dotbot_by_bot_id(self, bot_id: str) -> DotBot:
        return self.find_one_dotbot({'botId': bot_id})

    ### publisher_bot

    def find_publisherbot_by_publisher_token(self, pub_token: str):
        return self.find_one_publisherbot({'token': pub_token})

    def find_publisherbots_by_channel(self, channel: str) -> list:
        field = 'channels.' + channel
        return self.find_publisherbots({field: {'$exists': True}})

    def find_one_publisherbot(self, filters: dict) -> PublisherBot:
        """
        Retrieve a publisherbot by filters.

        :param filters: Dictionary with matching conditions.
        :return: PublisherBot instance or None if not found.
        """
        result = self.mongo.greenhouse_publisher_bots.find_one(filters)
        if not result:
            return None
        return self.marshall_publisherbot(result)

    def find_publisherbots(self, filters: dict) -> list:
        results = self.mongo.greenhouse_publisher_bots.find(filters)
        publisherbots = []
        for result in results:
            publisherbots.append(self.marshall_publisherbot(result))
        return publisherbots

    def marshall_publisherbot(self, result) -> PublisherBot:
        pub_bot = PublisherBot()
        pub_bot.id = result['subscriptionId']
        pub_bot.token = result['token']
        pub_bot.publisher_name = result['publisherName']
        pub_bot.bot_id = result['botId']
        pub_bot.bot_name = result['botName']
        pub_bot.subscription_type = result['subscriptionType']
        pub_bot.updated_at = result['updatedAt']
        pub_bot.channels = result['channels']
        pub_bot.services = result['services']
        try:
            pub_bot.predefined_vars = result['predefined_vars']
        except KeyError:
            pub_bot.predefined_vars = {}
        return pub_bot

    ### DOTFLOWS

    def marshall_dotflow(self, result) -> DotFlowContainer:
        """
        Marshall a DotFlowContainer.

        :param result: A mongodb document representing a DotFlow.
        :return: DotFlow instance
        """
        dotflow_container = DotFlowContainer()
        if result.get('dotflow'): dotflow_container.dotflow = result['dotflow']
        if result.get('dotbotId'):
            dotflow_container.dotbot = self.find_dotbot_container_by_idname(
                result['dotbotId'])
        if result.get('createdAt'):
            dotflow_container.createdAt = result['createdAt']
        if result.get('updatedAt'):
            dotflow_container.updatedAt = result['updatedAt']
        return dotflow_container

    def find_dotflows(self, filters: dict, projection: dict = None) -> list:
        """
        Retrieve a list of DotFlows.

        :param filters: Dictionary with matching conditions.
        :param projection: Dictionary with projection setting.
        :return: List of dotflows.
        """

        results = self.mongo.dotflow.find(filters, projection)
        dotflows = []
        for result in results:
            dotflows.append(self.marshall_dotflow(result))
        return dotflows

    def find_one_dotflow(self, filters: dict) -> DotFlowContainer:
        """
        Retrieve a DotFlow by filters.

        :param filters: Dictionary with matching conditions.
        :return: DotFlow instance or None if not found.
        """
        result = self.mongo.dotflow.find_one(filters)
        if not result:
            return None
        return self.marshall_dotflow(result)

    def find_dotflow_by_container_id(self, container_id) -> DotFlowContainer:
        """
        Retrieve a dotflow by its ID.

        :param container_id: DotFlow ID
        :return: DotFlow instance or None if not found.
        """
        return self.find_one_dotflow({"_id": ObjectId(str(container_id))})

    def find_dotflow_by_idname(self, dotflow_idname) -> DotFlowContainer:
        """
        Retrieve a DotFlowContainer object by its ID or name.

        :param dotflow_idname: DotFlow ID or name
        :return: DotFlowContainer instance or None if not found.
        """
        return self.find_one_dotflow({
            '$or': [{
                'dotflow.id': dotflow_idname
            }, {
                'dotflow.name': dotflow_idname
            }]
        })

    def find_dotflow_by_node_id(self, dotbot_id: str,
                                node_id: str) -> DotFlowContainer:
        """
        Retrieve a DotFlowContainer object containing the specified node id

        :param dotbot_id: DotBot ID
        :param node_id: Node ID
        :return: DotFlowContainer instance or None if not found.
        """
        return self.find_one_dotflow(
            {'$and': [{
                'dotbotId': dotbot_id
            }, {
                'dotflow.nodes.id': node_id
            }]})

    def find_node_by_id(self, dotbot_id: str, node_id: str) -> dict:
        """
        Retrieve a node by its id.

        :param dotbot_id: DotBot ID.
        :param node_id: Node ID.
        :return:
        """
        dfc = self.find_dotflow_by_node_id(dotbot_id, node_id)
        if not dfc:
            return None

        for n in dfc.dotflow['nodes']:
            if n['id'] == node_id:
                return n

    def find_dotflows_by_dotbot_idname(self,
                                       dotbot_idname: str,
                                       fields: list = []) -> list:
        """
        Retrieve a list of DotFlowContainer objects by DotBot id

        :param dotbot_idname: DotBot id
        :param fields: DotFlowContainer fields to project
        :return: List of DotFlowContainer objects
        """
        # we don't know if it's id or name. retrieve dotbot anyway to get id by id or name
        dotbot_container = self.find_dotbot_container_by_idname(dotbot_idname)

        query = {'dotbotId': dotbot_container.dotbot['id']}
        projection = DotRepository.get_projection_from_fields(fields)
        return self.find_dotflows(query, projection)

    def find_dotflows_by_context(self, dotbot_id: str, context: str) -> list:
        """
        Retrieve a list of DotFlow2 tagged with the specified context

        :param dotbot_id: DotBot ID
        :param context: Context
        :return: List of DotFlow2 objects
        """
        # Get flows with nodes with the wanted context
        query = {
            "$and": [{
                "dotbotId": dotbot_id
            }, {
                "dotflow.nodes": {
                    "$elemMatch": {
                        "context": context
                    }
                }
            }]
        }
        projection = {"dotflow.nodes": 1}
        dotflows = self.find_dotflows(query, projection)

        # Get nodes with the context
        context_nodes = []
        for df in dotflows:
            for n in df.dotflow['nodes']:
                if context in n.get('context', []):
                    context_nodes.append(n)

        return context_nodes

    def create_dotflow(self, dotflow: dict, dotbot: dict) -> DotFlowContainer:
        """
        Create a new dotflow.

        :param name: Unique dotflow name in dotbot.
        :param flow: DotFlow code as a JSON string.
        :param dotbot: A valid dotbot.
        :return: DotFlow created.
        """
        oid = ObjectId()
        if not dotflow.get('id'):  # Insert oid on dotflow id if not set
            dotflow['id'] = str(oid)

        param = {
            '_id': oid,
            'dotflow': dotflow,
            'dotbotId': dotbot['id'],
            'createdAt': datetime.datetime.utcnow(),
            'updatedAt': datetime.datetime.utcnow()
        }
        self.mongo.dotflow.insert_one(param)
        return self.find_dotflow_by_container_id(
            oid)  #TODO maybe this should be done from the api?

    def update_dotflow_by_container_id(self, container_id: str,
                                       dotflow: dict) -> DotFlowContainer:
        """
        Update a dotflow.

        :param container_id: DotFlowContainer ID
        :param name: Unique dotflow name in dotbot.
        :return: DotFlow updated.
        """

        # check updatedAt for mid-air collisions

        self.mongo.dotflow.update_one({"_id": ObjectId(str(container_id))}, {
            "$set": {
                "dotflow": dotflow,
                "updatedAt": datetime.datetime.utcnow()
            }
        })
        return self.find_dotflow_by_container_id(container_id)

    def update_dotflow_by_idname(self, dotflow_idname: str,
                                 dotflow: dict) -> DotFlowContainer:
        """
        Update a dotflow.

        :param dotflow_idname: DotFlow ID
        :param name: Unique dotflow name in dotbot.
        :return: DotFlow updated.
        """

        # check updatedAt for mid-air collisions

        self.mongo.dotflow.update_one(
            {
                '$or': [{
                    'dotflow.id': dotflow_idname
                }, {
                    'dotflow.name': dotflow_idname
                }]
            }, {
                "$set": {
                    "dotflow": dotflow,
                    "updatedAt": datetime.datetime.utcnow()
                }
            })
        return self.find_dotflow_by_idname(dotflow_idname)

    def delete_dotflow_by_container_id(self, container_id: str) -> None:
        """
        Delete a dotflow.

        :param container_id: DotFlowContainer ID
        """
        self.mongo.dotflow.delete_one({"_id": ObjectId(str(container_id))})

    def delete_dotflow_by_idname(self, dotflow_idname: str) -> None:
        """
        Delete a dotflow.

        :param dotflow_idname: DotFlow ID
        """
        self.mongo.dotflow.delete_one({
            '$or': [{
                'dotflow.id': dotflow_idname
            }, {
                'dotflow.name': dotflow_idname
            }]
        })

    def find_dotflow_formids(self, dotbot_id: str) -> list:
        """
        Retrieve a list of form-ids used in the dotbot's dotflows

        :param dotbot_id
        :return: list if forms ids
        """
        dotflows = self.mongo.dotflow.find({
            "$and": [{
                "flow.nodes.formId": {
                    "$exists": True
                }
            }, {
                "_id": dotbot_id
            }]
        })
        nodes = []
        for f in dotflows:
            nodes += f['dotflow']['nodes']
        formids = set()
        for n in nodes:
            if n.get('formId'):
                formids.add(n['formId'])
        return list(formids)

    def find_dotflow_fieldids(self, dotbot_id: str) -> list:
        """
        Retrieve a list of field-ids used in the dotbo's dotflows

        :param dotbot_id
        :return: list of field-ids
        """
        dotflows = self.mongo.dotflow.find({
            "$and": [{
                "dotflow.nodes.fieldId": {
                    "$exists": True
                }
            }, {
                "_id": dotbot_id
            }]
        })
        nodes = []
        for f in dotflows:
            nodes += f['dotflow']['nodes']
        fieldids = set()
        for n in nodes:
            if n.get('fieldId'):
                fieldids.add(n['fieldId'])
        return list(fieldids)

## REMOTE APIS

    def marshall_remote_api(self, result) -> RemoteAPI:
        """
        Marshall a RemoteAPI.

        :param result: A mongodb document representing a dotbot.
        :return: DotBot instance
        """
        rapi = RemoteAPI()
        rapi.name = result['name']
        rapi.category = result['category']
        rapi.function_name = result['function_name']
        rapi.url = result['url']
        rapi.method = result['method']
        rapi.headers = result.get('headers', {})
        rapi.predefined_vars = result.get('predefined_vars', {})
        rapi.mapped_vars = result.get('mapped_vars', [])
        rapi.cost = result['cost']
        return rapi

    def find_remote_api_by_id(self, remote_api_id) -> dict:
        """
        Retrieve a remote api doc
        
        """
        if isinstance(remote_api_id, str):
            filter = {"_id": ObjectId(remote_api_id)}
        else:
            obj_ids = list(map(lambda x: ObjectId(x), remote_api_id))
            filter = {"_id": {"$in": obj_ids}}

        rapis = []
        results = self.mongo.remote_apis.find(filter)
        for result in results:
            rapis.append(self.marshall_remote_api(result))
        return rapis

## WATSON ASSISTANT SESSION AND CONTEXT

    def get_watson_assistant_session(self, user_id: str, bot_id: str):
        """
        Returns user session id and context 

        :param: user_id: A string with user id
        :param bot_id: A string with bot id
        :return: A dict
        """
        r = self.mongo.watson_assistant_bot_data.find_one({
            'user_id': user_id,
            'bot_id': bot_id
        })
        return r

    def set_watson_assistant_session(self,
                                     user_id: str,
                                     bot_id: str,
                                     session_id: str,
                                     context: dict = {}):
        """
        Stores user session id and context

        :param user_id: A string with user id
        :param bot_id: A string with bot id
        :param session_id: A string with session id
        :param context: A dict with context
        """
        self.mongo.watson_assistant_bot_data.update(
            {
                'user_id': user_id,
                'bot_id': bot_id
            }, {
                'user_id': user_id,
                'bot_id': bot_id,
                'session_id': session_id,
                'context': context
            },
            upsert=True)

## MICROSOFT DIRECTLINE SESSION

    def get_directline_session(self, user_id: str, bot_id: str):
        """
        Returns conversation id and watermark

        :param: user_id: A string with user id
        :param bot_id: A string with bot id
        :return: A dict
        """
        r = self.mongo.directline_bot_data.find_one({
            'user_id': user_id,
            'bot_id': bot_id
        })
        if r:
            return [r['conversation_id'], r['watermark']]
        return None

    def set_directline_session(self, user_id: str, bot_id: str,
                               conversation_id: str, watermark: int):
        """
        Stores conversation id and watermark

        :param user_id: A string with user id
        :param bot_id: A string with bot id
        :param conversation_id: A string with conversation id
        :param watermar: An integer with watermark
        """
        self.mongo.directline_bot_data.update(
            {
                'user_id': user_id,
                'bot_id': bot_id
            }, {
                'user_id': user_id,
                'bot_id': bot_id,
                'conversation_id': conversation_id,
                'watermark': watermark
            },
            upsert=True)

## AZURE ACTIVE DIRECTORY ACCESS TOKEN

    def get_azure_ad_access_token(self, bot_id: str):
        r = self.mongo.azure_ad_access_token.find_one({'bot_id': bot_id})
        return r

    def set_azure_ad_access_token(self, bot_id: str, access_token: str,
                                  expire_date: int):
        self.mongo.azure_ad_access_token.update({'bot_id': bot_id}, {
            'bot_id': bot_id,
            'access_token': access_token,
            'expire_date': expire_date
        },
                                                upsert=True)

## GREENHOUSE SUBSCRIPTION_PAYMENTS

    def get_last_payment_date_by_subscription_id(self, subscription_id):
        res = self.mongo.subscription_payments.find_one(
            {'subscriptionId': ObjectId(subscription_id)})
        if res:
            return res['lastPaymentDate']
        return None

## PANDORABOTS SESSIONID AND CLIENT_NAME

    def get_pandorabots_session(self, bot_id: str, user_id: str):
        """
        Returns user sessionid and client_name

        :param: user_id: A string with user id
        :return: A dict
        """
        r = self.mongo.pandorabots_bot_data.find_one({
            'bot_id': bot_id,
            'user_id': user_id
        })
        return r

    def set_pandorabots_session(self, bot_id: str, user_id: str,
                                sessionid: str, client_name: str):
        """
        Stores user session id and context

        :param user_id: A string with user id
        :param session_id: A string with session id
        :param context: A dict with context
        """
        self.mongo.pandorabots_bot_data.update(
            {
                'bot_id': bot_id,
                'user_id': user_id
            }, {
                'bot_id': bot_id,
                'user_id': user_id,
                'sessionid': sessionid,
                'client_name': client_name
            },
            upsert=True)
Exemple #24
0
class DirectLine(ChatbotEngine):
    def __init__(self, config: dict, dotbot: dict) -> None:
        """
        Initialize the plugin.

        :param config: Configuration values for the instance.
        """
        super().__init__(config, dotbot)

        self.dotdb = None

        self.conversation_id = None
        self.watermark = None
        self.direct_line_secret = self.dotbot.chatbot_engine['secret']
        self.base_url = self.dotbot.chatbot_engine.get(
            'url') or 'https://directline.botframework.com/v3/directline'

    def init(self, core: BBotCore):
        """
        Initializebot engine 
        """
        super().init(core)

        self.logger = BBotLoggerAdapter(logging.getLogger('directline_cbe'),
                                        self, self.core)

    def get_response(self, request: dict) -> dict:
        """
        Return a response based on the input data.

        :param request: A dictionary with input data.
        :return: A response to the input data.
        """
        super().get_response(request)

        self.init_session()
        self.directline_send_message(request['input']['text'])
        response = self.directline_get_message()
        self.to_bbot_response(response)

    def to_bbot_response(self, response: list) -> dict:
        bbot_response = []
        resp = copy.deepcopy(response)

        for r in resp['activities']:
            # filter not needed data
            elms = [
                'id', 'conversation', 'conversationId', 'timestamp',
                'channelId', 'inputHint', 'from', 'recipient', 'replyToId',
                'serviceUrl'
            ]
            for i in elms:
                if i in r:
                    del r[i]

            self.core.add_output(r)

    def init_session(self):
        if not self.conversation_id or not self.watermark:
            # look on database first
            self.logger.debug(
                'Looking for conversation id and watermark in database')
            session = self.dotdb.get_directline_session(
                self.user_id, self.bot_id)
            if not session:
                self.logger.debug(
                    'Not in database, ask for a new one and store it')
                self.conversation_id = self.directline_get_new_conversation_id(
                )
                self.watermark = ""  # it will not filter by watermark so we can get initial welcome message from the bot
                self.dotdb.set_directline_session(self.user_id, self.bot_id,
                                                  self.conversation_id,
                                                  self.watermark)
            else:
                self.conversation_id, self.watermark = session
                self.logger.debug('Got conversation ID: ' +
                                  self.conversation_id + ' - watermark: ' +
                                  str(self.watermark))

    def directline_get_headers(self):
        return {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + self.direct_line_secret,
        }

    def directline_get_new_conversation_id(self):
        url = self.base_url + '/conversations'
        self.logger.debug('DirectLine requesting new conversation id')
        response = requests.post(url, headers=self.directline_get_headers())
        self.logger.debug('DirectLine response: ' + response.text)
        if response.status_code == 201 or response.status_code == 200:
            jsonresponse = response.json()
            return jsonresponse['conversationId']

        raise BBotException('Response error code: ' +
                            str(response.status_code))

    def directline_send_message(self, text):
        url = self.base_url + '/conversations/' + self.conversation_id + '/activities'
        payload = {
            'conversationId': self.conversation_id,
            'type': 'message',
            'from': {
                'id': self.request["user_id"]
            },
            'text': text
        }
        self.logger.debug('DirectLine sending message with payload: ' +
                          str(payload))
        self.logger.debug('url: ' + url)
        response = requests.post(url,
                                 headers=self.directline_get_headers(),
                                 data=json.dumps(payload))
        self.logger.debug('DirectLine response: ' + response.text)
        if response.status_code == 200:
            return response.json()

        raise BBotException('Response error code: ' +
                            str(response.status_code) + ' - Message: ' +
                            response.text)

    def directline_get_message(self):
        url = self.base_url + '/conversations/' + self.conversation_id + '/activities?watermark=' + self.watermark
        payload = {'conversationId': self.conversation_id}
        self.logger.debug('DirectLine getting message with payload: ' +
                          str(payload))
        self.logger.debug('url: ' + url)
        response = requests.get(url,
                                headers=self.directline_get_headers(),
                                json=payload)
        self.logger.debug('DirectLine response: ' + response.text)
        if response.status_code == 200:
            # store watermark
            json_response = response.json()
            self.watermark = json_response['watermark']
            self.dotdb.set_directline_session(self.user_id, self.bot_id,
                                              self.conversation_id,
                                              json_response['watermark'])
            return json_response

        raise BBotException('Response error code: ' +
                            str(response.status_code))