Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-03-24 13:08:51 +01:00
commit 8b0580901a
31 changed files with 661 additions and 373 deletions

View File

@ -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]);
} }

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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']
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import unittest
class ParserTests(unittest.TestCase):
"""There are no tests yet"""

View File

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

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

View File

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

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

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

View File

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

View File

@ -1,5 +0,0 @@
import unittest
class TLTests(unittest.TestCase):
"""There are no tests yet"""