From 40bcd4e59d8b96b2dddd70ce810006988672101d Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Wed, 26 Jun 2019 21:43:08 +0200 Subject: [PATCH 1/7] Fix delete_profile_photos. Closes #259 --- .../methods/users/delete_profile_photos.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pyrogram/client/methods/users/delete_profile_photos.py b/pyrogram/client/methods/users/delete_profile_photos.py index 1b46382c..a165f7d1 100644 --- a/pyrogram/client/methods/users/delete_profile_photos.py +++ b/pyrogram/client/methods/users/delete_profile_photos.py @@ -16,23 +16,24 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from base64 import b64decode from struct import unpack from typing import List, Union from pyrogram.api import functions, types +from pyrogram.client.ext import utils + from ...ext import BaseClient class DeleteProfilePhotos(BaseClient): def delete_profile_photos( self, - id: Union[str, List[str]] + photo_ids: Union[str, List[str]] ) -> bool: """Delete your own profile photos. Parameters: - id (``str`` | ``list``): + photo_ids (``str`` | List of ``str``): A single :obj:`Photo` id as string or multiple ids as list of strings for deleting more than one photos at once. @@ -42,16 +43,16 @@ class DeleteProfilePhotos(BaseClient): Raises: RPCError: In case of a Telegram RPC error. """ - id = id if isinstance(id, list) else [id] + photo_ids = photo_ids if isinstance(photo_ids, list) else [photo_ids] input_photos = [] - for i in id: - s = unpack(" Date: Thu, 27 Jun 2019 11:59:44 +0200 Subject: [PATCH 2/7] Make unknown errors with known error codes inherit from base categories --- compiler/error/compiler.py | 10 +++--- pyrogram/errors/rpc_error.py | 63 +++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/compiler/error/compiler.py b/compiler/error/compiler.py index 996c4981..80826396 100644 --- a/compiler/error/compiler.py +++ b/compiler/error/compiler.py @@ -81,6 +81,8 @@ def start(): sub_classes = [] + f_all.write(" \"_\": \"{}\",\n".format(super_class)) + for j, row in enumerate(reader): if j == 0: continue @@ -90,13 +92,13 @@ def start(): if not row: # Row is empty (blank line) continue - id, message = row + error_id, error_message = row - sub_class = caml(re.sub(r"_X", "_", id)) + sub_class = caml(re.sub(r"_X", "_", error_id)) - f_all.write(" \"{}\": \"{}\",\n".format(id, sub_class)) + f_all.write(" \"{}\": \"{}\",\n".format(error_id, sub_class)) - sub_classes.append((sub_class, id, message)) + sub_classes.append((sub_class, error_id, error_message)) with open("{}/template/class.txt".format(HOME), "r", encoding="utf-8") as f_class_template: class_template = f_class_template.read() diff --git a/pyrogram/errors/rpc_error.py b/pyrogram/errors/rpc_error.py index c1799f50..fb3a717d 100644 --- a/pyrogram/errors/rpc_error.py +++ b/pyrogram/errors/rpc_error.py @@ -17,65 +17,68 @@ # along with Pyrogram. If not, see . import re +from datetime import datetime from importlib import import_module +from typing import Type +from pyrogram.api.core import TLObject from pyrogram.api.types import RpcError as RawRPCError from .exceptions.all import exceptions class RPCError(Exception): - """This is the base exception class for all Telegram API related errors. - For a finer grained control, see the specific errors below. - """ ID = None CODE = None NAME = None - MESSAGE = None + MESSAGE = "{x}" - def __init__(self, x: int or RawRPCError = None, query_type: type = None): - super().__init__("[{} {}]: {}".format( + def __init__(self, x: int or RawRPCError, rpc_name: str, is_unknown: bool): + super().__init__("[{} {}]: {} ({})".format( self.CODE, self.ID or self.NAME, - str(self) or self.MESSAGE.format(x=x) + self.MESSAGE.format(x=x), + 'caused by "{}"'.format(rpc_name) )) - try: - self.x = int(x) - except (ValueError, TypeError): - self.x = x - - # TODO: Proper log unknown errors - if self.CODE == 520: + if is_unknown: with open("unknown_errors.txt", "a", encoding="utf-8") as f: - f.write("{}\t{}\t{}\n".format(x.error_code, x.error_message, query_type)) + f.write("{}\t{}\t{}\n".format(datetime.now(), x, rpc_name)) @staticmethod - def raise_it(rpc_error: RawRPCError, query_type: type): - code = rpc_error.error_code + def raise_it(rpc_error: RawRPCError, rpc_type: Type[TLObject]): + error_code = rpc_error.error_code + error_message = rpc_error.error_message + rpc_name = ".".join(rpc_type.QUALNAME.split(".")[1:]) - if code not in exceptions: - raise UnknownError(x=rpc_error, query_type=query_type) + if error_code not in exceptions: + raise UnknownError( + x="[{} {}]".format(error_code, error_message), + rpc_name=rpc_name, + is_unknown=True + ) - message = rpc_error.error_message - id = re.sub(r"_\d+", "_X", message) + error_id = re.sub(r"_\d+", "_X", error_message) - if id not in exceptions[code]: - raise UnknownError(x=rpc_error, query_type=query_type) + if error_id not in exceptions[error_code]: + raise getattr( + import_module("pyrogram.errors"), + exceptions[error_code]["_"] + )(x="[{} {}]".format(error_code, error_message), + rpc_name=rpc_name, + is_unknown=True) - x = re.search(r"_(\d+)", message) + x = re.search(r"_(\d+)", error_message) x = x.group(1) if x is not None else x raise getattr( import_module("pyrogram.errors"), - exceptions[code][id] - )(x=x) + exceptions[error_code][error_id] + )(x=x, + rpc_name=rpc_name, + is_unknown=False) class UnknownError(RPCError): - """This object represents an Unknown Error, that is, an error which - Pyrogram does not know anything about, yet. - """ CODE = 520 """:obj:`int`: Error code""" NAME = "Unknown error" - MESSAGE = "{x}" From a44c996302077b718a2d3c174c1edf9e06b2fd36 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 27 Jun 2019 23:15:12 +0200 Subject: [PATCH 3/7] Revamp text-formatting page, add info about the new styles --- docs/source/topics/text-formatting.rst | 228 +++++++++++++++++++------ 1 file changed, 178 insertions(+), 50 deletions(-) diff --git a/docs/source/topics/text-formatting.rst b/docs/source/topics/text-formatting.rst index bc74d562..03b50a8e 100644 --- a/docs/source/topics/text-formatting.rst +++ b/docs/source/topics/text-formatting.rst @@ -1,40 +1,99 @@ Text Formatting =============== -Pyrogram, just like the `Telegram Bot API`_, natively supports basic Markdown and HTML formatting styles for text -messages and media captions. +.. role:: strike + :class: strike -Markdown style uses the same syntax as Telegram Desktop's and is enabled by default. +.. role:: underline + :class: underline -Beside bold, italic, and pre-formatted code, **Pyrogram does also support inline URLs and inline mentions of users**. +.. role:: bold-underline + :class: bold-underline + +.. role:: strike-italic + :class: strike-italic + +Pyrogram uses a custom Markdown dialect for text formatting which adds some unique features that make writing styled +texts easier in both Markdown and HTML. You can send sophisticated text messages and media captions using a great +variety of decorations that can also be nested in order to combine multiple styles together. + +Basic Styles +------------ + +When formatting your messages, you can choose between Markdown-style, HTML-style or both (default). The following is a +list of the basic styles currently supported by Pyrogram. + +- **bold** +- *italic* +- :strike:`strike` +- :underline:`underline` +- `text URL `_ +- `user text mention `_ +- ``inline fixed-width code`` +- .. code-block:: text + + pre-formatted + fixed-width + code block + +.. note:: + + User text mentions are only guaranteed to work if you have already met the user (in groups or private chats). Markdown Style -------------- -To use this mode, pass "markdown" in the *parse_mode* field when using +To strictly use this mode, pass "markdown" to the *parse_mode* parameter when using :meth:`~pyrogram.Client.send_message`. Use the following syntax in your message: .. code-block:: text - **bold text** + **bold** - __italic text__ + __italic__ - [inline URL](https://docs.pyrogram.org/) + --underline-- - [inline mention of a user](tg://user?id=23122162) + ~~strike~~ + + [text URL](https://docs.pyrogram.org/) + + [text user mention](tg://user?id=23122162) `inline fixed-width code` - ```block_language - pre-formatted fixed-width code block + ``` + pre-formatted + fixed-width + code block ``` +**Example**: + +.. code-block:: python + + app.send_message( + "haskell", + ( + "**bold**, " + "__italic__, " + "--underline--, " + "~~strikethrough~~, " + "[mention](tg://user?id=23122162), " + "[URL](https://pyrogram.org), " + "`code`, " + "```" + "for i in range(10):\n" + " print(i)" + "```" + ), + parse_mode="markdown" + ) HTML Style ---------- -To use this mode, pass "html" in the *parse_mode* field when using :meth:`~pyrogram.Client.send_message`. +To strictly use this mode, pass "html" to the *parse_mode* parameter when using :meth:`~pyrogram.Client.send_message`. The following tags are currently supported: .. code-block:: text @@ -43,55 +102,124 @@ The following tags are currently supported: italic, italic - inline URL + underline - inline mention of a user + strike, strike, strike + + text URL + + inline mention inline fixed-width code -
pre-formatted fixed-width code block
+
+    pre-formatted
+      fixed-width
+        code block
+    
-.. note:: Mentions are only guaranteed to work if you have already met the user (in groups or private chats). +**Example**: -Examples --------- +.. code-block:: python -- Markdown: + app.send_message( + "haskell", + ( + "bold, " + "italic, " + "underline, " + "strikethrough, " + "mention, " + "URL, " + "code\n\n" + "
"
+            "for i in range(10):\n"
+            "    print(i)"
+            "
" + ), + parse_mode="html" + ) + +.. note:: + + All ``<``, ``>`` and ``&`` symbols that are not a part of a tag or an HTML entity must be replaced with the + corresponding HTML entities (``<`` with ``<``, ``>`` with ``>`` and ``&`` with ``&``). You can use this + snippet to quickly escape those characters: .. code-block:: python - app.send_message( - chat_id="haskell", - text=( - "**bold**, " - "__italic__, " - "[mention](tg://user?id=23122162), " - "[URL](https://docs.pyrogram.org), " - "`code`, " - "```" - "for i in range(10):\n" - " print(i)```" - ) - ) + import html -- HTML: + text = "" + text = html.escape(text) - .. code-block:: python + print(text) - app.send_message( - chat_id="haskell", - text=( - "bold, " - "italic, " - "mention, " - "URL, " - "code, " - "
"
-                "for i in range(10):\n"
-                "    print(i)"
-                "
" - ), - parse_mode="html" - ) + .. code-block:: text -.. _Telegram Bot API: https://core.telegram.org/bots/api#formatting-options \ No newline at end of file + <my text> + +Different Styles +---------------- + +By default, when ignoring the *parse_mode* parameter, both Markdown and HTML styles are enabled together. +This means you can combine together both syntaxes in the same text: + +.. code-block:: python + + app.send_message("haskell", "**bold**, italic") + +Result: + + **bold**, *italic* + +If you don't like this behaviour you can always choose to only enable either Markdown or HTML in strict mode by passing +"markdown" or "html" as argument to the *parse_mode* parameter. + +.. code-block:: + + app.send_message("haskell", "**bold**, italic", parse_mode="markdown") + app.send_message("haskell", "**bold**, italic", parse_mode="html") + +Result: + + **bold**, italic + + \*\*bold**, *italic* + +In case you want to completely turn off the style parser, simply pass ``None`` to *parse_mode*. The text will be sent +as-is. + +.. code-block:: python + + app.send_message("haskell", "**bold**, italic", parse_mode=None) + +Result: + + \*\*bold**, italic + +Nested and Overlapping Entities +------------------------------- + +You can also style texts with more than one decoration at once by nesting entities together. For example, you can send +a text message with both :bold-underline:`bold and underline` styles, or a text that has both :italic-strike:`italic and +strike` styles, and you can still combine both Markdown and HTML together. + +Here there are some example texts you can try sending: + +**Markdown**: + +- ``**bold, --underline--**`` +- ``**bold __italic --underline ~~striked~~--__**`` +- ``**bold __and** italic__`` + +**HTML**: + +- ``bold, underline`` +- ``bold italic underline striked`` +- ``bold and italic`` + +**Combined**: + +- ``--you can combine HTML with **Markdown**--`` +- ``**and also overlap** --entities this way--`` From 9f231bb880d99e7bef196f7914bc6e92a7cc4a89 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 27 Jun 2019 23:15:46 +0200 Subject: [PATCH 4/7] Update errors documentation page --- docs/source/start/errors.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/source/start/errors.rst b/docs/source/start/errors.rst index cf329947..bd82bf73 100644 --- a/docs/source/start/errors.rst +++ b/docs/source/start/errors.rst @@ -28,7 +28,7 @@ Error Categories ---------------- The ``RPCError`` packs together all the possible errors Telegram could raise, but to make things tidier, Pyrogram -provides categories of errors, which are named after the common HTTP errors and subclass-ed from the RPCError: +provides categories of errors, which are named after the common HTTP errors and are subclass-ed from the RPCError: .. code-block:: python @@ -71,14 +71,22 @@ RPCError, thus building a class of error hierarchy such as this: Unknown Errors -------------- -In case Pyrogram does not know anything yet about a specific error, it raises a special ``520 - UnknownError`` exception -and logs it in the ``unknown_errors.txt`` file. Users are invited to report these unknown errors. +In case Pyrogram does not know anything about a specific error yet, it raises a generic error from its known category, +for example, an unknown error with error code ``400``, will be raised as a ``BadRequest``. This way you can catch the +whole category of errors and be sure to also handle these unknown errors. + +In case a whole class of errors is unknown (that is, an error code that is unknown), Pyrogram will raise a special +``520 UnknownError`` exception. + +In both cases, Pyrogram will log them in the ``unknown_errors.txt`` file. Users are invited to report +these unknown errors in the `discussion group `_. Errors with Values ------------------ Exception objects may also contain some informative values. For example, ``FloodWait`` holds the amount of seconds you -have to wait before you can try again. The value is always stored in the ``x`` field of the returned exception object: +have to wait before you can try again, some other errors contain the DC number on which the request must be repeated on. +The value is stored in the ``x`` attribute of the exception object: .. code-block:: python @@ -88,4 +96,4 @@ have to wait before you can try again. The value is always stored in the ``x`` f try: ... except FloodWait as e: - time.sleep(e.x) # Wait before trying again + time.sleep(e.x) # Wait "x" seconds before continuing From b6f508711aff07b7a35f9f271514c4385bb4c2f4 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Thu, 27 Jun 2019 23:16:21 +0200 Subject: [PATCH 5/7] Many minor documentation enhancements --- docs/source/api/client.rst | 6 +++++- docs/source/api/decorators.rst | 2 +- docs/source/api/handlers.rst | 3 --- docs/source/api/methods.rst | 2 +- docs/source/api/types.rst | 2 +- docs/source/index.rst | 2 +- docs/source/start/updates.rst | 12 +++++++----- docs/source/topics/storage-engines.rst | 2 +- docs/source/topics/use-filters.rst | 4 ++-- 9 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/source/api/client.rst b/docs/source/api/client.rst index d1b8c4b0..d28b7f61 100644 --- a/docs/source/api/client.rst +++ b/docs/source/api/client.rst @@ -1,7 +1,11 @@ Pyrogram Client =============== -This is the Client class. It exposes high-level methods for an easy access to the API. +You have entered the API Reference section where you can find detailed information about Pyrogram's API. The main Client +class, all available methods and types, filters, handlers, decorators and bound-methods detailed descriptions can be +found starting from this page. + +This page is about the Client class, which exposes high-level methods for an easy access to the API. .. code-block:: python :emphasize-lines: 1-3 diff --git a/docs/source/api/decorators.rst b/docs/source/api/decorators.rst index ff31cb27..fd397cc4 100644 --- a/docs/source/api/decorators.rst +++ b/docs/source/api/decorators.rst @@ -6,7 +6,7 @@ deserve a dedicated page. Decorators are able to register callback functions for handling updates in a much easier and cleaner way compared to :doc:`Handlers `; they do so by instantiating the correct handler and calling -:meth:`~pyrogram.Client.add_handler`, automatically. All you need to do is adding the decorators on top of your +:meth:`~pyrogram.Client.add_handler` automatically. All you need to do is adding the decorators on top of your functions. .. code-block:: python diff --git a/docs/source/api/handlers.rst b/docs/source/api/handlers.rst index f91dd3d5..1ae0961b 100644 --- a/docs/source/api/handlers.rst +++ b/docs/source/api/handlers.rst @@ -2,10 +2,7 @@ Update Handlers =============== Handlers are used to instruct Pyrogram about which kind of updates you'd like to handle with your callback functions. - For a much more convenient way of registering callback functions have a look at :doc:`Decorators ` instead. -In case you decided to manually create a handler, use :class:`~pyrogram.Client.add_handler` to register -it. .. code-block:: python :emphasize-lines: 1, 10 diff --git a/docs/source/api/methods.rst b/docs/source/api/methods.rst index 2a08b37f..0cd7700f 100644 --- a/docs/source/api/methods.rst +++ b/docs/source/api/methods.rst @@ -1,7 +1,7 @@ Available Methods ================= -All Pyrogram methods listed here are bound to a :class:`~pyrogram.Client` instance. +This page is about Pyrogram methods. All the methods listed here are bound to a :class:`~pyrogram.Client` instance. .. code-block:: python :emphasize-lines: 6 diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 644f8bb2..f91e4f58 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -1,7 +1,7 @@ Available Types =============== -All Pyrogram types listed here are accessible through the main package directly. +This page is about Pyrogram types. All types listed here are accessible through the main package directly. .. code-block:: python :emphasize-lines: 1 diff --git a/docs/source/index.rst b/docs/source/index.rst index 66722690..682c883c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -28,8 +28,8 @@ Welcome to Pyrogram api/bound-methods api/handlers api/decorators - api/filters api/errors + api/filters .. toctree:: :hidden: diff --git a/docs/source/start/updates.rst b/docs/source/start/updates.rst index 9b0a5a32..056fcb3d 100644 --- a/docs/source/start/updates.rst +++ b/docs/source/start/updates.rst @@ -45,7 +45,9 @@ arrives: app.run() -#. Let's examine these four new pieces. First one: a callback function we defined which accepts two arguments - +Let's examine these four new pieces. + +#. A callback function we defined which accepts two arguments - *(client, message)*. This will be the function that gets executed every time a new message arrives and Pyrogram will call that function by passing the client instance and the new message instance as argument. @@ -54,14 +56,14 @@ arrives: def my_function(client, message): print(message) -#. Second one: the :class:`~pyrogram.MessageHandler`. This object tells Pyrogram the function we defined above must - only handle updates that are in form of a :class:`~pyrogram.Message`: +#. The :class:`~pyrogram.MessageHandler`. This object tells Pyrogram the function we defined above must only handle + updates that are in form of a :class:`~pyrogram.Message`: .. code-block:: python my_handler = MessageHandler(my_function) -#. Third: the method :meth:`~pyrogram.Client.add_handler`. This method is used to actually register the handler and let +#. The method :meth:`~pyrogram.Client.add_handler`. This method is used to actually register the handler and let Pyrogram know it needs to be taken into consideration when new updates arrive and the internal dispatching phase begins. @@ -69,7 +71,7 @@ arrives: app.add_handler(my_handler) -#. Last one, the :meth:`~pyrogram.Client.run` method. What this does is simply call :meth:`~pyrogram.Client.start` and +#. The :meth:`~pyrogram.Client.run` method. What this does is simply call :meth:`~pyrogram.Client.start` and a special method :meth:`~pyrogram.Client.idle` that keeps your main scripts alive until you press ``CTRL+C``; the client will be automatically stopped after that. diff --git a/docs/source/topics/storage-engines.rst b/docs/source/topics/storage-engines.rst index a161e50f..44b4afa6 100644 --- a/docs/source/topics/storage-engines.rst +++ b/docs/source/topics/storage-engines.rst @@ -95,5 +95,5 @@ engine to properly work as intended. But, why is the session string so long? Can't it be shorter? No, it can't. The session string already packs the bare minimum data Pyrogram needs to successfully reconnect to an authorized session, and the 2048-bits auth key is the major -contributor to the overall length. Needless to repeat that this string, as well as any other session storage, represent +contributor to the overall length. Needless to say that this string, as well as any other session storage, represent strictly personal data. Keep them safe. diff --git a/docs/source/topics/use-filters.rst b/docs/source/topics/use-filters.rst index d481b393..de7a35a8 100644 --- a/docs/source/topics/use-filters.rst +++ b/docs/source/topics/use-filters.rst @@ -1,8 +1,8 @@ Using Filters ============= -So far we've seen how to register a callback function that executes every time a specific update comes from the server, -but there's much more than that to come. +So far we've seen :doc:`how to register a callback function <../start/updates>` that executes every time a specific update +comes from the server, but there's much more than that to come. Here we'll discuss about :class:`~pyrogram.Filters`. Filters enable a fine-grain control over what kind of updates are allowed or not to be passed in your callback functions, based on their inner details. From 506253e506a8c22175346e92d7a017cd6118013b Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 28 Jun 2019 10:41:57 +0200 Subject: [PATCH 6/7] Fix objects failing to print in case there's no __slots__ attribute --- pyrogram/client/types/object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrogram/client/types/object.py b/pyrogram/client/types/object.py index 4d482e63..f7fc413f 100644 --- a/pyrogram/client/types/object.py +++ b/pyrogram/client/types/object.py @@ -50,7 +50,7 @@ class Object(metaclass=Meta): else (attr, str(datetime.fromtimestamp(getattr(obj, attr)))) if attr.endswith("date") else (attr, getattr(obj, attr)) - for attr in obj.__slots__ + for attr in getattr(obj, "__slots__", []) if getattr(obj, attr) is not None ] ) From 155580649ac7781029393ccdbab084a48d3ba853 Mon Sep 17 00:00:00 2001 From: Dan <14043624+delivrance@users.noreply.github.com> Date: Fri, 28 Jun 2019 11:11:59 +0200 Subject: [PATCH 7/7] Update filters: Make the name argument optional --- docs/source/topics/create-filters.rst | 39 +++---- pyrogram/client/filters/filters.py | 149 +++++++++++++------------- 2 files changed, 92 insertions(+), 96 deletions(-) diff --git a/docs/source/topics/create-filters.rst b/docs/source/topics/create-filters.rst index 6cb33a50..6ae6e98c 100644 --- a/docs/source/topics/create-filters.rst +++ b/docs/source/topics/create-filters.rst @@ -24,7 +24,7 @@ button: app.send_message( "username", # Change this to your username or id - "Pyrogram's custom filter test", + "Pyrogram custom filter test", reply_markup=InlineKeyboardMarkup( [[InlineKeyboardButton("Press me", "pyrogram")]] ) @@ -33,61 +33,54 @@ button: Basic Filters ------------- -For this basic filter we will be using only the first two parameters of :meth:`~pyrogram.Filters.create`. +For this basic filter we will be using only the first parameter of :meth:`~pyrogram.Filters.create`. The code below creates a simple filter for hardcoded, static callback data. This filter will only allow callback queries -containing "Pyrogram" as data, that is, the function *func* you pass returns True in case the callback query data -equals to ``"Pyrogram"``. +containing "pyrogram" as data, that is, the function *func* you pass returns True in case the callback query data +equals to ``"pyrogram"``. .. code-block:: python - static_data = Filters.create( - name="StaticdData", - func=lambda flt, query: query.data == "Pyrogram" - ) + static_data_filter = Filters.create(lambda _, query: query.data == "pyrogram") The ``lambda`` operator in python is used to create small anonymous functions and is perfect for this example, the same -could be achieved with a normal function, but we don't really need it as it makes sense only inside the filter's scope: +could be achieved with a normal function, but we don't really need it as it makes sense only inside the filter scope: .. code-block:: python - def func(flt, query): - return query.data == "Pyrogram" + def func(_, query): + return query.data == "pyrogram" - static_data = Filters.create( - name="StaticData", - func=func - ) + static_data_filter = Filters.create(func) The filter usage remains the same: .. code-block:: python - @app.on_callback_query(static_data) + @app.on_callback_query(static_data_filter) def pyrogram_data(_, query): query.answer("it works!") Filters with Arguments ---------------------- -A much cooler filter would be one that accepts "Pyrogram" or any other data as argument at usage time. -A dynamic filter like this will make use of the third parameter of :meth:`~pyrogram.Filters.create`. +A much cooler filter would be one that accepts "pyrogram" or any other data as argument at usage time. +A dynamic filter like this will make use of named arguments for the :meth:`~pyrogram.Filters.create` method. This is how a dynamic custom filter looks like: .. code-block:: python - def dynamic_data(data): + def dynamic_data_filter(data): return Filters.create( - name="DynamicData", - func=lambda flt, query: flt.data == query.data, - data=data # "data" kwarg is accessed with "flt.data" + lambda flt, query: flt.data == query.data, + data=data # "data" kwarg is accessed with "flt.data" above ) And its usage: .. code-block:: python - @app.on_callback_query(dynamic_data("Pyrogram")) + @app.on_callback_query(dynamic_data_filter("pyrogram")) def pyrogram_data(_, query): query.answer("it works!") diff --git a/pyrogram/client/filters/filters.py b/pyrogram/client/filters/filters.py index fb0a3615..d8768b3b 100644 --- a/pyrogram/client/filters/filters.py +++ b/pyrogram/client/filters/filters.py @@ -17,190 +17,193 @@ # along with Pyrogram. If not, see . import re +from typing import Callable from .filter import Filter from ..types.bots_and_keyboards import InlineKeyboardMarkup, ReplyKeyboardMarkup +CUSTOM_FILTER_NAME = "CustomFilter" -def create(name: str, func: callable, **kwargs) -> type: - """Create a Filter. + +def create(func: Callable, name: str = None, **kwargs) -> Filter: + """Easily create a custom filter. Custom filters give you extra control over which updates are allowed or not to be processed by your handlers. Parameters: - name (``str``): - Your filter's name. Can be anything you like. - func (``callable``): - A function that accepts two arguments *(filter, update)* and returns a Boolean: True if the update should be - handled, False otherwise. - The "update" argument type will vary depending on which `Handler `_ is coming from. - For example, in a :obj:`MessageHandler` the update type will be - a :obj:`Message`; in a :obj:`CallbackQueryHandler` the - update type will be a :obj:`CallbackQuery`. Your function body can then access the - incoming update and decide whether to allow it or not. + A function that accepts two positional arguments *(filter, update)* and returns a boolean: True if the + update should be handled, False otherwise. The *filter* argument refers to the filter itself and can be used + to access keyword arguments (read below). The *update* argument type will vary depending on which + `Handler `_ is coming from. For example, in a :obj:`MessageHandler` the *update* argument will be + a :obj:`Message`; in a :obj:`CallbackQueryHandler` the *update* will be a :obj:`CallbackQuery`. Your + function body can then access the incoming update attributes and decide whether to allow it or not. + + name (``str``, *optional*): + Your filter's name. Can be anything you like. + Defaults to "CustomFilter". **kwargs (``any``, *optional*): - Any keyword argument you would like to pass. Useful for custom filters that accept parameters (e.g.: - :meth:`~Filters.command`, :meth:`~Filters.regex`). + Any keyword argument you would like to pass. Useful when creating parameterized custom filters, such as + :meth:`~Filters.command` or :meth:`~Filters.regex`. """ # TODO: unpack kwargs using **kwargs into the dict itself. For Python 3.5+ only d = {"__call__": func} d.update(kwargs) - return type(name, (Filter,), d)() + return type(name or CUSTOM_FILTER_NAME, (Filter,), d)() class Filters: """This class provides access to all library-defined Filters available in Pyrogram. - The Filters listed here are intended to be used with the :obj:`MessageHandler` only. + The Filters listed here are currently intended to be used with the :obj:`MessageHandler` only. At the moment, if you want to filter updates coming from different `Handlers `_ you have to create your own filters with :meth:`~Filters.create` and use them in the same way. """ create = create - me = create("Me", lambda _, m: bool(m.from_user and m.from_user.is_self)) + me = create(lambda _, m: bool(m.from_user and m.from_user.is_self), "MeFilter") """Filter messages generated by you yourself.""" - bot = create("Bot", lambda _, m: bool(m.from_user and m.from_user.is_bot)) + bot = create(lambda _, m: bool(m.from_user and m.from_user.is_bot), "BotFilter") """Filter messages coming from bots.""" - incoming = create("Incoming", lambda _, m: not m.outgoing) + incoming = create(lambda _, m: not m.outgoing, "IncomingFilter") """Filter incoming messages. Messages sent to your own chat (Saved Messages) are also recognised as incoming.""" - outgoing = create("Outgoing", lambda _, m: m.outgoing) + outgoing = create(lambda _, m: m.outgoing, "OutgoingFilter") """Filter outgoing messages. Messages sent to your own chat (Saved Messages) are not recognized as outgoing.""" - text = create("Text", lambda _, m: bool(m.text)) + text = create(lambda _, m: bool(m.text), "TextFilter") """Filter text messages.""" - reply = create("Reply", lambda _, m: bool(m.reply_to_message)) + reply = create(lambda _, m: bool(m.reply_to_message), "ReplyFilter") """Filter messages that are replies to other messages.""" - forwarded = create("Forwarded", lambda _, m: bool(m.forward_date)) + forwarded = create(lambda _, m: bool(m.forward_date), "ForwardedFilter") """Filter messages that are forwarded.""" - caption = create("Caption", lambda _, m: bool(m.caption)) + caption = create(lambda _, m: bool(m.caption), "CaptionFilter") """Filter media messages that contain captions.""" - edited = create("Edited", lambda _, m: bool(m.edit_date)) + edited = create(lambda _, m: bool(m.edit_date), "EditedFilter") """Filter edited messages.""" - audio = create("Audio", lambda _, m: bool(m.audio)) + audio = create(lambda _, m: bool(m.audio), "AudioFilter") """Filter messages that contain :obj:`Audio` objects.""" - document = create("Document", lambda _, m: bool(m.document)) + document = create(lambda _, m: bool(m.document), "DocumentFilter") """Filter messages that contain :obj:`Document` objects.""" - photo = create("Photo", lambda _, m: bool(m.photo)) + photo = create(lambda _, m: bool(m.photo), "PhotoFilter") """Filter messages that contain :obj:`Photo` objects.""" - sticker = create("Sticker", lambda _, m: bool(m.sticker)) + sticker = create(lambda _, m: bool(m.sticker), "StickerFilter") """Filter messages that contain :obj:`Sticker` objects.""" - animation = create("Animation", lambda _, m: bool(m.animation)) + animation = create(lambda _, m: bool(m.animation), "AnimationFilter") """Filter messages that contain :obj:`Animation` objects.""" - game = create("Game", lambda _, m: bool(m.game)) + game = create(lambda _, m: bool(m.game), "GameFilter") """Filter messages that contain :obj:`Game` objects.""" - video = create("Video", lambda _, m: bool(m.video)) + video = create(lambda _, m: bool(m.video), "VideoFilter") """Filter messages that contain :obj:`Video` objects.""" - media_group = create("MediaGroup", lambda _, m: bool(m.media_group_id)) + media_group = create(lambda _, m: bool(m.media_group_id), "MediaGroupFilter") """Filter messages containing photos or videos being part of an album.""" - voice = create("Voice", lambda _, m: bool(m.voice)) + voice = create(lambda _, m: bool(m.voice), "VoiceFilter") """Filter messages that contain :obj:`Voice` note objects.""" - video_note = create("VideoNote", lambda _, m: bool(m.video_note)) + video_note = create(lambda _, m: bool(m.video_note), "VideoNoteFilter") """Filter messages that contain :obj:`VideoNote` objects.""" - contact = create("Contact", lambda _, m: bool(m.contact)) + contact = create(lambda _, m: bool(m.contact), "ContactFilter") """Filter messages that contain :obj:`Contact` objects.""" - location = create("Location", lambda _, m: bool(m.location)) + location = create(lambda _, m: bool(m.location), "LocationFilter") """Filter messages that contain :obj:`Location` objects.""" - venue = create("Venue", lambda _, m: bool(m.venue)) + venue = create(lambda _, m: bool(m.venue), "VenueFilter") """Filter messages that contain :obj:`Venue` objects.""" - web_page = create("WebPage", lambda _, m: m.web_page) + web_page = create(lambda _, m: m.web_page, "WebPageFilter") """Filter messages sent with a webpage preview.""" - poll = create("Poll", lambda _, m: m.poll) + poll = create(lambda _, m: m.poll, "PollFilter") """Filter messages that contain :obj:`Poll` objects.""" - private = create("Private", lambda _, m: bool(m.chat and m.chat.type == "private")) + private = create(lambda _, m: bool(m.chat and m.chat.type == "private"), "PrivateFilter") """Filter messages sent in private chats.""" - group = create("Group", lambda _, m: bool(m.chat and m.chat.type in {"group", "supergroup"})) + group = create(lambda _, m: bool(m.chat and m.chat.type in {"group", "supergroup"}), "GroupFilter") """Filter messages sent in group or supergroup chats.""" - channel = create("Channel", lambda _, m: bool(m.chat and m.chat.type == "channel")) + channel = create(lambda _, m: bool(m.chat and m.chat.type == "channel"), "ChannelFilter") """Filter messages sent in channels.""" - new_chat_members = create("NewChatMembers", lambda _, m: bool(m.new_chat_members)) + new_chat_members = create(lambda _, m: bool(m.new_chat_members), "NewChatMembersFilter") """Filter service messages for new chat members.""" - left_chat_member = create("LeftChatMember", lambda _, m: bool(m.left_chat_member)) + left_chat_member = create(lambda _, m: bool(m.left_chat_member), "LeftChatMemberFilter") """Filter service messages for members that left the chat.""" - new_chat_title = create("NewChatTitle", lambda _, m: bool(m.new_chat_title)) + new_chat_title = create(lambda _, m: bool(m.new_chat_title), "NewChatTitleFilter") """Filter service messages for new chat titles.""" - new_chat_photo = create("NewChatPhoto", lambda _, m: bool(m.new_chat_photo)) + new_chat_photo = create(lambda _, m: bool(m.new_chat_photo), "NewChatPhotoFilter") """Filter service messages for new chat photos.""" - delete_chat_photo = create("DeleteChatPhoto", lambda _, m: bool(m.delete_chat_photo)) + delete_chat_photo = create(lambda _, m: bool(m.delete_chat_photo), "DeleteChatPhotoFilter") """Filter service messages for deleted photos.""" - group_chat_created = create("GroupChatCreated", lambda _, m: bool(m.group_chat_created)) + group_chat_created = create(lambda _, m: bool(m.group_chat_created), "GroupChatCreatedFilter") """Filter service messages for group chat creations.""" - supergroup_chat_created = create("SupergroupChatCreated", lambda _, m: bool(m.supergroup_chat_created)) + supergroup_chat_created = create(lambda _, m: bool(m.supergroup_chat_created), "SupergroupChatCreatedFilter") """Filter service messages for supergroup chat creations.""" - channel_chat_created = create("ChannelChatCreated", lambda _, m: bool(m.channel_chat_created)) + channel_chat_created = create(lambda _, m: bool(m.channel_chat_created), "ChannelChatCreatedFilter") """Filter service messages for channel chat creations.""" - migrate_to_chat_id = create("MigrateToChatId", lambda _, m: bool(m.migrate_to_chat_id)) + migrate_to_chat_id = create(lambda _, m: bool(m.migrate_to_chat_id), "MigrateToChatIdFilter") """Filter service messages that contain migrate_to_chat_id.""" - migrate_from_chat_id = create("MigrateFromChatId", lambda _, m: bool(m.migrate_from_chat_id)) + migrate_from_chat_id = create(lambda _, m: bool(m.migrate_from_chat_id), "MigrateFromChatIdFilter") """Filter service messages that contain migrate_from_chat_id.""" - pinned_message = create("PinnedMessage", lambda _, m: bool(m.pinned_message)) + pinned_message = create(lambda _, m: bool(m.pinned_message), "PinnedMessageFilter") """Filter service messages for pinned messages.""" - game_high_score = create("GameHighScore", lambda _, m: bool(m.game_high_score)) + game_high_score = create(lambda _, m: bool(m.game_high_score), "GameHighScoreFilter") """Filter service messages for game high scores.""" - reply_keyboard = create("ReplyKeyboard", lambda _, m: isinstance(m.reply_markup, ReplyKeyboardMarkup)) + reply_keyboard = create(lambda _, m: isinstance(m.reply_markup, ReplyKeyboardMarkup), "ReplyKeyboardFilter") """Filter messages containing reply keyboard markups""" - inline_keyboard = create("InlineKeyboard", lambda _, m: isinstance(m.reply_markup, InlineKeyboardMarkup)) + inline_keyboard = create(lambda _, m: isinstance(m.reply_markup, InlineKeyboardMarkup), "InlineKeyboardFilter") """Filter messages containing inline keyboard markups""" - mentioned = create("Mentioned", lambda _, m: bool(m.mentioned)) + mentioned = create(lambda _, m: bool(m.mentioned), "MentionedFilter") """Filter messages containing mentions""" - via_bot = create("ViaBot", lambda _, m: bool(m.via_bot)) + via_bot = create(lambda _, m: bool(m.via_bot), "ViaBotFilter") """Filter messages sent via inline bots""" - service = create("Service", lambda _, m: bool(m.service)) + service = create(lambda _, m: bool(m.service), "ServiceFilter") """Filter service messages. - + A service message contains any of the following fields set: *left_chat_member*, *new_chat_title*, *new_chat_photo*, *delete_chat_photo*, *group_chat_created*, *supergroup_chat_created*, *channel_chat_created*, *migrate_to_chat_id*, *migrate_from_chat_id*, *pinned_message*, *game_score*. """ - media = create("Media", lambda _, m: bool(m.media)) + media = create(lambda _, m: bool(m.media), "MediaFilter") """Filter media messages. - + A media message contains any of the following fields set: *audio*, *document*, *photo*, *sticker*, *video*, *animation*, *voice*, *video_note*, *contact*, *location*, *venue*, *poll*. """ @@ -253,17 +256,17 @@ class Filters: commands = {c if case_sensitive else c.lower() for c in commands} prefixes = set(prefix) if prefix else {""} - return create("Command", func=func, c=commands, p=prefixes, s=separator, cs=case_sensitive) + return create(func, "CommandFilter", c=commands, p=prefixes, s=separator, cs=case_sensitive) @staticmethod def regex(pattern, flags: int = 0): - """Filter messages that match a given RegEx pattern. + """Filter message texts or captions that match a given regular expression pattern. Parameters: pattern (``str``): - The RegEx pattern as string, it will be applied to the text of a message. When a pattern matches, - all the `Match Objects `_ - are stored in the *matches* field of the :obj:`Message` itself. + The RegEx pattern as string, it will be applied to the text or the caption of a message. When a pattern + matches, all the `Match Objects `_ are stored + in the *matches* field of the :obj:`Message` itself. flags (``int``, *optional*): RegEx flags. @@ -273,7 +276,7 @@ class Filters: m.matches = [i for i in _.p.finditer(m.text or m.caption or "")] return bool(m.matches) - return create("Regex", f, p=re.compile(pattern, flags)) + return create(f, "RegexFilter", p=re.compile(pattern, flags)) # noinspection PyPep8Naming class user(Filter, set): @@ -285,7 +288,7 @@ class Filters: Parameters: users (``int`` | ``str`` | ``list``): Pass one or more user ids/usernames to filter users. - For you yourself, "me" or "self" can be used as well. + For you yourself, "me" or "self" can be used as well. Defaults to None (no users). """ @@ -342,12 +345,12 @@ class Filters: @staticmethod def callback_data(data: str or bytes): """Filter callback queries for their data. - + Parameters: data (``str`` | ``bytes``): Pass the data you want to filter for. """ - return create("CallbackData", lambda flt, cb: cb.data == flt.data, data=data) + return create(lambda flt, cb: cb.data == flt.data, "CallbackDataFilter", data=data) - dan = create("Dan", lambda _, m: bool(m.from_user and m.from_user.id == 23122162)) + dan = create(lambda _, m: bool(m.from_user and m.from_user.id == 23122162), "DanFilter")