mirror of
https://github.com/pyrogram/pyrogram
synced 2025-09-18 13:55:15 +00:00
Merge branch 'develop' into session_storage
# Conflicts: # pyrogram/client/client.py # pyrogram/client/ext/base_client.py # pyrogram/client/ext/syncer.py # pyrogram/client/methods/contacts/get_contacts.py
This commit is contained in:
@@ -23,13 +23,11 @@ import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import warnings
|
||||
from configparser import ConfigParser
|
||||
from datetime import datetime
|
||||
from hashlib import sha256, md5
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
@@ -38,7 +36,7 @@ from threading import Thread
|
||||
from typing import Union, List, Type
|
||||
|
||||
from pyrogram.api import functions, types
|
||||
from pyrogram.api.core import Object
|
||||
from pyrogram.api.core import TLObject
|
||||
from pyrogram.client.handlers import DisconnectHandler
|
||||
from pyrogram.client.handlers.handler import Handler
|
||||
from pyrogram.client.methods.password.utils import compute_check
|
||||
@@ -48,7 +46,7 @@ from pyrogram.errors import (
|
||||
PhoneNumberUnoccupied, PhoneCodeInvalid, PhoneCodeHashEmpty,
|
||||
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
|
||||
PasswordHashInvalid, FloodWait, PeerIdInvalid, FirstnameInvalid, PhoneNumberBanned,
|
||||
VolumeLocNotFound, UserMigrate, FileIdInvalid, ChannelPrivate, PhoneNumberOccupied,
|
||||
VolumeLocNotFound, UserMigrate, ChannelPrivate, PhoneNumberOccupied,
|
||||
PasswordRecoveryNa, PasswordEmpty
|
||||
)
|
||||
from pyrogram.session import Auth, Session
|
||||
@@ -63,27 +61,23 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client(Methods, BaseClient):
|
||||
"""This class represents a Client, the main mean for interacting with Telegram.
|
||||
It exposes bot-like methods for an easy access to the API as well as a simple way to
|
||||
invoke every single Telegram API method available.
|
||||
"""Pyrogram Client, the main means for interacting with Telegram.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
session_name (``str``):
|
||||
Name to uniquely identify a session of either a User or a Bot, e.g.: "my_account". This name will be used
|
||||
to save a file to disk that stores details needed for reconnecting without asking again for credentials.
|
||||
Note for bots: You can pass a bot token here, but this usage will be deprecated in next releases.
|
||||
Use *bot_token* instead.
|
||||
|
||||
api_id (``int``, *optional*):
|
||||
The *api_id* part of your Telegram API Key, as integer. E.g.: 12345
|
||||
This is an alternative way to pass it if you don't want to use the *config.ini* file.
|
||||
|
||||
api_hash (``str``, *optional*):
|
||||
The *api_hash* part of your Telegram API Key, as string. E.g.: "0123456789abcdef0123456789abcdef"
|
||||
The *api_hash* part of your Telegram API Key, as string. E.g.: "0123456789abcdef0123456789abcdef".
|
||||
This is an alternative way to pass it if you don't want to use the *config.ini* file.
|
||||
|
||||
app_version (``str``, *optional*):
|
||||
Application version. Defaults to "Pyrogram \U0001f525 vX.Y.Z"
|
||||
Application version. Defaults to "Pyrogram X.Y.Z"
|
||||
This is an alternative way to set it if you don't want to use the *config.ini* file.
|
||||
|
||||
device_model (``str``, *optional*):
|
||||
@@ -109,10 +103,14 @@ class Client(Methods, BaseClient):
|
||||
This is an alternative way to setup a proxy if you don't want to use the *config.ini* file.
|
||||
|
||||
test_mode (``bool``, *optional*):
|
||||
Enable or disable log-in to testing servers. Defaults to False.
|
||||
Enable or disable login to the test servers. Defaults to False.
|
||||
Only applicable for new sessions and will be ignored in case previously
|
||||
created sessions are loaded.
|
||||
|
||||
bot_token (``str``, *optional*):
|
||||
Pass your Bot API token to create a bot session, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
Only applicable for new sessions.
|
||||
|
||||
phone_number (``str`` | ``callable``, *optional*):
|
||||
Pass your phone number as string (with your Country Code prefix included) to avoid entering it manually.
|
||||
Or pass a callback function which accepts no arguments and must return the correct phone number as string
|
||||
@@ -146,10 +144,6 @@ class Client(Methods, BaseClient):
|
||||
a new Telegram account in case the phone number you passed is not registered yet.
|
||||
Only applicable for new sessions.
|
||||
|
||||
bot_token (``str``, *optional*):
|
||||
Pass your Bot API token to create a bot session, e.g.: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
Only applicable for new sessions.
|
||||
|
||||
last_name (``str``, *optional*):
|
||||
Same purpose as *first_name*; pass a Last Name to avoid entering it manually. It can
|
||||
be an empty string: "". Only applicable for new sessions.
|
||||
@@ -175,7 +169,7 @@ class Client(Methods, BaseClient):
|
||||
Defaults to False (updates enabled and always received).
|
||||
|
||||
takeout (``bool``, *optional*):
|
||||
Pass True to let the client use a takeout session instead of a normal one, implies no_updates.
|
||||
Pass True to let the client use a takeout session instead of a normal one, implies *no_updates=True*.
|
||||
Useful for exporting your Telegram data. Methods invoked inside a takeout session (such as get_history,
|
||||
download_media, ...) are less prone to throw FloodWait exceptions.
|
||||
Only available for users, bots will ignore this parameter.
|
||||
@@ -196,12 +190,12 @@ class Client(Methods, BaseClient):
|
||||
ipv6: bool = False,
|
||||
proxy: dict = None,
|
||||
test_mode: bool = False,
|
||||
bot_token: str = None,
|
||||
phone_number: str = None,
|
||||
phone_code: Union[str, callable] = None,
|
||||
password: str = None,
|
||||
recovery_code: callable = None,
|
||||
force_sms: bool = False,
|
||||
bot_token: str = None,
|
||||
first_name: str = None,
|
||||
last_name: str = None,
|
||||
workers: int = BaseClient.WORKERS,
|
||||
@@ -239,12 +233,12 @@ class Client(Methods, BaseClient):
|
||||
# TODO: Make code consistent, use underscore for private/protected fields
|
||||
self._proxy = proxy
|
||||
self.session_storage.test_mode = test_mode
|
||||
self.bot_token = bot_token
|
||||
self.phone_number = phone_number
|
||||
self.phone_code = phone_code
|
||||
self.password = password
|
||||
self.recovery_code = recovery_code
|
||||
self.force_sms = force_sms
|
||||
self.bot_token = bot_token
|
||||
self.first_name = first_name
|
||||
self.last_name = last_name
|
||||
self.workers = workers
|
||||
@@ -279,12 +273,11 @@ class Client(Methods, BaseClient):
|
||||
self._proxy.update(value)
|
||||
|
||||
def start(self):
|
||||
"""Use this method to start the Client after creating it.
|
||||
Requires no parameters.
|
||||
"""Start the Client.
|
||||
|
||||
Raises:
|
||||
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
|
||||
``ConnectionError`` in case you try to start an already started Client.
|
||||
RPCError: In case of a Telegram RPC error.
|
||||
ConnectionError: In case you try to start an already started Client.
|
||||
"""
|
||||
if self.is_started:
|
||||
raise ConnectionError("Client has already been started")
|
||||
@@ -297,7 +290,7 @@ class Client(Methods, BaseClient):
|
||||
warnings.warn('\nWARNING: You are using a bot token as session name!\n'
|
||||
'This usage will be deprecated soon. Please use a session file name to load '
|
||||
'an existing session and the bot_token argument to create new sessions.\n'
|
||||
'More info: https://docs.pyrogram.ml/start/Setup#bot-authorization\n')
|
||||
'More info: https://docs.pyrogram.org/intro/auth#bot-authorization\n')
|
||||
|
||||
self.load_config()
|
||||
self.load_session()
|
||||
@@ -336,7 +329,7 @@ class Client(Methods, BaseClient):
|
||||
self.get_initial_dialogs()
|
||||
self.get_contacts()
|
||||
else:
|
||||
self.send(functions.messages.GetPinnedDialogs())
|
||||
self.send(functions.messages.GetPinnedDialogs(folder_id=0))
|
||||
self.get_initial_dialogs_chunk()
|
||||
else:
|
||||
self.send(functions.updates.GetState())
|
||||
@@ -373,11 +366,10 @@ class Client(Methods, BaseClient):
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
"""Use this method to manually stop the Client.
|
||||
Requires no parameters.
|
||||
"""Stop the Client.
|
||||
|
||||
Raises:
|
||||
``ConnectionError`` in case you try to stop an already stopped Client.
|
||||
ConnectionError: In case you try to stop an already stopped Client.
|
||||
"""
|
||||
if not self.is_started:
|
||||
raise ConnectionError("Client is already stopped")
|
||||
@@ -416,25 +408,34 @@ class Client(Methods, BaseClient):
|
||||
return self
|
||||
|
||||
def restart(self):
|
||||
"""Use this method to restart the Client.
|
||||
Requires no parameters.
|
||||
"""Restart the Client.
|
||||
|
||||
Raises:
|
||||
``ConnectionError`` in case you try to restart a stopped Client.
|
||||
ConnectionError: In case you try to restart a stopped Client.
|
||||
"""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)):
|
||||
"""Blocks the program execution until one of the signals are received,
|
||||
then gently stop the Client by closing the underlying connection.
|
||||
"""Block the main script execution until a signal (e.g.: from CTRL+C) is received.
|
||||
Once the signal is received, the client will automatically stop and the main script will continue its execution.
|
||||
|
||||
Args:
|
||||
This is used after starting one or more clients and is useful for event-driven applications only, that are,
|
||||
applications which react upon incoming Telegram updates through handlers, rather than executing a set of methods
|
||||
sequentially.
|
||||
|
||||
The way Pyrogram works, will keep your handlers in a pool of workers, which are executed concurrently outside
|
||||
the main script; calling idle() will ensure the client(s) will be kept alive by not letting the main script to
|
||||
end, until you decide to quit.
|
||||
|
||||
Parameters:
|
||||
stop_signals (``tuple``, *optional*):
|
||||
Iterable containing signals the signal handler will listen to.
|
||||
Defaults to (SIGINT, SIGTERM, SIGABRT).
|
||||
"""
|
||||
|
||||
# TODO: Maybe make this method static and don't automatically stop
|
||||
|
||||
def signal_handler(*args):
|
||||
self.is_idle = False
|
||||
|
||||
@@ -449,23 +450,26 @@ class Client(Methods, BaseClient):
|
||||
self.stop()
|
||||
|
||||
def run(self):
|
||||
"""Use this method to automatically start and idle a Client.
|
||||
Requires no parameters.
|
||||
"""Start the Client and automatically idle the main script.
|
||||
|
||||
This is a convenience method that literally just calls :meth:`~Client.start` and :meth:`~Client.idle`. It makes
|
||||
running a client less verbose, but is not suitable in case you want to run more than one client in a single main
|
||||
script, since :meth:`~Client.idle` will block.
|
||||
|
||||
Raises:
|
||||
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
|
||||
RPCError: In case of a Telegram RPC error.
|
||||
"""
|
||||
self.start()
|
||||
self.idle()
|
||||
|
||||
def add_handler(self, handler: Handler, group: int = 0):
|
||||
"""Use this method to register an update handler.
|
||||
"""Register an update handler.
|
||||
|
||||
You can register multiple handlers, but at most one handler within a group
|
||||
will be used for a single update. To handle the same update more than once, register
|
||||
your handler using a different group id (lower group id == higher priority).
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
handler (``Handler``):
|
||||
The handler to be registered.
|
||||
|
||||
@@ -473,7 +477,7 @@ class Client(Methods, BaseClient):
|
||||
The group identifier, defaults to 0.
|
||||
|
||||
Returns:
|
||||
A tuple of (handler, group)
|
||||
``tuple``: A tuple consisting of (handler, group).
|
||||
"""
|
||||
if isinstance(handler, DisconnectHandler):
|
||||
self.disconnect_handler = handler.callback
|
||||
@@ -483,13 +487,13 @@ class Client(Methods, BaseClient):
|
||||
return handler, group
|
||||
|
||||
def remove_handler(self, handler: Handler, group: int = 0):
|
||||
"""Removes a previously-added update handler.
|
||||
"""Remove a previously-registered update handler.
|
||||
|
||||
Make sure to provide the right group that the handler was added in. You can use
|
||||
the return value of the :meth:`add_handler` method, a tuple of (handler, group), and
|
||||
the return value of the :meth:`~Client.add_handler` method, a tuple of (handler, group), and
|
||||
pass it directly.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
handler (``Handler``):
|
||||
The handler to be removed.
|
||||
|
||||
@@ -502,7 +506,7 @@ class Client(Methods, BaseClient):
|
||||
self.dispatcher.remove_handler(handler, group)
|
||||
|
||||
def stop_transmission(self):
|
||||
"""Use this method to stop downloading or uploading a file.
|
||||
"""Stop downloading or uploading a file.
|
||||
Must be called inside a progress callback function.
|
||||
"""
|
||||
raise Client.StopTransmission
|
||||
@@ -771,96 +775,52 @@ class Client(Methods, BaseClient):
|
||||
|
||||
print("Logged in successfully as {}".format(r.user.first_name))
|
||||
|
||||
def fetch_peers(self, entities: List[Union[types.User,
|
||||
types.Chat, types.ChatForbidden,
|
||||
types.Channel, types.ChannelForbidden]]):
|
||||
def fetch_peers(
|
||||
self,
|
||||
entities: List[
|
||||
Union[
|
||||
types.User,
|
||||
types.Chat, types.ChatForbidden,
|
||||
types.Channel, types.ChannelForbidden
|
||||
]
|
||||
]
|
||||
) -> bool:
|
||||
is_min = False
|
||||
|
||||
for entity in entities:
|
||||
if isinstance(entity, (types.User, types.Channel, types.ChannelForbidden)) and not entity.access_hash:
|
||||
continue
|
||||
self.session_storage.cache_peer(entity)
|
||||
|
||||
return is_min
|
||||
|
||||
def download_worker(self):
|
||||
name = threading.current_thread().name
|
||||
log.debug("{} started".format(name))
|
||||
|
||||
while True:
|
||||
media = self.download_queue.get()
|
||||
packet = self.download_queue.get()
|
||||
|
||||
if media is None:
|
||||
if packet is None:
|
||||
break
|
||||
|
||||
temp_file_path = ""
|
||||
final_file_path = ""
|
||||
|
||||
try:
|
||||
media, file_name, done, progress, progress_args, path = media
|
||||
|
||||
file_id = media.file_id
|
||||
size = media.file_size
|
||||
|
||||
directory, file_name = os.path.split(file_name)
|
||||
directory = directory or "downloads"
|
||||
|
||||
try:
|
||||
decoded = utils.decode(file_id)
|
||||
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
|
||||
unpacked = struct.unpack(fmt, decoded)
|
||||
except (AssertionError, binascii.Error, struct.error):
|
||||
raise FileIdInvalid from None
|
||||
else:
|
||||
media_type = unpacked[0]
|
||||
dc_id = unpacked[1]
|
||||
id = unpacked[2]
|
||||
access_hash = unpacked[3]
|
||||
volume_id = None
|
||||
secret = None
|
||||
local_id = None
|
||||
|
||||
if len(decoded) > 24:
|
||||
volume_id = unpacked[4]
|
||||
secret = unpacked[5]
|
||||
local_id = unpacked[6]
|
||||
|
||||
media_type_str = Client.MEDIA_TYPE_ID.get(media_type, None)
|
||||
|
||||
if media_type_str is None:
|
||||
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
|
||||
|
||||
file_name = file_name or getattr(media, "file_name", None)
|
||||
|
||||
if not file_name:
|
||||
if media_type == 3:
|
||||
extension = ".ogg"
|
||||
elif media_type in (4, 10, 13):
|
||||
extension = mimetypes.guess_extension(media.mime_type) or ".mp4"
|
||||
elif media_type == 5:
|
||||
extension = mimetypes.guess_extension(media.mime_type) or ".unknown"
|
||||
elif media_type == 8:
|
||||
extension = ".webp"
|
||||
elif media_type == 9:
|
||||
extension = mimetypes.guess_extension(media.mime_type) or ".mp3"
|
||||
elif media_type in (0, 1, 2):
|
||||
extension = ".jpg"
|
||||
else:
|
||||
continue
|
||||
|
||||
file_name = "{}_{}_{}{}".format(
|
||||
media_type_str,
|
||||
datetime.fromtimestamp(
|
||||
getattr(media, "date", None) or time.time()
|
||||
).strftime("%Y-%m-%d_%H-%M-%S"),
|
||||
self.rnd_id(),
|
||||
extension
|
||||
)
|
||||
data, directory, file_name, done, progress, progress_args, path = packet
|
||||
|
||||
temp_file_path = self.get_file(
|
||||
dc_id=dc_id,
|
||||
id=id,
|
||||
access_hash=access_hash,
|
||||
volume_id=volume_id,
|
||||
local_id=local_id,
|
||||
secret=secret,
|
||||
size=size,
|
||||
media_type=data.media_type,
|
||||
dc_id=data.dc_id,
|
||||
document_id=data.document_id,
|
||||
access_hash=data.access_hash,
|
||||
thumb_size=data.thumb_size,
|
||||
peer_id=data.peer_id,
|
||||
volume_id=data.volume_id,
|
||||
local_id=data.local_id,
|
||||
file_size=data.file_size,
|
||||
is_big=data.is_big,
|
||||
progress=progress,
|
||||
progress_args=progress_args
|
||||
)
|
||||
@@ -898,8 +858,10 @@ class Client(Methods, BaseClient):
|
||||
|
||||
try:
|
||||
if isinstance(updates, (types.Update, types.UpdatesCombined)):
|
||||
self.fetch_peers(updates.users)
|
||||
self.fetch_peers(updates.chats)
|
||||
is_min = self.fetch_peers(updates.users) or self.fetch_peers(updates.chats)
|
||||
|
||||
users = {u.id: u for u in updates.users}
|
||||
chats = {c.id: c for c in updates.chats}
|
||||
|
||||
for update in updates.updates:
|
||||
channel_id = getattr(
|
||||
@@ -916,7 +878,7 @@ class Client(Methods, BaseClient):
|
||||
if isinstance(update, types.UpdateChannelTooLong):
|
||||
log.warning(update)
|
||||
|
||||
if isinstance(update, types.UpdateNewChannelMessage):
|
||||
if isinstance(update, types.UpdateNewChannelMessage) and is_min:
|
||||
message = update.message
|
||||
|
||||
if not isinstance(message, types.MessageEmpty):
|
||||
@@ -938,22 +900,10 @@ class Client(Methods, BaseClient):
|
||||
pass
|
||||
else:
|
||||
if not isinstance(diff, types.updates.ChannelDifferenceEmpty):
|
||||
updates.users += diff.users
|
||||
updates.chats += diff.chats
|
||||
users.update({u.id: u for u in diff.users})
|
||||
chats.update({c.id: c for c in diff.chats})
|
||||
|
||||
if channel_id and pts:
|
||||
if channel_id not in self.channels_pts:
|
||||
self.channels_pts[channel_id] = []
|
||||
|
||||
if pts in self.channels_pts[channel_id]:
|
||||
continue
|
||||
|
||||
self.channels_pts[channel_id].append(pts)
|
||||
|
||||
if len(self.channels_pts[channel_id]) > 50:
|
||||
self.channels_pts[channel_id] = self.channels_pts[channel_id][25:]
|
||||
|
||||
self.dispatcher.updates_queue.put((update, updates.users, updates.chats))
|
||||
self.dispatcher.updates_queue.put((update, users, chats))
|
||||
elif isinstance(updates, (types.UpdateShortMessage, types.UpdateShortChatMessage)):
|
||||
diff = self.send(
|
||||
functions.updates.GetDifference(
|
||||
@@ -970,13 +920,13 @@ class Client(Methods, BaseClient):
|
||||
pts=updates.pts,
|
||||
pts_count=updates.pts_count
|
||||
),
|
||||
diff.users,
|
||||
diff.chats
|
||||
{u.id: u for u in diff.users},
|
||||
{c.id: c for c in diff.chats}
|
||||
))
|
||||
else:
|
||||
self.dispatcher.updates_queue.put((diff.other_updates[0], [], []))
|
||||
self.dispatcher.updates_queue.put((diff.other_updates[0], {}, {}))
|
||||
elif isinstance(updates, types.UpdateShort):
|
||||
self.dispatcher.updates_queue.put((updates.update, [], []))
|
||||
self.dispatcher.updates_queue.put((updates.update, {}, {}))
|
||||
elif isinstance(updates, types.UpdatesTooLong):
|
||||
log.warning(updates)
|
||||
except Exception as e:
|
||||
@@ -984,18 +934,21 @@ class Client(Methods, BaseClient):
|
||||
|
||||
log.debug("{} stopped".format(name))
|
||||
|
||||
def send(self,
|
||||
data: Object,
|
||||
retries: int = Session.MAX_RETRIES,
|
||||
timeout: float = Session.WAIT_TIMEOUT):
|
||||
"""Use this method to send Raw Function queries.
|
||||
def send(self, data: TLObject, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT):
|
||||
"""Send raw Telegram queries.
|
||||
|
||||
This method makes possible to manually call every single Telegram API method in a low-level manner.
|
||||
This method makes it possible to manually call every single Telegram API method in a low-level manner.
|
||||
Available functions are listed in the :obj:`functions <pyrogram.api.functions>` package and may accept compound
|
||||
data types from :obj:`types <pyrogram.api.types>` as well as bare types such as ``int``, ``str``, etc...
|
||||
|
||||
Args:
|
||||
data (``Object``):
|
||||
.. note::
|
||||
|
||||
This is a utility method intended to be used **only** when working with raw
|
||||
:obj:`functions <pyrogram.api.functions>` (i.e: a Telegram API method you wish to use which is not
|
||||
available yet in the Client class as an easy-to-use method).
|
||||
|
||||
Parameters:
|
||||
data (``RawFunction``):
|
||||
The API Schema function filled with proper arguments.
|
||||
|
||||
retries (``int``):
|
||||
@@ -1004,8 +957,11 @@ class Client(Methods, BaseClient):
|
||||
timeout (``float``):
|
||||
Timeout in seconds.
|
||||
|
||||
Returns:
|
||||
``RawType``: The raw type response generated by the query.
|
||||
|
||||
Raises:
|
||||
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
|
||||
RPCError: In case of a Telegram RPC error.
|
||||
"""
|
||||
if not self.is_started:
|
||||
raise ConnectionError("Client has not been started")
|
||||
@@ -1036,7 +992,7 @@ class Client(Methods, BaseClient):
|
||||
else:
|
||||
raise AttributeError(
|
||||
"No API Key found. "
|
||||
"More info: https://docs.pyrogram.ml/start/ProjectSetup#configuration"
|
||||
"More info: https://docs.pyrogram.org/intro/setup#configuration"
|
||||
)
|
||||
|
||||
for option in ["app_version", "device_model", "system_version", "lang_code"]:
|
||||
@@ -1065,29 +1021,34 @@ class Client(Methods, BaseClient):
|
||||
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
|
||||
self.plugins = {
|
||||
"enabled": bool(self.plugins.get("enabled", True)),
|
||||
"root": self.plugins.get("root", None),
|
||||
"include": self.plugins.get("include", []),
|
||||
"exclude": self.plugins.get("exclude", [])
|
||||
}
|
||||
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
|
||||
"root": section.get("root", None),
|
||||
"include": section.get("include", []),
|
||||
"exclude": section.get("exclude", [])
|
||||
}
|
||||
except KeyError:
|
||||
self.plugins = {}
|
||||
|
||||
if self.plugins:
|
||||
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")
|
||||
]
|
||||
include = self.plugins["include"]
|
||||
exclude = self.plugins["exclude"]
|
||||
|
||||
if include:
|
||||
self.plugins["include"] = include.strip().split("\n")
|
||||
|
||||
if exclude:
|
||||
self.plugins["exclude"] = exclude.strip().split("\n")
|
||||
|
||||
except KeyError:
|
||||
self.plugins = None
|
||||
|
||||
def load_session(self):
|
||||
try:
|
||||
@@ -1098,14 +1059,26 @@ class Client(Methods, BaseClient):
|
||||
self.ipv6, self._proxy).create()
|
||||
|
||||
def load_plugins(self):
|
||||
if self.plugins.get("enabled", False):
|
||||
root = self.plugins["root"]
|
||||
include = self.plugins["include"]
|
||||
exclude = self.plugins["exclude"]
|
||||
if self.plugins:
|
||||
plugins = self.plugins.copy()
|
||||
|
||||
for option in ["include", "exclude"]:
|
||||
if plugins[option]:
|
||||
plugins[option] = [
|
||||
(i.split()[0], i.split()[1:] or None)
|
||||
for i in self.plugins[option]
|
||||
]
|
||||
else:
|
||||
return
|
||||
|
||||
if plugins.get("enabled", False):
|
||||
root = plugins["root"]
|
||||
include = plugins["include"]
|
||||
exclude = plugins["exclude"]
|
||||
|
||||
count = 0
|
||||
|
||||
if include is None:
|
||||
if not include:
|
||||
for path in sorted(Path(root).rglob("*.py")):
|
||||
module_path = '.'.join(path.parent.parts + (path.stem,))
|
||||
module = import_module(module_path)
|
||||
@@ -1118,8 +1091,8 @@ class Client(Methods, BaseClient):
|
||||
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('[{}] [LOAD] {}("{}") in group {} from "{}"'.format(
|
||||
self.session_name, type(handler).__name__, name, group, module_path))
|
||||
|
||||
count += 1
|
||||
except Exception:
|
||||
@@ -1132,11 +1105,13 @@ class Client(Methods, BaseClient):
|
||||
try:
|
||||
module = import_module(module_path)
|
||||
except ImportError:
|
||||
log.warning('[LOAD] Ignoring non-existent module "{}"'.format(module_path))
|
||||
log.warning('[{}] [LOAD] Ignoring non-existent module "{}"'.format(
|
||||
self.session_name, module_path))
|
||||
continue
|
||||
|
||||
if "__path__" in dir(module):
|
||||
log.warning('[LOAD] Ignoring namespace "{}"'.format(module_path))
|
||||
log.warning('[{}] [LOAD] Ignoring namespace "{}"'.format(
|
||||
self.session_name, module_path))
|
||||
continue
|
||||
|
||||
if handlers is None:
|
||||
@@ -1151,16 +1126,16 @@ class Client(Methods, BaseClient):
|
||||
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('[{}] [LOAD] {}("{}") in group {} from "{}"'.format(
|
||||
self.session_name, 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))
|
||||
log.warning('[{}] [LOAD] Ignoring non-existent function "{}" from "{}"'.format(
|
||||
self.session_name, name, module_path))
|
||||
|
||||
if exclude is not None:
|
||||
if exclude:
|
||||
for path, handlers in exclude:
|
||||
module_path = root + "." + path
|
||||
warn_non_existent_functions = True
|
||||
@@ -1168,11 +1143,13 @@ class Client(Methods, BaseClient):
|
||||
try:
|
||||
module = import_module(module_path)
|
||||
except ImportError:
|
||||
log.warning('[UNLOAD] Ignoring non-existent module "{}"'.format(module_path))
|
||||
log.warning('[{}] [UNLOAD] Ignoring non-existent module "{}"'.format(
|
||||
self.session_name, module_path))
|
||||
continue
|
||||
|
||||
if "__path__" in dir(module):
|
||||
log.warning('[UNLOAD] Ignoring namespace "{}"'.format(module_path))
|
||||
log.warning('[{}] [UNLOAD] Ignoring namespace "{}"'.format(
|
||||
self.session_name, module_path))
|
||||
continue
|
||||
|
||||
if handlers is None:
|
||||
@@ -1187,25 +1164,26 @@ class Client(Methods, BaseClient):
|
||||
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))
|
||||
log.info('[{}] [UNLOAD] {}("{}") from group {} in "{}"'.format(
|
||||
self.session_name, 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))
|
||||
log.warning('[{}] [UNLOAD] Ignoring non-existent function "{}" from "{}"'.format(
|
||||
self.session_name, name, module_path))
|
||||
|
||||
if count > 0:
|
||||
log.warning('Successfully loaded {} plugin{} from "{}"'.format(count, "s" if count > 1 else "", root))
|
||||
log.warning('[{}] Successfully loaded {} plugin{} from "{}"'.format(
|
||||
self.session_name, count, "s" if count > 1 else "", root))
|
||||
else:
|
||||
log.warning('No plugin loaded from "{}"'.format(root))
|
||||
log.warning('[{}] No plugin loaded from "{}"'.format(
|
||||
self.session_name, root))
|
||||
|
||||
def save_session(self):
|
||||
self.session_storage.save()
|
||||
|
||||
def get_initial_dialogs_chunk(self,
|
||||
offset_date: int = 0):
|
||||
def get_initial_dialogs_chunk(self, offset_date: int = 0):
|
||||
while True:
|
||||
try:
|
||||
r = self.send(
|
||||
@@ -1226,7 +1204,7 @@ class Client(Methods, BaseClient):
|
||||
return r
|
||||
|
||||
def get_initial_dialogs(self):
|
||||
self.send(functions.messages.GetPinnedDialogs())
|
||||
self.send(functions.messages.GetPinnedDialogs(folder_id=0))
|
||||
|
||||
dialogs = self.get_initial_dialogs_chunk()
|
||||
offset_date = utils.get_offset_date(dialogs)
|
||||
@@ -1237,25 +1215,27 @@ class Client(Methods, BaseClient):
|
||||
|
||||
self.get_initial_dialogs_chunk()
|
||||
|
||||
def resolve_peer(self,
|
||||
peer_id: Union[int, str]):
|
||||
"""Use this method to get the InputPeer of a known peer_id.
|
||||
def resolve_peer(self, peer_id: Union[int, str]):
|
||||
"""Get the InputPeer of a known peer id.
|
||||
Useful whenever an InputPeer type is required.
|
||||
|
||||
This is a utility method intended to be used **only** when working with Raw Functions (i.e: a Telegram API
|
||||
method you wish to use which is not available yet in the Client class as an easy-to-use method), whenever an
|
||||
InputPeer type is required.
|
||||
.. note::
|
||||
|
||||
Args:
|
||||
This is a utility method intended to be used **only** when working with raw
|
||||
:obj:`functions <pyrogram.api.functions>` (i.e: a Telegram API method you wish to use which is not
|
||||
available yet in the Client class as an easy-to-use method).
|
||||
|
||||
Parameters:
|
||||
peer_id (``int`` | ``str``):
|
||||
The peer id you want to extract the InputPeer from.
|
||||
Can be a direct id (int), a username (str) or a phone number (str).
|
||||
|
||||
Returns:
|
||||
On success, the resolved peer id is returned in form of an InputPeer object.
|
||||
``InputPeer``: On success, the resolved peer id is returned in form of an InputPeer object.
|
||||
|
||||
Raises:
|
||||
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
|
||||
``KeyError`` in case the peer doesn't exist in the internal database.
|
||||
RPCError: In case of a Telegram RPC error.
|
||||
KeyError: In case the peer doesn't exist in the internal database.
|
||||
"""
|
||||
try:
|
||||
return self.session_storage.get_peer_by_id(peer_id)
|
||||
@@ -1312,19 +1292,24 @@ class Client(Methods, BaseClient):
|
||||
except KeyError:
|
||||
raise PeerIdInvalid
|
||||
|
||||
def save_file(self,
|
||||
path: str,
|
||||
file_id: int = None,
|
||||
file_part: int = 0,
|
||||
progress: callable = None,
|
||||
progress_args: tuple = ()):
|
||||
"""Use this method to upload a file onto Telegram servers, without actually sending the message to anyone.
|
||||
def save_file(
|
||||
self,
|
||||
path: str,
|
||||
file_id: int = None,
|
||||
file_part: int = 0,
|
||||
progress: callable = None,
|
||||
progress_args: tuple = ()
|
||||
):
|
||||
"""Upload a file onto Telegram servers, without actually sending the message to anyone.
|
||||
Useful whenever an InputFile type is required.
|
||||
|
||||
This is a utility method intended to be used **only** when working with Raw Functions (i.e: a Telegram API
|
||||
method you wish to use which is not available yet in the Client class as an easy-to-use method), whenever an
|
||||
InputFile type is required.
|
||||
.. note::
|
||||
|
||||
Args:
|
||||
This is a utility method intended to be used **only** when working with raw
|
||||
:obj:`functions <pyrogram.api.functions>` (i.e: a Telegram API method you wish to use which is not
|
||||
available yet in the Client class as an easy-to-use method).
|
||||
|
||||
Parameters:
|
||||
path (``str``):
|
||||
The path of the file you want to upload that exists on your local machine.
|
||||
|
||||
@@ -1344,7 +1329,7 @@ class Client(Methods, BaseClient):
|
||||
a chat_id and a message_id in order to edit a message with the updated progress.
|
||||
|
||||
Other Parameters:
|
||||
client (:obj:`Client <pyrogram.Client>`):
|
||||
client (:obj:`Client`):
|
||||
The Client itself, useful when you want to call other API methods inside the callback function.
|
||||
|
||||
current (``int``):
|
||||
@@ -1358,10 +1343,10 @@ class Client(Methods, BaseClient):
|
||||
You can either keep *\*args* or add every single extra argument in your function signature.
|
||||
|
||||
Returns:
|
||||
On success, the uploaded file is returned in form of an InputFile object.
|
||||
``InputFile``: On success, the uploaded file is returned in form of an InputFile object.
|
||||
|
||||
Raises:
|
||||
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
|
||||
RPCError: In case of a Telegram RPC error.
|
||||
"""
|
||||
part_size = 512 * 1024
|
||||
file_size = os.path.getsize(path)
|
||||
@@ -1445,16 +1430,21 @@ class Client(Methods, BaseClient):
|
||||
finally:
|
||||
session.stop()
|
||||
|
||||
def get_file(self,
|
||||
dc_id: int,
|
||||
id: int = None,
|
||||
access_hash: int = None,
|
||||
volume_id: int = None,
|
||||
local_id: int = None,
|
||||
secret: int = None,
|
||||
size: int = None,
|
||||
progress: callable = None,
|
||||
progress_args: tuple = ()) -> str:
|
||||
def get_file(
|
||||
self,
|
||||
media_type: int,
|
||||
dc_id: int,
|
||||
document_id: int,
|
||||
access_hash: int,
|
||||
thumb_size: str,
|
||||
peer_id: int,
|
||||
volume_id: int,
|
||||
local_id: int,
|
||||
file_size: int,
|
||||
is_big: bool,
|
||||
progress: callable,
|
||||
progress_args: tuple = ()
|
||||
) -> str:
|
||||
with self.media_sessions_lock:
|
||||
session = self.media_sessions.get(dc_id, None)
|
||||
|
||||
@@ -1495,18 +1485,33 @@ class Client(Methods, BaseClient):
|
||||
|
||||
self.media_sessions[dc_id] = session
|
||||
|
||||
if volume_id: # Photos are accessed by volume_id, local_id, secret
|
||||
location = types.InputFileLocation(
|
||||
if media_type == 1:
|
||||
location = types.InputPeerPhotoFileLocation(
|
||||
peer=self.resolve_peer(peer_id),
|
||||
volume_id=volume_id,
|
||||
local_id=local_id,
|
||||
secret=secret,
|
||||
file_reference=b""
|
||||
big=is_big or None
|
||||
)
|
||||
else: # Any other file can be more easily accessed by id and access_hash
|
||||
location = types.InputDocumentFileLocation(
|
||||
id=id,
|
||||
elif media_type in (0, 2):
|
||||
location = types.InputPhotoFileLocation(
|
||||
id=document_id,
|
||||
access_hash=access_hash,
|
||||
file_reference=b""
|
||||
file_reference=b"",
|
||||
thumb_size=thumb_size
|
||||
)
|
||||
elif media_type == 14:
|
||||
location = types.InputDocumentFileLocation(
|
||||
id=document_id,
|
||||
access_hash=access_hash,
|
||||
file_reference=b"",
|
||||
thumb_size=thumb_size
|
||||
)
|
||||
else:
|
||||
location = types.InputDocumentFileLocation(
|
||||
id=document_id,
|
||||
access_hash=access_hash,
|
||||
file_reference=b"",
|
||||
thumb_size=""
|
||||
)
|
||||
|
||||
limit = 1024 * 1024
|
||||
@@ -1537,7 +1542,14 @@ class Client(Methods, BaseClient):
|
||||
offset += limit
|
||||
|
||||
if progress:
|
||||
progress(self, min(offset, size) if size != 0 else offset, size, *progress_args)
|
||||
progress(
|
||||
self,
|
||||
min(offset, file_size)
|
||||
if file_size != 0
|
||||
else offset,
|
||||
file_size,
|
||||
*progress_args
|
||||
)
|
||||
|
||||
r = session.send(
|
||||
functions.upload.GetFile(
|
||||
@@ -1619,7 +1631,14 @@ class Client(Methods, BaseClient):
|
||||
offset += limit
|
||||
|
||||
if progress:
|
||||
progress(self, min(offset, size) if size != 0 else offset, size, *progress_args)
|
||||
progress(
|
||||
self,
|
||||
min(offset, file_size)
|
||||
if file_size != 0
|
||||
else offset,
|
||||
file_size,
|
||||
*progress_args
|
||||
)
|
||||
|
||||
if len(chunk) < limit:
|
||||
break
|
||||
@@ -1637,3 +1656,13 @@ class Client(Methods, BaseClient):
|
||||
return ""
|
||||
else:
|
||||
return file_name
|
||||
|
||||
def guess_mime_type(self, filename: str):
|
||||
extension = os.path.splitext(filename)[1]
|
||||
return self.extensions_to_mime_types.get(extension)
|
||||
|
||||
def guess_extension(self, mime_type: str):
|
||||
extensions = self.mime_types_to_extensions.get(mime_type)
|
||||
|
||||
if extensions:
|
||||
return extensions.split(" ")[0]
|
||||
|
Reference in New Issue
Block a user