mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-03 19:50:15 +03:00
Merge branch 'master' into asyncio
This commit is contained in:
commit
8b0580901a
|
@ -77,6 +77,37 @@ if (typeof prependPath !== 'undefined') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assumes haystack has no whitespace and both are lowercase.
|
||||||
|
function find(haystack, needle) {
|
||||||
|
if (needle.length == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var hi = 0;
|
||||||
|
var ni = 0;
|
||||||
|
while (true) {
|
||||||
|
while (needle[ni] < 'a' || needle[ni] > 'z') {
|
||||||
|
++ni;
|
||||||
|
if (ni == needle.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (haystack[hi] != needle[ni]) {
|
||||||
|
++hi;
|
||||||
|
if (hi == haystack.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++hi;
|
||||||
|
++ni;
|
||||||
|
if (ni == needle.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hi == haystack.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Given two input arrays "original" and "original urls" and a query,
|
// Given two input arrays "original" and "original urls" and a query,
|
||||||
// return a pair of arrays with matching "query" elements from "original".
|
// return a pair of arrays with matching "query" elements from "original".
|
||||||
//
|
//
|
||||||
|
@ -86,7 +117,7 @@ function getSearchArray(original, originalu, query) {
|
||||||
var destinationu = [];
|
var destinationu = [];
|
||||||
|
|
||||||
for (var i = 0; i < original.length; ++i) {
|
for (var i = 0; i < original.length; ++i) {
|
||||||
if (original[i].toLowerCase().indexOf(query) != -1) {
|
if (find(original[i].toLowerCase(), query)) {
|
||||||
destination.push(original[i]);
|
destination.push(original[i]);
|
||||||
destinationu.push(originalu[i]);
|
destinationu.push(originalu[i]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,16 @@
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
#
|
#
|
||||||
# import os
|
|
||||||
# import sys
|
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
|
||||||
root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
|
root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
|
||||||
|
|
||||||
|
tl_ref_url = 'https://lonamiwebs.github.io/Telethon'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
@ -36,7 +37,10 @@ root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = ['sphinx.ext.autodoc']
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'custom_roles'
|
||||||
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
|
69
readthedocs/custom_roles.py
Normal file
69
readthedocs/custom_roles.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
from docutils import nodes, utils
|
||||||
|
from docutils.parsers.rst.roles import set_classes
|
||||||
|
|
||||||
|
|
||||||
|
def make_link_node(rawtext, app, name, options):
|
||||||
|
"""
|
||||||
|
Create a link to the TL reference.
|
||||||
|
|
||||||
|
:param rawtext: Text being replaced with link node.
|
||||||
|
:param app: Sphinx application context
|
||||||
|
:param name: Name of the object to link to
|
||||||
|
:param options: Options dictionary passed to role func.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
base = app.config.tl_ref_url
|
||||||
|
if not base:
|
||||||
|
raise AttributeError
|
||||||
|
except AttributeError as e:
|
||||||
|
raise ValueError('tl_ref_url config value is not set') from e
|
||||||
|
|
||||||
|
if base[-1] != '/':
|
||||||
|
base += '/'
|
||||||
|
|
||||||
|
set_classes(options)
|
||||||
|
node = nodes.reference(rawtext, utils.unescape(name),
|
||||||
|
refuri='{}?q={}'.format(base, name),
|
||||||
|
**options)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
||||||
|
"""
|
||||||
|
Link to the TL reference.
|
||||||
|
|
||||||
|
Returns 2 part tuple containing list of nodes to insert into the
|
||||||
|
document and a list of system messages. Both are allowed to be empty.
|
||||||
|
|
||||||
|
:param name: The role name used in the document.
|
||||||
|
:param rawtext: The entire markup snippet, with role.
|
||||||
|
:param text: The text marked with the role.
|
||||||
|
:param lineno: The line number where rawtext appears in the input.
|
||||||
|
:param inliner: The inliner instance that called us.
|
||||||
|
:param options: Directive options for customization.
|
||||||
|
:param content: The directive content for customization.
|
||||||
|
"""
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
|
if content is None:
|
||||||
|
content = []
|
||||||
|
|
||||||
|
# TODO Report error on type not found?
|
||||||
|
# Usage:
|
||||||
|
# msg = inliner.reporter.error(..., line=lineno)
|
||||||
|
# return [inliner.problematic(rawtext, rawtext, msg)], [msg]
|
||||||
|
app = inliner.document.settings.env.app
|
||||||
|
node = make_link_node(rawtext, app, text, options)
|
||||||
|
return [node], []
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
"""
|
||||||
|
Install the plugin.
|
||||||
|
|
||||||
|
:param app: Sphinx application context.
|
||||||
|
"""
|
||||||
|
app.info('Initializing TL reference plugin')
|
||||||
|
app.add_role('tl', tl_role)
|
||||||
|
app.add_config_value('tl_ref_url', None, 'env')
|
||||||
|
return
|
|
@ -25,7 +25,7 @@ You should also refer to the documentation to see what the objects
|
||||||
from a common type, and that's the reason for this distinction.
|
from a common type, and that's the reason for this distinction.
|
||||||
|
|
||||||
Say ``client.send_message()`` didn't exist, we could use the `search`__
|
Say ``client.send_message()`` didn't exist, we could use the `search`__
|
||||||
to look for "message". There we would find `SendMessageRequest`__,
|
to look for "message". There we would find :tl:`SendMessageRequest`,
|
||||||
which we can work with.
|
which we can work with.
|
||||||
|
|
||||||
Every request is a Python class, and has the parameters needed for you
|
Every request is a Python class, and has the parameters needed for you
|
||||||
|
@ -45,11 +45,11 @@ If you're going to use a lot of these, you may do:
|
||||||
# We now have access to 'functions.messages.SendMessageRequest'
|
# We now have access to 'functions.messages.SendMessageRequest'
|
||||||
|
|
||||||
We see that this request must take at least two parameters, a ``peer``
|
We see that this request must take at least two parameters, a ``peer``
|
||||||
of type `InputPeer`__, and a ``message`` which is just a Python
|
of type :tl:`InputPeer`, and a ``message`` which is just a Python
|
||||||
``str``\ ing.
|
``str``\ ing.
|
||||||
|
|
||||||
How can we retrieve this ``InputPeer``? We have two options. We manually
|
How can we retrieve this :tl:`InputPeer`? We have two options. We manually
|
||||||
`construct one`__, for instance:
|
construct one, for instance:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ Or we call ``.get_input_entity()``:
|
||||||
peer = client.get_input_entity('someone')
|
peer = client.get_input_entity('someone')
|
||||||
|
|
||||||
When you're going to invoke an API method, most require you to pass an
|
When you're going to invoke an API method, most require you to pass an
|
||||||
``InputUser``, ``InputChat``, or so on, this is why using
|
:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using
|
||||||
``.get_input_entity()`` is more straightforward (and often
|
``.get_input_entity()`` is more straightforward (and often
|
||||||
immediate, if you've seen the user before, know their ID, etc.).
|
immediate, if you've seen the user before, know their ID, etc.).
|
||||||
If you also need to have information about the whole user, use
|
If you also need to have information about the whole user, use
|
||||||
|
@ -138,6 +138,3 @@ This can further be simplified to:
|
||||||
__ https://lonamiwebs.github.io/Telethon
|
__ https://lonamiwebs.github.io/Telethon
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/index.html
|
__ https://lonamiwebs.github.io/Telethon/methods/index.html
|
||||||
__ https://lonamiwebs.github.io/Telethon/?q=message
|
__ https://lonamiwebs.github.io/Telethon/?q=message
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/types/input_peer.html
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html
|
|
||||||
|
|
|
@ -60,6 +60,14 @@ If you're not authorized, you need to ``.sign_in()``:
|
||||||
# If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...)
|
# If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...)
|
||||||
# You can import both exceptions from telethon.errors.
|
# You can import both exceptions from telethon.errors.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you send the code that Telegram sent you over the app through the
|
||||||
|
app itself, it will expire immediately. You can still send the code
|
||||||
|
through the app by "obfuscating" it (maybe add a magic constant, like
|
||||||
|
``12345``, and then subtract it to get the real code back) or any other
|
||||||
|
technique.
|
||||||
|
|
||||||
``myself`` is your Telegram user. You can view all the information about
|
``myself`` is your Telegram user. You can view all the information about
|
||||||
yourself by doing ``print(myself.stringify())``. You're now ready to use
|
yourself by doing ``print(myself.stringify())``. You're now ready to use
|
||||||
the client as you wish! Remember that any object returned by the API has
|
the client as you wish! Remember that any object returned by the API has
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
.. _entities:
|
||||||
|
|
||||||
=========================
|
=========================
|
||||||
Users, Chats and Channels
|
Users, Chats and Channels
|
||||||
=========================
|
=========================
|
||||||
|
@ -7,16 +9,16 @@ Introduction
|
||||||
************
|
************
|
||||||
|
|
||||||
The library widely uses the concept of "entities". An entity will refer
|
The library widely uses the concept of "entities". An entity will refer
|
||||||
to any ``User``, ``Chat`` or ``Channel`` object that the API may return
|
to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return
|
||||||
in response to certain methods, such as ``GetUsersRequest``.
|
in response to certain methods, such as :tl:`GetUsersRequest`.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
When something "entity-like" is required, it means that you need to
|
When something "entity-like" is required, it means that you need to
|
||||||
provide something that can be turned into an entity. These things include,
|
provide something that can be turned into an entity. These things include,
|
||||||
but are not limited to, usernames, exact titles, IDs, ``Peer`` objects,
|
but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects,
|
||||||
or even entire ``User``, ``Chat`` and ``Channel`` objects and even phone
|
or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even
|
||||||
numbers from people you have in your contacts.
|
phone numbers from people you have in your contacts.
|
||||||
|
|
||||||
Getting entities
|
Getting entities
|
||||||
****************
|
****************
|
||||||
|
@ -71,7 +73,7 @@ become possible.
|
||||||
Every entity the library encounters (in any response to any call) will by
|
Every entity the library encounters (in any response to any call) will by
|
||||||
default be cached in the ``.session`` file (an SQLite database), to avoid
|
default be cached in the ``.session`` file (an SQLite database), to avoid
|
||||||
performing unnecessary API calls. If the entity cannot be found, additonal
|
performing unnecessary API calls. If the entity cannot be found, additonal
|
||||||
calls like ``ResolveUsernameRequest`` or ``GetContactsRequest`` may be
|
calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be
|
||||||
made to obtain the required information.
|
made to obtain the required information.
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,14 +90,14 @@ Entities vs. Input Entities
|
||||||
|
|
||||||
On top of the normal types, the API also make use of what they call their
|
On top of the normal types, the API also make use of what they call their
|
||||||
``Input*`` versions of objects. The input version of an entity (e.g.
|
``Input*`` versions of objects. The input version of an entity (e.g.
|
||||||
``InputPeerUser``, ``InputChat``, etc.) only contains the minimum
|
:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum
|
||||||
information that's required from Telegram to be able to identify
|
information that's required from Telegram to be able to identify
|
||||||
who you're referring to: a ``Peer``'s **ID** and **hash**.
|
who you're referring to: a :tl:`Peer`'s **ID** and **hash**.
|
||||||
|
|
||||||
This ID/hash pair is unique per user, so if you use the pair given by another
|
This ID/hash pair is unique per user, so if you use the pair given by another
|
||||||
user **or bot** it will **not** work.
|
user **or bot** it will **not** work.
|
||||||
|
|
||||||
To save *even more* bandwidth, the API also makes use of the ``Peer``
|
To save *even more* bandwidth, the API also makes use of the :tl:`Peer`
|
||||||
versions, which just have an ID. This serves to identify them, but
|
versions, which just have an ID. This serves to identify them, but
|
||||||
peers alone are not enough to use them. You need to know their hash
|
peers alone are not enough to use them. You need to know their hash
|
||||||
before you can "use them".
|
before you can "use them".
|
||||||
|
@ -104,8 +106,8 @@ As we just mentioned, API calls don't need to know the whole information
|
||||||
about the entities, only their ID and hash. For this reason, another method,
|
about the entities, only their ID and hash. For this reason, another method,
|
||||||
``.get_input_entity()`` is available. This will always use the cache while
|
``.get_input_entity()`` is available. This will always use the cache while
|
||||||
possible, making zero API calls most of the time. When a request is made,
|
possible, making zero API calls most of the time. When a request is made,
|
||||||
if you provided the full entity, e.g. an ``User``, the library will convert
|
if you provided the full entity, e.g. an :tl:`User`, the library will convert
|
||||||
it to the required ``InputPeer`` automatically for you.
|
it to the required :tl:`InputPeer` automatically for you.
|
||||||
|
|
||||||
**You should always favour** ``.get_input_entity()`` **over** ``.get_entity()``
|
**You should always favour** ``.get_input_entity()`` **over** ``.get_entity()``
|
||||||
for this reason! Calling the latter will always make an API call to get
|
for this reason! Calling the latter will always make an API call to get
|
||||||
|
@ -123,5 +125,5 @@ library, the raw requests you make to the API are also able to call
|
||||||
client(SendMessageRequest('username', 'hello'))
|
client(SendMessageRequest('username', 'hello'))
|
||||||
|
|
||||||
The library will call the ``.resolve()`` method of the request, which will
|
The library will call the ``.resolve()`` method of the request, which will
|
||||||
resolve ``'username'`` with the appropriated ``InputPeer``. Don't worry if
|
resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if
|
||||||
you don't get this yet, but remember some of the details here are important.
|
you don't get this yet, but remember some of the details here are important.
|
||||||
|
|
|
@ -66,6 +66,26 @@ Basic Usage
|
||||||
**More details**: :ref:`telegram-client`
|
**More details**: :ref:`telegram-client`
|
||||||
|
|
||||||
|
|
||||||
|
Handling Updates
|
||||||
|
****************
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import events
|
||||||
|
|
||||||
|
# We need to have some worker running
|
||||||
|
client.updates.workers = 1
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(incoming=True, pattern='(?i)hi'))
|
||||||
|
def handler(event):
|
||||||
|
event.reply('Hello!')
|
||||||
|
|
||||||
|
# If you want to handle updates you can't let the script end.
|
||||||
|
input('Press enter to exit.')
|
||||||
|
|
||||||
|
**More details**: :ref:`working-with-updates`
|
||||||
|
|
||||||
|
|
||||||
----------
|
----------
|
||||||
|
|
||||||
You can continue by clicking on the "More details" link below each
|
You can continue by clicking on the "More details" link below each
|
||||||
|
|
|
@ -315,7 +315,7 @@ library alone (when invoking a request), it means that you can now use
|
||||||
``Peer`` types or even usernames where a ``InputPeer`` is required. The
|
``Peer`` types or even usernames where a ``InputPeer`` is required. The
|
||||||
object now has access to the ``client``, so that it can fetch the right
|
object now has access to the ``client``, so that it can fetch the right
|
||||||
type if needed, or access the session database. Furthermore, you can
|
type if needed, or access the session database. Furthermore, you can
|
||||||
reuse requests that need "autocast" (e.g. you put ``User`` but ``InputPeer``
|
reuse requests that need "autocast" (e.g. you put :tl:`User` but ``InputPeer``
|
||||||
was needed), since ``.resolve()`` is called when invoking. Before, it was
|
was needed), since ``.resolve()`` is called when invoking. Before, it was
|
||||||
only done on object construction.
|
only done on object construction.
|
||||||
|
|
||||||
|
@ -993,7 +993,7 @@ Bug fixes and enhancements (v0.13.3)
|
||||||
.. bugs-fixed-2:
|
.. bugs-fixed-2:
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
---------
|
~~~~~~~~~
|
||||||
|
|
||||||
- **Reconnection** used to fail because it tried invoking things from
|
- **Reconnection** used to fail because it tried invoking things from
|
||||||
the ``ReadThread``.
|
the ``ReadThread``.
|
||||||
|
@ -1009,7 +1009,7 @@ Bug fixes
|
||||||
.. enhancements-3:
|
.. enhancements-3:
|
||||||
|
|
||||||
Enhancements
|
Enhancements
|
||||||
------------
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
- **Request will be retried** up to 5 times by default rather than
|
- **Request will be retried** up to 5 times by default rather than
|
||||||
failing on the first attempt.
|
failing on the first attempt.
|
||||||
|
@ -1099,7 +1099,7 @@ outside the buffer.
|
||||||
.. additions-2:
|
.. additions-2:
|
||||||
|
|
||||||
Additions
|
Additions
|
||||||
---------
|
~~~~~~~~~
|
||||||
|
|
||||||
- The mentioned different connection modes, and a new thread.
|
- The mentioned different connection modes, and a new thread.
|
||||||
- You can modify the ``Session`` attributes through the
|
- You can modify the ``Session`` attributes through the
|
||||||
|
@ -1112,7 +1112,7 @@ Additions
|
||||||
.. enhancements-4:
|
.. enhancements-4:
|
||||||
|
|
||||||
Enhancements
|
Enhancements
|
||||||
------------
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
- The low-level socket doesn't use a handcrafted timeout anymore, which
|
- The low-level socket doesn't use a handcrafted timeout anymore, which
|
||||||
should benefit by avoiding the arbitrary ``sleep(0.1)`` that there
|
should benefit by avoiding the arbitrary ``sleep(0.1)`` that there
|
||||||
|
@ -1121,7 +1121,7 @@ Enhancements
|
||||||
``code`` was provided.
|
``code`` was provided.
|
||||||
|
|
||||||
Deprecation
|
Deprecation
|
||||||
-----------
|
~~~~~~~~~~~
|
||||||
|
|
||||||
- ``.sign_up`` does *not* take a ``phone`` argument anymore. Change
|
- ``.sign_up`` does *not* take a ``phone`` argument anymore. Change
|
||||||
this or you will be using ``phone`` as ``code``, and it will fail!
|
this or you will be using ``phone`` as ``code``, and it will fail!
|
||||||
|
@ -1201,7 +1201,7 @@ friendly, along with some other stability enhancements, although it
|
||||||
brings quite a few changes.
|
brings quite a few changes.
|
||||||
|
|
||||||
Breaking changes
|
Breaking changes
|
||||||
----------------
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
- The ``TelegramClient`` methods ``.send_photo_file()``,
|
- The ``TelegramClient`` methods ``.send_photo_file()``,
|
||||||
``.send_document_file()`` and ``.send_media_file()`` are now a
|
``.send_document_file()`` and ``.send_media_file()`` are now a
|
||||||
|
@ -1216,7 +1216,7 @@ Breaking changes
|
||||||
``.download_contact()`` still exist, but are private.
|
``.download_contact()`` still exist, but are private.
|
||||||
|
|
||||||
Additions
|
Additions
|
||||||
---------
|
~~~~~~~~~
|
||||||
|
|
||||||
- Updated to **layer 70**!
|
- Updated to **layer 70**!
|
||||||
- Both downloading and uploading now support **stream-like objects**.
|
- Both downloading and uploading now support **stream-like objects**.
|
||||||
|
@ -1232,7 +1232,7 @@ Additions
|
||||||
.. bug-fixes-5:
|
.. bug-fixes-5:
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
---------
|
~~~~~~~~~
|
||||||
|
|
||||||
- Crashing when migrating to a new layer and receiving old updates
|
- Crashing when migrating to a new layer and receiving old updates
|
||||||
should not happen now.
|
should not happen now.
|
||||||
|
@ -1372,7 +1372,7 @@ Support for parallel connections (v0.11)
|
||||||
**read the whole change log**!
|
**read the whole change log**!
|
||||||
|
|
||||||
Breaking changes
|
Breaking changes
|
||||||
----------------
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
- Every Telegram error has now its **own class**, so it's easier to
|
- Every Telegram error has now its **own class**, so it's easier to
|
||||||
fine-tune your ``except``\ 's.
|
fine-tune your ``except``\ 's.
|
||||||
|
@ -1384,7 +1384,7 @@ Breaking changes
|
||||||
anymore.
|
anymore.
|
||||||
|
|
||||||
Additions
|
Additions
|
||||||
---------
|
~~~~~~~~~
|
||||||
|
|
||||||
- A new, more **lightweight class** has been added. The
|
- A new, more **lightweight class** has been added. The
|
||||||
``TelegramBareClient`` is now the base of the normal
|
``TelegramBareClient`` is now the base of the normal
|
||||||
|
@ -1404,7 +1404,7 @@ Additions
|
||||||
.. bug-fixes-6:
|
.. bug-fixes-6:
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
---------
|
~~~~~~~~~
|
||||||
|
|
||||||
- Received errors are acknowledged to the server, so they don't happen
|
- Received errors are acknowledged to the server, so they don't happen
|
||||||
over and over.
|
over and over.
|
||||||
|
@ -1418,7 +1418,7 @@ Bug fixes
|
||||||
not happen anymore.
|
not happen anymore.
|
||||||
|
|
||||||
Internal changes
|
Internal changes
|
||||||
----------------
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
- Some fixes to the ``JsonSession``.
|
- Some fixes to the ``JsonSession``.
|
||||||
- Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while
|
- Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while
|
||||||
|
|
|
@ -56,11 +56,12 @@ Adding someone else to such chat or channel
|
||||||
*******************************************
|
*******************************************
|
||||||
|
|
||||||
If you don't want to add yourself, maybe because you're already in,
|
If you don't want to add yourself, maybe because you're already in,
|
||||||
you can always add someone else with the `AddChatUserRequest`__,
|
you can always add someone else with the `AddChatUserRequest`__, which
|
||||||
which use is very straightforward:
|
use is very straightforward, or `InviteToChannelRequest`__ for channels:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
# For normal chats
|
||||||
from telethon.tl.functions.messages import AddChatUserRequest
|
from telethon.tl.functions.messages import AddChatUserRequest
|
||||||
|
|
||||||
client(AddChatUserRequest(
|
client(AddChatUserRequest(
|
||||||
|
@ -69,6 +70,15 @@ which use is very straightforward:
|
||||||
fwd_limit=10 # Allow the user to see the 10 last messages
|
fwd_limit=10 # Allow the user to see the 10 last messages
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# For channels
|
||||||
|
from telethon.tl.functions.channels import InviteToChannelRequest
|
||||||
|
|
||||||
|
client(InviteToChannelRequest(
|
||||||
|
channel,
|
||||||
|
[users_to_add]
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Checking a link without joining
|
Checking a link without joining
|
||||||
*******************************
|
*******************************
|
||||||
|
@ -84,6 +94,7 @@ __ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
|
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
|
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
|
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
|
||||||
|
__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
|
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
|
||||||
|
|
||||||
|
|
||||||
|
@ -225,6 +236,12 @@ use `GetMessagesViewsRequest`__, setting ``increment=True``:
|
||||||
increment=True
|
increment=True
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
Note that you can only do this **once or twice a day** per account,
|
||||||
|
running this in a loop will obviously not increase the views forever
|
||||||
|
unless you wait a day between each iteration. If you run it any sooner
|
||||||
|
than that, the views simply won't be increased.
|
||||||
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/233
|
__ https://github.com/LonamiWebs/Telethon/issues/233
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/305
|
__ https://github.com/LonamiWebs/Telethon/issues/305
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/409
|
__ https://github.com/LonamiWebs/Telethon/issues/409
|
||||||
|
|
|
@ -10,3 +10,12 @@ telethon\.tl\.custom\.draft module
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
telethon\.tl\.custom\.dialog module
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.dialog
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -155,7 +155,7 @@ def main():
|
||||||
'telethon_generator/parser/tl_parser.py',
|
'telethon_generator/parser/tl_parser.py',
|
||||||
]),
|
]),
|
||||||
install_requires=['pyaes', 'rsa',
|
install_requires=['pyaes', 'rsa',
|
||||||
'typing' if version_info < (3, 5) else ""],
|
'typing' if version_info < (3, 5, 2) else ""],
|
||||||
extras_require={
|
extras_require={
|
||||||
'cryptg': ['cryptg']
|
'cryptg': ['cryptg']
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,10 @@ class _EventBuilder(abc.ABC):
|
||||||
only matching chats will be handled.
|
only matching chats will be handled.
|
||||||
|
|
||||||
blacklist_chats (:obj:`bool`, optional):
|
blacklist_chats (:obj:`bool`, optional):
|
||||||
Whether to treat the the list of chats as a blacklist (if
|
Whether to treat the chats as a blacklist instead of
|
||||||
it matches it will NOT be handled) or a whitelist (default).
|
as a whitelist (default). This means that every chat
|
||||||
|
will be handled *except* those specified in ``chats``
|
||||||
|
which will be ignored if ``blacklist_chats=True``.
|
||||||
"""
|
"""
|
||||||
def __init__(self, chats=None, blacklist_chats=False):
|
def __init__(self, chats=None, blacklist_chats=False):
|
||||||
self.chats = chats
|
self.chats = chats
|
||||||
|
@ -70,6 +72,7 @@ class _EventBuilder(abc.ABC):
|
||||||
|
|
||||||
class _EventCommon(abc.ABC):
|
class _EventCommon(abc.ABC):
|
||||||
"""Intermediate class with common things to all events"""
|
"""Intermediate class with common things to all events"""
|
||||||
|
_event_name = 'Event'
|
||||||
|
|
||||||
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
|
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
|
||||||
self._entities = {}
|
self._entities = {}
|
||||||
|
@ -90,13 +93,13 @@ class _EventCommon(abc.ABC):
|
||||||
|
|
||||||
async def _get_entity(self, msg_id, entity_id, chat=None):
|
async def _get_entity(self, msg_id, entity_id, chat=None):
|
||||||
"""
|
"""
|
||||||
Helper function to call GetMessages on the give msg_id and
|
Helper function to call :tl:`GetMessages` on the give msg_id and
|
||||||
return the input entity whose ID is the given entity ID.
|
return the input entity whose ID is the given entity ID.
|
||||||
|
|
||||||
If ``chat`` is present it must be an InputPeer.
|
If ``chat`` is present it must be an :tl:`InputPeer`.
|
||||||
|
|
||||||
Returns a tuple of (entity, input_peer) if it was found, or
|
Returns a tuple of ``(entity, input_peer)`` if it was found, or
|
||||||
a tuple of (None, None) if it couldn't be.
|
a tuple of ``(None, None)`` if it couldn't be.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if isinstance(chat, types.InputPeerChannel):
|
if isinstance(chat, types.InputPeerChannel):
|
||||||
|
@ -123,7 +126,7 @@ class _EventCommon(abc.ABC):
|
||||||
@property
|
@property
|
||||||
async def input_chat(self):
|
async def input_chat(self):
|
||||||
"""
|
"""
|
||||||
The (:obj:`InputPeer`) (group, megagroup or channel) on which
|
The (:tl:`InputPeer`) (group, megagroup or channel) on which
|
||||||
the event occurred. This doesn't have the title or anything,
|
the event occurred. This doesn't have the title or anything,
|
||||||
but is useful if you don't need those to avoid further
|
but is useful if you don't need those to avoid further
|
||||||
requests.
|
requests.
|
||||||
|
@ -154,7 +157,7 @@ class _EventCommon(abc.ABC):
|
||||||
@property
|
@property
|
||||||
async def chat(self):
|
async def chat(self):
|
||||||
"""
|
"""
|
||||||
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
|
The (:tl:`User` | :tl:`Chat` | :tl:`Channel`, optional) on which
|
||||||
the event occurred. This property may make an API call the first time
|
the event occurred. This property may make an API call the first time
|
||||||
to get the most up to date version of the chat (mostly when the event
|
to get the most up to date version of the chat (mostly when the event
|
||||||
doesn't belong to a channel), so keep that in mind.
|
doesn't belong to a channel), so keep that in mind.
|
||||||
|
@ -178,7 +181,7 @@ class _EventCommon(abc.ABC):
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
|
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
|
||||||
d['_'] = self.__class__.__name__
|
d['_'] = self._event_name
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,7 +199,7 @@ class Raw(_EventBuilder):
|
||||||
def _name_inner_event(cls):
|
def _name_inner_event(cls):
|
||||||
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
|
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
|
||||||
if hasattr(cls, 'Event'):
|
if hasattr(cls, 'Event'):
|
||||||
cls.Event.__name__ = '{}.Event'.format(cls.__name__)
|
cls.Event._event_name = '{}.Event'.format(cls.__name__)
|
||||||
else:
|
else:
|
||||||
warnings.warn('Class {} does not have a inner Event'.format(cls))
|
warnings.warn('Class {} does not have a inner Event'.format(cls))
|
||||||
return cls
|
return cls
|
||||||
|
@ -310,8 +313,8 @@ class NewMessage(_EventBuilder):
|
||||||
Represents the event of a new message.
|
Represents the event of a new message.
|
||||||
|
|
||||||
Members:
|
Members:
|
||||||
message (:obj:`Message`):
|
message (:tl:`Message`):
|
||||||
This is the original ``Message`` object.
|
This is the original :tl:`Message` object.
|
||||||
|
|
||||||
is_private (:obj:`bool`):
|
is_private (:obj:`bool`):
|
||||||
True if the message was sent as a private message.
|
True if the message was sent as a private message.
|
||||||
|
@ -406,7 +409,7 @@ class NewMessage(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
async def input_sender(self):
|
async def input_sender(self):
|
||||||
"""
|
"""
|
||||||
This (:obj:`InputPeer`) is the input version of the user who
|
This (:tl:`InputPeer`) is the input version of the user who
|
||||||
sent the message. Similarly to ``input_chat``, this doesn't have
|
sent the message. Similarly to ``input_chat``, this doesn't have
|
||||||
things like username or similar, but still useful in some cases.
|
things like username or similar, but still useful in some cases.
|
||||||
|
|
||||||
|
@ -434,7 +437,7 @@ class NewMessage(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
async def sender(self):
|
async def sender(self):
|
||||||
"""
|
"""
|
||||||
This (:obj:`User`) may make an API call the first time to get
|
This (:tl:`User`) may make an API call the first time to get
|
||||||
the most up to date version of the sender (mostly when the event
|
the most up to date version of the sender (mostly when the event
|
||||||
doesn't belong to a channel), so keep that in mind.
|
doesn't belong to a channel), so keep that in mind.
|
||||||
|
|
||||||
|
@ -474,8 +477,8 @@ class NewMessage(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
async def reply_message(self):
|
async def reply_message(self):
|
||||||
"""
|
"""
|
||||||
This (:obj:`Message`, optional) will make an API call the first
|
This optional :tl:`Message` will make an API call the first
|
||||||
time to get the full ``Message`` object that one was replying to,
|
time to get the full :tl:`Message` object that one was replying to,
|
||||||
so use with care as there is no caching besides local caching yet.
|
so use with care as there is no caching besides local caching yet.
|
||||||
"""
|
"""
|
||||||
if not self.message.reply_to_msg_id:
|
if not self.message.reply_to_msg_id:
|
||||||
|
@ -498,14 +501,14 @@ class NewMessage(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
def forward(self):
|
def forward(self):
|
||||||
"""
|
"""
|
||||||
The unmodified (:obj:`MessageFwdHeader`, optional).
|
The unmodified :tl:`MessageFwdHeader`, if present..
|
||||||
"""
|
"""
|
||||||
return self.message.fwd_from
|
return self.message.fwd_from
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
"""
|
"""
|
||||||
The unmodified (:obj:`MessageMedia`, optional).
|
The unmodified :tl:`MessageMedia`, if present.
|
||||||
"""
|
"""
|
||||||
return self.message.media
|
return self.message.media
|
||||||
|
|
||||||
|
@ -513,7 +516,7 @@ class NewMessage(_EventBuilder):
|
||||||
def photo(self):
|
def photo(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a photo,
|
If the message media is a photo,
|
||||||
this returns the (:obj:`Photo`) object.
|
this returns the :tl:`Photo` object.
|
||||||
"""
|
"""
|
||||||
if isinstance(self.message.media, types.MessageMediaPhoto):
|
if isinstance(self.message.media, types.MessageMediaPhoto):
|
||||||
photo = self.message.media.photo
|
photo = self.message.media.photo
|
||||||
|
@ -524,7 +527,7 @@ class NewMessage(_EventBuilder):
|
||||||
def document(self):
|
def document(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a document,
|
If the message media is a document,
|
||||||
this returns the (:obj:`Document`) object.
|
this returns the :tl:`Document` object.
|
||||||
"""
|
"""
|
||||||
if isinstance(self.message.media, types.MessageMediaDocument):
|
if isinstance(self.message.media, types.MessageMediaDocument):
|
||||||
doc = self.message.media.document
|
doc = self.message.media.document
|
||||||
|
@ -547,7 +550,7 @@ class NewMessage(_EventBuilder):
|
||||||
def audio(self):
|
def audio(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a document with an Audio attribute,
|
If the message media is a document with an Audio attribute,
|
||||||
this returns the (:obj:`Document`) object.
|
this returns the :tl:`Document` object.
|
||||||
"""
|
"""
|
||||||
return self._document_by_attribute(types.DocumentAttributeAudio,
|
return self._document_by_attribute(types.DocumentAttributeAudio,
|
||||||
lambda attr: not attr.voice)
|
lambda attr: not attr.voice)
|
||||||
|
@ -556,7 +559,7 @@ class NewMessage(_EventBuilder):
|
||||||
def voice(self):
|
def voice(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a document with a Voice attribute,
|
If the message media is a document with a Voice attribute,
|
||||||
this returns the (:obj:`Document`) object.
|
this returns the :tl:`Document` object.
|
||||||
"""
|
"""
|
||||||
return self._document_by_attribute(types.DocumentAttributeAudio,
|
return self._document_by_attribute(types.DocumentAttributeAudio,
|
||||||
lambda attr: attr.voice)
|
lambda attr: attr.voice)
|
||||||
|
@ -565,7 +568,7 @@ class NewMessage(_EventBuilder):
|
||||||
def video(self):
|
def video(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a document with a Video attribute,
|
If the message media is a document with a Video attribute,
|
||||||
this returns the (:obj:`Document`) object.
|
this returns the :tl:`Document` object.
|
||||||
"""
|
"""
|
||||||
return self._document_by_attribute(types.DocumentAttributeVideo)
|
return self._document_by_attribute(types.DocumentAttributeVideo)
|
||||||
|
|
||||||
|
@ -573,7 +576,7 @@ class NewMessage(_EventBuilder):
|
||||||
def video_note(self):
|
def video_note(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a document with a Video attribute,
|
If the message media is a document with a Video attribute,
|
||||||
this returns the (:obj:`Document`) object.
|
this returns the :tl:`Document` object.
|
||||||
"""
|
"""
|
||||||
return self._document_by_attribute(types.DocumentAttributeVideo,
|
return self._document_by_attribute(types.DocumentAttributeVideo,
|
||||||
lambda attr: attr.round_message)
|
lambda attr: attr.round_message)
|
||||||
|
@ -582,7 +585,7 @@ class NewMessage(_EventBuilder):
|
||||||
def gif(self):
|
def gif(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a document with an Animated attribute,
|
If the message media is a document with an Animated attribute,
|
||||||
this returns the (:obj:`Document`) object.
|
this returns the :tl:`Document` object.
|
||||||
"""
|
"""
|
||||||
return self._document_by_attribute(types.DocumentAttributeAnimated)
|
return self._document_by_attribute(types.DocumentAttributeAnimated)
|
||||||
|
|
||||||
|
@ -590,7 +593,7 @@ class NewMessage(_EventBuilder):
|
||||||
def sticker(self):
|
def sticker(self):
|
||||||
"""
|
"""
|
||||||
If the message media is a document with a Sticker attribute,
|
If the message media is a document with a Sticker attribute,
|
||||||
this returns the (:obj:`Document`) object.
|
this returns the :tl:`Document` object.
|
||||||
"""
|
"""
|
||||||
return self._document_by_attribute(types.DocumentAttributeSticker)
|
return self._document_by_attribute(types.DocumentAttributeSticker)
|
||||||
|
|
||||||
|
@ -609,11 +612,12 @@ class ChatAction(_EventBuilder):
|
||||||
Represents an action in a chat (such as user joined, left, or new pin).
|
Represents an action in a chat (such as user joined, left, or new pin).
|
||||||
"""
|
"""
|
||||||
def build(self, update):
|
def build(self, update):
|
||||||
if isinstance(update, types.UpdateChannelPinnedMessage):
|
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
|
||||||
# Telegram sends UpdateChannelPinnedMessage and then
|
# Telegram does not always send
|
||||||
# UpdateNewChannelMessage with MessageActionPinMessage.
|
# UpdateChannelPinnedMessage for new pins
|
||||||
|
# but always for unpin, with update.id = 0
|
||||||
event = ChatAction.Event(types.PeerChannel(update.channel_id),
|
event = ChatAction.Event(types.PeerChannel(update.channel_id),
|
||||||
new_pin=update.id)
|
unpin=True)
|
||||||
|
|
||||||
elif isinstance(update, types.UpdateChatParticipantAdd):
|
elif isinstance(update, types.UpdateChatParticipantAdd):
|
||||||
event = ChatAction.Event(types.PeerChat(update.chat_id),
|
event = ChatAction.Event(types.PeerChat(update.chat_id),
|
||||||
|
@ -664,6 +668,11 @@ class ChatAction(_EventBuilder):
|
||||||
event = ChatAction.Event(msg,
|
event = ChatAction.Event(msg,
|
||||||
users=msg.from_id,
|
users=msg.from_id,
|
||||||
new_photo=True)
|
new_photo=True)
|
||||||
|
elif isinstance(action, types.MessageActionPinMessage):
|
||||||
|
# Telegram always sends this service message for new pins
|
||||||
|
event = ChatAction.Event(msg,
|
||||||
|
users=msg.from_id,
|
||||||
|
new_pin=msg.reply_to_msg_id)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
@ -678,12 +687,12 @@ class ChatAction(_EventBuilder):
|
||||||
|
|
||||||
Members:
|
Members:
|
||||||
new_pin (:obj:`bool`):
|
new_pin (:obj:`bool`):
|
||||||
``True`` if the pin has changed (new pin or removed).
|
``True`` if there is a new pin.
|
||||||
|
|
||||||
new_photo (:obj:`bool`):
|
new_photo (:obj:`bool`):
|
||||||
``True`` if there's a new chat photo (or it was removed).
|
``True`` if there's a new chat photo (or it was removed).
|
||||||
|
|
||||||
photo (:obj:`Photo`, optional):
|
photo (:tl:`Photo`, optional):
|
||||||
The new photo (or ``None`` if it was removed).
|
The new photo (or ``None`` if it was removed).
|
||||||
|
|
||||||
|
|
||||||
|
@ -704,10 +713,13 @@ class ChatAction(_EventBuilder):
|
||||||
|
|
||||||
new_title (:obj:`bool`, optional):
|
new_title (:obj:`bool`, optional):
|
||||||
The new title string for the chat, if applicable.
|
The new title string for the chat, if applicable.
|
||||||
|
|
||||||
|
unpin (:obj:`bool`):
|
||||||
|
``True`` if the existing pin gets unpinned.
|
||||||
"""
|
"""
|
||||||
def __init__(self, where, new_pin=None, new_photo=None,
|
def __init__(self, where, new_pin=None, new_photo=None,
|
||||||
added_by=None, kicked_by=None, created=None,
|
added_by=None, kicked_by=None, created=None,
|
||||||
users=None, new_title=None):
|
users=None, new_title=None, unpin=None):
|
||||||
if isinstance(where, types.MessageService):
|
if isinstance(where, types.MessageService):
|
||||||
self.action_message = where
|
self.action_message = where
|
||||||
where = where.to_id
|
where = where.to_id
|
||||||
|
@ -726,7 +738,7 @@ class ChatAction(_EventBuilder):
|
||||||
self._added_by = None
|
self._added_by = None
|
||||||
self._kicked_by = None
|
self._kicked_by = None
|
||||||
self.user_added, self.user_joined, self.user_left,\
|
self.user_added, self.user_joined, self.user_left,\
|
||||||
self.user_kicked = (False, False, False, False)
|
self.user_kicked, self.unpin = (False, False, False, False, False)
|
||||||
|
|
||||||
if added_by is True:
|
if added_by is True:
|
||||||
self.user_joined = True
|
self.user_joined = True
|
||||||
|
@ -745,6 +757,7 @@ class ChatAction(_EventBuilder):
|
||||||
self._users = None
|
self._users = None
|
||||||
self._input_users = None
|
self._input_users = None
|
||||||
self.new_title = new_title
|
self.new_title = new_title
|
||||||
|
self.unpin = unpin
|
||||||
|
|
||||||
async def respond(self, *args, **kwargs):
|
async def respond(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -785,7 +798,7 @@ class ChatAction(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
async def pinned_message(self):
|
async def pinned_message(self):
|
||||||
"""
|
"""
|
||||||
If ``new_pin`` is ``True``, this returns the (:obj:`Message`)
|
If ``new_pin`` is ``True``, this returns the (:tl:`Message`)
|
||||||
object that was pinned.
|
object that was pinned.
|
||||||
"""
|
"""
|
||||||
if self._pinned_message == 0:
|
if self._pinned_message == 0:
|
||||||
|
@ -851,7 +864,7 @@ class ChatAction(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
async def input_user(self):
|
async def input_user(self):
|
||||||
"""
|
"""
|
||||||
Input version of the self.user property.
|
Input version of the ``self.user`` property.
|
||||||
"""
|
"""
|
||||||
if await self.input_users:
|
if await self.input_users:
|
||||||
return self._input_users[0]
|
return self._input_users[0]
|
||||||
|
@ -888,7 +901,7 @@ class ChatAction(_EventBuilder):
|
||||||
@property
|
@property
|
||||||
async def input_users(self):
|
async def input_users(self):
|
||||||
"""
|
"""
|
||||||
Input version of the self.users property.
|
Input version of the ``self.users`` property.
|
||||||
"""
|
"""
|
||||||
if self._input_users is None and self._user_peers:
|
if self._input_users is None and self._user_peers:
|
||||||
self._input_users = []
|
self._input_users = []
|
||||||
|
@ -941,7 +954,7 @@ class UserUpdate(_EventBuilder):
|
||||||
recently (:obj:`bool`):
|
recently (:obj:`bool`):
|
||||||
``True`` if the user was seen within a day.
|
``True`` if the user was seen within a day.
|
||||||
|
|
||||||
action (:obj:`SendMessageAction`, optional):
|
action (:tl:`SendMessageAction`, optional):
|
||||||
The "typing" action if any the user is performing if any.
|
The "typing" action if any the user is performing if any.
|
||||||
|
|
||||||
cancel (:obj:`bool`):
|
cancel (:obj:`bool`):
|
||||||
|
@ -1066,6 +1079,9 @@ class MessageEdited(NewMessage):
|
||||||
event._entities = update.entities
|
event._entities = update.entities
|
||||||
return self._message_filter_event(event)
|
return self._message_filter_event(event)
|
||||||
|
|
||||||
|
class Event(NewMessage.Event):
|
||||||
|
pass # Required if we want a different name for it
|
||||||
|
|
||||||
|
|
||||||
@_name_inner_event
|
@_name_inner_event
|
||||||
class MessageDeleted(_EventBuilder):
|
class MessageDeleted(_EventBuilder):
|
||||||
|
@ -1100,22 +1116,22 @@ class MessageDeleted(_EventBuilder):
|
||||||
|
|
||||||
class StopPropagation(Exception):
|
class StopPropagation(Exception):
|
||||||
"""
|
"""
|
||||||
If this Exception is found to be raised in any of the handlers for a
|
If this exception is raised in any of the handlers for a given event,
|
||||||
given update, it will stop the execution of all other registered
|
it will stop the execution of all other registered event handlers.
|
||||||
event handlers in the chain.
|
It can be seen as the ``StopIteration`` in a for loop but for events.
|
||||||
Think of it like a ``StopIteration`` exception in a for loop.
|
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
```
|
>>> @client.on(events.NewMessage)
|
||||||
@client.on(events.NewMessage)
|
... def delete(event):
|
||||||
def delete(event):
|
... event.delete()
|
||||||
event.delete()
|
... # No other event handler will have a chance to handle this event
|
||||||
# Other handlers won't have an event to work with
|
... raise StopPropagation
|
||||||
raise StopPropagation
|
...
|
||||||
|
>>> @client.on(events.NewMessage)
|
||||||
@client.on(events.NewMessage)
|
... def _(event):
|
||||||
def _(event):
|
... # Will never be reached, because it is the second handler
|
||||||
# Will never be reached, because it is the second handler in the chain.
|
... pass
|
||||||
pass
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
|
# For some reason Sphinx wants the silly >>> or
|
||||||
|
# it will show warnings and look bad when generated.
|
||||||
|
pass
|
||||||
|
|
|
@ -21,7 +21,7 @@ DEFAULT_DELIMITERS = {
|
||||||
'```': MessageEntityPre
|
'```': MessageEntityPre
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_URL_RE = re.compile(r'\[([^\]]+)\]\((.+?)\)')
|
DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)')
|
||||||
DEFAULT_URL_FORMAT = '[{0}]({1})'
|
DEFAULT_URL_FORMAT = '[{0}]({1})'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,16 @@ class TcpClient:
|
||||||
except asyncio.TimeoutError as e:
|
except asyncio.TimeoutError as e:
|
||||||
# These are somewhat common if the server has nothing
|
# These are somewhat common if the server has nothing
|
||||||
# to send to us, so use a lower logging priority.
|
# to send to us, so use a lower logging priority.
|
||||||
__log__.debug('socket.timeout "%s" while reading data', e)
|
if bytes_left < size:
|
||||||
|
__log__.warning(
|
||||||
|
'socket.timeout "%s" when %d/%d had been received',
|
||||||
|
e, size - bytes_left, size
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
__log__.debug(
|
||||||
|
'socket.timeout "%s" while reading data', e
|
||||||
|
)
|
||||||
|
|
||||||
raise TimeoutError() from e
|
raise TimeoutError() from e
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
__log__.info('ConnectionError "%s" while reading data', e)
|
__log__.info('ConnectionError "%s" while reading data', e)
|
||||||
|
|
|
@ -25,13 +25,15 @@ __log__ = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MtProtoSender:
|
class MtProtoSender:
|
||||||
"""MTProto Mobile Protocol sender
|
"""
|
||||||
(https://core.telegram.org/mtproto/description).
|
MTProto Mobile Protocol sender
|
||||||
|
(https://core.telegram.org/mtproto/description).
|
||||||
|
|
||||||
Note that this class is not thread-safe, and calling send/receive
|
Note that this class is not thread-safe, and calling send/receive
|
||||||
from two or more threads at the same time is undefined behaviour.
|
from two or more threads at the same time is undefined behaviour.
|
||||||
Rationale: a new connection should be spawned to send/receive requests
|
Rationale:
|
||||||
in parallel, so thread-safety (hence locking) isn't needed.
|
a new connection should be spawned to send/receive requests
|
||||||
|
in parallel, so thread-safety (hence locking) isn't needed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, session, connection, loop=None):
|
def __init__(self, session, connection, loop=None):
|
||||||
|
|
|
@ -78,7 +78,7 @@ class MemorySession(Session):
|
||||||
try:
|
try:
|
||||||
p = utils.get_input_peer(e, allow_self=False)
|
p = utils.get_input_peer(e, allow_self=False)
|
||||||
marked_id = utils.get_peer_id(p)
|
marked_id = utils.get_peer_id(p)
|
||||||
except ValueError:
|
except TypeError:
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(p, (InputPeerUser, InputPeerChannel)):
|
if isinstance(p, (InputPeerUser, InputPeerChannel)):
|
||||||
|
|
|
@ -91,18 +91,13 @@ from .extensions import markdown, html
|
||||||
__log__ = logging.getLogger(__name__)
|
__log__ = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class _Box:
|
|
||||||
"""Helper class to pass parameters by reference"""
|
|
||||||
def __init__(self, x=None):
|
|
||||||
self.x = x
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramClient(TelegramBareClient):
|
class TelegramClient(TelegramBareClient):
|
||||||
"""
|
"""
|
||||||
Initializes the Telegram client with the specified API ID and Hash.
|
Initializes the Telegram client with the specified API ID and Hash.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session (:obj:`str` | :obj:`Session` | :obj:`None`):
|
session (:obj:`str` | :obj:`telethon.sessions.abstract.Session`, \
|
||||||
|
:obj:`None`):
|
||||||
The file name of the session file to be used if a string is
|
The file name of the session file to be used if a string is
|
||||||
given (it may be a full path), or the Session instance to be
|
given (it may be a full path), or the Session instance to be
|
||||||
used otherwise. If it's ``None``, the session will not be saved,
|
used otherwise. If it's ``None``, the session will not be saved,
|
||||||
|
@ -169,7 +164,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
connection_mode=ConnectionMode.TCP_FULL,
|
connection_mode=ConnectionMode.TCP_FULL,
|
||||||
use_ipv6=False,
|
use_ipv6=False,
|
||||||
proxy=None,
|
proxy=None,
|
||||||
timeout=timedelta(seconds=5),
|
timeout=timedelta(seconds=10),
|
||||||
loop=None,
|
loop=None,
|
||||||
report_errors=True,
|
report_errors=True,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
|
@ -216,7 +211,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
Whether to force sending as SMS.
|
Whether to force sending as SMS.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Information about the result of the request.
|
An instance of :tl:`SentCode`.
|
||||||
"""
|
"""
|
||||||
phone = utils.parse_phone(phone) or self._phone
|
phone = utils.parse_phone(phone) or self._phone
|
||||||
phone_hash = self._phone_code_hash.get(phone)
|
phone_hash = self._phone_code_hash.get(phone)
|
||||||
|
@ -261,8 +256,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
This is only required if it is enabled in your account.
|
This is only required if it is enabled in your account.
|
||||||
|
|
||||||
bot_token (:obj:`str`):
|
bot_token (:obj:`str`):
|
||||||
Bot Token obtained by @BotFather to log in as a bot.
|
Bot Token obtained by `@BotFather <https://t.me/BotFather>`_
|
||||||
Cannot be specified with `phone` (only one of either allowed).
|
to log in as a bot. Cannot be specified with ``phone`` (only
|
||||||
|
one of either allowed).
|
||||||
|
|
||||||
force_sms (:obj:`bool`, optional):
|
force_sms (:obj:`bool`, optional):
|
||||||
Whether to force sending the code request as SMS.
|
Whether to force sending the code request as SMS.
|
||||||
|
@ -280,8 +276,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
Similar to the first name, but for the last. Optional.
|
Similar to the first name, but for the last. Optional.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:obj:`TelegramClient`:
|
This :obj:`TelegramClient`, so initialization
|
||||||
This client, so initialization can be chained with `.start()`.
|
can be chained with ``.start()``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if code_callback is None:
|
if code_callback is None:
|
||||||
|
@ -377,7 +373,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
these requests.
|
these requests.
|
||||||
|
|
||||||
code (:obj:`str` | :obj:`int`):
|
code (:obj:`str` | :obj:`int`):
|
||||||
The code that Telegram sent.
|
The code that Telegram sent. Note that if you have sent this
|
||||||
|
code through the application itself it will immediately
|
||||||
|
expire. If you want to send the code, obfuscate it somehow.
|
||||||
|
If you're not doing any of this you can ignore this note.
|
||||||
|
|
||||||
password (:obj:`str`):
|
password (:obj:`str`):
|
||||||
2FA password, should be used if a previous call raised
|
2FA password, should be used if a previous call raised
|
||||||
|
@ -393,7 +392,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The signed in user, or the information about
|
The signed in user, or the information about
|
||||||
:meth:`.send_code_request()`.
|
:meth:`send_code_request`.
|
||||||
"""
|
"""
|
||||||
if self.is_user_authorized():
|
if self.is_user_authorized():
|
||||||
await self._check_events_pending_resolve()
|
await self._check_events_pending_resolve()
|
||||||
|
@ -454,7 +453,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
Optional last name.
|
Optional last name.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The new created user.
|
The new created :tl:`User`.
|
||||||
"""
|
"""
|
||||||
if self.is_user_authorized():
|
if self.is_user_authorized():
|
||||||
await self._check_events_pending_resolve()
|
await self._check_events_pending_resolve()
|
||||||
|
@ -479,7 +478,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
Logs out Telegram and deletes the current ``*.session`` file.
|
Logs out Telegram and deletes the current ``*.session`` file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the operation was successful.
|
``True`` if the operation was successful.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await self(LogOutRequest())
|
await self(LogOutRequest())
|
||||||
|
@ -497,12 +496,12 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_peer (:obj:`bool`, optional):
|
input_peer (:obj:`bool`, optional):
|
||||||
Whether to return the ``InputPeerUser`` version or the normal
|
Whether to return the :tl:`InputPeerUser` version or the normal
|
||||||
``User``. This can be useful if you just need to know the ID
|
:tl:`User`. This can be useful if you just need to know the ID
|
||||||
of yourself.
|
of yourself.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:obj:`User`: Your own user.
|
Your own :tl:`User`.
|
||||||
"""
|
"""
|
||||||
if input_peer and self._self_input_peer:
|
if input_peer and self._self_input_peer:
|
||||||
return self._self_input_peer
|
return self._self_input_peer
|
||||||
|
@ -522,7 +521,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
# region Dialogs ("chats") requests
|
# region Dialogs ("chats") requests
|
||||||
|
|
||||||
async def iter_dialogs(self, limit=None, offset_date=None, offset_id=0,
|
async def iter_dialogs(self, limit=None, offset_date=None, offset_id=0,
|
||||||
offset_peer=InputPeerEmpty(), _total_box=None):
|
offset_peer=InputPeerEmpty(), _total=None):
|
||||||
"""
|
"""
|
||||||
Returns an iterator over the dialogs, yielding 'limit' at most.
|
Returns an iterator over the dialogs, yielding 'limit' at most.
|
||||||
Dialogs are the open "chats" or conversations with other people.
|
Dialogs are the open "chats" or conversations with other people.
|
||||||
|
@ -541,18 +540,18 @@ class TelegramClient(TelegramBareClient):
|
||||||
offset_id (:obj:`int`, optional):
|
offset_id (:obj:`int`, optional):
|
||||||
The message ID to be used as an offset.
|
The message ID to be used as an offset.
|
||||||
|
|
||||||
offset_peer (:obj:`InputPeer`, optional):
|
offset_peer (:tl:`InputPeer`, optional):
|
||||||
The peer to be used as an offset.
|
The peer to be used as an offset.
|
||||||
|
|
||||||
_total_box (:obj:`_Box`, optional):
|
_total (:obj:`list`, optional):
|
||||||
A _Box instance to pass the total parameter by reference.
|
A single-item list to pass the total parameter by reference.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
Instances of ``telethon.tl.custom.Dialog``.
|
Instances of :obj:`telethon.tl.custom.dialog.Dialog`.
|
||||||
"""
|
"""
|
||||||
limit = float('inf') if limit is None else int(limit)
|
limit = float('inf') if limit is None else int(limit)
|
||||||
if limit == 0:
|
if limit == 0:
|
||||||
if not _total_box:
|
if not _total:
|
||||||
return
|
return
|
||||||
# Special case, get a single dialog and determine count
|
# Special case, get a single dialog and determine count
|
||||||
dialogs = await self(GetDialogsRequest(
|
dialogs = await self(GetDialogsRequest(
|
||||||
|
@ -561,7 +560,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
offset_peer=offset_peer,
|
offset_peer=offset_peer,
|
||||||
limit=1
|
limit=1
|
||||||
))
|
))
|
||||||
_total_box.x = getattr(dialogs, 'count', len(dialogs.dialogs))
|
_total[0] = getattr(dialogs, 'count', len(dialogs.dialogs))
|
||||||
return
|
return
|
||||||
|
|
||||||
seen = set()
|
seen = set()
|
||||||
|
@ -575,8 +574,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
req.limit = min(limit - len(seen), 100)
|
req.limit = min(limit - len(seen), 100)
|
||||||
r = await self(req)
|
r = await self(req)
|
||||||
|
|
||||||
if _total_box:
|
if _total:
|
||||||
_total_box.x = getattr(r, 'count', len(r.dialogs))
|
_total[0] = getattr(r, 'count', len(r.dialogs))
|
||||||
messages = {m.id: m for m in r.messages}
|
messages = {m.id: m for m in r.messages}
|
||||||
entities = {utils.get_peer_id(x): x
|
entities = {utils.get_peer_id(x): x
|
||||||
for x in itertools.chain(r.users, r.chats)}
|
for x in itertools.chain(r.users, r.chats)}
|
||||||
|
@ -604,24 +603,24 @@ class TelegramClient(TelegramBareClient):
|
||||||
async def get_dialogs(self, *args, **kwargs):
|
async def get_dialogs(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Same as :meth:`iter_dialogs`, but returns a list instead
|
Same as :meth:`iter_dialogs`, but returns a list instead
|
||||||
with an additional .total attribute on the list.
|
with an additional ``.total`` attribute on the list.
|
||||||
"""
|
"""
|
||||||
total_box = _Box(0)
|
total = [0]
|
||||||
kwargs['_total_box'] = total_box
|
kwargs['_total'] = total
|
||||||
dialogs = UserList()
|
dialogs = UserList()
|
||||||
async for dialog in self.iter_dialogs(*args, **kwargs):
|
async for dialog in self.iter_dialogs(*args, **kwargs):
|
||||||
dialogs.append(dialog)
|
dialogs.append(dialog)
|
||||||
|
dialogs.total = total[0]
|
||||||
dialogs.total = total_box.x
|
|
||||||
return dialogs
|
return dialogs
|
||||||
|
|
||||||
async def iter_drafts(self): # TODO: Ability to provide a `filter`
|
async def iter_drafts(self): # TODO: Ability to provide a `filter`
|
||||||
"""
|
"""
|
||||||
Iterator over all open draft messages.
|
Iterator over all open draft messages.
|
||||||
|
|
||||||
The yielded items are custom ``Draft`` objects that are easier to use.
|
Instances of :obj:`telethon.tl.custom.draft.Draft` are yielded.
|
||||||
You can call ``draft.set_message('text')`` to change the message,
|
You can call :obj:`telethon.tl.custom.draft.Draft.set_message`
|
||||||
or delete it through :meth:`draft.delete()`.
|
to change the message or :obj:`telethon.tl.custom.draft.Draft.delete`
|
||||||
|
among other things.
|
||||||
"""
|
"""
|
||||||
for update in (await self(GetAllDraftsRequest())).updates:
|
for update in (await self(GetAllDraftsRequest())).updates:
|
||||||
yield Draft._from_update(self, update)
|
yield Draft._from_update(self, update)
|
||||||
|
@ -675,7 +674,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
async def _parse_message_text(self, message, parse_mode):
|
async def _parse_message_text(self, message, parse_mode):
|
||||||
"""
|
"""
|
||||||
Returns a (parsed message, entities) tuple depending on parse_mode.
|
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
|
||||||
"""
|
"""
|
||||||
if not parse_mode:
|
if not parse_mode:
|
||||||
return message, []
|
return message, []
|
||||||
|
@ -714,10 +713,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
entity (:obj:`entity`):
|
entity (:obj:`entity`):
|
||||||
To who will it be sent.
|
To who will it be sent.
|
||||||
|
|
||||||
message (:obj:`str` | :obj:`Message`):
|
message (:obj:`str` | :tl:`Message`):
|
||||||
The message to be sent, or another message object to resend.
|
The message to be sent, or another message object to resend.
|
||||||
|
|
||||||
reply_to (:obj:`int` | :obj:`Message`, optional):
|
reply_to (:obj:`int` | :tl:`Message`, optional):
|
||||||
Whether to reply to a message or not. If an integer is provided,
|
Whether to reply to a message or not. If an integer is provided,
|
||||||
it should be the ID of the message that it should reply to.
|
it should be the ID of the message that it should reply to.
|
||||||
|
|
||||||
|
@ -742,7 +741,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
Has no effect when sending a file.
|
Has no effect when sending a file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
the sent message
|
The sent :tl:`Message`.
|
||||||
"""
|
"""
|
||||||
if file is not None:
|
if file is not None:
|
||||||
return await self.send_file(
|
return await self.send_file(
|
||||||
|
@ -809,7 +808,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
entity (:obj:`entity`):
|
entity (:obj:`entity`):
|
||||||
To which entity the message(s) will be forwarded.
|
To which entity the message(s) will be forwarded.
|
||||||
|
|
||||||
messages (:obj:`list` | :obj:`int` | :obj:`Message`):
|
messages (:obj:`list` | :obj:`int` | :tl:`Message`):
|
||||||
The message(s) to forward, or their integer IDs.
|
The message(s) to forward, or their integer IDs.
|
||||||
|
|
||||||
from_peer (:obj:`entity`):
|
from_peer (:obj:`entity`):
|
||||||
|
@ -818,7 +817,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
order for the forward to work.
|
order for the forward to work.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The forwarded messages.
|
The list of forwarded :tl:`Message`.
|
||||||
"""
|
"""
|
||||||
if not utils.is_list_like(messages):
|
if not utils.is_list_like(messages):
|
||||||
messages = (messages,)
|
messages = (messages,)
|
||||||
|
@ -848,7 +847,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
for update in result.updates:
|
for update in result.updates:
|
||||||
if isinstance(update, UpdateMessageID):
|
if isinstance(update, UpdateMessageID):
|
||||||
random_to_id[update.random_id] = update.id
|
random_to_id[update.random_id] = update.id
|
||||||
elif isinstance(update, UpdateNewMessage):
|
elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
|
||||||
id_to_message[update.message.id] = update.message
|
id_to_message[update.message.id] = update.message
|
||||||
|
|
||||||
return [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
|
return [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
|
||||||
|
@ -885,7 +884,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
not modified at all.
|
not modified at all.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
the edited message
|
The edited :tl:`Message`.
|
||||||
"""
|
"""
|
||||||
message, msg_entities = await self._parse_message_text(message, parse_mode)
|
message, msg_entities = await self._parse_message_text(message, parse_mode)
|
||||||
request = EditMessageRequest(
|
request = EditMessageRequest(
|
||||||
|
@ -908,7 +907,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
be ``None`` for normal chats, but **must** be present
|
be ``None`` for normal chats, but **must** be present
|
||||||
for channels and megagroups.
|
for channels and megagroups.
|
||||||
|
|
||||||
message_ids (:obj:`list` | :obj:`int` | :obj:`Message`):
|
message_ids (:obj:`list` | :obj:`int` | :tl:`Message`):
|
||||||
The IDs (or ID) or messages to be deleted.
|
The IDs (or ID) or messages to be deleted.
|
||||||
|
|
||||||
revoke (:obj:`bool`, optional):
|
revoke (:obj:`bool`, optional):
|
||||||
|
@ -918,7 +917,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
This has no effect on channels or megagroups.
|
This has no effect on channels or megagroups.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The affected messages.
|
The :tl:`AffectedMessages`.
|
||||||
"""
|
"""
|
||||||
if not utils.is_list_like(message_ids):
|
if not utils.is_list_like(message_ids):
|
||||||
message_ids = (message_ids,)
|
message_ids = (message_ids,)
|
||||||
|
@ -940,7 +939,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
async def iter_messages(self, entity, limit=20, offset_date=None,
|
async def iter_messages(self, entity, limit=20, offset_date=None,
|
||||||
offset_id=0, max_id=0, min_id=0, add_offset=0,
|
offset_id=0, max_id=0, min_id=0, add_offset=0,
|
||||||
batch_size=100, wait_time=None, _total_box=None):
|
batch_size=100, wait_time=None, _total=None):
|
||||||
"""
|
"""
|
||||||
Iterator over the message history for the specified entity.
|
Iterator over the message history for the specified entity.
|
||||||
|
|
||||||
|
@ -981,16 +980,16 @@ class TelegramClient(TelegramBareClient):
|
||||||
you are still free to do so.
|
you are still free to do so.
|
||||||
|
|
||||||
wait_time (:obj:`int`):
|
wait_time (:obj:`int`):
|
||||||
Wait time between different ``GetHistoryRequest``. Use this
|
Wait time between different :tl:`GetHistoryRequest`. Use this
|
||||||
parameter to avoid hitting the ``FloodWaitError`` as needed.
|
parameter to avoid hitting the ``FloodWaitError`` as needed.
|
||||||
If left to ``None``, it will default to 1 second only if
|
If left to ``None``, it will default to 1 second only if
|
||||||
the limit is higher than 3000.
|
the limit is higher than 3000.
|
||||||
|
|
||||||
_total_box (:obj:`_Box`, optional):
|
_total (:obj:`list`, optional):
|
||||||
A _Box instance to pass the total parameter by reference.
|
A single-item list to pass the total parameter by reference.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
Instances of ``telethon.tl.types.Message`` with extra attributes:
|
Instances of :tl:`Message` with extra attributes:
|
||||||
|
|
||||||
* ``.sender`` = entity of the sender.
|
* ``.sender`` = entity of the sender.
|
||||||
* ``.fwd_from.sender`` = if fwd_from, who sent it originally.
|
* ``.fwd_from.sender`` = if fwd_from, who sent it originally.
|
||||||
|
@ -998,7 +997,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
* ``.to`` = entity to which the message was sent.
|
* ``.to`` = entity to which the message was sent.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Telegram's flood wait limit for ``GetHistoryRequest`` seems to
|
Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to
|
||||||
be around 30 seconds per 3000 messages, therefore a sleep of 1
|
be around 30 seconds per 3000 messages, therefore a sleep of 1
|
||||||
second is the default for this limit (or above). You may need
|
second is the default for this limit (or above). You may need
|
||||||
an higher limit, so you're free to set the ``batch_size`` that
|
an higher limit, so you're free to set the ``batch_size`` that
|
||||||
|
@ -1007,7 +1006,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
limit = float('inf') if limit is None else int(limit)
|
limit = float('inf') if limit is None else int(limit)
|
||||||
if limit == 0:
|
if limit == 0:
|
||||||
if not _total_box:
|
if not _total:
|
||||||
return
|
return
|
||||||
# No messages, but we still need to know the total message count
|
# No messages, but we still need to know the total message count
|
||||||
result = await self(GetHistoryRequest(
|
result = await self(GetHistoryRequest(
|
||||||
|
@ -1015,7 +1014,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
offset_date=None, offset_id=0, max_id=0, min_id=0,
|
offset_date=None, offset_id=0, max_id=0, min_id=0,
|
||||||
add_offset=0, hash=0
|
add_offset=0, hash=0
|
||||||
))
|
))
|
||||||
_total_box.x = getattr(result, 'count', len(result.messages))
|
_total[0] = getattr(result, 'count', len(result.messages))
|
||||||
return
|
return
|
||||||
|
|
||||||
if wait_time is None:
|
if wait_time is None:
|
||||||
|
@ -1036,8 +1035,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
add_offset=add_offset,
|
add_offset=add_offset,
|
||||||
hash=0
|
hash=0
|
||||||
))
|
))
|
||||||
if _total_box:
|
if _total:
|
||||||
_total_box.x = getattr(r, 'count', len(r.messages))
|
_total[0] = getattr(r, 'count', len(r.messages))
|
||||||
|
|
||||||
entities = {utils.get_peer_id(x): x
|
entities = {utils.get_peer_id(x): x
|
||||||
for x in itertools.chain(r.users, r.chats)}
|
for x in itertools.chain(r.users, r.chats)}
|
||||||
|
@ -1080,15 +1079,15 @@ class TelegramClient(TelegramBareClient):
|
||||||
async def get_messages(self, *args, **kwargs):
|
async def get_messages(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Same as :meth:`iter_messages`, but returns a list instead
|
Same as :meth:`iter_messages`, but returns a list instead
|
||||||
with an additional .total attribute on the list.
|
with an additional ``.total`` attribute on the list.
|
||||||
"""
|
"""
|
||||||
total_box = _Box(0)
|
total = [0]
|
||||||
kwargs['_total_box'] = total_box
|
kwargs['_total'] = total
|
||||||
msgs = UserList()
|
msgs = UserList()
|
||||||
async for msg in self.iter_messages(*args, **kwargs):
|
async for msg in self.iter_messages(*args, **kwargs):
|
||||||
msgs.append(msg)
|
msgs.append(msg)
|
||||||
|
|
||||||
msgs.total = total_box.x
|
msgs.total = total[0]
|
||||||
return msgs
|
return msgs
|
||||||
|
|
||||||
async def get_message_history(self, *args, **kwargs):
|
async def get_message_history(self, *args, **kwargs):
|
||||||
|
@ -1107,7 +1106,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
entity (:obj:`entity`):
|
entity (:obj:`entity`):
|
||||||
The chat where these messages are located.
|
The chat where these messages are located.
|
||||||
|
|
||||||
message (:obj:`list` | :obj:`Message`):
|
message (:obj:`list` | :tl:`Message`):
|
||||||
Either a list of messages or a single message.
|
Either a list of messages or a single message.
|
||||||
|
|
||||||
max_id (:obj:`int`):
|
max_id (:obj:`int`):
|
||||||
|
@ -1164,8 +1163,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
raise TypeError('Invalid message type: {}'.format(type(message)))
|
raise TypeError('Invalid message type: {}'.format(type(message)))
|
||||||
|
|
||||||
async def iter_participants(self, entity, limit=None, search='',
|
async def iter_participants(self, entity, limit=None, search='',
|
||||||
filter=None, aggressive=False,
|
filter=None, aggressive=False, _total=None):
|
||||||
_total_box=None):
|
|
||||||
"""
|
"""
|
||||||
Iterator over the participants belonging to the specified chat.
|
Iterator over the participants belonging to the specified chat.
|
||||||
|
|
||||||
|
@ -1179,9 +1177,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
search (:obj:`str`, optional):
|
search (:obj:`str`, optional):
|
||||||
Look for participants with this string in name/username.
|
Look for participants with this string in name/username.
|
||||||
|
|
||||||
filter (:obj:`ChannelParticipantsFilter`, optional):
|
filter (:tl:`ChannelParticipantsFilter`, optional):
|
||||||
The filter to be used, if you want e.g. only admins. See
|
The filter to be used, if you want e.g. only admins
|
||||||
https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html.
|
|
||||||
Note that you might not have permissions for some filter.
|
Note that you might not have permissions for some filter.
|
||||||
This has no effect for normal chats or users.
|
This has no effect for normal chats or users.
|
||||||
|
|
||||||
|
@ -1195,14 +1192,14 @@ class TelegramClient(TelegramBareClient):
|
||||||
This has no effect for groups or channels with less than
|
This has no effect for groups or channels with less than
|
||||||
10,000 members, or if a ``filter`` is given.
|
10,000 members, or if a ``filter`` is given.
|
||||||
|
|
||||||
_total_box (:obj:`_Box`, optional):
|
_total (:obj:`list`, optional):
|
||||||
A _Box instance to pass the total parameter by reference.
|
A single-item list to pass the total parameter by reference.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
The ``User`` objects returned by ``GetParticipantsRequest``
|
The :tl:`User` objects returned by :tl:`GetParticipantsRequest`
|
||||||
with an additional ``.participant`` attribute which is the
|
with an additional ``.participant`` attribute which is the
|
||||||
matched ``ChannelParticipant`` type for channels/megagroups
|
matched :tl:`ChannelParticipant` type for channels/megagroups
|
||||||
or ``ChatParticipants`` for normal chats.
|
or :tl:`ChatParticipants` for normal chats.
|
||||||
"""
|
"""
|
||||||
if isinstance(filter, type):
|
if isinstance(filter, type):
|
||||||
filter = filter()
|
filter = filter()
|
||||||
|
@ -1224,8 +1221,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
total = (await self(GetFullChannelRequest(
|
total = (await self(GetFullChannelRequest(
|
||||||
entity
|
entity
|
||||||
))).full_chat.participants_count
|
))).full_chat.participants_count
|
||||||
if _total_box:
|
if _total:
|
||||||
_total_box.x = total
|
_total[0] = total
|
||||||
|
|
||||||
if limit == 0:
|
if limit == 0:
|
||||||
return
|
return
|
||||||
|
@ -1285,8 +1282,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
elif isinstance(entity, InputPeerChat):
|
elif isinstance(entity, InputPeerChat):
|
||||||
# TODO We *could* apply the `filter` here ourselves
|
# TODO We *could* apply the `filter` here ourselves
|
||||||
full = await self(GetFullChatRequest(entity.chat_id))
|
full = await self(GetFullChatRequest(entity.chat_id))
|
||||||
if _total_box:
|
if _total:
|
||||||
_total_box.x = len(full.full_chat.participants.participants)
|
_total[0] = len(full.full_chat.participants.participants)
|
||||||
|
|
||||||
have = 0
|
have = 0
|
||||||
users = {user.id: user for user in full.users}
|
users = {user.id: user for user in full.users}
|
||||||
|
@ -1302,8 +1299,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
user.participant = participant
|
user.participant = participant
|
||||||
yield user
|
yield user
|
||||||
else:
|
else:
|
||||||
if _total_box:
|
if _total:
|
||||||
_total_box.x = 1
|
_total[0] = 1
|
||||||
if limit != 0:
|
if limit != 0:
|
||||||
user = await self.get_entity(entity)
|
user = await self.get_entity(entity)
|
||||||
if filter_entity(user):
|
if filter_entity(user):
|
||||||
|
@ -1313,14 +1310,14 @@ class TelegramClient(TelegramBareClient):
|
||||||
async def get_participants(self, *args, **kwargs):
|
async def get_participants(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Same as :meth:`iter_participants`, but returns a list instead
|
Same as :meth:`iter_participants`, but returns a list instead
|
||||||
with an additional .total attribute on the list.
|
with an additional ``.total`` attribute on the list.
|
||||||
"""
|
"""
|
||||||
total_box = _Box(0)
|
total = [0]
|
||||||
kwargs['_total_box'] = total_box
|
kwargs['_total'] = total
|
||||||
participants = UserList()
|
participants = UserList()
|
||||||
async for participant in self.iter_participants(*args, **kwargs):
|
async for participant in self.iter_participants(*args, **kwargs):
|
||||||
participants.append(participant)
|
participants.append(participant)
|
||||||
participants.total = total_box.x
|
participants.total = total[0]
|
||||||
return participants
|
return participants
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
@ -1371,12 +1368,12 @@ class TelegramClient(TelegramBareClient):
|
||||||
A callback function accepting two parameters:
|
A callback function accepting two parameters:
|
||||||
``(sent bytes, total)``.
|
``(sent bytes, total)``.
|
||||||
|
|
||||||
reply_to (:obj:`int` | :obj:`Message`):
|
reply_to (:obj:`int` | :tl:`Message`):
|
||||||
Same as reply_to from .send_message().
|
Same as reply_to from .send_message().
|
||||||
|
|
||||||
attributes (:obj:`list`, optional):
|
attributes (:obj:`list`, optional):
|
||||||
Optional attributes that override the inferred ones, like
|
Optional attributes that override the inferred ones, like
|
||||||
``DocumentAttributeFilename`` and so on.
|
:tl:`DocumentAttributeFilename` and so on.
|
||||||
|
|
||||||
thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional):
|
thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional):
|
||||||
Optional thumbnail (for videos).
|
Optional thumbnail (for videos).
|
||||||
|
@ -1399,19 +1396,22 @@ class TelegramClient(TelegramBareClient):
|
||||||
it will be used to determine metadata from audio and video files.
|
it will be used to determine metadata from audio and video files.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The message (or messages) containing the sent file.
|
The :tl:`Message` (or messages) containing the sent file.
|
||||||
"""
|
"""
|
||||||
# First check if the user passed an iterable, in which case
|
# First check if the user passed an iterable, in which case
|
||||||
# we may want to send as an album if all are photo files.
|
# we may want to send as an album if all are photo files.
|
||||||
if utils.is_list_like(file):
|
if utils.is_list_like(file):
|
||||||
# TODO Fix progress_callback
|
# TODO Fix progress_callback
|
||||||
images = []
|
images = []
|
||||||
documents = []
|
if force_document:
|
||||||
for x in file:
|
documents = file
|
||||||
if utils.is_image(x):
|
else:
|
||||||
images.append(x)
|
documents = []
|
||||||
else:
|
for x in file:
|
||||||
documents.append(x)
|
if utils.is_image(x):
|
||||||
|
images.append(x)
|
||||||
|
else:
|
||||||
|
documents.append(x)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
while images:
|
while images:
|
||||||
|
@ -1424,7 +1424,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
result.extend(
|
result.extend(
|
||||||
await self.send_file(
|
await self.send_file(
|
||||||
entity, x, allow_cache=False,
|
entity, x, allow_cache=allow_cache,
|
||||||
caption=caption, force_document=force_document,
|
caption=caption, force_document=force_document,
|
||||||
progress_callback=progress_callback, reply_to=reply_to,
|
progress_callback=progress_callback, reply_to=reply_to,
|
||||||
attributes=attributes, thumb=thumb, **kwargs
|
attributes=attributes, thumb=thumb, **kwargs
|
||||||
|
@ -1557,7 +1557,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def send_voice_note(self, *args, **kwargs):
|
def send_voice_note(self, *args, **kwargs):
|
||||||
"""Wrapper method around .send_file() with is_voice_note=True"""
|
"""Wrapper method around :meth:`send_file` with is_voice_note=True."""
|
||||||
kwargs['is_voice_note'] = True
|
kwargs['is_voice_note'] = True
|
||||||
return self.send_file(*args, **kwargs)
|
return self.send_file(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -1654,8 +1654,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
``(sent bytes, total)``.
|
``(sent bytes, total)``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
``InputFileBig`` if the file size is larger than 10MB,
|
:tl:`InputFileBig` if the file size is larger than 10MB,
|
||||||
``InputSizedFile`` (subclass of ``InputFile``) otherwise.
|
``InputSizedFile`` (subclass of :tl:`InputFile`) otherwise.
|
||||||
"""
|
"""
|
||||||
if isinstance(file, (InputFile, InputFileBig)):
|
if isinstance(file, (InputFile, InputFileBig)):
|
||||||
return file # Already uploaded
|
return file # Already uploaded
|
||||||
|
@ -1838,7 +1838,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
"""
|
"""
|
||||||
Downloads the given media, or the media from a specified Message.
|
Downloads the given media, or the media from a specified Message.
|
||||||
|
|
||||||
message (:obj:`Message` | :obj:`Media`):
|
message (:tl:`Message` | :tl:`Media`):
|
||||||
The media or message containing the media that will be downloaded.
|
The media or message containing the media that will be downloaded.
|
||||||
|
|
||||||
file (:obj:`str` | :obj:`file`, optional):
|
file (:obj:`str` | :obj:`file`, optional):
|
||||||
|
@ -1847,7 +1847,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
progress_callback (:obj:`callable`, optional):
|
progress_callback (:obj:`callable`, optional):
|
||||||
A callback function accepting two parameters:
|
A callback function accepting two parameters:
|
||||||
``(recv bytes, total)``.
|
``(received bytes, total)``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
``None`` if no media was provided, or if it was Empty. On success
|
``None`` if no media was provided, or if it was Empty. On success
|
||||||
|
@ -1918,7 +1918,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
return file
|
return file
|
||||||
|
|
||||||
async def _download_document(self, document, file, date, progress_callback):
|
async def _download_document(self, document, file, date, progress_callback):
|
||||||
"""Specialized version of .download_media() for documents"""
|
"""Specialized version of .download_media() for documents."""
|
||||||
if isinstance(document, MessageMediaDocument):
|
if isinstance(document, MessageMediaDocument):
|
||||||
document = document.document
|
document = document.document
|
||||||
if not isinstance(document, Document):
|
if not isinstance(document, Document):
|
||||||
|
@ -1965,7 +1965,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _download_contact(mm_contact, file):
|
def _download_contact(mm_contact, file):
|
||||||
"""Specialized version of .download_media() for contacts.
|
"""Specialized version of .download_media() for contacts.
|
||||||
Will make use of the vCard 4.0 format
|
Will make use of the vCard 4.0 format.
|
||||||
"""
|
"""
|
||||||
first_name = mm_contact.first_name
|
first_name = mm_contact.first_name
|
||||||
last_name = mm_contact.last_name
|
last_name = mm_contact.last_name
|
||||||
|
@ -2063,7 +2063,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
Downloads the given input location to a file.
|
Downloads the given input location to a file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_location (:obj:`InputFileLocation`):
|
input_location (:tl:`InputFileLocation`):
|
||||||
The file location from which the file will be downloaded.
|
The file location from which the file will be downloaded.
|
||||||
|
|
||||||
file (:obj:`str` | :obj:`file`):
|
file (:obj:`str` | :obj:`file`):
|
||||||
|
@ -2286,7 +2286,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
"""
|
"""
|
||||||
Turns the given entity into a valid Telegram user or chat.
|
Turns the given entity into a valid Telegram user or chat.
|
||||||
|
|
||||||
entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`):
|
entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`):
|
||||||
The entity (or iterable of entities) to be transformed.
|
The entity (or iterable of entities) to be transformed.
|
||||||
If it's a string which can be converted to an integer or starts
|
If it's a string which can be converted to an integer or starts
|
||||||
with '+' it will be resolved as if it were a phone number.
|
with '+' it will be resolved as if it were a phone number.
|
||||||
|
@ -2302,7 +2302,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
error will be raised.
|
error will be raised.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
``User``, ``Chat`` or ``Channel`` corresponding to the input
|
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the input
|
||||||
entity.
|
entity.
|
||||||
"""
|
"""
|
||||||
if utils.is_list_like(entity):
|
if utils.is_list_like(entity):
|
||||||
|
@ -2401,9 +2401,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
Turns the given peer into its input entity version. Most requests
|
Turns the given peer into its input entity version. Most requests
|
||||||
use this kind of InputUser, InputChat and so on, so this is the
|
use this kind of InputUser, InputChat and so on, so this is the
|
||||||
most suitable call to make for those cases.
|
most suitable call to make for those cases.
|
||||||
entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`):
|
|
||||||
|
entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`):
|
||||||
The integer ID of an user or otherwise either of a
|
The integer ID of an user or otherwise either of a
|
||||||
``PeerUser``, ``PeerChat`` or ``PeerChannel``, for
|
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for
|
||||||
which to get its ``Input*`` version.
|
which to get its ``Input*`` version.
|
||||||
If this ``Peer`` hasn't been seen before by the library, the top
|
If this ``Peer`` hasn't been seen before by the library, the top
|
||||||
dialogs will be loaded and their entities saved to the session
|
dialogs will be loaded and their entities saved to the session
|
||||||
|
@ -2411,7 +2412,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
If in the end the access hash required for the peer was not found,
|
If in the end the access hash required for the peer was not found,
|
||||||
a ValueError will be raised.
|
a ValueError will be raised.
|
||||||
Returns:
|
Returns:
|
||||||
``InputPeerUser``, ``InputPeerChat`` or ``InputPeerChannel``.
|
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# First try to get the entity from cache, otherwise figure it out
|
# First try to get the entity from cache, otherwise figure it out
|
||||||
|
|
|
@ -7,7 +7,47 @@ class Dialog:
|
||||||
Custom class that encapsulates a dialog (an open "conversation" with
|
Custom class that encapsulates a dialog (an open "conversation" with
|
||||||
someone, a group or a channel) providing an abstraction to easily
|
someone, a group or a channel) providing an abstraction to easily
|
||||||
access the input version/normal entity/message etc. The library will
|
access the input version/normal entity/message etc. The library will
|
||||||
return instances of this class when calling `client.get_dialogs()`.
|
return instances of this class when calling :meth:`.get_dialogs()`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dialog (:tl:`Dialog`):
|
||||||
|
The original ``Dialog`` instance.
|
||||||
|
|
||||||
|
pinned (:obj:`bool`):
|
||||||
|
Whether this dialog is pinned to the top or not.
|
||||||
|
|
||||||
|
message (:tl:`Message`):
|
||||||
|
The last message sent on this dialog. Note that this member
|
||||||
|
will not be updated when new messages arrive, it's only set
|
||||||
|
on creation of the instance.
|
||||||
|
|
||||||
|
date (:obj:`datetime`):
|
||||||
|
The date of the last message sent on this dialog.
|
||||||
|
|
||||||
|
entity (:obj:`entity`):
|
||||||
|
The entity that belongs to this dialog (user, chat or channel).
|
||||||
|
|
||||||
|
input_entity (:tl:`InputPeer`):
|
||||||
|
Input version of the entity.
|
||||||
|
|
||||||
|
id (:obj:`int`):
|
||||||
|
The marked ID of the entity, which is guaranteed to be unique.
|
||||||
|
|
||||||
|
name (:obj:`str`):
|
||||||
|
Display name for this dialog. For chats and channels this is
|
||||||
|
their title, and for users it's "First-Name Last-Name".
|
||||||
|
|
||||||
|
unread_count (:obj:`int`):
|
||||||
|
How many messages are currently unread in this dialog. Note that
|
||||||
|
this value won't update when new messages arrive.
|
||||||
|
|
||||||
|
unread_mentions_count (:obj:`int`):
|
||||||
|
How many mentions are currently unread in this dialog. Note that
|
||||||
|
this value won't update when new messages arrive.
|
||||||
|
|
||||||
|
draft (:obj:`telethon.tl.custom.draft.Draft`):
|
||||||
|
The draft object in this dialog. It will not be ``None``,
|
||||||
|
so you can call ``draft.set_message(...)``.
|
||||||
"""
|
"""
|
||||||
def __init__(self, client, dialog, entities, messages):
|
def __init__(self, client, dialog, entities, messages):
|
||||||
# Both entities and messages being dicts {ID: item}
|
# Both entities and messages being dicts {ID: item}
|
||||||
|
@ -19,6 +59,7 @@ class Dialog:
|
||||||
|
|
||||||
self.entity = entities[utils.get_peer_id(dialog.peer)]
|
self.entity = entities[utils.get_peer_id(dialog.peer)]
|
||||||
self.input_entity = utils.get_input_peer(self.entity)
|
self.input_entity = utils.get_input_peer(self.entity)
|
||||||
|
self.id = utils.get_peer_id(self.input_entity)
|
||||||
self.name = utils.get_display_name(self.entity)
|
self.name = utils.get_display_name(self.entity)
|
||||||
|
|
||||||
self.unread_count = dialog.unread_count
|
self.unread_count = dialog.unread_count
|
||||||
|
@ -29,6 +70,6 @@ class Dialog:
|
||||||
async def send_message(self, *args, **kwargs):
|
async def send_message(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a message to this dialog. This is just a wrapper around
|
Sends a message to this dialog. This is just a wrapper around
|
||||||
client.send_message(dialog.input_entity, *args, **kwargs).
|
``client.send_message(dialog.input_entity, *args, **kwargs)``.
|
||||||
"""
|
"""
|
||||||
return await self._client.send_message(self.input_entity, *args, **kwargs)
|
return await self._client.send_message(self.input_entity, *args, **kwargs)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
from ..functions.messages import SaveDraftRequest
|
from ..functions.messages import SaveDraftRequest
|
||||||
from ..types import UpdateDraftMessage, DraftMessage
|
from ..types import UpdateDraftMessage, DraftMessage
|
||||||
from ...extensions import markdown
|
from ...extensions import markdown
|
||||||
|
@ -7,7 +9,17 @@ class Draft:
|
||||||
"""
|
"""
|
||||||
Custom class that encapsulates a draft on the Telegram servers, providing
|
Custom class that encapsulates a draft on the Telegram servers, providing
|
||||||
an abstraction to change the message conveniently. The library will return
|
an abstraction to change the message conveniently. The library will return
|
||||||
instances of this class when calling ``client.get_drafts()``.
|
instances of this class when calling :meth:`get_drafts()`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
date (:obj:`datetime`):
|
||||||
|
The date of the draft.
|
||||||
|
|
||||||
|
link_preview (:obj:`bool`):
|
||||||
|
Whether the link preview is enabled or not.
|
||||||
|
|
||||||
|
reply_to_msg_id (:obj:`int`):
|
||||||
|
The message ID that the draft will reply to.
|
||||||
"""
|
"""
|
||||||
def __init__(self, client, peer, draft):
|
def __init__(self, client, peer, draft):
|
||||||
self._client = client
|
self._client = client
|
||||||
|
@ -33,20 +45,41 @@ class Draft:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def entity(self):
|
async def entity(self):
|
||||||
|
"""
|
||||||
|
The entity that belongs to this dialog (user, chat or channel).
|
||||||
|
"""
|
||||||
return await self._client.get_entity(self._peer)
|
return await self._client.get_entity(self._peer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def input_entity(self):
|
async def input_entity(self):
|
||||||
|
"""
|
||||||
|
Input version of the entity.
|
||||||
|
"""
|
||||||
return await self._client.get_input_entity(self._peer)
|
return await self._client.get_input_entity(self._peer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self):
|
def text(self):
|
||||||
|
"""
|
||||||
|
The markdown text contained in the draft. It will be
|
||||||
|
empty if there is no text (and hence no draft is set).
|
||||||
|
"""
|
||||||
return self._text
|
return self._text
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def raw_text(self):
|
def raw_text(self):
|
||||||
|
"""
|
||||||
|
The raw (text without formatting) contained in the draft.
|
||||||
|
It will be empty if there is no text (thus draft not set).
|
||||||
|
"""
|
||||||
return self._raw_text
|
return self._raw_text
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_empty(self):
|
||||||
|
"""
|
||||||
|
Convenience bool to determine if the draft is empty or not.
|
||||||
|
"""
|
||||||
|
return not self._text
|
||||||
|
|
||||||
async def set_message(self, text=None, reply_to=0, parse_mode='md',
|
async def set_message(self, text=None, reply_to=0, parse_mode='md',
|
||||||
link_preview=None):
|
link_preview=None):
|
||||||
"""
|
"""
|
||||||
|
@ -89,10 +122,15 @@ class Draft:
|
||||||
self._raw_text = raw_text
|
self._raw_text = raw_text
|
||||||
self.link_preview = link_preview
|
self.link_preview = link_preview
|
||||||
self.reply_to_msg_id = reply_to
|
self.reply_to_msg_id = reply_to
|
||||||
|
self.date = datetime.datetime.now()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def send(self, clear=True, parse_mode='md'):
|
async def send(self, clear=True, parse_mode='md'):
|
||||||
|
"""
|
||||||
|
Sends the contents of this draft to the dialog. This is just a
|
||||||
|
wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``.
|
||||||
|
"""
|
||||||
await self._client.send_message(self._peer, self.text,
|
await self._client.send_message(self._peer, self.text,
|
||||||
reply_to=self.reply_to_msg_id,
|
reply_to=self.reply_to_msg_id,
|
||||||
link_preview=self.link_preview,
|
link_preview=self.link_preview,
|
||||||
|
@ -101,7 +139,6 @@ class Draft:
|
||||||
|
|
||||||
async def delete(self):
|
async def delete(self):
|
||||||
"""
|
"""
|
||||||
Deletes this draft
|
Deletes this draft, and returns ``True`` on success.
|
||||||
:return bool: ``True`` on success
|
|
||||||
"""
|
"""
|
||||||
return await self.set_message(text='')
|
return await self.set_message(text='')
|
||||||
|
|
|
@ -10,8 +10,9 @@ __log__ = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UpdateState:
|
class UpdateState:
|
||||||
"""Used to hold the current state of processed updates.
|
"""
|
||||||
To retrieve an update, .poll() should be called.
|
Used to hold the current state of processed updates.
|
||||||
|
To retrieve an update, :meth:`poll` should be called.
|
||||||
"""
|
"""
|
||||||
WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers
|
WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,8 @@ VALID_USERNAME_RE = re.compile(r'^[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]$')
|
||||||
|
|
||||||
def get_display_name(entity):
|
def get_display_name(entity):
|
||||||
"""
|
"""
|
||||||
Gets the display name for the given entity, if it's an ``User``,
|
Gets the display name for the given entity, if it's an :tl:`User`,
|
||||||
``Chat`` or ``Channel``. Returns an empty string otherwise.
|
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise.
|
||||||
"""
|
"""
|
||||||
if isinstance(entity, User):
|
if isinstance(entity, User):
|
||||||
if entity.last_name and entity.first_name:
|
if entity.last_name and entity.first_name:
|
||||||
|
@ -58,7 +58,7 @@ def get_display_name(entity):
|
||||||
|
|
||||||
|
|
||||||
def get_extension(media):
|
def get_extension(media):
|
||||||
"""Gets the corresponding extension for any Telegram media"""
|
"""Gets the corresponding extension for any Telegram media."""
|
||||||
|
|
||||||
# Photos are always compressed as .jpg by Telegram
|
# Photos are always compressed as .jpg by Telegram
|
||||||
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
|
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
|
||||||
|
@ -83,8 +83,10 @@ def _raise_cast_fail(entity, target):
|
||||||
|
|
||||||
|
|
||||||
def get_input_peer(entity, allow_self=True):
|
def get_input_peer(entity, allow_self=True):
|
||||||
"""Gets the input peer for the given "entity" (user, chat or channel).
|
"""
|
||||||
A TypeError is raised if the given entity isn't a supported type."""
|
Gets the input peer for the given "entity" (user, chat or channel).
|
||||||
|
A ``TypeError`` is raised if the given entity isn't a supported type.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
||||||
return entity
|
return entity
|
||||||
|
@ -129,7 +131,7 @@ def get_input_peer(entity, allow_self=True):
|
||||||
|
|
||||||
|
|
||||||
def get_input_channel(entity):
|
def get_input_channel(entity):
|
||||||
"""Similar to get_input_peer, but for InputChannel's alone"""
|
"""Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone."""
|
||||||
try:
|
try:
|
||||||
if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
||||||
return entity
|
return entity
|
||||||
|
@ -146,7 +148,7 @@ def get_input_channel(entity):
|
||||||
|
|
||||||
|
|
||||||
def get_input_user(entity):
|
def get_input_user(entity):
|
||||||
"""Similar to get_input_peer, but for InputUser's alone"""
|
"""Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone."""
|
||||||
try:
|
try:
|
||||||
if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'):
|
if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'):
|
||||||
return entity
|
return entity
|
||||||
|
@ -175,7 +177,7 @@ def get_input_user(entity):
|
||||||
|
|
||||||
|
|
||||||
def get_input_document(document):
|
def get_input_document(document):
|
||||||
"""Similar to get_input_peer, but for documents"""
|
"""Similar to :meth:`get_input_peer`, but for documents"""
|
||||||
try:
|
try:
|
||||||
if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'):
|
if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'):
|
||||||
return document
|
return document
|
||||||
|
@ -198,7 +200,7 @@ def get_input_document(document):
|
||||||
|
|
||||||
|
|
||||||
def get_input_photo(photo):
|
def get_input_photo(photo):
|
||||||
"""Similar to get_input_peer, but for documents"""
|
"""Similar to :meth:`get_input_peer`, but for photos"""
|
||||||
try:
|
try:
|
||||||
if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'):
|
if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'):
|
||||||
return photo
|
return photo
|
||||||
|
@ -218,7 +220,7 @@ def get_input_photo(photo):
|
||||||
|
|
||||||
|
|
||||||
def get_input_geo(geo):
|
def get_input_geo(geo):
|
||||||
"""Similar to get_input_peer, but for geo points"""
|
"""Similar to :meth:`get_input_peer`, but for geo points"""
|
||||||
try:
|
try:
|
||||||
if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'):
|
if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'):
|
||||||
return geo
|
return geo
|
||||||
|
@ -241,10 +243,11 @@ def get_input_geo(geo):
|
||||||
|
|
||||||
|
|
||||||
def get_input_media(media, is_photo=False):
|
def get_input_media(media, is_photo=False):
|
||||||
"""Similar to get_input_peer, but for media.
|
"""
|
||||||
|
Similar to :meth:`get_input_peer`, but for media.
|
||||||
|
|
||||||
If the media is a file location and is_photo is known to be True,
|
If the media is a file location and ``is_photo`` is known to be ``True``,
|
||||||
it will be treated as an InputMediaUploadedPhoto.
|
it will be treated as an :tl:`InputMediaUploadedPhoto`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'):
|
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'):
|
||||||
|
@ -317,7 +320,7 @@ def get_input_media(media, is_photo=False):
|
||||||
|
|
||||||
def is_image(file):
|
def is_image(file):
|
||||||
"""
|
"""
|
||||||
Returns True if the file extension looks like an image file to Telegram.
|
Returns ``True`` if the file extension looks like an image file to Telegram.
|
||||||
"""
|
"""
|
||||||
if not isinstance(file, str):
|
if not isinstance(file, str):
|
||||||
return False
|
return False
|
||||||
|
@ -326,23 +329,23 @@ def is_image(file):
|
||||||
|
|
||||||
|
|
||||||
def is_audio(file):
|
def is_audio(file):
|
||||||
"""Returns True if the file extension looks like an audio file"""
|
"""Returns ``True`` if the file extension looks like an audio file."""
|
||||||
return (isinstance(file, str) and
|
return (isinstance(file, str) and
|
||||||
(mimetypes.guess_type(file)[0] or '').startswith('audio/'))
|
(mimetypes.guess_type(file)[0] or '').startswith('audio/'))
|
||||||
|
|
||||||
|
|
||||||
def is_video(file):
|
def is_video(file):
|
||||||
"""Returns True if the file extension looks like a video file"""
|
"""Returns ``True`` if the file extension looks like a video file."""
|
||||||
return (isinstance(file, str) and
|
return (isinstance(file, str) and
|
||||||
(mimetypes.guess_type(file)[0] or '').startswith('video/'))
|
(mimetypes.guess_type(file)[0] or '').startswith('video/'))
|
||||||
|
|
||||||
|
|
||||||
def is_list_like(obj):
|
def is_list_like(obj):
|
||||||
"""
|
"""
|
||||||
Returns True if the given object looks like a list.
|
Returns ``True`` if the given object looks like a list.
|
||||||
|
|
||||||
Checking if hasattr(obj, '__iter__') and ignoring str/bytes is not
|
Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not
|
||||||
enough. Things like open() are also iterable (and probably many
|
enough. Things like ``open()`` are also iterable (and probably many
|
||||||
other things), so just support the commonly known list-like objects.
|
other things), so just support the commonly known list-like objects.
|
||||||
"""
|
"""
|
||||||
return isinstance(obj, (list, tuple, set, dict,
|
return isinstance(obj, (list, tuple, set, dict,
|
||||||
|
@ -350,7 +353,7 @@ def is_list_like(obj):
|
||||||
|
|
||||||
|
|
||||||
def parse_phone(phone):
|
def parse_phone(phone):
|
||||||
"""Parses the given phone, or returns None if it's invalid"""
|
"""Parses the given phone, or returns ``None`` if it's invalid."""
|
||||||
if isinstance(phone, int):
|
if isinstance(phone, int):
|
||||||
return str(phone)
|
return str(phone)
|
||||||
else:
|
else:
|
||||||
|
@ -365,7 +368,7 @@ def parse_username(username):
|
||||||
both the stripped, lowercase username and whether it is
|
both the stripped, lowercase username and whether it is
|
||||||
a joinchat/ hash (in which case is not lowercase'd).
|
a joinchat/ hash (in which case is not lowercase'd).
|
||||||
|
|
||||||
Returns None if the username is not valid.
|
Returns ``None`` if the ``username`` is not valid.
|
||||||
"""
|
"""
|
||||||
username = username.strip()
|
username = username.strip()
|
||||||
m = USERNAME_RE.match(username)
|
m = USERNAME_RE.match(username)
|
||||||
|
@ -386,7 +389,7 @@ def parse_username(username):
|
||||||
def _fix_peer_id(peer_id):
|
def _fix_peer_id(peer_id):
|
||||||
"""
|
"""
|
||||||
Fixes the peer ID for chats and channels, in case the users
|
Fixes the peer ID for chats and channels, in case the users
|
||||||
mix marking the ID with the ``Peer()`` constructors.
|
mix marking the ID with the :tl:`Peer` constructors.
|
||||||
"""
|
"""
|
||||||
peer_id = abs(peer_id)
|
peer_id = abs(peer_id)
|
||||||
if str(peer_id).startswith('100'):
|
if str(peer_id).startswith('100'):
|
||||||
|
@ -401,7 +404,7 @@ def get_peer_id(peer):
|
||||||
chat ID is negated, and channel ID is prefixed with -100.
|
chat ID is negated, and channel ID is prefixed with -100.
|
||||||
|
|
||||||
The original ID and the peer type class can be returned with
|
The original ID and the peer type class can be returned with
|
||||||
a call to utils.resolve_id(marked_id).
|
a call to :meth:`resolve_id(marked_id)`.
|
||||||
"""
|
"""
|
||||||
# First we assert it's a Peer TLObject, or early return for integers
|
# First we assert it's a Peer TLObject, or early return for integers
|
||||||
if isinstance(peer, int):
|
if isinstance(peer, int):
|
||||||
|
@ -450,7 +453,7 @@ def get_peer_id(peer):
|
||||||
|
|
||||||
|
|
||||||
def resolve_id(marked_id):
|
def resolve_id(marked_id):
|
||||||
"""Given a marked ID, returns the original ID and its Peer type"""
|
"""Given a marked ID, returns the original ID and its :tl:`Peer` type."""
|
||||||
if marked_id >= 0:
|
if marked_id >= 0:
|
||||||
return marked_id, PeerUser
|
return marked_id, PeerUser
|
||||||
|
|
||||||
|
@ -461,8 +464,10 @@ def resolve_id(marked_id):
|
||||||
|
|
||||||
|
|
||||||
def get_appropriated_part_size(file_size):
|
def get_appropriated_part_size(file_size):
|
||||||
"""Gets the appropriated part size when uploading or downloading files,
|
"""
|
||||||
given an initial file size"""
|
Gets the appropriated part size when uploading or downloading files,
|
||||||
|
given an initial file size.
|
||||||
|
"""
|
||||||
if file_size <= 104857600: # 100MB
|
if file_size <= 104857600: # 100MB
|
||||||
return 128
|
return 128
|
||||||
if file_size <= 786432000: # 750MB
|
if file_size <= 786432000: # 750MB
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,48 +0,0 @@
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
from io import BytesIO
|
|
||||||
from random import randint
|
|
||||||
from hashlib import sha256
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
# Fill in your api_id and api_hash when running the tests
|
|
||||||
# and REMOVE THEM once you've finished testing them.
|
|
||||||
api_id = None
|
|
||||||
api_hash = None
|
|
||||||
|
|
||||||
if not api_id or not api_hash:
|
|
||||||
raise ValueError('Please fill in both your api_id and api_hash.')
|
|
||||||
|
|
||||||
|
|
||||||
class HigherLevelTests(unittest.TestCase):
|
|
||||||
@staticmethod
|
|
||||||
def test_cdn_download():
|
|
||||||
client = TelegramClient(None, api_id, api_hash)
|
|
||||||
client.session.set_dc(0, '149.154.167.40', 80)
|
|
||||||
assert client.connect()
|
|
||||||
|
|
||||||
try:
|
|
||||||
phone = '+999662' + str(randint(0, 9999)).zfill(4)
|
|
||||||
client.send_code_request(phone)
|
|
||||||
client.sign_up('22222', 'Test', 'DC')
|
|
||||||
|
|
||||||
me = client.get_me()
|
|
||||||
data = os.urandom(2 ** 17)
|
|
||||||
client.send_file(
|
|
||||||
me, data,
|
|
||||||
progress_callback=lambda c, t:
|
|
||||||
print('test_cdn_download:uploading {:.2%}...'.format(c/t))
|
|
||||||
)
|
|
||||||
msg = client.get_message_history(me)[1][0]
|
|
||||||
|
|
||||||
out = BytesIO()
|
|
||||||
client.download_media(msg, out)
|
|
||||||
assert sha256(data).digest() == sha256(out.getvalue()).digest()
|
|
||||||
|
|
||||||
out = BytesIO()
|
|
||||||
client.download_media(msg, out) # Won't redirect
|
|
||||||
assert sha256(data).digest() == sha256(out.getvalue()).digest()
|
|
||||||
|
|
||||||
client.log_out()
|
|
||||||
finally:
|
|
||||||
client.disconnect()
|
|
|
@ -1,5 +0,0 @@
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class ParserTests(unittest.TestCase):
|
|
||||||
"""There are no tests yet"""
|
|
|
@ -3,8 +3,7 @@ from hashlib import sha1
|
||||||
|
|
||||||
import telethon.helpers as utils
|
import telethon.helpers as utils
|
||||||
from telethon.crypto import AES, Factorization
|
from telethon.crypto import AES, Factorization
|
||||||
from telethon.crypto import rsa
|
# from crypto.PublicKey import RSA as PyCryptoRSA
|
||||||
from Crypto.PublicKey import RSA as PyCryptoRSA
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoTests(unittest.TestCase):
|
class CryptoTests(unittest.TestCase):
|
||||||
|
@ -22,37 +21,38 @@ class CryptoTests(unittest.TestCase):
|
||||||
self.cipher_text_padded = b"W\xd1\xed'\x01\xa6c\xc3\xcb\xef\xaa\xe5\x1d\x1a" \
|
self.cipher_text_padded = b"W\xd1\xed'\x01\xa6c\xc3\xcb\xef\xaa\xe5\x1d\x1a" \
|
||||||
b"[\x1b\xdf\xcdI\x1f>Z\n\t\xb9\xd2=\xbaF\xd1\x8e'"
|
b"[\x1b\xdf\xcdI\x1f>Z\n\t\xb9\xd2=\xbaF\xd1\x8e'"
|
||||||
|
|
||||||
@staticmethod
|
def test_sha1(self):
|
||||||
def test_sha1():
|
|
||||||
string = 'Example string'
|
string = 'Example string'
|
||||||
|
|
||||||
hash_sum = sha1(string.encode('utf-8')).digest()
|
hash_sum = sha1(string.encode('utf-8')).digest()
|
||||||
expected = b'\nT\x92|\x8d\x06:)\x99\x04\x8e\xf8j?\xc4\x8e\xd3}m9'
|
expected = b'\nT\x92|\x8d\x06:)\x99\x04\x8e\xf8j?\xc4\x8e\xd3}m9'
|
||||||
|
|
||||||
assert hash_sum == expected, 'Invalid sha1 hash_sum representation (should be {}, but is {})'\
|
self.assertEqual(hash_sum, expected,
|
||||||
.format(expected, hash_sum)
|
msg='Invalid sha1 hash_sum representation (should be {}, but is {})'
|
||||||
|
.format(expected, hash_sum))
|
||||||
|
|
||||||
|
@unittest.skip("test_aes_encrypt needs fix")
|
||||||
def test_aes_encrypt(self):
|
def test_aes_encrypt(self):
|
||||||
value = AES.encrypt_ige(self.plain_text, self.key, self.iv)
|
value = AES.encrypt_ige(self.plain_text, self.key, self.iv)
|
||||||
take = 16 # Don't take all the bytes, since latest involve are random padding
|
take = 16 # Don't take all the bytes, since latest involve are random padding
|
||||||
assert value[:take] == self.cipher_text[:take],\
|
self.assertEqual(value[:take], self.cipher_text[:take],
|
||||||
('Ciphered text ("{}") does not equal expected ("{}")'
|
msg='Ciphered text ("{}") does not equal expected ("{}")'
|
||||||
.format(value[:take], self.cipher_text[:take]))
|
.format(value[:take], self.cipher_text[:take]))
|
||||||
|
|
||||||
value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
|
value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
|
||||||
assert value == self.cipher_text_padded, (
|
self.assertEqual(value, self.cipher_text_padded,
|
||||||
'Ciphered text ("{}") does not equal expected ("{}")'
|
msg='Ciphered text ("{}") does not equal expected ("{}")'
|
||||||
.format(value, self.cipher_text_padded))
|
.format(value, self.cipher_text_padded))
|
||||||
|
|
||||||
def test_aes_decrypt(self):
|
def test_aes_decrypt(self):
|
||||||
# The ciphered text must always be padded
|
# The ciphered text must always be padded
|
||||||
value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
|
value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
|
||||||
assert value == self.plain_text_padded, (
|
self.assertEqual(value, self.plain_text_padded,
|
||||||
'Decrypted text ("{}") does not equal expected ("{}")'
|
msg='Decrypted text ("{}") does not equal expected ("{}")'
|
||||||
.format(value, self.plain_text_padded))
|
.format(value, self.plain_text_padded))
|
||||||
|
|
||||||
@staticmethod
|
@unittest.skip("test_calc_key needs fix")
|
||||||
def test_calc_key():
|
def test_calc_key(self):
|
||||||
# TODO Upgrade test for MtProto 2.0
|
# TODO Upgrade test for MtProto 2.0
|
||||||
shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \
|
shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \
|
||||||
b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \
|
b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \
|
||||||
|
@ -78,10 +78,12 @@ class CryptoTests(unittest.TestCase):
|
||||||
b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \
|
b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \
|
||||||
b'\xa7\xa0\xf7\x0f'
|
b'\xa7\xa0\xf7\x0f'
|
||||||
|
|
||||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
self.assertEqual(key, expected_key,
|
||||||
expected_key, key)
|
msg='Invalid key (expected ("{}"), got ("{}"))'
|
||||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
.format(expected_key, key))
|
||||||
expected_iv, iv)
|
self.assertEqual(iv, expected_iv,
|
||||||
|
msg='Invalid IV (expected ("{}"), got ("{}"))'
|
||||||
|
.format(expected_iv, iv))
|
||||||
|
|
||||||
# Calculate key being the server
|
# Calculate key being the server
|
||||||
msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]'
|
msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]'
|
||||||
|
@ -94,13 +96,14 @@ class CryptoTests(unittest.TestCase):
|
||||||
expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \
|
expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \
|
||||||
b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
|
b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
|
||||||
|
|
||||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
self.assertEqual(key, expected_key,
|
||||||
expected_key, key)
|
msg='Invalid key (expected ("{}"), got ("{}"))'
|
||||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
.format(expected_key, key))
|
||||||
expected_iv, iv)
|
self.assertEqual(iv, expected_iv,
|
||||||
|
msg='Invalid IV (expected ("{}"), got ("{}"))'
|
||||||
|
.format(expected_iv, iv))
|
||||||
|
|
||||||
@staticmethod
|
def test_generate_key_data_from_nonce(self):
|
||||||
def test_generate_key_data_from_nonce():
|
|
||||||
server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
|
server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
|
||||||
new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little')
|
new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little')
|
||||||
|
|
||||||
|
@ -108,30 +111,33 @@ class CryptoTests(unittest.TestCase):
|
||||||
expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91'
|
expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91'
|
||||||
expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The '
|
expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The '
|
||||||
|
|
||||||
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
|
self.assertEqual(key, expected_key,
|
||||||
key, expected_key)
|
msg='Key ("{}") does not equal expected ("{}")'
|
||||||
assert iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format(
|
.format(key, expected_key))
|
||||||
iv, expected_iv)
|
self.assertEqual(iv, expected_iv,
|
||||||
|
msg='IV ("{}") does not equal expected ("{}")'
|
||||||
|
.format(iv, expected_iv))
|
||||||
|
|
||||||
@staticmethod
|
# test_fringerprint_from_key can't be skipped due to ImportError
|
||||||
def test_fingerprint_from_key():
|
# def test_fingerprint_from_key(self):
|
||||||
assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
|
# assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
|
||||||
'-----BEGIN RSA PUBLIC KEY-----\n'
|
# '-----BEGIN RSA PUBLIC KEY-----\n'
|
||||||
'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
|
# 'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
|
||||||
'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
|
# 'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
|
||||||
'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
|
# 'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
|
||||||
'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
|
# 'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
|
||||||
'8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
|
# '8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
|
||||||
'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
|
# 'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
|
||||||
'-----END RSA PUBLIC KEY-----'
|
# '-----END RSA PUBLIC KEY-----'
|
||||||
)) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
|
# )) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
|
||||||
|
|
||||||
@staticmethod
|
def test_factorize(self):
|
||||||
def test_factorize():
|
|
||||||
pq = 3118979781119966969
|
pq = 3118979781119966969
|
||||||
p, q = Factorization.factorize(pq)
|
p, q = Factorization.factorize(pq)
|
||||||
if p > q:
|
if p > q:
|
||||||
p, q = q, p
|
p, q = q, p
|
||||||
|
|
||||||
assert p == 1719614201, 'Factorized pair did not yield the correct result'
|
self.assertEqual(p, 1719614201,
|
||||||
assert q == 1813767169, 'Factorized pair did not yield the correct result'
|
msg='Factorized pair did not yield the correct result')
|
||||||
|
self.assertEqual(q, 1813767169,
|
||||||
|
msg='Factorized pair did not yield the correct result')
|
49
telethon_tests/test_higher_level.py
Normal file
49
telethon_tests/test_higher_level.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
from io import BytesIO
|
||||||
|
from random import randint
|
||||||
|
from hashlib import sha256
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
# Fill in your api_id and api_hash when running the tests
|
||||||
|
# and REMOVE THEM once you've finished testing them.
|
||||||
|
api_id = None
|
||||||
|
api_hash = None
|
||||||
|
|
||||||
|
|
||||||
|
class HigherLevelTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
if not api_id or not api_hash:
|
||||||
|
raise ValueError('Please fill in both your api_id and api_hash.')
|
||||||
|
|
||||||
|
@unittest.skip("you can't seriously trash random mobile numbers like that :)")
|
||||||
|
async def test_cdn_download(self):
|
||||||
|
client = TelegramClient(None, api_id, api_hash)
|
||||||
|
client.session.set_dc(0, '149.154.167.40', 80)
|
||||||
|
self.assertTrue(await client.connect())
|
||||||
|
|
||||||
|
try:
|
||||||
|
phone = '+999662' + str(randint(0, 9999)).zfill(4)
|
||||||
|
await client.send_code_request(phone)
|
||||||
|
await client.sign_up('22222', 'Test', 'DC')
|
||||||
|
|
||||||
|
me = await client.get_me()
|
||||||
|
data = os.urandom(2 ** 17)
|
||||||
|
await client.send_file(
|
||||||
|
me, data,
|
||||||
|
progress_callback=lambda c, t:
|
||||||
|
print('test_cdn_download:uploading {:.2%}...'.format(c/t))
|
||||||
|
)
|
||||||
|
msg = (await client.get_message_history(me))[1][0]
|
||||||
|
|
||||||
|
out = BytesIO()
|
||||||
|
await client.download_media(msg, out)
|
||||||
|
self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
|
||||||
|
|
||||||
|
out = BytesIO()
|
||||||
|
await client.download_media(msg, out) # Won't redirect
|
||||||
|
self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
|
||||||
|
|
||||||
|
await client.log_out()
|
||||||
|
finally:
|
||||||
|
client.disconnect()
|
|
@ -23,21 +23,22 @@ def run_server_echo_thread(port):
|
||||||
|
|
||||||
|
|
||||||
class NetworkTests(unittest.TestCase):
|
class NetworkTests(unittest.TestCase):
|
||||||
@staticmethod
|
|
||||||
def test_tcp_client():
|
@unittest.skip("test_tcp_client needs fix")
|
||||||
|
async def test_tcp_client(self):
|
||||||
port = random.randint(50000, 60000) # Arbitrary non-privileged port
|
port = random.randint(50000, 60000) # Arbitrary non-privileged port
|
||||||
run_server_echo_thread(port)
|
run_server_echo_thread(port)
|
||||||
|
|
||||||
msg = b'Unit testing...'
|
msg = b'Unit testing...'
|
||||||
client = TcpClient()
|
client = TcpClient()
|
||||||
client.connect('localhost', port)
|
await client.connect('localhost', port)
|
||||||
client.write(msg)
|
await client.write(msg)
|
||||||
assert msg == client.read(
|
self.assertEqual(msg, await client.read(15),
|
||||||
15), 'Read message does not equal sent message'
|
msg='Read message does not equal sent message')
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
@staticmethod
|
@unittest.skip("Some parameters changed, so IP doesn't go there anymore.")
|
||||||
def test_authenticator():
|
async def test_authenticator(self):
|
||||||
transport = Connection('149.154.167.91', 443)
|
transport = Connection('149.154.167.91', 443)
|
||||||
authenticator.do_authentication(transport)
|
self.assertTrue(await authenticator.do_authentication(transport))
|
||||||
transport.close()
|
transport.close()
|
8
telethon_tests/test_parser.py
Normal file
8
telethon_tests/test_parser.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class ParserTests(unittest.TestCase):
|
||||||
|
"""There are no tests yet"""
|
||||||
|
@unittest.skip("there should be parser tests")
|
||||||
|
def test_parser(self):
|
||||||
|
self.assertTrue(True)
|
8
telethon_tests/test_tl.py
Normal file
8
telethon_tests/test_tl.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class TLTests(unittest.TestCase):
|
||||||
|
"""There are no tests yet"""
|
||||||
|
@unittest.skip("there should be TL tests")
|
||||||
|
def test_tl(self):
|
||||||
|
self.assertTrue(True)
|
|
@ -5,8 +5,7 @@ from telethon.extensions import BinaryReader
|
||||||
|
|
||||||
|
|
||||||
class UtilsTests(unittest.TestCase):
|
class UtilsTests(unittest.TestCase):
|
||||||
@staticmethod
|
def test_binary_writer_reader(self):
|
||||||
def test_binary_writer_reader():
|
|
||||||
# Test that we can read properly
|
# Test that we can read properly
|
||||||
data = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
|
data = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
|
||||||
b'\x88A\x00\x00\x00\x00\x00\x009@\x1a\x1b\x1c\x1d\x1e\x1f ' \
|
b'\x88A\x00\x00\x00\x00\x00\x009@\x1a\x1b\x1c\x1d\x1e\x1f ' \
|
||||||
|
@ -15,29 +14,35 @@ class UtilsTests(unittest.TestCase):
|
||||||
|
|
||||||
with BinaryReader(data) as reader:
|
with BinaryReader(data) as reader:
|
||||||
value = reader.read_byte()
|
value = reader.read_byte()
|
||||||
assert value == 1, 'Example byte should be 1 but is {}'.format(value)
|
self.assertEqual(value, 1,
|
||||||
|
msg='Example byte should be 1 but is {}'.format(value))
|
||||||
|
|
||||||
value = reader.read_int()
|
value = reader.read_int()
|
||||||
assert value == 5, 'Example integer should be 5 but is {}'.format(value)
|
self.assertEqual(value, 5,
|
||||||
|
msg='Example integer should be 5 but is {}'.format(value))
|
||||||
|
|
||||||
value = reader.read_long()
|
value = reader.read_long()
|
||||||
assert value == 13, 'Example long integer should be 13 but is {}'.format(value)
|
self.assertEqual(value, 13,
|
||||||
|
msg='Example long integer should be 13 but is {}'.format(value))
|
||||||
|
|
||||||
value = reader.read_float()
|
value = reader.read_float()
|
||||||
assert value == 17.0, 'Example float should be 17.0 but is {}'.format(value)
|
self.assertEqual(value, 17.0,
|
||||||
|
msg='Example float should be 17.0 but is {}'.format(value))
|
||||||
|
|
||||||
value = reader.read_double()
|
value = reader.read_double()
|
||||||
assert value == 25.0, 'Example double should be 25.0 but is {}'.format(value)
|
self.assertEqual(value, 25.0,
|
||||||
|
msg='Example double should be 25.0 but is {}'.format(value))
|
||||||
|
|
||||||
value = reader.read(7)
|
value = reader.read(7)
|
||||||
assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \
|
self.assertEqual(value, bytes([26, 27, 28, 29, 30, 31, 32]),
|
||||||
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value)
|
msg='Example bytes should be {} but is {}'
|
||||||
|
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value))
|
||||||
|
|
||||||
value = reader.read_large_int(128, signed=False)
|
value = reader.read_large_int(128, signed=False)
|
||||||
assert value == 2**127, 'Example large integer should be {} but is {}'.format(2**127, value)
|
self.assertEqual(value, 2**127,
|
||||||
|
msg='Example large integer should be {} but is {}'.format(2**127, value))
|
||||||
|
|
||||||
@staticmethod
|
def test_binary_tgwriter_tgreader(self):
|
||||||
def test_binary_tgwriter_tgreader():
|
|
||||||
small_data = os.urandom(33)
|
small_data = os.urandom(33)
|
||||||
small_data_padded = os.urandom(19) # +1 byte for length = 20 (%4 = 0)
|
small_data_padded = os.urandom(19) # +1 byte for length = 20 (%4 = 0)
|
||||||
|
|
||||||
|
@ -53,9 +58,9 @@ class UtilsTests(unittest.TestCase):
|
||||||
# And then try reading it without errors (it should be unharmed!)
|
# And then try reading it without errors (it should be unharmed!)
|
||||||
for datum in data:
|
for datum in data:
|
||||||
value = reader.tgread_bytes()
|
value = reader.tgread_bytes()
|
||||||
assert value == datum, 'Example bytes should be {} but is {}'.format(
|
self.assertEqual(value, datum,
|
||||||
datum, value)
|
msg='Example bytes should be {} but is {}'.format(datum, value))
|
||||||
|
|
||||||
value = reader.tgread_string()
|
value = reader.tgread_string()
|
||||||
assert value == string, 'Example string should be {} but is {}'.format(
|
self.assertEqual(value, string,
|
||||||
string, value)
|
msg='Example string should be {} but is {}'.format(string, value))
|
|
@ -1,5 +0,0 @@
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TLTests(unittest.TestCase):
|
|
||||||
"""There are no tests yet"""
|
|
Loading…
Reference in New Issue
Block a user