Repository: python-telegram-bot/python-telegram-bot

Issue:
[FEATURE] introduce a file ending filter
Users might want to filter incoming updates based on the file endings on top of/instead of the mime type. Both attributes of documents are not safe, but that didn't stop us from introducing a mime type want and shouldn't stop us from a file ending one either. Both are a great way of initially filtering incoming files. We should just mention in the docs, as we do with mime types, that telegram doesnt offer any validation.

This filter can either be a subset of Filters.document or its own imo. 
Added: Filter for file ending/extension
# Feature: Added filter for file endings / file extensions on documents

Added class 'file_ending' that filter documents which end in the
extension passed by the user to Filters.document.file_ending;  
Also added name to AUTHORS.rst.

This pull request solves issue #2140



Discussion:
+1 for `Filters.document.extension`. As documents are the only valid use case, I thinks thats more natural than `Filters.extension`
Can i give this issue a shot?
Sure!


Resolving patch:
diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py
--- a/telegram/ext/filters.py
+++ b/telegram/ext/filters.py
@@ -603,6 +603,68 @@ def filter(self, message: Message) -> bool:
         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.
+            """
+
+            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 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)
 
@@ -659,6 +721,29 @@ def filter(self, message: Message) -> bool:
         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):


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -425,6 +425,113 @@ def test_filters_document_type(self, update):
         assert Filters.document.category("application/")(update)
         assert Filters.document.mime_type("application/x-sh")(update)
 
+    def test_filters_file_extension_basic(self, update):
+        update.message.document = Document(
+            "file_id",
+            "unique_id",
+            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)
+
+        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)
+
+        update.message.document = None
+        assert not Filters.document.file_extension("jpg")(update)
+
+    def test_filters_file_extension_minds_dots(self, update):
+        update.message.document = Document(
+            "file_id",
+            "unique_id",
+            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)
+
+        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)
+
+        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)
+
+        update.message.document.file_name = "file"
+        assert not Filters.document.file_extension("")(update)
+        assert not Filters.document.file_extension("file")(update)
+
+        update.message.document.file_name = "file."
+        assert Filters.document.file_extension("")(update)
+
+    def test_filters_file_extension_none_arg(self, update):
+        update.message.document = Document(
+            "file_id",
+            "unique_id",
+            file_name="file.jpg",
+            mime_type="image/jpeg",
+        )
+        assert not Filters.document.file_extension(None)(update)
+
+        update.message.document.file_name = "file"
+        assert Filters.document.file_extension(None)(update)
+        assert not Filters.document.file_extension("None")(update)
+
+        update.message.document.file_name = "file."
+        assert not Filters.document.file_extension(None)(update)
+
+        update.message.document = None
+        assert not Filters.document.file_extension(None)(update)
+
+    def test_filters_file_extension_case_sensitivity(self, update):
+        update.message.document = Document(
+            "file_id",
+            "unique_id",
+            file_name="file.jpg",
+            mime_type="image/jpeg",
+        )
+        assert Filters.document.file_extension("JPG")(update)
+        assert Filters.document.file_extension("jpG")(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)
+
+        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)
+
+    def test_filters_file_extension_name(self):
+        assert Filters.document.file_extension("jpg").name == (
+            "Filters.document.file_extension('jpg')"
+        )
+        assert Filters.document.file_extension("JPG").name == (
+            "Filters.document.file_extension('jpg')"
+        )
+        assert Filters.document.file_extension("jpg", case_sensitive=True).name == (
+            "Filters.document.file_extension('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.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)"
+        )
+
     def test_filters_animation(self, update):
         assert not Filters.animation(update)
         update.message.animation = 'test'