mirror of
https://github.com/pyrogram/pyrogram
synced 2025-08-29 13:27:47 +00:00
Merge branch 'master' into docs
This commit is contained in:
commit
6a5810134b
12
README.rst
12
README.rst
@ -81,6 +81,12 @@ Installation
|
|||||||
|
|
||||||
$ pip install --upgrade pyrogram
|
$ pip install --upgrade pyrogram
|
||||||
|
|
||||||
|
- Or, with TgCrypto_:
|
||||||
|
|
||||||
|
.. code:: shell
|
||||||
|
|
||||||
|
$ pip install --upgrade pyrogram[tgcrypto]
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@ -168,7 +174,7 @@ License
|
|||||||
|
|
||||||
.. _`Email`: admin@pyrogram.ml
|
.. _`Email`: admin@pyrogram.ml
|
||||||
|
|
||||||
.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto
|
.. _TgCrypto: https://github.com/pyrogram/tgcrypto
|
||||||
|
|
||||||
.. _`GNU Lesser General Public License v3 or later (LGPLv3+)`: COPYING.lesser
|
.. _`GNU Lesser General Public License v3 or later (LGPLv3+)`: COPYING.lesser
|
||||||
|
|
||||||
@ -195,8 +201,8 @@ License
|
|||||||
•
|
•
|
||||||
<a href="https://t.me/PyrogramChat">
|
<a href="https://t.me/PyrogramChat">
|
||||||
Community
|
Community
|
||||||
</a
|
</a>
|
||||||
<br><br><br>
|
<br><br>
|
||||||
<a href="compiler/api/source/main_api.tl">
|
<a href="compiler/api/source/main_api.tl">
|
||||||
<img src="https://www.pyrogram.ml/images/scheme.svg"
|
<img src="https://www.pyrogram.ml/images/scheme.svg"
|
||||||
alt="Scheme Layer 75">
|
alt="Scheme Layer 75">
|
||||||
|
@ -23,11 +23,12 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance
|
|||||||
"e" if sys.getfilesystemencoding() == "ascii" else "\xe8"
|
"e" if sys.getfilesystemencoding() == "ascii" else "\xe8"
|
||||||
)
|
)
|
||||||
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
|
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
|
||||||
__version__ = "0.6.0"
|
__version__ = "0.6.1"
|
||||||
|
|
||||||
from .api.errors import Error
|
from .api.errors import Error
|
||||||
from .client import ChatAction
|
from .client import ChatAction
|
||||||
from .client import Client
|
from .client import Client
|
||||||
from .client import ParseMode
|
from .client import ParseMode
|
||||||
from .client.input_media import InputMedia
|
from .client.input_media import InputMedia
|
||||||
|
from .client.input_phone_contact import InputPhoneContact
|
||||||
from .client import Emoji
|
from .client import Emoji
|
||||||
|
@ -27,6 +27,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
from datetime import datetime
|
||||||
from hashlib import sha256, md5
|
from hashlib import sha256, md5
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from signal import signal, SIGINT, SIGTERM, SIGABRT
|
from signal import signal, SIGINT, SIGTERM, SIGABRT
|
||||||
@ -39,8 +40,8 @@ from pyrogram.api.errors import (
|
|||||||
PhoneNumberUnoccupied, PhoneCodeInvalid, PhoneCodeHashEmpty,
|
PhoneNumberUnoccupied, PhoneCodeInvalid, PhoneCodeHashEmpty,
|
||||||
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
|
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
|
||||||
PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing,
|
PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing,
|
||||||
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned
|
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned,
|
||||||
)
|
VolumeLocNotFound)
|
||||||
from pyrogram.api.types import (
|
from pyrogram.api.types import (
|
||||||
User, Chat, Channel,
|
User, Chat, Channel,
|
||||||
PeerUser, PeerChannel,
|
PeerUser, PeerChannel,
|
||||||
@ -49,12 +50,13 @@ from pyrogram.api.types import (
|
|||||||
)
|
)
|
||||||
from pyrogram.crypto import AES
|
from pyrogram.crypto import AES
|
||||||
from pyrogram.session import Auth, Session
|
from pyrogram.session import Auth, Session
|
||||||
|
from pyrogram.session.internals import MsgId
|
||||||
from .input_media import InputMedia
|
from .input_media import InputMedia
|
||||||
from .style import Markdown, HTML
|
from .style import Markdown, HTML
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
Config = namedtuple("Config", ["api_id", "api_hash"])
|
ApiKey = namedtuple("ApiKey", ["api_id", "api_hash"])
|
||||||
Proxy = namedtuple("Proxy", ["enabled", "hostname", "port", "username", "password"])
|
Proxy = namedtuple("Proxy", ["enabled", "hostname", "port", "username", "password"])
|
||||||
|
|
||||||
|
|
||||||
@ -70,6 +72,17 @@ class Client:
|
|||||||
it when you restart your script. As long as a valid session file exists,
|
it when you restart your script. As long as a valid session file exists,
|
||||||
Pyrogram won't ask you again to input your phone number.
|
Pyrogram won't ask you again to input your phone number.
|
||||||
|
|
||||||
|
api_key (:obj:`tuple`, optional):
|
||||||
|
Your Telegram API Key as tuple: *(api_id, api_hash)*.
|
||||||
|
E.g.: *(12345, "0123456789abcdef0123456789abcdef")*. This is an alternative way to pass it if you
|
||||||
|
don't want to use the *config.ini* file.
|
||||||
|
|
||||||
|
proxy (:obj:`dict`, optional):
|
||||||
|
Your SOCKS5 Proxy settings as dict: *{hostname: str, port: int, username: str, password: str}*.
|
||||||
|
E.g.: *dict(hostname="11.22.33.44", port=1080, username="user", password="pass")*.
|
||||||
|
*username* and *password* can be omitted if your proxy doesn't require authorization.
|
||||||
|
This is an alternative way to setup a proxy if you don't want to use the *config.ini* file.
|
||||||
|
|
||||||
test_mode (:obj:`bool`, optional):
|
test_mode (:obj:`bool`, optional):
|
||||||
Enable or disable log-in to testing servers. Defaults to False.
|
Enable or disable log-in to testing servers. Defaults to False.
|
||||||
Only applicable for new sessions and will be ignored in case previously
|
Only applicable for new sessions and will be ignored in case previously
|
||||||
@ -103,9 +116,12 @@ class Client:
|
|||||||
INVITE_LINK_RE = re.compile(r"^(?:https?://)?t\.me/joinchat/(.+)$")
|
INVITE_LINK_RE = re.compile(r"^(?:https?://)?t\.me/joinchat/(.+)$")
|
||||||
DIALOGS_AT_ONCE = 100
|
DIALOGS_AT_ONCE = 100
|
||||||
UPDATES_WORKERS = 2
|
UPDATES_WORKERS = 2
|
||||||
|
DOWNLOAD_WORKERS = 1
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
session_name: str,
|
session_name: str,
|
||||||
|
api_key: tuple or ApiKey = None,
|
||||||
|
proxy: dict or Proxy = None,
|
||||||
test_mode: bool = False,
|
test_mode: bool = False,
|
||||||
phone_number: str = None,
|
phone_number: str = None,
|
||||||
phone_code: str or callable = None,
|
phone_code: str or callable = None,
|
||||||
@ -114,6 +130,8 @@ class Client:
|
|||||||
last_name: str = None,
|
last_name: str = None,
|
||||||
workers: int = 4):
|
workers: int = 4):
|
||||||
self.session_name = session_name
|
self.session_name = session_name
|
||||||
|
self.api_key = api_key
|
||||||
|
self.proxy = proxy
|
||||||
self.test_mode = test_mode
|
self.test_mode = test_mode
|
||||||
|
|
||||||
self.phone_number = phone_number
|
self.phone_number = phone_number
|
||||||
@ -132,14 +150,13 @@ class Client:
|
|||||||
|
|
||||||
self.peers_by_id = {}
|
self.peers_by_id = {}
|
||||||
self.peers_by_username = {}
|
self.peers_by_username = {}
|
||||||
|
self.peers_by_phone = {}
|
||||||
|
|
||||||
self.channels_pts = {}
|
self.channels_pts = {}
|
||||||
|
|
||||||
self.markdown = Markdown(self.peers_by_id)
|
self.markdown = Markdown(self.peers_by_id)
|
||||||
self.html = HTML(self.peers_by_id)
|
self.html = HTML(self.peers_by_id)
|
||||||
|
|
||||||
self.config = None
|
|
||||||
self.proxy = None
|
|
||||||
self.session = None
|
self.session = None
|
||||||
|
|
||||||
self.is_idle = Event()
|
self.is_idle = Event()
|
||||||
@ -148,6 +165,8 @@ class Client:
|
|||||||
self.update_queue = Queue()
|
self.update_queue = Queue()
|
||||||
self.update_handler = None
|
self.update_handler = None
|
||||||
|
|
||||||
|
self.download_queue = Queue()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Use this method to start the Client after creating it.
|
"""Use this method to start the Client after creating it.
|
||||||
Requires no parameters.
|
Requires no parameters.
|
||||||
@ -163,7 +182,7 @@ class Client:
|
|||||||
self.test_mode,
|
self.test_mode,
|
||||||
self.proxy,
|
self.proxy,
|
||||||
self.auth_key,
|
self.auth_key,
|
||||||
self.config.api_id,
|
self.api_key.api_id,
|
||||||
client=self
|
client=self
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -176,8 +195,9 @@ class Client:
|
|||||||
self.password = None
|
self.password = None
|
||||||
self.save_session()
|
self.save_session()
|
||||||
|
|
||||||
self.rnd_id = self.session.msg_id
|
self.rnd_id = MsgId
|
||||||
self.get_dialogs()
|
self.get_dialogs()
|
||||||
|
self.get_contacts()
|
||||||
|
|
||||||
for i in range(self.UPDATES_WORKERS):
|
for i in range(self.UPDATES_WORKERS):
|
||||||
Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start()
|
Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start()
|
||||||
@ -185,6 +205,9 @@ class Client:
|
|||||||
for i in range(self.workers):
|
for i in range(self.workers):
|
||||||
Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start()
|
Thread(target=self.update_worker, name="UpdateWorker#{}".format(i + 1)).start()
|
||||||
|
|
||||||
|
for i in range(self.DOWNLOAD_WORKERS):
|
||||||
|
Thread(target=self.download_worker, name="DownloadWorker#{}".format(i + 1)).start()
|
||||||
|
|
||||||
mimetypes.init()
|
mimetypes.init()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -199,6 +222,9 @@ class Client:
|
|||||||
for _ in range(self.workers):
|
for _ in range(self.workers):
|
||||||
self.update_queue.put(None)
|
self.update_queue.put(None)
|
||||||
|
|
||||||
|
for _ in range(self.DOWNLOAD_WORKERS):
|
||||||
|
self.download_queue.put(None)
|
||||||
|
|
||||||
def fetch_peers(self, entities: list):
|
def fetch_peers(self, entities: list):
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
if isinstance(entity, User):
|
if isinstance(entity, User):
|
||||||
@ -213,6 +239,7 @@ class Client:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
username = entity.username
|
username = entity.username
|
||||||
|
phone = entity.phone
|
||||||
|
|
||||||
input_peer = InputPeerUser(
|
input_peer = InputPeerUser(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@ -224,6 +251,9 @@ class Client:
|
|||||||
if username is not None:
|
if username is not None:
|
||||||
self.peers_by_username[username] = input_peer
|
self.peers_by_username[username] = input_peer
|
||||||
|
|
||||||
|
if phone is not None:
|
||||||
|
self.peers_by_phone[phone] = input_peer
|
||||||
|
|
||||||
if isinstance(entity, Chat):
|
if isinstance(entity, Chat):
|
||||||
chat_id = entity.id
|
chat_id = entity.id
|
||||||
|
|
||||||
@ -260,6 +290,62 @@ class Client:
|
|||||||
if username is not None:
|
if username is not None:
|
||||||
self.peers_by_username[username] = input_peer
|
self.peers_by_username[username] = input_peer
|
||||||
|
|
||||||
|
def download_worker(self):
|
||||||
|
name = threading.current_thread().name
|
||||||
|
log.debug("{} started".format(name))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
media = self.download_queue.get()
|
||||||
|
|
||||||
|
if media is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
media, file_name, done = media
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(media, types.MessageMediaDocument):
|
||||||
|
document = media.document
|
||||||
|
|
||||||
|
if isinstance(document, types.Document):
|
||||||
|
if not file_name:
|
||||||
|
file_name = "doc_{}{}".format(
|
||||||
|
datetime.fromtimestamp(document.date).strftime("%Y-%m-%d_%H-%M-%S"),
|
||||||
|
mimetypes.guess_extension(document.mime_type) or ".unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in document.attributes:
|
||||||
|
if isinstance(i, types.DocumentAttributeFilename):
|
||||||
|
file_name = i.file_name
|
||||||
|
break
|
||||||
|
elif isinstance(i, types.DocumentAttributeSticker):
|
||||||
|
file_name = file_name.replace("doc", "sticker")
|
||||||
|
elif isinstance(i, types.DocumentAttributeAudio):
|
||||||
|
file_name = file_name.replace("doc", "audio")
|
||||||
|
elif isinstance(i, types.DocumentAttributeVideo):
|
||||||
|
file_name = file_name.replace("doc", "video")
|
||||||
|
elif isinstance(i, types.DocumentAttributeAnimated):
|
||||||
|
file_name = file_name.replace("doc", "gif")
|
||||||
|
|
||||||
|
tmp_file_name = self.get_file(
|
||||||
|
dc_id=document.dc_id,
|
||||||
|
id=document.id,
|
||||||
|
access_hash=document.access_hash,
|
||||||
|
version=document.version
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove("./downloads/{}".format(file_name))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
os.renames("./{}".format(tmp_file_name), "./downloads/{}".format(file_name))
|
||||||
|
except Exception as e:
|
||||||
|
log.error(e, exc_info=True)
|
||||||
|
finally:
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
log.debug("{} stopped".format(name))
|
||||||
|
|
||||||
def updates_worker(self):
|
def updates_worker(self):
|
||||||
name = threading.current_thread().name
|
name = threading.current_thread().name
|
||||||
log.debug("{} started".format(name))
|
log.debug("{} started".format(name))
|
||||||
@ -447,8 +533,8 @@ class Client:
|
|||||||
r = self.send(
|
r = self.send(
|
||||||
functions.auth.SendCode(
|
functions.auth.SendCode(
|
||||||
self.phone_number,
|
self.phone_number,
|
||||||
self.config.api_id,
|
self.api_key.api_id,
|
||||||
self.config.api_hash
|
self.api_key.api_hash
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except (PhoneMigrate, NetworkMigrate) as e:
|
except (PhoneMigrate, NetworkMigrate) as e:
|
||||||
@ -462,7 +548,7 @@ class Client:
|
|||||||
self.test_mode,
|
self.test_mode,
|
||||||
self.proxy,
|
self.proxy,
|
||||||
self.auth_key,
|
self.auth_key,
|
||||||
self.config.api_id,
|
self.api_key.api_id,
|
||||||
client=self
|
client=self
|
||||||
)
|
)
|
||||||
self.session.start()
|
self.session.start()
|
||||||
@ -470,8 +556,8 @@ class Client:
|
|||||||
r = self.send(
|
r = self.send(
|
||||||
functions.auth.SendCode(
|
functions.auth.SendCode(
|
||||||
self.phone_number,
|
self.phone_number,
|
||||||
self.config.api_id,
|
self.api_key.api_id,
|
||||||
self.config.api_hash
|
self.api_key.api_hash
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@ -589,10 +675,16 @@ class Client:
|
|||||||
parser = ConfigParser()
|
parser = ConfigParser()
|
||||||
parser.read("config.ini")
|
parser.read("config.ini")
|
||||||
|
|
||||||
self.config = Config(
|
if parser.has_section("pyrogram"):
|
||||||
|
self.api_key = ApiKey(
|
||||||
api_id=parser.getint("pyrogram", "api_id"),
|
api_id=parser.getint("pyrogram", "api_id"),
|
||||||
api_hash=parser.get("pyrogram", "api_hash")
|
api_hash=parser.get("pyrogram", "api_hash")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.api_key = ApiKey(
|
||||||
|
api_id=int(self.api_key[0]),
|
||||||
|
api_hash=self.api_key[1]
|
||||||
|
)
|
||||||
|
|
||||||
if parser.has_section("proxy"):
|
if parser.has_section("proxy"):
|
||||||
self.proxy = Proxy(
|
self.proxy = Proxy(
|
||||||
@ -602,6 +694,15 @@ class Client:
|
|||||||
username=parser.get("proxy", "username", fallback=None) or None,
|
username=parser.get("proxy", "username", fallback=None) or None,
|
||||||
password=parser.get("proxy", "password", fallback=None) or None
|
password=parser.get("proxy", "password", fallback=None) or None
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
if self.proxy is not None:
|
||||||
|
self.proxy = Proxy(
|
||||||
|
enabled=True,
|
||||||
|
hostname=self.proxy["hostname"],
|
||||||
|
port=int(self.proxy["port"]),
|
||||||
|
username=self.proxy.get("username", None),
|
||||||
|
password=self.proxy.get("password", None)
|
||||||
|
)
|
||||||
|
|
||||||
def load_session(self, session_name):
|
def load_session(self, session_name):
|
||||||
try:
|
try:
|
||||||
@ -727,12 +828,20 @@ class Client:
|
|||||||
if peer_id in ("self", "me"):
|
if peer_id in ("self", "me"):
|
||||||
return InputPeerSelf()
|
return InputPeerSelf()
|
||||||
|
|
||||||
peer_id = peer_id.lower().strip("@")
|
peer_id = peer_id.lower().strip("@+")
|
||||||
|
|
||||||
|
try:
|
||||||
|
int(peer_id)
|
||||||
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
return self.peers_by_username[peer_id]
|
return self.peers_by_username[peer_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return self.resolve_username(peer_id)
|
return self.resolve_username(peer_id)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return self.peers_by_phone[peer_id]
|
||||||
|
except KeyError:
|
||||||
|
raise PeerIdInvalid
|
||||||
|
|
||||||
if type(peer_id) is not int:
|
if type(peer_id) is not int:
|
||||||
if isinstance(peer_id, types.PeerUser):
|
if isinstance(peer_id, types.PeerUser):
|
||||||
@ -1667,13 +1776,12 @@ class Client:
|
|||||||
part_size = 512 * 1024
|
part_size = 512 * 1024
|
||||||
file_size = os.path.getsize(path)
|
file_size = os.path.getsize(path)
|
||||||
file_total_parts = math.ceil(file_size / part_size)
|
file_total_parts = math.ceil(file_size / part_size)
|
||||||
# is_big = True if file_size > 10 * 1024 * 1024 else False
|
is_big = True if file_size > 10 * 1024 * 1024 else False
|
||||||
is_big = False # Treat all files as not-big to have the server check for the md5 sum
|
|
||||||
is_missing_part = True if file_id is not None else False
|
is_missing_part = True if file_id is not None else False
|
||||||
file_id = file_id or self.rnd_id()
|
file_id = file_id or self.rnd_id()
|
||||||
md5_sum = md5() if not is_big and not is_missing_part else None
|
md5_sum = md5() if not is_big and not is_missing_part else None
|
||||||
|
|
||||||
session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.config.api_id)
|
session = Session(self.dc_id, self.test_mode, self.proxy, self.auth_key, self.api_key.api_id)
|
||||||
session.start()
|
session.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1723,11 +1831,7 @@ class Client:
|
|||||||
volume_id: int = None,
|
volume_id: int = None,
|
||||||
local_id: int = None,
|
local_id: int = None,
|
||||||
secret: int = None,
|
secret: int = None,
|
||||||
version: int = 0):
|
version: int = 0) -> str:
|
||||||
# TODO: Refine
|
|
||||||
# TODO: Use proper file name and extension
|
|
||||||
# TODO: Remove redundant code
|
|
||||||
|
|
||||||
if dc_id != self.dc_id:
|
if dc_id != self.dc_id:
|
||||||
exported_auth = self.send(
|
exported_auth = self.send(
|
||||||
functions.auth.ExportAuthorization(
|
functions.auth.ExportAuthorization(
|
||||||
@ -1740,7 +1844,7 @@ class Client:
|
|||||||
self.test_mode,
|
self.test_mode,
|
||||||
self.proxy,
|
self.proxy,
|
||||||
Auth(dc_id, self.test_mode, self.proxy).create(),
|
Auth(dc_id, self.test_mode, self.proxy).create(),
|
||||||
self.config.api_id
|
self.api_key.api_id
|
||||||
)
|
)
|
||||||
|
|
||||||
session.start()
|
session.start()
|
||||||
@ -1757,7 +1861,7 @@ class Client:
|
|||||||
self.test_mode,
|
self.test_mode,
|
||||||
self.proxy,
|
self.proxy,
|
||||||
self.auth_key,
|
self.auth_key,
|
||||||
self.config.api_id
|
self.api_key.api_id
|
||||||
)
|
)
|
||||||
|
|
||||||
session.start()
|
session.start()
|
||||||
@ -1775,7 +1879,8 @@ class Client:
|
|||||||
version=version
|
version=version
|
||||||
)
|
)
|
||||||
|
|
||||||
limit = 512 * 1024
|
file_name = str(MsgId())
|
||||||
|
limit = 1024 * 1024
|
||||||
offset = 0
|
offset = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1788,7 +1893,7 @@ class Client:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(r, types.upload.File):
|
if isinstance(r, types.upload.File):
|
||||||
with open("_".join([str(id), str(access_hash), str(version)]) + ".jpg", "wb") as f:
|
with open(file_name, "wb") as f:
|
||||||
while True:
|
while True:
|
||||||
chunk = r.bytes
|
chunk = r.bytes
|
||||||
|
|
||||||
@ -1796,6 +1901,9 @@ class Client:
|
|||||||
break
|
break
|
||||||
|
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
|
||||||
offset += limit
|
offset += limit
|
||||||
|
|
||||||
r = session.send(
|
r = session.send(
|
||||||
@ -1805,20 +1913,21 @@ class Client:
|
|||||||
limit=limit
|
limit=limit
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(r, types.upload.FileCdnRedirect):
|
if isinstance(r, types.upload.FileCdnRedirect):
|
||||||
cdn_session = Session(
|
cdn_session = Session(
|
||||||
r.dc_id,
|
r.dc_id,
|
||||||
self.test_mode,
|
self.test_mode,
|
||||||
self.proxy,
|
self.proxy,
|
||||||
Auth(r.dc_id, self.test_mode, self.proxy).create(),
|
Auth(r.dc_id, self.test_mode, self.proxy).create(),
|
||||||
self.config.api_id,
|
self.api_key.api_id,
|
||||||
is_cdn=True
|
is_cdn=True
|
||||||
)
|
)
|
||||||
|
|
||||||
cdn_session.start()
|
cdn_session.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open("_".join([str(id), str(access_hash), str(version)]) + ".jpg", "wb") as f:
|
with open(file_name, "wb") as f:
|
||||||
while True:
|
while True:
|
||||||
r2 = cdn_session.send(
|
r2 = cdn_session.send(
|
||||||
functions.upload.GetCdnFile(
|
functions.upload.GetCdnFile(
|
||||||
@ -1830,26 +1939,47 @@ class Client:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(r2, types.upload.CdnFileReuploadNeeded):
|
if isinstance(r2, types.upload.CdnFileReuploadNeeded):
|
||||||
|
try:
|
||||||
session.send(
|
session.send(
|
||||||
functions.upload.ReuploadCdnFile(
|
functions.upload.ReuploadCdnFile(
|
||||||
file_token=r.file_token,
|
file_token=r.file_token,
|
||||||
request_token=r2.request_token
|
request_token=r2.request_token
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
except VolumeLocNotFound:
|
||||||
|
break
|
||||||
|
else:
|
||||||
continue
|
continue
|
||||||
elif isinstance(r2, types.upload.CdnFile):
|
|
||||||
chunk = r2.bytes
|
chunk = r2.bytes
|
||||||
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
|
|
||||||
# https://core.telegram.org/cdn#decrypting-files
|
# https://core.telegram.org/cdn#decrypting-files
|
||||||
decrypted_chunk = AES.ctr_decrypt(chunk, r.encryption_key, r.encryption_iv, offset)
|
decrypted_chunk = AES.ctr_decrypt(
|
||||||
|
chunk,
|
||||||
|
r.encryption_key,
|
||||||
|
r.encryption_iv,
|
||||||
|
offset
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: https://core.telegram.org/cdn#verifying-files
|
hashes = session.send(
|
||||||
# TODO: Save to temp file, flush each chunk, rename to full if everything is ok
|
functions.upload.GetCdnFileHashes(
|
||||||
|
r.file_token,
|
||||||
|
offset
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# https://core.telegram.org/cdn#verifying-files
|
||||||
|
for i, h in enumerate(hashes):
|
||||||
|
cdn_chunk = decrypted_chunk[h.limit * i: h.limit * (i + 1)]
|
||||||
|
assert h.hash == sha256(cdn_chunk).digest(), "Invalid CDN hash part {}".format(i)
|
||||||
|
|
||||||
f.write(decrypted_chunk)
|
f.write(decrypted_chunk)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
|
||||||
|
if len(chunk) < limit:
|
||||||
|
break
|
||||||
|
|
||||||
offset += limit
|
offset += limit
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(e)
|
log.error(e)
|
||||||
@ -1858,7 +1988,7 @@ class Client:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(e)
|
log.error(e)
|
||||||
else:
|
else:
|
||||||
return True
|
return file_name
|
||||||
finally:
|
finally:
|
||||||
session.stop()
|
session.stop()
|
||||||
|
|
||||||
@ -2210,3 +2340,78 @@ class Client:
|
|||||||
reply_to_msg_id=reply_to_message_id
|
reply_to_msg_id=reply_to_message_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def download_media(self, message: types.Message, file_name: str = None):
|
||||||
|
done = Event()
|
||||||
|
media = message.media if isinstance(message, types.Message) else message
|
||||||
|
|
||||||
|
self.download_queue.put((media, file_name, done))
|
||||||
|
|
||||||
|
done.wait()
|
||||||
|
|
||||||
|
def add_contacts(self, contacts: list):
|
||||||
|
"""Use this method to add contacts to your Telegram address book.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contacts (:obj:`list`):
|
||||||
|
A list of :obj:`InputPhoneContact <pyrogram.InputPhoneContact>`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
On success, the added contacts are returned.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:class:`pyrogram.Error`
|
||||||
|
"""
|
||||||
|
imported_contacts = self.send(
|
||||||
|
functions.contacts.ImportContacts(
|
||||||
|
contacts=contacts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fetch_peers(imported_contacts.users)
|
||||||
|
|
||||||
|
return imported_contacts
|
||||||
|
|
||||||
|
def delete_contacts(self, ids: list):
|
||||||
|
"""Use this method to delete contacts from your Telegram address book
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids (:obj:`list`):
|
||||||
|
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.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:class:`pyrogram.Error`
|
||||||
|
"""
|
||||||
|
contacts = []
|
||||||
|
|
||||||
|
for i in ids:
|
||||||
|
try:
|
||||||
|
input_user = self.resolve_peer(i)
|
||||||
|
except PeerIdInvalid:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if isinstance(input_user, types.InputPeerUser):
|
||||||
|
contacts.append(input_user)
|
||||||
|
|
||||||
|
return self.send(
|
||||||
|
functions.contacts.DeleteContacts(
|
||||||
|
id=contacts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_contacts(self, _hash: int = 0):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
contacts = self.send(functions.contacts.GetContacts(_hash))
|
||||||
|
except FloodWait as e:
|
||||||
|
log.info("Get contacts flood wait: {}".format(e.x))
|
||||||
|
time.sleep(e.x)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
log.info("Contacts count: {}".format(len(contacts.users)))
|
||||||
|
self.fetch_peers(contacts.users)
|
||||||
|
return contacts
|
||||||
|
@ -1,3 +1,22 @@
|
|||||||
|
# Pyrogram - Telegram MTProto API Client Library for Python
|
||||||
|
# Copyright (C) 2017-2018 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 Emoji:
|
class Emoji:
|
||||||
HELMET_WITH_WHITE_CROSS_TYPE_1_2 = "\u26d1\U0001f3fb"
|
HELMET_WITH_WHITE_CROSS_TYPE_1_2 = "\u26d1\U0001f3fb"
|
||||||
HELMET_WITH_WHITE_CROSS_TYPE_3 = "\u26d1\U0001f3fc"
|
HELMET_WITH_WHITE_CROSS_TYPE_3 = "\u26d1\U0001f3fc"
|
||||||
|
43
pyrogram/client/input_phone_contact.py
Normal file
43
pyrogram/client/input_phone_contact.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Pyrogram - Telegram MTProto API Client Library for Python
|
||||||
|
# Copyright (C) 2017-2018 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.types import InputPhoneContact as RawInputPhoneContact
|
||||||
|
|
||||||
|
|
||||||
|
class InputPhoneContact:
|
||||||
|
"""This object represents a Phone Contact to be added in your Telegram address book.
|
||||||
|
It is intended to be used with :obj:`pyrogram.Client.add_contacts`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phone (:obj:`str`):
|
||||||
|
Contact's phone number
|
||||||
|
|
||||||
|
first_name (:obj:`str`):
|
||||||
|
Contact's first name
|
||||||
|
|
||||||
|
last_name (:obj:`str`, optional):
|
||||||
|
Contact's last name
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, phone: str, first_name: str, last_name: str = ""):
|
||||||
|
return RawInputPhoneContact(
|
||||||
|
client_id=0,
|
||||||
|
phone="+" + phone.strip("+"),
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name
|
||||||
|
)
|
@ -44,6 +44,11 @@ class TCP(socks.socksocket):
|
|||||||
password=proxy.password
|
password=proxy.password
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.info("Using proxy {}:{}".format(
|
||||||
|
proxy.hostname,
|
||||||
|
proxy.port
|
||||||
|
))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
try:
|
try:
|
||||||
self.shutdown(socket.SHUT_RDWR)
|
self.shutdown(socket.SHUT_RDWR)
|
||||||
|
@ -23,7 +23,7 @@ log = logging.getLogger(__name__)
|
|||||||
try:
|
try:
|
||||||
import tgcrypto
|
import tgcrypto
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logging.warning(
|
log.warning(
|
||||||
"TgCrypto is missing! "
|
"TgCrypto is missing! "
|
||||||
"Pyrogram will work the same, but at a much slower speed. "
|
"Pyrogram will work the same, but at a much slower speed. "
|
||||||
"More info: https://docs.pyrogram.ml/resources/TgCrypto"
|
"More info: https://docs.pyrogram.ml/resources/TgCrypto"
|
||||||
|
@ -51,12 +51,12 @@ class Auth:
|
|||||||
self.test_mode = test_mode
|
self.test_mode = test_mode
|
||||||
|
|
||||||
self.connection = Connection(DataCenter(dc_id, test_mode), proxy)
|
self.connection = Connection(DataCenter(dc_id, test_mode), proxy)
|
||||||
self.msg_id = MsgId()
|
|
||||||
|
|
||||||
def pack(self, data: Object) -> bytes:
|
@staticmethod
|
||||||
|
def pack(data: Object) -> bytes:
|
||||||
return (
|
return (
|
||||||
bytes(8)
|
bytes(8)
|
||||||
+ Long(self.msg_id())
|
+ Long(MsgId())
|
||||||
+ Int(len(data.write()))
|
+ Int(len(data.write()))
|
||||||
+ data.write()
|
+ data.write()
|
||||||
)
|
)
|
||||||
|
@ -26,14 +26,13 @@ not_content_related = [Ping, HttpWait, MsgsAck, MsgContainer]
|
|||||||
|
|
||||||
|
|
||||||
class MsgFactory:
|
class MsgFactory:
|
||||||
def __init__(self, msg_id: MsgId):
|
def __init__(self):
|
||||||
self.msg_id = msg_id
|
|
||||||
self.seq_no = SeqNo()
|
self.seq_no = SeqNo()
|
||||||
|
|
||||||
def __call__(self, body: Object) -> Message:
|
def __call__(self, body: Object) -> Message:
|
||||||
return Message(
|
return Message(
|
||||||
body,
|
body,
|
||||||
self.msg_id(),
|
MsgId(),
|
||||||
self.seq_no(type(body) not in not_content_related),
|
self.seq_no(type(body) not in not_content_related),
|
||||||
len(body)
|
len(body)
|
||||||
)
|
)
|
||||||
|
@ -21,17 +21,15 @@ from time import time
|
|||||||
|
|
||||||
|
|
||||||
class MsgId:
|
class MsgId:
|
||||||
def __init__(self, delta_time: float = 0.0):
|
last_time = 0
|
||||||
self.delta_time = delta_time
|
offset = 0
|
||||||
self.last_time = 0
|
lock = Lock()
|
||||||
self.offset = 0
|
|
||||||
self.lock = Lock()
|
|
||||||
|
|
||||||
def __call__(self) -> int:
|
def __new__(cls) -> int:
|
||||||
with self.lock:
|
with cls.lock:
|
||||||
now = time()
|
now = time()
|
||||||
self.offset = self.offset + 4 if now == self.last_time else 0
|
cls.offset = cls.offset + 4 if now == cls.last_time else 0
|
||||||
msg_id = int((now + self.delta_time) * 2 ** 32) + self.offset
|
msg_id = int(now * 2 ** 32) + cls.offset
|
||||||
self.last_time = now
|
cls.last_time = now
|
||||||
|
|
||||||
return msg_id
|
return msg_id
|
||||||
|
@ -89,9 +89,8 @@ class Session:
|
|||||||
self.auth_key = auth_key
|
self.auth_key = auth_key
|
||||||
self.auth_key_id = sha1(auth_key).digest()[-8:]
|
self.auth_key_id = sha1(auth_key).digest()[-8:]
|
||||||
|
|
||||||
self.msg_id = MsgId()
|
self.session_id = Long(MsgId())
|
||||||
self.session_id = Long(self.msg_id())
|
self.msg_factory = MsgFactory()
|
||||||
self.msg_factory = MsgFactory(self.msg_id)
|
|
||||||
|
|
||||||
self.current_salt = None
|
self.current_salt = None
|
||||||
|
|
||||||
@ -146,7 +145,7 @@ class Session:
|
|||||||
self.ping_thread.start()
|
self.ping_thread.start()
|
||||||
|
|
||||||
log.info("Connection inited: Layer {}".format(layer))
|
log.info("Connection inited: Layer {}".format(layer))
|
||||||
except (OSError, TimeoutError):
|
except (OSError, TimeoutError, Error):
|
||||||
self.stop()
|
self.stop()
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
@ -338,7 +337,10 @@ class Session:
|
|||||||
while True:
|
while True:
|
||||||
packet = self.connection.recv()
|
packet = self.connection.recv()
|
||||||
|
|
||||||
if packet is None or (len(packet) == 4 and Int.read(BytesIO(packet)) == -404):
|
if packet is None or len(packet) == 4:
|
||||||
|
if packet:
|
||||||
|
log.warning("Server sent \"{}\"".format(Int.read(BytesIO(packet))))
|
||||||
|
|
||||||
if self.is_connected.is_set():
|
if self.is_connected.is_set():
|
||||||
Thread(target=self.restart, name="RestartThread").start()
|
Thread(target=self.restart, name="RestartThread").start()
|
||||||
break
|
break
|
||||||
|
10
setup.py
10
setup.py
@ -66,6 +66,14 @@ setup(
|
|||||||
],
|
],
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
install_requires=["pyaes", "pysocks"],
|
install_requires=[
|
||||||
|
"pyaes",
|
||||||
|
"pysocks"
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
"tgcrypto": [
|
||||||
|
"tgcrypto"
|
||||||
|
]
|
||||||
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user