diff --git a/telegram/ext/commandhandler.py b/telegram/ext/commandhandler.py index 1387c18d662..d98b36eeda3 100644 --- a/telegram/ext/commandhandler.py +++ b/telegram/ext/commandhandler.py @@ -47,6 +47,7 @@ class CommandHandler(Handler): callback (:obj:`callable`): The callback function for this handler. filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these Filters. + description (:obj:`str`, optional): Description of what this command does. allow_edited (:obj:`bool`): Determines Whether the handler should also accept edited messages. pass_args (:obj:`bool`): Determines whether the handler should be passed @@ -85,6 +86,9 @@ class CommandHandler(Handler): :class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in :class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). + description (:obj:`str`, optional): Description of what this command does. Set, if you + register the bots commands with via :meth:`telegram.ext.Dispatcher.set_commands`. Must + be 3-256 characters. allow_edited (:obj:`bool`, optional): Determines whether the handler should also accept edited messages. Default is ``False``. DEPRECATED: Edited is allowed by default. To change this behavior use @@ -124,7 +128,8 @@ def __init__(self, pass_update_queue=False, pass_job_queue=False, pass_user_data=False, - pass_chat_data=False): + pass_chat_data=False, + description=None): super(CommandHandler, self).__init__( callback, pass_update_queue=pass_update_queue, @@ -132,6 +137,10 @@ def __init__(self, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data) + self.description = description + if self.description is not None and not 3 <= len(self.description) <= 256: + raise ValueError('Command description is not valid.') + if isinstance(command, string_types): self.command = [command.lower()] else: diff --git a/telegram/ext/dispatcher.py b/telegram/ext/dispatcher.py index 4e60c0e44bd..e438f95845d 100644 --- a/telegram/ext/dispatcher.py +++ b/telegram/ext/dispatcher.py @@ -31,7 +31,7 @@ from future.builtins import range -from telegram import TelegramError, Update +from telegram import TelegramError, Update, BotCommand from telegram.ext.handler import Handler from telegram.ext.callbackcontext import CallbackContext from telegram.utils.deprecate import TelegramDeprecationWarning @@ -529,3 +529,72 @@ def dispatch_error(self, update, error): else: self.logger.exception( 'No error handlers are registered, logging exception.', exc_info=error) + + def set_commands(self, add=True, update=True, delete=True, skip_empty=False, + alphabetical=False): + """ + Convenience method or registering bot commands with Botfather. Uses + :meth:`telegram.Bot.set_my_commands` for all :class:`telegram.ext.CommandHandler` s + registered with this dispatcher. + + Note: + While this method already allows for some customization, it is to be understood as a + simple helper for the most common use cases. If you need to set your bots commands in + a specific manner not covered by this method, use :meth:`telegram.Bot.set_my_commands` + directly. + + Args: + add (:obj:`bool`, optional): If there are commands set for the bot and :attr:`add` is + :obj:`False`, no new commands are added. Defaults to :obj:`True`. + update (:obj:`bool`, optional): Whether to override descriptions of commands already + set. Defaults to :obj:`True`. + delete (:obj:`bool`, optional): Whether to delete commands, which are currently set but + don't have a corresponding :class:`telegram.ext.CommandHandler` listening for that + command. Defaults to :obj:`True`. + skip_empty (:obj:`bool`, optional): Whether to skip + :class:`telegram.ext.CommandHandler` s with an empty description. If :obj:`False`, + empty descriptions will be replaced by ``Command ""``. + alphabetical (:obj:`bool`, optional): Whether to sort commands by alphabetical order. + If :obj:`False`, commands are sorted in the same order, in with :meth:`add_handler` + is invoked. If :attr:`delete` is :obj:`False`, commands without corresponding + handler will be last. Defaults to :obj:`False`. + """ + set_bot_commands = self.bot.get_my_commands() + + dp_bot_commands = [] + for group in self.handlers: + for handler in self.handlers[group]: + if hasattr(handler, 'command') and hasattr(handler, 'description'): + for cmd in handler.command: + if handler.description or not skip_empty: + desc = handler.description or 'Command "{}"'.format(cmd) + dp_bot_commands.extend([BotCommand(cmd, desc)]) + + if not set_bot_commands: + new_bot_commands = [bc for bc in dp_bot_commands] + else: + if add: + new_bot_commands = [bc for bc in dp_bot_commands] + new_commands = [nbc.command for nbc in new_bot_commands] + new_bot_commands.extend([bc for bc in set_bot_commands + if bc.command not in new_commands]) + else: + new_bot_commands = [bc for bc in set_bot_commands] + + if delete: + dp_commands = [bc.command for bc in dp_bot_commands] + new_bot_commands = [bc for bc in new_bot_commands if bc.command in dp_commands] + + if update: + for bot_command in new_bot_commands: + old_bc = next((bc for bc in set_bot_commands + if bc.command == bot_command.command), None) + new_bc = next((bc for bc in dp_bot_commands + if bc.command == bot_command.command), None) + if new_bc and old_bc and new_bc.description != old_bc.description: + bot_command.description = new_bc.description + + if alphabetical: + new_bot_commands = sorted(new_bot_commands, key=lambda c: c.command) + + self.bot.set_my_commands(new_bot_commands) diff --git a/tests/test_commandhandler.py b/tests/test_commandhandler.py index b37c76594c2..0470190507d 100644 --- a/tests/test_commandhandler.py +++ b/tests/test_commandhandler.py @@ -170,6 +170,19 @@ def test_basic(self, dp, command): assert not is_match(handler, make_command_update('/not{}'.format(command[1:]))) assert not is_match(handler, make_command_update('not {} at start'.format(command))) + def test_description(self): + handler = CommandHandler('start', callback=None) + assert handler.description is None + + handler = CommandHandler('start', callback=None, description='Here is a description') + assert handler.description == 'Here is a description' + + with pytest.raises(ValueError, match='description'): + CommandHandler('start', callback=None, description='ds') + + with pytest.raises(ValueError, match='description'): + CommandHandler('start', callback=None, description=257 * '.') + @pytest.mark.parametrize('cmd', ['way_too_longcommand1234567yes_way_toooooooLong', 'ïñválídletters', 'invalid #&* chars'], diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index b3e1c3eb32b..f988197ee7b 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -534,3 +534,145 @@ def callback(update, context): assert cdp.persistence.test_flag_bot_data assert not cdp.persistence.test_flag_user_data assert cdp.persistence.test_flag_chat_data + + def test_set_commands_simple(self, dp): + dp.bot.set_my_commands([]) + + handler_1 = CommandHandler('start1', None, description='One') + handler_2 = CommandHandler('start2', None, description='Two') + handler_3 = CommandHandler('start3', None, description='Three') + handler_4 = CommandHandler('start4', None, description='Four') + dp.add_handler(handler_1) + dp.add_handler(handler_2) + dp.add_handler(handler_3) + dp.add_handler(handler_4) + + dp.set_commands() + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 4 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two' + assert bot_commands[2].command == 'start3' + assert bot_commands[2].description == 'Three' + assert bot_commands[3].command == 'start4' + assert bot_commands[3].description == 'Four' + + handler_1.description = 'One1' + handler_2.description = 'Two2' + handler_3.description = 'Three3' + handler_4.description = 'Four4' + + dp.set_commands(update=True) + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 4 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One1' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two2' + assert bot_commands[2].command == 'start3' + assert bot_commands[2].description == 'Three3' + assert bot_commands[3].command == 'start4' + assert bot_commands[3].description == 'Four4' + + def test_set_commands_alphabetical(self, dp): + dp.bot.set_my_commands([]) + + dp.add_handler(CommandHandler('start4', None, description='Four')) + dp.add_handler(CommandHandler('start3', None, description='Three')) + dp.add_handler(CommandHandler('start2', None, description='Two')) + dp.add_handler(CommandHandler('start1', None, description='One')) + + dp.set_commands(alphabetical=True) + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 4 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two' + assert bot_commands[2].command == 'start3' + assert bot_commands[2].description == 'Three' + assert bot_commands[3].command == 'start4' + assert bot_commands[3].description == 'Four' + + def test_set_commands_update_add(self, dp): + dp.bot.set_my_commands([]) + + handler_1 = CommandHandler('start1', None, description='One') + handler_2 = CommandHandler('start2', None, description='Two') + handler_3 = CommandHandler('start3', None, description='Three') + handler_4 = CommandHandler('start4', None, description='Four') + dp.add_handler(handler_1) + dp.add_handler(handler_2) + + dp.set_commands() + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 2 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two' + + handler_1.description = 'One1' + handler_2.description = 'Two2' + dp.add_handler(handler_3) + dp.add_handler(handler_4) + + dp.set_commands(add=False, update=False) + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 2 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two' + + dp.set_commands(add=False, update=True) + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 2 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One1' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two2' + + def test_set_command_delete(self, dp): + dp.bot.set_my_commands([('one', 'desc1'), ('two', 'desc2')]) + + handler_1 = CommandHandler('start1', None, description='One') + handler_2 = CommandHandler('start2', None, description='Two') + dp.add_handler(handler_1) + dp.add_handler(handler_2) + + dp.set_commands(delete=False) + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 4 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two' + assert bot_commands[2].command == 'one' + assert bot_commands[2].description == 'desc1' + assert bot_commands[3].command == 'two' + assert bot_commands[3].description == 'desc2' + + dp.set_commands(delete=True) + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 2 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'One' + assert bot_commands[1].command == 'start2' + assert bot_commands[1].description == 'Two' + + def test_set_command_skip_empty(self, dp): + dp.bot.set_my_commands([]) + dp.add_handler(CommandHandler('start1', None, description=None)) + + dp.set_commands(skip_empty=True) + bot_commands = dp.bot.get_my_commands() + assert bot_commands == [] + + dp.set_commands(skip_empty=False) + bot_commands = dp.bot.get_my_commands() + assert len(bot_commands) == 1 + assert bot_commands[0].command == 'start1' + assert bot_commands[0].description == 'Command "start1"'