Merge branch 'plugins' into develop

This commit is contained in:
Dan 2018-10-16 12:12:21 +02:00
commit 9e159a3f50
9 changed files with 214 additions and 13 deletions

View File

@ -84,6 +84,7 @@ To get started, press the Next button.
resources/UpdateHandling
resources/UsingFilters
resources/Plugins
resources/AutoAuthorization
resources/CustomizeSessions
resources/TgCrypto

View File

@ -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 <pyrogram.Client.add_handler>` and manually instantiate each
:obj:`MessageHandler <pyrogram.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 <pyrogram.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.

View File

@ -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)]

View File

@ -52,6 +52,7 @@ class BaseClient:
WORKERS = 4
WORKDIR = "."
CONFIG_FILE = "./config.ini"
PLUGINS_DIR = "./plugins"
MEDIA_TYPE_ID = {
0: "thumbnail",

View File

@ -17,6 +17,7 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -17,6 +17,7 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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

View File

@ -17,11 +17,12 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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