Repository: python-telegram-bot/python-telegram-bot
Pull request: Add file extension filter
Resolve #2140.
Based on an abandoned PR of another contributor.
Fix #2156.

Problem statement:
[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.

Hints from issue 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!

Commit message: Added unit tests for class test_ending

Added tests for some possible cases of file extensions.

Patch to review:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index e8d349b2995..59afbb242d1 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -62,6 +62,7 @@ The following wonderful people contributed directly or indirectly to this projec
 - `Loo Zheng Yuan <https://github.com/loozhengyuan>`_
 - `LRezende <https://github.com/lrezende>`_
 - `macrojames <https://github.com/macrojames>`_
+- `Matheus Lemos <https://github.com/mlemosf>`_
 - `Michael Elovskikh <https://github.com/wronglink>`_
 - `Mischa Krüger <https://github.com/Makman2>`_
 - `naveenvhegde <https://github.com/naveenvhegde>`_
diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py
index 1736b04571e..75707b37f76 100644
--- a/telegram/ext/filters.py
+++ b/telegram/ext/filters.py
@@ -603,6 +603,33 @@ 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.
+
+            Example:
+                ``Filters.document.file_extension('jpg')`` filters all files with extension ``.jpg``
+            """
+
+            def __init__(self, file_extension: Optional[str]):
+                """Initialize the extension you want to filter
+
+                Args:
+                    file_extension (str, optional): file extension of the media you want to filter"""
+                self.file_extension = file_extension
+                self.name = f"Filters.document.file_extenision('{self.file_extension}')"
+
+            def filter(self, message : Message) -> bool:
+                """"""  # remove method from docs
+                if message.document:
+                    return message.document.file_name.endswith(self.file_extension)
+                return False
+
         def filter(self, message: Message) -> bool:
             return bool(message.document)
 
diff --git a/tests/test_filters.py b/tests/test_filters.py
index b129b0510cc..25419e91d42 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -424,7 +424,19 @@ def test_filters_document_type(self, update):
         update.message.document.mime_type = "application/x-sh"
         assert Filters.document.category("application/")(update)
         assert Filters.document.mime_type("application/x-sh")(update)
-
+ 
+        update.message.document.file_name = "file.docx"
+        assert Filters.document.file_extension('docx')(update)
+        assert not Filters.document.file_extension('pdf')(update)
+        
+        update.message.document.file_name = "file.tar.gz"
+        assert Filters.document.file_extension('tar.gz')(update)
+        assert not Filters.document.file_extension('zip')(update)
+        
+        update.message.document.file_name = "file.png"
+        assert Filters.document.file_extension('png')(update)
+        assert not Filters.document.file_extension('jpeg')(update)
+        
     def test_filters_animation(self, update):
         assert not Filters.animation(update)
         update.message.animation = 'test'


Code review comments:
diff_hunk: @@ -603,6 +603,45 @@ 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.
+
+            Example:
+                ``Filters.document.file_extension('jpg')`` filters files with extension ``.jpg``.
+            """
+
+            file_extension: Optional[List[str]] = None
path: telegram/ext/filters.py
text: @user1:
What is this good for?

@author:
To not assing the attr in `__init__()` if it's None. Not much good, i'll might edit this.

@user1:
please do ;)

diff_hunk: @@ -603,6 +603,45 @@ 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.
+
+            Example:
+                ``Filters.document.file_extension('jpg')`` filters files with extension ``.jpg``.
+            """
+
+            file_extension: Optional[List[str]] = None
+
+            def __init__(self, file_extension: Optional[str]):
+                """Initialize the extension you want to filter.
+
+                Args:
+                    file_extension (str, optional): media file extension you want to filter.
+                """
+                if file_extension is not None:
+                    file_extension = file_extension.lstrip(".")
+                    self.file_extension = file_extension.split(".")
+                self.name = f"Filters.document.file_extenision({file_extension!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 True
+                filename_parts = message.document.file_name.split(".")
+                extension_len = len(self.file_extension)
+                if len(filename_parts) - extension_len < 1:
+                    # Exclude extension-only filenames.
+                    return False
+                return filename_parts[-extension_len:] == self.file_extension
path: telegram/ext/filters.py
text: @user1:
why the hole splitting? `return message.document.file_name.endswith(f'.{self.file_extension}')` should do the trick

diff_hunk: @@ -603,6 +603,45 @@ 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.
+
+            Example:
+                ``Filters.document.file_extension('jpg')`` filters files with extension ``.jpg``.
+            """
+
+            file_extension: Optional[List[str]] = None
+
+            def __init__(self, file_extension: Optional[str]):
+                """Initialize the extension you want to filter.
+
+                Args:
+                    file_extension (str, optional): media file extension you want to filter.
+                """
+                if file_extension is not None:
+                    file_extension = file_extension.lstrip(".")
path: telegram/ext/filters.py
text: @user1:
I do see why you want the user to not have to care about the dot, but I'd prefer to not fiddle with it and instead just document if the dot must be passed or not. Image someone for some reason wanting to filter for a `......ext` extension. That wouldn't work in the current setting.

@author:
This is the case i didn't think of. Okay.