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,
// return a pair of arrays with matching "query" elements from "original".
//
@ -86,7 +117,7 @@ function getSearchArray(original, originalu, query) {
var destinationu = [];
for (var i = 0; i < original.length; ++i) {
if (original[i].toLowerCase().indexOf(query) != -1) {
if (find(original[i].toLowerCase(), query)) {
destination.push(original[i]);
destinationu.push(originalu[i]);
}

View File

@ -17,15 +17,16 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import os
import re
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
tl_ref_url = 'https://lonamiwebs.github.io/Telethon'
# -- General configuration ------------------------------------------------
@ -36,7 +37,10 @@ root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc']
extensions = [
'sphinx.ext.autodoc',
'custom_roles'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

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.
Say ``client.send_message()`` didn't exist, we could use the `search`__
to look for "message". There we would find `SendMessageRequest`__,
to look for "message". There we would find :tl:`SendMessageRequest`,
which we can work with.
Every request is a Python class, and has the parameters needed for you
@ -45,11 +45,11 @@ If you're going to use a lot of these, you may do:
# We now have access to 'functions.messages.SendMessageRequest'
We see that this request must take at least two parameters, a ``peer``
of type `InputPeer`__, and a ``message`` which is just a Python
of type :tl:`InputPeer`, and a ``message`` which is just a Python
``str``\ ing.
How can we retrieve this ``InputPeer``? We have two options. We manually
`construct one`__, for instance:
How can we retrieve this :tl:`InputPeer`? We have two options. We manually
construct one, for instance:
.. code-block:: python
@ -64,7 +64,7 @@ Or we call ``.get_input_entity()``:
peer = client.get_input_entity('someone')
When you're going to invoke an API method, most require you to pass an
``InputUser``, ``InputChat``, or so on, this is why using
:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using
``.get_input_entity()`` is more straightforward (and often
immediate, if you've seen the user before, know their ID, etc.).
If you also need to have information about the whole user, use
@ -138,6 +138,3 @@ This can further be simplified to:
__ https://lonamiwebs.github.io/Telethon
__ https://lonamiwebs.github.io/Telethon/methods/index.html
__ https://lonamiwebs.github.io/Telethon/?q=message
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html
__ https://lonamiwebs.github.io/Telethon/types/input_peer.html
__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html

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=...)
# You can import both exceptions from telethon.errors.
.. note::
If you send the code that Telegram sent you over the app through the
app itself, it will expire immediately. You can still send the code
through the app by "obfuscating" it (maybe add a magic constant, like
``12345``, and then subtract it to get the real code back) or any other
technique.
``myself`` is your Telegram user. You can view all the information about
yourself by doing ``print(myself.stringify())``. You're now ready to use
the client as you wish! Remember that any object returned by the API has

View File

@ -1,3 +1,5 @@
.. _entities:
=========================
Users, Chats and Channels
=========================
@ -7,16 +9,16 @@ Introduction
************
The library widely uses the concept of "entities". An entity will refer
to any ``User``, ``Chat`` or ``Channel`` object that the API may return
in response to certain methods, such as ``GetUsersRequest``.
to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return
in response to certain methods, such as :tl:`GetUsersRequest`.
.. note::
When something "entity-like" is required, it means that you need to
provide something that can be turned into an entity. These things include,
but are not limited to, usernames, exact titles, IDs, ``Peer`` objects,
or even entire ``User``, ``Chat`` and ``Channel`` objects and even phone
numbers from people you have in your contacts.
but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects,
or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even
phone numbers from people you have in your contacts.
Getting entities
****************
@ -71,7 +73,7 @@ become possible.
Every entity the library encounters (in any response to any call) will by
default be cached in the ``.session`` file (an SQLite database), to avoid
performing unnecessary API calls. If the entity cannot be found, additonal
calls like ``ResolveUsernameRequest`` or ``GetContactsRequest`` may be
calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be
made to obtain the required information.
@ -88,14 +90,14 @@ Entities vs. Input Entities
On top of the normal types, the API also make use of what they call their
``Input*`` versions of objects. The input version of an entity (e.g.
``InputPeerUser``, ``InputChat``, etc.) only contains the minimum
:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum
information that's required from Telegram to be able to identify
who you're referring to: a ``Peer``'s **ID** and **hash**.
who you're referring to: a :tl:`Peer`'s **ID** and **hash**.
This ID/hash pair is unique per user, so if you use the pair given by another
user **or bot** it will **not** work.
To save *even more* bandwidth, the API also makes use of the ``Peer``
To save *even more* bandwidth, the API also makes use of the :tl:`Peer`
versions, which just have an ID. This serves to identify them, but
peers alone are not enough to use them. You need to know their hash
before you can "use them".
@ -104,8 +106,8 @@ As we just mentioned, API calls don't need to know the whole information
about the entities, only their ID and hash. For this reason, another method,
``.get_input_entity()`` is available. This will always use the cache while
possible, making zero API calls most of the time. When a request is made,
if you provided the full entity, e.g. an ``User``, the library will convert
it to the required ``InputPeer`` automatically for you.
if you provided the full entity, e.g. an :tl:`User`, the library will convert
it to the required :tl:`InputPeer` automatically for you.
**You should always favour** ``.get_input_entity()`` **over** ``.get_entity()``
for this reason! Calling the latter will always make an API call to get
@ -123,5 +125,5 @@ library, the raw requests you make to the API are also able to call
client(SendMessageRequest('username', 'hello'))
The library will call the ``.resolve()`` method of the request, which will
resolve ``'username'`` with the appropriated ``InputPeer``. Don't worry if
resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if
you don't get this yet, but remember some of the details here are important.

View File

@ -66,6 +66,26 @@ Basic Usage
**More details**: :ref:`telegram-client`
Handling Updates
****************
.. code-block:: python
from telethon import events
# We need to have some worker running
client.updates.workers = 1
@client.on(events.NewMessage(incoming=True, pattern='(?i)hi'))
def handler(event):
event.reply('Hello!')
# If you want to handle updates you can't let the script end.
input('Press enter to exit.')
**More details**: :ref:`working-with-updates`
----------
You can continue by clicking on the "More details" link below each

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
object now has access to the ``client``, so that it can fetch the right
type if needed, or access the session database. Furthermore, you can
reuse requests that need "autocast" (e.g. you put ``User`` but ``InputPeer``
reuse requests that need "autocast" (e.g. you put :tl:`User` but ``InputPeer``
was needed), since ``.resolve()`` is called when invoking. Before, it was
only done on object construction.
@ -993,7 +993,7 @@ Bug fixes and enhancements (v0.13.3)
.. bugs-fixed-2:
Bug fixes
---------
~~~~~~~~~
- **Reconnection** used to fail because it tried invoking things from
the ``ReadThread``.
@ -1009,7 +1009,7 @@ Bug fixes
.. enhancements-3:
Enhancements
------------
~~~~~~~~~~~~
- **Request will be retried** up to 5 times by default rather than
failing on the first attempt.
@ -1099,7 +1099,7 @@ outside the buffer.
.. additions-2:
Additions
---------
~~~~~~~~~
- The mentioned different connection modes, and a new thread.
- You can modify the ``Session`` attributes through the
@ -1112,7 +1112,7 @@ Additions
.. enhancements-4:
Enhancements
------------
~~~~~~~~~~~~
- The low-level socket doesn't use a handcrafted timeout anymore, which
should benefit by avoiding the arbitrary ``sleep(0.1)`` that there
@ -1121,7 +1121,7 @@ Enhancements
``code`` was provided.
Deprecation
-----------
~~~~~~~~~~~
- ``.sign_up`` does *not* take a ``phone`` argument anymore. Change
this or you will be using ``phone`` as ``code``, and it will fail!
@ -1201,7 +1201,7 @@ friendly, along with some other stability enhancements, although it
brings quite a few changes.
Breaking changes
----------------
~~~~~~~~~~~~~~~~
- The ``TelegramClient`` methods ``.send_photo_file()``,
``.send_document_file()`` and ``.send_media_file()`` are now a
@ -1216,7 +1216,7 @@ Breaking changes
``.download_contact()`` still exist, but are private.
Additions
---------
~~~~~~~~~
- Updated to **layer 70**!
- Both downloading and uploading now support **stream-like objects**.
@ -1232,7 +1232,7 @@ Additions
.. bug-fixes-5:
Bug fixes
---------
~~~~~~~~~
- Crashing when migrating to a new layer and receiving old updates
should not happen now.
@ -1372,7 +1372,7 @@ Support for parallel connections (v0.11)
**read the whole change log**!
Breaking changes
----------------
~~~~~~~~~~~~~~~~
- Every Telegram error has now its **own class**, so it's easier to
fine-tune your ``except``\ 's.
@ -1384,7 +1384,7 @@ Breaking changes
anymore.
Additions
---------
~~~~~~~~~
- A new, more **lightweight class** has been added. The
``TelegramBareClient`` is now the base of the normal
@ -1404,7 +1404,7 @@ Additions
.. bug-fixes-6:
Bug fixes
---------
~~~~~~~~~
- Received errors are acknowledged to the server, so they don't happen
over and over.
@ -1418,7 +1418,7 @@ Bug fixes
not happen anymore.
Internal changes
----------------
~~~~~~~~~~~~~~~~
- Some fixes to the ``JsonSession``.
- Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while

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,
you can always add someone else with the `AddChatUserRequest`__,
which use is very straightforward:
you can always add someone else with the `AddChatUserRequest`__, which
use is very straightforward, or `InviteToChannelRequest`__ for channels:
.. code-block:: python
# For normal chats
from telethon.tl.functions.messages import AddChatUserRequest
client(AddChatUserRequest(
@ -69,6 +70,15 @@ which use is very straightforward:
fwd_limit=10 # Allow the user to see the 10 last messages
))
# For channels
from telethon.tl.functions.channels import InviteToChannelRequest
client(InviteToChannelRequest(
channel,
[users_to_add]
))
Checking a link without joining
*******************************
@ -84,6 +94,7 @@ __ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/invite_to_channel.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
@ -225,6 +236,12 @@ use `GetMessagesViewsRequest`__, setting ``increment=True``:
increment=True
))
Note that you can only do this **once or twice a day** per account,
running this in a loop will obviously not increase the views forever
unless you wait a day between each iteration. If you run it any sooner
than that, the views simply won't be increased.
__ https://github.com/LonamiWebs/Telethon/issues/233
__ https://github.com/LonamiWebs/Telethon/issues/305
__ https://github.com/LonamiWebs/Telethon/issues/409

View File

@ -10,3 +10,12 @@ telethon\.tl\.custom\.draft module
:undoc-members:
:show-inheritance:
telethon\.tl\.custom\.dialog module
-----------------------------------
.. automodule:: telethon.tl.custom.dialog
:members:
:undoc-members:
:show-inheritance:

View File

@ -155,7 +155,7 @@ def main():
'telethon_generator/parser/tl_parser.py',
]),
install_requires=['pyaes', 'rsa',
'typing' if version_info < (3, 5) else ""],
'typing' if version_info < (3, 5, 2) else ""],
extras_require={
'cryptg': ['cryptg']
}

View File

@ -37,8 +37,10 @@ class _EventBuilder(abc.ABC):
only matching chats will be handled.
blacklist_chats (:obj:`bool`, optional):
Whether to treat the the list of chats as a blacklist (if
it matches it will NOT be handled) or a whitelist (default).
Whether to treat the chats as a blacklist instead of
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``chats``
which will be ignored if ``blacklist_chats=True``.
"""
def __init__(self, chats=None, blacklist_chats=False):
self.chats = chats
@ -70,6 +72,7 @@ class _EventBuilder(abc.ABC):
class _EventCommon(abc.ABC):
"""Intermediate class with common things to all events"""
_event_name = 'Event'
def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
self._entities = {}
@ -90,13 +93,13 @@ class _EventCommon(abc.ABC):
async def _get_entity(self, msg_id, entity_id, chat=None):
"""
Helper function to call GetMessages on the give msg_id and
Helper function to call :tl:`GetMessages` on the give msg_id and
return the input entity whose ID is the given entity ID.
If ``chat`` is present it must be an InputPeer.
If ``chat`` is present it must be an :tl:`InputPeer`.
Returns a tuple of (entity, input_peer) if it was found, or
a tuple of (None, None) if it couldn't be.
Returns a tuple of ``(entity, input_peer)`` if it was found, or
a tuple of ``(None, None)`` if it couldn't be.
"""
try:
if isinstance(chat, types.InputPeerChannel):
@ -123,7 +126,7 @@ class _EventCommon(abc.ABC):
@property
async def input_chat(self):
"""
The (:obj:`InputPeer`) (group, megagroup or channel) on which
The (:tl:`InputPeer`) (group, megagroup or channel) on which
the event occurred. This doesn't have the title or anything,
but is useful if you don't need those to avoid further
requests.
@ -154,7 +157,7 @@ class _EventCommon(abc.ABC):
@property
async def chat(self):
"""
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
The (:tl:`User` | :tl:`Chat` | :tl:`Channel`, optional) on which
the event occurred. This property may make an API call the first time
to get the most up to date version of the chat (mostly when the event
doesn't belong to a channel), so keep that in mind.
@ -178,7 +181,7 @@ class _EventCommon(abc.ABC):
def to_dict(self):
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
d['_'] = self.__class__.__name__
d['_'] = self._event_name
return d
@ -196,7 +199,7 @@ class Raw(_EventBuilder):
def _name_inner_event(cls):
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
if hasattr(cls, 'Event'):
cls.Event.__name__ = '{}.Event'.format(cls.__name__)
cls.Event._event_name = '{}.Event'.format(cls.__name__)
else:
warnings.warn('Class {} does not have a inner Event'.format(cls))
return cls
@ -310,8 +313,8 @@ class NewMessage(_EventBuilder):
Represents the event of a new message.
Members:
message (:obj:`Message`):
This is the original ``Message`` object.
message (:tl:`Message`):
This is the original :tl:`Message` object.
is_private (:obj:`bool`):
True if the message was sent as a private message.
@ -406,7 +409,7 @@ class NewMessage(_EventBuilder):
@property
async def input_sender(self):
"""
This (:obj:`InputPeer`) is the input version of the user who
This (:tl:`InputPeer`) is the input version of the user who
sent the message. Similarly to ``input_chat``, this doesn't have
things like username or similar, but still useful in some cases.
@ -434,7 +437,7 @@ class NewMessage(_EventBuilder):
@property
async def sender(self):
"""
This (:obj:`User`) may make an API call the first time to get
This (:tl:`User`) may make an API call the first time to get
the most up to date version of the sender (mostly when the event
doesn't belong to a channel), so keep that in mind.
@ -474,8 +477,8 @@ class NewMessage(_EventBuilder):
@property
async def reply_message(self):
"""
This (:obj:`Message`, optional) will make an API call the first
time to get the full ``Message`` object that one was replying to,
This optional :tl:`Message` will make an API call the first
time to get the full :tl:`Message` object that one was replying to,
so use with care as there is no caching besides local caching yet.
"""
if not self.message.reply_to_msg_id:
@ -498,14 +501,14 @@ class NewMessage(_EventBuilder):
@property
def forward(self):
"""
The unmodified (:obj:`MessageFwdHeader`, optional).
The unmodified :tl:`MessageFwdHeader`, if present..
"""
return self.message.fwd_from
@property
def media(self):
"""
The unmodified (:obj:`MessageMedia`, optional).
The unmodified :tl:`MessageMedia`, if present.
"""
return self.message.media
@ -513,7 +516,7 @@ class NewMessage(_EventBuilder):
def photo(self):
"""
If the message media is a photo,
this returns the (:obj:`Photo`) object.
this returns the :tl:`Photo` object.
"""
if isinstance(self.message.media, types.MessageMediaPhoto):
photo = self.message.media.photo
@ -524,7 +527,7 @@ class NewMessage(_EventBuilder):
def document(self):
"""
If the message media is a document,
this returns the (:obj:`Document`) object.
this returns the :tl:`Document` object.
"""
if isinstance(self.message.media, types.MessageMediaDocument):
doc = self.message.media.document
@ -547,7 +550,7 @@ class NewMessage(_EventBuilder):
def audio(self):
"""
If the message media is a document with an Audio attribute,
this returns the (:obj:`Document`) object.
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: not attr.voice)
@ -556,7 +559,7 @@ class NewMessage(_EventBuilder):
def voice(self):
"""
If the message media is a document with a Voice attribute,
this returns the (:obj:`Document`) object.
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAudio,
lambda attr: attr.voice)
@ -565,7 +568,7 @@ class NewMessage(_EventBuilder):
def video(self):
"""
If the message media is a document with a Video attribute,
this returns the (:obj:`Document`) object.
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo)
@ -573,7 +576,7 @@ class NewMessage(_EventBuilder):
def video_note(self):
"""
If the message media is a document with a Video attribute,
this returns the (:obj:`Document`) object.
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeVideo,
lambda attr: attr.round_message)
@ -582,7 +585,7 @@ class NewMessage(_EventBuilder):
def gif(self):
"""
If the message media is a document with an Animated attribute,
this returns the (:obj:`Document`) object.
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeAnimated)
@ -590,7 +593,7 @@ class NewMessage(_EventBuilder):
def sticker(self):
"""
If the message media is a document with a Sticker attribute,
this returns the (:obj:`Document`) object.
this returns the :tl:`Document` object.
"""
return self._document_by_attribute(types.DocumentAttributeSticker)
@ -609,11 +612,12 @@ class ChatAction(_EventBuilder):
Represents an action in a chat (such as user joined, left, or new pin).
"""
def build(self, update):
if isinstance(update, types.UpdateChannelPinnedMessage):
# Telegram sends UpdateChannelPinnedMessage and then
# UpdateNewChannelMessage with MessageActionPinMessage.
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
# Telegram does not always send
# UpdateChannelPinnedMessage for new pins
# but always for unpin, with update.id = 0
event = ChatAction.Event(types.PeerChannel(update.channel_id),
new_pin=update.id)
unpin=True)
elif isinstance(update, types.UpdateChatParticipantAdd):
event = ChatAction.Event(types.PeerChat(update.chat_id),
@ -664,6 +668,11 @@ class ChatAction(_EventBuilder):
event = ChatAction.Event(msg,
users=msg.from_id,
new_photo=True)
elif isinstance(action, types.MessageActionPinMessage):
# Telegram always sends this service message for new pins
event = ChatAction.Event(msg,
users=msg.from_id,
new_pin=msg.reply_to_msg_id)
else:
return
else:
@ -678,12 +687,12 @@ class ChatAction(_EventBuilder):
Members:
new_pin (:obj:`bool`):
``True`` if the pin has changed (new pin or removed).
``True`` if there is a new pin.
new_photo (:obj:`bool`):
``True`` if there's a new chat photo (or it was removed).
photo (:obj:`Photo`, optional):
photo (:tl:`Photo`, optional):
The new photo (or ``None`` if it was removed).
@ -704,10 +713,13 @@ class ChatAction(_EventBuilder):
new_title (:obj:`bool`, optional):
The new title string for the chat, if applicable.
unpin (:obj:`bool`):
``True`` if the existing pin gets unpinned.
"""
def __init__(self, where, new_pin=None, new_photo=None,
added_by=None, kicked_by=None, created=None,
users=None, new_title=None):
users=None, new_title=None, unpin=None):
if isinstance(where, types.MessageService):
self.action_message = where
where = where.to_id
@ -726,7 +738,7 @@ class ChatAction(_EventBuilder):
self._added_by = None
self._kicked_by = None
self.user_added, self.user_joined, self.user_left,\
self.user_kicked = (False, False, False, False)
self.user_kicked, self.unpin = (False, False, False, False, False)
if added_by is True:
self.user_joined = True
@ -745,6 +757,7 @@ class ChatAction(_EventBuilder):
self._users = None
self._input_users = None
self.new_title = new_title
self.unpin = unpin
async def respond(self, *args, **kwargs):
"""
@ -785,7 +798,7 @@ class ChatAction(_EventBuilder):
@property
async def pinned_message(self):
"""
If ``new_pin`` is ``True``, this returns the (:obj:`Message`)
If ``new_pin`` is ``True``, this returns the (:tl:`Message`)
object that was pinned.
"""
if self._pinned_message == 0:
@ -851,7 +864,7 @@ class ChatAction(_EventBuilder):
@property
async def input_user(self):
"""
Input version of the self.user property.
Input version of the ``self.user`` property.
"""
if await self.input_users:
return self._input_users[0]
@ -888,7 +901,7 @@ class ChatAction(_EventBuilder):
@property
async def input_users(self):
"""
Input version of the self.users property.
Input version of the ``self.users`` property.
"""
if self._input_users is None and self._user_peers:
self._input_users = []
@ -941,7 +954,7 @@ class UserUpdate(_EventBuilder):
recently (:obj:`bool`):
``True`` if the user was seen within a day.
action (:obj:`SendMessageAction`, optional):
action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
cancel (:obj:`bool`):
@ -1066,6 +1079,9 @@ class MessageEdited(NewMessage):
event._entities = update.entities
return self._message_filter_event(event)
class Event(NewMessage.Event):
pass # Required if we want a different name for it
@_name_inner_event
class MessageDeleted(_EventBuilder):
@ -1100,22 +1116,22 @@ class MessageDeleted(_EventBuilder):
class StopPropagation(Exception):
"""
If this Exception is found to be raised in any of the handlers for a
given update, it will stop the execution of all other registered
event handlers in the chain.
Think of it like a ``StopIteration`` exception in a for loop.
If this exception is raised in any of the handlers for a given event,
it will stop the execution of all other registered event handlers.
It can be seen as the ``StopIteration`` in a for loop but for events.
Example usage:
```
@client.on(events.NewMessage)
def delete(event):
event.delete()
# Other handlers won't have an event to work with
raise StopPropagation
@client.on(events.NewMessage)
def _(event):
# Will never be reached, because it is the second handler in the chain.
pass
```
>>> @client.on(events.NewMessage)
... def delete(event):
... event.delete()
... # No other event handler will have a chance to handle this event
... raise StopPropagation
...
>>> @client.on(events.NewMessage)
... def _(event):
... # Will never be reached, because it is the second handler
... pass
"""
# For some reason Sphinx wants the silly >>> or
# it will show warnings and look bad when generated.
pass

View File

@ -21,7 +21,7 @@ DEFAULT_DELIMITERS = {
'```': MessageEntityPre
}
DEFAULT_URL_RE = re.compile(r'\[([^\]]+)\]\((.+?)\)')
DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)')
DEFAULT_URL_FORMAT = '[{0}]({1})'

View File

@ -175,7 +175,16 @@ class TcpClient:
except asyncio.TimeoutError as e:
# These are somewhat common if the server has nothing
# to send to us, so use a lower logging priority.
__log__.debug('socket.timeout "%s" while reading data', e)
if bytes_left < size:
__log__.warning(
'socket.timeout "%s" when %d/%d had been received',
e, size - bytes_left, size
)
else:
__log__.debug(
'socket.timeout "%s" while reading data', e
)
raise TimeoutError() from e
except ConnectionError as e:
__log__.info('ConnectionError "%s" while reading data', e)

View File

@ -25,12 +25,14 @@ __log__ = logging.getLogger(__name__)
class MtProtoSender:
"""MTProto Mobile Protocol sender
"""
MTProto Mobile Protocol sender
(https://core.telegram.org/mtproto/description).
Note that this class is not thread-safe, and calling send/receive
from two or more threads at the same time is undefined behaviour.
Rationale: a new connection should be spawned to send/receive requests
Rationale:
a new connection should be spawned to send/receive requests
in parallel, so thread-safety (hence locking) isn't needed.
"""

View File

@ -78,7 +78,7 @@ class MemorySession(Session):
try:
p = utils.get_input_peer(e, allow_self=False)
marked_id = utils.get_peer_id(p)
except ValueError:
except TypeError:
return
if isinstance(p, (InputPeerUser, InputPeerChannel)):

View File

@ -91,18 +91,13 @@ from .extensions import markdown, html
__log__ = logging.getLogger(__name__)
class _Box:
"""Helper class to pass parameters by reference"""
def __init__(self, x=None):
self.x = x
class TelegramClient(TelegramBareClient):
"""
Initializes the Telegram client with the specified API ID and Hash.
Args:
session (:obj:`str` | :obj:`Session` | :obj:`None`):
session (:obj:`str` | :obj:`telethon.sessions.abstract.Session`, \
:obj:`None`):
The file name of the session file to be used if a string is
given (it may be a full path), or the Session instance to be
used otherwise. If it's ``None``, the session will not be saved,
@ -169,7 +164,7 @@ class TelegramClient(TelegramBareClient):
connection_mode=ConnectionMode.TCP_FULL,
use_ipv6=False,
proxy=None,
timeout=timedelta(seconds=5),
timeout=timedelta(seconds=10),
loop=None,
report_errors=True,
**kwargs):
@ -216,7 +211,7 @@ class TelegramClient(TelegramBareClient):
Whether to force sending as SMS.
Returns:
Information about the result of the request.
An instance of :tl:`SentCode`.
"""
phone = utils.parse_phone(phone) or self._phone
phone_hash = self._phone_code_hash.get(phone)
@ -261,8 +256,9 @@ class TelegramClient(TelegramBareClient):
This is only required if it is enabled in your account.
bot_token (:obj:`str`):
Bot Token obtained by @BotFather to log in as a bot.
Cannot be specified with `phone` (only one of either allowed).
Bot Token obtained by `@BotFather <https://t.me/BotFather>`_
to log in as a bot. Cannot be specified with ``phone`` (only
one of either allowed).
force_sms (:obj:`bool`, optional):
Whether to force sending the code request as SMS.
@ -280,8 +276,8 @@ class TelegramClient(TelegramBareClient):
Similar to the first name, but for the last. Optional.
Returns:
:obj:`TelegramClient`:
This client, so initialization can be chained with `.start()`.
This :obj:`TelegramClient`, so initialization
can be chained with ``.start()``.
"""
if code_callback is None:
@ -377,7 +373,10 @@ class TelegramClient(TelegramBareClient):
these requests.
code (:obj:`str` | :obj:`int`):
The code that Telegram sent.
The code that Telegram sent. Note that if you have sent this
code through the application itself it will immediately
expire. If you want to send the code, obfuscate it somehow.
If you're not doing any of this you can ignore this note.
password (:obj:`str`):
2FA password, should be used if a previous call raised
@ -393,7 +392,7 @@ class TelegramClient(TelegramBareClient):
Returns:
The signed in user, or the information about
:meth:`.send_code_request()`.
:meth:`send_code_request`.
"""
if self.is_user_authorized():
await self._check_events_pending_resolve()
@ -454,7 +453,7 @@ class TelegramClient(TelegramBareClient):
Optional last name.
Returns:
The new created user.
The new created :tl:`User`.
"""
if self.is_user_authorized():
await self._check_events_pending_resolve()
@ -479,7 +478,7 @@ class TelegramClient(TelegramBareClient):
Logs out Telegram and deletes the current ``*.session`` file.
Returns:
True if the operation was successful.
``True`` if the operation was successful.
"""
try:
await self(LogOutRequest())
@ -497,12 +496,12 @@ class TelegramClient(TelegramBareClient):
Args:
input_peer (:obj:`bool`, optional):
Whether to return the ``InputPeerUser`` version or the normal
``User``. This can be useful if you just need to know the ID
Whether to return the :tl:`InputPeerUser` version or the normal
:tl:`User`. This can be useful if you just need to know the ID
of yourself.
Returns:
:obj:`User`: Your own user.
Your own :tl:`User`.
"""
if input_peer and self._self_input_peer:
return self._self_input_peer
@ -522,7 +521,7 @@ class TelegramClient(TelegramBareClient):
# region Dialogs ("chats") requests
async def iter_dialogs(self, limit=None, offset_date=None, offset_id=0,
offset_peer=InputPeerEmpty(), _total_box=None):
offset_peer=InputPeerEmpty(), _total=None):
"""
Returns an iterator over the dialogs, yielding 'limit' at most.
Dialogs are the open "chats" or conversations with other people.
@ -541,18 +540,18 @@ class TelegramClient(TelegramBareClient):
offset_id (:obj:`int`, optional):
The message ID to be used as an offset.
offset_peer (:obj:`InputPeer`, optional):
offset_peer (:tl:`InputPeer`, optional):
The peer to be used as an offset.
_total_box (:obj:`_Box`, optional):
A _Box instance to pass the total parameter by reference.
_total (:obj:`list`, optional):
A single-item list to pass the total parameter by reference.
Yields:
Instances of ``telethon.tl.custom.Dialog``.
Instances of :obj:`telethon.tl.custom.dialog.Dialog`.
"""
limit = float('inf') if limit is None else int(limit)
if limit == 0:
if not _total_box:
if not _total:
return
# Special case, get a single dialog and determine count
dialogs = await self(GetDialogsRequest(
@ -561,7 +560,7 @@ class TelegramClient(TelegramBareClient):
offset_peer=offset_peer,
limit=1
))
_total_box.x = getattr(dialogs, 'count', len(dialogs.dialogs))
_total[0] = getattr(dialogs, 'count', len(dialogs.dialogs))
return
seen = set()
@ -575,8 +574,8 @@ class TelegramClient(TelegramBareClient):
req.limit = min(limit - len(seen), 100)
r = await self(req)
if _total_box:
_total_box.x = getattr(r, 'count', len(r.dialogs))
if _total:
_total[0] = getattr(r, 'count', len(r.dialogs))
messages = {m.id: m for m in r.messages}
entities = {utils.get_peer_id(x): x
for x in itertools.chain(r.users, r.chats)}
@ -604,24 +603,24 @@ class TelegramClient(TelegramBareClient):
async def get_dialogs(self, *args, **kwargs):
"""
Same as :meth:`iter_dialogs`, but returns a list instead
with an additional .total attribute on the list.
with an additional ``.total`` attribute on the list.
"""
total_box = _Box(0)
kwargs['_total_box'] = total_box
total = [0]
kwargs['_total'] = total
dialogs = UserList()
async for dialog in self.iter_dialogs(*args, **kwargs):
dialogs.append(dialog)
dialogs.total = total_box.x
dialogs.total = total[0]
return dialogs
async def iter_drafts(self): # TODO: Ability to provide a `filter`
"""
Iterator over all open draft messages.
The yielded items are custom ``Draft`` objects that are easier to use.
You can call ``draft.set_message('text')`` to change the message,
or delete it through :meth:`draft.delete()`.
Instances of :obj:`telethon.tl.custom.draft.Draft` are yielded.
You can call :obj:`telethon.tl.custom.draft.Draft.set_message`
to change the message or :obj:`telethon.tl.custom.draft.Draft.delete`
among other things.
"""
for update in (await self(GetAllDraftsRequest())).updates:
yield Draft._from_update(self, update)
@ -675,7 +674,7 @@ class TelegramClient(TelegramBareClient):
async def _parse_message_text(self, message, parse_mode):
"""
Returns a (parsed message, entities) tuple depending on parse_mode.
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
"""
if not parse_mode:
return message, []
@ -714,10 +713,10 @@ class TelegramClient(TelegramBareClient):
entity (:obj:`entity`):
To who will it be sent.
message (:obj:`str` | :obj:`Message`):
message (:obj:`str` | :tl:`Message`):
The message to be sent, or another message object to resend.
reply_to (:obj:`int` | :obj:`Message`, optional):
reply_to (:obj:`int` | :tl:`Message`, optional):
Whether to reply to a message or not. If an integer is provided,
it should be the ID of the message that it should reply to.
@ -742,7 +741,7 @@ class TelegramClient(TelegramBareClient):
Has no effect when sending a file.
Returns:
the sent message
The sent :tl:`Message`.
"""
if file is not None:
return await self.send_file(
@ -809,7 +808,7 @@ class TelegramClient(TelegramBareClient):
entity (:obj:`entity`):
To which entity the message(s) will be forwarded.
messages (:obj:`list` | :obj:`int` | :obj:`Message`):
messages (:obj:`list` | :obj:`int` | :tl:`Message`):
The message(s) to forward, or their integer IDs.
from_peer (:obj:`entity`):
@ -818,7 +817,7 @@ class TelegramClient(TelegramBareClient):
order for the forward to work.
Returns:
The forwarded messages.
The list of forwarded :tl:`Message`.
"""
if not utils.is_list_like(messages):
messages = (messages,)
@ -848,7 +847,7 @@ class TelegramClient(TelegramBareClient):
for update in result.updates:
if isinstance(update, UpdateMessageID):
random_to_id[update.random_id] = update.id
elif isinstance(update, UpdateNewMessage):
elif isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
id_to_message[update.message.id] = update.message
return [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
@ -885,7 +884,7 @@ class TelegramClient(TelegramBareClient):
not modified at all.
Returns:
the edited message
The edited :tl:`Message`.
"""
message, msg_entities = await self._parse_message_text(message, parse_mode)
request = EditMessageRequest(
@ -908,7 +907,7 @@ class TelegramClient(TelegramBareClient):
be ``None`` for normal chats, but **must** be present
for channels and megagroups.
message_ids (:obj:`list` | :obj:`int` | :obj:`Message`):
message_ids (:obj:`list` | :obj:`int` | :tl:`Message`):
The IDs (or ID) or messages to be deleted.
revoke (:obj:`bool`, optional):
@ -918,7 +917,7 @@ class TelegramClient(TelegramBareClient):
This has no effect on channels or megagroups.
Returns:
The affected messages.
The :tl:`AffectedMessages`.
"""
if not utils.is_list_like(message_ids):
message_ids = (message_ids,)
@ -940,7 +939,7 @@ class TelegramClient(TelegramBareClient):
async def iter_messages(self, entity, limit=20, offset_date=None,
offset_id=0, max_id=0, min_id=0, add_offset=0,
batch_size=100, wait_time=None, _total_box=None):
batch_size=100, wait_time=None, _total=None):
"""
Iterator over the message history for the specified entity.
@ -981,16 +980,16 @@ class TelegramClient(TelegramBareClient):
you are still free to do so.
wait_time (:obj:`int`):
Wait time between different ``GetHistoryRequest``. Use this
Wait time between different :tl:`GetHistoryRequest`. Use this
parameter to avoid hitting the ``FloodWaitError`` as needed.
If left to ``None``, it will default to 1 second only if
the limit is higher than 3000.
_total_box (:obj:`_Box`, optional):
A _Box instance to pass the total parameter by reference.
_total (:obj:`list`, optional):
A single-item list to pass the total parameter by reference.
Yields:
Instances of ``telethon.tl.types.Message`` with extra attributes:
Instances of :tl:`Message` with extra attributes:
* ``.sender`` = entity of the sender.
* ``.fwd_from.sender`` = if fwd_from, who sent it originally.
@ -998,7 +997,7 @@ class TelegramClient(TelegramBareClient):
* ``.to`` = entity to which the message was sent.
Notes:
Telegram's flood wait limit for ``GetHistoryRequest`` seems to
Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to
be around 30 seconds per 3000 messages, therefore a sleep of 1
second is the default for this limit (or above). You may need
an higher limit, so you're free to set the ``batch_size`` that
@ -1007,7 +1006,7 @@ class TelegramClient(TelegramBareClient):
entity = await self.get_input_entity(entity)
limit = float('inf') if limit is None else int(limit)
if limit == 0:
if not _total_box:
if not _total:
return
# No messages, but we still need to know the total message count
result = await self(GetHistoryRequest(
@ -1015,7 +1014,7 @@ class TelegramClient(TelegramBareClient):
offset_date=None, offset_id=0, max_id=0, min_id=0,
add_offset=0, hash=0
))
_total_box.x = getattr(result, 'count', len(result.messages))
_total[0] = getattr(result, 'count', len(result.messages))
return
if wait_time is None:
@ -1036,8 +1035,8 @@ class TelegramClient(TelegramBareClient):
add_offset=add_offset,
hash=0
))
if _total_box:
_total_box.x = getattr(r, 'count', len(r.messages))
if _total:
_total[0] = getattr(r, 'count', len(r.messages))
entities = {utils.get_peer_id(x): x
for x in itertools.chain(r.users, r.chats)}
@ -1080,15 +1079,15 @@ class TelegramClient(TelegramBareClient):
async def get_messages(self, *args, **kwargs):
"""
Same as :meth:`iter_messages`, but returns a list instead
with an additional .total attribute on the list.
with an additional ``.total`` attribute on the list.
"""
total_box = _Box(0)
kwargs['_total_box'] = total_box
total = [0]
kwargs['_total'] = total
msgs = UserList()
async for msg in self.iter_messages(*args, **kwargs):
msgs.append(msg)
msgs.total = total_box.x
msgs.total = total[0]
return msgs
async def get_message_history(self, *args, **kwargs):
@ -1107,7 +1106,7 @@ class TelegramClient(TelegramBareClient):
entity (:obj:`entity`):
The chat where these messages are located.
message (:obj:`list` | :obj:`Message`):
message (:obj:`list` | :tl:`Message`):
Either a list of messages or a single message.
max_id (:obj:`int`):
@ -1164,8 +1163,7 @@ class TelegramClient(TelegramBareClient):
raise TypeError('Invalid message type: {}'.format(type(message)))
async def iter_participants(self, entity, limit=None, search='',
filter=None, aggressive=False,
_total_box=None):
filter=None, aggressive=False, _total=None):
"""
Iterator over the participants belonging to the specified chat.
@ -1179,9 +1177,8 @@ class TelegramClient(TelegramBareClient):
search (:obj:`str`, optional):
Look for participants with this string in name/username.
filter (:obj:`ChannelParticipantsFilter`, optional):
The filter to be used, if you want e.g. only admins. See
https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html.
filter (:tl:`ChannelParticipantsFilter`, optional):
The filter to be used, if you want e.g. only admins
Note that you might not have permissions for some filter.
This has no effect for normal chats or users.
@ -1195,14 +1192,14 @@ class TelegramClient(TelegramBareClient):
This has no effect for groups or channels with less than
10,000 members, or if a ``filter`` is given.
_total_box (:obj:`_Box`, optional):
A _Box instance to pass the total parameter by reference.
_total (:obj:`list`, optional):
A single-item list to pass the total parameter by reference.
Yields:
The ``User`` objects returned by ``GetParticipantsRequest``
The :tl:`User` objects returned by :tl:`GetParticipantsRequest`
with an additional ``.participant`` attribute which is the
matched ``ChannelParticipant`` type for channels/megagroups
or ``ChatParticipants`` for normal chats.
matched :tl:`ChannelParticipant` type for channels/megagroups
or :tl:`ChatParticipants` for normal chats.
"""
if isinstance(filter, type):
filter = filter()
@ -1224,8 +1221,8 @@ class TelegramClient(TelegramBareClient):
total = (await self(GetFullChannelRequest(
entity
))).full_chat.participants_count
if _total_box:
_total_box.x = total
if _total:
_total[0] = total
if limit == 0:
return
@ -1285,8 +1282,8 @@ class TelegramClient(TelegramBareClient):
elif isinstance(entity, InputPeerChat):
# TODO We *could* apply the `filter` here ourselves
full = await self(GetFullChatRequest(entity.chat_id))
if _total_box:
_total_box.x = len(full.full_chat.participants.participants)
if _total:
_total[0] = len(full.full_chat.participants.participants)
have = 0
users = {user.id: user for user in full.users}
@ -1302,8 +1299,8 @@ class TelegramClient(TelegramBareClient):
user.participant = participant
yield user
else:
if _total_box:
_total_box.x = 1
if _total:
_total[0] = 1
if limit != 0:
user = await self.get_entity(entity)
if filter_entity(user):
@ -1313,14 +1310,14 @@ class TelegramClient(TelegramBareClient):
async def get_participants(self, *args, **kwargs):
"""
Same as :meth:`iter_participants`, but returns a list instead
with an additional .total attribute on the list.
with an additional ``.total`` attribute on the list.
"""
total_box = _Box(0)
kwargs['_total_box'] = total_box
total = [0]
kwargs['_total'] = total
participants = UserList()
async for participant in self.iter_participants(*args, **kwargs):
participants.append(participant)
participants.total = total_box.x
participants.total = total[0]
return participants
# endregion
@ -1371,12 +1368,12 @@ class TelegramClient(TelegramBareClient):
A callback function accepting two parameters:
``(sent bytes, total)``.
reply_to (:obj:`int` | :obj:`Message`):
reply_to (:obj:`int` | :tl:`Message`):
Same as reply_to from .send_message().
attributes (:obj:`list`, optional):
Optional attributes that override the inferred ones, like
``DocumentAttributeFilename`` and so on.
:tl:`DocumentAttributeFilename` and so on.
thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional):
Optional thumbnail (for videos).
@ -1399,13 +1396,16 @@ class TelegramClient(TelegramBareClient):
it will be used to determine metadata from audio and video files.
Returns:
The message (or messages) containing the sent file.
The :tl:`Message` (or messages) containing the sent file.
"""
# First check if the user passed an iterable, in which case
# we may want to send as an album if all are photo files.
if utils.is_list_like(file):
# TODO Fix progress_callback
images = []
if force_document:
documents = file
else:
documents = []
for x in file:
if utils.is_image(x):
@ -1424,7 +1424,7 @@ class TelegramClient(TelegramBareClient):
result.extend(
await self.send_file(
entity, x, allow_cache=False,
entity, x, allow_cache=allow_cache,
caption=caption, force_document=force_document,
progress_callback=progress_callback, reply_to=reply_to,
attributes=attributes, thumb=thumb, **kwargs
@ -1557,7 +1557,7 @@ class TelegramClient(TelegramBareClient):
return msg
def send_voice_note(self, *args, **kwargs):
"""Wrapper method around .send_file() with is_voice_note=True"""
"""Wrapper method around :meth:`send_file` with is_voice_note=True."""
kwargs['is_voice_note'] = True
return self.send_file(*args, **kwargs)
@ -1654,8 +1654,8 @@ class TelegramClient(TelegramBareClient):
``(sent bytes, total)``.
Returns:
``InputFileBig`` if the file size is larger than 10MB,
``InputSizedFile`` (subclass of ``InputFile``) otherwise.
:tl:`InputFileBig` if the file size is larger than 10MB,
``InputSizedFile`` (subclass of :tl:`InputFile`) otherwise.
"""
if isinstance(file, (InputFile, InputFileBig)):
return file # Already uploaded
@ -1838,7 +1838,7 @@ class TelegramClient(TelegramBareClient):
"""
Downloads the given media, or the media from a specified Message.
message (:obj:`Message` | :obj:`Media`):
message (:tl:`Message` | :tl:`Media`):
The media or message containing the media that will be downloaded.
file (:obj:`str` | :obj:`file`, optional):
@ -1847,7 +1847,7 @@ class TelegramClient(TelegramBareClient):
progress_callback (:obj:`callable`, optional):
A callback function accepting two parameters:
``(recv bytes, total)``.
``(received bytes, total)``.
Returns:
``None`` if no media was provided, or if it was Empty. On success
@ -1918,7 +1918,7 @@ class TelegramClient(TelegramBareClient):
return file
async def _download_document(self, document, file, date, progress_callback):
"""Specialized version of .download_media() for documents"""
"""Specialized version of .download_media() for documents."""
if isinstance(document, MessageMediaDocument):
document = document.document
if not isinstance(document, Document):
@ -1965,7 +1965,7 @@ class TelegramClient(TelegramBareClient):
@staticmethod
def _download_contact(mm_contact, file):
"""Specialized version of .download_media() for contacts.
Will make use of the vCard 4.0 format
Will make use of the vCard 4.0 format.
"""
first_name = mm_contact.first_name
last_name = mm_contact.last_name
@ -2063,7 +2063,7 @@ class TelegramClient(TelegramBareClient):
Downloads the given input location to a file.
Args:
input_location (:obj:`InputFileLocation`):
input_location (:tl:`InputFileLocation`):
The file location from which the file will be downloaded.
file (:obj:`str` | :obj:`file`):
@ -2286,7 +2286,7 @@ class TelegramClient(TelegramBareClient):
"""
Turns the given entity into a valid Telegram user or chat.
entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`):
entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`):
The entity (or iterable of entities) to be transformed.
If it's a string which can be converted to an integer or starts
with '+' it will be resolved as if it were a phone number.
@ -2302,7 +2302,7 @@ class TelegramClient(TelegramBareClient):
error will be raised.
Returns:
``User``, ``Chat`` or ``Channel`` corresponding to the input
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the input
entity.
"""
if utils.is_list_like(entity):
@ -2401,9 +2401,10 @@ class TelegramClient(TelegramBareClient):
Turns the given peer into its input entity version. Most requests
use this kind of InputUser, InputChat and so on, so this is the
most suitable call to make for those cases.
entity (:obj:`str` | :obj:`int` | :obj:`Peer` | :obj:`InputPeer`):
entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`):
The integer ID of an user or otherwise either of a
``PeerUser``, ``PeerChat`` or ``PeerChannel``, for
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for
which to get its ``Input*`` version.
If this ``Peer`` hasn't been seen before by the library, the top
dialogs will be loaded and their entities saved to the session
@ -2411,7 +2412,7 @@ class TelegramClient(TelegramBareClient):
If in the end the access hash required for the peer was not found,
a ValueError will be raised.
Returns:
``InputPeerUser``, ``InputPeerChat`` or ``InputPeerChannel``.
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`.
"""
try:
# First try to get the entity from cache, otherwise figure it out

View File

@ -7,7 +7,47 @@ class Dialog:
Custom class that encapsulates a dialog (an open "conversation" with
someone, a group or a channel) providing an abstraction to easily
access the input version/normal entity/message etc. The library will
return instances of this class when calling `client.get_dialogs()`.
return instances of this class when calling :meth:`.get_dialogs()`.
Args:
dialog (:tl:`Dialog`):
The original ``Dialog`` instance.
pinned (:obj:`bool`):
Whether this dialog is pinned to the top or not.
message (:tl:`Message`):
The last message sent on this dialog. Note that this member
will not be updated when new messages arrive, it's only set
on creation of the instance.
date (:obj:`datetime`):
The date of the last message sent on this dialog.
entity (:obj:`entity`):
The entity that belongs to this dialog (user, chat or channel).
input_entity (:tl:`InputPeer`):
Input version of the entity.
id (:obj:`int`):
The marked ID of the entity, which is guaranteed to be unique.
name (:obj:`str`):
Display name for this dialog. For chats and channels this is
their title, and for users it's "First-Name Last-Name".
unread_count (:obj:`int`):
How many messages are currently unread in this dialog. Note that
this value won't update when new messages arrive.
unread_mentions_count (:obj:`int`):
How many mentions are currently unread in this dialog. Note that
this value won't update when new messages arrive.
draft (:obj:`telethon.tl.custom.draft.Draft`):
The draft object in this dialog. It will not be ``None``,
so you can call ``draft.set_message(...)``.
"""
def __init__(self, client, dialog, entities, messages):
# Both entities and messages being dicts {ID: item}
@ -19,6 +59,7 @@ class Dialog:
self.entity = entities[utils.get_peer_id(dialog.peer)]
self.input_entity = utils.get_input_peer(self.entity)
self.id = utils.get_peer_id(self.input_entity)
self.name = utils.get_display_name(self.entity)
self.unread_count = dialog.unread_count
@ -29,6 +70,6 @@ class Dialog:
async def send_message(self, *args, **kwargs):
"""
Sends a message to this dialog. This is just a wrapper around
client.send_message(dialog.input_entity, *args, **kwargs).
``client.send_message(dialog.input_entity, *args, **kwargs)``.
"""
return await self._client.send_message(self.input_entity, *args, **kwargs)

View File

@ -1,3 +1,5 @@
import datetime
from ..functions.messages import SaveDraftRequest
from ..types import UpdateDraftMessage, DraftMessage
from ...extensions import markdown
@ -7,7 +9,17 @@ class Draft:
"""
Custom class that encapsulates a draft on the Telegram servers, providing
an abstraction to change the message conveniently. The library will return
instances of this class when calling ``client.get_drafts()``.
instances of this class when calling :meth:`get_drafts()`.
Args:
date (:obj:`datetime`):
The date of the draft.
link_preview (:obj:`bool`):
Whether the link preview is enabled or not.
reply_to_msg_id (:obj:`int`):
The message ID that the draft will reply to.
"""
def __init__(self, client, peer, draft):
self._client = client
@ -33,20 +45,41 @@ class Draft:
@property
async def entity(self):
"""
The entity that belongs to this dialog (user, chat or channel).
"""
return await self._client.get_entity(self._peer)
@property
async def input_entity(self):
"""
Input version of the entity.
"""
return await self._client.get_input_entity(self._peer)
@property
def text(self):
"""
The markdown text contained in the draft. It will be
empty if there is no text (and hence no draft is set).
"""
return self._text
@property
def raw_text(self):
"""
The raw (text without formatting) contained in the draft.
It will be empty if there is no text (thus draft not set).
"""
return self._raw_text
@property
def is_empty(self):
"""
Convenience bool to determine if the draft is empty or not.
"""
return not self._text
async def set_message(self, text=None, reply_to=0, parse_mode='md',
link_preview=None):
"""
@ -89,10 +122,15 @@ class Draft:
self._raw_text = raw_text
self.link_preview = link_preview
self.reply_to_msg_id = reply_to
self.date = datetime.datetime.now()
return result
async def send(self, clear=True, parse_mode='md'):
"""
Sends the contents of this draft to the dialog. This is just a
wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``.
"""
await self._client.send_message(self._peer, self.text,
reply_to=self.reply_to_msg_id,
link_preview=self.link_preview,
@ -101,7 +139,6 @@ class Draft:
async def delete(self):
"""
Deletes this draft
:return bool: ``True`` on success
Deletes this draft, and returns ``True`` on success.
"""
return await self.set_message(text='')

View File

@ -10,8 +10,9 @@ __log__ = logging.getLogger(__name__)
class UpdateState:
"""Used to hold the current state of processed updates.
To retrieve an update, .poll() should be called.
"""
Used to hold the current state of processed updates.
To retrieve an update, :meth:`poll` should be called.
"""
WORKER_POLL_TIMEOUT = 5.0 # Avoid waiting forever on the workers

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):
"""
Gets the display name for the given entity, if it's an ``User``,
``Chat`` or ``Channel``. Returns an empty string otherwise.
Gets the display name for the given entity, if it's an :tl:`User`,
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise.
"""
if isinstance(entity, User):
if entity.last_name and entity.first_name:
@ -58,7 +58,7 @@ def get_display_name(entity):
def get_extension(media):
"""Gets the corresponding extension for any Telegram media"""
"""Gets the corresponding extension for any Telegram media."""
# Photos are always compressed as .jpg by Telegram
if isinstance(media, (UserProfilePhoto, ChatPhoto, MessageMediaPhoto)):
@ -83,8 +83,10 @@ def _raise_cast_fail(entity, target):
def get_input_peer(entity, allow_self=True):
"""Gets the input peer for the given "entity" (user, chat or channel).
A TypeError is raised if the given entity isn't a supported type."""
"""
Gets the input peer for the given "entity" (user, chat or channel).
A ``TypeError`` is raised if the given entity isn't a supported type.
"""
try:
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
return entity
@ -129,7 +131,7 @@ def get_input_peer(entity, allow_self=True):
def get_input_channel(entity):
"""Similar to get_input_peer, but for InputChannel's alone"""
"""Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone."""
try:
if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
return entity
@ -146,7 +148,7 @@ def get_input_channel(entity):
def get_input_user(entity):
"""Similar to get_input_peer, but for InputUser's alone"""
"""Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone."""
try:
if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'):
return entity
@ -175,7 +177,7 @@ def get_input_user(entity):
def get_input_document(document):
"""Similar to get_input_peer, but for documents"""
"""Similar to :meth:`get_input_peer`, but for documents"""
try:
if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'):
return document
@ -198,7 +200,7 @@ def get_input_document(document):
def get_input_photo(photo):
"""Similar to get_input_peer, but for documents"""
"""Similar to :meth:`get_input_peer`, but for photos"""
try:
if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'):
return photo
@ -218,7 +220,7 @@ def get_input_photo(photo):
def get_input_geo(geo):
"""Similar to get_input_peer, but for geo points"""
"""Similar to :meth:`get_input_peer`, but for geo points"""
try:
if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'):
return geo
@ -241,10 +243,11 @@ def get_input_geo(geo):
def get_input_media(media, is_photo=False):
"""Similar to get_input_peer, but for media.
"""
Similar to :meth:`get_input_peer`, but for media.
If the media is a file location and is_photo is known to be True,
it will be treated as an InputMediaUploadedPhoto.
If the media is a file location and ``is_photo`` is known to be ``True``,
it will be treated as an :tl:`InputMediaUploadedPhoto`.
"""
try:
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'):
@ -317,7 +320,7 @@ def get_input_media(media, is_photo=False):
def is_image(file):
"""
Returns True if the file extension looks like an image file to Telegram.
Returns ``True`` if the file extension looks like an image file to Telegram.
"""
if not isinstance(file, str):
return False
@ -326,23 +329,23 @@ def is_image(file):
def is_audio(file):
"""Returns True if the file extension looks like an audio file"""
"""Returns ``True`` if the file extension looks like an audio file."""
return (isinstance(file, str) and
(mimetypes.guess_type(file)[0] or '').startswith('audio/'))
def is_video(file):
"""Returns True if the file extension looks like a video file"""
"""Returns ``True`` if the file extension looks like a video file."""
return (isinstance(file, str) and
(mimetypes.guess_type(file)[0] or '').startswith('video/'))
def is_list_like(obj):
"""
Returns True if the given object looks like a list.
Returns ``True`` if the given object looks like a list.
Checking if hasattr(obj, '__iter__') and ignoring str/bytes is not
enough. Things like open() are also iterable (and probably many
Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not
enough. Things like ``open()`` are also iterable (and probably many
other things), so just support the commonly known list-like objects.
"""
return isinstance(obj, (list, tuple, set, dict,
@ -350,7 +353,7 @@ def is_list_like(obj):
def parse_phone(phone):
"""Parses the given phone, or returns None if it's invalid"""
"""Parses the given phone, or returns ``None`` if it's invalid."""
if isinstance(phone, int):
return str(phone)
else:
@ -365,7 +368,7 @@ def parse_username(username):
both the stripped, lowercase username and whether it is
a joinchat/ hash (in which case is not lowercase'd).
Returns None if the username is not valid.
Returns ``None`` if the ``username`` is not valid.
"""
username = username.strip()
m = USERNAME_RE.match(username)
@ -386,7 +389,7 @@ def parse_username(username):
def _fix_peer_id(peer_id):
"""
Fixes the peer ID for chats and channels, in case the users
mix marking the ID with the ``Peer()`` constructors.
mix marking the ID with the :tl:`Peer` constructors.
"""
peer_id = abs(peer_id)
if str(peer_id).startswith('100'):
@ -401,7 +404,7 @@ def get_peer_id(peer):
chat ID is negated, and channel ID is prefixed with -100.
The original ID and the peer type class can be returned with
a call to utils.resolve_id(marked_id).
a call to :meth:`resolve_id(marked_id)`.
"""
# First we assert it's a Peer TLObject, or early return for integers
if isinstance(peer, int):
@ -450,7 +453,7 @@ def get_peer_id(peer):
def resolve_id(marked_id):
"""Given a marked ID, returns the original ID and its Peer type"""
"""Given a marked ID, returns the original ID and its :tl:`Peer` type."""
if marked_id >= 0:
return marked_id, PeerUser
@ -461,8 +464,10 @@ def resolve_id(marked_id):
def get_appropriated_part_size(file_size):
"""Gets the appropriated part size when uploading or downloading files,
given an initial file size"""
"""
Gets the appropriated part size when uploading or downloading files,
given an initial file size.
"""
if file_size <= 104857600: # 100MB
return 128
if file_size <= 786432000: # 750MB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

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
from telethon.crypto import AES, Factorization
from telethon.crypto import rsa
from Crypto.PublicKey import RSA as PyCryptoRSA
# from crypto.PublicKey import RSA as PyCryptoRSA
class CryptoTests(unittest.TestCase):
@ -22,37 +21,38 @@ class CryptoTests(unittest.TestCase):
self.cipher_text_padded = b"W\xd1\xed'\x01\xa6c\xc3\xcb\xef\xaa\xe5\x1d\x1a" \
b"[\x1b\xdf\xcdI\x1f>Z\n\t\xb9\xd2=\xbaF\xd1\x8e'"
@staticmethod
def test_sha1():
def test_sha1(self):
string = 'Example string'
hash_sum = sha1(string.encode('utf-8')).digest()
expected = b'\nT\x92|\x8d\x06:)\x99\x04\x8e\xf8j?\xc4\x8e\xd3}m9'
assert hash_sum == expected, 'Invalid sha1 hash_sum representation (should be {}, but is {})'\
.format(expected, hash_sum)
self.assertEqual(hash_sum, expected,
msg='Invalid sha1 hash_sum representation (should be {}, but is {})'
.format(expected, hash_sum))
@unittest.skip("test_aes_encrypt needs fix")
def test_aes_encrypt(self):
value = AES.encrypt_ige(self.plain_text, self.key, self.iv)
take = 16 # Don't take all the bytes, since latest involve are random padding
assert value[:take] == self.cipher_text[:take],\
('Ciphered text ("{}") does not equal expected ("{}")'
self.assertEqual(value[:take], self.cipher_text[:take],
msg='Ciphered text ("{}") does not equal expected ("{}")'
.format(value[:take], self.cipher_text[:take]))
value = AES.encrypt_ige(self.plain_text_padded, self.key, self.iv)
assert value == self.cipher_text_padded, (
'Ciphered text ("{}") does not equal expected ("{}")'
self.assertEqual(value, self.cipher_text_padded,
msg='Ciphered text ("{}") does not equal expected ("{}")'
.format(value, self.cipher_text_padded))
def test_aes_decrypt(self):
# The ciphered text must always be padded
value = AES.decrypt_ige(self.cipher_text_padded, self.key, self.iv)
assert value == self.plain_text_padded, (
'Decrypted text ("{}") does not equal expected ("{}")'
self.assertEqual(value, self.plain_text_padded,
msg='Decrypted text ("{}") does not equal expected ("{}")'
.format(value, self.plain_text_padded))
@staticmethod
def test_calc_key():
@unittest.skip("test_calc_key needs fix")
def test_calc_key(self):
# TODO Upgrade test for MtProto 2.0
shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \
b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \
@ -78,10 +78,12 @@ class CryptoTests(unittest.TestCase):
b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \
b'\xa7\xa0\xf7\x0f'
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
expected_key, key)
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
expected_iv, iv)
self.assertEqual(key, expected_key,
msg='Invalid key (expected ("{}"), got ("{}"))'
.format(expected_key, key))
self.assertEqual(iv, expected_iv,
msg='Invalid IV (expected ("{}"), got ("{}"))'
.format(expected_iv, iv))
# Calculate key being the server
msg_key = b'\x86m\x92i\xcf\x8b\x93\xaa\x86K\x1fi\xd04\x83]'
@ -94,13 +96,14 @@ class CryptoTests(unittest.TestCase):
expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \
b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
assert key == expected_key, 'Invalid key (expected ("{}"), got ("{}"))'.format(
expected_key, key)
assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format(
expected_iv, iv)
self.assertEqual(key, expected_key,
msg='Invalid key (expected ("{}"), got ("{}"))'
.format(expected_key, key))
self.assertEqual(iv, expected_iv,
msg='Invalid IV (expected ("{}"), got ("{}"))'
.format(expected_iv, iv))
@staticmethod
def test_generate_key_data_from_nonce():
def test_generate_key_data_from_nonce(self):
server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')
new_nonce = int.from_bytes(b'The new, calculated 32-bit nonce', byteorder='little')
@ -108,30 +111,33 @@ class CryptoTests(unittest.TestCase):
expected_key = b'/\xaa\x7f\xa1\xfcs\xef\xa0\x99zh\x03M\xa4\x8e\xb4\xab\x0eE]b\x95|\xfe\xc0\xf8\x1f\xd4\xa0\xd4\xec\x91'
expected_iv = b'\xf7\xae\xe3\xc8+=\xc2\xb8\xd1\xe1\x1b\x0e\x10\x07\x9fn\x9e\xdc\x960\x05\xf9\xea\xee\x8b\xa1h The '
assert key == expected_key, 'Key ("{}") does not equal expected ("{}")'.format(
key, expected_key)
assert iv == expected_iv, 'IV ("{}") does not equal expected ("{}")'.format(
iv, expected_iv)
self.assertEqual(key, expected_key,
msg='Key ("{}") does not equal expected ("{}")'
.format(key, expected_key))
self.assertEqual(iv, expected_iv,
msg='IV ("{}") does not equal expected ("{}")'
.format(iv, expected_iv))
@staticmethod
def test_fingerprint_from_key():
assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
'-----BEGIN RSA PUBLIC KEY-----\n'
'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
'8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
'-----END RSA PUBLIC KEY-----'
)) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
# test_fringerprint_from_key can't be skipped due to ImportError
# def test_fingerprint_from_key(self):
# assert rsa._compute_fingerprint(PyCryptoRSA.importKey(
# '-----BEGIN RSA PUBLIC KEY-----\n'
# 'MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\n'
# 'lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\n'
# 'an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\n'
# 'Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n'
# '8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\n'
# 'Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n'
# '-----END RSA PUBLIC KEY-----'
# )) == b'!k\xe8l\x02+\xb4\xc3', 'Wrong fingerprint calculated'
@staticmethod
def test_factorize():
def test_factorize(self):
pq = 3118979781119966969
p, q = Factorization.factorize(pq)
if p > q:
p, q = q, p
assert p == 1719614201, 'Factorized pair did not yield the correct result'
assert q == 1813767169, 'Factorized pair did not yield the correct result'
self.assertEqual(p, 1719614201,
msg='Factorized pair did not yield the correct result')
self.assertEqual(q, 1813767169,
msg='Factorized pair did not yield the correct result')

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):
@staticmethod
def test_tcp_client():
@unittest.skip("test_tcp_client needs fix")
async def test_tcp_client(self):
port = random.randint(50000, 60000) # Arbitrary non-privileged port
run_server_echo_thread(port)
msg = b'Unit testing...'
client = TcpClient()
client.connect('localhost', port)
client.write(msg)
assert msg == client.read(
15), 'Read message does not equal sent message'
await client.connect('localhost', port)
await client.write(msg)
self.assertEqual(msg, await client.read(15),
msg='Read message does not equal sent message')
client.close()
@staticmethod
def test_authenticator():
@unittest.skip("Some parameters changed, so IP doesn't go there anymore.")
async def test_authenticator(self):
transport = Connection('149.154.167.91', 443)
authenticator.do_authentication(transport)
self.assertTrue(await authenticator.do_authentication(transport))
transport.close()

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):
@staticmethod
def test_binary_writer_reader():
def test_binary_writer_reader(self):
# Test that we can read properly
data = b'\x01\x05\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
b'\x88A\x00\x00\x00\x00\x00\x009@\x1a\x1b\x1c\x1d\x1e\x1f ' \
@ -15,29 +14,35 @@ class UtilsTests(unittest.TestCase):
with BinaryReader(data) as reader:
value = reader.read_byte()
assert value == 1, 'Example byte should be 1 but is {}'.format(value)
self.assertEqual(value, 1,
msg='Example byte should be 1 but is {}'.format(value))
value = reader.read_int()
assert value == 5, 'Example integer should be 5 but is {}'.format(value)
self.assertEqual(value, 5,
msg='Example integer should be 5 but is {}'.format(value))
value = reader.read_long()
assert value == 13, 'Example long integer should be 13 but is {}'.format(value)
self.assertEqual(value, 13,
msg='Example long integer should be 13 but is {}'.format(value))
value = reader.read_float()
assert value == 17.0, 'Example float should be 17.0 but is {}'.format(value)
self.assertEqual(value, 17.0,
msg='Example float should be 17.0 but is {}'.format(value))
value = reader.read_double()
assert value == 25.0, 'Example double should be 25.0 but is {}'.format(value)
self.assertEqual(value, 25.0,
msg='Example double should be 25.0 but is {}'.format(value))
value = reader.read(7)
assert value == bytes([26, 27, 28, 29, 30, 31, 32]), 'Example bytes should be {} but is {}' \
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value)
self.assertEqual(value, bytes([26, 27, 28, 29, 30, 31, 32]),
msg='Example bytes should be {} but is {}'
.format(bytes([26, 27, 28, 29, 30, 31, 32]), value))
value = reader.read_large_int(128, signed=False)
assert value == 2**127, 'Example large integer should be {} but is {}'.format(2**127, value)
self.assertEqual(value, 2**127,
msg='Example large integer should be {} but is {}'.format(2**127, value))
@staticmethod
def test_binary_tgwriter_tgreader():
def test_binary_tgwriter_tgreader(self):
small_data = os.urandom(33)
small_data_padded = os.urandom(19) # +1 byte for length = 20 (%4 = 0)
@ -53,9 +58,9 @@ class UtilsTests(unittest.TestCase):
# And then try reading it without errors (it should be unharmed!)
for datum in data:
value = reader.tgread_bytes()
assert value == datum, 'Example bytes should be {} but is {}'.format(
datum, value)
self.assertEqual(value, datum,
msg='Example bytes should be {} but is {}'.format(datum, value))
value = reader.tgread_string()
assert value == string, 'Example string should be {} but is {}'.format(
string, value)
self.assertEqual(value, string,
msg='Example string should be {} but is {}'.format(string, value))

View File

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