Exemplo n.º 1
0
    def __init__(self, *args, **kwargs):
        context = zmq.Context(1)
        # 所有插件都应该使用这个socket,否则会自动负载均衡,破坏逻辑
        # TODO put broker_socket in holder
        if not holder.broker_socket:
            self.broker_socket = context.socket(zmq.DEALER)
            self.broker_socket.connect('ipc://{0}/broker.ipc'.format(
                config.ipc_path))
            holder.broker_socket = self.broker_socket
        else:
            self.broker_socket = holder.broker_socket

        self.names = tuple(name.lower() for name in config.names)

        self.room_manager = RoomManager()

        logger.info('Starting to load user...')

        if config.backend_count:
            from ..common.threadpool import ThreadPool

            self.thread_pool = ThreadPool(config.backend_count)
            logger.debug('Created the thread pool {0}'.format(
                self.thread_pool))

        self.commands = {}
        self.re_commands = {}
        self.sessions = {}
        self.admins = []
Exemplo n.º 2
0
    def __init__(self, *args, **kwargs):
        context = zmq.Context(1)
        # 所有插件都应该使用这个socket,否则会自动负载均衡,破坏逻辑
        # TODO put broker_socket in holder
        if not holder.broker_socket:
            self.broker_socket = context.socket(zmq.DEALER)
            self.broker_socket.connect('ipc://{0}/broker.ipc'.format(config.ipc_path))
            holder.broker_socket = self.broker_socket
        else:
            self.broker_socket = holder.broker_socket

        self.names = tuple(name.lower() for name in config.names)

        self.room_manager = RoomManager()

        logger.info('Starting to load user...')

        if config.backend_count:
            from ..common.threadpool import ThreadPool

            self.thread_pool = ThreadPool(config.backend_count)
            logger.debug('Created the thread pool {0}'.format(self.thread_pool))

        self.commands = {}
        self.re_commands = {}
        self.sessions = {}
        self.admins = []
Exemplo n.º 3
0
class Backend(object):
    cmd_history = defaultdict(lambda: deque(maxlen=10))

    MSG_ERROR_OCCURRED = '消息处理发生异常'
    MESSAGE_SIZE_LIMIT = config.max_message_size
    MSG_UNKNOWN_COMMAND = 'Unknown command: "%(command)s". ' \
                          'Type "' + config.main_name + ' help" for available commands.'
    MSG_HELP_TAIL = 'Type help <command names> to get more info ' \
                    'about that specific command.'
    MSG_HELP_UNDEFINED_COMMAND = 'That command is not defined.'

    def __init__(self, *args, **kwargs):
        context = zmq.Context(1)
        # 所有插件都应该使用这个socket,否则会自动负载均衡,破坏逻辑
        # TODO put broker_socket in holder
        if not holder.broker_socket:
            self.broker_socket = context.socket(zmq.DEALER)
            self.broker_socket.connect('ipc://{0}/broker.ipc'.format(config.ipc_path))
            holder.broker_socket = self.broker_socket
        else:
            self.broker_socket = holder.broker_socket

        self.names = tuple(name.lower() for name in config.names)

        self.room_manager = RoomManager()

        logger.info('Starting to load user...')

        if config.backend_count:
            from ..common.threadpool import ThreadPool

            self.thread_pool = ThreadPool(config.backend_count)
            logger.debug('Created the thread pool {0}'.format(self.thread_pool))

        self.commands = {}
        self.re_commands = {}
        self.sessions = {}
        self.admins = []

    def check_command_access(self, message, cmd):
        f = self.commands[cmd] if cmd in self.commands else self.re_commands[cmd]
        if f._tom_command_admin_only:
            if not self.auth_admin(message):
                raise ACLViolation('Command needs admin privileges but authentication failed.')
        return True

    def auth_admin(self, message):
        password = message.get_input('Please give me your pass code.')
        if password == config.admin_pass:
            message.ok('Authentication passed!')
            return True
        else:
            message.warn('Authentication failed!!!')
            return False

    def get_commands(self):
        return self.commands

    def get_re_commands(self):
        return self.re_commands

    def unknown_command(self, mess, cmd, args):
        """ Override the default unknown command behavior
        """
        full_cmd = cmd + ' ' + args.split(' ')[0] if args else None
        if full_cmd:
            part1 = 'Command "%s" / "%s" not found.' % (cmd, full_cmd)
        else:
            part1 = 'Command "%s" not found.' % cmd
        underscore_keys = [m.replace('_', ' ') for m in self.commands.keys()]
        matches = difflib.get_close_matches(cmd, underscore_keys)
        if full_cmd:
            matches.extend(difflib.get_close_matches(full_cmd, underscore_keys))
        matches = set(matches)
        if matches:
            return part1 + '\n\nDid you mean "' + config.main_name + ' ' + ('" or "' + config.main_name + ' ').join(
                matches) + '" ?'
        else:
            return part1

    @staticmethod
    def send_simple_reply(message, text):
        message.send(text)

    def get_sender_username(self, mess):
        """
        override for special username convertion
        :param mess: Message object
        """
        # TODO Make it possible to convert from uin to QQ number.
        return mess['user']

    def callback_message(self, mess):
        """
        Needs to return False if we want to stop further treatment
        """
        # Prepare to handle either private chats or group chats

        msg_type = mess['type']
        user = mess['user']
        message_id = mess['message_id']
        content = mess['content']
        username = self.get_sender_username(mess)
        user_cmd_history = self.cmd_history[username]

        # 3 types of QQ chat, there's sess msg_type for non-friend talk, but with crypt issue, don't use it
        if msg_type not in ('buddy', 'group', 'discu', 'api', 'chat', 'groupchat'):
            logger.warn("unhandled message msg_type %s" % mess)
            return False

        logger.debug("*** Message_ID = %s" % message_id)
        logger.debug("*** user = %s" % user)
        # logger.debug("*** username = %s" % username)
        logger.debug("*** msg_type = %s" % msg_type)
        logger.debug("*** content = %s" % content)

        # If a message format is not supported (eg. encrypted),
        # txt will be None
        if not content:
            return False

        suppress_cad_not_found = False

        prefixed = False  # Keeps track whether content was prefixed with a bot prefix
        only_check_re_command = False  # Becomes true if content is determed to not be a regular command
        tomatch = content.lower()
        if len(self.names) > 0 and tomatch.startswith(self.names):
            # Yay! We were called by one of our alternate prefixes. Now we just have to find out
            # which one... (And find the longest matching, in case you have 'err' and 'errbot' and
            # someone uses 'errbot', which also matches 'err' but would leave 'bot' to be taken as
            # part of the called command in that case)
            prefixed = True
            longest = 0
            for name in self.names:
                l = len(name)
                if tomatch.startswith(name) and l > longest:
                    longest = l
            logger.debug("Called with alternate name '{}'".format(content[:longest]))
            content = content[longest:]

            # Now also remove the separator from the content
            for sep in config.bot_alt_separators:
                # While unlikely, one may have separators consisting of
                # more than one character
                l = len(sep)
                if content[:l] == sep:
                    content = content[l:]
        elif not content.startswith(config.prefix):
            only_check_re_command = True
        if content.startswith(config.prefix):
            content = content[len(config.prefix):]
            prefixed = True

        content = content.strip()
        text_split = content.split(' ')
        cmd = None
        command = None
        args = ''
        if not only_check_re_command:
            if len(text_split) > 1:
                command = (text_split[0] + '_' + text_split[1]).lower()
                if command in self.commands:
                    cmd = command
                    args = ' '.join(text_split[2:])

            if not cmd:
                command = text_split[0].lower()
                args = ' '.join(text_split[1:])
                if command in self.commands:
                    cmd = command
                    if len(text_split) > 1:
                        args = ' '.join(text_split[1:])

            if content == '!':  # we did "!!" so recall the last command
                if len(user_cmd_history):
                    cmd, args = user_cmd_history[-1]
                else:
                    return False  # no command in history
            elif content and content.isdigit():  # we did "!#" so we recall the specified command
                index = int(content)
                if len(user_cmd_history) >= index:
                    cmd, args = user_cmd_history[-index]
                else:
                    return False  # no command in history

        # Try to match one of the regex commands if the regular commands produced no match
        matched_on_re_command = False
        if not cmd:
            if prefixed:
                commands = self.re_commands
            else:
                commands = {k: self.re_commands[k] for k in self.re_commands
                            if not self.re_commands[k]._tom_command_prefix_required}

            for name, func in commands.items():
                match = func._tom_command_re_pattern.search(content)
                if match:
                    logger.debug(u"Matching '{}' against '{}' produced a match"
                                 .format(content, func._tom_command_re_pattern.pattern))
                    matched_on_re_command = True
                    self._process_command(mess, name, content, match)
                else:
                    logger.debug(u"Matching '{}' against '{}' produced no match"
                                 .format(content, func._tom_command_re_pattern.pattern))
        if matched_on_re_command:
            return True

        if cmd:
            self._process_command(mess, cmd, args, match=None)
        elif not only_check_re_command:
            logger.debug("Command not found")
            if suppress_cad_not_found:
                logger.debug("Surpressing command not found feedback")
            else:
                reply = self.unknown_command(mess, command, args)
                if reply is None:
                    reply = self.MSG_UNKNOWN_COMMAND % {'command': command}
                if reply:
                    self.send_simple_reply(mess, reply)

        return True

    def _process_command(self, mess, cmd, args, match):
        """Process and execute a bot command"""
        logger.info(u"Processing command {} with parameters '{}'".format(cmd, args))

        user = mess['user']
        username = self.get_sender_username(mess)
        user_cmd_history = self.cmd_history[username]

        if (cmd, args) in user_cmd_history:
            user_cmd_history.remove((cmd, args))  # Avoids duplicate history items

        if user not in [base64.encodestring(a) for a in self.admins]:
            try:
                self.check_command_access(mess, cmd)
            except ACLViolation as e:
                mess.error("You don't have permission to execute this command!")
                return

        f = self.re_commands[cmd] if match else self.commands[cmd]

        if f._tom_command_admin_only:
            self.thread_pool.wait()  # If it is an admin command, wait that the queue is completely depleted so we don't have strange concurrency issues on load/unload/updates etc ...

        if f._tom_command_historize:
            user_cmd_history.append((cmd, args))  # add it to the history only if it is authorized to be so
            mess.session['history'].append((cmd, args))

        # Don't check for None here as None can be a valid argument to split.
        # '' was chosen as default argument because this isn't a valid argument to split()
        if not match and f._tom_command_split_args_with != '':
            args = args.split(f._tom_command_split_args_with)
        wr = WorkRequest(self._execute_and_send,
            [], {'cmd': cmd, 'args': args, 'match': match, 'mess': mess, 'user': user,
                 'template_name': f._tom_command_template})
        self.thread_pool.put_request(wr)
        if f._tom_command_admin_only:
            self.thread_pool.wait()  # Again wait for the completion before accepting a new command that could generate weird concurrency issues

    def _execute_and_send(self, cmd, args, match, mess, user, template_name=None):
        """Execute a bot command and send output back to the caller

        cmd: The command that was given to the bot (after being expanded)
        args: Arguments given along with cmd
        match: A re.MatchObject if command is coming from a regex-based command, else None
        mess: The message object
        jid: The jid of the person executing the command
        template_name: The names of the template which should be used to render
            html-im output, if any

        """

        def process_reply(reply):
            # integrated templating
            if template_name:
                reply = tenv().get_template(template_name + '.html').render(**reply)
                # mess['content'], mess.html = build_text_html_message_pair(str(reply))
                mess['html'] = reply
                mess['content'] = reply

            # Reply should be all text at this point (See https://github.com/gbin/err/issues/96)
            return reply

        def send_reply(reply):
            if mess['type'] == 'api' or (reply) <= self.MESSAGE_SIZE_LIMIT:
                # FIXME temporary disable reply split
                self.send_simple_reply(mess, reply)
            else:
                mess.session['outbox'] = split_string_after(reply, self.MESSAGE_SIZE_LIMIT)
                mess.send_next()

        commands = self.re_commands if match else self.commands
        try:
            if inspect.isgeneratorfunction(commands[cmd]):
                replies = commands[cmd](mess, match) if match else commands[cmd](mess, args)
                for reply in replies:
                    if reply:
                        send_reply(process_reply(reply))
            else:
                reply = commands[cmd](mess, match) if match else commands[cmd](mess, args)
                if reply:
                    send_reply(process_reply(reply))
        except Exception as e:
            tb = traceback.format_exc()
            logger.exception('An error happened while processing '
                             'a message ("%s") from %s: %s"' %
                             (mess['content'], mess['user'], tb))
            send_reply(self.MSG_ERROR_OCCURRED + ':\n %s' % e)

    def serve_forever(self):
        """
        Must be override

        :raise NotImplementedError:
        """
        raise NotImplementedError

    def create_room(self, rid, rtype):
        """
        create room if not exist

        :param rid: room id
        :param rtype: room type
        :return: Room instance
        """
        if not self.room_manager.get_room(rid):
            room = Room(rid)
            self.room_manager.add_room(room)
            room.rtype = rtype
        else:
            room = self.room_manager.get_room(rid)

        return room

    def inject_commands_from(self, instance_to_inject):
        """

        :param instance_to_inject: instance that contain @botcmd decorated commands, can be `self`
                                    user can use holder.bot.inject_commands_from(self)  to inject commands for current
                                    class instance
        """
        classname = instance_to_inject.__class__.__name__
        for name, value in inspect.getmembers(instance_to_inject, inspect.ismethod):
            if getattr(value, '_tom_command', False):
                commands = self.re_commands if getattr(value, '_tom_re_command') else self.commands
                name = getattr(value, '_tom_command_name')

                if name in commands:
                    f = commands[name]
                    new_name = (classname + '-' + name).lower()
                    self.warn_admins('%s.%s clashes with %s.%s so it has been renamed %s' % (
                        classname, name, type(f.__self__).__name__, f.__name__, new_name))
                    name = new_name
                commands[name] = value

                if getattr(value, '_tom_re_command'):
                    logger.debug('Adding regex command : %s -> %s' % (name, value.__name__))
                    self.re_commands = commands
                else:
                    logger.debug('Adding command : %s -> %s' % (name, value.__name__))
                    self.commands = commands

    def remove_commands_from(self, instance_to_inject):
        for name, value in inspect.getmembers(instance_to_inject, inspect.ismethod):
            if getattr(value, '_tom_command', False):
                name = getattr(value, '_tom_command_name')
                if getattr(value, '_tom_re_command') and name in self.re_commands:
                    del (self.re_commands[name])
                elif not getattr(value, '_tom_re_command') and name in self.commands:
                    del (self.commands[name])

    def warn_admins(self, warning):
        pass
        # for admin in config.admins:
        #     self.send(admin, warning)

    def top_of_help_message(self):
        """Returns a string that forms the top of the help message

        Override this method in derived class if you
        want to add additional help text at the
        beginning of the help message.
        """
        return ""

    def bottom_of_help_message(self):
        """Returns a string that forms the bottom of the help message

        Override this method in derived class if you
        want to add additional help text at the end
        of the help message.
        """
        return ""
Exemplo n.º 4
0
class Backend(object):
    cmd_history = defaultdict(lambda: deque(maxlen=10))

    MSG_ERROR_OCCURRED = '消息处理发生异常'
    MESSAGE_SIZE_LIMIT = config.max_message_size
    MSG_UNKNOWN_COMMAND = 'Unknown command: "%(command)s". ' \
                          'Type "' + config.main_name + ' help" for available commands.'
    MSG_HELP_TAIL = 'Type help <command names> to get more info ' \
                    'about that specific command.'
    MSG_HELP_UNDEFINED_COMMAND = 'That command is not defined.'

    def __init__(self, *args, **kwargs):
        context = zmq.Context(1)
        # 所有插件都应该使用这个socket,否则会自动负载均衡,破坏逻辑
        # TODO put broker_socket in holder
        if not holder.broker_socket:
            self.broker_socket = context.socket(zmq.DEALER)
            self.broker_socket.connect('ipc://{0}/broker.ipc'.format(
                config.ipc_path))
            holder.broker_socket = self.broker_socket
        else:
            self.broker_socket = holder.broker_socket

        self.names = tuple(name.lower() for name in config.names)

        self.room_manager = RoomManager()

        logger.info('Starting to load user...')

        if config.backend_count:
            from ..common.threadpool import ThreadPool

            self.thread_pool = ThreadPool(config.backend_count)
            logger.debug('Created the thread pool {0}'.format(
                self.thread_pool))

        self.commands = {}
        self.re_commands = {}
        self.sessions = {}
        self.admins = []

    def check_command_access(self, message, cmd):
        f = self.commands[cmd] if cmd in self.commands else self.re_commands[
            cmd]
        if f._tom_command_admin_only:
            if not self.auth_admin(message):
                raise ACLViolation(
                    'Command needs admin privileges but authentication failed.'
                )
        return True

    def auth_admin(self, message):
        password = message.get_input('Please give me your pass code.')
        if password == config.admin_pass:
            message.ok('Authentication passed!')
            return True
        else:
            message.warn('Authentication failed!!!')
            return False

    def get_commands(self):
        return self.commands

    def get_re_commands(self):
        return self.re_commands

    def unknown_command(self, mess, cmd, args):
        """ Override the default unknown command behavior
        """
        full_cmd = cmd + ' ' + args.split(' ')[0] if args else None
        if full_cmd:
            part1 = 'Command "%s" / "%s" not found.' % (cmd, full_cmd)
        else:
            part1 = 'Command "%s" not found.' % cmd
        underscore_keys = [m.replace('_', ' ') for m in self.commands.keys()]
        matches = difflib.get_close_matches(cmd, underscore_keys)
        if full_cmd:
            matches.extend(difflib.get_close_matches(full_cmd,
                                                     underscore_keys))
        matches = set(matches)
        if matches:
            return part1 + '\n\nDid you mean "' + config.main_name + ' ' + (
                '" or "' + config.main_name + ' ').join(matches) + '" ?'
        else:
            return part1

    @staticmethod
    def send_simple_reply(message, text):
        message.send(text)

    def get_sender_username(self, mess):
        """
        override for special username convertion
        :param mess: Message object
        """
        # TODO Make it possible to convert from uin to QQ number.
        return mess['user']

    def callback_message(self, mess):
        """
        Needs to return False if we want to stop further treatment
        """
        # Prepare to handle either private chats or group chats

        msg_type = mess['type']
        user = mess['user']
        message_id = mess['message_id']
        content = mess['content']
        username = self.get_sender_username(mess)
        user_cmd_history = self.cmd_history[username]

        # 3 types of QQ chat, there's sess msg_type for non-friend talk, but with crypt issue, don't use it
        if msg_type not in ('buddy', 'group', 'discu', 'api', 'chat',
                            'groupchat'):
            logger.warn("unhandled message msg_type %s" % mess)
            return False

        logger.debug("*** Message_ID = %s" % message_id)
        logger.debug("*** user = %s" % user)
        # logger.debug("*** username = %s" % username)
        logger.debug("*** msg_type = %s" % msg_type)
        logger.debug("*** content = %s" % content)

        # If a message format is not supported (eg. encrypted),
        # txt will be None
        if not content:
            return False

        suppress_cad_not_found = False

        prefixed = False  # Keeps track whether content was prefixed with a bot prefix
        only_check_re_command = False  # Becomes true if content is determed to not be a regular command
        tomatch = content.lower()
        if len(self.names) > 0 and tomatch.startswith(self.names):
            # Yay! We were called by one of our alternate prefixes. Now we just have to find out
            # which one... (And find the longest matching, in case you have 'err' and 'errbot' and
            # someone uses 'errbot', which also matches 'err' but would leave 'bot' to be taken as
            # part of the called command in that case)
            prefixed = True
            longest = 0
            for name in self.names:
                l = len(name)
                if tomatch.startswith(name) and l > longest:
                    longest = l
            logger.debug("Called with alternate name '{}'".format(
                content[:longest]))
            content = content[longest:]

            # Now also remove the separator from the content
            for sep in config.bot_alt_separators:
                # While unlikely, one may have separators consisting of
                # more than one character
                l = len(sep)
                if content[:l] == sep:
                    content = content[l:]
        elif not content.startswith(config.prefix):
            only_check_re_command = True
        if content.startswith(config.prefix):
            content = content[len(config.prefix):]
            prefixed = True

        content = content.strip()
        text_split = content.split(' ')
        cmd = None
        command = None
        args = ''
        if not only_check_re_command:
            if len(text_split) > 1:
                command = (text_split[0] + '_' + text_split[1]).lower()
                if command in self.commands:
                    cmd = command
                    args = ' '.join(text_split[2:])

            if not cmd:
                command = text_split[0].lower()
                args = ' '.join(text_split[1:])
                if command in self.commands:
                    cmd = command
                    if len(text_split) > 1:
                        args = ' '.join(text_split[1:])

            if content == '!':  # we did "!!" so recall the last command
                if len(user_cmd_history):
                    cmd, args = user_cmd_history[-1]
                else:
                    return False  # no command in history
            elif content and content.isdigit(
            ):  # we did "!#" so we recall the specified command
                index = int(content)
                if len(user_cmd_history) >= index:
                    cmd, args = user_cmd_history[-index]
                else:
                    return False  # no command in history

        # Try to match one of the regex commands if the regular commands produced no match
        matched_on_re_command = False
        if not cmd:
            if prefixed:
                commands = self.re_commands
            else:
                commands = {
                    k: self.re_commands[k]
                    for k in self.re_commands
                    if not self.re_commands[k]._tom_command_prefix_required
                }

            for name, func in commands.items():
                match = func._tom_command_re_pattern.search(content)
                if match:
                    logger.debug(
                        u"Matching '{}' against '{}' produced a match".format(
                            content, func._tom_command_re_pattern.pattern))
                    matched_on_re_command = True
                    self._process_command(mess, name, content, match)
                else:
                    logger.debug(
                        u"Matching '{}' against '{}' produced no match".format(
                            content, func._tom_command_re_pattern.pattern))
        if matched_on_re_command:
            return True

        if cmd:
            self._process_command(mess, cmd, args, match=None)
        elif not only_check_re_command:
            logger.debug("Command not found")
            if suppress_cad_not_found:
                logger.debug("Surpressing command not found feedback")
            else:
                reply = self.unknown_command(mess, command, args)
                if reply is None:
                    reply = self.MSG_UNKNOWN_COMMAND % {'command': command}
                if reply:
                    self.send_simple_reply(mess, reply)

        return True

    def _process_command(self, mess, cmd, args, match):
        """Process and execute a bot command"""
        logger.info(u"Processing command {} with parameters '{}'".format(
            cmd, args))

        user = mess['user']
        username = self.get_sender_username(mess)
        user_cmd_history = self.cmd_history[username]

        if (cmd, args) in user_cmd_history:
            user_cmd_history.remove(
                (cmd, args))  # Avoids duplicate history items

        if user not in [base64.encodestring(a) for a in self.admins]:
            try:
                self.check_command_access(mess, cmd)
            except ACLViolation as e:
                mess.error(
                    "You don't have permission to execute this command!")
                return

        f = self.re_commands[cmd] if match else self.commands[cmd]

        if f._tom_command_admin_only:
            self.thread_pool.wait(
            )  # If it is an admin command, wait that the queue is completely depleted so we don't have strange concurrency issues on load/unload/updates etc ...

        if f._tom_command_historize:
            user_cmd_history.append(
                (cmd, args
                 ))  # add it to the history only if it is authorized to be so
            mess.session['history'].append((cmd, args))

        # Don't check for None here as None can be a valid argument to split.
        # '' was chosen as default argument because this isn't a valid argument to split()
        if not match and f._tom_command_split_args_with != '':
            args = args.split(f._tom_command_split_args_with)
        wr = WorkRequest(
            self._execute_and_send, [], {
                'cmd': cmd,
                'args': args,
                'match': match,
                'mess': mess,
                'user': user,
                'template_name': f._tom_command_template
            })
        self.thread_pool.put_request(wr)
        if f._tom_command_admin_only:
            self.thread_pool.wait(
            )  # Again wait for the completion before accepting a new command that could generate weird concurrency issues

    def _execute_and_send(self,
                          cmd,
                          args,
                          match,
                          mess,
                          user,
                          template_name=None):
        """Execute a bot command and send output back to the caller

        cmd: The command that was given to the bot (after being expanded)
        args: Arguments given along with cmd
        match: A re.MatchObject if command is coming from a regex-based command, else None
        mess: The message object
        jid: The jid of the person executing the command
        template_name: The names of the template which should be used to render
            html-im output, if any

        """
        def process_reply(reply):
            # integrated templating
            if template_name:
                reply = tenv().get_template(template_name +
                                            '.html').render(**reply)
                # mess['content'], mess.html = build_text_html_message_pair(str(reply))
                mess['html'] = reply
                mess['content'] = reply

            # Reply should be all text at this point (See https://github.com/gbin/err/issues/96)
            return reply

        def send_reply(reply):
            if mess['type'] == 'api' or (reply) <= self.MESSAGE_SIZE_LIMIT:
                # FIXME temporary disable reply split
                self.send_simple_reply(mess, reply)
            else:
                mess.session['outbox'] = split_string_after(
                    reply, self.MESSAGE_SIZE_LIMIT)
                mess.send_next()

        commands = self.re_commands if match else self.commands
        try:
            if inspect.isgeneratorfunction(commands[cmd]):
                replies = commands[cmd](
                    mess, match) if match else commands[cmd](mess, args)
                for reply in replies:
                    if reply:
                        send_reply(process_reply(reply))
            else:
                reply = commands[cmd](mess, match) if match else commands[cmd](
                    mess, args)
                if reply:
                    send_reply(process_reply(reply))
        except Exception as e:
            tb = traceback.format_exc()
            logger.exception('An error happened while processing '
                             'a message ("%s") from %s: %s"' %
                             (mess['content'], mess['user'], tb))
            send_reply(self.MSG_ERROR_OCCURRED + ':\n %s' % e)

    def serve_forever(self):
        """
        Must be override

        :raise NotImplementedError:
        """
        raise NotImplementedError

    def create_room(self, rid, rtype):
        """
        create room if not exist

        :param rid: room id
        :param rtype: room type
        :return: Room instance
        """
        if not self.room_manager.get_room(rid):
            room = Room(rid)
            self.room_manager.add_room(room)
            room.rtype = rtype
        else:
            room = self.room_manager.get_room(rid)

        return room

    def inject_commands_from(self, instance_to_inject):
        """

        :param instance_to_inject: instance that contain @botcmd decorated commands, can be `self`
                                    user can use holder.bot.inject_commands_from(self)  to inject commands for current
                                    class instance
        """
        classname = instance_to_inject.__class__.__name__
        for name, value in inspect.getmembers(instance_to_inject,
                                              inspect.ismethod):
            if getattr(value, '_tom_command', False):
                commands = self.re_commands if getattr(
                    value, '_tom_re_command') else self.commands
                name = getattr(value, '_tom_command_name')

                if name in commands:
                    f = commands[name]
                    new_name = (classname + '-' + name).lower()
                    self.warn_admins(
                        '%s.%s clashes with %s.%s so it has been renamed %s' %
                        (classname, name, type(
                            f.__self__).__name__, f.__name__, new_name))
                    name = new_name
                commands[name] = value

                if getattr(value, '_tom_re_command'):
                    logger.debug('Adding regex command : %s -> %s' %
                                 (name, value.__name__))
                    self.re_commands = commands
                else:
                    logger.debug('Adding command : %s -> %s' %
                                 (name, value.__name__))
                    self.commands = commands

    def remove_commands_from(self, instance_to_inject):
        for name, value in inspect.getmembers(instance_to_inject,
                                              inspect.ismethod):
            if getattr(value, '_tom_command', False):
                name = getattr(value, '_tom_command_name')
                if getattr(value,
                           '_tom_re_command') and name in self.re_commands:
                    del (self.re_commands[name])
                elif not getattr(value,
                                 '_tom_re_command') and name in self.commands:
                    del (self.commands[name])

    def warn_admins(self, warning):
        pass
        # for admin in config.admins:
        #     self.send(admin, warning)

    def top_of_help_message(self):
        """Returns a string that forms the top of the help message

        Override this method in derived class if you
        want to add additional help text at the
        beginning of the help message.
        """
        return ""

    def bottom_of_help_message(self):
        """Returns a string that forms the bottom of the help message

        Override this method in derived class if you
        want to add additional help text at the end
        of the help message.
        """
        return ""