diff --git a/README.rst b/README.rst index f7d2b44e..fcc6407a 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,8 @@ Pyrogram ======== + `A fully asynchronous variant is also available! `_ + .. code-block:: python from pyrogram import Client, Filters @@ -17,18 +19,20 @@ Pyrogram app.run() -**Pyrogram** is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for -building custom Telegram applications that interact with the MTProto API as both User and Bot. +**Pyrogram** is an elegant, easy-to-use Telegram_ client library and framework written from the ground up in Python and C. +It enables you to easily build custom Telegram applications that interact with the MTProto API as both user and bot. Features -------- -- **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. -- **High-level**: The low-level details of MTProto are abstracted and automatically handled. +- **Easy**: You can install Pyrogram with pip and start building your app right away. +- **Elegant**: Low-level details are abstracted and re-presented in a much nicer and easier way. - **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. -- **Updated** to the latest Telegram API version, currently Layer 91 on top of MTProto 2.0. -- **Documented**: The Pyrogram API is well documented and resembles the Telegram Bot API. -- **Full API**, allowing to execute any advanced action an official client is able to do, and more. +- **Documented**: Pyrogram API methods, types and public interfaces are well documented. +- **Type-hinted**: Exposed Pyrogram types and method parameters are all type-hinted. +- **Updated**, to the latest Telegram API version, currently Layer 91 on top of MTProto 2.0. +- **Pluggable**: The Smart Plugin system allows to write components with minimal boilerplate code. +- **Comprehensive**: Execute any advanced action an official client is able to do, and even more. Requirements ------------ @@ -47,7 +51,7 @@ Getting Started --------------- - The Docs contain lots of resources to help you getting started with Pyrogram: https://docs.pyrogram.ml. -- Reading Examples_ in this repository is also a good way for learning how things work. +- Reading `Examples in this repository`_ is also a good way for learning how Pyrogram works. - Seeking extra help? Don't be shy, come join and ask our Community_! - For other requests you can send an Email_ or a Message_. @@ -67,7 +71,7 @@ Copyright & License .. _`Telegram`: https://telegram.org/ .. _`Telegram API key`: https://docs.pyrogram.ml/start/ProjectSetup#api-keys .. _`Community`: https://t.me/PyrogramChat -.. _`Examples`: https://github.com/pyrogram/pyrogram/tree/master/examples +.. _`Examples in this repository`: https://github.com/pyrogram/pyrogram/tree/master/examples .. _`GitHub`: https://github.com/pyrogram/pyrogram/issues .. _`Email`: admin@pyrogram.ml .. _`Message`: https://t.me/haskell @@ -83,17 +87,17 @@ Copyright & License

- Telegram MTProto API Client Library for Python + Telegram MTProto API Framework for Python
- - Download - - • Documentation • + + Changelog + + • Community @@ -104,7 +108,7 @@ Copyright & License TgCrypto + alt="TgCrypto Version">

@@ -112,12 +116,12 @@ Copyright & License :target: https://pyrogram.ml :alt: Pyrogram -.. |description| replace:: **Telegram MTProto API Client Library for Python** +.. |description| replace:: **Telegram MTProto API Framework for Python** -.. |scheme| image:: "https://img.shields.io/badge/schema-layer%2091-eda738.svg?longCache=true&colorA=262b30" +.. |schema| image:: "https://img.shields.io/badge/schema-layer%2091-eda738.svg?longCache=true&colorA=262b30" :target: compiler/api/source/main_api.tl - :alt: Scheme Layer + :alt: Schema Layer .. |tgcrypto| image:: "https://img.shields.io/badge/tgcrypto-v1.1.1-eda738.svg?longCache=true&colorA=262b30" :target: https://github.com/pyrogram/tgcrypto - :alt: TgCrypto + :alt: TgCrypto Version diff --git a/compiler/error/source/400_BAD_REQUEST.tsv b/compiler/error/source/400_BAD_REQUEST.tsv index 0f174c66..82474096 100644 --- a/compiler/error/source/400_BAD_REQUEST.tsv +++ b/compiler/error/source/400_BAD_REQUEST.tsv @@ -62,7 +62,7 @@ USER_IS_BOT A bot cannot send messages to other bots or to itself WEBPAGE_CURL_FAILED Telegram server could not fetch the provided URL STICKERSET_INVALID The requested sticker set is invalid PEER_FLOOD The method can't be used because your account is limited -MEDIA_CAPTION_TOO_LONG The media caption is longer than 200 characters +MEDIA_CAPTION_TOO_LONG The media caption is longer than 1024 characters USER_NOT_MUTUAL_CONTACT The user is not a mutual contact USER_CHANNELS_TOO_MUCH The user is already in too many channels or supergroups API_ID_PUBLISHED_FLOOD You are using an API key that is limited on the server side @@ -86,4 +86,4 @@ TAKEOUT_REQUIRED The method must be invoked inside a takeout session MESSAGE_POLL_CLOSED You can't interact with a closed poll MEDIA_INVALID The media is invalid BOT_SCORE_NOT_MODIFIED The bot score was not modified -USER_BOT_REQUIRED The method can be used by bots only \ No newline at end of file +USER_BOT_REQUIRED The method can be used by bots only diff --git a/docs/source/index.rst b/docs/source/index.rst index ca9a38a3..067e6fbf 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,27 +10,28 @@ Welcome to Pyrogram

- Telegram MTProto API Client Library for Python + Telegram MTProto API Framework for Python +
- - Download + + Documentation • - - Source code + + Changelog Community
- + Scheme Layer + alt="Schema Layer"> TgCrypto + alt="TgCrypto Version">

@@ -48,25 +49,27 @@ Welcome to Pyrogram app.run() -Welcome to Pyrogram's Documentation! Here you can find resources for learning how to use the library. +Welcome to Pyrogram's Documentation! Here you can find resources for learning how to use the framework. Contents are organized into self-contained topics and can be accessed from the sidebar, or by following them in order using the Next button at the end of each page. But first, here's a brief overview of what is this all about. About ----- -**Pyrogram** is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for -building custom Telegram applications that interact with the MTProto API as both User and Bot. +**Pyrogram** is an elegant, easy-to-use Telegram_ client library and framework written from the ground up in Python and +C. It enables you to easily build custom Telegram applications that interact with the MTProto API as both user and bot. Features -------- -- **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. -- **High-level**: The low-level details of MTProto are abstracted and automatically handled. +- **Easy**: You can install Pyrogram with pip and start building your app right away. +- **Elegant**: Low-level details are abstracted and re-presented in a much nicer and easier way. - **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. -- **Updated** to the latest Telegram API version, currently Layer 91 on top of MTProto 2.0. -- **Documented**: The Pyrogram API is well documented and resembles the Telegram Bot API. -- **Full API**, allowing to execute any advanced action an official client is able to do, and more. +- **Documented**: Pyrogram API methods, types and public interfaces are well documented. +- **Type-hinted**: Exposed Pyrogram types and method parameters are all type-hinted. +- **Updated**, to the latest Telegram API version, currently Layer 91 on top of MTProto 2.0. +- **Pluggable**: The Smart Plugin system allows to write components with minimal boilerplate code. +- **Comprehensive**: Execute any advanced action an official client is able to do, and even more. To get started, press the Next button. @@ -85,6 +88,7 @@ To get started, press the Next button. resources/UpdateHandling resources/UsingFilters resources/MoreOnUpdates + resources/ConfigurationFile resources/SmartPlugins resources/AutoAuthorization resources/CustomizeSessions diff --git a/docs/source/resources/ConfigurationFile.rst b/docs/source/resources/ConfigurationFile.rst new file mode 100644 index 00000000..759bfd9f --- /dev/null +++ b/docs/source/resources/ConfigurationFile.rst @@ -0,0 +1,90 @@ +Configuration File +================== + +As already mentioned in previous sections, Pyrogram can also be configured by the use of an INI file. +This page explains how this file is structured in Pyrogram, how to use it and why. + +Introduction +------------ + +The idea behind using a configuration file is to help keeping your code free of settings (private) information such as +the API Key and Proxy without having you to even deal with how to load such settings. The configuration file, usually +referred as ``config.ini`` file, is automatically loaded from the root of your working directory; all you need to do is +fill in the necessary parts. + +.. note:: + + The configuration file is optional, but recommended. If, for any reason, you prefer not to use it, there's always an + alternative way to configure Pyrogram via Client's parameters. Doing so, you can have full control on how to store + and load your settings (e.g.: from environment variables). + + Settings specified via Client's parameter have higher priority and will override any setting stored in the + configuration file. + + +The config.ini File +------------------- + +By default, Pyrogram will look for a file named ``config.ini`` placed at the root of your working directory, that is, +the same folder of your running script. You can change the name or location of your configuration file by specifying it +in your Client's parameter *config_file*. + +- Replace the default *config.ini* file with *my_configuration.ini*: + + .. code-block:: python + + from pyrogram import Client + + app = Client("my_account", config_file="my_configuration.ini") + + +Configuration Sections +---------------------- + +These are all the sections Pyrogram uses in its configuration file: + +Pyrogram +^^^^^^^^ + +The ``[pyrogram]`` section contains your Telegram API credentials *api_id* and *api_hash*. + +.. code-block:: ini + + [pyrogram] + api_id = 12345 + api_hash = 0123456789abcdef0123456789abcdef + +`More info about API Key. <../start/Setup.html#configuration>`_ + +Proxy +^^^^^ + +The ``[proxy]`` section contains settings about your SOCKS5 proxy. + +.. code-block:: ini + + [proxy] + enabled = True + hostname = 11.22.33.44 + port = 1080 + username = + password = + +`More info about SOCKS5 Proxy. `_ + +Plugins +^^^^^^^ + +The ``[plugins]`` section contains settings about Smart Plugins. + +.. code-block:: ini + + [plugins] + root = plugins + include = + module + folder.module + exclude = + module fn2 + +`More info about Smart Plugins. `_ diff --git a/docs/source/resources/SmartPlugins.rst b/docs/source/resources/SmartPlugins.rst index 46c4e17a..972efdd8 100644 --- a/docs/source/resources/SmartPlugins.rst +++ b/docs/source/resources/SmartPlugins.rst @@ -1,9 +1,9 @@ Smart Plugins ============= -Pyrogram embeds a **smart** (automatic) and lightweight plugin system that is meant to further simplify the organization -of large projects and to provide a way for creating pluggable components that can be **easily shared** across different -Pyrogram applications with **minimal boilerplate code**. +Pyrogram embeds a **smart**, lightweight yet powerful plugin system that is meant to further simplify the organization +of large projects and to provide a way for creating pluggable (modular) components that can be **easily shared** across +different Pyrogram applications with **minimal boilerplate code**. .. tip:: @@ -13,7 +13,8 @@ Introduction ------------ Prior to the Smart Plugin system, pluggable handlers were already possible. For example, if you wanted to modularize -your applications, you had to do something like this... +your applications, you had to put your function definitions in separate files and register them inside your main script, +like this: .. note:: @@ -63,19 +64,19 @@ your applications, you had to do something like this... app.run() -...which is already nice and doesn't add *too much* boilerplate code, but things can get boring still; you have to +This is already nice and doesn't add *too much* boilerplate code, but things can get boring still; you have to manually ``import``, manually :meth:`add_handler ` and manually instantiate each :obj:`MessageHandler ` object because **you can't use those cool decorators** for your -functions. So... What if you could? +functions. So, what if you could? Smart Plugins solve this issue by taking care of handlers registration automatically. Using Smart Plugins ------------------- -Setting up your Pyrogram project to accommodate Smart Plugins is pretty straightforward: +Setting up your Pyrogram project to accommodate Smart Plugins is straightforward: -#. Create a new folder to store all the plugins (e.g.: "plugins"). -#. Put your files full of plugins inside. -#. Enable plugins in your Client. +#. Create a new folder to store all the plugins (e.g.: "plugins", "handlers", ...). +#. Put your python files full of plugins inside. Organize them as you wish. +#. Enable plugins in your Client or via the *config.ini* file. .. note:: @@ -107,20 +108,252 @@ Setting up your Pyrogram project to accommodate Smart Plugins is pretty straight def echo_reversed(client, message): message.reply(message.text[::-1]) +- ``config.ini`` + + .. code-block:: ini + + [plugins] + root = plugins + - ``main.py`` .. code-block:: python from pyrogram import Client - Client("my_account", plugins_dir="plugins").run() + Client("my_account").run() -The first important thing to note is the new ``plugins`` folder, whose name is passed to the the ``plugins_dir`` -parameter when creating a :obj:`Client ` in the ``main.py`` file — you can put *any python file* in -there and each file can contain *any decorated function* (handlers) with only one limitation: within a single plugin -file you must use different names for each decorated function. Your Pyrogram Client instance will **automatically** -scan the folder upon creation to search for valid handlers and register them for you. + Alternatively, without using the *config.ini* file: + + .. code-block:: python + + from pyrogram import Client + + plugins = dict( + root="plugins" + ) + + Client("my_account", plugins=plugins).run() + +The first important thing to note is the new ``plugins`` folder. You can put *any python file* in *any subfolder* and +each file can contain *any decorated function* (handlers) with one limitation: within a single module (file) you must +use different names for each decorated function. + +The second thing is telling Pyrogram where to look for your plugins: you can either use the *config.ini* file or +the Client parameter "plugins"; the *root* value must match the name of your plugins folder. Your Pyrogram Client +instance will **automatically** scan the folder upon starting to search for valid handlers and register them for you. Then you'll notice you can now use decorators. That's right, you can apply the usual decorators to your callback functions in a static way, i.e. **without having the Client instance around**: simply use ``@Client`` (Client class) -instead of the usual ``@app`` (Client instance) namespace and things will work just the same. +instead of the usual ``@app`` (Client instance) and things will work just the same. + +Specifying the Plugins to include +--------------------------------- + +By default, if you don't explicitly supply a list of plugins, every valid one found inside your plugins root folder will +be included by following the alphabetical order of the directory structure (files and subfolders); the single handlers +found inside each module will be, instead, loaded in the order they are defined, from top to bottom. + +.. note:: + + Remember: there can be at most one handler, within a group, dealing with a specific update. Plugins with overlapping + filters included a second time will not work. Learn more at `More on Updates `_. + +This default loading behaviour is usually enough, but sometimes you want to have more control on what to include (or +exclude) and in which exact order to load plugins. The way to do this is to make use of ``include`` and ``exclude`` +keys, either in the *config.ini* file or in the dictionary passed as Client argument. Here's how they work: + +- If both ``include`` and ``exclude`` are omitted, all plugins are loaded as described above. +- If ``include`` is given, only the specified plugins will be loaded, in the order they are passed. +- If ``exclude`` is given, the plugins specified here will be unloaded. + +The ``include`` and ``exclude`` value is a **list of strings**. Each string containing the path of the module relative +to the plugins root folder, in Python notation (dots instead of slashes). + + E.g.: ``subfolder.module`` refers to ``plugins/subfolder/module.py``, with ``root="plugins"``. + +You can also choose the order in which the single handlers inside a module are loaded, thus overriding the default +top-to-bottom loading policy. You can do this by appending the name of the functions to the module path, each one +separated by a blank space. + + E.g.: ``subfolder.module fn2 fn1 fn3`` will load *fn2*, *fn1* and *fn3* from *subfolder.module*, in this order. + +Examples +^^^^^^^^ + +Given this plugins folder structure with three modules, each containing their own handlers (fn1, fn2, etc...), which are +also organized in subfolders: + +.. code-block:: text + + myproject/ + plugins/ + subfolder1/ + plugins1.py + - fn1 + - fn2 + - fn3 + subfolder2/ + plugins2.py + ... + plugins0.py + ... + ... + +- Load every handler from every module, namely *plugins0.py*, *plugins1.py* and *plugins2.py* in alphabetical order + (files) and definition order (handlers inside files): + + Using *config.ini* file: + + .. code-block:: ini + + [plugins] + root = plugins + + Using *Client*'s parameter: + + .. code-block:: python + + plugins = dict( + root="plugins" + ) + + Client("my_account", plugins=plugins).run() + +- Load only handlers defined inside *plugins2.py* and *plugins0.py*, in this order: + + Using *config.ini* file: + + .. code-block:: ini + + [plugins] + root = plugins + include = + subfolder2.plugins2 + plugins0 + + Using *Client*'s parameter: + + .. code-block:: python + + plugins = dict( + root="plugins", + include=[ + "subfolder2.plugins2", + "plugins0" + ] + ) + + Client("my_account", plugins=plugins).run() + +- Load everything except the handlers inside *plugins2.py*: + + Using *config.ini* file: + + .. code-block:: ini + + [plugins] + root = plugins + exclude = subfolder2.plugins2 + + Using *Client*'s parameter: + + .. code-block:: python + + plugins = dict( + root="plugins", + exclude=["subfolder2.plugins2"] + ) + + Client("my_account", plugins=plugins).run() + +- Load only *fn3*, *fn1* and *fn2* (in this order) from *plugins1.py*: + + Using *config.ini* file: + + .. code-block:: ini + + [plugins] + root = plugins + include = subfolder1.plugins1 fn3 fn1 fn2 + + Using *Client*'s parameter: + + .. code-block:: python + + plugins = dict( + root="plugins", + include=["subfolder1.plugins1 fn3 fn1 fn2"] + ) + + Client("my_account", plugins=plugins).run() + +Load/Unload Plugins at Runtime +------------------------------ + +In the `previous section <#specifying-the-plugins-to-include>`_ we've explained how to specify which plugins to load and +which to ignore before your Client starts. Here we'll show, instead, how to unload and load again a previously +registered plugins at runtime. + +Each function decorated with the usual ``on_message`` decorator (or any other decorator that deals with Telegram updates +) will be modified in such a way that, when you reference them later on, they will be actually pointing to a tuple of +*(handler: Handler, group: int)*. The actual callback function is therefore stored inside the handler's *callback* +attribute. Here's an example: + +- ``plugins/handlers.py`` + + .. code-block:: python + :emphasize-lines: 5, 6 + + @Client.on_message(Filters.text & Filters.private) + def echo(client, message): + message.reply(message.text) + + print(echo) + print(echo[0].callback) + +- Printing ``echo`` will show something like ``(, 0)``. + +- Printing ``echo[0].callback``, that is, the *callback* attribute of the first eleent of the tuple, which is an + Handler, will reveal the actual callback ````. + +Unloading +^^^^^^^^^ + +In order to unload a plugin, or any other handler, all you need to do is obtain a reference to it (by importing the +relevant module) and call :meth:`remove_handler ` Client's method with your function +name preceded by the star ``*`` operator as argument. Example: + +- ``main.py`` + + .. code-block:: python + + from plugins.handlers import echo + + ... + + app.remove_handler(*echo) + +The star ``*`` operator is used to unpack the tuple into positional arguments so that *remove_handler* will receive +exactly what is needed. The same could have been achieved with: + +.. code-block:: python + + handler, group = echo + app.remove_handler(handler, group) + +Loading +^^^^^^^ + +Similarly to the unloading process, in order to load again a previously unloaded plugin you do the same, but this time +using :meth:`add_handler ` instead. Example: + +- ``main.py`` + + .. code-block:: python + + from plugins.handlers import echo + + ... + + app.add_handler(*echo) \ No newline at end of file diff --git a/docs/source/resources/TgCrypto.rst b/docs/source/resources/TgCrypto.rst index 734c48e4..2af09a06 100644 --- a/docs/source/resources/TgCrypto.rst +++ b/docs/source/resources/TgCrypto.rst @@ -1,5 +1,5 @@ -TgCrypto -======== +Fast Crypto +=========== Pyrogram's speed can be *dramatically* boosted up by TgCrypto_, a high-performance, easy-to-install Telegram Crypto Library specifically written in C for Pyrogram [#f1]_ as a Python extension. diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index 732630c4..fb505031 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -159,10 +159,8 @@ class Client(Methods, BaseClient): config_file (``str``, *optional*): Path of the configuration file. Defaults to ./config.ini - plugins_dir (``str``, *optional*): - Define a custom directory for your plugins. The plugins directory is the location in your - filesystem where Pyrogram will automatically load your update handlers. - Defaults to None (plugins disabled). + plugins (``dict``, *optional*): + TODO: doctrings no_updates (``bool``, *optional*): Pass True to completely disable incoming updates for the current session. @@ -199,7 +197,7 @@ class Client(Methods, BaseClient): workers: int = BaseClient.WORKERS, workdir: str = BaseClient.WORKDIR, config_file: str = BaseClient.CONFIG_FILE, - plugins_dir: str = None, + plugins: dict = None, no_updates: bool = None, takeout: bool = None): super().__init__() @@ -225,7 +223,7 @@ class Client(Methods, BaseClient): self.workers = workers self.workdir = workdir self.config_file = config_file - self.plugins_dir = plugins_dir + self.plugins = plugins self.no_updates = no_updates self.takeout = takeout @@ -243,7 +241,14 @@ class Client(Methods, BaseClient): @proxy.setter def proxy(self, value): - self._proxy["enabled"] = True + if value is None: + self._proxy = None + return + + if self._proxy is None: + self._proxy = {} + + self._proxy["enabled"] = bool(value.get("enabled", True)) self._proxy.update(value) async def start(self): @@ -1056,17 +1061,41 @@ class Client(Methods, BaseClient): setattr(self, option, getattr(Client, option.upper())) if self._proxy: - self._proxy["enabled"] = True + self._proxy["enabled"] = bool(self._proxy.get("enabled", True)) else: self._proxy = {} if parser.has_section("proxy"): - self._proxy["enabled"] = parser.getboolean("proxy", "enabled") + self._proxy["enabled"] = parser.getboolean("proxy", "enabled", fallback=True) self._proxy["hostname"] = parser.get("proxy", "hostname") self._proxy["port"] = parser.getint("proxy", "port") self._proxy["username"] = parser.get("proxy", "username", fallback=None) or None self._proxy["password"] = parser.get("proxy", "password", fallback=None) or None + if self.plugins: + self.plugins["enabled"] = bool(self.plugins.get("enabled", True)) + self.plugins["include"] = "\n".join(self.plugins.get("include", [])) or None + self.plugins["exclude"] = "\n".join(self.plugins.get("exclude", [])) or None + else: + try: + section = parser["plugins"] + + self.plugins = { + "enabled": section.getboolean("enabled", True), + "root": section.get("root"), + "include": section.get("include") or None, + "exclude": section.get("exclude") or None + } + except KeyError: + pass + + for option in ["include", "exclude"]: + if self.plugins[option] is not None: + self.plugins[option] = [ + (i.split()[0], i.split()[1:] or None) + for i in self.plugins[option].strip().split("\n") + ] + async def load_session(self): try: with open(os.path.join(self.workdir, "{}.session".format(self.session_name)), encoding="utf-8") as f: @@ -1098,43 +1127,108 @@ class Client(Methods, BaseClient): self.peers_by_phone[k] = peer def load_plugins(self): - if self.plugins_dir is not None: - plugins_count = 0 + if self.plugins.get("enabled", False): + root = self.plugins["root"] + include = self.plugins["include"] + exclude = self.plugins["exclude"] - for path in Path(self.plugins_dir).rglob("*.py"): - file_path = os.path.splitext(str(path))[0] - import_path = [] + count = 0 - while file_path: - file_path, tail = os.path.split(file_path) - import_path.insert(0, tail) + if include is None: + for path in sorted(Path(root).rglob("*.py")): + module_path = os.path.splitext(str(path))[0].replace("/", ".") + module = import_module(module_path) - import_path = ".".join(import_path) - module = import_module(import_path) + for name in vars(module).keys(): + # noinspection PyBroadException + try: + handler, group = getattr(module, name) - for name in dir(module): - # noinspection PyBroadException - try: - handler, group = getattr(module, name) + if isinstance(handler, Handler) and isinstance(group, int): + self.add_handler(handler, group) - if isinstance(handler, Handler) and isinstance(group, int): - self.add_handler(handler, group) + log.info('[LOAD] {}("{}") in group {} from "{}"'.format( + type(handler).__name__, name, group, module_path)) - log.info('{}("{}") from "{}" loaded in group {}'.format( - type(handler).__name__, name, import_path, group)) - - plugins_count += 1 - except Exception: - pass - - if plugins_count > 0: - log.warning('Successfully loaded {} plugin{} from "{}"'.format( - plugins_count, - "s" if plugins_count > 1 else "", - self.plugins_dir - )) + count += 1 + except Exception: + pass else: - log.warning('No plugin loaded: "{}" doesn\'t contain any valid plugin'.format(self.plugins_dir)) + for path, handlers in include: + module_path = root + "." + path + warn_non_existent_functions = True + + try: + module = import_module(module_path) + except ModuleNotFoundError: + log.warning('[LOAD] Ignoring non-existent module "{}"'.format(module_path)) + continue + + if "__path__" in dir(module): + log.warning('[LOAD] Ignoring namespace "{}"'.format(module_path)) + continue + + if handlers is None: + handlers = vars(module).keys() + warn_non_existent_functions = False + + for name in handlers: + # noinspection PyBroadException + try: + handler, group = getattr(module, name) + + if isinstance(handler, Handler) and isinstance(group, int): + self.add_handler(handler, group) + + log.info('[LOAD] {}("{}") in group {} from "{}"'.format( + type(handler).__name__, name, group, module_path)) + + count += 1 + except Exception: + if warn_non_existent_functions: + log.warning('[LOAD] Ignoring non-existent function "{}" from "{}"'.format( + name, module_path)) + + if exclude is not None: + for path, handlers in exclude: + module_path = root + "." + path + warn_non_existent_functions = True + + try: + module = import_module(module_path) + except ModuleNotFoundError: + log.warning('[UNLOAD] Ignoring non-existent module "{}"'.format(module_path)) + continue + + if "__path__" in dir(module): + log.warning('[UNLOAD] Ignoring namespace "{}"'.format(module_path)) + continue + + if handlers is None: + handlers = vars(module).keys() + warn_non_existent_functions = False + + for name in handlers: + # noinspection PyBroadException + try: + handler, group = getattr(module, name) + + if isinstance(handler, Handler) and isinstance(group, int): + self.remove_handler(handler, group) + + log.info('[UNLOAD] {}("{}") from group {} in "{}"'.format( + type(handler).__name__, name, group, module_path)) + + count -= 1 + except Exception: + if warn_non_existent_functions: + log.warning('[UNLOAD] Ignoring non-existent function "{}" from "{}"'.format( + name, module_path)) + + if count > 0: + log.warning('Successfully loaded {} plugin{} from "{}"'.format(count, "s" if count > 1 else "", root)) + else: + log.warning('No plugin loaded from "{}"'.format(root)) def save_session(self): auth_key = base64.b64encode(self.auth_key).decode() diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index d33b5bb9..80b719af 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -125,3 +125,6 @@ class BaseClient: async def get_chat_members(self, *args, **kwargs): pass + + def get_chat_members_count(self, *args, **kwargs): + pass diff --git a/pyrogram/client/methods/chats/get_chat_member.py b/pyrogram/client/methods/chats/get_chat_member.py index a8af8dd1..6e86aaf6 100644 --- a/pyrogram/client/methods/chats/get_chat_member.py +++ b/pyrogram/client/methods/chats/get_chat_member.py @@ -67,6 +67,8 @@ class GetChatMember(BaseClient): ) ) - return pyrogram.ChatMember._parse(self, r.participant, r.users[0]) + users = {i.id: i for i in r.users} + + return pyrogram.ChatMember._parse(self, r.participant, users) else: raise ValueError("The chat_id \"{}\" belongs to a user".format(chat_id)) diff --git a/pyrogram/client/methods/chats/iter_chat_members.py b/pyrogram/client/methods/chats/iter_chat_members.py index c37e9785..d1ac8d6a 100644 --- a/pyrogram/client/methods/chats/iter_chat_members.py +++ b/pyrogram/client/methods/chats/iter_chat_members.py @@ -84,9 +84,14 @@ class IterChatMembers(BaseClient): yielded = set() queries = [query] if query else QUERIES total = limit or (1 << 31) - 1 - filter = Filters.RECENT if total <= 10000 and filter == Filters.ALL else filter limit = min(200, total) + filter = ( + Filters.RECENT + if self.get_chat_members_count(chat_id) <= 10000 and filter == Filters.ALL + else filter + ) + if filter not in QUERYABLE_FILTERS: queries = [""] diff --git a/pyrogram/client/types/user_and_chats/chat_member.py b/pyrogram/client/types/user_and_chats/chat_member.py index e901e0e1..70f32540 100644 --- a/pyrogram/client/types/user_and_chats/chat_member.py +++ b/pyrogram/client/types/user_and_chats/chat_member.py @@ -33,6 +33,19 @@ class ChatMember(PyrogramType): The member's status in the chat. Can be "creator", "administrator", "member", "restricted", "left" or "kicked". + date (``int``, *optional*): + Date when the user joined, unix time. Not available for creator. + + invited_by (:obj:`User `, *optional*): + Administrators and self member only. Information about the user who invited this member. + In case the user joined by himself this will be the same as "user". + + promoted_by (:obj:`User `, *optional*): + Administrators only. Information about the user who promoted this member as administrator. + + restricted_by (:obj:`User `, *optional*): + Restricted and kicked only. Information about the user who restricted or kicked this member. + until_date (``int``, *optional*): Restricted and kicked only. Date when restrictions will be lifted for this user, unix time. @@ -86,6 +99,10 @@ class ChatMember(PyrogramType): client: "pyrogram.client.ext.BaseClient", user: "pyrogram.User", status: str, + date: int = None, + invited_by: "pyrogram.User" = None, + promoted_by: "pyrogram.User" = None, + restricted_by: "pyrogram.User" = None, until_date: int = None, can_be_edited: bool = None, can_change_info: bool = None, @@ -104,6 +121,10 @@ class ChatMember(PyrogramType): self.user = user self.status = status + self.date = date + self.invited_by = invited_by + self.promoted_by = promoted_by + self.restricted_by = restricted_by self.until_date = until_date self.can_be_edited = can_be_edited self.can_change_info = can_change_info @@ -120,17 +141,18 @@ class ChatMember(PyrogramType): self.can_add_web_page_previews = can_add_web_page_previews @staticmethod - def _parse(client, member, user) -> "ChatMember": - user = pyrogram.User._parse(client, user) + def _parse(client, member, users) -> "ChatMember": + user = pyrogram.User._parse(client, users[member.user_id]) + invited_by = pyrogram.User._parse(client, users[member.inviter_id]) if hasattr(member, "inviter_id") else None if isinstance(member, (types.ChannelParticipant, types.ChannelParticipantSelf, types.ChatParticipant)): - return ChatMember(user=user, status="member", client=client) + return ChatMember(user=user, status="member", date=member.date, invited_by=invited_by, client=client) if isinstance(member, (types.ChannelParticipantCreator, types.ChatParticipantCreator)): return ChatMember(user=user, status="creator", client=client) if isinstance(member, types.ChatParticipantAdmin): - return ChatMember(user=user, status="administrator", client=client) + return ChatMember(user=user, status="administrator", date=member.date, invited_by=invited_by, client=client) if isinstance(member, types.ChannelParticipantAdmin): rights = member.admin_rights @@ -138,6 +160,9 @@ class ChatMember(PyrogramType): return ChatMember( user=user, status="administrator", + date=member.date, + invited_by=invited_by, + promoted_by=pyrogram.User._parse(client, users[member.promoted_by]), can_be_edited=member.can_edit, can_change_info=rights.change_info, can_post_messages=rights.post_messages, @@ -155,7 +180,13 @@ class ChatMember(PyrogramType): chat_member = ChatMember( user=user, - status="kicked" if rights.view_messages else "restricted", + status=( + "kicked" if rights.view_messages + else "left" if member.left + else "restricted" + ), + date=member.date, + restricted_by=pyrogram.User._parse(client, users[member.kicked_by]), until_date=0 if rights.until_date == (1 << 31) - 1 else rights.until_date, client=client ) diff --git a/pyrogram/client/types/user_and_chats/chat_members.py b/pyrogram/client/types/user_and_chats/chat_members.py index 88219514..39d69089 100644 --- a/pyrogram/client/types/user_and_chats/chat_members.py +++ b/pyrogram/client/types/user_and_chats/chat_members.py @@ -59,7 +59,7 @@ class ChatMembers(PyrogramType): total_count = len(members) for member in members: - chat_members.append(ChatMember._parse(client, member, users[member.user_id])) + chat_members.append(ChatMember._parse(client, member, users)) return ChatMembers( total_count=total_count,