Merge branch 'develop' into asyncio

# Conflicts:
#	pyrogram/client/client.py
#	pyrogram/client/ext/base_client.py
#	pyrogram/client/methods/bots/request_callback_answer.py
#	pyrogram/session/session.py
This commit is contained in:
Dan 2018-06-24 19:27:37 +02:00
commit 5f727cb5a2
8 changed files with 165 additions and 100 deletions

View File

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

View File

@ -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
</a>
<br><br>
<a href="compiler/api/source/main_api.tl">
<img src="https://media.pyrogram.ml/images/scheme.svg"
alt="Scheme Layer 76">
<img src="https://img.shields.io/badge/SCHEME-LAYER%2079-eda738.svg?longCache=true&style=for-the-badge&colorA=262b30"
alt="Scheme Layer">
</a>
<a href="https://github.com/pyrogram/tgcrypto">
<img src="https://media.pyrogram.ml/images/tgcrypto.svg"
<img src="https://img.shields.io/badge/TGCRYPTO-V1.0.4-eda738.svg?longCache=true&style=for-the-badge&colorA=262b30"
alt="TgCrypto">
</a>
</p>
.. |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

View File

@ -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()

View File

@ -31,7 +31,7 @@ __copyright__ = "Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance
"e" if sys.getfilesystemencoding() != "utf-8" else "\xe8"
)
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
__version__ = "0.7.5.dev2"
__version__ = "0.7.5.dev3"
from .api.errors import Error
from .client.types import (

View File

@ -50,9 +50,6 @@ from .ext import BaseClient, Syncer, utils
from .handlers import DisconnectHandler
from .methods import Methods
# Custom format for nice looking log lines
LOG_FORMAT = "[%(asctime)s.%(msecs)03d] %(filename)s:%(lineno)s %(levelname)s: %(message)s"
log = logging.getLogger(__name__)
@ -76,6 +73,26 @@ class Client(Methods, BaseClient):
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"
This is an alternative way to set it if you don't want to use the *config.ini* file.
device_model (``str``, *optional*):
Device model. Defaults to *platform.python_implementation() + " " + platform.python_version()*
This is an alternative way to set it if you don't want to use the *config.ini* file.
system_version (``str``, *optional*):
Operating System version. Defaults to *platform.system() + " " + platform.release()*
This is an alternative way to set it if you don't want to use the *config.ini* file.
system_lang_code (``str``, *optional*):
Code of the language used on the system, in ISO 639-1 standard. Defaults to "en".
This is an alternative way to set it if you don't want to use the *config.ini* file.
lang_code (``str``, *optional*):
Code of the language used on the client, in ISO 639-1 standard. Defaults to "en".
This is an alternative way to set it if you don't want to use the *config.ini* file.
proxy (``dict``, *optional*):
Your SOCKS5 Proxy settings as dict,
e.g.: *dict(hostname="11.22.33.44", port=1080, username="user", password="pass")*.
@ -92,8 +109,8 @@ class Client(Methods, BaseClient):
entering it manually. Only applicable for new sessions.
phone_code (``str`` | ``callable``, *optional*):
Pass the phone code as string (for test numbers only), or pass a callback function
which must return the correct phone code as string (e.g., "12345").
Pass the phone code as string (for test numbers only), or pass a callback function which accepts
a single positional argument *(phone_number)* and must return the correct phone code (e.g., "12345").
Only applicable for new sessions.
password (``str``, *optional*):
@ -128,6 +145,11 @@ class Client(Methods, BaseClient):
session_name: str,
api_id: int or str = None,
api_hash: str = None,
app_version: str = None,
device_model: str = None,
system_version: str = None,
system_lang_code: str = None,
lang_code: str = None,
proxy: dict = None,
test_mode: bool = False,
phone_number: str = None,
@ -144,6 +166,11 @@ class Client(Methods, BaseClient):
self.session_name = session_name
self.api_id = int(api_id) if api_id else None
self.api_hash = api_hash
self.app_version = app_version
self.device_model = device_model
self.system_version = system_version
self.system_lang_code = system_lang_code
self.lang_code = lang_code
# TODO: Make code consistent, use underscore for private/protected fields
self._proxy = proxy
self.test_mode = test_mode
@ -186,12 +213,9 @@ class Client(Methods, BaseClient):
await self.load_session()
self.session = Session(
self,
self.dc_id,
self.test_mode,
self._proxy,
self.auth_key,
self.api_id,
client=self
self.auth_key
)
await self.session.start()
@ -274,6 +298,7 @@ class Client(Methods, BaseClient):
Iterable containing signals the signal handler will listen to.
Defaults to (SIGINT, SIGTERM, SIGABRT).
"""
def signal_handler(*args):
log.info("Stop signal received ({}). Exiting...".format(args[0]))
self.is_idle = False
@ -368,12 +393,9 @@ class Client(Methods, BaseClient):
self.auth_key = await Auth(self.dc_id, self.test_mode, self._proxy).create()
self.session = Session(
self,
self.dc_id,
self.test_mode,
self._proxy,
self.auth_key,
self.api_id,
client=self
self.auth_key
)
await self.session.start()
@ -416,12 +438,9 @@ class Client(Methods, BaseClient):
self.auth_key = await Auth(self.dc_id, self.test_mode, self._proxy).create()
self.session = Session(
self,
self.dc_id,
self.test_mode,
self._proxy,
self.auth_key,
self.api_id,
client=self
self.auth_key
)
await self.session.start()
@ -465,7 +484,7 @@ class Client(Methods, BaseClient):
self.phone_code = (
input("Enter phone code: ") if self.phone_code is None
else self.phone_code if type(self.phone_code) is str
else str(self.phone_code())
else str(self.phone_code(self.phone_number))
)
try:
@ -807,7 +826,7 @@ class Client(Methods, BaseClient):
log.info("UpdatesWorkerTask stopped")
async def send(self, data: Object):
async def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT):
"""Use this method to send Raw Function queries.
This method makes possible to manually call every single Telegram API method in a low-level manner.
@ -818,13 +837,19 @@ class Client(Methods, BaseClient):
data (``Object``):
The API Scheme function filled with proper arguments.
retries (``int``):
Number of retries.
timeout (``float``):
Timeout in seconds.
Raises:
:class:`Error <pyrogram.Error>`
"""
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
)

View File

@ -17,14 +17,32 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
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):

View File

@ -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 <pyrogram.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
)

View File

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