Exemplo n.º 1
0
class BotWrapper:
    def __init__(self,
                 url,
                 magic_phrase,
                 max_turns=10,
                 callback=None,
                 callback_params=1,
                 msg_q=False):
        print('starting service')
        self.start_proba = 1.0
        self.magic_phrase = magic_phrase
        self.url = replace_localhost(url)
        self.bot = Alice()
        self.max_turns = max_turns
        self.sending_message = False
        self._id = None
        self.use_msg_q = msg_q  # msg_q sets whether or not we are queueing messages
        self.websocket = 'ws://%s/websocket' % self.url
        self.client = MeteorClient(self.websocket)
        self.client.ddp_client.ddpsocket.extra_headers = [('Bot', 'true')]
        print(self.client.ddp_client.ddpsocket.handshake_headers)
        self.client.connect()

        self.idle_time = 3 * 60
        self.thread_time = 2
        self.max_retry = 3

    def restart_idler(self):
        ''' Restarts the idle watcher '''
        print('restarting idler')
        if hasattr(self, 'idler_thread') and self.idler_thread:
            self.idler_thread.cancel()
        self.idler_thread = threading.Timer(self.idle_time,
                                            self.idle_user_handler)
        self.idler_thread.start()

    def idle_user_handler(self):
        """ Handler that disconnects conversation in the event that a user leaves """

        print('user is idle disconnect')
        self.idler_thread = None
        self.end_convo()

    def login(self,
              user='******',
              pwd='botbot',
              callback=None,
              callback_params=0):
        print('logging in')

        def set_user(data):
            self.set_user_id(data['id'])
            print('user id set to', self._id)
            if callback and callback_params == 1:
                print('running callback with 1 parameter')
                callback(self)
            elif callback and callback_params == 0:
                callback()

        # TODO make this into threading timers.
        while not self._id:
            self.client.login(user,
                              pwd,
                              callback=func_wrap(set_user, params=1))
            time.sleep(0.5)

    def logout(self):
        self.client.logout()


#    def find_and_join_room(self):
#        """ Finds a room and joins it """
#        self.find_room(callback=(lambda roomId : self.join_room(roomId)))
#
#    def find_room(self, callback=None):
#        print('looking for an open room')
#        def room_callback():
#            print('looking for a room')
#            user = self.client.find_one('users')
#            print('user dict',user.items())
#            if user["in_convo"]:
#                roomObj = user["curConvo"]
#                print('roomid: ', roomObj)
#            else:
#                openrooms = self.client.find('convos') # {curSessions : {$lt  :2}}
#                roomObj = openrooms[0] if openrooms and len(openrooms) > 0 else -1
#
#            # TODO may have issues with room id when user is in convo
#            if roomObj != -1:
#                if type(roomObj) == str:
#                    print(roomObj, 'room')
#                    print('openrooms', openrooms)
#                callback(roomObj['_id'])
#                # Add user to room
#
#            else:
#                print('No rooms found. Back to the bat cave')
#        self.subscribe('currentUser',params=[], callback=func_wrap(
#            lambda : room_callback()
#            )
#        )

    def subscribe(self, collection, params=[], callback=None):
        """ Wrapper for subscribe to avoid issues with already subscribed rooms """
        try:
            print("subscribing to {}".format(collection))
            self.client.subscribe(collection, params, callback)
        except MeteorClientException:
            print(
                'Already subscribed to {}. Running callback with None'.format(
                    collection))
            if callback:
                callback(None)

    def join_room(self, roomId, otherUserId, callback=None):
        """ Join a room based on roomId """
        print('join room with id', roomId)
        self.roomId = roomId
        self.msg_queue = []
        self.available = False
        self.client.call(
            'convos.addUserToRoom',
            params=[roomId, self.magic_phrase],
            callback=func_wrap(lambda: self.subscribe(
                'chat', [roomId],
                func_wrap(lambda: self.subscribe(
                    'msgs', [roomId],
                    func_wrap(lambda: self.subscribe(
                        'currentUsers', [roomId],
                        func_wrap(lambda: self.watch_room(
                            roomId,
                            func_wrap(lambda: self.send_ready(
                                roomId, otherUserId, callback)))))))))))

    def send_ready(self, roomId, otherUserId, callback=None):
        self.client.call('convos.botReady',
                         params=[roomId, otherUserId, self.magic_phrase],
                         callback=callback)

    def unsubscribe(self, collection):
        """ Unsubscribe from the collection """
        try:
            self.client.unsubscribe(collection)
        except MeteorClientException:
            print('\t"{}" not subscribed to.'.format(collection))

    def end_convo(self):
        """ End the conversation """
        print('end conversation and unsubscribe from it all')
        self.client.remove_all_listeners('added')
        self.client.remove_all_listeners('changed')

        self.unsubscribe('chat')
        self.unsubscribe('msgs')
        self.unsubscribe('currentUsers')

        self.client.call('users.exitConvo', [])
        self.client.call('convos.updateRatings', [self.roomId, 'not'])
        self.available = True
        if hasattr(self, 'idler_thread') and self.idler_thread:
            self.idler_thread.cancel()

    def set_wpm(self):
        """ Set the words per minute of the bot """
        wpm = random.randint(150, 200)
        self.cps = 60 / (wpm * 5)
        print('Setting wpm : {} '.format(wpm))

    def prime_bot(self, convo_obj):
        """  the conversational bot """
        print('convo_obj', convo_obj)
        input_msg = 'hi'
        if 'msgs' in convo_obj and convo_obj['msgs']:
            topic_msg_id = convo_obj['msgs'][0]
            msg_obj = self.client.find_one('messages',
                                           selector={'_id': topic_msg_id})
            if msg_obj:
                input_msg = msg_obj['message']

        msg = self.bot.message(input_msg, self.roomId)
        if random.random() > self.start_proba:
            self.send_message(msg)

    def watch_room(self, roomId, callback=None):
        """
        Setup Event Listeneres for a room and checks to make sure that the room is updating
        """
        self.turns = 0
        convo_obj = self.client.find_one('convos', selector={'_id': roomId})
        self.room_closed = convo_obj['closed']
        self.set_wpm()

        self.last_message = ""
        self.confirmed_messages = [
        ]  # all messages sent by the user that have been confirmed
        self.thread = MessageHandlerThread(self)

        def message_added(collection, id, fields):
            """ callback for when a message is added """
            if (collection == 'messages' and 'message' in fields
                    and 'user' in fields):
                print(type(self._id), type(fields['user']), self._id,
                      fields['user'])
                if fields['user'] != self._id and self.last_message != fields[
                        'message']:
                    self.restart_idler()
                    self.receive_message(fields['message'])
                    self.last_message = fields['message']
                    self.thread.message_received = True
                elif fields['user'] == self._id:
                    print('\t messages from self detected')
                    self.confirmed_messages.append(fields['message'])

        self.client.on('added', message_added)

        def watch_convo(collection, id, fields, cleared):
            """ callback for when any part of the conversation is updated """
            if self.roomId and collection == "convos" and id == self.roomId:
                # print('\t',fields)
                if 'closed' in fields:
                    print('\tRoom is closed: ', fields['closed'])
                    self.room_closed = fields['closed']
                    self.end_convo()
                if 'msgs' in fields:
                    print('\tMessages updated in convo "{}"'.format(id))
                    # TODO this is bugggy
                    self.thread.convo_updated = True
                if 'turns' in fields:
                    print('\tTurns updated to "{}"'.format(fields['turns']))
                    self.turns = fields['turns']
            elif self.roomId == id:
                print(collection, id, fields)

        self.client.on('changed', watch_convo)
        # mark the bot as ready to talk
        self.restart_idler()
        self.prime_bot(convo_obj)
        print("before thread")
        self.thread.start()
        print("after thread")

        if callback:
            callback(None)

    def respond(self):
        """ Kind of a hacky way to respond to the conversation """
        print("responding")
        if self.msg_queue and self.use_msg_q:
            partner_msg = self.msg_queue[0]
            self.msg_queue = self.msg_queue[1:]
            msg = self.bot.message(partner_msg, self.roomId)
            print(msg)
            self.send_message(msg)

        if self.msg_queue and not self.sending_message:
            partner_msg = self.msg_queue[-1]
            self.msg_queue = self.msg_queue[:-1]
            msg = self.bot.message(partner_msg, self.roomId)
            print(msg)
            self.send_message(msg)

    def still_in_conv(self):
        """ Returns whether the conversation is still moving """
        in_conv = self.roomId != None and not self.client.find_one(
            'convos', selector={'_id': self.roomId})['closed']
        print('\tstill in conv', in_conv)
        if not in_conv:
            self.end_convo()
        print(
            '\tclosed: ',
            self.client.find_one('convos', selector={'_id':
                                                     self.roomId})['closed'])
        return in_conv

    def get_convo_dict(self):
        if self.roomId:
            return self.client.find_one('convos',
                                        selector={'_id': self.roomId})
        else:
            return {}

    def get_message(self, idx):
        ''' Returns the message at idx'''
        convo_dict = self.get_convo_dict()
        if convo_dict:
            topic_msg_id = convo_dict['msgs'][idx]
            msg_dict = self.client.find_one('messages',
                                            selector={'_id': topic_msg_id})
            # print(msg_dict)
            if msg_dict:
                return msg_dict['message']
        return ''

    def received_message(self, message):
        """ Checks whether the bot actually sent the message """
        # TODO add handler that removes a confirmed message to save memory
        return message in self.confirmed_messages

    def retry_message(self, message, retry=0, callback=None):
        """ Handler that makes attempts to connect a user back into a conversation """
        # TODO set as properties
        if retry == 0 or not self.received_message(
                message) and retry < self.max_retry:
            self.update_conversation(message, callback)

            if retry != 0:
                print('\t\tRetry {} of sending "{}"'.format(retry, message))

            t = threading.Timer(self.thread_time,
                                lambda: self.retry_message(message, retry + 1))
            t.start()
        elif retry >= self.max_retry:
            print(
                '\tMax retries reached - couldn\'t verify whether {} was received'
                .format(message))
        else:
            print('\t"{}" successfully received'.format(message))

    def update_conversation(self, message, callback=None):
        self.client.call('convos.updateChat', [message, self.roomId], callback)

    def _send_message(self, message, callback=None):
        self.last_message_sent = message
        if self.still_in_conv():
            self.retry_message(message, callback=callback)
        else:
            print('Not responding - conversation is OVER')
        self.sending_message = False

    def send_message(self, message, callback=None):
        # calculates typing speed based on rough cps for user
        sleep_time = self.cps * len(message)
        print("Preparing to send '{}' Waiting '{}' seconds.".format(
            message, sleep_time))
        t = threading.Timer(sleep_time,
                            lambda: self._send_message(message, callback))
        t.start()

    def receive_message(self, message):
        """ Called whenever the bot receives a message """
        print('Received "{}"'.format(message))
        self.msg_queue.append(message)
        # message = 'sup then' # self.bot.message(message)

        # self.send_message(message)

    def set_user_id(self, id):
        self.available = True
        print('set user id to ', id)
        self._id = id
class AoikRocketChatErrbot(ErrBot):
    """
    Errbot backend for Rocket.Chat.

    The backend logs in as a Rocket.Chat user, receiving and sending messages.
    """
    def __init__(self, config):
        """
        Constructor.

        :param config: Errbot's config module.

        :return: None.
        """
        # Call super method
        super(AoikRocketChatErrbot, self).__init__(config)

        # Get the backend's config object
        self._config_obj = getattr(self.bot_config, _CONFIG_OBJ_KEY, None)

        # Get logging level from env variable or config object
        log_level = orig_log_level = self._get_config(
            CONFIG_KEYS.BOT_LOG_LEVEL, None)

        # If not specified
        if log_level is None:
            # Get logging level from config module
            log_level = orig_log_level = getattr(self.bot_config,
                                                 CONFIG_KEYS.BOT_LOG_LEVEL,
                                                 None)

            # If not specified
            if log_level is None:
                # Use default
                log_level = logging.DEBUG

        # If the logging level is string, e.g. 'DEBUG'.
        # This means it may be an attribute name of the `logging` module.
        if isinstance(log_level, str):
            # Get attribute value from the `logging` module
            log_level = getattr(logging, log_level, None)

        # Error message
        error_msg = None

        # If the logging level is not int
        if not isinstance(log_level, int):
            # Get message
            error_msg = 'Config `BOT_LOG_LEVEL` value is invalid: {}'.format(
                repr(orig_log_level))

            # Log message
            self._log_error(error_msg)

            # Raise error
            raise ValueError(error_msg)

        # Get logger
        self._logger = logging.getLogger('aoikrocketchaterrbot')

        # Set logging level
        self._logger.setLevel(log_level)

        # Get message
        msg = '# ----- Created logger -----\nBOT_LOG_LEVEL: {}'.format(
            log_level)

        # Log message
        self._logger.debug(msg)

        # Get rocket chat server URI
        self._server_uri = self._get_config(CONFIG_KEYS.SERVER_URI)

        # If server URI is not specified
        if self._server_uri is None:
            # Get message
            error_msg = 'Missing config `SERVER_URI`.'

            # Log message
            self._log_error(error_msg)

            # Raise error
            raise ValueError(error_msg)

        # Get login username
        self._login_username = self._get_config(CONFIG_KEYS.LOGIN_USERNAME)

        # If login username is not specified
        if self._login_username is None:
            # Get message
            error_msg = 'Missing config `LOGIN_USERNAME`.'

            # Log message
            self._log_error(error_msg)

            # Raise error
            raise ValueError(error_msg)

        # Get login password
        self._login_password = self._get_config(CONFIG_KEYS.LOGIN_PASSWORD)

        # If login password is not specified
        if self._login_password is None:
            # Get message
            error_msg = 'Missing config `LOGIN_PASSWORD`.'

            # Log message
            self._log_error(error_msg)

            # Raise error
            raise ValueError(error_msg)

        # If login password is not bytes
        if not isinstance(self._login_password, bytes):
            # Convert login password to bytes
            self._login_password = bytes(self._login_password, 'utf-8')

        # Create login user's identifier object.
        #
        # This attribute is required by superclass.
        #
        self.bot_identifier = self.build_identifier(self._login_username)

        # Event set when the the meteor client has done topic subscribing.
        #
        # When the event is set, the meteor client is in one of the two states:
        # - The topic subscribing has succeeded and the meteor client has
        #   started handling messages.
        # - The topic subscribing has failed and the meteor client has been
        #   closed.
        #
        # The rationale is that the loop at 65ZNO uses the meteor client's
        # `connected` attribute to decide whether continue, and the attribute
        # value is ready for use only after this event is set.
        self._subscribing_done_event = Event()

        # Event set when the meteor client calls the `closed` callback at
        # 3DMYH.
        #
        # The rationale is that the main thread code at 5W6XQ has to wait until
        # the meteor client is closed and the `closed` callback hooked at 7MOJX
        # is called. This ensures the cleanup is fully done.
        #
        self._meteor_closed_event = Event()

    @property
    def mode(self):
        """
        Get mode name.

        :return: Mode name.
        """
        # Return mode name
        return 'aoikrocketchaterrbot'

    def _log_debug(self, msg):
        """
        Log debug message.

        :param msg: Message to log.

        :return: None.
        """
        # Log the message
        self._logger.debug(msg)

    def __hash__(self):
        """Bots are now stored as a key in the bot so they need to be hashable."""
        return id(self)

    def _log_error(self, msg):
        """
        Log error message.

        :param msg: Message to log.

        :return: None.
        """
        # Log the message
        self._logger.error(msg)

    def _get_config(self, key, default=None):
        """
        Get config value from env variable or config object.

        Env variable takes precedence.

        :param key: Config key.

        :param default: Default value.

        :return: Config value.
        """
        # Get env variable name
        env_var_name = _ENV_VAR_NAME_PREFIX + key

        # Get config value from env variable
        config_value = os.environ.get(env_var_name, None)

        # If not specified
        if config_value is None:
            # If not have config object
            if self._config_obj is None:
                # Use default
                config_value = default

            # If have config object
            else:
                # Get config value from config object
                config_value = getattr(self._config_obj, key, default)

        # Return config value
        return config_value

    def _get_bool_config(self, key, default=None):
        """
        Get boolean config value from env variable or config object.

        Env variable takes precedence.

        :param key: Config key.

        :param default: Default value.

        :return: Config value.
        """
        # Get config value
        config_value = self._get_config(key=key, default=default)

        # If config value is false.
        # This aims to handle False, 0, and None.
        if not config_value:
            # Return False
            return False

        # If config value is not false
        else:
            # Get config value's string in lower case
            config_value_str_lower = str(config_value).lower()

            # Consider '0', case-insensitive 'false' and 'no' as false,
            # otherwise as true.
            return config_value_str_lower not in ['0', 'false', 'no']

    def _patch_meteor_client(self):
        """
        Patch meteor client to fix an existing bug.

        :return: None.
        """
        # Get whether need patch meteor client. Default is True.
        need_patch = self._get_bool_config(CONFIG_KEYS.PATCH_METEOR_CLIENT,
                                           True)

        # If need patch meteor client
        if need_patch:
            # Create `change_data` function
            def change_data(self, collection, id, fields, cleared):
                """
                Callback called when data change occurred.

                :param self: CollectionData object.

                :param collection: Data collection key.

                :param id: Data item key.

                :param fields: Data fields changed.

                :param cleared: Data fields to be cleared.

                :return None.
                """
                # If the data collection key not exists
                #
                # The original `change_data` function assumes it is existing,
                # but it is not in some cases.
                #
                if collection not in self.data:
                    # Add data collection
                    self.data[collection] = {}

                # If the data item key not exists.
                #
                # The original `change_data` function assumes it is existing,
                # but it is not in some cases.
                #
                if id not in self.data[collection]:
                    # Add data item
                    self.data[collection][id] = {}

                # For each data field changed
                for key, value in fields.items():
                    # Add to the data item
                    self.data[collection][id][key] = value

                # For each data field to be cleared
                for key in cleared:
                    # Delete from the data item
                    del self.data[collection][id][key]

            # Store original `change_data`.
            #
            # pylint: disable=protected-access
            CollectionData._orig_change_data = CollectionData.change_data
            # pylint: enable=protected-access

            # Replace original `change_data`
            CollectionData.change_data = change_data

    def build_identifier(self, username):
        """
        Create identifier object for given username.

        :param username: Rocket chat user name.

        :return: RocketChatUser instance.
        """
        # Create identifier object
        return RocketChatUser(username)

    def serve_forever(self):
        """
        Run the bot.

        Called by the Errbot framework.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- serve_forever -----')

        # Patch meteor client
        self._patch_meteor_client()

        # Get whether reconnect is enabled
        reconnect_enabled = self._get_bool_config(
            CONFIG_KEYS.RECONNECT_ENABLED,
            default=True,
        )

        try:
            # Loop
            while True:
                try:
                    # Run for once
                    self.serve_once()

                # If have error
                except Exception:
                    # Log message
                    self._log_error(('# ----- `serve_once` failed with error'
                                     ' -----\n{}').format(format_exc()))

                # If reconnect is enabled
                if reconnect_enabled:
                    # Get message
                    msg = ('# ----- Sleep before reconnect -----\n'
                           'Interval: {:.2f}').format(self._reconnection_delay)

                    # Log message
                    self._log_debug(msg)

                    # Sleep before reconnect
                    self._delay_reconnect()

                    # Log message
                    self._log_debug('# ----- Wake up to reconnect -----')

                    # Continue the loop
                    continue

                # If reconnect is not enabled
                else:
                    # Break the loop
                    break

        # If have `KeyboardInterrupt`
        except KeyboardInterrupt:
            # Do not treat as error
            pass

        # Always do
        finally:
            # Call `shutdown`
            self.shutdown()

        # Log message
        self._log_debug('# ===== serve_forever =====')

    def serve_once(self):
        """
        Run the bot until the connection is disconnected.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- serve_once -----')

        # Log message
        self._log_debug(('# ----- Create meteor client -----\n'
                         'SERVER_URI: {}').format(self._server_uri))

        # Create meteor client
        self._meteor_client = MeteorClient(
            self._server_uri,
            # Disable the meteor client's auto reconnect.
            # Let `serve_forever` handle reconnect.
            auto_reconnect=False,
        )

        # Log message
        self._log_debug('# ----- Hook meteor client callbacks -----')

        # 5DI82
        # Hook meteor client `connected` callback
        self._meteor_client.on('connected', self._meteor_connected_callback)

        # 2RAYF
        # Hook meteor client `changed` callback
        self._meteor_client.on('changed', self._meteor_changed_callback)

        # 4XIZB
        # Hook meteor client `added` callback
        self._meteor_client.on('added', self._meteor_added_callback)

        # 2JEIK
        # Hook meteor client `removed` callback
        self._meteor_client.on('removed', self._meteor_removed_callback)

        # 32TF2
        # Hook meteor client `failed` callback
        self._meteor_client.on('failed', self._meteor_failed_callback)

        # 5W6RX
        # Hook meteor client `reconnected` callback
        self._meteor_client.on('reconnected',
                               self._meteor_reconnected_callback)

        # 7MOJX
        # Hook meteor client `closed` callback
        self._meteor_client.on('closed', self._meteor_closed_callback)

        # Clear the event
        self._subscribing_done_event.clear()

        # Clear the event
        self._meteor_closed_event.clear()

        # Log message
        self._log_debug('# ----- Connect to meteor server -----')

        try:
            # Connect to meteor server.
            #
            # If the connecting succeeds, the meteor client's thread will call
            # `self._meteor_connected_callback` hooked at 5DI82. The login,
            # topic subscribing, and message handling are run in that thread.
            #
            # The main thread merely waits until the meteor client is closed,
            # meanwhile it calls heartbeat function at interval if specified.
            #
            self._meteor_client.connect()

        # If have error
        except:
            # Log message
            self._log_debug('# ----- Connecting failed -----')

            # Log message
            self._log_debug('# ----- Unhook meteor client callbacks -----')

            # Remove meteor client callbacks
            self._meteor_client.remove_all_listeners()

            # Remove meteor client reference
            self._meteor_client = None

            # The two events below should not have been set if the connecting
            # failed. Just in case.
            #
            # Clear the event
            self._subscribing_done_event.clear()

            # Clear the event
            self._meteor_closed_event.clear()

            # Raise the error
            raise

        # Get whether heartbeat is enabled
        heartbeat_enabled = self._get_bool_config(
            CONFIG_KEYS.HEARTBEAT_ENABLED)

        try:
            # Wait until the topic subscribing is done in another thread at
            # 5MS7A
            self._subscribing_done_event.wait()

            # If heartbeat is enabled
            if heartbeat_enabled:
                # Get heartbeat function
                heartbeat_func = self._get_config(CONFIG_KEYS.HEARTBEAT_FUNC)

                # Assert the function is callable
                assert callable(heartbeat_func), repr(heartbeat_func)

                # Get heartbeat interval
                heartbeat_interval = self._get_config(
                    CONFIG_KEYS.HEARTBEAT_INTERVAL,
                    default=10,
                )

                # Convert heartbeat interval to float
                heartbeat_interval = float(heartbeat_interval)

                # 65ZNO
                # Loop until the meteor client is disconnected
                while self._meteor_client.connected:
                    # Send heartbeat
                    heartbeat_func(self)

                    # Sleep before sending next heartbeat
                    time.sleep(heartbeat_interval)

            # 5W6XQ
            # Wait until the meteor client is closed and the `closed` callback
            # is called at 3DMYH
            self._meteor_closed_event.wait()

        # If have error
        except:
            # Close meteor client.
            #
            # This will cause `self._meteor_closed_callback` to be called,
            # which will set the `self._meteor_closed_event` below.
            #
            self._meteor_client.close()

            # See 5W6XQ
            self._meteor_closed_event.wait()

            # Raise the error
            raise

        # Always do
        finally:
            # Log message
            self._log_debug('# ----- Unhook meteor client callbacks -----')

            # Remove meteor client callbacks
            self._meteor_client.remove_all_listeners()

            # Remove meteor client reference.
            #
            # Do this before calling `callback_presence` below so that the
            # plugins will not be able to access the already closed client.
            #
            self._meteor_client = None

            # Log message
            self._log_debug('# ----- Call `callback_presence` -----')

            # Call `callback_presence`
            self.callback_presence(
                Presence(identifier=self.bot_identifier, status=OFFLINE))

            # Log message
            self._log_debug('# ----- Call `disconnect_callback` -----')

            # Call `disconnect_callback` to unload plugins
            self.disconnect_callback()

            # Clear the event
            self._subscribing_done_event.clear()

            # Clear the event
            self._meteor_closed_event.clear()

        # Log message
        self._log_debug('# ===== serve_once =====')

    def _meteor_connected_callback(self):
        """
        Callback called when the meteor client is connected.

        Hooked at 5DI82.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- _meteor_connected_callback -----')

        # Log message
        self._log_debug(
            '# ----- Log in to meteor server -----\nUser: {}'.format(
                self._login_username))

        # Log in to meteor server
        self._meteor_client.login(
            user=self._login_username,
            password=self._login_password,
            # 2I0GP
            callback=self._meteor_login_callback,
        )

    def _meteor_login_callback(self, error_info, success_info):
        """
        Callback called when the meteor client has succeeded or failed login.

        Hooked at 2I0GP.

        :param error_info: Error info.

        :param success_info: Success info.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- _meteor_login_callback -----')

        # If have error info
        if error_info is not None:
            # Get message
            msg = 'Login failed:\n{}'.format(pformat(error_info, width=1))

            # Log message
            self._log_debug(msg)

            # Close meteor client.
            # This will cause `_meteor_closed_callback` be called.
            self._meteor_client.close()

        # If not have error info
        else:
            # Get message
            msg = 'Login succeeded:\n{}'.format(pformat(success_info, width=1))

            # Log message
            self._log_debug(msg)

            # Subscribe to message events
            self._meteor_client.subscribe(
                # Topic name
                name='stream-room-messages',
                params=[
                    # All messages from rooms the rocket chat user has joined
                    '__my_messages__',
                    False,
                ],
                # 6BKIR
                callback=self._meteor_subscribe_callback,
            )

    def _meteor_subscribe_callback(self, error_info):
        """
        Callback called when the meteor client has succeeded or failed \
            subscribing.

        Hooked at 6BKIR.

        :param error_info: Error info.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- _meteor_subscribe_callback -----')

        # If have error info
        if error_info is not None:
            # Get message
            msg = 'Subscribing failed:\n{}'.format(pformat(error_info,
                                                           width=1))

            # Log message
            self._log_debug(msg)

            # Close meteor client.
            # This will cause `self._meteor_closed_callback` to be called.
            self._meteor_client.close()

        # If not have error info
        else:
            # Log message
            self._log_debug('Subscribing succeeded.')

            # Log message
            self._log_debug('# ----- Call `connect_callback` -----')

            # Call `connect_callback` to load plugins.
            #
            # This is called in meteor client's thread.
            # Plugins should not assume they are loaded from the main thread.
            #
            self.connect_callback()

            # Log message
            self._log_debug('# ----- Call `callback_presence` -----')

            # Call `callback_presence`
            self.callback_presence(
                Presence(identifier=self.bot_identifier, status=ONLINE))

            # Log message
            self._log_debug(
                '# ----- Hook callback `_meteor_changed_callback` -----')

            # Reset reconnection count
            self.reset_reconnection_count()

            # 5MS7A
            # Set the topic subscribing is done
            self._subscribing_done_event.set()

    def _meteor_changed_callback(self, collection, id, fields, cleared):
        """
        Callback called when the meteor client received message.

        Hooked at 2RAYF.

        :param collection: Data collection key.

        :param id: Data item key.

        :param fields: Data fields changed.

        :param cleared: Data fields to be cleared.

        :return: None.
        """
        # If is message event
        if collection == 'stream-room-messages':
            # Get `args` value
            args = fields.get('args', None)

            # If `args` value is list
            if isinstance(args, list):
                # For each message info
                for msg_info in args:
                    # Get message
                    msg = msg_info.get('msg', None)

                    # If have message
                    if msg is not None:
                        # Get sender info
                        sender_info = msg_info['u']

                        # Get sender username
                        sender_username = sender_info['username']

                        # If the sender is not the bot itself
                        if sender_username != self._login_username:
                            # Create sender's identifier object
                            sender_identifier = self.build_identifier(
                                sender_username)

                            # Create extras info
                            extras = {
                                # 2QTGO
                                'msg_info': msg_info,
                            }

                            # Create received message object
                            msg_obj = Message(
                                body=msg,
                                frm=sender_identifier,
                                to=self.bot_identifier,
                                extras=extras,
                            )

                            # Log message
                            self._log_debug(
                                '# ----- Call `callback_message` -----')

                            # Call `callback_message` to dispatch the message
                            # to plugins
                            self.callback_message(msg_obj)

    def _meteor_added_callback(self, collection, id, fields):
        """
        Callback called when the meteor client emits `added` event.

        Hooked at 4XIZB.

        :param collection: Data collection key.

        :param id: Data item key.

        :param fields: Data fields.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- _meteor_added_callback -----')

    def _meteor_removed_callback(self, collection, id):
        """
        Callback called when the meteor client emits `removed` event.

        Hooked at 2JEIK.

        :param collection: Data collection key.

        :param id: Data item key.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- _meteor_removed_callback -----')

    def _meteor_failed_callback(self):
        """
        Callback called when the meteor client emits `failed` event.

        Hooked at 32TF2.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- _meteor_failed_callback -----')

    def _meteor_reconnected_callback(self):
        """
        Callback called when the meteor client emits `reconnected` event.

        Hooked at 5W6RX.

        :return: None.
        """
        # Log message
        self._log_debug('# ----- _meteor_reconnected_callback -----')

    def _meteor_closed_callback(self, code, reason):
        """
        Callback called when the meteor client emits `closed` event.

        Hooked at 7MOJX.

        :param code: Close code.

        :param reason: Close reason.

        :return: None.
        """
        # Log message
        self._log_debug(
            '# ----- _meteor_closed_callback -----\nCode: {}\nReason: {}'.
            format(code, reason))

        # Set the topic subscribing is done
        self._subscribing_done_event.set()

        # 3DMYH
        # Set the meteor client's `closed` event is emitted
        self._meteor_closed_event.set()

    def build_reply(self, mess, text=None, private=False, threaded=False):
        """
        Create reply message object.

        Used by `self.send_simple_reply`.

        :param mess: The original message object.

        :param text: Reply message text.

        :param private: Whether the reply message is private.

        :return: Message object.
        """
        # Create reply message object
        reply = Message(
            body=text,
            frm=mess.to,
            to=mess.frm,
            extras={
                # 5QXGV
                # Store the original message object
                'orig_msg': mess
            })

        # Return reply message object
        return reply

    def prefix_groupchat_reply(self, message, identifier):
        """
        Add group chat prefix to the message.

        Used by `self.send` and `self.send_simple_reply`.

        :param message: Message object to send.

        :param identifier: The message receiver's identifier object.

        :return: None.
        """
        # Do nothing

    def send_rocketchat_message(self, params):
        """
        Send message to meteor server.

        :param params: RPC method `sendMessage`'s parameters.

        :return: None.
        """
        # If argument `params` is not list
        if not isinstance(params, list):
            # Put it in a list
            params = [params]

        # Send message to meteor server
        self._meteor_client.call(
            method='sendMessage',
            params=params,
        )

    def send_message(self, mess):
        """
        Send message to meteor server.

        Used by `self.split_and_send_message`. `self.split_and_send_message` is
        used by `self.send` and `self.send_simple_reply`.

        :param mess: Message object to send.

        :return: None.
        """
        # Call super method to dispatch to plugins
        super(AoikRocketChatErrbot, self).send_message(mess)

        # Get original message object.
        #
        # The key is set at 5QXGV and 3YRCT.
        #
        orig_msg = mess.extras['orig_msg']

        # Get original message info.
        #
        # The key is set at 2QTGO
        #
        msg_info = orig_msg.extras['msg_info']

        # Get room ID
        room_id = msg_info['rid']

        # Send message to meteor server
        self.send_rocketchat_message(params={
            'rid': room_id,
            'msg': mess.body,
        })

    def send(
        self,
        identifier,
        text,
        in_reply_to=None,
        groupchat_nick_reply=False,
    ):
        """
        Send message to meteor server.

        :param identifier: Receiver's identifier object.

        :param text: Message text to send.

        :param in_reply_to: Original message object.

        :param groupchat_nick_reply: Whether the message to send is group chat.

        `self.prefix_groupchat_reply` will be called to process the message if
        it is group chat.

        :return: None.
        """
        # If the identifier object is not Identifier instance
        if not isinstance(identifier, Identifier):
            # Get message
            error_msg = (
                'Argument `identifier` is not Identifier instance: {}').format(
                    repr(identifier))

            # Raise error
            raise ValueError(error_msg)

        # If the original message is not given
        if in_reply_to is None:
            # Get message
            error_msg = 'Argument `in_reply_to` must be given.'

            # Raise error
            raise ValueError(error_msg)

        # Create message object
        msg_obj = Message(
            body=text,
            frm=in_reply_to.to,
            to=identifier,
            extras={
                # 3YRCT
                # Store the original message object
                'orig_msg': in_reply_to,
            },
        )

        # Get group chat prefix from config
        group_chat_prefix = self.bot_config.GROUPCHAT_NICK_PREFIXED

        # If the receiver is a room
        if isinstance(identifier, Room):
            # If have group chat prefix,
            # or the message is group chat.
            if group_chat_prefix or groupchat_nick_reply:
                # Call `prefix_groupchat_reply` to process the message
                self.prefix_groupchat_reply(msg_obj, in_reply_to.frm)

        # Send the message
        self.split_and_send_message(msg_obj)

    def query_room(self, room):
        """
        Query room info. Not implemented.

        :param room: Room ID.

        :return: None.
        """
        # Return None
        return None

    def rooms(self):
        """
        Get room list. Not implemented.

        :return: Empty list.
        """
        # Return empty list
        return []

    def change_presence(self, status=ONLINE, message=''):
        """