diff --git a/docs/source/conf.py b/docs/source/conf.py index a4c49d6ba..430464f3d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,6 +19,7 @@ from typing import Tuple # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. +import inspect from docutils.nodes import Element from sphinx.application import Sphinx from sphinx.domains.python import PyXRefRole @@ -358,7 +359,23 @@ class TGConstXRefRole(PyXRefRole): def autodoc_skip_member(app, what, name, obj, skip, options): - pass + """We use this to not document certain members like filter() or check_update() for filters. + See https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#skipping-members""" + + included = {'MessageFilter', 'UpdateFilter'} # filter() and check_update() only for these. + included_in_obj = any(inc in repr(obj) for inc in included) + + if included_in_obj: # it's difficult to see if check_update is from an inherited-member or not + for frame in inspect.stack(): # From https://github.com/sphinx-doc/sphinx/issues/9533 + if frame.function == "filter_members": + docobj = frame.frame.f_locals["self"].object + if not any(inc in str(docobj) for inc in included) and name == 'check_update': + return True + break + + if name == 'filter' and obj.__module__ == 'telegram.ext.filters': + if not included_in_obj: + return True # return True to exclude from docs. def setup(app: Sphinx): diff --git a/docs/source/telegram.ext.filters.rst b/docs/source/telegram.ext.filters.rst index c4e12c714..8eb415771 100644 --- a/docs/source/telegram.ext.filters.rst +++ b/docs/source/telegram.ext.filters.rst @@ -3,6 +3,11 @@ telegram.ext.filters Module =========================== +.. :bysource: since e.g filters.CHAT is much above filters.Chat() in the docs when it shouldn't. + The classes in `filters.py` are sorted alphabetically such that :bysource: still is readable + .. automodule:: telegram.ext.filters + :inherited-members: :members: :show-inheritance: + :member-order: bysource diff --git a/examples/conversationbot.py b/examples/conversationbot.py index ec3e636bf..1b0b19830 100644 --- a/examples/conversationbot.py +++ b/examples/conversationbot.py @@ -20,7 +20,7 @@ from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update from telegram.ext import ( CommandHandler, MessageHandler, - Filters, + filters, ConversationHandler, Updater, CallbackContext, @@ -146,13 +146,13 @@ def main() -> None: conv_handler = ConversationHandler( entry_points=[CommandHandler('start', start)], states={ - GENDER: [MessageHandler(Filters.regex('^(Boy|Girl|Other)$'), gender)], - PHOTO: [MessageHandler(Filters.photo, photo), CommandHandler('skip', skip_photo)], + GENDER: [MessageHandler(filters.Regex('^(Boy|Girl|Other)$'), gender)], + PHOTO: [MessageHandler(filters.PHOTO, photo), CommandHandler('skip', skip_photo)], LOCATION: [ - MessageHandler(Filters.location, location), + MessageHandler(filters.LOCATION, location), CommandHandler('skip', skip_location), ], - BIO: [MessageHandler(Filters.text & ~Filters.command, bio)], + BIO: [MessageHandler(filters.TEXT & ~filters.COMMAND, bio)], }, fallbacks=[CommandHandler('cancel', cancel)], ) diff --git a/examples/conversationbot2.py b/examples/conversationbot2.py index 6fbb1d51e..dfdd5f5aa 100644 --- a/examples/conversationbot2.py +++ b/examples/conversationbot2.py @@ -21,7 +21,7 @@ from telegram import ReplyKeyboardMarkup, Update, ReplyKeyboardRemove from telegram.ext import ( CommandHandler, MessageHandler, - Filters, + filters, ConversationHandler, Updater, CallbackContext, @@ -126,23 +126,23 @@ def main() -> None: states={ CHOOSING: [ MessageHandler( - Filters.regex('^(Age|Favourite colour|Number of siblings)$'), regular_choice + filters.Regex('^(Age|Favourite colour|Number of siblings)$'), regular_choice ), - MessageHandler(Filters.regex('^Something else...$'), custom_choice), + MessageHandler(filters.Regex('^Something else...$'), custom_choice), ], TYPING_CHOICE: [ MessageHandler( - Filters.text & ~(Filters.command | Filters.regex('^Done$')), regular_choice + filters.TEXT & ~(filters.COMMAND | filters.Regex('^Done$')), regular_choice ) ], TYPING_REPLY: [ MessageHandler( - Filters.text & ~(Filters.command | Filters.regex('^Done$')), + filters.TEXT & ~(filters.COMMAND | filters.Regex('^Done$')), received_information, ) ], }, - fallbacks=[MessageHandler(Filters.regex('^Done$'), done)], + fallbacks=[MessageHandler(filters.Regex('^Done$'), done)], ) dispatcher.add_handler(conv_handler) diff --git a/examples/deeplinking.py b/examples/deeplinking.py index 534dfab6f..88a7cd45b 100644 --- a/examples/deeplinking.py +++ b/examples/deeplinking.py @@ -25,7 +25,7 @@ from telegram.constants import ParseMode from telegram.ext import ( CommandHandler, CallbackQueryHandler, - Filters, + filters, Updater, CallbackContext, ) @@ -115,20 +115,20 @@ def main() -> None: # Register a deep-linking handler dispatcher.add_handler( - CommandHandler("start", deep_linked_level_1, Filters.regex(CHECK_THIS_OUT)) + CommandHandler("start", deep_linked_level_1, filters.Regex(CHECK_THIS_OUT)) ) # This one works with a textual link instead of an URL - dispatcher.add_handler(CommandHandler("start", deep_linked_level_2, Filters.regex(SO_COOL))) + dispatcher.add_handler(CommandHandler("start", deep_linked_level_2, filters.Regex(SO_COOL))) # We can also pass on the deep-linking payload dispatcher.add_handler( - CommandHandler("start", deep_linked_level_3, Filters.regex(USING_ENTITIES)) + CommandHandler("start", deep_linked_level_3, filters.Regex(USING_ENTITIES)) ) # Possible with inline keyboard buttons as well dispatcher.add_handler( - CommandHandler("start", deep_linked_level_4, Filters.regex(USING_KEYBOARD)) + CommandHandler("start", deep_linked_level_4, filters.Regex(USING_KEYBOARD)) ) # register callback handler for inline keyboard button diff --git a/examples/echobot.py b/examples/echobot.py index 0d7b12ad9..278df7d9a 100644 --- a/examples/echobot.py +++ b/examples/echobot.py @@ -21,7 +21,7 @@ from telegram import Update, ForceReply from telegram.ext import ( CommandHandler, MessageHandler, - Filters, + filters, Updater, CallbackContext, ) @@ -68,7 +68,7 @@ def main() -> None: dispatcher.add_handler(CommandHandler("help", help_command)) # on non command i.e message - echo the message on Telegram - dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, echo)) + dispatcher.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) # Start the Bot updater.start_polling() diff --git a/examples/nestedconversationbot.py b/examples/nestedconversationbot.py index 75799b28e..414a90d61 100644 --- a/examples/nestedconversationbot.py +++ b/examples/nestedconversationbot.py @@ -21,7 +21,7 @@ from telegram import InlineKeyboardMarkup, InlineKeyboardButton, Update from telegram.ext import ( CommandHandler, MessageHandler, - Filters, + filters, ConversationHandler, CallbackQueryHandler, Updater, @@ -319,7 +319,7 @@ def main() -> None: SELECTING_FEATURE: [ CallbackQueryHandler(ask_for_input, pattern='^(?!' + str(END) + ').*$') ], - TYPING: [MessageHandler(Filters.text & ~Filters.command, save_input)], + TYPING: [MessageHandler(filters.TEXT & ~filters.COMMAND, save_input)], }, fallbacks=[ CallbackQueryHandler(end_describing, pattern='^' + str(END) + '$'), diff --git a/examples/passportbot.py b/examples/passportbot.py index 4807b3d54..3722da781 100644 --- a/examples/passportbot.py +++ b/examples/passportbot.py @@ -14,7 +14,7 @@ import logging from pathlib import Path from telegram import Update -from telegram.ext import MessageHandler, Filters, Updater, CallbackContext +from telegram.ext import MessageHandler, filters, Updater, CallbackContext # Enable logging @@ -110,7 +110,7 @@ def main() -> None: dispatcher = updater.dispatcher # On messages that include passport data call msg - dispatcher.add_handler(MessageHandler(Filters.passport_data, msg)) + dispatcher.add_handler(MessageHandler(filters.PASSPORT_DATA, msg)) # Start the Bot updater.start_polling() diff --git a/examples/paymentbot.py b/examples/paymentbot.py index 54f7523be..e44c0fcbf 100644 --- a/examples/paymentbot.py +++ b/examples/paymentbot.py @@ -10,7 +10,7 @@ from telegram import LabeledPrice, ShippingOption, Update from telegram.ext import ( CommandHandler, MessageHandler, - Filters, + filters, PreCheckoutQueryHandler, ShippingQueryHandler, Updater, @@ -149,7 +149,7 @@ def main() -> None: dispatcher.add_handler(PreCheckoutQueryHandler(precheckout_callback)) # Success! Notify your user! - dispatcher.add_handler(MessageHandler(Filters.successful_payment, successful_payment_callback)) + dispatcher.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_callback)) # Start the Bot updater.start_polling() diff --git a/examples/persistentconversationbot.py b/examples/persistentconversationbot.py index f267e4e7a..8defda533 100644 --- a/examples/persistentconversationbot.py +++ b/examples/persistentconversationbot.py @@ -21,7 +21,7 @@ from telegram import ReplyKeyboardMarkup, Update, ReplyKeyboardRemove from telegram.ext import ( CommandHandler, MessageHandler, - Filters, + filters, ConversationHandler, PicklePersistence, Updater, @@ -144,23 +144,23 @@ def main() -> None: states={ CHOOSING: [ MessageHandler( - Filters.regex('^(Age|Favourite colour|Number of siblings)$'), regular_choice + filters.Regex('^(Age|Favourite colour|Number of siblings)$'), regular_choice ), - MessageHandler(Filters.regex('^Something else...$'), custom_choice), + MessageHandler(filters.Regex('^Something else...$'), custom_choice), ], TYPING_CHOICE: [ MessageHandler( - Filters.text & ~(Filters.command | Filters.regex('^Done$')), regular_choice + filters.TEXT & ~(filters.COMMAND | filters.Regex('^Done$')), regular_choice ) ], TYPING_REPLY: [ MessageHandler( - Filters.text & ~(Filters.command | Filters.regex('^Done$')), + filters.TEXT & ~(filters.COMMAND | filters.Regex('^Done$')), received_information, ) ], }, - fallbacks=[MessageHandler(Filters.regex('^Done$'), done)], + fallbacks=[MessageHandler(filters.Regex('^Done$'), done)], name="my_conversation", persistent=True, ) diff --git a/examples/pollbot.py b/examples/pollbot.py index 5aa8968ca..85680613b 100644 --- a/examples/pollbot.py +++ b/examples/pollbot.py @@ -23,7 +23,7 @@ from telegram.ext import ( PollAnswerHandler, PollHandler, MessageHandler, - Filters, + filters, Updater, CallbackContext, ) @@ -163,7 +163,7 @@ def main() -> None: dispatcher.add_handler(CommandHandler('quiz', quiz)) dispatcher.add_handler(PollHandler(receive_quiz_answer)) dispatcher.add_handler(CommandHandler('preview', preview)) - dispatcher.add_handler(MessageHandler(Filters.poll, receive_poll)) + dispatcher.add_handler(MessageHandler(filters.POLL, receive_poll)) dispatcher.add_handler(CommandHandler('help', help_handler)) # Start the Bot diff --git a/telegram/ext/__init__.py b/telegram/ext/__init__.py index 5eeff7dc8..fb8891d35 100644 --- a/telegram/ext/__init__.py +++ b/telegram/ext/__init__.py @@ -31,7 +31,7 @@ from ._updater import Updater from ._callbackqueryhandler import CallbackQueryHandler from ._choseninlineresulthandler import ChosenInlineResultHandler from ._inlinequeryhandler import InlineQueryHandler -from .filters import BaseFilter, MessageFilter, UpdateFilter, Filters +from . import filters from ._messagehandler import MessageHandler from ._commandhandler import CommandHandler, PrefixHandler from ._stringcommandhandler import StringCommandHandler @@ -49,7 +49,6 @@ from ._callbackdatacache import CallbackDataCache, InvalidCallbackData from ._builders import DispatcherBuilder, UpdaterBuilder __all__ = ( - 'BaseFilter', 'BasePersistence', 'CallbackContext', 'CallbackDataCache', @@ -66,13 +65,12 @@ __all__ = ( 'DispatcherBuilder', 'DispatcherHandlerStop', 'ExtBot', - 'Filters', + 'filters', 'Handler', 'InlineQueryHandler', 'InvalidCallbackData', 'Job', 'JobQueue', - 'MessageFilter', 'MessageHandler', 'PersistenceInput', 'PicklePersistence', @@ -84,7 +82,6 @@ __all__ = ( 'StringCommandHandler', 'StringRegexHandler', 'TypeHandler', - 'UpdateFilter', 'Updater', 'UpdaterBuilder', ) diff --git a/telegram/ext/_basepersistence.py b/telegram/ext/_basepersistence.py index 85f88900a..a3b97e67c 100644 --- a/telegram/ext/_basepersistence.py +++ b/telegram/ext/_basepersistence.py @@ -29,7 +29,7 @@ from telegram._utils.warnings import warn from telegram.ext._utils.types import UD, CD, BD, ConversationDict, CDCData -class PersistenceInput(NamedTuple): +class PersistenceInput(NamedTuple): # skipcq: PYL-E0239 """Convenience wrapper to group boolean input for :class:`BasePersistence`. Args: diff --git a/telegram/ext/_callbackcontext.py b/telegram/ext/_callbackcontext.py index 8c705ec1c..0ee4913ba 100644 --- a/telegram/ext/_callbackcontext.py +++ b/telegram/ext/_callbackcontext.py @@ -68,10 +68,9 @@ class CallbackContext(Generic[BT, UD, CD, BD]): Attributes: matches (List[:obj:`re match object`]): Optional. If the associated update originated from - a regex-supported handler or had a :class:`Filters.regex`, this will contain a list of - match objects for every pattern where ``re.search(pattern, string)`` returned a match. - Note that filters short circuit, so combined regex filters will not always - be evaluated. + a :class:`filters.Regex`, this will contain a list of match objects for every pattern + where ``re.search(pattern, string)`` returned a match. Note that filters short circuit, + so combined regex filters will not always be evaluated. args (List[:obj:`str`]): Optional. Arguments passed to a command if the associated update 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 diff --git a/telegram/ext/_commandhandler.py b/telegram/ext/_commandhandler.py index 5cc341010..02cf6152d 100644 --- a/telegram/ext/_commandhandler.py +++ b/telegram/ext/_commandhandler.py @@ -21,7 +21,7 @@ import re from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, TypeVar, Union from telegram import MessageEntity, Update -from telegram.ext import BaseFilter, Filters, Handler +from telegram.ext import filters as filters_module, Handler from telegram._utils.types import SLT from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE from telegram.ext._utils.types import CCT @@ -41,7 +41,7 @@ class CommandHandler(Handler[Update, CCT]): which is the text following the command split on single or consecutive whitespace characters. By default the handler listens to messages as well as edited messages. To change this behavior - use ``~Filters.update.edited_message`` in the filter argument. + use ``~filters.UpdateType.EDITED_MESSAGE`` in the filter argument. Note: * :class:`CommandHandler` does *not* handle (edited) channel posts. @@ -62,7 +62,7 @@ class CommandHandler(Handler[Update, CCT]): :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 + :mod:`telegram.ext.filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. @@ -86,13 +86,10 @@ class CommandHandler(Handler[Update, CCT]): self, command: SLT[str], callback: Callable[[Update, CCT], RT], - filters: BaseFilter = None, + filters: filters_module.BaseFilter = None, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): - super().__init__( - callback, - run_async=run_async, - ) + super().__init__(callback, run_async=run_async) if isinstance(command, str): self.command = [command.lower()] @@ -102,10 +99,7 @@ class CommandHandler(Handler[Update, CCT]): if not re.match(r'^[\da-z_]{1,32}$', comm): raise ValueError('Command is not a valid bot command') - if filters: - self.filters = Filters.update.messages & filters - else: - self.filters = Filters.update.messages + self.filters = filters if filters is not None else filters_module.UpdateType.MESSAGES def check_update( self, update: object @@ -140,7 +134,7 @@ class CommandHandler(Handler[Update, CCT]): ): return None - filter_result = self.filters(update) + filter_result = self.filters.check_update(update) if filter_result: return args, filter_result return False @@ -167,7 +161,7 @@ class PrefixHandler(CommandHandler): 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 + every combination of :attr:`prefix` and :attr:`command`. It will add a :obj:`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. @@ -194,7 +188,7 @@ class PrefixHandler(CommandHandler): By default the handler listens to messages as well as edited messages. To change this behavior - use ``~Filters.update.edited_message``. + use ``~filters.UpdateType.EDITED_MESSAGE``. Note: * :class:`PrefixHandler` does *not* handle (edited) channel posts. @@ -216,7 +210,7 @@ class PrefixHandler(CommandHandler): :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 + :mod:`telegram.ext.filters`. Filters can be combined using bitwise operators (& for and, | for or, ~ for not). run_async (:obj:`bool`): Determines whether the callback will run asynchronously. Defaults to :obj:`False`. @@ -237,7 +231,7 @@ class PrefixHandler(CommandHandler): prefix: SLT[str], command: SLT[str], callback: Callable[[Update, CCT], RT], - filters: BaseFilter = None, + filters: filters_module.BaseFilter = None, run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): @@ -314,7 +308,7 @@ class PrefixHandler(CommandHandler): text_list = message.text.split() if text_list[0].lower() not in self._commands: return None - filter_result = self.filters(update) + filter_result = self.filters.check_update(update) if filter_result: return text_list[1:], filter_result return False diff --git a/telegram/ext/_messagehandler.py b/telegram/ext/_messagehandler.py index d7f764bf1..8d20fbb15 100644 --- a/telegram/ext/_messagehandler.py +++ b/telegram/ext/_messagehandler.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Callable, Dict, Optional, TypeVar, Union from telegram import Update -from telegram.ext import BaseFilter, Filters, Handler +from telegram.ext import filters as filters_module, Handler from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE from telegram.ext._utils.types import CCT @@ -39,13 +39,13 @@ class MessageHandler(Handler[Update, CCT]): attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info. Args: - filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from + filters (:class:`telegram.ext.BaseFilter`): 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). Default is - :attr:`telegram.ext.filters.Filters.update`. This defaults to all message_type updates - being: ``message``, ``edited_message``, ``channel_post`` and ``edited_channel_post``. - If you don't want or need any of those pass ``~Filters.update.*`` in the filter + :mod:`telegram.ext.filters`. Filters can be combined using bitwise + operators (& for and, | for or, ~ for not). This defaults to all message updates + being: :attr:`Update.message`, :attr:`Update.edited_message`, + :attr:`Update.channel_post` and :attr:`Update.edited_channel_post`. + If you don't want or need any of those pass ``~filters.UpdateType.*`` in the filter argument. 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. @@ -60,8 +60,8 @@ class MessageHandler(Handler[Update, CCT]): ValueError Attributes: - filters (:obj:`Filter`): Only allow updates with these Filters. See - :mod:`telegram.ext.filters` for a full list of all available filters. + filters (:class:`telegram.ext.filters.BaseFilter`): Only allow updates with these Filters. + See :mod:`telegram.ext.filters` for a full list of all available filters. callback (:obj:`callable`): The callback function for this handler. run_async (:obj:`bool`): Determines whether the callback will run asynchronously. @@ -71,19 +71,13 @@ class MessageHandler(Handler[Update, CCT]): def __init__( self, - filters: BaseFilter, + filters: filters_module.BaseFilter, callback: Callable[[Update, CCT], RT], run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, ): - super().__init__( - callback, - run_async=run_async, - ) - if filters is not None: - self.filters = Filters.update & filters - else: - self.filters = Filters.update + super().__init__(callback, run_async=run_async) + self.filters = filters if filters is not None else filters_module.ALL def check_update(self, update: object) -> Optional[Union[bool, Dict[str, list]]]: """Determines whether an update should be passed to this handlers :attr:`callback`. @@ -96,7 +90,7 @@ class MessageHandler(Handler[Update, CCT]): """ if isinstance(update, Update): - return self.filters(update) + return self.filters.check_update(update) return None def collect_additional_context( diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index cec89ef0b..972e495d6 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -16,9 +16,27 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -# pylint: disable=empty-docstring, invalid-name, arguments-differ -"""This module contains the Filters for use with the MessageHandler class.""" +""" +This module contains filters for use with :class:`telegram.ext.MessageHandler`, +:class:`telegram.ext.CommandHandler`, or :class:`telegram.ext.PrefixHandler`. +.. versionchanged:: 14.0 + + #. Filters are no longer callable, if you're using a custom filter and are calling an existing + filter, then switch to the new syntax: ``filters.{filter}.check_update(update)``. + #. Removed the ``Filters`` class. The filters are now directly attributes/classes of the + :mod:`filters` module. + #. The names of all filters has been updated: + + * Filter classes which are ready for use, e.g ``Filters.all`` are now capitalized, e.g + ``filters.ALL``. + * Filters which need to be initialized are now in CamelCase. E.g. ``filters.User(...)``. + * Filters which do both (like ``Filters.text``) are now split as ready-to-use version + ``filters.TEXT`` and class version ``filters.Text(...)``. + +""" + +import mimetypes import re from abc import ABC, abstractmethod @@ -37,106 +55,105 @@ from typing import ( NoReturn, ) -from telegram import Chat, Message, MessageEntity, Update, User - -__all__ = [ - 'Filters', - 'BaseFilter', - 'MessageFilter', - 'UpdateFilter', - 'InvertedFilter', - 'MergedFilter', - 'XORFilter', -] +from telegram import Chat as TGChat, Message, MessageEntity, Update, User as TGUser from telegram._utils.types import SLT -from telegram.constants import DiceEmoji +from telegram.constants import DiceEmoji as DiceEmojiEnum DataDict = Dict[str, list] -class BaseFilter(ABC): +class BaseFilter: """Base class for all Filters. Filters subclassing from this class can combined using bitwise operators: And: - >>> (Filters.text & Filters.entity(MENTION)) + >>> (filters.TEXT & filters.Entity(MENTION)) Or: - >>> (Filters.audio | Filters.video) + >>> (filters.AUDIO | filters.VIDEO) Exclusive Or: - >>> (Filters.regex('To Be') ^ Filters.regex('Not 2B')) + >>> (filters.Regex('To Be') ^ filters.Regex('Not 2B')) Not: - >>> ~ Filters.command + >>> ~ filters.COMMAND Also works with more than two filters: - >>> (Filters.text & (Filters.entity(URL) | Filters.entity(TEXT_LINK))) - >>> Filters.text & (~ Filters.forwarded) + >>> (filters.TEXT & (filters.Entity(URL) | filters.Entity(TEXT_LINK))) + >>> filters.TEXT & (~ filters.FORWARDED) Note: Filters use the same short circuiting logic as python's `and`, `or` and `not`. This means that for example: - >>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)') + >>> filters.Regex(r'(a?x)') | filters.Regex(r'(b?x)') - With ``message.text == x``, will only ever return the matches for the first filter, + With ``message.text == 'x'``, will only ever return the matches for the first filter, since the second one is never evaluated. If you want to create your own filters create a class inheriting from either - :class:`MessageFilter` or :class:`UpdateFilter` and implement a :meth:`filter` method that - returns a boolean: :obj:`True` if the message should be + :class:`MessageFilter` or :class:`UpdateFilter` and implement a ``filter()`` + method that returns a boolean: :obj:`True` if the message should be handled, :obj:`False` otherwise. - Note that the filters work only as class instances, not - actual class objects (so remember to + Note that the filters work only as class instances, not actual class objects (so remember to initialize your filter classes). By default the filters name (what will get printed when converted to a string for display) will be the class name. If you want to overwrite this assign a better name to the :attr:`name` class variable. - Attributes: + .. versionadded:: 14.0 + Added the arguments :attr:`name` and :attr:`data_filter`. + + Args: name (:obj:`str`): Name for this filter. Defaults to the type of filter. data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should return a dict with lists. The dict will be merged with :class:`telegram.ext.CallbackContext`'s internal dict in most cases (depends on the handler). + + Attributes: + name (:obj:`str`): Name for this filter. + data_filter (:obj:`bool`): Whether this filter is a data filter. """ __slots__ = ('_name', '_data_filter') - # pylint: disable=unused-argument - def __new__(cls, *args: object, **kwargs: object) -> 'BaseFilter': - # We do this here instead of in a __init__ so filter don't have to call __init__ or super() - instance = super().__new__(cls) - instance._name = None - instance._data_filter = False + def __init__(self, name: str = None, data_filter: bool = False): + self._name = self.__class__.__name__ if name is None else name + self._data_filter = data_filter - return instance - - @abstractmethod - def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: - ... + # pylint: disable=no-self-use + def check_update(self, update: Update) -> Optional[Union[bool, DataDict]]: + """Checks if the specified update is a message.""" + if ( # Only message updates should be handled. + update.channel_post + or update.message + or update.edited_channel_post + or update.edited_message + ): + return True + return False def __and__(self, other: 'BaseFilter') -> 'BaseFilter': - return MergedFilter(self, and_filter=other) + return _MergedFilter(self, and_filter=other) def __or__(self, other: 'BaseFilter') -> 'BaseFilter': - return MergedFilter(self, or_filter=other) + return _MergedFilter(self, or_filter=other) def __xor__(self, other: 'BaseFilter') -> 'BaseFilter': - return XORFilter(self, other) + return _XORFilter(self, other) def __invert__(self) -> 'BaseFilter': - return InvertedFilter(self) + return _InvertedFilter(self) @property def data_filter(self) -> bool: @@ -147,26 +164,22 @@ class BaseFilter(ABC): self._data_filter = value @property - def name(self) -> Optional[str]: + def name(self) -> str: return self._name @name.setter - def name(self, name: Optional[str]) -> None: - self._name = name # pylint: disable=assigning-non-slot + def name(self, name: str) -> None: + self._name = name def __repr__(self) -> str: - # We do this here instead of in a __init__ so filter don't have to call __init__ or super() - if self.name is None: - self.name = self.__class__.__name__ return self.name class MessageFilter(BaseFilter): """Base class for all Message Filters. In contrast to :class:`UpdateFilter`, the object passed - to :meth:`filter` is ``update.effective_message``. + to :meth:`filter` is :obj:`telegram.Update.effective_message`. - Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom - filters. + Please see :class:`BaseFilter` for details on how to create custom filters. Attributes: name (:obj:`str`): Name for this filter. Defaults to the type of filter. @@ -179,8 +192,8 @@ class MessageFilter(BaseFilter): __slots__ = () - def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: - return self.filter(update.effective_message) + def check_update(self, update: Update) -> Optional[Union[bool, DataDict]]: + return self.filter(update.effective_message) if super().check_update(update) else False @abstractmethod def filter(self, message: Message) -> Optional[Union[bool, DataDict]]: @@ -197,8 +210,8 @@ class MessageFilter(BaseFilter): class UpdateFilter(BaseFilter): """Base class for all Update Filters. In contrast to :class:`MessageFilter`, the object - passed to :meth:`filter` is ``update``, which allows to create filters like - :attr:`Filters.update.edited_message`. + passed to :meth:`filter` is an instance of :class:`telegram.Update`, which allows to create + filters like :attr:`telegram.ext.filters.UpdateType.EDITED_MESSAGE`. Please see :class:`telegram.ext.filters.BaseFilter` for details on how to create custom filters. @@ -214,8 +227,8 @@ class UpdateFilter(BaseFilter): __slots__ = () - def __call__(self, update: Update) -> Optional[Union[bool, DataDict]]: - return self.filter(update) + def check_update(self, update: Update) -> Optional[Union[bool, DataDict]]: + return self.filter(update) if super().check_update(update) else False @abstractmethod def filter(self, update: Update) -> Optional[Union[bool, DataDict]]: @@ -230,7 +243,7 @@ class UpdateFilter(BaseFilter): """ -class InvertedFilter(UpdateFilter): +class _InvertedFilter(UpdateFilter): """Represents a filter that has been inverted. Args: @@ -238,24 +251,25 @@ class InvertedFilter(UpdateFilter): """ - __slots__ = ('f',) + __slots__ = ('inv_filter',) def __init__(self, f: BaseFilter): - self.f = f + super().__init__() + self.inv_filter = f def filter(self, update: Update) -> bool: - return not bool(self.f(update)) + return not bool(self.inv_filter.check_update(update)) @property def name(self) -> str: - return f"" + return f"" @name.setter def name(self, name: str) -> NoReturn: - raise RuntimeError('Cannot set name for InvertedFilter') + raise RuntimeError('Cannot set name for combined filters.') -class MergedFilter(UpdateFilter): +class _MergedFilter(UpdateFilter): """Represents a filter consisting of two other filters. Args: @@ -270,6 +284,7 @@ class MergedFilter(UpdateFilter): def __init__( self, base_filter: BaseFilter, and_filter: BaseFilter = None, or_filter: BaseFilter = None ): + super().__init__() self.base_filter = base_filter if self.base_filter.data_filter: self.data_filter = True @@ -303,13 +318,13 @@ class MergedFilter(UpdateFilter): # pylint: disable=too-many-return-statements def filter(self, update: Update) -> Union[bool, DataDict]: - base_output = self.base_filter(update) + base_output = self.base_filter.check_update(update) # We need to check if the filters are data filters and if so return the merged data. # If it's not a data filter or an or_filter but no matches return bool if self.and_filter: - # And filter needs to short circuit if base is falsey + # And filter needs to short circuit if base is falsy if base_output: - comp_output = self.and_filter(update) + comp_output = self.and_filter.check_update(update) if comp_output: if self.data_filter: merged = self._merge(base_output, comp_output) @@ -317,13 +332,13 @@ class MergedFilter(UpdateFilter): return merged return True elif self.or_filter: - # Or filter needs to short circuit if base is truthey + # Or filter needs to short circuit if base is truthy if base_output: if self.data_filter: return base_output return True - comp_output = self.or_filter(update) + comp_output = self.or_filter.check_update(update) if comp_output: if self.data_filter: return comp_output @@ -339,10 +354,10 @@ class MergedFilter(UpdateFilter): @name.setter def name(self, name: str) -> NoReturn: - raise RuntimeError('Cannot set name for MergedFilter') + raise RuntimeError('Cannot set name for combined filters.') -class XORFilter(UpdateFilter): +class _XORFilter(UpdateFilter): """Convenience filter acting as wrapper for :class:`MergedFilter` representing the an XOR gate for two filters. @@ -355,12 +370,13 @@ class XORFilter(UpdateFilter): __slots__ = ('base_filter', 'xor_filter', 'merged_filter') def __init__(self, base_filter: BaseFilter, xor_filter: BaseFilter): + super().__init__() self.base_filter = base_filter self.xor_filter = xor_filter self.merged_filter = (base_filter & ~xor_filter) | (~base_filter & xor_filter) def filter(self, update: Update) -> Optional[Union[bool, DataDict]]: - return self.merged_filter(update) + return self.merged_filter.check_update(update) @property def name(self) -> str: @@ -368,1928 +384,1810 @@ class XORFilter(UpdateFilter): @name.setter def name(self, name: str) -> NoReturn: - raise RuntimeError('Cannot set name for XORFilter') + raise RuntimeError('Cannot set name for combined filters.') -class _DiceEmoji(MessageFilter): - __slots__ = ('emoji',) - - def __init__(self, emoji: str = None, name: str = None): - self.name = f'Filters.dice.{name}' if name else 'Filters.dice' - self.emoji = emoji - - class _DiceValues(MessageFilter): - __slots__ = ('values', 'emoji') - - def __init__( - self, - values: SLT[int], - name: str, - emoji: str = None, - ): - self.values = [values] if isinstance(values, int) else values - self.emoji = emoji - self.name = f'{name}({values})' - - def filter(self, message: Message) -> bool: - if message.dice and message.dice.value in self.values: - if self.emoji: - return message.dice.emoji == self.emoji - return True - return False - - def __call__( # type: ignore[override] - self, update: Union[Update, List[int], Tuple[int]] - ) -> Union[bool, '_DiceValues']: - if isinstance(update, Update): - return self.filter(update.effective_message) - return self._DiceValues(update, self.name, emoji=self.emoji) +class _All(MessageFilter): + __slots__ = () def filter(self, message: Message) -> bool: - if bool(message.dice): - if self.emoji: - return message.dice.emoji == self.emoji - return True - return False + return True -class Filters: - """Predefined filters for use as the ``filter`` argument of - :class:`telegram.ext.MessageHandler`. +ALL = _All(name="filters.ALL") +"""All Messages.""" + + +class _Animation(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.animation) + + +ANIMATION = _Animation(name="filters.ANIMATION") +"""Messages that contain :attr:`telegram.Message.animation`.""" + + +class _Attachment(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.effective_attachment) + + +ATTACHMENT = _Attachment(name="filters.ATTACHMENT") +"""Messages that contain :meth:`telegram.Message.effective_attachment`. + +.. versionadded:: 13.6""" + + +class _Audio(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.audio) + + +AUDIO = _Audio(name="filters.AUDIO") +"""Messages that contain :attr:`telegram.Message.audio`.""" + + +class Caption(MessageFilter): + """Messages with a caption. If a list of strings is passed, it filters messages to only + allow those whose caption is appearing in the given list. Examples: - Use ``MessageHandler(Filters.video, callback_method)`` to filter all video - messages. Use ``MessageHandler(Filters.contact, callback_method)`` for all contacts. etc. + ``MessageHandler(filters.Caption(['PTB rocks!', 'PTB'], callback_method_2)`` + .. seealso:: + :attr:`telegram.ext.filters.CAPTION` + + Args: + strings (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which captions to allow. Only + exact matches are allowed. If not specified, will allow any message with a caption. + """ + + __slots__ = ('strings',) + + def __init__(self, strings: Union[List[str], Tuple[str, ...]] = None): + self.strings = strings + super().__init__(name=f'filters.Caption({strings})' if strings else 'filters.CAPTION') + + def filter(self, message: Message) -> bool: + if self.strings is None: + return bool(message.caption) + return message.caption in self.strings if message.caption else False + + +CAPTION = Caption() +"""Shortcut for :class:`telegram.ext.filters.Caption()`. + +Examples: + To allow any caption, simply use ``MessageHandler(filters.CAPTION, callback_method)``. +""" + + +class CaptionEntity(MessageFilter): + """ + Filters media messages to only allow those which have a :class:`telegram.MessageEntity` + where their :class:`~telegram.MessageEntity.type` matches `entity_type`. + + Examples: + ``MessageHandler(filters.CaptionEntity("hashtag"), callback_method)`` + + Args: + entity_type (:obj:`str`): Caption Entity type to check for. All types can be found as + constants in :class:`telegram.MessageEntity`. + + """ + + __slots__ = ('entity_type',) + + def __init__(self, entity_type: str): + self.entity_type = entity_type + super().__init__(name=f'filters.CaptionEntity({self.entity_type})') + + def filter(self, message: Message) -> bool: + return any(entity.type == self.entity_type for entity in message.caption_entities) + + +class CaptionRegex(MessageFilter): + """ + Filters updates by searching for an occurrence of ``pattern`` in the message caption. + + This filter works similarly to :class:`Regex`, with the only exception being that + it applies to the message caption instead of the text. + + Examples: + Use ``MessageHandler(filters.PHOTO & filters.CaptionRegex(r'help'), callback)`` + to capture all photos with caption containing the word 'help'. + + Note: + This filter will not work on simple text messages, but only on media with caption. + + Args: + pattern (:obj:`str` | :obj:`re.Pattern`): The regex pattern. + """ + + __slots__ = ('pattern',) + + def __init__(self, pattern: Union[str, Pattern]): + if isinstance(pattern, str): + pattern = re.compile(pattern) + self.pattern: Pattern = pattern + super().__init__(name=f'filters.CaptionRegex({self.pattern})', data_filter=True) + + def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: + if message.caption: + match = self.pattern.search(message.caption) + if match: + return {'matches': [match]} + return {} + + +class _ChatUserBaseFilter(MessageFilter, ABC): + __slots__ = ( + '_chat_id_name', + '_username_name', + 'allow_empty', + '__lock', + '_chat_ids', + '_usernames', + ) + + def __init__( + self, + chat_id: SLT[int] = None, + username: SLT[str] = None, + allow_empty: bool = False, + ): + super().__init__() + self._chat_id_name = 'chat_id' + self._username_name = 'username' + self.allow_empty = allow_empty + self.__lock = Lock() + + self._chat_ids: Set[int] = set() + self._usernames: Set[str] = set() + + self._set_chat_ids(chat_id) + self._set_usernames(username) + + @abstractmethod + def get_chat_or_user(self, message: Message) -> Union[TGChat, TGUser, None]: + ... + + @staticmethod + def _parse_chat_id(chat_id: SLT[int]) -> Set[int]: + if chat_id is None: + return set() + if isinstance(chat_id, int): + return {chat_id} + return set(chat_id) + + @staticmethod + def _parse_username(username: SLT[str]) -> Set[str]: + if username is None: + return set() + if isinstance(username, str): + return {username[1:] if username.startswith('@') else username} + return {chat[1:] if chat.startswith('@') else chat for chat in username} + + def _set_chat_ids(self, chat_id: SLT[int]) -> None: + with self.__lock: + if chat_id and self._usernames: + raise RuntimeError( + f"Can't set {self._chat_id_name} in conjunction with (already set) " + f"{self._username_name}s." + ) + self._chat_ids = self._parse_chat_id(chat_id) + + def _set_usernames(self, username: SLT[str]) -> None: + with self.__lock: + if username and self._chat_ids: + raise RuntimeError( + f"Can't set {self._username_name} in conjunction with (already set) " + f"{self._chat_id_name}s." + ) + self._usernames = self._parse_username(username) + + @property + def chat_ids(self) -> FrozenSet[int]: + with self.__lock: + return frozenset(self._chat_ids) + + @chat_ids.setter + def chat_ids(self, chat_id: SLT[int]) -> None: + self._set_chat_ids(chat_id) + + @property + def usernames(self) -> FrozenSet[str]: + """Which username(s) to allow through. + + Warning: + :attr:`usernames` will give a *copy* of the saved usernames as :obj:`frozenset`. This + is to ensure thread safety. To add/remove a user, you should use :meth:`add_usernames`, + and :meth:`remove_usernames`. Only update the entire set by + ``filter.usernames = new_set``, if you are entirely sure that it is not causing race + conditions, as this will complete replace the current set of allowed users. + + Returns: + frozenset(:obj:`str`) + """ + with self.__lock: + return frozenset(self._usernames) + + @usernames.setter + def usernames(self, username: SLT[str]) -> None: + self._set_usernames(username) + + def add_usernames(self, username: SLT[str]) -> None: + """ + Add one or more chats to the allowed usernames. + + Args: + username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]): Which username(s) to + allow through. Leading ``'@'`` s in usernames will be discarded. + """ + with self.__lock: + if self._chat_ids: + raise RuntimeError( + f"Can't set {self._username_name} in conjunction with (already set) " + f"{self._chat_id_name}s." + ) + + parsed_username = self._parse_username(username) + self._usernames |= parsed_username + + def _add_chat_ids(self, chat_id: SLT[int]) -> None: + with self.__lock: + if self._usernames: + raise RuntimeError( + f"Can't set {self._chat_id_name} in conjunction with (already set) " + f"{self._username_name}s." + ) + + parsed_chat_id = self._parse_chat_id(chat_id) + + self._chat_ids |= parsed_chat_id + + def remove_usernames(self, username: SLT[str]) -> None: + """ + Remove one or more chats from allowed usernames. + + Args: + username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]): Which username(s) to + disallow through. Leading ``'@'`` s in usernames will be discarded. + """ + with self.__lock: + if self._chat_ids: + raise RuntimeError( + f"Can't set {self._username_name} in conjunction with (already set) " + f"{self._chat_id_name}s." + ) + + parsed_username = self._parse_username(username) + self._usernames -= parsed_username + + def _remove_chat_ids(self, chat_id: SLT[int]) -> None: + with self.__lock: + if self._usernames: + raise RuntimeError( + f"Can't set {self._chat_id_name} in conjunction with (already set) " + f"{self._username_name}s." + ) + parsed_chat_id = self._parse_chat_id(chat_id) + self._chat_ids -= parsed_chat_id + + def filter(self, message: Message) -> bool: + chat_or_user = self.get_chat_or_user(message) + if chat_or_user: + if self.chat_ids: + return chat_or_user.id in self.chat_ids + if self.usernames: + return bool(chat_or_user.username and chat_or_user.username in self.usernames) + return self.allow_empty + return False + + @property + def name(self) -> str: + return ( + f'filters.{self.__class__.__name__}(' + f'{", ".join(str(s) for s in (self.usernames or self.chat_ids))})' + ) + + @name.setter + def name(self, name: str) -> NoReturn: + raise RuntimeError(f'Cannot set name for filters.{self.__class__.__name__}') + + +class Chat(_ChatUserBaseFilter): + """Filters messages to allow only those which are from a specified chat ID or username. + + Examples: + ``MessageHandler(filters.Chat(-1234), callback_method)`` + + Warning: + :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This + is to ensure thread safety. To add/remove a chat, you should use :meth:`add_chat_ids`, and + :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids = new_set``, + if you are entirely sure that it is not causing race conditions, as this will complete + replace the current set of allowed chats. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): + Which chat ID(s) to allow through. + username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): + Which username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. + + Attributes: + chat_ids (set(:obj:`int`)): Which chat ID(s) to allow through. + allow_empty (:obj:`bool`): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. + + Raises: + RuntimeError: If ``chat_id`` and ``username`` are both present. """ __slots__ = () - class _All(MessageFilter): + def get_chat_or_user(self, message: Message) -> Optional[TGChat]: + return message.chat + + def add_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Add one or more chats to the allowed chat ids. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat ID(s) to allow + through. + """ + return super()._add_chat_ids(chat_id) + + def remove_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Remove one or more chats from allowed chat ids. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat ID(s) to + disallow through. + """ + return super()._remove_chat_ids(chat_id) + + +class _Chat(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.chat) + + +CHAT = _Chat(name="filters.CHAT") +"""This filter filters *any* message that has a :attr:`telegram.Message.chat`.""" + + +class ChatType: # A convenience namespace for Chat types. + """Subset for filtering the type of chat. + + Examples: + Use these filters like: ``filters.ChatType.CHANNEL`` or + ``filters.ChatType.SUPERGROUP`` etc. + + Note: + ``filters.ChatType`` itself is *not* a filter, but just a convenience namespace. + """ + + __slots__ = () + + class _Channel(MessageFilter): __slots__ = () - name = 'Filters.all' def filter(self, message: Message) -> bool: - return True + return message.chat.type == TGChat.CHANNEL - all = _All() - """All Messages.""" + CHANNEL = _Channel(name="filters.ChatType.CHANNEL") + """Updates from channel.""" - class _Text(MessageFilter): + class _Group(MessageFilter): __slots__ = () - name = 'Filters.text' - class _TextStrings(MessageFilter): - __slots__ = ('strings',) + def filter(self, message: Message) -> bool: + return message.chat.type == TGChat.GROUP - def __init__(self, strings: Union[List[str], Tuple[str]]): - self.strings = strings - self.name = f'Filters.text({strings})' + GROUP = _Group(name="filters.ChatType.GROUP") + """Updates from group.""" - def filter(self, message: Message) -> bool: - if message.text: - return message.text in self.strings + class _Groups(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return message.chat.type in [TGChat.GROUP, TGChat.SUPERGROUP] + + GROUPS = _Groups(name="filters.ChatType.GROUPS") + """Update from group *or* supergroup.""" + + class _Private(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return message.chat.type == TGChat.PRIVATE + + PRIVATE = _Private(name="filters.ChatType.PRIVATE") + """Update from private chats.""" + + class _SuperGroup(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return message.chat.type == TGChat.SUPERGROUP + + SUPERGROUP = _SuperGroup(name="filters.ChatType.SUPERGROUP") + """Updates from supergroup.""" + + +class Command(MessageFilter): + """ + Messages with a :attr:`telegram.MessageEntity.BOT_COMMAND`. By default only allows + messages `starting` with a bot command. Pass :obj:`False` to also allow messages that contain a + bot command `anywhere` in the text. + + Examples: + ``MessageHandler(filters.Command(False), command_anywhere_callback)`` + + .. seealso:: + :attr:`telegram.ext.filters.COMMAND`. + + Note: + :attr:`telegram.ext.filters.TEXT` also accepts messages containing a command. + + Args: + only_start (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot + command. Defaults to :obj:`True`. + """ + + __slots__ = ('only_start',) + + def __init__(self, only_start: bool = True): + self.only_start = only_start + super().__init__(f'filters.Command({only_start})' if not only_start else 'filters.COMMAND') + + def filter(self, message: Message) -> bool: + if not message.entities: + return False + + first = message.entities[0] + + if self.only_start: + return bool(first.type == MessageEntity.BOT_COMMAND and first.offset == 0) + return bool(any(e.type == MessageEntity.BOT_COMMAND for e in message.entities)) + + +COMMAND = Command() +"""Shortcut for :class:`telegram.ext.filters.Command()`. + +Examples: + To allow messages starting with a command use + ``MessageHandler(filters.COMMAND, command_at_start_callback)``. +""" + + +class _Contact(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.contact) + + +CONTACT = _Contact(name="filters.CONTACT") +"""Messages that contain :attr:`telegram.Message.contact`.""" + + +class _Dice(MessageFilter): + __slots__ = ('emoji', 'values') + + def __init__(self, values: SLT[int] = None, emoji: DiceEmojiEnum = None): + super().__init__() + self.emoji = emoji + self.values = [values] if isinstance(values, int) else values + + if emoji: # for filters.Dice.BASKETBALL + self.name = f"filters.Dice.{emoji.name}" + if self.values and emoji: # for filters.Dice.Dice(4) SLOT_MACHINE -> SlotMachine + self.name = f"filters.Dice.{emoji.name.title().replace('_', '')}({self.values})" + elif values: # for filters.Dice(4) + self.name = f"filters.Dice({self.values})" + else: + self.name = "filters.Dice.ALL" + + def filter(self, message: Message) -> bool: + if not message.dice: # no dice + return False + + if self.emoji: + emoji_match = message.dice.emoji == self.emoji + if self.values: + return message.dice.value in self.values and emoji_match # emoji and value + return emoji_match # emoji, no value + return message.dice.value in self.values if self.values else True # no emoji, only value + + +class Dice(_Dice): + """Dice Messages. If an integer or a list of integers is passed, it filters messages to only + allow those whose dice value is appearing in the given list. + + .. versionadded:: 13.4 + + Examples: + To allow any dice message, simply use + ``MessageHandler(filters.Dice.ALL, callback_method)``. + + To allow any dice message, but with value 3 `or` 4, use + ``MessageHandler(filters.Dice([3, 4]), callback_method)`` + + To allow only dice messages with the emoji 🎲, but any value, use + ``MessageHandler(filters.Dice.DICE, callback_method)``. + + To allow only dice messages with the emoji 🎯 and with value 6, use + ``MessageHandler(filters.Dice.Darts(6), callback_method)``. + + To allow only dice messages with the emoji ⚽ and with value 5 `or` 6, use + ``MessageHandler(filters.Dice.Football([5, 6]), callback_method)``. + + Note: + Dice messages don't have text. If you want to filter either text or dice messages, use + ``filters.TEXT | filters.Dice.ALL``. + + Args: + values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): + Which values to allow. If not specified, will allow the specified dice message. + """ + + __slots__ = () + + ALL = _Dice() + """Dice messages with any value and any emoji.""" + + class Basketball(_Dice): + """Dice messages with the emoji 🏀. Supports passing a list of integers. + + Args: + values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow. + """ + + __slots__ = () + + def __init__(self, values: SLT[int]): + super().__init__(values, emoji=DiceEmojiEnum.BASKETBALL) + + BASKETBALL = _Dice(emoji=DiceEmojiEnum.BASKETBALL) + """Dice messages with the emoji 🏀. Matches any dice value.""" + + class Bowling(_Dice): + """Dice messages with the emoji 🎳. Supports passing a list of integers. + + Args: + values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow. + """ + + __slots__ = () + + def __init__(self, values: SLT[int]): + super().__init__(values, emoji=DiceEmojiEnum.BOWLING) + + BOWLING = _Dice(emoji=DiceEmojiEnum.BOWLING) + """Dice messages with the emoji 🎳. Matches any dice value.""" + + class Darts(_Dice): + """Dice messages with the emoji 🎯. Supports passing a list of integers. + + Args: + values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow. + """ + + __slots__ = () + + def __init__(self, values: SLT[int]): + super().__init__(values, emoji=DiceEmojiEnum.DARTS) + + DARTS = _Dice(emoji=DiceEmojiEnum.DARTS) + """Dice messages with the emoji 🎯. Matches any dice value.""" + + class Dice(_Dice): + """Dice messages with the emoji 🎲. Supports passing a list of integers. + + Args: + values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow. + """ + + __slots__ = () + + def __init__(self, values: SLT[int]): + super().__init__(values, emoji=DiceEmojiEnum.DICE) + + DICE = _Dice(emoji=DiceEmojiEnum.DICE) # skipcq: PTC-W0052 + """Dice messages with the emoji 🎲. Matches any dice value.""" + + class Football(_Dice): + """Dice messages with the emoji ⚽. Supports passing a list of integers. + + Args: + values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow. + """ + + __slots__ = () + + def __init__(self, values: SLT[int]): + super().__init__(values, emoji=DiceEmojiEnum.FOOTBALL) + + FOOTBALL = _Dice(emoji=DiceEmojiEnum.FOOTBALL) + """Dice messages with the emoji ⚽. Matches any dice value.""" + + class SlotMachine(_Dice): + """Dice messages with the emoji 🎰. Supports passing a list of integers. + + Args: + values (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which values to allow. + """ + + __slots__ = () + + def __init__(self, values: SLT[int]): + super().__init__(values, emoji=DiceEmojiEnum.SLOT_MACHINE) + + SLOT_MACHINE = _Dice(emoji=DiceEmojiEnum.SLOT_MACHINE) + """Dice messages with the emoji 🎰. Matches any dice value.""" + + +class Document(MessageFilter): + """ + Subset for messages containing a document/file. + + Examples: + Use these filters like: ``filters.Document.MP3``, + ``filters.Document.MimeType("text/plain")`` etc. Or use just ``filters.DOCUMENT`` for all + document messages. + """ + + __slots__ = () + + class Category(MessageFilter): + """Filters documents by their category in the mime-type attribute. + + Args: + category (:obj:`str`): Category of the media you want to filter. + + Example: + ``filters.Document.Category('audio/')`` returns :obj:`True` for all types + of audio sent as a file, for example ``'audio/mpeg'`` or ``'audio/x-wav'``. + + Note: + This Filter only filters by the mime_type of the document, it doesn't check the + validity of the document. The user can manipulate the mime-type of a message and + send media with wrong types that don't fit to this handler. + """ + + __slots__ = ('_category',) + + def __init__(self, category: str): + self._category = category + super().__init__(name=f"filters.Document.Category('{self._category}')") + + def filter(self, message: Message) -> bool: + if message.document: + return message.document.mime_type.startswith(self._category) + return False + + APPLICATION = Category('application/') + """Use as ``filters.Document.APPLICATION``.""" + AUDIO = Category('audio/') + """Use as ``filters.Document.AUDIO``.""" + IMAGE = Category('image/') + """Use as ``filters.Document.IMAGE``.""" + VIDEO = Category('video/') + """Use as ``filters.Document.VIDEO``.""" + TEXT = Category('text/') + """Use as ``filters.Document.TEXT``.""" + + class FileExtension(MessageFilter): + """This filter filters documents by their file ending/extension. + + Args: + file_extension (:obj:`str` | :obj:`None`): Media file extension you want to filter. + case_sensitive (:obj:`bool`, optional): Pass :obj:`True` to make the filter case + sensitive. Default: :obj:`False`. + + Example: + * ``filters.Document.FileExtension("jpg")`` + filters files with extension ``".jpg"``. + * ``filters.Document.FileExtension(".jpg")`` + filters files with extension ``"..jpg"``. + * ``filters.Document.FileExtension("Dockerfile", case_sensitive=True)`` + filters files with extension ``".Dockerfile"`` minding the case. + * ``filters.Document.FileExtension(None)`` + filters files without a dot in the filename. + + Note: + * This Filter only filters by the file ending/extension of the document, + it doesn't check the validity of document. + * The user can manipulate the file extension of a document and + send media with wrong types that don't fit to this handler. + * Case insensitive by default, + you may change this with the flag ``case_sensitive=True``. + * Extension should be passed without leading dot + unless it's a part of the extension. + * Pass :obj:`None` to filter files with no extension, + i.e. without a dot in the filename. + """ + + __slots__ = ('_file_extension', 'is_case_sensitive') + + def __init__(self, file_extension: Optional[str], case_sensitive: bool = False): + super().__init__() + self.is_case_sensitive = case_sensitive + if file_extension is None: + self._file_extension = None + self.name = "filters.Document.FileExtension(None)" + elif self.is_case_sensitive: + self._file_extension = f".{file_extension}" + self.name = ( + f"filters.Document.FileExtension({file_extension!r}, case_sensitive=True)" + ) + else: + self._file_extension = f".{file_extension}".lower() + self.name = f"filters.Document.FileExtension({file_extension.lower()!r})" + + def filter(self, message: Message) -> bool: + if message.document is None: return False + if self._file_extension is None: + return "." not in message.document.file_name + if self.is_case_sensitive: + filename = message.document.file_name + else: + filename = message.document.file_name.lower() + return filename.endswith(self._file_extension) - def __call__( # type: ignore[override] - self, update: Union[Update, List[str], Tuple[str]] - ) -> Union[bool, '_TextStrings']: - if isinstance(update, Update): - return self.filter(update.effective_message) - return self._TextStrings(update) + class MimeType(MessageFilter): + """This Filter filters documents by their mime-type attribute. + + Args: + mimetype (:obj:`str`): The mimetype to filter. + + Example: + ``filters.Document.MimeType('audio/mpeg')`` filters all audio in `.mp3` format. + + Note: + This Filter only filters by the mime_type of the document, it doesn't check the + validity of document. The user can manipulate the mime-type of a message and + send media with wrong types that don't fit to this handler. + """ + + __slots__ = ('mimetype',) + + def __init__(self, mimetype: str): + self.mimetype = mimetype # skipcq: PTC-W0052 + super().__init__(name=f"filters.Document.MimeType('{self.mimetype}')") def filter(self, message: Message) -> bool: - return bool(message.text) + if message.document: + return message.document.mime_type == self.mimetype + return False - text = _Text() + APK = MimeType('application/vnd.android.package-archive') + """Use as ``filters.Document.APK``.""" + DOC = MimeType(mimetypes.types_map.get('.doc')) + """Use as ``filters.Document.DOC``.""" + DOCX = MimeType('application/vnd.openxmlformats-officedocument.wordprocessingml.document') + """Use as ``filters.Document.DOCX``.""" + EXE = MimeType(mimetypes.types_map.get('.exe')) + """Use as ``filters.Document.EXE``.""" + MP4 = MimeType(mimetypes.types_map.get('.mp4')) + """Use as ``filters.Document.MP4``.""" + GIF = MimeType(mimetypes.types_map.get('.gif')) + """Use as ``filters.Document.GIF``.""" + JPG = MimeType(mimetypes.types_map.get('.jpg')) + """Use as ``filters.Document.JPG``.""" + MP3 = MimeType(mimetypes.types_map.get('.mp3')) + """Use as ``filters.Document.MP3``.""" + PDF = MimeType(mimetypes.types_map.get('.pdf')) + """Use as ``filters.Document.PDF``.""" + PY = MimeType(mimetypes.types_map.get('.py')) + """Use as ``filters.Document.PY``.""" + SVG = MimeType(mimetypes.types_map.get('.svg')) + """Use as ``filters.Document.SVG``.""" + TXT = MimeType(mimetypes.types_map.get('.txt')) + """Use as ``filters.Document.TXT``.""" + TARGZ = MimeType('application/x-compressed-tar') + """Use as ``filters.Document.TARGZ``.""" + WAV = MimeType(mimetypes.types_map.get('.wav')) + """Use as ``filters.Document.WAV``.""" + XML = MimeType(mimetypes.types_map.get('.xml')) + """Use as ``filters.Document.XML``.""" + ZIP = MimeType(mimetypes.types_map.get('.zip')) + """Use as ``filters.Document.ZIP``.""" + + def filter(self, message: Message) -> bool: + return bool(message.document) + + +DOCUMENT = Document(name="filters.DOCUMENT") +"""Shortcut for :class:`telegram.ext.filters.Document()`.""" + + +class Entity(MessageFilter): + """ + Filters messages to only allow those which have a :class:`telegram.MessageEntity` + where their :class:`~telegram.MessageEntity.type` matches `entity_type`. + + Examples: + ``MessageHandler(filters.Entity("hashtag"), callback_method)`` + + Args: + entity_type (:obj:`str`): Entity type to check for. All types can be found as constants + in :class:`telegram.MessageEntity`. + + """ + + __slots__ = ('entity_type',) + + def __init__(self, entity_type: str): + self.entity_type = entity_type + super().__init__(name=f'filters.Entity({self.entity_type})') + + def filter(self, message: Message) -> bool: + return any(entity.type == self.entity_type for entity in message.entities) + + +class _Forwarded(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.forward_date) + + +FORWARDED = _Forwarded(name="filters.FORWARDED") +"""Messages that contain :attr:`telegram.Message.forward_date`.""" + + +class ForwardedFrom(_ChatUserBaseFilter): + """Filters messages to allow only those which are forwarded from the specified chat ID(s) + or username(s) based on :attr:`telegram.Message.forward_from` and + :attr:`telegram.Message.forward_from_chat`. + + .. versionadded:: 13.5 + + Examples: + ``MessageHandler(filters.ForwardedFrom(chat_id=1234), callback_method)`` + + Note: + When a user has disallowed adding a link to their account while forwarding their + messages, this filter will *not* work since both + :attr:`telegram.Message.forwarded_from` and + :attr:`telegram.Message.forwarded_from_chat` are :obj:`None`. However, this behaviour + is undocumented and might be changed by Telegram. + + Warning: + :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This + is to ensure thread safety. To add/remove a chat, you should use :meth:`add_chat_ids`, and + :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids = new_set``, if + you are entirely sure that it is not causing race conditions, as this will complete replace + the current set of allowed chats. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): + Which chat/user ID(s) to allow through. + username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): + Which username(s) to allow through. Leading ``'@'`` s in usernames will be + discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. + + Attributes: + chat_ids (set(:obj:`int`)): Which chat/user ID(s) to allow through. + allow_empty (:obj:`bool`): Whether updates should be processed, if no chat + is specified in :attr:`chat_ids` and :attr:`usernames`. + + Raises: + RuntimeError: If both ``chat_id`` and ``username`` are present. + """ + + __slots__ = () + + def get_chat_or_user(self, message: Message) -> Union[TGUser, TGChat, None]: + return message.forward_from or message.forward_from_chat + + def add_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Add one or more chats to the allowed chat ids. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat/user ID(s) to + allow through. + """ + return super()._add_chat_ids(chat_id) + + def remove_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Remove one or more chats from allowed chat ids. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which chat/user ID(s) to + disallow through. + """ + return super()._remove_chat_ids(chat_id) + + +class _Game(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.game) + + +GAME = _Game(name="filters.GAME") +"""Messages that contain :attr:`telegram.Message.game`.""" + + +class _HasProtectedContent(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.has_protected_content) + + +HAS_PROTECTED_CONTENT = _HasProtectedContent(name='Filters.has_protected_content') +"""Messages that contain :attr:`telegram.Message.has_protected_content`. + + .. versionadded:: 13.9 +""" + + +class _Invoice(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.invoice) + + +INVOICE = _Invoice(name="filters.INVOICE") +"""Messages that contain :attr:`telegram.Message.invoice`.""" + + +class _IsAutomaticForward(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.is_automatic_forward) + + +IS_AUTOMATIC_FORWARD = _IsAutomaticForward(name='Filters.is_automatic_forward') +"""Messages that contain :attr:`telegram.Message.is_automatic_forward`. + + .. versionadded:: 13.9 +""" + + +class Language(MessageFilter): + """Filters messages to only allow those which are from users with a certain language code. + + Note: + According to official Telegram Bot API documentation, not every single user has the + `language_code` attribute. Do not count on this filter working on all users. + + Examples: + ``MessageHandler(filters.Language("en"), callback_method)`` + + Args: + lang (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]): + Which language code(s) to allow through. + This will be matched using :obj:`str.startswith` meaning that + 'en' will match both 'en_US' and 'en_GB'. + + """ + + __slots__ = ('lang',) + + def __init__(self, lang: SLT[str]): + if isinstance(lang, str): + lang = cast(str, lang) + self.lang = [lang] + else: + lang = cast(List[str], lang) + self.lang = lang + super().__init__(name=f"filters.Language({self.lang})") + + def filter(self, message: Message) -> bool: + return bool( + message.from_user.language_code + and any(message.from_user.language_code.startswith(x) for x in self.lang) + ) + + +class _Location(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.location) + + +LOCATION = _Location(name="filters.LOCATION") +"""Messages that contain :attr:`telegram.Message.location`.""" + + +class _PassportData(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.passport_data) + + +PASSPORT_DATA = _PassportData(name="filters.PASSPORT_DATA") +"""Messages that contain :attr:`telegram.Message.passport_data`.""" + + +class _Photo(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.photo) + + +PHOTO = _Photo("filters.PHOTO") +"""Messages that contain :attr:`telegram.Message.photo`.""" + + +class _Poll(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.poll) + + +POLL = _Poll(name="filters.POLL") +"""Messages that contain :attr:`telegram.Message.poll`.""" + + +class Regex(MessageFilter): + """ + Filters updates by searching for an occurrence of ``pattern`` in the message text. + The :obj:`re.search()` function is used to determine whether an update should be filtered. + + Refer to the documentation of the :obj:`re` module for more information. + + To get the groups and groupdict matched, see :attr:`telegram.ext.CallbackContext.matches`. + + Examples: + Use ``MessageHandler(filters.Regex(r'help'), callback)`` to capture all messages that + contain the word 'help'. You can also use + ``MessageHandler(filters.Regex(re.compile(r'help', re.IGNORECASE)), callback)`` if + you want your pattern to be case insensitive. This approach is recommended + if you need to specify flags on your pattern. + + Note: + Filters use the same short circuiting logic as python's `and`, `or` and `not`. + This means that for example: + + >>> filters.Regex(r'(a?x)') | filters.Regex(r'(b?x)') + + With a :attr:`telegram.Message.text` of `x`, will only ever return the matches for the + first filter, since the second one is never evaluated. + + Args: + pattern (:obj:`str` | :obj:`re.Pattern`): The regex pattern. + """ + + __slots__ = ('pattern',) + + def __init__(self, pattern: Union[str, Pattern]): + if isinstance(pattern, str): + pattern = re.compile(pattern) + self.pattern: Pattern = pattern + super().__init__(name=f'filters.Regex({self.pattern})', data_filter=True) + + def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: + if message.text: + match = self.pattern.search(message.text) + if match: + return {'matches': [match]} + return {} + + +class _Reply(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.reply_to_message) + + +REPLY = _Reply(name="filters.REPLY") +"""Messages that contain :attr:`telegram.Message.reply_to_message`.""" + + +class _SenderChat(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.sender_chat) + + +class SenderChat(_ChatUserBaseFilter): + """Filters messages to allow only those which are from a specified sender chat's chat ID or + username. + + Examples: + * To filter for messages sent to a group by a channel with ID + ``-1234``, use ``MessageHandler(filters.SenderChat(-1234), callback_method)``. + * To filter for messages of anonymous admins in a super group with username + ``@anonymous``, use + ``MessageHandler(filters.SenderChat(username='anonymous'), callback_method)``. + * To filter for messages sent to a group by *any* channel, use + ``MessageHandler(filters.SenderChat.CHANNEL, callback_method)``. + * To filter for messages of anonymous admins in *any* super group, use + ``MessageHandler(filters.SenderChat.SUPERGROUP, callback_method)``. + * To filter for messages forwarded to a discussion group from *any* channel or of anonymous + admins in *any* super group, use ``MessageHandler(filters.SenderChat.ALL, callback)`` + + Note: + Remember, ``sender_chat`` is also set for messages in a channel as the channel itself, + so when your bot is an admin in a channel and the linked discussion group, you would + receive the message twice (once from inside the channel, once inside the discussion + group). Since v13.9, the field :attr:`telegram.Message.is_automatic_forward` will be + :obj:`True` for the discussion group message. + + .. seealso:: :attr:`Filters.is_automatic_forward` + + Warning: + :attr:`chat_ids` will return a *copy* of the saved chat ids as :obj:`frozenset`. This + is to ensure thread safety. To add/remove a chat, you should use :meth:`add_chat_ids`, and + :meth:`remove_chat_ids`. Only update the entire set by ``filter.chat_ids = new_set``, if + you are entirely sure that it is not causing race conditions, as this will complete replace + the current set of allowed chats. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): + Which sender chat chat ID(s) to allow through. + username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): + Which sender chat username(s) to allow through. + Leading ``'@'`` s in usernames will be discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender + chat is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. + + Attributes: + chat_ids (set(:obj:`int`)): Which sender chat chat ID(s) to allow through. + allow_empty (:obj:`bool`): Whether updates should be processed, if no sender chat is + specified in :attr:`chat_ids` and :attr:`usernames`. + + Raises: + RuntimeError: If both ``chat_id`` and ``username`` are present. + """ + + __slots__ = () + + class _CHANNEL(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + if message.sender_chat: + return message.sender_chat.type == TGChat.CHANNEL + return False + + class _SUPERGROUP(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + if message.sender_chat: + return message.sender_chat.type == TGChat.SUPERGROUP + return False + + ALL = _SenderChat(name="filters.SenderChat.ALL") + """All messages with a :attr:`telegram.Message.sender_chat`.""" + SUPER_GROUP = _SUPERGROUP(name="filters.SenderChat.SUPER_GROUP") + """Messages whose sender chat is a super group.""" + CHANNEL = _CHANNEL(name="filters.SenderChat.CHANNEL") + """Messages whose sender chat is a channel.""" + + def add_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Add one or more sender chats to the allowed chat ids. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which sender chat ID(s) to + allow through. + """ + return super()._add_chat_ids(chat_id) + + def get_chat_or_user(self, message: Message) -> Optional[TGChat]: + return message.sender_chat + + def remove_chat_ids(self, chat_id: SLT[int]) -> None: + """ + Remove one or more sender chats from allowed chat ids. + + Args: + chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which sender chat ID(s) to + disallow through. + """ + return super()._remove_chat_ids(chat_id) + + +class StatusUpdate: + """Subset for messages containing a status update. + + Examples: + Use these filters like: ``filters.StatusUpdate.NEW_CHAT_MEMBERS`` etc. Or use just + ``filters.StatusUpdate.ALL`` for all status update messages. + + Note: + ``filters.StatusUpdate`` itself is *not* a filter, but just a convenience namespace. + """ + + __slots__ = () + + class _All(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return bool( + StatusUpdate.NEW_CHAT_MEMBERS.check_update(update) + or StatusUpdate.LEFT_CHAT_MEMBER.check_update(update) + or StatusUpdate.NEW_CHAT_TITLE.check_update(update) + or StatusUpdate.NEW_CHAT_PHOTO.check_update(update) + or StatusUpdate.DELETE_CHAT_PHOTO.check_update(update) + or StatusUpdate.CHAT_CREATED.check_update(update) + or StatusUpdate.MESSAGE_AUTO_DELETE_TIMER_CHANGED.check_update(update) + or StatusUpdate.MIGRATE.check_update(update) + or StatusUpdate.PINNED_MESSAGE.check_update(update) + or StatusUpdate.CONNECTED_WEBSITE.check_update(update) + or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) + or StatusUpdate.VOICE_CHAT_SCHEDULED.check_update(update) + or StatusUpdate.VOICE_CHAT_STARTED.check_update(update) + or StatusUpdate.VOICE_CHAT_ENDED.check_update(update) + or StatusUpdate.VOICE_CHAT_PARTICIPANTS_INVITED.check_update(update) + ) + + ALL = _All(name="filters.StatusUpdate.ALL") + """Messages that contain any of the below.""" + + class _ChatCreated(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool( + message.group_chat_created + or message.supergroup_chat_created + or message.channel_chat_created + ) + + CHAT_CREATED = _ChatCreated(name="filters.StatusUpdate.CHAT_CREATED") + """Messages that contain :attr:`telegram.Message.group_chat_created`, + :attr:`telegram.Message.supergroup_chat_created` or + :attr:`telegram.Message.channel_chat_created`.""" + + class _ConnectedWebsite(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.connected_website) + + CONNECTED_WEBSITE = _ConnectedWebsite(name="filters.StatusUpdate.CONNECTED_WEBSITE") + """Messages that contain :attr:`telegram.Message.connected_website`.""" + + class _DeleteChatPhoto(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.delete_chat_photo) + + DELETE_CHAT_PHOTO = _DeleteChatPhoto(name="filters.StatusUpdate.DELETE_CHAT_PHOTO") + """Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" + + class _LeftChatMember(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.left_chat_member) + + LEFT_CHAT_MEMBER = _LeftChatMember(name="filters.StatusUpdate.LEFT_CHAT_MEMBER") + """Messages that contain :attr:`telegram.Message.left_chat_member`.""" + + class _MessageAutoDeleteTimerChanged(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.message_auto_delete_timer_changed) + + MESSAGE_AUTO_DELETE_TIMER_CHANGED = _MessageAutoDeleteTimerChanged( + "filters.StatusUpdate.MESSAGE_AUTO_DELETE_TIMER_CHANGED" + ) + """Messages that contain :attr:`telegram.Message.message_auto_delete_timer_changed` + + .. versionadded:: 13.4 + """ + + class _Migrate(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.migrate_from_chat_id or message.migrate_to_chat_id) + + MIGRATE = _Migrate(name="filters.StatusUpdate.MIGRATE") + """Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or + :attr:`telegram.Message.migrate_to_chat_id`.""" + + class _NewChatMembers(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.new_chat_members) + + NEW_CHAT_MEMBERS = _NewChatMembers(name="filters.StatusUpdate.NEW_CHAT_MEMBERS") + """Messages that contain :attr:`telegram.Message.new_chat_members`.""" + + class _NewChatPhoto(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.new_chat_photo) + + NEW_CHAT_PHOTO = _NewChatPhoto(name="filters.StatusUpdate.NEW_CHAT_PHOTO") + """Messages that contain :attr:`telegram.Message.new_chat_photo`.""" + + class _NewChatTitle(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.new_chat_title) + + NEW_CHAT_TITLE = _NewChatTitle(name="filters.StatusUpdate.NEW_CHAT_TITLE") + """Messages that contain :attr:`telegram.Message.new_chat_title`.""" + + class _PinnedMessage(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.pinned_message) + + PINNED_MESSAGE = _PinnedMessage(name="filters.StatusUpdate.PINNED_MESSAGE") + """Messages that contain :attr:`telegram.Message.pinned_message`.""" + + class _ProximityAlertTriggered(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.proximity_alert_triggered) + + PROXIMITY_ALERT_TRIGGERED = _ProximityAlertTriggered( + "filters.StatusUpdate.PROXIMITY_ALERT_TRIGGERED" + ) + """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" + + class _VoiceChatEnded(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_ended) + + VOICE_CHAT_ENDED = _VoiceChatEnded(name="filters.StatusUpdate.VOICE_CHAT_ENDED") + """Messages that contain :attr:`telegram.Message.voice_chat_ended`. + + .. versionadded:: 13.4 + """ + + class _VoiceChatScheduled(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_scheduled) + + VOICE_CHAT_SCHEDULED = _VoiceChatScheduled(name="filters.StatusUpdate.VOICE_CHAT_SCHEDULED") + """Messages that contain :attr:`telegram.Message.voice_chat_scheduled`. + + .. versionadded:: 13.5 + """ + + class _VoiceChatStarted(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_started) + + VOICE_CHAT_STARTED = _VoiceChatStarted(name="filters.StatusUpdate.VOICE_CHAT_STARTED") + """Messages that contain :attr:`telegram.Message.voice_chat_started`. + + .. versionadded:: 13.4 + """ + + class _VoiceChatParticipantsInvited(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.voice_chat_participants_invited) + + VOICE_CHAT_PARTICIPANTS_INVITED = _VoiceChatParticipantsInvited( + "filters.StatusUpdate.VOICE_CHAT_PARTICIPANTS_INVITED" + ) + """Messages that contain :attr:`telegram.Message.voice_chat_participants_invited`. + + .. versionadded:: 13.4 + """ + + +class _Sticker(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.sticker) + + +STICKER = _Sticker(name="filters.STICKER") +"""Messages that contain :attr:`telegram.Message.sticker`.""" + + +class _SuccessfulPayment(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.successful_payment) + + +SUCCESSFUL_PAYMENT = _SuccessfulPayment(name="filters.SUCCESSFUL_PAYMENT") +"""Messages that contain :attr:`telegram.Message.successful_payment`.""" + + +class Text(MessageFilter): """Text Messages. If a list of strings is passed, it filters messages to only allow those whose text is appearing in the given list. Examples: - To allow any text message, simply use - ``MessageHandler(Filters.text, callback_method)``. - A simple use case for passing a list is to allow only messages that were sent by a custom :class:`telegram.ReplyKeyboardMarkup`:: buttons = ['Start', 'Settings', 'Back'] markup = ReplyKeyboardMarkup.from_column(buttons) ... - MessageHandler(Filters.text(buttons), callback_method) + MessageHandler(filters.Text(buttons), callback_method) + + .. seealso:: + :attr:`telegram.ext.filters.TEXT` + Note: * Dice messages don't have text. If you want to filter either text or dice messages, use - ``Filters.text | Filters.dice``. + ``filters.TEXT | filters.Dice.ALL``. * Messages containing a command are accepted by this filter. Use - ``Filters.text & (~Filters.command)``, if you want to filter only text messages without + ``filters.TEXT & (~filters.COMMAND)``, if you want to filter only text messages without commands. Args: - update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only + strings (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only exact matches are allowed. If not specified, will allow any text message. """ - class _Caption(MessageFilter): - __slots__ = () - name = 'Filters.caption' + __slots__ = ('strings',) - class _CaptionStrings(MessageFilter): - __slots__ = ('strings',) + def __init__(self, strings: Union[List[str], Tuple[str, ...]] = None): + self.strings = strings + super().__init__(name=f'filters.Text({strings})' if strings else 'filters.TEXT') - def __init__(self, strings: Union[List[str], Tuple[str]]): - self.strings = strings - self.name = f'Filters.caption({strings})' + def filter(self, message: Message) -> bool: + if self.strings is None: + return bool(message.text) + return message.text in self.strings if message.text else False - def filter(self, message: Message) -> bool: - if message.caption: - return message.caption in self.strings - return False - def __call__( # type: ignore[override] - self, update: Union[Update, List[str], Tuple[str]] - ) -> Union[bool, '_CaptionStrings']: - if isinstance(update, Update): - return self.filter(update.effective_message) - return self._CaptionStrings(update) +TEXT = Text() +""" +Shortcut for :class:`telegram.ext.filters.Text()`. - def filter(self, message: Message) -> bool: - return bool(message.caption) +Examples: + To allow any text message, simply use ``MessageHandler(filters.TEXT, callback_method)``. +""" - caption = _Caption() - """Messages with a caption. If a list of strings is passed, it filters messages to only - allow those whose caption is appearing in the given list. + +class UpdateType: + """ + Subset for filtering the type of update. Examples: - ``MessageHandler(Filters.caption, callback_method)`` - - Args: - update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which captions to allow. Only - exact matches are allowed. If not specified, will allow any message with a caption. - """ - - class _Command(MessageFilter): - __slots__ = () - name = 'Filters.command' - - class _CommandOnlyStart(MessageFilter): - __slots__ = ('only_start',) - - def __init__(self, only_start: bool): - self.only_start = only_start - self.name = f'Filters.command({only_start})' - - def filter(self, message: Message) -> bool: - return bool( - message.entities - and any(e.type == MessageEntity.BOT_COMMAND for e in message.entities) - ) - - def __call__( # type: ignore[override] - self, update: Union[bool, Update] - ) -> Union[bool, '_CommandOnlyStart']: - if isinstance(update, Update): - return self.filter(update.effective_message) - return self._CommandOnlyStart(update) - - def filter(self, message: Message) -> bool: - return bool( - message.entities - and message.entities[0].type == MessageEntity.BOT_COMMAND - and message.entities[0].offset == 0 - ) - - command = _Command() - """ - Messages with a :attr:`telegram.MessageEntity.BOT_COMMAND`. By default only allows - messages `starting` with a bot command. Pass :obj:`False` to also allow messages that contain a - bot command `anywhere` in the text. - - Examples:: - - MessageHandler(Filters.command, command_at_start_callback) - MessageHandler(Filters.command(False), command_anywhere_callback) + Use these filters like: ``filters.UpdateType.MESSAGE`` or + ``filters.UpdateType.CHANNEL_POSTS`` etc. Note: - ``Filters.text`` also accepts messages containing a command. - - Args: - update (:obj:`bool`, optional): Whether to only allow messages that `start` with a bot - command. Defaults to :obj:`True`. + ``filters.UpdateType`` itself is *not* a filter, but just a convenience namespace. """ - class regex(MessageFilter): - """ - Filters updates by searching for an occurrence of ``pattern`` in the message text. - The ``re.search()`` function is used to determine whether an update should be filtered. + __slots__ = () - Refer to the documentation of the ``re`` module for more information. - - To get the groups and groupdict matched, see :attr:`telegram.ext.CallbackContext.matches`. - - Examples: - Use ``MessageHandler(Filters.regex(r'help'), callback)`` to capture all messages that - contain the word 'help'. You can also use - ``MessageHandler(Filters.regex(re.compile(r'help', re.IGNORECASE)), callback)`` if - you want your pattern to be case insensitive. This approach is recommended - if you need to specify flags on your pattern. - - Note: - Filters use the same short circuiting logic as python's `and`, `or` and `not`. - This means that for example: - - >>> Filters.regex(r'(a?x)') | Filters.regex(r'(b?x)') - - With a message.text of `x`, will only ever return the matches for the first filter, - since the second one is never evaluated. - - Args: - pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. - """ - - __slots__ = ('pattern',) - data_filter = True - - def __init__(self, pattern: Union[str, Pattern]): - if isinstance(pattern, str): - pattern = re.compile(pattern) - pattern = cast(Pattern, pattern) - self.pattern: Pattern = pattern - self.name = f'Filters.regex({self.pattern})' - - def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: - """""" # remove method from docs - if message.text: - match = self.pattern.search(message.text) - if match: - return {'matches': [match]} - return {} - - class caption_regex(MessageFilter): - """ - Filters updates by searching for an occurrence of ``pattern`` in the message caption. - - This filter works similarly to :class:`Filters.regex`, with the only exception being that - it applies to the message caption instead of the text. - - Examples: - Use ``MessageHandler(Filters.photo & Filters.caption_regex(r'help'), callback)`` - to capture all photos with caption containing the word 'help'. - - Note: - This filter will not work on simple text messages, but only on media with caption. - - Args: - pattern (:obj:`str` | :obj:`Pattern`): The regex pattern. - """ - - __slots__ = ('pattern',) - data_filter = True - - def __init__(self, pattern: Union[str, Pattern]): - if isinstance(pattern, str): - pattern = re.compile(pattern) - pattern = cast(Pattern, pattern) - self.pattern: Pattern = pattern - self.name = f'Filters.caption_regex({self.pattern})' - - def filter(self, message: Message) -> Optional[Dict[str, List[Match]]]: - """""" # remove method from docs - if message.caption: - match = self.pattern.search(message.caption) - if match: - return {'matches': [match]} - return {} - - class _Reply(MessageFilter): + class _ChannelPost(UpdateFilter): __slots__ = () - name = 'Filters.reply' - - def filter(self, message: Message) -> bool: - return bool(message.reply_to_message) - - reply = _Reply() - """Messages that are a reply to another message.""" - - class _Audio(MessageFilter): - __slots__ = () - name = 'Filters.audio' - - def filter(self, message: Message) -> bool: - return bool(message.audio) - - audio = _Audio() - """Messages that contain :class:`telegram.Audio`.""" - - class _Document(MessageFilter): - __slots__ = () - name = 'Filters.document' - - class category(MessageFilter): - """Filters documents by their category in the mime-type attribute. - - Note: - This Filter only filters by the mime_type of the document, - it doesn't check the validity of the document. - The user can manipulate the mime-type of a message and - send media with wrong types that don't fit to this handler. - - Example: - Filters.document.category('audio/') returns :obj:`True` for all types - of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. - """ - - __slots__ = ('_category',) - - def __init__(self, category: Optional[str]): - """Initialize the category you want to filter - - Args: - category (str, optional): category of the media you want to filter - """ - self._category = category - self.name = f"Filters.document.category('{self._category}')" - - def filter(self, message: Message) -> bool: - """""" # remove method from docs - if message.document: - return message.document.mime_type.startswith(self._category) - return False - - application = category('application/') - audio = category('audio/') - image = category('image/') - video = category('video/') - text = category('text/') - - class mime_type(MessageFilter): - """This Filter filters documents by their mime-type attribute - - Note: - This Filter only filters by the mime_type of the document, - it doesn't check the validity of document. - The user can manipulate the mime-type of a message and - send media with wrong types that don't fit to this handler. - - Example: - ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format. - """ - - __slots__ = ('mimetype',) - - def __init__(self, mimetype: Optional[str]): - self.mimetype = mimetype - self.name = f"Filters.document.mime_type('{self.mimetype}')" - - def filter(self, message: Message) -> bool: - """""" # remove method from docs - if message.document: - return message.document.mime_type == self.mimetype - return False - - apk = mime_type('application/vnd.android.package-archive') - doc = mime_type('application/msword') - docx = mime_type('application/vnd.openxmlformats-officedocument.wordprocessingml.document') - exe = mime_type('application/x-ms-dos-executable') - gif = mime_type('video/mp4') - jpg = mime_type('image/jpeg') - mp3 = mime_type('audio/mpeg') - pdf = mime_type('application/pdf') - py = mime_type('text/x-python') - svg = mime_type('image/svg+xml') - txt = mime_type('text/plain') - targz = mime_type('application/x-compressed-tar') - wav = mime_type('audio/x-wav') - xml = mime_type('application/xml') - zip = mime_type('application/zip') - - class file_extension(MessageFilter): - """This filter filters documents by their file ending/extension. - - Note: - * This Filter only filters by the file ending/extension of the document, - it doesn't check the validity of document. - * The user can manipulate the file extension of a document and - send media with wrong types that don't fit to this handler. - * Case insensitive by default, - you may change this with the flag ``case_sensitive=True``. - * Extension should be passed without leading dot - unless it's a part of the extension. - * Pass :obj:`None` to filter files with no extension, - i.e. without a dot in the filename. - - Example: - * ``Filters.document.file_extension("jpg")`` - filters files with extension ``".jpg"``. - * ``Filters.document.file_extension(".jpg")`` - filters files with extension ``"..jpg"``. - * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)`` - filters files with extension ``".Dockerfile"`` minding the case. - * ``Filters.document.file_extension(None)`` - filters files without a dot in the filename. - """ - - __slots__ = ('_file_extension', 'is_case_sensitive') - - def __init__(self, file_extension: Optional[str], case_sensitive: bool = False): - """Initialize the extension you want to filter. - - Args: - file_extension (:obj:`str` | :obj:`None`): - media file extension you want to filter. - case_sensitive (:obj:bool, optional): - pass :obj:`True` to make the filter case sensitive. - Default: :obj:`False`. - """ - self.is_case_sensitive = case_sensitive - if file_extension is None: - self._file_extension = None - self.name = "Filters.document.file_extension(None)" - elif self.is_case_sensitive: - self._file_extension = f".{file_extension}" - self.name = ( - f"Filters.document.file_extension({file_extension!r}," - " case_sensitive=True)" - ) - else: - self._file_extension = f".{file_extension}".lower() - self.name = f"Filters.document.file_extension({file_extension.lower()!r})" - - def filter(self, message: Message) -> bool: - """""" # remove method from docs - if message.document is None: - return False - if self._file_extension is None: - return "." not in message.document.file_name - if self.is_case_sensitive: - filename = message.document.file_name - else: - filename = message.document.file_name.lower() - return filename.endswith(self._file_extension) - - def filter(self, message: Message) -> bool: - return bool(message.document) - - document = _Document() - """ - Subset for messages containing a document/file. - - Examples: - Use these filters like: ``Filters.document.mp3``, - ``Filters.document.mime_type("text/plain")`` etc. Or use just - ``Filters.document`` for all document messages. - - Attributes: - category: Filters documents by their category in the mime-type attribute - - Note: - This Filter only filters by the mime_type of the document, - it doesn't check the validity of the document. - The user can manipulate the mime-type of a message and - send media with wrong types that don't fit to this handler. - - Example: - ``Filters.document.category('audio/')`` filters all types - of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav'. - application: Same as ``Filters.document.category("application")``. - audio: Same as ``Filters.document.category("audio")``. - image: Same as ``Filters.document.category("image")``. - video: Same as ``Filters.document.category("video")``. - text: Same as ``Filters.document.category("text")``. - mime_type: Filters documents by their mime-type attribute - - Note: - This Filter only filters by the mime_type of the document, - it doesn't check the validity of document. - - The user can manipulate the mime-type of a message and - send media with wrong types that don't fit to this handler. - - Example: - ``Filters.document.mime_type('audio/mpeg')`` filters all audio in mp3 format. - apk: Same as ``Filters.document.mime_type("application/vnd.android.package-archive")``. - doc: Same as ``Filters.document.mime_type("application/msword")``. - docx: Same as ``Filters.document.mime_type("application/vnd.openxmlformats-\ -officedocument.wordprocessingml.document")``. - exe: Same as ``Filters.document.mime_type("application/x-ms-dos-executable")``. - gif: Same as ``Filters.document.mime_type("video/mp4")``. - jpg: Same as ``Filters.document.mime_type("image/jpeg")``. - mp3: Same as ``Filters.document.mime_type("audio/mpeg")``. - pdf: Same as ``Filters.document.mime_type("application/pdf")``. - py: Same as ``Filters.document.mime_type("text/x-python")``. - svg: Same as ``Filters.document.mime_type("image/svg+xml")``. - txt: Same as ``Filters.document.mime_type("text/plain")``. - targz: Same as ``Filters.document.mime_type("application/x-compressed-tar")``. - wav: Same as ``Filters.document.mime_type("audio/x-wav")``. - xml: Same as ``Filters.document.mime_type("application/xml")``. - zip: Same as ``Filters.document.mime_type("application/zip")``. - file_extension: This filter filters documents by their file ending/extension. - - Note: - * This Filter only filters by the file ending/extension of the document, - it doesn't check the validity of document. - * The user can manipulate the file extension of a document and - send media with wrong types that don't fit to this handler. - * Case insensitive by default, - you may change this with the flag ``case_sensitive=True``. - * Extension should be passed without leading dot - unless it's a part of the extension. - * Pass :obj:`None` to filter files with no extension, - i.e. without a dot in the filename. - - Example: - * ``Filters.document.file_extension("jpg")`` - filters files with extension ``".jpg"``. - * ``Filters.document.file_extension(".jpg")`` - filters files with extension ``"..jpg"``. - * ``Filters.document.file_extension("Dockerfile", case_sensitive=True)`` - filters files with extension ``".Dockerfile"`` minding the case. - * ``Filters.document.file_extension(None)`` - filters files without a dot in the filename. - """ - - class _Animation(MessageFilter): - __slots__ = () - name = 'Filters.animation' - - def filter(self, message: Message) -> bool: - return bool(message.animation) - - animation = _Animation() - """Messages that contain :class:`telegram.Animation`.""" - - class _Photo(MessageFilter): - __slots__ = () - name = 'Filters.photo' - - def filter(self, message: Message) -> bool: - return bool(message.photo) - - photo = _Photo() - """Messages that contain :class:`telegram.PhotoSize`.""" - - class _Sticker(MessageFilter): - __slots__ = () - name = 'Filters.sticker' - - def filter(self, message: Message) -> bool: - return bool(message.sticker) - - sticker = _Sticker() - """Messages that contain :class:`telegram.Sticker`.""" - - class _Video(MessageFilter): - __slots__ = () - name = 'Filters.video' - - def filter(self, message: Message) -> bool: - return bool(message.video) - - video = _Video() - """Messages that contain :class:`telegram.Video`.""" - - class _Voice(MessageFilter): - __slots__ = () - name = 'Filters.voice' - - def filter(self, message: Message) -> bool: - return bool(message.voice) - - voice = _Voice() - """Messages that contain :class:`telegram.Voice`.""" - - class _VideoNote(MessageFilter): - __slots__ = () - name = 'Filters.video_note' - - def filter(self, message: Message) -> bool: - return bool(message.video_note) - - video_note = _VideoNote() - """Messages that contain :class:`telegram.VideoNote`.""" - - class _Contact(MessageFilter): - __slots__ = () - name = 'Filters.contact' - - def filter(self, message: Message) -> bool: - return bool(message.contact) - - contact = _Contact() - """Messages that contain :class:`telegram.Contact`.""" - - class _Location(MessageFilter): - __slots__ = () - name = 'Filters.location' - - def filter(self, message: Message) -> bool: - return bool(message.location) - - location = _Location() - """Messages that contain :class:`telegram.Location`.""" - - class _Venue(MessageFilter): - __slots__ = () - name = 'Filters.venue' - - def filter(self, message: Message) -> bool: - return bool(message.venue) - - venue = _Venue() - """Messages that contain :class:`telegram.Venue`.""" - - class _StatusUpdate(UpdateFilter): - """Subset for messages containing a status update. - - Examples: - Use these filters like: ``Filters.status_update.new_chat_members`` etc. Or use just - ``Filters.status_update`` for all status update messages. - - """ - - __slots__ = () - - class _NewChatMembers(MessageFilter): - __slots__ = () - name = 'Filters.status_update.new_chat_members' - - def filter(self, message: Message) -> bool: - return bool(message.new_chat_members) - - new_chat_members = _NewChatMembers() - """Messages that contain :attr:`telegram.Message.new_chat_members`.""" - - class _LeftChatMember(MessageFilter): - __slots__ = () - name = 'Filters.status_update.left_chat_member' - - def filter(self, message: Message) -> bool: - return bool(message.left_chat_member) - - left_chat_member = _LeftChatMember() - """Messages that contain :attr:`telegram.Message.left_chat_member`.""" - - class _NewChatTitle(MessageFilter): - __slots__ = () - name = 'Filters.status_update.new_chat_title' - - def filter(self, message: Message) -> bool: - return bool(message.new_chat_title) - - new_chat_title = _NewChatTitle() - """Messages that contain :attr:`telegram.Message.new_chat_title`.""" - - class _NewChatPhoto(MessageFilter): - __slots__ = () - name = 'Filters.status_update.new_chat_photo' - - def filter(self, message: Message) -> bool: - return bool(message.new_chat_photo) - - new_chat_photo = _NewChatPhoto() - """Messages that contain :attr:`telegram.Message.new_chat_photo`.""" - - class _DeleteChatPhoto(MessageFilter): - __slots__ = () - name = 'Filters.status_update.delete_chat_photo' - - def filter(self, message: Message) -> bool: - return bool(message.delete_chat_photo) - - delete_chat_photo = _DeleteChatPhoto() - """Messages that contain :attr:`telegram.Message.delete_chat_photo`.""" - - class _ChatCreated(MessageFilter): - __slots__ = () - name = 'Filters.status_update.chat_created' - - def filter(self, message: Message) -> bool: - return bool( - message.group_chat_created - or message.supergroup_chat_created - or message.channel_chat_created - ) - - chat_created = _ChatCreated() - """Messages that contain :attr:`telegram.Message.group_chat_created`, - :attr: `telegram.Message.supergroup_chat_created` or - :attr: `telegram.Message.channel_chat_created`.""" - - class _MessageAutoDeleteTimerChanged(MessageFilter): - __slots__ = () - name = 'MessageAutoDeleteTimerChanged' - - def filter(self, message: Message) -> bool: - return bool(message.message_auto_delete_timer_changed) - - message_auto_delete_timer_changed = _MessageAutoDeleteTimerChanged() - """Messages that contain :attr:`message_auto_delete_timer_changed`""" - - class _Migrate(MessageFilter): - __slots__ = () - name = 'Filters.status_update.migrate' - - def filter(self, message: Message) -> bool: - return bool(message.migrate_from_chat_id or message.migrate_to_chat_id) - - migrate = _Migrate() - """Messages that contain :attr:`telegram.Message.migrate_from_chat_id` or - :attr:`telegram.Message.migrate_to_chat_id`.""" - - class _PinnedMessage(MessageFilter): - __slots__ = () - name = 'Filters.status_update.pinned_message' - - def filter(self, message: Message) -> bool: - return bool(message.pinned_message) - - pinned_message = _PinnedMessage() - """Messages that contain :attr:`telegram.Message.pinned_message`.""" - - class _ConnectedWebsite(MessageFilter): - __slots__ = () - name = 'Filters.status_update.connected_website' - - def filter(self, message: Message) -> bool: - return bool(message.connected_website) - - connected_website = _ConnectedWebsite() - """Messages that contain :attr:`telegram.Message.connected_website`.""" - - class _ProximityAlertTriggered(MessageFilter): - __slots__ = () - name = 'Filters.status_update.proximity_alert_triggered' - - def filter(self, message: Message) -> bool: - return bool(message.proximity_alert_triggered) - - proximity_alert_triggered = _ProximityAlertTriggered() - """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" - - class _VoiceChatScheduled(MessageFilter): - __slots__ = () - name = 'Filters.status_update.voice_chat_scheduled' - - def filter(self, message: Message) -> bool: - return bool(message.voice_chat_scheduled) - - voice_chat_scheduled = _VoiceChatScheduled() - """Messages that contain :attr:`telegram.Message.voice_chat_scheduled`.""" - - class _VoiceChatStarted(MessageFilter): - __slots__ = () - name = 'Filters.status_update.voice_chat_started' - - def filter(self, message: Message) -> bool: - return bool(message.voice_chat_started) - - voice_chat_started = _VoiceChatStarted() - """Messages that contain :attr:`telegram.Message.voice_chat_started`.""" - - class _VoiceChatEnded(MessageFilter): - __slots__ = () - name = 'Filters.status_update.voice_chat_ended' - - def filter(self, message: Message) -> bool: - return bool(message.voice_chat_ended) - - voice_chat_ended = _VoiceChatEnded() - """Messages that contain :attr:`telegram.Message.voice_chat_ended`.""" - - class _VoiceChatParticipantsInvited(MessageFilter): - __slots__ = () - name = 'Filters.status_update.voice_chat_participants_invited' - - def filter(self, message: Message) -> bool: - return bool(message.voice_chat_participants_invited) - - voice_chat_participants_invited = _VoiceChatParticipantsInvited() - """Messages that contain :attr:`telegram.Message.voice_chat_participants_invited`.""" - - name = 'Filters.status_update' def filter(self, update: Update) -> bool: - return bool( - self.new_chat_members(update) - or self.left_chat_member(update) - or self.new_chat_title(update) - or self.new_chat_photo(update) - or self.delete_chat_photo(update) - or self.chat_created(update) - or self.message_auto_delete_timer_changed(update) - or self.migrate(update) - or self.pinned_message(update) - or self.connected_website(update) - or self.proximity_alert_triggered(update) - or self.voice_chat_scheduled(update) - or self.voice_chat_started(update) - or self.voice_chat_ended(update) - or self.voice_chat_participants_invited(update) - ) + return update.channel_post is not None - status_update = _StatusUpdate() - """Subset for messages containing a status update. + CHANNEL_POST = _ChannelPost(name="filters.UpdateType.CHANNEL_POST") + """Updates with :attr:`telegram.Update.channel_post`.""" - Examples: - Use these filters like: ``Filters.status_update.new_chat_members`` etc. Or use just - ``Filters.status_update`` for all status update messages. - - Attributes: - chat_created: Messages that contain - :attr:`telegram.Message.group_chat_created`, - :attr:`telegram.Message.supergroup_chat_created` or - :attr:`telegram.Message.channel_chat_created`. - connected_website: Messages that contain - :attr:`telegram.Message.connected_website`. - delete_chat_photo: Messages that contain - :attr:`telegram.Message.delete_chat_photo`. - left_chat_member: Messages that contain - :attr:`telegram.Message.left_chat_member`. - migrate: Messages that contain - :attr:`telegram.Message.migrate_to_chat_id` or - :attr:`telegram.Message.migrate_from_chat_id`. - new_chat_members: Messages that contain - :attr:`telegram.Message.new_chat_members`. - new_chat_photo: Messages that contain - :attr:`telegram.Message.new_chat_photo`. - new_chat_title: Messages that contain - :attr:`telegram.Message.new_chat_title`. - message_auto_delete_timer_changed: Messages that contain - :attr:`message_auto_delete_timer_changed`. - - .. versionadded:: 13.4 - pinned_message: Messages that contain - :attr:`telegram.Message.pinned_message`. - proximity_alert_triggered: Messages that contain - :attr:`telegram.Message.proximity_alert_triggered`. - voice_chat_scheduled: Messages that contain - :attr:`telegram.Message.voice_chat_scheduled`. - - .. versionadded:: 13.5 - voice_chat_started: Messages that contain - :attr:`telegram.Message.voice_chat_started`. - - .. versionadded:: 13.4 - voice_chat_ended: Messages that contain - :attr:`telegram.Message.voice_chat_ended`. - - .. versionadded:: 13.4 - voice_chat_participants_invited: Messages that contain - :attr:`telegram.Message.voice_chat_participants_invited`. - - .. versionadded:: 13.4 - - """ - - class _Forwarded(MessageFilter): + class _ChannelPosts(UpdateFilter): __slots__ = () - name = 'Filters.forwarded' - - def filter(self, message: Message) -> bool: - return bool(message.forward_date) - - forwarded = _Forwarded() - """Messages that are forwarded.""" - - class _Game(MessageFilter): - __slots__ = () - name = 'Filters.game' - - def filter(self, message: Message) -> bool: - return bool(message.game) - - game = _Game() - """Messages that contain :class:`telegram.Game`.""" - - class entity(MessageFilter): - """ - Filters messages to only allow those which have a :class:`telegram.MessageEntity` - where their `type` matches `entity_type`. - - Examples: - Example ``MessageHandler(Filters.entity("hashtag"), callback_method)`` - - Args: - entity_type: Entity type to check for. All types can be found as constants - in :class:`telegram.MessageEntity`. - - """ - - __slots__ = ('entity_type',) - - def __init__(self, entity_type: str): - self.entity_type = entity_type - self.name = f'Filters.entity({self.entity_type})' - - def filter(self, message: Message) -> bool: - """""" # remove method from docs - return any(entity.type == self.entity_type for entity in message.entities) - - class caption_entity(MessageFilter): - """ - Filters media messages to only allow those which have a :class:`telegram.MessageEntity` - where their `type` matches `entity_type`. - - Examples: - Example ``MessageHandler(Filters.caption_entity("hashtag"), callback_method)`` - - Args: - entity_type: Caption Entity type to check for. All types can be found as constants - in :class:`telegram.MessageEntity`. - - """ - - __slots__ = ('entity_type',) - - def __init__(self, entity_type: str): - self.entity_type = entity_type - self.name = f'Filters.caption_entity({self.entity_type})' - - def filter(self, message: Message) -> bool: - """""" # remove method from docs - return any(entity.type == self.entity_type for entity in message.caption_entities) - - class _ChatType(MessageFilter): - __slots__ = () - name = 'Filters.chat_type' - - class _Channel(MessageFilter): - __slots__ = () - name = 'Filters.chat_type.channel' - - def filter(self, message: Message) -> bool: - return message.chat.type == Chat.CHANNEL - - channel = _Channel() - - class _Group(MessageFilter): - __slots__ = () - name = 'Filters.chat_type.group' - - def filter(self, message: Message) -> bool: - return message.chat.type == Chat.GROUP - - group = _Group() - - class _SuperGroup(MessageFilter): - __slots__ = () - name = 'Filters.chat_type.supergroup' - - def filter(self, message: Message) -> bool: - return message.chat.type == Chat.SUPERGROUP - - supergroup = _SuperGroup() - - class _Groups(MessageFilter): - __slots__ = () - name = 'Filters.chat_type.groups' - - def filter(self, message: Message) -> bool: - return message.chat.type in [Chat.GROUP, Chat.SUPERGROUP] - - groups = _Groups() - - class _Private(MessageFilter): - __slots__ = () - name = 'Filters.chat_type.private' - - def filter(self, message: Message) -> bool: - return message.chat.type == Chat.PRIVATE - - private = _Private() - - def filter(self, message: Message) -> bool: - return bool(message.chat.type) - - chat_type = _ChatType() - """Subset for filtering the type of chat. - - Examples: - Use these filters like: ``Filters.chat_type.channel`` or - ``Filters.chat_type.supergroup`` etc. Or use just ``Filters.chat_type`` for all - chat types. - - Attributes: - channel: Updates from channel - group: Updates from group - supergroup: Updates from supergroup - groups: Updates from group *or* supergroup - private: Updates sent in private chat - """ - - class _ChatUserBaseFilter(MessageFilter, ABC): - __slots__ = ( - 'chat_id_name', - 'username_name', - 'allow_empty', - '__lock', - '_chat_ids', - '_usernames', - ) - - def __init__( - self, - chat_id: SLT[int] = None, - username: SLT[str] = None, - allow_empty: bool = False, - ): - self.chat_id_name = 'chat_id' - self.username_name = 'username' - self.allow_empty = allow_empty - self.__lock = Lock() - - self._chat_ids: Set[int] = set() - self._usernames: Set[str] = set() - - self._set_chat_ids(chat_id) - self._set_usernames(username) - - @abstractmethod - def get_chat_or_user(self, message: Message) -> Union[Chat, User, None]: - ... - - @staticmethod - def _parse_chat_id(chat_id: SLT[int]) -> Set[int]: - if chat_id is None: - return set() - if isinstance(chat_id, int): - return {chat_id} - return set(chat_id) - - @staticmethod - def _parse_username(username: SLT[str]) -> Set[str]: - if username is None: - return set() - if isinstance(username, str): - return {username[1:] if username.startswith('@') else username} - return {chat[1:] if chat.startswith('@') else chat for chat in username} - - def _set_chat_ids(self, chat_id: SLT[int]) -> None: - with self.__lock: - if chat_id and self._usernames: - raise RuntimeError( - f"Can't set {self.chat_id_name} in conjunction with (already set) " - f"{self.username_name}s." - ) - self._chat_ids = self._parse_chat_id(chat_id) - - def _set_usernames(self, username: SLT[str]) -> None: - with self.__lock: - if username and self._chat_ids: - raise RuntimeError( - f"Can't set {self.username_name} in conjunction with (already set) " - f"{self.chat_id_name}s." - ) - self._usernames = self._parse_username(username) - - @property - def chat_ids(self) -> FrozenSet[int]: - with self.__lock: - return frozenset(self._chat_ids) - - @chat_ids.setter - def chat_ids(self, chat_id: SLT[int]) -> None: - self._set_chat_ids(chat_id) - - @property - def usernames(self) -> FrozenSet[str]: - with self.__lock: - return frozenset(self._usernames) - - @usernames.setter - def usernames(self, username: SLT[str]) -> None: - self._set_usernames(username) - - def add_usernames(self, username: SLT[str]) -> None: - with self.__lock: - if self._chat_ids: - raise RuntimeError( - f"Can't set {self.username_name} in conjunction with (already set) " - f"{self.chat_id_name}s." - ) - - parsed_username = self._parse_username(username) - self._usernames |= parsed_username - - def add_chat_ids(self, chat_id: SLT[int]) -> None: - with self.__lock: - if self._usernames: - raise RuntimeError( - f"Can't set {self.chat_id_name} in conjunction with (already set) " - f"{self.username_name}s." - ) - - parsed_chat_id = self._parse_chat_id(chat_id) - - self._chat_ids |= parsed_chat_id - - def remove_usernames(self, username: SLT[str]) -> None: - with self.__lock: - if self._chat_ids: - raise RuntimeError( - f"Can't set {self.username_name} in conjunction with (already set) " - f"{self.chat_id_name}s." - ) - - parsed_username = self._parse_username(username) - self._usernames -= parsed_username - - def remove_chat_ids(self, chat_id: SLT[int]) -> None: - with self.__lock: - if self._usernames: - raise RuntimeError( - f"Can't set {self.chat_id_name} in conjunction with (already set) " - f"{self.username_name}s." - ) - parsed_chat_id = self._parse_chat_id(chat_id) - self._chat_ids -= parsed_chat_id - - def filter(self, message: Message) -> bool: - """""" # remove method from docs - chat_or_user = self.get_chat_or_user(message) - if chat_or_user: - if self.chat_ids: - return chat_or_user.id in self.chat_ids - if self.usernames: - return bool(chat_or_user.username and chat_or_user.username in self.usernames) - return self.allow_empty - return False - - @property - def name(self) -> str: - return ( - f'Filters.{self.__class__.__name__}(' - f'{", ".join(str(s) for s in (self.usernames or self.chat_ids))})' - ) - - @name.setter - def name(self, name: str) -> NoReturn: - raise RuntimeError(f'Cannot set name for Filters.{self.__class__.__name__}') - - class user(_ChatUserBaseFilter): - # pylint: disable=useless-super-delegation - """Filters messages to allow only those which are from specified user ID(s) or - username(s). - - Examples: - ``MessageHandler(Filters.user(1234), callback_method)`` - - Warning: - :attr:`user_ids` will give a *copy* of the saved user ids as :class:`frozenset`. This - is to ensure thread safety. To add/remove a user, you should use :meth:`add_usernames`, - :meth:`add_user_ids`, :meth:`remove_usernames` and :meth:`remove_user_ids`. Only update - the entire set by ``filter.user_ids/usernames = new_set``, if you are entirely sure - that it is not causing race conditions, as this will complete replace the current set - of allowed users. - - Args: - user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which user ID(s) to allow through. - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. Leading ``'@'`` s in usernames will be - discarded. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user - is specified in :attr:`user_ids` and :attr:`usernames`. Defaults to :obj:`False` - - Raises: - RuntimeError: If user_id and username are both present. - - Attributes: - user_ids(set(:obj:`int`), optional): Which user ID(s) to allow through. - usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to - allow through. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user - is specified in :attr:`user_ids` and :attr:`usernames`. - - """ - - __slots__ = () - - def __init__( - self, - user_id: SLT[int] = None, - username: SLT[str] = None, - allow_empty: bool = False, - ): - super().__init__(chat_id=user_id, username=username, allow_empty=allow_empty) - self.chat_id_name = 'user_id' - - def get_chat_or_user(self, message: Message) -> Optional[User]: - return message.from_user - - @property - def user_ids(self) -> FrozenSet[int]: - return self.chat_ids - - @user_ids.setter - def user_ids(self, user_id: SLT[int]) -> None: - self.chat_ids = user_id # type: ignore[assignment] - - def add_usernames(self, username: SLT[str]) -> None: - """ - Add one or more users to the allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().add_usernames(username) - - def add_user_ids(self, user_id: SLT[int]) -> None: - """ - Add one or more users to the allowed user ids. - - Args: - user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which user ID(s) to allow through. - """ - return super().add_chat_ids(user_id) - - def remove_usernames(self, username: SLT[str]) -> None: - """ - Remove one or more users from allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to disallow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().remove_usernames(username) - - def remove_user_ids(self, user_id: SLT[int]) -> None: - """ - Remove one or more users from allowed user ids. - - Args: - user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which user ID(s) to disallow through. - """ - return super().remove_chat_ids(user_id) - - class via_bot(_ChatUserBaseFilter): - # pylint: disable=useless-super-delegation - """Filters messages to allow only those which are from specified via_bot ID(s) or - username(s). - - Examples: - ``MessageHandler(Filters.via_bot(1234), callback_method)`` - - Warning: - :attr:`bot_ids` will give a *copy* of the saved bot ids as :class:`frozenset`. This - is to ensure thread safety. To add/remove a bot, you should use :meth:`add_usernames`, - :meth:`add_bot_ids`, :meth:`remove_usernames` and :meth:`remove_bot_ids`. Only update - the entire set by ``filter.bot_ids/usernames = new_set``, if you are entirely sure - that it is not causing race conditions, as this will complete replace the current set - of allowed bots. - - Args: - bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which bot ID(s) to allow through. - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. Leading ``'@'`` s in usernames will be - discarded. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user - is specified in :attr:`bot_ids` and :attr:`usernames`. Defaults to :obj:`False` - - Raises: - RuntimeError: If bot_id and username are both present. - - Attributes: - bot_ids(set(:obj:`int`), optional): Which bot ID(s) to allow through. - usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to - allow through. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no bot - is specified in :attr:`bot_ids` and :attr:`usernames`. - - """ - - __slots__ = () - - def __init__( - self, - bot_id: SLT[int] = None, - username: SLT[str] = None, - allow_empty: bool = False, - ): - super().__init__(chat_id=bot_id, username=username, allow_empty=allow_empty) - self.chat_id_name = 'bot_id' - - def get_chat_or_user(self, message: Message) -> Optional[User]: - return message.via_bot - - @property - def bot_ids(self) -> FrozenSet[int]: - return self.chat_ids - - @bot_ids.setter - def bot_ids(self, bot_id: SLT[int]) -> None: - self.chat_ids = bot_id # type: ignore[assignment] - - def add_usernames(self, username: SLT[str]) -> None: - """ - Add one or more users to the allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().add_usernames(username) - - def add_bot_ids(self, bot_id: SLT[int]) -> None: - """ - - Add one or more users to the allowed user ids. - - Args: - bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which bot ID(s) to allow through. - """ - return super().add_chat_ids(bot_id) - - def remove_usernames(self, username: SLT[str]) -> None: - """ - Remove one or more users from allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to disallow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().remove_usernames(username) - - def remove_bot_ids(self, bot_id: SLT[int]) -> None: - """ - Remove one or more users from allowed user ids. - - Args: - bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which bot ID(s) to disallow through. - """ - return super().remove_chat_ids(bot_id) - - class chat(_ChatUserBaseFilter): - # pylint: disable=useless-super-delegation - """Filters messages to allow only those which are from a specified chat ID or username. - - Examples: - ``MessageHandler(Filters.chat(-1234), callback_method)`` - - Warning: - :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This - is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`, - :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update - the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure - that it is not causing race conditions, as this will complete replace the current set - of allowed chats. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which chat ID(s) to allow through. - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. - Leading ``'@'`` s in usernames will be discarded. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat - is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False` - - Raises: - RuntimeError: If chat_id and username are both present. - - Attributes: - chat_ids(set(:obj:`int`), optional): Which chat ID(s) to allow through. - usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to - allow through. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat - is specified in :attr:`chat_ids` and :attr:`usernames`. - - """ - - __slots__ = () - - def get_chat_or_user(self, message: Message) -> Optional[Chat]: - return message.chat - - def add_usernames(self, username: SLT[str]) -> None: - """ - Add one or more chats to the allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().add_usernames(username) - - def add_chat_ids(self, chat_id: SLT[int]) -> None: - """ - Add one or more chats to the allowed chat ids. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which chat ID(s) to allow through. - """ - return super().add_chat_ids(chat_id) - - def remove_usernames(self, username: SLT[str]) -> None: - """ - Remove one or more chats from allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to disallow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().remove_usernames(username) - - def remove_chat_ids(self, chat_id: SLT[int]) -> None: - """ - Remove one or more chats from allowed chat ids. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which chat ID(s) to disallow through. - """ - return super().remove_chat_ids(chat_id) - - class forwarded_from(_ChatUserBaseFilter): - # pylint: disable=useless-super-delegation - """Filters messages to allow only those which are forwarded from the specified chat ID(s) - or username(s) based on :attr:`telegram.Message.forward_from` and - :attr:`telegram.Message.forward_from_chat`. - - .. versionadded:: 13.5 - - Examples: - ``MessageHandler(Filters.forwarded_from(chat_id=1234), callback_method)`` - - Note: - When a user has disallowed adding a link to their account while forwarding their - messages, this filter will *not* work since both - :attr:`telegram.Message.forwarded_from` and - :attr:`telegram.Message.forwarded_from_chat` are :obj:`None`. However, this behaviour - is undocumented and might be changed by Telegram. - - Warning: - :attr:`chat_ids` will give a *copy* of the saved chat ids as :class:`frozenset`. This - is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`, - :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update - the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure - that it is not causing race conditions, as this will complete replace the current set - of allowed chats. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which chat/user ID(s) to allow through. - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. Leading ``'@'`` s in usernames will be - discarded. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat - is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to :obj:`False`. - - Raises: - RuntimeError: If both chat_id and username are present. - - Attributes: - chat_ids(set(:obj:`int`), optional): Which chat/user ID(s) to allow through. - usernames(set(:obj:`str`), optional): Which username(s) (without leading ``'@'``) to - allow through. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no chat - is specified in :attr:`chat_ids` and :attr:`usernames`. - """ - - __slots__ = () - - def get_chat_or_user(self, message: Message) -> Union[User, Chat, None]: - return message.forward_from or message.forward_from_chat - - def add_usernames(self, username: SLT[str]) -> None: - """ - Add one or more chats to the allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to allow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().add_usernames(username) - - def add_chat_ids(self, chat_id: SLT[int]) -> None: - """ - Add one or more chats to the allowed chat ids. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which chat/user ID(s) to allow through. - """ - return super().add_chat_ids(chat_id) - - def remove_usernames(self, username: SLT[str]) -> None: - """ - Remove one or more chats from allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which username(s) to disallow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().remove_usernames(username) - - def remove_chat_ids(self, chat_id: SLT[int]) -> None: - """ - Remove one or more chats from allowed chat ids. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which chat/user ID(s) to disallow through. - """ - return super().remove_chat_ids(chat_id) - - class sender_chat(_ChatUserBaseFilter): - # pylint: disable=useless-super-delegation - """Filters messages to allow only those which are from a specified sender chat's chat ID or - username. - - Examples: - * To filter for messages sent to a group by a channel with ID - ``-1234``, use ``MessageHandler(Filters.sender_chat(-1234), callback_method)``. - * To filter for messages of anonymous admins in a super group with username - ``@anonymous``, use - ``MessageHandler(Filters.sender_chat(username='anonymous'), callback_method)``. - * To filter for messages sent to a group by *any* channel, use - ``MessageHandler(Filters.sender_chat.channel, callback_method)``. - * To filter for messages of anonymous admins in *any* super group, use - ``MessageHandler(Filters.sender_chat.super_group, callback_method)``. - - Note: - Remember, ``sender_chat`` is also set for messages in a channel as the channel itself, - so when your bot is an admin in a channel and the linked discussion group, you would - receive the message twice (once from inside the channel, once inside the discussion - group). Since v13.9, the field :attr:`telegram.Message.is_automatic_forward` will be - :obj:`True` for the discussion group message. - - .. seealso:: :attr:`Filters.is_automatic_forward` - - Warning: - :attr:`chat_ids` will return a *copy* of the saved chat ids as :class:`frozenset`. This - is to ensure thread safety. To add/remove a chat, you should use :meth:`add_usernames`, - :meth:`add_chat_ids`, :meth:`remove_usernames` and :meth:`remove_chat_ids`. Only update - the entire set by ``filter.chat_ids/usernames = new_set``, if you are entirely sure - that it is not causing race conditions, as this will complete replace the current set - of allowed chats. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which sender chat chat ID(s) to allow through. - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which sender chat username(s) to allow through. - Leading ``'@'`` s in usernames will be discarded. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender - chat is specified in :attr:`chat_ids` and :attr:`usernames`. Defaults to - :obj:`False` - - Raises: - RuntimeError: If both chat_id and username are present. - - Attributes: - chat_ids(set(:obj:`int`), optional): Which sender chat chat ID(s) to allow through. - usernames(set(:obj:`str`), optional): Which sender chat username(s) (without leading - ``'@'``) to allow through. - allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no sender - chat is specified in :attr:`chat_ids` and :attr:`usernames`. - super_group: Messages whose sender chat is a super group. - - Examples: - ``Filters.sender_chat.supergroup`` - channel: Messages whose sender chat is a channel. - - Examples: - ``Filters.sender_chat.channel`` - - """ - - __slots__ = () - - def get_chat_or_user(self, message: Message) -> Optional[Chat]: - return message.sender_chat - - def add_usernames(self, username: SLT[str]) -> None: - """ - Add one or more sender chats to the allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which sender chat username(s) to allow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().add_usernames(username) - - def add_chat_ids(self, chat_id: SLT[int]) -> None: - """ - Add one or more sender chats to the allowed chat ids. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which sender chat ID(s) to allow through. - """ - return super().add_chat_ids(chat_id) - - def remove_usernames(self, username: SLT[str]) -> None: - """ - Remove one or more sender chats from allowed usernames. - - Args: - username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): - Which sender chat username(s) to disallow through. - Leading ``'@'`` s in usernames will be discarded. - """ - return super().remove_usernames(username) - - def remove_chat_ids(self, chat_id: SLT[int]) -> None: - """ - Remove one or more sender chats from allowed chat ids. - - Args: - chat_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which sender chat ID(s) to disallow through. - """ - return super().remove_chat_ids(chat_id) - - class _SuperGroup(MessageFilter): - __slots__ = () - - def filter(self, message: Message) -> bool: - if message.sender_chat: - return message.sender_chat.type == Chat.SUPERGROUP - return False - - class _Channel(MessageFilter): - __slots__ = () - - def filter(self, message: Message) -> bool: - if message.sender_chat: - return message.sender_chat.type == Chat.CHANNEL - return False - - super_group = _SuperGroup() - channel = _Channel() - - class _IsAutomaticForward(MessageFilter): - __slots__ = () - name = 'Filters.is_automatic_forward' - - def filter(self, message: Message) -> bool: - return bool(message.is_automatic_forward) - - is_automatic_forward = _IsAutomaticForward() - """Messages that contain :attr:`telegram.Message.is_automatic_forward`. - - .. versionadded:: 13.9 - """ - - class _HasProtectedContent(MessageFilter): - __slots__ = () - name = 'Filters.has_protected_content' - - def filter(self, message: Message) -> bool: - return bool(message.has_protected_content) - - has_protected_content = _HasProtectedContent() - """Messages that contain :attr:`telegram.Message.has_protected_content`. - - .. versionadded:: 13.9 - """ - - class _Invoice(MessageFilter): - __slots__ = () - name = 'Filters.invoice' - - def filter(self, message: Message) -> bool: - return bool(message.invoice) - - invoice = _Invoice() - """Messages that contain :class:`telegram.Invoice`.""" - - class _SuccessfulPayment(MessageFilter): - __slots__ = () - name = 'Filters.successful_payment' - - def filter(self, message: Message) -> bool: - return bool(message.successful_payment) - - successful_payment = _SuccessfulPayment() - """Messages that confirm a :class:`telegram.SuccessfulPayment`.""" - - class _PassportData(MessageFilter): - __slots__ = () - name = 'Filters.passport_data' - - def filter(self, message: Message) -> bool: - return bool(message.passport_data) - - passport_data = _PassportData() - """Messages that contain a :class:`telegram.PassportData`""" - - class _Poll(MessageFilter): - __slots__ = () - name = 'Filters.poll' - - def filter(self, message: Message) -> bool: - return bool(message.poll) - - poll = _Poll() - """Messages that contain a :class:`telegram.Poll`.""" - - class _Dice(_DiceEmoji): - __slots__ = () - # pylint: disable=no-member - dice = _DiceEmoji(DiceEmoji.DICE, DiceEmoji.DICE.name.lower()) - darts = _DiceEmoji(DiceEmoji.DARTS, DiceEmoji.DARTS.name.lower()) - basketball = _DiceEmoji(DiceEmoji.BASKETBALL, DiceEmoji.BASKETBALL.name.lower()) - football = _DiceEmoji(DiceEmoji.FOOTBALL, DiceEmoji.FOOTBALL.name.lower()) - slot_machine = _DiceEmoji(DiceEmoji.SLOT_MACHINE, DiceEmoji.SLOT_MACHINE.name.lower()) - bowling = _DiceEmoji(DiceEmoji.BOWLING, DiceEmoji.BOWLING.name.lower()) - - dice = _Dice() - """Dice Messages. If an integer or a list of integers is passed, it filters messages to only - allow those whose dice value is appearing in the given list. - - Examples: - To allow any dice message, simply use - ``MessageHandler(Filters.dice, callback_method)``. - - To allow only dice messages with the emoji 🎲, but any value, use - ``MessageHandler(Filters.dice.dice, callback_method)``. - - To allow only dice messages with the emoji 🎯 and with value 6, use - ``MessageHandler(Filters.dice.darts(6), callback_method)``. - - To allow only dice messages with the emoji ⚽ and with value 5 `or` 6, use - ``MessageHandler(Filters.dice.football([5, 6]), callback_method)``. - - Note: - Dice messages don't have text. If you want to filter either text or dice messages, use - ``Filters.text | Filters.dice``. - - Args: - update (:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): - Which values to allow. If not specified, will allow any dice message. - - Attributes: - dice: Dice messages with the emoji 🎲. Passing a list of integers is supported just as for - :attr:`Filters.dice`. - darts: Dice messages with the emoji 🎯. Passing a list of integers is supported just as for - :attr:`Filters.dice`. - basketball: Dice messages with the emoji 🏀. Passing a list of integers is supported just - as for :attr:`Filters.dice`. - football: Dice messages with the emoji ⚽. Passing a list of integers is supported just - as for :attr:`Filters.dice`. - slot_machine: Dice messages with the emoji 🎰. Passing a list of integers is supported just - as for :attr:`Filters.dice`. - bowling: Dice messages with the emoji 🎳. Passing a list of integers is supported just - as for :attr:`Filters.dice`. - - .. versionadded:: 13.4 - - """ - - class language(MessageFilter): - """Filters messages to only allow those which are from users with a certain language code. - - Note: - According to official Telegram API documentation, not every single user has the - `language_code` attribute. Do not count on this filter working on all users. - - Examples: - ``MessageHandler(Filters.language("en"), callback_method)`` - - Args: - lang (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]): - Which language code(s) to allow through. - This will be matched using ``.startswith`` meaning that - 'en' will match both 'en_US' and 'en_GB'. - - """ - - __slots__ = ('lang',) - - def __init__(self, lang: SLT[str]): - if isinstance(lang, str): - lang = cast(str, lang) - self.lang = [lang] - else: - lang = cast(List[str], lang) - self.lang = lang - self.name = f'Filters.language({self.lang})' - - def filter(self, message: Message) -> bool: - """""" # remove method from docs - return bool( - message.from_user.language_code - and any(message.from_user.language_code.startswith(x) for x in self.lang) - ) - - class _Attachment(MessageFilter): - __slots__ = () - - name = 'Filters.attachment' - - def filter(self, message: Message) -> bool: - return bool(message.effective_attachment) - - attachment = _Attachment() - """Messages that contain :meth:`telegram.Message.effective_attachment`. - - - .. versionadded:: 13.6""" - - class _UpdateType(UpdateFilter): - __slots__ = () - name = 'Filters.update' - - class _Message(UpdateFilter): - __slots__ = () - name = 'Filters.update.message' - - def filter(self, update: Update) -> bool: - return update.message is not None - - message = _Message() - - class _EditedMessage(UpdateFilter): - __slots__ = () - name = 'Filters.update.edited_message' - - def filter(self, update: Update) -> bool: - return update.edited_message is not None - - edited_message = _EditedMessage() - - class _Messages(UpdateFilter): - __slots__ = () - name = 'Filters.update.messages' - - def filter(self, update: Update) -> bool: - return update.message is not None or update.edited_message is not None - - messages = _Messages() - - class _ChannelPost(UpdateFilter): - __slots__ = () - name = 'Filters.update.channel_post' - - def filter(self, update: Update) -> bool: - return update.channel_post is not None - - channel_post = _ChannelPost() - - class _EditedChannelPost(UpdateFilter): - __slots__ = () - name = 'Filters.update.edited_channel_post' - - def filter(self, update: Update) -> bool: - return update.edited_channel_post is not None - - edited_channel_post = _EditedChannelPost() - - class _Edited(UpdateFilter): - __slots__ = () - name = 'Filters.update.edited' - - def filter(self, update: Update) -> bool: - return update.edited_message is not None or update.edited_channel_post is not None - - edited = _Edited() - - class _ChannelPosts(UpdateFilter): - __slots__ = () - name = 'Filters.update.channel_posts' - - def filter(self, update: Update) -> bool: - return update.channel_post is not None or update.edited_channel_post is not None - - channel_posts = _ChannelPosts() def filter(self, update: Update) -> bool: - return bool(self.messages(update) or self.channel_posts(update)) + return update.channel_post is not None or update.edited_channel_post is not None - update = _UpdateType() - """Subset for filtering the type of update. + CHANNEL_POSTS = _ChannelPosts(name="filters.UpdateType.CHANNEL_POSTS") + """Updates with either :attr:`telegram.Update.channel_post` or + :attr:`telegram.Update.edited_channel_post`.""" + + class _Edited(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.edited_message is not None or update.edited_channel_post is not None + + EDITED = _Edited(name="filters.UpdateType.EDITED") + """Updates with either :attr:`telegram.Update.edited_message` or + :attr:`telegram.Update.edited_channel_post`.""" + + class _EditedChannelPost(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.edited_channel_post is not None + + EDITED_CHANNEL_POST = _EditedChannelPost(name="filters.UpdateType.EDITED_CHANNEL_POST") + """Updates with :attr:`telegram.Update.edited_channel_post`.""" + + class _EditedMessage(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.edited_message is not None + + EDITED_MESSAGE = _EditedMessage(name="filters.UpdateType.EDITED_MESSAGE") + """Updates with :attr:`telegram.Update.edited_message`.""" + + class _Message(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.message is not None + + MESSAGE = _Message(name="filters.UpdateType.MESSAGE") + """Updates with :attr:`telegram.Update.message`.""" + + class _Messages(UpdateFilter): + __slots__ = () + + def filter(self, update: Update) -> bool: + return update.message is not None or update.edited_message is not None + + MESSAGES = _Messages(name="filters.UpdateType.MESSAGES") + """Updates with either :attr:`telegram.Update.message` or + :attr:`telegram.Update.edited_message`.""" + + +class User(_ChatUserBaseFilter): + """Filters messages to allow only those which are from specified user ID(s) or + username(s). Examples: - Use these filters like: ``Filters.update.message`` or - ``Filters.update.channel_posts`` etc. Or use just ``Filters.update`` for all - types. + ``MessageHandler(filters.User(1234), callback_method)`` + + Args: + user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): Which user ID(s) to + allow through. + username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): + Which username(s) to allow through. Leading ``'@'`` s in usernames will be discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user is + specified in :attr:`user_ids` and :attr:`usernames`. Defaults to :obj:`False`. + + Raises: + RuntimeError: If ``user_id`` and ``username`` are both present. Attributes: - message: Updates with :attr:`telegram.Update.message` - edited_message: Updates with :attr:`telegram.Update.edited_message` - messages: Updates with either :attr:`telegram.Update.message` or - :attr:`telegram.Update.edited_message` - channel_post: Updates with :attr:`telegram.Update.channel_post` - edited_channel_post: Updates with - :attr:`telegram.Update.edited_channel_post` - channel_posts: Updates with either :attr:`telegram.Update.channel_post` or - :attr:`telegram.Update.edited_channel_post` - edited: Updates with either :attr:`telegram.Update.edited_message` or - :attr:`telegram.Update.edited_channel_post` + allow_empty (:obj:`bool`): Whether updates should be processed, if no user is specified in + :attr:`user_ids` and :attr:`usernames`. """ + + __slots__ = () + + def __init__( + self, + user_id: SLT[int] = None, + username: SLT[str] = None, + allow_empty: bool = False, + ): + super().__init__(chat_id=user_id, username=username, allow_empty=allow_empty) + self._chat_id_name = 'user_id' + + def get_chat_or_user(self, message: Message) -> Optional[TGUser]: + return message.from_user + + @property + def user_ids(self) -> FrozenSet[int]: + """ + Which user ID(s) to allow through. + + Warning: + :attr:`user_ids` will give a *copy* of the saved user ids as :obj:`frozenset`. This + is to ensure thread safety. To add/remove a user, you should use :meth:`add_user_ids`, + and :meth:`remove_user_ids`. Only update the entire set by + ``filter.user_ids = new_set``, if you are entirely sure that it is not causing race + conditions, as this will complete replace the current set of allowed users. + + Returns: + frozenset(:obj:`int`) + """ + return self.chat_ids + + @user_ids.setter + def user_ids(self, user_id: SLT[int]) -> None: + self.chat_ids = user_id # type: ignore[assignment] + + def add_user_ids(self, user_id: SLT[int]) -> None: + """ + Add one or more users to the allowed user ids. + + Args: + user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which user ID(s) to allow + through. + """ + return super()._add_chat_ids(user_id) + + def remove_user_ids(self, user_id: SLT[int]) -> None: + """ + Remove one or more users from allowed user ids. + + Args: + user_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which user ID(s) to + disallow through. + """ + return super()._remove_chat_ids(user_id) + + +class _User(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.from_user) + + +USER = _User(name="filters.USER") +"""This filter filters *any* message that has a :attr:`telegram.Message.from_user`.""" + + +class _Venue(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.venue) + + +VENUE = _Venue(name="filters.VENUE") +"""Messages that contain :attr:`telegram.Message.venue`.""" + + +class ViaBot(_ChatUserBaseFilter): + """Filters messages to allow only those which are from specified via_bot ID(s) or username(s). + + Examples: + ``MessageHandler(filters.ViaBot(1234), callback_method)`` + + Args: + bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): Which bot ID(s) to + allow through. + username(:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`], optional): + Which username(s) to allow through. Leading ``'@'`` s in usernames will be + discarded. + allow_empty(:obj:`bool`, optional): Whether updates should be processed, if no user + is specified in :attr:`bot_ids` and :attr:`usernames`. Defaults to :obj:`False`. + + Raises: + RuntimeError: If ``bot_id`` and ``username`` are both present. + + Attributes: + allow_empty (:obj:`bool`): Whether updates should be processed, if no bot is specified in + :attr:`bot_ids` and :attr:`usernames`. + """ + + __slots__ = () + + def __init__( + self, + bot_id: SLT[int] = None, + username: SLT[str] = None, + allow_empty: bool = False, + ): + super().__init__(chat_id=bot_id, username=username, allow_empty=allow_empty) + self._chat_id_name = 'bot_id' + + def get_chat_or_user(self, message: Message) -> Optional[TGUser]: + return message.via_bot + + @property + def bot_ids(self) -> FrozenSet[int]: + """ + Which bot ID(s) to allow through. + + Warning: + :attr:`bot_ids` will give a *copy* of the saved bot ids as :obj:`frozenset`. This + is to ensure thread safety. To add/remove a bot, you should use :meth:`add_bot_ids`, + and :meth:`remove_bot_ids`. Only update the entire set by ``filter.bot_ids = new_set``, + if you are entirely sure that it is not causing race conditions, as this will complete + replace the current set of allowed bots. + + Returns: + frozenset(:obj:`int`) + """ + return self.chat_ids + + @bot_ids.setter + def bot_ids(self, bot_id: SLT[int]) -> None: + self.chat_ids = bot_id # type: ignore[assignment] + + def add_bot_ids(self, bot_id: SLT[int]) -> None: + """ + Add one or more bots to the allowed bot ids. + + Args: + bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`]): Which bot ID(s) to allow + through. + """ + return super()._add_chat_ids(bot_id) + + def remove_bot_ids(self, bot_id: SLT[int]) -> None: + """ + Remove one or more bots from allowed bot ids. + + Args: + bot_id(:obj:`int` | Tuple[:obj:`int`] | List[:obj:`int`], optional): Which bot ID(s) to + disallow through. + """ + return super()._remove_chat_ids(bot_id) + + +class _ViaBot(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.via_bot) + + +VIA_BOT = _ViaBot(name="filters.VIA_BOT") +"""This filter filters for message that were sent via *any* bot.""" + + +class _Video(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.video) + + +VIDEO = _Video(name="filters.VIDEO") +"""Messages that contain :attr:`telegram.Message.video`.""" + + +class _VideoNote(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.video_note) + + +VIDEO_NOTE = _VideoNote(name="filters.VIDEO_NOTE") +"""Messages that contain :attr:`telegram.Message.video_note`.""" + + +class _Voice(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.voice) + + +VOICE = _Voice("filters.VOICE") +"""Messages that contain :attr:`telegram.Message.voice`.""" diff --git a/telegram/helpers.py b/telegram/helpers.py index 633c13152..df91708ce 100644 --- a/telegram/helpers.py +++ b/telegram/helpers.py @@ -134,7 +134,7 @@ def create_deep_linked_url(bot_username: str, payload: str = None, group: bool = Note: Works well in conjunction with - ``CommandHandler("start", callback, filters = Filters.regex('payload'))`` + ``CommandHandler("start", callback, filters = filters.Regex('payload'))`` Examples: ``create_deep_linked_url(bot.get_me().username, "some-params")`` diff --git a/tests/conftest.py b/tests/conftest.py index b86e678ef..79abb79ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,13 +54,12 @@ from telegram import ( ) from telegram.ext import ( Dispatcher, - MessageFilter, Defaults, - UpdateFilter, ExtBot, DispatcherBuilder, UpdaterBuilder, ) +from telegram.ext.filters import UpdateFilter, MessageFilter from telegram.error import BadRequest from telegram._utils.defaultvalue import DefaultValue, DEFAULT_NONE from telegram.request import Request @@ -335,6 +334,7 @@ def make_command_update(message, edited=False, **kwargs): def mock_filter(request): class MockFilter(request.param['class']): def __init__(self): + super().__init__() self.tested = False def filter(self, _): diff --git a/tests/test_commandhandler.py b/tests/test_commandhandler.py index 0b639b72e..5dbf8005b 100644 --- a/tests/test_commandhandler.py +++ b/tests/test_commandhandler.py @@ -22,7 +22,7 @@ from queue import Queue import pytest from telegram import Message, Update, Chat, Bot -from telegram.ext import CommandHandler, Filters, CallbackContext, JobQueue, PrefixHandler +from telegram.ext import CommandHandler, filters, CallbackContext, JobQueue, PrefixHandler from tests.conftest import ( make_command_message, make_command_update, @@ -186,7 +186,7 @@ class TestCommandHandler(BaseTest): def test_edited(self, command_message): """Test that a CH responds to an edited message if its filters allow it""" handler_edited = self.make_default_handler() - handler_no_edited = self.make_default_handler(filters=~Filters.update.edited_message) + handler_no_edited = self.make_default_handler(filters=~filters.UpdateType.EDITED_MESSAGE) self._test_edited(command_message, handler_edited, handler_no_edited) def test_directed_commands(self, bot, command): @@ -197,7 +197,7 @@ class TestCommandHandler(BaseTest): def test_with_filter(self, command): """Test that a CH with a (generic) filter responds if its filters match""" - handler = self.make_default_handler(filters=Filters.chat_type.group) + handler = self.make_default_handler(filters=filters.ChatType.GROUP) assert is_match(handler, make_command_update(command, chat=Chat(-23, Chat.GROUP))) assert not is_match(handler, make_command_update(command, chat=Chat(23, Chat.PRIVATE))) @@ -234,14 +234,14 @@ class TestCommandHandler(BaseTest): def test_context_regex(self, dp, command): """Test CHs with context-based callbacks and a single filter""" handler = self.make_default_handler( - self.callback_context_regex1, filters=Filters.regex('one two') + self.callback_context_regex1, filters=filters.Regex('one two') ) self._test_context_args_or_regex(dp, handler, command) def test_context_multiple_regex(self, dp, command): """Test CHs with context-based callbacks and filters combined""" handler = self.make_default_handler( - self.callback_context_regex2, filters=Filters.regex('one') & Filters.regex('two') + self.callback_context_regex2, filters=filters.Regex('one') & filters.Regex('two') ) self._test_context_args_or_regex(dp, handler, command) @@ -317,11 +317,11 @@ class TestPrefixHandler(BaseTest): def test_edited(self, prefix_message): handler_edited = self.make_default_handler() - handler_no_edited = self.make_default_handler(filters=~Filters.update.edited_message) + handler_no_edited = self.make_default_handler(filters=~filters.UpdateType.EDITED_MESSAGE) self._test_edited(prefix_message, handler_edited, handler_no_edited) def test_with_filter(self, prefix_message_text): - handler = self.make_default_handler(filters=Filters.chat_type.group) + handler = self.make_default_handler(filters=filters.ChatType.GROUP) text = prefix_message_text assert is_match(handler, make_message_update(text, chat=Chat(-23, Chat.GROUP))) assert not is_match(handler, make_message_update(text, chat=Chat(23, Chat.PRIVATE))) @@ -370,12 +370,12 @@ class TestPrefixHandler(BaseTest): def test_context_regex(self, dp, prefix_message_text): handler = self.make_default_handler( - self.callback_context_regex1, filters=Filters.regex('one two') + self.callback_context_regex1, filters=filters.Regex('one two') ) self._test_context_args_or_regex(dp, handler, prefix_message_text) def test_context_multiple_regex(self, dp, prefix_message_text): handler = self.make_default_handler( - self.callback_context_regex2, filters=Filters.regex('one') & Filters.regex('two') + self.callback_context_regex2, filters=filters.Regex('one') & filters.Regex('two') ) self._test_context_args_or_regex(dp, handler, prefix_message_text) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 772127e39..c44df7995 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -40,7 +40,7 @@ from telegram.ext import ( CommandHandler, CallbackQueryHandler, MessageHandler, - Filters, + filters, InlineQueryHandler, CallbackContext, DispatcherHandlerStop, @@ -770,7 +770,7 @@ class TestConversationHandler: def test_channel_message_without_chat(self, bot): handler = ConversationHandler( - entry_points=[MessageHandler(Filters.all, self.start_end)], states={}, fallbacks=[] + entry_points=[MessageHandler(filters.ALL, self.start_end)], states={}, fallbacks=[] ) message = Message(0, date=None, chat=Chat(0, Chat.CHANNEL, 'Misses Test'), bot=bot) @@ -885,7 +885,7 @@ class TestConversationHandler: handler = ConversationHandler( entry_points=[CommandHandler("start", conv_entry)], - states={1: [MessageHandler(Filters.all, raise_error)]}, + states={1: [MessageHandler(filters.ALL, raise_error)]}, fallbacks=self.fallbacks, run_async=True, ) @@ -1168,7 +1168,7 @@ class TestConversationHandler: { ConversationHandler.TIMEOUT: [ CommandHandler('brew', self.passout), - MessageHandler(~Filters.regex('oding'), self.passout2), + MessageHandler(~filters.Regex('oding'), self.passout2), ] } ) @@ -1228,7 +1228,7 @@ class TestConversationHandler: { ConversationHandler.TIMEOUT: [ CommandHandler('brew', self.passout_context), - MessageHandler(~Filters.regex('oding'), self.passout2_context), + MessageHandler(~filters.Regex('oding'), self.passout2_context), ] } ) diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index f828ccf95..72e63ab96 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -28,7 +28,7 @@ from telegram.ext import ( CommandHandler, MessageHandler, JobQueue, - Filters, + filters, Defaults, CallbackContext, ContextTypes, @@ -164,7 +164,7 @@ class TestDispatcher: if hasattr(context, 'my_flag'): pytest.fail() - dp.add_handler(MessageHandler(Filters.regex('test'), one), group=1) + dp.add_handler(MessageHandler(filters.Regex('test'), one), group=1) dp.add_handler(MessageHandler(None, two), group=2) u = Update(1, Message(1, None, None, None, text='test')) dp.process_update(u) @@ -207,8 +207,8 @@ class TestDispatcher: """ Make sure that errors raised in error handlers don't break the main loop of the dispatcher """ - handler_raise_error = MessageHandler(Filters.all, self.callback_raise_error) - handler_increase_count = MessageHandler(Filters.all, self.callback_increase_count) + handler_raise_error = MessageHandler(filters.ALL, self.callback_raise_error) + handler_increase_count = MessageHandler(filters.ALL, self.callback_increase_count) error = TelegramError('Unauthorized.') dp.add_error_handler(self.error_handler_raise_error) @@ -235,7 +235,7 @@ class TestDispatcher: # set defaults value to dp.bot dp.bot._defaults = Defaults(run_async=run_async) try: - dp.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) + dp.add_handler(MessageHandler(filters.ALL, self.callback_raise_error)) dp.add_error_handler(self.error_handler_context) monkeypatch.setattr(dp, 'run_async', mock_async_err_handler) @@ -257,7 +257,7 @@ class TestDispatcher: # set defaults value to dp.bot dp.bot._defaults = Defaults(run_async=run_async) try: - dp.add_handler(MessageHandler(Filters.all, lambda u, c: None)) + dp.add_handler(MessageHandler(filters.ALL, lambda u, c: None)) monkeypatch.setattr(dp, 'run_async', mock_run_async) dp.process_update(self.message_update) assert self.received == expected_output @@ -287,7 +287,7 @@ class TestDispatcher: def callback(update, context): raise DispatcherHandlerStop() - dp.add_handler(MessageHandler(Filters.all, callback, run_async=True)) + dp.add_handler(MessageHandler(filters.ALL, callback, run_async=True)) dp.update_queue.put(self.message_update) sleep(0.1) @@ -299,7 +299,7 @@ class TestDispatcher: def test_add_async_handler(self, dp): dp.add_handler( MessageHandler( - Filters.all, + filters.ALL, self.callback_received, run_async=True, ) @@ -320,7 +320,7 @@ class TestDispatcher: assert caplog.records[-1].getMessage().startswith('No error handlers are registered') def test_async_handler_async_error_handler_context(self, dp): - dp.add_handler(MessageHandler(Filters.all, self.callback_raise_error, run_async=True)) + dp.add_handler(MessageHandler(filters.ALL, self.callback_raise_error, run_async=True)) dp.add_error_handler(self.error_handler_context, run_async=True) dp.update_queue.put(self.message_update) @@ -328,7 +328,7 @@ class TestDispatcher: assert self.received == self.message_update.message.text def test_async_handler_error_handler_that_raises_error(self, dp, caplog): - handler = MessageHandler(Filters.all, self.callback_raise_error, run_async=True) + handler = MessageHandler(filters.ALL, self.callback_raise_error, run_async=True) dp.add_handler(handler) dp.add_error_handler(self.error_handler_raise_error, run_async=False) @@ -342,13 +342,13 @@ class TestDispatcher: # Make sure that the main loop still runs dp.remove_handler(handler) - dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count, run_async=True)) + dp.add_handler(MessageHandler(filters.ALL, self.callback_increase_count, run_async=True)) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 1 def test_async_handler_async_error_handler_that_raises_error(self, dp, caplog): - handler = MessageHandler(Filters.all, self.callback_raise_error, run_async=True) + handler = MessageHandler(filters.ALL, self.callback_raise_error, run_async=True) dp.add_handler(handler) dp.add_error_handler(self.error_handler_raise_error, run_async=True) @@ -362,13 +362,13 @@ class TestDispatcher: # Make sure that the main loop still runs dp.remove_handler(handler) - dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count, run_async=True)) + dp.add_handler(MessageHandler(filters.ALL, self.callback_increase_count, run_async=True)) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 1 def test_error_in_handler(self, dp): - dp.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) + dp.add_handler(MessageHandler(filters.ALL, self.callback_raise_error)) dp.add_error_handler(self.error_handler_context) dp.update_queue.put(self.message_update) @@ -376,7 +376,7 @@ class TestDispatcher: assert self.received == self.message_update.message.text def test_add_remove_handler(self, dp): - handler = MessageHandler(Filters.all, self.callback_increase_count) + handler = MessageHandler(filters.ALL, self.callback_increase_count) dp.add_handler(handler) dp.update_queue.put(self.message_update) sleep(0.1) @@ -386,7 +386,7 @@ class TestDispatcher: assert self.count == 1 def test_add_remove_handler_non_default_group(self, dp): - handler = MessageHandler(Filters.all, self.callback_increase_count) + handler = MessageHandler(filters.ALL, self.callback_increase_count) dp.add_handler(handler, group=2) with pytest.raises(KeyError): dp.remove_handler(handler) @@ -397,17 +397,17 @@ class TestDispatcher: dp.start() def test_handler_order_in_group(self, dp): - dp.add_handler(MessageHandler(Filters.photo, self.callback_set_count(1))) - dp.add_handler(MessageHandler(Filters.all, self.callback_set_count(2))) - dp.add_handler(MessageHandler(Filters.text, self.callback_set_count(3))) + dp.add_handler(MessageHandler(filters.PHOTO, self.callback_set_count(1))) + dp.add_handler(MessageHandler(filters.ALL, self.callback_set_count(2))) + dp.add_handler(MessageHandler(filters.TEXT, self.callback_set_count(3))) dp.update_queue.put(self.message_update) sleep(0.1) assert self.count == 2 def test_groups(self, dp): - dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count)) - dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count), group=2) - dp.add_handler(MessageHandler(Filters.all, self.callback_increase_count), group=-1) + dp.add_handler(MessageHandler(filters.ALL, self.callback_increase_count)) + dp.add_handler(MessageHandler(filters.ALL, self.callback_increase_count), group=2) + dp.add_handler(MessageHandler(filters.ALL, self.callback_increase_count), group=-1) dp.update_queue.put(self.message_update) sleep(0.1) @@ -418,7 +418,7 @@ class TestDispatcher: with pytest.raises(TypeError, match='handler is not an instance of'): dp.add_handler(handler) - handler = MessageHandler(Filters.photo, self.callback_set_count(1)) + handler = MessageHandler(filters.PHOTO, self.callback_set_count(1)) with pytest.raises(TypeError, match='group is not int'): dp.add_handler(handler, 'one') @@ -733,7 +733,7 @@ class TestDispatcher: update = Update( 1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') ) - handler = MessageHandler(Filters.all, callback) + handler = MessageHandler(filters.ALL, callback) dp.add_handler(handler) dp.add_error_handler(error) @@ -801,7 +801,7 @@ class TestDispatcher: def callback(update, context): pass - handler = MessageHandler(Filters.all, callback) + handler = MessageHandler(filters.ALL, callback) dp.add_handler(handler) dp.persistence = OwnPersistence() @@ -832,7 +832,7 @@ class TestDispatcher: monkeypatch.setattr(dp, 'update_persistence', update_persistence) for group in range(5): - dp.add_handler(MessageHandler(Filters.text, dummy_callback), group=group) + dp.add_handler(MessageHandler(filters.TEXT, dummy_callback), group=group) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text=None)) dp.process_update(update) @@ -854,7 +854,7 @@ class TestDispatcher: for group in range(5): dp.add_handler( - MessageHandler(Filters.text, dummy_callback, run_async=True), group=group + MessageHandler(filters.TEXT, dummy_callback, run_async=True), group=group ) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) @@ -864,7 +864,7 @@ class TestDispatcher: dp.bot._defaults = Defaults(run_async=True) try: for group in range(5): - dp.add_handler(MessageHandler(Filters.text, dummy_callback), group=group) + dp.add_handler(MessageHandler(filters.TEXT, dummy_callback), group=group) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) dp.process_update(update) @@ -885,9 +885,9 @@ class TestDispatcher: for group in range(5): dp.add_handler( - MessageHandler(Filters.text, dummy_callback, run_async=True), group=group + MessageHandler(filters.TEXT, dummy_callback, run_async=True), group=group ) - dp.add_handler(MessageHandler(Filters.text, dummy_callback, run_async=run_async), group=5) + dp.add_handler(MessageHandler(filters.TEXT, dummy_callback, run_async=run_async), group=5) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) dp.process_update(update) @@ -907,7 +907,7 @@ class TestDispatcher: try: for group in range(5): - dp.add_handler(MessageHandler(Filters.text, dummy_callback), group=group) + dp.add_handler(MessageHandler(filters.TEXT, dummy_callback), group=group) update = Update(1, message=Message(1, None, Chat(1, ''), from_user=None, text='Text')) dp.process_update(update) @@ -949,7 +949,7 @@ class TestDispatcher: .build() ) dispatcher.add_error_handler(error_handler) - dispatcher.add_handler(MessageHandler(Filters.all, self.callback_raise_error)) + dispatcher.add_handler(MessageHandler(filters.ALL, self.callback_raise_error)) dispatcher.process_update(self.message_update) sleep(0.1) @@ -974,7 +974,7 @@ class TestDispatcher: ) .build() ) - dispatcher.add_handler(MessageHandler(Filters.all, callback)) + dispatcher.add_handler(MessageHandler(filters.ALL, callback)) dispatcher.process_update(self.message_update) sleep(0.1) diff --git a/tests/test_filters.py b/tests/test_filters.py index 2aaba6c79..c9d526c8e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -20,9 +20,8 @@ import datetime import pytest -from telegram import Message, User, Chat, MessageEntity, Document, Update, Dice -from telegram.ext import Filters, BaseFilter, MessageFilter, UpdateFilter - +from telegram import Message, User, Chat, MessageEntity, Document, Update, Dice, CallbackQuery +from telegram.ext import filters import inspect import re @@ -51,7 +50,7 @@ def message_entity(request): @pytest.fixture( scope='class', - params=[{'class': MessageFilter}, {'class': UpdateFilter}], + params=[{'class': filters.MessageFilter}, {'class': filters.UpdateFilter}], ids=['MessageFilter', 'UpdateFilter'], ) def base_class(request): @@ -65,10 +64,14 @@ class TestFilters: the correct number of arguments, then test each filter separately. Also tests setting custom attributes on custom filters. """ - # The total no. of filters excluding filters defined in __all__ is about 70 as of 16/2/21. + + def filter_class(obj): + return True if inspect.isclass(obj) and "filters" in repr(obj) else False + + # The total no. of filters is about 72 as of 31/10/21. # Gather all the filters to test using DFS- visited = [] - classes = inspect.getmembers(Filters, predicate=inspect.isclass) # List[Tuple[str, type]] + classes = inspect.getmembers(filters, predicate=filter_class) # List[Tuple[str, type]] stack = classes.copy() while stack: cls = stack[-1][-1] # get last element and its class @@ -87,23 +90,34 @@ class TestFilters: # Now start the actual testing for name, cls in classes: # Can't instantiate abstract classes without overriding methods, so skip them for now - if inspect.isabstract(cls) or name in {'__class__', '__base__'}: + exclude = {'_MergedFilter', '_XORFilter'} + if inspect.isabstract(cls) or name in {'__class__', '__base__'} | exclude: continue assert '__slots__' in cls.__dict__, f"Filter {name!r} doesn't have __slots__" - # get no. of args minus the 'self' argument - args = len(inspect.signature(cls.__init__).parameters) - 1 - if cls.__base__.__name__ == '_ChatUserBaseFilter': # Special case, only 1 arg needed + # get no. of args minus the 'self', 'args' and 'kwargs' argument + init_sig = inspect.signature(cls.__init__).parameters + extra = 0 + for param in init_sig: + if param in {'self', 'args', 'kwargs'}: + extra += 1 + args = len(init_sig) - extra + + if not args: + inst = cls() + elif args == 1: inst = cls('1') else: - inst = cls() if args < 1 else cls(*['blah'] * args) # unpack variable no. of args + inst = cls(*['blah']) + + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), f"same slot in {name}" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), f"same slot in {name}" for attr in cls.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}' for {name}" - class CustomFilter(MessageFilter): + class CustomFilter(filters.MessageFilter): def filter(self, message: Message): pass @@ -111,455 +125,453 @@ class TestFilters: CustomFilter().custom = 'allowed' # Test setting custom attr to custom filters def test_filters_all(self, update): - assert Filters.all(update) + assert filters.ALL.check_update(update) def test_filters_text(self, update): update.message.text = 'test' - assert (Filters.text)(update) + assert filters.TEXT.check_update(update) update.message.text = '/test' - assert (Filters.text)(update) + assert filters.Text().check_update(update) def test_filters_text_strings(self, update): update.message.text = '/test' - assert Filters.text({'/test', 'test1'})(update) - assert not Filters.text(['test1', 'test2'])(update) + assert filters.Text(('/test', 'test1')).check_update(update) + assert not filters.Text(['test1', 'test2']).check_update(update) def test_filters_caption(self, update): update.message.caption = 'test' - assert (Filters.caption)(update) + assert filters.CAPTION.check_update(update) update.message.caption = None - assert not (Filters.caption)(update) + assert not filters.CAPTION.check_update(update) def test_filters_caption_strings(self, update): update.message.caption = 'test' - assert Filters.caption({'test', 'test1'})(update) - assert not Filters.caption(['test1', 'test2'])(update) + assert filters.Caption(('test', 'test1')).check_update(update) + assert not filters.Caption(['test1', 'test2']).check_update(update) def test_filters_command_default(self, update): update.message.text = 'test' - assert not Filters.command(update) + assert not filters.COMMAND.check_update(update) update.message.text = '/test' - assert not Filters.command(update) + assert not filters.COMMAND.check_update(update) # Only accept commands at the beginning update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 3, 5)] - assert not Filters.command(update) + assert not filters.COMMAND.check_update(update) update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] - assert Filters.command(update) + assert filters.COMMAND.check_update(update) def test_filters_command_anywhere(self, update): - update.message.text = 'test /cmd' - assert not (Filters.command(False))(update) update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 5, 4)] - assert (Filters.command(False))(update) + assert filters.Command(False).check_update(update) def test_filters_regex(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.text = '/start deep-linked param' - result = Filters.regex(r'deep-linked param')(update) + result = filters.Regex(r'deep-linked param').check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert type(matches[0]) is SRE_TYPE + assert type(matches[0]) is sre_type update.message.text = '/help' - assert Filters.regex(r'help')(update) + assert filters.Regex(r'help').check_update(update) update.message.text = 'test' - assert not Filters.regex(r'fail')(update) - assert Filters.regex(r'test')(update) - assert Filters.regex(re.compile(r'test'))(update) - assert Filters.regex(re.compile(r'TEST', re.IGNORECASE))(update) + assert not filters.Regex(r'fail').check_update(update) + assert filters.Regex(r'test').check_update(update) + assert filters.Regex(re.compile(r'test')).check_update(update) + assert filters.Regex(re.compile(r'TEST', re.IGNORECASE)).check_update(update) update.message.text = 'i love python' - assert Filters.regex(r'.\b[lo]{2}ve python')(update) + assert filters.Regex(r'.\b[lo]{2}ve python').check_update(update) update.message.text = None - assert not Filters.regex(r'fail')(update) + assert not filters.Regex(r'fail').check_update(update) def test_filters_regex_multiple(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.text = '/start deep-linked param' - result = (Filters.regex('deep') & Filters.regex(r'linked param'))(update) + result = (filters.Regex('deep') & filters.Regex(r'linked param')).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.regex('deep') | Filters.regex(r'linked param'))(update) + assert all(type(res) is sre_type for res in matches) + result = (filters.Regex('deep') | filters.Regex(r'linked param')).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.regex('not int') | Filters.regex(r'linked param'))(update) + assert all(type(res) is sre_type for res in matches) + result = (filters.Regex('not int') | filters.Regex(r'linked param')).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.regex('not int') & Filters.regex(r'linked param'))(update) + assert all(type(res) is sre_type for res in matches) + result = (filters.Regex('not int') & filters.Regex(r'linked param')).check_update(update) assert not result def test_filters_merged_with_regex(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.text = '/start deep-linked param' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = (Filters.command & Filters.regex(r'linked param'))(update) + result = (filters.COMMAND & filters.Regex(r'linked param')).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.regex(r'linked param') & Filters.command)(update) + assert all(type(res) is sre_type for res in matches) + result = (filters.Regex(r'linked param') & filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.regex(r'linked param') | Filters.command)(update) + assert all(type(res) is sre_type for res in matches) + result = (filters.Regex(r'linked param') | filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) # Should not give a match since it's a or filter and it short circuits - result = (Filters.command | Filters.regex(r'linked param'))(update) + result = (filters.COMMAND | filters.Regex(r'linked param')).check_update(update) assert result is True def test_regex_complex_merges(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.text = 'test it out' - test_filter = Filters.regex('test') & ( - (Filters.status_update | Filters.forwarded) | Filters.regex('out') + test_filter = filters.Regex('test') & ( + (filters.StatusUpdate.ALL | filters.FORWARDED) | filters.Regex('out') ) - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 2 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.forward_date = datetime.datetime.utcnow() - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.text = 'test it' - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.forward_date = None - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.text = 'test it out' - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.pinned_message = True - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.text = 'it out' - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.text = 'test it out' update.message.forward_date = None update.message.pinned_message = None - test_filter = (Filters.regex('test') | Filters.command) & ( - Filters.regex('it') | Filters.status_update + test_filter = (filters.Regex('test') | filters.COMMAND) & ( + filters.Regex('it') | filters.StatusUpdate.ALL ) - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 2 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.text = 'test' - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.pinned_message = True - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 1 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.text = 'nothing' - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, bool) update.message.text = '/start it' - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 1 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) def test_regex_inverted(self, update): update.message.text = '/start deep-linked param' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] - filter = ~Filters.regex(r'deep-linked param') - result = filter(update) + inv = ~filters.Regex(r'deep-linked param') + result = inv.check_update(update) assert not result update.message.text = 'not it' - result = filter(update) + result = inv.check_update(update) assert result assert isinstance(result, bool) - filter = ~Filters.regex('linked') & Filters.command + inv = ~filters.Regex('linked') & filters.COMMAND update.message.text = "it's linked" - result = filter(update) + result = inv.check_update(update) assert not result update.message.text = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = filter(update) + result = inv.check_update(update) assert result update.message.text = '/linked' - result = filter(update) + result = inv.check_update(update) assert not result - filter = ~Filters.regex('linked') | Filters.command + inv = ~filters.Regex('linked') | filters.COMMAND update.message.text = "it's linked" update.message.entities = [] - result = filter(update) + result = inv.check_update(update) assert not result update.message.text = '/start linked' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = filter(update) + result = inv.check_update(update) assert result update.message.text = '/start' - result = filter(update) + result = inv.check_update(update) assert result update.message.text = 'nothig' update.message.entities = [] - result = filter(update) + result = inv.check_update(update) assert result def test_filters_caption_regex(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.caption = '/start deep-linked param' - result = Filters.caption_regex(r'deep-linked param')(update) + result = filters.CaptionRegex(r'deep-linked param').check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert type(matches[0]) is SRE_TYPE + assert type(matches[0]) is sre_type update.message.caption = '/help' - assert Filters.caption_regex(r'help')(update) + assert filters.CaptionRegex(r'help').check_update(update) update.message.caption = 'test' - assert not Filters.caption_regex(r'fail')(update) - assert Filters.caption_regex(r'test')(update) - assert Filters.caption_regex(re.compile(r'test'))(update) - assert Filters.caption_regex(re.compile(r'TEST', re.IGNORECASE))(update) + assert not filters.CaptionRegex(r'fail').check_update(update) + assert filters.CaptionRegex(r'test').check_update(update) + assert filters.CaptionRegex(re.compile(r'test')).check_update(update) + assert filters.CaptionRegex(re.compile(r'TEST', re.IGNORECASE)).check_update(update) update.message.caption = 'i love python' - assert Filters.caption_regex(r'.\b[lo]{2}ve python')(update) + assert filters.CaptionRegex(r'.\b[lo]{2}ve python').check_update(update) update.message.caption = None - assert not Filters.caption_regex(r'fail')(update) + assert not filters.CaptionRegex(r'fail').check_update(update) def test_filters_caption_regex_multiple(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.caption = '/start deep-linked param' - result = (Filters.caption_regex('deep') & Filters.caption_regex(r'linked param'))(update) + _and = filters.CaptionRegex('deep') & filters.CaptionRegex(r'linked param') + result = _and.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.caption_regex('deep') | Filters.caption_regex(r'linked param'))(update) + assert all(type(res) is sre_type for res in matches) + _or = filters.CaptionRegex('deep') | filters.CaptionRegex(r'linked param') + result = _or.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.caption_regex('not int') | Filters.caption_regex(r'linked param'))( - update - ) + assert all(type(res) is sre_type for res in matches) + _or = filters.CaptionRegex('not int') | filters.CaptionRegex(r'linked param') + result = _or.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.caption_regex('not int') & Filters.caption_regex(r'linked param'))( - update - ) + assert all(type(res) is sre_type for res in matches) + _and = filters.CaptionRegex('not int') & filters.CaptionRegex(r'linked param') + result = _and.check_update(update) assert not result def test_filters_merged_with_caption_regex(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.caption = '/start deep-linked param' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = (Filters.command & Filters.caption_regex(r'linked param'))(update) + result = (filters.COMMAND & filters.CaptionRegex(r'linked param')).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.caption_regex(r'linked param') & Filters.command)(update) + assert all(type(res) is sre_type for res in matches) + result = (filters.CaptionRegex(r'linked param') & filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) - result = (Filters.caption_regex(r'linked param') | Filters.command)(update) + assert all(type(res) is sre_type for res in matches) + result = (filters.CaptionRegex(r'linked param') | filters.COMMAND).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) # Should not give a match since it's a or filter and it short circuits - result = (Filters.command | Filters.caption_regex(r'linked param'))(update) + result = (filters.COMMAND | filters.CaptionRegex(r'linked param')).check_update(update) assert result is True def test_caption_regex_complex_merges(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.caption = 'test it out' - test_filter = Filters.caption_regex('test') & ( - (Filters.status_update | Filters.forwarded) | Filters.caption_regex('out') + test_filter = filters.CaptionRegex('test') & ( + (filters.StatusUpdate.ALL | filters.FORWARDED) | filters.CaptionRegex('out') ) - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 2 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.forward_date = datetime.datetime.utcnow() - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.caption = 'test it' - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.forward_date = None - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.caption = 'test it out' - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.pinned_message = True - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.caption = 'it out' - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.caption = 'test it out' update.message.forward_date = None update.message.pinned_message = None - test_filter = (Filters.caption_regex('test') | Filters.command) & ( - Filters.caption_regex('it') | Filters.status_update + test_filter = (filters.CaptionRegex('test') | filters.COMMAND) & ( + filters.CaptionRegex('it') | filters.StatusUpdate.ALL ) - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 2 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.caption = 'test' - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.pinned_message = True - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 1 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) update.message.caption = 'nothing' - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.caption = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, bool) update.message.caption = '/start it' - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) assert len(matches) == 1 - assert all(type(res) is SRE_TYPE for res in matches) + assert all(type(res) is sre_type for res in matches) def test_caption_regex_inverted(self, update): update.message.caption = '/start deep-linked param' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] - test_filter = ~Filters.caption_regex(r'deep-linked param') - result = test_filter(update) + test_filter = ~filters.CaptionRegex(r'deep-linked param') + result = test_filter.check_update(update) assert not result update.message.caption = 'not it' - result = test_filter(update) + result = test_filter.check_update(update) assert result assert isinstance(result, bool) - test_filter = ~Filters.caption_regex('linked') & Filters.command + test_filter = ~filters.CaptionRegex('linked') & filters.COMMAND update.message.caption = "it's linked" - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.caption = '/start' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = test_filter(update) + result = test_filter.check_update(update) assert result update.message.caption = '/linked' - result = test_filter(update) + result = test_filter.check_update(update) assert not result - test_filter = ~Filters.caption_regex('linked') | Filters.command + test_filter = ~filters.CaptionRegex('linked') | filters.COMMAND update.message.caption = "it's linked" update.message.entities = [] - result = test_filter(update) + result = test_filter.check_update(update) assert not result update.message.caption = '/start linked' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 6)] - result = test_filter(update) + result = test_filter.check_update(update) assert result update.message.caption = '/start' - result = test_filter(update) + result = test_filter.check_update(update) assert result update.message.caption = 'nothig' update.message.entities = [] - result = test_filter(update) + result = test_filter.check_update(update) assert result def test_filters_reply(self, update): @@ -570,121 +582,121 @@ class TestFilters: from_user=User(1, 'TestOther', False), ) update.message.text = 'test' - assert not Filters.reply(update) + assert not filters.REPLY.check_update(update) update.message.reply_to_message = another_message - assert Filters.reply(update) + assert filters.REPLY.check_update(update) def test_filters_audio(self, update): - assert not Filters.audio(update) + assert not filters.AUDIO.check_update(update) update.message.audio = 'test' - assert Filters.audio(update) + assert filters.AUDIO.check_update(update) def test_filters_document(self, update): - assert not Filters.document(update) + assert not filters.DOCUMENT.check_update(update) update.message.document = 'test' - assert Filters.document(update) + assert filters.DOCUMENT.check_update(update) def test_filters_document_type(self, update): update.message.document = Document( "file_id", 'unique_id', mime_type="application/vnd.android.package-archive" ) - assert Filters.document.apk(update) - assert Filters.document.application(update) - assert not Filters.document.doc(update) - assert not Filters.document.audio(update) + assert filters.Document.APK.check_update(update) + assert filters.Document.APPLICATION.check_update(update) + assert not filters.Document.DOC.check_update(update) + assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "application/msword" - assert Filters.document.doc(update) - assert Filters.document.application(update) - assert not Filters.document.docx(update) - assert not Filters.document.audio(update) + assert filters.Document.DOC.check_update(update) + assert filters.Document.APPLICATION.check_update(update) + assert not filters.Document.DOCX.check_update(update) + assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = ( "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) - assert Filters.document.docx(update) - assert Filters.document.application(update) - assert not Filters.document.exe(update) - assert not Filters.document.audio(update) + assert filters.Document.DOCX.check_update(update) + assert filters.Document.APPLICATION.check_update(update) + assert not filters.Document.EXE.check_update(update) + assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "application/x-ms-dos-executable" - assert Filters.document.exe(update) - assert Filters.document.application(update) - assert not Filters.document.docx(update) - assert not Filters.document.audio(update) + update.message.document.mime_type = "application/octet-stream" + assert filters.Document.EXE.check_update(update) + assert filters.Document.APPLICATION.check_update(update) + assert not filters.Document.DOCX.check_update(update) + assert not filters.Document.AUDIO.check_update(update) - update.message.document.mime_type = "video/mp4" - assert Filters.document.gif(update) - assert Filters.document.video(update) - assert not Filters.document.jpg(update) - assert not Filters.document.text(update) + update.message.document.mime_type = "image/gif" + assert filters.Document.GIF.check_update(update) + assert filters.Document.IMAGE.check_update(update) + assert not filters.Document.JPG.check_update(update) + assert not filters.Document.TEXT.check_update(update) update.message.document.mime_type = "image/jpeg" - assert Filters.document.jpg(update) - assert Filters.document.image(update) - assert not Filters.document.mp3(update) - assert not Filters.document.video(update) + assert filters.Document.JPG.check_update(update) + assert filters.Document.IMAGE.check_update(update) + assert not filters.Document.MP3.check_update(update) + assert not filters.Document.VIDEO.check_update(update) update.message.document.mime_type = "audio/mpeg" - assert Filters.document.mp3(update) - assert Filters.document.audio(update) - assert not Filters.document.pdf(update) - assert not Filters.document.image(update) + assert filters.Document.MP3.check_update(update) + assert filters.Document.AUDIO.check_update(update) + assert not filters.Document.PDF.check_update(update) + assert not filters.Document.IMAGE.check_update(update) update.message.document.mime_type = "application/pdf" - assert Filters.document.pdf(update) - assert Filters.document.application(update) - assert not Filters.document.py(update) - assert not Filters.document.audio(update) + assert filters.Document.PDF.check_update(update) + assert filters.Document.APPLICATION.check_update(update) + assert not filters.Document.PY.check_update(update) + assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "text/x-python" - assert Filters.document.py(update) - assert Filters.document.text(update) - assert not Filters.document.svg(update) - assert not Filters.document.application(update) + assert filters.Document.PY.check_update(update) + assert filters.Document.TEXT.check_update(update) + assert not filters.Document.SVG.check_update(update) + assert not filters.Document.APPLICATION.check_update(update) update.message.document.mime_type = "image/svg+xml" - assert Filters.document.svg(update) - assert Filters.document.image(update) - assert not Filters.document.txt(update) - assert not Filters.document.video(update) + assert filters.Document.SVG.check_update(update) + assert filters.Document.IMAGE.check_update(update) + assert not filters.Document.TXT.check_update(update) + assert not filters.Document.VIDEO.check_update(update) update.message.document.mime_type = "text/plain" - assert Filters.document.txt(update) - assert Filters.document.text(update) - assert not Filters.document.targz(update) - assert not Filters.document.application(update) + assert filters.Document.TXT.check_update(update) + assert filters.Document.TEXT.check_update(update) + assert not filters.Document.TARGZ.check_update(update) + assert not filters.Document.APPLICATION.check_update(update) update.message.document.mime_type = "application/x-compressed-tar" - assert Filters.document.targz(update) - assert Filters.document.application(update) - assert not Filters.document.wav(update) - assert not Filters.document.audio(update) + assert filters.Document.TARGZ.check_update(update) + assert filters.Document.APPLICATION.check_update(update) + assert not filters.Document.WAV.check_update(update) + assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "audio/x-wav" - assert Filters.document.wav(update) - assert Filters.document.audio(update) - assert not Filters.document.xml(update) - assert not Filters.document.image(update) + assert filters.Document.WAV.check_update(update) + assert filters.Document.AUDIO.check_update(update) + assert not filters.Document.XML.check_update(update) + assert not filters.Document.IMAGE.check_update(update) - update.message.document.mime_type = "application/xml" - assert Filters.document.xml(update) - assert Filters.document.application(update) - assert not Filters.document.zip(update) - assert not Filters.document.audio(update) + update.message.document.mime_type = "text/xml" + assert filters.Document.XML.check_update(update) + assert filters.Document.TEXT.check_update(update) + assert not filters.Document.ZIP.check_update(update) + assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "application/zip" - assert Filters.document.zip(update) - assert Filters.document.application(update) - assert not Filters.document.apk(update) - assert not Filters.document.audio(update) + assert filters.Document.ZIP.check_update(update) + assert filters.Document.APPLICATION.check_update(update) + assert not filters.Document.APK.check_update(update) + assert not filters.Document.AUDIO.check_update(update) update.message.document.mime_type = "image/x-rgb" - assert not Filters.document.category("application/")(update) - assert not Filters.document.mime_type("application/x-sh")(update) + assert not filters.Document.Category("application/").check_update(update) + assert not filters.Document.MimeType("application/x-sh").check_update(update) update.message.document.mime_type = "application/x-sh" - assert Filters.document.category("application/")(update) - assert Filters.document.mime_type("application/x-sh")(update) + assert filters.Document.Category("application/").check_update(update) + assert filters.Document.MimeType("application/x-sh").check_update(update) def test_filters_file_extension_basic(self, update): update.message.document = Document( @@ -693,18 +705,18 @@ class TestFilters: file_name="file.jpg", mime_type="image/jpeg", ) - assert Filters.document.file_extension("jpg")(update) - assert not Filters.document.file_extension("jpeg")(update) - assert not Filters.document.file_extension("file.jpg")(update) + assert filters.Document.FileExtension("jpg").check_update(update) + assert not filters.Document.FileExtension("jpeg").check_update(update) + assert not filters.Document.FileExtension("file.jpg").check_update(update) update.message.document.file_name = "file.tar.gz" - assert Filters.document.file_extension("tar.gz")(update) - assert Filters.document.file_extension("gz")(update) - assert not Filters.document.file_extension("tgz")(update) - assert not Filters.document.file_extension("jpg")(update) + assert filters.Document.FileExtension("tar.gz").check_update(update) + assert filters.Document.FileExtension("gz").check_update(update) + assert not filters.Document.FileExtension("tgz").check_update(update) + assert not filters.Document.FileExtension("jpg").check_update(update) update.message.document = None - assert not Filters.document.file_extension("jpg")(update) + assert not filters.Document.FileExtension("jpg").check_update(update) def test_filters_file_extension_minds_dots(self, update): update.message.document = Document( @@ -713,27 +725,27 @@ class TestFilters: file_name="file.jpg", mime_type="image/jpeg", ) - assert not Filters.document.file_extension(".jpg")(update) - assert not Filters.document.file_extension("e.jpg")(update) - assert not Filters.document.file_extension("file.jpg")(update) - assert not Filters.document.file_extension("")(update) + assert not filters.Document.FileExtension(".jpg").check_update(update) + assert not filters.Document.FileExtension("e.jpg").check_update(update) + assert not filters.Document.FileExtension("file.jpg").check_update(update) + assert not filters.Document.FileExtension("").check_update(update) update.message.document.file_name = "file..jpg" - assert Filters.document.file_extension("jpg")(update) - assert Filters.document.file_extension(".jpg")(update) - assert not Filters.document.file_extension("..jpg")(update) + assert filters.Document.FileExtension("jpg").check_update(update) + assert filters.Document.FileExtension(".jpg").check_update(update) + assert not filters.Document.FileExtension("..jpg").check_update(update) update.message.document.file_name = "file.docx" - assert Filters.document.file_extension("docx")(update) - assert not Filters.document.file_extension("doc")(update) - assert not Filters.document.file_extension("ocx")(update) + assert filters.Document.FileExtension("docx").check_update(update) + assert not filters.Document.FileExtension("doc").check_update(update) + assert not filters.Document.FileExtension("ocx").check_update(update) update.message.document.file_name = "file" - assert not Filters.document.file_extension("")(update) - assert not Filters.document.file_extension("file")(update) + assert not filters.Document.FileExtension("").check_update(update) + assert not filters.Document.FileExtension("file").check_update(update) update.message.document.file_name = "file." - assert Filters.document.file_extension("")(update) + assert filters.Document.FileExtension("").check_update(update) def test_filters_file_extension_none_arg(self, update): update.message.document = Document( @@ -742,17 +754,17 @@ class TestFilters: file_name="file.jpg", mime_type="image/jpeg", ) - assert not Filters.document.file_extension(None)(update) + assert not filters.Document.FileExtension(None).check_update(update) update.message.document.file_name = "file" - assert Filters.document.file_extension(None)(update) - assert not Filters.document.file_extension("None")(update) + assert filters.Document.FileExtension(None).check_update(update) + assert not filters.Document.FileExtension("None").check_update(update) update.message.document.file_name = "file." - assert not Filters.document.file_extension(None)(update) + assert not filters.Document.FileExtension(None).check_update(update) update.message.document = None - assert not Filters.document.file_extension(None)(update) + assert not filters.Document.FileExtension(None).check_update(update) def test_filters_file_extension_case_sensitivity(self, update): update.message.document = Document( @@ -761,370 +773,372 @@ class TestFilters: file_name="file.jpg", mime_type="image/jpeg", ) - assert Filters.document.file_extension("JPG")(update) - assert Filters.document.file_extension("jpG")(update) + assert filters.Document.FileExtension("JPG").check_update(update) + assert filters.Document.FileExtension("jpG").check_update(update) update.message.document.file_name = "file.JPG" - assert Filters.document.file_extension("jpg")(update) - assert not Filters.document.file_extension("jpg", case_sensitive=True)(update) + assert filters.Document.FileExtension("jpg").check_update(update) + assert not filters.Document.FileExtension("jpg", case_sensitive=True).check_update(update) update.message.document.file_name = "file.Dockerfile" - assert Filters.document.file_extension("Dockerfile", case_sensitive=True)(update) - assert not Filters.document.file_extension("DOCKERFILE", case_sensitive=True)(update) + assert filters.Document.FileExtension("Dockerfile", case_sensitive=True).check_update( + update + ) + assert not filters.Document.FileExtension("DOCKERFILE", case_sensitive=True).check_update( + update + ) def test_filters_file_extension_name(self): - assert Filters.document.file_extension("jpg").name == ( - "Filters.document.file_extension('jpg')" + assert filters.Document.FileExtension("jpg").name == ( + "filters.Document.FileExtension('jpg')" ) - assert Filters.document.file_extension("JPG").name == ( - "Filters.document.file_extension('jpg')" + assert filters.Document.FileExtension("JPG").name == ( + "filters.Document.FileExtension('jpg')" ) - assert Filters.document.file_extension("jpg", case_sensitive=True).name == ( - "Filters.document.file_extension('jpg', case_sensitive=True)" + assert filters.Document.FileExtension("jpg", case_sensitive=True).name == ( + "filters.Document.FileExtension('jpg', case_sensitive=True)" ) - assert Filters.document.file_extension("JPG", case_sensitive=True).name == ( - "Filters.document.file_extension('JPG', case_sensitive=True)" + assert filters.Document.FileExtension("JPG", case_sensitive=True).name == ( + "filters.Document.FileExtension('JPG', case_sensitive=True)" ) - assert Filters.document.file_extension(".jpg").name == ( - "Filters.document.file_extension('.jpg')" - ) - assert Filters.document.file_extension("").name == "Filters.document.file_extension('')" - assert ( - Filters.document.file_extension(None).name == "Filters.document.file_extension(None)" + assert filters.Document.FileExtension(".jpg").name == ( + "filters.Document.FileExtension('.jpg')" ) + assert filters.Document.FileExtension("").name == "filters.Document.FileExtension('')" + assert filters.Document.FileExtension(None).name == "filters.Document.FileExtension(None)" def test_filters_animation(self, update): - assert not Filters.animation(update) + assert not filters.ANIMATION.check_update(update) update.message.animation = 'test' - assert Filters.animation(update) + assert filters.ANIMATION.check_update(update) def test_filters_photo(self, update): - assert not Filters.photo(update) + assert not filters.PHOTO.check_update(update) update.message.photo = 'test' - assert Filters.photo(update) + assert filters.PHOTO.check_update(update) def test_filters_sticker(self, update): - assert not Filters.sticker(update) + assert not filters.STICKER.check_update(update) update.message.sticker = 'test' - assert Filters.sticker(update) + assert filters.STICKER.check_update(update) def test_filters_video(self, update): - assert not Filters.video(update) + assert not filters.VIDEO.check_update(update) update.message.video = 'test' - assert Filters.video(update) + assert filters.VIDEO.check_update(update) def test_filters_voice(self, update): - assert not Filters.voice(update) + assert not filters.VOICE.check_update(update) update.message.voice = 'test' - assert Filters.voice(update) + assert filters.VOICE.check_update(update) def test_filters_video_note(self, update): - assert not Filters.video_note(update) + assert not filters.VIDEO_NOTE.check_update(update) update.message.video_note = 'test' - assert Filters.video_note(update) + assert filters.VIDEO_NOTE.check_update(update) def test_filters_contact(self, update): - assert not Filters.contact(update) + assert not filters.CONTACT.check_update(update) update.message.contact = 'test' - assert Filters.contact(update) + assert filters.CONTACT.check_update(update) def test_filters_location(self, update): - assert not Filters.location(update) + assert not filters.LOCATION.check_update(update) update.message.location = 'test' - assert Filters.location(update) + assert filters.LOCATION.check_update(update) def test_filters_venue(self, update): - assert not Filters.venue(update) + assert not filters.VENUE.check_update(update) update.message.venue = 'test' - assert Filters.venue(update) + assert filters.VENUE.check_update(update) def test_filters_status_update(self, update): - assert not Filters.status_update(update) + assert not filters.StatusUpdate.ALL.check_update(update) update.message.new_chat_members = ['test'] - assert Filters.status_update(update) - assert Filters.status_update.new_chat_members(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.NEW_CHAT_MEMBERS.check_update(update) update.message.new_chat_members = None update.message.left_chat_member = 'test' - assert Filters.status_update(update) - assert Filters.status_update.left_chat_member(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.LEFT_CHAT_MEMBER.check_update(update) update.message.left_chat_member = None update.message.new_chat_title = 'test' - assert Filters.status_update(update) - assert Filters.status_update.new_chat_title(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.NEW_CHAT_TITLE.check_update(update) update.message.new_chat_title = '' update.message.new_chat_photo = 'test' - assert Filters.status_update(update) - assert Filters.status_update.new_chat_photo(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.NEW_CHAT_PHOTO.check_update(update) update.message.new_chat_photo = None update.message.delete_chat_photo = True - assert Filters.status_update(update) - assert Filters.status_update.delete_chat_photo(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.DELETE_CHAT_PHOTO.check_update(update) update.message.delete_chat_photo = False update.message.group_chat_created = True - assert Filters.status_update(update) - assert Filters.status_update.chat_created(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.CHAT_CREATED.check_update(update) update.message.group_chat_created = False update.message.supergroup_chat_created = True - assert Filters.status_update(update) - assert Filters.status_update.chat_created(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.CHAT_CREATED.check_update(update) update.message.supergroup_chat_created = False update.message.channel_chat_created = True - assert Filters.status_update(update) - assert Filters.status_update.chat_created(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.CHAT_CREATED.check_update(update) update.message.channel_chat_created = False update.message.message_auto_delete_timer_changed = True - assert Filters.status_update(update) - assert Filters.status_update.message_auto_delete_timer_changed(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.MESSAGE_AUTO_DELETE_TIMER_CHANGED.check_update(update) update.message.message_auto_delete_timer_changed = False update.message.migrate_to_chat_id = 100 - assert Filters.status_update(update) - assert Filters.status_update.migrate(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.MIGRATE.check_update(update) update.message.migrate_to_chat_id = 0 update.message.migrate_from_chat_id = 100 - assert Filters.status_update(update) - assert Filters.status_update.migrate(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.MIGRATE.check_update(update) update.message.migrate_from_chat_id = 0 update.message.pinned_message = 'test' - assert Filters.status_update(update) - assert Filters.status_update.pinned_message(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.PINNED_MESSAGE.check_update(update) update.message.pinned_message = None - update.message.connected_website = 'http://example.com/' - assert Filters.status_update(update) - assert Filters.status_update.connected_website(update) + update.message.connected_website = 'https://example.com/' + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.CONNECTED_WEBSITE.check_update(update) update.message.connected_website = None update.message.proximity_alert_triggered = 'alert' - assert Filters.status_update(update) - assert Filters.status_update.proximity_alert_triggered(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) update.message.proximity_alert_triggered = None update.message.voice_chat_scheduled = 'scheduled' - assert Filters.status_update(update) - assert Filters.status_update.voice_chat_scheduled(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.VOICE_CHAT_SCHEDULED.check_update(update) update.message.voice_chat_scheduled = None update.message.voice_chat_started = 'hello' - assert Filters.status_update(update) - assert Filters.status_update.voice_chat_started(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.VOICE_CHAT_STARTED.check_update(update) update.message.voice_chat_started = None update.message.voice_chat_ended = 'bye' - assert Filters.status_update(update) - assert Filters.status_update.voice_chat_ended(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.VOICE_CHAT_ENDED.check_update(update) update.message.voice_chat_ended = None update.message.voice_chat_participants_invited = 'invited' - assert Filters.status_update(update) - assert Filters.status_update.voice_chat_participants_invited(update) + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.VOICE_CHAT_PARTICIPANTS_INVITED.check_update(update) update.message.voice_chat_participants_invited = None def test_filters_forwarded(self, update): - assert not Filters.forwarded(update) + assert not filters.FORWARDED.check_update(update) update.message.forward_date = datetime.datetime.utcnow() - assert Filters.forwarded(update) + assert filters.FORWARDED.check_update(update) def test_filters_game(self, update): - assert not Filters.game(update) + assert not filters.GAME.check_update(update) update.message.game = 'test' - assert Filters.game(update) + assert filters.GAME.check_update(update) def test_entities_filter(self, update, message_entity): update.message.entities = [message_entity] - assert Filters.entity(message_entity.type)(update) + assert filters.Entity(message_entity.type).check_update(update) update.message.entities = [] - assert not Filters.entity(MessageEntity.MENTION)(update) + assert not filters.Entity(MessageEntity.MENTION).check_update(update) second = message_entity.to_dict() second['type'] = 'bold' second = MessageEntity.de_json(second, None) update.message.entities = [message_entity, second] - assert Filters.entity(message_entity.type)(update) - assert not Filters.caption_entity(message_entity.type)(update) + assert filters.Entity(message_entity.type).check_update(update) + assert not filters.CaptionEntity(message_entity.type).check_update(update) def test_caption_entities_filter(self, update, message_entity): update.message.caption_entities = [message_entity] - assert Filters.caption_entity(message_entity.type)(update) + assert filters.CaptionEntity(message_entity.type).check_update(update) update.message.caption_entities = [] - assert not Filters.caption_entity(MessageEntity.MENTION)(update) + assert not filters.CaptionEntity(MessageEntity.MENTION).check_update(update) second = message_entity.to_dict() second['type'] = 'bold' second = MessageEntity.de_json(second, None) update.message.caption_entities = [message_entity, second] - assert Filters.caption_entity(message_entity.type)(update) - assert not Filters.entity(message_entity.type)(update) + assert filters.CaptionEntity(message_entity.type).check_update(update) + assert not filters.Entity(message_entity.type).check_update(update) @pytest.mark.parametrize( - ('chat_type, results'), + 'chat_type, results', [ - (None, (False, False, False, False, False, False)), - (Chat.PRIVATE, (True, True, False, False, False, False)), - (Chat.GROUP, (True, False, True, False, True, False)), - (Chat.SUPERGROUP, (True, False, False, True, True, False)), - (Chat.CHANNEL, (True, False, False, False, False, True)), + (Chat.PRIVATE, (True, False, False, False, False)), + (Chat.GROUP, (False, True, False, True, False)), + (Chat.SUPERGROUP, (False, False, True, True, False)), + (Chat.CHANNEL, (False, False, False, False, True)), ], ) def test_filters_chat_types(self, update, chat_type, results): update.message.chat.type = chat_type - assert Filters.chat_type(update) is results[0] - assert Filters.chat_type.private(update) is results[1] - assert Filters.chat_type.group(update) is results[2] - assert Filters.chat_type.supergroup(update) is results[3] - assert Filters.chat_type.groups(update) is results[4] - assert Filters.chat_type.channel(update) is results[5] + assert filters.ChatType.PRIVATE.check_update(update) is results[0] + assert filters.ChatType.GROUP.check_update(update) is results[1] + assert filters.ChatType.SUPERGROUP.check_update(update) is results[2] + assert filters.ChatType.GROUPS.check_update(update) is results[3] + assert filters.ChatType.CHANNEL.check_update(update) is results[4] def test_filters_user_init(self): with pytest.raises(RuntimeError, match='in conjunction with'): - Filters.user(user_id=1, username='user') + filters.User(user_id=1, username='user') def test_filters_user_allow_empty(self, update): - assert not Filters.user()(update) - assert Filters.user(allow_empty=True)(update) + assert not filters.User().check_update(update) + assert filters.User(allow_empty=True).check_update(update) def test_filters_user_id(self, update): - assert not Filters.user(user_id=1)(update) + assert not filters.User(user_id=1).check_update(update) update.message.from_user.id = 1 - assert Filters.user(user_id=1)(update) + assert filters.User(user_id=1).check_update(update) + assert filters.USER.check_update(update) update.message.from_user.id = 2 - assert Filters.user(user_id=[1, 2])(update) - assert not Filters.user(user_id=[3, 4])(update) + assert filters.User(user_id=[1, 2]).check_update(update) + assert not filters.User(user_id=[3, 4]).check_update(update) update.message.from_user = None - assert not Filters.user(user_id=[3, 4])(update) + assert not filters.USER.check_update(update) + assert not filters.User(user_id=[3, 4]).check_update(update) def test_filters_username(self, update): - assert not Filters.user(username='user')(update) - assert not Filters.user(username='Testuser')(update) + assert not filters.User(username='user').check_update(update) + assert not filters.User(username='Testuser').check_update(update) update.message.from_user.username = 'user@' - assert Filters.user(username='@user@')(update) - assert Filters.user(username='user@')(update) - assert Filters.user(username=['user1', 'user@', 'user2'])(update) - assert not Filters.user(username=['@username', '@user_2'])(update) + assert filters.User(username='@user@').check_update(update) + assert filters.User(username='user@').check_update(update) + assert filters.User(username=['user1', 'user@', 'user2']).check_update(update) + assert not filters.User(username=['@username', '@user_2']).check_update(update) update.message.from_user = None - assert not Filters.user(username=['@username', '@user_2'])(update) + assert not filters.User(username=['@username', '@user_2']).check_update(update) def test_filters_user_change_id(self, update): - f = Filters.user(user_id=1) + f = filters.User(user_id=1) assert f.user_ids == {1} update.message.from_user.id = 1 - assert f(update) + assert f.check_update(update) update.message.from_user.id = 2 - assert not f(update) + assert not f.check_update(update) f.user_ids = 2 assert f.user_ids == {2} - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.usernames = 'user' def test_filters_user_change_username(self, update): - f = Filters.user(username='user') + f = filters.User(username='user') update.message.from_user.username = 'user' - assert f(update) + assert f.check_update(update) update.message.from_user.username = 'User' - assert not f(update) + assert not f.check_update(update) f.usernames = 'User' - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='user_id in conjunction'): f.user_ids = 1 def test_filters_user_add_user_by_name(self, update): users = ['user_a', 'user_b', 'user_c'] - f = Filters.user() + f = filters.User() for user in users: update.message.from_user.username = user - assert not f(update) + assert not f.check_update(update) f.add_usernames('user_a') f.add_usernames(['user_b', 'user_c']) for user in users: update.message.from_user.username = user - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='user_id in conjunction'): f.add_user_ids(1) def test_filters_user_add_user_by_id(self, update): users = [1, 2, 3] - f = Filters.user() + f = filters.User() for user in users: update.message.from_user.id = user - assert not f(update) + assert not f.check_update(update) f.add_user_ids(1) f.add_user_ids([2, 3]) for user in users: update.message.from_user.username = user - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.add_usernames('user') def test_filters_user_remove_user_by_name(self, update): users = ['user_a', 'user_b', 'user_c'] - f = Filters.user(username=users) + f = filters.User(username=users) with pytest.raises(RuntimeError, match='user_id in conjunction'): f.remove_user_ids(1) for user in users: update.message.from_user.username = user - assert f(update) + assert f.check_update(update) f.remove_usernames('user_a') f.remove_usernames(['user_b', 'user_c']) for user in users: update.message.from_user.username = user - assert not f(update) + assert not f.check_update(update) def test_filters_user_remove_user_by_id(self, update): users = [1, 2, 3] - f = Filters.user(user_id=users) + f = filters.User(user_id=users) with pytest.raises(RuntimeError, match='username in conjunction'): f.remove_usernames('user') for user in users: update.message.from_user.id = user - assert f(update) + assert f.check_update(update) f.remove_user_ids(1) f.remove_user_ids([2, 3]) for user in users: update.message.from_user.username = user - assert not f(update) + assert not f.check_update(update) def test_filters_user_repr(self): - f = Filters.user([1, 2]) - assert str(f) == 'Filters.user(1, 2)' + f = filters.User([1, 2]) + assert str(f) == 'filters.User(1, 2)' f.remove_user_ids(1) f.remove_user_ids(2) - assert str(f) == 'Filters.user()' + assert str(f) == 'filters.User()' f.add_usernames('@foobar') - assert str(f) == 'Filters.user(foobar)' + assert str(f) == 'filters.User(foobar)' f.add_usernames('@barfoo') - assert str(f).startswith('Filters.user(') + assert str(f).startswith('filters.User(') # we don't know th exact order assert 'barfoo' in str(f) and 'foobar' in str(f) @@ -1133,141 +1147,144 @@ class TestFilters: def test_filters_chat_init(self): with pytest.raises(RuntimeError, match='in conjunction with'): - Filters.chat(chat_id=1, username='chat') + filters.Chat(chat_id=1, username='chat') def test_filters_chat_allow_empty(self, update): - assert not Filters.chat()(update) - assert Filters.chat(allow_empty=True)(update) + assert not filters.Chat().check_update(update) + assert filters.Chat(allow_empty=True).check_update(update) def test_filters_chat_id(self, update): - assert not Filters.chat(chat_id=1)(update) + assert not filters.Chat(chat_id=1).check_update(update) + assert filters.CHAT.check_update(update) update.message.chat.id = 1 - assert Filters.chat(chat_id=1)(update) + assert filters.Chat(chat_id=1).check_update(update) + assert filters.CHAT.check_update(update) update.message.chat.id = 2 - assert Filters.chat(chat_id=[1, 2])(update) - assert not Filters.chat(chat_id=[3, 4])(update) + assert filters.Chat(chat_id=[1, 2]).check_update(update) + assert not filters.Chat(chat_id=[3, 4]).check_update(update) update.message.chat = None - assert not Filters.chat(chat_id=[3, 4])(update) + assert not filters.CHAT.check_update(update) + assert not filters.Chat(chat_id=[3, 4]).check_update(update) def test_filters_chat_username(self, update): - assert not Filters.chat(username='chat')(update) - assert not Filters.chat(username='Testchat')(update) + assert not filters.Chat(username='chat').check_update(update) + assert not filters.Chat(username='Testchat').check_update(update) update.message.chat.username = 'chat@' - assert Filters.chat(username='@chat@')(update) - assert Filters.chat(username='chat@')(update) - assert Filters.chat(username=['chat1', 'chat@', 'chat2'])(update) - assert not Filters.chat(username=['@username', '@chat_2'])(update) + assert filters.Chat(username='@chat@').check_update(update) + assert filters.Chat(username='chat@').check_update(update) + assert filters.Chat(username=['chat1', 'chat@', 'chat2']).check_update(update) + assert not filters.Chat(username=['@username', '@chat_2']).check_update(update) update.message.chat = None - assert not Filters.chat(username=['@username', '@chat_2'])(update) + assert not filters.Chat(username=['@username', '@chat_2']).check_update(update) def test_filters_chat_change_id(self, update): - f = Filters.chat(chat_id=1) + f = filters.Chat(chat_id=1) assert f.chat_ids == {1} update.message.chat.id = 1 - assert f(update) + assert f.check_update(update) update.message.chat.id = 2 - assert not f(update) + assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.usernames = 'chat' def test_filters_chat_change_username(self, update): - f = Filters.chat(username='chat') + f = filters.Chat(username='chat') update.message.chat.username = 'chat' - assert f(update) + assert f.check_update(update) update.message.chat.username = 'User' - assert not f(update) + assert not f.check_update(update) f.usernames = 'User' - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.chat_ids = 1 def test_filters_chat_add_chat_by_name(self, update): chats = ['chat_a', 'chat_b', 'chat_c'] - f = Filters.chat() + f = filters.Chat() for chat in chats: update.message.chat.username = chat - assert not f(update) + assert not f.check_update(update) f.add_usernames('chat_a') f.add_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.chat.username = chat - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.add_chat_ids(1) def test_filters_chat_add_chat_by_id(self, update): chats = [1, 2, 3] - f = Filters.chat() + f = filters.Chat() for chat in chats: update.message.chat.id = chat - assert not f(update) + assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.chat.username = chat - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.add_usernames('chat') def test_filters_chat_remove_chat_by_name(self, update): chats = ['chat_a', 'chat_b', 'chat_c'] - f = Filters.chat(username=chats) + f = filters.Chat(username=chats) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.remove_chat_ids(1) for chat in chats: update.message.chat.username = chat - assert f(update) + assert f.check_update(update) f.remove_usernames('chat_a') f.remove_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.chat.username = chat - assert not f(update) + assert not f.check_update(update) def test_filters_chat_remove_chat_by_id(self, update): chats = [1, 2, 3] - f = Filters.chat(chat_id=chats) + f = filters.Chat(chat_id=chats) with pytest.raises(RuntimeError, match='username in conjunction'): f.remove_usernames('chat') for chat in chats: update.message.chat.id = chat - assert f(update) + assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.chat.username = chat - assert not f(update) + assert not f.check_update(update) def test_filters_chat_repr(self): - f = Filters.chat([1, 2]) - assert str(f) == 'Filters.chat(1, 2)' + f = filters.Chat([1, 2]) + assert str(f) == 'filters.Chat(1, 2)' f.remove_chat_ids(1) f.remove_chat_ids(2) - assert str(f) == 'Filters.chat()' + assert str(f) == 'filters.Chat()' f.add_usernames('@foobar') - assert str(f) == 'Filters.chat(foobar)' + assert str(f) == 'filters.Chat(foobar)' f.add_usernames('@barfoo') - assert str(f).startswith('Filters.chat(') + assert str(f).startswith('filters.Chat(') # we don't know th exact order assert 'barfoo' in str(f) and 'foobar' in str(f) @@ -1276,174 +1293,175 @@ class TestFilters: def test_filters_forwarded_from_init(self): with pytest.raises(RuntimeError, match='in conjunction with'): - Filters.forwarded_from(chat_id=1, username='chat') + filters.ForwardedFrom(chat_id=1, username='chat') def test_filters_forwarded_from_allow_empty(self, update): - assert not Filters.forwarded_from()(update) - assert Filters.forwarded_from(allow_empty=True)(update) + assert not filters.ForwardedFrom().check_update(update) + assert filters.ForwardedFrom(allow_empty=True).check_update(update) def test_filters_forwarded_from_id(self, update): # Test with User id- - assert not Filters.forwarded_from(chat_id=1)(update) + assert not filters.ForwardedFrom(chat_id=1).check_update(update) update.message.forward_from.id = 1 - assert Filters.forwarded_from(chat_id=1)(update) + assert filters.ForwardedFrom(chat_id=1).check_update(update) update.message.forward_from.id = 2 - assert Filters.forwarded_from(chat_id=[1, 2])(update) - assert not Filters.forwarded_from(chat_id=[3, 4])(update) + assert filters.ForwardedFrom(chat_id=[1, 2]).check_update(update) + assert not filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) update.message.forward_from = None - assert not Filters.forwarded_from(chat_id=[3, 4])(update) + assert not filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) # Test with Chat id- update.message.forward_from_chat.id = 4 - assert Filters.forwarded_from(chat_id=[4])(update) - assert Filters.forwarded_from(chat_id=[3, 4])(update) + assert filters.ForwardedFrom(chat_id=[4]).check_update(update) + assert filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) update.message.forward_from_chat.id = 2 - assert not Filters.forwarded_from(chat_id=[3, 4])(update) - assert Filters.forwarded_from(chat_id=2)(update) + assert not filters.ForwardedFrom(chat_id=[3, 4]).check_update(update) + assert filters.ForwardedFrom(chat_id=2).check_update(update) + update.message.forward_from_chat = None def test_filters_forwarded_from_username(self, update): # For User username - assert not Filters.forwarded_from(username='chat')(update) - assert not Filters.forwarded_from(username='Testchat')(update) + assert not filters.ForwardedFrom(username='chat').check_update(update) + assert not filters.ForwardedFrom(username='Testchat').check_update(update) update.message.forward_from.username = 'chat@' - assert Filters.forwarded_from(username='@chat@')(update) - assert Filters.forwarded_from(username='chat@')(update) - assert Filters.forwarded_from(username=['chat1', 'chat@', 'chat2'])(update) - assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) + assert filters.ForwardedFrom(username='@chat@').check_update(update) + assert filters.ForwardedFrom(username='chat@').check_update(update) + assert filters.ForwardedFrom(username=['chat1', 'chat@', 'chat2']).check_update(update) + assert not filters.ForwardedFrom(username=['@username', '@chat_2']).check_update(update) update.message.forward_from = None - assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) + assert not filters.ForwardedFrom(username=['@username', '@chat_2']).check_update(update) # For Chat username - assert not Filters.forwarded_from(username='chat')(update) - assert not Filters.forwarded_from(username='Testchat')(update) + assert not filters.ForwardedFrom(username='chat').check_update(update) + assert not filters.ForwardedFrom(username='Testchat').check_update(update) update.message.forward_from_chat.username = 'chat@' - assert Filters.forwarded_from(username='@chat@')(update) - assert Filters.forwarded_from(username='chat@')(update) - assert Filters.forwarded_from(username=['chat1', 'chat@', 'chat2'])(update) - assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) + assert filters.ForwardedFrom(username='@chat@').check_update(update) + assert filters.ForwardedFrom(username='chat@').check_update(update) + assert filters.ForwardedFrom(username=['chat1', 'chat@', 'chat2']).check_update(update) + assert not filters.ForwardedFrom(username=['@username', '@chat_2']).check_update(update) update.message.forward_from_chat = None - assert not Filters.forwarded_from(username=['@username', '@chat_2'])(update) + assert not filters.ForwardedFrom(username=['@username', '@chat_2']).check_update(update) def test_filters_forwarded_from_change_id(self, update): - f = Filters.forwarded_from(chat_id=1) + f = filters.ForwardedFrom(chat_id=1) # For User ids- assert f.chat_ids == {1} update.message.forward_from.id = 1 - assert f(update) + assert f.check_update(update) update.message.forward_from.id = 2 - assert not f(update) + assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} - assert f(update) + assert f.check_update(update) # For Chat ids- - f = Filters.forwarded_from(chat_id=1) # reset this + f = filters.ForwardedFrom(chat_id=1) # reset this update.message.forward_from = None # and change this to None, only one of them can be True assert f.chat_ids == {1} update.message.forward_from_chat.id = 1 - assert f(update) + assert f.check_update(update) update.message.forward_from_chat.id = 2 - assert not f(update) + assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.usernames = 'chat' def test_filters_forwarded_from_change_username(self, update): # For User usernames - f = Filters.forwarded_from(username='chat') + f = filters.ForwardedFrom(username='chat') update.message.forward_from.username = 'chat' - assert f(update) + assert f.check_update(update) update.message.forward_from.username = 'User' - assert not f(update) + assert not f.check_update(update) f.usernames = 'User' - assert f(update) + assert f.check_update(update) # For Chat usernames update.message.forward_from = None - f = Filters.forwarded_from(username='chat') + f = filters.ForwardedFrom(username='chat') update.message.forward_from_chat.username = 'chat' - assert f(update) + assert f.check_update(update) update.message.forward_from_chat.username = 'User' - assert not f(update) + assert not f.check_update(update) f.usernames = 'User' - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.chat_ids = 1 def test_filters_forwarded_from_add_chat_by_name(self, update): chats = ['chat_a', 'chat_b', 'chat_c'] - f = Filters.forwarded_from() + f = filters.ForwardedFrom() # For User usernames for chat in chats: update.message.forward_from.username = chat - assert not f(update) + assert not f.check_update(update) f.add_usernames('chat_a') f.add_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from.username = chat - assert f(update) + assert f.check_update(update) # For Chat usernames update.message.forward_from = None - f = Filters.forwarded_from() + f = filters.ForwardedFrom() for chat in chats: update.message.forward_from_chat.username = chat - assert not f(update) + assert not f.check_update(update) f.add_usernames('chat_a') f.add_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from_chat.username = chat - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.add_chat_ids(1) def test_filters_forwarded_from_add_chat_by_id(self, update): chats = [1, 2, 3] - f = Filters.forwarded_from() + f = filters.ForwardedFrom() # For User ids for chat in chats: update.message.forward_from.id = chat - assert not f(update) + assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.forward_from.username = chat - assert f(update) + assert f.check_update(update) # For Chat ids- update.message.forward_from = None - f = Filters.forwarded_from() + f = filters.ForwardedFrom() for chat in chats: update.message.forward_from_chat.id = chat - assert not f(update) + assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.forward_from_chat.username = chat - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.add_usernames('chat') def test_filters_forwarded_from_remove_chat_by_name(self, update): chats = ['chat_a', 'chat_b', 'chat_c'] - f = Filters.forwarded_from(username=chats) + f = filters.ForwardedFrom(username=chats) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.remove_chat_ids(1) @@ -1451,32 +1469,32 @@ class TestFilters: # For User usernames for chat in chats: update.message.forward_from.username = chat - assert f(update) + assert f.check_update(update) f.remove_usernames('chat_a') f.remove_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from.username = chat - assert not f(update) + assert not f.check_update(update) # For Chat usernames update.message.forward_from = None - f = Filters.forwarded_from(username=chats) + f = filters.ForwardedFrom(username=chats) for chat in chats: update.message.forward_from_chat.username = chat - assert f(update) + assert f.check_update(update) f.remove_usernames('chat_a') f.remove_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.forward_from_chat.username = chat - assert not f(update) + assert not f.check_update(update) def test_filters_forwarded_from_remove_chat_by_id(self, update): chats = [1, 2, 3] - f = Filters.forwarded_from(chat_id=chats) + f = filters.ForwardedFrom(chat_id=chats) with pytest.raises(RuntimeError, match='username in conjunction'): f.remove_usernames('chat') @@ -1484,39 +1502,39 @@ class TestFilters: # For User ids for chat in chats: update.message.forward_from.id = chat - assert f(update) + assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.forward_from.username = chat - assert not f(update) + assert not f.check_update(update) # For Chat ids update.message.forward_from = None - f = Filters.forwarded_from(chat_id=chats) + f = filters.ForwardedFrom(chat_id=chats) for chat in chats: update.message.forward_from_chat.id = chat - assert f(update) + assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.forward_from_chat.username = chat - assert not f(update) + assert not f.check_update(update) def test_filters_forwarded_from_repr(self): - f = Filters.forwarded_from([1, 2]) - assert str(f) == 'Filters.forwarded_from(1, 2)' + f = filters.ForwardedFrom([1, 2]) + assert str(f) == 'filters.ForwardedFrom(1, 2)' f.remove_chat_ids(1) f.remove_chat_ids(2) - assert str(f) == 'Filters.forwarded_from()' + assert str(f) == 'filters.ForwardedFrom()' f.add_usernames('@foobar') - assert str(f) == 'Filters.forwarded_from(foobar)' + assert str(f) == 'filters.ForwardedFrom(foobar)' f.add_usernames('@barfoo') - assert str(f).startswith('Filters.forwarded_from(') + assert str(f).startswith('filters.ForwardedFrom(') # we don't know the exact order assert 'barfoo' in str(f) and 'foobar' in str(f) @@ -1525,141 +1543,145 @@ class TestFilters: def test_filters_sender_chat_init(self): with pytest.raises(RuntimeError, match='in conjunction with'): - Filters.sender_chat(chat_id=1, username='chat') + filters.SenderChat(chat_id=1, username='chat') def test_filters_sender_chat_allow_empty(self, update): - assert not Filters.sender_chat()(update) - assert Filters.sender_chat(allow_empty=True)(update) + assert not filters.SenderChat().check_update(update) + assert filters.SenderChat(allow_empty=True).check_update(update) def test_filters_sender_chat_id(self, update): - assert not Filters.sender_chat(chat_id=1)(update) + assert not filters.SenderChat(chat_id=1).check_update(update) update.message.sender_chat.id = 1 - assert Filters.sender_chat(chat_id=1)(update) + assert filters.SenderChat(chat_id=1).check_update(update) update.message.sender_chat.id = 2 - assert Filters.sender_chat(chat_id=[1, 2])(update) - assert not Filters.sender_chat(chat_id=[3, 4])(update) + assert filters.SenderChat(chat_id=[1, 2]).check_update(update) + assert not filters.SenderChat(chat_id=[3, 4]).check_update(update) + assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat = None - assert not Filters.sender_chat(chat_id=[3, 4])(update) + assert not filters.SenderChat(chat_id=[3, 4]).check_update(update) + assert not filters.SenderChat.ALL.check_update(update) def test_filters_sender_chat_username(self, update): - assert not Filters.sender_chat(username='chat')(update) - assert not Filters.sender_chat(username='Testchat')(update) + assert not filters.SenderChat(username='chat').check_update(update) + assert not filters.SenderChat(username='Testchat').check_update(update) update.message.sender_chat.username = 'chat@' - assert Filters.sender_chat(username='@chat@')(update) - assert Filters.sender_chat(username='chat@')(update) - assert Filters.sender_chat(username=['chat1', 'chat@', 'chat2'])(update) - assert not Filters.sender_chat(username=['@username', '@chat_2'])(update) + assert filters.SenderChat(username='@chat@').check_update(update) + assert filters.SenderChat(username='chat@').check_update(update) + assert filters.SenderChat(username=['chat1', 'chat@', 'chat2']).check_update(update) + assert not filters.SenderChat(username=['@username', '@chat_2']).check_update(update) + assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat = None - assert not Filters.sender_chat(username=['@username', '@chat_2'])(update) + assert not filters.SenderChat(username=['@username', '@chat_2']).check_update(update) + assert not filters.SenderChat.ALL.check_update(update) def test_filters_sender_chat_change_id(self, update): - f = Filters.sender_chat(chat_id=1) + f = filters.SenderChat(chat_id=1) assert f.chat_ids == {1} update.message.sender_chat.id = 1 - assert f(update) + assert f.check_update(update) update.message.sender_chat.id = 2 - assert not f(update) + assert not f.check_update(update) f.chat_ids = 2 assert f.chat_ids == {2} - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.usernames = 'chat' def test_filters_sender_chat_change_username(self, update): - f = Filters.sender_chat(username='chat') + f = filters.SenderChat(username='chat') update.message.sender_chat.username = 'chat' - assert f(update) + assert f.check_update(update) update.message.sender_chat.username = 'User' - assert not f(update) + assert not f.check_update(update) f.usernames = 'User' - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.chat_ids = 1 def test_filters_sender_chat_add_sender_chat_by_name(self, update): chats = ['chat_a', 'chat_b', 'chat_c'] - f = Filters.sender_chat() + f = filters.SenderChat() for chat in chats: update.message.sender_chat.username = chat - assert not f(update) + assert not f.check_update(update) f.add_usernames('chat_a') f.add_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.sender_chat.username = chat - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.add_chat_ids(1) def test_filters_sender_chat_add_sender_chat_by_id(self, update): chats = [1, 2, 3] - f = Filters.sender_chat() + f = filters.SenderChat() for chat in chats: update.message.sender_chat.id = chat - assert not f(update) + assert not f.check_update(update) f.add_chat_ids(1) f.add_chat_ids([2, 3]) for chat in chats: update.message.sender_chat.username = chat - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.add_usernames('chat') def test_filters_sender_chat_remove_sender_chat_by_name(self, update): chats = ['chat_a', 'chat_b', 'chat_c'] - f = Filters.sender_chat(username=chats) + f = filters.SenderChat(username=chats) with pytest.raises(RuntimeError, match='chat_id in conjunction'): f.remove_chat_ids(1) for chat in chats: update.message.sender_chat.username = chat - assert f(update) + assert f.check_update(update) f.remove_usernames('chat_a') f.remove_usernames(['chat_b', 'chat_c']) for chat in chats: update.message.sender_chat.username = chat - assert not f(update) + assert not f.check_update(update) def test_filters_sender_chat_remove_sender_chat_by_id(self, update): chats = [1, 2, 3] - f = Filters.sender_chat(chat_id=chats) + f = filters.SenderChat(chat_id=chats) with pytest.raises(RuntimeError, match='username in conjunction'): f.remove_usernames('chat') for chat in chats: update.message.sender_chat.id = chat - assert f(update) + assert f.check_update(update) f.remove_chat_ids(1) f.remove_chat_ids([2, 3]) for chat in chats: update.message.sender_chat.username = chat - assert not f(update) + assert not f.check_update(update) def test_filters_sender_chat_repr(self): - f = Filters.sender_chat([1, 2]) - assert str(f) == 'Filters.sender_chat(1, 2)' + f = filters.SenderChat([1, 2]) + assert str(f) == 'filters.SenderChat(1, 2)' f.remove_chat_ids(1) f.remove_chat_ids(2) - assert str(f) == 'Filters.sender_chat()' + assert str(f) == 'filters.SenderChat()' f.add_usernames('@foobar') - assert str(f) == 'Filters.sender_chat(foobar)' + assert str(f) == 'filters.SenderChat(foobar)' f.add_usernames('@barfoo') - assert str(f).startswith('Filters.sender_chat(') + assert str(f).startswith('filters.SenderChat(') # we don't know th exact order assert 'barfoo' in str(f) and 'foobar' in str(f) @@ -1668,324 +1690,340 @@ class TestFilters: def test_filters_sender_chat_super_group(self, update): update.message.sender_chat.type = Chat.PRIVATE - assert not Filters.sender_chat.super_group(update) + assert not filters.SenderChat.SUPER_GROUP.check_update(update) + assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat.type = Chat.CHANNEL - assert not Filters.sender_chat.super_group(update) + assert not filters.SenderChat.SUPER_GROUP.check_update(update) update.message.sender_chat.type = Chat.SUPERGROUP - assert Filters.sender_chat.super_group(update) + assert filters.SenderChat.SUPER_GROUP.check_update(update) + assert filters.SenderChat.ALL.check_update(update) update.message.sender_chat = None - assert not Filters.sender_chat.super_group(update) + assert not filters.SenderChat.SUPER_GROUP.check_update(update) + assert not filters.SenderChat.ALL.check_update(update) def test_filters_sender_chat_channel(self, update): update.message.sender_chat.type = Chat.PRIVATE - assert not Filters.sender_chat.channel(update) + assert not filters.SenderChat.CHANNEL.check_update(update) update.message.sender_chat.type = Chat.SUPERGROUP - assert not Filters.sender_chat.channel(update) + assert not filters.SenderChat.CHANNEL.check_update(update) update.message.sender_chat.type = Chat.CHANNEL - assert Filters.sender_chat.channel(update) + assert filters.SenderChat.CHANNEL.check_update(update) update.message.sender_chat = None - assert not Filters.sender_chat.channel(update) + assert not filters.SenderChat.CHANNEL.check_update(update) def test_filters_is_automatic_forward(self, update): - assert not Filters.is_automatic_forward(update) + assert not filters.IS_AUTOMATIC_FORWARD.check_update(update) update.message.is_automatic_forward = True - assert Filters.is_automatic_forward(update) + assert filters.IS_AUTOMATIC_FORWARD.check_update(update) def test_filters_has_protected_content(self, update): - assert not Filters.has_protected_content(update) + assert not filters.HAS_PROTECTED_CONTENT.check_update(update) update.message.has_protected_content = True - assert Filters.has_protected_content(update) + assert filters.HAS_PROTECTED_CONTENT.check_update(update) def test_filters_invoice(self, update): - assert not Filters.invoice(update) + assert not filters.INVOICE.check_update(update) update.message.invoice = 'test' - assert Filters.invoice(update) + assert filters.INVOICE.check_update(update) def test_filters_successful_payment(self, update): - assert not Filters.successful_payment(update) + assert not filters.SUCCESSFUL_PAYMENT.check_update(update) update.message.successful_payment = 'test' - assert Filters.successful_payment(update) + assert filters.SUCCESSFUL_PAYMENT.check_update(update) def test_filters_passport_data(self, update): - assert not Filters.passport_data(update) + assert not filters.PASSPORT_DATA.check_update(update) update.message.passport_data = 'test' - assert Filters.passport_data(update) + assert filters.PASSPORT_DATA.check_update(update) def test_filters_poll(self, update): - assert not Filters.poll(update) + assert not filters.POLL.check_update(update) update.message.poll = 'test' - assert Filters.poll(update) + assert filters.POLL.check_update(update) @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) def test_filters_dice(self, update, emoji): update.message.dice = Dice(4, emoji) - assert Filters.dice(update) + assert filters.Dice.ALL.check_update(update) and filters.Dice().check_update(update) + + to_camel = emoji.name.title().replace('_', '') + assert repr(filters.Dice.ALL) == "filters.Dice.ALL" + assert repr(getattr(filters.Dice, to_camel)(4)) == f"filters.Dice.{to_camel}([4])" + update.message.dice = None - assert not Filters.dice(update) + assert not filters.Dice.ALL.check_update(update) @pytest.mark.parametrize('emoji', Dice.ALL_EMOJI) def test_filters_dice_list(self, update, emoji): update.message.dice = None - assert not Filters.dice(5)(update) + assert not filters.Dice(5).check_update(update) update.message.dice = Dice(5, emoji) - assert Filters.dice(5)(update) - assert Filters.dice({5, 6})(update) - assert not Filters.dice(1)(update) - assert not Filters.dice([2, 3])(update) + assert filters.Dice(5).check_update(update) + assert repr(filters.Dice(5)) == "filters.Dice([5])" + assert filters.Dice({5, 6}).check_update(update) + assert not filters.Dice(1).check_update(update) + assert not filters.Dice([2, 3]).check_update(update) def test_filters_dice_type(self, update): update.message.dice = Dice(5, '🎲') - assert Filters.dice.dice(update) - assert Filters.dice.dice([4, 5])(update) - assert not Filters.dice.darts(update) - assert not Filters.dice.basketball(update) - assert not Filters.dice.dice([6])(update) + assert filters.Dice.DICE.check_update(update) + assert repr(filters.Dice.DICE) == "filters.Dice.DICE" + assert filters.Dice.Dice([4, 5]).check_update(update) + assert not filters.Dice.Darts(5).check_update(update) + assert not filters.Dice.BASKETBALL.check_update(update) + assert not filters.Dice.Dice([6]).check_update(update) update.message.dice = Dice(5, '🎯') - assert Filters.dice.darts(update) - assert Filters.dice.darts([4, 5])(update) - assert not Filters.dice.dice(update) - assert not Filters.dice.basketball(update) - assert not Filters.dice.darts([6])(update) + assert filters.Dice.DARTS.check_update(update) + assert filters.Dice.Darts([4, 5]).check_update(update) + assert not filters.Dice.Dice(5).check_update(update) + assert not filters.Dice.BASKETBALL.check_update(update) + assert not filters.Dice.Darts([6]).check_update(update) update.message.dice = Dice(5, '🏀') - assert Filters.dice.basketball(update) - assert Filters.dice.basketball([4, 5])(update) - assert not Filters.dice.dice(update) - assert not Filters.dice.darts(update) - assert not Filters.dice.basketball([4])(update) + assert filters.Dice.BASKETBALL.check_update(update) + assert filters.Dice.Basketball([4, 5]).check_update(update) + assert not filters.Dice.Dice(5).check_update(update) + assert not filters.Dice.DARTS.check_update(update) + assert not filters.Dice.Basketball([4]).check_update(update) update.message.dice = Dice(5, '⚽') - assert Filters.dice.football(update) - assert Filters.dice.football([4, 5])(update) - assert not Filters.dice.dice(update) - assert not Filters.dice.darts(update) - assert not Filters.dice.football([4])(update) + assert filters.Dice.FOOTBALL.check_update(update) + assert filters.Dice.Football([4, 5]).check_update(update) + assert not filters.Dice.Dice(5).check_update(update) + assert not filters.Dice.DARTS.check_update(update) + assert not filters.Dice.Football([4]).check_update(update) update.message.dice = Dice(5, '🎰') - assert Filters.dice.slot_machine(update) - assert Filters.dice.slot_machine([4, 5])(update) - assert not Filters.dice.dice(update) - assert not Filters.dice.darts(update) - assert not Filters.dice.slot_machine([4])(update) + assert filters.Dice.SLOT_MACHINE.check_update(update) + assert filters.Dice.SlotMachine([4, 5]).check_update(update) + assert not filters.Dice.Dice(5).check_update(update) + assert not filters.Dice.DARTS.check_update(update) + assert not filters.Dice.SlotMachine([4]).check_update(update) update.message.dice = Dice(5, '🎳') - assert Filters.dice.bowling(update) - assert Filters.dice.bowling([4, 5])(update) - assert not Filters.dice.dice(update) - assert not Filters.dice.darts(update) - assert not Filters.dice.bowling([4])(update) + assert filters.Dice.BOWLING.check_update(update) + assert filters.Dice.Bowling([4, 5]).check_update(update) + assert not filters.Dice.Dice(5).check_update(update) + assert not filters.Dice.DARTS.check_update(update) + assert not filters.Dice.Bowling([4]).check_update(update) def test_language_filter_single(self, update): update.message.from_user.language_code = 'en_US' - assert (Filters.language('en_US'))(update) - assert (Filters.language('en'))(update) - assert not (Filters.language('en_GB'))(update) - assert not (Filters.language('da'))(update) + assert filters.Language('en_US').check_update(update) + assert filters.Language('en').check_update(update) + assert not filters.Language('en_GB').check_update(update) + assert not filters.Language('da').check_update(update) update.message.from_user.language_code = 'da' - assert not (Filters.language('en_US'))(update) - assert not (Filters.language('en'))(update) - assert not (Filters.language('en_GB'))(update) - assert (Filters.language('da'))(update) + assert not filters.Language('en_US').check_update(update) + assert not filters.Language('en').check_update(update) + assert not filters.Language('en_GB').check_update(update) + assert filters.Language('da').check_update(update) def test_language_filter_multiple(self, update): - f = Filters.language(['en_US', 'da']) + f = filters.Language(['en_US', 'da']) update.message.from_user.language_code = 'en_US' - assert f(update) + assert f.check_update(update) update.message.from_user.language_code = 'en_GB' - assert not f(update) + assert not f.check_update(update) update.message.from_user.language_code = 'da' - assert f(update) + assert f.check_update(update) def test_and_filters(self, update): update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() - assert (Filters.text & Filters.forwarded)(update) + assert (filters.TEXT & filters.FORWARDED).check_update(update) update.message.text = '/test' - assert (Filters.text & Filters.forwarded)(update) + assert (filters.TEXT & filters.FORWARDED).check_update(update) update.message.text = 'test' update.message.forward_date = None - assert not (Filters.text & Filters.forwarded)(update) + assert not (filters.TEXT & filters.FORWARDED).check_update(update) update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() - assert (Filters.text & Filters.forwarded & Filters.chat_type.private)(update) + assert (filters.TEXT & filters.FORWARDED & filters.ChatType.PRIVATE).check_update(update) def test_or_filters(self, update): update.message.text = 'test' - assert (Filters.text | Filters.status_update)(update) + assert (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) update.message.group_chat_created = True - assert (Filters.text | Filters.status_update)(update) + assert (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) update.message.text = None - assert (Filters.text | Filters.status_update)(update) + assert (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) update.message.group_chat_created = False - assert not (Filters.text | Filters.status_update)(update) + assert not (filters.TEXT | filters.StatusUpdate.ALL).check_update(update) def test_and_or_filters(self, update): update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() - assert (Filters.text & (Filters.status_update | Filters.forwarded))(update) + assert (filters.TEXT & (filters.StatusUpdate.ALL | filters.FORWARDED)).check_update(update) update.message.forward_date = None - assert not (Filters.text & (Filters.forwarded | Filters.status_update))(update) + assert not (filters.TEXT & (filters.FORWARDED | filters.StatusUpdate.ALL)).check_update( + update + ) update.message.pinned_message = True - assert Filters.text & (Filters.forwarded | Filters.status_update)(update) + assert filters.TEXT & (filters.FORWARDED | filters.StatusUpdate.ALL).check_update(update) assert ( - str(Filters.text & (Filters.forwarded | Filters.entity(MessageEntity.MENTION))) - == '>' + str(filters.TEXT & (filters.FORWARDED | filters.Entity(MessageEntity.MENTION))) + == '>' ) def test_xor_filters(self, update): update.message.text = 'test' update.effective_user.id = 123 - assert not (Filters.text ^ Filters.user(123))(update) + assert not (filters.TEXT ^ filters.User(123)).check_update(update) update.message.text = None update.effective_user.id = 1234 - assert not (Filters.text ^ Filters.user(123))(update) + assert not (filters.TEXT ^ filters.User(123)).check_update(update) update.message.text = 'test' - assert (Filters.text ^ Filters.user(123))(update) + assert (filters.TEXT ^ filters.User(123)).check_update(update) update.message.text = None update.effective_user.id = 123 - assert (Filters.text ^ Filters.user(123))(update) + assert (filters.TEXT ^ filters.User(123)).check_update(update) def test_xor_filters_repr(self, update): - assert str(Filters.text ^ Filters.user(123)) == '' + assert str(filters.TEXT ^ filters.User(123)) == '' with pytest.raises(RuntimeError, match='Cannot set name'): - (Filters.text ^ Filters.user(123)).name = 'foo' + (filters.TEXT ^ filters.User(123)).name = 'foo' def test_and_xor_filters(self, update): update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() - assert (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) + assert (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.text = None update.effective_user.id = 123 - assert (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) + assert (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.text = 'test' - assert not (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) + assert not (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.forward_date = None update.message.text = None update.effective_user.id = 123 - assert not (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) + assert not (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) update.message.text = 'test' update.effective_user.id = 456 - assert not (Filters.forwarded & (Filters.text ^ Filters.user(123)))(update) + assert not (filters.FORWARDED & (filters.TEXT ^ filters.User(123))).check_update(update) assert ( - str(Filters.forwarded & (Filters.text ^ Filters.user(123))) - == '>' + str(filters.FORWARDED & (filters.TEXT ^ filters.User(123))) + == '>' ) def test_xor_regex_filters(self, update): - SRE_TYPE = type(re.match("", "")) + sre_type = type(re.match("", "")) update.message.text = 'test' update.message.forward_date = datetime.datetime.utcnow() - assert not (Filters.forwarded ^ Filters.regex('^test$'))(update) + assert not (filters.FORWARDED ^ filters.Regex('^test$')).check_update(update) update.message.forward_date = None - result = (Filters.forwarded ^ Filters.regex('^test$'))(update) + result = (filters.FORWARDED ^ filters.Regex('^test$')).check_update(update) assert result assert isinstance(result, dict) matches = result['matches'] assert isinstance(matches, list) - assert type(matches[0]) is SRE_TYPE + assert type(matches[0]) is sre_type update.message.forward_date = datetime.datetime.utcnow() update.message.text = None - assert (Filters.forwarded ^ Filters.regex('^test$'))(update) is True + assert (filters.FORWARDED ^ filters.Regex('^test$')).check_update(update) is True def test_inverted_filters(self, update): update.message.text = '/test' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] - assert Filters.command(update) - assert not (~Filters.command)(update) + assert filters.COMMAND.check_update(update) + assert not (~filters.COMMAND).check_update(update) update.message.text = 'test' update.message.entities = [] - assert not Filters.command(update) - assert (~Filters.command)(update) + assert not filters.COMMAND.check_update(update) + assert (~filters.COMMAND).check_update(update) def test_inverted_filters_repr(self, update): - assert str(~Filters.text) == '' + assert str(~filters.TEXT) == '' with pytest.raises(RuntimeError, match='Cannot set name'): - (~Filters.text).name = 'foo' + (~filters.TEXT).name = 'foo' def test_inverted_and_filters(self, update): update.message.text = '/test' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] update.message.forward_date = 1 - assert (Filters.forwarded & Filters.command)(update) - assert not (~Filters.forwarded & Filters.command)(update) - assert not (Filters.forwarded & ~Filters.command)(update) - assert not (~(Filters.forwarded & Filters.command))(update) + assert (filters.FORWARDED & filters.COMMAND).check_update(update) + assert not (~filters.FORWARDED & filters.COMMAND).check_update(update) + assert not (filters.FORWARDED & ~filters.COMMAND).check_update(update) + assert not (~(filters.FORWARDED & filters.COMMAND)).check_update(update) update.message.forward_date = None - assert not (Filters.forwarded & Filters.command)(update) - assert (~Filters.forwarded & Filters.command)(update) - assert not (Filters.forwarded & ~Filters.command)(update) - assert (~(Filters.forwarded & Filters.command))(update) + assert not (filters.FORWARDED & filters.COMMAND).check_update(update) + assert (~filters.FORWARDED & filters.COMMAND).check_update(update) + assert not (filters.FORWARDED & ~filters.COMMAND).check_update(update) + assert (~(filters.FORWARDED & filters.COMMAND)).check_update(update) update.message.text = 'test' update.message.entities = [] - assert not (Filters.forwarded & Filters.command)(update) - assert not (~Filters.forwarded & Filters.command)(update) - assert not (Filters.forwarded & ~Filters.command)(update) - assert (~(Filters.forwarded & Filters.command))(update) + assert not (filters.FORWARDED & filters.COMMAND).check_update(update) + assert not (~filters.FORWARDED & filters.COMMAND).check_update(update) + assert not (filters.FORWARDED & ~filters.COMMAND).check_update(update) + assert (~(filters.FORWARDED & filters.COMMAND)).check_update(update) - def test_faulty_custom_filter(self, update): - class _CustomFilter(BaseFilter): - pass + def test_indirect_message(self, update): + class _CustomFilter(filters.MessageFilter): + test_flag = False - with pytest.raises(TypeError, match='Can\'t instantiate abstract class _CustomFilter'): - _CustomFilter() + def filter(self, message: Message): + self.test_flag = True + return self.test_flag + + c = _CustomFilter() + u = Update(0, callback_query=CallbackQuery('0', update.effective_user, '', update.message)) + assert not c.check_update(u) + assert not c.test_flag + assert c.check_update(update) + assert c.test_flag def test_custom_unnamed_filter(self, update, base_class): class Unnamed(base_class): - def filter(self, mes): + def filter(self, _): return True unnamed = Unnamed() assert str(unnamed) == Unnamed.__name__ def test_update_type_message(self, update): - assert Filters.update.message(update) - assert not Filters.update.edited_message(update) - assert Filters.update.messages(update) - assert not Filters.update.channel_post(update) - assert not Filters.update.edited_channel_post(update) - assert not Filters.update.channel_posts(update) - assert not Filters.update.edited(update) - assert Filters.update(update) + assert filters.UpdateType.MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert filters.UpdateType.MESSAGES.check_update(update) + assert not filters.UpdateType.CHANNEL_POST.check_update(update) + assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert not filters.UpdateType.EDITED.check_update(update) def test_update_type_edited_message(self, update): update.edited_message, update.message = update.message, update.edited_message - assert not Filters.update.message(update) - assert Filters.update.edited_message(update) - assert Filters.update.messages(update) - assert not Filters.update.channel_post(update) - assert not Filters.update.edited_channel_post(update) - assert not Filters.update.channel_posts(update) - assert Filters.update.edited(update) - assert Filters.update(update) + assert not filters.UpdateType.MESSAGE.check_update(update) + assert filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert filters.UpdateType.MESSAGES.check_update(update) + assert not filters.UpdateType.CHANNEL_POST.check_update(update) + assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert not filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert filters.UpdateType.EDITED.check_update(update) def test_update_type_channel_post(self, update): update.channel_post, update.message = update.message, update.edited_message - assert not Filters.update.message(update) - assert not Filters.update.edited_message(update) - assert not Filters.update.messages(update) - assert Filters.update.channel_post(update) - assert not Filters.update.edited_channel_post(update) - assert Filters.update.channel_posts(update) - assert not Filters.update.edited(update) - assert Filters.update(update) + assert not filters.UpdateType.MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert not filters.UpdateType.MESSAGES.check_update(update) + assert filters.UpdateType.CHANNEL_POST.check_update(update) + assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert not filters.UpdateType.EDITED.check_update(update) def test_update_type_edited_channel_post(self, update): update.edited_channel_post, update.message = update.message, update.edited_message - assert not Filters.update.message(update) - assert not Filters.update.edited_message(update) - assert not Filters.update.messages(update) - assert not Filters.update.channel_post(update) - assert Filters.update.edited_channel_post(update) - assert Filters.update.channel_posts(update) - assert Filters.update.edited(update) - assert Filters.update(update) + assert not filters.UpdateType.MESSAGE.check_update(update) + assert not filters.UpdateType.EDITED_MESSAGE.check_update(update) + assert not filters.UpdateType.MESSAGES.check_update(update) + assert not filters.UpdateType.CHANNEL_POST.check_update(update) + assert filters.UpdateType.EDITED_CHANNEL_POST.check_update(update) + assert filters.UpdateType.CHANNEL_POSTS.check_update(update) + assert filters.UpdateType.EDITED.check_update(update) def test_merged_short_circuit_and(self, update, base_class): update.message.text = '/test' @@ -2001,15 +2039,15 @@ class TestFilters: raising_filter = RaisingFilter() with pytest.raises(TestException): - (Filters.command & raising_filter)(update) + (filters.COMMAND & raising_filter).check_update(update) update.message.text = 'test' update.message.entities = [] - (Filters.command & raising_filter)(update) + (filters.COMMAND & raising_filter).check_update(update) def test_merged_filters_repr(self, update): with pytest.raises(RuntimeError, match='Cannot set name'): - (Filters.text & Filters.photo).name = 'foo' + (filters.TEXT & filters.PHOTO).name = 'foo' def test_merged_short_circuit_or(self, update, base_class): update.message.text = 'test' @@ -2024,11 +2062,11 @@ class TestFilters: raising_filter = RaisingFilter() with pytest.raises(TestException): - (Filters.command | raising_filter)(update) + (filters.COMMAND | raising_filter).check_update(update) update.message.text = '/test' update.message.entities = [MessageEntity(MessageEntity.BOT_COMMAND, 0, 5)] - (Filters.command | raising_filter)(update) + (filters.COMMAND | raising_filter).check_update(update) def test_merged_data_merging_and(self, update, base_class): update.message.text = '/test' @@ -2043,15 +2081,15 @@ class TestFilters: def filter(self, _): return {'test': [self.data]} - result = (Filters.command & DataFilter('blah'))(update) + result = (filters.COMMAND & DataFilter('blah')).check_update(update) assert result['test'] == ['blah'] - result = (DataFilter('blah1') & DataFilter('blah2'))(update) + result = (DataFilter('blah1') & DataFilter('blah2')).check_update(update) assert result['test'] == ['blah1', 'blah2'] update.message.text = 'test' update.message.entities = [] - result = (Filters.command & DataFilter('blah'))(update) + result = (filters.COMMAND & DataFilter('blah')).check_update(update) assert not result def test_merged_data_merging_or(self, update, base_class): @@ -2066,153 +2104,153 @@ class TestFilters: def filter(self, _): return {'test': [self.data]} - result = (Filters.command | DataFilter('blah'))(update) + result = (filters.COMMAND | DataFilter('blah')).check_update(update) assert result - result = (DataFilter('blah1') | DataFilter('blah2'))(update) + result = (DataFilter('blah1') | DataFilter('blah2')).check_update(update) assert result['test'] == ['blah1'] update.message.text = 'test' - result = (Filters.command | DataFilter('blah'))(update) + result = (filters.COMMAND | DataFilter('blah')).check_update(update) assert result['test'] == ['blah'] def test_filters_via_bot_init(self): with pytest.raises(RuntimeError, match='in conjunction with'): - Filters.via_bot(bot_id=1, username='bot') + filters.ViaBot(bot_id=1, username='bot') def test_filters_via_bot_allow_empty(self, update): - assert not Filters.via_bot()(update) - assert Filters.via_bot(allow_empty=True)(update) + assert not filters.ViaBot().check_update(update) + assert filters.ViaBot(allow_empty=True).check_update(update) def test_filters_via_bot_id(self, update): - assert not Filters.via_bot(bot_id=1)(update) + assert not filters.ViaBot(bot_id=1).check_update(update) update.message.via_bot.id = 1 - assert Filters.via_bot(bot_id=1)(update) + assert filters.ViaBot(bot_id=1).check_update(update) update.message.via_bot.id = 2 - assert Filters.via_bot(bot_id=[1, 2])(update) - assert not Filters.via_bot(bot_id=[3, 4])(update) + assert filters.ViaBot(bot_id=[1, 2]).check_update(update) + assert not filters.ViaBot(bot_id=[3, 4]).check_update(update) update.message.via_bot = None - assert not Filters.via_bot(bot_id=[3, 4])(update) + assert not filters.ViaBot(bot_id=[3, 4]).check_update(update) def test_filters_via_bot_username(self, update): - assert not Filters.via_bot(username='bot')(update) - assert not Filters.via_bot(username='Testbot')(update) + assert not filters.ViaBot(username='bot').check_update(update) + assert not filters.ViaBot(username='Testbot').check_update(update) update.message.via_bot.username = 'bot@' - assert Filters.via_bot(username='@bot@')(update) - assert Filters.via_bot(username='bot@')(update) - assert Filters.via_bot(username=['bot1', 'bot@', 'bot2'])(update) - assert not Filters.via_bot(username=['@username', '@bot_2'])(update) + assert filters.ViaBot(username='@bot@').check_update(update) + assert filters.ViaBot(username='bot@').check_update(update) + assert filters.ViaBot(username=['bot1', 'bot@', 'bot2']).check_update(update) + assert not filters.ViaBot(username=['@username', '@bot_2']).check_update(update) update.message.via_bot = None - assert not Filters.user(username=['@username', '@bot_2'])(update) + assert not filters.User(username=['@username', '@bot_2']).check_update(update) def test_filters_via_bot_change_id(self, update): - f = Filters.via_bot(bot_id=3) + f = filters.ViaBot(bot_id=3) assert f.bot_ids == {3} update.message.via_bot.id = 3 - assert f(update) + assert f.check_update(update) update.message.via_bot.id = 2 - assert not f(update) + assert not f.check_update(update) f.bot_ids = 2 assert f.bot_ids == {2} - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.usernames = 'user' def test_filters_via_bot_change_username(self, update): - f = Filters.via_bot(username='bot') + f = filters.ViaBot(username='bot') update.message.via_bot.username = 'bot' - assert f(update) + assert f.check_update(update) update.message.via_bot.username = 'Bot' - assert not f(update) + assert not f.check_update(update) f.usernames = 'Bot' - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='bot_id in conjunction'): f.bot_ids = 1 def test_filters_via_bot_add_user_by_name(self, update): users = ['bot_a', 'bot_b', 'bot_c'] - f = Filters.via_bot() + f = filters.ViaBot() for user in users: update.message.via_bot.username = user - assert not f(update) + assert not f.check_update(update) f.add_usernames('bot_a') f.add_usernames(['bot_b', 'bot_c']) for user in users: update.message.via_bot.username = user - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='bot_id in conjunction'): f.add_bot_ids(1) def test_filters_via_bot_add_user_by_id(self, update): users = [1, 2, 3] - f = Filters.via_bot() + f = filters.ViaBot() for user in users: update.message.via_bot.id = user - assert not f(update) + assert not f.check_update(update) f.add_bot_ids(1) f.add_bot_ids([2, 3]) for user in users: update.message.via_bot.username = user - assert f(update) + assert f.check_update(update) with pytest.raises(RuntimeError, match='username in conjunction'): f.add_usernames('bot') def test_filters_via_bot_remove_user_by_name(self, update): users = ['bot_a', 'bot_b', 'bot_c'] - f = Filters.via_bot(username=users) + f = filters.ViaBot(username=users) with pytest.raises(RuntimeError, match='bot_id in conjunction'): f.remove_bot_ids(1) for user in users: update.message.via_bot.username = user - assert f(update) + assert f.check_update(update) f.remove_usernames('bot_a') f.remove_usernames(['bot_b', 'bot_c']) for user in users: update.message.via_bot.username = user - assert not f(update) + assert not f.check_update(update) def test_filters_via_bot_remove_user_by_id(self, update): users = [1, 2, 3] - f = Filters.via_bot(bot_id=users) + f = filters.ViaBot(bot_id=users) with pytest.raises(RuntimeError, match='username in conjunction'): f.remove_usernames('bot') for user in users: update.message.via_bot.id = user - assert f(update) + assert f.check_update(update) f.remove_bot_ids(1) f.remove_bot_ids([2, 3]) for user in users: update.message.via_bot.username = user - assert not f(update) + assert not f.check_update(update) def test_filters_via_bot_repr(self): - f = Filters.via_bot([1, 2]) - assert str(f) == 'Filters.via_bot(1, 2)' + f = filters.ViaBot([1, 2]) + assert str(f) == 'filters.ViaBot(1, 2)' f.remove_bot_ids(1) f.remove_bot_ids(2) - assert str(f) == 'Filters.via_bot()' + assert str(f) == 'filters.ViaBot()' f.add_usernames('@foobar') - assert str(f) == 'Filters.via_bot(foobar)' + assert str(f) == 'filters.ViaBot(foobar)' f.add_usernames('@barfoo') - assert str(f).startswith('Filters.via_bot(') + assert str(f).startswith('filters.ViaBot(') # we don't know th exact order assert 'barfoo' in str(f) and 'foobar' in str(f) @@ -2220,7 +2258,7 @@ class TestFilters: f.name = 'foo' def test_filters_attachment(self, update): - assert not Filters.attachment(update) + assert not filters.ATTACHMENT.check_update(update) # we need to define a new Update (or rather, message class) here because # effective_attachment is only evaluated once per instance, and the filter relies on that up = Update( @@ -2232,4 +2270,4 @@ class TestFilters: document=Document("str", "other_str"), ), ) - assert Filters.attachment(up) + assert filters.ATTACHMENT.check_update(up) diff --git a/tests/test_jobqueue.py b/tests/test_jobqueue.py index d28f390c1..2fb19ed43 100644 --- a/tests/test_jobqueue.py +++ b/tests/test_jobqueue.py @@ -350,7 +350,7 @@ class TestJobQueue: sleep(delta + 0.1) assert self.result == 1 scheduled_time = job_queue.jobs()[0].next_t.timestamp() - assert scheduled_time == pytest.approx(expected_reschedule_time) + assert scheduled_time == pytest.approx(expected_reschedule_time, rel=1e-3) def test_run_monthly_non_strict_day(self, job_queue, timezone): delta, now = 1, dtm.datetime.now(timezone) diff --git a/tests/test_messagehandler.py b/tests/test_messagehandler.py index 8bd22be06..1a1891072 100644 --- a/tests/test_messagehandler.py +++ b/tests/test_messagehandler.py @@ -33,7 +33,7 @@ from telegram import ( ShippingQuery, PreCheckoutQuery, ) -from telegram.ext import Filters, MessageHandler, CallbackContext, JobQueue, UpdateFilter +from telegram.ext import filters, MessageHandler, CallbackContext, JobQueue message = Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text') @@ -71,7 +71,7 @@ class TestMessageHandler: SRE_TYPE = type(re.match("", "")) def test_slot_behaviour(self, mro_slots): - handler = MessageHandler(Filters.all, self.callback_context) + handler = MessageHandler(filters.ALL, self.callback_context) for attr in handler.__slots__: assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" @@ -120,7 +120,7 @@ class TestMessageHandler: self.test_flag = types and num def test_with_filter(self, message): - handler = MessageHandler(Filters.chat_type.group, self.callback_context) + handler = MessageHandler(filters.ChatType.GROUP, self.callback_context) message.chat.type = 'group' assert handler.check_update(Update(0, message)) @@ -129,7 +129,7 @@ class TestMessageHandler: assert not handler.check_update(Update(0, message)) def test_callback_query_with_filter(self, message): - class TestFilter(UpdateFilter): + class TestFilter(filters.UpdateFilter): flag = False def filter(self, u): @@ -146,9 +146,9 @@ class TestMessageHandler: def test_specific_filters(self, message): f = ( - ~Filters.update.messages - & ~Filters.update.channel_post - & Filters.update.edited_channel_post + ~filters.UpdateType.MESSAGES + & ~filters.UpdateType.CHANNEL_POST + & filters.UpdateType.EDITED_CHANNEL_POST ) handler = MessageHandler(f, self.callback_context) @@ -184,7 +184,7 @@ class TestMessageHandler: assert self.test_flag def test_context_regex(self, dp, message): - handler = MessageHandler(Filters.regex('one two'), self.callback_context_regex1) + handler = MessageHandler(filters.Regex('one two'), self.callback_context_regex1) dp.add_handler(handler) message.text = 'not it' @@ -197,7 +197,7 @@ class TestMessageHandler: def test_context_multiple_regex(self, dp, message): handler = MessageHandler( - Filters.regex('one') & Filters.regex('two'), self.callback_context_regex2 + filters.Regex('one') & filters.Regex('two'), self.callback_context_regex2 ) dp.add_handler(handler) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index f8b8677e8..941a29f5d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -42,7 +42,7 @@ from telegram.ext import ( BasePersistence, ConversationHandler, MessageHandler, - Filters, + filters, PicklePersistence, CommandHandler, DictPersistence, @@ -401,15 +401,15 @@ class TestBasePersistence: context.bot.callback_data_cache.put('test0') known_user = MessageHandler( - Filters.user(user_id=12345), + filters.User(user_id=12345), callback_known_user, ) known_chat = MessageHandler( - Filters.chat(chat_id=-67890), + filters.Chat(chat_id=-67890), callback_known_chat, ) unknown = MessageHandler( - Filters.all, + filters.ALL, callback_unknown_user_or_chat, ) dp.add_handler(known_user) @@ -530,12 +530,12 @@ class TestBasePersistence: self.test_flag = 'bot_data was wrongly refreshed' with_user_and_chat = MessageHandler( - Filters.user(user_id=12345), + filters.User(user_id=12345), callback_with_user_and_chat, run_async=run_async, ) without_user_and_chat = MessageHandler( - Filters.all, + filters.ALL, callback_without_user_and_chat, run_async=run_async, ) @@ -2221,8 +2221,8 @@ class TestDictPersistence: if not context.bot.callback_data_cache.persistence_data == ([], {'test1': 'test0'}): pytest.fail() - h1 = MessageHandler(Filters.all, first) - h2 = MessageHandler(Filters.all, second) + h1 = MessageHandler(filters.ALL, first) + h2 = MessageHandler(filters.ALL, second) dp.add_handler(h1) dp.process_update(update) user_data = dict_persistence.user_data_json