diff --git a/docs/source/index.rst b/docs/source/index.rst index 94a14228..6210ff05 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -84,6 +84,7 @@ To get started, press the Next button. resources/UpdateHandling resources/UsingFilters + resources/Plugins resources/AutoAuthorization resources/CustomizeSessions resources/TgCrypto diff --git a/docs/source/resources/Plugins.rst b/docs/source/resources/Plugins.rst new file mode 100644 index 00000000..d490b37b --- /dev/null +++ b/docs/source/resources/Plugins.rst @@ -0,0 +1,116 @@ +Plugins +======= + +Pyrogram embeds an **automatic** and lightweight plugin system that is meant to greatly 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**. + +Introduction +------------ + +Prior to the plugin system, pluggable handlers were already possible. For instance, if you wanted to modularize your +applications, you had to do something like this... + + .. note:: This is an example application that replies in private chats with two messages: one containing the same + text message you sent and the other containing the reversed text message (e.g.: "pyrogram" -> "pyrogram" and + "margoryp"): + + .. code-block:: text + + myproject/ + config.ini + handlers.py + main.py + + - ``handlers.py`` + + .. code-block:: python + + def echo(client, message): + message.reply(message.text) + + + def echo_reversed(client, message): + message.reply(message.text[::-1]) + + - ``main.py`` + + .. code-block:: python + + from pyrogram import Client, MessageHandler, Filters + + from handlers import echo, echo_reversed + + app = Client("my_account") + + app.add_handler( + MessageHandler( + echo, + Filters.text & Filters.private)) + + app.add_handler( + MessageHandler( + echo_reversed, + Filters.text & Filters.private), + group=1) + + app.run() + +...which 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? + +Creating Plugins +---------------- + +Setting up your Pyrogram project to accommodate plugins is as easy as creating a folder and putting your files full of +handlers inside. + + .. note:: This is the same example application `as shown above <#introduction>`_, written using the plugin system. + + .. code-block:: text + :emphasize-lines: 2, 3 + + myproject/ + plugins/ + handlers.py + config.ini + main.py + + - ``plugins/handlers.py`` + + .. code-block:: python + :emphasize-lines: 4, 9 + + from pyrogram import Client, Filters + + + @Client.on_message(Filters.text & Filters.private) + def echo(client, message): + message.reply(message.text) + + + @Client.on_message(Filters.text & Filters.private, group=1) + def echo_reversed(client, message): + message.reply(message.text[::-1]) + + - ``main.py`` + + .. code-block:: python + + from pyrogram import Client + + Client("my_account").run() + +The first important thing to note is the ``plugins`` folder, whose name is default and can be changed easily by setting +the ``plugins_dir`` parameter when creating a :obj:`Client `; you can put any python file in the +plugins folder and each file can contain any decorated function (handlers). Your Pyrogram Client instance (in the +``main.py`` file) will **automatically** scan the folder upon creation 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. + +The ``main.py`` script is now at its bare minimum and cleanest state. diff --git a/pyrogram/client/client.py b/pyrogram/client/client.py index c607207f..b19bb486 100644 --- a/pyrogram/client/client.py +++ b/pyrogram/client/client.py @@ -33,6 +33,7 @@ import time from configparser import ConfigParser from datetime import datetime from hashlib import sha256, md5 +from importlib import import_module from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Thread @@ -45,6 +46,7 @@ from pyrogram.api.errors import ( PasswordHashInvalid, FloodWait, PeerIdInvalid, FirstnameInvalid, PhoneNumberBanned, VolumeLocNotFound, UserMigrate, FileIdInvalid, ChannelPrivate) from pyrogram.client.handlers import DisconnectHandler +from pyrogram.client.handlers.handler import Handler from pyrogram.crypto import AES from pyrogram.session import Auth, Session from .dispatcher import Dispatcher @@ -140,6 +142,11 @@ 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 "./plugins". Set to None to completely disable plugins. """ def __init__(self, @@ -161,7 +168,8 @@ class Client(Methods, BaseClient): last_name: str = None, workers: int = BaseClient.WORKERS, workdir: str = BaseClient.WORKDIR, - config_file: str = BaseClient.CONFIG_FILE): + config_file: str = BaseClient.CONFIG_FILE, + plugins_dir: str or None = BaseClient.PLUGINS_DIR): super().__init__() self.session_name = session_name @@ -184,6 +192,7 @@ class Client(Methods, BaseClient): self.workers = workers self.workdir = workdir self.config_file = config_file + self.plugins_dir = plugins_dir self.dispatcher = Dispatcher(self, workers) @@ -219,6 +228,7 @@ class Client(Methods, BaseClient): self.load_config() self.load_session() + self.load_plugins() self.session = Session( self, @@ -968,6 +978,44 @@ class Client(Methods, BaseClient): if peer: self.peers_by_phone[k] = peer + def load_plugins(self): + if self.plugins_dir is not None: + try: + dirs = os.listdir(self.plugins_dir) + except FileNotFoundError: + if self.plugins_dir == Client.PLUGINS_DIR: + log.info("No plugin loaded: default directory is missing") + else: + log.warning('No plugin loaded: "{}" directory is missing'.format(self.plugins_dir)) + else: + plugins_dir = self.plugins_dir.lstrip("./").replace("/", ".") + plugins_count = 0 + + for i in dirs: + module = import_module("{}.{}".format(plugins_dir, i.split(".")[0])) + + for j in dir(module): + # noinspection PyBroadException + try: + handler, group = getattr(module, j) + + if isinstance(handler, Handler) and isinstance(group, int): + self.add_handler(handler, group) + + log.info('{}("{}") from "{}/{}" loaded in group {}'.format( + type(handler).__name__, j, self.plugins_dir, i, group) + ) + + plugins_count += 1 + except Exception: + pass + + log.warning('Successfully loaded {} plugin{} from "{}"'.format( + plugins_count, + "s" if plugins_count > 1 else "", + self.plugins_dir + )) + def save_session(self): auth_key = base64.b64encode(self.auth_key).decode() auth_key = [auth_key[i: i + 43] for i in range(0, len(auth_key), 43)] diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index c18c4050..5801b4e8 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -52,6 +52,7 @@ class BaseClient: WORKERS = 4 WORKDIR = "." CONFIG_FILE = "./config.ini" + PLUGINS_DIR = "./plugins" MEDIA_TYPE_ID = { 0: "thumbnail", diff --git a/pyrogram/client/methods/decorators/on_callback_query.py b/pyrogram/client/methods/decorators/on_callback_query.py index 5f22fc92..8413515d 100644 --- a/pyrogram/client/methods/decorators/on_callback_query.py +++ b/pyrogram/client/methods/decorators/on_callback_query.py @@ -17,6 +17,7 @@ # along with Pyrogram. If not, see . import pyrogram +from pyrogram.client.filters.filter import Filter from ...ext import BaseClient @@ -36,7 +37,14 @@ class OnCallbackQuery(BaseClient): """ def decorator(func): - self.add_handler(pyrogram.CallbackQueryHandler(func, filters), group) - return func + handler = pyrogram.CallbackQueryHandler(func, filters) + + if isinstance(self, Filter): + return pyrogram.CallbackQueryHandler(func, self), group if filters is None else filters + + if self is not None: + self.add_handler(handler, group) + + return handler, group return decorator diff --git a/pyrogram/client/methods/decorators/on_deleted_messages.py b/pyrogram/client/methods/decorators/on_deleted_messages.py index 3f603c41..e4b2bc97 100644 --- a/pyrogram/client/methods/decorators/on_deleted_messages.py +++ b/pyrogram/client/methods/decorators/on_deleted_messages.py @@ -17,6 +17,7 @@ # along with Pyrogram. If not, see . import pyrogram +from pyrogram.client.filters.filter import Filter from ...ext import BaseClient @@ -36,7 +37,14 @@ class OnDeletedMessages(BaseClient): """ def decorator(func): - self.add_handler(pyrogram.DeletedMessagesHandler(func, filters), group) - return func + handler = pyrogram.DeletedMessagesHandler(func, filters) + + if isinstance(self, Filter): + return pyrogram.DeletedMessagesHandler(func, self), group if filters is None else filters + + if self is not None: + self.add_handler(handler, group) + + return handler, group return decorator diff --git a/pyrogram/client/methods/decorators/on_disconnect.py b/pyrogram/client/methods/decorators/on_disconnect.py index 4bc593e3..a639471b 100644 --- a/pyrogram/client/methods/decorators/on_disconnect.py +++ b/pyrogram/client/methods/decorators/on_disconnect.py @@ -28,7 +28,11 @@ class OnDisconnect(BaseClient): """ def decorator(func): - self.add_handler(pyrogram.DisconnectHandler(func)) - return func + handler = pyrogram.DisconnectHandler(func) + + if self is not None: + self.add_handler(handler) + + return handler return decorator diff --git a/pyrogram/client/methods/decorators/on_message.py b/pyrogram/client/methods/decorators/on_message.py index 0011e083..7a0d54a0 100644 --- a/pyrogram/client/methods/decorators/on_message.py +++ b/pyrogram/client/methods/decorators/on_message.py @@ -17,11 +17,12 @@ # along with Pyrogram. If not, see . import pyrogram +from pyrogram.client.filters.filter import Filter from ...ext import BaseClient class OnMessage(BaseClient): - def on_message(self, filters=None, group: int = 0): + def on_message(self=None, filters=None, group: int = 0): """Use this decorator to automatically register a function for handling messages. This does the same thing as :meth:`add_handler` using the :class:`MessageHandler`. @@ -36,7 +37,14 @@ class OnMessage(BaseClient): """ def decorator(func): - self.add_handler(pyrogram.MessageHandler(func, filters), group) - return func + handler = pyrogram.MessageHandler(func, filters) + + if isinstance(self, Filter): + return pyrogram.MessageHandler(func, self), group if filters is None else filters + + if self is not None: + self.add_handler(handler, group) + + return handler, group return decorator diff --git a/pyrogram/client/methods/decorators/on_raw_update.py b/pyrogram/client/methods/decorators/on_raw_update.py index 902d9854..7675a4f0 100644 --- a/pyrogram/client/methods/decorators/on_raw_update.py +++ b/pyrogram/client/methods/decorators/on_raw_update.py @@ -21,7 +21,7 @@ from ...ext import BaseClient class OnRawUpdate(BaseClient): - def on_raw_update(self, group: int = 0): + def on_raw_update(self=None, group: int = 0): """Use this decorator to automatically register a function for handling raw updates. This does the same thing as :meth:`add_handler` using the :class:`RawUpdateHandler`. @@ -32,7 +32,14 @@ class OnRawUpdate(BaseClient): """ def decorator(func): - self.add_handler(pyrogram.RawUpdateHandler(func), group) - return func + handler = pyrogram.RawUpdateHandler(func) + + if isinstance(self, int): + return handler, group if self is None else group + + if self is not None: + self.add_handler(handler, group) + + return handler, group return decorator