コード例 #1
0
ファイル: run.py プロジェクト: jacklqiu/zulip
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = RestrictedClient(client)

    message_handler = lib_module.handler_class()

    class StateHandler(object):
        def __init__(self):
            self.state = None

        def set_state(self, state):
            self.state = state

        def get_state(self):
            return self.state

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def handle_message(message):
        logging.info('waiting for next message')
        if message_handler.triage_message(message=message):
            message_handler.handle_message(message=message,
                                           client=restricted_client,
                                           state_handler=state_handler)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #2
0
ファイル: run.py プロジェクト: TijeeCorp/zulip
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = RestrictedClient(client)

    message_handler = lib_module.handler_class()

    class StateHandler(object):
        def __init__(self):
            self.state = None

        def set_state(self, state):
            self.state = state

        def get_state(self):
            return self.state

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def handle_message(message):
        logging.info('waiting for next message')
        if message_handler.triage_message(message=message):
            message_handler.handle_message(
                message=message,
                client=restricted_client,
                state_handler=state_handler
                )

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #3
0
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = BotHandlerApi(client)

    message_handler = lib_module.handler_class()

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def extract_query_without_mention(message, client):
        """
        If the bot is the first @mention in the message, then this function returns
        the message with the bot's @mention removed.  Otherwise, it returns None.
        """
        bot_mention = r'^@(\*\*{0}\*\*)'.format(client.full_name)
        start_with_mention = re.compile(bot_mention).match(message['content'])
        if start_with_mention is None:
            return None
        query_without_mention = message['content'][len(start_with_mention.
                                                       group()):]
        return query_without_mention.lstrip()

    def is_private(message, client):
        # bot will not reply if the sender name is the same as the bot name
        # to prevent infinite loop
        if message['type'] == 'private':
            return client.full_name != message['sender_full_name']
        return False

    def handle_message(message):
        logging.info('waiting for next message')

        # is_mentioned is true if the bot is mentioned at ANY position (not necessarily
        # the first @mention in the message).
        is_mentioned = message['is_mentioned']
        is_private_message = is_private(message, restricted_client)

        # Strip at-mention botname from the message
        if is_mentioned:
            # message['content'] will be None when the bot's @-mention is not at the beginning.
            # In that case, the message shall not be handled.
            message['content'] = extract_query_without_mention(
                message=message, client=restricted_client)
            if message['content'] is None:
                return

        if is_private_message or is_mentioned:
            message_handler.handle_message(message=message,
                                           client=restricted_client,
                                           state_handler=state_handler)

    signal.signal(signal.SIGINT, exit_gracefully)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #4
0
ファイル: bot_lib.py プロジェクト: christi3k/zulip
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = BotHandlerApi(client)

    message_handler = lib_module.handler_class()

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def extract_query_without_mention(message, client):
        """
        If the bot is the first @mention in the message, then this function returns
        the message with the bot's @mention removed.  Otherwise, it returns None.
        """
        bot_mention = r'^@(\*\*{0}\*\*)'.format(client.full_name)
        start_with_mention = re.compile(bot_mention).match(message['content'])
        if start_with_mention is None:
            return None
        query_without_mention = message['content'][len(start_with_mention.group()):]
        return query_without_mention.lstrip()

    def is_private(message, client):
        # bot will not reply if the sender name is the same as the bot name
        # to prevent infinite loop
        if message['type'] == 'private':
            return client.full_name != message['sender_full_name']
        return False

    def handle_message(message):
        logging.info('waiting for next message')

        # is_mentioned is true if the bot is mentioned at ANY position (not necessarily
        # the first @mention in the message).
        is_mentioned = message['is_mentioned']
        is_private_message = is_private(message, restricted_client)

        # Strip at-mention botname from the message
        if is_mentioned:
            # message['content'] will be None when the bot's @-mention is not at the beginning.
            # In that case, the message shall not be handled.
            message['content'] = extract_query_without_mention(message=message, client=restricted_client)
            if message['content'] is None:
                return

        if is_private_message or is_mentioned:
            message_handler.handle_message(
                message=message,
                client=restricted_client,
                state_handler=state_handler
            )

    signal.signal(signal.SIGINT, exit_gracefully)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #5
0
def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name):
    # type: (Any, bool, str) -> Any
    #
    # lib_module is of type Any, since it can contain any bot's
    # handler class. Eventually, we want bot's handler classes to
    # inherit from a common prototype specifying the handle_message
    # function.
    #
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file,
                    client="Zulip{}Bot".format(bot_name.capitalize()))
    bot_dir = os.path.dirname(lib_module.__file__)
    restricted_client = ExternalBotHandler(client, bot_dir)

    message_handler = lib_module.handler_class()
    if hasattr(message_handler, 'initialize'):
        message_handler.initialize(bot_handler=restricted_client)

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def handle_message(message):
        # type: (Dict[str, Any]) -> None
        logging.info('waiting for next message')

        # is_mentioned is true if the bot is mentioned at ANY position (not necessarily
        # the first @mention in the message).
        is_mentioned = message['is_mentioned']
        is_private_message = is_private_message_from_another_user(
            message, restricted_client.user_id)

        # Strip at-mention botname from the message
        if is_mentioned:
            # message['content'] will be None when the bot's @-mention is not at the beginning.
            # In that case, the message shall not be handled.
            message['content'] = extract_query_without_mention(
                message=message, client=restricted_client)
            if message['content'] is None:
                return

        if is_private_message or is_mentioned:
            message_handler.handle_message(message=message,
                                           bot_handler=restricted_client,
                                           state_handler=state_handler)

    signal.signal(signal.SIGINT, exit_gracefully)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #6
0
ファイル: run.py プロジェクト: namratab94/zulip
def run_message_handler_for_bot(lib_module, quiet):
    # Make sure you set up your ~/.zuliprc
    client = Client()
    restricted_client = RestrictedClient(client)

    message_handler = lib_module.handler_class()

    if not quiet:
        print(message_handler.usage())

    def handle_message(message):
        logging.info('waiting for next message')
        if message_handler.triage_message(message=message):
            message_handler.handle_message(
                message=message,
                client=restricted_client)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #7
0
ファイル: run.py プロジェクト: ahmadassaf/Zulip
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = RestrictedClient(client)

    message_handler = lib_module.handler_class()

    if not quiet:
        print(message_handler.usage())

    def handle_message(message):
        logging.info('waiting for next message')
        if message_handler.triage_message(message=message):
            message_handler.handle_message(
                message=message,
                client=restricted_client)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #8
0
ファイル: bot_lib.py プロジェクト: vabs22/python-zulip-api
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # type: (Any, bool, str) -> Any
    #
    # lib_module is of type Any, since it can contain any bot's
    # handler class. Eventually, we want bot's handler classes to
    # inherit from a common prototype specifying the handle_message
    # function.
    #
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = ExternalBotHandler(client)

    message_handler = lib_module.handler_class()
    if hasattr(message_handler, 'initialize'):
        message_handler.initialize(bot_handler=restricted_client)

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def extract_query_without_mention(message, client):
        # type: (Dict[str, Any], ExternalBotHandler) -> str
        """
        If the bot is the first @mention in the message, then this function returns
        the message with the bot's @mention removed.  Otherwise, it returns None.
        """
        bot_mention = r'^@(\*\*{0}\*\*)'.format(client.full_name)
        start_with_mention = re.compile(bot_mention).match(message['content'])
        if start_with_mention is None:
            return None
        query_without_mention = message['content'][len(start_with_mention.
                                                       group()):]
        return query_without_mention.lstrip()

    def is_private(message, client):
        # type: (Dict[str, Any], ExternalBotHandler) -> bool
        # bot will not reply if the sender name is the same as the bot name
        # to prevent infinite loop
        if message['type'] == 'private':
            return client.full_name != message['sender_full_name']
        return False

    def handle_message(message):
        # type: (Dict[str, Any]) -> None
        logging.info('waiting for next message')

        # is_mentioned is true if the bot is mentioned at ANY position (not necessarily
        # the first @mention in the message).
        is_mentioned = message['is_mentioned']
        is_private_message = is_private(message, restricted_client)

        # Strip at-mention botname from the message
        if is_mentioned:
            # message['content'] will be None when the bot's @-mention is not at the beginning.
            # In that case, the message shall not be handled.
            message['content'] = extract_query_without_mention(
                message=message, client=restricted_client)
            if message['content'] is None:
                return

        if is_private_message or is_mentioned:
            message_handler.handle_message(message=message,
                                           bot_handler=restricted_client,
                                           state_handler=state_handler)

    signal.signal(signal.SIGINT, exit_gracefully)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #9
0
class ZulipBackend(ErrBot):
    def __init__(self, config):
        super().__init__(config)
        config.MESSAGE_SIZE_LIMIT = ZULIP_MESSAGE_SIZE_LIMIT

        self.identity = config.BOT_IDENTITY
        for key in ('email', 'key', 'site'):
            if key not in self.identity:
                log.fatal(
                    "You need to supply the key `{}` for me to use. `{key}` and its value "
                    "can be found in your bot's `zuliprc` config file.".format(
                        key))
                sys.exit(1)

        compact = config.COMPACT_OUTPUT if hasattr(config,
                                                   'COMPACT_OUTPUT') else False
        enable_format('text', TEXT_CHRS, borders=not compact)
        self.client = Client(email=self.identity['email'],
                             api_key=self.identity['key'],
                             site=self.identity['site'])

    def serve_once(self):
        self.bot_identifier = self.build_identifier(self.client.email)
        log.info("Initializing connection")
        self.client.ensure_session()
        log.info("Connected")
        self.reset_reconnection_count()
        self.connect_callback()
        try:
            self.client.call_on_each_message(self._handle_message)
        except KeyboardInterrupt:
            log.info("Interrupt received, shutting down..")
            return True  # True means shutdown was requested.
        except Exception:
            log.exception("Error reading from Zulip updates stream.")
            raise
        finally:
            log.debug("Triggering disconnect callback.")
            self.disconnect_callback()

    def _handle_message(self, message):
        """
        Handles incoming messages.
        In Zulip, there are three types of messages: Private messages, Private group messages,
        and Stream messages. This plugin handles Group PMs as normal PMs between the bot and the
        user. Stream messages are handled as messages to rooms.
        """
        if not message['content']:
            log.warning("Unhandled message type (not a text message) ignored")
            return

        message_instance = self.build_message(message['content'])
        if message['type'] == 'private':
            message_instance.frm = ZulipPerson(
                id=message['sender_email'],
                full_name=message['sender_full_name'],
                emails=[message['sender_email']],
                client=message['client'])
            message_instance.to = ZulipPerson(
                id=message['sender_email'],
                full_name=','.join([
                    recipient['full_name']
                    for recipient in message['display_recipient']
                ]),
                emails=[
                    recipient['email']
                    for recipient in message['display_recipient']
                ],
                client=None)
        elif message['type'] == 'stream':
            room = ZulipRoom(id=message['display_recipient'],
                             title=message['display_recipient'],
                             subject=message['subject'])
            message_instance.frm = ZulipRoomOccupant(
                id=message['sender_email'],
                full_name=message['sender_full_name'],
                emails=[message['sender_email']],
                client=message['client'],
                room=room)
            message_instance.to = room
            message_instance.extras['url'] = (
                '{site}/#narrow/stream/{stream}'
                '/subject/{subject}/near/{mid}').format(
                    site=self.identity['site'],
                    stream=quote(room.title),
                    subject=quote(room.subject),
                    mid=message['id'],
                )
        else:
            raise ValueError("Invalid message type `{}`.".format(
                message['type']))
        self.callback_message(message_instance)

    def send_message(self, msg):
        super().send_message(msg)
        msg_data = {
            'content': msg.body,
        }
        if isinstance(msg.to, ZulipRoom):
            msg_data['type'] = 'stream'
            msg_data['subject'] = msg.to.subject
            msg_data['to'] = msg.to.title

        elif isinstance(msg.to, ZulipPerson):
            if isinstance(msg.to, ZulipRoomOccupant):
                msg_data['type'] = 'stream'
                msg_data['subject'] = msg.to.room.subject
                msg_data['to'] = msg.to.room.title
            else:
                msg_data['type'] = 'private'
                msg_data['to'] = msg.to.emails
        else:
            raise ValueError("Invalid message recipient of type {}".format(
                type(msg.to).__name__))
        try:
            self.client.send_message(msg_data)
        except Exception:
            log.exception(
                "An exception occurred while trying to send the following message "
                "to %s: %s" % (msg.to.id, msg.body))
            raise

    def is_from_self(self, msg):
        return msg.frm.aclattr == self.client.email

    def change_presence(self, status: str = ONLINE, message: str = '') -> None:
        # At this time, Zulip doesn't support active presence change.
        pass

    def build_identifier(self, txtrep):
        return ZulipPerson(id=txtrep,
                           full_name=txtrep,
                           emails=[txtrep],
                           client=self.client)

    def build_reply(self, msg, text=None, private=False, threaded=False):
        response = self.build_message(text)
        response.to = msg.to
        return response

    @property
    def mode(self):
        return 'zulip'

    def query_room(self, room):
        return ZulipRoom(title=room, client=self.client)

    def rooms(self):
        result = parse_query_result(self.client.list_subscriptions())
        return [
            ZulipRoom(title=subscription['name'], id=subscription['name'])
            for subscription in result['subscriptions']
        ]

    def prefix_groupchat_reply(self, message, identifier):
        super().prefix_groupchat_reply(message, identifier)
        message.body = '@**{0}** {1}'.format(identifier.full_name,
                                             message.body)

    def _zulip_upload_stream(self, stream):
        """Perform upload defined in a stream."""
        try:
            stream.accept()
            result = self.client.upload_file(stream.raw)
            if result['result'] == 'success':
                message_instance = self.build_message("[{}]({})".format(
                    stream.name, result['uri']))
                message_instance.to = stream.identifier
                self.send_message(message_instance)
                stream.success()

            else:
                stream.error()
        except Exception:
            log.exception("Upload of {0} to {1} failed.".format(
                stream.name, stream.identifier))

    def send_stream_request(self,
                            identifier,
                            fsource,
                            name='file',
                            size=None,
                            stream_type=None):
        """Starts a file transfer.

        :param identifier: ZulipPerson or ZulipRoom
            Identifier of the Person or Room to send the stream to.

        :param fsource: str, dict or binary data
            File URL or binary content from a local file.
            Optionally a dict with binary content plus metadata can be given.
            See `stream_type` for more details.

        :param name: str, optional
            Name of the file. Not sure if this works always.

        :param size: str, optional
            Size of the file obtained with os.path.getsize.
            This is only used for debug logging purposes.

        :param stream_type: str, optional
            Type of the stream. Choices: 'document', 'photo', 'audio', 'video', 'sticker', 'location'.
            Right now used for debug logging purposes only.

        :return stream: str or Stream
            If `fsource` is str will return str, else return Stream.
        """
        def _metadata(fsource):
            if isinstance(fsource, dict):
                return fsource.pop('content'), fsource
            else:
                return fsource, None

        def _is_valid_url(url):
            try:
                from urlparse import urlparse
            except Exception:
                from urllib.parse import urlparse

            return bool(urlparse(url).scheme)

        content, meta = _metadata(fsource)
        if isinstance(content, str):
            if not _is_valid_url(content):
                raise ValueError("Not valid URL: {}".format(content))
            else:
                raise NotImplementedError(
                    "The Zulip backend does not yet support URL stream requests."
                )
        else:
            stream = Stream(identifier, content, name, size, stream_type)
            log.debug(
                "Requesting upload of {0} to {1} (size hint: {2}, stream type: {3})"
                .format(name, identifier, size, stream_type))
            self.thread_pool.apply_async(self._zulip_upload_stream, (stream, ))

        return stream
コード例 #10
0
ファイル: bot_lib.py プロジェクト: aakash-cr7/zulip
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = BotHandlerApi(client)

    message_handler = lib_module.handler_class()

    class StateHandler(object):
        def __init__(self):
            self.state = None

        def set_state(self, state):
            self.state = state

        def get_state(self):
            return self.state

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def extract_message_if_mentioned(message, client):
        bot_mention = r'^@(\*\*{0}\*\*\s|{0}\s)(?=.*)'.format(client.full_name)
        start_with_mention = re.compile(bot_mention).match(message['content'])
        if start_with_mention:
            query = message['content'][len(start_with_mention.group()):]
            return query
        else:
            bot_response = 'Please mention me first, then type the query.'
            if message['type'] == 'private':
                client.send_message(dict(
                    type='private',
                    to=message['sender_email'],
                    content=bot_response,
                ))
            else:
                client.send_message(dict(
                    type='stream',
                    to=message['display_recipient'],
                    subject=message['subject'],
                    content=bot_response,
                ))
            return None

    def is_private(message, client):
        # bot will not reply if the sender name is the same as the bot name
        # to prevent infinite loop
        if message['type'] == 'private':
            return client.full_name != message['sender_full_name']
        return False

    def handle_message(message):
        logging.info('waiting for next message')

        is_mentioned = message['is_mentioned']
        is_private_message = is_private(message, restricted_client)

        # Strip at-mention botname from the message
        if is_mentioned:
            message['content'] = extract_message_if_mentioned(message=message, client=restricted_client)
            if message['content'] is None:
                return

        if is_private_message or is_mentioned:
            message_handler.handle_message(
                message=message,
                client=restricted_client,
                state_handler=state_handler
            )

    signal.signal(signal.SIGINT, exit_gracefully)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #11
0
ファイル: bot_lib.py プロジェクト: JamesLinus/zulip
def run_message_handler_for_bot(lib_module, quiet, config_file):
    # type: (Any, bool, str) -> Any
    #
    # lib_module is of type Any, since it can contain any bot's
    # handler class. Eventually, we want bot's handler classes to
    # inherit from a common prototype specifying the handle_message
    # function.
    #
    # Make sure you set up your ~/.zuliprc
    client = Client(config_file=config_file)
    restricted_client = ExternalBotHandler(client)

    message_handler = lib_module.handler_class()
    if hasattr(message_handler, 'initialize'):
        message_handler.initialize(bot_handler=restricted_client)

    state_handler = StateHandler()

    if not quiet:
        print(message_handler.usage())

    def extract_query_without_mention(message, client):
        # type: (Dict[str, Any], ExternalBotHandler) -> str
        """
        If the bot is the first @mention in the message, then this function returns
        the message with the bot's @mention removed.  Otherwise, it returns None.
        """
        bot_mention = r'^@(\*\*{0}\*\*)'.format(client.full_name)
        start_with_mention = re.compile(bot_mention).match(message['content'])
        if start_with_mention is None:
            return None
        query_without_mention = message['content'][len(start_with_mention.group()):]
        return query_without_mention.lstrip()

    def is_private(message, client):
        # type: (Dict[str, Any], ExternalBotHandler) -> bool
        # bot will not reply if the sender name is the same as the bot name
        # to prevent infinite loop
        if message['type'] == 'private':
            return client.full_name != message['sender_full_name']
        return False

    def handle_message(message):
        # type: (Dict[str, Any]) -> None
        logging.info('waiting for next message')

        # is_mentioned is true if the bot is mentioned at ANY position (not necessarily
        # the first @mention in the message).
        is_mentioned = message['is_mentioned']
        is_private_message = is_private(message, restricted_client)

        # Strip at-mention botname from the message
        if is_mentioned:
            # message['content'] will be None when the bot's @-mention is not at the beginning.
            # In that case, the message shall not be handled.
            message['content'] = extract_query_without_mention(message=message, client=restricted_client)
            if message['content'] is None:
                return

        if is_private_message or is_mentioned:
            message_handler.handle_message(
                message=message,
                bot_handler=restricted_client,
                state_handler=state_handler
            )

    signal.signal(signal.SIGINT, exit_gracefully)

    logging.info('starting message handling...')
    client.call_on_each_message(handle_message)
コード例 #12
0
class ZulipBot(object):
    def __init__(self, name, stream):
        self._client = Client()
        self._name = name
        self._self_short_name = u'{name}-bot'.format(name=self._name)
        self._self_mention = self._format_mention(self._name)
        self._stream = stream
        self._topic_router = {}
        self._command_handlers = {
            'hello': self._hello_command_handler,
            'help': self._help_command_handler,
        }

    def run(self):
        self._client.call_on_each_message(self._message_handler)

    def send_public_message(self, content, topic, stream=None):
        message = {
            'type': 'stream',
            'to': stream or self._stream,
            'content': content,
            'topic': topic,
        }
        self._client.send_message(message)

    def send_private_message(self, content, address):
        message = {'type': 'private', 'to': address, 'content': content}
        self._client.send_message(message)

    def send_reply(self, content, message):
        """Send content as reply to message."""
        if self._is_private_message(message):
            self.send_private_message(content, message.get('sender_email'))
        else:
            self.send_public_message(content, message.get('subject'),
                                     message.get('stream'))

    @staticmethod
    def _format_mention(name):
        return u'@**{name}**'.format(name=name)

    def _is_self_sent(self, message):
        return message.get('sender_short_name') == self._self_short_name

    def _is_self_mention(self, message):
        return message.get('content').find(self._self_mention) != -1

    def _strip_self_mention(self, content):
        return content.replace(self._self_mention, '')

    @staticmethod
    def _is_private_message(message):
        return message.get('type') == 'private'

    def _message_handler(self, message):
        is_relevant_message = not self._is_self_sent(message) and (
            self._is_self_mention(message)
            or self._is_private_message(message))
        if is_relevant_message:
            topic = message.get('subject')
            self._topic_router.get(topic, self._default_router)(message)

    def _default_router(self, message):
        commands = [
            cmd for cmd in self._strip_self_mention(message.get(
                'content')).split(' ') if cmd
        ]
        self._command_handlers.get(commands[0],
                                   self._default_command_handler)(commands[1:],
                                                                  message)

    def _default_command_handler(self, subcommands, message):
        reply = (u'I did not understand the message:\n'
                 u'```quote\n'
                 u'{content}\n'
                 u'```\n'
                 u'For a list of recognized commands, send `help`.').format(
                     content=message.get('content'))
        self.send_reply(reply, message)

    def _help_command_handler(self, subcommands, message):
        """Get help about recognized commands."""
        if subcommands and subcommands[0] in self._command_handlers:
            command = subcommands[0]
            reply = u'*{command}*: {desc}'.format(
                command=command, desc=self._command_handlers[command].__doc__)
        else:
            reply = [(u'**Supported commands**\n'
                      u'\n'
                      u'Command|Description\n'
                      u'-------|-----------')]
            reply.extend(
                u'{cmd}|{desc}'.format(cmd=k, desc=v.__doc__.split('\n')[0])
                for (k, v) in iteritems(self._command_handlers))
            reply.append(u'\nSend `help {command}` for more information.')
            reply = '\n'.join(reply)

        self.send_reply(reply, message)

    def _hello_command_handler(self, subcommands, message):
        """Say hello."""
        sender = message.get('sender_short_name')
        reply = u'Hi {mention} :wave:'.format(
            mention=self._format_mention(sender))
        self.send_reply(reply, message)