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,
|
||||
// return a pair of arrays with matching "query" elements from "original".
|
||||
//
|
||||
|
@ -86,7 +117,7 @@ function getSearchArray(original, originalu, query) {
|
|||
var destinationu = [];
|
||||
|
||||
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]);
|
||||
destinationu.push(originalu[i]);
|
||||
}
|
||||
|
|
|
@ -17,15 +17,16 @@
|
|||
# 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.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
import os
|
||||
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))
|
||||
|
||||
tl_ref_url = 'https://lonamiwebs.github.io/Telethon'
|
||||
|
||||
|
||||
# -- 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
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc']
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'custom_roles'
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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 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.
|
||||
|
||||
How can we retrieve this ``InputPeer``? We have two options. We manually
|
||||
`construct one`__, for instance:
|
||||
How can we retrieve this :tl:`InputPeer`? We have two options. We manually
|
||||
construct one, for instance:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -64,7 +64,7 @@ Or we call ``.get_input_entity()``:
|
|||
peer = client.get_input_entity('someone')
|
||||
|
||||
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
|
||||
immediate, if you've seen the user before, know their ID, etc.).
|
||||
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/methods/index.html
|
||||
__ 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=...)
|
||||
# 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
|
||||
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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
.. _entities:
|
||||
|
||||
=========================
|
||||
Users, Chats and Channels
|
||||
=========================
|
||||
|
@ -7,16 +9,16 @@ Introduction
|
|||
************
|
||||
|
||||
The library widely uses the concept of "entities". An entity will refer
|
||||
to any ``User``, ``Chat`` or ``Channel`` object that the API may return
|
||||
in response to certain methods, such as ``GetUsersRequest``.
|
||||
to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return
|
||||
in response to certain methods, such as :tl:`GetUsersRequest`.
|
||||
|
||||
.. note::
|
||||
|
||||
When something "entity-like" is required, it means that you need to
|
||||
provide something that can be turned into an entity. These things include,
|
||||
but are not limited to, usernames, exact titles, IDs, ``Peer`` objects,
|
||||
or even entire ``User``, ``Chat`` and ``Channel`` objects and even phone
|
||||
numbers from people you have in your contacts.
|
||||
but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects,
|
||||
or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even
|
||||
phone numbers from people you have in your contacts.
|
||||
|
||||
Getting entities
|
||||
****************
|
||||
|
@ -71,7 +73,7 @@ become possible.
|
|||
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
|
||||
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.
|
||||
|
||||
|
||||
|
@ -88,14 +90,14 @@ Entities vs. Input Entities
|
|||
|
||||
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.
|
||||
``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
|
||||
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
|
||||
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
|
||||
peers alone are not enough to use them. You need to know their hash
|
||||
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,
|
||||
``.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,
|
||||
if you provided the full entity, e.g. an ``User``, the library will convert
|
||||
it to the required ``InputPeer`` automatically for you.
|
||||
if you provided the full entity, e.g. an :tl:`User`, the library will convert
|
||||
it to the required :tl:`InputPeer` automatically for you.
|
||||
|
||||
**You should always favour** ``.get_input_entity()`` **over** ``.get_entity()``
|
||||
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'))
|
||||
|
||||
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.
|
||||
|
|
|
@ -66,6 +66,26 @@ Basic Usage
|
|||
**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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
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
|
||||
only done on object construction.
|
||||
|
||||
|
@ -993,7 +993,7 @@ Bug fixes and enhancements (v0.13.3)
|
|||
.. bugs-fixed-2:
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
~~~~~~~~~
|
||||
|
||||
- **Reconnection** used to fail because it tried invoking things from
|
||||
the ``ReadThread``.
|
||||
|
@ -1009,7 +1009,7 @@ Bug fixes
|
|||
.. enhancements-3:
|
||||
|
||||
Enhancements
|
||||
------------
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- **Request will be retried** up to 5 times by default rather than
|
||||
failing on the first attempt.
|
||||
|
@ -1099,7 +1099,7 @@ outside the buffer.
|
|||
.. additions-2:
|
||||
|
||||
Additions
|
||||
---------
|
||||
~~~~~~~~~
|
||||
|
||||
- The mentioned different connection modes, and a new thread.
|
||||
- You can modify the ``Session`` attributes through the
|
||||
|
@ -1112,7 +1112,7 @@ Additions
|
|||
.. enhancements-4:
|
||||
|
||||
Enhancements
|
||||
------------
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- The low-level socket doesn't use a handcrafted timeout anymore, which
|
||||
should benefit by avoiding the arbitrary ``sleep(0.1)`` that there
|
||||
|
@ -1121,7 +1121,7 @@ Enhancements
|
|||
``code`` was provided.
|
||||
|
||||
Deprecation
|
||||
-----------
|
||||
~~~~~~~~~~~
|
||||
|
||||
- ``.sign_up`` does *not* take a ``phone`` argument anymore. Change
|
||||
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.
|
||||
|
||||
Breaking changes
|
||||
----------------
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
- The ``TelegramClient`` methods ``.send_photo_file()``,
|
||||
``.send_document_file()`` and ``.send_media_file()`` are now a
|
||||
|
@ -1216,7 +1216,7 @@ Breaking changes
|
|||
``.download_contact()`` still exist, but are private.
|
||||
|
||||
Additions
|
||||
---------
|
||||
~~~~~~~~~
|
||||
|
||||
- Updated to **layer 70**!
|
||||
- Both downloading and uploading now support **stream-like objects**.
|
||||
|
@ -1232,7 +1232,7 @@ Additions
|
|||
.. bug-fixes-5:
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
~~~~~~~~~
|
||||
|
||||
- Crashing when migrating to a new layer and receiving old updates
|
||||
should not happen now.
|
||||
|
@ -1372,7 +1372,7 @@ Support for parallel connections (v0.11)
|
|||
**read the whole change log**!
|
||||
|
||||
Breaking changes
|
||||
----------------
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
- Every Telegram error has now its **own class**, so it's easier to
|
||||
fine-tune your ``except``\ 's.
|
||||
|
@ -1384,7 +1384,7 @@ Breaking changes
|
|||
anymore.
|
||||
|
||||
Additions
|
||||
---------
|
||||
~~~~~~~~~
|
||||
|
||||
- A new, more **lightweight class** has been added. The
|
||||
``TelegramBareClient`` is now the base of the normal
|
||||
|
@ -1404,7 +1404,7 @@ Additions
|
|||
.. bug-fixes-6:
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
~~~~~~~~~
|
||||
|
||||
- Received errors are acknowledged to the server, so they don't happen
|
||||
over and over.
|
||||
|
@ -1418,7 +1418,7 @@ Bug fixes
|
|||
not happen anymore.
|
||||
|
||||
Internal changes
|
||||
----------------
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
- Some fixes to the ``JsonSession``.
|
||||
- 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,
|
||||
you can always add someone else with the `AddChatUserRequest`__,
|
||||
which use is very straightforward:
|
||||
you can always add someone else with the `AddChatUserRequest`__, which
|
||||
use is very straightforward, or `InviteToChannelRequest`__ for channels:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# For normal chats
|
||||
from telethon.tl.functions.messages import AddChatUserRequest
|
||||
|
||||
client(AddChatUserRequest(
|
||||
|
@ -69,6 +70,15 @@ which use is very straightforward:
|
|||
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
|
||||
*******************************
|
||||
|
@ -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/messages/import_chat_invite.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
|
||||
|
||||
|
||||
|
@ -225,6 +236,12 @@ use `GetMessagesViewsRequest`__, setting ``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/305
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/409
|
||||
|
|
|
@ -10,3 +10,12 @@ telethon\.tl\.custom\.draft module
|
|||
:undoc-members:
|
||||
: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',
|
||||
]),
|
||||
install_requires=['pyaes', 'rsa',
|
||||
'typing' if version_info < (3, 5) else ""],
|
||||
'typing' if version_info < (3, 5, 2) else ""],
|
||||
extras_require={
|
||||
'cryptg': ['cryptg']
|
||||
}
|
||||
|
|
|
@ -37,8 +37,10 @@ class _EventBuilder(abc.ABC):
|
|||
only matching chats will be handled.
|
||||
|
||||
blacklist_chats (:obj:`bool`, optional):
|
||||
Whether to treat the the list of chats as a blacklist (if
|
||||
it matches it will NOT be handled) or a whitelist (default).
|
||||
Whether to treat the chats as a blacklist instead of
|
||||
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):
|
||||
self.chats = chats
|
||||
|
@ -70,6 +72,7 @@ class _EventBuilder(abc.ABC):
|
|||
|
||||
class _EventCommon(abc.ABC):
|
||||
"""Intermediate class with common things to all events"""
|
||||
_event_name = 'Event'
|
||||
|
||||
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
|
||||
self._entities = {}
|
||||
|
@ -90,13 +93,13 @@ class _EventCommon(abc.ABC):
|
|||
|
||||
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.
|
||||
|
||||
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
|
||||
a tuple of (None, None) if it couldn't be.
|
||||
Returns a tuple of ``(entity, input_peer)`` if it was found, or
|
||||
a tuple of ``(None, None)`` if it couldn't be.
|
||||
"""
|
||||
try:
|
||||
if isinstance(chat, types.InputPeerChannel):
|
||||
|
@ -123,7 +126,7 @@ class _EventCommon(abc.ABC):
|
|||
@property
|
||||
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,
|
||||
but is useful if you don't need those to avoid further
|
||||
requests.
|
||||
|
@ -154,7 +157,7 @@ class _EventCommon(abc.ABC):
|
|||
@property
|
||||
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
|
||||
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.
|
||||
|
@ -178,7 +181,7 @@ class _EventCommon(abc.ABC):
|
|||
|
||||
def to_dict(self):
|
||||
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
|
||||
d['_'] = self.__class__.__name__
|
||||
d['_'] = self._event_name
|
||||
return d
|
||||
|
||||
|
||||
|
@ -196,7 +199,7 @@ class Raw(_EventBuilder):
|
|||
def _name_inner_event(cls):
|
||||
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
|
||||
if hasattr(cls, 'Event'):
|
||||
cls.Event.__name__ = '{}.Event'.format(cls.__name__)
|
||||
cls.Event._event_name = '{}.Event'.format(cls.__name__)
|
||||
else:
|
||||
warnings.warn('Class {} does not have a inner Event'.format(cls))
|
||||
return cls
|
||||
|
@ -310,8 +313,8 @@ class NewMessage(_EventBuilder):
|
|||
Represents the event of a new message.
|
||||
|
||||
Members:
|
||||
message (:obj:`Message`):
|
||||
This is the original ``Message`` object.
|
||||
message (:tl:`Message`):
|
||||
This is the original :tl:`Message` object.
|
||||
|
||||
is_private (:obj:`bool`):
|
||||
True if the message was sent as a private message.
|
||||
|
@ -406,7 +409,7 @@ class NewMessage(_EventBuilder):
|
|||
@property
|
||||
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
|
||||
things like username or similar, but still useful in some cases.
|
||||
|
||||
|
@ -434,7 +437,7 @@ class NewMessage(_EventBuilder):
|
|||
@property
|
||||
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
|
||||
doesn't belong to a channel), so keep that in mind.
|
||||
|
||||
|
@ -474,8 +477,8 @@ class NewMessage(_EventBuilder):
|
|||
@property
|
||||
async def reply_message(self):
|
||||
"""
|
||||
This (:obj:`Message`, optional) will make an API call the first
|
||||
time to get the full ``Message`` object that one was replying to,
|
||||
This optional :tl:`Message` will make an API call the first
|
||||
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.
|
||||
"""
|
||||
if not self.message.reply_to_msg_id:
|
||||
|
@ -498,14 +501,14 @@ class NewMessage(_EventBuilder):
|
|||
@property
|
||||
def forward(self):
|
||||
"""
|
||||
The unmodified (:obj:`MessageFwdHeader`, optional).
|
||||
The unmodified :tl:`MessageFwdHeader`, if present..
|
||||
"""
|
||||
return self.message.fwd_from
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
"""
|
||||
The unmodified (:obj:`MessageMedia`, optional).
|
||||
The unmodified :tl:`MessageMedia`, if present.
|
||||
"""
|
||||
return self.message.media
|
||||
|
||||
|
@ -513,7 +516,7 @@ class NewMessage(_EventBuilder):
|
|||
def photo(self):
|
||||
"""
|
||||
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):
|
||||
photo = self.message.media.photo
|
||||
|
@ -524,7 +527,7 @@ class NewMessage(_EventBuilder):
|
|||
def document(self):
|
||||
"""
|
||||
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):
|
||||
doc = self.message.media.document
|
||||
|
@ -547,7 +550,7 @@ class NewMessage(_EventBuilder):
|
|||
def audio(self):
|
||||
"""
|
||||
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,
|
||||
lambda attr: not attr.voice)
|
||||
|
@ -556,7 +559,7 @@ class NewMessage(_EventBuilder):
|
|||
def voice(self):
|
||||
"""
|
||||
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,
|
||||
lambda attr: attr.voice)
|
||||
|
@ -565,7 +568,7 @@ class NewMessage(_EventBuilder):
|
|||
def video(self):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
@ -573,7 +576,7 @@ class NewMessage(_EventBuilder):
|
|||
def video_note(self):
|
||||
"""
|
||||
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,
|
||||
lambda attr: attr.round_message)
|
||||
|
@ -582,7 +585,7 @@ class NewMessage(_EventBuilder):
|
|||
def gif(self):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
@ -590,7 +593,7 @@ class NewMessage(_EventBuilder):
|
|||
def sticker(self):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
@ -609,11 +612,12 @@ class ChatAction(_EventBuilder):
|
|||
Represents an action in a chat (such as user joined, left, or new pin).
|
||||
"""
|
||||
def build(self, update):
|
||||
if isinstance(update, types.UpdateChannelPinnedMessage):
|
||||
# Telegram sends UpdateChannelPinnedMessage and then
|
||||
# UpdateNewChannelMessage with MessageActionPinMessage.
|
||||
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
|
||||
# Telegram does not always send
|
||||
# UpdateChannelPinnedMessage for new pins
|
||||
# but always for unpin, with update.id = 0
|
||||
event = ChatAction.Event(types.PeerChannel(update.channel_id),
|
||||
new_pin=update.id)
|
||||
unpin=True)
|
||||
|
||||
elif isinstance(update, types.UpdateChatParticipantAdd):
|
||||
event = ChatAction.Event(types.PeerChat(update.chat_id),
|
||||
|
@ -664,6 +668,11 @@ class ChatAction(_EventBuilder):
|
|||
event = ChatAction.Event(msg,
|
||||
users=msg.from_id,
|
||||
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:
|
||||
return
|
||||
else:
|
||||
|
@ -678,12 +687,12 @@ class ChatAction(_EventBuilder):
|
|||
|
||||
Members:
|
||||
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`):
|
||||
``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).
|
||||
|
||||
|
||||
|
@ -704,10 +713,13 @@ class ChatAction(_EventBuilder):
|
|||
|
||||
new_title (:obj:`bool`, optional):
|
||||
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,
|
||||
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):
|
||||
self.action_message = where
|
||||
where = where.to_id
|
||||
|
@ -726,7 +738,7 @@ class ChatAction(_EventBuilder):
|
|||
self._added_by = None
|
||||
self._kicked_by = None
|
||||
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:
|
||||
self.user_joined = True
|
||||
|
@ -745,6 +757,7 @@ class ChatAction(_EventBuilder):
|
|||
self._users = None
|
||||
self._input_users = None
|
||||
self.new_title = new_title
|
||||
self.unpin = unpin
|
||||
|
||||
async def respond(self, *args, **kwargs):
|
||||
"""
|
||||
|
@ -785,7 +798,7 @@ class ChatAction(_EventBuilder):
|
|||
@property
|
||||
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.
|
||||
"""
|
||||
if self._pinned_message == 0:
|
||||
|
@ -851,7 +864,7 @@ class ChatAction(_EventBuilder):
|
|||
@property
|
||||
async def input_user(self):
|
||||
"""
|
||||
Input version of the self.user property.
|
||||
Input version of the ``self.user`` property.
|
||||
"""
|
||||
if await self.input_users:
|
||||
return self._input_users[0]
|
||||
|
@ -888,7 +901,7 @@ class ChatAction(_EventBuilder):
|
|||
@property
|
||||
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:
|
||||
self._input_users = []
|
||||
|
@ -941,7 +954,7 @@ class UserUpdate(_EventBuilder):
|
|||
recently (:obj:`bool`):
|
||||
``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.
|
||||
|
||||
cancel (:obj:`bool`):
|
||||
|
@ -1066,6 +1079,9 @@ class MessageEdited(NewMessage):
|
|||
event._entities = update.entities
|
||||
return self._message_filter_event(event)
|
||||
|
||||
class Event(NewMessage.Event):
|
||||
pass # Required if we want a different name for it
|
||||
|
||||
|
||||
@_name_inner_event
|
||||
class MessageDeleted(_EventBuilder):
|
||||
|
@ -1100,22 +1116,22 @@ class MessageDeleted(_EventBuilder):
|
|||
|
||||
class StopPropagation(Exception):
|
||||
"""
|
||||
If this Exception is found to be raised in any of the handlers for a
|
||||
given update, it will stop the execution of all other registered
|
||||
event handlers in the chain.
|
||||
Think of it like a ``StopIteration`` exception in a for loop.
|
||||
If this exception is raised in any of the handlers for a given event,
|
||||
it will stop the execution of all other registered event handlers.
|
||||
It can be seen as the ``StopIteration`` in a for loop but for events.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
@client.on(events.NewMessage)
|
||||
def delete(event):
|
||||
event.delete()
|
||||
# Other handlers won't have an event to work with
|
||||
raise StopPropagation
|
||||
|
||||
@client.on(events.NewMessage)
|
||||
def _(event):
|
||||
# Will never be reached, because it is the second handler in the chain.
|
||||
pass
|
||||
```
|
||||
>>> @client.on(events.NewMessage)
|
||||
... def delete(event):
|
||||
... event.delete()
|
||||
... # No other event handler will have a chance to handle this event
|
||||
... raise StopPropagation
|
||||
...
|
||||
>>> @client.on(events.NewMessage)
|
||||
... def _(event):
|
||||
... # Will never be reached, because it is the second handler
|
||||
... 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
|
||||
}
|
||||
|
||||
DEFAULT_URL_RE = re.compile(r'\[([^\]]+)\]\((.+?)\)')
|
||||
DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)')
|
||||
DEFAULT_URL_FORMAT = '[{0}]({1})'
|
||||
|
||||
|
||||
|
|
|
@ -175,7 +175,16 @@ class TcpClient:
|
|||
except asyncio.TimeoutError as e:
|
||||
# These are somewhat common if the server has nothing
|
||||
# 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
|
||||
except ConnectionError as e:
|
||||
__log__.info('ConnectionError "%s" while reading data', e)
|
||||
|
|
|
@ -25,12 +25,14 @@ __log__ = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class MtProtoSender:
|
||||
"""MTProto Mobile Protocol sender
|
||||
"""
|
||||
MTProto Mobile Protocol sender
|
||||
(https://core.telegram.org/mtproto/description).
|
||||
|
||||
Note that this class is not thread-safe, and calling send/receive
|
||||
from two or more threads at the same time is undefined behaviour.
|
||||
Rationale: a new connection should be spawned to send/receive requests
|
||||
Rationale:
|
||||
a new connection should be spawned to send/receive requests
|
||||
in parallel, so thread-safety (hence locking) isn't needed.
|
||||
"""
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ class MemorySession(Session):
|
|||
try:
|
||||
p = utils.get_input_peer(e, allow_self=False)
|
||||
marked_id = utils.get_peer_id(p)
|
||||
except ValueError:
|
||||
except TypeError:
|
||||
return
|
||||
|
||||
if isinstance(p, (InputPeerUser, InputPeerChannel)):
|
||||
|
|
|
@ -91,18 +91,13 @@ from .extensions import markdown, html
|
|||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _Box:
|
||||
"""Helper class to pass parameters by reference"""
|
||||
def __init__(self, x=None):
|
||||
self.x = x
|
||||
|
||||
|
||||
class TelegramClient(TelegramBareClient):
|
||||
"""
|
||||
Initializes the Telegram client with the specified API ID and Hash.
|
||||
|
||||
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
|
||||
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,
|
||||
|
@ -169,7 +164,7 @@ class TelegramClient(TelegramBareClient):
|
|||
connection_mode=ConnectionMode.TCP_FULL,
|
||||
use_ipv6=False,
|
||||
proxy=None,
|
||||
timeout=timedelta(seconds=5),
|
||||
timeout=timedelta(seconds=10),
|
||||
loop=None,
|
||||
report_errors=True,
|
||||
**kwargs):
|
||||
|
@ -216,7 +211,7 @@ class TelegramClient(TelegramBareClient):
|
|||
Whether to force sending as SMS.
|
||||
|
||||
Returns:
|
||||
Information about the result of the request.
|
||||
An instance of :tl:`SentCode`.
|
||||
"""
|
||||
phone = utils.parse_phone(phone) or self._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.
|
||||
|
||||
bot_token (:obj:`str`):
|
||||
Bot Token obtained by @BotFather to log in as a bot.
|
||||
Cannot be specified with `phone` (only one of either allowed).
|
||||
Bot Token obtained by `@BotFather <https://t.me/BotFather>`_
|
||||
to log in as a bot. Cannot be specified with ``phone`` (only
|
||||
one of either allowed).
|
||||
|
||||
force_sms (:obj:`bool`, optional):
|
||||
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.
|
||||
|
||||
Returns:
|
||||
:obj:`TelegramClient`:
|
||||
This client, so initialization can be chained with `.start()`.
|
||||
This :obj:`TelegramClient`, so initialization
|
||||
can be chained with ``.start()``.
|
||||
"""
|
||||
|
||||
if code_callback is None:
|
||||
|
@ -377,7 +373,10 @@ class TelegramClient(TelegramBareClient):
|
|||
these requests.
|
||||
|
||||
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`):
|
||||
2FA password, should be used if a previous call raised
|
||||
|
@ -393,7 +392,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
Returns:
|
||||
The signed in user, or the information about
|
||||
:meth:`.send_code_request()`.
|
||||
:meth:`send_code_request`.
|
||||
"""
|
||||
if self.is_user_authorized():
|
||||
await self._check_events_pending_resolve()
|
||||
|
@ -454,7 +453,7 @@ class TelegramClient(TelegramBareClient):
|
|||
Optional last name.
|
||||
|
||||
Returns:
|
||||
The new created user.
|
||||
The new created :tl:`User`.
|
||||
"""
|
||||
if self.is_user_authorized():
|
||||
await self._check_events_pending_resolve()
|
||||
|
@ -479,7 +478,7 @@ class TelegramClient(TelegramBareClient):
|
|||
Logs out Telegram and deletes the current ``*.session`` file.
|
||||
|
||||
Returns:
|
||||
True if the operation was successful.
|
||||
``True`` if the operation was successful.
|
||||
"""
|
||||
try:
|
||||
await self(LogOutRequest())
|
||||
|
@ -497,12 +496,12 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
Args:
|
||||
input_peer (:obj:`bool`, optional):
|
||||
Whether to return the ``InputPeerUser`` version or the normal
|
||||
``User``. This can be useful if you just need to know the ID
|
||||
Whether to return the :tl:`InputPeerUser` version or the normal
|
||||
:tl:`User`. This can be useful if you just need to know the ID
|
||||
of yourself.
|
||||
|
||||
Returns:
|
||||
:obj:`User`: Your own user.
|
||||
Your own :tl:`User`.
|
||||
"""
|
||||
if input_peer and self._self_input_peer:
|
||||
return self._self_input_peer
|
||||
|
@ -522,7 +521,7 @@ class TelegramClient(TelegramBareClient):
|
|||
# region Dialogs ("chats") requests
|
||||
|
||||
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.
|
||||
Dialogs are the open "chats" or conversations with other people.
|
||||
|
@ -541,18 +540,18 @@ class TelegramClient(TelegramBareClient):
|
|||
offset_id (:obj:`int`, optional):
|
||||
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.
|
||||
|
||||
_total_box (:obj:`_Box`, optional):
|
||||
A _Box instance to pass the total parameter by reference.
|
||||
_total (:obj:`list`, optional):
|
||||
A single-item list to pass the total parameter by reference.
|
||||
|
||||
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)
|
||||
if limit == 0:
|
||||
if not _total_box:
|
||||
if not _total:
|
||||
return
|
||||
# Special case, get a single dialog and determine count
|
||||
dialogs = await self(GetDialogsRequest(
|
||||
|
@ -561,7 +560,7 @@ class TelegramClient(TelegramBareClient):
|
|||
offset_peer=offset_peer,
|
||||
limit=1
|
||||
))
|
||||
_total_box.x = getattr(dialogs, 'count', len(dialogs.dialogs))
|
||||
_total[0] = getattr(dialogs, 'count', len(dialogs.dialogs))
|
||||
return
|
||||
|
||||
seen = set()
|
||||
|
@ -575,8 +574,8 @@ class TelegramClient(TelegramBareClient):
|
|||
req.limit = min(limit - len(seen), 100)
|
||||
r = await self(req)
|
||||
|
||||
if _total_box:
|
||||
_total_box.x = getattr(r, 'count', len(r.dialogs))
|
||||
if _total:
|
||||
_total[0] = getattr(r, 'count', len(r.dialogs))
|
||||
messages = {m.id: m for m in r.messages}
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(r.users, r.chats)}
|
||||
|
@ -604,24 +603,24 @@ class TelegramClient(TelegramBareClient):
|
|||
async def get_dialogs(self, *args, **kwargs):
|
||||
"""
|
||||
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)
|
||||
kwargs['_total_box'] = total_box
|
||||
total = [0]
|
||||
kwargs['_total'] = total
|
||||
dialogs = UserList()
|
||||
async for dialog in self.iter_dialogs(*args, **kwargs):
|
||||
dialogs.append(dialog)
|
||||
|
||||
dialogs.total = total_box.x
|
||||
dialogs.total = total[0]
|
||||
return dialogs
|
||||
|
||||
async def iter_drafts(self): # TODO: Ability to provide a `filter`
|
||||
"""
|
||||
Iterator over all open draft messages.
|
||||
|
||||
The yielded items are custom ``Draft`` objects that are easier to use.
|
||||
You can call ``draft.set_message('text')`` to change the message,
|
||||
or delete it through :meth:`draft.delete()`.
|
||||
Instances of :obj:`telethon.tl.custom.draft.Draft` are yielded.
|
||||
You can call :obj:`telethon.tl.custom.draft.Draft.set_message`
|
||||
to change the message or :obj:`telethon.tl.custom.draft.Draft.delete`
|
||||
among other things.
|
||||
"""
|
||||
for update in (await self(GetAllDraftsRequest())).updates:
|
||||
yield Draft._from_update(self, update)
|
||||
|
@ -675,7 +674,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
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:
|
||||
return message, []
|
||||
|
@ -714,10 +713,10 @@ class TelegramClient(TelegramBareClient):
|
|||
entity (:obj:`entity`):
|
||||
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.
|
||||
|
||||
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,
|
||||
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.
|
||||
|
||||
Returns:
|
||||
the sent message
|
||||
The sent :tl:`Message`.
|
||||
"""
|
||||
if file is not None:
|
||||
return await self.send_file(
|
||||
|
@ -809,7 +808,7 @@ class TelegramClient(TelegramBareClient):
|
|||
entity (:obj:`entity`):
|
||||
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.
|
||||
|
||||
from_peer (:obj:`entity`):
|
||||
|
@ -818,7 +817,7 @@ class TelegramClient(TelegramBareClient):
|
|||
order for the forward to work.
|
||||
|
||||
Returns:
|
||||
The forwarded messages.
|
||||
The list of forwarded :tl:`Message`.
|
||||
"""
|
||||
if not utils.is_list_like(messages):
|
||||
messages = (messages,)
|
||||
|
@ -848,7 +847,7 @@ class TelegramClient(TelegramBareClient):
|
|||
for update in result.updates:
|
||||
if isinstance(update, UpdateMessageID):
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
the edited message
|
||||
The edited :tl:`Message`.
|
||||
"""
|
||||
message, msg_entities = await self._parse_message_text(message, parse_mode)
|
||||
request = EditMessageRequest(
|
||||
|
@ -908,7 +907,7 @@ class TelegramClient(TelegramBareClient):
|
|||
be ``None`` for normal chats, but **must** be present
|
||||
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.
|
||||
|
||||
revoke (:obj:`bool`, optional):
|
||||
|
@ -918,7 +917,7 @@ class TelegramClient(TelegramBareClient):
|
|||
This has no effect on channels or megagroups.
|
||||
|
||||
Returns:
|
||||
The affected messages.
|
||||
The :tl:`AffectedMessages`.
|
||||
"""
|
||||
if not utils.is_list_like(message_ids):
|
||||
message_ids = (message_ids,)
|
||||
|
@ -940,7 +939,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
async def iter_messages(self, entity, limit=20, offset_date=None,
|
||||
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.
|
||||
|
||||
|
@ -981,16 +980,16 @@ class TelegramClient(TelegramBareClient):
|
|||
you are still free to do so.
|
||||
|
||||
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.
|
||||
If left to ``None``, it will default to 1 second only if
|
||||
the limit is higher than 3000.
|
||||
|
||||
_total_box (:obj:`_Box`, optional):
|
||||
A _Box instance to pass the total parameter by reference.
|
||||
_total (:obj:`list`, optional):
|
||||
A single-item list to pass the total parameter by reference.
|
||||
|
||||
Yields:
|
||||
Instances of ``telethon.tl.types.Message`` with extra attributes:
|
||||
Instances of :tl:`Message` with extra attributes:
|
||||
|
||||
* ``.sender`` = entity of the sender.
|
||||
* ``.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.
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -1007,7 +1006,7 @@ class TelegramClient(TelegramBareClient):
|
|||
entity = await self.get_input_entity(entity)
|
||||
limit = float('inf') if limit is None else int(limit)
|
||||
if limit == 0:
|
||||
if not _total_box:
|
||||
if not _total:
|
||||
return
|
||||
# No messages, but we still need to know the total message count
|
||||
result = await self(GetHistoryRequest(
|
||||
|
@ -1015,7 +1014,7 @@ class TelegramClient(TelegramBareClient):
|
|||
offset_date=None, offset_id=0, max_id=0, min_id=0,
|
||||
add_offset=0, hash=0
|
||||
))
|
||||
_total_box.x = getattr(result, 'count', len(result.messages))
|
||||
_total[0] = getattr(result, 'count', len(result.messages))
|
||||
return
|
||||
|
||||
if wait_time is None:
|
||||
|
@ -1036,8 +1035,8 @@ class TelegramClient(TelegramBareClient):
|
|||
add_offset=add_offset,
|
||||
hash=0
|
||||
))
|
||||
if _total_box:
|
||||
_total_box.x = getattr(r, 'count', len(r.messages))
|
||||
if _total:
|
||||
_total[0] = getattr(r, 'count', len(r.messages))
|
||||
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(r.users, r.chats)}
|
||||
|
@ -1080,15 +1079,15 @@ class TelegramClient(TelegramBareClient):
|
|||
async def get_messages(self, *args, **kwargs):
|
||||
"""
|
||||
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)
|
||||
kwargs['_total_box'] = total_box
|
||||
total = [0]
|
||||
kwargs['_total'] = total
|
||||
msgs = UserList()
|
||||
async for msg in self.iter_messages(*args, **kwargs):
|
||||
msgs.append(msg)
|
||||
|
||||
msgs.total = total_box.x
|
||||
msgs.total = total[0]
|
||||
return msgs
|
||||
|
||||
async def get_message_history(self, *args, **kwargs):
|
||||
|
@ -1107,7 +1106,7 @@ class TelegramClient(TelegramBareClient):
|
|||
entity (:obj:`entity`):
|
||||
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.
|
||||
|
||||
max_id (:obj:`int`):
|
||||
|
@ -1164,8 +1163,7 @@ class TelegramClient(TelegramBareClient):
|
|||
raise TypeError('Invalid message type: {}'.format(type(message)))
|
||||
|
||||
async def iter_participants(self, entity, limit=None, search='',
|
||||
filter=None, aggressive=False,
|
||||
_total_box=None):
|
||||
filter=None, aggressive=False, _total=None):
|
||||
"""
|
||||
Iterator over the participants belonging to the specified chat.
|
||||
|
||||
|
@ -1179,9 +1177,8 @@ class TelegramClient(TelegramBareClient):
|
|||
search (:obj:`str`, optional):
|
||||
Look for participants with this string in name/username.
|
||||
|
||||
filter (:obj:`ChannelParticipantsFilter`, optional):
|
||||
The filter to be used, if you want e.g. only admins. See
|
||||
https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html.
|
||||
filter (:tl:`ChannelParticipantsFilter`, optional):
|
||||
The filter to be used, if you want e.g. only admins
|
||||
Note that you might not have permissions for some filter.
|
||||
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
|
||||
10,000 members, or if a ``filter`` is given.
|
||||
|
||||
_total_box (:obj:`_Box`, optional):
|
||||
A _Box instance to pass the total parameter by reference.
|
||||
_total (:obj:`list`, optional):
|
||||
A single-item list to pass the total parameter by reference.
|
||||
|
||||
Yields:
|
||||
The ``User`` objects returned by ``GetParticipantsRequest``
|
||||
The :tl:`User` objects returned by :tl:`GetParticipantsRequest`
|
||||
with an additional ``.participant`` attribute which is the
|
||||
matched ``ChannelParticipant`` type for channels/megagroups
|
||||
or ``ChatParticipants`` for normal chats.
|
||||
matched :tl:`ChannelParticipant` type for channels/megagroups
|
||||
or :tl:`ChatParticipants` for normal chats.
|
||||
"""
|
||||
if isinstance(filter, type):
|
||||
filter = filter()
|
||||
|
@ -1224,8 +1221,8 @@ class TelegramClient(TelegramBareClient):
|
|||
total = (await self(GetFullChannelRequest(
|
||||
entity
|
||||
))).full_chat.participants_count
|
||||
if _total_box:
|
||||
_total_box.x = total
|
||||
if _total:
|
||||
_total[0] = total
|
||||
|
||||
if limit == 0:
|
||||
return
|
||||
|
@ -1285,8 +1282,8 @@ class TelegramClient(TelegramBareClient):
|
|||
elif isinstance(entity, InputPeerChat):
|
||||
# TODO We *could* apply the `filter` here ourselves
|
||||
full = await self(GetFullChatRequest(entity.chat_id))
|
||||
if _total_box:
|
||||
_total_box.x = len(full.full_chat.participants.participants)
|
||||
if _total:
|
||||
_total[0] = len(full.full_chat.participants.participants)
|
||||
|
||||
have = 0
|
||||
users = {user.id: user for user in full.users}
|
||||
|
@ -1302,8 +1299,8 @@ class TelegramClient(TelegramBareClient):
|
|||
user.participant = participant
|
||||
yield user
|
||||
else:
|
||||
if _total_box:
|
||||
_total_box.x = 1
|
||||
if _total:
|
||||
_total[0] = 1
|
||||
if limit != 0:
|
||||
user = await self.get_entity(entity)
|
||||
if filter_entity(user):
|
||||
|
@ -1313,14 +1310,14 @@ class TelegramClient(TelegramBareClient):
|
|||
async def get_participants(self, *args, **kwargs):
|
||||
"""
|
||||
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)
|
||||
kwargs['_total_box'] = total_box
|
||||
total = [0]
|
||||
kwargs['_total'] = total
|
||||
participants = UserList()
|
||||
async for participant in self.iter_participants(*args, **kwargs):
|
||||
participants.append(participant)
|
||||
participants.total = total_box.x
|
||||
participants.total = total[0]
|
||||
return participants
|
||||
|
||||
# endregion
|
||||
|
@ -1371,12 +1368,12 @@ class TelegramClient(TelegramBareClient):
|
|||
A callback function accepting two parameters:
|
||||
``(sent bytes, total)``.
|
||||
|
||||
reply_to (:obj:`int` | :obj:`Message`):
|
||||
reply_to (:obj:`int` | :tl:`Message`):
|
||||
Same as reply_to from .send_message().
|
||||
|
||||
attributes (:obj:`list`, optional):
|
||||
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):
|
||||
Optional thumbnail (for videos).
|
||||
|
@ -1399,13 +1396,16 @@ class TelegramClient(TelegramBareClient):
|
|||
it will be used to determine metadata from audio and video files.
|
||||
|
||||
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
|
||||
# we may want to send as an album if all are photo files.
|
||||
if utils.is_list_like(file):
|
||||
# TODO Fix progress_callback
|
||||
images = []
|
||||
if force_document:
|
||||
documents = file
|
||||
else:
|
||||
documents = []
|
||||
for x in file:
|
||||
if utils.is_image(x):
|
||||
|
@ -1424,7 +1424,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
result.extend(
|
||||
await self.send_file(
|
||||
entity, x, allow_cache=False,
|
||||
entity, x, allow_cache=allow_cache,
|
||||
caption=caption, force_document=force_document,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
attributes=attributes, thumb=thumb, **kwargs
|
||||
|
@ -1557,7 +1557,7 @@ class TelegramClient(TelegramBareClient):
|
|||
return msg
|
||||
|
||||
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
|
||||
return self.send_file(*args, **kwargs)
|
||||
|
||||
|
@ -1654,8 +1654,8 @@ class TelegramClient(TelegramBareClient):
|
|||
``(sent bytes, total)``.
|
||||
|
||||
Returns:
|
||||
``InputFileBig`` if the file size is larger than 10MB,
|
||||
``InputSizedFile`` (subclass of ``InputFile``) otherwise.
|
||||
:tl:`InputFileBig` if the file size is larger than 10MB,
|
||||
``InputSizedFile`` (subclass of :tl:`InputFile`) otherwise.
|
||||
"""
|
||||
if isinstance(file, (InputFile, InputFileBig)):
|
||||
return file # Already uploaded
|
||||
|
@ -1838,7 +1838,7 @@ class TelegramClient(TelegramBareClient):
|
|||
"""
|
||||
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.
|
||||
|
||||
file (:obj:`str` | :obj:`file`, optional):
|
||||
|
@ -1847,7 +1847,7 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
progress_callback (:obj:`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
``(recv bytes, total)``.
|
||||
``(received bytes, total)``.
|
||||
|
||||
Returns:
|
||||
``None`` if no media was provided, or if it was Empty. On success
|
||||
|
@ -1918,7 +1918,7 @@ class TelegramClient(TelegramBareClient):
|
|||
return file
|
||||
|
||||
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):
|
||||
document = document.document
|
||||
if not isinstance(document, Document):
|
||||
|
@ -1965,7 +1965,7 @@ class TelegramClient(TelegramBareClient):
|
|||
@staticmethod
|
||||
def _download_contact(mm_contact, file):
|
||||
"""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
|
||||
last_name = mm_contact.last_name
|
||||
|
@ -2063,7 +2063,7 @@ class TelegramClient(TelegramBareClient):
|
|||
Downloads the given input location to a file.
|
||||
|
||||
Args:
|
||||
input_location (:obj:`InputFileLocation`):
|
||||
input_location (:tl:`InputFileLocation`):
|
||||
The file location from which the file will be downloaded.
|
||||
|
||||
file (:obj:`str` | :obj:`file`):
|
||||
|
@ -2286,7 +2286,7 @@ class TelegramClient(TelegramBareClient):
|
|||
"""
|
||||
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.
|
||||
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.
|
||||
|
@ -2302,7 +2302,7 @@ class TelegramClient(TelegramBareClient):
|
|||
error will be raised.
|
||||
|
||||
Returns:
|
||||
``User``, ``Chat`` or ``Channel`` corresponding to the input
|
||||
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the input
|
||||
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
|
||||
use this kind of InputUser, InputChat and so on, so this is the
|
||||
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
|
||||
``PeerUser``, ``PeerChat`` or ``PeerChannel``, for
|
||||
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for
|
||||
which to get its ``Input*`` version.
|
||||
If this ``Peer`` hasn't been seen before by the library, the top
|
||||
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,
|
||||
a ValueError will be raised.
|
||||
Returns:
|
||||
``InputPeerUser``, ``InputPeerChat`` or ``InputPeerChannel``.
|
||||
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`.
|
||||
"""
|
||||
try:
|
||||
# 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
|
||||
someone, a group or a channel) providing an abstraction to easily
|
||||
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):
|
||||
# Both entities and messages being dicts {ID: item}
|
||||
|
@ -19,6 +59,7 @@ class Dialog:
|
|||
|
||||
self.entity = entities[utils.get_peer_id(dialog.peer)]
|
||||
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.unread_count = dialog.unread_count
|
||||
|
@ -29,6 +70,6 @@ class Dialog:
|
|||
async def send_message(self, *args, **kwargs):
|
||||
"""
|
||||
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)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import datetime
|
||||
|
||||
from ..functions.messages import SaveDraftRequest
|
||||
from ..types import UpdateDraftMessage, DraftMessage
|
||||
from ...extensions import markdown
|
||||
|
@ -7,7 +9,17 @@ class Draft:
|
|||
"""
|
||||
Custom class that encapsulates a draft on the Telegram servers, providing
|
||||
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):
|
||||
self._client = client
|
||||
|
@ -33,20 +45,41 @@ class Draft:
|
|||
|
||||
@property
|
||||
async def entity(self):
|
||||
"""
|
||||
The entity that belongs to this dialog (user, chat or channel).
|
||||
"""
|
||||
return await self._client.get_entity(self._peer)
|
||||
|
||||
@property
|
||||
async def input_entity(self):
|
||||
"""
|
||||
Input version of the entity.
|
||||
"""
|
||||
return await self._client.get_input_entity(self._peer)
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
@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',
|
||||
link_preview=None):
|
||||
"""
|
||||
|
@ -89,10 +122,15 @@ class Draft:
|
|||
self._raw_text = raw_text
|
||||
self.link_preview = link_preview
|
||||
self.reply_to_msg_id = reply_to
|
||||
self.date = datetime.datetime.now()
|
||||
|
||||
return result
|
||||
|
||||
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,
|
||||
reply_to=self.reply_to_msg_id,
|
||||
link_preview=self.link_preview,
|
||||
|
@ -101,7 +139,6 @@ class Draft:
|
|||
|
||||
async def delete(self):
|
||||
"""
|
||||
Deletes this draft
|
||||
:return bool: ``True`` on success
|
||||
Deletes this draft, and returns ``True`` on success.
|
||||
"""
|
||||
return await self.set_message(text='')
|
||||
|
|
|
@ -10,8 +10,9 @@ __log__ = logging.getLogger(__name__)
|
|||
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
Gets the display name for the given entity, if it's an ``User``,
|
||||
``Chat`` or ``Channel``. Returns an empty string otherwise.
|
||||
Gets the display name for the given entity, if it's an :tl:`User`,
|
||||
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise.
|
||||
"""
|
||||
if isinstance(entity, User):
|
||||
if entity.last_name and entity.first_name:
|
||||
|
@ -58,7 +58,7 @@ def get_display_name(entity):
|
|||
|
||||
|
||||
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
|
||||
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
|
||||
|
@ -83,8 +83,10 @@ def _raise_cast_fail(entity, target):
|
|||
|
||||
|
||||
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:
|
||||
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
||||
return entity
|
||||
|
@ -129,7 +131,7 @@ def get_input_peer(entity, allow_self=True):
|
|||
|
||||
|
||||
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:
|
||||
if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
|
||||
return entity
|
||||
|
@ -146,7 +148,7 @@ def get_input_channel(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:
|
||||
if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'):
|
||||
return entity
|
||||
|
@ -175,7 +177,7 @@ def get_input_user(entity):
|
|||
|
||||
|
||||
def get_input_document(document):
|
||||
"""Similar to get_input_peer, but for documents"""
|
||||
"""Similar to :meth:`get_input_peer`, but for documents"""
|
||||
try:
|
||||
if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'):
|
||||
return document
|
||||
|
@ -198,7 +200,7 @@ def get_input_document(document):
|
|||
|
||||
|
||||
def get_input_photo(photo):
|
||||
"""Similar to get_input_peer, but for documents"""
|
||||
"""Similar to :meth:`get_input_peer`, but for photos"""
|
||||
try:
|
||||
if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'):
|
||||
return photo
|
||||
|
@ -218,7 +220,7 @@ def get_input_photo(photo):
|
|||
|
||||
|
||||
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:
|
||||
if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'):
|
||||
return geo
|
||||
|
@ -241,10 +243,11 @@ def get_input_geo(geo):
|
|||
|
||||
|
||||
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,
|
||||
it will be treated as an InputMediaUploadedPhoto.
|
||||
If the media is a file location and ``is_photo`` is known to be ``True``,
|
||||
it will be treated as an :tl:`InputMediaUploadedPhoto`.
|
||||
"""
|
||||
try:
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
return False
|
||||
|
@ -326,23 +329,23 @@ def is_image(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
|
||||
(mimetypes.guess_type(file)[0] or '').startswith('audio/'))
|
||||
|
||||
|
||||
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
|
||||
(mimetypes.guess_type(file)[0] or '').startswith('video/'))
|
||||
|
||||
|
||||
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
|
||||
enough. Things like open() are also iterable (and probably many
|
||||
Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not
|
||||
enough. Things like ``open()`` are also iterable (and probably many
|
||||
other things), so just support the commonly known list-like objects.
|
||||
"""
|
||||
return isinstance(obj, (list, tuple, set, dict,
|
||||
|
@ -350,7 +353,7 @@ def is_list_like(obj):
|
|||
|
||||
|
||||
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):
|
||||
return str(phone)
|
||||
else:
|
||||
|
@ -365,7 +368,7 @@ def parse_username(username):
|
|||
both the stripped, lowercase username and whether it is
|
||||
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()
|
||||
m = USERNAME_RE.match(username)
|
||||
|
@ -386,7 +389,7 @@ def parse_username(username):
|
|||
def _fix_peer_id(peer_id):
|
||||
"""
|
||||
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)
|
||||
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.
|
||||
|
||||
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
|
||||
if isinstance(peer, int):
|
||||
|
@ -450,7 +453,7 @@ def get_peer_id(peer):
|
|||
|
||||
|
||||
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:
|
||||
return marked_id, PeerUser
|
||||
|
||||
|
@ -461,8 +464,10 @@ def resolve_id(marked_id):
|
|||
|
||||
|
||||
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
|
||||
return 128
|
||||
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
|
||||
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):
|
||||
|
@ -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" \
|
||||
b"[\x1b\xdf\xcdI\x1f>Z\n\t\xb9\xd2=\xbaF\xd1\x8e'"
|
||||
|
||||
@staticmethod
|
||||
def test_sha1():
|
||||
def test_sha1(self):
|
||||
string = 'Example string'
|
||||
|
||||
hash_sum = sha1(string.encode('utf-8')).digest()
|
||||
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 {})'\
|
||||
.format(expected, hash_sum)
|
||||
self.assertEqual(hash_sum, expected,
|
||||
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):
|
||||
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
|
||||
assert value[:take] == self.cipher_text[:take],\
|
||||
('Ciphered text ("{}") does not equal expected ("{}")'
|
||||
self.assertEqual(value[:take], self.cipher_text[:take],
|
||||
msg='Ciphered text ("{}") does not equal expected ("{}")'
|
||||
.format(value[:take], self.cipher_text[:take]))
|
||||
|
||||
value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
|
||||
assert value == self.cipher_text_padded, (
|
||||
'Ciphered text ("{}") does not equal expected ("{}")'
|
||||
self.assertEqual(value, self.cipher_text_padded,
|
||||
msg='Ciphered text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.cipher_text_padded))
|
||||
|
||||
def test_aes_decrypt(self):
|
||||
# The ciphered text must always be padded
|
||||
value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
|
||||
assert value == self.plain_text_padded, (
|
||||
'Decrypted text ("{}") does not equal expected ("{}")'
|
||||
self.assertEqual(value, self.plain_text_padded,
|
||||
msg='Decrypted text ("{}") does not equal expected ("{}")'
|
||||
.format(value, self.plain_text_padded))
|
||||
|
||||
@staticmethod
|
||||
def test_calc_key():
|
||||
@unittest.skip("test_calc_key needs fix")
|
||||
def test_calc_key(self):
|
||||
# TODO Upgrade test for MtProto 2.0
|
||||
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' \
|
||||
|
@ -78,10 +78,12 @@ class CryptoTests(unittest.TestCase):
|
|||
b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \
|
||||
b'\xa7\xa0\xf7\x0f'
|
||||
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
||||
expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
||||
expected_iv, iv)
|
||||
self.assertEqual(key, expected_key,
|
||||
msg='Invalid key (expected ("{}"), got ("{}"))'
|
||||
.format(expected_key, key))
|
||||
self.assertEqual(iv, expected_iv,
|
||||
msg='Invalid IV (expected ("{}"), got ("{}"))'
|
||||
.format(expected_iv, iv))
|
||||
|
||||
# Calculate key being the server
|
||||
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' \
|
||||
b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
|
||||
|
||||
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
|
||||
expected_key, key)
|
||||
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
|
||||
expected_iv, iv)
|
||||
self.assertEqual(key, expected_key,
|
||||
msg='Invalid key (expected ("{}"), got ("{}"))'
|
||||
.format(expected_key, key))
|
||||
self.assertEqual(iv, expected_iv,
|
||||
msg='Invalid IV (expected ("{}"), got ("{}"))'
|
||||
.format(expected_iv, iv))
|
||||
|
||||
@staticmethod
|
||||
def test_generate_key_data_from_nonce():
|
||||
def test_generate_key_data_from_nonce(self):
|
||||
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')
|
||||
|
||||
|
@ -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_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(
|
||||
key, expected_key)
|
||||
assert iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format(
|
||||
iv, expected_iv)
|
||||
self.assertEqual(key, expected_key,
|
||||
msg='Key ("{}") does not equal expected ("{}")'
|
||||
.format(key, expected_key))
|
||||
self.assertEqual(iv, expected_iv,
|
||||
msg='IV ("{}") does not equal expected ("{}")'
|
||||
.format(iv, expected_iv))
|
||||
|
||||
@staticmethod
|
||||
def test_fingerprint_from_key():
|
||||
assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
|
||||
'-----BEGIN RSA PUBLIC KEY-----\n'
|
||||
'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
|
||||
'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
|
||||
'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
|
||||
'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
|
||||
'8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
|
||||
'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
|
||||
'-----END RSA PUBLIC KEY-----'
|
||||
)) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
|
||||
# test_fringerprint_from_key can't be skipped due to ImportError
|
||||
# def test_fingerprint_from_key(self):
|
||||
# assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
|
||||
# '-----BEGIN RSA PUBLIC KEY-----\n'
|
||||
# 'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
|
||||
# 'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
|
||||
# 'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
|
||||
# 'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
|
||||
# '8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
|
||||
# 'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
|
||||
# '-----END RSA PUBLIC KEY-----'
|
||||
# )) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
|
||||
|
||||
@staticmethod
|
||||
def test_factorize():
|
||||
def test_factorize(self):
|
||||
pq = 3118979781119966969
|
||||
p, q = Factorization.factorize(pq)
|
||||
if p > q:
|
||||
p, q = q, p
|
||||
|
||||
assert p == 1719614201, 'Factorized pair did not yield the correct result'
|
||||
assert q == 1813767169, 'Factorized pair did not yield the correct result'
|
||||
self.assertEqual(p, 1719614201,
|
||||
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):
|
||||
@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
|
||||
run_server_echo_thread(port)
|
||||
|
||||
msg = b'Unit testing...'
|
||||
client = TcpClient()
|
||||
client.connect('localhost', port)
|
||||
client.write(msg)
|
||||
assert msg == client.read(
|
||||
15), 'Read message does not equal sent message'
|
||||
await client.connect('localhost', port)
|
||||
await client.write(msg)
|
||||
self.assertEqual(msg, await client.read(15),
|
||||
msg='Read message does not equal sent message')
|
||||
client.close()
|
||||
|
||||
@staticmethod
|
||||
def test_authenticator():
|
||||
@unittest.skip("Some parameters changed, so IP doesn't go there anymore.")
|
||||
async def test_authenticator(self):
|
||||
transport = Connection('149.154.167.91', 443)
|
||||
authenticator.do_authentication(transport)
|
||||
self.assertTrue(await authenticator.do_authentication(transport))
|
||||
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):
|
||||
@staticmethod
|
||||
def test_binary_writer_reader():
|
||||
def test_binary_writer_reader(self):
|
||||
# Test that we can read properly
|
||||
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 ' \
|
||||
|
@ -15,29 +14,35 @@ class UtilsTests(unittest.TestCase):
|
|||
|
||||
with BinaryReader(data) as reader:
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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)
|
||||
assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \
|
||||
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value)
|
||||
self.assertEqual(value, bytes([26, 27, 28, 29, 30, 31, 32]),
|
||||
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)
|
||||
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():
|
||||
def test_binary_tgwriter_tgreader(self):
|
||||
small_data = os.urandom(33)
|
||||
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!)
|
||||
for datum in data:
|
||||
value = reader.tgread_bytes()
|
||||
assert value == datum, 'Example bytes should be {} but is {}'.format(
|
||||
datum, value)
|
||||
self.assertEqual(value, datum,
|
||||
msg='Example bytes should be {} but is {}'.format(datum, value))
|
||||
|
||||
value = reader.tgread_string()
|
||||
assert value == string, 'Example string should be {} but is {}'.format(
|
||||
string, value)
|
||||
self.assertEqual(value, string,
|
||||
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