mirror of
https://github.com/TeamPGM/pyrogram.git
synced 2024-11-16 20:59:29 +00:00
Merge branch 'master' into new-api
This commit is contained in:
commit
4132a1d266
48
README.rst
48
README.rst
@ -3,7 +3,7 @@
|
||||
Table of Contents
|
||||
=================
|
||||
|
||||
- `Overview`_
|
||||
- `About`_
|
||||
|
||||
- `Features`_
|
||||
|
||||
@ -26,8 +26,8 @@ Table of Contents
|
||||
- `License`_
|
||||
|
||||
|
||||
Overview
|
||||
========
|
||||
About
|
||||
=====
|
||||
|
||||
**Pyrogram** is a fully functional Telegram Client Library written from the ground up in Python.
|
||||
It offers simple and complete access to the `Telegram Messenger API`_ and is designed for Python
|
||||
@ -37,20 +37,23 @@ developers keen on building custom Telegram applications.
|
||||
Features
|
||||
--------
|
||||
|
||||
- **Easy to setup**: Pyrogram can be easily installed and upgraded using ``pip``, requires
|
||||
- **Easy to setup**: Pyrogram can be easily installed and upgraded using **pip**, requires
|
||||
a minimal set of dependencies (which are also automatically managed) and very few lines
|
||||
of code to get started with.
|
||||
|
||||
- **Easy to use**: Pyrogram provides idiomatic, developer-friendly, clean and readable
|
||||
Python code (either generated or hand-written) making the Telegram API simple to use.
|
||||
|
||||
|
||||
- **High level**: Pyrogram automatically handles all the low-level details of
|
||||
communication with the Telegram servers by implementing the
|
||||
`MTProto Mobile Protocol v2.0`_ and the mechanisms needed for establishing
|
||||
a reliable connection.
|
||||
|
||||
|
||||
- **Fast**: Pyrogram's speed is boosted up by `TgCrypto`_, a high-performance, easy-to-install
|
||||
Telegram Crypto Library written in C as a Python extension.
|
||||
|
||||
- **Updated**: Pyrogram makes use of the latest Telegram API version, currently `Layer 75`_.
|
||||
|
||||
|
||||
- **Documented**: Pyrogram API public methods are documented and resemble the well
|
||||
established Telegram Bot API, thus offering a familiar look to Bot developers.
|
||||
|
||||
@ -77,6 +80,12 @@ Installation
|
||||
.. code:: shell
|
||||
|
||||
$ pip install --upgrade pyrogram
|
||||
|
||||
- Or, with TgCrypto_:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
$ pip install --upgrade pyrogram[tgcrypto]
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
@ -93,7 +102,7 @@ Configuration
|
||||
Usage
|
||||
-----
|
||||
|
||||
- And here's how Pyrogram looks:
|
||||
- And here's how Pyrogram looks like:
|
||||
|
||||
.. code:: python
|
||||
|
||||
@ -130,6 +139,7 @@ Feedback
|
||||
|
||||
Means for getting in touch:
|
||||
|
||||
- `Community`_
|
||||
- `Telegram`_
|
||||
- `GitHub`_
|
||||
- `Email`_
|
||||
@ -156,12 +166,16 @@ License
|
||||
|
||||
.. _`Telegram`: https://t.me/haskell
|
||||
|
||||
.. _`Community`: https://t.me/PyrogramChat
|
||||
|
||||
.. _`bot-like`: https://core.telegram.org/bots/api#available-methods
|
||||
|
||||
.. _`GitHub`: https://github.com/pyrogram/pyrogram/issues
|
||||
|
||||
.. _`Email`: admin@pyrogram.ml
|
||||
|
||||
.. _TgCrypto: https://github.com/pyrogram/tgcrypto
|
||||
|
||||
.. _`GNU Lesser General Public License v3 or later (LGPLv3+)`: COPYING.lesser
|
||||
|
||||
.. |header| raw:: html
|
||||
@ -177,7 +191,7 @@ License
|
||||
<b>Telegram MTProto API Client Library for Python</b>
|
||||
|
||||
<br>
|
||||
<a href="https://pypi.python.org/pypi/Pyrogram">
|
||||
<a href="https://github.com/pyrogram/pyrogram/releases/latest">
|
||||
Download
|
||||
</a>
|
||||
•
|
||||
@ -187,15 +201,15 @@ 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">
|
||||
</a>
|
||||
<a href="https://core.telegram.org/mtproto">
|
||||
<img src="https://www.pyrogram.ml/images/mtproto.svg"
|
||||
alt="MTProto v2.0">
|
||||
<a href="https://github.com/pyrogram/tgcrypto">
|
||||
<img src="https://www.pyrogram.ml/images/tgcrypto.svg"
|
||||
alt="TgCrypto">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@ -209,6 +223,6 @@ License
|
||||
:target: compiler/api/source/main_api.tl
|
||||
:alt: Scheme Layer 75
|
||||
|
||||
.. |mtproto| image:: https://www.pyrogram.ml/images/mtproto.svg
|
||||
:target: https://core.telegram.org/mtproto
|
||||
:alt: MTProto v2.0
|
||||
.. |tgcrypto| image:: https://www.pyrogram.ml/images/tgcrypto.svg
|
||||
:target: https://github.com/pyrogram/tgcrypto
|
||||
:alt: TgCrypto
|
||||
|
@ -29,6 +29,7 @@ destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;
|
||||
---functions---
|
||||
|
||||
req_pq#60469778 nonce:int128 = ResPQ;
|
||||
req_pq_multi#be7e8ef1 nonce:int128 = ResPQ;
|
||||
|
||||
req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params;
|
||||
|
||||
|
@ -42,7 +42,10 @@ CDN_METHOD_INVALID The method can't be used on CDN DCs
|
||||
VOLUME_LOC_NOT_FOUND The volume location can't be found
|
||||
FILE_ID_INVALID The file id is invalid
|
||||
LOCATION_INVALID The file location is invalid
|
||||
CHAT_ADMIN_REQUIRED The method requires admin privileges
|
||||
CHAT_ADMIN_REQUIRED The method requires chat admin privileges
|
||||
PHONE_NUMBER_BANNED The phone number is banned
|
||||
ABOUT_TOO_LONG The about text is too long
|
||||
MULTI_MEDIA_TOO_LONG The album contains more than 10 items
|
||||
USERNAME_OCCUPIED The username is already in use
|
||||
BOT_INLINE_DISABLED The inline feature of the bot is disabled
|
||||
INLINE_RESULT_EXPIRED The inline bot query expired
|
|
@ -3,16 +3,77 @@ Welcome to Pyrogram
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<p align="right">
|
||||
<a class="github-button" href="https://github.com/pyrogram/pyrogram/subscription" data-icon="octicon-eye" data-size="large" data-show-count="true" aria-label="Watch pyrogram/pyrogram on GitHub">Watch</a>
|
||||
<a class="github-button" href="https://github.com/pyrogram/pyrogram" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star pyrogram/pyrogram on GitHub">Star</a>
|
||||
<a class="github-button" href="https://github.com/pyrogram/pyrogram/fork" data-icon="octicon-repo-forked" data-size="large" data-show-count="true" aria-label="Fork pyrogram/pyrogram on GitHub">Fork</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://pyrogram.ml">
|
||||
<img src="https://pyrogram.ml/images/logo2.png" alt="Logo">
|
||||
<div><img src="https://pyrogram.ml/images/icon.png" alt="Pyrogram Icon"></div>
|
||||
<div><img src="https://pyrogram.ml/images/label.png" alt="Pyrogram Label"></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<b>Telegram MTProto API Client Library for Python</b>
|
||||
<br>
|
||||
<a href="https://github.com/pyrogram/pyrogram/releases/latest">
|
||||
Download
|
||||
</a>
|
||||
•
|
||||
<a href="https://github.com/pyrogram/pyrogram">
|
||||
Source code
|
||||
</a>
|
||||
•
|
||||
<a href="https://t.me/PyrogramChat">
|
||||
Community
|
||||
</a>
|
||||
<br><br>
|
||||
<a href="https://github.com/pyrogram/pyrogram/blob/master/compiler/api/source/main_api.tl">
|
||||
<img src="https://www.pyrogram.ml/images/scheme.svg"
|
||||
alt="Scheme Layer 75">
|
||||
</a>
|
||||
<a href="https://core.telegram.org/mtproto">
|
||||
<img src="https://www.pyrogram.ml/images/mtproto.svg"
|
||||
alt="MTProto v2.0">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
About
|
||||
-----
|
||||
|
||||
Pyrogram is a fully functional Telegram Client Library written from the ground up in Python.
|
||||
It offers **simple** and **complete** access to the Telegram Messenger API and is designed for Python developers
|
||||
keen on building custom Telegram applications.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- **Easy to setup**: Pyrogram can be easily installed and upgraded using **pip**, requires
|
||||
a minimal set of dependencies (which are also automatically managed) and very few lines
|
||||
of code to get started with.
|
||||
|
||||
- **Easy to use**: Pyrogram provides idiomatic, developer-friendly, clean and readable
|
||||
Python code (either generated or hand-written) making the Telegram API simple to use.
|
||||
|
||||
- **High level**: Pyrogram automatically handles all the low-level details of
|
||||
communication with the Telegram servers by implementing the
|
||||
`MTProto Mobile Protocol v2.0`_ and the mechanisms needed for establishing
|
||||
a reliable connection.
|
||||
|
||||
- **Fast**: Pyrogram's speed is boosted up by `TgCrypto`_, a high-performance, easy-to-install
|
||||
crypto library written in C.
|
||||
|
||||
- **Updated**: Pyrogram makes use of the latest Telegram API version, currently `Layer 75`_.
|
||||
|
||||
- **Documented**: Pyrogram API public methods are documented and resemble the well
|
||||
established Telegram Bot API, thus offering a familiar look to Bot developers.
|
||||
|
||||
- **Full API support**: Beside the simple, bot-like methods offered by the Pyrogram API,
|
||||
the library also provides a complete, low-level access to every single Telegram API method.
|
||||
|
||||
Preview
|
||||
-------
|
||||
|
||||
@ -28,12 +89,6 @@ Preview
|
||||
|
||||
client.stop()
|
||||
|
||||
About
|
||||
-----
|
||||
|
||||
Welcome to the Pyrogram's documentation! Here you can find resources for learning how to use the library.
|
||||
Contents are organized by topic and are accessible from the sidebar.
|
||||
|
||||
To get started, press Next.
|
||||
|
||||
.. toctree::
|
||||
@ -51,6 +106,9 @@ To get started, press Next.
|
||||
resources/TextFormatting
|
||||
resources/UpdateHandling
|
||||
resources/ErrorHandling
|
||||
resources/ProxyServer
|
||||
resources/AutoAuthorization
|
||||
resources/FastCrypto
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
@ -64,3 +122,9 @@ To get started, press Next.
|
||||
|
||||
functions/index
|
||||
types/index
|
||||
|
||||
.. _`MTProto Mobile Protocol v2.0`: https://core.telegram.org/mtproto
|
||||
|
||||
.. _TgCrypto: https://docs.pyrogram.ml/resources/FastCrypto/
|
||||
|
||||
.. _`Layer 75`: https://github.com/pyrogram/pyrogram/blob/master/compiler/api/source/main_api.tl
|
37
docs/source/resources/FastCrypto.rst
Normal file
37
docs/source/resources/FastCrypto.rst
Normal file
@ -0,0 +1,37 @@
|
||||
Fast Crypto
|
||||
===========
|
||||
|
||||
Pyrogram's speed can be *dramatically* boosted up by installing TgCrypto_, a high-performance, easy-to-install crypto
|
||||
library specifically written in C for Pyrogram [#f1]_. TgCrypto is a replacement for the painfully slow PyAES and
|
||||
implements the crypto algorithms MTProto requires, namely AES-IGE and AES-CTR 256 bit.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install --upgrade tgcrypto
|
||||
|
||||
|
||||
.. note:: Being a C extension for Python, TgCrypto is an optional but *highly recommended* dependency; when TgCrypto
|
||||
is not detected on your system, Pyrogram will automatically fall back to PyAES and will show you a warning.
|
||||
|
||||
The reason about being an optional package is that TgCrypto requires some extra system tools in order to be compiled.
|
||||
Usually the errors you receive when trying to install TgCrypto are enough to understand what you should do next.
|
||||
|
||||
- **Windows**: Install `Visual C++ 2015 Build Tools <http://landinghub.visualstudio.com/visual-cpp-build-tools>`_.
|
||||
|
||||
- **macOS**: A pop-up will automatically ask you to install the command line developer tools as soon as you issue the
|
||||
installation command.
|
||||
|
||||
- **Linux**: Depending on your distro, install a proper C compiler (``gcc``, ``clang``) and the Python header files
|
||||
(``python3-dev``).
|
||||
|
||||
- **Termux (Android)**: Install ``clang`` and ``python-dev`` packages.
|
||||
|
||||
More help on the `Pyrogram group chat <https://t.me/PyrogramChat>`_.
|
||||
|
||||
.. _TgCrypto: https://github.com/pyrogram/tgcrypto
|
||||
|
||||
.. [#f1] Although TgCrypto is intended for Pyrogram, it is shipped as a standalone package and can thus be used for
|
||||
other projects too.
|
16
examples/README.md
Normal file
16
examples/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Examples
|
||||
|
||||
This folder contains example scripts to show you how **Pyrogram** looks like.
|
||||
You can start with [hello_world.py](https://github.com/pyrogram/pyrogram/blob/master/examples/hello_world.py) and continue
|
||||
with the more advanced examples. Every script is working right away, meaning you can simply copy-paste and run, the only things
|
||||
you have to change are the target chats (username, id) and file paths for sending media (photo, video, ...).
|
||||
|
||||
- [**hello_world.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/hello_world.py)
|
||||
- [**get_history.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_history.py)
|
||||
- [**get_participants.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/get_participants.py)
|
||||
- [**inline_bots.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/inline_bots.py)
|
||||
- [**updates.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/updates.py)
|
||||
- [**simple_echo.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/simple_echo.py)
|
||||
- [**advanced_echo.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/advanced_echo.py)
|
||||
- [**advanced_echo2.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/advanced_echo2.py)
|
||||
- [**welcome_bot.py**](https://github.com/pyrogram/pyrogram/blob/master/examples/welcome_bot.py)
|
64
examples/advanced_echo.py
Normal file
64
examples/advanced_echo.py
Normal file
@ -0,0 +1,64 @@
|
||||
from pyrogram import Client
|
||||
from pyrogram.api import types
|
||||
|
||||
"""This is a more advanced example bot that will reply to all private and basic groups text messages
|
||||
by also mentioning the Users.
|
||||
|
||||
Beware! This script will make you reply to ALL new messages in private chats and in every basic group you are in.
|
||||
Make sure you add an extra check to filter them:
|
||||
|
||||
# Filter Groups by ID
|
||||
if message.to_id.chat_id == MY_GROUP_ID:
|
||||
...
|
||||
"""
|
||||
|
||||
|
||||
def update_handler(client, update, users, chats):
|
||||
if isinstance(update, types.UpdateNewMessage): # Filter by UpdateNewMessage (PM and Chats)
|
||||
message = update.message
|
||||
|
||||
if isinstance(message, types.Message): # Filter by Message to exclude MessageService and MessageEmpty
|
||||
if isinstance(message.to_id, types.PeerUser): # Private Messages
|
||||
text = '[{}](tg://user?id={}) said "{}" to me ([{}](tg://user?id={}))'.format(
|
||||
users[message.from_id].first_name,
|
||||
users[message.from_id].id,
|
||||
message.message,
|
||||
users[message.to_id.user_id].first_name,
|
||||
users[message.to_id.user_id].id
|
||||
)
|
||||
|
||||
client.send_message(
|
||||
message.from_id, # Send the message to the private chat (from_id)
|
||||
text,
|
||||
reply_to_message_id=message.id
|
||||
)
|
||||
else: # Group chats
|
||||
text = '[{}](tg://user?id={}) said "{}" in **{}** group'.format(
|
||||
users[message.from_id].first_name,
|
||||
users[message.from_id].id,
|
||||
message.message,
|
||||
chats[message.to_id.chat_id].title
|
||||
)
|
||||
|
||||
client.send_message(
|
||||
message.to_id, # Send the message to the group chat (to_id)
|
||||
text,
|
||||
reply_to_message_id=message.id
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
# Pyrogram setup
|
||||
client = Client("example")
|
||||
|
||||
# Set the update_handler callback function
|
||||
client.set_update_handler(update_handler)
|
||||
client.start()
|
||||
|
||||
# Blocks the program execution until you press CTRL+C then
|
||||
# automatically stops the Client by closing the underlying connection
|
||||
client.idle()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
55
examples/advanced_echo2.py
Normal file
55
examples/advanced_echo2.py
Normal file
@ -0,0 +1,55 @@
|
||||
from pyrogram import Client
|
||||
from pyrogram.api import types
|
||||
|
||||
"""This example is similar to advanced_echo.py, except for the fact that it will reply to Supergroup text messages only.
|
||||
|
||||
Beware! This script will make you reply to ALL new messages in every single supergroup you are in.
|
||||
Make sure you add an extra check to filter them:
|
||||
|
||||
# Filter Supergroups by ID
|
||||
if message.to_id.channel_id == MY_SUPERGROUP_ID:
|
||||
...
|
||||
|
||||
# Filter Supergroups by Username
|
||||
if chats[message.to_id.channel_id].username == MY_SUPERGROUP_USERNAME:
|
||||
...
|
||||
"""
|
||||
|
||||
|
||||
def update_handler(client, update, users, chats):
|
||||
# Channels and Supergroups share the same type (Channel). The .megagroup field is used to tell them apart, and is
|
||||
# True for Supegroups, False for Channels.
|
||||
if isinstance(update, types.UpdateNewChannelMessage): # Filter by UpdateNewChannelMessage (Channels/Supergroups)
|
||||
message = update.message
|
||||
|
||||
if isinstance(message, types.Message): # Filter by Message to exclude MessageService and MessageEmpty
|
||||
if chats[message.to_id.channel_id].megagroup: # Only handle messages from Supergroups not Channels
|
||||
text = '[{}](tg://user?id={}) said "{}" in **{}** supergroup'.format(
|
||||
users[message.from_id].first_name,
|
||||
users[message.from_id].id,
|
||||
message.message,
|
||||
chats[message.to_id.channel_id].title
|
||||
)
|
||||
|
||||
client.send_message(
|
||||
message.to_id,
|
||||
text,
|
||||
reply_to_message_id=message.id
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
# Pyrogram setup
|
||||
client = Client("example")
|
||||
|
||||
# Set the update_handler callback function
|
||||
client.set_update_handler(update_handler)
|
||||
client.start()
|
||||
|
||||
# Blocks the program execution until you press CTRL+C then
|
||||
# automatically stops the Client by closing the underlying connection
|
||||
client.idle()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
BIN
examples/data/pyrogram.png
Normal file
BIN
examples/data/pyrogram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
37
examples/get_history.py
Normal file
37
examples/get_history.py
Normal file
@ -0,0 +1,37 @@
|
||||
import time
|
||||
|
||||
from pyrogram import Client
|
||||
from pyrogram.api import functions
|
||||
from pyrogram.api.errors import FloodWait
|
||||
|
||||
client = Client("example")
|
||||
client.start()
|
||||
|
||||
target = "me" # "me" refers to your own chat (Saved Messages)
|
||||
history = [] # List that will contain all the messages of the target chat
|
||||
limit = 100 # Amount of messages to retrieve for each API call
|
||||
offset = 0 # Offset starts at 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
messages = client.send(
|
||||
functions.messages.GetHistory(
|
||||
client.resolve_peer(target),
|
||||
0, 0, offset, limit, 0, 0, 0
|
||||
)
|
||||
)
|
||||
except FloodWait as e:
|
||||
# For very large chats the method call can raise a FloodWait
|
||||
time.sleep(e.x) # Sleep X seconds before continuing
|
||||
continue
|
||||
|
||||
if not messages.messages:
|
||||
break # No more messages left
|
||||
|
||||
history.extend(messages.messages)
|
||||
offset += limit
|
||||
|
||||
client.stop()
|
||||
|
||||
# Now the "history" list contains all the messages sorted by date in
|
||||
# descending order (from the most recent to the oldest one)
|
40
examples/get_participants.py
Normal file
40
examples/get_participants.py
Normal file
@ -0,0 +1,40 @@
|
||||
import time
|
||||
|
||||
from pyrogram import Client
|
||||
from pyrogram.api import functions, types
|
||||
from pyrogram.api.errors import FloodWait
|
||||
|
||||
client = Client("example")
|
||||
client.start()
|
||||
|
||||
target = "username" # Target channel/supergroup
|
||||
users = [] # List that will contain all the users of the target chat
|
||||
limit = 200 # Amount of users to retrieve for each API call
|
||||
offset = 0 # Offset starts at 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
participants = client.send(
|
||||
functions.channels.GetParticipants(
|
||||
channel=client.resolve_peer(target),
|
||||
filter=types.ChannelParticipantsSearch(""), # Filter by empty string (search for all)
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
hash=0
|
||||
)
|
||||
)
|
||||
except FloodWait as e:
|
||||
# Very large channels will trigger FloodWait.
|
||||
# When happens, wait X seconds before continuing
|
||||
time.sleep(e.x)
|
||||
continue
|
||||
|
||||
if not participants.participants:
|
||||
break # No more participants left
|
||||
|
||||
users.extend(participants.users)
|
||||
offset += limit
|
||||
|
||||
client.stop()
|
||||
|
||||
# Now the "users" list contains all the members of the target chat
|
19
examples/hello_world.py
Normal file
19
examples/hello_world.py
Normal file
@ -0,0 +1,19 @@
|
||||
from pyrogram import Client
|
||||
|
||||
# Create a new Client
|
||||
client = Client("example")
|
||||
|
||||
# Start the Client
|
||||
client.start()
|
||||
|
||||
# Send a message to yourself, Markdown is enabled by default
|
||||
client.send_message("me", "Hi there! I'm using **Pyrogram**")
|
||||
|
||||
# Send a photo with a formatted caption to yourself
|
||||
client.send_photo("me", "data/pyrogram.png", "__This is a formatted__ **caption**")
|
||||
|
||||
# Send a location to yourself
|
||||
client.send_location("me", 51.500729, -0.124583)
|
||||
|
||||
# Stop the client
|
||||
client.stop()
|
15
examples/inline_bots.py
Normal file
15
examples/inline_bots.py
Normal file
@ -0,0 +1,15 @@
|
||||
from pyrogram import Client
|
||||
|
||||
# Create a new Client
|
||||
client = Client("example")
|
||||
|
||||
# Start the Client
|
||||
client.start()
|
||||
|
||||
# Get bot results for "Fuzz Universe" from the inline bot @vid
|
||||
bot_results = client.get_inline_bot_results("vid", "Fuzz Universe")
|
||||
# Send the first result (bot_results.results[0]) to your own chat (Saved Messages)
|
||||
client.send_inline_bot_result("me", bot_results.query_id, bot_results.results[0].id)
|
||||
|
||||
# Stop the client
|
||||
client.stop()
|
34
examples/simple_echo.py
Normal file
34
examples/simple_echo.py
Normal file
@ -0,0 +1,34 @@
|
||||
from pyrogram import Client
|
||||
from pyrogram.api import types
|
||||
|
||||
"""This simple example bot will reply to all private text messages"""
|
||||
|
||||
|
||||
def update_handler(client, update, users, chats):
|
||||
if isinstance(update, types.UpdateNewMessage): # Filter by UpdateNewMessage (Private Messages)
|
||||
message = update.message # type: types.Message
|
||||
|
||||
if isinstance(message, types.Message): # Filter by Message to exclude MessageService and MessageEmpty
|
||||
if isinstance(message.to_id, types.PeerUser): # Private Messages (Message from user)
|
||||
client.send_message(
|
||||
chat_id=message.from_id,
|
||||
text=message.message,
|
||||
reply_to_message_id=message.id
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
# Pyrogram setup
|
||||
client = Client("example")
|
||||
|
||||
# Set the update_handler callback function
|
||||
client.set_update_handler(update_handler)
|
||||
client.start()
|
||||
|
||||
# Blocks the program execution until you press CTRL+C then
|
||||
# automatically stops the Client by closing the underlying connection
|
||||
client.idle()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
25
examples/updates.py
Normal file
25
examples/updates.py
Normal file
@ -0,0 +1,25 @@
|
||||
from pyrogram import Client
|
||||
|
||||
|
||||
# This function will be called every time a new Update is received from Telegram
|
||||
def update_handler(client, update, users, chats):
|
||||
# Send EVERY update that arrives to your own chat (Saved Messages)
|
||||
# Use triple backticks to make the text look nicer.
|
||||
client.send_message("me", "```\n" + str(update) + "```")
|
||||
|
||||
|
||||
def main():
|
||||
# Pyrogram setup
|
||||
client = Client("example")
|
||||
|
||||
# Set the update_handler callback function
|
||||
client.set_update_handler(update_handler)
|
||||
client.start()
|
||||
|
||||
# Blocks the program execution until you press CTRL+C then
|
||||
# automatically stops the Client by closing the underlying connection
|
||||
client.idle()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
52
examples/welcome_bot.py
Normal file
52
examples/welcome_bot.py
Normal file
@ -0,0 +1,52 @@
|
||||
from pyrogram import Client, Emoji
|
||||
from pyrogram.api import types
|
||||
|
||||
"""
|
||||
This is the Welcome Bot in @PyrogramChat
|
||||
The code is commented to help you understand each part
|
||||
|
||||
It also uses the Emoji module to easily add emojis in your text messages
|
||||
"""
|
||||
|
||||
# Your Supergroup ID
|
||||
SUPERGROUP_ID = 1387666944
|
||||
|
||||
|
||||
def update_handler(client, update, users, chats):
|
||||
# Supergroup messages are contained in the "UpdateNewChannelMessage" update type
|
||||
if isinstance(update, types.UpdateNewChannelMessage):
|
||||
message = update.message
|
||||
# When a user joins, a "MessageService" is received
|
||||
if isinstance(message, types.MessageService):
|
||||
# Check if the message is sent to your SUPERGROUP_ID
|
||||
if message.to_id.channel_id == SUPERGROUP_ID:
|
||||
# A "MessageService" contains the "action" field.
|
||||
# The action for user joins is "MessageActionChatAddUser" if the user
|
||||
# joined using the username, otherwise is "MessageActionChatJoinedByLink" if
|
||||
# the user joined a private group by link
|
||||
if isinstance(message.action, (types.MessageActionChatAddUser, types.MessageActionChatJoinedByLink)):
|
||||
# Now send the welcome message. Extra info about a user (such as the first_name, username, ...)
|
||||
# are contained in the users dictionary and can be accessed by the user ID
|
||||
client.send_message(
|
||||
SUPERGROUP_ID,
|
||||
"{} Welcome to [Pyrogram](https://docs.pyrogram.ml/)'s "
|
||||
"group chat, [{}](tg://user?id={})!".format(
|
||||
Emoji.SPARKLES, # Add an emoji
|
||||
users[message.from_id].first_name,
|
||||
users[message.from_id].id
|
||||
),
|
||||
reply_to_message_id=message.id,
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
client = Client("example")
|
||||
client.set_update_handler(update_handler)
|
||||
|
||||
client.start()
|
||||
client.idle()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -23,10 +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.5.0"
|
||||
__version__ = "0.6.2"
|
||||
|
||||
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
|
||||
|
@ -19,3 +19,4 @@
|
||||
from .chat_action import ChatAction
|
||||
from .client import Client
|
||||
from .parse_mode import ParseMode
|
||||
from .emoji import Emoji
|
||||
|
File diff suppressed because it is too large
Load Diff
7829
pyrogram/client/emoji.py
Normal file
7829
pyrogram/client/emoji.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -20,10 +20,11 @@
|
||||
class InputMedia:
|
||||
class Photo:
|
||||
"""This object represents a photo to be sent inside an album.
|
||||
It is intended to be used with :obj:`pyrogram.Client.send_media_group`.
|
||||
|
||||
Args:
|
||||
media (:obj:`str`):
|
||||
File to send.
|
||||
Photo file to send.
|
||||
Pass a file path as string to send a photo that exists on your local machine.
|
||||
|
||||
caption (:obj:`str`):
|
||||
@ -45,19 +46,32 @@ class InputMedia:
|
||||
|
||||
class Video:
|
||||
"""This object represents a video to be sent inside an album.
|
||||
It is intended to be used with :obj:`pyrogram.Client.send_media_group`.
|
||||
|
||||
Args:
|
||||
media (:obj:`str`):
|
||||
File to send.
|
||||
Video file to send.
|
||||
Pass a file path as string to send a video that exists on your local machine.
|
||||
|
||||
caption (:obj:`str`):
|
||||
caption (:obj:`str`, optional):
|
||||
Caption of the video to be sent, 0-200 characters
|
||||
|
||||
parse_mode (:obj:`str`):
|
||||
parse_mode (:obj:`str`, optional):
|
||||
Use :obj:`pyrogram.ParseMode.MARKDOWN` or :obj:`pyrogram.ParseMode.HTML` if you want Telegram apps
|
||||
to show bold, italic, fixed-width text or inline URLs in your caption.
|
||||
Defaults to Markdown.
|
||||
|
||||
width (:obj:`int`, optional):
|
||||
Video width.
|
||||
|
||||
height (:obj:`int`, optional):
|
||||
Video height
|
||||
|
||||
duration (:obj:`int`, optional):
|
||||
Video duration.
|
||||
|
||||
supports_streaming (:obj:`bool`, optional):
|
||||
Pass True, if the uploaded video is suitable for streaming.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
@ -66,10 +80,12 @@ class InputMedia:
|
||||
parse_mode: str = "",
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
duration: int = 0):
|
||||
duration: int = 0,
|
||||
supports_streaming: bool = None):
|
||||
self.media = media
|
||||
self.caption = caption
|
||||
self.parse_mode = parse_mode
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.duration = duration
|
||||
self.supports_streaming = supports_streaming
|
||||
|
@ -16,20 +16,29 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
from pyaes import AESModeOfOperationCTR
|
||||
except ImportError:
|
||||
pass
|
||||
from pyrogram.api.types import InputPhoneContact as RawInputPhoneContact
|
||||
from pyrogram.session.internals import MsgId
|
||||
|
||||
|
||||
class CTR:
|
||||
def __init__(self, key: bytes, iv: bytes):
|
||||
self.ctr = AESModeOfOperationCTR(key)
|
||||
self.iv = iv
|
||||
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`
|
||||
|
||||
def decrypt(self, data: bytes, offset: int) -> bytes:
|
||||
replace = int.to_bytes(offset // 16, byteorder="big", length=4)
|
||||
iv = self.iv[:-4] + replace
|
||||
self.ctr._counter._counter = list(iv)
|
||||
Args:
|
||||
phone (:obj:`str`):
|
||||
Contact's phone number
|
||||
|
||||
return self.ctr.decrypt(data)
|
||||
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=MsgId(),
|
||||
phone="+" + phone.strip("+"),
|
||||
first_name=first_name,
|
||||
last_name=last_name
|
||||
)
|
@ -31,7 +31,7 @@ from . import utils
|
||||
|
||||
|
||||
class HTML:
|
||||
HTML_RE = re.compile(r"<(\w+)(?: href=([\"'])(.*)\2)?>(.*)</\1>")
|
||||
HTML_RE = re.compile(r"<(\w+)(?: href=([\"'])([^<]+)\2)?>([^>]+)</\1>")
|
||||
MENTION_RE = re.compile(r"tg://user\?id=(\d+)")
|
||||
|
||||
def __init__(self, peers_by_id):
|
||||
@ -44,7 +44,7 @@ class HTML:
|
||||
|
||||
for match in self.HTML_RE.finditer(text):
|
||||
start = match.start() - offset
|
||||
style, url, body = match.groups()
|
||||
style, url, body = match.group(1, 3, 4)
|
||||
|
||||
if url:
|
||||
mention = self.MENTION_RE.match(url)
|
||||
|
@ -24,95 +24,84 @@ from pyrogram.api.types import (
|
||||
MessageEntityCode as Code,
|
||||
MessageEntityTextUrl as Url,
|
||||
MessageEntityPre as Pre,
|
||||
MessageEntityMentionName as MentionInvalid,
|
||||
InputMessageEntityMentionName as Mention
|
||||
)
|
||||
from . import utils
|
||||
|
||||
|
||||
class Markdown:
|
||||
INLINE_DELIMITERS = {
|
||||
"**": Bold,
|
||||
"__": Italic,
|
||||
"`": Code
|
||||
}
|
||||
BOLD_DELIMITER = "**"
|
||||
ITALIC_DELIMITER = "__"
|
||||
CODE_DELIMITER = "`"
|
||||
PRE_DELIMITER = "```"
|
||||
|
||||
# ``` python
|
||||
# for i in range(10):
|
||||
# print(i)
|
||||
# ```
|
||||
PRE_RE = r"(?P<pre>```(?P<lang>.*)\n(?P<code>(.|\n)*)\n```)"
|
||||
|
||||
# [url](github.com)
|
||||
URL_RE = r"(?P<url>(\[(?P<url_text>.+?)\]\((?P<url_path>.+?)\)))"
|
||||
|
||||
# [name](tg://user?id=123456789)
|
||||
MENTION_RE = r"(?P<mention>(\[(?P<mention_text>.+?)\]\(tg:\/\/user\?id=(?P<user_id>\d+?)\)))"
|
||||
|
||||
# **bold**
|
||||
# __italic__
|
||||
# `code`
|
||||
INLINE_RE = r"(?P<inline>(?P<start_delimiter>{d})(?P<body>.+?)(?P<end_delimiter>{d}))".format(
|
||||
MARKDOWN_RE = re.compile(r"```([\w ]*)\n([\w\W]*)(?:\n|)```|\[([^[(]+)\]\(([^])]+)\)|({d})(.+?)\5".format(
|
||||
d="|".join(
|
||||
["".join(i) for i in [
|
||||
["\{}".format(j) for j in i]
|
||||
for i in sorted( # Sort delimiters by length
|
||||
INLINE_DELIMITERS.keys(),
|
||||
key=lambda k: len(k), # Or: key=len
|
||||
reverse=True
|
||||
)
|
||||
for i in [
|
||||
PRE_DELIMITER,
|
||||
CODE_DELIMITER,
|
||||
ITALIC_DELIMITER,
|
||||
BOLD_DELIMITER
|
||||
]
|
||||
]]
|
||||
)
|
||||
)
|
||||
))
|
||||
MENTION_RE = re.compile(r"tg://user\?id=(\d+)")
|
||||
|
||||
MARKDOWN_RE = re.compile("|".join([PRE_RE, MENTION_RE, URL_RE, INLINE_RE]))
|
||||
|
||||
def __init__(self, peers_by_id):
|
||||
def __init__(self, peers_by_id: dict):
|
||||
self.peers_by_id = peers_by_id
|
||||
|
||||
def parse(self, text):
|
||||
def parse(self, message: str):
|
||||
entities = []
|
||||
text = utils.add_surrogates(text)
|
||||
message = utils.add_surrogates(message).strip()
|
||||
offset = 0
|
||||
|
||||
for match in self.MARKDOWN_RE.finditer(text):
|
||||
for match in self.MARKDOWN_RE.finditer(message):
|
||||
start = match.start() - offset
|
||||
lang, pre, text, url, style, body = match.groups()
|
||||
|
||||
if match.group("pre"):
|
||||
pattern = match.group("pre")
|
||||
lang = match.group("lang")
|
||||
replace = match.group("code")
|
||||
entity = Pre(start, len(replace), lang.strip())
|
||||
offset += len(lang) + 8
|
||||
elif match.group("url"):
|
||||
pattern = match.group("url")
|
||||
replace = match.group("url_text")
|
||||
path = match.group("url_path")
|
||||
entity = Url(start, len(replace), path)
|
||||
offset += len(path) + 4
|
||||
elif match.group("mention"):
|
||||
pattern = match.group("mention")
|
||||
replace = match.group("mention_text")
|
||||
user_id = match.group("user_id")
|
||||
entity = Mention(start, len(replace), self.peers_by_id[int(user_id)])
|
||||
offset += len(user_id) + 17
|
||||
elif match.group("inline"):
|
||||
pattern = match.group("inline")
|
||||
replace = match.group("body")
|
||||
start_delimiter = match.group("start_delimiter")
|
||||
end_delimiter = match.group("end_delimiter")
|
||||
if pre:
|
||||
body = pre = pre.strip()
|
||||
entity = Pre(start, len(pre), lang.strip() or "")
|
||||
offset += len(lang) + len(self.PRE_DELIMITER) * 2
|
||||
elif url:
|
||||
mention = self.MENTION_RE.match(url)
|
||||
|
||||
if start_delimiter != end_delimiter:
|
||||
if mention:
|
||||
user_id = int(mention.group(1))
|
||||
input_user = self.peers_by_id.get(user_id, None)
|
||||
|
||||
entity = (
|
||||
Mention(start, len(text), input_user)
|
||||
if input_user
|
||||
else MentionInvalid(start, len(text), user_id)
|
||||
)
|
||||
else:
|
||||
entity = Url(start, len(text), url)
|
||||
|
||||
body = text
|
||||
offset += len(url) + 4
|
||||
else:
|
||||
if style == self.BOLD_DELIMITER:
|
||||
entity = Bold(start, len(body))
|
||||
elif style == self.ITALIC_DELIMITER:
|
||||
entity = Italic(start, len(body))
|
||||
elif style == self.CODE_DELIMITER:
|
||||
entity = Code(start, len(body))
|
||||
elif style == self.PRE_DELIMITER:
|
||||
entity = Pre(start, len(body), "")
|
||||
else:
|
||||
continue
|
||||
|
||||
entity = self.INLINE_DELIMITERS[start_delimiter](start, len(replace))
|
||||
offset += len(start_delimiter) * 2
|
||||
else:
|
||||
continue
|
||||
offset += len(style) * 2
|
||||
|
||||
entities.append(entity)
|
||||
text = text.replace(pattern, replace)
|
||||
message = message.replace(match.group(), body)
|
||||
|
||||
return dict(
|
||||
message=utils.remove_surrogates(text),
|
||||
message=utils.remove_surrogates(message),
|
||||
entities=entities
|
||||
)
|
||||
|
@ -30,6 +30,7 @@ Proxy = namedtuple("Proxy", ["enabled", "hostname", "port", "username", "passwor
|
||||
class TCP(socks.socksocket):
|
||||
def __init__(self, proxy: Proxy):
|
||||
super().__init__()
|
||||
self.settimeout(10)
|
||||
self.proxy_enabled = False
|
||||
|
||||
if proxy and proxy.enabled:
|
||||
@ -43,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)
|
||||
|
@ -16,8 +16,7 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .ctr import CTR
|
||||
from .ige import IGE
|
||||
from .aes import AES
|
||||
from .kdf import KDF
|
||||
from .prime import Prime
|
||||
from .rsa import RSA
|
||||
|
@ -16,21 +16,52 @@
|
||||
# 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 pyaes import AES
|
||||
import logging
|
||||
|
||||
BLOCK_SIZE = 16
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import tgcrypto
|
||||
except ImportError:
|
||||
log.warning(
|
||||
"TgCrypto is missing! "
|
||||
"Pyrogram will work the same, but at a much slower speed. "
|
||||
"More info: https://docs.pyrogram.ml/resources/TgCrypto"
|
||||
)
|
||||
is_fast = False
|
||||
import pyaes
|
||||
else:
|
||||
log.info("Using TgCrypto")
|
||||
is_fast = True
|
||||
|
||||
|
||||
# TODO: Performance optimization
|
||||
|
||||
class IGE:
|
||||
# TODO: Ugly IFs
|
||||
class AES:
|
||||
@classmethod
|
||||
def encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
|
||||
return cls.ige(data, key, iv, True)
|
||||
def ige_encrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
|
||||
if is_fast:
|
||||
return tgcrypto.ige_encrypt(data, key, iv)
|
||||
else:
|
||||
return cls.ige(data, key, iv, True)
|
||||
|
||||
@classmethod
|
||||
def decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
|
||||
return cls.ige(data, key, iv, False)
|
||||
def ige_decrypt(cls, data: bytes, key: bytes, iv: bytes) -> bytes:
|
||||
if is_fast:
|
||||
return tgcrypto.ige_decrypt(data, key, iv)
|
||||
else:
|
||||
return cls.ige(data, key, iv, False)
|
||||
|
||||
@staticmethod
|
||||
def ctr_decrypt(data: bytes, key: bytes, iv: bytes, offset: int) -> bytes:
|
||||
replace = int.to_bytes(offset // 16, byteorder="big", length=4)
|
||||
iv = iv[:-4] + replace
|
||||
|
||||
if is_fast:
|
||||
return tgcrypto.ctr_decrypt(data, key, iv)
|
||||
else:
|
||||
ctr = pyaes.AESModeOfOperationCTR(key)
|
||||
ctr._counter._counter = list(iv)
|
||||
return ctr.decrypt(data)
|
||||
|
||||
@staticmethod
|
||||
def xor(a: bytes, b: bytes) -> bytes:
|
||||
@ -42,12 +73,12 @@ class IGE:
|
||||
|
||||
@classmethod
|
||||
def ige(cls, data: bytes, key: bytes, iv: bytes, encrypt: bool) -> bytes:
|
||||
cipher = AES(key)
|
||||
cipher = pyaes.AES(key)
|
||||
|
||||
iv_1 = iv[:BLOCK_SIZE]
|
||||
iv_2 = iv[BLOCK_SIZE:]
|
||||
iv_1 = iv[:16]
|
||||
iv_2 = iv[16:]
|
||||
|
||||
data = [data[i: i + BLOCK_SIZE] for i in range(0, len(data), BLOCK_SIZE)]
|
||||
data = [data[i: i + 16] for i in range(0, len(data), 16)]
|
||||
|
||||
if encrypt:
|
||||
for i, chunk in enumerate(data):
|
@ -23,10 +23,16 @@ PublicKey = namedtuple("PublicKey", ["m", "e"])
|
||||
|
||||
class RSA:
|
||||
# To get modulus and exponent:
|
||||
#
|
||||
# [RSA PUBLIC KEY]:
|
||||
# grep -v -- - public.key | tr -d \\n | base64 -d | openssl asn1parse -inform DER -i
|
||||
#
|
||||
# [PUBLIC KEY]:
|
||||
# openssl rsa -pubin -in key -text -noout
|
||||
|
||||
server_public_keys = {
|
||||
0xc3b42b026ce86b21 - (1 << 64): PublicKey( # Telegram servers
|
||||
# -4344800451088585951
|
||||
0xc3b42b026ce86b21 - (1 << 64): PublicKey( # Telegram servers #1
|
||||
# -----BEGIN RSA PUBLIC KEY-----
|
||||
# MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
|
||||
# lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
|
||||
@ -48,6 +54,108 @@ class RSA:
|
||||
), # Modulus
|
||||
int("010001", 16) # Exponent
|
||||
),
|
||||
|
||||
# 847625836280919973
|
||||
0x10bc35f3509f7b7a5 - (1 << 64): PublicKey( # Telegram servers #2
|
||||
# -----BEGIN PUBLIC KEY-----
|
||||
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAruw2yP/BCcsJliRoW5eB
|
||||
# VBVle9dtjJw+OYED160Wybum9SXtBBLXriwt4rROd9csv0t0OHCaTmRqBcQ0J8fx
|
||||
# hN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvd
|
||||
# l84Kd9ORYjDEAyFnEA7dD556OptgLQQ2e2iVNq8NZLYTzLp5YpOdO1doK+ttrltg
|
||||
# gTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnSLj16yE5HvJQn0CNpRdENvRUXe6tBP78O
|
||||
# 39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wFXGF710w9lwCGNbmNxNYhtIkdqfsEcwR5
|
||||
# JwIDAQAB
|
||||
# -----END PUBLIC KEY-----
|
||||
int(
|
||||
"AEEC36C8FFC109CB099624685B97815415657BD76D8C9C3E398103D7AD16C9BB"
|
||||
"A6F525ED0412D7AE2C2DE2B44E77D72CBF4B7438709A4E646A05C43427C7F184"
|
||||
"DEBF72947519680E651500890C6832796DD11F772C25FF8F576755AFE055B0A3"
|
||||
"752C696EB7D8DA0D8BE1FAF38C9BDD97CE0A77D3916230C4032167100EDD0F9E"
|
||||
"7A3A9B602D04367B689536AF0D64B613CCBA7962939D3B57682BEB6DAE5B6081"
|
||||
"30B2E52ACA78BA023CF6CE806B1DC49C72CF928A7199D22E3D7AC84E47BC9427"
|
||||
"D0236945D10DBD15177BAB413FBF0EDFDA09F014C7A7DA088DDE9759702CA760"
|
||||
"AF2B8E4E97CC055C617BD74C3D97008635B98DC4D621B4891DA9FB0473047927",
|
||||
16
|
||||
), # Modulus
|
||||
int("010001", 16) # Exponent
|
||||
),
|
||||
|
||||
# 1562291298945373506
|
||||
0x115ae5fa8b5529542 - (1 << 64): PublicKey( # Telegram servers #3
|
||||
# -----BEGIN PUBLIC KEY-----
|
||||
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvfLHfYH2r9R70w8prHbl
|
||||
# Wt/nDkh+XkgpflqQVcnAfSuTtO05lNPspQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOO
|
||||
# KPi0OfJXoRVylFzAQG/j83u5K3kRLbae7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ
|
||||
# 3TDS2pQOCtovG4eDl9wacrXOJTG2990VjgnIKNA0UMoP+KF03qzryqIt3oTvZq03
|
||||
# DyWdGK+AZjgBLaDKSnC6qD2cFY81UryRWOab8zKkWAnhw2kFpcqhI0jdV5QaSCEx
|
||||
# vnsjVaX0Y1N0870931/5Jb9ICe4nweZ9kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV
|
||||
# /wIDAQAB
|
||||
# -----END PUBLIC KEY-----
|
||||
int(
|
||||
"BDF2C77D81F6AFD47BD30F29AC76E55ADFE70E487E5E48297E5A9055C9C07D2B"
|
||||
"93B4ED3994D3ECA5098BF18D978D54F8B7C713EB10247607E69AF9EF44F38E28"
|
||||
"F8B439F257A11572945CC0406FE3F37BB92B79112DB69EEDF2DC71584A661638"
|
||||
"EA5BECB9E23585074B80D57D9F5710DD30D2DA940E0ADA2F1B878397DC1A72B5"
|
||||
"CE2531B6F7DD158E09C828D03450CA0FF8A174DEACEBCAA22DDE84EF66AD370F"
|
||||
"259D18AF806638012DA0CA4A70BAA83D9C158F3552BC9158E69BF332A45809E1"
|
||||
"C36905A5CAA12348DD57941A482131BE7B2355A5F4635374F3BD3DDF5FF925BF"
|
||||
"4809EE27C1E67D9120C5FE08A9DE458B1B4A3C5D0A428437F2BECA81F4E2D5FF",
|
||||
16
|
||||
), # Modulus
|
||||
int("010001", 16) # Exponent
|
||||
),
|
||||
|
||||
# -5859577972006586033
|
||||
0xaeae98e13cd7f94f - (1 << 64): PublicKey( # Telegram servers #4
|
||||
# -----BEGIN PUBLIC KEY-----
|
||||
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ditzm+mPND6xkhzwFI
|
||||
# z6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGrzqTDHkO30R8VeRM/Kz2f4nR05GIFiITl
|
||||
# 4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+th6knSU0yLtNKuQVP6voMrnt9MV1X92L
|
||||
# GZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvSUwwc+yi1/gGaybwlzZwqXYoPOhwMebzK
|
||||
# Uk0xW14htcJrRrq+PXXQbRzTMynseCoPIoke0dtCodbA3qQxQovE16q9zz4Otv2k
|
||||
# 4j63cz53J+mhkVWAeWxVGI0lltJmWtEYK6er8VqqWot3nqmWMXogrgRLggv/Nbbo
|
||||
# oQIDAQAB
|
||||
# -----END PUBLIC KEY-----
|
||||
int(
|
||||
"B3F762B739BE98F343EB1921CF0148CFA27FF7AF02B6471213FED9DAA0098976"
|
||||
"E667750324F1ABCEA4C31E43B7D11F1579133F2B3D9FE27474E462058884E5E1"
|
||||
"B123BE9CBBC6A443B2925C08520E7325E6F1A6D50E117EB61EA49D2534C8BB4D"
|
||||
"2AE4153FABE832B9EDF4C5755FDD8B19940B81D1D96CF433D19E6A22968A85DC"
|
||||
"80F0312F596BD2530C1CFB28B5FE019AC9BC25CD9C2A5D8A0F3A1C0C79BCCA52"
|
||||
"4D315B5E21B5C26B46BABE3D75D06D1CD33329EC782A0F22891ED1DB42A1D6C0"
|
||||
"DEA431428BC4D7AABDCF3E0EB6FDA4E23EB7733E7727E9A1915580796C55188D"
|
||||
"2596D2665AD1182BA7ABF15AAA5A8B779EA996317A20AE044B820BFF35B6E8A1",
|
||||
16
|
||||
), # Modulus
|
||||
int("010001", 16) # Exponent
|
||||
),
|
||||
|
||||
# 6491968696586960280
|
||||
0x15a181b2235057d98 - (1 << 64): PublicKey( # Telegram servers #5
|
||||
# -----BEGIN PUBLIC KEY-----
|
||||
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q0
|
||||
# 5shjg8/4p6047bn6/m8yPy1RBsvIyvuDuGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xb
|
||||
# nfxL5BXHplJhMtADXKM9bWB11PU1Eioc3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA
|
||||
# 9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvifRLJbY08/Gp66KpQvy7g8w7VB8wlgePe
|
||||
# xW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqePji9NP3tJUFQjcECqcm0yV7/2d0t/pbC
|
||||
# m+ZH1sadZspQCEPPrtbkQBlvHb4OLiIWPGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6M
|
||||
# AQIDAQAB
|
||||
# -----END PUBLIC KEY-----
|
||||
int(
|
||||
"BE6A71558EE577FF03023CFA17AAB4E6C86383CFF8A7AD38EDB9FAFE6F323F2D"
|
||||
"5106CBC8CAFB83B869CFFD1CCF121CD743D509E589E68765C96601E813DC5B9D"
|
||||
"FC4BE415C7A6526132D0035CA33D6D6075D4F535122A1CDFE017041F1088D141"
|
||||
"9F65C8E5490EE613E16DBF662698C0F54870F0475FA893FC41EB55B08FF1AC21"
|
||||
"1BC045DED31BE27D12C96D8D3CFC6A7AE8AA50BF2EE0F30ED507CC2581E3DEC5"
|
||||
"6DE94F5DC0A7ABEE0BE990B893F2887BD2C6310A1E0A9E3E38BD34FDED254150"
|
||||
"8DC102A9C9B4C95EFFD9DD2DFE96C29BE647D6C69D66CA500843CFAED6E44019"
|
||||
"6F1DBE0E2E22163C61CA48C79116FA77216726749A976A1C4B0944B5121E8C01",
|
||||
16
|
||||
), # Modulus
|
||||
int("010001", 16) # Exponent
|
||||
),
|
||||
|
||||
# 6427105915145367799
|
||||
0x15931aac70e0d30f7 - (1 << 64): PublicKey( # CDN DC-121
|
||||
# -----BEGIN RSA PUBLIC KEY-----
|
||||
# MIIBCgKCAQEA+Lf3PvgE1yxbJUCMaEAkV0QySTVpnaDjiednB5RbtNWjCeqSVakY
|
||||
@ -70,6 +178,8 @@ class RSA:
|
||||
), # Modulus
|
||||
int("010001", 16) # Exponent
|
||||
),
|
||||
|
||||
# 2685959930972952888
|
||||
0x1254672538e935938 - (1 << 64): PublicKey( # CDN DC-140
|
||||
# -----BEGIN RSA PUBLIC KEY-----
|
||||
# MIIBCgKCAQEAzuHVC7sE50Kho/yDVZtWnlmA5Bf/aM8KZY3WzS16w6w1sBqipj8o
|
||||
|
@ -25,7 +25,7 @@ from os import urandom
|
||||
from pyrogram.api import functions, types
|
||||
from pyrogram.api.core import Object, Long, Int
|
||||
from pyrogram.connection import Connection
|
||||
from pyrogram.crypto import IGE, RSA, Prime
|
||||
from pyrogram.crypto import AES, RSA, Prime
|
||||
from .internals import MsgId, DataCenter
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -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()
|
||||
)
|
||||
@ -91,8 +91,19 @@ class Auth:
|
||||
# Step 1; Step 2
|
||||
nonce = int.from_bytes(urandom(16), "little", signed=True)
|
||||
log.debug("Send req_pq: {}".format(nonce))
|
||||
res_pq = self.send(functions.ReqPq(nonce))
|
||||
res_pq = self.send(functions.ReqPqMulti(nonce))
|
||||
log.debug("Got ResPq: {}".format(res_pq.server_nonce))
|
||||
log.debug("Server public key fingerprints: {}".format(res_pq.server_public_key_fingerprints))
|
||||
|
||||
for i in res_pq.server_public_key_fingerprints:
|
||||
if i in RSA.server_public_keys:
|
||||
log.debug("Using fingerprint: {}".format(i))
|
||||
public_key_fingerprint = i
|
||||
break
|
||||
else:
|
||||
log.debug("Fingerprint unknown: {}".format(i))
|
||||
else:
|
||||
raise Exception("Public key not found")
|
||||
|
||||
# Step 3
|
||||
pq = int.from_bytes(res_pq.pq, "big")
|
||||
@ -118,7 +129,7 @@ class Auth:
|
||||
sha = sha1(data).digest()
|
||||
padding = urandom(- (len(data) + len(sha)) % 255)
|
||||
data_with_hash = sha + data + padding
|
||||
encrypted_data = RSA.encrypt(data_with_hash, res_pq.server_public_key_fingerprints[0])
|
||||
encrypted_data = RSA.encrypt(data_with_hash, public_key_fingerprint)
|
||||
|
||||
log.debug("Done encrypt data with RSA")
|
||||
|
||||
@ -130,7 +141,7 @@ class Auth:
|
||||
server_nonce,
|
||||
int.to_bytes(p, 4, "big"),
|
||||
int.to_bytes(q, 4, "big"),
|
||||
res_pq.server_public_key_fingerprints[0],
|
||||
public_key_fingerprint,
|
||||
encrypted_data
|
||||
)
|
||||
)
|
||||
@ -152,7 +163,7 @@ class Auth:
|
||||
|
||||
server_nonce = int.from_bytes(server_nonce, "little", signed=True)
|
||||
|
||||
answer_with_hash = IGE.decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv)
|
||||
answer_with_hash = AES.ige_decrypt(encrypted_answer, tmp_aes_key, tmp_aes_iv)
|
||||
answer = answer_with_hash[20:]
|
||||
|
||||
server_dh_inner_data = Object.read(BytesIO(answer))
|
||||
@ -181,7 +192,7 @@ class Auth:
|
||||
sha = sha1(data).digest()
|
||||
padding = urandom(- (len(data) + len(sha)) % 16)
|
||||
data_with_hash = sha + data + padding
|
||||
encrypted_data = IGE.encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv)
|
||||
encrypted_data = AES.ige_encrypt(data_with_hash, tmp_aes_key, tmp_aes_iv)
|
||||
|
||||
log.debug("Send set_client_DH_params")
|
||||
set_client_dh_params_answer = self.send(
|
||||
@ -236,7 +247,7 @@ class Auth:
|
||||
log.debug("Nonce fields check: OK")
|
||||
|
||||
# Step 9
|
||||
server_salt = IGE.xor(new_nonce[:8], server_nonce[:8])
|
||||
server_salt = AES.xor(new_nonce[:8], server_nonce[:8])
|
||||
|
||||
log.debug("Server salt: {}".format(int.from_bytes(server_salt, "little")))
|
||||
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -16,19 +16,20 @@
|
||||
# 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 threading import Lock
|
||||
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
|
||||
last_time = 0
|
||||
offset = 0
|
||||
lock = Lock()
|
||||
|
||||
def __call__(self) -> int:
|
||||
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
|
||||
def __new__(cls) -> int:
|
||||
with cls.lock:
|
||||
now = time()
|
||||
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
|
||||
return msg_id
|
||||
|
@ -16,15 +16,19 @@
|
||||
# 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 threading import Lock
|
||||
|
||||
|
||||
class SeqNo:
|
||||
def __init__(self):
|
||||
self.content_related_messages_sent = 0
|
||||
self.lock = Lock()
|
||||
|
||||
def __call__(self, is_content_related: bool) -> int:
|
||||
seq_no = (self.content_related_messages_sent * 2) + (1 if is_content_related else 0)
|
||||
with self.lock:
|
||||
seq_no = (self.content_related_messages_sent * 2) + (1 if is_content_related else 0)
|
||||
|
||||
if is_content_related:
|
||||
self.content_related_messages_sent += 1
|
||||
if is_content_related:
|
||||
self.content_related_messages_sent += 1
|
||||
|
||||
return seq_no
|
||||
return seq_no
|
||||
|
@ -33,7 +33,7 @@ from pyrogram.api.all import layer
|
||||
from pyrogram.api.core import Message, Object, MsgContainer, Long, FutureSalt, Int
|
||||
from pyrogram.api.errors import Error
|
||||
from pyrogram.connection import Connection
|
||||
from pyrogram.crypto import IGE, KDF
|
||||
from pyrogram.crypto import AES, KDF
|
||||
from .internals import MsgId, MsgFactory, DataCenter
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -60,7 +60,7 @@ class Session:
|
||||
)
|
||||
|
||||
INITIAL_SALT = 0x616e67656c696361
|
||||
NET_WORKERS = 2
|
||||
NET_WORKERS = 1
|
||||
WAIT_TIMEOUT = 10
|
||||
MAX_RETRIES = 5
|
||||
ACKS_THRESHOLD = 8
|
||||
@ -68,6 +68,20 @@ class Session:
|
||||
|
||||
notice_displayed = False
|
||||
|
||||
BAD_MSG_DESCRIPTION = {
|
||||
16: "[16] msg_id too low, the client time has to be synchronized",
|
||||
17: "[17] msg_id too high, the client time has to be synchronized",
|
||||
18: "[18] incorrect two lower order msg_id bits, the server expects client message msg_id to be divisible by 4",
|
||||
19: "[19] container msg_id is the same as msg_id of a previously received message",
|
||||
20: "[20] message too old, it cannot be verified by the server",
|
||||
32: "[32] msg_seqno too low",
|
||||
33: "[33] msg_seqno too high",
|
||||
34: "[34] an even msg_seqno expected, but odd received",
|
||||
35: "[35] odd msg_seqno expected, but even received",
|
||||
48: "[48] incorrect server salt",
|
||||
64: "[64] invalid container"
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
dc_id: int,
|
||||
test_mode: bool,
|
||||
@ -89,9 +103,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 +159,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
|
||||
@ -192,14 +205,14 @@ class Session:
|
||||
msg_key = msg_key_large[8:24]
|
||||
aes_key, aes_iv = KDF(self.auth_key, msg_key, True)
|
||||
|
||||
return self.auth_key_id + msg_key + IGE.encrypt(data + padding, aes_key, aes_iv)
|
||||
return self.auth_key_id + msg_key + AES.ige_encrypt(data + padding, aes_key, aes_iv)
|
||||
|
||||
def unpack(self, b: BytesIO) -> Message:
|
||||
assert b.read(8) == self.auth_key_id, b.getvalue()
|
||||
|
||||
msg_key = b.read(16)
|
||||
aes_key, aes_iv = KDF(self.auth_key, msg_key, False)
|
||||
data = BytesIO(IGE.decrypt(b.read(), aes_key, aes_iv))
|
||||
data = BytesIO(AES.ige_decrypt(b.read(), aes_key, aes_iv))
|
||||
data.read(8)
|
||||
|
||||
# https://core.telegram.org/mtproto/security_guidelines#checking-session-id
|
||||
@ -270,7 +283,7 @@ class Session:
|
||||
msg_id = msg.body.msg_id
|
||||
else:
|
||||
if self.client is not None:
|
||||
self.client.update_queue.put(msg.body)
|
||||
self.client.updates_queue.put(msg.body)
|
||||
|
||||
if msg_id in self.results:
|
||||
self.results[msg_id].value = getattr(msg.body, "result", msg.body)
|
||||
@ -296,7 +309,7 @@ class Session:
|
||||
break
|
||||
|
||||
try:
|
||||
self._send(functions.Ping(0), False)
|
||||
self._send(functions.PingDelayDisconnect(0, self.PING_INTERVAL + 15), False)
|
||||
except (OSError, TimeoutError):
|
||||
pass
|
||||
|
||||
@ -338,7 +351,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
|
||||
@ -370,6 +386,11 @@ class Session:
|
||||
raise TimeoutError
|
||||
elif isinstance(result, types.RpcError):
|
||||
Error.raise_it(result, type(data))
|
||||
elif isinstance(result, types.BadMsgNotification):
|
||||
raise Exception(self.BAD_MSG_DESCRIPTION.get(
|
||||
result.error_code,
|
||||
"Error code {}".format(result.error_code)
|
||||
))
|
||||
else:
|
||||
return result
|
||||
|
||||
|
12
setup.py
12
setup.py
@ -37,7 +37,7 @@ with open("pyrogram/__init__.py", encoding="utf-8") as f:
|
||||
# PyPI doesn't like raw html
|
||||
with open("README.rst", encoding="utf-8") as f:
|
||||
readme = re.sub(r"\.\. \|.+\| raw:: html(?:\s{4}.+)+\n\n", "", f.read())
|
||||
readme = re.sub(r"\|header\|", "|logo|\n\n|description|\n\n|scheme| |mtproto|", readme)
|
||||
readme = re.sub(r"\|header\|", "|logo|\n\n|description|\n\n|scheme| |tgcrypto|", readme)
|
||||
|
||||
setup(
|
||||
name="Pyrogram",
|
||||
@ -67,6 +67,14 @@ setup(
|
||||
],
|
||||
packages=find_packages(),
|
||||
zip_safe=False,
|
||||
install_requires=["pyaes", "pysocks"],
|
||||
install_requires=[
|
||||
"pyaes",
|
||||
"pysocks"
|
||||
],
|
||||
extras_require={
|
||||
"tgcrypto": [
|
||||
"tgcrypto"
|
||||
]
|
||||
},
|
||||
include_package_data=True,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user