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
|
||||
|
||||
- Or, with TgCrypto_:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
$ pip install --upgrade pyrogram[tgcrypto]
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
@ -168,7 +174,7 @@ License
|
||||
|
||||
.. _`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
|
||||
|
||||
@ -195,8 +201,8 @@ License
|
||||
•
|
||||
<a href="https://t.me/PyrogramChat">
|
||||
Community
|
||||
</a
|
||||
<br><br><br>
|
||||
</a>
|
||||
<br><br>
|
||||
<a href="compiler/api/source/main_api.tl">
|
||||
<img src="https://www.pyrogram.ml/images/scheme.svg"
|
||||
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"
|
||||
)
|
||||
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.6.1"
|
||||
|
||||
from .api.errors import Error
|
||||
from .client import ChatAction
|
||||
from .client import Client
|
||||
from .client import ParseMode
|
||||
from .client.input_media import InputMedia
|
||||
from .client.input_phone_contact import InputPhoneContact
|
||||
from .client import Emoji
|
||||
|
@ -27,6 +27,7 @@ import threading
|
||||
import time
|
||||
from collections import namedtuple
|
||||
from configparser import ConfigParser
|
||||
from datetime import datetime
|
||||
from hashlib import sha256, md5
|
||||
from queue import Queue
|
||||
from signal import signal, SIGINT, SIGTERM, SIGABRT
|
||||
@ -39,8 +40,8 @@ from pyrogram.api.errors import (
|
||||
PhoneNumberUnoccupied, PhoneCodeInvalid, PhoneCodeHashEmpty,
|
||||
PhoneCodeExpired, PhoneCodeEmpty, SessionPasswordNeeded,
|
||||
PasswordHashInvalid, FloodWait, PeerIdInvalid, FilePartMissing,
|
||||
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned
|
||||
)
|
||||
ChatAdminRequired, FirstnameInvalid, PhoneNumberBanned,
|
||||
VolumeLocNotFound)
|
||||
from pyrogram.api.types import (
|
||||
User, Chat, Channel,
|
||||
PeerUser, PeerChannel,
|
||||
@ -49,12 +50,13 @@ from pyrogram.api.types import (
|
||||
)
|
||||
from pyrogram.crypto import AES
|
||||
from pyrogram.session import Auth, Session
|
||||
from pyrogram.session.internals import MsgId
|
||||
from .input_media import InputMedia
|
||||
from .style import Markdown, HTML
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
@ -70,6 +72,17 @@ class Client:
|
||||
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.
|
||||
|
||||
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):
|
||||
Enable or disable log-in to testing servers. Defaults to False.
|
||||
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/(.+)$")
|
||||
DIALOGS_AT_ONCE = 100
|
||||
UPDATES_WORKERS = 2
|
||||
DOWNLOAD_WORKERS = 1
|
||||
|
||||
def __init__(self,
|
||||
session_name: str,
|
||||
api_key: tuple or ApiKey = None,
|
||||
proxy: dict or Proxy = None,
|
||||
test_mode: bool = False,
|
||||
phone_number: str = None,
|
||||
phone_code: str or callable = None,
|
||||
@ -114,6 +130,8 @@ class Client:
|
||||
last_name: str = None,
|
||||
workers: int = 4):
|
||||
self.session_name = session_name
|
||||
self.api_key = api_key
|
||||
self.proxy = proxy
|
||||
self.test_mode = test_mode
|
||||
|
||||
self.phone_number = phone_number
|
||||
@ -132,14 +150,13 @@ class Client:
|
||||
|
||||
self.peers_by_id = {}
|
||||
self.peers_by_username = {}
|
||||
self.peers_by_phone = {}
|
||||
|
||||
self.channels_pts = {}
|
||||
|
||||
self.markdown = Markdown(self.peers_by_id)
|
||||
self.html = HTML(self.peers_by_id)
|
||||
|
||||
self.config = None
|
||||
self.proxy = None
|
||||
self.session = None
|
||||
|
||||
self.is_idle = Event()
|
||||
@ -148,6 +165,8 @@ class Client:
|
||||
self.update_queue = Queue()
|
||||
self.update_handler = None
|
||||
|
||||
self.download_queue = Queue()
|
||||
|
||||
def start(self):
|
||||
"""Use this method to start the Client after creating it.
|
||||
Requires no parameters.
|
||||
@ -163,7 +182,7 @@ class Client:
|
||||
self.test_mode,
|
||||
self.proxy,
|
||||
self.auth_key,
|
||||
self.config.api_id,
|
||||
self.api_key.api_id,
|
||||
client=self
|
||||
)
|
||||
|
||||
@ -176,8 +195,9 @@ class Client:
|
||||
self.password = None
|
||||
self.save_session()
|
||||
|
||||
self.rnd_id = self.session.msg_id
|
||||
self.rnd_id = MsgId
|
||||
self.get_dialogs()
|
||||
self.get_contacts()
|
||||
|
||||
for i in range(self.UPDATES_WORKERS):
|
||||
Thread(target=self.updates_worker, name="UpdatesWorker#{}".format(i + 1)).start()
|
||||
@ -185,6 +205,9 @@ class Client:
|
||||
for i in range(self.workers):
|
||||
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()
|
||||
|
||||
def stop(self):
|
||||
@ -199,6 +222,9 @@ class Client:
|
||||
for _ in range(self.workers):
|
||||
self.update_queue.put(None)
|
||||
|
||||
for _ in range(self.DOWNLOAD_WORKERS):
|
||||
self.download_queue.put(None)
|
||||
|
||||
def fetch_peers(self, entities: list):
|
||||
for entity in entities:
|
||||
if isinstance(entity, User):
|
||||
@ -213,6 +239,7 @@ class Client:
|
||||
continue
|
||||
|
||||
username = entity.username
|
||||
phone = entity.phone
|
||||
|
||||
input_peer = InputPeerUser(
|
||||
user_id=user_id,
|
||||
@ -224,6 +251,9 @@ class Client:
|
||||
if username is not None:
|
||||
self.peers_by_username[username] = input_peer
|
||||
|
||||
if phone is not None:
|
||||
self.peers_by_phone[phone] = input_peer
|
||||
|
||||
if isinstance(entity, Chat):
|
||||
chat_id = entity.id
|
||||
|
||||
@ -260,6 +290,62 @@ class Client:
|
||||
if username is not None:
|
||||
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):
|
||||
name = threading.current_thread().name
|
||||
log.debug("{} started".format(name))
|
||||
@ -447,8 +533,8 @@ class Client:
|
||||
r = self.send(
|
||||
functions.auth.SendCode(
|
||||
self.phone_number,
|
||||
self.config.api_id,
|
||||
self.config.api_hash
|
||||
self.api_key.api_id,
|
||||
self.api_key.api_hash
|
||||
)
|
||||
)
|
||||
except (PhoneMigrate, NetworkMigrate) as e:
|
||||
@ -462,7 +548,7 @@ class Client:
|
||||
self.test_mode,
|
||||
self.proxy,
|
||||
self.auth_key,
|
||||
self.config.api_id,
|
||||
self.api_key.api_id,
|
||||
client=self
|
||||
)
|
||||
self.session.start()
|
||||
@ -470,8 +556,8 @@ class Client:
|
||||
r = self.send(
|
||||
functions.auth.SendCode(
|
||||
self.phone_number,
|
||||
self.config.api_id,
|
||||
self.config.api_hash
|
||||
self.api_key.api_id,
|
||||
self.api_key.api_hash
|
||||
)
|
||||
)
|
||||
break
|
||||
@ -589,10 +675,16 @@ class Client:
|
||||
parser = ConfigParser()
|
||||
parser.read("config.ini")
|
||||
|
||||
self.config = Config(
|
||||
api_id=parser.getint("pyrogram", "api_id"),
|
||||
api_hash=parser.get("pyrogram", "api_hash")
|
||||
)
|
||||
if parser.has_section("pyrogram"):
|
||||
self.api_key = ApiKey(
|
||||
api_id=parser.getint("pyrogram", "api_id"),
|
||||
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"):
|
||||
self.proxy = Proxy(
|
||||
@ -602,6 +694,15 @@ class Client:
|
||||
username=parser.get("proxy", "username", 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):
|
||||
try:
|
||||
@ -727,12 +828,20 @@ class Client:
|
||||
if peer_id in ("self", "me"):
|
||||
return InputPeerSelf()
|
||||
|
||||
peer_id = peer_id.lower().strip("@")
|
||||
peer_id = peer_id.lower().strip("@+")
|
||||
|
||||
try:
|
||||
return self.peers_by_username[peer_id]
|
||||
except KeyError:
|
||||
return self.resolve_username(peer_id)
|
||||
int(peer_id)
|
||||
except ValueError:
|
||||
try:
|
||||
return self.peers_by_username[peer_id]
|
||||
except KeyError:
|
||||
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 isinstance(peer_id, types.PeerUser):
|
||||
@ -1667,13 +1776,12 @@ class Client:
|
||||
part_size = 512 * 1024
|
||||
file_size = os.path.getsize(path)
|
||||
file_total_parts = math.ceil(file_size / part_size)
|
||||
# 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_big = True if file_size > 10 * 1024 * 1024 else False
|
||||
is_missing_part = True if file_id is not None else False
|
||||
file_id = file_id or self.rnd_id()
|
||||
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()
|
||||
|
||||
try:
|
||||
@ -1723,11 +1831,7 @@ class Client:
|
||||
volume_id: int = None,
|
||||
local_id: int = None,
|
||||
secret: int = None,
|
||||
version: int = 0):
|
||||
# TODO: Refine
|
||||
# TODO: Use proper file name and extension
|
||||
# TODO: Remove redundant code
|
||||
|
||||
version: int = 0) -> str:
|
||||
if dc_id != self.dc_id:
|
||||
exported_auth = self.send(
|
||||
functions.auth.ExportAuthorization(
|
||||
@ -1740,7 +1844,7 @@ class Client:
|
||||
self.test_mode,
|
||||
self.proxy,
|
||||
Auth(dc_id, self.test_mode, self.proxy).create(),
|
||||
self.config.api_id
|
||||
self.api_key.api_id
|
||||
)
|
||||
|
||||
session.start()
|
||||
@ -1757,7 +1861,7 @@ class Client:
|
||||
self.test_mode,
|
||||
self.proxy,
|
||||
self.auth_key,
|
||||
self.config.api_id
|
||||
self.api_key.api_id
|
||||
)
|
||||
|
||||
session.start()
|
||||
@ -1775,7 +1879,8 @@ class Client:
|
||||
version=version
|
||||
)
|
||||
|
||||
limit = 512 * 1024
|
||||
file_name = str(MsgId())
|
||||
limit = 1024 * 1024
|
||||
offset = 0
|
||||
|
||||
try:
|
||||
@ -1788,7 +1893,7 @@ class Client:
|
||||
)
|
||||
|
||||
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:
|
||||
chunk = r.bytes
|
||||
|
||||
@ -1796,6 +1901,9 @@ class Client:
|
||||
break
|
||||
|
||||
f.write(chunk)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
offset += limit
|
||||
|
||||
r = session.send(
|
||||
@ -1805,20 +1913,21 @@ class Client:
|
||||
limit=limit
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(r, types.upload.FileCdnRedirect):
|
||||
cdn_session = Session(
|
||||
r.dc_id,
|
||||
self.test_mode,
|
||||
self.proxy,
|
||||
Auth(r.dc_id, self.test_mode, self.proxy).create(),
|
||||
self.config.api_id,
|
||||
self.api_key.api_id,
|
||||
is_cdn=True
|
||||
)
|
||||
|
||||
cdn_session.start()
|
||||
|
||||
try:
|
||||
with open("_".join([str(id), str(access_hash), str(version)]) + ".jpg", "wb") as f:
|
||||
with open(file_name, "wb") as f:
|
||||
while True:
|
||||
r2 = cdn_session.send(
|
||||
functions.upload.GetCdnFile(
|
||||
@ -1830,27 +1939,48 @@ class Client:
|
||||
)
|
||||
|
||||
if isinstance(r2, types.upload.CdnFileReuploadNeeded):
|
||||
session.send(
|
||||
functions.upload.ReuploadCdnFile(
|
||||
file_token=r.file_token,
|
||||
request_token=r2.request_token
|
||||
try:
|
||||
session.send(
|
||||
functions.upload.ReuploadCdnFile(
|
||||
file_token=r.file_token,
|
||||
request_token=r2.request_token
|
||||
)
|
||||
)
|
||||
)
|
||||
continue
|
||||
elif isinstance(r2, types.upload.CdnFile):
|
||||
chunk = r2.bytes
|
||||
|
||||
if not chunk:
|
||||
except VolumeLocNotFound:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
# https://core.telegram.org/cdn#decrypting-files
|
||||
decrypted_chunk = AES.ctr_decrypt(chunk, r.encryption_key, r.encryption_iv, offset)
|
||||
chunk = r2.bytes
|
||||
|
||||
# TODO: https://core.telegram.org/cdn#verifying-files
|
||||
# TODO: Save to temp file, flush each chunk, rename to full if everything is ok
|
||||
# https://core.telegram.org/cdn#decrypting-files
|
||||
decrypted_chunk = AES.ctr_decrypt(
|
||||
chunk,
|
||||
r.encryption_key,
|
||||
r.encryption_iv,
|
||||
offset
|
||||
)
|
||||
|
||||
f.write(decrypted_chunk)
|
||||
offset += limit
|
||||
hashes = session.send(
|
||||
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.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
if len(chunk) < limit:
|
||||
break
|
||||
|
||||
offset += limit
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
finally:
|
||||
@ -1858,7 +1988,7 @@ class Client:
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
else:
|
||||
return True
|
||||
return file_name
|
||||
finally:
|
||||
session.stop()
|
||||
|
||||
@ -2210,3 +2340,78 @@ class Client:
|
||||
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:
|
||||
HELMET_WITH_WHITE_CROSS_TYPE_1_2 = "\u26d1\U0001f3fb"
|
||||
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
|
||||
)
|
||||
|
||||
log.info("Using proxy {}:{}".format(
|
||||
proxy.hostname,
|
||||
proxy.port
|
||||
))
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.shutdown(socket.SHUT_RDWR)
|
||||
|
@ -23,7 +23,7 @@ log = logging.getLogger(__name__)
|
||||
try:
|
||||
import tgcrypto
|
||||
except ImportError:
|
||||
logging.warning(
|
||||
log.warning(
|
||||
"TgCrypto is missing! "
|
||||
"Pyrogram will work the same, but at a much slower speed. "
|
||||
"More info: https://docs.pyrogram.ml/resources/TgCrypto"
|
||||
|
@ -51,12 +51,12 @@ class Auth:
|
||||
self.test_mode = test_mode
|
||||
|
||||
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 (
|
||||
bytes(8)
|
||||
+ Long(self.msg_id())
|
||||
+ Long(MsgId())
|
||||
+ Int(len(data.write()))
|
||||
+ data.write()
|
||||
)
|
||||
|
@ -26,14 +26,13 @@ not_content_related = [Ping, HttpWait, MsgsAck, MsgContainer]
|
||||
|
||||
|
||||
class MsgFactory:
|
||||
def __init__(self, msg_id: MsgId):
|
||||
self.msg_id = msg_id
|
||||
def __init__(self):
|
||||
self.seq_no = SeqNo()
|
||||
|
||||
def __call__(self, body: Object) -> Message:
|
||||
return Message(
|
||||
body,
|
||||
self.msg_id(),
|
||||
MsgId(),
|
||||
self.seq_no(type(body) not in not_content_related),
|
||||
len(body)
|
||||
)
|
||||
|
@ -21,17 +21,15 @@ from time import time
|
||||
|
||||
|
||||
class MsgId:
|
||||
def __init__(self, delta_time: float = 0.0):
|
||||
self.delta_time = delta_time
|
||||
self.last_time = 0
|
||||
self.offset = 0
|
||||
self.lock = Lock()
|
||||
last_time = 0
|
||||
offset = 0
|
||||
lock = Lock()
|
||||
|
||||
def __call__(self) -> int:
|
||||
with self.lock:
|
||||
def __new__(cls) -> int:
|
||||
with cls.lock:
|
||||
now = time()
|
||||
self.offset = self.offset + 4 if now == self.last_time else 0
|
||||
msg_id = int((now + self.delta_time) * 2 ** 32) + self.offset
|
||||
self.last_time = now
|
||||
cls.offset = cls.offset + 4 if now == cls.last_time else 0
|
||||
msg_id = int(now * 2 ** 32) + cls.offset
|
||||
cls.last_time = now
|
||||
|
||||
return msg_id
|
||||
|
@ -89,9 +89,8 @@ class Session:
|
||||
self.auth_key = auth_key
|
||||
self.auth_key_id = sha1(auth_key).digest()[-8:]
|
||||
|
||||
self.msg_id = MsgId()
|
||||
self.session_id = Long(self.msg_id())
|
||||
self.msg_factory = MsgFactory(self.msg_id)
|
||||
self.session_id = Long(MsgId())
|
||||
self.msg_factory = MsgFactory()
|
||||
|
||||
self.current_salt = None
|
||||
|
||||
@ -146,7 +145,7 @@ class Session:
|
||||
self.ping_thread.start()
|
||||
|
||||
log.info("Connection inited: Layer {}".format(layer))
|
||||
except (OSError, TimeoutError):
|
||||
except (OSError, TimeoutError, Error):
|
||||
self.stop()
|
||||
else:
|
||||
break
|
||||
@ -338,7 +337,10 @@ class Session:
|
||||
while True:
|
||||
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():
|
||||
Thread(target=self.restart, name="RestartThread").start()
|
||||
break
|
||||
|
Loading…
x
Reference in New Issue
Block a user