2
0
mirror of https://github.com/pyrogram/pyrogram synced 2025-09-03 07:45:14 +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:
Dan
2019-06-15 23:52:34 +02:00
313 changed files with 9183 additions and 4943 deletions

View File

@@ -24,11 +24,9 @@ if sys.version_info[:3] in [(3, 5, 0), (3, 5, 1), (3, 5, 2)]:
# Monkey patch the standard "typing" module because Python versions from 3.5.0 to 3.5.2 have a broken one.
sys.modules["typing"] = typing
__version__ = "0.12.0"
__version__ = "0.15.0-develop"
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
__copyright__ = "Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>".replace(
"\xe8", "e" if sys.getfilesystemencoding() != "utf-8" else "\xe8"
)
__copyright__ = "Copyright (C) 2017-2019 Dan <https://github.com/delivrance>"
from .errors import RPCError
from .client import *

View File

@@ -19,8 +19,8 @@
from importlib import import_module
from .all import objects
from .core.object import Object
from .core.tl_object import TLObject
for k, v in objects.items():
path, name = v.rsplit(".", 1)
Object.all[k] = getattr(import_module(path), name)
TLObject.all[k] = getattr(import_module(path), name)

View File

@@ -19,10 +19,11 @@
from .future_salt import FutureSalt
from .future_salts import FutureSalts
from .gzip_packed import GzipPacked
from .list import List
from .message import Message
from .msg_container import MsgContainer
from .object import Object
from .primitives import (
Bool, BoolTrue, BoolFalse, Bytes, Double,
Int, Long, Int128, Int256, Null, String, Vector
)
from .tl_object import TLObject

View File

@@ -16,29 +16,28 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from io import BytesIO
from .object import Object
from .primitives import Int, Long
from .tl_object import TLObject
class FutureSalt(Object):
class FutureSalt(TLObject):
ID = 0x0949d9dc
__slots__ = ["valid_since", "valid_until", "salt"]
QUALNAME = "FutureSalt"
def __init__(self, valid_since: int or datetime, valid_until: int or datetime, salt: int):
def __init__(self, valid_since: int, valid_until: int, salt: int):
self.valid_since = valid_since
self.valid_until = valid_until
self.salt = salt
@staticmethod
def read(b: BytesIO, *args) -> "FutureSalt":
valid_since = datetime.fromtimestamp(Int.read(b))
valid_until = datetime.fromtimestamp(Int.read(b))
valid_since = Int.read(b)
valid_until = Int.read(b)
salt = Long.read(b)
return FutureSalt(valid_since, valid_until, salt)

View File

@@ -16,22 +16,21 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from io import BytesIO
from . import FutureSalt
from .object import Object
from .primitives import Int, Long
from .tl_object import TLObject
class FutureSalts(Object):
class FutureSalts(TLObject):
ID = 0xae500895
__slots__ = ["req_msg_id", "now", "salts"]
QUALNAME = "FutureSalts"
def __init__(self, req_msg_id: int, now: int or datetime, salts: list):
def __init__(self, req_msg_id: int, now: int, salts: list):
self.req_msg_id = req_msg_id
self.now = now
self.salts = salts
@@ -39,7 +38,7 @@ class FutureSalts(Object):
@staticmethod
def read(b: BytesIO, *args) -> "FutureSalts":
req_msg_id = Long.read(b)
now = datetime.fromtimestamp(Int.read(b))
now = Int.read(b)
count = Int.read(b)
salts = [FutureSalt.read(b) for _ in range(count)]

View File

@@ -19,24 +19,24 @@
from gzip import compress, decompress
from io import BytesIO
from .object import Object
from .primitives import Int, Bytes
from .tl_object import TLObject
class GzipPacked(Object):
class GzipPacked(TLObject):
ID = 0x3072cfa1
__slots__ = ["packed_data"]
QUALNAME = "GzipPacked"
def __init__(self, packed_data: Object):
def __init__(self, packed_data: TLObject):
self.packed_data = packed_data
@staticmethod
def read(b: BytesIO, *args) -> "GzipPacked":
# Return the Object itself instead of a GzipPacked wrapping it
return Object.read(
return TLObject.read(
BytesIO(
decompress(
Bytes.read(b)

View File

@@ -16,14 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .tl_object import TLObject
class ParseMode:
"""This class provides a convenient access to Parse Modes.
Parse Modes are intended to be used with any method that accepts the optional argument **parse_mode**.
"""
HTML = "html"
"""Set the parse mode to HTML style"""
class List(list, TLObject):
__slots__ = []
MARKDOWN = "markdown"
"""Set the parse mode to Markdown style"""
def __repr__(self):
return "pyrogram.api.core.List([{}])".format(
",".join(TLObject.__repr__(i) for i in self)
)

View File

@@ -18,18 +18,18 @@
from io import BytesIO
from .object import Object
from .primitives import Int, Long
from .tl_object import TLObject
class Message(Object):
class Message(TLObject):
ID = 0x5bb8e511 # hex(crc32(b"message msg_id:long seqno:int bytes:int body:Object = Message"))
__slots__ = ["msg_id", "seq_no", "length", "body"]
QUALNAME = "Message"
def __init__(self, body: Object, msg_id: int, seq_no: int, length: int):
def __init__(self, body: TLObject, msg_id: int, seq_no: int, length: int):
self.msg_id = msg_id
self.seq_no = seq_no
self.length = length
@@ -42,7 +42,7 @@ class Message(Object):
length = Int.read(b)
body = b.read(length)
return Message(Object.read(BytesIO(body)), msg_id, seq_no, length)
return Message(TLObject.read(BytesIO(body)), msg_id, seq_no, length)
def write(self) -> bytes:
b = BytesIO()

View File

@@ -19,11 +19,11 @@
from io import BytesIO
from .message import Message
from .object import Object
from .primitives import Int
from .tl_object import TLObject
class MsgContainer(Object):
class MsgContainer(TLObject):
ID = 0x73f1f8dc
__slots__ = ["messages"]

View File

@@ -1,72 +0,0 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from collections import OrderedDict
from datetime import datetime
from io import BytesIO
from json import dumps
class Object:
all = {}
__slots__ = []
QUALNAME = "Base"
@staticmethod
def read(b: BytesIO, *args):
return Object.all[int.from_bytes(b.read(4), "little")].read(b, *args)
def write(self, *args) -> bytes:
pass
def __str__(self) -> str:
return dumps(self, indent=4, default=default, ensure_ascii=False)
def __len__(self) -> int:
return len(self.write())
def __getitem__(self, item):
return getattr(self, item)
def remove_none(obj):
if isinstance(obj, (list, tuple, set)):
return type(obj)(remove_none(x) for x in obj if x is not None)
elif isinstance(obj, dict):
return type(obj)((remove_none(k), remove_none(v)) for k, v in obj.items() if k is not None and v is not None)
else:
return obj
def default(o: "Object"):
try:
content = {i: getattr(o, i) for i in o.__slots__}
return remove_none(
OrderedDict(
[("_", o.QUALNAME)]
+ [i for i in content.items()]
)
)
except AttributeError:
if isinstance(o, datetime):
return o.strftime("%d-%b-%Y %H:%M:%S")
else:
return repr(o)

View File

@@ -18,10 +18,10 @@
from io import BytesIO
from ..object import Object
from ..tl_object import TLObject
class BoolFalse(Object):
class BoolFalse(TLObject):
ID = 0xbc799737
value = False
@@ -38,7 +38,7 @@ class BoolTrue(BoolFalse):
value = True
class Bool(Object):
class Bool(TLObject):
@classmethod
def read(cls, b: BytesIO) -> bool:
return int.from_bytes(b.read(4), "little") == BoolTrue.ID

View File

@@ -18,10 +18,10 @@
from io import BytesIO
from ..object import Object
from ..tl_object import TLObject
class Bytes(Object):
class Bytes(TLObject):
@staticmethod
def read(b: BytesIO, *args) -> bytes:
length = int.from_bytes(b.read(1), "little")

View File

@@ -19,10 +19,10 @@
from io import BytesIO
from struct import unpack, pack
from ..object import Object
from ..tl_object import TLObject
class Double(Object):
class Double(TLObject):
@staticmethod
def read(b: BytesIO, *args) -> float:
return unpack("d", b.read(8))[0]

View File

@@ -18,10 +18,10 @@
from io import BytesIO
from ..object import Object
from ..tl_object import TLObject
class Int(Object):
class Int(TLObject):
SIZE = 4
@classmethod

View File

@@ -18,10 +18,10 @@
from io import BytesIO
from ..object import Object
from ..tl_object import TLObject
class Null(Object):
class Null(TLObject):
ID = 0x56730bcc
@staticmethod

View File

@@ -19,31 +19,32 @@
from io import BytesIO
from . import Int
from ..object import Object
from ..list import List
from ..tl_object import TLObject
class Vector(Object):
class Vector(TLObject):
ID = 0x1cb5c415
# Method added to handle the special case when a query returns a bare Vector (of Ints);
# i.e., RpcResult body starts with 0x1cb5c415 (Vector Id) - e.g., messages.GetMessagesViews.
@staticmethod
def _read(b: BytesIO) -> Object or int:
def _read(b: BytesIO) -> TLObject or int:
try:
return Object.read(b)
return TLObject.read(b)
except KeyError:
b.seek(-4, 1)
return Int.read(b)
@staticmethod
def read(b: BytesIO, t: Object = None) -> list:
return [
def read(b: BytesIO, t: TLObject = None) -> list:
return List(
t.read(b) if t
else Vector._read(b)
for _ in range(Int.read(b))
]
)
def __new__(cls, value: list, t: Object = None) -> bytes:
def __new__(cls, value: list, t: TLObject = None) -> bytes:
return b"".join(
[Int(cls.ID, False), Int(len(value))]
+ [

View File

@@ -0,0 +1,82 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from collections import OrderedDict
from io import BytesIO
from json import dumps
class TLObject:
all = {}
__slots__ = []
QUALNAME = "Base"
@staticmethod
def read(b: BytesIO, *args): # TODO: Rename b -> data
return TLObject.all[int.from_bytes(b.read(4), "little")].read(b, *args)
def write(self, *args) -> bytes:
pass
@staticmethod
def default(obj: "TLObject"):
if isinstance(obj, bytes):
return repr(obj)
return OrderedDict(
[("_", obj.QUALNAME)]
+ [
(attr, getattr(obj, attr))
for attr in obj.__slots__
if getattr(obj, attr) is not None
]
)
def __str__(self) -> str:
return dumps(self, indent=4, default=TLObject.default, ensure_ascii=False)
def __repr__(self) -> str:
return "pyrogram.api.{}({})".format(
self.QUALNAME,
", ".join(
"{}={}".format(attr, repr(getattr(self, attr)))
for attr in self.__slots__
if getattr(self, attr) is not None
)
)
def __eq__(self, other: "TLObject") -> bool:
for attr in self.__slots__:
try:
if getattr(self, attr) != getattr(other, attr):
return False
except AttributeError:
return False
return True
def __len__(self) -> int:
return len(self.write())
def __getitem__(self, item):
return getattr(self, item)
def __setitem__(self, key, value):
setattr(self, key, value)

View File

@@ -17,9 +17,9 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .client import Client
from .ext import BaseClient, ChatAction, Emoji, ParseMode
from .ext import BaseClient, Emoji
from .filters import Filters
__all__ = [
"Client", "BaseClient", "ChatAction", "Emoji", "ParseMode", "Filters",
"Client", "BaseClient", "Emoji", "Filters",
]

View File

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

View File

@@ -17,8 +17,7 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .base_client import BaseClient
from .chat_action import ChatAction
from .dispatcher import Dispatcher
from .emoji import Emoji
from .parse_mode import ParseMode
from .file_data import FileData
from .syncer import Syncer

View File

@@ -16,8 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import os
import platform
import re
import sys
from pathlib import Path
from queue import Queue
from threading import Lock
@@ -31,7 +34,7 @@ class BaseClient:
class StopTransmission(StopIteration):
pass
APP_VERSION = "Pyrogram \U0001f525 {}".format(__version__)
APP_VERSION = "Pyrogram {}".format(__version__)
DEVICE_MODEL = "{} {}".format(
platform.python_implementation(),
@@ -45,18 +48,20 @@ class BaseClient:
LANG_CODE = "en"
PARENT_DIR = Path(sys.argv[0]).parent
INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/joinchat/)([\w-]+)$")
BOT_TOKEN_RE = re.compile(r"^\d+:[\w-]+$")
DIALOGS_AT_ONCE = 100
UPDATES_WORKERS = 1
DOWNLOAD_WORKERS = 1
OFFLINE_SLEEP = 300
OFFLINE_SLEEP = 900
WORKERS = 4
WORKDIR = "."
CONFIG_FILE = "./config.ini"
WORKDIR = PARENT_DIR
CONFIG_FILE = PARENT_DIR / "config.ini"
MEDIA_TYPE_ID = {
0: "thumbnail",
0: "photo_thumbnail",
1: "chat_photo",
2: "photo",
3: "voice",
@@ -65,14 +70,28 @@ class BaseClient:
8: "sticker",
9: "audio",
10: "animation",
13: "video_note"
13: "video_note",
14: "document_thumbnail"
}
mime_types_to_extensions = {}
extensions_to_mime_types = {}
with open("{}/mime.types".format(os.path.dirname(__file__)), "r", encoding="UTF-8") as f:
for match in re.finditer(r"^([^#\s]+)\s+(.+)$", f.read(), flags=re.M):
mime_type, extensions = match.groups()
extensions = [".{}".format(ext) for ext in extensions.split(" ")]
for ext in extensions:
extensions_to_mime_types[ext] = mime_type
mime_types_to_extensions[mime_type] = " ".join(extensions)
def __init__(self, session_storage: SessionStorage):
self.session_storage = session_storage
self.rnd_id = MsgId
self.channels_pts = {}
self.markdown = Markdown(self.session_storage, self)
self.html = HTML(self.session_storage, self)
@@ -125,3 +144,30 @@ class BaseClient:
def answer_inline_query(self, *args, **kwargs):
pass
def guess_mime_type(self, *args, **kwargs):
pass
def guess_extension(self, *args, **kwargs):
pass
def get_profile_photos(self, *args, **kwargs):
pass
def edit_message_text(self, *args, **kwargs):
pass
def edit_inline_text(self, *args, **kwargs):
pass
def edit_message_media(self, *args, **kwargs):
pass
def edit_inline_media(self, *args, **kwargs):
pass
def edit_message_reply_markup(self, *args, **kwargs):
pass
def edit_inline_reply_markup(self, *args, **kwargs):
pass

View File

@@ -1,77 +0,0 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum
from pyrogram.api import types
class ChatAction(Enum):
"""This enumeration provides a convenient access to all Chat Actions available.
Chat Actions are intended to be used with
:meth:`send_chat_action() <pyrogram.Client.send_chat_action>`.
"""
CANCEL = types.SendMessageCancelAction
"""Cancels any chat action currently displayed."""
TYPING = types.SendMessageTypingAction
"""User is typing a text message."""
PLAYING = types.SendMessageGamePlayAction
"""User is playing a game."""
CHOOSE_CONTACT = types.SendMessageChooseContactAction
"""User is choosing a contact to share."""
UPLOAD_PHOTO = types.SendMessageUploadPhotoAction
"""User is uploading a photo."""
RECORD_VIDEO = types.SendMessageRecordVideoAction
"""User is recording a video."""
UPLOAD_VIDEO = types.SendMessageUploadVideoAction
"""User is uploading a video."""
RECORD_AUDIO = types.SendMessageRecordAudioAction
"""User is recording an audio message."""
UPLOAD_AUDIO = types.SendMessageUploadAudioAction
"""User is uploading an audio message."""
UPLOAD_DOCUMENT = types.SendMessageUploadDocumentAction
"""User is uploading a generic document."""
FIND_LOCATION = types.SendMessageGeoLocationAction
"""User is searching for a location on the map."""
RECORD_VIDEO_NOTE = types.SendMessageRecordRoundAction
"""User is recording a round video note."""
UPLOAD_VIDEO_NOTE = types.SendMessageUploadRoundAction
"""User is uploading a round video note."""
@staticmethod
def from_string(action: str) -> "ChatAction":
for a in ChatAction:
if a.name.lower() == action.lower():
return a
raise ValueError("Invalid ChatAction: '{}'. Possible types are {}".format(
action, [x.name.lower() for x in ChatAction]
))

View File

@@ -24,9 +24,10 @@ from threading import Thread
import pyrogram
from pyrogram.api import types
from . import utils
from ..handlers import (
CallbackQueryHandler, MessageHandler, DeletedMessagesHandler,
UserStatusHandler, RawUpdateHandler, InlineQueryHandler
UserStatusHandler, RawUpdateHandler, InlineQueryHandler, PollHandler
)
log = logging.getLogger(__name__)
@@ -68,7 +69,7 @@ class Dispatcher:
lambda upd, usr, cht: (pyrogram.Message._parse(self.client, upd.message, usr, cht), MessageHandler),
Dispatcher.DELETE_MESSAGES_UPDATES:
lambda upd, usr, cht: (pyrogram.Messages._parse_deleted(self.client, upd), DeletedMessagesHandler),
lambda upd, usr, cht: (utils.parse_deleted_messages(self.client, upd), DeletedMessagesHandler),
Dispatcher.CALLBACK_QUERY_UPDATES:
lambda upd, usr, cht: (pyrogram.CallbackQuery._parse(self.client, upd, usr), CallbackQueryHandler),
@@ -79,7 +80,10 @@ class Dispatcher:
),
(types.UpdateBotInlineQuery,):
lambda upd, usr, cht: (pyrogram.InlineQuery._parse(self.client, upd, usr), InlineQueryHandler)
lambda upd, usr, cht: (pyrogram.InlineQuery._parse(self.client, upd, usr), InlineQueryHandler),
(types.UpdateMessagePoll,):
lambda upd, usr, cht: (pyrogram.Poll._parse_update(self.client, upd), PollHandler)
}
self.update_parsers = {key: value for key_tuple, value in self.update_parsers.items() for key in key_tuple}
@@ -103,6 +107,7 @@ class Dispatcher:
worker.join()
self.workers_list.clear()
self.groups.clear()
def add_handler(self, handler, group: int):
if group not in self.groups:
@@ -122,16 +127,13 @@ class Dispatcher:
log.debug("{} started".format(name))
while True:
update = self.updates_queue.get()
packet = self.updates_queue.get()
if update is None:
if packet is None:
break
try:
users = {i.id: i for i in update[1]}
chats = {i.id: i for i in update[2]}
update = update[0]
update, users, chats = packet
parser = self.update_parsers.get(type(update), None)
parsed_update, handler_type = (

View File

@@ -0,0 +1,38 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
class FileData:
def __init__(
self, *, media_type: int = None, dc_id: int = None, document_id: int = None, access_hash: int = None,
thumb_size: str = None, peer_id: int = None, volume_id: int = None, local_id: int = None, is_big: bool = None,
file_size: int = None, mime_type: str = None, file_name: str = None, date: int = None
):
self.media_type = media_type
self.dc_id = dc_id
self.document_id = document_id
self.access_hash = access_hash
self.thumb_size = thumb_size
self.peer_id = peer_id
self.volume_id = volume_id
self.local_id = local_id
self.is_big = is_big
self.file_size = file_size
self.mime_type = mime_type
self.file_name = file_name
self.date = date

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import base64
import struct
from base64 import b64decode, b64encode
from typing import Union, List
import pyrogram
from . import BaseClient
from ...api import types
@@ -82,3 +87,119 @@ def get_offset_date(dialogs):
return m.date
else:
return 0
def get_input_media_from_file_id(
file_id_str: str,
expected_media_type: int = None
) -> Union[types.InputMediaPhoto, types.InputMediaDocument]:
try:
decoded = decode(file_id_str)
except Exception:
raise ValueError("Failed to decode file_id: {}".format(file_id_str))
else:
media_type = decoded[0]
if expected_media_type is not None:
if media_type != expected_media_type:
media_type_str = BaseClient.MEDIA_TYPE_ID.get(media_type, None)
expected_media_type_str = BaseClient.MEDIA_TYPE_ID.get(expected_media_type, None)
raise ValueError(
'Expected: "{}", got "{}" file_id instead'.format(expected_media_type_str, media_type_str)
)
if media_type in (0, 1, 14):
raise ValueError("This file_id can only be used for download: {}".format(file_id_str))
if media_type == 2:
unpacked = struct.unpack("<iiqqc", decoded)
dc_id, file_id, access_hash, thumb_size = unpacked[1:]
return types.InputMediaPhoto(
id=types.InputPhoto(
id=file_id,
access_hash=access_hash,
file_reference=b""
)
)
if media_type in (3, 4, 5, 8, 9, 10, 13):
unpacked = struct.unpack("<iiqq", decoded)
dc_id, file_id, access_hash = unpacked[1:]
return types.InputMediaDocument(
id=types.InputDocument(
id=file_id,
access_hash=access_hash,
file_reference=b""
)
)
raise ValueError("Unknown media type: {}".format(file_id_str))
def parse_messages(client, messages: types.messages.Messages, replies: int = 1) -> List["pyrogram.Message"]:
users = {i.id: i for i in messages.users}
chats = {i.id: i for i in messages.chats}
if not messages.messages:
return pyrogram.List()
parsed_messages = [
pyrogram.Message._parse(client, message, users, chats, replies=0)
for message in messages.messages
]
if replies:
messages_with_replies = {i.id: getattr(i, "reply_to_msg_id", None) for i in messages.messages}
reply_message_ids = [i[0] for i in filter(lambda x: x[1] is not None, messages_with_replies.items())]
if reply_message_ids:
reply_messages = client.get_messages(
parsed_messages[0].chat.id,
reply_to_message_ids=reply_message_ids,
replies=replies - 1
)
for message in parsed_messages:
reply_id = messages_with_replies[message.message_id]
for reply in reply_messages:
if reply.message_id == reply_id:
message.reply_to_message = reply
return pyrogram.List(parsed_messages)
def parse_deleted_messages(client, update) -> List["pyrogram.Message"]:
messages = update.messages
channel_id = getattr(update, "channel_id", None)
parsed_messages = []
for message in messages:
parsed_messages.append(
pyrogram.Message(
message_id=message,
chat=pyrogram.Chat(
id=int("-100" + str(channel_id)),
type="channel",
client=client
) if channel_id is not None else None,
client=client
)
)
return pyrogram.List(parsed_messages)
def unpack_inline_message_id(inline_message_id: str) -> types.InputBotInlineMessageID:
r = inline_message_id + "=" * (-len(inline_message_id) % 4)
r = struct.unpack("<iqq", base64.b64decode(r, altchars="-_"))
return types.InputBotInlineMessageID(
dc_id=r[0],
id=r[1],
access_hash=r[2]
)

View File

@@ -19,15 +19,15 @@
import re
from .filter import Filter
from ..types.bots import InlineKeyboardMarkup, ReplyKeyboardMarkup
from ..types.bots_and_keyboards import InlineKeyboardMarkup, ReplyKeyboardMarkup
def create(name: str, func: callable, **kwargs) -> type:
"""Use this method to create a Filter.
"""Create a Filter.
Custom filters give you extra control over which updates are allowed or not to be processed by your handlers.
Args:
Parameters:
name (``str``):
Your filter's name. Can be anything you like.
@@ -35,14 +35,14 @@ def create(name: str, func: callable, **kwargs) -> type:
A function that accepts two arguments *(filter, update)* and returns a Boolean: True if the update should be
handled, False otherwise.
The "update" argument type will vary depending on which `Handler <Handlers.html>`_ is coming from.
For example, in a :obj:`MessageHandler <pyrogram.MessageHandler>` the update type will be
a :obj:`Message <pyrogram.Message>`; in a :obj:`CallbackQueryHandler <pyrogram.CallbackQueryHandler>` the
update type will be a :obj:`CallbackQuery <pyrogram.CallbackQuery>`. Your function body can then access the
For example, in a :obj:`MessageHandler` the update type will be
a :obj:`Message`; in a :obj:`CallbackQueryHandler` the
update type will be a :obj:`CallbackQuery`. Your function body can then access the
incoming update and decide whether to allow it or not.
**kwargs (``any``, *optional*):
Any keyword argument you would like to pass. Useful for custom filters that accept parameters (e.g.:
:meth:`Filters.command`, :meth:`Filters.regex`).
:meth:`~Filters.command`, :meth:`~Filters.regex`).
"""
# TODO: unpack kwargs using **kwargs into the dict itself. For Python 3.5+ only
d = {"__call__": func}
@@ -54,9 +54,9 @@ def create(name: str, func: callable, **kwargs) -> type:
class Filters:
"""This class provides access to all library-defined Filters available in Pyrogram.
The Filters listed here are intended to be used with the :obj:`MessageHandler <pyrogram.MessageHandler>` only.
The Filters listed here are intended to be used with the :obj:`MessageHandler` only.
At the moment, if you want to filter updates coming from different `Handlers <Handlers.html>`_ you have to create
your own filters with :meth:`Filters.create` and use them in the same way.
your own filters with :meth:`~Filters.create` and use them in the same way.
"""
create = create
@@ -89,49 +89,49 @@ class Filters:
"""Filter edited messages."""
audio = create("Audio", lambda _, m: bool(m.audio))
"""Filter messages that contain :obj:`Audio <pyrogram.Audio>` objects."""
"""Filter messages that contain :obj:`Audio` objects."""
document = create("Document", lambda _, m: bool(m.document))
"""Filter messages that contain :obj:`Document <pyrogram.Document>` objects."""
"""Filter messages that contain :obj:`Document` objects."""
photo = create("Photo", lambda _, m: bool(m.photo))
"""Filter messages that contain :obj:`Photo <pyrogram.PhotoSize>` objects."""
"""Filter messages that contain :obj:`Photo` objects."""
sticker = create("Sticker", lambda _, m: bool(m.sticker))
"""Filter messages that contain :obj:`Sticker <pyrogram.Sticker>` objects."""
"""Filter messages that contain :obj:`Sticker` objects."""
animation = create("Animation", lambda _, m: bool(m.animation))
"""Filter messages that contain :obj:`Animation <pyrogram.Animation>` objects."""
"""Filter messages that contain :obj:`Animation` objects."""
game = create("Game", lambda _, m: bool(m.game))
"""Filter messages that contain :obj:`Game <pyrogram.Game>` objects."""
"""Filter messages that contain :obj:`Game` objects."""
video = create("Video", lambda _, m: bool(m.video))
"""Filter messages that contain :obj:`Video <pyrogram.Video>` objects."""
"""Filter messages that contain :obj:`Video` objects."""
media_group = create("MediaGroup", lambda _, m: bool(m.media_group_id))
"""Filter messages containing photos or videos being part of an album."""
voice = create("Voice", lambda _, m: bool(m.voice))
"""Filter messages that contain :obj:`Voice <pyrogram.Voice>` note objects."""
"""Filter messages that contain :obj:`Voice` note objects."""
video_note = create("VideoNote", lambda _, m: bool(m.video_note))
"""Filter messages that contain :obj:`VideoNote <pyrogram.VideoNote>` objects."""
"""Filter messages that contain :obj:`VideoNote` objects."""
contact = create("Contact", lambda _, m: bool(m.contact))
"""Filter messages that contain :obj:`Contact <pyrogram.Contact>` objects."""
"""Filter messages that contain :obj:`Contact` objects."""
location = create("Location", lambda _, m: bool(m.location))
"""Filter messages that contain :obj:`Location <pyrogram.Location>` objects."""
"""Filter messages that contain :obj:`Location` objects."""
venue = create("Venue", lambda _, m: bool(m.venue))
"""Filter messages that contain :obj:`Venue <pyrogram.Venue>` objects."""
"""Filter messages that contain :obj:`Venue` objects."""
web_page = create("WebPage", lambda _, m: m.web_page)
"""Filter messages sent with a webpage preview."""
poll = create("Poll", lambda _, m: m.poll)
"""Filter messages that contain :obj:`Poll <pyrogram.Poll>` objects."""
"""Filter messages that contain :obj:`Poll` objects."""
private = create("Private", lambda _, m: bool(m.chat and m.chat.type == "private"))
"""Filter messages sent in private chats."""
@@ -191,35 +191,19 @@ class Filters:
"""Filter messages sent via inline bots"""
service = create("Service", lambda _, m: bool(m.service))
"""Filter service messages. A service message contains any of the following fields set
"""Filter service messages.
- left_chat_member
- new_chat_title
- new_chat_photo
- delete_chat_photo
- group_chat_created
- supergroup_chat_created
- channel_chat_created
- migrate_to_chat_id
- migrate_from_chat_id
- pinned_message
- game_score"""
A service message contains any of the following fields set: *left_chat_member*,
*new_chat_title*, *new_chat_photo*, *delete_chat_photo*, *group_chat_created*, *supergroup_chat_created*,
*channel_chat_created*, *migrate_to_chat_id*, *migrate_from_chat_id*, *pinned_message*, *game_score*.
"""
media = create("Media", lambda _, m: bool(m.media))
"""Filter media messages. A media message contains any of the following fields set
"""Filter media messages.
- audio
- document
- photo
- sticker
- video
- animation
- voice
- video_note
- contact
- location
- venue
- poll"""
A media message contains any of the following fields set: *audio*, *document*, *photo*, *sticker*, *video*,
*animation*, *voice*, *video_note*, *contact*, *location*, *venue*, *poll*.
"""
@staticmethod
def command(
@@ -230,12 +214,12 @@ class Filters:
):
"""Filter commands, i.e.: text messages starting with "/" or any other custom prefix.
Args:
Parameters:
commands (``str`` | ``list``):
The command or list of commands as string the filter should look for.
Examples: "start", ["start", "help", "settings"]. When a message text containing
a command arrives, the command itself and its arguments will be stored in the *command*
field of the :class:`Message <pyrogram.Message>`.
field of the :obj:`Message`.
prefix (``str`` | ``list``, *optional*):
A prefix or a list of prefixes as string the filter should look for.
@@ -275,11 +259,11 @@ class Filters:
def regex(pattern, flags: int = 0):
"""Filter messages that match a given RegEx pattern.
Args:
Parameters:
pattern (``str``):
The RegEx pattern as string, it will be applied to the text of a message. When a pattern matches,
all the `Match Objects <https://docs.python.org/3/library/re.html#match-objects>`_
are stored in the *matches* field of the :class:`Message <pyrogram.Message>` itself.
are stored in the *matches* field of the :obj:`Message` itself.
flags (``int``, *optional*):
RegEx flags.
@@ -298,7 +282,7 @@ class Filters:
You can use `set bound methods <https://docs.python.org/3/library/stdtypes.html#set>`_ to manipulate the
users container.
Args:
Parameters:
users (``int`` | ``str`` | ``list``):
Pass one or more user ids/usernames to filter users.
For you yourself, "me" or "self" can be used as well.
@@ -329,7 +313,7 @@ class Filters:
You can use `set bound methods <https://docs.python.org/3/library/stdtypes.html#set>`_ to manipulate the
chats container.
Args:
Parameters:
chats (``int`` | ``str`` | ``list``):
Pass one or more chat ids/usernames to filter chats.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -355,4 +339,15 @@ class Filters:
and message.from_user.is_self
and not message.outgoing)))
@staticmethod
def callback_data(data: str or bytes):
"""Filter callback queries for their data.
Parameters:
data (``str`` | ``bytes``):
Pass the data you want to filter for.
"""
return create("CallbackData", lambda flt, cb: cb.data == flt.data, data=data)
dan = create("Dan", lambda _, m: bool(m.from_user and m.from_user.id == 23122162))

View File

@@ -21,10 +21,11 @@ from .deleted_messages_handler import DeletedMessagesHandler
from .disconnect_handler import DisconnectHandler
from .inline_query_handler import InlineQueryHandler
from .message_handler import MessageHandler
from .poll_handler import PollHandler
from .raw_update_handler import RawUpdateHandler
from .user_status_handler import UserStatusHandler
__all__ = [
"MessageHandler", "DeletedMessagesHandler", "CallbackQueryHandler", "RawUpdateHandler", "DisconnectHandler",
"UserStatusHandler", "InlineQueryHandler"
"UserStatusHandler", "InlineQueryHandler", "PollHandler"
]

View File

@@ -21,22 +21,22 @@ from .handler import Handler
class CallbackQueryHandler(Handler):
"""The CallbackQuery handler class. Used to handle callback queries coming from inline buttons.
It is intended to be used with :meth:`add_handler() <pyrogram.Client.add_handler>`
It is intended to be used with :meth:`~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`on_callback_query() <pyrogram.Client.on_callback_query>` decorator.
:meth:`~Client.on_callback_query` decorator.
Args:
Parameters:
callback (``callable``):
Pass a function that will be called when a new CallbackQuery arrives. It takes *(client, callback_query)*
as positional arguments (look at the section below for a detailed description).
filters (:obj:`Filters <pyrogram.Filters>`):
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of callback queries to be passed
in your callback function.
Other parameters:
client (:obj:`Client <pyrogram.Client>`):
client (:obj:`Client`):
The Client itself, useful when you want to call other API methods inside the message handler.
callback_query (:obj:`CallbackQuery <pyrogram.CallbackQuery>`):

View File

@@ -20,32 +20,31 @@ from .handler import Handler
class DeletedMessagesHandler(Handler):
"""The deleted Messages handler class. Used to handle deleted messages coming from any chat
(private, group, channel). It is intended to be used with
:meth:`add_handler() <pyrogram.Client.add_handler>`
"""The deleted messages handler class. Used to handle deleted messages coming from any chat
(private, group, channel). It is intended to be used with :meth:`~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`on_deleted_messages() <pyrogram.Client.on_deleted_messages>` decorator.
:meth:`~Client.on_deleted_messages` decorator.
Args:
Parameters:
callback (``callable``):
Pass a function that will be called when one or more Messages have been deleted.
Pass a function that will be called when one or more messages have been deleted.
It takes *(client, messages)* as positional arguments (look at the section below for a detailed description).
filters (:obj:`Filters <pyrogram.Filters>`):
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of messages to be passed
in your callback function.
Other parameters:
client (:obj:`Client <pyrogram.Client>`):
client (:obj:`Client`):
The Client itself, useful when you want to call other API methods inside the message handler.
messages (:obj:`Messages <pyrogram.Messages>`):
The deleted messages.
messages (List of :obj:`Message`):
The deleted messages, as list.
"""
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, messages):
return super().check(messages.messages[0])
return super().check(messages[0])

View File

@@ -21,18 +21,18 @@ from .handler import Handler
class DisconnectHandler(Handler):
"""The Disconnect handler class. Used to handle disconnections. It is intended to be used with
:meth:`add_handler() <pyrogram.Client.add_handler>`
:meth:~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`on_disconnect() <pyrogram.Client.on_disconnect>` decorator.
:meth:`~Client.on_disconnect` decorator.
Args:
Parameters:
callback (``callable``):
Pass a function that will be called when a disconnection occurs. It takes *(client)*
as positional argument (look at the section below for a detailed description).
Other parameters:
client (:obj:`Client <pyrogram.Client>`):
client (:obj:`Client`):
The Client itself. Useful, for example, when you want to change the proxy before a new connection
is established.
"""

View File

@@ -21,34 +21,27 @@ from .handler import Handler
class InlineQueryHandler(Handler):
"""The InlineQuery handler class. Used to handle inline queries.
It is intended to be used with :meth:`add_handler() <pyrogram.Client.add_handler>`
It is intended to be used with :meth:`~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`on_inline_query() <pyrogram.Client.on_inline_query>` decorator.
:meth:`~Client.on_inline_query` decorator.
Args:
Parameters:
callback (``callable``):
Pass a function that will be called when a new InlineQuery arrives. It takes *(client, inline_query)*
as positional arguments (look at the section below for a detailed description).
filters (:obj:`Filters <pyrogram.Filters>`):
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of inline queries to be passed
in your callback function.
Other parameters:
client (:obj:`Client <pyrogram.Client>`):
client (:obj:`Client`):
The Client itself, useful when you want to call other API methods inside the inline query handler.
inline_query (:obj:`InlineQuery <pyrogram.InlineQuery>`):
inline_query (:obj:`InlineQuery`):
The received inline query.
"""
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, callback_query):
return (
self.filters(callback_query)
if callable(self.filters)
else True
)

View File

@@ -21,26 +21,25 @@ from .handler import Handler
class MessageHandler(Handler):
"""The Message handler class. Used to handle text, media and service messages coming from
any chat (private, group, channel). It is intended to be used with
:meth:`add_handler() <pyrogram.Client.add_handler>`
any chat (private, group, channel). It is intended to be used with :meth:`~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`on_message() <pyrogram.Client.on_message>` decorator.
:meth:`~Client.on_message` decorator.
Args:
Parameters:
callback (``callable``):
Pass a function that will be called when a new Message arrives. It takes *(client, message)*
as positional arguments (look at the section below for a detailed description).
filters (:obj:`Filters <pyrogram.Filters>`):
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of messages to be passed
in your callback function.
Other parameters:
client (:obj:`Client <pyrogram.Client>`):
client (:obj:`Client`):
The Client itself, useful when you want to call other API methods inside the message handler.
message (:obj:`Message <pyrogram.Message>`):
message (:obj:`Message`):
The received message.
"""

View File

@@ -0,0 +1,48 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .handler import Handler
class PollHandler(Handler):
"""The Poll handler class. Used to handle polls updates.
It is intended to be used with :meth:`~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`~Client.on_poll` decorator.
Parameters:
callback (``callable``):
Pass a function that will be called when a new poll update arrives. It takes *(client, poll)*
as positional arguments (look at the section below for a detailed description).
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of polls to be passed
in your callback function.
Other parameters:
client (:obj:`Client`):
The Client itself, useful when you want to call other API methods inside the poll handler.
poll (:obj:`Poll`):
The received poll.
"""
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)

View File

@@ -21,19 +21,19 @@ from .handler import Handler
class RawUpdateHandler(Handler):
"""The Raw Update handler class. Used to handle raw updates. It is intended to be used with
:meth:`add_handler() <pyrogram.Client.add_handler>`
:meth:`~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`on_raw_update() <pyrogram.Client.on_raw_update>` decorator.
:meth:`~Client.on_raw_update` decorator.
Args:
Parameters:
callback (``callable``):
A function that will be called when a new update is received from the server. It takes
*(client, update, users, chats)* as positional arguments (look at the section below for
a detailed description).
Other Parameters:
client (:class:`Client <pyrogram.Client>`):
client (:obj:`Client`):
The Client itself, useful when you want to call other API methods inside the update handler.
update (``Update``):

View File

@@ -21,25 +21,25 @@ from .handler import Handler
class UserStatusHandler(Handler):
"""The UserStatus handler class. Used to handle user status updates (user going online or offline).
It is intended to be used with :meth:`add_handler() <pyrogram.Client.add_handler>`
It is intended to be used with :meth:`~Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`on_user_status() <pyrogram.Client.on_user_status>` decorator.
:meth:`~Client.on_user_status` decorator.
Args:
Parameters:
callback (``callable``):
Pass a function that will be called when a new UserStatus update arrives. It takes *(client, user_status)*
as positional arguments (look at the section below for a detailed description).
filters (:obj:`Filters <pyrogram.Filters>`):
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of messages to be passed
in your callback function.
Other parameters:
client (:obj:`Client <pyrogram.Client>`):
client (:obj:`Client`):
The Client itself, useful when you want to call other API methods inside the user status handler.
user_status (:obj:`UserStatus <pyrogram.UserStatus>`):
user_status (:obj:`UserStatus`):
The received UserStatus update.
"""

View File

@@ -29,10 +29,10 @@ class AnswerCallbackQuery(BaseClient):
url: str = None,
cache_time: int = 0
):
"""Use this method to send answers to callback queries sent from inline keyboards.
"""Send answers to callback queries sent from inline keyboards.
The answer will be displayed to the user as a notification at the top of the chat screen or as an alert.
Args:
Parameters:
callback_query_id (``str``):
Unique identifier for the query to be answered.
@@ -54,10 +54,10 @@ class AnswerCallbackQuery(BaseClient):
Telegram apps will support caching starting in version 3.14. Defaults to 0.
Returns:
True, on success.
``bool``: True, on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
return self.send(
functions.messages.SetBotCallbackAnswer(

View File

@@ -34,10 +34,10 @@ class AnswerInlineQuery(BaseClient):
switch_pm_text: str = "",
switch_pm_parameter: str = ""
):
"""Use this method to send answers to an inline query.
"""Send answers to an inline query.
No more than 50 results per query are allowed.
Args:
Parameters:
inline_query_id (``str``):
Unique identifier for the answered query.
@@ -73,7 +73,10 @@ class AnswerInlineQuery(BaseClient):
where they wanted to use the bot's inline capabilities.
Returns:
On success, True is returned.
``bool``: True, on success.
Raises:
RPCError: In case of a Telegram RPC error.
"""
return self.send(
functions.messages.SetInlineBotResults(

View File

@@ -16,7 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Union
from typing import Union, List
import pyrogram
from pyrogram.api import functions
@@ -29,10 +29,10 @@ class GetGameHighScores(BaseClient):
user_id: Union[int, str],
chat_id: Union[int, str],
message_id: int = None
):
"""Use this method to get data for high score tables.
) -> List["pyrogram.GameHighScore"]:
"""Get data for high score tables.
Args:
Parameters:
user_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -49,20 +49,19 @@ class GetGameHighScores(BaseClient):
Required if inline_message_id is not specified.
Returns:
On success, a :obj:`GameHighScores <pyrogram.GameHighScores>` object is returned.
List of :obj:`GameHighScore`: On success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
# TODO: inline_message_id
return pyrogram.GameHighScores._parse(
self,
self.send(
functions.messages.GetGameHighScores(
peer=self.resolve_peer(chat_id),
id=message_id,
user_id=self.resolve_peer(user_id)
)
r = self.send(
functions.messages.GetGameHighScores(
peer=self.resolve_peer(chat_id),
id=message_id,
user_id=self.resolve_peer(user_id)
)
)
return pyrogram.List(pyrogram.GameHighScore._parse(self, score, r.users) for score in r.scores)

View File

@@ -19,8 +19,8 @@
from typing import Union
from pyrogram.api import functions, types
from pyrogram.errors import UnknownError
from pyrogram.client.ext import BaseClient
from pyrogram.errors import UnknownError
class GetInlineBotResults(BaseClient):
@@ -32,10 +32,10 @@ class GetInlineBotResults(BaseClient):
latitude: float = None,
longitude: float = None
):
"""Use this method to get bot results via inline queries.
"""Get bot results via inline queries.
You can then send a result using :obj:`send_inline_bot_result <pyrogram.Client.send_inline_bot_result>`
Args:
Parameters:
bot (``int`` | ``str``):
Unique identifier of the inline bot you want to get results from. You can specify
a @username (str) or a bot ID (int).
@@ -55,11 +55,11 @@ class GetInlineBotResults(BaseClient):
Useful for location-based results only.
Returns:
On Success, :obj:`BotResults <pyrogram.api.types.messages.BotResults>` is returned.
:obj:`BotResults <pyrogram.api.types.messages.BotResults>`: On Success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``TimeoutError`` if the bot fails to answer within 10 seconds
RPCError: In case of a Telegram RPC error.
TimeoutError: In case the bot fails to answer within 10 seconds.
"""
# TODO: Don't return the raw type

View File

@@ -27,12 +27,13 @@ class RequestCallbackAnswer(BaseClient):
self,
chat_id: Union[int, str],
message_id: int,
callback_data: bytes
callback_data: Union[str, bytes],
timeout: int = 10
):
"""Use this method to request a callback answer from bots.
"""Request a callback answer from bots.
This is the equivalent of clicking an inline button containing callback data.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -41,23 +42,30 @@ class RequestCallbackAnswer(BaseClient):
message_id (``int``):
The message id the inline keyboard is attached on.
callback_data (``bytes``):
callback_data (``str`` | ``bytes``):
Callback data associated with the inline button you want to get the answer from.
timeout (``int``, *optional*):
Timeout in seconds.
Returns:
The answer containing info useful for clients to display a notification at the top of the chat screen
or as an alert.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``TimeoutError`` if the bot fails to answer within 10 seconds.
RPCError: In case of a Telegram RPC error.
TimeoutError: In case the bot fails to answer within 10 seconds.
"""
# Telegram only wants bytes, but we are allowed to pass strings too.
data = bytes(callback_data, "utf-8") if isinstance(callback_data, str) else callback_data
return self.send(
functions.messages.GetBotCallbackAnswer(
peer=self.resolve_peer(chat_id),
msg_id=message_id,
data=callback_data
data=data
),
retries=0,
timeout=10
timeout=timeout
)

View File

@@ -37,9 +37,9 @@ class SendGame(BaseClient):
"pyrogram.ForceReply"
] = None
) -> "pyrogram.Message":
"""Use this method to send a game.
"""Send a game.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -60,10 +60,10 @@ class SendGame(BaseClient):
If not empty, the first button must launch the game.
Returns:
On success, the sent :obj:`Message` is returned.
:obj:`Message`: On success, the sent game message is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
r = self.send(
functions.messages.SendMedia(

View File

@@ -32,10 +32,10 @@ class SendInlineBotResult(BaseClient):
reply_to_message_id: int = None,
hide_via: bool = None
):
"""Use this method to send an inline bot result.
"""Send an inline bot result.
Bot results can be retrieved using :obj:`get_inline_bot_results <pyrogram.Client.get_inline_bot_results>`
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -58,10 +58,10 @@ class SendInlineBotResult(BaseClient):
Sends the message with *via @bot* hidden.
Returns:
On success, the sent Message is returned.
:obj:`Message`: On success, the sent inline result message is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
return self.send(
functions.messages.SendInlineBotResult(

View File

@@ -32,11 +32,11 @@ class SetGameScore(BaseClient):
disable_edit_message: bool = None,
chat_id: Union[int, str] = None,
message_id: int = None
):
) -> Union["pyrogram.Message", bool]:
# inline_message_id: str = None): TODO Add inline_message_id
"""Use this method to set the score of the specified user in a game.
"""Set the score of the specified user in a game.
Args:
Parameters:
user_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -63,12 +63,11 @@ class SetGameScore(BaseClient):
Required if inline_message_id is not specified.
Returns:
On success, if the message was sent by the bot, returns the edited :obj:`Message <pyrogram.Message>`,
otherwise returns True.
:obj:`Message` | ``bool``: On success, if the message was sent by the bot, the edited message is returned,
True otherwise.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
:class:`BotScoreNotModified` if the new score is not greater than the user's current score in the chat and force is False.
RPCError: In case of a Telegram RPC error.
"""
r = self.send(
functions.messages.SetGameScore(

View File

@@ -16,14 +16,15 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .archive_chats import ArchiveChats
from .delete_chat_photo import DeleteChatPhoto
from .export_chat_invite_link import ExportChatInviteLink
from .get_chat import GetChat
from .get_chat_member import GetChatMember
from .get_chat_members import GetChatMembers
from .get_chat_members_count import GetChatMembersCount
from .get_chat_preview import GetChatPreview
from .get_dialogs import GetDialogs
from .get_dialogs_count import GetDialogsCount
from .iter_chat_members import IterChatMembers
from .iter_dialogs import IterDialogs
from .join_chat import JoinChat
@@ -36,6 +37,7 @@ from .restrict_chat_member import RestrictChatMember
from .set_chat_description import SetChatDescription
from .set_chat_photo import SetChatPhoto
from .set_chat_title import SetChatTitle
from .unarchive_chats import UnarchiveChats
from .unban_chat_member import UnbanChatMember
from .unpin_chat_message import UnpinChatMessage
from .update_chat_username import UpdateChatUsername
@@ -60,10 +62,12 @@ class Chats(
UnpinChatMessage,
GetDialogs,
GetChatMembersCount,
GetChatPreview,
IterDialogs,
IterChatMembers,
UpdateChatUsername,
RestrictChat
RestrictChat,
GetDialogsCount,
ArchiveChats,
UnarchiveChats
):
pass

View File

@@ -0,0 +1,58 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Union, List
from pyrogram.api import functions, types
from ...ext import BaseClient
class ArchiveChats(BaseClient):
def archive_chats(
self,
chat_ids: Union[int, str, List[Union[int, str]]],
) -> bool:
"""Archive one or more chats.
Parameters:
chat_ids (``int`` | ``str`` | List[``int``, ``str``]):
Unique identifier (int) or username (str) of the target chat.
You can also pass a list of ids (int) or usernames (str).
Returns:
``bool``: On success, True is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
if not isinstance(chat_ids, list):
chat_ids = [chat_ids]
self.send(
functions.folders.EditPeerFolders(
folder_peers=[
types.InputFolderPeer(
peer=self.resolve_peer(chat),
folder_id=1
) for chat in chat_ids
]
)
)
return True

View File

@@ -27,7 +27,7 @@ class DeleteChatPhoto(BaseClient):
self,
chat_id: Union[int, str]
) -> bool:
"""Use this method to delete a chat photo.
"""Delete a chat photo.
Photos can't be changed for private chats.
You must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -35,15 +35,15 @@ class DeleteChatPhoto(BaseClient):
In regular groups (non-supergroups), this method will only work if the "All Members Are Admins"
setting is off.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
``ValueError`` if a chat_id belongs to user.
"""
peer = self.resolve_peer(chat_id)

View File

@@ -27,27 +27,34 @@ class ExportChatInviteLink(BaseClient):
self,
chat_id: Union[int, str]
) -> str:
"""Use this method to generate a new invite link for a chat; any previously generated link is revoked.
"""Generate a new invite link for a chat; any previously generated link is revoked.
You must be an administrator in the chat for this to work and have the appropriate admin rights.
Args:
.. note ::
Each administrator in a chat generates their own invite links. Bots can't use invite links generated by
other administrators. If you want your bot to work with invite links, it will need to generate its own link
using this method after this the link will become available to the bot via the :meth:`~Client.get_chat`
method. If your bot needs to generate a new invite link replacing its previous one, use this method again.
Parameters:
chat_id (``int`` | ``str``):
Unique identifier for the target chat or username of the target channel/supergroup
(in the format @username).
Returns:
On success, the exported invite link as string is returned.
``str``: On success, the exported invite link is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
peer = self.resolve_peer(chat_id)
if isinstance(peer, types.InputPeerChat):
return self.send(
functions.messages.ExportChatInvite(
peer=peer.chat_id
peer=peer
)
).link
elif isinstance(peer, types.InputPeerChannel):

View File

@@ -27,37 +27,37 @@ class GetChat(BaseClient):
def get_chat(
self,
chat_id: Union[int, str]
) -> "pyrogram.Chat":
"""Use this method to get up to date information about the chat.
) -> Union["pyrogram.Chat", "pyrogram.ChatPreview"]:
"""Get up to date information about a chat.
Information include current name of the user for one-on-one conversations, current username of a user, group or
channel, etc.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
Unique identifier for the target chat in form of a *t.me/joinchat/* link, identifier (int) or username
of the target channel/supergroup (in the format @username).
Returns:
On success, a :obj:`Chat <pyrogram.Chat>` object is returned.
:obj:`Chat` | :obj:`ChatPreview`: On success, if you've already joined the chat, a chat object is returned,
otherwise, a chat preview object is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``ValueError`` in case the chat invite link refers to a chat you haven't joined yet.
RPCError: In case of a Telegram RPC error.
ValueError: In case the chat invite link points to a chat you haven't joined yet.
"""
match = self.INVITE_LINK_RE.match(str(chat_id))
if match:
h = match.group(1)
r = self.send(
functions.messages.CheckChatInvite(
hash=h
hash=match.group(1)
)
)
if isinstance(r, types.ChatInvite):
raise ValueError("You haven't joined \"t.me/joinchat/{}\" yet".format(h))
return pyrogram.ChatPreview._parse(self, r)
self.fetch_peers([r.chat])

View File

@@ -30,43 +30,52 @@ class GetChatMember(BaseClient):
chat_id: Union[int, str],
user_id: Union[int, str]
) -> "pyrogram.ChatMember":
"""Use this method to get information about one member of a chat.
"""Get information about one member of a chat.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
user_id (``int`` | ``str``)::
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
Unique identifier (int) or username (str) of the target user.
For you yourself you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
Returns:
On success, a :obj:`ChatMember <pyrogram.ChatMember>` object is returned.
:obj:`ChatMember`: On success, a chat member is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
chat_id = self.resolve_peer(chat_id)
user_id = self.resolve_peer(user_id)
chat = self.resolve_peer(chat_id)
user = self.resolve_peer(user_id)
if isinstance(chat_id, types.InputPeerChat):
full_chat = self.send(
if isinstance(chat, types.InputPeerChat):
r = self.send(
functions.messages.GetFullChat(
chat_id=chat_id.chat_id
chat_id=chat.chat_id
)
)
for member in pyrogram.ChatMembers._parse(self, full_chat).chat_members:
if member.user.is_self:
return member
members = r.full_chat.participants.participants
users = {i.id: i for i in r.users}
for member in members:
member = pyrogram.ChatMember._parse(self, member, users)
if isinstance(user, types.InputPeerSelf):
if member.user.is_self:
return member
else:
if member.user.id == user.user_id:
return member
else:
raise UserNotParticipant
elif isinstance(chat_id, types.InputPeerChannel):
elif isinstance(chat, types.InputPeerChannel):
r = self.send(
functions.channels.GetParticipant(
channel=chat_id,
user_id=user_id
channel=chat,
user_id=user
)
)

View File

@@ -18,7 +18,7 @@
import logging
import time
from typing import Union
from typing import Union, List
import pyrogram
from pyrogram.api import functions, types
@@ -45,29 +45,30 @@ class GetChatMembers(BaseClient):
limit: int = 200,
query: str = "",
filter: str = Filters.ALL
) -> "pyrogram.ChatMembers":
"""Use this method to get a chunk of the members list of a chat.
) -> List["pyrogram.ChatMember"]:
"""Get a chunk of the members list of a chat.
You can get up to 200 chat members at once.
A chat can be either a basic group, a supergroup or a channel.
You must be admin to retrieve the members list of a channel (also known as "subscribers").
For a more convenient way of getting chat members see :meth:`iter_chat_members`.
For a more convenient way of getting chat members see :meth:`~Client.iter_chat_members`.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
offset (``int``, *optional*):
Sequential number of the first member to be returned.
Defaults to 0 [1]_.
Only applicable to supergroups and channels. Defaults to 0 [1]_.
limit (``int``, *optional*):
Limits the number of members to be retrieved.
Only applicable to supergroups and channels.
Defaults to 200, which is also the maximum server limit allowed per method call.
query (``str``, *optional*):
Query string to filter members based on their display names and usernames.
Defaults to "" (empty string) [2]_.
Only applicable to supergroups and channels. Defaults to "" (empty string) [2]_.
filter (``str``, *optional*):
Filter used to select the kind of members you want to retrieve. Only applicable for supergroups
@@ -78,6 +79,7 @@ class GetChatMembers(BaseClient):
*"bots"* - bots only,
*"recent"* - recent members only,
*"administrators"* - chat administrators only.
Only applicable to supergroups and channels.
Defaults to *"all"*.
.. [1] Server limit: on supergroups, you can get up to 10,000 members for a single query and up to 200 members
@@ -86,23 +88,25 @@ class GetChatMembers(BaseClient):
.. [2] A query string is applicable only for *"all"*, *"kicked"* and *"restricted"* filters only.
Returns:
On success, a :obj:`ChatMembers` object is returned.
List of :obj:`ChatMember`: On success, a list of chat members is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``ValueError`` if you used an invalid filter or a chat_id that belongs to a user.
RPCError: In case of a Telegram RPC error.
ValueError: In case you used an invalid filter or a chat id that belongs to a user.
"""
peer = self.resolve_peer(chat_id)
if isinstance(peer, types.InputPeerChat):
return pyrogram.ChatMembers._parse(
self,
self.send(
functions.messages.GetFullChat(
chat_id=peer.chat_id
)
r = self.send(
functions.messages.GetFullChat(
chat_id=peer.chat_id
)
)
members = r.full_chat.participants.participants
users = {i.id: i for i in r.users}
return pyrogram.List(pyrogram.ChatMember._parse(self, member, users) for member in members)
elif isinstance(peer, types.InputPeerChannel):
filter = filter.lower()
@@ -123,18 +127,20 @@ class GetChatMembers(BaseClient):
while True:
try:
return pyrogram.ChatMembers._parse(
self,
self.send(
functions.channels.GetParticipants(
channel=peer,
filter=filter,
offset=offset,
limit=limit,
hash=0
)
r = self.send(
functions.channels.GetParticipants(
channel=peer,
filter=filter,
offset=offset,
limit=limit,
hash=0
)
)
members = r.participants
users = {i.id: i for i in r.users}
return pyrogram.List(pyrogram.ChatMember._parse(self, member, users) for member in members)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)

View File

@@ -27,18 +27,18 @@ class GetChatMembersCount(BaseClient):
self,
chat_id: Union[int, str]
) -> int:
"""Use this method to get the number of members in a chat.
"""Get the number of members in a chat.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
Returns:
On success, an integer is returned.
``int``: On success, the chat members count is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``ValueError`` if a chat_id belongs to user.
RPCError: In case of a Telegram RPC error.
ValueError: In case a chat id belongs to user.
"""
peer = self.resolve_peer(chat_id)

View File

@@ -1,65 +0,0 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import pyrogram
from pyrogram.api import functions, types
from ...ext import BaseClient
class GetChatPreview(BaseClient):
def get_chat_preview(
self,
invite_link: str
):
"""Use this method to get the preview of a chat using the invite link.
This method only returns a chat preview, if you want to join a chat use :meth:`join_chat`
Args:
invite_link (``str``):
Unique identifier for the target chat in form of *t.me/joinchat/* links.
Returns:
Either :obj:`Chat` or :obj:`ChatPreview`, depending on whether you already joined the chat or not.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``ValueError`` in case of an invalid invite_link.
"""
match = self.INVITE_LINK_RE.match(invite_link)
if match:
r = self.send(
functions.messages.CheckChatInvite(
hash=match.group(1)
)
)
if isinstance(r, types.ChatInvite):
return pyrogram.ChatPreview._parse(self, r)
if isinstance(r, types.ChatInviteAlready):
chat = r.chat
if isinstance(chat, types.Chat):
return pyrogram.Chat._parse_chat_chat(self, chat)
if isinstance(chat, types.Channel):
return pyrogram.Chat._parse_channel_chat(self, chat)
else:
raise ValueError("The invite_link is invalid")

View File

@@ -18,6 +18,7 @@
import logging
import time
from typing import List
import pyrogram
from pyrogram.api import functions, types
@@ -33,13 +34,13 @@ class GetDialogs(BaseClient):
offset_date: int = 0,
limit: int = 100,
pinned_only: bool = False
) -> "pyrogram.Dialogs":
"""Use this method to get a chunk of the user's dialogs.
) -> List["pyrogram.Dialog"]:
"""Get a chunk of the user's dialogs.
You can get up to 100 dialogs at once.
For a more convenient way of getting a user's dialogs see :meth:`iter_dialogs`.
For a more convenient way of getting a user's dialogs see :meth:`~Client.iter_dialogs`.
Args:
Parameters:
offset_date (``int``):
The offset date in Unix time taken from the top message of a :obj:`Dialog`.
Defaults to 0. Valid for non-pinned dialogs only.
@@ -53,16 +54,16 @@ class GetDialogs(BaseClient):
Defaults to False.
Returns:
On success, a :obj:`Dialogs` object is returned.
List of :obj:`Dialog`: On success, a list of dialogs is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
while True:
try:
if pinned_only:
r = self.send(functions.messages.GetPinnedDialogs())
r = self.send(functions.messages.GetPinnedDialogs(folder_id=0))
else:
r = self.send(
functions.messages.GetDialogs(
@@ -80,4 +81,32 @@ class GetDialogs(BaseClient):
else:
break
return pyrogram.Dialogs._parse(self, r)
users = {i.id: i for i in r.users}
chats = {i.id: i for i in r.chats}
messages = {}
for message in r.messages:
to_id = message.to_id
if isinstance(to_id, types.PeerUser):
if message.out:
chat_id = to_id.user_id
else:
chat_id = message.from_id
elif isinstance(to_id, types.PeerChat):
chat_id = -to_id.chat_id
else:
chat_id = int("-100" + str(to_id.channel_id))
messages[chat_id] = pyrogram.Message._parse(self, message, users, chats)
parsed_dialogs = []
for dialog in r.dialogs:
if not isinstance(dialog, types.Dialog):
continue
parsed_dialogs.append(pyrogram.Dialog._parse(self, dialog, messages, users, chats))
return pyrogram.List(parsed_dialogs)

View File

@@ -0,0 +1,54 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from pyrogram.api import functions, types
from ...ext import BaseClient
class GetDialogsCount(BaseClient):
def get_dialogs_count(self, pinned_only: bool = False) -> int:
"""Get the total count of your dialogs.
pinned_only (``bool``, *optional*):
Pass True if you want to count only pinned dialogs.
Defaults to False.
Returns:
``int``: On success, the dialogs count is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
if pinned_only:
return len(self.send(functions.messages.GetPinnedDialogs(folder_id=0)).dialogs)
else:
r = self.send(
functions.messages.GetDialogs(
offset_date=0,
offset_id=0,
offset_peer=types.InputPeerEmpty(),
limit=1,
hash=0
)
)
if isinstance(r, types.messages.Dialogs):
return len(r.dialogs)
else:
return r.count

View File

@@ -45,13 +45,13 @@ class IterChatMembers(BaseClient):
query: str = "",
filter: str = Filters.ALL
) -> Generator["pyrogram.ChatMember", None, None]:
"""Use this method to iterate through the members of a chat sequentially.
"""Iterate through the members of a chat sequentially.
This convenience method does the same as repeatedly calling :meth:`get_chat_members` in a loop, thus saving you
This convenience method does the same as repeatedly calling :meth:`~Client.get_chat_members` in a loop, thus saving you
from the hassle of setting up boilerplate code. It is useful for getting the whole members list of a chat with
a single call.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -75,10 +75,10 @@ class IterChatMembers(BaseClient):
Defaults to *"all"*.
Returns:
A generator yielding :obj:`ChatMember <pyrogram.ChatMember>` objects.
``Generator``: A generator yielding :obj:`ChatMember` objects.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
current = 0
yielded = set()
@@ -106,7 +106,7 @@ class IterChatMembers(BaseClient):
limit=limit,
query=q,
filter=filter
).chat_members
)
if not chat_members:
break

View File

@@ -26,14 +26,15 @@ class IterDialogs(BaseClient):
def iter_dialogs(
self,
offset_date: int = 0,
limit: int = 0
limit: int = None
) -> Generator["pyrogram.Dialog", None, None]:
"""Use this method to iterate through a user's dialogs sequentially.
"""Iterate through a user's dialogs sequentially.
This convenience method does the same as repeatedly calling :meth:`get_dialogs` in a loop, thus saving you from
the hassle of setting up boilerplate code. It is useful for getting the whole dialogs list with a single call.
This convenience method does the same as repeatedly calling :meth:`~Client.get_dialogs` in a loop, thus saving
you from the hassle of setting up boilerplate code. It is useful for getting the whole dialogs list with a
single call.
Args:
Parameters:
offset_date (``int``):
The offset date in Unix time taken from the top message of a :obj:`Dialog`.
Defaults to 0 (most recent dialog).
@@ -43,10 +44,10 @@ class IterDialogs(BaseClient):
By default, no limit is applied and all dialogs are returned.
Returns:
A generator yielding :obj:`Dialog <pyrogram.Dialog>` objects.
``Generator``: A generator yielding :obj:`Dialog` objects.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
current = 0
total = limit or (1 << 31) - 1
@@ -54,7 +55,7 @@ class IterDialogs(BaseClient):
pinned_dialogs = self.get_dialogs(
pinned_only=True
).dialogs
)
for dialog in pinned_dialogs:
yield dialog
@@ -68,7 +69,7 @@ class IterDialogs(BaseClient):
dialogs = self.get_dialogs(
offset_date=offset_date,
limit=limit
).dialogs
)
if not dialogs:
return

View File

@@ -26,18 +26,18 @@ class JoinChat(BaseClient):
self,
chat_id: str
):
"""Use this method to join a group chat or channel.
"""Join a group chat or channel.
Args:
Parameters:
chat_id (``str``):
Unique identifier for the target chat in form of a *t.me/joinchat/* link or username of the target
channel/supergroup (in the format @username).
Returns:
On success, a :obj:`Chat <pyrogram.Chat>` object is returned.
:obj:`Chat`: On success, a chat object is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
match = self.INVITE_LINK_RE.match(chat_id)

View File

@@ -30,7 +30,7 @@ class KickChatMember(BaseClient):
user_id: Union[int, str],
until_date: int = 0
) -> Union["pyrogram.Message", bool]:
"""Use this method to kick a user from a group, a supergroup or a channel.
"""Kick a user from a group, a supergroup or a channel.
In the case of supergroups and channels, the user will not be able to return to the group on their own using
invite links, etc., unless unbanned first. You must be an administrator in the chat for this to work and must
have the appropriate admin rights.
@@ -40,7 +40,7 @@ class KickChatMember(BaseClient):
off in the target group. Otherwise members may only be removed by the group's creator or by the member
that added them.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -54,10 +54,11 @@ class KickChatMember(BaseClient):
considered to be banned forever. Defaults to 0 (ban forever).
Returns:
On success, either True or a service :obj:`Message <pyrogram.Message>` will be returned (when applicable).
:obj:`Message` | ``bool``: On success, a service message will be returned (when applicable), otherwise, in
case a message object couldn't be returned, True is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
chat_peer = self.resolve_peer(chat_id)
user_peer = self.resolve_peer(user_id)

View File

@@ -28,9 +28,9 @@ class LeaveChat(BaseClient):
chat_id: Union[int, str],
delete: bool = False
):
"""Use this method to leave a group chat or channel.
"""Leave a group chat or channel.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier for the target chat or username of the target channel/supergroup
(in the format @username).
@@ -39,7 +39,7 @@ class LeaveChat(BaseClient):
Deletes the group chat dialog after leaving (for simple group chats, not supergroups).
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
peer = self.resolve_peer(chat_id)

View File

@@ -29,11 +29,11 @@ class PinChatMessage(BaseClient):
message_id: int,
disable_notification: bool = None
) -> bool:
"""Use this method to pin a message in a group, channel or your own chat.
"""Pin a message in a group, channel or your own chat.
You must be an administrator in the chat for this to work and must have the "can_pin_messages" admin right in
the supergroup or "can_edit_messages" admin right in the channel.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -45,10 +45,10 @@ class PinChatMessage(BaseClient):
message. Notifications are always disabled in channels.
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
self.send(
functions.messages.UpdatePinnedMessage(

View File

@@ -36,12 +36,12 @@ class PromoteChatMember(BaseClient):
can_pin_messages: bool = False,
can_promote_members: bool = False
) -> bool:
"""Use this method to promote or demote a user in a supergroup or a channel.
"""Promote or demote a user in a supergroup or a channel.
You must be an administrator in the chat for this to work and must have the appropriate admin rights.
Pass False for all boolean parameters to demote a user.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -76,10 +76,10 @@ class PromoteChatMember(BaseClient):
were appointed by him).
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
self.send(
functions.channels.EditAdmin(

View File

@@ -36,10 +36,10 @@ class RestrictChat(BaseClient):
can_invite_users: bool = False,
can_pin_messages: bool = False
) -> Chat:
"""Use this method to restrict a chat.
"""Restrict a chat.
Pass True for all boolean parameters to lift restrictions from a chat.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -70,10 +70,10 @@ class RestrictChat(BaseClient):
Pass True, if the user can pin messages.
Returns:
On success, a :obj:`Chat <pyrogram.Chat>` object is returned.
:obj:`Chat`: On success, a chat object is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
send_messages = True
send_media = True

View File

@@ -38,12 +38,12 @@ class RestrictChatMember(BaseClient):
can_invite_users: bool = False,
can_pin_messages: bool = False
) -> Chat:
"""Use this method to restrict a user in a supergroup.
"""Restrict a user in a supergroup.
The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights.
Pass True for all boolean parameters to lift restrictions from a user.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -83,10 +83,10 @@ class RestrictChatMember(BaseClient):
Pass True, if the user can pin messages.
Returns:
On success, a :obj:`Chat <pyrogram.Chat>` object is returned.
:obj:`Chat`: On success, a chat object is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
send_messages = True
send_media = True

View File

@@ -28,10 +28,10 @@ class SetChatDescription(BaseClient):
chat_id: Union[int, str],
description: str
) -> bool:
"""Use this method to change the description of a supergroup or a channel.
"""Change the description of a supergroup or a channel.
You must be an administrator in the chat for this to work and must have the appropriate admin rights.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -39,10 +39,10 @@ class SetChatDescription(BaseClient):
New chat description, 0-255 characters.
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
``ValueError`` if a chat_id doesn't belong to a supergroup or a channel.
"""
peer = self.resolve_peer(chat_id)

View File

@@ -31,7 +31,7 @@ class SetChatPhoto(BaseClient):
chat_id: Union[int, str],
photo: str
) -> bool:
"""Use this method to set a new profile photo for the chat.
"""Set a new profile photo for the chat.
Photos can't be changed for private chats.
You must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -39,19 +39,19 @@ class SetChatPhoto(BaseClient):
In regular groups (non-supergroups), this method will only work if the "All Members Are Admins"
setting is off.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
photo (``str``):
New chat photo. You can pass a :class:`Photo` id or a file path to upload a new photo.
New chat photo. You can pass a :obj:`Photo` id or a file path to upload a new photo.
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``ValueError`` if a chat_id belongs to user.
RPCError: In case of a Telegram RPC error.
ValueError: if a chat_id belongs to user.
"""
peer = self.resolve_peer(chat_id)

View File

@@ -28,7 +28,7 @@ class SetChatTitle(BaseClient):
chat_id: Union[int, str],
title: str
) -> bool:
"""Use this method to change the title of a chat.
"""Change the title of a chat.
Titles can't be changed for private chats.
You must be an administrator in the chat for this to work and must have the appropriate admin rights.
@@ -36,7 +36,7 @@ class SetChatTitle(BaseClient):
In regular groups (non-supergroups), this method will only work if the "All Members Are Admins"
setting is off.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -44,11 +44,11 @@ class SetChatTitle(BaseClient):
New chat title, 1-255 characters.
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``ValueError`` if a chat_id belongs to user.
RPCError: In case of a Telegram RPC error.
ValueError: In case a chat id belongs to user.
"""
peer = self.resolve_peer(chat_id)

View File

@@ -0,0 +1,58 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Union, List
from pyrogram.api import functions, types
from ...ext import BaseClient
class UnarchiveChats(BaseClient):
def unarchive_chats(
self,
chat_ids: Union[int, str, List[Union[int, str]]],
) -> bool:
"""Unarchive one or more chats.
Parameters:
chat_ids (``int`` | ``str`` | List[``int``, ``str``]):
Unique identifier (int) or username (str) of the target chat.
You can also pass a list of ids (int) or usernames (str).
Returns:
``bool``: On success, True is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
if not isinstance(chat_ids, list):
chat_ids = [chat_ids]
self.send(
functions.folders.EditPeerFolders(
folder_peers=[
types.InputFolderPeer(
peer=self.resolve_peer(chat),
folder_id=0
) for chat in chat_ids
]
)
)
return True

View File

@@ -28,11 +28,11 @@ class UnbanChatMember(BaseClient):
chat_id: Union[int, str],
user_id: Union[int, str]
) -> bool:
"""Use this method to unban a previously kicked user in a supergroup or channel.
"""Unban a previously kicked user in a supergroup or channel.
The user will **not** return to the group or channel automatically, but will be able to join via link, etc.
You must be an administrator for this to work.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
@@ -41,10 +41,10 @@ class UnbanChatMember(BaseClient):
For a contact that exists in your Telegram address book you can use his phone number (str).
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
self.send(
functions.channels.EditBanned(

View File

@@ -27,19 +27,19 @@ class UnpinChatMessage(BaseClient):
self,
chat_id: Union[int, str]
) -> bool:
"""Use this method to unpin a message in a group, channel or your own chat.
"""Unpin a message in a group, channel or your own chat.
You must be an administrator in the chat for this to work and must have the "can_pin_messages" admin
right in the supergroup or "can_edit_messages" admin right in the channel.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
self.send(
functions.messages.UpdatePinnedMessage(

View File

@@ -28,22 +28,22 @@ class UpdateChatUsername(BaseClient):
chat_id: Union[int, str],
username: Union[str, None]
) -> bool:
"""Use this method to update a channel or a supergroup username.
"""Update a channel or a supergroup username.
To update your own username (for users only, not bots) you can use :meth:`update_username`.
To update your own username (for users only, not bots) you can use :meth:`~Client.update_username`.
Args:
Parameters:
chat_id (``int`` | ``str``)
Unique identifier (int) or username (str) of the target chat.
username (``str`` | ``None``):
Username to set. Pass "" (empty string) or None to remove the username.
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
``ValueError`` if a chat_id belongs to a user or chat.
RPCError: In case of a Telegram RPC error.
ValueError: In case a chat id belongs to a user or chat.
"""
peer = self.resolve_peer(chat_id)

View File

@@ -19,11 +19,13 @@
from .add_contacts import AddContacts
from .delete_contacts import DeleteContacts
from .get_contacts import GetContacts
from .get_contacts_count import GetContactsCount
class Contacts(
GetContacts,
DeleteContacts,
AddContacts
AddContacts,
GetContactsCount
):
pass

View File

@@ -28,17 +28,14 @@ class AddContacts(BaseClient):
self,
contacts: List["pyrogram.InputPhoneContact"]
):
"""Use this method to add contacts to your Telegram address book.
"""Add contacts to your Telegram address book.
Args:
contacts (List of :obj:`InputPhoneContact <pyrogram.InputPhoneContact>`):
Parameters:
contacts (List of :obj:`InputPhoneContact`):
The contact list to be added
Returns:
On success, the added contacts are returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
imported_contacts = self.send(
functions.contacts.ImportContacts(

View File

@@ -28,18 +28,18 @@ class DeleteContacts(BaseClient):
self,
ids: List[int]
):
"""Use this method to delete contacts from your Telegram address book.
"""Delete contacts from your Telegram address book.
Args:
Parameters:
ids (List of ``int``):
A list of unique identifiers for the target users.
Can be an ID (int), a username (string) or phone number (string).
Returns:
True on success.
``bool``: True on success.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
contacts = []

View File

@@ -18,6 +18,7 @@
import logging
import time
from typing import List
import pyrogram
from pyrogram.api import functions
@@ -28,14 +29,15 @@ log = logging.getLogger(__name__)
class GetContacts(BaseClient):
def get_contacts(self):
"""Use this method to get contacts from your Telegram address book.
def get_contacts(self) -> List["pyrogram.User"]:
# TODO: Create a Users object and return that
"""Get contacts from your Telegram address book.
Returns:
On success, a list of :obj:`User` objects is returned.
List of :obj:`User`: On success, a list of users is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
while True:
try:
@@ -45,4 +47,4 @@ class GetContacts(BaseClient):
time.sleep(e.x)
else:
log.info("Total contacts: {}".format(self.session_storage.contacts_count()))
return [pyrogram.User._parse(self, user) for user in contacts.users]
return pyrogram.List(pyrogram.User._parse(self, user) for user in contacts.users)

View File

@@ -0,0 +1,34 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from pyrogram.api import functions
from ...ext import BaseClient
class GetContactsCount(BaseClient):
def get_contacts_count(self) -> int:
"""Get the total count of contacts from your Telegram address book.
Returns:
``int``: On success, the contacts count is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
return len(self.send(functions.contacts.GetContacts(hash=0)).contacts)

View File

@@ -21,6 +21,7 @@ from .on_deleted_messages import OnDeletedMessages
from .on_disconnect import OnDisconnect
from .on_inline_query import OnInlineQuery
from .on_message import OnMessage
from .on_poll import OnPoll
from .on_raw_update import OnRawUpdate
from .on_user_status import OnUserStatus
@@ -32,6 +33,7 @@ class Decorators(
OnRawUpdate,
OnDisconnect,
OnUserStatus,
OnInlineQuery
OnInlineQuery,
OnPoll
):
pass

View File

@@ -30,11 +30,13 @@ class OnCallbackQuery(BaseClient):
filters=None,
group: int = 0
) -> callable:
"""Use this decorator to automatically register a function for handling callback queries.
This does the same thing as :meth:`add_handler` using the :class:`CallbackQueryHandler`.
"""Decorator for handling callback queries.
Args:
filters (:obj:`Filters <pyrogram.Filters>`):
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the
:obj:`~pyrogram.CallbackQueryHandler`.
Parameters:
filters (:obj:`~pyrogram.Filters`, *optional*):
Pass one or more filters to allow only a subset of callback queries to be passed
in your function.

View File

@@ -30,11 +30,13 @@ class OnDeletedMessages(BaseClient):
filters=None,
group: int = 0
) -> callable:
"""Use this decorator to automatically register a function for handling deleted messages.
This does the same thing as :meth:`add_handler` using the :class:`DeletedMessagesHandler`.
"""Decorator for handling deleted messages.
Args:
filters (:obj:`Filters <pyrogram.Filters>`):
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the
:obj:`~pyrogram.DeletedMessagesHandler`.
Parameters:
filters (:obj:`~pyrogram.Filters`, *optional*):
Pass one or more filters to allow only a subset of messages to be passed
in your function.

View File

@@ -23,8 +23,9 @@ from ...ext import BaseClient
class OnDisconnect(BaseClient):
def on_disconnect(self=None) -> callable:
"""Use this decorator to automatically register a function for handling disconnections.
This does the same thing as :meth:`add_handler` using the :class:`DisconnectHandler`.
"""Decorator for handling disconnections.
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the :obj:`~pyrogram.DisconnectHandler`.
"""
def decorator(func: callable) -> Handler:

View File

@@ -30,11 +30,12 @@ class OnInlineQuery(BaseClient):
filters=None,
group: int = 0
) -> callable:
"""Use this decorator to automatically register a function for handling inline queries.
This does the same thing as :meth:`add_handler` using the :class:`InlineQueryHandler`.
"""Decorator for handling inline queries.
Args:
filters (:obj:`Filters <pyrogram.Filters>`):
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the :obj:`~pyrogram.InlineQueryHandler`.
Parameters:
filters (:obj:`~pyrogram.Filters`, *optional*):
Pass one or more filters to allow only a subset of inline queries to be passed
in your function.

View File

@@ -30,11 +30,12 @@ class OnMessage(BaseClient):
filters=None,
group: int = 0
) -> callable:
"""Use this decorator to automatically register a function for handling messages.
This does the same thing as :meth:`add_handler` using the :class:`MessageHandler`.
"""Decorator for handling messages.
Args:
filters (:obj:`Filters <pyrogram.Filters>`):
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the :obj:`~pyrogram.MessageHandler`.
Parameters:
filters (:obj:`~pyrogram.Filters`, *optional*):
Pass one or more filters to allow only a subset of messages to be passed
in your function.

View File

@@ -0,0 +1,60 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Tuple
import pyrogram
from pyrogram.client.filters.filter import Filter
from pyrogram.client.handlers.handler import Handler
from ...ext import BaseClient
class OnPoll(BaseClient):
def on_poll(
self=None,
filters=None,
group: int = 0
) -> callable:
"""Decorator for handling poll updates.
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the :obj:`~pyrogram.PollHandler`.
Parameters:
filters (:obj:`~pyrogram.Filters`, *optional*):
Pass one or more filters to allow only a subset of polls to be passed
in your function.
group (``int``, *optional*):
The group identifier, defaults to 0.
"""
def decorator(func: callable) -> Tuple[Handler, int]:
if isinstance(func, tuple):
func = func[0].callback
handler = pyrogram.PollHandler(func, filters)
if isinstance(self, Filter):
return pyrogram.PollHandler(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,10 +28,11 @@ class OnRawUpdate(BaseClient):
self=None,
group: int = 0
) -> callable:
"""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`.
"""Decorator for handling raw updates.
Args:
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the :obj:`~pyrogram.RawUpdateHandler`.
Parameters:
group (``int``, *optional*):
The group identifier, defaults to 0.
"""

View File

@@ -30,11 +30,11 @@ class OnUserStatus(BaseClient):
filters=None,
group: int = 0
) -> callable:
"""Use this decorator to automatically register a function for handling user status updates.
This does the same thing as :meth:`add_handler` using the :class:`UserStatusHandler`.
"""Decorator for handling user status updates.
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the :obj:`~pyrogram.UserStatusHandler`.
Args:
filters (:obj:`Filters <pyrogram.Filters>`):
Parameters:
filters (:obj:`~pyrogram.Filters`, *optional*):
Pass one or more filters to allow only a subset of UserStatus updated to be passed in your function.
group (``int``, *optional*):

View File

@@ -16,18 +16,24 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .close_poll import ClosePoll
from .delete_messages import DeleteMessages
from .download_media import DownloadMedia
from .edit_inline_caption import EditInlineCaption
from .edit_inline_media import EditInlineMedia
from .edit_inline_reply_markup import EditInlineReplyMarkup
from .edit_inline_text import EditInlineText
from .edit_message_caption import EditMessageCaption
from .edit_message_media import EditMessageMedia
from .edit_message_reply_markup import EditMessageReplyMarkup
from .edit_message_text import EditMessageText
from .forward_messages import ForwardMessages
from .get_history import GetHistory
from .get_history_count import GetHistoryCount
from .get_messages import GetMessages
from .iter_history import IterHistory
from .read_history import ReadHistory
from .retract_vote import RetractVote
from .send_animated_sticker import SendAnimatedSticker
from .send_animation import SendAnimation
from .send_audio import SendAudio
from .send_cached_media import SendCachedMedia
@@ -44,6 +50,7 @@ from .send_venue import SendVenue
from .send_video import SendVideo
from .send_video_note import SendVideoNote
from .send_voice import SendVoice
from .stop_poll import StopPoll
from .vote_poll import VotePoll
@@ -72,10 +79,17 @@ class Messages(
SendVoice,
SendPoll,
VotePoll,
ClosePoll,
StopPoll,
RetractVote,
DownloadMedia,
IterHistory,
SendCachedMedia
SendCachedMedia,
GetHistoryCount,
SendAnimatedSticker,
ReadHistory,
EditInlineText,
EditInlineCaption,
EditInlineMedia,
EditInlineReplyMarkup
):
pass

View File

@@ -29,9 +29,9 @@ class DeleteMessages(BaseClient):
message_ids: Iterable[int],
revoke: bool = True
) -> bool:
"""Use this method to delete messages, including service messages.
"""Delete messages, including service messages.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -48,27 +48,29 @@ class DeleteMessages(BaseClient):
Defaults to True.
Returns:
True on success.
``bool``: True on success, False otherwise.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
peer = self.resolve_peer(chat_id)
message_ids = list(message_ids) if not isinstance(message_ids, int) else [message_ids]
if isinstance(peer, types.InputPeerChannel):
self.send(
r = self.send(
functions.channels.DeleteMessages(
channel=peer,
id=message_ids
)
)
else:
self.send(
r = self.send(
functions.messages.DeleteMessages(
id=message_ids,
revoke=revoke or None
)
)
return True
# Deleting messages you don't have right onto, won't raise any error.
# Check for pts_count, which is 0 in case deletes fail.
return bool(r.pts_count)

View File

@@ -16,26 +16,34 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import binascii
import os
import struct
import time
from datetime import datetime
from threading import Event
from typing import Union
import pyrogram
from pyrogram.client.ext import BaseClient
from pyrogram.client.ext import BaseClient, FileData, utils
from pyrogram.errors import FileIdInvalid
DEFAULT_DOWNLOAD_DIR = "downloads/"
class DownloadMedia(BaseClient):
def download_media(
self,
message: Union["pyrogram.Message", str],
file_name: str = "",
file_name: str = DEFAULT_DOWNLOAD_DIR,
block: bool = True,
progress: callable = None,
progress_args: tuple = ()
) -> Union[str, None]:
"""Use this method to download the media from a message.
"""Download the media from a message.
Args:
message (:obj:`Message <pyrogram.Message>` | ``str``):
Parameters:
message (:obj:`Message` | ``str``):
Pass a Message containing the media, the media itself (message.audio, message.video, ...) or
the file id as string.
@@ -59,7 +67,7 @@ class DownloadMedia(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``):
@@ -73,75 +81,133 @@ class DownloadMedia(BaseClient):
You can either keep *\*args* or add every single extra argument in your function signature.
Returns:
On success, the absolute path of the downloaded file as string is returned, None otherwise.
In case the download is deliberately stopped with :meth:`stop_transmission`, None is returned as well.
``str`` | ``None``: On success, the absolute path of the downloaded file is returned, otherwise, in case
the download failed or was deliberately stopped with :meth:`~Client.stop_transmission`, None is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
``ValueError`` if the message doesn't contain any downloadable media
"""
error_message = "This message doesn't contain any downloadable media"
available_media = ("audio", "document", "photo", "sticker", "animation", "video", "voice", "video_note")
media_file_name = None
file_size = None
mime_type = None
date = None
if isinstance(message, pyrogram.Message):
if message.photo:
media = pyrogram.Document(
file_id=message.photo.sizes[-1].file_id,
file_size=message.photo.sizes[-1].file_size,
mime_type="",
date=message.photo.date,
client=self
)
elif message.audio:
media = message.audio
elif message.document:
media = message.document
elif message.video:
media = message.video
elif message.voice:
media = message.voice
elif message.video_note:
media = message.video_note
elif message.sticker:
media = message.sticker
elif message.animation:
media = message.animation
for kind in available_media:
media = getattr(message, kind, None)
if media is not None:
break
else:
raise ValueError(error_message)
elif isinstance(message, (
pyrogram.Photo,
pyrogram.PhotoSize,
pyrogram.Audio,
pyrogram.Document,
pyrogram.Video,
pyrogram.Voice,
pyrogram.VideoNote,
pyrogram.Sticker,
pyrogram.Animation
)):
if isinstance(message, pyrogram.Photo):
media = pyrogram.Document(
file_id=message.sizes[-1].file_id,
file_size=message.sizes[-1].file_size,
mime_type="",
date=message.date,
client=self
else:
media = message
if isinstance(media, str):
file_id_str = media
else:
file_id_str = media.file_id
media_file_name = getattr(media, "file_name", "")
file_size = getattr(media, "file_size", None)
mime_type = getattr(media, "mime_type", None)
date = getattr(media, "date", None)
data = FileData(
file_name=media_file_name,
file_size=file_size,
mime_type=mime_type,
date=date
)
def get_existing_attributes() -> dict:
return dict(filter(lambda x: x[1] is not None, data.__dict__.items()))
try:
decoded = utils.decode(file_id_str)
media_type = decoded[0]
if media_type == 1:
unpacked = struct.unpack("<iiqqib", decoded)
dc_id, peer_id, volume_id, local_id, is_big = unpacked[1:]
data = FileData(
**get_existing_attributes(),
media_type=media_type,
dc_id=dc_id,
peer_id=peer_id,
volume_id=volume_id,
local_id=local_id,
is_big=bool(is_big)
)
elif media_type in (0, 2, 14):
unpacked = struct.unpack("<iiqqc", decoded)
dc_id, document_id, access_hash, thumb_size = unpacked[1:]
data = FileData(
**get_existing_attributes(),
media_type=media_type,
dc_id=dc_id,
document_id=document_id,
access_hash=access_hash,
thumb_size=thumb_size.decode()
)
elif media_type in (3, 4, 5, 8, 9, 10, 13):
unpacked = struct.unpack("<iiqq", decoded)
dc_id, document_id, access_hash = unpacked[1:]
data = FileData(
**get_existing_attributes(),
media_type=media_type,
dc_id=dc_id,
document_id=document_id,
access_hash=access_hash
)
else:
media = message
elif isinstance(message, str):
media = pyrogram.Document(
file_id=message,
file_size=0,
mime_type="",
client=self
)
else:
raise ValueError(error_message)
raise ValueError("Unknown media type: {}".format(file_id_str))
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
done = Event()
path = [None]
self.download_queue.put((media, file_name, done, progress, progress_args, path))
directory, file_name = os.path.split(file_name)
file_name = file_name or data.file_name or ""
if not os.path.isabs(file_name):
directory = self.PARENT_DIR / (directory or DEFAULT_DOWNLOAD_DIR)
media_type_str = self.MEDIA_TYPE_ID[data.media_type]
if not file_name:
guessed_extension = self.guess_extension(data.mime_type)
if data.media_type in (0, 1, 2, 14):
extension = ".jpg"
elif data.media_type == 3:
extension = guessed_extension or ".ogg"
elif data.media_type in (4, 10, 13):
extension = guessed_extension or ".mp4"
elif data.media_type == 5:
extension = guessed_extension or ".zip"
elif data.media_type == 8:
extension = guessed_extension or ".webp"
elif data.media_type == 9:
extension = guessed_extension or ".mp3"
else:
extension = ".unknown"
file_name = "{}_{}_{}{}".format(
media_type_str,
datetime.fromtimestamp(data.date or time.time()).strftime("%Y-%m-%d_%H-%M-%S"),
self.rnd_id(),
extension
)
self.download_queue.put((data, directory, file_name, done, progress, progress_args, path))
if block:
done.wait()

View File

@@ -0,0 +1,58 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import pyrogram
from pyrogram.client.ext import BaseClient
class EditInlineCaption(BaseClient):
def edit_inline_caption(
self,
inline_message_id: str,
caption: str,
parse_mode: str = "",
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
) -> bool:
"""Edit the caption of **inline** media messages.
Parameters:
inline_message_id (``str``):
Identifier of the inline message.
caption (``str``):
New caption of the media message.
parse_mode (``str``, *optional*):
Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline
URLs in your message. Defaults to "markdown".
reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
An InlineKeyboardMarkup object.
Returns:
``bool``: On success, True is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
return self.edit_inline_text(
inline_message_id=inline_message_id,
text=caption,
parse_mode=parse_mode,
reply_markup=reply_markup
)

View File

@@ -0,0 +1,104 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import pyrogram
from pyrogram.api import functions, types
from pyrogram.client.ext import BaseClient, utils
from pyrogram.client.types import (
InputMediaPhoto, InputMediaVideo, InputMediaAudio,
InputMediaAnimation, InputMediaDocument
)
from pyrogram.client.types.input_media import InputMedia
class EditInlineMedia(BaseClient):
def edit_inline_media(
self,
inline_message_id: str,
media: InputMedia,
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
) -> bool:
"""Edit **inline** animation, audio, document, photo or video messages.
When the inline message is edited, a new file can't be uploaded. Use a previously uploaded file via its file_id
or specify a URL.
Parameters:
inline_message_id (``str``):
Required if *chat_id* and *message_id* are not specified.
Identifier of the inline message.
media (:obj:`InputMedia`):
One of the InputMedia objects describing an animation, audio, document, photo or video.
reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
An InlineKeyboardMarkup object.
Returns:
``bool``: On success, True is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
style = self.html if media.parse_mode.lower() == "html" else self.markdown
caption = media.caption
if isinstance(media, InputMediaPhoto):
if media.media.startswith("http"):
media = types.InputMediaPhotoExternal(
url=media.media
)
else:
media = utils.get_input_media_from_file_id(media.media, 2)
elif isinstance(media, InputMediaVideo):
if media.media.startswith("http"):
media = types.InputMediaDocumentExternal(
url=media.media
)
else:
media = utils.get_input_media_from_file_id(media.media, 4)
elif isinstance(media, InputMediaAudio):
if media.media.startswith("http"):
media = types.InputMediaDocumentExternal(
url=media.media
)
else:
media = utils.get_input_media_from_file_id(media.media, 9)
elif isinstance(media, InputMediaAnimation):
if media.media.startswith("http"):
media = types.InputMediaDocumentExternal(
url=media.media
)
else:
media = utils.get_input_media_from_file_id(media.media, 10)
elif isinstance(media, InputMediaDocument):
if media.media.startswith("http"):
media = types.InputMediaDocumentExternal(
url=media.media
)
else:
media = utils.get_input_media_from_file_id(media.media, 5)
return self.send(
functions.messages.EditInlineBotMessage(
id=utils.unpack_inline_message_id(inline_message_id),
media=media,
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)

View File

@@ -0,0 +1,50 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import pyrogram
from pyrogram.api import functions
from pyrogram.client.ext import BaseClient, utils
class EditInlineReplyMarkup(BaseClient):
def edit_inline_reply_markup(
self,
inline_message_id: str,
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
) -> bool:
"""Edit only the reply markup of **inline** messages sent via the bot (for inline bots).
Parameters:
inline_message_id (``str``):
Identifier of the inline message.
reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
An InlineKeyboardMarkup object.
Returns:
``bool``: On success, True is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
return self.send(
functions.messages.EditInlineBotMessage(
id=utils.unpack_inline_message_id(inline_message_id),
reply_markup=reply_markup.write() if reply_markup else None,
)
)

View File

@@ -0,0 +1,67 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import pyrogram
from pyrogram.api import functions
from pyrogram.client.ext import BaseClient, utils
class EditInlineText(BaseClient):
def edit_inline_text(
self,
inline_message_id: str,
text: str,
parse_mode: str = "",
disable_web_page_preview: bool = None,
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
) -> bool:
"""Edit the text of **inline** messages.
Parameters:
inline_message_id (``str``):
Identifier of the inline message.
text (``str``):
New text of the message.
parse_mode (``str``, *optional*):
Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline
URLs in your message. Defaults to "markdown".
disable_web_page_preview (``bool``, *optional*):
Disables link previews for links in this message.
reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
An InlineKeyboardMarkup object.
Returns:
``bool``: On success, True is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
return self.send(
functions.messages.EditInlineBotMessage(
id=utils.unpack_inline_message_id(inline_message_id),
no_webpage=disable_web_page_preview or None,
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(text)
)
)

View File

@@ -19,7 +19,6 @@
from typing import Union
import pyrogram
from pyrogram.api import functions, types
from pyrogram.client.ext import BaseClient
@@ -32,9 +31,9 @@ class EditMessageCaption(BaseClient):
parse_mode: str = "",
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
) -> "pyrogram.Message":
"""Use this method to edit captions of messages.
"""Edit the caption of media messages.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -44,37 +43,25 @@ class EditMessageCaption(BaseClient):
Message identifier in the chat specified in chat_id.
caption (``str``):
New caption of the message.
New caption of the media message.
parse_mode (``str``, *optional*):
Use :obj:`MARKDOWN <pyrogram.ParseMode.MARKDOWN>` or :obj:`HTML <pyrogram.ParseMode.HTML>`
if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your caption.
Defaults to Markdown.
Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline
URLs in your message. Defaults to "markdown".
reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
An InlineKeyboardMarkup object.
Returns:
On success, the edited :obj:`Message <pyrogram.Message>` is returned.
:obj:`Message`: On success, the edited message is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
style = self.html if parse_mode.lower() == "html" else self.markdown
r = self.send(
functions.messages.EditMessage(
peer=self.resolve_peer(chat_id),
id=message_id,
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
return self.edit_message_text(
chat_id=chat_id,
message_id=message_id,
text=caption,
parse_mode=parse_mode,
reply_markup=reply_markup
)
for i in r.updates:
if isinstance(i, (types.UpdateEditMessage, types.UpdateEditChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)

View File

@@ -16,14 +16,11 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import binascii
import os
import struct
from typing import Union
import pyrogram
from pyrogram.api import functions, types
from pyrogram.errors import FileIdInvalid
from pyrogram.client.ext import BaseClient, utils
from pyrogram.client.types import (
InputMediaPhoto, InputMediaVideo, InputMediaAudio,
@@ -40,14 +37,12 @@ class EditMessageMedia(BaseClient):
media: InputMedia,
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
) -> "pyrogram.Message":
"""Use this method to edit audio, document, photo, or video messages.
"""Edit animation, audio, document, photo or video messages.
If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise,
message type can be changed arbitrarily. When inline message is edited, new file can't be uploaded.
Use previously uploaded file via its file_id or specify a URL. On success, if the edited message was sent
by the bot, the edited Message is returned, otherwise True is returned.
If a message is a part of a message album, then it can be edited only to a photo or a video. Otherwise, the
message type can be changed arbitrarily.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -56,17 +51,17 @@ class EditMessageMedia(BaseClient):
message_id (``int``):
Message identifier in the chat specified in chat_id.
media (:obj:`InputMedia`)
media (:obj:`InputMedia`):
One of the InputMedia objects describing an animation, audio, document, photo or video.
reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
An InlineKeyboardMarkup object.
Returns:
On success, the edited :obj:`Message <pyrogram.Message>` is returned.
:obj:`Message`: On success, the edited message is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
style = self.html if media.parse_mode.lower() == "html" else self.markdown
caption = media.caption
@@ -94,36 +89,14 @@ class EditMessageMedia(BaseClient):
url=media.media
)
else:
try:
decoded = utils.decode(media.media)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 2:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaPhoto(
id=types.InputPhoto(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
if isinstance(media, InputMediaVideo):
media = utils.get_input_media_from_file_id(media.media, 2)
elif isinstance(media, InputMediaVideo):
if os.path.exists(media.media):
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedDocument(
mime_type="video/mp4",
mime_type=self.guess_mime_type(media.media) or "video/mp4",
thumb=None if media.thumb is None else self.save_file(media.thumb),
file=self.save_file(media.media),
attributes=[
@@ -153,36 +126,14 @@ class EditMessageMedia(BaseClient):
url=media.media
)
else:
try:
decoded = utils.decode(media.media)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 4:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
if isinstance(media, InputMediaAudio):
media = utils.get_input_media_from_file_id(media.media, 4)
elif isinstance(media, InputMediaAudio):
if os.path.exists(media.media):
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedDocument(
mime_type="audio/mpeg",
mime_type=self.guess_mime_type(media.media) or "audio/mpeg",
thumb=None if media.thumb is None else self.save_file(media.thumb),
file=self.save_file(media.media),
attributes=[
@@ -211,36 +162,14 @@ class EditMessageMedia(BaseClient):
url=media.media
)
else:
try:
decoded = utils.decode(media.media)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 9:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
if isinstance(media, InputMediaAnimation):
media = utils.get_input_media_from_file_id(media.media, 9)
elif isinstance(media, InputMediaAnimation):
if os.path.exists(media.media):
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedDocument(
mime_type="video/mp4",
mime_type=self.guess_mime_type(media.media) or "video/mp4",
thumb=None if media.thumb is None else self.save_file(media.thumb),
file=self.save_file(media.media),
attributes=[
@@ -271,36 +200,14 @@ class EditMessageMedia(BaseClient):
url=media.media
)
else:
try:
decoded = utils.decode(media.media)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 10:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
if isinstance(media, InputMediaDocument):
media = utils.get_input_media_from_file_id(media.media, 10)
elif isinstance(media, InputMediaDocument):
if os.path.exists(media.media):
media = self.send(
functions.messages.UploadMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaUploadedDocument(
mime_type="application/zip",
mime_type=self.guess_mime_type(media.media) or "application/zip",
thumb=None if media.thumb is None else self.save_file(media.thumb),
file=self.save_file(media.media),
attributes=[
@@ -324,35 +231,14 @@ class EditMessageMedia(BaseClient):
url=media.media
)
else:
try:
decoded = utils.decode(media.media)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] not in (5, 10):
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
media = utils.get_input_media_from_file_id(media.media, 5)
r = self.send(
functions.messages.EditMessage(
peer=self.resolve_peer(chat_id),
id=message_id,
reply_markup=reply_markup.write() if reply_markup else None,
media=media,
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)

View File

@@ -28,11 +28,11 @@ class EditMessageReplyMarkup(BaseClient):
self,
chat_id: Union[int, str],
message_id: int,
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
reply_markup: "pyrogram.InlineKeyboardMarkup" = None,
) -> "pyrogram.Message":
"""Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots).
"""Edit only the reply markup of messages sent by the bot.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -45,18 +45,16 @@ class EditMessageReplyMarkup(BaseClient):
An InlineKeyboardMarkup object.
Returns:
On success, if edited message is sent by the bot, the edited
:obj:`Message <pyrogram.Message>` is returned, otherwise True is returned.
:obj:`Message`: On success, the edited message is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
r = self.send(
functions.messages.EditMessage(
peer=self.resolve_peer(chat_id),
id=message_id,
reply_markup=reply_markup.write() if reply_markup else None
reply_markup=reply_markup.write() if reply_markup else None,
)
)

View File

@@ -33,9 +33,9 @@ class EditMessageText(BaseClient):
disable_web_page_preview: bool = None,
reply_markup: "pyrogram.InlineKeyboardMarkup" = None
) -> "pyrogram.Message":
"""Use this method to edit text messages.
"""Edit the text of messages.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -48,9 +48,8 @@ class EditMessageText(BaseClient):
New text of the message.
parse_mode (``str``, *optional*):
Use :obj:`MARKDOWN <pyrogram.ParseMode.MARKDOWN>` or :obj:`HTML <pyrogram.ParseMode.HTML>`
if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your message.
Defaults to Markdown.
Pass "markdown" or "html" if you want Telegram apps to show bold, italic, fixed-width text or inline
URLs in your message. Defaults to "markdown".
disable_web_page_preview (``bool``, *optional*):
Disables link previews for links in this message.
@@ -59,10 +58,10 @@ class EditMessageText(BaseClient):
An InlineKeyboardMarkup object.
Returns:
On success, the edited :obj:`Message <pyrogram.Message>` is returned.
:obj:`Message`: On success, the edited message is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
style = self.html if parse_mode.lower() == "html" else self.markdown

View File

@@ -16,7 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Union, Iterable
from typing import Union, Iterable, List
import pyrogram
from pyrogram.api import functions, types
@@ -28,14 +28,14 @@ class ForwardMessages(BaseClient):
self,
chat_id: Union[int, str],
from_chat_id: Union[int, str],
message_ids: Iterable[int],
message_ids: Union[int, Iterable[int]],
disable_notification: bool = None,
as_copy: bool = False,
remove_caption: bool = False
) -> "pyrogram.Messages":
"""Use this method to forward messages of any kind.
) -> List["pyrogram.Message"]:
"""Forward messages of any kind.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -64,13 +64,12 @@ class ForwardMessages(BaseClient):
Defaults to False.
Returns:
On success and in case *message_ids* was an iterable, the returned value will be a list of the forwarded
:obj:`Messages <pyrogram.Message>` even if a list contains just one element, otherwise if
*message_ids* was an integer, the single forwarded :obj:`Message <pyrogram.Message>`
is returned.
:obj:`Message` | List of :obj:`Message`: In case *message_ids* was an integer, the single forwarded message
is returned, otherwise, in case *message_ids* was an iterable, the returned value will be a list of
messages, even if such iterable contained just a single element.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
is_iterable = not isinstance(message_ids, int)
@@ -80,9 +79,9 @@ class ForwardMessages(BaseClient):
forwarded_messages = []
for chunk in [message_ids[i:i + 200] for i in range(0, len(message_ids), 200)]:
messages = self.get_messages(chat_id=from_chat_id, message_ids=chunk) # type: pyrogram.Messages
messages = self.get_messages(chat_id=from_chat_id, message_ids=chunk)
for message in messages.messages:
for message in messages:
forwarded_messages.append(
message.forward(
chat_id,
@@ -92,11 +91,7 @@ class ForwardMessages(BaseClient):
)
)
return pyrogram.Messages(
client=self,
total_count=len(forwarded_messages),
messages=forwarded_messages
) if is_iterable else forwarded_messages[0]
return pyrogram.List(forwarded_messages) if is_iterable else forwarded_messages[0]
else:
r = self.send(
functions.messages.ForwardMessages(
@@ -122,8 +117,4 @@ class ForwardMessages(BaseClient):
)
)
return pyrogram.Messages(
client=self,
total_count=len(forwarded_messages),
messages=forwarded_messages
) if is_iterable else forwarded_messages[0]
return pyrogram.List(forwarded_messages) if is_iterable else forwarded_messages[0]

View File

@@ -18,10 +18,11 @@
import logging
import time
from typing import Union
from typing import Union, List
import pyrogram
from pyrogram.api import functions
from pyrogram.client.ext import utils
from pyrogram.errors import FloodWait
from ...ext import BaseClient
@@ -37,13 +38,13 @@ class GetHistory(BaseClient):
offset_id: int = 0,
offset_date: int = 0,
reverse: bool = False
):
"""Use this method to retrieve a chunk of the history of a chat.
) -> List["pyrogram.Message"]:
"""Retrieve a chunk of the history of a chat.
You can get up to 100 messages at once.
For a more convenient way of getting a chat history see :meth:`iter_history`.
For a more convenient way of getting a chat history see :meth:`~Client.iter_history`.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -67,15 +68,17 @@ class GetHistory(BaseClient):
Pass True to retrieve the messages in reversed order (from older to most recent).
Returns:
On success, a :obj:`Messages <pyrogram.Messages>` object is returned.
List of :obj:`Message` - On success, a list of the retrieved messages is returned.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
offset_id = offset_id or (1 if reverse else 0)
while True:
try:
messages = pyrogram.Messages._parse(
messages = utils.parse_messages(
self,
self.send(
functions.messages.GetHistory(
@@ -97,6 +100,6 @@ class GetHistory(BaseClient):
break
if reverse:
messages.messages.reverse()
messages.reverse()
return messages

View File

@@ -0,0 +1,68 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
from typing import Union
from pyrogram.api import types, functions
from pyrogram.client.ext import BaseClient
log = logging.getLogger(__name__)
class GetHistoryCount(BaseClient):
def get_history_count(
self,
chat_id: Union[int, str]
) -> int:
"""Get the total count of messages in a chat.
.. note::
Due to Telegram latest internal changes, the server can't reliably find anymore the total count of messages
a **private** or a **basic group** chat has with a single method call. To overcome this limitation, Pyrogram
has to iterate over all the messages. Channels and supergroups are not affected by this limitation.
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
Returns:
``int``: On success, the chat history count is returned.
Raises:
RPCError: In case of a Telegram RPC error.
"""
r = self.send(
functions.messages.GetHistory(
peer=self.resolve_peer(chat_id),
offset_id=0,
offset_date=0,
add_offset=0,
limit=1,
max_id=0,
min_id=0,
hash=0
)
)
if isinstance(r, types.messages.Messages):
return len(r.messages)
else:
return r.count

View File

@@ -18,12 +18,12 @@
import logging
import time
from typing import Union, Iterable
from typing import Union, Iterable, List
import pyrogram
from pyrogram.api import functions, types
from pyrogram.errors import FloodWait
from ...ext import BaseClient
from ...ext import BaseClient, utils
log = logging.getLogger(__name__)
@@ -35,11 +35,11 @@ class GetMessages(BaseClient):
message_ids: Union[int, Iterable[int]] = None,
reply_to_message_ids: Union[int, Iterable[int]] = None,
replies: int = 1
) -> Union["pyrogram.Message", "pyrogram.Messages"]:
"""Use this method to get one or more messages that belong to a specific chat.
) -> Union["pyrogram.Message", List["pyrogram.Message"]]:
"""Get one or more messages that belong to a specific chat.
You can retrieve up to 200 messages at once.
Args:
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
@@ -55,15 +55,17 @@ class GetMessages(BaseClient):
If *message_ids* is set, this argument will be ignored.
replies (``int``, *optional*):
The number of subsequent replies to get for each message. Defaults to 1.
The number of subsequent replies to get for each message.
Pass 0 for no reply at all or -1 for unlimited replies.
Defaults to 1.
Returns:
On success and in case *message_ids* or *reply_to_message_ids* was an iterable, the returned value will be a
:obj:`Messages <pyrogram.Messages>` even if a list contains just one element. Otherwise, if *message_ids* or
*reply_to_message_ids* was an integer, the single requested :obj:`Message <pyrogram.Message>` is returned.
:obj:`Message` | List of :obj:`Message`: In case *message_ids* was an integer, the single requested message is
returned, otherwise, in case *message_ids* was an iterable, the returned value will be a list of messages,
even if such iterable contained just a single element.
Raises:
:class:`RPCError <pyrogram.RPCError>` in case of a Telegram RPC error.
RPCError: In case of a Telegram RPC error.
"""
ids, ids_type = (
(message_ids, types.InputMessageID) if message_ids
@@ -80,6 +82,9 @@ class GetMessages(BaseClient):
ids = list(ids) if is_iterable else [ids]
ids = [ids_type(id=i) for i in ids]
if replies < 0:
replies = (1 << 31) - 1
if isinstance(peer, types.InputPeerChannel):
rpc = functions.channels.GetMessages(channel=peer, id=ids)
else:
@@ -94,6 +99,6 @@ class GetMessages(BaseClient):
else:
break
messages = pyrogram.Messages._parse(self, r, replies=replies)
messages = utils.parse_messages(self, r, replies=replies)
return messages if is_iterable else messages.messages[0]
return messages if is_iterable else messages[0]

Some files were not shown because too many files have changed in this diff Show More