diff --git a/docs/source/telegram.ext.prefixhandler.rst b/docs/source/telegram.ext.prefixhandler.rst new file mode 100644 index 000000000..18fb2be14 --- /dev/null +++ b/docs/source/telegram.ext.prefixhandler.rst @@ -0,0 +1,6 @@ +telegram.ext.PrefixHandler +=========================== + +.. autoclass:: telegram.ext.PrefixHandler + :members: + :show-inheritance: diff --git a/docs/source/telegram.ext.rst b/docs/source/telegram.ext.rst index aaad83d4f..4c7230ad5 100644 --- a/docs/source/telegram.ext.rst +++ b/docs/source/telegram.ext.rst @@ -25,6 +25,7 @@ Handlers telegram.ext.inlinequeryhandler telegram.ext.messagehandler telegram.ext.precheckoutqueryhandler + telegram.ext.prefixhandler telegram.ext.regexhandler telegram.ext.shippingqueryhandler telegram.ext.stringcommandhandler diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index b83814969..c17cae4aa 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -28,7 +28,7 @@ from .jobqueue import JobQueue, Job from .updater import Updater from .callbackqueryhandler import CallbackQueryHandler from .choseninlineresulthandler import ChosenInlineResultHandler -from .commandhandler import CommandHandler +from .commandhandler import CommandHandler, PrefixHandler from .inlinequeryhandler import InlineQueryHandler from .messagehandler import MessageHandler from .filters import BaseFilter, Filters @@ -48,4 +48,4 @@ __all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler', 'StringRegexHandler', 'TypeHandler', 'ConversationHandler', 'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue', 'DispatcherHandlerStop', 'run_async', 'CallbackContext', 'BasePersistence', - 'PicklePersistence', 'DictPersistence') + 'PicklePersistence', 'DictPersistence', 'PrefixHandler') diff --git a/telegram/ext/callbackcontext.py b/telegram/ext/callbackcontext.py index e36f877c4..bf502dc54 100644 --- a/telegram/ext/callbackcontext.py +++ b/telegram/ext/callbackcontext.py @@ -37,9 +37,9 @@ class CallbackContext(object): regex-supported handler, this will contain the object returned from ``re.match(pattern, string)``. args (List[:obj:`str`], optional): Arguments passed to a command if the associated update - is handled by :class:`telegram.ext.CommandHandler` or - :class:`telegram.ext.StringCommandHandler`. It contains a list of the words in the text - after the command, using any whitespace string as a delimiter. + is handled by :class:`telegram.ext.CommandHandler`, :class:`telegram.ext.PrefixHandler` + or :class:`telegram.ext.StringCommandHandler`. It contains a list of the words in the + text after the command, using any whitespace string as a delimiter. error (:class:`telegram.TelegramError`, optional): The Telegram error that was raised. Only present when passed to a error handler registered with :attr:`telegram.ext.Dispatcher.add_error_handler`. diff --git a/telegram/ext/commandhandler.py b/telegram/ext/commandhandler.py index d25daf17d..508661730 100644 --- a/telegram/ext/commandhandler.py +++ b/telegram/ext/commandhandler.py @@ -16,10 +16,12 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -"""This module contains the CommandHandler class.""" +"""This module contains the CommandHandler and PrefixHandler classes.""" +import re + from future.utils import string_types -from telegram import Update +from telegram import Update, MessageEntity from .handler import Handler @@ -27,9 +29,177 @@ class CommandHandler(Handler): """Handler class to handle Telegram commands. Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the - bot's name and/or some additional text. + bot's name and/or some additional text. The handler will add a ``list`` to the + :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings, + which is the text following the command split on single or consecutive whitespace characters. Attributes: + command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler + should listen for. Limitations are the same as described here + https://core.telegram.org/bots#commands + callback (:obj:`callable`): The callback function for this handler. + filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these + Filters. + allow_edited (:obj:`bool`): Determines Whether the handler should also accept + edited messages. + pass_args (:obj:`bool`): Determines whether the handler should be passed + ``args``. + pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be + passed to the callback function. + pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to + the callback function. + pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to + the callback function. + pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to + the callback function. + + Note: + :attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you + can use to keep any data in will be sent to the :attr:`callback` function. Related to + either the user or the chat that the update was sent in. For each update from the same user + or in the same chat, it will be the same ``dict``. + + Note that this is DEPRECATED, and you should use context based callbacks. See + https://git.io/vp113 for more info. + + Args: + command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler + should listen for. Limitations are the same as described here + https://core.telegram.org/bots#commands + callback (:obj:`callable`): The callback function for this handler. Will be called when + :attr:`check_update` has determined that an update should be processed by this handler. + Callback signature for context based API: + + ``def callback(update: Update, context: CallbackContext)`` + + The return value of the callback is usually ignored except for the special case of + :class:`telegram.ext.ConversationHandler`. + filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from + :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). + allow_edited (:obj:`bool`, optional): Determines whether the handler should also accept + edited messages. Default is ``False``. + pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the + arguments passed to the command as a keyword argument called ``args``. It will contain + a list of strings, which is the text following the command split on single or + consecutive whitespace characters. Default is ``False`` + DEPRECATED: Please switch to context based callbacks. + pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``update_queue`` will be passed to the callback function. It will be the ``Queue`` + instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher` + that contains new updates which can be used to insert updates. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``job_queue`` will be passed to the callback function. It will be a + :class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater` + which can be used to schedule new jobs. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``user_data`` will be passed to the callback function. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called + ``chat_data`` will be passed to the callback function. Default is ``False``. + DEPRECATED: Please switch to context based callbacks. + + Raises: + ValueError - when command is too long or has illegal chars. + """ + + def __init__(self, + command, + callback, + filters=None, + allow_edited=False, + pass_args=False, + pass_update_queue=False, + pass_job_queue=False, + pass_user_data=False, + pass_chat_data=False): + super(CommandHandler, self).__init__( + callback, + pass_update_queue=pass_update_queue, + pass_job_queue=pass_job_queue, + pass_user_data=pass_user_data, + pass_chat_data=pass_chat_data) + + if isinstance(command, string_types): + self.command = [command.lower()] + else: + self.command = [x.lower() for x in command] + for comm in self.command: + if not re.match(r'^[\da-z_]{1,32}$', comm): + raise ValueError('Command is not a valid bot command') + + self.filters = filters + self.allow_edited = allow_edited + self.pass_args = pass_args + + def check_update(self, update): + """Determines whether an update should be passed to this handlers :attr:`callback`. + + Args: + update (:class:`telegram.Update`): Incoming telegram update. + + Returns: + :obj:`bool` + + """ + if (isinstance(update, Update) and + (update.message or update.edited_message and self.allow_edited)): + message = update.effective_message + + if (message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND and + message.entities[0].offset == 0): + command = message.text[1:message.entities[0].length] + args = message.text.split()[1:] + command = command.split('@') + command.append(message.bot.username) + + if not (command[0].lower() in self.command and + command[1].lower() == message.bot.username.lower()): + return None + + if self.filters is None or self.filters(message): + return args + + def collect_optional_args(self, dispatcher, update=None, check_result=None): + optional_args = super(CommandHandler, self).collect_optional_args(dispatcher, update) + if self.pass_args: + optional_args['args'] = check_result + return optional_args + + def collect_additional_context(self, context, update, dispatcher, check_result): + context.args = check_result + + +class PrefixHandler(CommandHandler): + """Handler class to handle custom prefix commands + + This is a intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`. + It supports configurable commands with the same options as CommandHandler. It will respond to + every combination of :attr:`prefix` and :attr:`command`. It will add a ``list`` to the + :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings, + which is the text following the command split on single or consecutive whitespace characters. + + Examples:: + + Single prefix and command: + + PrefixHandler('!', 'test', callback) will respond to '!test'. + + Multiple prefixes, single command: + + PrefixHandler(['!', '#'], 'test', callback) will respond to '!test' and + '#test'. + + Miltiple prefixes and commands: + + PrefixHandler(['!', '#'], ['test', 'help`], callback) will respond to '!test', + '#test', '!help' and '#help'. + + Attributes: + prefix (:obj:`str` | List[:obj:`str`]): The prefix(es) that will precede :attr:`command`. command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler should listen for. callback (:obj:`callable`): The callback function for this handler. @@ -58,6 +228,7 @@ class CommandHandler(Handler): https://git.io/vp113 for more info. Args: + prefix (:obj:`str` | List[:obj:`str`]): The prefix(es) that will precede :attr:`command`. command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler should listen for. callback (:obj:`callable`): The callback function for this handler. Will be called when @@ -99,6 +270,7 @@ class CommandHandler(Handler): """ def __init__(self, + prefix, command, callback, filters=None, @@ -108,20 +280,23 @@ class CommandHandler(Handler): pass_job_queue=False, pass_user_data=False, pass_chat_data=False): - super(CommandHandler, self).__init__( - callback, + + super(PrefixHandler, self).__init__( + 'nocommand', callback, filters=filters, allow_edited=allow_edited, pass_args=pass_args, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue, pass_user_data=pass_user_data, pass_chat_data=pass_chat_data) + if isinstance(prefix, string_types): + self.prefix = [prefix.lower()] + else: + self.prefix = prefix if isinstance(command, string_types): self.command = [command.lower()] else: - self.command = [x.lower() for x in command] - self.filters = filters - self.allow_edited = allow_edited - self.pass_args = pass_args + self.command = command + self.command = [x.lower() + y.lower() for x in self.prefix for y in self.command] def check_update(self, update): """Determines whether an update should be passed to this handlers :attr:`callback`. @@ -135,27 +310,10 @@ class CommandHandler(Handler): """ if (isinstance(update, Update) and (update.message or update.edited_message and self.allow_edited)): - message = update.message or update.edited_message + message = update.effective_message - if message.text and message.text.startswith('/') and len(message.text) > 1: - first_word = message.text_html.split(None, 1)[0] - if len(first_word) > 1 and first_word.startswith('/'): - command = first_word[1:].split('@') - command.append( - message.bot.username) # in case the command was sent without a username - - if not (command[0].lower() in self.command - and command[1].lower() == message.bot.username.lower()): - return None - - if self.filters is None or self.filters(message): - return message.text.split()[1:] - - def collect_optional_args(self, dispatcher, update=None, check_result=None): - optional_args = super(CommandHandler, self).collect_optional_args(dispatcher, update) - if self.pass_args: - optional_args['args'] = check_result - return optional_args - - def collect_additional_context(self, context, update, dispatcher, check_result): - context.args = check_result + text_list = message.text.split() + if text_list[0].lower() not in self.command: + return None + if self.filters is None or self.filters(message): + return text_list[1:] diff --git a/tests/test_commandhandler.py b/tests/test_commandhandler.py index 6264f9d77..beeb35878 100644 --- a/tests/test_commandhandler.py +++ b/tests/test_commandhandler.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015-2018 @@ -21,8 +22,9 @@ from queue import Queue import pytest from telegram import (Message, Update, Chat, Bot, User, CallbackQuery, InlineQuery, - ChosenInlineResult, ShippingQuery, PreCheckoutQuery) -from telegram.ext import CommandHandler, Filters, BaseFilter, CallbackContext, JobQueue + ChosenInlineResult, ShippingQuery, PreCheckoutQuery, MessageEntity) +from telegram.ext import CommandHandler, Filters, BaseFilter, CallbackContext, JobQueue, \ + PrefixHandler message = Message(1, User(1, '', False), None, Chat(1, ''), text='test') @@ -49,7 +51,15 @@ def false_update(request): @pytest.fixture(scope='function') def message(bot): - return Message(1, User(1, '', False), None, Chat(1, ''), bot=bot) + return Message(message_id=1, + from_user=User(id=1, first_name='', is_bot=False), + date=None, + chat=Chat(id=1, type=''), + message='/test', + bot=bot, + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, + length=len('/test'))]) class TestCommandHandler(object): @@ -117,13 +127,26 @@ class TestCommandHandler(object): check = handler.check_update(Update(0, message)) assert check is None or check is False + message.entities = [] + message.text = '/test' + check = handler.check_update(Update(0, message)) + assert check is None or check is False + + @pytest.mark.parametrize('command', + ['way_too_longcommand1234567yes_way_toooooooLong', 'ïñválídletters', + 'invalid #&* chars'], + ids=['too long', 'invalid letter', 'invalid characters']) + def test_invalid_commands(self, command): + with pytest.raises(ValueError, match='not a valid bot command'): + CommandHandler(command, self.callback_basic) + def test_command_list(self, message): - handler = CommandHandler(['test', 'start'], self.callback_basic) + handler = CommandHandler(['test', 'star'], self.callback_basic) message.text = '/test' check = handler.check_update(Update(0, message)) - message.text = '/start' + message.text = '/star' check = handler.check_update(Update(0, message)) message.text = '/stop' @@ -137,11 +160,14 @@ class TestCommandHandler(object): message.text = '/test' check = handler.check_update(Update(0, message)) assert check is not None and check is not False + check = handler.check_update(Update(0, edited_message=message)) assert check is None or check is False + handler.allow_edited = True check = handler.check_update(Update(0, message)) assert check is not None and check is not False + check = handler.check_update(Update(0, edited_message=message)) assert check is not None and check is not False @@ -149,11 +175,13 @@ class TestCommandHandler(object): handler = CommandHandler('test', self.callback_basic) message.text = '/test@{}'.format(message.bot.username) + message.entities[0].length = len(message.text) check = handler.check_update(Update(0, message)) assert check is not None and check is not False message.text = '/test@otherbot' - assert not handler.check_update(Update(0, message)) + check = handler.check_update(Update(0, message)) + assert check is None or check is False def test_with_filter(self, message): handler = CommandHandler('test', self.callback_basic, Filters.group) @@ -177,11 +205,7 @@ class TestCommandHandler(object): self.test_flag = False message.text = '/test@{}'.format(message.bot.username) - dp.process_update(Update(0, message=message)) - assert self.test_flag - - self.test_flag = False - message.text = '/test one two' + message.entities[0].length = len(message.text) dp.process_update(Update(0, message=message)) assert self.test_flag @@ -190,6 +214,12 @@ class TestCommandHandler(object): dp.process_update(Update(0, message=message)) assert self.test_flag + self.test_flag = False + message.text = '/test one two' + message.entities[0].length = len('/test') + dp.process_update(Update(0, message=message)) + assert self.test_flag + def test_newline(self, dp, message): handler = CommandHandler('test', self.callback_basic) dp.add_handler(handler) @@ -197,31 +227,10 @@ class TestCommandHandler(object): message.text = '/test\nfoobar' check = handler.check_update(Update(0, message)) assert check is not None and check is not False + dp.process_update(Update(0, message)) assert self.test_flag - def test_single_char(self, dp, message): - # Regression test for https://github.com/python-telegram-bot/python-telegram-bot/issues/871 - handler = CommandHandler('test', self.callback_basic) - dp.add_handler(handler) - - message.text = 'a' - check = handler.check_update(Update(0, message)) - assert check is None or check is False - - def test_single_slash(self, dp, message): - # Regression test for https://github.com/python-telegram-bot/python-telegram-bot/issues/871 - handler = CommandHandler('test', self.callback_basic) - dp.add_handler(handler) - - message.text = '/' - check = handler.check_update(Update(0, message)) - assert check is None or check is False - - message.text = '/ test' - check = handler.check_update(Update(0, message)) - assert check is None or check is False - def test_pass_user_or_chat_data(self, dp, message): handler = CommandHandler('test', self.callback_data_1, pass_user_data=True) @@ -295,9 +304,9 @@ class TestCommandHandler(object): test_filter = TestFilter() - handler = CommandHandler('foo', self.callback_basic, + handler = CommandHandler('test', self.callback_basic, filters=test_filter) - message.text = '/bar' + message.text = '/star' check = handler.check_update(Update(0, message=message)) assert check is None or check is False @@ -323,3 +332,241 @@ class TestCommandHandler(object): message.text = '/test one two' cdp.process_update(Update(0, message)) assert self.test_flag + + +par = ['!help', '!test', '#help', '#test', 'mytrig-help', 'mytrig-test'] + + +@pytest.fixture(scope='function', params=par) +def prefixmessage(bot, request): + return Message(message_id=1, + from_user=User(id=1, first_name='', is_bot=False), + date=None, + chat=Chat(id=1, type=''), + text=request.param, + bot=bot) + + +class TestPrefixHandler(object): + test_flag = False + + @pytest.fixture(autouse=True) + def reset(self): + self.test_flag = False + + def callback_basic(self, bot, update): + test_bot = isinstance(bot, Bot) + test_update = isinstance(update, Update) + self.test_flag = test_bot and test_update + + def callback_data_1(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) or (chat_data is not None) + + def callback_data_2(self, bot, update, user_data=None, chat_data=None): + self.test_flag = (user_data is not None) and (chat_data is not None) + + def callback_queue_1(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) or (update_queue is not None) + + def callback_queue_2(self, bot, update, job_queue=None, update_queue=None): + self.test_flag = (job_queue is not None) and (update_queue is not None) + + def ch_callback_args(self, bot, update, args): + if update.message.text in par: + self.test_flag = len(args) == 0 + else: + self.test_flag = args == ['one', 'two'] + + def callback_context(self, update, context): + self.test_flag = (isinstance(context, CallbackContext) and + isinstance(context.bot, Bot) and + isinstance(update, Update) and + isinstance(context.update_queue, Queue) and + isinstance(context.job_queue, JobQueue) and + isinstance(context.user_data, dict) and + isinstance(context.chat_data, dict) and + isinstance(update.message, Message)) + + def callback_context_args(self, update, context): + self.test_flag = context.args == ['one', 'two'] + + def test_basic(self, dp, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic) + dp.add_handler(handler) + + dp.process_update(Update(0, prefixmessage)) + assert self.test_flag + + prefixmessage.text = 'test' + check = handler.check_update(Update(0, prefixmessage)) + assert check is None or check is False + + prefixmessage.text = '#nocom' + check = handler.check_update(Update(0, prefixmessage)) + assert check is None or check is False + + message.text = 'not !test at start' + check = handler.check_update(Update(0, message)) + assert check is None or check is False + + def test_single_prefix_single_command(self, prefixmessage): + handler = PrefixHandler('!', 'test', self.callback_basic) + + check = handler.check_update(Update(0, prefixmessage)) + if prefixmessage.text in ['!test']: + assert check is not None and check is not False + else: + assert check is None or check is False + + def test_single_prefix_multi_command(self, prefixmessage): + handler = PrefixHandler('!', ['test', 'help'], self.callback_basic) + + check = handler.check_update(Update(0, prefixmessage)) + if prefixmessage.text in ['!test', '!help']: + assert check is not None and check is not False + else: + assert check is None or check is False + + def test_multi_prefix_single_command(self, prefixmessage): + handler = PrefixHandler(['!', '#'], 'test', self.callback_basic) + + check = handler.check_update(Update(0, prefixmessage)) + if prefixmessage.text in ['!test', '#test']: + assert check is not None and check is not False + else: + assert check is None or check is False + + def test_edited(self, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic) + + check = handler.check_update(Update(0, prefixmessage)) + assert check is not None and check is not False + + check = handler.check_update(Update(0, edited_message=prefixmessage)) + assert check is None or check is False + + handler.allow_edited = True + check = handler.check_update(Update(0, prefixmessage)) + assert check is not None and check is not False + + check = handler.check_update(Update(0, edited_message=prefixmessage)) + assert check is not None and check is not False + + def test_with_filter(self, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic, + filters=Filters.group) + + prefixmessage.chat = Chat(-23, 'group') + check = handler.check_update(Update(0, prefixmessage)) + assert check is not None and check is not False + + prefixmessage.chat = Chat(23, 'private') + check = handler.check_update(Update(0, prefixmessage)) + assert check is None or check is False + + def test_pass_args(self, dp, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.ch_callback_args, + pass_args=True) + dp.add_handler(handler) + + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + self.test_flag = False + prefixmessage.text += ' one two' + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + def test_pass_user_or_chat_data(self, dp, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1, + pass_user_data=True) + dp.add_handler(handler) + + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + dp.remove_handler(handler) + self.test_flag = False + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1, + pass_chat_data=True) + dp.add_handler(handler) + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + dp.remove_handler(handler) + self.test_flag = False + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_2, + pass_chat_data=True, pass_user_data=True) + dp.add_handler(handler) + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + def test_pass_job_or_update_queue(self, dp, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1, + pass_job_queue=True) + dp.add_handler(handler) + + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + dp.remove_handler(handler) + self.test_flag = False + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1, + pass_update_queue=True) + dp.add_handler(handler) + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + dp.remove_handler(handler) + self.test_flag = False + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_2, + pass_job_queue=True, pass_update_queue=True) + dp.add_handler(handler) + dp.process_update(Update(0, message=prefixmessage)) + assert self.test_flag + + def test_other_update_types(self, false_update): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic) + check = handler.check_update(false_update) + assert check is None or check is False + + def test_filters_for_wrong_command(self, prefixmessage): + """Filters should not be executed if the command does not match the handler""" + + class TestFilter(BaseFilter): + def __init__(self): + self.tested = False + + def filter(self, message): + self.tested = True + + test_filter = TestFilter() + + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic, + filters=test_filter) + + prefixmessage.text = '/star' + + check = handler.check_update(Update(0, message=prefixmessage)) + assert check is None or check is False + + assert not test_filter.tested + + def test_context(self, cdp, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_context) + cdp.add_handler(handler) + + cdp.process_update(Update(0, prefixmessage)) + assert self.test_flag + + def test_context_args(self, cdp, prefixmessage): + handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], + self.callback_context_args) + cdp.add_handler(handler) + + cdp.process_update(Update(0, prefixmessage)) + assert not self.test_flag + + prefixmessage.text += ' one two' + cdp.process_update(Update(0, prefixmessage)) + assert self.test_flag diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 6779ad584..c8c739ded 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -22,7 +22,7 @@ from time import sleep import pytest from telegram import (CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message, - PreCheckoutQuery, ShippingQuery, Update, User) + PreCheckoutQuery, ShippingQuery, Update, User, MessageEntity) from telegram.ext import (ConversationHandler, CommandHandler, CallbackQueryHandler) @@ -109,22 +109,28 @@ class TestConversationHandler(object): dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY # The user is thirsty and wants to brew coffee. message.text = '/brew' + message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Lets see if an invalid command makes sure, no state is changed. message.text = '/nothing' + message.entities[0].length = len('/nothing') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Lets see if the state machine still works by pouring coffee. message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING @@ -140,13 +146,19 @@ class TestConversationHandler(object): fallbacks=self.fallbacks) dp.add_handler(handler) - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) message.text = '/brew' + message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=0, message=message)) message.text = '/end' + message.entities[0].length = len('/end') with caplog.at_level(logging.ERROR): dp.process_update(Update(update_id=0, message=message)) assert len(caplog.records) == 0 @@ -160,23 +172,29 @@ class TestConversationHandler(object): dp.add_handler(handler) # first check if fallback will not trigger start when not started - message = Message(0, user1, None, self.group, text='/eat', bot=bot) + message = Message(0, user1, None, self.group, text='/eat', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/eat'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) with pytest.raises(KeyError): self.current_state[user1.id] # User starts the state machine. message.text = '/start' + message.entities[0].length = len('/start') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY # The user is thirsty and wants to brew coffee. message.text = '/brew' + message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING # Now a fallback command is issued message.text = '/eat' + message.entities[0].length = len('/eat') dp.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY @@ -189,17 +207,22 @@ class TestConversationHandler(object): dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) # The user is thirsty and wants to brew coffee. message.text = '/brew' + message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) # Let's now verify that for another user, who did not start yet, # the state will be changed because they are in the same group. message.from_user = user2 message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations[(self.group.id,)] == self.DRINKING @@ -213,17 +236,22 @@ class TestConversationHandler(object): dp.add_handler(handler) # User one, starts the state machine. - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) # The user is thirsty and wants to brew coffee. message.text = '/brew' + message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) # Let's now verify that for the same user in a different group, the state will still be # updated message.chat = self.second_group message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations[(user1.id,)] == self.DRINKING @@ -272,7 +300,10 @@ class TestConversationHandler(object): dp.add_handler(handler) # User starts the state machine and immediately ends it. - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) assert len(handler.conversations) == 0 @@ -286,13 +317,17 @@ class TestConversationHandler(object): # User starts the state machine with an async function that immediately ends the # conversation. Async results are resolved when the users state is queried next time. - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.update_queue.put(Update(update_id=0, message=message)) sleep(.1) # Assert that the Promise has been accepted as the new state assert len(handler.conversations) == 1 message.text = 'resolve promise pls' + message.entities[0].length = len('resolve promise pls') dp.update_queue.put(Update(update_id=0, message=message)) sleep(.1) # Assert that the Promise has been resolved and the conversation ended. @@ -335,7 +370,10 @@ class TestConversationHandler(object): dp.add_handler(handler) # Start state machine, then reach timeout - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY sleep(0.5) @@ -346,6 +384,7 @@ class TestConversationHandler(object): dp.process_update(Update(update_id=1, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY message.text = '/brew' + message.entities[0].length = len('/brew') dp.job_queue.tick() dp.process_update(Update(update_id=2, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING @@ -365,19 +404,24 @@ class TestConversationHandler(object): # t=.6 /pourCoffee (timeout=1.1) # t=.75 second timeout # t=1.1 actual timeout - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY sleep(0.25) # t=.25 dp.job_queue.tick() assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY message.text = '/brew' + message.entities[0].length = len('/brew') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING sleep(0.35) # t=.6 dp.job_queue.tick() assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING sleep(.4) # t=1 @@ -393,15 +437,21 @@ class TestConversationHandler(object): dp.add_handler(handler) # Start state machine, do something as second user, then reach timeout - message = Message(0, user1, None, self.group, text='/start', bot=bot) + message = Message(0, user1, None, self.group, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, + length=len('/start'))], + bot=bot) dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY message.text = '/brew' + message.entities[0].length = len('/brew') + message.entities[0].length = len('/brew') message.from_user = user2 dp.job_queue.tick() dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user2.id)) is None message.text = '/start' + message.entities[0].length = len('/start') dp.job_queue.tick() dp.process_update(Update(update_id=0, message=message)) assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 7400bcf65..7e42b05f8 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -23,7 +23,7 @@ from time import sleep import pytest -from telegram import TelegramError, Message, User, Chat, Update, Bot +from telegram import TelegramError, Message, User, Chat, Update, Bot, MessageEntity from telegram.ext import MessageHandler, Filters, CommandHandler, CallbackContext, JobQueue from telegram.ext.dispatcher import run_async, Dispatcher, DispatcherHandlerStop from telegram.utils.deprecate import TelegramDeprecationWarning @@ -237,7 +237,11 @@ class TestDispatcher(object): passed.append('error') passed.append(e) - update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot)) + update = Update(1, message=Message(1, None, None, None, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, + length=len('/start'))], + bot=bot)) # If Stop raised handlers in other groups should not be called. passed = [] @@ -264,7 +268,11 @@ class TestDispatcher(object): passed.append('error') passed.append(e) - update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot)) + update = Update(1, message=Message(1, None, None, None, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, + length=len('/start'))], + bot=bot)) # If an unhandled exception was caught, no further handlers from the same group should be # called. @@ -294,7 +302,11 @@ class TestDispatcher(object): passed.append('error') passed.append(e) - update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot)) + update = Update(1, message=Message(1, None, None, None, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, + length=len('/start'))], + bot=bot)) # If a TelegramException was caught, an error handler should be called and no further # handlers from the same group should be called. @@ -325,7 +337,11 @@ class TestDispatcher(object): passed.append(e) raise DispatcherHandlerStop - update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot)) + update = Update(1, message=Message(1, None, None, None, text='/start', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, + offset=0, + length=len('/start'))], + bot=bot)) # If a TelegramException was caught, an error handler should be called and no further # handlers from the same group should be called.