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)
Esempio n. 2
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
Esempio n. 3
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)
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
Esempio n. 5
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)
Esempio n. 6
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