2
0
mirror of https://github.com/pyrogram/pyrogram synced 2025-08-29 13:27:47 +00:00

Merge branch 'develop'

This commit is contained in:
Dan 2019-02-04 16:41:04 +01:00
commit 89ffa6b196
85 changed files with 2687 additions and 1087 deletions

View File

@ -17,18 +17,22 @@ Pyrogram
app.run()
**Pyrogram** is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for
building custom Telegram applications that interact with the MTProto API as both User and Bot.
**Pyrogram** is an elegant, easy-to-use Telegram_ client library and framework written from the ground up in Python and C.
It enables you to easily create custom apps using both user and bot identities (bot API alternative) via the `MTProto API`_.
`A fully-asynchronous variant is also available » <https://github.com/pyrogram/pyrogram/issues/181>`_
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.
- **Easy**: You can install Pyrogram with pip and start building your applications right away.
- **Elegant**: Low-level details are abstracted and re-presented in a much nicer and easier way.
- **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 91 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.
- **Documented**: Pyrogram API methods, types and public interfaces are well documented.
- **Type-hinted**: Exposed Pyrogram types and method parameters are all type-hinted.
- **Updated**, to the latest Telegram API version, currently Layer 91 on top of `MTProto 2.0`_.
- **Pluggable**: The Smart Plugin system allows to write components with minimal boilerplate code.
- **Comprehensive**: Execute any advanced action an official client is able to do, and even more.
Requirements
------------
@ -43,11 +47,11 @@ Installing
pip3 install pyrogram
Getting Started
---------------
Resources
---------
- The Docs contain lots of resources to help you getting started with Pyrogram: https://docs.pyrogram.ml.
- Reading Examples_ in this repository is also a good way for learning how things work.
- Reading `Examples in this repository`_ is also a good way for learning how Pyrogram works.
- Seeking extra help? Don't be shy, come join and ask our Community_!
- For other requests you can send an Email_ or a Message_.
@ -61,17 +65,19 @@ and documentation. Any help is appreciated!
Copyright & License
-------------------
- Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>
- Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
- Licensed under the terms of the `GNU Lesser General Public License v3 or later (LGPLv3+)`_
.. _`Telegram`: https://telegram.org/
.. _`MTProto API`: https://core.telegram.org/api#telegram-api
.. _`Telegram API key`: https://docs.pyrogram.ml/start/ProjectSetup#api-keys
.. _`Community`: https://t.me/PyrogramChat
.. _`Examples`: https://github.com/pyrogram/pyrogram/tree/master/examples
.. _`Examples in this repository`: https://github.com/pyrogram/pyrogram/tree/master/examples
.. _`GitHub`: https://github.com/pyrogram/pyrogram/issues
.. _`Email`: admin@pyrogram.ml
.. _`Message`: https://t.me/haskell
.. _TgCrypto: https://github.com/pyrogram/tgcrypto
.. _`MTProto 2.0`: https://core.telegram.org/mtproto
.. _`GNU Lesser General Public License v3 or later (LGPLv3+)`: COPYING.lesser
.. |header| raw:: html
@ -83,17 +89,17 @@ Copyright & License
</h1>
<p align="center">
<b>Telegram MTProto API Client Library for Python</b>
<b>Telegram MTProto API Framework for Python</b>
<br>
<a href="https://github.com/pyrogram/pyrogram/releases/latest">
Download
</a>
<a href="https://docs.pyrogram.ml">
Documentation
</a>
<a href="https://github.com/pyrogram/pyrogram/releases">
Changelog
</a>
<a href="https://t.me/PyrogramChat">
Community
</a>
@ -104,7 +110,7 @@ Copyright & License
</a>
<a href="https://github.com/pyrogram/tgcrypto">
<img src="https://img.shields.io/badge/tgcrypto-v1.1.1-eda738.svg?longCache=true&colorA=262b30"
alt="TgCrypto">
alt="TgCrypto Version">
</a>
</p>
@ -112,12 +118,12 @@ Copyright & License
:target: https://pyrogram.ml
:alt: Pyrogram
.. |description| replace:: **Telegram MTProto API Client Library for Python**
.. |description| replace:: **Telegram MTProto API Framework for Python**
.. |scheme| image:: "https://img.shields.io/badge/schema-layer%2091-eda738.svg?longCache=true&colorA=262b30"
.. |schema| image:: https://img.shields.io/badge/schema-layer%2091-eda738.svg?longCache=true&colorA=262b30
:target: compiler/api/source/main_api.tl
:alt: Scheme Layer
:alt: Schema Layer
.. |tgcrypto| image:: "https://img.shields.io/badge/tgcrypto-v1.1.1-eda738.svg?longCache=true&colorA=262b30"
.. |tgcrypto| image:: https://img.shields.io/badge/tgcrypto-v1.1.1-eda738.svg?longCache=true&colorA=262b30
:target: https://github.com/pyrogram/tgcrypto
:alt: TgCrypto
:alt: TgCrypto Version

View File

@ -26,7 +26,7 @@ NOTICE_PATH = "NOTICE"
SECTION_RE = re.compile(r"---(\w+)---")
LAYER_RE = re.compile(r"//\sLAYER\s(\d+)")
COMBINATOR_RE = re.compile(r"^([\w.]+)#([0-9a-f]+)\s(?:.*)=\s([\w<>.]+);(?: // Docs: (.+))?$", re.MULTILINE)
ARGS_RE = re.compile("[^{](\w+):([\w?!.<>]+)")
ARGS_RE = re.compile("[^{](\w+):([\w?!.<>#]+)")
FLAGS_RE = re.compile(r"flags\.(\d+)\?")
FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)")
FLAGS_RE_3 = re.compile(r"flags:#")
@ -288,17 +288,20 @@ def start():
sorted_args = sort_args(c.args)
arguments = ", " + ", ".join(
[get_argument_type(i) for i in sorted_args]
[get_argument_type(i) for i in sorted_args if i != ("flags", "#")]
) if c.args else ""
fields = "\n ".join(
["self.{0} = {0} # {1}".format(i[0], i[1]) for i in c.args]
["self.{0} = {0} # {1}".format(i[0], i[1]) for i in c.args if i != ("flags", "#")]
) if c.args else "pass"
docstring_args = []
docs = c.docs.split("|")[1:] if c.docs else None
for i, arg in enumerate(sorted_args):
if arg == ("flags", "#"):
continue
arg_name, arg_type = arg
is_optional = FLAGS_RE.match(arg_type)
flag_number = is_optional.group(1) if is_optional else -1
@ -338,28 +341,31 @@ def start():
if references:
docstring_args += "\n\n See Also:\n This object can be returned by " + references + "."
if c.has_flags:
write_flags = []
for i in c.args:
flag = FLAGS_RE.match(i[1])
if flag:
write_flags.append("flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0]))
write_flags = "\n ".join([
"flags = 0",
"\n ".join(write_flags),
"b.write(Int(flags))"
])
else:
write_flags = "# No flags"
read_flags = "flags = Int.read(b)" if c.has_flags else "# No flags"
write_types = read_types = ""
write_types = read_types = "" if c.has_flags else "# No flags\n "
for arg_name, arg_type in c.args:
flag = FLAGS_RE_2.findall(arg_type)
if arg_name == "flags" and arg_type == "#":
write_flags = []
for i in c.args:
flag = FLAGS_RE.match(i[1])
if flag:
write_flags.append(
"flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0]))
write_flags = "\n ".join([
"flags = 0",
"\n ".join(write_flags),
"b.write(Int(flags))\n "
])
write_types += write_flags
read_types += "flags = Int.read(b)\n "
continue
if flag:
index, flag_type = flag[0]
@ -448,11 +454,9 @@ def start():
object_id=c.id,
arguments=arguments,
fields=fields,
read_flags=read_flags,
read_types=read_types,
write_flags=write_flags,
write_types=write_types,
return_arguments=", ".join([i[0] for i in sorted_args])
return_arguments=", ".join([i[0] for i in sorted_args if i != ("flags", "#")])
)
)

View File

@ -16,7 +16,6 @@ class {class_name}(Object):
@staticmethod
def read(b: BytesIO, *args) -> "{class_name}":
{read_flags}
{read_types}
return {class_name}({return_arguments})
@ -24,6 +23,5 @@ class {class_name}(Object):
b = BytesIO()
b.write(Int(self.ID, False))
{write_flags}
{write_types}
return b.getvalue()

View File

@ -62,7 +62,7 @@ USER_IS_BOT A bot cannot send messages to other bots or to itself
WEBPAGE_CURL_FAILED Telegram server could not fetch the provided URL
STICKERSET_INVALID The requested sticker set is invalid
PEER_FLOOD The method can't be used because your account is limited
MEDIA_CAPTION_TOO_LONG The media caption is longer than 200 characters
MEDIA_CAPTION_TOO_LONG The media caption is longer than 1024 characters
USER_NOT_MUTUAL_CONTACT The user is not a mutual contact
USER_CHANNELS_TOO_MUCH The user is already in too many channels or supergroups
API_ID_PUBLISHED_FLOOD You are using an API key that is limited on the server side
@ -81,3 +81,10 @@ INPUT_USER_DEACTIVATED The target user has been deactivated
PASSWORD_RECOVERY_NA The password recovery e-mail is not available
PASSWORD_EMPTY The password entered is empty
PHONE_NUMBER_FLOOD This number has tried to login too many times
TAKEOUT_INVALID The takeout id is invalid
TAKEOUT_REQUIRED The method must be invoked inside a takeout session
MESSAGE_POLL_CLOSED You can't interact with a closed poll
MEDIA_INVALID The media is invalid
BOT_SCORE_NOT_MODIFIED The bot score was not modified
USER_BOT_REQUIRED The method can be used by bots only
IMAGE_PROCESS_FAILED The server failed to process your image
1 id message
62 WEBPAGE_CURL_FAILED Telegram server could not fetch the provided URL
63 STICKERSET_INVALID The requested sticker set is invalid
64 PEER_FLOOD The method can't be used because your account is limited
65 MEDIA_CAPTION_TOO_LONG The media caption is longer than 200 characters The media caption is longer than 1024 characters
66 USER_NOT_MUTUAL_CONTACT The user is not a mutual contact
67 USER_CHANNELS_TOO_MUCH The user is already in too many channels or supergroups
68 API_ID_PUBLISHED_FLOOD You are using an API key that is limited on the server side
81 PASSWORD_RECOVERY_NA The password recovery e-mail is not available
82 PASSWORD_EMPTY The password entered is empty
83 PHONE_NUMBER_FLOOD This number has tried to login too many times
84 TAKEOUT_INVALID The takeout id is invalid
85 TAKEOUT_REQUIRED The method must be invoked inside a takeout session
86 MESSAGE_POLL_CLOSED You can't interact with a closed poll
87 MEDIA_INVALID The media is invalid
88 BOT_SCORE_NOT_MODIFIED The bot score was not modified
89 USER_BOT_REQUIRED The method can be used by bots only
90 IMAGE_PROCESS_FAILED The server failed to process your image

View File

@ -1,2 +1,3 @@
id message
FLOOD_WAIT_X A wait of {x} seconds is required
TAKEOUT_INIT_DELAY_X You have to confirm the data export request using one of your mobile devices or wait {x} seconds

1 id message
2 FLOOD_WAIT_X A wait of {x} seconds is required
3 TAKEOUT_INIT_DELAY_X You have to confirm the data export request using one of your mobile devices or wait {x} seconds

View File

@ -10,27 +10,28 @@ Welcome to Pyrogram
</div>
<p align="center">
<b>Telegram MTProto API Client Library for Python</b>
<b>Telegram MTProto API Framework for Python</b>
<br>
<a href="https://github.com/pyrogram/pyrogram/releases/latest">
Download
<a href="https://docs.pyrogram.ml">
Documentation
</a>
<a href="https://github.com/pyrogram/pyrogram">
Source code
<a href="https://github.com/pyrogram/pyrogram/releases">
Changelog
</a>
<a href="https://t.me/PyrogramChat">
Community
</a>
<br>
<a href="https://github.com/pyrogram/pyrogram/blob/master/compiler/api/source/main_api.tl">
<a href="compiler/api/source/main_api.tl">
<img src="https://img.shields.io/badge/schema-layer%2091-eda738.svg?longCache=true&colorA=262b30"
alt="Scheme Layer">
alt="Schema Layer">
</a>
<a href="https://github.com/pyrogram/tgcrypto">
<img src="https://img.shields.io/badge/tgcrypto-v1.1.1-eda738.svg?longCache=true&colorA=262b30"
alt="TgCrypto">
alt="TgCrypto Version">
</a>
</p>
@ -48,25 +49,27 @@ Welcome to Pyrogram
app.run()
Welcome to Pyrogram's Documentation! Here you can find resources for learning how to use the library.
Welcome to Pyrogram's Documentation! Here you can find resources for learning how to use the framework.
Contents are organized into self-contained topics and can be accessed from the sidebar, or by following them in order
using the Next button at the end of each page. But first, here's a brief overview of what is this all about.
About
-----
**Pyrogram** is a brand new Telegram_ Client Library written from the ground up in Python and C. It can be used for
building custom Telegram applications that interact with the MTProto API as both User and Bot.
**Pyrogram** is an elegant, easy-to-use Telegram_ client library and framework written from the ground up in Python and C.
It enables you to easily create custom apps using both user and bot identities (bot API alternative) via the `MTProto API`_.
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.
- **Easy**: You can install Pyrogram with pip and start building your applications right away.
- **Elegant**: Low-level details are abstracted and re-presented in a much nicer and easier way.
- **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 91 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.
- **Documented**: Pyrogram API methods, types and public interfaces are well documented.
- **Type-hinted**: Exposed Pyrogram types and method parameters are all type-hinted.
- **Updated**, to the latest Telegram API version, currently Layer 91 on top of `MTProto 2.0`_.
- **Pluggable**: The Smart Plugin system allows to write components with minimal boilerplate code.
- **Comprehensive**: Execute any advanced action an official client is able to do, and even more.
To get started, press the Next button.
@ -85,6 +88,7 @@ To get started, press the Next button.
resources/UpdateHandling
resources/UsingFilters
resources/MoreOnUpdates
resources/ConfigurationFile
resources/SmartPlugins
resources/AutoAuthorization
resources/CustomizeSessions
@ -95,6 +99,7 @@ To get started, press the Next button.
resources/ErrorHandling
resources/TestServers
resources/AdvancedUsage
resources/VoiceCalls
resources/Changelog
.. toctree::
@ -112,3 +117,5 @@ To get started, press the Next button.
.. _`Telegram`: https://telegram.org/
.. _TgCrypto: https://docs.pyrogram.ml/resources/TgCrypto/
.. _`MTProto API`: https://core.telegram.org/api#telegram-api
.. _`MTProto 2.0`: https://core.telegram.org/mtproto

View File

@ -13,6 +13,7 @@ Utilities
start
stop
restart
idle
run
add_handler
@ -20,6 +21,7 @@ Utilities
send
resolve_peer
save_file
stop_transmission
Decorators
----------
@ -62,6 +64,7 @@ Messages
delete_messages
get_messages
get_history
iter_history
send_poll
vote_poll
retract_vote
@ -91,7 +94,9 @@ Chats
get_chat_member
get_chat_members
get_chat_members_count
iter_chat_members
get_dialogs
iter_dialogs
Users
-----
@ -135,6 +140,9 @@ Bots
send_inline_bot_result
answer_callback_query
request_callback_answer
send_game
set_game_score
get_game_high_scores
.. autoclass:: pyrogram.Client

View File

@ -57,6 +57,7 @@ Bots
InlineKeyboardButton
ForceReply
CallbackQuery
Game
Input Media
-----------
@ -182,6 +183,15 @@ Input Media
.. autoclass:: CallbackQuery
:members:
.. autoclass:: Game
:members:
.. autoclass:: GameHighScore
:members:
.. autoclass:: GameHighScores
:members:
.. Input Media
-----------

View File

@ -0,0 +1,90 @@
Configuration File
==================
As already mentioned in previous sections, Pyrogram can also be configured by the use of an INI file.
This page explains how this file is structured in Pyrogram, how to use it and why.
Introduction
------------
The idea behind using a configuration file is to help keeping your code free of settings (private) information such as
the API Key and Proxy without having you to even deal with how to load such settings. The configuration file, usually
referred as ``config.ini`` file, is automatically loaded from the root of your working directory; all you need to do is
fill in the necessary parts.
.. note::
The configuration file is optional, but recommended. If, for any reason, you prefer not to use it, there's always an
alternative way to configure Pyrogram via Client's parameters. Doing so, you can have full control on how to store
and load your settings (e.g.: from environment variables).
Settings specified via Client's parameter have higher priority and will override any setting stored in the
configuration file.
The config.ini File
-------------------
By default, Pyrogram will look for a file named ``config.ini`` placed at the root of your working directory, that is,
the same folder of your running script. You can change the name or location of your configuration file by specifying it
in your Client's parameter *config_file*.
- Replace the default *config.ini* file with *my_configuration.ini*:
.. code-block:: python
from pyrogram import Client
app = Client("my_account", config_file="my_configuration.ini")
Configuration Sections
----------------------
These are all the sections Pyrogram uses in its configuration file:
Pyrogram
^^^^^^^^
The ``[pyrogram]`` section contains your Telegram API credentials *api_id* and *api_hash*.
.. code-block:: ini
[pyrogram]
api_id = 12345
api_hash = 0123456789abcdef0123456789abcdef
`More info about API Key. <../start/Setup.html#configuration>`_
Proxy
^^^^^
The ``[proxy]`` section contains settings about your SOCKS5 proxy.
.. code-block:: ini
[proxy]
enabled = True
hostname = 11.22.33.44
port = 1080
username = <your_username>
password = <your_password>
`More info about SOCKS5 Proxy. <SOCKS5Proxy.html>`_
Plugins
^^^^^^^
The ``[plugins]`` section contains settings about Smart Plugins.
.. code-block:: ini
[plugins]
root = plugins
include =
module
folder.module
exclude =
module fn2
`More info about Smart Plugins. <SmartPlugins.html>`_

View File

@ -91,13 +91,14 @@ Stop Propagation
In order to prevent further propagation of an update in the dispatching phase, you can do *one* of the following:
- Call the update's bound-method ``.stop_propagation()`` (preferred way).
- Manually ``raise StopPropagation`` error (more suitable for raw updates only).
- Manually ``raise StopPropagation`` exception (more suitable for raw updates only).
.. note::
Note that ``.stop_propagation()`` is just an elegant and intuitive way to raise a ``StopPropagation`` error;
this means that any code coming *after* calling it won't be executed as your function just raised a custom exception
to signal the dispatcher not to propagate the update anymore.
Internally, the propagation is stopped by handling a custom exception. ``.stop_propagation()`` is just an elegant
and intuitive way to ``raise StopPropagation``; this also means that any code coming *after* calling the method
won't be executed as your function just raised an exception to signal the dispatcher not to propagate the
update anymore.
Example with ``stop_propagation()``:
@ -139,10 +140,82 @@ Example with ``raise StopPropagation``:
def _(client, message):
print(2)
The handler in group number 2 will never be executed because the propagation was stopped before. The output of both
examples will be:
Each handler is registered in a different group, but the handler in group number 2 will never be executed because the
propagation was stopped earlier. The output of both (equivalent) examples will be:
.. code-block:: text
0
1
Continue Propagation
^^^^^^^^^^^^^^^^^^^^
As opposed to `stopping the update propagation <#stop-propagation>`_ and also as an alternative to the
`handler groups <#handler-groups>`_, you can signal the internal dispatcher to continue the update propagation within
the group regardless of the next handler's filters. This allows you to register multiple handlers with overlapping
filters in the same group; to let the dispatcher process the next handler you can do *one* of the following in each
handler you want to grant permission to continue:
- Call the update's bound-method ``.continue_propagation()`` (preferred way).
- Manually ``raise ContinuePropagation`` exception (more suitable for raw updates only).
.. note::
Internally, the propagation is continued by handling a custom exception. ``.continue_propagation()`` is just an
elegant and intuitive way to ``raise ContinuePropagation``; this also means that any code coming *after* calling the
method won't be executed as your function just raised an exception to signal the dispatcher to continue with the
next available handler.
Example with ``continue_propagation()``:
.. code-block:: python
@app.on_message(Filters.private)
def _(client, message):
print(0)
message.continue_propagation()
@app.on_message(Filters.private)
def _(client, message):
print(1)
message.continue_propagation()
@app.on_message(Filters.private)
def _(client, message):
print(2)
Example with ``raise ContinuePropagation``:
.. code-block:: python
from pyrogram import ContinuePropagation
@app.on_message(Filters.private)
def _(client, message):
print(0)
raise ContinuePropagation
@app.on_message(Filters.private)
def _(client, message):
print(1)
raise ContinuePropagation
@app.on_message(Filters.private)
def _(client, message):
print(2)
Three handlers are registered in the same group, and all of them will be executed because the propagation was continued
in each handler (except in the last one, where is useless to do so since there is no more handlers after).
The output of both (equivalent) examples will be:
.. code-block:: text
0
1
2

View File

@ -1,9 +1,9 @@
Smart Plugins
=============
Pyrogram embeds a **smart** (automatic) and lightweight plugin system that is meant to further simplify the organization
of large projects and to provide a way for creating pluggable components that can be **easily shared** across different
Pyrogram applications with **minimal boilerplate code**.
Pyrogram embeds a **smart**, lightweight yet powerful plugin system that is meant to further simplify the organization
of large projects and to provide a way for creating pluggable (modular) components that can be **easily shared** across
different Pyrogram applications with **minimal boilerplate code**.
.. tip::
@ -13,7 +13,8 @@ Introduction
------------
Prior to the Smart Plugin system, pluggable handlers were already possible. For example, if you wanted to modularize
your applications, you had to do something like this...
your applications, you had to put your function definitions in separate files and register them inside your main script,
like this:
.. note::
@ -63,19 +64,19 @@ your applications, you had to do something like this...
app.run()
...which is already nice and doesn't add *too much* boilerplate code, but things can get boring still; you have to
This is already nice and doesn't add *too much* boilerplate code, but things can get boring still; you have to
manually ``import``, manually :meth:`add_handler <pyrogram.Client.add_handler>` and manually instantiate each
:obj:`MessageHandler <pyrogram.MessageHandler>` object because **you can't use those cool decorators** for your
functions. So... What if you could?
functions. So, what if you could? Smart Plugins solve this issue by taking care of handlers registration automatically.
Using Smart Plugins
-------------------
Setting up your Pyrogram project to accommodate Smart Plugins is pretty straightforward:
Setting up your Pyrogram project to accommodate Smart Plugins is straightforward:
#. Create a new folder to store all the plugins (e.g.: "plugins").
#. Put your files full of plugins inside.
#. Enable plugins in your Client.
#. Create a new folder to store all the plugins (e.g.: "plugins", "handlers", ...).
#. Put your python files full of plugins inside. Organize them as you wish.
#. Enable plugins in your Client or via the *config.ini* file.
.. note::
@ -107,20 +108,252 @@ Setting up your Pyrogram project to accommodate Smart Plugins is pretty straight
def echo_reversed(client, message):
message.reply(message.text[::-1])
- ``config.ini``
.. code-block:: ini
[plugins]
root = plugins
- ``main.py``
.. code-block:: python
from pyrogram import Client
Client("my_account", plugins_dir="plugins").run()
Client("my_account").run()
The first important thing to note is the new ``plugins`` folder, whose name is passed to the the ``plugins_dir``
parameter when creating a :obj:`Client <pyrogram.Client>` in the ``main.py`` file — you can put *any python file* in
there and each file can contain *any decorated function* (handlers) with only one limitation: within a single plugin
file you must use different names for each decorated function. Your Pyrogram Client instance will **automatically**
scan the folder upon creation to search for valid handlers and register them for you.
Alternatively, without using the *config.ini* file:
.. code-block:: python
from pyrogram import Client
plugins = dict(
root="plugins"
)
Client("my_account", plugins=plugins).run()
The first important thing to note is the new ``plugins`` folder. You can put *any python file* in *any subfolder* and
each file can contain *any decorated function* (handlers) with one limitation: within a single module (file) you must
use different names for each decorated function.
The second thing is telling Pyrogram where to look for your plugins: you can either use the *config.ini* file or
the Client parameter "plugins"; the *root* value must match the name of your plugins folder. Your Pyrogram Client
instance will **automatically** scan the folder upon starting to search for valid handlers and register them for you.
Then you'll notice you can now use decorators. That's right, you can apply the usual decorators to your callback
functions in a static way, i.e. **without having the Client instance around**: simply use ``@Client`` (Client class)
instead of the usual ``@app`` (Client instance) namespace and things will work just the same.
instead of the usual ``@app`` (Client instance) and things will work just the same.
Specifying the Plugins to include
---------------------------------
By default, if you don't explicitly supply a list of plugins, every valid one found inside your plugins root folder will
be included by following the alphabetical order of the directory structure (files and subfolders); the single handlers
found inside each module will be, instead, loaded in the order they are defined, from top to bottom.
.. note::
Remember: there can be at most one handler, within a group, dealing with a specific update. Plugins with overlapping
filters included a second time will not work. Learn more at `More on Updates <MoreOnUpdates.html>`_.
This default loading behaviour is usually enough, but sometimes you want to have more control on what to include (or
exclude) and in which exact order to load plugins. The way to do this is to make use of ``include`` and ``exclude``
keys, either in the *config.ini* file or in the dictionary passed as Client argument. Here's how they work:
- If both ``include`` and ``exclude`` are omitted, all plugins are loaded as described above.
- If ``include`` is given, only the specified plugins will be loaded, in the order they are passed.
- If ``exclude`` is given, the plugins specified here will be unloaded.
The ``include`` and ``exclude`` value is a **list of strings**. Each string containing the path of the module relative
to the plugins root folder, in Python notation (dots instead of slashes).
E.g.: ``subfolder.module`` refers to ``plugins/subfolder/module.py``, with ``root="plugins"``.
You can also choose the order in which the single handlers inside a module are loaded, thus overriding the default
top-to-bottom loading policy. You can do this by appending the name of the functions to the module path, each one
separated by a blank space.
E.g.: ``subfolder.module fn2 fn1 fn3`` will load *fn2*, *fn1* and *fn3* from *subfolder.module*, in this order.
Examples
^^^^^^^^
Given this plugins folder structure with three modules, each containing their own handlers (fn1, fn2, etc...), which are
also organized in subfolders:
.. code-block:: text
myproject/
plugins/
subfolder1/
plugins1.py
- fn1
- fn2
- fn3
subfolder2/
plugins2.py
...
plugins0.py
...
...
- Load every handler from every module, namely *plugins0.py*, *plugins1.py* and *plugins2.py* in alphabetical order
(files) and definition order (handlers inside files):
Using *config.ini* file:
.. code-block:: ini
[plugins]
root = plugins
Using *Client*'s parameter:
.. code-block:: python
plugins = dict(
root="plugins"
)
Client("my_account", plugins=plugins).run()
- Load only handlers defined inside *plugins2.py* and *plugins0.py*, in this order:
Using *config.ini* file:
.. code-block:: ini
[plugins]
root = plugins
include =
subfolder2.plugins2
plugins0
Using *Client*'s parameter:
.. code-block:: python
plugins = dict(
root="plugins",
include=[
"subfolder2.plugins2",
"plugins0"
]
)
Client("my_account", plugins=plugins).run()
- Load everything except the handlers inside *plugins2.py*:
Using *config.ini* file:
.. code-block:: ini
[plugins]
root = plugins
exclude = subfolder2.plugins2
Using *Client*'s parameter:
.. code-block:: python
plugins = dict(
root="plugins",
exclude=["subfolder2.plugins2"]
)
Client("my_account", plugins=plugins).run()
- Load only *fn3*, *fn1* and *fn2* (in this order) from *plugins1.py*:
Using *config.ini* file:
.. code-block:: ini
[plugins]
root = plugins
include = subfolder1.plugins1 fn3 fn1 fn2
Using *Client*'s parameter:
.. code-block:: python
plugins = dict(
root="plugins",
include=["subfolder1.plugins1 fn3 fn1 fn2"]
)
Client("my_account", plugins=plugins).run()
Load/Unload Plugins at Runtime
------------------------------
In the `previous section <#specifying-the-plugins-to-include>`_ we've explained how to specify which plugins to load and
which to ignore before your Client starts. Here we'll show, instead, how to unload and load again a previously
registered plugins at runtime.
Each function decorated with the usual ``on_message`` decorator (or any other decorator that deals with Telegram updates
) will be modified in such a way that, when you reference them later on, they will be actually pointing to a tuple of
*(handler: Handler, group: int)*. The actual callback function is therefore stored inside the handler's *callback*
attribute. Here's an example:
- ``plugins/handlers.py``
.. code-block:: python
:emphasize-lines: 5, 6
@Client.on_message(Filters.text & Filters.private)
def echo(client, message):
message.reply(message.text)
print(echo)
print(echo[0].callback)
- Printing ``echo`` will show something like ``(<MessageHandler object at 0x10e3abc50>, 0)``.
- Printing ``echo[0].callback``, that is, the *callback* attribute of the first eleent of the tuple, which is an
Handler, will reveal the actual callback ``<function echo at 0x10e3b6598>``.
Unloading
^^^^^^^^^
In order to unload a plugin, or any other handler, all you need to do is obtain a reference to it (by importing the
relevant module) and call :meth:`remove_handler <pyrogram.Client.remove_handler>` Client's method with your function
name preceded by the star ``*`` operator as argument. Example:
- ``main.py``
.. code-block:: python
from plugins.handlers import echo
...
app.remove_handler(*echo)
The star ``*`` operator is used to unpack the tuple into positional arguments so that *remove_handler* will receive
exactly what is needed. The same could have been achieved with:
.. code-block:: python
handler, group = echo
app.remove_handler(handler, group)
Loading
^^^^^^^
Similarly to the unloading process, in order to load again a previously unloaded plugin you do the same, but this time
using :meth:`add_handler <pyrogram.Client.add_handler>` instead. Example:
- ``main.py``
.. code-block:: python
from plugins.handlers import echo
...
app.add_handler(*echo)

View File

@ -1,5 +1,5 @@
TgCrypto
========
Fast Crypto
===========
Pyrogram's speed can be *dramatically* boosted up by TgCrypto_, a high-performance, easy-to-install Telegram Crypto
Library specifically written in C for Pyrogram [#f1]_ as a Python extension.

View File

@ -0,0 +1,10 @@
Voice Calls
===========
A working proof-of-concept of Telegram voice calls using Pyrogram can be found here:
https://github.com/bakatrouble/pylibtgvoip. Thanks to `@bakatrouble <https://t.me/bakatrouble>`_.
.. note::
This page will be updated with more information once voice calls become eventually more usable and more integrated
in Pyrogram itself.

View File

@ -2,21 +2,21 @@
This folder contains example scripts to show you how **Pyrogram** looks like.
Every script is working right away (provided you correctly set up your credentials), meaning
you can simply copy-paste and run. The only things you have to change are session names and target chats.
Every script is working right away (provided you correctly set up your credentials), meaning you can simply copy-paste
and run. The only things you have to change are session names and target chats.
All the examples listed in this directory are licensed under the terms of the [CC0 1.0 Universal](LICENSE) license and
can be freely used as basic building blocks for your own applications without worrying about copyrights.
Example | Description
---: | :---
[**hello_world**](hello_world.py) | Demonstration of basic API usages
[**echo_bot**](echo_bot.py) | Echo bot that replies to every private text message
[**welcome_bot**](welcome_bot.py) | The Welcome Bot source code in [@PyrogramChat](https://t.me/pyrogramchat)
[**get_history**](get_history.py) | How to retrieve the full message history of a chat
[**get_chat_members**](get_chat_members.py) | How to get the first 10.000 members of a supergroup/channel
[**get_chat_members2**](get_chat_members2.py) | Improved version to get more than 10.000 members
[**query_inline_bots**](query_inline_bots.py) | How to query an inline bot and send a result to a chat
[**send_bot_keyboards**](send_bot_keyboards.py) | How to send normal and inline keyboards using regular bots
[**callback_query_handler**](callback_query_handler.py) | How to handle queries coming from inline button presses
[**raw_update_handler**](raw_update_handler.py) | How to handle raw updates (old, should be avoided)
[**hello**](hello.py) | Demonstration of basic API usage
[**echo**](echo.py) | Reply to every private text message
[**welcome**](welcome.py) | The Welcome Bot in [@PyrogramChat](https://t.me/pyrogramchat)
[**history**](history.py) | Get the full message history of a chat
[**chat_members**](chat_members.py) | Get all the members of a chat
[**dialogs**](dialogs.py) | Get all of your dialog chats
[**inline_bots**](inline_bots.py) | Query an inline bot and send a result to a chat
[**keyboards**](keyboards.py) | Send normal and inline keyboards using regular bots
[**callback_queries**](callback_queries.py) | Handle queries coming from inline button presses
[**raw_updates**](raw_updates.py) | Handle raw updates (old, should be avoided)

10
examples/chat_members.py Normal file
View File

@ -0,0 +1,10 @@
"""This example shows how to get all the members of a chat."""
from pyrogram import Client
app = Client("my_count")
target = "pyrogramchat" # Target channel/supergroup
with app:
for member in app.iter_chat_members(target):
print(member.user.first_name)

9
examples/dialogs.py Normal file
View File

@ -0,0 +1,9 @@
"""This example shows how to get the full dialogs list of a user."""
from pyrogram import Client
app = Client("my_account")
with app:
for dialog in app.iter_dialogs():
print(dialog.chat.title or dialog.chat.first_name)

View File

@ -11,7 +11,7 @@ app = Client("my_account")
@app.on_message(Filters.text & Filters.private)
def echo(client, message):
message.reply(message.text, quote=True)
message.reply(message.text)
app.run() # Automatically start() and idle()

View File

@ -1,31 +0,0 @@
"""This example shows you how to get the first 10.000 members of a chat.
Refer to get_chat_members2.py for more than 10.000 members.
"""
import time
from pyrogram import Client
from pyrogram.api.errors import FloodWait
app = Client("my_account")
target = "pyrogramchat" # Target channel/supergroup
members = [] # List that will contain all the members of the target chat
offset = 0 # Offset starts at 0
limit = 200 # Amount of users to retrieve for each API call (max 200)
with app:
while True:
try:
chunk = app.get_chat_members(target, offset)
except FloodWait as e: # Very large chats could trigger FloodWait
time.sleep(e.x) # When it happens, wait X seconds and try again
continue
if not chunk.chat_members:
break # No more members left
members.extend(chunk.chat_members)
offset += len(chunk.chat_members)
# Now the "members" list contains all the members of the target chat

View File

@ -1,50 +0,0 @@
"""This is an improved version of get_chat_members.py
Since Telegram will return at most 10.000 members for a single query, this script
repeats the search using numbers ("0" to "9") and all the available ascii letters ("a" to "z").
This can be further improved by also searching for non-ascii characters (e.g.: Japanese script),
as some user names may not contain ascii letters at all.
"""
import time
from string import ascii_lowercase
from pyrogram import Client
from pyrogram.api.errors import FloodWait
app = Client("my_account")
target = "pyrogramchat" # Target channel/supergroup
members = {} # List that will contain all the members of the target chat
limit = 200 # Amount of users to retrieve for each API call (max 200)
# "" + "0123456789" + "abcdefghijklmnopqrstuvwxyz" (as list)
queries = [""] + [str(i) for i in range(10)] + list(ascii_lowercase)
with app:
for q in queries:
print('Searching for "{}"'.format(q))
offset = 0 # For each query, offset restarts from 0
while True:
try:
chunk = app.get_chat_members(target, offset, query=q)
except FloodWait as e: # Very large chats could trigger FloodWait
print("Flood wait: {} seconds".format(e.x))
time.sleep(e.x) # When it happens, wait X seconds and try again
continue
if not chunk.chat_members:
print('Done searching for "{}"'.format(q))
print()
break # No more members left
members.update({i.user.id: i for i in chunk.chat_members})
offset += len(chunk.chat_members)
print("Total members: {}".format(len(members)))
print("Grand total: {}".format(len(members)))
# Now the "members" list contains all the members of the target chat

View File

@ -1,31 +0,0 @@
"""This example shows how to retrieve the full message history of a chat"""
import time
from pyrogram import Client
from pyrogram.api.errors import FloodWait
app = Client("my_account")
target = "me" # "me" refers to your own chat (Saved Messages)
messages = [] # List that will contain all the messages of the target chat
offset_id = 0 # ID of the last message of the chunk
with app:
while True:
try:
m = app.get_history(target, offset_id=offset_id)
except FloodWait as e: # For very large chats the method call can raise a FloodWait
print("waiting {}".format(e.x))
time.sleep(e.x) # Sleep X seconds before continuing
continue
if not m.messages:
break
messages += m.messages
offset_id = m.messages[-1].message_id
print("Messages: {}".format(len(messages)))
# Now the "messages" list contains all the messages sorted by date in
# descending order (from the most recent to the oldest one)

16
examples/hello.py Normal file
View File

@ -0,0 +1,16 @@
"""This example demonstrates a basic API usage"""
from pyrogram import Client
# Create a new Client instance
app = Client("my_account")
with app:
# Send a message, Markdown is enabled by default
app.send_message("me", "Hi there! I'm using **Pyrogram**")
# Send a location
app.send_location("me", 51.500729, -0.124583)
# Send a sticker
app.send_sticker("me", "CAADBAADhw4AAvLQYAHICbZ5SUs_jwI")

View File

@ -1,18 +0,0 @@
"""This example demonstrates a basic API usage"""
from pyrogram import Client
# Create a new Client instance
app = Client("my_account")
# Start the Client before calling any API method
app.start()
# Send a message to yourself, Markdown is enabled by default
app.send_message("me", "Hi there! I'm using **Pyrogram**")
# Send a location to yourself
app.send_location("me", 51.500729, -0.124583)
# Stop the client when you're done
app.stop()

10
examples/history.py Normal file
View File

@ -0,0 +1,10 @@
"""This example shows how to get the full message history of a chat, starting from the latest message"""
from pyrogram import Client
app = Client("my_account")
target = "me" # "me" refers to your own chat (Saved Messages)
with app:
for message in app.iter_history(target):
print(message.text)

59
examples/keyboards.py Normal file
View File

@ -0,0 +1,59 @@
"""This example will show you how to send normal and inline keyboards.
You must log-in as a regular bot in order to send keyboards (use the token from @BotFather).
Any attempt in sending keyboards with a user account will be simply ignored by the server.
send_message() is used as example, but a keyboard can be sent with any other send_* methods,
like send_audio(), send_document(), send_location(), etc...
"""
from pyrogram import Client, ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton
# Create a client using your bot token
app = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11")
with app:
app.send_message(
"haskell", # Edit this
"This is a ReplyKeyboardMarkup example",
reply_markup=ReplyKeyboardMarkup(
[
["A", "B", "C", "D"], # First row
["E", "F", "G"], # Second row
["H", "I"], # Third row
["J"] # Fourth row
],
resize_keyboard=True # Make the keyboard smaller
)
)
app.send_message(
"haskell", # Edit this
"This is a InlineKeyboardMarkup example",
reply_markup=InlineKeyboardMarkup(
[
[ # First row
InlineKeyboardButton( # Generates a callback query when pressed
"Button",
callback_data=b"data"
), # Note how callback_data must be bytes
InlineKeyboardButton( # Opens a web URL
"URL",
url="https://docs.pyrogram.ml"
),
],
[ # Second row
# Opens the inline interface
InlineKeyboardButton(
"Choose chat",
switch_inline_query="pyrogram"
),
InlineKeyboardButton( # Opens the inline interface in the current chat
"Inline here",
switch_inline_query_current_chat="pyrogram"
)
]
]
)
)

View File

@ -1,51 +0,0 @@
"""This example will show you how to send normal and inline keyboards.
You must log-in as a regular bot in order to send keyboards (use the token from @BotFather).
Any attempt in sending keyboards with a user account will be simply ignored by the server.
send_message() is used as example, but a keyboard can be sent with any other send_* methods,
like send_audio(), send_document(), send_location(), etc...
"""
from pyrogram import Client, ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton
# Create a client using your bot token
app = Client("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11")
app.start()
app.send_message(
"haskell", # Edit this
"This is a ReplyKeyboardMarkup example",
reply_markup=ReplyKeyboardMarkup(
[
["A", "B", "C", "D"], # First row
["E", "F", "G"], # Second row
["H", "I"], # Third row
["J"] # Fourth row
],
resize_keyboard=True # Make the keyboard smaller
)
)
app.send_message(
"haskell", # Edit this
"This is a InlineKeyboardMarkup example",
reply_markup=InlineKeyboardMarkup(
[
[ # First row
# Generates a callback query when pressed
InlineKeyboardButton("Button", callback_data="data"),
# Opens a web URL
InlineKeyboardButton("URL", url="https://docs.pyrogram.ml"),
],
[ # Second row
# Opens the inline interface of a bot in another chat with a pre-defined query
InlineKeyboardButton("Choose chat", switch_inline_query="pyrogram"),
# Same as the button above, but the inline interface is opened in the current chat
InlineKeyboardButton("Inline here", switch_inline_query_current_chat="pyrogram"),
]
]
)
)
app.stop()

29
examples/welcome.py Normal file
View File

@ -0,0 +1,29 @@
"""This is the Welcome Bot in @PyrogramChat.
It uses the Emoji module to easily add emojis in your text messages and Filters
to make it only work for specific messages in a specific chat.
"""
from pyrogram import Client, Emoji, Filters
TARGET = "PyrogramChat" # Target chat. Can also be a list of multiple chat ids/usernames
MENTION = "[{}](tg://user?id={})" # User mention markup
MESSAGE = "{} Welcome to [Pyrogram](https://docs.pyrogram.ml/)'s group chat {}!" # Welcome message
app = Client("my_account")
# Filter in only new_chat_members updates generated in TARGET chat
@app.on_message(Filters.chat(TARGET) & Filters.new_chat_members)
def welcome(client, message):
# Build the new members list (with mentions) by using their first_name
new_members = [MENTION.format(i.first_name, i.id) for i in message.new_chat_members]
# Build the welcome message by using an emoji and the list we built above
text = MESSAGE.format(Emoji.SPARKLES, ", ".join(new_members))
# Send the welcome message, without the web page preview
message.reply(text, disable_web_page_preview=True)
app.run() # Automatically start() and idle()

View File

@ -1,45 +0,0 @@
"""This is the Welcome Bot in @PyrogramChat.
It uses the Emoji module to easily add emojis in your text messages and Filters
to make it only work for specific messages in a specific chat.
"""
from pyrogram import Client, Emoji, Filters
USER = "**{}**"
MESSAGE = "{} Welcome to [Pyrogram](https://docs.pyrogram.ml/)'s group chat {{}}!".format(Emoji.SPARKLES)
enabled_groups = Filters.chat("PyrogramChat")
last_welcomes = {}
app = Client("my_account")
@app.on_message(enabled_groups & Filters.new_chat_members)
def welcome(client, message):
chat_id = message.chat.id
# Get the previous welcome message and members, if any
previous_welcome, previous_members = last_welcomes.pop(chat_id, (None, []))
# Delete the previous message, if exists
if previous_welcome:
previous_welcome.delete()
# Build the new members list by using their first_name. Also append the previous members, if any
new_members = [USER.format(i.first_name) for i in message.new_chat_members] + previous_members
# Build the welcome message by using an emoji and the list we created above
text = MESSAGE.format(", ".join(new_members))
# Actually send the welcome and save the new message and the new members list
last_welcomes[message.chat.id] = message.reply(text, disable_web_page_preview=True), new_members
@app.on_message(enabled_groups)
def reset(client, message):
# Don't make the bot delete the previous welcome in case someone talks in the middle
last_welcomes.pop(message.chat.id, None)
app.run() # Automatically start() and idle()

View File

@ -18,12 +18,12 @@
import sys
__copyright__ = "Copyright (C) 2017-2018 Dan Tès <https://github.com/delivrance>".replace(
__copyright__ = "Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>".replace(
"\xe8",
"e" if sys.getfilesystemencoding() != "utf-8" else "\xe8"
)
__license__ = "GNU Lesser General Public License v3 or later (LGPLv3+)"
__version__ = "0.10.3"
__version__ = "0.11.0"
from .api.errors import Error
from .client.types import (
@ -32,7 +32,8 @@ from .client.types import (
Location, Message, MessageEntity, Dialog, Dialogs, Photo, PhotoSize, Sticker, User, UserStatus,
UserProfilePhotos, Venue, Animation, Video, VideoNote, Voice, CallbackQuery, Messages, ForceReply,
InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove,
Poll, PollOption, ChatPreview, StopPropagation
Poll, PollOption, ChatPreview, StopPropagation, ContinuePropagation, Game, CallbackGame, GameHighScore,
GameHighScores
)
from .client import (
Client, ChatAction, ParseMode, Emoji,

View File

@ -111,26 +111,37 @@ class Client(Methods, BaseClient):
Only applicable for new sessions and will be ignored in case previously
created sessions are loaded.
phone_number (``str``, *optional*):
Pass your phone number (with your Country Code prefix included) to avoid
entering it manually. Only applicable for new sessions.
phone_number (``str`` | ``callable``, *optional*):
Pass your phone number as string (with your Country Code prefix included) to avoid entering it manually.
Or pass a callback function which accepts no arguments and must return the correct phone number as string
(e.g., "391234567890").
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 accepts
a single positional argument *(phone_number)* and must return the correct phone code (e.g., "12345").
Pass the phone code as string (for test numbers only) to avoid entering it manually. Or pass a callback
function which accepts a single positional argument *(phone_number)* and must return the correct phone code
as string (e.g., "12345").
Only applicable for new sessions.
password (``str``, *optional*):
Pass your Two-Step Verification password (if you have one) to avoid entering it
manually. Only applicable for new sessions.
Pass your Two-Step Verification password as string (if you have one) to avoid entering it manually.
Or pass a callback function which accepts a single positional argument *(password_hint)* and must return
the correct password as string (e.g., "password").
Only applicable for new sessions.
recovery_code (``callable``, *optional*):
Pass a callback function which accepts a single positional argument *(email_pattern)* and must return the
correct password recovery code as string (e.g., "987654").
Only applicable for new sessions.
force_sms (``str``, *optional*):
Pass True to force Telegram sending the authorization code via SMS.
Only applicable for new sessions.
first_name (``str``, *optional*):
Pass a First Name to avoid entering it manually. It will be used to automatically
create a new Telegram account in case the phone number you passed is not registered yet.
Pass a First Name as string to avoid entering it manually. Or pass a callback function which accepts no
arguments and must return the correct name as string (e.g., "Dan"). It will be used to automatically create
a new Telegram account in case the phone number you passed is not registered yet.
Only applicable for new sessions.
last_name (``str``, *optional*):
@ -147,10 +158,22 @@ class Client(Methods, BaseClient):
config_file (``str``, *optional*):
Path of the configuration file. Defaults to ./config.ini
plugins_dir (``str``, *optional*):
Define a custom directory for your plugins. The plugins directory is the location in your
filesystem where Pyrogram will automatically load your update handlers.
Defaults to None (plugins disabled).
plugins (``dict``, *optional*):
Your Smart Plugins settings as dict, e.g.: *dict(root="plugins")*.
This is an alternative way to setup plugins if you don't want to use the *config.ini* file.
no_updates (``bool``, *optional*):
Pass True to completely disable incoming updates for the current session.
When updates are disabled your client can't receive any new message.
Useful for batch programs that don't need to deal with updates.
Defaults to False (updates enabled and always received).
takeout (``bool``, *optional*):
Pass True to let the client use a takeout session instead of a normal one, implies no_updates.
Useful for exporting your Telegram data. Methods invoked inside a takeout session (such as get_history,
download_media, ...) are less prone to throw FloodWait exceptions.
Only available for users, bots will ignore this parameter.
Defaults to False (normal session).
"""
def __init__(self,
@ -167,13 +190,16 @@ class Client(Methods, BaseClient):
phone_number: str = None,
phone_code: Union[str, callable] = None,
password: str = None,
recovery_code: callable = None,
force_sms: bool = False,
first_name: str = None,
last_name: str = None,
workers: int = BaseClient.WORKERS,
workdir: str = BaseClient.WORKDIR,
config_file: str = BaseClient.CONFIG_FILE,
plugins_dir: str = None):
plugins: dict = None,
no_updates: bool = None,
takeout: bool = None):
super().__init__()
self.session_name = session_name
@ -190,13 +216,16 @@ class Client(Methods, BaseClient):
self.phone_number = phone_number
self.phone_code = phone_code
self.password = password
self.recovery_code = recovery_code
self.force_sms = force_sms
self.first_name = first_name
self.last_name = last_name
self.workers = workers
self.workdir = workdir
self.config_file = config_file
self.plugins_dir = plugins_dir
self.plugins = plugins
self.no_updates = no_updates
self.takeout = takeout
self.dispatcher = Dispatcher(self, workers)
@ -212,7 +241,14 @@ class Client(Methods, BaseClient):
@proxy.setter
def proxy(self, value):
self._proxy["enabled"] = True
if value is None:
self._proxy = None
return
if self._proxy is None:
self._proxy = {}
self._proxy["enabled"] = bool(value.get("enabled", True))
self._proxy.update(value)
def start(self):
@ -253,6 +289,10 @@ class Client(Methods, BaseClient):
self.save_session()
if self.bot_token is None:
if self.takeout:
self.takeout_id = self.send(functions.account.InitTakeoutSession()).id
log.warning("Takeout session {} initiated".format(self.takeout_id))
now = time.time()
if abs(now - self.date) > Client.OFFLINE_SLEEP:
@ -308,6 +348,10 @@ class Client(Methods, BaseClient):
if not self.is_started:
raise ConnectionError("Client is already stopped")
if self.takeout_id:
self.send(functions.account.FinishTakeoutSession())
log.warning("Takeout session {} finished".format(self.takeout_id))
Syncer.remove(self)
self.dispatcher.stop()
@ -337,6 +381,16 @@ class Client(Methods, BaseClient):
return self
def restart(self):
"""Use this method to restart the Client.
Requires no parameters.
Raises:
``ConnectionError`` in case you try to restart a stopped Client.
"""
self.stop()
self.start()
def idle(self, stop_signals: tuple = (SIGINT, SIGTERM, SIGABRT)):
"""Blocks the program execution until one of the signals are received,
then gently stop the Client by closing the underlying connection.
@ -413,6 +467,12 @@ class Client(Methods, BaseClient):
else:
self.dispatcher.remove_handler(handler, group)
def stop_transmission(self):
"""Use this method to stop downloading or uploading a file.
Must be called inside a progress callback function.
"""
raise Client.StopTransmission
def authorize_bot(self):
try:
r = self.send(
@ -445,20 +505,25 @@ class Client(Methods, BaseClient):
def authorize_user(self):
phone_number_invalid_raises = self.phone_number is not None
phone_code_invalid_raises = self.phone_code is not None
password_hash_invalid_raises = self.password is not None
password_invalid_raises = self.password is not None
first_name_invalid_raises = self.first_name is not None
def default_phone_number_callback():
while True:
phone_number = input("Enter phone number: ")
confirm = input("Is \"{}\" correct? (y/n): ".format(phone_number))
if confirm in ("y", "1"):
return phone_number
elif confirm in ("n", "2"):
continue
while True:
if self.phone_number is None:
self.phone_number = input("Enter phone number: ")
while True:
confirm = input("Is \"{}\" correct? (y/n): ".format(self.phone_number))
if confirm in ("y", "1"):
break
elif confirm in ("n", "2"):
self.phone_number = input("Enter phone number: ")
self.phone_number = (
default_phone_number_callback() if self.phone_number is None
else str(self.phone_number()) if callable(self.phone_number)
else str(self.phone_number)
)
self.phone_number = self.phone_number.strip("+")
@ -474,23 +539,21 @@ class Client(Methods, BaseClient):
self.session.stop()
self.dc_id = e.x
self.auth_key = Auth(self.dc_id, self.test_mode, self.ipv6, self._proxy).create()
self.auth_key = Auth(
self.dc_id,
self.test_mode,
self.ipv6,
self._proxy
).create()
self.session = Session(
self,
self.dc_id,
self.auth_key
)
self.session.start()
r = self.send(
functions.auth.SendCode(
self.phone_number,
self.api_id,
self.api_hash
)
)
break
self.session.start()
except (PhoneNumberInvalid, PhoneNumberBanned) as e:
if phone_number_invalid_raises:
raise
@ -505,6 +568,7 @@ class Client(Methods, BaseClient):
time.sleep(e.x)
except Exception as e:
log.error(e, exc_info=True)
raise
else:
break
@ -524,10 +588,23 @@ class Client(Methods, BaseClient):
)
while True:
if not phone_registered:
self.first_name = (
input("First name: ") if self.first_name is None
else str(self.first_name()) if callable(self.first_name)
else str(self.first_name)
)
self.last_name = (
input("Last name: ") if self.last_name is None
else str(self.last_name()) if callable(self.last_name)
else str(self.last_name)
)
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(self.phone_number))
else str(self.phone_code(self.phone_number)) if callable(self.phone_code)
else str(self.phone_code)
)
try:
@ -545,9 +622,6 @@ class Client(Methods, BaseClient):
phone_registered = False
continue
else:
self.first_name = self.first_name if self.first_name is not None else input("First name: ")
self.last_name = self.last_name if self.last_name is not None else input("Last name: ")
try:
r = self.send(
functions.auth.SignUp(
@ -577,60 +651,62 @@ class Client(Methods, BaseClient):
except SessionPasswordNeeded as e:
print(e.MESSAGE)
def default_password_callback(password_hint: str) -> str:
print("Hint: {}".format(password_hint))
return input("Enter password (empty to recover): ")
def default_recovery_callback(email_pattern: str) -> str:
print("An e-mail containing the recovery code has been sent to {}".format(email_pattern))
return input("Enter password recovery code: ")
while True:
try:
r = self.send(functions.account.GetPassword())
if self.password is None:
print("Hint: {}".format(r.hint))
self.password = (
default_password_callback(r.hint) if self.password is None
else str(self.password(r.hint) or "") if callable(self.password)
else str(self.password)
)
self.password = input("Enter password (empty to recover): ")
if self.password == "":
r = self.send(functions.auth.RequestPasswordRecovery())
if self.password == "":
r = self.send(functions.auth.RequestPasswordRecovery())
self.recovery_code = (
default_recovery_callback(r.email_pattern) if self.recovery_code is None
else str(self.recovery_code(r.email_pattern)) if callable(self.recovery_code)
else str(self.recovery_code)
)
print("An e-mail containing the recovery code has been sent to {}".format(
r.email_pattern
))
r = self.send(
functions.auth.RecoverPassword(
code=input("Enter password recovery code: ")
)
r = self.send(
functions.auth.RecoverPassword(
code=self.recovery_code
)
else:
r = self.send(
functions.auth.CheckPassword(
password=compute_check(r, self.password)
)
)
else:
r = self.send(
functions.auth.CheckPassword(
password=compute_check(r, self.password)
)
except PasswordEmpty as e:
if password_hash_invalid_raises:
raise
else:
print(e.MESSAGE)
self.password = None
except PasswordRecoveryNa as e:
if password_hash_invalid_raises:
raise
else:
print(e.MESSAGE)
self.password = None
except PasswordHashInvalid as e:
if password_hash_invalid_raises:
)
except (PasswordEmpty, PasswordRecoveryNa, PasswordHashInvalid) as e:
if password_invalid_raises:
raise
else:
print(e.MESSAGE)
self.password = None
self.recovery_code = None
except FloodWait as e:
if password_hash_invalid_raises:
if password_invalid_raises:
raise
else:
print(e.MESSAGE.format(x=e.x))
time.sleep(e.x)
self.password = None
self.recovery_code = None
except Exception as e:
log.error(e, exc_info=True)
raise
else:
break
break
@ -642,6 +718,7 @@ class Client(Methods, BaseClient):
time.sleep(e.x)
except Exception as e:
log.error(e, exc_info=True)
raise
else:
break
@ -943,6 +1020,12 @@ class Client(Methods, BaseClient):
if not self.is_started:
raise ConnectionError("Client has not been started")
if self.no_updates:
data = functions.InvokeWithoutUpdates(data)
if self.takeout_id:
data = functions.InvokeWithTakeout(self.takeout_id, data)
r = self.session.send(data, retries, timeout)
self.fetch_peers(getattr(r, "users", []))
@ -980,17 +1063,42 @@ class Client(Methods, BaseClient):
setattr(self, option, getattr(Client, option.upper()))
if self._proxy:
self._proxy["enabled"] = True
self._proxy["enabled"] = bool(self._proxy.get("enabled", True))
else:
self._proxy = {}
if parser.has_section("proxy"):
self._proxy["enabled"] = parser.getboolean("proxy", "enabled")
self._proxy["enabled"] = parser.getboolean("proxy", "enabled", fallback=True)
self._proxy["hostname"] = parser.get("proxy", "hostname")
self._proxy["port"] = parser.getint("proxy", "port")
self._proxy["username"] = parser.get("proxy", "username", fallback=None) or None
self._proxy["password"] = parser.get("proxy", "password", fallback=None) or None
if self.plugins:
self.plugins["enabled"] = bool(self.plugins.get("enabled", True))
self.plugins["include"] = "\n".join(self.plugins.get("include", [])) or None
self.plugins["exclude"] = "\n".join(self.plugins.get("exclude", [])) or None
else:
try:
section = parser["plugins"]
self.plugins = {
"enabled": section.getboolean("enabled", True),
"root": section.get("root"),
"include": section.get("include") or None,
"exclude": section.get("exclude") or None
}
except KeyError:
self.plugins = {}
if self.plugins:
for option in ["include", "exclude"]:
if self.plugins[option] is not None:
self.plugins[option] = [
(i.split()[0], i.split()[1:] or None)
for i in self.plugins[option].strip().split("\n")
]
def load_session(self):
try:
with open(os.path.join(self.workdir, "{}.session".format(self.session_name)), encoding="utf-8") as f:
@ -1022,43 +1130,108 @@ class Client(Methods, BaseClient):
self.peers_by_phone[k] = peer
def load_plugins(self):
if self.plugins_dir is not None:
plugins_count = 0
if self.plugins.get("enabled", False):
root = self.plugins["root"]
include = self.plugins["include"]
exclude = self.plugins["exclude"]
for path in Path(self.plugins_dir).rglob("*.py"):
file_path = os.path.splitext(str(path))[0]
import_path = []
count = 0
while file_path:
file_path, tail = os.path.split(file_path)
import_path.insert(0, tail)
if include is None:
for path in sorted(Path(root).rglob("*.py")):
module_path = os.path.splitext(str(path))[0].replace("/", ".")
module = import_module(module_path)
import_path = ".".join(import_path)
module = import_module(import_path)
for name in vars(module).keys():
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
for name in dir(module):
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
if isinstance(handler, Handler) and isinstance(group, int):
self.add_handler(handler, group)
if isinstance(handler, Handler) and isinstance(group, int):
self.add_handler(handler, group)
log.info('[LOAD] {}("{}") in group {} from "{}"'.format(
type(handler).__name__, name, group, module_path))
log.info('{}("{}") from "{}" loaded in group {}'.format(
type(handler).__name__, name, import_path, group))
plugins_count += 1
except Exception:
pass
if plugins_count > 0:
log.warning('Successfully loaded {} plugin{} from "{}"'.format(
plugins_count,
"s" if plugins_count > 1 else "",
self.plugins_dir
))
count += 1
except Exception:
pass
else:
log.warning('No plugin loaded: "{}" doesn\'t contain any valid plugin'.format(self.plugins_dir))
for path, handlers in include:
module_path = root + "." + path
warn_non_existent_functions = True
try:
module = import_module(module_path)
except ModuleNotFoundError:
log.warning('[LOAD] Ignoring non-existent module "{}"'.format(module_path))
continue
if "__path__" in dir(module):
log.warning('[LOAD] Ignoring namespace "{}"'.format(module_path))
continue
if handlers is None:
handlers = vars(module).keys()
warn_non_existent_functions = False
for name in handlers:
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
if isinstance(handler, Handler) and isinstance(group, int):
self.add_handler(handler, group)
log.info('[LOAD] {}("{}") in group {} from "{}"'.format(
type(handler).__name__, name, group, module_path))
count += 1
except Exception:
if warn_non_existent_functions:
log.warning('[LOAD] Ignoring non-existent function "{}" from "{}"'.format(
name, module_path))
if exclude is not None:
for path, handlers in exclude:
module_path = root + "." + path
warn_non_existent_functions = True
try:
module = import_module(module_path)
except ModuleNotFoundError:
log.warning('[UNLOAD] Ignoring non-existent module "{}"'.format(module_path))
continue
if "__path__" in dir(module):
log.warning('[UNLOAD] Ignoring namespace "{}"'.format(module_path))
continue
if handlers is None:
handlers = vars(module).keys()
warn_non_existent_functions = False
for name in handlers:
# noinspection PyBroadException
try:
handler, group = getattr(module, name)
if isinstance(handler, Handler) and isinstance(group, int):
self.remove_handler(handler, group)
log.info('[UNLOAD] {}("{}") from group {} in "{}"'.format(
type(handler).__name__, name, group, module_path))
count -= 1
except Exception:
if warn_non_existent_functions:
log.warning('[UNLOAD] Ignoring non-existent function "{}" from "{}"'.format(
name, module_path))
if count > 0:
log.warning('Successfully loaded {} plugin{} from "{}"'.format(count, "s" if count > 1 else "", root))
else:
log.warning('No plugin loaded from "{}"'.format(root))
def save_session(self):
auth_key = base64.b64encode(self.auth_key).decode()
@ -1292,6 +1465,8 @@ class Client(Methods, BaseClient):
if progress:
progress(self, min(file_part * part_size, file_size), file_size, *progress_args)
except Client.StopTransmission:
raise
except Exception as e:
log.error(e, exc_info=True)
else:
@ -1493,7 +1668,8 @@ class Client(Methods, BaseClient):
except Exception as e:
raise e
except Exception as e:
log.error(e, exc_info=True)
if not isinstance(e, Client.StopTransmission):
log.error(e, exc_info=True)
try:
os.remove(file_name)

View File

@ -128,35 +128,37 @@ class Dispatcher:
parser = self.update_parsers.get(type(update), None)
if parser is None:
continue
parsed_update, handler_type = parser(update, users, chats)
parsed_update, handler_type = (
parser(update, users, chats)
if parser is not None
else (None, type(None))
)
for group in self.groups.values():
try:
for handler in group:
args = None
for handler in group:
args = None
if isinstance(handler, RawUpdateHandler):
args = (update, users, chats)
elif isinstance(handler, handler_type):
if handler.check(parsed_update):
args = (parsed_update,)
if isinstance(handler, handler_type):
if handler.check(parsed_update):
args = (parsed_update,)
elif isinstance(handler, RawUpdateHandler):
args = (update, users, chats)
if args is None:
continue
if args is None:
continue
try:
handler.callback(self.client, *args)
except StopIteration:
raise
except Exception as e:
log.error(e, exc_info=True)
try:
handler.callback(self.client, *args)
except pyrogram.StopPropagation:
raise
except pyrogram.ContinuePropagation:
continue
except Exception as e:
log.error(e, exc_info=True)
break
except StopIteration:
break
except pyrogram.StopPropagation:
pass
except Exception as e:
log.error(e, exc_info=True)

View File

@ -23,12 +23,13 @@ from threading import Lock
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:
class StopTransmission(StopIteration):
pass
APP_VERSION = "Pyrogram \U0001f525 {}".format(__version__)
DEVICE_MODEL = "{} {}".format(
@ -90,6 +91,8 @@ class BaseClient:
self.is_started = None
self.is_idle = None
self.takeout_id = None
self.updates_queue = Queue()
self.updates_workers_list = []
self.download_queue = Queue()
@ -97,33 +100,32 @@ class BaseClient:
self.disconnect_handler = None
def send(self, data: Object, retries: int = Session.MAX_RETRIES, timeout: float = Session.WAIT_TIMEOUT):
def send(self, *args, **kwargs):
pass
def resolve_peer(self, peer_id: int or str):
def resolve_peer(self, *args, **kwargs):
pass
def fetch_peers(self, entities):
def fetch_peers(self, *args, **kwargs):
pass
def add_handler(self, handler, group: int = 0) -> tuple:
def add_handler(self, *args, **kwargs):
pass
def save_file(
self,
path: str,
file_id: int = None,
file_part: int = 0,
progress: callable = None,
progress_args: tuple = ()
):
def save_file(self, *args, **kwargs):
pass
def get_messages(
self,
chat_id: int or str,
message_ids: int or list = None,
reply_to_message_ids: int or list = None,
replies: int = 1
):
def get_messages(self, *args, **kwargs):
pass
def get_history(self, *args, **kwargs):
pass
def get_dialogs(self, *args, **kwargs):
pass
def get_chat_members(self, *args, **kwargs):
pass
def get_chat_members_count(self, *args, **kwargs):
pass

View File

@ -61,6 +61,9 @@ class Filters:
create = create
me = create("Me", lambda _, m: bool(m.from_user and m.from_user.is_self))
"""Filter messages coming from you yourself"""
bot = create("Bot", lambda _, m: bool(m.from_user and m.from_user.is_bot))
"""Filter messages coming from bots"""
@ -97,12 +100,18 @@ class Filters:
sticker = create("Sticker", lambda _, m: bool(m.sticker))
"""Filter messages that contain :obj:`Sticker <pyrogram.Sticker>` objects."""
animation = create("GIF", lambda _, m: bool(m.animation))
animation = create("Animation", lambda _, m: bool(m.animation))
"""Filter messages that contain :obj:`Animation <pyrogram.Animation>` objects."""
game = create("Game", lambda _, m: bool(m.game))
"""Filter messages that contain :obj:`Game <pyrogram.Game>` objects."""
video = create("Video", lambda _, m: bool(m.video))
"""Filter messages that contain :obj:`Video <pyrogram.Video>` objects."""
media_group = create("MediaGroup", lambda _, m: bool(m.media_group_id))
"""Filter messages containing photos or videos being part of an album."""
voice = create("Voice", lambda _, m: bool(m.voice))
"""Filter messages that contain :obj:`Voice <pyrogram.Voice>` note objects."""
@ -166,6 +175,9 @@ class Filters:
pinned_message = create("PinnedMessage", lambda _, m: bool(m.pinned_message))
"""Filter service messages for pinned messages."""
game_high_score = create("GameHighScore", lambda _, m: bool(m.game_high_score))
"""Filter service messages for game high scores."""
reply_keyboard = create("ReplyKeyboard", lambda _, m: isinstance(m.reply_markup, ReplyKeyboardMarkup))
"""Filter messages containing reply keyboard markups"""
@ -190,7 +202,8 @@ class Filters:
- channel_chat_created
- migrate_to_chat_id
- migrate_from_chat_id
- pinned_message"""
- pinned_message
- game_score"""
media = create("Media", lambda _, m: bool(m.media))
"""Filter media messages. A media message contains any of the following fields set
@ -205,7 +218,8 @@ class Filters:
- video_note
- contact
- location
- venue"""
- venue
- poll"""
@staticmethod
def command(command: str or list,
@ -276,7 +290,7 @@ class Filters:
"""
def f(_, m):
m.matches = [i for i in _.p.finditer(m.text or "")]
m.matches = [i for i in _.p.finditer(m.text or m.caption or "")]
return bool(m.matches)
return create("Regex", f, p=re.compile(pattern, flags))

View File

@ -45,10 +45,3 @@ class CallbackQueryHandler(Handler):
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, callback_query):
return (
self.filters(callback_query)
if callable(self.filters)
else True
)

View File

@ -48,8 +48,4 @@ class DeletedMessagesHandler(Handler):
super().__init__(callback, filters)
def check(self, messages):
return (
self.filters(messages.messages[0])
if callable(self.filters)
else True
)
return super().check(messages.messages[0])

View File

@ -21,3 +21,10 @@ class Handler:
def __init__(self, callback: callable, filters=None):
self.callback = callback
self.filters = filters
def check(self, update):
return (
self.filters(update)
if callable(self.filters)
else True
)

View File

@ -46,10 +46,3 @@ class MessageHandler(Handler):
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, message):
return (
self.filters(message)
if callable(self.filters)
else True
)

View File

@ -45,10 +45,3 @@ class UserStatusHandler(Handler):
def __init__(self, callback: callable, filters=None):
super().__init__(callback, filters)
def check(self, user_status):
return (
self.filters(user_status)
if callable(self.filters)
else True
)

View File

@ -17,15 +17,21 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .answer_callback_query import AnswerCallbackQuery
from .get_game_high_scores import GetGameHighScores
from .get_inline_bot_results import GetInlineBotResults
from .request_callback_answer import RequestCallbackAnswer
from .send_game import SendGame
from .send_inline_bot_result import SendInlineBotResult
from .set_game_score import SetGameScore
class Bots(
AnswerCallbackQuery,
GetInlineBotResults,
RequestCallbackAnswer,
SendInlineBotResult
SendInlineBotResult,
SendGame,
SetGameScore,
GetGameHighScores
):
pass

View File

@ -0,0 +1,66 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 typing import Union
import pyrogram
from pyrogram.api import functions
from pyrogram.client.ext import BaseClient
class GetGameHighScores(BaseClient):
def get_game_high_scores(self,
user_id: Union[int, str],
chat_id: Union[int, str],
message_id: int = None):
"""Use this method to get data for high score tables.
Args:
user_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
chat_id (``int`` | ``str``, *optional*):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
Required if inline_message_id is not specified.
message_id (``int``, *optional*):
Identifier of the sent message.
Required if inline_message_id is not specified.
Returns:
On success, a :obj:`GameHighScores <pyrogram.GameHighScores>` object is returned.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
# TODO: inline_message_id
return pyrogram.GameHighScores._parse(
self,
self.send(
functions.messages.GetGameHighScores(
peer=self.resolve_peer(chat_id),
id=message_id,
user_id=self.resolve_peer(user_id)
)
)
)

View File

@ -0,0 +1,87 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 typing import Union
import pyrogram
from pyrogram.api import functions, types
from pyrogram.client.ext import BaseClient
class SendGame(BaseClient):
def send_game(self,
chat_id: Union[int, str],
game_short_name: str,
disable_notification: bool = None,
reply_to_message_id: int = None,
reply_markup: Union["pyrogram.InlineKeyboardMarkup",
"pyrogram.ReplyKeyboardMarkup",
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None) -> "pyrogram.Message":
"""Use this method to send a game.
Args:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
game_short_name (``str``):
Short name of the game, serves as the unique identifier for the game. Set up your games via Botfather.
disable_notification (``bool``, *optional*):
Sends the message silently.
Users will receive a notification with no sound.
reply_to_message_id (``int``, *optional*):
If the message is a reply, ID of the original message.
reply_markup (:obj:`InlineKeyboardMarkup`, *optional*):
An object for an inline keyboard. If empty, one Play game_title button will be shown automatically.
If not empty, the first button must launch the game.
Returns:
On success, the sent :obj:`Message` is returned.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=types.InputMediaGame(
id=types.InputGameShortName(
bot_id=types.InputUserSelf(),
short_name=game_short_name
),
),
message="",
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None
)
)
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)

View File

@ -0,0 +1,90 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 typing import Union
import pyrogram
from pyrogram.api import functions, types
from pyrogram.client.ext import BaseClient
class SetGameScore(BaseClient):
def set_game_score(self,
user_id: Union[int, str],
score: int,
force: bool = None,
disable_edit_message: bool = None,
chat_id: Union[int, str] = None,
message_id: int = None):
# inline_message_id: str = None): TODO Add inline_message_id
"""Use this method to set the score of the specified user in a game.
Args:
user_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
score (``int``):
New score, must be non-negative.
force (``bool``, *optional*):
Pass True, if the high score is allowed to decrease.
This can be useful when fixing mistakes or banning cheaters.
disable_edit_message (``bool``, *optional*):
Pass True, if the game message should not be automatically edited to include the current scoreboard.
chat_id (``int`` | ``str``, *optional*):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
Required if inline_message_id is not specified.
message_id (``int``, *optional*):
Identifier of the sent message.
Required if inline_message_id is not specified.
Returns:
On success, if the message was sent by the bot, returns the edited :obj:`Message <pyrogram.Message>`,
otherwise returns True.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
:class:`BotScoreNotModified` if the new score is not greater than the user's current score in the chat and force is False.
"""
r = self.send(
functions.messages.SetGameScore(
peer=self.resolve_peer(chat_id),
score=score,
id=message_id,
user_id=self.resolve_peer(user_id),
force=force or None,
edit_message=not disable_edit_message or None
)
)
for i in r.updates:
if isinstance(i, (types.UpdateEditMessage, types.UpdateEditChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
return True

View File

@ -24,6 +24,8 @@ from .get_chat_members import GetChatMembers
from .get_chat_members_count import GetChatMembersCount
from .get_chat_preview import GetChatPreview
from .get_dialogs import GetDialogs
from .iter_chat_members import IterChatMembers
from .iter_dialogs import IterDialogs
from .join_chat import JoinChat
from .kick_chat_member import KickChatMember
from .leave_chat import LeaveChat
@ -56,6 +58,8 @@ class Chats(
UnpinChatMessage,
GetDialogs,
GetChatMembersCount,
GetChatPreview
GetChatPreview,
IterDialogs,
IterChatMembers
):
pass

View File

@ -67,6 +67,8 @@ class GetChatMember(BaseClient):
)
)
return pyrogram.ChatMember._parse(self, r.participant, r.users[0])
users = {i.id: i for i in r.users}
return pyrogram.ChatMember._parse(self, r.participant, users)
else:
raise ValueError("The chat_id \"{}\" belongs to a user".format(chat_id))

View File

@ -39,10 +39,12 @@ class GetChatMembers(BaseClient):
limit: int = 200,
query: str = "",
filter: str = Filters.ALL) -> "pyrogram.ChatMembers":
"""Use this method to get the members list of a chat.
"""Use this method to get a chunk of the members list of a chat.
You can get up to 200 chat members at once.
A chat can be either a basic group, a supergroup or a channel.
You must be admin to retrieve the members list of a channel (also known as "subscribers").
For a more convenient way of getting chat members see :meth:`iter_chat_members`.
Args:
chat_id (``int`` | ``str``):

View File

@ -16,19 +16,26 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
import pyrogram
from pyrogram.api import functions, types
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetDialogs(BaseClient):
def get_dialogs(self,
offset_date: int = 0,
limit: int = 100,
pinned_only: bool = False) -> "pyrogram.Dialogs":
"""Use this method to get the user's dialogs
"""Use this method to get a chunk of the user's dialogs
You can get up to 100 dialogs at once.
For a more convenient way of getting a user's dialogs see :meth:`iter_dialogs`.
Args:
offset_date (``int``):
@ -50,18 +57,25 @@ class GetDialogs(BaseClient):
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
if pinned_only:
r = self.send(functions.messages.GetPinnedDialogs())
else:
r = self.send(
functions.messages.GetDialogs(
offset_date=offset_date,
offset_id=0,
offset_peer=types.InputPeerEmpty(),
limit=limit,
hash=0,
exclude_pinned=True
)
)
while True:
try:
if pinned_only:
r = self.send(functions.messages.GetPinnedDialogs())
else:
r = self.send(
functions.messages.GetDialogs(
offset_date=offset_date,
offset_id=0,
offset_peer=types.InputPeerEmpty(),
limit=limit,
hash=0,
exclude_pinned=True
)
)
except FloodWait as e:
log.warning("Sleeping {}s".format(e.x))
time.sleep(e.x)
else:
break
return pyrogram.Dialogs._parse(self, r)

View File

@ -0,0 +1,125 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 string import ascii_lowercase
from typing import Union, Generator
import pyrogram
from ...ext import BaseClient
class Filters:
ALL = "all"
KICKED = "kicked"
RESTRICTED = "restricted"
BOTS = "bots"
RECENT = "recent"
ADMINISTRATORS = "administrators"
QUERIES = [""] + [str(i) for i in range(10)] + list(ascii_lowercase)
QUERYABLE_FILTERS = (Filters.ALL, Filters.KICKED, Filters.RESTRICTED)
class IterChatMembers(BaseClient):
def iter_chat_members(self,
chat_id: Union[int, str],
limit: int = 0,
query: str = "",
filter: str = Filters.ALL) -> Generator["pyrogram.ChatMember", None, None]:
"""Use this method to iterate through the members of a chat sequentially.
This convenience method does the same as repeatedly calling :meth:`get_chat_members` in a loop, thus saving you
from the hassle of setting up boilerplate code. It is useful for getting the whole members list of a chat with
a single call.
Args:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
limit (``int``, *optional*):
Limits the number of members to be retrieved.
By default, no limit is applied and all members are returned.
query (``str``, *optional*):
Query string to filter members based on their display names and usernames.
Defaults to "" (empty string).
filter (``str``, *optional*):
Filter used to select the kind of members you want to retrieve. Only applicable for supergroups
and channels. It can be any of the followings:
*"all"* - all kind of members,
*"kicked"* - kicked (banned) members only,
*"restricted"* - restricted members only,
*"bots"* - bots only,
*"recent"* - recent members only,
*"administrators"* - chat administrators only.
Defaults to *"all"*.
Returns:
A generator yielding :obj:`ChatMember <pyrogram.ChatMember>` objects.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
current = 0
yielded = set()
queries = [query] if query else QUERIES
total = limit or (1 << 31) - 1
limit = min(200, total)
filter = (
Filters.RECENT
if self.get_chat_members_count(chat_id) <= 10000 and filter == Filters.ALL
else filter
)
if filter not in QUERYABLE_FILTERS:
queries = [""]
for q in queries:
offset = 0
while True:
chat_members = self.get_chat_members(
chat_id=chat_id,
offset=offset,
limit=limit,
query=q,
filter=filter
).chat_members
if not chat_members:
break
offset += len(chat_members)
for chat_member in chat_members:
user_id = chat_member.user.id
if user_id in yielded:
continue
yield chat_member
yielded.add(chat_member.user.id)
current += 1
if current >= total:
return

View File

@ -0,0 +1,82 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 typing import Generator
import pyrogram
from ...ext import BaseClient
class IterDialogs(BaseClient):
def iter_dialogs(self,
offset_date: int = 0,
limit: int = 0) -> Generator["pyrogram.Dialog", None, None]:
"""Use this method to iterate through a user's dialogs sequentially.
This convenience method does the same as repeatedly calling :meth:`get_dialogs` in a loop, thus saving you from
the hassle of setting up boilerplate code. It is useful for getting the whole dialogs list with a single call.
Args:
offset_date (``int``):
The offset date in Unix time taken from the top message of a :obj:`Dialog`.
Defaults to 0 (most recent dialog).
limit (``str``, *optional*):
Limits the number of dialogs to be retrieved.
By default, no limit is applied and all dialogs are returned.
Returns:
A generator yielding :obj:`Dialog <pyrogram.Dialog>` objects.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
current = 0
total = limit or (1 << 31) - 1
limit = min(100, total)
pinned_dialogs = self.get_dialogs(
pinned_only=True
).dialogs
for dialog in pinned_dialogs:
yield dialog
current += 1
if current >= total:
return
while True:
dialogs = self.get_dialogs(
offset_date=offset_date,
limit=limit
).dialogs
if not dialogs:
return
offset_date = dialogs[-1].top_message.date
for dialog in dialogs:
yield dialog
current += 1
if current >= total:
return

View File

@ -27,7 +27,7 @@ class KickChatMember(BaseClient):
def kick_chat_member(self,
chat_id: Union[int, str],
user_id: Union[int, str],
until_date: int = 0) -> "pyrogram.Message":
until_date: int = 0) -> Union["pyrogram.Message", bool]:
"""Use this method to kick a user from a group, a supergroup or a channel.
In the case of supergroups and channels, the user will not be able to return to the group on their own using
invite links, etc., unless unbanned first. You must be an administrator in the chat for this to work and must
@ -52,7 +52,7 @@ class KickChatMember(BaseClient):
considered to be banned forever. Defaults to 0 (ban forever).
Returns:
True on success.
On success, either True or a service :obj:`Message <pyrogram.Message>` will be returned (when applicable).
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -93,3 +93,5 @@ class KickChatMember(BaseClient):
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
else:
return True

View File

@ -16,6 +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 .close_poll import ClosePoll
from .delete_messages import DeleteMessages
from .download_media import DownloadMedia
from .edit_message_caption import EditMessageCaption
@ -25,6 +26,7 @@ from .edit_message_text import EditMessageText
from .forward_messages import ForwardMessages
from .get_history import GetHistory
from .get_messages import GetMessages
from .iter_history import IterHistory
from .retract_vote import RetractVote
from .send_animation import SendAnimation
from .send_audio import SendAudio
@ -69,7 +71,9 @@ class Messages(
SendVoice,
SendPoll,
VotePoll,
ClosePoll,
RetractVote,
DownloadMedia
DownloadMedia,
IterHistory
):
pass

View File

@ -0,0 +1,65 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 typing import Union
from pyrogram.api import functions, types
from pyrogram.client.ext import BaseClient
class ClosePoll(BaseClient):
def close_poll(self,
chat_id: Union[int, str],
message_id: id) -> bool:
"""Use this method to close (stop) a poll.
Closed polls can't be reopened and nobody will be able to vote in it anymore.
Args:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
message_id (``int``):
Unique poll message identifier inside this chat.
Returns:
On success, True is returned.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
poll = self.get_messages(chat_id, message_id).poll
self.send(
functions.messages.EditMessage(
peer=self.resolve_peer(chat_id),
id=message_id,
media=types.InputMediaPoll(
poll=types.Poll(
id=poll.id,
closed=True,
question="",
answers=[]
)
)
)
)
return True

View File

@ -72,6 +72,7 @@ class DownloadMedia(BaseClient):
Returns:
On success, the absolute path of the downloaded file as string is returned, None otherwise.
In case the download is deliberately stopped with :meth:`stop_transmission`, None is returned as well.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.

View File

@ -16,12 +16,17 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
from typing import Union
import pyrogram
from pyrogram.api import functions
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetHistory(BaseClient):
def get_history(self,
@ -30,10 +35,11 @@ class GetHistory(BaseClient):
offset: int = 0,
offset_id: int = 0,
offset_date: int = 0,
reversed: bool = False):
"""Use this method to retrieve the history of a chat.
reverse: bool = False):
"""Use this method to retrieve a chunk of the history of a chat.
You can get up to 100 messages at once.
For a more convenient way of getting a chat history see :meth:`iter_history`.
Args:
chat_id (``int`` | ``str``):
@ -55,7 +61,7 @@ class GetHistory(BaseClient):
offset_date (``int``, *optional*):
Pass a date in Unix time as offset to retrieve only older messages starting from that date.
reversed (``bool``, *optional*):
reverse (``bool``, *optional*):
Pass True to retrieve the messages in reversed order (from older to most recent).
Returns:
@ -65,23 +71,30 @@ class GetHistory(BaseClient):
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
messages = pyrogram.Messages._parse(
self,
self.send(
functions.messages.GetHistory(
peer=self.resolve_peer(chat_id),
offset_id=offset_id,
offset_date=offset_date,
add_offset=offset - (limit if reversed else 0),
limit=limit,
max_id=0,
min_id=0,
hash=0
while True:
try:
messages = pyrogram.Messages._parse(
self,
self.send(
functions.messages.GetHistory(
peer=self.resolve_peer(chat_id),
offset_id=offset_id,
offset_date=offset_date,
add_offset=offset * (-1 if reverse else 1) - (limit if reverse else 0),
limit=limit,
max_id=0,
min_id=0,
hash=0
)
)
)
)
)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)
else:
break
if reversed:
if reverse:
messages.messages.reverse()
return messages

View File

@ -16,19 +16,24 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import logging
import time
from typing import Union, Iterable
import pyrogram
from pyrogram.api import functions, types
from pyrogram.api.errors import FloodWait
from ...ext import BaseClient
log = logging.getLogger(__name__)
class GetMessages(BaseClient):
def get_messages(self,
chat_id: Union[int, str],
message_ids: Union[int, Iterable[int]] = None,
reply_to_message_ids: Union[int, Iterable[int]] = None,
replies: int = 1) -> "pyrogram.Messages":
replies: int = 1) -> Union["pyrogram.Message", "pyrogram.Messages"]:
"""Use this method to get one or more messages that belong to a specific chat.
You can retrieve up to 200 messages at once.
@ -78,6 +83,15 @@ class GetMessages(BaseClient):
else:
rpc = functions.messages.GetMessages(id=ids)
messages = pyrogram.Messages._parse(self, self.send(rpc), replies)
while True:
try:
r = self.send(rpc)
except FloodWait as e:
log.warning("Sleeping for {}s".format(e.x))
time.sleep(e.x)
else:
break
messages = pyrogram.Messages._parse(self, r, replies=replies)
return messages if is_iterable else messages.messages[0]

View File

@ -0,0 +1,93 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 typing import Union, Generator
import pyrogram
from ...ext import BaseClient
class IterHistory(BaseClient):
def iter_history(self,
chat_id: Union[int, str],
limit: int = 0,
offset: int = 0,
offset_id: int = 0,
offset_date: int = 0,
reverse: bool = False) -> Generator["pyrogram.Message", None, None]:
"""Use this method to iterate through a chat history sequentially.
This convenience method does the same as repeatedly calling :meth:`get_history` in a loop, thus saving you from
the hassle of setting up boilerplate code. It is useful for getting the whole chat history with a single call.
Args:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
For your personal cloud (Saved Messages) you can simply use "me" or "self".
For a contact that exists in your Telegram address book you can use his phone number (str).
limit (``int``, *optional*):
Limits the number of messages to be retrieved.
By default, no limit is applied and all messages are returned.
offset (``int``, *optional*):
Sequential number of the first message to be returned..
Negative values are also accepted and become useful in case you set offset_id or offset_date.
offset_id (``int``, *optional*):
Identifier of the first message to be returned.
offset_date (``int``, *optional*):
Pass a date in Unix time as offset to retrieve only older messages starting from that date.
reverse (``bool``, *optional*):
Pass True to retrieve the messages in reversed order (from older to most recent).
Returns:
A generator yielding :obj:`Message <pyrogram.Message>` objects.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
offset_id = offset_id or (1 if reverse else 0)
current = 0
total = limit or (1 << 31) - 1
limit = min(100, total)
while True:
messages = self.get_history(
chat_id=chat_id,
limit=limit,
offset=offset,
offset_id=offset_id,
offset_date=offset_date,
reverse=reverse
).messages
if not messages:
return
offset_id = messages[-1].message_id + (1 if reverse else 0)
for message in messages:
yield message
current += 1
if current >= total:
return

View File

@ -45,7 +45,7 @@ class SendAnimation(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send animation files (animation or H.264/MPEG-4 AVC video without sound).
Args:
@ -119,6 +119,7 @@ class SendAnimation(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -126,72 +127,75 @@ class SendAnimation(BaseClient):
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown
if os.path.exists(animation):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(animation, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map[".mp4"],
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeVideo(
supports_streaming=True,
duration=duration,
w=width,
h=height
),
types.DocumentAttributeFilename(os.path.basename(animation)),
types.DocumentAttributeAnimated()
]
)
elif animation.startswith("http"):
media = types.InputMediaDocumentExternal(
url=animation
)
else:
try:
decoded = utils.decode(animation)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 10:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
try:
if os.path.exists(animation):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(animation, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map[".mp4"],
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeVideo(
supports_streaming=True,
duration=duration,
w=width,
h=height
),
types.DocumentAttributeFilename(os.path.basename(animation)),
types.DocumentAttributeAnimated()
]
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
elif animation.startswith("http"):
media = types.InputMediaDocumentExternal(
url=animation
)
except FilePartMissing as e:
self.save_file(animation, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
decoded = utils.decode(animation)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 10:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)
except FilePartMissing as e:
self.save_file(animation, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -45,7 +45,7 @@ class SendAudio(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send audio files.
For sending voice messages, use the :obj:`send_voice()` method instead.
@ -121,6 +121,7 @@ class SendAudio(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -128,70 +129,73 @@ class SendAudio(BaseClient):
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown
if os.path.exists(audio):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(audio, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map.get("." + audio.split(".")[-1], "audio/mpeg"),
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeAudio(
duration=duration,
performer=performer,
title=title
),
types.DocumentAttributeFilename(os.path.basename(audio))
]
)
elif audio.startswith("http"):
media = types.InputMediaDocumentExternal(
url=audio
)
else:
try:
decoded = utils.decode(audio)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 9:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
try:
if os.path.exists(audio):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(audio, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map.get("." + audio.split(".")[-1], "audio/mpeg"),
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeAudio(
duration=duration,
performer=performer,
title=title
),
types.DocumentAttributeFilename(os.path.basename(audio))
]
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
elif audio.startswith("http"):
media = types.InputMediaDocumentExternal(
url=audio
)
except FilePartMissing as e:
self.save_file(audio, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
decoded = utils.decode(audio)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 9:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)
except FilePartMissing as e:
self.save_file(audio, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -42,7 +42,7 @@ class SendDocument(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send general files.
Args:
@ -107,6 +107,7 @@ class SendDocument(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -114,65 +115,68 @@ class SendDocument(BaseClient):
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown
if os.path.exists(document):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(document, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map.get("." + document.split(".")[-1], "text/plain"),
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeFilename(os.path.basename(document))
]
)
elif document.startswith("http"):
media = types.InputMediaDocumentExternal(
url=document
)
else:
try:
decoded = utils.decode(document)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] not in (5, 10):
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
try:
if os.path.exists(document):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(document, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map.get("." + document.split(".")[-1], "text/plain"),
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeFilename(os.path.basename(document))
]
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
elif document.startswith("http"):
media = types.InputMediaDocumentExternal(
url=document
)
except FilePartMissing as e:
self.save_file(document, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
decoded = utils.decode(document)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] not in (5, 10):
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)
except FilePartMissing as e:
self.save_file(document, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -88,10 +88,18 @@ class SendMessage(BaseClient):
)
if isinstance(r, types.UpdateShortSentMessage):
peer = self.resolve_peer(chat_id)
peer_id = (
peer.user_id
if isinstance(peer, types.InputPeerUser)
else -peer.chat_id
)
return pyrogram.Message(
message_id=r.id,
chat=pyrogram.Chat(
id=list(self.resolve_peer(chat_id).__dict__.values())[0],
id=peer_id,
type="private",
client=self
),

View File

@ -41,7 +41,7 @@ class SendPhoto(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send photos.
Args:
@ -105,6 +105,7 @@ class SendPhoto(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -112,62 +113,65 @@ class SendPhoto(BaseClient):
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown
if os.path.exists(photo):
file = self.save_file(photo, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedPhoto(
file=file,
ttl_seconds=ttl_seconds
)
elif photo.startswith("http"):
media = types.InputMediaPhotoExternal(
url=photo,
ttl_seconds=ttl_seconds
)
else:
try:
decoded = utils.decode(photo)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 2:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaPhoto(
id=types.InputPhoto(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
),
try:
if os.path.exists(photo):
file = self.save_file(photo, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedPhoto(
file=file,
ttl_seconds=ttl_seconds
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
elif photo.startswith("http"):
media = types.InputMediaPhotoExternal(
url=photo,
ttl_seconds=ttl_seconds
)
except FilePartMissing as e:
self.save_file(photo, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
decoded = utils.decode(photo)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 2:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaPhoto(
id=types.InputPhoto(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
),
ttl_seconds=ttl_seconds
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)
except FilePartMissing as e:
self.save_file(photo, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -38,7 +38,7 @@ class SendSticker(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send .webp stickers.
Args:
@ -89,69 +89,73 @@ class SendSticker(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
file = None
if os.path.exists(sticker):
file = self.save_file(sticker, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type="image/webp",
file=file,
attributes=[
types.DocumentAttributeFilename(os.path.basename(sticker))
]
)
elif sticker.startswith("http"):
media = types.InputMediaDocumentExternal(
url=sticker
)
else:
try:
decoded = utils.decode(sticker)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 8:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
try:
if os.path.exists(sticker):
file = self.save_file(sticker, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type="image/webp",
file=file,
attributes=[
types.DocumentAttributeFilename(os.path.basename(sticker))
]
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
message=""
)
elif sticker.startswith("http"):
media = types.InputMediaDocumentExternal(
url=sticker
)
except FilePartMissing as e:
self.save_file(sticker, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
decoded = utils.decode(sticker)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 8:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
message=""
)
)
except FilePartMissing as e:
self.save_file(sticker, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -46,7 +46,7 @@ class SendVideo(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send video files.
Args:
@ -123,6 +123,7 @@ class SendVideo(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -130,71 +131,74 @@ class SendVideo(BaseClient):
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown
if os.path.exists(video):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(video, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map[".mp4"],
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeVideo(
supports_streaming=supports_streaming or None,
duration=duration,
w=width,
h=height
),
types.DocumentAttributeFilename(os.path.basename(video))
]
)
elif video.startswith("http"):
media = types.InputMediaDocumentExternal(
url=video
)
else:
try:
decoded = utils.decode(video)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 4:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
try:
if os.path.exists(video):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(video, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map[".mp4"],
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeVideo(
supports_streaming=supports_streaming or None,
duration=duration,
w=width,
h=height
),
types.DocumentAttributeFilename(os.path.basename(video))
]
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
elif video.startswith("http"):
media = types.InputMediaDocumentExternal(
url=video
)
except FilePartMissing as e:
self.save_file(video, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
decoded = utils.decode(video)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 4:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)
except FilePartMissing as e:
self.save_file(video, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -42,7 +42,7 @@ class SendVideoNote(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send video messages.
Args:
@ -105,72 +105,76 @@ class SendVideoNote(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
"""
file = None
if os.path.exists(video_note):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(video_note, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map[".mp4"],
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeVideo(
round_message=True,
duration=duration,
w=length,
h=length
)
]
)
else:
try:
decoded = utils.decode(video_note)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 13:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
message=""
)
)
except FilePartMissing as e:
self.save_file(video_note, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
if os.path.exists(video_note):
thumb = None if thumb is None else self.save_file(thumb)
file = self.save_file(video_note, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map[".mp4"],
file=file,
thumb=thumb,
attributes=[
types.DocumentAttributeVideo(
round_message=True,
duration=duration,
w=length,
h=length
)
]
)
else:
try:
decoded = utils.decode(video_note)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 13:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
message=""
)
)
except FilePartMissing as e:
self.save_file(video_note, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -42,7 +42,7 @@ class SendVoice(BaseClient):
"pyrogram.ReplyKeyboardRemove",
"pyrogram.ForceReply"] = None,
progress: callable = None,
progress_args: tuple = ()) -> "pyrogram.Message":
progress_args: tuple = ()) -> Union["pyrogram.Message", None]:
"""Use this method to send audio files.
Args:
@ -104,6 +104,7 @@ class SendVoice(BaseClient):
Returns:
On success, the sent :obj:`Message <pyrogram.Message>` is returned.
In case the upload is deliberately stopped with :meth:`stop_transmission`, None is returned instead.
Raises:
:class:`Error <pyrogram.Error>` in case of a Telegram RPC error.
@ -111,66 +112,69 @@ class SendVoice(BaseClient):
file = None
style = self.html if parse_mode.lower() == "html" else self.markdown
if os.path.exists(voice):
file = self.save_file(voice, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map.get("." + voice.split(".")[-1], "audio/mpeg"),
file=file,
attributes=[
types.DocumentAttributeAudio(
voice=True,
duration=duration
)
]
)
elif voice.startswith("http"):
media = types.InputMediaDocumentExternal(
url=voice
)
else:
try:
decoded = utils.decode(voice)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 3:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)
except FilePartMissing as e:
self.save_file(voice, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
try:
if os.path.exists(voice):
file = self.save_file(voice, progress=progress, progress_args=progress_args)
media = types.InputMediaUploadedDocument(
mime_type=mimetypes.types_map.get("." + voice.split(".")[-1], "audio/mpeg"),
file=file,
attributes=[
types.DocumentAttributeAudio(
voice=True,
duration=duration
)
]
)
elif voice.startswith("http"):
media = types.InputMediaDocumentExternal(
url=voice
)
else:
try:
decoded = utils.decode(voice)
fmt = "<iiqqqqi" if len(decoded) > 24 else "<iiqq"
unpacked = struct.unpack(fmt, decoded)
except (AssertionError, binascii.Error, struct.error):
raise FileIdInvalid from None
else:
if unpacked[0] != 3:
media_type = BaseClient.MEDIA_TYPE_ID.get(unpacked[0], None)
if media_type:
raise FileIdInvalid("The file_id belongs to a {}".format(media_type))
else:
raise FileIdInvalid("Unknown media type: {}".format(unpacked[0]))
media = types.InputMediaDocument(
id=types.InputDocument(
id=unpacked[2],
access_hash=unpacked[3],
file_reference=b""
)
)
while True:
try:
r = self.send(
functions.messages.SendMedia(
peer=self.resolve_peer(chat_id),
media=media,
silent=disable_notification or None,
reply_to_msg_id=reply_to_message_id,
random_id=self.rnd_id(),
reply_markup=reply_markup.write() if reply_markup else None,
**style.parse(caption)
)
)
except FilePartMissing as e:
self.save_file(voice, file_id=file.id, file_part=e.x)
else:
for i in r.updates:
if isinstance(i, (types.UpdateNewMessage, types.UpdateNewChannelMessage)):
return pyrogram.Message._parse(
self, i.message,
{i.id: i for i in r.users},
{i.id: i for i in r.chats}
)
except BaseClient.StopTransmission:
return None

View File

@ -38,12 +38,12 @@ class HTML:
def __init__(self, peers_by_id):
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(str(message))
offset = 0
for match in self.HTML_RE.finditer(text):
for match in self.HTML_RE.finditer(message):
start = match.start() - offset
style, url, body = match.group(1, 3, 4)
@ -73,12 +73,12 @@ class HTML:
continue
entities.append(entity)
text = text.replace(match.group(), body)
message = message.replace(match.group(), body)
offset += len(style) * 2 + 5 + (len(url) + 8 if url else 0)
# TODO: OrderedDict to be removed in Python3.6
return OrderedDict([
("message", utils.remove_surrogates(text)),
("message", utils.remove_surrogates(message)),
("entities", entities)
])

View File

@ -56,7 +56,7 @@ class Markdown:
self.peers_by_id = peers_by_id
def parse(self, message: str):
message = utils.add_surrogates(message).strip()
message = utils.add_surrogates(str(message)).strip()
entities = []
offset = 0

View File

@ -18,11 +18,8 @@
from .bots import (
CallbackQuery, ForceReply, InlineKeyboardButton, InlineKeyboardMarkup,
KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
)
from .bots import (
ForceReply, InlineKeyboardButton, InlineKeyboardMarkup,
KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove
KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, CallbackGame,
GameHighScore, GameHighScores
)
from .input_media import (
InputMediaAudio, InputPhoneContact, InputMediaVideo, InputMediaPhoto,
@ -31,10 +28,10 @@ from .input_media import (
from .messages_and_media import (
Audio, Contact, Document, Animation, Location, Photo, PhotoSize,
Sticker, Venue, Video, VideoNote, Voice, UserProfilePhotos,
Message, Messages, MessageEntity, Poll, PollOption
Message, Messages, MessageEntity, Poll, PollOption, Game
)
from .update import StopPropagation, ContinuePropagation
from .user_and_chats import (
Chat, ChatMember, ChatMembers, ChatPhoto,
Dialog, Dialogs, User, UserStatus, ChatPreview
)
from .update import StopPropagation

View File

@ -16,8 +16,11 @@
# 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 .callback_game import CallbackGame
from .callback_query import CallbackQuery
from .force_reply import ForceReply
from .game_high_score import GameHighScore
from .game_high_scores import GameHighScores
from .inline_keyboard_button import InlineKeyboardButton
from .inline_keyboard_markup import InlineKeyboardMarkup
from .keyboard_button import KeyboardButton

View File

@ -0,0 +1,29 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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_type import PyrogramType
class CallbackGame(PyrogramType):
"""A placeholder, currently holds no information.
Use BotFather to set up your game.
"""
def __init__(self):
super().__init__(None)

View File

@ -40,15 +40,15 @@ class CallbackQuery(PyrogramType, Update):
Sender.
chat_instance (``str``, *optional*):
Global identifier, uniquely corresponding to the chat to which the message with the callback button was
sent. Useful for high scores in games.
message (:obj:`Message <pyrogram.Message>`, *optional*):
Message with the callback button that originated the query. Note that message content and message date will
not be available if the message is too old.
message (:obj:`Message <pyrogram.Message>`, *optional*):
Identifier of the message sent via the bot in inline mode, that originated the query.
inline_message_id (``str``):
Global identifier, uniquely corresponding to the chat to which the message with the callback button was
sent. Useful for high scores in games.
Identifier of the message sent via the bot in inline mode, that originated the query.
data (``bytes``, *optional*):
Data associated with the callback button. Be aware that a bad client can send arbitrary data in this field.
@ -72,9 +72,9 @@ class CallbackQuery(PyrogramType, Update):
self.id = id
self.from_user = from_user
self.chat_instance = chat_instance
self.message = message
self.inline_message_id = inline_message_id
self.chat_instance = chat_instance
self.data = data
self.game_short_name = game_short_name

View File

@ -0,0 +1,69 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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/>.
import pyrogram
from pyrogram.api import types
from pyrogram.client.types.pyrogram_type import PyrogramType
from pyrogram.client.types.user_and_chats import User
class GameHighScore(PyrogramType):
"""This object represents one row of the high scores table for a game.
Args:
user (:obj:`User`):
User.
score (``int``):
Score.
position (``position``, *optional*):
Position in high score table for the game.
"""
def __init__(self,
*,
client: "pyrogram.client.ext.BaseClient",
user: User,
score: int,
position: int = None):
super().__init__(client)
self.user = user
self.score = score
self.position = position
@staticmethod
def _parse(client, game_high_score: types.HighScore, users: dict) -> "GameHighScore":
users = {i.id: i for i in users}
return GameHighScore(
user=User._parse(client, users[game_high_score.user_id]),
score=game_high_score.score,
position=game_high_score.pos,
client=client
)
@staticmethod
def _parse_action(client, service: types.MessageService, users: dict):
return GameHighScore(
user=User._parse(client, users[service.from_id]),
score=service.action.score,
client=client
)

View File

@ -0,0 +1,56 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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 typing import List
import pyrogram
from pyrogram.api import types
from pyrogram.client.types.pyrogram_type import PyrogramType
from .game_high_score import GameHighScore
class GameHighScores(PyrogramType):
"""This object represents the high scores table for a game.
Args:
total_count (``int``):
Total number of scores the target game has.
game_high_scores (List of :obj:`GameHighScore <pyrogram.GameHighScore>`):
Game scores.
"""
def __init__(self,
*,
client: "pyrogram.client.ext.BaseClient",
total_count: int,
game_high_scores: List[GameHighScore]):
super().__init__(client)
self.total_count = total_count
self.game_high_scores = game_high_scores
@staticmethod
def _parse(client, game_high_scores: types.messages.HighScores) -> "GameHighScores":
return GameHighScores(
total_count=len(game_high_scores.scores),
game_high_scores=[
GameHighScore._parse(client, score, game_high_scores.users)
for score in game_high_scores.scores],
client=client
)

View File

@ -18,8 +18,9 @@
from pyrogram.api.types import (
KeyboardButtonUrl, KeyboardButtonCallback,
KeyboardButtonSwitchInline
KeyboardButtonSwitchInline, KeyboardButtonGame
)
from .callback_game import CallbackGame
from ..pyrogram_type import PyrogramType
@ -58,7 +59,8 @@ class InlineKeyboardButton(PyrogramType):
callback_data: bytes = None,
url: str = None,
switch_inline_query: str = None,
switch_inline_query_current_chat: str = None):
switch_inline_query_current_chat: str = None,
callback_game: CallbackGame = None):
super().__init__(None)
self.text = text
@ -66,7 +68,7 @@ class InlineKeyboardButton(PyrogramType):
self.callback_data = callback_data
self.switch_inline_query = switch_inline_query
self.switch_inline_query_current_chat = switch_inline_query_current_chat
# self.callback_game = callback_game
self.callback_game = callback_game
# self.pay = pay
@staticmethod
@ -95,6 +97,12 @@ class InlineKeyboardButton(PyrogramType):
switch_inline_query=o.query
)
if isinstance(o, KeyboardButtonGame):
return InlineKeyboardButton(
text=o.text,
callback_game=CallbackGame()
)
def write(self):
if self.callback_data:
return KeyboardButtonCallback(self.text, self.callback_data)
@ -107,3 +115,6 @@ class InlineKeyboardButton(PyrogramType):
if self.switch_inline_query_current_chat:
return KeyboardButtonSwitchInline(self.text, self.switch_inline_query_current_chat, same_peer=True)
if self.callback_game:
return KeyboardButtonGame(self.text)

View File

@ -16,7 +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 typing import List
from typing import List, Union
from pyrogram.api.types import KeyboardButtonRow
from pyrogram.api.types import ReplyKeyboardMarkup as RawReplyKeyboardMarkup
@ -50,7 +50,7 @@ class ReplyKeyboardMarkup(PyrogramType):
"""
def __init__(self,
keyboard: List[List[KeyboardButton]],
keyboard: List[List[Union[KeyboardButton, str]]],
resize_keyboard: bool = None,
one_time_keyboard: bool = None,
selective: bool = None):

View File

@ -20,7 +20,7 @@ from . import InputMedia
class InputMediaAudio(InputMedia):
"""This object represents a video to be sent inside an album.
"""This object represents an audio to be sent inside an album.
It is intended to be used with :obj:`send_media_group() <pyrogram.Client.send_media_group>`.
Args:

View File

@ -20,6 +20,7 @@ from .animation import Animation
from .audio import Audio
from .contact import Contact
from .document import Document
from .game import Game
from .location import Location
from .message import Message
from .message_entity import MessageEntity

View File

@ -0,0 +1,98 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-2019 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/>.
import pyrogram
from pyrogram.api import types
from .animation import Animation
from .photo import Photo
from ..pyrogram_type import PyrogramType
class Game(PyrogramType):
"""This object represents a game.
Use BotFather to create and edit games, their short names will act as unique identifiers.
Args:
id (``int``):
Unique identifier of the game.
title (``str``):
Title of the game.
short_name (``str``):
Unique short name of the game.
description (``str``):
Description of the game.
photo (:obj:`Photo <pyrogram.Photo>`):
Photo that will be displayed in the game message in chats.
animation (:obj:`Animation <pyrogram.Animation>`, *optional*):
Animation that will be displayed in the game message in chats.
Upload via BotFather.
"""
def __init__(self,
*,
client: "pyrogram.client.ext.BaseClient",
id: int,
title: str,
short_name: str,
description: str,
photo: Photo,
animation: Animation = None):
super().__init__(client)
self.id = id
self.title = title
self.short_name = short_name
self.description = description
self.photo = photo
self.animation = animation
@staticmethod
def _parse(client, message: types.Message) -> "Game":
game = message.media.game # type: types.Game
animation = None
if game.document:
attributes = {type(i): i for i in game.document.attributes}
file_name = getattr(
attributes.get(
types.DocumentAttributeFilename, None
), "file_name", None
)
animation = Animation._parse(
client,
game.document,
attributes.get(types.DocumentAttributeVideo, None),
file_name
)
return Game(
id=game.id,
title=game.title,
short_name=game.short_name,
description=game.description,
photo=Photo._parse(client, game.photo),
animation=animation,
client=client
)

View File

@ -121,6 +121,9 @@ class Message(PyrogramType, Update):
animation (:obj:`Animation <pyrogram.Animation>`, *optional*):
Message is an animation, information about the animation.
game (:obj:`Game <pyrogram.Game>`, *optional*):
Message is a game, information about the game.
video (:obj:`Video <pyrogram.Video>`, *optional*):
Message is a video, information about the video.
@ -199,6 +202,10 @@ class Message(PyrogramType, Update):
Note that the Message object in this field will not contain further reply_to_message fields even if it
is itself a reply.
game_high_score (:obj:`GameHighScore <pyrogram.GameHighScore>`, *optional*):
The game score for a user.
The reply_to_message field will contain the game Message.
views (``int``, *optional*):
Channel post views.
@ -255,6 +262,7 @@ class Message(PyrogramType, Update):
photo: "pyrogram.Photo" = None,
sticker: "pyrogram.Sticker" = None,
animation: "pyrogram.Animation" = None,
game: "pyrogram.Game" = None,
video: "pyrogram.Video" = None,
voice: "pyrogram.Voice" = None,
video_note: "pyrogram.VideoNote" = None,
@ -275,6 +283,7 @@ class Message(PyrogramType, Update):
migrate_to_chat_id: int = None,
migrate_from_chat_id: int = None,
pinned_message: "Message" = None,
game_high_score: int = None,
views: int = None,
via_bot: User = None,
outgoing: bool = None,
@ -311,6 +320,7 @@ class Message(PyrogramType, Update):
self.photo = photo
self.sticker = sticker
self.animation = animation
self.game = game
self.video = video
self.voice = voice
self.video_note = video_note
@ -331,6 +341,7 @@ class Message(PyrogramType, Update):
self.migrate_to_chat_id = migrate_to_chat_id
self.migrate_from_chat_id = migrate_from_chat_id
self.pinned_message = pinned_message
self.game_high_score = game_high_score
self.views = views
self.via_bot = via_bot
self.outgoing = outgoing
@ -407,6 +418,19 @@ class Message(PyrogramType, Update):
except MessageIdsEmpty:
pass
if isinstance(action, types.MessageActionGameScore):
parsed_message.game_high_score = pyrogram.GameHighScore._parse_action(client, message, users)
if message.reply_to_msg_id and replies:
try:
parsed_message.reply_to_message = client.get_messages(
parsed_message.chat.id,
reply_to_message_ids=message.id,
replies=0
)
except MessageIdsEmpty:
pass
return parsed_message
if isinstance(message, types.Message):
@ -435,6 +459,7 @@ class Message(PyrogramType, Update):
location = None
contact = None
venue = None
game = None
audio = None
voice = None
animation = None
@ -456,6 +481,8 @@ class Message(PyrogramType, Update):
contact = Contact._parse(client, media)
elif isinstance(media, types.MessageMediaVenue):
venue = pyrogram.Venue._parse(client, media)
elif isinstance(media, types.MessageMediaGame):
game = pyrogram.Game._parse(client, message)
elif isinstance(media, types.MessageMediaDocument):
doc = media.document
@ -543,6 +570,7 @@ class Message(PyrogramType, Update):
audio=audio,
voice=voice,
animation=animation,
game=game,
video=video,
video_note=video_note,
sticker=sticker,
@ -884,7 +912,7 @@ class Message(PyrogramType, Update):
else:
raise ValueError("The message doesn't contain any keyboard")
def download(self, file_name: str = "", block: bool = True, progress: callable = None, progress_args: tuple = None):
def download(self, file_name: str = "", block: bool = True, progress: callable = None, progress_args: tuple = ()):
"""Bound method *download* of :obj:`Message <pyrogram.Message>`.
Use as a shortcut for:

View File

@ -52,9 +52,38 @@ class Messages(PyrogramType, Update):
users = {i.id: i for i in messages.users}
chats = {i.id: i for i in messages.chats}
total_count = getattr(messages, "count", len(messages.messages))
if not messages.messages:
return Messages(
total_count=total_count,
messages=[],
client=client
)
parsed_messages = [Message._parse(client, message, users, chats, replies=0) for message in messages.messages]
if replies:
messages_with_replies = {i.id: getattr(i, "reply_to_msg_id", None) for i in messages.messages}
reply_message_ids = [i[0] for i in filter(lambda x: x[1] is not None, messages_with_replies.items())]
if reply_message_ids:
reply_messages = client.get_messages(
parsed_messages[0].chat.id,
reply_to_message_ids=reply_message_ids,
replies=0
).messages
for message in parsed_messages:
reply_id = messages_with_replies[message.message_id]
for reply in reply_messages:
if reply.message_id == reply_id:
message.reply_to_message = reply
return Messages(
total_count=getattr(messages, "count", len(messages.messages)),
messages=[Message._parse(client, message, users, chats, replies) for message in messages.messages],
total_count=total_count,
messages=parsed_messages,
client=client
)

View File

@ -21,6 +21,13 @@ class StopPropagation(StopIteration):
pass
class ContinuePropagation(StopIteration):
pass
class Update:
def stop_propagation(self):
raise StopPropagation
def continue_propagation(self):
raise ContinuePropagation

View File

@ -33,6 +33,19 @@ class ChatMember(PyrogramType):
The member's status in the chat. Can be "creator", "administrator", "member", "restricted",
"left" or "kicked".
date (``int``, *optional*):
Date when the user joined, unix time. Not available for creator.
invited_by (:obj:`User <pyrogram.User>`, *optional*):
Administrators and self member only. Information about the user who invited this member.
In case the user joined by himself this will be the same as "user".
promoted_by (:obj:`User <pyrogram.User>`, *optional*):
Administrators only. Information about the user who promoted this member as administrator.
restricted_by (:obj:`User <pyrogram.User>`, *optional*):
Restricted and kicked only. Information about the user who restricted or kicked this member.
until_date (``int``, *optional*):
Restricted and kicked only. Date when restrictions will be lifted for this user, unix time.
@ -86,6 +99,10 @@ class ChatMember(PyrogramType):
client: "pyrogram.client.ext.BaseClient",
user: "pyrogram.User",
status: str,
date: int = None,
invited_by: "pyrogram.User" = None,
promoted_by: "pyrogram.User" = None,
restricted_by: "pyrogram.User" = None,
until_date: int = None,
can_be_edited: bool = None,
can_change_info: bool = None,
@ -104,6 +121,10 @@ class ChatMember(PyrogramType):
self.user = user
self.status = status
self.date = date
self.invited_by = invited_by
self.promoted_by = promoted_by
self.restricted_by = restricted_by
self.until_date = until_date
self.can_be_edited = can_be_edited
self.can_change_info = can_change_info
@ -120,17 +141,18 @@ class ChatMember(PyrogramType):
self.can_add_web_page_previews = can_add_web_page_previews
@staticmethod
def _parse(client, member, user) -> "ChatMember":
user = pyrogram.User._parse(client, user)
def _parse(client, member, users) -> "ChatMember":
user = pyrogram.User._parse(client, users[member.user_id])
invited_by = pyrogram.User._parse(client, users[member.inviter_id]) if hasattr(member, "inviter_id") else None
if isinstance(member, (types.ChannelParticipant, types.ChannelParticipantSelf, types.ChatParticipant)):
return ChatMember(user=user, status="member", client=client)
return ChatMember(user=user, status="member", date=member.date, invited_by=invited_by, client=client)
if isinstance(member, (types.ChannelParticipantCreator, types.ChatParticipantCreator)):
return ChatMember(user=user, status="creator", client=client)
if isinstance(member, types.ChatParticipantAdmin):
return ChatMember(user=user, status="administrator", client=client)
return ChatMember(user=user, status="administrator", date=member.date, invited_by=invited_by, client=client)
if isinstance(member, types.ChannelParticipantAdmin):
rights = member.admin_rights
@ -138,6 +160,9 @@ class ChatMember(PyrogramType):
return ChatMember(
user=user,
status="administrator",
date=member.date,
invited_by=invited_by,
promoted_by=pyrogram.User._parse(client, users[member.promoted_by]),
can_be_edited=member.can_edit,
can_change_info=rights.change_info,
can_post_messages=rights.post_messages,
@ -155,7 +180,13 @@ class ChatMember(PyrogramType):
chat_member = ChatMember(
user=user,
status="kicked" if rights.view_messages else "restricted",
status=(
"kicked" if rights.view_messages
else "left" if member.left
else "restricted"
),
date=member.date,
restricted_by=pyrogram.User._parse(client, users[member.kicked_by]),
until_date=0 if rights.until_date == (1 << 31) - 1 else rights.until_date,
client=client
)

View File

@ -59,7 +59,7 @@ class ChatMembers(PyrogramType):
total_count = len(members)
for member in members:
chat_members.append(ChatMember._parse(client, member, users[member.user_id]))
chat_members.append(ChatMember._parse(client, member, users))
return ChatMembers(
total_count=total_count,

View File

@ -39,10 +39,10 @@ def get_version():
def get_readme():
# PyPI doesn"t like raw html
# 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())
return re.sub(r"\|header\|", "|logo|\n\n|description|\n\n|scheme| |tgcrypto|", readme)
return re.sub(r"\|header\|", "|logo|\n\n|description|\n\n|schema| |tgcrypto|", readme)
class Clean(Command):
@ -127,7 +127,7 @@ class Generate(Command):
docs_compiler.start()
if len(argv) > 1 and argv[1] in ["bdist_wheel", "install"]:
if len(argv) > 1 and argv[1] in ["bdist_wheel", "install", "develop"]:
error_compiler.start()
api_compiler.start()
docs_compiler.start()