diff --git a/MANIFEST.in b/MANIFEST.in index a1d19d94..f818e13a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ ## Include -include COPYING COPYING.lesser NOTICE requirements.txt requirements_extras.txt +include COPYING COPYING.lesser NOTICE requirements.txt recursive-include compiler *.py *.tl *.tsv *.txt ## Exclude diff --git a/README.rst b/README.rst index b1015db2..e704d46a 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ |header| -Pyrogram |twitter| -================== +Pyrogram +======== .. code-block:: python @@ -26,8 +26,8 @@ Features - **Easy to use**: You can easily install Pyrogram using pip and start building your app right away. - **High-level**: The low-level details of MTProto are abstracted and automatically handled. - **Fast**: Crypto parts are boosted up by TgCrypto_, a high-performance library written in pure C. -- **Updated** to the latest Telegram API version, currently Layer 76 running on MTProto 2.0. -- **Documented**: Pyrogram API methods are documented and resemble the Telegram Bot API. +- **Updated** to the latest Telegram API version, currently Layer 79 on top of MTProto 2.0. +- **Documented**: The Pyrogram API is well documented and resembles the Telegram Bot API. - **Full API**, allowing to execute any advanced action an official client is able to do, and more. Requirements @@ -54,7 +54,7 @@ Getting Started Contributing ------------ -Pyrogram is brand new! **You are welcome to try it and help make it better** by either submitting pull +Pyrogram is brand new, and **you are welcome to try it and help make it even better** by either submitting pull requests or reporting issues/bugs as well as suggesting best practices, ideas, enhancements on both code and documentation. Any help is appreciated! @@ -100,28 +100,25 @@ Copyright & License

- Scheme Layer 76 + Scheme Layer - TgCrypto

-.. |twitter| image:: https://media.pyrogram.ml/images/twitter.svg - :target: https://twitter.com/intent/tweet?text=Build%20custom%20Telegram%20applications%20with%20Pyrogram&url=https://github.com/pyrogram/pyrogram&hashtags=Telegram,MTProto,Python - .. |logo| image:: https://pyrogram.ml/images/logo.png :target: https://pyrogram.ml :alt: Pyrogram .. |description| replace:: **Telegram MTProto API Client Library for Python** -.. |scheme| image:: https://www.pyrogram.ml/images/scheme.svg +.. |scheme| image:: "https://img.shields.io/badge/SCHEME-LAYER%2079-eda738.svg?longCache=true&style=for-the-badge&colorA=262b30" :target: compiler/api/source/main_api.tl - :alt: Scheme Layer 76 + :alt: Scheme Layer -.. |tgcrypto| image:: https://www.pyrogram.ml/images/tgcrypto.svg +.. |tgcrypto| image:: "https://img.shields.io/badge/TGCRYPTO-V1.0.4-eda738.svg?longCache=true&style=for-the-badge&colorA=262b30" :target: https://github.com/pyrogram/tgcrypto :alt: TgCrypto diff --git a/docs/source/resources/AutoAuthorization.rst b/docs/source/resources/AutoAuthorization.rst index 73504f80..d7a099fe 100644 --- a/docs/source/resources/AutoAuthorization.rst +++ b/docs/source/resources/AutoAuthorization.rst @@ -14,27 +14,31 @@ Log In To automate the **Log In** process, pass your ``phone_number`` and ``password`` (if you have one) in the Client parameters. If you want to retrieve the phone code programmatically, pass a callback function in the ``phone_code`` field — this -function must return the correct phone code as string (e.g., "12345") — otherwise, ignore this parameter, Pyrogram will -ask you to input the phone code manually. +function accepts a single positional argument (phone_number) and must return the correct phone code (e.g., "12345") +— otherwise, ignore this parameter, Pyrogram will ask you to input the phone code manually. + +Example: .. code-block:: python from pyrogram import Client - def phone_code_callback(): + def phone_code_callback(phone_number): code = ... # Get your code programmatically - return code # Must be string, e.g., "12345" + return code # e.g., "12345" app = Client( session_name="example", phone_number="39**********", - phone_code=phone_code_callback, + phone_code=phone_code_callback, # Note the missing parentheses password="password" # (if you have one) ) app.start() + print(app.get_me()) + app.stop() Sign Up @@ -44,23 +48,27 @@ To automate the **Sign Up** process (i.e., automatically create a new Telegram a ``first_name`` and ``last_name`` fields alongside the other parameters; they will be used to automatically create a new Telegram account in case the phone number you passed is not registered yet. +Example: + .. code-block:: python from pyrogram import Client - def phone_code_callback(): + def phone_code_callback(phone_number): code = ... # Get your code programmatically - return code # Must be string, e.g., "12345" + return code # e.g., "12345" app = Client( session_name="example", phone_number="39**********", - phone_code=phone_code_callback, + phone_code=phone_code_callback, # Note the missing parentheses first_name="Pyrogram", last_name="" # Can be an empty string ) app.start() + print(app.get_me()) + app.stop() \ No newline at end of file diff --git a/pyrogram/__init__.py b/pyrogram/__init__.py index 298b52c2..e02cc540 100644 --- a/pyrogram/__init__.py +++ b/pyrogram/__init__.py @@ -31,7 +31,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès ` """ if not self.is_started: raise ConnectionError("Client has not been started") - r = await self.session.send(data) + r = await self.session.send(data, retries, timeout) self.fetch_peers(getattr(r, "users", [])) self.fetch_peers(getattr(r, "chats", [])) @@ -847,6 +872,31 @@ class Client(Methods, BaseClient): "More info: https://docs.pyrogram.ml/start/ProjectSetup#configuration" ) + for option in {"app_version", "device_model", "system_version", "system_lang_code", "lang_code"}: + if getattr(self, option): + pass + else: + setattr(self, option, Client.APP_VERSION) + + if parser.has_section("pyrogram"): + setattr(self, option, parser.get( + "pyrogram", + option, + fallback=getattr(Client, option.upper()) + )) + + if self.lang_code: + pass + else: + self.lang_code = Client.LANG_CODE + + if parser.has_section("pyrogram"): + self.lang_code = parser.get( + "pyrogram", + "lang_code", + fallback=Client.LANG_CODE + ) + if self._proxy: self._proxy["enabled"] = True else: @@ -1017,7 +1067,7 @@ class Client(Methods, BaseClient): 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.api_id) + session = Session(self, self.dc_id, self.auth_key, is_media=True) await session.start() try: @@ -1101,11 +1151,10 @@ class Client(Methods, BaseClient): ) session = Session( + self, dc_id, - self.test_mode, - self._proxy, await Auth(dc_id, self.test_mode, self._proxy).create(), - self.api_id + is_media=True ) await session.start() @@ -1120,11 +1169,10 @@ class Client(Methods, BaseClient): ) else: session = Session( + self, dc_id, - self.test_mode, - self._proxy, self.auth_key, - self.api_id + is_media=True ) await session.start() @@ -1190,11 +1238,10 @@ class Client(Methods, BaseClient): if cdn_session is None: cdn_session = Session( + self, r.dc_id, - self.test_mode, - self._proxy, await Auth(r.dc_id, self.test_mode, self._proxy).create(), - self.api_id, + is_media=True, is_cdn=True ) diff --git a/pyrogram/client/ext/base_client.py b/pyrogram/client/ext/base_client.py index a5f4dd91..2a438168 100644 --- a/pyrogram/client/ext/base_client.py +++ b/pyrogram/client/ext/base_client.py @@ -17,14 +17,32 @@ # along with Pyrogram. If not, see . import asyncio +import platform import re +from pyrogram import __version__ from ..style import Markdown, HTML from ...api.core import Object +from ...session import Session from ...session.internals import MsgId class BaseClient: + APP_VERSION = "Pyrogram \U0001f525 {}".format(__version__) + + DEVICE_MODEL = "{} {}".format( + platform.python_implementation(), + platform.python_version() + ) + + SYSTEM_VERSION = "{} {}".format( + platform.system(), + platform.release() + ) + + SYSTEM_LANG_CODE = "en" + LANG_CODE = "en" + 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 @@ -76,7 +94,7 @@ class BaseClient: self.disconnect_handler = None - def send(self, data: Object): + def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT): pass def resolve_peer(self, peer_id: int or str): diff --git a/pyrogram/client/methods/bots/request_callback_answer.py b/pyrogram/client/methods/bots/request_callback_answer.py index 4ca72592..ad197652 100644 --- a/pyrogram/client/methods/bots/request_callback_answer.py +++ b/pyrogram/client/methods/bots/request_callback_answer.py @@ -25,9 +25,8 @@ class RequestCallbackAnswer(BaseClient): chat_id: int or str, message_id: int, callback_data: str): - """Use this method to request a callback answer from bots. This is the equivalent of clicking an inline button - containing callback data. The answer contains info useful for clients to display a notification at the top of - the chat screen or as an alert. + """Use this method to request a callback answer from bots. This is the equivalent of clicking an + inline button containing callback data. Args: chat_id (``int`` | ``str``): @@ -41,11 +40,21 @@ class RequestCallbackAnswer(BaseClient): callback_data (``str``): Callback data associated with the inline button you want to get the answer from. + + 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:`Error ` + ``TimeoutError``: If the bot fails to answer within 10 seconds """ return await self.send( functions.messages.GetBotCallbackAnswer( peer=self.resolve_peer(chat_id), msg_id=message_id, data=callback_data.encode() - ) + ), + retries=0, + timeout=10 ) diff --git a/pyrogram/session/session.py b/pyrogram/session/session.py index a41ff5fa..9dfe92d1 100644 --- a/pyrogram/session/session.py +++ b/pyrogram/session/session.py @@ -18,7 +18,6 @@ import asyncio import logging -import platform from datetime import datetime, timedelta from hashlib import sha1 from io import BytesIO @@ -43,19 +42,8 @@ class Result: class Session: - VERSION = __version__ - APP_VERSION = "Pyrogram \U0001f525 {}".format(VERSION) - - DEVICE_MODEL = "{} {}".format( - platform.python_implementation(), - platform.python_version() - ) - - SYSTEM_VERSION = "{} {}".format( - platform.system(), - platform.release() - ) - + INITIAL_SALT = 0x616e67656c696361 + NET_WORKERS = 1 WAIT_TIMEOUT = 15 MAX_RETRIES = 5 ACKS_THRESHOLD = 8 @@ -78,28 +66,24 @@ class Session: } def __init__(self, + client: pyrogram, dc_id: int, - test_mode: bool, - proxy: dict, auth_key: bytes, - api_id: int, - is_cdn: bool = False, - client: pyrogram = None): + is_media: bool = False, + is_cdn: bool = False): if not Session.notice_displayed: print("Pyrogram v{}, {}".format(__version__, __copyright__)) print("Licensed under the terms of the " + __license__, end="\n\n") Session.notice_displayed = True - self.dc_id = dc_id - self.test_mode = test_mode - self.proxy = proxy - self.api_id = api_id - self.is_cdn = is_cdn self.client = client + self.dc_id = dc_id + self.auth_key = auth_key + self.is_media = is_media + self.is_cdn = is_cdn self.connection = None - self.auth_key = auth_key self.auth_key_id = sha1(auth_key).digest()[-8:] self.session_id = Long(MsgId()) @@ -125,7 +109,7 @@ class Session: async def start(self): while True: - self.connection = Connection(DataCenter(self.dc_id, self.test_mode), self.proxy) + self.connection = Connection(DataCenter(self.dc_id, self.client.test_mode), self.client.proxy) try: await self.connection.connect() @@ -144,12 +128,14 @@ class Session: functions.InvokeWithLayer( layer, functions.InitConnection( - self.api_id, - self.DEVICE_MODEL, - self.SYSTEM_VERSION, - self.APP_VERSION, - "en", "", "en", - functions.help.GetConfig(), + api_id=self.client.api_id, + app_version=self.client.app_version, + device_model=self.client.device_model, + system_version=self.client.system_version, + system_lang_code=self.client.system_lang_code, + lang_code=self.client.lang_code, + lang_pack="", + query=functions.help.GetConfig(), ) ) ) @@ -349,7 +335,7 @@ class Session: log.info("RecvTask stopped") - async def _send(self, data: Object, wait_response: bool = True): + async def _send(self, data: Object, wait_response: bool = True, timeout: float = WAIT_TIMEOUT): message = self.msg_factory(data) msg_id = message.msg_id @@ -372,7 +358,7 @@ class Session: if wait_response: try: - await asyncio.wait_for(self.results[msg_id].event.wait(), self.WAIT_TIMEOUT) + await asyncio.wait_for(self.results[msg_id].event.wait(), timeout) except asyncio.TimeoutError: pass @@ -390,14 +376,14 @@ class Session: else: return result - async def send(self, data: Object, retries: int = MAX_RETRIES): + async def send(self, data: Object, retries: int = MAX_RETRIES, timeout: float = WAIT_TIMEOUT): try: await asyncio.wait_for(self.is_connected.wait(), self.WAIT_TIMEOUT) except asyncio.TimeoutError: pass try: - return await self._send(data) + return await self._send(data, timeout=timeout) except (OSError, TimeoutError, InternalServerError) as e: if retries == 0: raise e from None @@ -408,7 +394,7 @@ class Session: datetime.now(), type(data))) await asyncio.sleep(0.5) - return await self.send(data, retries - 1) + return await self.send(data, retries - 1, timeout) # class Result: # def __init__(self): @@ -807,4 +793,4 @@ class Session: # datetime.now(), type(data))) # # time.sleep(0.5) -# return self.send(data, retries - 1) +# return self.send(data, retries - 1, timeout)