From 986ddbe600137a19e75324ad99e56e10123f4ecc Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 18 Mar 2018 18:23:10 +0200 Subject: [PATCH 01/17] Fix forwarding messages to channels (#705) --- telethon/telegram_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e4647a8e..84e52479 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -846,7 +846,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] From 35eccc0ba379b427ba0d390216c6739d7dca046f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Mar 2018 17:18:45 +0100 Subject: [PATCH 02/17] Remove unwanted binary file from #389 --- telethon_examples/anytime.png | Bin 2291 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 telethon_examples/anytime.png diff --git a/telethon_examples/anytime.png b/telethon_examples/anytime.png deleted file mode 100644 index c8663cfa21f253019a139d9c41f279b3134f0ecb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2291 zcmbu8c{J4R9>9OIG4>{dFtW_3i7^?*R(1`dE;9_Fx@BvSZ4hR-F+~ioQYgjP#+Id~ z*D@&*VJtV*-|?;m&hKIePRbI$Yme$V%Fp6A(qz5Nc@+2}B}Mb-#(3v7RYjpQyLf1SBRVCMhAgM@njso;pfh@4t@iRsaD90)UGU zkOlxofFKCa_A@{g06;L|v^xX;GeBSn6ec1H7ZaBd0`zN9rYJ7QxZhyr*)MfiD<+Z#qj%TwY<1qLSvm{n!IqL<2)3W0ONR zM@Y7I_D3CDT&ZsECp>t5MO~e{XVAJFPMVui12~S6@EjyDJr0_#P&|uEovbjtTAR)Ud^D;P>(^ zkKY(<=*F+B%jm%#vm+$m_F(6idMPJM%bWxV&tnseue8y+k;+q#bBr>JY~zd2Y9eTK zTPTDPSsYWygk8zin^panHx2`LQ?7TV*zCrOWaW8%{oC@5NJT z)~r*SQ+}MoNp(i*99rx7y)26-tV4zu#RWek+XSbkRIzzJ{l_=MH}oH^RY}y&5^T{P zH)v_yQ%KE{PQ&ZA_FXC=Z17T##&=R>|k}?EVb2~-_KypQ6E1}>Ci9h44dDLIlj=3bCv44 z(cSOj$Yi1#4Bf)CQ))(X(-I`?U%!c2XiMB$WPZ5JxfC2oE@o?`?0hj|0V&r%`Lz?| zv_sDmf6Dk!nBYx#9dosd5yy}m>J=ZK@cc8X<&3PhuBG77EmVctqzbR!lNm%afEySu zIzF10%C``F04-YNOBsHCM_-KqcKsujy&xQ|5 z=j1^;Nb0uV1Y zmMABK_JuaE@V*MVIQ80)xL1}K_qoK(mB~FSfu-5gqcFkB#aXbST8B6jf`ZU^~G;K=ft^ru#zEKRQ&n@n+s zO>OWcw1J+DVmqq4YS2E}^oUQB<7VNU;k3J@e<`mZUYeh&)T=uB$lOw|Bxh#TC$t_g zKPZ*M!Nl7RO+E6mgJWPUxIfWX3L#(AX1cJ+si^X<9bmA|%0znMn))KCk->_?`_j!f$w zWmxhHLS|+cvY}rq#GE-co@^2QYDi@pC^Jd>8 xfxJ89PP4%sY^`P_EY0doaJx|t{qfp&*Q!{0T78H=^?6vWoq;tQFK@9u`Zo{N?wSAq From 987cf41ec636f97c36dc1c43946e1626ac46894f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Mar 2018 20:01:19 +0100 Subject: [PATCH 03/17] Higher timeout and log them as warning if any data was received This might be the cause for "number of retries reached 0" so more specific logging calls might be useful. If while reading a response it times out but some data had already been read, said data will be lost. The sequence of events that triggered reaching 0 retries was: - Sending requests with IDs XYZ - socket.timeout while reading - Items timed out. Retrying - Processing RPC result - Received response for XYZ - Lost request with ID XYZ --- telethon/extensions/tcp_client.py | 12 ++++++++++-- telethon/telegram_client.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index d335e57a..1b7d0113 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -153,7 +153,6 @@ class TcpClient: if self._socket is None: self._raise_connection_reset(None) - # TODO Remove the timeout from this method, always use previous one with BufferedWriter(BytesIO(), buffer_size=size) as buffer: bytes_left = size while bytes_left != 0: @@ -162,7 +161,16 @@ class TcpClient: except socket.timeout 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) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84e52479..ece8045c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -170,7 +170,7 @@ class TelegramClient(TelegramBareClient): use_ipv6=False, proxy=None, update_workers=None, - timeout=timedelta(seconds=5), + timeout=timedelta(seconds=10), spawn_read_thread=True, report_errors=True, **kwargs): From 89ae0cb164e12571435fb33e8113712ab5c65b8d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 21 Mar 2018 08:55:13 +0100 Subject: [PATCH 04/17] Make readthedocs build run without warnings --- readthedocs/extra/basic/entities.rst | 2 ++ readthedocs/extra/changelog.rst | 24 ++++++++++----------- telethon/events/__init__.py | 32 ++++++++++++++-------------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index ce7e569a..598a4230 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -1,3 +1,5 @@ +.. _entities: + ========================= Users, Chats and Channels ========================= diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 31d58d6b..a22ad725 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -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 diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 59d15821..d8d90bea 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1095,22 +1095,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 From 3550974b71087c3e66e8166b4ed623d2c160379c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 21 Mar 2018 09:17:56 +0100 Subject: [PATCH 05/17] Fix documentation for events Changing the .__name__ of a class will make it not show in the generated documentation, so instead we need to use a different variable. --- telethon/events/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d8d90bea..898f29a4 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -70,6 +70,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 = {} @@ -179,7 +180,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 @@ -197,7 +198,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 @@ -1061,6 +1062,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): From f2407409b37c1d11aee7bbc600e445a80183bcc7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 21 Mar 2018 09:46:57 +0100 Subject: [PATCH 06/17] Fix send_file(force_document=True) for albums (closes #713) --- telethon/telegram_client.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ece8045c..b8133d44 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1399,12 +1399,15 @@ class TelegramClient(TelegramBareClient): if utils.is_list_like(file): # TODO Fix progress_callback images = [] - documents = [] - for x in file: - if utils.is_image(x): - images.append(x) - else: - documents.append(x) + if force_document: + documents = file + else: + documents = [] + for x in file: + if utils.is_image(x): + images.append(x) + else: + documents.append(x) result = [] while images: @@ -1417,7 +1420,7 @@ class TelegramClient(TelegramBareClient): result.extend( 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 From 95f368201e53f440ba5226f2df78fd2c87cb2b7c Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 21 Mar 2018 17:01:14 +0800 Subject: [PATCH 07/17] Fix ChatAction not handling all pin events (#715) --- telethon/events/__init__.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 898f29a4..a91665fb 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -609,11 +609,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 +665,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,7 +684,7 @@ 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). @@ -704,10 +710,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 +735,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 +754,7 @@ class ChatAction(_EventBuilder): self._users = None self._input_users = None self.new_title = new_title + self.unpin = unpin def respond(self, *args, **kwargs): """ From dce0fd9e03e544e13a64175308c2f0de1aceb966 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 18:39:42 +0100 Subject: [PATCH 08/17] Add missing documentation for telethon.tl.custom and crosslinks --- .../extra/examples/chats-and-channels.rst | 21 ++++++++- readthedocs/telethon.tl.custom.rst | 9 ++++ telethon/telegram_client.py | 29 ++++++------ telethon/tl/custom/dialog.py | 45 ++++++++++++++++++- telethon/tl/custom/draft.py | 43 ++++++++++++++++-- 5 files changed, 126 insertions(+), 21 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 95fa1b1e..f38519c6 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -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 diff --git a/readthedocs/telethon.tl.custom.rst b/readthedocs/telethon.tl.custom.rst index a1290869..7f59596c 100644 --- a/readthedocs/telethon.tl.custom.rst +++ b/readthedocs/telethon.tl.custom.rst @@ -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: diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index b8133d44..361366d3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -102,7 +102,8 @@ 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, @@ -394,7 +395,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(): self._check_events_pending_resolve() @@ -550,7 +551,7 @@ class TelegramClient(TelegramBareClient): A _Box instance 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: @@ -606,7 +607,7 @@ class TelegramClient(TelegramBareClient): 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 @@ -618,9 +619,10 @@ class TelegramClient(TelegramBareClient): """ 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 self(GetAllDraftsRequest()).updates: yield Draft._from_update(self, update) @@ -674,7 +676,7 @@ class TelegramClient(TelegramBareClient): 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, [] @@ -741,7 +743,7 @@ class TelegramClient(TelegramBareClient): Has no effect when sending a file. Returns: - the sent message + the sent message. """ if file is not None: return self.send_file( @@ -1001,7 +1003,6 @@ class TelegramClient(TelegramBareClient): 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 you think may be good. - """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -1079,7 +1080,7 @@ class TelegramClient(TelegramBareClient): 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 @@ -1308,7 +1309,7 @@ class TelegramClient(TelegramBareClient): 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 @@ -1918,7 +1919,7 @@ class TelegramClient(TelegramBareClient): return file 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 +1966,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 diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index 366a19bf..a2b1a966 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -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 (:obj:`Dialog`): + The original ``Dialog`` instance. + + pinned (:obj:`bool`): + Whether this dialog is pinned to the top or not. + + message (:obj:`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 (:obj:`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: 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 self._client.send_message(self.input_entity, *args, **kwargs) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index 8f3aac60..fc40c1cf 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -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 def entity(self): + """ + The entity that belongs to this dialog (user, chat or channel). + """ return self._client.get_entity(self._peer) @property def input_entity(self): + """ + Input version of the entity. + """ return 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 + def set_message(self, text=None, reply_to=0, parse_mode='md', link_preview=None): """ @@ -88,10 +121,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 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). + """ self._client.send_message(self._peer, self.text, reply_to=self.reply_to_msg_id, link_preview=self.link_preview, @@ -100,7 +138,6 @@ class Draft: def delete(self): """ - Deletes this draft - :return bool: ``True`` on success + Deletes this draft, and returns ``True`` on success. """ return self.set_message(text='') From c71d2e18cb06c6967493f5f364ffb9d6e40502c5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 18:54:08 +0100 Subject: [PATCH 09/17] Don't perform exact search on the docs --- docs/res/js/search.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/res/js/search.js b/docs/res/js/search.js index 1b33980b..3e24bf0c 100644 --- a/docs/res/js/search.js +++ b/docs/res/js/search.js @@ -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]); } From 33e908de422508d28be66cd8bb16a698ce9b54aa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:01:50 +0100 Subject: [PATCH 10/17] Fix markdown regex not supporting [] inside URLs --- telethon/extensions/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index a5dde5c6..680aabda 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -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})' From 09c04282c9c67a98cc1aa9ecfdd3a921d202ff30 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:02:40 +0100 Subject: [PATCH 11/17] Fix typing dependency must be installed below Python 3.5.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0d6d757c..7178c131 100755 --- a/setup.py +++ b/setup.py @@ -154,7 +154,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'] } From 021cb21686e0b9171cc99df4a4d4ae93d563c139 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:13:42 +0100 Subject: [PATCH 12/17] Replace custom Box class with a single-item list for args by ref --- telethon/telegram_client.py | 70 +++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 38 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 361366d3..a4ae4ebc 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -91,12 +91,6 @@ 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. @@ -525,7 +519,7 @@ class TelegramClient(TelegramBareClient): # region Dialogs ("chats") requests 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. @@ -547,15 +541,15 @@ class TelegramClient(TelegramBareClient): offset_peer (:obj:`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 :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 = self(GetDialogsRequest( @@ -564,7 +558,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() @@ -578,8 +572,8 @@ class TelegramClient(TelegramBareClient): req.limit = min(limit - len(seen), 100) r = 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)} @@ -609,10 +603,10 @@ class TelegramClient(TelegramBareClient): Same as :meth:`iter_dialogs`, but returns a list instead with an additional ``.total`` attribute on the list. """ - total_box = _Box(0) - kwargs['_total_box'] = total_box + total = [0] + kwargs['_total'] = total dialogs = UserList(self.iter_dialogs(*args, **kwargs)) - dialogs.total = total_box.x + dialogs.total = total[0] return dialogs def iter_drafts(self): # TODO: Ability to provide a `filter` @@ -940,7 +934,7 @@ class TelegramClient(TelegramBareClient): 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. @@ -986,8 +980,8 @@ class TelegramClient(TelegramBareClient): 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: @@ -1007,7 +1001,7 @@ class TelegramClient(TelegramBareClient): entity = 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 = self(GetHistoryRequest( @@ -1015,7 +1009,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 +1030,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)} @@ -1082,10 +1076,10 @@ class TelegramClient(TelegramBareClient): Same as :meth:`iter_messages`, but returns a list instead with an additional ``.total`` attribute on the list. """ - total_box = _Box(0) - kwargs['_total_box'] = total_box + total = [0] + kwargs['_total'] = total msgs = UserList(self.iter_messages(*args, **kwargs)) - msgs.total = total_box.x + msgs.total = total[0] return msgs def get_message_history(self, *args, **kwargs): @@ -1161,7 +1155,7 @@ class TelegramClient(TelegramBareClient): raise TypeError('Invalid message type: {}'.format(type(message))) 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. @@ -1191,8 +1185,8 @@ 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`` @@ -1220,8 +1214,8 @@ class TelegramClient(TelegramBareClient): total = self(GetFullChannelRequest( entity )).full_chat.participants_count - if _total_box: - _total_box.x = total + if _total: + _total[0] = total if limit == 0: return @@ -1281,8 +1275,8 @@ class TelegramClient(TelegramBareClient): elif isinstance(entity, InputPeerChat): # TODO We *could* apply the `filter` here ourselves full = 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} @@ -1298,8 +1292,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 = self.get_entity(entity) if filter_entity(user): @@ -1311,10 +1305,10 @@ class TelegramClient(TelegramBareClient): Same as :meth:`iter_participants`, but returns a list instead with an additional ``.total`` attribute on the list. """ - total_box = _Box(0) - kwargs['_total_box'] = total_box + total = [0] + kwargs['_total'] = total participants = UserList(self.iter_participants(*args, **kwargs)) - participants.total = total_box.x + participants.total = total[0] return participants # endregion From c6d821910ec35663a752ff3e6bcf59878bdaaf75 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Mar 2018 19:20:35 +0100 Subject: [PATCH 13/17] Mention that codes can expire immediately --- readthedocs/extra/basic/creating-a-client.rst | 8 ++++++++ telethon/telegram_client.py | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index e68f170b..384ebd47 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -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 diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a4ae4ebc..6a3822d5 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -373,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 From 43c6896481fa8845d06adb174cd65c8f3b0e1bba Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Mar 2018 21:40:24 +0100 Subject: [PATCH 14/17] Add a custom role for TL references and make use of it --- readthedocs/conf.py | 14 +-- readthedocs/custom_roles.py | 69 +++++++++++++++ .../advanced-usage/accessing-the-full-api.rst | 13 ++- readthedocs/extra/basic/entities.rst | 24 +++--- readthedocs/extra/changelog.rst | 2 +- telethon/events/__init__.py | 54 ++++++------ telethon/network/mtproto_sender.py | 14 +-- telethon/telegram_client.py | 86 +++++++++---------- telethon/tl/custom/dialog.py | 6 +- telethon/tl/custom/draft.py | 2 +- telethon/update_state.py | 5 +- telethon/utils.py | 57 ++++++------ 12 files changed, 212 insertions(+), 134 deletions(-) create mode 100644 readthedocs/custom_roles.py diff --git a/readthedocs/conf.py b/readthedocs/conf.py index efb14992..35dadb24 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -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'] diff --git a/readthedocs/custom_roles.py b/readthedocs/custom_roles.py new file mode 100644 index 00000000..89a5bd79 --- /dev/null +++ b/readthedocs/custom_roles.py @@ -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 diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index edbe821d..b8d63eb6 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -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 diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index 598a4230..c7e55524 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -9,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 **************** @@ -73,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. @@ -90,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". @@ -106,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 @@ -125,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. diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index a22ad725..612547af 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -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. diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a91665fb..a3c4774e 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -91,13 +91,13 @@ class _EventCommon(abc.ABC): 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): @@ -124,7 +124,7 @@ class _EventCommon(abc.ABC): @property 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. @@ -156,7 +156,7 @@ class _EventCommon(abc.ABC): @property 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. @@ -312,8 +312,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 +406,7 @@ class NewMessage(_EventBuilder): @property 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 +434,7 @@ class NewMessage(_EventBuilder): @property 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 +474,8 @@ class NewMessage(_EventBuilder): @property 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 +498,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 +513,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 +524,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 +547,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 +556,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 +565,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 +573,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 +582,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 +590,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) @@ -689,7 +689,7 @@ class ChatAction(_EventBuilder): 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). @@ -793,7 +793,7 @@ class ChatAction(_EventBuilder): @property 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: @@ -857,7 +857,7 @@ class ChatAction(_EventBuilder): @property def input_user(self): """ - Input version of the self.user property. + Input version of the ``self.user`` property. """ if self.input_users: return self._input_users[0] @@ -894,7 +894,7 @@ class ChatAction(_EventBuilder): @property 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 = [] @@ -947,7 +947,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`): diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 532a8da7..8206fcaa 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -24,13 +24,15 @@ __log__ = logging.getLogger(__name__) 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 - from two or more threads at the same time is undefined behaviour. - Rationale: a new connection should be spawned to send/receive requests - in parallel, so thread-safety (hence locking) isn't needed. + 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 + in parallel, so thread-safety (hence locking) isn't needed. """ def __init__(self, session, connection): diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6a3822d5..046838bd 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -213,7 +213,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) @@ -257,8 +257,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 `_ + 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. @@ -276,8 +277,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: @@ -453,7 +454,7 @@ class TelegramClient(TelegramBareClient): Optional last name. Returns: - The new created user. + The new created :tl:`User`. """ if self.is_user_authorized(): self._check_events_pending_resolve() @@ -478,7 +479,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: self(LogOutRequest()) @@ -496,12 +497,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 @@ -541,7 +542,7 @@ 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 (:obj:`list`, optional): @@ -712,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. @@ -740,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 self.send_file( @@ -806,7 +807,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`): @@ -815,7 +816,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,) @@ -882,7 +883,7 @@ class TelegramClient(TelegramBareClient): not modified at all. Returns: - the edited message + The edited :tl:`Message`. """ message, msg_entities = self._parse_message_text(message, parse_mode) request = EditMessageRequest( @@ -905,7 +906,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): @@ -915,7 +916,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,) @@ -978,7 +979,7 @@ 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. @@ -987,7 +988,7 @@ class TelegramClient(TelegramBareClient): 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. @@ -995,7 +996,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 @@ -1101,7 +1102,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`): @@ -1172,9 +1173,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. @@ -1192,10 +1192,10 @@ class TelegramClient(TelegramBareClient): 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() @@ -1362,12 +1362,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). @@ -1390,7 +1390,7 @@ 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. @@ -1551,7 +1551,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) @@ -1652,8 +1652,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 @@ -1836,7 +1836,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): @@ -1845,7 +1845,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 @@ -2065,7 +2065,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`): @@ -2293,7 +2293,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. @@ -2309,7 +2309,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): @@ -2410,9 +2410,9 @@ class TelegramClient(TelegramBareClient): 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 @@ -2423,7 +2423,7 @@ class TelegramClient(TelegramBareClient): 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 diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index a2b1a966..86265140 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -10,13 +10,13 @@ class Dialog: return instances of this class when calling :meth:`.get_dialogs()`. Args: - dialog (:obj:`Dialog`): + dialog (:tl:`Dialog`): The original ``Dialog`` instance. pinned (:obj:`bool`): Whether this dialog is pinned to the top or not. - message (:obj:`Message`): + 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. @@ -27,7 +27,7 @@ class Dialog: entity (:obj:`entity`): The entity that belongs to this dialog (user, chat or channel). - input_entity (:obj:`InputPeer`): + input_entity (:tl:`InputPeer`): Input version of the entity. id (:obj:`int`): diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index fc40c1cf..f52ac6c9 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -128,7 +128,7 @@ class Draft: 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). + wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``. """ self._client.send_message(self._peer, self.text, reply_to=self.reply_to_msg_id, diff --git a/telethon/update_state.py b/telethon/update_state.py index 9f26e3a4..509697a0 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -11,8 +11,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 diff --git a/telethon/utils.py b/telethon/utils.py index 286853ad..faa1537a 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -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 From 898e550335b9979a131dd956c8aa26a01d341b4c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Mar 2018 12:09:33 +0100 Subject: [PATCH 15/17] Except the right type for get_input_peer (closes #722) --- telethon/sessions/memory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index e5223cac..5faf721d 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -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)): From 69d283a29648cbea78640c66a6b537774f0a0a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20R=C3=A4sener?= Date: Sat, 24 Mar 2018 12:12:47 +0100 Subject: [PATCH 16/17] Tests cleanup (#717) --- telethon_tests/parser_test.py | 5 - .../{crypto_test.py => test_crypto.py} | 100 ++++++++++-------- ...her_level_test.py => test_higher_level.py} | 17 +-- .../{network_test.py => test_network.py} | 15 +-- telethon_tests/test_parser.py | 8 ++ telethon_tests/test_tl.py | 8 ++ .../{utils_test.py => test_utils.py} | 37 ++++--- telethon_tests/tl_test.py | 5 - 8 files changed, 107 insertions(+), 88 deletions(-) delete mode 100644 telethon_tests/parser_test.py rename telethon_tests/{crypto_test.py => test_crypto.py} (57%) rename telethon_tests/{higher_level_test.py => test_higher_level.py} (71%) rename telethon_tests/{network_test.py => test_network.py} (72%) create mode 100644 telethon_tests/test_parser.py create mode 100644 telethon_tests/test_tl.py rename telethon_tests/{utils_test.py => test_utils.py} (52%) delete mode 100644 telethon_tests/tl_test.py diff --git a/telethon_tests/parser_test.py b/telethon_tests/parser_test.py deleted file mode 100644 index fc366b45..00000000 --- a/telethon_tests/parser_test.py +++ /dev/null @@ -1,5 +0,0 @@ -import unittest - - -class ParserTests(unittest.TestCase): - """There are no tests yet""" diff --git a/telethon_tests/crypto_test.py b/telethon_tests/test_crypto.py similarity index 57% rename from telethon_tests/crypto_test.py rename to telethon_tests/test_crypto.py index 17453f62..136e6091 100644 --- a/telethon_tests/crypto_test.py +++ b/telethon_tests/test_crypto.py @@ -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 ("{}")' - .format(value[:take], self.cipher_text[:take])) + 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 ("{}")' - .format(value, self.cipher_text_padded)) + 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 ("{}")' - .format(value, self.plain_text_padded)) + 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') diff --git a/telethon_tests/higher_level_test.py b/telethon_tests/test_higher_level.py similarity index 71% rename from telethon_tests/higher_level_test.py rename to telethon_tests/test_higher_level.py index 7433fac9..8e933056 100644 --- a/telethon_tests/higher_level_test.py +++ b/telethon_tests/test_higher_level.py @@ -10,16 +10,17 @@ from telethon import TelegramClient 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(): + 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 :)") + def test_cdn_download(self): client = TelegramClient(None, api_id, api_hash) client.session.set_dc(0, '149.154.167.40', 80) - assert client.connect() + self.assertTrue(client.connect()) try: phone = '+999662' + str(randint(0, 9999)).zfill(4) @@ -37,11 +38,11 @@ class HigherLevelTests(unittest.TestCase): out = BytesIO() client.download_media(msg, out) - assert sha256(data).digest() == sha256(out.getvalue()).digest() + self.assertEqual(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() + self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest()) client.log_out() finally: diff --git a/telethon_tests/network_test.py b/telethon_tests/test_network.py similarity index 72% rename from telethon_tests/network_test.py rename to telethon_tests/test_network.py index 559eab45..031ad99d 100644 --- a/telethon_tests/network_test.py +++ b/telethon_tests/test_network.py @@ -23,8 +23,9 @@ def run_server_echo_thread(port): class NetworkTests(unittest.TestCase): - @staticmethod - def test_tcp_client(): + + @unittest.skip("test_tcp_client needs fix") + def test_tcp_client(self): port = random.randint(50000, 60000) # Arbitrary non-privileged port run_server_echo_thread(port) @@ -32,12 +33,12 @@ class NetworkTests(unittest.TestCase): client = TcpClient() client.connect('localhost', port) client.write(msg) - assert msg == client.read( - 15), 'Read message does not equal sent message' + self.assertEqual(msg, 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.") + def test_authenticator(self): transport = Connection('149.154.167.91', 443) - authenticator.do_authentication(transport) + self.assertTrue(authenticator.do_authentication(transport)) transport.close() diff --git a/telethon_tests/test_parser.py b/telethon_tests/test_parser.py new file mode 100644 index 00000000..c87686a6 --- /dev/null +++ b/telethon_tests/test_parser.py @@ -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) diff --git a/telethon_tests/test_tl.py b/telethon_tests/test_tl.py new file mode 100644 index 00000000..189259f5 --- /dev/null +++ b/telethon_tests/test_tl.py @@ -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) \ No newline at end of file diff --git a/telethon_tests/utils_test.py b/telethon_tests/test_utils.py similarity index 52% rename from telethon_tests/utils_test.py rename to telethon_tests/test_utils.py index 790f3f4d..4a550e3d 100644 --- a/telethon_tests/utils_test.py +++ b/telethon_tests/test_utils.py @@ -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)) diff --git a/telethon_tests/tl_test.py b/telethon_tests/tl_test.py deleted file mode 100644 index 37f0bbe5..00000000 --- a/telethon_tests/tl_test.py +++ /dev/null @@ -1,5 +0,0 @@ -import unittest - - -class TLTests(unittest.TestCase): - """There are no tests yet""" From 13e59983af49e2ec7dca23ee333b3d81b1b4fb00 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Mar 2018 12:41:42 +0100 Subject: [PATCH 17/17] Slightly change docs for events (#668) --- readthedocs/extra/basic/getting-started.rst | 20 ++++++++++++++++++++ telethon/events/__init__.py | 6 ++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 87c142e9..e40bae44 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -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 diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index a3c4774e..8b80bfd8 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -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