From ec4ca5dbfc6e6e7e3d7275eb8c19745daf1d7b63 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 18:31:48 +0100 Subject: [PATCH 001/108] More consistent with asyncio branch (style/small fixes) Like passing an extra (invalid) dt parameter when serializing a datetime, and handling more errors in the TcpClient class. --- telethon/extensions/markdown.py | 4 ++-- telethon/extensions/tcp_client.py | 20 +++++++++++++++----- telethon/telegram_client.py | 21 ++++----------------- telethon/tl/tlobject.py | 2 +- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 24ae5aa7..6285bf28 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -192,10 +192,10 @@ def get_inner_text(text, entity): :param entity: the entity or entities that must be matched. :return: a single result or a list of the text surrounded by the entities. """ - if not isinstance(entity, TLObject) and hasattr(entity, '__iter__'): + if isinstance(entity, TLObject): + entity = (entity,) multiple = True else: - entity = [entity] multiple = False text = text.encode(ENC) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 61be30f5..e67c032c 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -3,10 +3,17 @@ This module holds a rough implementation of the C# TCP client. """ import errno import socket +import time from datetime import timedelta from io import BytesIO, BufferedWriter from threading import Lock +MAX_TIMEOUT = 15 # in seconds +CONN_RESET_ERRNOS = { + errno.EBADF, errno.ENOTSOCK, errno.ENETUNREACH, + errno.EINVAL, errno.ENOTCONN +} + class TcpClient: """A simple TCP client to ease the work with sockets and proxies.""" @@ -59,6 +66,7 @@ class TcpClient: else: mode, address = socket.AF_INET, (ip, port) + timeout = 1 while True: try: while not self._socket: @@ -69,10 +77,12 @@ class TcpClient: except OSError as e: # There are some errors that we know how to handle, and # the loop will allow us to retry - if e.errno == errno.EBADF: + if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL): # Bad file descriptor, i.e. socket was closed, set it # to none to recreate it on the next iteration self._socket = None + time.sleep(timeout) + timeout = min(timeout * 2, MAX_TIMEOUT) else: raise @@ -105,7 +115,7 @@ class TcpClient: :param data: the data to send. """ if self._socket is None: - raise ConnectionResetError() + self._raise_connection_reset() # TODO Timeout may be an issue when sending the data, Changed in v3.5: # The socket timeout is now the maximum total duration to send all data. @@ -116,7 +126,7 @@ class TcpClient: except ConnectionError: self._raise_connection_reset() except OSError as e: - if e.errno == errno.EBADF: + if e.errno in CONN_RESET_ERRNOS: self._raise_connection_reset() else: raise @@ -129,7 +139,7 @@ class TcpClient: :return: the read data with len(data) == size. """ if self._socket is None: - raise ConnectionResetError() + self._raise_connection_reset() # TODO Remove the timeout from this method, always use previous one with BufferedWriter(BytesIO(), buffer_size=size) as buffer: @@ -142,7 +152,7 @@ class TcpClient: except ConnectionError: self._raise_connection_reset() except OSError as e: - if e.errno == errno.EBADF or e.errno == errno.ENOTSOCK: + if e.errno in CONN_RESET_ERRNOS: self._raise_connection_reset() else: raise diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 7b8a84fa..2f9eaecf 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -317,10 +317,7 @@ class TelegramClient(TelegramBareClient): # region Dialogs ("chats") requests - def get_dialogs(self, - limit=10, - offset_date=None, - offset_id=0, + def get_dialogs(self, limit=10, offset_date=None, offset_id=0, offset_peer=InputPeerEmpty()): """ Gets N "dialogs" (open "chats" or conversations with other people). @@ -425,11 +422,7 @@ class TelegramClient(TelegramBareClient): if update.message.id == msg_id: return update.message - def send_message(self, - entity, - message, - reply_to=None, - parse_mode=None, + def send_message(self, entity, message, reply_to=None, parse_mode=None, link_preview=True): """ Sends the given message to the specified entity (user/chat/channel). @@ -523,14 +516,8 @@ class TelegramClient(TelegramBareClient): else: return self(messages.DeleteMessagesRequest(message_ids, revoke=revoke)) - def get_message_history(self, - entity, - limit=20, - offset_date=None, - offset_id=0, - max_id=0, - min_id=0, - add_offset=0): + def get_message_history(self, entity, limit=20, offset_date=None, + offset_id=0, max_id=0, min_id=0, add_offset=0): """ Gets the message history for the specified entity diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 0ed7b015..ad930f9c 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -134,7 +134,7 @@ class TLObject: if isinstance(dt, datetime): dt = int(dt.timestamp()) elif isinstance(dt, date): - dt = int(datetime(dt.year, dt.month, dt.day, dt).timestamp()) + dt = int(datetime(dt.year, dt.month, dt.day).timestamp()) elif isinstance(dt, float): dt = int(dt) From 4871a6fb96dc740900cdfee67617758fb9b2f633 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 19:51:44 +0100 Subject: [PATCH 002/108] Accept 'me' and 'self' usernames to get self user entity --- telethon/telegram_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 2f9eaecf..6ec8fd02 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -52,7 +52,7 @@ from .tl.types import ( InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - ChatInvite, ChatInviteAlready, PeerChannel, Photo + ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -1202,6 +1202,8 @@ class TelegramClient(TelegramBareClient): elif isinstance(invite, ChatInviteAlready): return invite.chat else: + if string in ('me', 'self'): + return self.get_me() result = self(ResolveUsernameRequest(string)) for entity in itertools.chain(result.users, result.chats): if entity.username.lower() == string: @@ -1239,6 +1241,8 @@ class TelegramClient(TelegramBareClient): pass if isinstance(peer, str): + if peer in ('me', 'self'): + return InputPeerSelf() return utils.get_input_peer(self._get_entity_from_string(peer)) is_peer = False From 60594920bd0e1d56822f2474bfb56894c62a40e1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 23:19:58 +0100 Subject: [PATCH 003/108] Add changelog from GitHub releases to RTD --- readthedocs/extra/changelog.rst | 1285 +++++++++++++++++++++++++++++++ readthedocs/index.rst | 3 +- 2 files changed, 1287 insertions(+), 1 deletion(-) create mode 100644 readthedocs/extra/changelog.rst diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst new file mode 100644 index 00000000..96ab594c --- /dev/null +++ b/readthedocs/extra/changelog.rst @@ -0,0 +1,1285 @@ +.. _changelog: + + +=========================== +Changelog (Version History) +=========================== + + +This page lists all the available versions of the library, +in chronological order. You should read this when upgrading +the library to know where your code can break, and where +it can take advantage of new goodies! + +.. contents:: List of All Versions + + +Sessions as sqlite databases (v0.16) +==================================== + +*Published at 2017/12/28* + +In the beginning, session files used to be pickle. This proved to be bad +as soon as one wanted to add more fields. For this reason, they were +migrated to use JSON instead. But this proved to be bad as soon as one +wanted to save things like entities (usernames, their ID and hash), so +now it properly uses +`sqlite3 `__, +which has been well tested, to save the session files! Calling +``.get_input_entity`` using a ``username`` no longer will need to fetch +it first, so it's really 0 calls again. Calling ``.get_entity`` will +always fetch the most up to date version. + +Furthermore, nearly everything has been documented, thus preparing the +library for `Read the Docs `__ (although there +are a few things missing I'd like to polish first), and the +`logging `__ are now +better placed. + +Breaking changes +~~~~~~~~~~~~~~~~ + +- ``.get_dialogs()`` now returns a **single list** instead a tuple + consisting of a **custom class** that should make everything easier + to work with. +- ``.get_message_history()`` also returns a **single list** instead a + tuple, with the ``Message`` instances modified to make them more + convenient. + +Both lists have a ``.total`` attribute so you can still know how many +dialogs/messages are in total. + +New stuff +~~~~~~~~~ + +- The mentioned use of ``sqlite3`` for the session file. +- ``.get_entity()`` now supports lists too, and it will make as little + API calls as possible if you feed it ``InputPeer`` types. Usernames + will always be resolved, since they may have changed. +- ``.set_proxy()`` method, to avoid having to create a new + ``TelegramClient``. +- More ``date`` types supported to represent a date parameter. + +Bug fixes +~~~~~~~~~ + +- Empty strings weren't working when they were a flag parameter (e.g., + setting no last name). +- Fix invalid assertion regarding flag parameters as well. +- Avoid joining the background thread on disconnect, as it would be + ``None`` due to a race condition. +- Correctly handle ``None`` dates when downloading media. +- ``.download_profile_photo`` was failing for some channels. +- ``.download_media`` wasn't handling ``Photo``. + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``date`` was being serialized as local date, but that was wrong. +- ``date`` was being represented as a ``float`` instead of an ``int``. +- ``.tl`` parser wasn't stripping inline comments. +- Removed some redundant checks on ``update_state.py``. +- Use a `synchronized + queue `__ instead a + hand crafted version. +- Use signed integers consistently (e.g. ``salt``). +- Always read the corresponding ``TLObject`` from API responses, except + for some special cases still. +- A few more ``except`` low level to correctly wrap errors. +- More accurate exception types. +- ``invokeWithLayer(initConnection(X))`` now wraps every first request + after ``.connect()``. + +As always, report if you have issues with some of the changes! + +IPv6 support (v0.15.5) +====================== + +*Published at 2017/11/16* + ++-----------------------+ +| Scheme layer used: 73 | ++-----------------------+ + +It's here, it has come! The library now **supports IPv6**! Just pass +``use_ipv6=True`` when creating a ``TelegramClient``. Note that I could +*not* test this feature because my machine doesn't have IPv6 setup. If +you know IPv6 works in your machine but the library doesn't, please +refer to `#425 `_. + +Additions +~~~~~~~~~ + +- IPv6 support. +- New method to extract the text surrounded by ``MessageEntity``\ 's, + in the ``extensions.markdown`` module. + +Enhancements +~~~~~~~~~~~~ + +- Markdown parsing is Done Right. +- Reconnection on failed invoke. Should avoid "number of retries + reached 0" (#270). +- Some missing autocast to ``Input*`` types. +- The library uses the ``NullHandler`` for ``logging`` as it should + have always done. +- ``TcpClient.is_connected()`` is now more reliable. + +.. bug-fixes-1: + +Bug fixes +~~~~~~~~~ + +- Getting an entity using their phone wasn't actually working. +- Full entities aren't saved unless they have an ``access_hash``, to + avoid some ``None`` errors. +- ``.get_message_history`` was failing when retrieving items that had + messages forwarded from a channel. + +General enhancements (v0.15.4) +============================== + +*Published at 2017/11/04* + ++-----------------------+ +| Scheme layer used: 72 | ++-----------------------+ + +This update brings a few general enhancements that are enough to deserve +a new release, with a new feature: beta **markdown-like parsing** for +``.send_message()``! + +.. additions-1: + +Additions +~~~~~~~~~ + +- ``.send_message()`` supports ``parse_mode='md'`` for **Markdown**! It + works in a similar fashion to the official clients (defaults to + double underscore/asterisk, like ``**this**``). Please report any + issues with emojies or enhancements for the parser! +- New ``.idle()`` method so your main thread can do useful job (listen + for updates). +- Add missing ``.to_dict()``, ``__str__`` and ``.stringify()`` for + ``TLMessage`` and ``MessageContainer``. + +.. bug-fixes-2: + +Bug fixes +~~~~~~~~~ + +- The list of known peers could end "corrupted" and have users with + ``access_hash=None``, resulting in ``struct`` error for it not being + an integer. You shouldn't encounter this issue anymore. +- The warning for "added update handler but no workers set" wasn't + actually working. +- ``.get_input_peer`` was ignoring a case for ``InputPeerSelf``. +- There used to be an exception when logging exceptions (whoops) on + update handlers. +- "Downloading contacts" would produce strange output if they had + semicolons (``;``) in their name. +- Fix some cyclic imports and installing dependencies from the ``git`` + repository. +- Code generation was using f-strings, which are only supported on + Python ≥3.6. + +Other changes +~~~~~~~~~~~~~ + +- The ``auth_key`` generation has been moved from ``.connect()`` to + ``.invoke()``. There were some issues were ``.connect()`` failed and + the ``auth_key`` was ``None`` so this will ensure to have a valid + ``auth_key`` when needed, even if ``BrokenAuthKeyError`` is raised. +- Support for higher limits on ``.get_history()`` and + ``.get_dialogs()``. +- Much faster integer factorization when generating the required + ``auth_key``. Thanks @delivrance for making me notice this, and for + the pull request. + +Bug fixes with updates (v0.15.3) +================================ + +*Published at 2017/10/20* + +Hopefully a very ungrateful bug has been removed. When you used to +invoke some request through update handlers, it could potentially enter +an infinite loop. This has been mitigated and it's now safe to invoke +things again! A lot of updates were being dropped (all those gzipped), +and this has been fixed too. + +More bug fixes include a `correct +parsing `__ +of certain TLObjects thanks to @stek29, and +`some `__ +`wrong +calls `__ +that would cause the library to crash thanks to @andr-04, and the +``ReadThread`` not re-starting if you were already authorized. + +Internally, the ``.to_bytes()`` function has been replaced with +``__bytes__`` so now you can do ``bytes(tlobject)``. + +Bug fixes and new small features (v0.15.2) +========================================== + +*Published at 2017/10/14* + +This release primarly focuses on a few bug fixes and enhancements. +Although more stuff may have broken along the way. + +.. bug-fixes-3: + +Bug fixes: +~~~~~~~~~~ + +- ``.get_input_entity`` was failing for IDs and other cases, also + making more requests than it should. +- Use ``basename`` instead ``abspath`` when sending a file. You can now + also override the attributes. +- ``EntityDatabase.__delitem__`` wasn't working. +- ``.send_message()`` was failing with channels. +- ``.get_dialogs(limit=None)`` should now return all the dialogs + correctly. +- Temporary fix for abusive duplicated updates. + +.. enhancements-1: + +Enhancements: +~~~~~~~~~~~~~ + +- You will be warned if you call ``.add_update_handler`` with no + ``update_workers``. +- New customizable threshold value on the session to determine when to + automatically sleep on flood waits. See + ``client.session.flood_sleep_threshold``. +- New ``.get_drafts()`` method with a custom ``Draft`` class by @JosXa. +- Join all threads when calling ``.disconnect()``, to assert no + dangling thread is left alive. +- Larger chunk when downloading files should result in faster + downloads. +- You can use a callable key for the ``EntityDatabase``, so it can be + any filter you need. + +.. internal-changes-1: + +Internal changes: +~~~~~~~~~~~~~~~~~ + +- MsgsAck is now sent in a container rather than its own request. +- ``.get_input_photo`` is now used in the generated code. +- ``.process_entities`` was being called from more places than only + ``__call__``. +- ``MtProtoSender`` now relies more on the generated code to read + responses. + +Custom Entity Database (v0.15.1) +================================ + +*Published at 2017/10/05* + +The main feature of this release is that Telethon now has a custom +database for all the entities you encounter, instead depending on +``@lru_cache`` on the ``.get_entity()`` method. + +The ``EntityDatabase`` will, by default, **cache** all the users, chats +and channels you find in memory for as long as the program is running. +The session will, by default, save all key-value pairs of the entity +identifiers and their hashes (since Telegram may send an ID that it +thinks you already know about, we need to save this information). + +You can **prevent** the ``EntityDatabase`` from saving users by setting +``client.session.entities.enabled = False``, and prevent the ``Session`` +from saving input entities at all by setting +``client.session.save_entities = False``. You can also clear the cache +for a certain user through +``client.session.entities.clear_cache(entity=None)``, which will clear +all if no entity is given. + +More things: + +- ``.sign_in`` accepts phones as integers. +- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the + right amount of dialogs. +- New method to ``.delete_messages()``. +- New ``ChannelPrivateError`` class. +- Changing the IP to which you connect to is as simple as + ``client.session.server_address = 'ip'``, since now the + server address is always queried from the session. +- ``GeneralProxyError`` should be passed to the main thread + again, so that you can handle it. + +Updates Overhaul Update (v0.15) +=============================== + +*Published at 2017/10/01* + +After hundreds of lines changed on a major refactor, *it's finally +here*. It's the **Updates Overhaul Update**; let's get right into it! + +New stuff and enhancements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- You can **invoke** requests from **update handlers**. And **any other + thread**. A new temporary will be made, so that you can be sending + even several requests at the same time! +- **Several worker threads** for your updates! By default, ``None`` + will spawn. I recommend you to work with ``update_workers=4`` to get + started, these will be polling constantly for updates. +- You can also change the number of workers at any given time. +- The library can now run **in a single thread** again, if you don't + need to spawn any at all. Simply set ``spawn_read_thread=False`` when + creating the ``TelegramClient``! +- You can specify ``limit=None`` on ``.get_dialogs()`` to get **all** + of them[1]. +- **Updates are expanded**, so you don't need to check if the update + has ``.updates`` or an inner ``.update`` anymore. +- All ``InputPeer`` entities are **saved in the session** file, but you + can disable this by setting ``save_entities=False``. +- New ``.get_input_entity`` method, which makes use of the above + feature. You **should use this** when a request needs a + ``InputPeer``, rather than the whole entity (although both work). + +Less important enhancements +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Assert that either all or None dependent-flag parameters are set + before sending the request. +- Phone numbers can have dashes, spaces, or parenthesis. They'll be + removed before making the request. +- You can override the phone and its hash on ``.sign_in()``, if you're + creating a new ``TelegramClient`` on two different places. + +Compatibility breaks +~~~~~~~~~~~~~~~~~~~~ + +- ``.create_new_connection()`` is gone for good. No need to deal with + this manually since new connections are now handled on demand by the + library itself. + +Bugs fixed +~~~~~~~~~~ + +- ``.log_out()`` was consuming all retries. It should work just fine + now. +- The session would fail to load if the ``auth_key`` had been removed + manually. +- ``Updates.check_error`` was popping wrong side, although it's been + completely removed. +- ``ServerError``\ 's will be **ignored**, and the request will + immediately be retried. +- Cross-thread safety when saving the session file. +- Some things changed on a matter of when to reconnect, so please + report any bugs! + +.. internal-changes-2: + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``TelegramClient`` is now only an abstraction over the + ``TelegramBareClient``, which can only do basic things, such as + invoking requests, working with files, etc. If you don't need any of + the abstractions the ``TelegramClient``, you can now use the + ``TelegramBareClient`` in a much more comfortable way. +- ``MtProtoSender`` is not thread-safe, but it doesn't need to be since + a new connection will be spawned when needed. +- New connections used to be cached and then reused. Now only their + sessions are saved, as temporary connections are spawned only when + needed. +- Added more RPC errors to the list. + +**[1]:** Broken due to a condition which should had been the opposite +(sigh), fixed 4 commits ahead on +https://github.com/LonamiWebs/Telethon/commit/62ea77cbeac7c42bfac85aa8766a1b5b35e3a76c. + +-------------- + +**That's pretty much it**, although there's more work to be done to make +the overall experience of working with updates *even better*. Stay +tuned! + +Serialization bug fixes (v0.14.2) +================================= + +*Published at 2017/09/29* + +Two bug fixes, one of them quite **important**, related to the +serialization. Every object or request that had to serialize a +``True/False`` type was always being serialized as ``false``! + +Another bug that didn't allow you to leave as ``None`` flag parameters +that needed a list has been fixed. + +Other internal changes include a somewhat more readable ``.to_bytes()`` +function and pre-computing the flag instead using bit shifting. The +``TLObject.constructor_id`` has been renamed to +``TLObject.CONSTRUCTOR_ID``, and ``.subclass_of_id`` is also uppercase +now. + +Farewell, BinaryWriter (v0.14.1) +================================ + +*Published at 2017/09/28* + +Version ``v0.14`` had started working on the new ``.to_bytes()`` method +to dump the ``BinaryWriter`` and its usage on the ``.on_send()`` when +serializing TLObjects, and this release finally removes it. The speed up +when serializing things to bytes should now be over twice as fast +wherever it's needed. + +Other internal changes include using proper classes (including the +generated code) for generating authorization keys and to write out +``TLMessage``\ 's. + +For **bug fixes**, this version is again compatible with Python 3.x +versions **below 3.5** (there was a method call that was Python 3.5 and +above). + +Several requests at once and upload compression (v0.14) +======================================================= + +*Published at 2017/09/27* + +New major release, since I've decided that these two features are big +enough: + +- Requests larger than 512 bytes will be **compressed through + gzip**, and if the result is smaller, this will be uploaded instead. +- You can now send **multiple requests at once**, they're simply + ``*var_args`` on the ``.invoke()``. Note that the server doesn't + guarantee the order in which they'll be executed! + +Internally, another important change. The ``.on_send`` function on the +``TLObjects`` is **gone**, and now there's a new ``.to_bytes()``. From +my tests, this has always been over twice as fast serializing objects, +although more replacements need to be done, so please report any issues. + +Besides this: + +- Downloading media from CDNs wasn't working (wrong + access to a parameter). +- Correct type hinting. +- Added a tiny sleep when trying to perform automatic reconnection. +- Error reporting is done in the background, and has a shorter timeout. +- Implemented ``.get_input_media`` helper methods. Now you can even use + another message as input media! +- ``setup.py`` used to fail with wrongly generated code. + +Quick fix-up (v0.13.6) +====================== + +*Published at 2017/09/23* + +Before getting any further, here's a quick fix-up with things that +should have been on ``v0.13.5`` but were missed. Specifically, the +**timeout when receiving** a request will now work properly. + +Some other additions are a tiny fix when **handling updates**, which was +ignoring some of them, nicer ``__str__`` and ``.stringify()`` methods +for the ``TLObject``\ 's, and not stopping the ``ReadThread`` if you try +invoking something there (now it simply returns ``None``). + +Attempts at more stability (v0.13.5) +==================================== + +*Published at 2017/09/23* + +Yet another update to fix some bugs and increase the stability of the +library, or, at least, that was the attempt! + +This release should really **improve the experience with the background +thread** that the library starts to read things from the network as soon +as it can, but I can't spot every use case, so please report any bug +(and as always, minimal reproducible use cases will help a lot). + +.. bug-fixes-4: + +Bug fixes +~~~~~~~~~ + +- ``setup.py`` was failing on Python < 3.5 due to some imports. +- Duplicated updates should now be ignored. +- ``.send_message`` would crash in some cases, due to having a typo + using the wrong object. +- ``"socket is None"`` when calling ``.connect()`` should not happen + anymore. +- ``BrokenPipeError`` was still being raised due to an incorrect order + on the ``try/except`` block. + +.. enhancements-2: + +Enhancements +~~~~~~~~~~~~ + +- **Type hinting** for all the generated ``Request``\ 's and + ``TLObjects``! IDEs like PyCharm will benefit from this. +- ``ProxyConnectionError`` should properly be passed to the main thread + for you to handle. +- The background thread will only be started after you're authorized on + Telegram (i.e. logged in), and several other attempts at polishing + the experience with this thread. +- The ``Connection`` instance is only created once now, and reused + later. +- Calling ``.connect()`` should have a better behavior now (like + actually *trying* to connect even if we seemingly were connected + already). +- ``.reconnect()`` behavior has been changed to also be more consistent + by making the assumption that we'll only reconnect if the server has + disconnected us, and is now private. + +.. other-changes-1: + +Other changes +~~~~~~~~~~~~~ + +- ``TLObject.__repr__`` doesn't show the original TL definition + anymore, it was a lot of clutter. If you have any complaints open an + issue and we can discuss it. +- Internally, the ``'+'`` from the phone number is now stripped, since + it shouldn't be included. +- Spotted a new place where ``BrokenAuthKeyError`` would be raised, and + it now is raised there. + +More bug fixes and enhancements (v0.13.4) +========================================= + +*Published at 2017/09/18* + +.. new-stuff-1: + +New stuff: +~~~~~~~~~~ + +- ``TelegramClient`` now exposes a ``.is_connected()`` method. +- Initial authorization on a new data center will retry up to 5 times + by default. +- Errors that couldn't be handled on the background thread will be + raised on the next call to ``.invoke()`` or ``updates.poll()``. + +.. bugs-fixed-1: + +Bugs fixed: +~~~~~~~~~~~ + +- Now you should be able to sign in even if you have + ``process_updates=True`` and no previous session. +- Some errors and methods are documented a bit clearer. +- ``.send_message()`` could randomly fail, as the returned type was not + expected. + +Things that should reduce the amount of crashes: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- ``TimeoutError`` is now ignored, since the request will be retried up + to 5 times by default. +- "-404" errors (``BrokenAuthKeyError``\ 's) are now detected when + first connecting to a new data center. +- ``BufferError`` is handled more gracefully, in the same way as + ``InvalidCheckSumError``\ 's. +- Attempt at fixing some "NoneType has no attribute…" errors (with the + ``.sender``). + +Other internal changes: +~~~~~~~~~~~~~~~~~~~~~~~ + +- Calling ``GetConfigRequest`` is now made less often. +- The ``initial_query`` parameter from ``.connect()`` is gone, as it's + not needed anymore. +- Renamed ``all_tlobjects.layer`` to ``all_tlobjects.LAYER`` (since + it's a constant). +- The message from ``BufferError`` is now more useful. + +Bug fixes and enhancements (v0.13.3) +==================================== + +*Published at 2017/09/14* + +.. bugs-fixed-2: + +Bugs fixed +---------- + +- **Reconnection** used to fail because it tried invoking things from + the ``ReadThread``. +- Inferring **random ids** for ``ForwardMessagesRequest`` wasn't + working. +- Downloading media from **CDNs** failed due to having forgotten to + remove a single line. +- ``TcpClient.close()`` now has a **``threading.Lock``**, so + ``NoneType has no close()`` should not happen. +- New **workaround** for ``msg seqno too low/high``. Also, both + ``Session.id/seq`` are not saved anymore. + +.. enhancements-3: + +Enhancements +------------ + +- **Request will be retried** up to 5 times by default rather than + failing on the first attempt. +- ``InvalidChecksumError``\ 's are now **ignored** by the library. +- ``TelegramClient.get_entity()`` is now **public**, and uses the + ``@lru_cache()`` decorator. +- New method to **``.send_voice_note()``**\ 's. +- Methods to send message and media now support a **``reply_to`` + parameter**. +- ``.send_message()`` now returns the **full message** which was just + sent. + +New way to work with updates (v0.13.2) +====================================== + +*Published at 2017/09/08* + +This update brings a new way to work with updates, and it's begging for +your **feedback**, or better names or ways to do what you can do now. + +Please refer to the `wiki/Usage +Modes `__ for +an in-depth description on how to work with updates now. Notice that you +cannot invoke requests from within handlers anymore, only the +``v.0.13.1`` patch allowed you to do so. + +**Other fixes**: + +- Periodic pings are back. +- The username regex mentioned on ``UsernameInvalidError`` was invalid, + but it has now been fixed. +- Sending a message to a phone number was failing because the type used + for a request had changed on layer 71. +- CDN downloads weren't working properly, and now a few patches have been + applied to ensure more reliability, although I couldn't personally test + this, so again, report any feedback. + +Invoke other requests from within update callbacks (v0.13.1) +============================================================ + +*Published at 2017/09/04* + +.. warning:: + + This update brings some big changes to the update system, + so please read it if you work with them! + +A silly "bug" which hadn't been spotted has now been fixed. Now you can +invoke other requests from within your update callbacks. However **this +is not advised**. You should post these updates to some other thread, +and let that thread do the job instead. Invoking a request from within a +callback will mean that, while this request is being invoked, no other +things will be read. + +Internally, the generated code now resides under a *lot* less files, +simply for the sake of avoiding so many unnecessary files. The generated +code is not meant to be read by anyone, simply to do its job. + +Unused attributes have been removed from the ``TLObject`` class too, and +``.sign_up()`` returns the user that just logged in in a similar way to +``.sign_in()`` now. + +Connection modes (v0.13) +======================== + +*Published at 2017/09/04* + ++-----------------------+ +| Scheme layer used: 71 | ++-----------------------+ + +The purpose of this release is to denote a big change, now you can +connect to Telegram through different `**connection +modes** `__. +Also, a **second thread** will *always* be started when you connect a +``TelegramClient``, despite whether you'll be handling updates or +ignoring them, whose sole purpose is to constantly read from the +network. + +The reason for this change is as simple as *"reading and writing +shouldn't be related"*. Even when you're simply ignoring updates, this +way, once you send a request you will only need to read the result for +the request. Whatever Telegram sent before has already been read and +outside the buffer. + +.. additions-2: + +Additions +--------- + +- The mentioned different connection modes, and a new thread. +- You can modify the ``Session`` attributes through the + ``TelegramClient`` constructor (using ``**kwargs``). +- ``RPCError``\ 's now belong to some request you've made, which makes + more sense. +- ``get_input_*`` now handles ``None`` (default) parameters more + gracefully (it used to crash). + +.. 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 + used to be. +- ``TelegramClient.sign_in`` will call ``.send_code_request`` if no + ``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! + The definition looks like + ``def sign_up(self, code, first_name, last_name='')``. +- The old ``JsonSession`` finally replaces the original ``Session`` + (which used pickle). If you were overriding any of these, you should + only worry about overriding ``Session`` now. + +Added verification for CDN file (v0.12.2) +========================================= + +*Published at 2017/08/28* + +Since the Content Distributed Network (CDN) is not handled by Telegram +itself, the owners may tamper these files. Telegram sends their sha256 +sum for clients to implement this additional verification step, which +now the library has. If any CDN has altered the file you're trying to +download, ``CdnFileTamperedError`` will be raised to let you know. + +Besides this. ``TLObject.stringify()`` was showing bytes as lists (now +fixed) and RPC errors are reported by default: + + In an attempt to help everyone who works with the Telegram API, + Telethon will by default report all Remote Procedure Call errors to + `PWRTelegram `__, a public database anyone can + query, made by `Daniil `__. All the information + sent is a GET request with the error code, error message and method used. + + +.. note:: + + If you still would like to opt out, simply set + ``client.session.report_errors = False`` to disable this feature. + However Daniil would really thank you if you helped him (and everyone) + by keeping it on! + +CDN support (v0.12.1) +===================== + +*Published at 2017/08/24* + +The biggest news for this update are that downloading media from CDN's +(you'll often encounter this when working with popular channels) now +**works**. + +Some bug fixes: + +- The method used to download documents crashed because + two lines were swapped. +- Determining the right path when downloading any file was + very weird, now it's been enhanced. +- The ``.sign_in()`` method didn't support integer values for the code! + Now it does again. + +Some important internal changes are that the old way to deal with RSA +public keys now uses a different module instead the old strange +hand-crafted version. + +Hope the new, super simple ``README.rst`` encourages people to use +Telethon and make it better with either suggestions, or pull request. +Pull requests are *super* appreciated, but showing some support by +leaving a star also feels nice ⭐️. + +Newbie friendly update (v0.12) +============================== + +*Published at 2017/08/22* + ++-----------------------+ +| Scheme layer used: 70 | ++-----------------------+ + +This update is overall an attempt to make Telethon a bit more user +friendly, along with some other stability enhancements, although it +brings quite a few changes. + +Things that will probably break your code +----------------------------------------- + +- The ``TelegramClient`` methods ``.send_photo_file()``, + ``.send_document_file()`` and ``.send_media_file()`` are now a + **single method** called ``.send_file()``. It's also important to + note that the **order** of the parameters has been **swapped**: first + to *who* you want to send it, then the file itself. + +- The same applies to ``.download_msg_media()``, which has been renamed + to ``.download_media()``. The method now supports a ``Message`` + itself too, rather than only ``Message.media``. The specialized + ``.download_photo()``, ``.download_document()`` and + ``.download_contact()`` still exist, but are private. + +More new stuff +-------------- + +- Updated to **layer 70**! +- Both downloading and uploading now support **stream-like objects**. +- A lot **faster initial connection** if ``sympy`` is installed (can be + installed through ``pip``). +- ``libssl`` will also be used if available on your system (likely on + Linux based systems). This speed boost should also apply to uploading + and downloading files. +- You can use a **phone number** or an **username** for methods like + ``.send_message()``, ``.send_file()``, and all the other quick-access + methods provided by the ``TelegramClient``. + +.. bug-fixes-5: + +Bug fixes +--------- + +- Crashing when migrating to a new layer and receiving old updates + should not happen now. +- ``InputPeerChannel`` is now casted to ``InputChannel`` automtically + too. +- ``.get_new_msg_id()`` should now be thread-safe. No promises. +- Logging out on macOS caused a crash, which should be gone now. +- More checks to ensure that the connection is flagged correctly as + either connected or not. + +Bug additions +------------- + +- Downloading files from CDN's will **not work** yet (something new + that comes with layer 70). + +-------------- + +That's it, any new idea or suggestion about how to make the project even +more friendly is highly appreciated. + +.. note:: + + Did you know that you can pretty print any result Telegram returns + (called ``TLObject``\ 's) by using their ``.stringify()`` function? + Great for debugging! + +get_input_* now works with vectors (v0.11.5) +============================================= + +*Published at 2017/07/11* + +Quick fix-up of a bug which hadn't been encountered until now. Auto-cast +by using ``get_input_*`` now works. + +get_input_* everywhere (v0.11.4) +================================= + +*Published at 2017/07/10* + +For some reason, Telegram doesn't have enough with the +`InputPeer `__. +There also exist +`InputChannel `__ +and +`InputUser `__! +You don't have to worry about those anymore, it's handled internally +now. + +Besides this, every Telegram object now features a new default +``.__str__`` look, and also a `.stringify() +method `__ +to pretty format them, if you ever need to inspect them. + +The library now uses `the DEBUG +level `__ +everywhere, so no more warnings or information messages if you had +logging enabled. + +The ``no_webpage`` parameter from ``.send_message`` `has been +renamed `__ +to ``link_preview`` for clarity, so now it does the opposite (but has a +clearer intention). + +Quick .send_message() fix (v0.11.3) +=================================== + +*Published at 2017/07/05* + +A very quick follow-up release to fix a tiny bug with +``.send_message()``, no new features. + +Callable TelegramClient (v0.11.2) +================================= + +*Published at 2017/07/04* + ++-----------------------+ +| Scheme layer used: 68 | ++-----------------------+ + +There is a new preferred way to **invoke requests**, which you're +encouraged to use: + +.. code:: python + + # New! + result = client(SomeRequest()) + + # Old. + result = client.invoke(SomeRequest()) + +Existing code will continue working, since the old ``.invoke()`` has not +been deprecated. + +When you ``.create_new_connection()``, it will also handle +``FileMigrateError``\ 's for you, so you don't need to worry about those +anymore. + +.. bugs-fixed-3: + +Bugs fixed: +----------- + +- Fixed some errors when installing Telethon via ``pip`` (for those + using either source distributions or a Python version ≤ 3.5). +- ``ConnectionResetError`` didn't flag sockets as closed, but now it + does. + +On a more technical side, ``msg_id``\ 's are now more accurate. + +Improvements to the updates (v0.11.1) +===================================== + +*Published at 2017/06/24* + +Receiving new updates shouldn't miss any anymore, also, periodic pings +are back again so it should work on the long run. + +On a different order of things, ``.connect()`` also features a timeout. +Notice that the ``timeout=`` is **not** passed as a **parameter** +anymore, and is instead specified when creating the ``TelegramClient``. + +Some other bug fixes: +- Fixed some name class when a request had a ``.msg_id`` parameter. +- The correct amount of random bytes is now used in DH request +- Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions. +- Avoid connecting if already connected. + +Support for parallel connections (v0.11) +======================================== + +*Published at 2017/06/16* + +*This update brings a lot of changes, so it would be nice if you could* +**read the whole change log**! + +Things that may break your code +------------------------------- + +- Every Telegram error has now its **own class**, so it's easier to + fine-tune your ``except``\ 's. +- Markdown parsing is **not part** of Telethon itself anymore, although + there are plans to support it again through a some external module. +- The ``.list_sessions()`` has been moved to the ``Session`` class + instead. +- The ``InteractiveTelegramClient`` is **not** shipped with ``pip`` + anymore. + +New features +------------ + +- A new, more **lightweight class** has been added. The + ``TelegramBareClient`` is now the base of the normal + ``TelegramClient``, and has the most basic features. +- New method to ``.create_new_connection()``, which can be ran **in + parallel** with the original connection. This will return the + previously mentioned ``TelegramBareClient`` already connected. +- Any file object can now be used to download a file (for instance, a + ``BytesIO()`` instead a file name). +- Vales like ``random_id`` are now **automatically inferred**, so you + can save yourself from the hassle of writing + ``generate_random_long()`` everywhere. Same applies to + ``.get_input_peer()``, unless you really need the extra performance + provided by skipping one ``if`` if called manually. +- Every type now features a new ``.to_dict()`` method. + +.. bug-fixes-6: + +Bug fixes +--------- + +- Received errors are acknowledged to the server, so they don't happen + over and over. +- Downloading media on different data centers is now up to **x2 + faster**, since there used to be an ``InvalidDCError`` for each file + part tried to be downloaded. +- Lost messages are now properly skipped. +- New way to handle the **result of requests**. The old ``ValueError`` + "*The previously sent request must be resent. However, no request was + previously sent (possibly called from a different thread).*" *should* + not happen anymore. + +Minor highlights +---------------- + +- Some fixes to the ``JsonSession``. +- Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while + ``.reconnect()`` was being called on the ``UpdatesThread``. +- Some improvements on the ``TcpClient``, such as not switching between + blocking and non-blocking sockets. +- The code now uses ASCII characters only. +- Some enhancements to ``.find_user_or_chat()`` and + ``.get_input_peer()``. + +JSON session file (v0.10.1) +=========================== + +*Published at 2017/06/07* + +This version is primarily for people to **migrate** their ``.session`` +files, which are *pickled*, to the new *JSON* format. Although slightly +slower, and a bit more vulnerable since it's plain text, it's a lot more +resistant to upgrades. + +.. warning:: + + You **must** upgrade to this version before any higher one if you've + used Telethon ≤ v0.10. If you happen to upgrade to an higher version, + that's okay, but you will have to manually delete the ``*.session`` file, + and logout from that session from an official client. + +Other highlights: + +- New ``.get_me()`` function to get the **current** user. +- ``.is_user_authorized()`` is now more reliable. +- New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy`` + on the online documentation. +- Everything on the documentation is now, theoretically, **sorted + alphabetically**. +- **More error codes** added to the ``errors`` file. +- No second thread is spawned unless one or more update handlers are added. + +Full support for different DCs and ++stable (v0.10) +=================================================== + +*Published at 2017/06/03* + +Working with **different data centers** finally *works*! On a different +order of things, **reconnection** is now performed automatically every +time Telegram decides to kick us off their servers, so now Telethon can +really run **forever and ever**! In theory. + +Another important highlights: + +- **Documentation** improvements, such as showing the return type. +- The ``msg_id too low/high`` error should happen **less often**, if + any. +- Sleeping on the main thread is **not done anymore**. You will have to + ``except FloodWaitError``\ 's. +- You can now specify your *own application version*, device model, + system version and language code. +- Code is now more *pythonic* (such as making some members private), + and other internal improvements (which affect the **updates + thread**), such as using ``logger`` instead a bare ``print()`` too. + +This brings Telethon a whole step closer to ``v1.0``, though more things +should preferably be changed. + +Stability improvements (v0.9.1) +=============================== + +*Published at 2017/05/23* + +Telethon used to crash a lot when logging in for the very first time. +The reason for this was that the reconnection (or dead connections) were +not handled properly. Now they are, so you should be able to login +directly, without needing to delete the ``*.session`` file anymore. +Notice that downloading from a different DC is still a WIP. + +Some highlights: + +- Updates thread is only started after a successful login. +- Files meant to be ran by the user now use **shebangs** and + proper permissions. +- In-code documentation now shows the returning type. +- **Relative import** is now used everywhere, so you can rename + ``telethon`` to anything else. +- **Dead connections** are now **detected** instead entering an infinite loop. +- **Sockets** can now be **closed** (and re-opened) properly. +- Telegram decided to update the layer 66 without increasing the number. + This has been fixed and now we're up-to-date again. + +General improvements (v0.9) +=========================== + +*Published at 2017/05/19* + ++-----------------------+ +| Scheme layer used: 66 | ++-----------------------+ + +This release features: + +- The **documentation**, available online + `here `__, has a new search bar. +- Better **cross-thread safety** by using ``threading.Event``. +- More improvements for running Telethon during a **long period of time**. + +With the following bug fixes: + +- **Avoid a certain crash on login** (occurred if an unexpected object + ID was received). +- Avoid crashing with certain invalid UTF-8 strings. +- Avoid crashing on certain terminals by using known ASCII characters + where possible. +- The ``UpdatesThread`` is now a daemon, and should cause less issues. +- Temporary sessions didn't actually work (with ``session=None``). + +Minor notes: + +- ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. + +Bot login and proxy support (v0.8) +================================== + +*Published at 2017/04/14* + +This release features: + +- **Bot login**, thanks to @JuanPotato for hinting me about how to do + it. +- **Proxy support**, thanks to @exzhawk for implementing it. +- **Logging support**, used by passing ``--telethon-log=DEBUG`` (or + ``INFO``) as a command line argument. + +With the following bug fixes: + +- Connection fixes, such as avoiding connection until ``.connect()`` is + explicitly invoked. +- Uploading big files now works correctly. +- Fix uploading big files. +- Some fixes on the updates thread, such as correctly sleeping when required. + +Long-run bug fix (v0.7.1) +========================= + +*Published at 2017/02/19* + +If you're one of those who runs Telethon for a long time (more than 30 +minutes), this update by @strayge will be great for you. It sends +periodic pings to the Telegram servers so you don't get disconnected and +you can still send and receive updates! + +Two factor authentication (v0.7) +================================ + +*Published at 2017/01/31* + ++-----------------------+ +| Scheme layer used: 62 | ++-----------------------+ + +If you're one of those who love security the most, these are good news. +You can now use two factor authentication with Telethon too! As internal +changes, the coding style has been improved, and you can easily use +custom session objects, and various little bugs have been fixed. + +Updated pip version (v0.6) +========================== + +*Published at 2016/11/13* + ++-----------------------+ +| Scheme layer used: 57 | ++-----------------------+ + +This release has no new major features. However, it contains some small +changes that make using Telethon a little bit easier. Now those who have +installed Telethon via ``pip`` can also take advantage of changes, such +as less bugs, creating empty instances of ``TLObjects``, specifying a +timeout and more! + +Ready, pip, go! (v0.5) +====================== + +*Published at 2016/09/18* + +Telethon is now available as a **`Python +package `__**! Those are +really exciting news (except, sadly, the project structure had to change +*a lot* to be able to do that; but hopefully it won't need to change +much more, any more!) + +Not only that, but more improvements have also been made: you're now +able to both **sign up** and **logout**, watch a pretty +"Uploading/Downloading… x%" progress, and other minor changes which make +using Telethon **easier**. + +Made InteractiveTelegramClient cool (v0.4) +========================================== + +*Published at 2016/09/12* + +Yes, really cool! I promise. Even though this is meant to be a +*library*, that doesn't mean it can't have a good *interactive client* +for you to try the library out. This is why now you can do many, many +things with the ``InteractiveTelegramClient``: + +- **List dialogs** (chats) and pick any you wish. +- **Send any message** you like, text, photos or even documents. +- **List** the **latest messages** in the chat. +- **Download** any message's media (photos, documents or even contacts!). +- **Receive message updates** as you talk (i.e., someone sent you a message). + +It actually is an usable-enough client for your day by day. You could +even add ``libnotify`` and pop, you're done! A great cli-client with +desktop notifications. + +Also, being able to download and upload media implies that you can do +the same with the library itself. Did I need to mention that? Oh, and +now, with even less bugs! I hope. + +Media revolution and improvements to update handling! (v0.3) +============================================================ + +*Published at 2016/09/11* + +Telegram is more than an application to send and receive messages. You +can also **send and receive media**. Now, this implementation also gives +you the power to upload and download media from any message that +contains it! Nothing can now stop you from filling up all your disk +space with all the photos! If you want to, of course. + +Handle updates in their own thread! (v0.2) +========================================== + +*Published at 2016/09/10* + +This version handles **updates in a different thread** (if you wish to +do so). This means that both the low level ``TcpClient`` and the +not-so-low-level ``MtProtoSender`` are now multi-thread safe, so you can +use them with more than a single thread without worrying! + +This also implies that you won't need to send a request to **receive an +update** (is someone typing? did they send me a message? has someone +gone offline?). They will all be received **instantly**. + +Some other cool examples of things that you can do: when someone tells +you "*Hello*", you can automatically reply with another "*Hello*" +without even needing to type it by yourself :) + +However, be careful with spamming!! Do **not** use the program for that! + +First working alpha version! (v0.1) +=================================== + +*Published at 2016/09/06* + ++-----------------------+ +| Scheme layer used: 55 | ++-----------------------+ + +There probably are some bugs left, which haven't yet been found. +However, the majority of code works and the application is already +usable! Not only that, but also uses the latest scheme as of now *and* +handles way better the errors. This tag is being used to mark this +release as stable enough. diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 8e5c6053..161c4b1a 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -10,7 +10,8 @@ Welcome to Telethon's documentation! Pure Python 3 Telegram client library. Official Site `here `_. -Please follow the links below to get you started. +Please follow the links below to get you started, and remember +to read the :ref:`changelog` when you upgrade! .. _installation-and-usage: From c039ba3e16bedfe7eaea87a8cac7c87dac3fc5d0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 5 Jan 2018 23:48:21 +0100 Subject: [PATCH 004/108] Be consistent with the titles in the changelog --- readthedocs/extra/changelog.rst | 242 ++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 104 deletions(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 96ab594c..569f21ca 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -49,7 +49,7 @@ Breaking changes Both lists have a ``.total`` attribute so you can still know how many dialogs/messages are in total. -New stuff +Additions ~~~~~~~~~ - The mentioned use of ``sqlite3`` for the session file. @@ -183,8 +183,8 @@ Bug fixes - Code generation was using f-strings, which are only supported on Python ≥3.6. -Other changes -~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - The ``auth_key`` generation has been moved from ``.connect()`` to ``.invoke()``. There were some issues were ``.connect()`` failed and @@ -227,25 +227,8 @@ Bug fixes and new small features (v0.15.2) This release primarly focuses on a few bug fixes and enhancements. Although more stuff may have broken along the way. -.. bug-fixes-3: - -Bug fixes: -~~~~~~~~~~ - -- ``.get_input_entity`` was failing for IDs and other cases, also - making more requests than it should. -- Use ``basename`` instead ``abspath`` when sending a file. You can now - also override the attributes. -- ``EntityDatabase.__delitem__`` wasn't working. -- ``.send_message()`` was failing with channels. -- ``.get_dialogs(limit=None)`` should now return all the dialogs - correctly. -- Temporary fix for abusive duplicated updates. - -.. enhancements-1: - -Enhancements: -~~~~~~~~~~~~~ +Enhancements +~~~~~~~~~~~~ - You will be warned if you call ``.add_update_handler`` with no ``update_workers``. @@ -260,10 +243,27 @@ Enhancements: - You can use a callable key for the ``EntityDatabase``, so it can be any filter you need. +.. bug-fixes-3: + +Bug fixes +~~~~~~~~~ + +- ``.get_input_entity`` was failing for IDs and other cases, also + making more requests than it should. +- Use ``basename`` instead ``abspath`` when sending a file. You can now + also override the attributes. +- ``EntityDatabase.__delitem__`` wasn't working. +- ``.send_message()`` was failing with channels. +- ``.get_dialogs(limit=None)`` should now return all the dialogs + correctly. +- Temporary fix for abusive duplicated updates. + +.. enhancements-1: + .. internal-changes-1: -Internal changes: -~~~~~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - MsgsAck is now sent in a container rather than its own request. - ``.get_input_photo`` is now used in the generated code. @@ -295,16 +295,26 @@ for a certain user through ``client.session.entities.clear_cache(entity=None)``, which will clear all if no entity is given. -More things: -- ``.sign_in`` accepts phones as integers. -- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the - right amount of dialogs. +Additions +~~~~~~~~~ + - New method to ``.delete_messages()``. - New ``ChannelPrivateError`` class. + +Enhancements +~~~~~~~~~~~~ + +- ``.sign_in`` accepts phones as integers. - Changing the IP to which you connect to is as simple as ``client.session.server_address = 'ip'``, since now the server address is always queried from the session. + +Bug fixes +~~~~~~~~~ + +- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the + right amount of dialogs. - ``GeneralProxyError`` should be passed to the main thread again, so that you can handle it. @@ -316,8 +326,15 @@ Updates Overhaul Update (v0.15) After hundreds of lines changed on a major refactor, *it's finally here*. It's the **Updates Overhaul Update**; let's get right into it! -New stuff and enhancements -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Breaking changes +~~~~~~~~~~~~~~~~ + +- ``.create_new_connection()`` is gone for good. No need to deal with + this manually since new connections are now handled on demand by the + library itself. + +Enhancements +~~~~~~~~~~~~ - You can **invoke** requests from **update handlers**. And **any other thread**. A new temporary will be made, so that you can be sending @@ -338,10 +355,6 @@ New stuff and enhancements - New ``.get_input_entity`` method, which makes use of the above feature. You **should use this** when a request needs a ``InputPeer``, rather than the whole entity (although both work). - -Less important enhancements -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Assert that either all or None dependent-flag parameters are set before sending the request. - Phone numbers can have dashes, spaces, or parenthesis. They'll be @@ -349,15 +362,8 @@ Less important enhancements - You can override the phone and its hash on ``.sign_in()``, if you're creating a new ``TelegramClient`` on two different places. -Compatibility breaks -~~~~~~~~~~~~~~~~~~~~ - -- ``.create_new_connection()`` is gone for good. No need to deal with - this manually since new connections are now handled on demand by the - library itself. - -Bugs fixed -~~~~~~~~~~ +Bug fixes +~~~~~~~~~ - ``.log_out()`` was consuming all retries. It should work just fine now. @@ -403,18 +409,22 @@ Serialization bug fixes (v0.14.2) *Published at 2017/09/29* -Two bug fixes, one of them quite **important**, related to the -serialization. Every object or request that had to serialize a -``True/False`` type was always being serialized as ``false``! +Bug fixes +~~~~~~~~~ -Another bug that didn't allow you to leave as ``None`` flag parameters -that needed a list has been fixed. +- **Important**, related to the serialization. Every object or request + that had to serialize a ``True/False`` type was always being serialized + as ``false``! +- Another bug that didn't allow you to leave as ``None`` flag parameters + that needed a list has been fixed. -Other internal changes include a somewhat more readable ``.to_bytes()`` -function and pre-computing the flag instead using bit shifting. The -``TLObject.constructor_id`` has been renamed to -``TLObject.CONSTRUCTOR_ID``, and ``.subclass_of_id`` is also uppercase -now. +Internal changes +~~~~~~~~~~~~~~~~ + +- Other internal changes include a somewhat more readable ``.to_bytes()`` + function and pre-computing the flag instead using bit shifting. The + ``TLObject.constructor_id`` has been renamed to ``TLObject.CONSTRUCTOR_ID``, + and ``.subclass_of_id`` is also uppercase now. Farewell, BinaryWriter (v0.14.1) ================================ @@ -427,13 +437,18 @@ serializing TLObjects, and this release finally removes it. The speed up when serializing things to bytes should now be over twice as fast wherever it's needed. -Other internal changes include using proper classes (including the -generated code) for generating authorization keys and to write out -``TLMessage``\ 's. +Bug fixes +~~~~~~~~~ + +- This version is again compatible with Python 3.x versions **below 3.5** + (there was a method call that was Python 3.5 and above). + +Internal changes +~~~~~~~~~~~~~~~~ + +- Using proper classes (including the generated code) for generating + authorization keys and to write out ``TLMessage``\ 's. -For **bug fixes**, this version is again compatible with Python 3.x -versions **below 3.5** (there was a method call that was Python 3.5 and -above). Several requests at once and upload compression (v0.14) ======================================================= @@ -443,6 +458,9 @@ Several requests at once and upload compression (v0.14) New major release, since I've decided that these two features are big enough: +Additions +~~~~~~~~~ + - Requests larger than 512 bytes will be **compressed through gzip**, and if the result is smaller, this will be uploaded instead. - You can now send **multiple requests at once**, they're simply @@ -454,15 +472,20 @@ Internally, another important change. The ``.on_send`` function on the my tests, this has always been over twice as fast serializing objects, although more replacements need to be done, so please report any issues. -Besides this: +Enhancements +~~~~~~~~~~~~ +- Implemented ``.get_input_media`` helper methods. Now you can even use + another message as input media! + + +Bug fixes +~~~~~~~~~ - Downloading media from CDNs wasn't working (wrong access to a parameter). - Correct type hinting. - Added a tiny sleep when trying to perform automatic reconnection. - Error reporting is done in the background, and has a shorter timeout. -- Implemented ``.get_input_media`` helper methods. Now you can even use - another message as input media! - ``setup.py`` used to fail with wrongly generated code. Quick fix-up (v0.13.6) @@ -529,8 +552,8 @@ Enhancements .. other-changes-1: -Other changes -~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - ``TLObject.__repr__`` doesn't show the original TL definition anymore, it was a lot of clutter. If you have any complaints open an @@ -547,8 +570,8 @@ More bug fixes and enhancements (v0.13.4) .. new-stuff-1: -New stuff: -~~~~~~~~~~ +Additions +~~~~~~~~~ - ``TelegramClient`` now exposes a ``.is_connected()`` method. - Initial authorization on a new data center will retry up to 5 times @@ -558,18 +581,14 @@ New stuff: .. bugs-fixed-1: -Bugs fixed: -~~~~~~~~~~~ +Bug fixes +~~~~~~~~~~ - Now you should be able to sign in even if you have ``process_updates=True`` and no previous session. - Some errors and methods are documented a bit clearer. - ``.send_message()`` could randomly fail, as the returned type was not expected. - -Things that should reduce the amount of crashes: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - ``TimeoutError`` is now ignored, since the request will be retried up to 5 times by default. - "-404" errors (``BrokenAuthKeyError``\ 's) are now detected when @@ -579,8 +598,8 @@ Things that should reduce the amount of crashes: - Attempt at fixing some "NoneType has no attribute…" errors (with the ``.sender``). -Other internal changes: -~~~~~~~~~~~~~~~~~~~~~~~ +Internal changes +~~~~~~~~~~~~~~~~ - Calling ``GetConfigRequest`` is now made less often. - The ``initial_query`` parameter from ``.connect()`` is gone, as it's @@ -596,8 +615,8 @@ Bug fixes and enhancements (v0.13.3) .. bugs-fixed-2: -Bugs fixed ----------- +Bug fixes +--------- - **Reconnection** used to fail because it tried invoking things from the ``ReadThread``. @@ -640,7 +659,8 @@ an in-depth description on how to work with updates now. Notice that you cannot invoke requests from within handlers anymore, only the ``v.0.13.1`` patch allowed you to do so. -**Other fixes**: +Bug fixes +~~~~~~~~~ - Periodic pings are back. - The username regex mentioned on ``UsernameInvalidError`` was invalid, @@ -723,8 +743,8 @@ Enhancements - ``TelegramClient.sign_in`` will call ``.send_code_request`` if no ``code`` was provided. -Deprecation: ------------- +Deprecation +----------- - ``.sign_up`` does *not* take a ``phone`` argument anymore. Change this or you will be using ``phone`` as ``code``, and it will fail! @@ -771,7 +791,8 @@ The biggest news for this update are that downloading media from CDN's (you'll often encounter this when working with popular channels) now **works**. -Some bug fixes: +Bug fixes +~~~~~~~~~ - The method used to download documents crashed because two lines were swapped. @@ -802,8 +823,8 @@ This update is overall an attempt to make Telethon a bit more user friendly, along with some other stability enhancements, although it brings quite a few changes. -Things that will probably break your code ------------------------------------------ +Breaking changes +---------------- - The ``TelegramClient`` methods ``.send_photo_file()``, ``.send_document_file()`` and ``.send_media_file()`` are now a @@ -817,8 +838,8 @@ Things that will probably break your code ``.download_photo()``, ``.download_document()`` and ``.download_contact()`` still exist, but are private. -More new stuff --------------- +Additions +--------- - Updated to **layer 70**! - Both downloading and uploading now support **stream-like objects**. @@ -845,10 +866,9 @@ Bug fixes - More checks to ensure that the connection is flagged correctly as either connected or not. -Bug additions -------------- +.. note:: -- Downloading files from CDN's will **not work** yet (something new + Downloading files from CDN's will **not work** yet (something new that comes with layer 70). -------------- @@ -936,8 +956,8 @@ anymore. .. bugs-fixed-3: -Bugs fixed: ------------ +Bugs fixes +~~~~~~~~~~ - Fixed some errors when installing Telethon via ``pip`` (for those using either source distributions or a Python version ≤ 3.5). @@ -958,7 +978,9 @@ On a different order of things, ``.connect()`` also features a timeout. Notice that the ``timeout=`` is **not** passed as a **parameter** anymore, and is instead specified when creating the ``TelegramClient``. -Some other bug fixes: +Bug fixes +~~~~~~~~~ + - Fixed some name class when a request had a ``.msg_id`` parameter. - The correct amount of random bytes is now used in DH request - Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions. @@ -972,8 +994,8 @@ Support for parallel connections (v0.11) *This update brings a lot of changes, so it would be nice if you could* **read the whole change log**! -Things that may break your code -------------------------------- +Breaking changes +---------------- - Every Telegram error has now its **own class**, so it's easier to fine-tune your ``except``\ 's. @@ -984,8 +1006,8 @@ Things that may break your code - The ``InteractiveTelegramClient`` is **not** shipped with ``pip`` anymore. -New features ------------- +Additions +--------- - A new, more **lightweight class** has been added. The ``TelegramBareClient`` is now the base of the normal @@ -1018,7 +1040,7 @@ Bug fixes previously sent (possibly called from a different thread).*" *should* not happen anymore. -Minor highlights +Internal changes ---------------- - Some fixes to the ``JsonSession``. @@ -1047,15 +1069,20 @@ resistant to upgrades. that's okay, but you will have to manually delete the ``*.session`` file, and logout from that session from an official client. -Other highlights: +Additions +~~~~~~~~~ - New ``.get_me()`` function to get the **current** user. - ``.is_user_authorized()`` is now more reliable. - New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy`` on the online documentation. +- **More error codes** added to the ``errors`` file. + +Enhancements +~~~~~~~~~~~~ + - Everything on the documentation is now, theoretically, **sorted alphabetically**. -- **More error codes** added to the ``errors`` file. - No second thread is spawned unless one or more update handlers are added. Full support for different DCs and ++stable (v0.10) @@ -1068,7 +1095,8 @@ order of things, **reconnection** is now performed automatically every time Telegram decides to kick us off their servers, so now Telethon can really run **forever and ever**! In theory. -Another important highlights: +Enhancements +~~~~~~~~~~~~ - **Documentation** improvements, such as showing the return type. - The ``msg_id too low/high`` error should happen **less often**, if @@ -1095,7 +1123,8 @@ not handled properly. Now they are, so you should be able to login directly, without needing to delete the ``*.session`` file anymore. Notice that downloading from a different DC is still a WIP. -Some highlights: +Enhancements +~~~~~~~~~~~~ - Updates thread is only started after a successful login. - Files meant to be ran by the user now use **shebangs** and @@ -1117,14 +1146,16 @@ General improvements (v0.9) | Scheme layer used: 66 | +-----------------------+ -This release features: +Additions +~~~~~~~~~ - The **documentation**, available online `here `__, has a new search bar. - Better **cross-thread safety** by using ``threading.Event``. - More improvements for running Telethon during a **long period of time**. -With the following bug fixes: +Bug fixes +~~~~~~~~~ - **Avoid a certain crash on login** (occurred if an unexpected object ID was received). @@ -1134,7 +1165,8 @@ With the following bug fixes: - The ``UpdatesThread`` is now a daemon, and should cause less issues. - Temporary sessions didn't actually work (with ``session=None``). -Minor notes: +Internal changes +~~~~~~~~~~~~~~~~ - ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. @@ -1143,7 +1175,8 @@ Bot login and proxy support (v0.8) *Published at 2017/04/14* -This release features: +Additions +~~~~~~~~~ - **Bot login**, thanks to @JuanPotato for hinting me about how to do it. @@ -1151,7 +1184,8 @@ This release features: - **Logging support**, used by passing ``--telethon-log=DEBUG`` (or ``INFO``) as a command line argument. -With the following bug fixes: +Bug fixes +~~~~~~~~~ - Connection fixes, such as avoiding connection until ``.connect()`` is explicitly invoked. From 3eafe18d0b8bf535c140e845eae2e3a0be78701c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 01:55:11 +0100 Subject: [PATCH 005/108] Implement MtProto 2.0 (closes #484, thanks @delivrance!) Huge shoutout to @delivrance's pyrogram, specially this commit: pyrogram/pyrogram/commit/42f9a2d6994baaf9ecad590d1ff4d175a8c56454 --- telethon/extensions/binary_reader.py | 5 ++- telethon/helpers.py | 67 ++++++++++++++++++++++++++-- telethon/network/mtproto_sender.py | 44 +++--------------- 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 460bed96..1402083f 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -56,8 +56,11 @@ class BinaryReader: return int.from_bytes( self.read(bits // 8), byteorder='little', signed=signed) - def read(self, length): + def read(self, length=None): """Read the given amount of bytes.""" + if length is None: + return self.reader.read() + result = self.reader.read(length) if len(result) != length: raise BufferError( diff --git a/telethon/helpers.py b/telethon/helpers.py index 3c9af2cb..d97b8a9f 100644 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -1,6 +1,11 @@ """Various helpers not related to the Telegram API itself""" -from hashlib import sha1, sha256 import os +import struct +from hashlib import sha1, sha256 + +from telethon.crypto import AES +from telethon.extensions import BinaryReader + # region Multiple utilities @@ -21,9 +26,48 @@ def ensure_parent_dir_exists(file_path): # region Cryptographic related utils +def pack_message(session, message): + """Packs a message following MtProto 2.0 guidelines""" + # See https://core.telegram.org/mtproto/description + data = struct.pack(' Date: Sat, 6 Jan 2018 02:03:23 +0100 Subject: [PATCH 006/108] Add a few security checks when unpacking messages from server Also delete MtProto 1.0 leftovers. --- telethon/helpers.py | 40 +++++++++++++---------------------- telethon_tests/crypto_test.py | 1 + 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/telethon/helpers.py b/telethon/helpers.py index d97b8a9f..82b551ab 100644 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -4,6 +4,7 @@ import struct from hashlib import sha1, sha256 from telethon.crypto import AES +from telethon.errors import SecurityError from telethon.extensions import BinaryReader @@ -39,7 +40,7 @@ def pack_message(session, message): # "msg_key = substr (msg_key_large, 8, 16)" msg_key = msg_key_large[8:24] - aes_key, aes_iv = calc_key_2(session.auth_key.key, msg_key, True) + aes_key, aes_iv = calc_key(session.auth_key.key, msg_key, True) key_id = struct.pack(' Date: Sat, 6 Jan 2018 13:37:46 +0100 Subject: [PATCH 007/108] Fix a few more issue styles with RTD (mostly lists/nested md) --- .../telegram-api-in-other-languages.rst | 20 ++++++++-------- .../extra/troubleshooting/rpc-errors.rst | 13 +++++----- readthedocs/extra/wall-of-shame.rst | 24 ++++++++++--------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/readthedocs/extra/developing/telegram-api-in-other-languages.rst b/readthedocs/extra/developing/telegram-api-in-other-languages.rst index 0adeb988..44e45d51 100644 --- a/readthedocs/extra/developing/telegram-api-in-other-languages.rst +++ b/readthedocs/extra/developing/telegram-api-in-other-languages.rst @@ -13,18 +13,18 @@ C * Possibly the most well-known unofficial open source implementation out -there by `**@vysheng** `__, -```tgl`` `__, and its console client -```telegram-cli`` `__. Latest development +there by `@vysheng `__, +`tgl `__, and its console client +`telegram-cli `__. Latest development has been moved to `BitBucket `__. JavaScript ********** -`**@zerobias** `__ is working on -```telegram-mtproto`` `__, +`@zerobias `__ is working on +`telegram-mtproto `__, a work-in-progress JavaScript library installable via -```npm`` `__. +`npm `__. Kotlin ****** @@ -34,14 +34,14 @@ implementation written in Kotlin (the now `official `__ language for `Android `__) by -`**@badoualy** `__, currently as a beta– +`@badoualy `__, currently as a beta– yet working. PHP *** A PHP implementation is also available thanks to -`**@danog** `__ and his +`@danog `__ and his `MadelineProto `__ project, with a very nice `online documentation `__ too. @@ -51,7 +51,7 @@ Python A fairly new (as of the end of 2017) Telegram library written from the ground up in Python by -`**@delivrance** `__ and his +`@delivrance `__ and his `Pyrogram `__ library! No hard feelings Dan and good luck dealing with some of your users ;) @@ -59,6 +59,6 @@ Rust **** Yet another work-in-progress implementation, this time for Rust thanks -to `**@JuanPotato** `__ under the fancy +to `@JuanPotato `__ under the fancy name of `Vail `__. This one is very early still, but progress is being made at a steady rate. diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst index 55a21d7b..0d36bec6 100644 --- a/readthedocs/extra/troubleshooting/rpc-errors.rst +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -17,11 +17,12 @@ something went wrong on Telegram's server). The most common are: said operation on a chat or channel. Try avoiding filters, i.e. when searching messages. -The generic classes for different error codes are: \* ``InvalidDCError`` -(303), the request must be repeated on another DC. \* -``BadRequestError`` (400), the request contained errors. \* -``UnauthorizedError`` (401), the user is not authorized yet. \* -``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError`` -(404), make sure you're invoking ``Request``\ 's! +The generic classes for different error codes are: + +- ``InvalidDCError`` (303), the request must be repeated on another DC. +- ``BadRequestError`` (400), the request contained errors. +- ``UnauthorizedError`` (401), the user is not authorized yet. +- ``ForbiddenError`` (403), privacy violation error. +- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's! If the error is not recognised, it will only be an ``RPCError``. diff --git a/readthedocs/extra/wall-of-shame.rst b/readthedocs/extra/wall-of-shame.rst index 95ad3e04..dfede312 100644 --- a/readthedocs/extra/wall-of-shame.rst +++ b/readthedocs/extra/wall-of-shame.rst @@ -17,17 +17,19 @@ Shame `__: -> > **rtfm** -> > Literally "Read The F\ **king Manual"; a term showing the -frustration of being bothered with questions so trivial that the asker -could have quickly figured out the answer on their own with minimal -effort, usually by reading readily-available documents. People who -say"RTFM!" might be considered rude, but the true rude ones are the -annoying people who take absolutely no self-responibility and expect to -have all the answers handed to them personally. -> > *"Damn, that's the twelveth time that somebody posted this question -to the messageboard today! RTFM, already!"* -> > **\ by Bill M. July 27, 2004*\* + **rtfm** + Literally "Read The F--king Manual"; a term showing the + frustration of being bothered with questions so trivial that the asker + could have quickly figured out the answer on their own with minimal + effort, usually by reading readily-available documents. People who + say"RTFM!" might be considered rude, but the true rude ones are the + annoying people who take absolutely no self-responibility and expect to + have all the answers handed to them personally. + + *"Damn, that's the twelveth time that somebody posted this question + to the messageboard today! RTFM, already!"* + + *by Bill M. July 27, 2004* If you have indeed read the wiki, and have tried looking for the method, and yet you didn't find what you need, **that's fine**. Telegram's API From f357d00911ccc720857077e2c16c8046b6a869b7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 15:54:27 +0100 Subject: [PATCH 008/108] Assert user/channel ID is non-zero too for #392 --- telethon/tl/session.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 59794f16..c7f72c0c 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -323,12 +323,19 @@ class Session: except ValueError: continue - p_hash = getattr(p, 'access_hash', 0) - if p_hash is None: - # Some users and channels seem to be returned without - # an 'access_hash', meaning Telegram doesn't want you - # to access them. This is the reason behind ensuring - # that the 'access_hash' is non-zero. See issue #354. + if isinstance(p, (InputPeerUser, InputPeerChannel)): + if not p.access_hash: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + # Note that this checks for zero or None, see #392. + continue + else: + p_hash = p.access_hash + elif isinstance(p, InputPeerChat): + p_hash = 0 + else: continue username = getattr(e, 'username', None) or None From 7745b8e7eeb73b103b577a2c7890e456982eaab5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 19:35:24 +0100 Subject: [PATCH 009/108] Use without rowid only if supported (closes #523) --- telethon/tl/session.py | 58 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index c7f72c0c..930b6973 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -107,36 +107,34 @@ class Session: c.close() else: # Tables don't exist, create new ones - c.execute("create table version (version integer)") - c.execute("insert into version values (?)", (CURRENT_VERSION,)) - c.execute( - """create table sessions ( + self._create_table( + c, + "version (version integer primary key)" + , + """sessions ( dc_id integer primary key, server_address text, port integer, auth_key blob - ) without rowid""" - ) - c.execute( - """create table entities ( + )""" + , + """entities ( id integer primary key, hash integer not null, username text, phone integer, name text - ) without rowid""" - ) - # Save file_size along with md5_digest - # to make collisions even more unlikely. - c.execute( - """create table sent_files ( + )""" + , + """sent_files ( md5_digest blob, file_size integer, file_id integer, part_count integer, primary key(md5_digest, file_size) - ) without rowid""" + )""" ) + c.execute("insert into version values (?)", (CURRENT_VERSION,)) # Migrating from JSON -> new table and may have entities if entities: c.executemany( @@ -170,17 +168,29 @@ class Session: return [] # No entities def _upgrade_database(self, old): + c = self._conn.cursor() if old == 1: - self._conn.execute( - """create table sent_files ( - md5_digest blob, - file_size integer, - file_id integer, - part_count integer, - primary key(md5_digest, file_size) - ) without rowid""" - ) + self._create_table(c,"""sent_files ( + md5_digest blob, + file_size integer, + file_id integer, + part_count integer, + primary key(md5_digest, file_size) + )""") old = 2 + c.close() + + def _create_table(self, c, *definitions): + """ + Creates a table given its definition 'name (columns). + If the sqlite version is >= 3.8.2, it will use "without rowid". + See http://www.sqlite.org/releaselog/3_8_2.html. + """ + required = (3, 8, 2) + sqlite_v = tuple(int(x) for x in sqlite3.sqlite_version.split('.')) + extra = ' without rowid' if sqlite_v >= required else '' + for definition in definitions: + c.execute('create table {}{}'.format(definition, extra)) # Data from sessions should be kept as properties # not to fetch the database every time we need it From d81dd055e6d54c23f093a52151110b49f7552e64 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 6 Jan 2018 23:43:40 +0100 Subject: [PATCH 010/108] Remove temporary connections and use a lock again These seem to be the reason for missing some updates (#237) --- telethon/network/mtproto_sender.py | 11 ++-- telethon/telegram_bare_client.py | 88 +++++++++--------------------- 2 files changed, 31 insertions(+), 68 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 82a378ba..0e960181 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -5,6 +5,7 @@ encrypting every packet, and relies on a valid AuthKey in the used Session. import gzip import logging import struct +from threading import Lock from .. import helpers as utils from ..crypto import AES @@ -53,6 +54,9 @@ class MtProtoSender: # Requests (as msg_id: Message) sent waiting to be received self._pending_receive = {} + # Multithreading + self._send_lock = Lock() + def connect(self): """Connects to the server.""" self.connection.connect(self.session.server_address, self.session.port) @@ -71,10 +75,6 @@ class MtProtoSender: self._need_confirmation.clear() self._clear_all_pending() - def clone(self): - """Creates a copy of this MtProtoSender as a new connection.""" - return MtProtoSender(self.session, self.connection.clone()) - # region Send and receive def send(self, *requests): @@ -156,7 +156,8 @@ class MtProtoSender: :param message: the TLMessage to be sent. """ - self.connection.send(utils.pack_message(self.session, message)) + with self._send_lock: + self.connection.send(utils.pack_message(self.session, message)) def _decode_msg(self, body): """ diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index ab6d3bbb..429a4306 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -163,11 +163,6 @@ class TelegramBareClient: self._spawn_read_thread = spawn_read_thread self._recv_thread = None - # Identifier of the main thread (the one that called .connect()). - # This will be used to create new connections from any other thread, - # so that requests can be sent in parallel. - self._main_thread_ident = None - # Default PingRequest delay self._last_ping = datetime.now() self._ping_delay = timedelta(minutes=1) @@ -198,7 +193,6 @@ class TelegramBareClient: __log__.info('Connecting to %s:%d...', self.session.server_address, self.session.port) - self._main_thread_ident = threading.get_ident() self._background_error = None # Clear previous errors try: @@ -431,6 +425,9 @@ class TelegramBareClient: x.content_related for x in requests): raise TypeError('You can only invoke requests, not types!') + if self._background_error: + raise self._background_error + # For logging purposes if len(requests) == 1: which = type(requests[0]).__name__ @@ -439,66 +436,31 @@ class TelegramBareClient: len(requests), [type(x).__name__ for x in requests]) # Determine the sender to be used (main or a new connection) - on_main_thread = threading.get_ident() == self._main_thread_ident - if on_main_thread or self._on_read_thread(): - __log__.debug('Invoking %s from main thread', which) - sender = self._sender - update_state = self.updates - else: - __log__.debug('Invoking %s from background thread. ' - 'Creating temporary connection', which) + __log__.debug('Invoking %s', which) - sender = self._sender.clone() - sender.connect() - # We're on another connection, Telegram will resend all the - # updates that we haven't acknowledged (potentially entering - # an infinite loop if we're calling this in response to an - # update event, as it would be received again and again). So - # to avoid this we will simply not process updates on these - # new temporary connections, as they will be sent and later - # acknowledged over the main connection. - update_state = None + call_receive = self._recv_thread is None or self._reconnect_lock.locked() + for retry in range(retries): + result = self._invoke(call_receive, *requests) + if result is not None: + return result - # We should call receive from this thread if there's no background - # thread reading or if the server disconnected us and we're trying - # to reconnect. This is because the read thread may either be - # locked also trying to reconnect or we may be said thread already. - call_receive = not on_main_thread or self._recv_thread is None \ - or self._reconnect_lock.locked() - try: - for attempt in range(retries): - if self._background_error and on_main_thread: - raise self._background_error + __log__.warning('Invoking %s failed %d times, ' + 'reconnecting and retrying', + [str(x) for x in requests], retry + 1) + sleep(1) + # The ReadThread has priority when attempting reconnection, + # since this thread is constantly running while __call__ is + # only done sometimes. Here try connecting only once/retry. + if not self._reconnect_lock.locked(): + with self._reconnect_lock: + self._reconnect() - result = self._invoke( - sender, call_receive, update_state, *requests - ) - if result is not None: - return result - - __log__.warning('Invoking %s failed %d times, ' - 'reconnecting and retrying', - [str(x) for x in requests], attempt + 1) - sleep(1) - # The ReadThread has priority when attempting reconnection, - # since this thread is constantly running while __call__ is - # only done sometimes. Here try connecting only once/retry. - if sender == self._sender: - if not self._reconnect_lock.locked(): - with self._reconnect_lock: - self._reconnect() - else: - sender.connect() - - raise RuntimeError('Number of retries reached 0.') - finally: - if sender != self._sender: - sender.disconnect() # Close temporary connections + raise RuntimeError('Number of retries reached 0.') # Let people use client.invoke(SomeRequest()) instead client(...) invoke = __call__ - def _invoke(self, sender, call_receive, update_state, *requests): + def _invoke(self, call_receive, *requests): try: # Ensure that we start with no previous errors (i.e. resending) for x in requests: @@ -523,7 +485,7 @@ class TelegramBareClient: self._wrap_init_connection(GetConfigRequest()) ) - sender.send(*requests) + self._sender.send(*requests) if not call_receive: # TODO This will be slightly troublesome if we allow @@ -532,11 +494,11 @@ class TelegramBareClient: # in which case a Lock would be required for .receive(). for x in requests: x.confirm_received.wait( - sender.connection.get_timeout() + self._sender.connection.get_timeout() ) else: while not all(x.confirm_received.is_set() for x in requests): - sender.receive(update_state=update_state) + self._sender.receive(update_state=self.updates) except BrokenAuthKeyError: __log__.error('Authorization key seems broken and was invalid!') @@ -578,7 +540,7 @@ class TelegramBareClient: # be on the very first connection (not authorized, not running), # but may be an issue for people who actually travel? self._reconnect(new_dc=e.new_dc) - return self._invoke(sender, call_receive, update_state, *requests) + return self._invoke(call_receive, *requests) except ServerError as e: # Telegram is having some issues, just retry From 34fe1500962349ef005376f867a2bb045ee43448 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 7 Jan 2018 00:38:30 +0100 Subject: [PATCH 011/108] Save only one auth_key on the database again --- telethon/tl/session.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 930b6973..549bbb29 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -231,6 +231,12 @@ class Session: def _update_session_table(self): with self._db_lock: c = self._conn.cursor() + # While we can save multiple rows into the sessions table + # currently we only want to keep ONE as the tables don't + # tell us which auth_key's are usable and will work. Needs + # some more work before being able to save auth_key's for + # multiple DCs. Probably done differently. + c.execute('delete from sessions') c.execute('insert or replace into sessions values (?,?,?,?)', ( self._dc_id, self._server_address, From 59a1a6aef22c67947489266bdf56609caf8196a7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 7 Jan 2018 16:18:54 +0100 Subject: [PATCH 012/108] Stop working with bytes on the markdown parser --- telethon/extensions/markdown.py | 77 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 6285bf28..10327c46 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -4,6 +4,7 @@ for use within the library, which attempts to handle emojies correctly, since they seem to count as two characters and it's a bit strange. """ import re +import struct from ..tl import TLObject @@ -20,15 +21,24 @@ DEFAULT_DELIMITERS = { '```': MessageEntityPre } -# Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)', -# reason why there's '\0' after every match-literal character. -DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0') +# Regex used to match r'\[(.+?)\]\((.+?)\)' (for URLs. +DEFAULT_URL_RE = re.compile(r'\[(.+?)\]\((.+?)\)') # Reverse operation for DEFAULT_URL_RE. {0} for text, {1} for URL. DEFAULT_URL_FORMAT = '[{0}]({1})' -# Encoding to be used -ENC = 'utf-16le' + +def _add_surrogate(text): + return ''.join( + # SMP -> Surrogate Pairs (Telegram offsets are calculated with these). + # See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more. + ''.join(chr(y) for y in struct.unpack(' Date: Mon, 8 Jan 2018 12:01:38 +0100 Subject: [PATCH 013/108] Move utils.calc_msg_key into auth_key (cyclic imports py3.4) --- telethon/crypto/auth_key.py | 5 +++-- telethon/helpers.py | 5 ----- telethon_tests/crypto_test.py | 7 ------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/telethon/crypto/auth_key.py b/telethon/crypto/auth_key.py index 679e62ff..a6c0675b 100644 --- a/telethon/crypto/auth_key.py +++ b/telethon/crypto/auth_key.py @@ -4,7 +4,6 @@ This module holds the AuthKey class. import struct from hashlib import sha1 -from .. import helpers as utils from ..extensions import BinaryReader @@ -36,4 +35,6 @@ class AuthKey: """ new_nonce = new_nonce.to_bytes(32, 'little', signed=True) data = new_nonce + struct.pack(' Date: Mon, 8 Jan 2018 12:14:03 +0100 Subject: [PATCH 014/108] Avoid more cyclic imports on the session file --- telethon/tl/session.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 549bbb29..636d512d 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -2,12 +2,13 @@ import json import os import platform import sqlite3 +import struct import time from base64 import b64decode from os.path import isfile as file_exists from threading import Lock -from .. import utils, helpers +from .. import utils from ..tl import TLObject from ..tl.types import ( PeerUser, PeerChat, PeerChannel, @@ -62,7 +63,7 @@ class Session: self.save_entities = True self.flood_sleep_threshold = 60 - self.id = helpers.generate_random_long(signed=True) + self.id = struct.unpack('q', os.urandom(8))[0] self._sequence = 0 self.time_offset = 0 self._last_msg_id = 0 # Long From 46b088d44c14e856c045e2330a7da5b95881afd6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 8 Jan 2018 12:26:32 +0100 Subject: [PATCH 015/108] Also handle ECONNREFUSED on .connect() (report on #392) --- telethon/extensions/tcp_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index e67c032c..d01c2b13 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -77,7 +77,8 @@ class TcpClient: except OSError as e: # There are some errors that we know how to handle, and # the loop will allow us to retry - if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL): + if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, + errno.ECONNREFUSED): # Bad file descriptor, i.e. socket was closed, set it # to none to recreate it on the next iteration self._socket = None From 0c3216cb366dfcb88ab09df7ec9f1bbce7a0e0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Vlahovi=C4=87?= Date: Mon, 8 Jan 2018 12:46:47 +0100 Subject: [PATCH 016/108] Fix channel check issue on send_read_acknowledge (#526) --- 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 6ec8fd02..3bb0f997 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -662,7 +662,7 @@ class TelegramClient(TelegramBareClient): max_id = message.id entity = self.get_input_entity(entity) - if entity == InputPeerChannel: + if isinstance(entity, InputPeerChannel): return self(channels.ReadHistoryRequest(entity, max_id=max_id)) else: return self(messages.ReadHistoryRequest(entity, max_id=max_id)) From c12af5e41296ae84349f6b2c1df3b2feb5ee762f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 8 Jan 2018 14:04:04 +0100 Subject: [PATCH 017/108] Remove references to the wiki --- readthedocs/extra/examples/chats-and-channels.rst | 3 +-- readthedocs/extra/wall-of-shame.rst | 9 ++++----- telethon/telegram_bare_client.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 1bafec80..11e1c624 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -70,7 +70,7 @@ Checking a link without joining If you don't need to join but rather check whether it's a group or a channel, you can use the `CheckChatInviteRequest`__, which takes in -the `hash`__ of said channel or group. +the hash of said channel or group. __ https://lonamiwebs.github.io/Telethon/constructors/chat.html __ https://lonamiwebs.github.io/Telethon/constructors/channel.html @@ -80,7 +80,6 @@ __ 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/messages/check_chat_invite.html -__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel Retrieving all chat members (channels too) diff --git a/readthedocs/extra/wall-of-shame.rst b/readthedocs/extra/wall-of-shame.rst index dfede312..4f7b5660 100644 --- a/readthedocs/extra/wall-of-shame.rst +++ b/readthedocs/extra/wall-of-shame.rst @@ -9,10 +9,9 @@ you to file **issues** whenever you encounter any when working with the library. Said section is **not** for issues on *your* program but rather issues with Telethon itself. -If you have not made the effort to 1. `read through the -wiki `__ and 2. `look for -the method you need `__, you -will end up on the `Wall of +If you have not made the effort to 1. read through the docs and 2. +`look for the method you need `__, +you will end up on the `Wall of Shame `__, i.e. all issues labeled `"RTFM" `__: @@ -31,7 +30,7 @@ i.e. all issues labeled *by Bill M. July 27, 2004* -If you have indeed read the wiki, and have tried looking for the method, +If you have indeed read the docs, and have tried looking for the method, and yet you didn't find what you need, **that's fine**. Telegram's API can have some obscure names at times, and for this reason, there is a `"question" diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 429a4306..498b749e 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -87,7 +87,7 @@ class TelegramBareClient: if not api_id or not api_hash: raise ValueError( "Your API ID or Hash cannot be empty or None. " - "Refer to Telethon's wiki for more information.") + "Refer to telethon.rtfd.io for more information.") self._use_ipv6 = use_ipv6 From 01820c9943cba09637aecb0e1d9928cda1503b45 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 8 Jan 2018 14:18:36 +0100 Subject: [PATCH 018/108] Associate phone code hash with phone (so phone can change) --- telethon/telegram_client.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 3bb0f997..5d315ad7 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -142,8 +142,9 @@ class TelegramClient(TelegramBareClient): **kwargs ) - # Some fields to easy signing in - self._phone_code_hash = None + # Some fields to easy signing in. Let {phone: hash} be + # a dictionary because the user may change their mind. + self._phone_code_hash = {} self._phone = None # endregion @@ -167,18 +168,19 @@ class TelegramClient(TelegramBareClient): Information about the result of the request. """ phone = utils.parse_phone(phone) or self._phone + phone_hash = self._phone_code_hash.get(phone) - if not self._phone_code_hash: + if not phone_hash: result = self(SendCodeRequest(phone, self.api_id, self.api_hash)) - self._phone_code_hash = result.phone_code_hash + self._phone_code_hash[phone] = phone_hash = result.phone_code_hash else: force_sms = True self._phone = phone if force_sms: - result = self(ResendCodeRequest(phone, self._phone_code_hash)) - self._phone_code_hash = result.phone_code_hash + result = self(ResendCodeRequest(phone, phone_hash)) + self._phone_code_hash[phone] = result.phone_code_hash return result @@ -218,7 +220,9 @@ class TelegramClient(TelegramBareClient): return self.send_code_request(phone) elif code: phone = utils.parse_phone(phone) or self._phone - phone_code_hash = phone_code_hash or self._phone_code_hash + phone_code_hash = \ + phone_code_hash or self._phone_code_hash.get(phone, None) + if not phone: raise ValueError( 'Please make sure to call send_code_request first.' @@ -274,7 +278,7 @@ class TelegramClient(TelegramBareClient): """ result = self(SignUpRequest( phone_number=self._phone, - phone_code_hash=self._phone_code_hash, + phone_code_hash=self._phone_code_hash.get(self._phone, ''), phone_code=code, first_name=first_name, last_name=last_name From 146a91f83744004e772ddf00c201d6b83d26b341 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 9 Jan 2018 18:04:51 +0100 Subject: [PATCH 019/108] Add a brief description for newcomers --- README.rst | 10 ++++++++++ readthedocs/index.rst | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/README.rst b/README.rst index f524384e..25165b5c 100755 --- a/README.rst +++ b/README.rst @@ -7,6 +7,16 @@ Telethon **Telethon** is Telegram client implementation in **Python 3** which uses the latest available API of Telegram. Remember to use **pip3** to install! + +What is this? +------------- + +Telegram is a popular messaging application. This library is meant +to make it easy for you to write Python programs that can interact +with Telegram. Think of it as a wrapper that has already done the +heavy job for you, so you can focus on developing an application. + + Installing ---------- diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 161c4b1a..cae75541 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -14,6 +14,15 @@ Please follow the links below to get you started, and remember to read the :ref:`changelog` when you upgrade! +What is this? +************* + +Telegram is a popular messaging application. This library is meant +to make it easy for you to write Python programs that can interact +with Telegram. Think of it as a wrapper that has already done the +heavy job for you, so you can focus on developing an application. + + .. _installation-and-usage: .. toctree:: From 045f7f5643539b21f6f46ee1ce2daee76f74a954 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 10 Jan 2018 10:46:43 +0100 Subject: [PATCH 020/108] Assert hash is not None when migrating from JSON sessions --- telethon/tl/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 636d512d..34427314 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -163,7 +163,8 @@ class Session: rows = [] for p_id, p_hash in data.get('entities', []): - rows.append((p_id, p_hash, None, None, None)) + if p_hash is not None: + rows.append((p_id, p_hash, None, None, None)) return rows except UnicodeDecodeError: return [] # No entities From 8038971753cee1adb4a89ccdda112075286ff54a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 10 Jan 2018 12:50:49 +0100 Subject: [PATCH 021/108] Add clear_mentions parameter to .send_read_acknowledge() --- telethon/telegram_client.py | 40 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5d315ad7..031ff7fb 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -32,7 +32,7 @@ from .tl.functions.contacts import ( from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, - CheckChatInviteRequest + CheckChatInviteRequest, ReadMentionsRequest ) from .tl.functions import channels @@ -639,7 +639,8 @@ class TelegramClient(TelegramBareClient): return messages - def send_read_acknowledge(self, entity, message=None, max_id=None): + def send_read_acknowledge(self, entity, message=None, max_id=None, + clear_mentions=False): """ Sends a "read acknowledge" (i.e., notifying the given peer that we've read their messages, also known as the "double check"). @@ -654,22 +655,37 @@ class TelegramClient(TelegramBareClient): max_id (:obj:`int`): Overrides messages, until which message should the acknowledge should be sent. + + clear_mentions (:obj:`bool`): + Whether the mention badge should be cleared (so that + there are no more mentions) or not for the given entity. + + If no message is provided, this will be the only action + taken. """ if max_id is None: - if not messages: + if message: + if hasattr(message, '__iter__'): + max_id = max(msg.id for msg in message) + else: + max_id = message.id + elif not clear_mentions: raise ValueError( 'Either a message list or a max_id must be provided.') - if hasattr(message, '__iter__'): - max_id = max(msg.id for msg in message) - else: - max_id = message.id - entity = self.get_input_entity(entity) - if isinstance(entity, InputPeerChannel): - return self(channels.ReadHistoryRequest(entity, max_id=max_id)) - else: - return self(messages.ReadHistoryRequest(entity, max_id=max_id)) + if clear_mentions: + self(ReadMentionsRequest(entity)) + if max_id is None: + return True + + if max_id is not None: + if isinstance(entity, InputPeerChannel): + return self(channels.ReadHistoryRequest(entity, max_id=max_id)) + else: + return self(messages.ReadHistoryRequest(entity, max_id=max_id)) + + return False @staticmethod def _get_reply_to(reply_to): From eaef392a9b58aa658bd4e9f46c559f53be181705 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 10 Jan 2018 17:34:34 +0100 Subject: [PATCH 022/108] Add and except missing FLOOD_TEST_PHONE_WAIT_X error --- telethon/telegram_bare_client.py | 7 ++++--- telethon_generator/error_descriptions | 1 + telethon_generator/error_generator.py | 5 ++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 498b749e..14e02acf 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -13,8 +13,9 @@ from . import helpers as utils, version from .crypto import rsa, CdnDecrypter from .errors import ( RPCError, BrokenAuthKeyError, ServerError, - FloodWaitError, FileMigrateError, TypeNotFoundError, - UnauthorizedError, PhoneMigrateError, NetworkMigrateError, UserMigrateError + FloodWaitError, FloodTestPhoneWaitError, FileMigrateError, + TypeNotFoundError, UnauthorizedError, PhoneMigrateError, + NetworkMigrateError, UserMigrateError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .tl import TLObject, Session @@ -546,7 +547,7 @@ class TelegramBareClient: # Telegram is having some issues, just retry __log__.error('Telegram servers are having internal errors %s', e) - except FloodWaitError as e: + except (FloodWaitError, FloodTestPhoneWaitError) as e: __log__.warning('Request invoked too often, wait %ds', e.seconds) if e.seconds > self.session.flood_sleep_threshold | 0: raise diff --git a/telethon_generator/error_descriptions b/telethon_generator/error_descriptions index 65894ba1..2754ce5e 100644 --- a/telethon_generator/error_descriptions +++ b/telethon_generator/error_descriptions @@ -63,3 +63,4 @@ SESSION_REVOKED=The authorization has been invalidated, because of the user term USER_ALREADY_PARTICIPANT=The authenticated user is already a participant of the chat USER_DEACTIVATED=The user has been deleted/deactivated FLOOD_WAIT_X=A wait of {} seconds is required +FLOOD_TEST_PHONE_WAIT_X=A wait of {} seconds is required in the test servers diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index 30163dfc..5b14f22e 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -79,7 +79,9 @@ def generate_code(output, json_file, errors_desc): errors = defaultdict(set) # PWRTelegram's API doesn't return all errors, which we do need here. # Add some special known-cases manually first. - errors[420].add('FLOOD_WAIT_X') + errors[420].update(( + 'FLOOD_WAIT_X', 'FLOOD_TEST_PHONE_WAIT_X' + )) errors[401].update(( 'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED' )) @@ -118,6 +120,7 @@ def generate_code(output, json_file, errors_desc): # Names for the captures, or 'x' if unknown capture_names = { 'FloodWaitError': 'seconds', + 'FloodTestPhoneWaitError': 'seconds', 'FileMigrateError': 'new_dc', 'NetworkMigrateError': 'new_dc', 'PhoneMigrateError': 'new_dc', From 80f81fe69a0709378da99702853c37d8b6706799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Thu, 11 Jan 2018 12:43:47 +0100 Subject: [PATCH 023/108] Added .start() convenience method to quickly connect/authorize (#528) --- README.rst | 6 +- readthedocs/extra/basic/creating-a-client.rst | 19 +++ telethon/telegram_client.py | 125 +++++++++++++++--- 3 files changed, 129 insertions(+), 21 deletions(-) diff --git a/README.rst b/README.rst index 25165b5c..6d9f2c39 100755 --- a/README.rst +++ b/README.rst @@ -39,11 +39,7 @@ Creating a client phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) - client.connect() - - # If you already have a previous 'session_name.session' file, skip this. - client.sign_in(phone=phone) - me = client.sign_in(code=77777) # Put whatever code you received here. + client.start() Doing stuff diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 81e19c83..dd468abc 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -76,6 +76,22 @@ As a full example: me = client.sign_in(phone_number, input('Enter code: ')) +All of this, however, can be done through a call to ``.start()``: + + .. code-block:: python + + client = TelegramClient('anon', api_id, api_hash) + client.start() + + +The code shown is just what ``.start()`` will be doing behind the scenes +(with a few extra checks), so that you know how to sign in case you want +to avoid using ``input()`` (the default) for whatever reason. + +You can use either, as both will work. Determining which +is just a matter of taste, and how much control you need. + + .. note:: If you want to use a **proxy**, you have to `install PySocks`__ (via pip or manual) and then set the appropriated parameters: @@ -113,6 +129,9 @@ account, calling :meth:`telethon.TelegramClient.sign_in` will raise a client.sign_in(password=getpass.getpass()) +The mentioned ``.start()`` method will handle this for you as well, but +you must set the ``password=`` parameter beforehand (it won't be asked). + If you don't have 2FA enabled, but you would like to do so through the library, take as example the following code snippet: diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 031ff7fb..9134feef 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,5 +1,6 @@ import itertools import os +import sys import time from collections import OrderedDict, UserList from datetime import datetime, timedelta @@ -14,8 +15,8 @@ from . import TelegramBareClient from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, - PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError -) + PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, + SessionPasswordNeededError) from .network import ConnectionMode from .tl import TLObject from .tl.custom import Draft, Dialog @@ -184,6 +185,104 @@ class TelegramClient(TelegramBareClient): return result + def start(self, phone=None, password=None, bot_token=None, + force_sms=False, code_callback=None): + """ + Convenience method to interactively connect and sign in if required, + also taking into consideration that 2FA may be enabled in the account. + + Example usage: + >>> client = TelegramClient(session, api_id, api_hash).start(phone) + Please enter the code you received: 12345 + Please enter your password: ******* + (You are now logged in) + + Args: + phone (:obj:`str` | :obj:`int`): + The phone to which the code will be sent. + + password (:obj:`callable`, optional): + The password for 2 Factor Authentication (2FA). + 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). + + force_sms (:obj:`bool`, optional): + Whether to force sending the code request as SMS. + This only makes sense when signing in with a `phone`. + + code_callback (:obj:`callable`, optional): + A callable that will be used to retrieve the Telegram + login code. Defaults to `input()`. + + Returns: + :obj:`TelegramClient`: + This client, so initialization can be chained with `.start()`. + """ + + if code_callback is None: + def code_callback(): + return input('Please enter the code you received: ') + elif not callable(code_callback): + raise ValueError( + 'The code_callback parameter needs to be a callable ' + 'function that returns the code you received by Telegram.' + ) + + if (phone and bot_token) or (not phone and not bot_token): + raise ValueError( + 'You must provide either a phone number or a bot token, ' + 'not both (or neither).' + ) + + if not self.is_connected(): + self.connect() + + if self.is_user_authorized(): + return self + + if bot_token: + self.sign_in(bot_token=bot_token) + return self + + me = None + attempts = 0 + max_attempts = 3 + two_step_detected = False + + self.send_code_request(phone, force_sms=force_sms) + while attempts < max_attempts: + try: + # Raises SessionPasswordNeededError if 2FA enabled + me = self.sign_in(phone, code_callback()) + break + except SessionPasswordNeededError: + two_step_detected = True + break + except (PhoneCodeEmptyError, PhoneCodeExpiredError, + PhoneCodeHashEmptyError, PhoneCodeInvalidError): + print('Invalid code. Please try again.', file=sys.stderr) + attempts += 1 + else: + raise RuntimeError( + '{} consecutive sign-in attempts failed. Aborting' + .format(max_attempts) + ) + + if two_step_detected: + if not password: + raise ValueError( + "Two-step verification is enabled for this account. " + "Please provide the 'password' argument to 'start()'." + ) + me = self.sign_in(phone=phone, password=password) + + # We won't reach here if any step failed (exit by exception) + print('Signed in successfully as', utils.get_display_name(me)) + return self + def sign_in(self, phone=None, code=None, password=None, bot_token=None, phone_code_hash=None): """ @@ -216,7 +315,7 @@ class TelegramClient(TelegramBareClient): :meth:`.send_code_request()`. """ - if phone and not code: + if phone and not code and not password: return self.send_code_request(phone) elif code: phone = utils.parse_phone(phone) or self._phone @@ -230,15 +329,9 @@ class TelegramClient(TelegramBareClient): if not phone_code_hash: raise ValueError('You also need to provide a phone_code_hash.') - try: - if isinstance(code, int): - code = str(code) - - result = self(SignInRequest(phone, phone_code_hash, code)) - - except (PhoneCodeEmptyError, PhoneCodeExpiredError, - PhoneCodeHashEmptyError, PhoneCodeInvalidError): - return None + # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, + # PhoneCodeHashEmptyError or PhoneCodeInvalidError. + result = self(SignInRequest(phone, phone_code_hash, str(code))) elif password: salt = self(GetPasswordRequest()).current_salt result = self(CheckPasswordRequest( @@ -310,7 +403,7 @@ class TelegramClient(TelegramBareClient): or None if the request fails (hence, not authenticated). Returns: - Your own user. + :obj:`User`: Your own user. """ try: return self(GetUsersRequest([InputUserSelf()]))[0] @@ -779,14 +872,14 @@ class TelegramClient(TelegramBareClient): mime_type = guess_type(file)[0] attr_dict = { DocumentAttributeFilename: - DocumentAttributeFilename(os.path.basename(file)) + DocumentAttributeFilename(os.path.basename(file)) # TODO If the input file is an audio, find out: # Performer and song title and add DocumentAttributeAudio } else: attr_dict = { DocumentAttributeFilename: - DocumentAttributeFilename('unnamed') + DocumentAttributeFilename('unnamed') } if 'is_voice_note' in kwargs: @@ -1305,4 +1398,4 @@ class TelegramClient(TelegramBareClient): 'Make sure you have encountered this peer before.'.format(peer) ) - # endregion + # endregion From 4f441219b164e6f5dee5e12f356869c40c8ba7ef Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 11 Jan 2018 12:45:59 +0100 Subject: [PATCH 024/108] Fix not all docs using new start method --- readthedocs/extra/basic/getting-started.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 88a6247c..129d752d 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -30,11 +30,7 @@ Creating a client phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) - client.connect() - - # If you already have a previous 'session_name.session' file, skip this. - client.sign_in(phone=phone) - me = client.sign_in(code=77777) # Put whatever code you received here. + client.start() **More details**: :ref:`creating-a-client` From 77ef659cbf988047621bd2bc69ea58fdf18f7393 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 11 Jan 2018 15:41:57 +0100 Subject: [PATCH 025/108] Clearer error when invoking without calling .connect() (#532) --- telethon/telegram_bare_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 14e02acf..8adf2567 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -508,14 +508,14 @@ class TelegramBareClient: except TimeoutError: __log__.warning('Invoking timed out') # We will just retry - except ConnectionResetError: + except ConnectionResetError as e: __log__.warning('Connection was reset while invoking') if self._user_connected: # Server disconnected us, __call__ will try reconnecting. return None else: # User never called .connect(), so raise this error. - raise + raise RuntimeError('Tried to invoke without .connect()') from e # Clear the flag if we got this far self._first_request = False From 1fd20ace2c820cb20aa52c546c8eec8979e97d66 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 11 Jan 2018 22:18:58 +0100 Subject: [PATCH 026/108] Update to v0.16.1 --- readthedocs/extra/changelog.rst | 41 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 569f21ca..9457c4f4 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,47 @@ it can take advantage of new goodies! .. contents:: List of All Versions +MtProto 2.0 (v0.16.1) +===================== + +*Published at 2018/01/11* + ++-----------------------+ +| Scheme layer used: 74 | ++-----------------------+ + +The library is now using MtProto 2.0! This shouldn't really affect you +as an end user, but at least it means the library will be ready by the +time MtProto 1.0 is deprecated. + +Additions +~~~~~~~~~ + +- New ``.start()`` method, to make the library avoid boilerplate code. +- ``.send_file`` accepts a new optional ``thumbnail`` parameter, and + returns the ``Message`` with the sent file. + + +Bug fixes +~~~~~~~~~ + +- The library uses again only a single connection. Less updates are + be dropped now, and the performance is even better than using temporary + connections. +- ``without rowid`` will only be used on the ``*.session`` if supported. +- Phone code hash is associated with phone, so you can change your mind + when calling ``.sign_in()``. + + +Internal changes +~~~~~~~~~~~~~~~~ + +- File cache now relies on the hash of the file uploaded instead its path, + and is now persistent in the ``*.session`` file. Report any bugs on this! +- Clearer error when invoking without being connected. +- Markdown parser doesn't work on bytes anymore (which makes it cleaner). + + Sessions as sqlite databases (v0.16) ==================================== diff --git a/telethon/version.py b/telethon/version.py index e7fcc442..e0220c01 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.16' +__version__ = '0.16.1' From 6cb8f2e3da3e330c8461eb9672f296a1e1cf71ec Mon Sep 17 00:00:00 2001 From: Noah Overcash Date: Fri, 12 Jan 2018 04:08:40 -0500 Subject: [PATCH 027/108] Update pip references with pip3 (#527) --- README.rst | 2 +- readthedocs/extra/basic/getting-started.rst | 2 +- readthedocs/extra/basic/installation.rst | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 6d9f2c39..09ddaf90 100755 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Installing .. code:: sh - pip install telethon + pip3 install telethon Creating a client diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 129d752d..912ea768 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -11,7 +11,7 @@ Getting Started Simple Installation ******************* - ``pip install telethon`` + ``pip3 install telethon`` **More details**: :ref:`installation` diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index b4fb1ac2..945576d0 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -10,21 +10,20 @@ Automatic Installation To install Telethon, simply do: - ``pip install telethon`` + ``pip3 install telethon`` -If you get something like ``"SyntaxError: invalid syntax"`` or any other -error while installing/importing the library, it's probably because ``pip`` -defaults to Python 2, which is not supported. Use ``pip3`` instead. +Needless to say, you must have Python 3 and PyPi installed in your system. +See https://python.org and https://pypi.python.org/pypi/pip for more. If you already have the library installed, upgrade with: - ``pip install --upgrade telethon`` + ``pip3 install --upgrade telethon`` You can also install the library directly from GitHub or a fork: .. code-block:: sh - # pip install git+https://github.com/LonamiWebs/Telethon.git + # pip3 install git+https://github.com/LonamiWebs/Telethon.git or $ git clone https://github.com/LonamiWebs/Telethon.git $ cd Telethon/ @@ -39,7 +38,7 @@ Manual Installation 1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and ``rsa`` (`GitHub`__ | `PyPi`__) modules: - ``sudo -H pip install pyaes rsa`` + ``sudo -H pip3 install pyaes rsa`` 2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git`` From ef3ea11e38dd6c302543db636c0703d5dd10027e Mon Sep 17 00:00:00 2001 From: Lonami Date: Fri, 12 Jan 2018 18:21:02 +0100 Subject: [PATCH 028/108] Remove pesky minus character hiding example --- readthedocs/extra/examples/chats-and-channels.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 11e1c624..99ce235f 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -41,7 +41,7 @@ enough information to join! The part after the example, is the ``hash`` of the chat or channel. Now you can use `ImportChatInviteRequest`__ as follows: - .. -block:: python + .. code-block:: python from telethon.tl.functions.messages import ImportChatInviteRequest updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) From 77301378f85ba4976c02e7e5704cdde88cd501a0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 13 Jan 2018 11:54:41 +0100 Subject: [PATCH 029/108] Make .start() more friendly by asking phone if not given Ping #530 --- readthedocs/extra/basic/creating-a-client.rst | 6 ++++-- readthedocs/extra/basic/getting-started.rst | 1 - telethon/telegram_client.py | 10 +++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index dd468abc..10ae5f60 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -31,7 +31,6 @@ one is very simple: # Use your own values here api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - phone_number = '+34600000000' client = TelegramClient('some_name', api_id, api_hash) @@ -54,6 +53,7 @@ If you're not authorized, you need to ``.sign_in()``: .. code-block:: python + phone_number = '+34600000000' client.send_code_request(phone_number) myself = client.sign_in(phone_number, input('Enter code: ')) # If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead @@ -86,7 +86,9 @@ All of this, however, can be done through a call to ``.start()``: The code shown is just what ``.start()`` will be doing behind the scenes (with a few extra checks), so that you know how to sign in case you want -to avoid using ``input()`` (the default) for whatever reason. +to avoid using ``input()`` (the default) for whatever reason. If no phone +or bot token is provided, you will be asked one through ``input()``. The +method also accepts a ``phone=`` and ``bot_token`` parameters. You can use either, as both will work. Determining which is just a matter of taste, and how much control you need. diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index 912ea768..e69cc3ef 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -27,7 +27,6 @@ Creating a client # api_hash from https://my.telegram.org, under API Development. api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) client.start() diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9134feef..98b22940 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -231,7 +231,15 @@ class TelegramClient(TelegramBareClient): 'function that returns the code you received by Telegram.' ) - if (phone and bot_token) or (not phone and not bot_token): + if not phone and not bot_token: + value = input('Please enter your phone/bot token: ') + phone = utils.parse_phone(phone) + if not phone: + bot_token = value + print("Note: input doesn't look like a phone, " + "using as bot token") + + if phone and bot_token: raise ValueError( 'You must provide either a phone number or a bot token, ' 'not both (or neither).' From 0d429f55c54ce8b89a9df6222229a51be5d3a6bf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 13 Jan 2018 12:00:53 +0100 Subject: [PATCH 030/108] Fix asking for phone on .start() --- README.rst | 1 - telethon/telegram_client.py | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 09ddaf90..6343e6e1 100755 --- a/README.rst +++ b/README.rst @@ -36,7 +36,6 @@ Creating a client # api_hash from https://my.telegram.org, under API Development. api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - phone = '+34600000000' client = TelegramClient('session_name', api_id, api_hash) client.start() diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 98b22940..674e6045 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -232,14 +232,10 @@ class TelegramClient(TelegramBareClient): ) if not phone and not bot_token: - value = input('Please enter your phone/bot token: ') - phone = utils.parse_phone(phone) - if not phone: - bot_token = value - print("Note: input doesn't look like a phone, " - "using as bot token") + while not phone: + phone = utils.parse_phone(input('Please enter your phone: ')) - if phone and bot_token: + elif phone and bot_token: raise ValueError( 'You must provide either a phone number or a bot token, ' 'not both (or neither).' From c5e969d5854bbb44d895fcf19563606aaecab07e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 13 Jan 2018 19:26:45 +0100 Subject: [PATCH 031/108] Add more useful logging on invalid packet length received --- telethon/network/connection.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/telethon/network/connection.py b/telethon/network/connection.py index ff255d00..0adaf98a 100644 --- a/telethon/network/connection.py +++ b/telethon/network/connection.py @@ -2,6 +2,7 @@ This module holds both the Connection class and the ConnectionMode enum, which specifies the protocol to be used by the Connection. """ +import logging import os import struct from datetime import timedelta @@ -14,6 +15,8 @@ from ..crypto import AESModeCTR from ..extensions import TcpClient from ..errors import InvalidChecksumError +__log__ = logging.getLogger(__name__) + class ConnectionMode(Enum): """Represents which mode should be used to stabilise a connection. @@ -181,6 +184,21 @@ class Connection: packet_len_seq = self.read(8) # 4 and 4 packet_len, seq = struct.unpack(' Date: Sun, 14 Jan 2018 10:53:29 +0100 Subject: [PATCH 032/108] Note the errors package on the RPC errors section --- readthedocs/extra/troubleshooting/rpc-errors.rst | 7 ++++--- readthedocs/telethon.errors.rst | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst index 0d36bec6..17299f1f 100644 --- a/readthedocs/extra/troubleshooting/rpc-errors.rst +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -2,10 +2,11 @@ RPC Errors ========== -RPC stands for Remote Procedure Call, and when Telethon raises an -``RPCError``, it's most likely because you have invoked some of the API +RPC stands for Remote Procedure Call, and when the library raises +a ``RPCError``, it's because you have invoked some of the API methods incorrectly (wrong parameters, wrong permissions, or even -something went wrong on Telegram's server). The most common are: +something went wrong on Telegram's server). All the errors are +available in :ref:`telethon-errors-package`, but some examples are: - ``FloodWaitError`` (420), the same request was repeated many times. Must wait ``.seconds`` (you can access this parameter). diff --git a/readthedocs/telethon.errors.rst b/readthedocs/telethon.errors.rst index 2e94fe33..e90d1819 100644 --- a/readthedocs/telethon.errors.rst +++ b/readthedocs/telethon.errors.rst @@ -1,3 +1,6 @@ +.. _telethon-errors-package: + + telethon\.errors package ======================== From 8be7e76b741db51f7160d0e8868b089aa029e77f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 14 Jan 2018 21:20:22 +0100 Subject: [PATCH 033/108] Use the idling state instead checking if read thread is present This caused some multithreading bugs, for instance, when there was no read thread and the main thread was idling, and there were some update workers. Several threads would try to read from the socket at the same time (since there's no lock for reading), causing reads to be corrupted and receiving "invalid packet lengths" from the network. Closes #538. --- telethon/telegram_bare_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 8adf2567..2c1b6188 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -163,6 +163,7 @@ class TelegramBareClient: # if the user has left enabled such option. self._spawn_read_thread = spawn_read_thread self._recv_thread = None + self._idling = threading.Event() # Default PingRequest delay self._last_ping = datetime.now() @@ -438,8 +439,9 @@ class TelegramBareClient: # Determine the sender to be used (main or a new connection) __log__.debug('Invoking %s', which) + call_receive = \ + not self._idling.is_set() or self._reconnect_lock.locked() - call_receive = self._recv_thread is None or self._reconnect_lock.locked() for retry in range(retries): result = self._invoke(call_receive, *requests) if result is not None: @@ -829,6 +831,7 @@ class TelegramBareClient: if self._spawn_read_thread and not self._on_read_thread(): raise RuntimeError('Can only idle if spawn_read_thread=False') + self._idling.set() for sig in stop_signals: signal(sig, self._signal_handler) @@ -857,7 +860,11 @@ class TelegramBareClient: with self._reconnect_lock: while self._user_connected and not self._reconnect(): sleep(0.1) # Retry forever, this is instant messaging + except: + self._idling.clear() + raise + self._idling.clear() __log__.info('Connection closed by the user, not reading anymore') # By using this approach, another thread will be From 00859d52c354f7b5ac27293a2e3b1f229004aef2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 09:48:37 +0100 Subject: [PATCH 034/108] Ask for the phone on start only if required --- telethon/telegram_client.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 674e6045..1fccda50 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -185,7 +185,9 @@ class TelegramClient(TelegramBareClient): return result - def start(self, phone=None, password=None, bot_token=None, + def start(self, + phone=lambda: input('Please enter your phone: '), + password=None, bot_token=None, force_sms=False, code_callback=None): """ Convenience method to interactively connect and sign in if required, @@ -198,8 +200,9 @@ class TelegramClient(TelegramBareClient): (You are now logged in) Args: - phone (:obj:`str` | :obj:`int`): - The phone to which the code will be sent. + phone (:obj:`str` | :obj:`int` | :obj:`callable`): + The phone (or callable without arguments to get it) + to which the code will be sent. password (:obj:`callable`, optional): The password for 2 Factor Authentication (2FA). @@ -232,14 +235,11 @@ class TelegramClient(TelegramBareClient): ) if not phone and not bot_token: - while not phone: - phone = utils.parse_phone(input('Please enter your phone: ')) + raise ValueError('No phone number or bot token provided.') - elif phone and bot_token: - raise ValueError( - 'You must provide either a phone number or a bot token, ' - 'not both (or neither).' - ) + if phone and bot_token: + raise ValueError('Both a phone and a bot token provided, ' + 'must only provide one of either') if not self.is_connected(): self.connect() @@ -251,6 +251,10 @@ class TelegramClient(TelegramBareClient): self.sign_in(bot_token=bot_token) return self + # Turn the callable into a valid phone number + while callable(phone): + phone = utils.parse_phone(phone()) or phone + me = None attempts = 0 max_attempts = 3 From 494c90af692648f9b6f8cfc69c1860a51c46b41c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 12:36:46 +0100 Subject: [PATCH 035/108] Fix uploaded files cache may have expired --- telethon/telegram_bare_client.py | 5 ++++- telethon/telegram_client.py | 27 +++++++++++++++++++++------ telethon/tl/session.py | 8 ++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 2c1b6188..d2e84ee6 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -571,6 +571,7 @@ class TelegramBareClient: file, part_size_kb=None, file_name=None, + allow_cache=True, progress_callback=None): """Uploads the specified file and returns a handle (an instance of InputFile or InputFileBig, as required) which can be later used. @@ -633,10 +634,12 @@ class TelegramBareClient: file = stream.read() hash_md5 = md5(file) tuple_ = self.session.get_file(hash_md5.digest(), file_size) - if tuple_: + if tuple_ and allow_cache: __log__.info('File was already cached, not uploading again') return InputFile(name=file_name, md5_checksum=tuple_[0], id=tuple_[2], parts=tuple_[3]) + elif tuple_ and not allow_cache: + self.session.clear_file(hash_md5.digest(), file_size) else: hash_md5 = None diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1fccda50..ac06b8eb 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -16,7 +16,7 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError) + SessionPasswordNeededError, FilePartMissingError) from .network import ConnectionMode from .tl import TLObject from .tl.custom import Draft, Dialog @@ -813,6 +813,7 @@ class TelegramClient(TelegramBareClient): reply_to=None, attributes=None, thumb=None, + allow_cache=True, **kwargs): """ Sends a file to the specified entity. @@ -849,9 +850,13 @@ class TelegramClient(TelegramBareClient): Optional attributes that override the inferred ones, like ``DocumentAttributeFilename`` and so on. - thumb (:obj:`str` | :obj:`bytes` | :obj:`file`): + thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional): Optional thumbnail (for videos). + allow_cache (:obj:`bool`, optional): + Whether to allow using the cached version stored in the + database or not. Defaults to ``True`` to avoid reuploads. + Kwargs: If "is_voice_note" in kwargs, despite its value, and the file is sent as a document, it will be sent as a voice note. @@ -868,7 +873,7 @@ class TelegramClient(TelegramBareClient): ) file_handle = self.upload_file( - file, progress_callback=progress_callback) + file, progress_callback=progress_callback, allow_cache=allow_cache) if as_photo and not force_document: media = InputMediaUploadedPhoto(file_handle, caption) @@ -926,9 +931,19 @@ class TelegramClient(TelegramBareClient): media=media, reply_to_msg_id=self._get_reply_to(reply_to) ) - result = self(request) - - return self._get_response_message(request, result) + try: + return self._get_response_message(request, self(request)) + except FilePartMissingError: + # After a while, cached files are invalidated and this + # error is raised. The file needs to be uploaded again. + if not allow_cache: + raise + return self.send_file( + entity, file, allow_cache=False, + caption=caption, force_document=force_document, + progress_callback=progress_callback, reply_to=reply_to, + attributes=attributes, thumb=thumb, **kwargs + ) def send_voice_note(self, entity, file, caption='', upload_progress=None, reply_to=None): diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 34427314..1dbf99c5 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -433,3 +433,11 @@ class Session: (md5_digest, file_size, file_id, part_count) ) self.save() + + def clear_file(self, md5_digest, file_size): + with self._db_lock: + self._conn.execute( + 'delete from sent_files where ' + 'md5_digest = ? and file_size = ?', (md5_digest, file_size) + ) + self.save() From 36e210191085fbc822d394340de2f5033a4d2c2b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 18:15:30 +0100 Subject: [PATCH 036/108] Allow sending multiple files as album (closes #455) --- telethon/telegram_client.py | 103 ++++++++++++++++++++++++++++-------- telethon/utils.py | 6 +++ 2 files changed, 87 insertions(+), 22 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ac06b8eb..f5c10e66 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -33,7 +33,8 @@ from .tl.functions.contacts import ( from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, - CheckChatInviteRequest, ReadMentionsRequest + CheckChatInviteRequest, ReadMentionsRequest, + SendMultiMediaRequest, UploadMediaRequest ) from .tl.functions import channels @@ -53,7 +54,8 @@ from .tl.types import ( InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, - ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf + ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, + InputSingleMedia, InputMediaPhoto, InputPhoto ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -512,15 +514,21 @@ class TelegramClient(TelegramBareClient): @staticmethod def _get_response_message(request, result): - """Extracts the response message known a request and Update result""" + """ + Extracts the response message known a request and Update result. + The request may also be the ID of the message to match. + """ # Telegram seems to send updateMessageID first, then updateNewMessage, # however let's not rely on that just in case. - msg_id = None - for update in result.updates: - if isinstance(update, UpdateMessageID): - if update.random_id == request.random_id: - msg_id = update.id - break + if isinstance(request, int): + msg_id = request + else: + msg_id = None + for update in result.updates: + if isinstance(update, UpdateMessageID): + if update.random_id == request.random_id: + msg_id = update.id + break for update in result.updates: if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)): @@ -861,21 +869,34 @@ class TelegramClient(TelegramBareClient): If "is_voice_note" in kwargs, despite its value, and the file is sent as a document, it will be sent as a voice note. - Returns: - The message containing the sent file. + Returns: + The message (or messages) containing the sent file. """ - as_photo = False - if isinstance(file, str): - lowercase_file = file.lower() - as_photo = any( - lowercase_file.endswith(ext) - for ext in ('.png', '.jpg', '.gif', '.jpeg') - ) + # First check if the user passed an iterable, in which case + # we may want to send as an album if all are photo files. + if hasattr(file, '__iter__'): + # Convert to tuple so we can iterate several times + file = tuple(x for x in file) + if all(utils.is_image(x) for x in file): + return self._send_album( + entity, file, caption=caption, + progress_callback=progress_callback, reply_to=reply_to, + allow_cache=allow_cache + ) + # Not all are images, so send all the files one by one + return [ + self.send_file( + entity, x, allow_cache=False, + caption=caption, force_document=force_document, + progress_callback=progress_callback, reply_to=reply_to, + attributes=attributes, thumb=thumb, **kwargs + ) for x in file + ] file_handle = self.upload_file( file, progress_callback=progress_callback, allow_cache=allow_cache) - if as_photo and not force_document: + if utils.is_image(file) and not force_document: media = InputMediaUploadedPhoto(file_handle, caption) else: mime_type = None @@ -945,14 +966,52 @@ class TelegramClient(TelegramBareClient): attributes=attributes, thumb=thumb, **kwargs ) - def send_voice_note(self, entity, file, caption='', upload_progress=None, + def send_voice_note(self, entity, file, caption='', progress_callback=None, reply_to=None): """Wrapper method around .send_file() with is_voice_note=()""" return self.send_file(entity, file, caption, - upload_progress=upload_progress, + progress_callback=progress_callback, reply_to=reply_to, is_voice_note=()) # empty tuple is enough + def _send_album(self, entity, files, caption='', + progress_callback=None, reply_to=None, + allow_cache=True): + """Specialized version of .send_file for albums""" + entity = self.get_input_entity(entity) + reply_to = self._get_reply_to(reply_to) + try: + # Need to upload the media first + media = [ + self(UploadMediaRequest(entity, InputMediaUploadedPhoto( + self.upload_file(file), + caption=caption + ))) + for file in files + ] + # Now we can construct the multi-media request + result = self(SendMultiMediaRequest( + entity, reply_to_msg_id=reply_to, multi_media=[ + InputSingleMedia(InputMediaPhoto( + InputPhoto(m.photo.id, m.photo.access_hash), + caption=caption + )) + for m in media + ] + )) + return [ + self._get_response_message(update.id, result) + for update in result.updates + if isinstance(update, UpdateMessageID) + ] + except FilePartMissingError: + if not allow_cache: + raise + return self._send_album( + entity, files, allow_cache=False, caption=caption, + progress_callback=progress_callback, reply_to=reply_to + ) + # endregion # region Downloading media requests @@ -1421,4 +1480,4 @@ class TelegramClient(TelegramBareClient): 'Make sure you have encountered this peer before.'.format(peer) ) - # endregion + # endregion diff --git a/telethon/utils.py b/telethon/utils.py index 48c867d1..b1053504 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -312,6 +312,12 @@ def get_input_media(media, user_caption=None, is_photo=False): _raise_cast_fail(media, 'InputMedia') +def is_image(file): + """Returns True if the file extension looks like an image file""" + return (isinstance(file, str) and + bool(re.search(r'\.(png|jpe?g|gif)$', file, re.IGNORECASE))) + + def parse_phone(phone): """Parses the given phone, or returns None if it's invalid""" if isinstance(phone, int): From 2ccb6063e0f3784035d1b2e064f2da212a6e5346 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 15 Jan 2018 18:46:04 +0100 Subject: [PATCH 037/108] Call gen_tl() when installing through setup.py (#530) --- setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c531d70..2682e099 100755 --- a/setup.py +++ b/setup.py @@ -45,11 +45,13 @@ GENERATOR_DIR = 'telethon/tl' IMPORT_DEPTH = 2 -def gen_tl(): +def gen_tl(force=True): from telethon_generator.tl_generator import TLGenerator from telethon_generator.error_generator import generate_code generator = TLGenerator(GENERATOR_DIR) if generator.tlobjects_exist(): + if not force: + return print('Detected previous TLObjects. Cleaning...') generator.clean_tlobjects() @@ -99,6 +101,10 @@ def main(): fetch_errors(ERRORS_JSON) else: + # Call gen_tl() if the scheme.tl file exists, e.g. install from GitHub + if os.path.isfile(SCHEME_TL): + gen_tl(force=False) + # Get the long description from the README file with open('README.rst', encoding='utf-8') as f: long_description = f.read() From 49f204c95546e7f368d5854680e9cd30adf1d7c0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 16 Jan 2018 14:01:14 +0100 Subject: [PATCH 038/108] Fix .get_input_media using None caption and missing venue type --- telethon/utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index b1053504..8549e18d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -248,15 +248,17 @@ def get_input_media(media, user_caption=None, is_photo=False): if isinstance(media, MessageMediaPhoto): return InputMediaPhoto( id=get_input_photo(media.photo), - caption=media.caption if user_caption is None else user_caption, - ttl_seconds=media.ttl_seconds + ttl_seconds=media.ttl_seconds, + caption=((media.caption if user_caption is None else user_caption) + or '') ) if isinstance(media, MessageMediaDocument): return InputMediaDocument( id=get_input_document(media.document), - caption=media.caption if user_caption is None else user_caption, - ttl_seconds=media.ttl_seconds + ttl_seconds=media.ttl_seconds, + caption=((media.caption if user_caption is None else user_caption) + or '') ) if isinstance(media, FileLocation): @@ -298,7 +300,8 @@ def get_input_media(media, user_caption=None, is_photo=False): title=media.title, address=media.address, provider=media.provider, - venue_id=media.venue_id + venue_id=media.venue_id, + venue_type='' ) if isinstance(media, ( From fde0d60f726df8f7887c9fb83f3b532effa2164d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 16 Jan 2018 18:36:50 +0100 Subject: [PATCH 039/108] Update old interactive example (#546) --- telethon/telegram_client.py | 1 - .../interactive_telegram_client.py | 65 ++++++++----------- 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f5c10e66..9e192028 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -728,7 +728,6 @@ class TelegramClient(TelegramBareClient): # Add a few extra attributes to the Message to make it friendlier. messages.total = total_messages for m in messages: - # TODO Better way to return a total without tuples? m.sender = (None if not m.from_id else entities[utils.get_peer_id(m.from_id)]) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 501d557b..d45d2ff1 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -84,9 +84,9 @@ class InteractiveTelegramClient(TelegramClient): update_workers=1 ) - # Store all the found media in memory here, - # so it can be downloaded if the user wants - self.found_media = set() + # Store {message.id: message} map here so that we can download + # media known the message ID, for every message having media. + self.found_media = {} # Calling .connect() may return False, so you need to assert it's # True before continuing. Otherwise you may want to retry as done here. @@ -204,27 +204,21 @@ class InteractiveTelegramClient(TelegramClient): # History elif msg == '!h': # First retrieve the messages and some information - total_count, messages, senders = \ - self.get_message_history(entity, limit=10) + messages = self.get_message_history(entity, limit=10) # Iterate over all (in reverse order so the latest appear # the last in the console) and print them with format: # "[hh:mm] Sender: Message" - for msg, sender in zip( - reversed(messages), reversed(senders)): - # Get the name of the sender if any - if sender: - name = getattr(sender, 'first_name', None) - if not name: - name = getattr(sender, 'title') - if not name: - name = '???' - else: - name = '???' + for msg in reversed(messages): + # Note that the .sender attribute is only there for + # convenience, the API returns it differently. But + # this shouldn't concern us. See the documentation + # for .get_message_history() for more information. + name = get_display_name(msg.sender) # Format the message content if getattr(msg, 'media', None): - self.found_media.add(msg) + self.found_media[msg.id] = msg # The media may or may not have a caption caption = getattr(msg.media, 'caption', '') content = '<{}> {}'.format( @@ -257,8 +251,7 @@ class InteractiveTelegramClient(TelegramClient): elif msg.startswith('!d '): # Slice the message to get message ID deleted_msg = self.delete_messages(entity, msg[len('!d '):]) - print('Deleted. {}'.format(deleted_msg)) - + print('Deleted {}'.format(deleted_msg)) # Download media elif msg.startswith('!dm '): @@ -275,12 +268,11 @@ class InteractiveTelegramClient(TelegramClient): 'Profile picture downloaded to {}'.format(output) ) else: - print('No profile picture found for this user.') + print('No profile picture found for this user!') # Send chat message (if any) elif msg: - self.send_message( - entity, msg, link_preview=False) + self.send_message(entity, msg, link_preview=False) def send_photo(self, path, entity): """Sends the file located at path to the desired entity as a photo""" @@ -304,23 +296,20 @@ class InteractiveTelegramClient(TelegramClient): downloads it. """ try: - # The user may have entered a non-integer string! - msg_media_id = int(media_id) + msg = self.found_media[int(media_id)] + except (ValueError, KeyError): + # ValueError when parsing, KeyError when accessing dictionary + print('Invalid media ID given or message not found!') + return - # Search the message ID - for msg in self.found_media: - if msg.id == msg_media_id: - print('Downloading media to usermedia/...') - os.makedirs('usermedia', exist_ok=True) - output = self.download_media( - msg.media, - file='usermedia/', - progress_callback=self.download_progress_callback - ) - print('Media downloaded to {}!'.format(output)) - - except ValueError: - print('Invalid media ID given!') + print('Downloading media to usermedia/...') + os.makedirs('usermedia', exist_ok=True) + output = self.download_media( + msg.media, + file='usermedia/', + progress_callback=self.download_progress_callback + ) + print('Media downloaded to {}!'.format(output)) @staticmethod def download_progress_callback(downloaded_bytes, total_bytes): From bfe9378054c30e59b37afaf5c2202f56c3f8ea3d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 17 Jan 2018 13:28:56 +0100 Subject: [PATCH 040/108] Fix .send_file failing with strings (as they are iterable) --- telethon/telegram_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9e192028..fdd96d47 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -873,7 +873,7 @@ class TelegramClient(TelegramBareClient): """ # First check if the user passed an iterable, in which case # we may want to send as an album if all are photo files. - if hasattr(file, '__iter__'): + if hasattr(file, '__iter__') and not isinstance(file, (str, bytes)): # Convert to tuple so we can iterate several times file = tuple(x for x in file) if all(utils.is_image(x) for x in file): @@ -1321,7 +1321,7 @@ class TelegramClient(TelegramBareClient): ``User``, ``Chat`` or ``Channel`` corresponding to the input entity. """ - if not isinstance(entity, str) and hasattr(entity, '__iter__'): + if hasattr(entity, '__iter__') and not isinstance(entity, str): single = False else: single = True From 428abebed8f429397c672a9d428d18b5a8051d38 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 17 Jan 2018 13:29:08 +0100 Subject: [PATCH 041/108] Fix sending albums failing on invalid cache --- 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 fdd96d47..84c9beea 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -983,7 +983,7 @@ class TelegramClient(TelegramBareClient): # Need to upload the media first media = [ self(UploadMediaRequest(entity, InputMediaUploadedPhoto( - self.upload_file(file), + self.upload_file(file, allow_cache=allow_cache), caption=caption ))) for file in files From 55efb2b104f8e1d827a7e947983938df173a77b9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 09:52:39 +0100 Subject: [PATCH 042/108] Use a different schema for file cache which actually persists Caching the inputFile values would not persist accross several days so the cache was nearly unnecessary. Saving the id/hash of the actual inputMedia sent is a much better/persistent idea. --- telethon/telegram_bare_client.py | 10 ---- telethon/tl/session.py | 84 ++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index d2e84ee6..af86c0f1 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -633,13 +633,6 @@ class TelegramBareClient: with open(file, 'rb') as stream: file = stream.read() hash_md5 = md5(file) - tuple_ = self.session.get_file(hash_md5.digest(), file_size) - if tuple_ and allow_cache: - __log__.info('File was already cached, not uploading again') - return InputFile(name=file_name, - md5_checksum=tuple_[0], id=tuple_[2], parts=tuple_[3]) - elif tuple_ and not allow_cache: - self.session.clear_file(hash_md5.digest(), file_size) else: hash_md5 = None @@ -673,9 +666,6 @@ class TelegramBareClient: if is_large: return InputFileBig(file_id, part_count, file_name) else: - self.session.cache_file( - hash_md5.digest(), file_size, file_id, part_count) - return InputFile(file_id, part_count, file_name, md5_checksum=hash_md5.hexdigest()) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 1dbf99c5..5d89a5f7 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -5,6 +5,7 @@ import sqlite3 import struct import time from base64 import b64decode +from enum import Enum from os.path import isfile as file_exists from threading import Lock @@ -12,11 +13,26 @@ from .. import utils from ..tl import TLObject from ..tl.types import ( PeerUser, PeerChat, PeerChannel, - InputPeerUser, InputPeerChat, InputPeerChannel + InputPeerUser, InputPeerChat, InputPeerChannel, + InputPhoto, InputDocument ) EXTENSION = '.session' -CURRENT_VERSION = 2 # database version +CURRENT_VERSION = 3 # database version + + +class _SentFileType(Enum): + DOCUMENT = 0 + PHOTO = 1 + + @staticmethod + def from_type(cls): + if cls == InputDocument: + return _SentFileType.DOCUMENT + elif cls == InputPhoto: + return _SentFileType.PHOTO + else: + raise ValueError('The cls must be either InputDocument/InputPhoto') class Session: @@ -130,9 +146,10 @@ class Session: """sent_files ( md5_digest blob, file_size integer, - file_id integer, - part_count integer, - primary key(md5_digest, file_size) + type integer, + id integer, + hash integer, + primary key(md5_digest, file_size, type) )""" ) c.execute("insert into version values (?)", (CURRENT_VERSION,)) @@ -171,18 +188,22 @@ class Session: def _upgrade_database(self, old): c = self._conn.cursor() - if old == 1: - self._create_table(c,"""sent_files ( - md5_digest blob, - file_size integer, - file_id integer, - part_count integer, - primary key(md5_digest, file_size) - )""") - old = 2 + # old == 1 doesn't have the old sent_files so no need to drop + if old == 2: + # Old cache from old sent_files lasts then a day anyway, drop + c.execute('drop table sent_files') + self._create_table(c, """sent_files ( + md5_digest blob, + file_size integer, + type integer, + id integer, + hash integer, + primary key(md5_digest, file_size, type) + )""") c.close() - def _create_table(self, c, *definitions): + @staticmethod + def _create_table(c, *definitions): """ Creates a table given its definition 'name (columns). If the sqlite version is >= 3.8.2, it will use "without rowid". @@ -420,24 +441,25 @@ class Session: # File processing - def get_file(self, md5_digest, file_size): - return self._conn.execute( - 'select * from sent_files ' - 'where md5_digest = ? and file_size = ?', (md5_digest, file_size) + def get_file(self, md5_digest, file_size, cls): + tuple_ = self._conn.execute( + 'select id, hash from sent_files ' + 'where md5_digest = ? and file_size = ? and type = ?', + (md5_digest, file_size, _SentFileType.from_type(cls)) ).fetchone() + if tuple_: + # Both allowed classes have (id, access_hash) as parameters + return cls(tuple_[0], tuple_[1]) + + def cache_file(self, md5_digest, file_size, instance): + if not isinstance(instance, (InputDocument, InputPhoto)): + raise TypeError('Cannot cache %s instance' % type(instance)) - def cache_file(self, md5_digest, file_size, file_id, part_count): with self._db_lock: self._conn.execute( - 'insert into sent_files values (?,?,?,?)', - (md5_digest, file_size, file_id, part_count) - ) - self.save() - - def clear_file(self, md5_digest, file_size): - with self._db_lock: - self._conn.execute( - 'delete from sent_files where ' - 'md5_digest = ? and file_size = ?', (md5_digest, file_size) - ) + 'insert into sent_files values (?,?,?,?,?)', ( + md5_digest, file_size, + _SentFileType.from_type(type(instance)), + instance.id, instance.access_hash + )) self.save() From 1a3feec481f33035f356971ef1f8a7f7cc9d0d48 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 13:55:03 +0100 Subject: [PATCH 043/108] Move upload/download file methods to the TelegramClient --- telethon/telegram_bare_client.py | 217 +-------------------------- telethon/telegram_client.py | 248 ++++++++++++++++++++++++++++++- 2 files changed, 251 insertions(+), 214 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index af86c0f1..9684a034 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -3,19 +3,16 @@ import os import threading import warnings from datetime import timedelta, datetime -from hashlib import md5 -from io import BytesIO from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock from time import sleep -from . import helpers as utils, version -from .crypto import rsa, CdnDecrypter +from . import version +from .crypto import rsa from .errors import ( - RPCError, BrokenAuthKeyError, ServerError, - FloodWaitError, FloodTestPhoneWaitError, FileMigrateError, - TypeNotFoundError, UnauthorizedError, PhoneMigrateError, - NetworkMigrateError, UserMigrateError + RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, + FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError, + PhoneMigrateError, NetworkMigrateError, UserMigrateError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .tl import TLObject, Session @@ -30,15 +27,8 @@ from .tl.functions.help import ( GetCdnConfigRequest, GetConfigRequest ) from .tl.functions.updates import GetStateRequest -from .tl.functions.upload import ( - GetFileRequest, SaveBigFilePartRequest, SaveFilePartRequest -) -from .tl.types import InputFile, InputFileBig from .tl.types.auth import ExportedAuthorization -from .tl.types.upload import FileCdnRedirect from .update_state import UpdateState -from .utils import get_appropriated_part_size - DEFAULT_DC_ID = 4 DEFAULT_IPV4_IP = '149.154.167.51' @@ -565,203 +555,6 @@ class TelegramBareClient: # endregion - # region Uploading media - - def upload_file(self, - file, - part_size_kb=None, - file_name=None, - allow_cache=True, - progress_callback=None): - """Uploads the specified file and returns a handle (an instance - of InputFile or InputFileBig, as required) which can be later used. - - Uploading a file will simply return a "handle" to the file stored - remotely in the Telegram servers, which can be later used on. This - will NOT upload the file to your own chat. - - 'file' may be either a file path, a byte array, or a stream. - Note that if the file is a stream it will need to be read - entirely into memory to tell its size first. - - If 'progress_callback' is not None, it should be a function that - takes two parameters, (bytes_uploaded, total_bytes). - - Default values for the optional parameters if left as None are: - part_size_kb = get_appropriated_part_size(file_size) - file_name = os.path.basename(file_path) - """ - if isinstance(file, (InputFile, InputFileBig)): - return file # Already uploaded - - if isinstance(file, str): - file_size = os.path.getsize(file) - elif isinstance(file, bytes): - file_size = len(file) - else: - file = file.read() - file_size = len(file) - - # File will now either be a string or bytes - if not part_size_kb: - part_size_kb = get_appropriated_part_size(file_size) - - if part_size_kb > 512: - raise ValueError('The part size must be less or equal to 512KB') - - part_size = int(part_size_kb * 1024) - if part_size % 1024 != 0: - raise ValueError('The part size must be evenly divisible by 1024') - - # Set a default file name if None was specified - file_id = utils.generate_random_long() - if not file_name: - if isinstance(file, str): - file_name = os.path.basename(file) - else: - file_name = str(file_id) - - # Determine whether the file is too big (over 10MB) or not - # Telegram does make a distinction between smaller or larger files - is_large = file_size > 10 * 1024 * 1024 - if not is_large: - # Calculate the MD5 hash before anything else. - # As this needs to be done always for small files, - # might as well do it before anything else and - # check the cache. - if isinstance(file, str): - with open(file, 'rb') as stream: - file = stream.read() - hash_md5 = md5(file) - else: - hash_md5 = None - - part_count = (file_size + part_size - 1) // part_size - __log__.info('Uploading file of %d bytes in %d chunks of %d', - file_size, part_count, part_size) - - with open(file, 'rb') if isinstance(file, str) else BytesIO(file) \ - as stream: - for part_index in range(part_count): - # Read the file by in chunks of size part_size - part = stream.read(part_size) - - # The SavePartRequest is different depending on whether - # the file is too large or not (over or less than 10MB) - if is_large: - request = SaveBigFilePartRequest(file_id, part_index, - part_count, part) - else: - request = SaveFilePartRequest(file_id, part_index, part) - - result = self(request) - if result: - __log__.debug('Uploaded %d/%d', part_index + 1, part_count) - if progress_callback: - progress_callback(stream.tell(), file_size) - else: - raise RuntimeError( - 'Failed to upload file part {}.'.format(part_index)) - - if is_large: - return InputFileBig(file_id, part_count, file_name) - else: - return InputFile(file_id, part_count, file_name, - md5_checksum=hash_md5.hexdigest()) - - # endregion - - # region Downloading media - - def download_file(self, - input_location, - file, - part_size_kb=None, - file_size=None, - progress_callback=None): - """Downloads the given InputFileLocation to file (a stream or str). - - If 'progress_callback' is not None, it should be a function that - takes two parameters, (bytes_downloaded, total_bytes). Note that - 'total_bytes' simply equals 'file_size', and may be None. - """ - if not part_size_kb: - if not file_size: - part_size_kb = 64 # Reasonable default - else: - part_size_kb = get_appropriated_part_size(file_size) - - part_size = int(part_size_kb * 1024) - # https://core.telegram.org/api/files says: - # > part_size % 1024 = 0 (divisible by 1KB) - # - # But https://core.telegram.org/cdn (more recent) says: - # > limit must be divisible by 4096 bytes - # So we just stick to the 4096 limit. - if part_size % 4096 != 0: - raise ValueError('The part size must be evenly divisible by 4096.') - - if isinstance(file, str): - # Ensure that we'll be able to download the media - utils.ensure_parent_dir_exists(file) - f = open(file, 'wb') - else: - f = file - - # The used client will change if FileMigrateError occurs - client = self - cdn_decrypter = None - - __log__.info('Downloading file in chunks of %d bytes', part_size) - try: - offset = 0 - while True: - try: - if cdn_decrypter: - result = cdn_decrypter.get_file() - else: - result = client(GetFileRequest( - input_location, offset, part_size - )) - - if isinstance(result, FileCdnRedirect): - __log__.info('File lives in a CDN') - cdn_decrypter, result = \ - CdnDecrypter.prepare_decrypter( - client, self._get_cdn_client(result), result - ) - - except FileMigrateError as e: - __log__.info('File lives in another DC') - client = self._get_exported_client(e.new_dc) - continue - - offset += part_size - - # If we have received no data (0 bytes), the file is over - # So there is nothing left to download and write - if not result.bytes: - # Return some extra information, unless it's a CDN file - return getattr(result, 'type', '') - - f.write(result.bytes) - __log__.debug('Saved %d more bytes', len(result.bytes)) - if progress_callback: - progress_callback(f.tell(), file_size) - finally: - if client != self: - client.disconnect() - - if cdn_decrypter: - try: - cdn_decrypter.client.disconnect() - except: - pass - if isinstance(file, str): - f.close() - - # endregion - # region Updates handling def sync_updates(self): diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84c9beea..6e69e3f6 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,4 +1,6 @@ +import hashlib import itertools +import logging import os import sys import time @@ -6,6 +8,14 @@ from collections import OrderedDict, UserList from datetime import datetime, timedelta from mimetypes import guess_type +from io import BytesIO + +from telethon.crypto import CdnDecrypter +from telethon.tl.functions.upload import ( + SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest +) +from telethon.tl.types.upload import FileCdnRedirect + try: import socks except ImportError: @@ -16,7 +26,8 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError, FilePartMissingError) + SessionPasswordNeededError, FilePartMissingError, FileMigrateError +) from .network import ConnectionMode from .tl import TLObject from .tl.custom import Draft, Dialog @@ -55,11 +66,13 @@ from .tl.types import ( UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, - InputSingleMedia, InputMediaPhoto, InputPhoto + InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig ) from .tl.types.messages import DialogsSlice from .extensions import markdown +__log__ = logging.getLogger(__name__) + class TelegramClient(TelegramBareClient): """ @@ -1011,6 +1024,130 @@ class TelegramClient(TelegramBareClient): progress_callback=progress_callback, reply_to=reply_to ) + def upload_file(self, + file, + part_size_kb=None, + file_name=None, + allow_cache=True, + progress_callback=None): + """ + Uploads the specified file and returns a handle (an instance of + InputFile or InputFileBig, as required) which can be later used + before it expires (they are usable during less than a day). + + Uploading a file will simply return a "handle" to the file stored + remotely in the Telegram servers, which can be later used on. This + will **not** upload the file to your own chat or any chat at all. + + Args: + file (:obj:`str` | :obj:`bytes` | :obj:`file`): + The path of the file, byte array, or stream that will be sent. + Note that if a byte array or a stream is given, a filename + or its type won't be inferred, and it will be sent as an + "unnamed application/octet-stream". + + Subsequent calls with the very same file will result in + immediate uploads, unless ``.clear_file_cache()`` is called. + + part_size_kb (:obj:`int`, optional): + Chunk size when uploading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_name (:obj:`str`, optional): + The file name which will be used on the resulting InputFile. + If not specified, the name will be taken from the ``file`` + and if this is not a ``str``, it will be ``"unnamed"``. + + allow_cache (:obj:`bool`, optional): + Whether to allow reusing the file from cache or not. Unused. + + progress_callback (:obj:`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + Returns: + The InputFile (or InputFileBig if >10MB). + """ + if isinstance(file, (InputFile, InputFileBig)): + return file # Already uploaded + + if isinstance(file, str): + file_size = os.path.getsize(file) + elif isinstance(file, bytes): + file_size = len(file) + else: + file = file.read() + file_size = len(file) + + # File will now either be a string or bytes + if not part_size_kb: + part_size_kb = utils.get_appropriated_part_size(file_size) + + if part_size_kb > 512: + raise ValueError('The part size must be less or equal to 512KB') + + part_size = int(part_size_kb * 1024) + if part_size % 1024 != 0: + raise ValueError( + 'The part size must be evenly divisible by 1024') + + # Set a default file name if None was specified + file_id = helpers.generate_random_long() + if not file_name: + if isinstance(file, str): + file_name = os.path.basename(file) + else: + file_name = str(file_id) + + # Determine whether the file is too big (over 10MB) or not + # Telegram does make a distinction between smaller or larger files + is_large = file_size > 10 * 1024 * 1024 + if not is_large: + # Calculate the MD5 hash before anything else. + # As this needs to be done always for small files, + # might as well do it before anything else and + # check the cache. + if isinstance(file, str): + with open(file, 'rb') as stream: + file = stream.read() + hash_md5 = hashlib.md5(file) + else: + hash_md5 = None + + part_count = (file_size + part_size - 1) // part_size + __log__.info('Uploading file of %d bytes in %d chunks of %d', + file_size, part_count, part_size) + + with open(file, 'rb') if isinstance(file, str) else BytesIO(file) \ + as stream: + for part_index in range(part_count): + # Read the file by in chunks of size part_size + part = stream.read(part_size) + + # The SavePartRequest is different depending on whether + # the file is too large or not (over or less than 10MB) + if is_large: + request = SaveBigFilePartRequest(file_id, part_index, + part_count, part) + else: + request = SaveFilePartRequest(file_id, part_index, part) + + result = self(request) + if result: + __log__.debug('Uploaded %d/%d', part_index + 1, + part_count) + if progress_callback: + progress_callback(stream.tell(), file_size) + else: + raise RuntimeError( + 'Failed to upload file part {}.'.format(part_index)) + + if is_large: + return InputFileBig(file_id, part_count, file_name) + else: + return InputFile(file_id, part_count, file_name, + md5_checksum=hash_md5.hexdigest()) + # endregion # region Downloading media requests @@ -1292,6 +1429,113 @@ class TelegramClient(TelegramBareClient): return result i += 1 + def download_file(self, + input_location, + file, + part_size_kb=None, + file_size=None, + progress_callback=None): + """ + Downloads the given input location to a file. + + Args: + input_location (:obj:`InputFileLocation`): + The file location from which the file will be downloaded. + + file (:obj:`str` | :obj:`file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + + part_size_kb (:obj:`int`, optional): + Chunk size when downloading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (:obj:`int`, optional): + The file size that is about to be downloaded, if known. + Only used if ``progress_callback`` is specified. + + progress_callback (:obj:`callable`, optional): + A callback function accepting two parameters: + ``(downloaded bytes, total)``. Note that the + ``total`` is the provided ``file_size``. + """ + if not part_size_kb: + if not file_size: + part_size_kb = 64 # Reasonable default + else: + part_size_kb = utils.get_appropriated_part_size(file_size) + + part_size = int(part_size_kb * 1024) + # https://core.telegram.org/api/files says: + # > part_size % 1024 = 0 (divisible by 1KB) + # + # But https://core.telegram.org/cdn (more recent) says: + # > limit must be divisible by 4096 bytes + # So we just stick to the 4096 limit. + if part_size % 4096 != 0: + raise ValueError( + 'The part size must be evenly divisible by 4096.') + + if isinstance(file, str): + # Ensure that we'll be able to download the media + helpers.ensure_parent_dir_exists(file) + f = open(file, 'wb') + else: + f = file + + # The used client will change if FileMigrateError occurs + client = self + cdn_decrypter = None + + __log__.info('Downloading file in chunks of %d bytes', part_size) + try: + offset = 0 + while True: + try: + if cdn_decrypter: + result = cdn_decrypter.get_file() + else: + result = client(GetFileRequest( + input_location, offset, part_size + )) + + if isinstance(result, FileCdnRedirect): + __log__.info('File lives in a CDN') + cdn_decrypter, result = \ + CdnDecrypter.prepare_decrypter( + client, self._get_cdn_client(result), + result + ) + + except FileMigrateError as e: + __log__.info('File lives in another DC') + client = self._get_exported_client(e.new_dc) + continue + + offset += part_size + + # If we have received no data (0 bytes), the file is over + # So there is nothing left to download and write + if not result.bytes: + # Return some extra information, unless it's a CDN file + return getattr(result, 'type', '') + + f.write(result.bytes) + __log__.debug('Saved %d more bytes', len(result.bytes)) + if progress_callback: + progress_callback(f.tell(), file_size) + finally: + if client != self: + client.disconnect() + + if cdn_decrypter: + try: + cdn_decrypter.client.disconnect() + except: + pass + if isinstance(file, str): + f.close() + # endregion # endregion From 7e707dbbd991ba22cdf70a1346683837ba970a2f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 19:35:46 +0100 Subject: [PATCH 044/108] Fix using enum on sqlite instead its value --- telethon/tl/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/tl/session.py b/telethon/tl/session.py index 5d89a5f7..e2c653d4 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -445,7 +445,7 @@ class Session: tuple_ = self._conn.execute( 'select id, hash from sent_files ' 'where md5_digest = ? and file_size = ? and type = ?', - (md5_digest, file_size, _SentFileType.from_type(cls)) + (md5_digest, file_size, _SentFileType.from_type(cls).value) ).fetchone() if tuple_: # Both allowed classes have (id, access_hash) as parameters @@ -459,7 +459,7 @@ class Session: self._conn.execute( 'insert into sent_files values (?,?,?,?,?)', ( md5_digest, file_size, - _SentFileType.from_type(type(instance)), + _SentFileType.from_type(type(instance)).value, instance.id, instance.access_hash )) self.save() From 0e4611a593dd805c05a4420aced410166504c57c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 19:36:47 +0100 Subject: [PATCH 045/108] Properly implement InputPhoto/InputDocument caching Since uploading a file is done on the TelegramClient, and the InputFiles are only valid for a short period of time, it only makes sense to cache the sent media instead (which should not expire). The problem is the MD5 is only needed when uploading the file. The solution is to allow this method to check for the wanted cache, and if available, return an instance of that, so to preserve the flexibility of both options (always InputFile, or the cached InputPhoto/InputDocument) instead reuploading. --- telethon/telegram_client.py | 146 +++++++++++++++++++++--------------- telethon/tl/session.py | 2 +- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6e69e3f6..6e249cc4 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -66,7 +66,8 @@ from .tl.types import ( UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, - InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig + InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, + InputDocument, InputMediaDocument ) from .tl.types.messages import DialogsSlice from .extensions import markdown @@ -875,7 +876,9 @@ class TelegramClient(TelegramBareClient): allow_cache (:obj:`bool`, optional): Whether to allow using the cached version stored in the - database or not. Defaults to ``True`` to avoid reuploads. + database or not. Defaults to ``True`` to avoid re-uploads. + Must be ``False`` if you wish to use different attributes + or thumb than those that were used when the file was cached. Kwargs: If "is_voice_note" in kwargs, despite its value, and the file is @@ -892,8 +895,7 @@ class TelegramClient(TelegramBareClient): if all(utils.is_image(x) for x in file): return self._send_album( entity, file, caption=caption, - progress_callback=progress_callback, reply_to=reply_to, - allow_cache=allow_cache + progress_callback=progress_callback, reply_to=reply_to ) # Not all are images, so send all the files one by one return [ @@ -905,10 +907,20 @@ class TelegramClient(TelegramBareClient): ) for x in file ] + as_image = utils.is_image(file) and not force_document + use_cache = InputPhoto if as_image else InputDocument file_handle = self.upload_file( - file, progress_callback=progress_callback, allow_cache=allow_cache) + file, progress_callback=progress_callback, + use_cache=use_cache if allow_cache else None + ) - if utils.is_image(file) and not force_document: + if isinstance(file_handle, use_cache): + # File was cached, so an instance of use_cache was returned + if as_image: + media = InputMediaPhoto(file_handle, caption) + else: + media = InputMediaDocument(file_handle, caption) + elif as_image: media = InputMediaUploadedPhoto(file_handle, caption) else: mime_type = None @@ -964,19 +976,19 @@ class TelegramClient(TelegramBareClient): media=media, reply_to_msg_id=self._get_reply_to(reply_to) ) - try: - return self._get_response_message(request, self(request)) - except FilePartMissingError: - # After a while, cached files are invalidated and this - # error is raised. The file needs to be uploaded again. - if not allow_cache: - raise - return self.send_file( - entity, file, allow_cache=False, - caption=caption, force_document=force_document, - progress_callback=progress_callback, reply_to=reply_to, - attributes=attributes, thumb=thumb, **kwargs - ) + msg = self._get_response_message(request, self(request)) + if msg and isinstance(file_handle, InputFile): + # There was a response message and we didn't use cached + # version, so cache whatever we just sent to the database. + # Note that the InputFile was modified to have md5/size. + md5, size = file_handle.md5, file_handle.size + if as_image: + to_cache = utils.get_input_photo(msg.media.photo) + else: + to_cache = utils.get_input_document(msg.media.document) + self.session.cache_file(md5, size, to_cache) + + return msg def send_voice_note(self, entity, file, caption='', progress_callback=None, reply_to=None): @@ -987,48 +999,44 @@ class TelegramClient(TelegramBareClient): is_voice_note=()) # empty tuple is enough def _send_album(self, entity, files, caption='', - progress_callback=None, reply_to=None, - allow_cache=True): + progress_callback=None, reply_to=None): """Specialized version of .send_file for albums""" + # We don't care if the user wants to avoid cache, we will use it + # anyway. Why? The cached version will be exactly the same thing + # we need to produce right now to send albums (uploadMedia), and + # cache only makes a difference for documents where the user may + # want the attributes used on them to change. Caption's ignored. entity = self.get_input_entity(entity) reply_to = self._get_reply_to(reply_to) - try: - # Need to upload the media first - media = [ - self(UploadMediaRequest(entity, InputMediaUploadedPhoto( - self.upload_file(file, allow_cache=allow_cache), - caption=caption - ))) - for file in files - ] - # Now we can construct the multi-media request - result = self(SendMultiMediaRequest( - entity, reply_to_msg_id=reply_to, multi_media=[ - InputSingleMedia(InputMediaPhoto( - InputPhoto(m.photo.id, m.photo.access_hash), - caption=caption - )) - for m in media - ] - )) - return [ - self._get_response_message(update.id, result) - for update in result.updates - if isinstance(update, UpdateMessageID) - ] - except FilePartMissingError: - if not allow_cache: - raise - return self._send_album( - entity, files, allow_cache=False, caption=caption, - progress_callback=progress_callback, reply_to=reply_to - ) + + # Need to upload the media first, but only if they're not cached yet + media = [] + for file in files: + # fh will either be InputPhoto or a modified InputFile + fh = self.upload_file(file, use_cache=InputPhoto) + if not isinstance(fh, InputPhoto): + input_photo = utils.get_input_photo(self(UploadMediaRequest( + entity, media=InputMediaUploadedPhoto(fh, caption) + )).photo) + self.session.cache_file(fh.md5, fh.size, input_photo) + fh = input_photo + media.append(InputSingleMedia(InputMediaPhoto(fh, caption))) + + # Now we can construct the multi-media request + result = self(SendMultiMediaRequest( + entity, reply_to_msg_id=reply_to, multi_media=media + )) + return [ + self._get_response_message(update.id, result) + for update in result.updates + if isinstance(update, UpdateMessageID) + ] def upload_file(self, file, part_size_kb=None, file_name=None, - allow_cache=True, + use_cache=None, progress_callback=None): """ Uploads the specified file and returns a handle (an instance of @@ -1058,15 +1066,20 @@ class TelegramClient(TelegramBareClient): If not specified, the name will be taken from the ``file`` and if this is not a ``str``, it will be ``"unnamed"``. - allow_cache (:obj:`bool`, optional): - Whether to allow reusing the file from cache or not. Unused. + use_cache (:obj:`type`, optional): + The type of cache to use (currently either ``InputDocument`` + or ``InputPhoto``). If present and the file is small enough + to need the MD5, it will be checked against the database, + and if a match is found, the upload won't be made. Instead, + an instance of type ``use_cache`` will be returned. progress_callback (:obj:`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. Returns: - The InputFile (or InputFileBig if >10MB). + The InputFile (or InputFileBig if >10MB) with two extra + attributes: ``.md5`` (its ``.digest()``) and ``size``. """ if isinstance(file, (InputFile, InputFileBig)): return file # Already uploaded @@ -1102,6 +1115,7 @@ class TelegramClient(TelegramBareClient): # Determine whether the file is too big (over 10MB) or not # Telegram does make a distinction between smaller or larger files is_large = file_size > 10 * 1024 * 1024 + hash_md5 = hashlib.md5() if not is_large: # Calculate the MD5 hash before anything else. # As this needs to be done always for small files, @@ -1110,9 +1124,13 @@ class TelegramClient(TelegramBareClient): if isinstance(file, str): with open(file, 'rb') as stream: file = stream.read() - hash_md5 = hashlib.md5(file) - else: - hash_md5 = None + hash_md5.update(file) + if use_cache: + cached = self.session.get_file( + hash_md5.digest(), file_size, cls=use_cache + ) + if cached: + return cached part_count = (file_size + part_size - 1) // part_size __log__.info('Uploading file of %d bytes in %d chunks of %d', @@ -1143,10 +1161,14 @@ class TelegramClient(TelegramBareClient): 'Failed to upload file part {}.'.format(part_index)) if is_large: - return InputFileBig(file_id, part_count, file_name) + result = InputFileBig(file_id, part_count, file_name) else: - return InputFile(file_id, part_count, file_name, - md5_checksum=hash_md5.hexdigest()) + result = InputFile(file_id, part_count, file_name, + md5_checksum=hash_md5.hexdigest()) + + result.md5 = hash_md5.digest() + result.size = file_size + return result # endregion diff --git a/telethon/tl/session.py b/telethon/tl/session.py index e2c653d4..bfed1a79 100644 --- a/telethon/tl/session.py +++ b/telethon/tl/session.py @@ -457,7 +457,7 @@ class Session: with self._db_lock: self._conn.execute( - 'insert into sent_files values (?,?,?,?,?)', ( + 'insert or replace into sent_files values (?,?,?,?,?)', ( md5_digest, file_size, _SentFileType.from_type(type(instance)).value, instance.id, instance.access_hash From b546c022109429d061dd6482fde928721252944e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 18 Jan 2018 20:08:05 +0100 Subject: [PATCH 046/108] Return a custom class for sized InputFile instead extra attrs --- telethon/telegram_client.py | 32 +++++++++++--------------- telethon/tl/custom/__init__.py | 1 + telethon/tl/custom/input_sized_file.py | 9 ++++++++ 3 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 telethon/tl/custom/input_sized_file.py diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 6e249cc4..5c4493f3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -6,15 +6,15 @@ import sys import time from collections import OrderedDict, UserList from datetime import datetime, timedelta +from io import BytesIO from mimetypes import guess_type -from io import BytesIO - -from telethon.crypto import CdnDecrypter -from telethon.tl.functions.upload import ( - SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest +from .crypto import CdnDecrypter +from .tl.custom import InputSizedFile +from .tl.functions.upload import ( + SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest ) -from telethon.tl.types.upload import FileCdnRedirect +from .tl.types.upload import FileCdnRedirect try: import socks @@ -26,7 +26,7 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError, FilePartMissingError, FileMigrateError + SessionPasswordNeededError, FileMigrateError ) from .network import ConnectionMode from .tl import TLObject @@ -977,10 +977,9 @@ class TelegramClient(TelegramBareClient): reply_to_msg_id=self._get_reply_to(reply_to) ) msg = self._get_response_message(request, self(request)) - if msg and isinstance(file_handle, InputFile): + if msg and isinstance(file_handle, InputSizedFile): # There was a response message and we didn't use cached # version, so cache whatever we just sent to the database. - # Note that the InputFile was modified to have md5/size. md5, size = file_handle.md5, file_handle.size if as_image: to_cache = utils.get_input_photo(msg.media.photo) @@ -1078,8 +1077,8 @@ class TelegramClient(TelegramBareClient): ``(sent bytes, total)``. Returns: - The InputFile (or InputFileBig if >10MB) with two extra - attributes: ``.md5`` (its ``.digest()``) and ``size``. + ``InputFileBig`` if the file size is larger than 10MB, + ``InputSizedFile`` (subclass of ``InputFile``) otherwise. """ if isinstance(file, (InputFile, InputFileBig)): return file # Already uploaded @@ -1161,14 +1160,11 @@ class TelegramClient(TelegramBareClient): 'Failed to upload file part {}.'.format(part_index)) if is_large: - result = InputFileBig(file_id, part_count, file_name) + return InputFileBig(file_id, part_count, file_name) else: - result = InputFile(file_id, part_count, file_name, - md5_checksum=hash_md5.hexdigest()) - - result.md5 = hash_md5.digest() - result.size = file_size - return result + return InputSizedFile( + file_id, part_count, file_name, md5=hash_md5, size=file_size + ) # endregion diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py index 5b6bf44d..f74189f6 100644 --- a/telethon/tl/custom/__init__.py +++ b/telethon/tl/custom/__init__.py @@ -1,2 +1,3 @@ from .draft import Draft from .dialog import Dialog +from .input_sized_file import InputSizedFile diff --git a/telethon/tl/custom/input_sized_file.py b/telethon/tl/custom/input_sized_file.py new file mode 100644 index 00000000..fcb743f6 --- /dev/null +++ b/telethon/tl/custom/input_sized_file.py @@ -0,0 +1,9 @@ +from ..types import InputFile + + +class InputSizedFile(InputFile): + """InputFile class with two extra parameters: md5 (digest) and size""" + def __init__(self, id_, parts, name, md5, size): + super().__init__(id_, parts, name, md5.hexdigest()) + self.md5 = md5.digest() + self.size = size From 1c9fa76edeedc6d17325216cab9c574a23ca0caa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 11:47:45 +0100 Subject: [PATCH 047/108] Add new method to .resolve() parameters instead on init TLObject's __init__ used to call utils.get_input_* methods and similar to auto-cast things like User into InputPeerUser as required. Now there's a custom .resolve() method for this purpose with several advantages: - Old behaviour still works, autocasts work like usual. - A request can be constructed and later modified, before the autocast only occured on the constructor but now while invoking. - This allows us to not only use the utils module but also the client, so it's even possible to use usernames or phone numbers for things that require an InputPeer. This actually assumes the TelegramClient subclass is being used and not the bare version which would fail when calling .get_input_peer(). --- telethon/telegram_bare_client.py | 5 +- telethon/tl/tlobject.py | 3 + telethon_generator/tl_generator.py | 104 +++++++++++++++-------------- 3 files changed, 60 insertions(+), 52 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 9684a034..ba6ae374 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -7,7 +7,7 @@ from signal import signal, SIGINT, SIGTERM, SIGABRT from threading import Lock from time import sleep -from . import version +from . import version, utils from .crypto import rsa from .errors import ( RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, @@ -420,6 +420,9 @@ class TelegramBareClient: if self._background_error: raise self._background_error + for request in requests: + request.resolve(self, utils) + # For logging purposes if len(requests) == 1: which = type(requests[0]).__name__ diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index ad930f9c..7c86a24a 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -144,6 +144,9 @@ class TLObject: raise TypeError('Cannot interpret "{}" as a date.'.format(dt)) # These should be overrode + def resolve(self, client, utils): + pass + def to_dict(self, recursive=True): return {} diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 3116003a..39bad15f 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -10,6 +10,15 @@ AUTO_GEN_NOTICE = \ '"""File generated by TLObjects\' generator. All changes will be ERASED"""' +AUTO_CASTS = { + 'InputPeer': 'utils.get_input_peer(client.get_input_entity({}))', + 'InputChannel': 'utils.get_input_channel(client.get_input_entity({}))', + 'InputUser': 'utils.get_input_user(client.get_input_entity({}))', + 'InputMedia': 'utils.get_input_media({})', + 'InputPhoto': 'utils.get_input_photo({})' +} + + class TLGenerator: def __init__(self, output_dir): self.output_dir = output_dir @@ -257,10 +266,45 @@ class TLGenerator: builder.writeln() for arg in args: - TLGenerator._write_self_assigns(builder, tlobject, arg, args) + if not arg.can_be_inferred: + builder.writeln('self.{0} = {0}'.format(arg.name)) + continue + + # Currently the only argument that can be + # inferred are those called 'random_id' + if arg.name == 'random_id': + # Endianness doesn't really matter, and 'big' is shorter + code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \ + .format(8 if arg.type == 'long' else 4) + + if arg.is_vector: + # Currently for the case of "messages.forwardMessages" + # Ensure we can infer the length from id:Vector<> + if not next( + a for a in args if a.name == 'id').is_vector: + raise ValueError( + 'Cannot infer list of random ids for ', tlobject + ) + code = '[{} for _ in range(len(id))]'.format(code) + + builder.writeln( + "self.random_id = random_id if random_id " + "is not None else {}".format(code) + ) + else: + raise ValueError('Cannot infer a value for ', arg) builder.end_block() + # Write the resolve(self, client, utils) method + if any(arg.type in AUTO_CASTS for arg in args): + builder.writeln('def resolve(self, client, utils):') + for arg in args: + ac = AUTO_CASTS.get(arg.type, None) + if ac: + TLGenerator._write_self_assign(builder, arg, ac) + builder.end_block() + # Write the to_dict(self) method builder.writeln('def to_dict(self, recursive=True):') if args: @@ -370,59 +414,17 @@ class TLGenerator: # builder.end_block() # No need to end the last block @staticmethod - def _write_self_assigns(builder, tlobject, arg, args): - if arg.can_be_inferred: - # Currently the only argument that can be - # inferred are those called 'random_id' - if arg.name == 'random_id': - # Endianness doesn't really matter, and 'big' is shorter - code = "int.from_bytes(os.urandom({}), 'big', signed=True)"\ - .format(8 if arg.type == 'long' else 4) - - if arg.is_vector: - # Currently for the case of "messages.forwardMessages" - # Ensure we can infer the length from id:Vector<> - if not next(a for a in args if a.name == 'id').is_vector: - raise ValueError( - 'Cannot infer list of random ids for ', tlobject - ) - code = '[{} for _ in range(len(id))]'.format(code) - - builder.writeln( - "self.random_id = random_id if random_id " - "is not None else {}".format(code) - ) - else: - raise ValueError('Cannot infer a value for ', arg) - - # Well-known cases, auto-cast it to the right type - elif arg.type == 'InputPeer' and tlobject.is_function: - TLGenerator.write_get_input(builder, arg, 'get_input_peer') - elif arg.type == 'InputChannel' and tlobject.is_function: - TLGenerator.write_get_input(builder, arg, 'get_input_channel') - elif arg.type == 'InputUser' and tlobject.is_function: - TLGenerator.write_get_input(builder, arg, 'get_input_user') - elif arg.type == 'InputMedia' and tlobject.is_function: - TLGenerator.write_get_input(builder, arg, 'get_input_media') - elif arg.type == 'InputPhoto' and tlobject.is_function: - TLGenerator.write_get_input(builder, arg, 'get_input_photo') - - else: - builder.writeln('self.{0} = {0}'.format(arg.name)) - - @staticmethod - def write_get_input(builder, arg, get_input_code): - """Returns "True" if the get_input_* code was written when assigning - a parameter upon creating the request. Returns False otherwise - """ + def _write_self_assign(builder, arg, get_input_code): + """Writes self.arg = input.format(self.arg), considering vectors""" if arg.is_vector: - builder.write('self.{0} = [{1}(_x) for _x in {0}]' - .format(arg.name, get_input_code)) + builder.write('self.{0} = [{1} for _x in self.{0}]' + .format(arg.name, get_input_code.format('_x'))) else: - builder.write('self.{0} = {1}({0})' - .format(arg.name, get_input_code)) + builder.write('self.{} = {}'.format( + arg.name, get_input_code.format('self.' + arg.name))) + builder.writeln( - ' if {} else None'.format(arg.name) if arg.is_flag else '' + ' if self.{} else None'.format(arg.name) if arg.is_flag else '' ) @staticmethod From f6d98a61cfe4decb2900671ad84e994d19583309 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 11:52:44 +0100 Subject: [PATCH 048/108] Add stub .get_input_entity() to TelegramBareClient .resolve() calls should now work even if the subclass isn't in use. --- telethon/telegram_bare_client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index ba6ae374..bee3ecdd 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -556,6 +556,13 @@ class TelegramBareClient: (code request sent and confirmed)?""" return self._authorized + def get_input_entity(self, peer): + """ + Stub method, no functionality so that calling + ``.get_input_entity()`` from ``.resolve()`` doesn't fail. + """ + return peer + # endregion # region Updates handling From 33e50aaee1a7c599ffbfd3aabd463e6b3b9a9675 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 12:12:52 +0100 Subject: [PATCH 049/108] Reuse .on_response/.__str__/.stringify, override iff necessary --- telethon/extensions/binary_reader.py | 2 ++ telethon/tl/tlobject.py | 11 ++++++++++ telethon_generator/tl_generator.py | 33 +++++++++++++++++----------- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 1402083f..ecf7dd1b 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -133,6 +133,8 @@ class BinaryReader: return True elif value == 0xbc799737: # boolFalse return False + elif value == 0x1cb5c415: # Vector + return [self.tgread_object() for _ in range(self.read_int())] # If there was still no luck, give up self.seek(-4) # Go back diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 7c86a24a..ac0b65f8 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -7,6 +7,7 @@ class TLObject: def __init__(self): self.confirm_received = Event() self.rpc_error = None + self.result = None # These should be overrode self.content_related = False # Only requests/functions/queries are @@ -143,6 +144,16 @@ class TLObject: raise TypeError('Cannot interpret "{}" as a date.'.format(dt)) + # These are nearly always the same for all subclasses + def on_response(self, reader): + self.result = reader.tgread_object() + + def __str__(self): + return TLObject.pretty_format(self) + + def stringify(self): + return TLObject.pretty_format(self, indent=0) + # These should be overrode def resolve(self, client, utils): pass diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 39bad15f..fb8ca4bd 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -395,23 +395,30 @@ class TLGenerator: if not a.flag_indicator and not a.generic_definition ) )) - builder.end_block() # Only requests can have a different response that's not their # serialized body, that is, we'll be setting their .result. - if tlobject.is_function: + # + # The default behaviour is reading a TLObject too, so no need + # to override it unless necessary. + if tlobject.is_function and not TLGenerator._is_boxed(tlobject.result): + builder.end_block() builder.writeln('def on_response(self, reader):') TLGenerator.write_request_result_code(builder, tlobject) - builder.end_block() - # Write the __str__(self) and stringify(self) functions - builder.writeln('def __str__(self):') - builder.writeln('return TLObject.pretty_format(self)') - builder.end_block() - - builder.writeln('def stringify(self):') - builder.writeln('return TLObject.pretty_format(self, indent=0)') - # builder.end_block() # No need to end the last block + @staticmethod + def _is_boxed(type_): + # https://core.telegram.org/mtproto/serialize#boxed-and-bare-types + # TL;DR; boxed types start with uppercase always, so we can use + # this to check whether everything in it is boxed or not. + # + # The API always returns a boxed type, but it may inside a Vector<> + # or a namespace, and the Vector may have a not-boxed type. For this + # reason we find whatever index, '<' or '.'. If neither are present + # we will get -1, and the 0th char is always upper case thus works. + # For Vector types and namespaces, it will check in the right place. + check_after = max(type_.find('<'), type_.find('.')) + return type_[check_after + 1].isupper() @staticmethod def _write_self_assign(builder, arg, get_input_code): @@ -697,13 +704,13 @@ class TLGenerator: # not parsed as arguments are and it's a bit harder to tell which # is which. if tlobject.result == 'Vector': - builder.writeln('reader.read_int() # Vector id') + builder.writeln('reader.read_int() # Vector ID') builder.writeln('count = reader.read_int()') builder.writeln( 'self.result = [reader.read_int() for _ in range(count)]' ) elif tlobject.result == 'Vector': - builder.writeln('reader.read_int() # Vector id') + builder.writeln('reader.read_int() # Vector ID') builder.writeln('count = reader.read_long()') builder.writeln( 'self.result = [reader.read_long() for _ in range(count)]' From e3c56b0d98d6862d6fe03071f526652bd662d2b3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 13:00:17 +0100 Subject: [PATCH 050/108] Reduce autocast overhead as much as possible Rationale: if the user is doing things right, the penalty for being friendly (i.e. autocasting to the right version, like User -> InputPeerUser), should be as little as possible. Removing the redundant type() call to access .SUBCLASS_OF_ID and assuming the user provided a TLObject (through excepting whenever the attribute is not available) is x2 and x4 times faster respectively. Of course, this is a micro-optimization, but I still consider it's good to benefit users doing things right or avoiding redundant calls. --- telethon/telegram_client.py | 32 ++++++++------- telethon/utils.py | 77 ++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5c4493f3..f09a62fa 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -818,10 +818,12 @@ class TelegramClient(TelegramBareClient): if isinstance(reply_to, int): return reply_to - if isinstance(reply_to, TLObject) and \ - type(reply_to).SUBCLASS_OF_ID == 0x790009e3: - # hex(crc32(b'Message')) = 0x790009e3 - return reply_to.id + try: + if reply_to.SUBCLASS_OF_ID == 0x790009e3: + # hex(crc32(b'Message')) = 0x790009e3 + return reply_to.id + except AttributeError: + pass raise TypeError('Invalid reply_to type: {}'.format(type(reply_to))) @@ -1191,9 +1193,14 @@ class TelegramClient(TelegramBareClient): """ photo = entity possible_names = [] - if not isinstance(entity, TLObject) or type(entity).SUBCLASS_OF_ID in ( + try: + is_entity = entity.SUBCLASS_OF_ID in ( 0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697 - ): + ) + except AttributeError: + return None # Not even a TLObject as attribute access failed + + if is_entity: # Maybe it is an user or a chat? Or their full versions? # # The hexadecimal numbers above are simply: @@ -1705,14 +1712,13 @@ class TelegramClient(TelegramBareClient): if isinstance(peer, int): peer = PeerUser(peer) is_peer = True - - elif isinstance(peer, TLObject): - is_peer = type(peer).SUBCLASS_OF_ID == 0x2d45687 # crc32(b'Peer') - if not is_peer: - try: + else: + try: + is_peer = peer.SUBCLASS_OF_ID == 0x2d45687 # crc32(b'Peer') + if not is_peer: return utils.get_input_peer(peer) - except TypeError: - pass + except (AttributeError, TypeError): + pass # Attribute if not TLObject, Type if not "casteable" if not is_peer: raise TypeError( diff --git a/telethon/utils.py b/telethon/utils.py index 8549e18d..a4850d4c 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -3,11 +3,9 @@ Utilities for working with the Telegram API itself (such as handy methods to convert between an entity like an User, Chat, etc. into its Input version) """ import math +import re from mimetypes import add_type, guess_extension -import re - -from .tl import TLObject from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, @@ -25,7 +23,6 @@ from .tl.types import ( InputMediaUploadedPhoto, DocumentAttributeFilename, photos ) - USERNAME_RE = re.compile( r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) @@ -81,12 +78,12 @@ 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.""" - if not isinstance(entity, TLObject): + try: + if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') + return entity + except AttributeError: _raise_cast_fail(entity, 'InputPeer') - if type(entity).SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return entity - if isinstance(entity, User): if entity.is_self and allow_self: return InputPeerSelf() @@ -123,12 +120,12 @@ def get_input_peer(entity, allow_self=True): def get_input_channel(entity): """Similar to get_input_peer, but for InputChannel's alone""" - if not isinstance(entity, TLObject): + try: + if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') + return entity + except AttributeError: _raise_cast_fail(entity, 'InputChannel') - if type(entity).SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') - return entity - if isinstance(entity, (Channel, ChannelForbidden)): return InputChannel(entity.id, entity.access_hash or 0) @@ -140,12 +137,12 @@ def get_input_channel(entity): def get_input_user(entity): """Similar to get_input_peer, but for InputUser's alone""" - if not isinstance(entity, TLObject): + try: + if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'): + return entity + except AttributeError: _raise_cast_fail(entity, 'InputUser') - if type(entity).SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser') - return entity - if isinstance(entity, User): if entity.is_self: return InputUserSelf() @@ -169,12 +166,12 @@ def get_input_user(entity): def get_input_document(document): """Similar to get_input_peer, but for documents""" - if not isinstance(document, TLObject): + try: + if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'): + return document + except AttributeError: _raise_cast_fail(document, 'InputDocument') - if type(document).SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') - return document - if isinstance(document, Document): return InputDocument(id=document.id, access_hash=document.access_hash) @@ -192,12 +189,12 @@ def get_input_document(document): def get_input_photo(photo): """Similar to get_input_peer, but for documents""" - if not isinstance(photo, TLObject): + try: + if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'): + return photo + except AttributeError: _raise_cast_fail(photo, 'InputPhoto') - if type(photo).SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') - return photo - if isinstance(photo, photos.Photo): photo = photo.photo @@ -212,12 +209,12 @@ def get_input_photo(photo): def get_input_geo(geo): """Similar to get_input_peer, but for geo points""" - if not isinstance(geo, TLObject): + try: + if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'): + return geo + except AttributeError: _raise_cast_fail(geo, 'InputGeoPoint') - if type(geo).SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint') - return geo - if isinstance(geo, GeoPoint): return InputGeoPoint(lat=geo.lat, long=geo.long) @@ -239,12 +236,12 @@ def get_input_media(media, user_caption=None, is_photo=False): If the media is a file location and is_photo is known to be True, it will be treated as an InputMediaUploadedPhoto. """ - if not isinstance(media, TLObject): + try: + if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'): + return media + except AttributeError: _raise_cast_fail(media, 'InputMedia') - if type(media).SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia') - return media - if isinstance(media, MessageMediaPhoto): return InputMediaPhoto( id=get_input_photo(media.photo), @@ -357,15 +354,15 @@ def get_peer_id(peer): a call to utils.resolve_id(marked_id). """ # First we assert it's a Peer TLObject, or early return for integers - if not isinstance(peer, TLObject): - if isinstance(peer, int): - return peer - else: - _raise_cast_fail(peer, 'int') + if isinstance(peer, int): + return peer - elif type(peer).SUBCLASS_OF_ID not in {0x2d45687, 0xc91c90b6}: - # Not a Peer or an InputPeer, so first get its Input version - peer = get_input_peer(peer, allow_self=False) + try: + if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): + # Not a Peer or an InputPeer, so first get its Input version + peer = get_input_peer(peer, allow_self=False) + except AttributeError: + _raise_cast_fail(peer, 'int') # Set the right ID/kind, or raise if the TLObject is not recognised if isinstance(peer, (PeerUser, InputPeerUser)): From 0e43022959c94faafc06482d1da93663c46b3b8b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 13:40:04 +0100 Subject: [PATCH 051/108] Remove redundant import, show type instead TLObject on docstring --- telethon_generator/parser/tl_object.py | 2 +- telethon_generator/tl_generator.py | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/telethon_generator/parser/tl_object.py b/telethon_generator/parser/tl_object.py index 278a66eb..034cb3c3 100644 --- a/telethon_generator/parser/tl_object.py +++ b/telethon_generator/parser/tl_object.py @@ -264,7 +264,7 @@ class TLArg: 'date': 'datetime.datetime | None', # None date = 0 timestamp 'bytes': 'bytes', 'true': 'bool', - }.get(self.type, 'TLObject') + }.get(self.type, self.type) if self.is_vector: result = 'list[{}]'.format(result) if self.is_flag and self.type != 'date': diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index fb8ca4bd..18293ba1 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -146,15 +146,6 @@ class TLGenerator: x for x in namespace_tlobjects.keys() if x ))) - # Import 'get_input_*' utils - # TODO Support them on types too - if 'functions' in out_dir: - builder.writeln( - 'from {}.utils import get_input_peer, ' - 'get_input_channel, get_input_user, ' - 'get_input_media, get_input_photo'.format('.' * depth) - ) - # Import 'os' for those needing access to 'os.urandom()' # Currently only 'random_id' needs 'os' to be imported, # for all those TLObjects with arg.can_be_inferred. From 519c113b5888cb61daea38db558cebf85068009f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 21:13:57 +0100 Subject: [PATCH 052/108] Update to v0.16.2 --- readthedocs/extra/changelog.rst | 46 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 9457c4f4..580ebe4b 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,52 @@ it can take advantage of new goodies! .. contents:: List of All Versions +New ``.resolve()`` method (v0.16.2) +=================================== + +*Published at 2018/01/19* + +The ``TLObject``'s (instances returned by the API and ``Request``'s) have +now acquired a new ``.resolve()`` method. While this should be used by the +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`` +was needed), since ``.resolve()`` is called when invoking. Before, it was +only done on object construction. + +Additions +~~~~~~~~~ + +- Album support. Just pass a list, tuple or any iterable to ``.send_file()``. + + +Enhancements +~~~~~~~~~~~~ + +- ``.start()`` asks for your phone only if required. +- Better file cache. All files under 10MB, once uploaded, should never be + needed to be re-uploaded again, as the sent media is cached to the session. + + +Bug fixes +~~~~~~~~~ + +- ``setup.py`` now calls ``gen_tl`` when installing the library if needed. + + +Internal changes +~~~~~~~~~~~~~~~~ + +- The mentioned ``.resolve()`` to perform "autocast", more powerful. +- Upload and download methods are no longer part of ``TelegramBareClient``. +- Reuse ``.on_response()``, ``.__str__`` and ``.stringify()``. + Only override ``.on_response()`` if necessary (small amount of cases). +- Reduced "autocast" overhead as much as possible. + You shouldn't be penalized if you've provided the right type. + + MtProto 2.0 (v0.16.1) ===================== diff --git a/telethon/version.py b/telethon/version.py index e0220c01..28c39d24 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.16.1' +__version__ = '0.16.2' From 4d4e81e609dbb247281cc924196e7be2569a5d78 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 19 Jan 2018 22:55:28 +0100 Subject: [PATCH 053/108] Fix cyclic imports on Python 3.4 by moving Session one level up --- telethon/{tl => }/session.py | 10 ++++------ telethon/telegram_bare_client.py | 3 ++- telethon/tl/__init__.py | 1 - 3 files changed, 6 insertions(+), 8 deletions(-) rename telethon/{tl => }/session.py (98%) diff --git a/telethon/tl/session.py b/telethon/session.py similarity index 98% rename from telethon/tl/session.py rename to telethon/session.py index bfed1a79..f7170478 100644 --- a/telethon/tl/session.py +++ b/telethon/session.py @@ -9,9 +9,10 @@ from enum import Enum from os.path import isfile as file_exists from threading import Lock -from .. import utils -from ..tl import TLObject -from ..tl.types import ( +from . import utils +from .crypto import AuthKey +from .tl import TLObject +from .tl.types import ( PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel, InputPhoto, InputDocument @@ -118,7 +119,6 @@ class Session: tuple_ = c.fetchone() if tuple_: self._dc_id, self._server_address, self._port, key, = tuple_ - from ..crypto import AuthKey self._auth_key = AuthKey(data=key) c.close() @@ -173,7 +173,6 @@ class Session: self._server_address = \ data.get('server_address', self._server_address) - from ..crypto import AuthKey if data.get('auth_key_data', None) is not None: key = b64decode(data['auth_key_data']) self._auth_key = AuthKey(data=key) @@ -228,7 +227,6 @@ class Session: c.execute('select auth_key from sessions') tuple_ = c.fetchone() if tuple_: - from ..crypto import AuthKey self._auth_key = AuthKey(data=tuple_[0]) else: self._auth_key = None diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index bee3ecdd..9b756f43 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -15,7 +15,8 @@ from .errors import ( PhoneMigrateError, NetworkMigrateError, UserMigrateError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode -from .tl import TLObject, Session +from .session import Session +from .tl import TLObject from .tl.all_tlobjects import LAYER from .tl.functions import ( InitConnectionRequest, InvokeWithLayerRequest, PingRequest diff --git a/telethon/tl/__init__.py b/telethon/tl/__init__.py index 403e481a..96c934bb 100644 --- a/telethon/tl/__init__.py +++ b/telethon/tl/__init__.py @@ -1,5 +1,4 @@ from .tlobject import TLObject -from .session import Session from .gzip_packed import GzipPacked from .tl_message import TLMessage from .message_container import MessageContainer From b716c4fe6792246c7bec3c06d8edd911b169f0ba Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 11:47:17 +0100 Subject: [PATCH 054/108] Several documentation enhancements and build warnings fixes - Made the documentation even more friendly towards newbies. - Eased the usage of methods like get history which now set a default empty message for message actions and vice versa. - Fixed some docstring documentations too. - Updated the old normal docs/ to link back and forth RTD. - Fixed the version of the documentation, now auto-loaded. --- docs/res/core.html | 78 +++++-------------- readthedocs/conf.py | 12 ++- .../advanced-usage/accessing-the-full-api.rst | 18 ++++- readthedocs/extra/basic/creating-a-client.rst | 2 + readthedocs/extra/basic/entities.rst | 53 ++++++++----- readthedocs/extra/basic/getting-started.rst | 35 +++++++-- readthedocs/extra/basic/installation.rst | 10 ++- readthedocs/extra/basic/telegram-client.rst | 31 ++++---- .../extra/basic/working-with-updates.rst | 6 ++ readthedocs/extra/examples/bots.rst | 5 ++ .../extra/examples/chats-and-channels.rst | 5 ++ .../extra/examples/working-with-messages.rst | 5 ++ readthedocs/index.rst | 15 ++-- readthedocs/telethon.rst | 13 +++- readthedocs/telethon.tl.rst | 16 ---- telethon/telegram_client.py | 6 +- telethon/tl/custom/dialog.py | 5 +- telethon/tl/custom/draft.py | 6 +- 18 files changed, 180 insertions(+), 141 deletions(-) diff --git a/docs/res/core.html b/docs/res/core.html index bc5c04b3..8c8bc9d8 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -44,8 +44,15 @@ page aims to provide easy access to all the available methods, their definition and parameters.

-

Although this documentation was generated for Telethon, it may - be useful for any other Telegram library out there.

+

Please note that when you see this:

+
---functions---
+users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User>
+ +

This is not Python code. It's the "TL definition". It's + an easy-to-read line that gives a quick overview on the parameters + and its result. You don't need to worry about this. See + here + for more details on it.

Index

    @@ -69,12 +76,12 @@

    Currently there are {method_count} methods available for the layer {layer}. The complete list can be seen here.

    - Methods, also known as requests, are used to interact with - the Telegram API itself and are invoked with a call to .invoke(). - Only these can be passed to .invoke()! You cannot - .invoke() types or constructors, only requests. After this, - Telegram will return a result, which may be, for instance, - a bunch of messages, some dialogs, users, etc.

    + Methods, also known as requests, are used to interact with the + Telegram API itself and are invoked through client(Request(...)). + Only these can be used like that! You cannot invoke types or + constructors, only requests. After this, Telegram will return a + result, which may be, for instance, a bunch of messages, + some dialogs, users, etc.

    Types

    Currently there are {type_count} types. You can see the full @@ -151,58 +158,9 @@

Full example

-

The following example demonstrates:

-
    -
  1. How to create a TelegramClient.
  2. -
  3. Connecting to the Telegram servers and authorizing an user.
  4. -
  5. Retrieving a list of chats (dialogs).
  6. -
  7. Invoking a request without the built-in methods.
  8. -
-
#!/usr/bin/python3
-from telethon import TelegramClient
-from telethon.tl.functions.messages import GetHistoryRequest
-
-# (1) Use your own values here
-api_id   = 12345
-api_hash = '0123456789abcdef0123456789abcdef'
-phone    = '+34600000000'
-
-# (2) Create the client and connect
-client = TelegramClient('username', api_id, api_hash)
-client.connect()
-
-# Ensure you're authorized
-if not client.is_user_authorized():
-    client.send_code_request(phone)
-    client.sign_in(phone, input('Enter the code: '))
-
-# (3) Using built-in methods
-dialogs, entities = client.get_dialogs(10)
-entity = entities[0]
-
-# (4) !! Invoking a request manually !!
-result = client(GetHistoryRequest(
-    entity,
-    limit=20,
-    offset_date=None,
-    offset_id=0,
-    max_id=0,
-    min_id=0,
-    add_offset=0
-))
-
-# Now you have access to the first 20 messages
-messages = result.messages
- -

As it can be seen, manually calling requests with - client(request) (or using the old way, by calling - client.invoke(request)) is way more verbose than using the - built-in methods (such as client.get_dialogs()).

- -

However, and - given that there are so many methods available, it's impossible to provide - a nice interface to things that may change over time. To get full access, - however, you're still able to invoke these methods manually.

+

Documentation for this is now + here. +

diff --git a/readthedocs/conf.py b/readthedocs/conf.py index 18ff1a17..efb14992 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -20,6 +20,11 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) +import os +import re + + +root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir)) # -- General configuration ------------------------------------------------ @@ -55,9 +60,12 @@ author = 'Lonami' # built documents. # # The short X.Y version. -version = '0.15' +with open(os.path.join(root, 'telethon', 'version.py')) as f: + version = re.search(r"^__version__\s+=\s+'(.*)'$", + f.read(), flags=re.MULTILINE).group(1) + # The full version, including alpha/beta/rc tags. -release = '0.15.5' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst index 04659bdb..7276aa43 100644 --- a/readthedocs/extra/advanced-usage/accessing-the-full-api.rst +++ b/readthedocs/extra/advanced-usage/accessing-the-full-api.rst @@ -14,8 +14,10 @@ through a sorted list of everything you can do. .. note:: - Removing the hand crafted documentation for methods is still - a work in progress! + The reason to keep both https://lonamiwebs.github.io/Telethon and this + documentation alive is that the former allows instant search results + as you type, and a "Copy import" button. If you like namespaces, you + can also do ``from telethon.tl import types, functions``. Both work. You should also refer to the documentation to see what the objects @@ -39,8 +41,8 @@ If you're going to use a lot of these, you may do: .. code-block:: python - import telethon.tl.functions as tl - # We now have access to 'tl.messages.SendMessageRequest' + from telethon.tl import types, functions + # 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 @@ -82,6 +84,14 @@ every time its used, simply call ``.get_input_peer``: from telethon import utils peer = utils.get_input_user(entity) + +.. note:: + + Since ``v0.16.2`` this is further simplified. The ``Request`` itself + will call ``client.get_input_entity()`` for you when required, but + it's good to remember what's happening. + + After this small parenthesis about ``.get_entity`` versus ``.get_input_entity``, we have everything we need. To ``.invoke()`` our request we do: diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 10ae5f60..bf565bb0 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -93,6 +93,8 @@ method also accepts a ``phone=`` and ``bot_token`` parameters. You can use either, as both will work. Determining which is just a matter of taste, and how much control you need. +Remember that you can get yourself at any time with ``client.get_me()``. + .. note:: If you want to use a **proxy**, you have to `install PySocks`__ diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index bc87539a..472942a7 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -10,21 +10,6 @@ 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 save bandwidth, the API also makes use of their "input" versions. -The input version of an entity (e.g. ``InputPeerUser``, ``InputChat``, -etc.) only contains the minimum required information that's required -for Telegram to be able to identify who you're referring to: their 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`` -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". - -Luckily, the library tries to simplify this mess the best it can. - - Getting entities **************** @@ -58,8 +43,8 @@ you're able to just do this: my_channel = client.get_entity(PeerChannel(some_id)) -All methods in the :ref:`telegram-client` call ``.get_entity()`` to further -save you from the hassle of doing so manually, so doing things like +All methods in the :ref:`telegram-client` call ``.get_input_entity()`` to +further save you from the hassle of doing so manually, so doing things like ``client.send_message('lonami', 'hi!')`` is possible. Every entity the library "sees" (in any response to any call) will by @@ -72,7 +57,27 @@ made to obtain the required information. Entities vs. Input Entities *************************** -As we mentioned before, API calls don't need to know the whole information +.. note:: + + Don't worry if you don't understand this section, just remember some + of the details listed here are important. When you're calling a method, + don't call ``.get_entity()`` before, just use the username or phone, + or the entity retrieved by other means like ``.get_dialogs()``. + + +To save bandwidth, the API also makes use of their "input" versions. +The input version of an entity (e.g. ``InputPeerUser``, ``InputChat``, +etc.) only contains the minimum required information that's required +for Telegram to be able to identify who you're referring to: their 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`` +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". + +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, @@ -85,3 +90,15 @@ the most recent information about said entity, but invoking requests don't need this information, just the ``InputPeer``. Only use ``.get_entity()`` if you need to get actual information, like the username, name, title, etc. of the entity. + +To further simplify the workflow, since the version ``0.16.2`` of the +library, the raw requests you make to the API are also able to call +``.get_input_entity`` wherever needed, so you can even do things like: + + .. code-block:: python + + 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 +you don't get this yet, but remember some of the details here are important. diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst index e69cc3ef..87c142e9 100644 --- a/readthedocs/extra/basic/getting-started.rst +++ b/readthedocs/extra/basic/getting-started.rst @@ -1,7 +1,5 @@ -.. Telethon documentation master file, created by - sphinx-quickstart on Fri Nov 17 15:36:11 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. _getting-started: + =============== Getting Started @@ -39,13 +37,36 @@ Basic Usage .. code-block:: python - print(me.stringify()) + # Getting information about yourself + print(client.get_me().stringify()) - client.send_message('username', 'Hello! Talking to you from Telethon') + # Sending a message (you can use 'me' or 'self' to message yourself) + client.send_message('username', 'Hello World from Telethon!') + + # Sending a file client.send_file('username', '/home/myself/Pictures/holidays.jpg') - client.download_profile_photo(me) + # Retrieving messages from a chat + from telethon import utils + for message in client.get_message_history('username', limit=10): + print(utils.get_display_name(message.sender), message.message) + + # Listing all the dialogs (conversations you have open) + for dialog in client.get_dialogs(limit=10): + print(utils.get_display_name(dialog.entity), dialog.draft.message) + + # Downloading profile photos (default path is the working directory) + client.download_profile_photo('username') + + # Once you have a message with .media (if message.media) + # you can download it using client.download_media(): messages = client.get_message_history('username') client.download_media(messages[0]) **More details**: :ref:`telegram-client` + + +---------- + +You can continue by clicking on the "More details" link below each +snippet of code or the "Next" button at the bottom of the page. diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst index 945576d0..e74cdae6 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -29,7 +29,9 @@ You can also install the library directly from GitHub or a fork: $ cd Telethon/ # pip install -Ue . -If you don't have root access, simply pass the ``--user`` flag to the pip command. +If you don't have root access, simply pass the ``--user`` flag to the pip +command. If you want to install a specific branch, append ``@branch`` to +the end of the first install command. Manual Installation @@ -49,7 +51,8 @@ Manual Installation 5. Done! -To generate the documentation, ``cd docs`` and then ``python3 generate.py``. +To generate the `method documentation`__, ``cd docs`` and then +``python3 generate.py`` (if some pages render bad do it twice). Optional dependencies @@ -62,5 +65,6 @@ will also work without it. __ https://github.com/ricmoo/pyaes __ https://pypi.python.org/pypi/pyaes -__ https://github.com/sybrenstuvel/python-rsa/ +__ https://github.com/sybrenstuvel/python-rsa __ https://pypi.python.org/pypi/rsa/3.4.2 +__ https://lonamiwebs.github.io/Telethon diff --git a/readthedocs/extra/basic/telegram-client.rst b/readthedocs/extra/basic/telegram-client.rst index 5663f533..d3375200 100644 --- a/readthedocs/extra/basic/telegram-client.rst +++ b/readthedocs/extra/basic/telegram-client.rst @@ -43,30 +43,29 @@ how the library refers to either of these: lonami = client.get_entity('lonami') The so called "entities" are another important whole concept on its own, -and you should -Note that saving and using these entities will be more important when -Accessing the Full API. For now, this is a good way to get information -about an user or chat. +but for now you don't need to worry about it. Simply know that they are +a good way to get information about an user, chat or channel. -Other common methods for quick scripts are also available: +Many other common methods for quick scripts are also available: .. code-block:: python - # Sending a message (use an entity/username/etc) - client.send_message('TheAyyBot', 'ayy') + # Note that you can use 'me' or 'self' to message yourself + client.send_message('username', 'Hello World from Telethon!') - # Sending a photo, or a file - client.send_file(myself, '/path/to/the/file.jpg', force_document=True) + client.send_file('username', '/home/myself/Pictures/holidays.jpg') - # Downloading someone's profile photo. File is saved to 'where' - where = client.download_profile_photo(someone) + # The utils package has some goodies, like .get_display_name() + from telethon import utils + for message in client.get_message_history('username', limit=10): + print(utils.get_display_name(message.sender), message.message) - # Retrieving the message history - messages = client.get_message_history(someone) + # Dialogs are the conversations you have open + for dialog in client.get_dialogs(limit=10): + print(utils.get_display_name(dialog.entity), dialog.draft.message) - # Downloading the media from a specific message - # You can specify either a directory, a filename, or nothing at all - where = client.download_media(message, '/path/to/output') + # Default path is the working directory + client.download_profile_photo('username') # Call .disconnect() when you're done client.disconnect() diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index bb78eb97..72155d86 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -4,6 +4,12 @@ Working with Updates ==================== + +.. note:: + + There are plans to make working with updates more friendly. Stay tuned! + + .. contents:: diff --git a/readthedocs/extra/examples/bots.rst b/readthedocs/extra/examples/bots.rst index b231e200..fd4d54de 100644 --- a/readthedocs/extra/examples/bots.rst +++ b/readthedocs/extra/examples/bots.rst @@ -3,6 +3,11 @@ Bots ==== +.. note:: + + These examples assume you have read :ref:`accessing-the-full-api`. + + Talking to Inline Bots ********************** diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 99ce235f..be836b16 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -3,6 +3,11 @@ Working with Chats and Channels =============================== +.. note:: + + These examples assume you have read :ref:`accessing-the-full-api`. + + Joining a chat or channel ************************* diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 880bac6f..43492605 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -3,6 +3,11 @@ Working with messages ===================== +.. note:: + + These examples assume you have read :ref:`accessing-the-full-api`. + + Forwarding messages ******************* diff --git a/readthedocs/index.rst b/readthedocs/index.rst index cae75541..74c3b8e6 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -10,8 +10,12 @@ Welcome to Telethon's documentation! Pure Python 3 Telegram client library. Official Site `here `_. -Please follow the links below to get you started, and remember -to read the :ref:`changelog` when you upgrade! +Please follow the links on the index below to navigate from here, +or use the menu on the left. Remember to read the :ref:`changelog` +when you upgrade! + +.. important:: + If you're new here, you want to read :ref:`getting-started`. What is this? @@ -85,19 +89,20 @@ heavy job for you, so you can focus on developing an application. extra/developing/telegram-api-in-other-languages.rst -.. _Wall-of-shame: +.. _More: .. toctree:: :maxdepth: 2 - :caption: Wall of Shame + :caption: More + extra/changelog extra/wall-of-shame.rst .. toctree:: :caption: Telethon modules - telethon + modules Indices and tables diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst index 2d3c269c..e7a30c42 100644 --- a/readthedocs/telethon.rst +++ b/readthedocs/telethon.rst @@ -42,6 +42,13 @@ telethon\.utils module :undoc-members: :show-inheritance: +telethon\.session module +------------------------ + +.. automodule:: telethon.session + :members: + :undoc-members: + :show-inheritance: telethon\.cryto package ------------------------ @@ -58,21 +65,21 @@ telethon\.errors package telethon.errors telethon\.extensions package ------------------------- +---------------------------- .. toctree:: telethon.extensions telethon\.network package ------------------------- +------------------------- .. toctree:: telethon.network telethon\.tl package ------------------------- +-------------------- .. toctree:: diff --git a/readthedocs/telethon.tl.rst b/readthedocs/telethon.tl.rst index 6fbb1f00..a10ecc68 100644 --- a/readthedocs/telethon.tl.rst +++ b/readthedocs/telethon.tl.rst @@ -7,14 +7,6 @@ telethon\.tl package telethon.tl.custom -telethon\.tl\.entity\_database module -------------------------------------- - -.. automodule:: telethon.tl.entity_database - :members: - :undoc-members: - :show-inheritance: - telethon\.tl\.gzip\_packed module --------------------------------- @@ -31,14 +23,6 @@ telethon\.tl\.message\_container module :undoc-members: :show-inheritance: -telethon\.tl\.session module ----------------------------- - -.. automodule:: telethon.tl.session - :members: - :undoc-members: - :show-inheritance: - telethon\.tl\.tl\_message module -------------------------------- diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index f09a62fa..5fe186f3 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -406,7 +406,7 @@ class TelegramClient(TelegramBareClient): def log_out(self): """ - Logs out Telegram and deletes the current *.session file. + Logs out Telegram and deletes the current ``*.session`` file. Returns: True if the operation was successful. @@ -742,6 +742,10 @@ class TelegramClient(TelegramBareClient): # Add a few extra attributes to the Message to make it friendlier. messages.total = total_messages for m in messages: + # To make messages more friendly, always add message + # to service messages, and action to normal messages. + m.message = getattr(m, 'message', None) + m.action = getattr(m, 'action', None) m.sender = (None if not m.from_id else entities[utils.get_peer_id(m.from_id)]) diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index fd36ba8f..366a19bf 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -24,10 +24,7 @@ class Dialog: self.unread_count = dialog.unread_count self.unread_mentions_count = dialog.unread_mentions_count - if dialog.draft: - self.draft = Draft(client, dialog.peer, dialog.draft) - else: - self.draft = None + self.draft = Draft(client, dialog.peer, dialog.draft) def send_message(self, *args, **kwargs): """ diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index abf84548..ae08403a 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -1,16 +1,18 @@ from ..functions.messages import SaveDraftRequest -from ..types import UpdateDraftMessage +from ..types import UpdateDraftMessage, DraftMessage 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 ``client.get_drafts()``. """ def __init__(self, client, peer, draft): self._client = client self._peer = peer + if not draft: + draft = DraftMessage('', None, None, None, None) self.text = draft.message self.date = draft.date From 3379330f9b2872b8e7ca56f3e70872ec3986a39a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 12:25:31 +0100 Subject: [PATCH 055/108] Add an exact match list on the documentation --- docs/res/core.html | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/res/core.html b/docs/res/core.html index 8c8bc9d8..368a04d5 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -19,6 +19,11 @@ placeholder="Search for requests and types…" />
+
Methods (0)
    @@ -179,6 +184,10 @@ typesCount = document.getElementById("typesCount"); constructorsList = document.getElementById("constructorsList"); constructorsCount = document.getElementById("constructorsCount"); +// Exact match +exactMatch = document.getElementById("exactMatch"); +exactList = document.getElementById("exactList"); + try { requests = [{request_names}]; types = [{type_names}]; @@ -225,7 +234,9 @@ function buildList(countSpan, resultList, foundElements) { result += ''; } - countSpan.innerHTML = "" + foundElements[0].length; + if (countSpan) { + countSpan.innerHTML = "" + foundElements[0].length; + } resultList.innerHTML = result; } @@ -245,6 +256,26 @@ function updateSearch() { buildList(methodsCount, methodsList, foundRequests); buildList(typesCount, typesList, foundTypes); buildList(constructorsCount, constructorsList, foundConstructors); + + // Now look for exact matches + var original = requests.concat(constructors); + var originalu = requestsu.concat(constructorsu); + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().replace("request", "") == query) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + if (destination.length == 0) { + exactMatch.style.display = "none"; + } else { + exactMatch.style.display = ""; + buildList(null, exactList, [destination, destinationu]); + } } else { contentDiv.style.display = ""; searchDiv.style.display = "none"; From 644105d0384634918ac6137e9614d1f2f88ee431 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 13:11:22 +0100 Subject: [PATCH 056/108] Separate docs search into its own script and use it everywhere --- docs/docs_writer.py | 11 ++- docs/generate.py | 56 ++++++++------ docs/res/core.html | 162 +-------------------------------------- docs/res/js/search.js | 172 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 184 deletions(-) create mode 100644 docs/res/js/search.js diff --git a/docs/docs_writer.py b/docs/docs_writer.py index 9eec6cd7..82241a48 100644 --- a/docs/docs_writer.py +++ b/docs/docs_writer.py @@ -28,6 +28,7 @@ class DocsWriter: self.table_columns = 0 self.table_columns_left = None self.write_copy_script = False + self._script = '' # High level writing def write_head(self, title, relative_css_path): @@ -254,6 +255,12 @@ class DocsWriter: self.write('' .format(text_to_copy, text)) + def add_script(self, src='', relative_src=None): + if relative_src: + self._script += ''.format(relative_src) + elif src: + self._script += ''.format(src) + def end_body(self): """Ends the whole document. This should be called the last""" if self.write_copy_script: @@ -268,7 +275,9 @@ class DocsWriter: 'catch(e){}}' '') - self.write('
') + self.write('') + self.write(self._script) + self.write('') # "Low" level writing def write(self, s): diff --git a/docs/generate.py b/docs/generate.py index 4feb1518..ae2bd43c 100755 --- a/docs/generate.py +++ b/docs/generate.py @@ -224,6 +224,16 @@ def get_description(arg): return ' '.join(desc) +def copy_replace(src, dst, replacements): + """Copies the src file into dst applying the replacements dict""" + with open(src) as infile, open(dst, 'w') as outfile: + outfile.write(re.sub( + '|'.join(re.escape(k) for k in replacements), + lambda m: str(replacements[m.group(0)]), + infile.read() + )) + + def generate_documentation(scheme_file): """Generates the documentation HTML files from from scheme.tl to /methods and /constructors, etc. @@ -231,6 +241,7 @@ def generate_documentation(scheme_file): original_paths = { 'css': 'css/docs.css', 'arrow': 'img/arrow.svg', + 'search.js': 'js/search.js', '404': '404.html', 'index_all': 'index.html', 'index_types': 'types/index.html', @@ -366,6 +377,10 @@ def generate_documentation(scheme_file): else: docs.write_text('This type has no members.') + # TODO Bit hacky, make everything like this? (prepending '../') + depth = '../' * (2 if tlobject.namespace else 1) + docs.add_script(src='prependPath = "{}";'.format(depth)) + docs.add_script(relative_src=paths['search.js']) docs.end_body() # Find all the available types (which are not the same as the constructors) @@ -540,36 +555,31 @@ def generate_documentation(scheme_file): type_urls = fmt(types, get_path_for_type) constructor_urls = fmt(constructors, get_create_path_for) - replace_dict = { - 'type_count': len(types), - 'method_count': len(methods), - 'constructor_count': len(tlobjects) - len(methods), - 'layer': layer, - - 'request_names': request_names, - 'type_names': type_names, - 'constructor_names': constructor_names, - 'request_urls': request_urls, - 'type_urls': type_urls, - 'constructor_urls': constructor_urls - } - shutil.copy('../res/404.html', original_paths['404']) - - with open('../res/core.html') as infile,\ - open(original_paths['index_all'], 'w') as outfile: - text = infile.read() - for key, value in replace_dict.items(): - text = text.replace('{' + key + '}', str(value)) - - outfile.write(text) + copy_replace('../res/core.html', original_paths['index_all'], { + '{type_count}': len(types), + '{method_count}': len(methods), + '{constructor_count}': len(tlobjects) - len(methods), + '{layer}': layer, + }) + os.makedirs(os.path.abspath(os.path.join( + original_paths['search.js'], os.path.pardir + )), exist_ok=True) + copy_replace('../res/js/search.js', original_paths['search.js'], { + '{request_names}': request_names, + '{type_names}': type_names, + '{constructor_names}': constructor_names, + '{request_urls}': request_urls, + '{type_urls}': type_urls, + '{constructor_urls}': constructor_urls + }) # Everything done print('Documentation generated.') def copy_resources(): - for d in ['css', 'img']: + for d in ('css', 'img'): os.makedirs(d, exist_ok=True) shutil.copy('../res/img/arrow.svg', 'img') diff --git a/docs/res/core.html b/docs/res/core.html index 368a04d5..0d1673aa 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -14,34 +14,7 @@
- - - -
- - -
Methods (0) -
    -
-
- -
Types (0) -
    -
-
- -
Constructors (0) -
    -
-
-
- -
+

Telethon API

This documentation was generated straight from the scheme.tl provided by Telegram. However, there is no official documentation per se @@ -167,137 +140,6 @@ users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User> here.

- -
- + diff --git a/docs/res/js/search.js b/docs/res/js/search.js new file mode 100644 index 00000000..c63672e7 --- /dev/null +++ b/docs/res/js/search.js @@ -0,0 +1,172 @@ +root = document.getElementById("main_div"); +root.innerHTML = ` + + + +
+ + +
Methods (0) +
    +
+
+ +
Types (0) +
    +
+
+ +
Constructors (0) +
    +
+
+
+
+` + root.innerHTML + "
"; + +// HTML modified, now load documents +contentDiv = document.getElementById("contentDiv"); +searchDiv = document.getElementById("searchDiv"); +searchBox = document.getElementById("searchBox"); + +// Search lists +methodsList = document.getElementById("methodsList"); +methodsCount = document.getElementById("methodsCount"); + +typesList = document.getElementById("typesList"); +typesCount = document.getElementById("typesCount"); + +constructorsList = document.getElementById("constructorsList"); +constructorsCount = document.getElementById("constructorsCount"); + +// Exact match +exactMatch = document.getElementById("exactMatch"); +exactList = document.getElementById("exactList"); + +try { + requests = [{request_names}]; + types = [{type_names}]; + constructors = [{constructor_names}]; + + requestsu = [{request_urls}]; + typesu = [{type_urls}]; + constructorsu = [{constructor_urls}]; +} catch (e) { + requests = []; + types = []; + constructors = []; + requestsu = []; + typesu = []; + constructorsu = []; +} + +if (typeof prependPath !== 'undefined') { + for (var i = 0; i != requestsu.length; ++i) { + requestsu[i] = prependPath + requestsu[i]; + } + for (var i = 0; i != typesu.length; ++i) { + typesu[i] = prependPath + typesu[i]; + } + for (var i = 0; i != constructorsu.length; ++i) { + constructorsu[i] = prependPath + constructorsu[i]; + } +} + +// Given two input arrays "original" and "original urls" and a query, +// return a pair of arrays with matching "query" elements from "original". +// +// TODO Perhaps return an array of pairs instead a pair of arrays (for cache). +function getSearchArray(original, originalu, query) { + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().indexOf(query) != -1) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + return [destination, destinationu]; +} + +// Modify "countSpan" and "resultList" accordingly based on the elements +// given as [[elements], [element urls]] (both with the same length) +function buildList(countSpan, resultList, foundElements) { + var result = ""; + for (var i = 0; i < foundElements[0].length; ++i) { + result += '
  • '; + result += ''; + result += foundElements[0][i]; + result += '
  • '; + } + + if (countSpan) { + countSpan.innerHTML = "" + foundElements[0].length; + } + resultList.innerHTML = result; +} + +function updateSearch() { + if (searchBox.value) { + contentDiv.style.display = "none"; + searchDiv.style.display = ""; + + var query = searchBox.value.toLowerCase(); + + var foundRequests = getSearchArray(requests, requestsu, query); + var foundTypes = getSearchArray(types, typesu, query); + var foundConstructors = getSearchArray( + constructors, constructorsu, query + ); + + buildList(methodsCount, methodsList, foundRequests); + buildList(typesCount, typesList, foundTypes); + buildList(constructorsCount, constructorsList, foundConstructors); + + // Now look for exact matches + var original = requests.concat(constructors); + var originalu = requestsu.concat(constructorsu); + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().replace("request", "") == query) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + if (destination.length == 0) { + exactMatch.style.display = "none"; + } else { + exactMatch.style.display = ""; + buildList(null, exactList, [destination, destinationu]); + } + } else { + contentDiv.style.display = ""; + searchDiv.style.display = "none"; + } +} + +function getQuery(name) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i != vars.length; ++i) { + var pair = vars[i].split("="); + if (pair[0] == name) + return pair[1]; + } +} + +var query = getQuery('q'); +if (query) { + searchBox.value = query; +} + +updateSearch(); From 86816a3bdf7281866b89567f427810165e42c3b1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 19:29:05 +0100 Subject: [PATCH 057/108] Add missing InputChannel case on .get_input_peer() --- telethon/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index a4850d4c..d4be0e25 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -97,15 +97,18 @@ def get_input_peer(entity, allow_self=True): return InputPeerChannel(entity.id, entity.access_hash or 0) # Less common cases - if isinstance(entity, UserEmpty): - return InputPeerEmpty() - if isinstance(entity, InputUser): return InputPeerUser(entity.user_id, entity.access_hash) + if isinstance(entity, InputChannel): + return InputPeerChannel(entity.channel_id, entity.access_hash) + if isinstance(entity, InputUserSelf): return InputPeerSelf() + if isinstance(entity, UserEmpty): + return InputPeerEmpty() + if isinstance(entity, UserFull): return get_input_peer(entity.user) From f1371c3999c8936434f08c0ff774d3e9fb9e7528 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 19:39:48 +0100 Subject: [PATCH 058/108] Early return from Session.get_input_entity() if Input* given --- telethon/session.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/telethon/session.py b/telethon/session.py index f7170478..21c0e105 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -401,12 +401,16 @@ class Session: Raises ValueError if it cannot be found. """ - if isinstance(key, TLObject): - try: - # Try to early return if this key can be casted as input peer - return utils.get_input_peer(key) - except TypeError: - # Otherwise, get the ID of the peer + try: + if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): + # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) + # We already have an Input version, so nothing else required + return key + # Try to early return if this key can be casted as input peer + return utils.get_input_peer(key) + except (AttributeError, TypeError): + # Not a TLObject or can't be cast into InputPeer + if isinstance(key, TLObject): key = utils.get_peer_id(key) c = self._conn.cursor() From ec38bd94d8036f08ad99e2da901b94696f48ac1d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 20 Jan 2018 19:50:48 +0100 Subject: [PATCH 059/108] Fix .rst not showing code blocks on "unknown" languages --- .../extra/developing/understanding-the-type-language.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/readthedocs/extra/developing/understanding-the-type-language.rst b/readthedocs/extra/developing/understanding-the-type-language.rst index c82063ef..8e5259a7 100644 --- a/readthedocs/extra/developing/understanding-the-type-language.rst +++ b/readthedocs/extra/developing/understanding-the-type-language.rst @@ -10,9 +10,7 @@ what other programming languages commonly call classes or structs. Every definition is written as follows for a Telegram object is defined as follows: -.. code:: tl - - name#id argument_name:argument_type = CommonType + ``name#id argument_name:argument_type = CommonType`` This means that in a single line you know what the ``TLObject`` name is. You know it's unique ID, and you know what arguments it has. It really From 182b6fc1cb3ea1dd7ee9d29522bd0531344a81d0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 21 Jan 2018 10:57:58 +0100 Subject: [PATCH 060/108] Update old examples --- .../extra/examples/chats-and-channels.rst | 7 ++++--- .../extra/examples/working-with-messages.rst | 20 ++++++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index be836b16..30b94178 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -66,7 +66,7 @@ which use is very straightforward: client(AddChatUserRequest( chat_id, user_to_add, - fwd_limit=10 # allow the user to see the 10 last messages + fwd_limit=10 # Allow the user to see the 10 last messages )) @@ -111,8 +111,9 @@ a fixed limit: all_participants = [] while True: - participants = client.invoke(GetParticipantsRequest( - channel, ChannelParticipantsSearch(''), offset, limit + participants = client(GetParticipantsRequest( + channel, ChannelParticipantsSearch(''), offset, limit, + hash=0 )) if not participants.users: break diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 43492605..ab38788c 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -47,12 +47,26 @@ into issues_. A valid example would be: .. code-block:: python + from telethon.tl.functions.messages import SearchRequest + from telethon.tl.types import InputMessagesFilterEmpty + + filter = InputMessagesFilterEmpty() result = client(SearchRequest( - entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100 + peer=peer, # On which chat/conversation + q='query', # What to search for + filter=filter, # Filter to use (maybe filter for media) + min_date=None, # Minimum date + max_date=None, # Maximum date + offset_id=0, # ID of the message to use as offset + add_offset=0, # Additional offset + limit=10, # How many results + max_id=0, # Maximum message ID + min_id=0, # Minimum message ID + from_id=None # Who must have sent the message (peer) )) -It's important to note that the optional parameter ``from_id`` has been left -omitted and thus defaults to ``None``. Changing it to InputUserEmpty_, as one +It's important to note that the optional parameter ``from_id`` could have +been omitted (defaulting to ``None``). Changing it to InputUserEmpty_, as one could think to specify "no user", won't work because this parameter is a flag, and it being unspecified has a different meaning. From abe26625e6a733e1b6f73ecf5798539aacbaa1cf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 21 Jan 2018 11:04:46 +0100 Subject: [PATCH 061/108] Add missing ResolvedPeer, InputNotifyPeer, TopPeer cases --- telethon/utils.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index d4be0e25..d113da73 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -6,6 +6,7 @@ import math import re from mimetypes import add_type, guess_extension +from .tl.types.contacts import ResolvedPeer from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, @@ -20,7 +21,8 @@ from .tl.types import ( GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull, - InputMediaUploadedPhoto, DocumentAttributeFilename, photos + InputMediaUploadedPhoto, DocumentAttributeFilename, photos, + TopPeer, InputNotifyPeer ) USERNAME_RE = re.compile( @@ -362,8 +364,11 @@ def get_peer_id(peer): try: if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): - # Not a Peer or an InputPeer, so first get its Input version - peer = get_input_peer(peer, allow_self=False) + if isinstance(peer, (ResolvedPeer, InputNotifyPeer, TopPeer)): + peer = peer.peer + else: + # Not a Peer or an InputPeer, so first get its Input version + peer = get_input_peer(peer, allow_self=False) except AttributeError: _raise_cast_fail(peer, 'int') From 5f2f04c6c2676201cbb19e894d7d5bb3f3335ff0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 22 Jan 2018 11:06:11 +0200 Subject: [PATCH 062/108] Add HTML parse mode (#554) --- telethon/extensions/html.py | 167 ++++++++++++++++++++++++++++++++++++ telethon/telegram_client.py | 4 +- 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 telethon/extensions/html.py diff --git a/telethon/extensions/html.py b/telethon/extensions/html.py new file mode 100644 index 00000000..8cd170cb --- /dev/null +++ b/telethon/extensions/html.py @@ -0,0 +1,167 @@ +""" +Simple HTML -> Telegram entity parser. +""" +from html import escape, unescape +from html.parser import HTMLParser +from collections import deque + +from ..tl.types import ( + MessageEntityBold, MessageEntityItalic, MessageEntityCode, + MessageEntityPre, MessageEntityEmail, MessageEntityUrl, + MessageEntityTextUrl +) + + +class HTMLToTelegramParser(HTMLParser): + def __init__(self): + super().__init__() + self.text = '' + self.entities = [] + self._building_entities = {} + self._open_tags = deque() + self._open_tags_meta = deque() + + def handle_starttag(self, tag, attrs): + self._open_tags.appendleft(tag) + self._open_tags_meta.appendleft(None) + + attrs = dict(attrs) + EntityType = None + args = {} + if tag == 'strong' or tag == 'b': + EntityType = MessageEntityBold + elif tag == 'em' or tag == 'i': + EntityType = MessageEntityItalic + elif tag == 'code': + try: + # If we're in the middle of a
     tag, this  tag is
    +                # probably intended for syntax highlighting.
    +                #
    +                # Syntax highlighting is set with
    +                #     codeblock
    +                # inside 
     tags
    +                pre = self._building_entities['pre']
    +                try:
    +                    pre.language = attrs['class'][len('language-'):]
    +                except KeyError:
    +                    pass
    +            except KeyError:
    +                EntityType = MessageEntityCode
    +        elif tag == 'pre':
    +            EntityType = MessageEntityPre
    +            args['language'] = ''
    +        elif tag == 'a':
    +            try:
    +                url = attrs['href']
    +            except KeyError:
    +                return
    +            if url.startswith('mailto:'):
    +                url = url[len('mailto:'):]
    +                EntityType = MessageEntityEmail
    +            else:
    +                if self.get_starttag_text() == url:
    +                    EntityType = MessageEntityUrl
    +                else:
    +                    EntityType = MessageEntityTextUrl
    +                    args['url'] = url
    +                    url = None
    +            self._open_tags_meta.popleft()
    +            self._open_tags_meta.appendleft(url)
    +
    +        if EntityType and tag not in self._building_entities:
    +            self._building_entities[tag] = EntityType(
    +                offset=len(self.text),
    +                # The length will be determined when closing the tag.
    +                length=0,
    +                **args)
    +
    +    def handle_data(self, text):
    +        text = unescape(text)
    +
    +        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
    +        if previous_tag == 'a':
    +            url = self._open_tags_meta[0]
    +            if url:
    +                text = url
    +
    +        for tag, entity in self._building_entities.items():
    +            entity.length += len(text.strip('\n'))
    +
    +        self.text += text
    +
    +    def handle_endtag(self, tag):
    +        try:
    +            self._open_tags.popleft()
    +            self._open_tags_meta.popleft()
    +        except IndexError:
    +            pass
    +        entity = self._building_entities.pop(tag, None)
    +        if entity:
    +            self.entities.append(entity)
    +
    +
    +def parse(html):
    +    """
    +    Parses the given HTML message and returns its stripped representation
    +    plus a list of the MessageEntity's that were found.
    +
    +    :param message: the message with HTML to be parsed.
    +    :return: a tuple consisting of (clean message, [message entities]).
    +    """
    +    parser = HTMLToTelegramParser()
    +    parser.feed(html)
    +    return parser.text, parser.entities
    +
    +
    +def unparse(text, entities):
    +    """
    +    Performs the reverse operation to .parse(), effectively returning HTML
    +    given a normal text and its MessageEntity's.
    +
    +    :param text: the text to be reconverted into HTML.
    +    :param entities: the MessageEntity's applied to the text.
    +    :return: a HTML representation of the combination of both inputs.
    +    """
    +    if not entities:
    +        return text
    +    html = []
    +    last_offset = 0
    +    for entity in entities:
    +        if entity.offset > last_offset:
    +            html.append(escape(text[last_offset:entity.offset]))
    +        elif entity.offset < last_offset:
    +            continue
    +
    +        skip_entity = False
    +        entity_text = escape(text[entity.offset:entity.offset + entity.length])
    +        entity_type = type(entity)
    +
    +        if entity_type == MessageEntityBold:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityItalic:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityCode:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityPre:
    +            if entity.language:
    +                html.append(
    +                    "
    \n"
    +                    "    \n"
    +                    "        {}\n"
    +                    "    \n"
    +                    "
    ".format(entity.language, entity_text)) + else: + html.append('
    {}
    ' + .format(entity_text)) + elif entity_type == MessageEntityEmail: + html.append('{0}'.format(entity_text)) + elif entity_type == MessageEntityUrl: + html.append('{0}'.format(entity_text)) + elif entity_type == MessageEntityTextUrl: + html.append('{}' + .format(escape(entity.url), entity_text)) + else: + skip_entity = True + last_offset = entity.offset + (0 if skip_entity else entity.length) + html.append(text[last_offset:]) + return ''.join(html) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5fe186f3..67644a7e 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -70,7 +70,7 @@ from .tl.types import ( InputDocument, InputMediaDocument ) from .tl.types.messages import DialogsSlice -from .extensions import markdown +from .extensions import markdown, html __log__ = logging.getLogger(__name__) @@ -580,6 +580,8 @@ class TelegramClient(TelegramBareClient): parse_mode = parse_mode.lower() if parse_mode in {'md', 'markdown'}: message, msg_entities = markdown.parse(message) + elif parse_mode.startswith('htm'): + message, msg_entities = html.parse(message) else: raise ValueError('Unknown parsing mode: {}'.format(parse_mode)) else: From a437881ce235b1861a3e503a318fe23511228421 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 10:01:58 +0100 Subject: [PATCH 063/108] Note that date objects should be UTC --- docs/res/core.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/res/core.html b/docs/res/core.html index 0d1673aa..25295494 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -130,8 +130,12 @@ users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User>
  • date: Although this type is internally used as an int, - you can pass a datetime object instead to work - with date parameters. + you can pass a datetime or date object + instead to work with date parameters.
    + Note that the library uses the date in UTC+0, since timezone + conversion is not responsibility of the library. Furthermore, this + eases converting into any other timezone without the need for a middle + step.
  • From f0eb41b90235c7931cf995084c25cfa4116f6907 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 11:59:35 +0100 Subject: [PATCH 064/108] Accept message/media on .send_file, remove redundancy off README --- README.rst | 2 +- telethon/telegram_client.py | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 6343e6e1..febc43cd 100755 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Telethon ⭐️ Thanks **everyone** who has starred the project, it means a lot! **Telethon** is Telegram client implementation in **Python 3** which uses -the latest available API of Telegram. Remember to use **pip3** to install! +the latest available API of Telegram. What is this? diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 67644a7e..73a5b66c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,4 +1,5 @@ import hashlib +import io import itertools import logging import os @@ -29,7 +30,6 @@ from .errors import ( SessionPasswordNeededError, FileMigrateError ) from .network import ConnectionMode -from .tl import TLObject from .tl.custom import Draft, Dialog from .tl.functions.account import ( GetPasswordRequest @@ -915,6 +915,22 @@ class TelegramClient(TelegramBareClient): ) for x in file ] + entity = self.get_input_entity(entity) + reply_to = self._get_reply_to(reply_to) + + if not isinstance(file, (str, bytes, io.IOBase)): + # The user may pass a Message containing media (or the media, + # or anything similar) that should be treated as a file. Try + # getting the input media for whatever they passed and send it. + try: + media = utils.get_input_media(file, user_caption=caption) + except TypeError: + pass # Can't turn whatever was given into media + else: + request = SendMediaRequest(entity, media, + reply_to_msg_id=reply_to) + return self._get_response_message(request, self(request)) + as_image = utils.is_image(file) and not force_document use_cache = InputPhoto if as_image else InputDocument file_handle = self.upload_file( @@ -979,11 +995,7 @@ class TelegramClient(TelegramBareClient): # Once the media type is properly specified and the file uploaded, # send the media message to the desired entity. - request = SendMediaRequest( - peer=self.get_input_entity(entity), - media=media, - reply_to_msg_id=self._get_reply_to(reply_to) - ) + request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to) msg = self._get_response_message(request, self(request)) if msg and isinstance(file_handle, InputSizedFile): # There was a response message and we didn't use cached From 81c95b5a607cc16e00732d0e0113eece5873c016 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 12:04:35 +0100 Subject: [PATCH 065/108] Fix recursive .get_input_media() forgetting parameters --- telethon/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index d113da73..16257be2 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -282,9 +282,10 @@ def get_input_media(media, user_caption=None, is_photo=False): if isinstance(media, (ChatPhoto, UserProfilePhoto)): if isinstance(media.photo_big, FileLocationUnavailable): - return get_input_media(media.photo_small, is_photo=True) + media = media.photo_small else: - return get_input_media(media.photo_big, is_photo=True) + media = media.photo_big + return get_input_media(media, user_caption=user_caption, is_photo=True) if isinstance(media, MessageMediaContact): return InputMediaContact( @@ -312,7 +313,9 @@ def get_input_media(media, user_caption=None, is_photo=False): return InputMediaEmpty() if isinstance(media, Message): - return get_input_media(media.media) + return get_input_media( + media.media, user_caption=user_caption, is_photo=is_photo + ) _raise_cast_fail(media, 'InputMedia') From 58d90e7e340486bb89cc09bf2573cd4be77c4f37 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 12:10:23 +0100 Subject: [PATCH 066/108] Fix .download_media() not accepting Document --- telethon/telegram_client.py | 15 +++++++++------ telethon/utils.py | 14 +++++++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 73a5b66c..ad917f71 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -67,7 +67,7 @@ from .tl.types import ( PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf, InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, - InputDocument, InputMediaDocument + InputDocument, InputMediaDocument, Document ) from .tl.types.messages import DialogsSlice from .extensions import markdown, html @@ -1308,7 +1308,7 @@ class TelegramClient(TelegramBareClient): return self._download_photo( media, file, date, progress_callback ) - elif isinstance(media, MessageMediaDocument): + elif isinstance(media, (MessageMediaDocument, Document)): return self._download_document( media, file, date, progress_callback ) @@ -1319,7 +1319,6 @@ class TelegramClient(TelegramBareClient): def _download_photo(self, photo, file, date, progress_callback): """Specialized version of .download_media() for photos""" - # Determine the photo and its largest size if isinstance(photo, MessageMediaPhoto): photo = photo.photo @@ -1345,9 +1344,13 @@ class TelegramClient(TelegramBareClient): ) return file - def _download_document(self, mm_doc, file, date, progress_callback): + def _download_document(self, document, file, date, progress_callback): """Specialized version of .download_media() for documents""" - document = mm_doc.document + if isinstance(document, MessageMediaDocument): + document = document.document + if not isinstance(document, Document): + return + file_size = document.size possible_names = [] @@ -1361,7 +1364,7 @@ class TelegramClient(TelegramBareClient): )) file = self._get_proper_filename( - file, 'document', utils.get_extension(mm_doc), + file, 'document', utils.get_extension(document), date=date, possible_names=possible_names ) diff --git a/telethon/utils.py b/telethon/utils.py index 16257be2..3e310d3d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -61,13 +61,13 @@ def get_extension(media): # Documents will come with a mime type if isinstance(media, MessageMediaDocument): - if isinstance(media.document, Document): - if media.document.mime_type == 'application/octet-stream': - # Octet stream are just bytes, which have no default extension - return '' - else: - extension = guess_extension(media.document.mime_type) - return extension if extension else '' + media = media.document + if isinstance(media, Document): + if media.mime_type == 'application/octet-stream': + # Octet stream are just bytes, which have no default extension + return '' + else: + return guess_extension(media.mime_type) or '' return '' From 32b92b32a7dfd808c500a1a63b6a0e868ebff659 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 12:13:03 +0100 Subject: [PATCH 067/108] Update .send_file() documentation (for f0eb41b) --- telethon/telegram_client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ad917f71..e0a90d23 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -851,7 +851,7 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): Who will receive the file. - file (:obj:`str` | :obj:`bytes` | :obj:`file`): + file (:obj:`str` | :obj:`bytes` | :obj:`file` | :obj:`media`): The path of the file, byte array, or stream that will be sent. Note that if a byte array or a stream is given, a filename or its type won't be inferred, and it will be sent as an @@ -860,6 +860,10 @@ class TelegramClient(TelegramBareClient): Subsequent calls with the very same file will result in immediate uploads, unless ``.clear_file_cache()`` is called. + Furthermore the file may be any media (a message, document, + photo or similar) so that it can be resent without the need + to download and re-upload it again. + caption (:obj:`str`, optional): Optional caption for the sent media message. From 6c73538bd412717143b7fad459f529f81807b4fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 11:39:43 +0100 Subject: [PATCH 068/108] Fix time_offset not being used at all after BadMsgNotification Telegram would refuse to reply any further unless the message ID had the correct time (causing some behaviour like .connect() never connecting, due to the first request being sent always failing). The fix was to use time_offset when calculating the message ID, while this was right, it wasn't in use. --- telethon/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/session.py b/telethon/session.py index 21c0e105..5657f339 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -311,7 +311,7 @@ class Session: now = time.time() nanoseconds = int((now - int(now)) * 1e+9) # "message identifiers are divisible by 4" - new_msg_id = (int(now) << 32) | (nanoseconds << 2) + new_msg_id = ((int(now) + self.time_offset) << 32) | (nanoseconds << 2) with self._msg_id_lock: if self._last_msg_id >= new_msg_id: From db698858e0183692c993e20e69cf7fa31f972ffe Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 23 Jan 2018 22:25:52 +0100 Subject: [PATCH 069/108] Except TypeNotFoundError on ._invoke() --- telethon/telegram_bare_client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 9b756f43..11a8c184 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -501,6 +501,15 @@ class TelegramBareClient: __log__.error('Authorization key seems broken and was invalid!') self.session.auth_key = None + except TypeNotFoundError as e: + # Only occurs when we call receive. May happen when + # we need to reconnect to another DC on login and + # Telegram somehow sends old objects (like configOld) + self._first_request = True + __log__.warning('Read unknown TLObject code ({}). ' + 'Setting again first_request flag.' + .format(hex(e.invalid_constructor_id))) + except TimeoutError: __log__.warning('Invoking timed out') # We will just retry From 2873dcf1c6e914c026c2ed4259bcfc0f89c9026c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 25 Jan 2018 09:44:07 +0100 Subject: [PATCH 070/108] Add '_' key to TLObject's .to_dict() and remove recursive param Closes #559 --- telethon/tl/tlobject.py | 54 +++++++++++++++++------------- telethon_generator/tl_generator.py | 21 ++++++------ 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index ac0b65f8..87edd83e 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -20,15 +20,18 @@ class TLObject: """ if indent is None: if isinstance(obj, TLObject): - return '{}({})'.format(type(obj).__name__, ', '.join( - '{}={}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.to_dict(recursive=False).items() - )) + obj = obj.to_dict() + if isinstance(obj, dict): - return '{{{}}}'.format(', '.join( - '{}: {}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() - )) + if '_' in obj: + pre, left, right, sep = obj['_'], '(', ')', '{}={}' + else: + pre, left, right, sep = '', '{', '}', '{}: {}' + + mid = ', '.join(sep.format(k, TLObject.pretty_format(v)) + for k, v in obj.items() if not pre or k != '_') + return '{}{}{}{}'.format(pre, left, mid, right) + elif isinstance(obj, str) or isinstance(obj, bytes): return repr(obj) elif hasattr(obj, '__iter__'): @@ -43,30 +46,33 @@ class TLObject: return repr(obj) else: result = [] - if isinstance(obj, TLObject) or isinstance(obj, dict): - if isinstance(obj, dict): - d = obj - start, end, sep = '{', '}', ': ' - else: - d = obj.to_dict(recursive=False) - start, end, sep = '(', ')', '=' - result.append(type(obj).__name__) + if isinstance(obj, TLObject): + obj = obj.to_dict() - result.append(start) - if d: + if isinstance(obj, dict): + if '_' in obj: + pre, left, right, sep = obj['_'], '(', ')', '{}={}' + else: + pre, left, right, sep = '', '{', '}', '{}: {}' + + result.append(pre) + result.append(left) + if obj: result.append('\n') indent += 1 - for k, v in d.items(): + for k, v in obj.items(): + if pre and k == '_': + continue result.append('\t' * indent) - result.append(k) - result.append(sep) - result.append(TLObject.pretty_format(v, indent)) + result.append(sep.format( + k, TLObject.pretty_format(v, indent) + )) result.append(',\n') result.pop() # last ',\n' indent -= 1 result.append('\n') result.append('\t' * indent) - result.append(end) + result.append(right) elif isinstance(obj, str) or isinstance(obj, bytes): result.append(repr(obj)) @@ -158,7 +164,7 @@ class TLObject: def resolve(self, client, utils): pass - def to_dict(self, recursive=True): + def to_dict(self): return {} def __bytes__(self): diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index 18293ba1..ff12acfe 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -297,17 +297,16 @@ class TLGenerator: builder.end_block() # Write the to_dict(self) method - builder.writeln('def to_dict(self, recursive=True):') - if args: - builder.writeln('return {') - else: - builder.write('return {') + builder.writeln('def to_dict(self):') + builder.writeln('return {') builder.current_indent += 1 base_types = ('string', 'bytes', 'int', 'long', 'int128', 'int256', 'double', 'Bool', 'true', 'date') + builder.write("'_': '{}'".format(tlobject.class_name())) for arg in args: + builder.writeln(',') builder.write("'{}': ".format(arg.name)) if arg.type in base_types: if arg.is_vector: @@ -318,17 +317,17 @@ class TLGenerator: else: if arg.is_vector: builder.write( - '([] if self.{0} is None else [None' - ' if x is None else x.to_dict() for x in self.{0}]' - ') if recursive else self.{0}'.format(arg.name) + '[] if self.{0} is None else [None ' + 'if x is None else x.to_dict() for x in self.{0}]' + .format(arg.name) ) else: builder.write( - '(None if self.{0} is None else self.{0}.to_dict())' - ' if recursive else self.{0}'.format(arg.name) + 'None if self.{0} is None else self.{0}.to_dict()' + .format(arg.name) ) - builder.writeln(',') + builder.writeln() builder.current_indent -= 1 builder.writeln("}") From 4a83784fe8ebe3d8dcf041369f41d850f44549f3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 25 Jan 2018 09:51:12 +0100 Subject: [PATCH 071/108] Simplify TLObject.pretty_format since Telegram returns no dicts --- telethon/tl/tlobject.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 87edd83e..db1982c4 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -23,15 +23,10 @@ class TLObject: obj = obj.to_dict() if isinstance(obj, dict): - if '_' in obj: - pre, left, right, sep = obj['_'], '(', ')', '{}={}' - else: - pre, left, right, sep = '', '{', '}', '{}: {}' - - mid = ', '.join(sep.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() if not pre or k != '_') - return '{}{}{}{}'.format(pre, left, mid, right) - + return '{}({})'.format(obj.get('_', 'dict'), ', '.join( + '{}={}'.format(k, TLObject.pretty_format(v)) + for k, v in obj.items() if k != '_' + )) elif isinstance(obj, str) or isinstance(obj, bytes): return repr(obj) elif hasattr(obj, '__iter__'): @@ -50,29 +45,24 @@ class TLObject: obj = obj.to_dict() if isinstance(obj, dict): - if '_' in obj: - pre, left, right, sep = obj['_'], '(', ')', '{}={}' - else: - pre, left, right, sep = '', '{', '}', '{}: {}' - - result.append(pre) - result.append(left) + result.append(obj.get('_', 'dict')) + result.append('(') if obj: result.append('\n') indent += 1 for k, v in obj.items(): - if pre and k == '_': + if k == '_': continue result.append('\t' * indent) - result.append(sep.format( - k, TLObject.pretty_format(v, indent) - )) + result.append(k) + result.append('=') + result.append(TLObject.pretty_format(v, indent)) result.append(',\n') result.pop() # last ',\n' indent -= 1 result.append('\n') result.append('\t' * indent) - result.append(right) + result.append(')') elif isinstance(obj, str) or isinstance(obj, bytes): result.append(repr(obj)) From 5c2dfc17a8b8b0fc636d9aea199e41f9c41ab71e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 25 Jan 2018 18:44:21 +0100 Subject: [PATCH 072/108] Make timeout logging message debug to scare people less --- telethon/telegram_bare_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 11a8c184..c2955469 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -658,7 +658,7 @@ class TelegramBareClient: self._sender.receive(update_state=self.updates) except TimeoutError: # No problem - __log__.info('Receiving items from the network timed out') + __log__.debug('Receiving items from the network timed out') except ConnectionResetError: if self._user_connected: __log__.error('Connection was reset while receiving ' From 43a3f405271560d705d5c3cc1f7ffb647ce477b9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 26 Jan 2018 09:59:49 +0100 Subject: [PATCH 073/108] Properly close the sqlite3 connection (#560) --- telethon/session.py | 42 ++++++++++++++++++++++---------- telethon/telegram_bare_client.py | 1 + 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/telethon/session.py b/telethon/session.py index 5657f339..266d732e 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -7,7 +7,7 @@ import time from base64 import b64decode from enum import Enum from os.path import isfile as file_exists -from threading import Lock +from threading import Lock, RLock from . import utils from .crypto import AuthKey @@ -89,7 +89,7 @@ class Session: # Cross-thread safety self._seq_no_lock = Lock() self._msg_id_lock = Lock() - self._db_lock = Lock() + self._db_lock = RLock() # These values will be saved self._dc_id = 0 @@ -100,8 +100,8 @@ class Session: # Migrating from .json -> SQL entities = self._check_migrate_json() - self._conn = sqlite3.connect(self.filename, check_same_thread=False) - c = self._conn.cursor() + self._conn = None + c = self._cursor() c.execute("select name from sqlite_master " "where type='table' and name='version'") if c.fetchone(): @@ -186,7 +186,7 @@ class Session: return [] # No entities def _upgrade_database(self, old): - c = self._conn.cursor() + c = self._cursor() # old == 1 doesn't have the old sent_files so no need to drop if old == 2: # Old cache from old sent_files lasts then a day anyway, drop @@ -223,7 +223,7 @@ class Session: self._update_session_table() # Fetch the auth_key corresponding to this data center - c = self._conn.cursor() + c = self._cursor() c.execute('select auth_key from sessions') tuple_ = c.fetchone() if tuple_: @@ -251,7 +251,7 @@ class Session: def _update_session_table(self): with self._db_lock: - c = self._conn.cursor() + c = self._cursor() # While we can save multiple rows into the sessions table # currently we only want to keep ONE as the tables don't # tell us which auth_key's are usable and will work. Needs @@ -271,6 +271,22 @@ class Session: with self._db_lock: self._conn.commit() + def _cursor(self): + """Asserts that the connection is open and returns a cursor""" + with self._db_lock: + if self._conn is None: + self._conn = sqlite3.connect(self.filename, + check_same_thread=False) + return self._conn.cursor() + + def close(self): + """Closes the connection unless we're working in-memory""" + if self.filename != ':memory:': + with self._db_lock: + if self._conn is not None: + self._conn.close() + self._conn = None + def delete(self): """Deletes the current session file""" if self.filename == ':memory:': @@ -385,10 +401,10 @@ class Session: return with self._db_lock: - self._conn.executemany( + self._cursor().executemany( 'insert or replace into entities values (?,?,?,?,?)', rows ) - self.save() + self.save() def get_input_entity(self, key): """Parses the given string, integer or TLObject key into a @@ -413,7 +429,7 @@ class Session: if isinstance(key, TLObject): key = utils.get_peer_id(key) - c = self._conn.cursor() + c = self._cursor() if isinstance(key, str): phone = utils.parse_phone(key) if phone: @@ -444,7 +460,7 @@ class Session: # File processing def get_file(self, md5_digest, file_size, cls): - tuple_ = self._conn.execute( + tuple_ = self._cursor().execute( 'select id, hash from sent_files ' 'where md5_digest = ? and file_size = ? and type = ?', (md5_digest, file_size, _SentFileType.from_type(cls).value) @@ -458,10 +474,10 @@ class Session: raise TypeError('Cannot cache %s instance' % type(instance)) with self._db_lock: - self._conn.execute( + self._cursor().execute( 'insert or replace into sent_files values (?,?,?,?,?)', ( md5_digest, file_size, _SentFileType.from_type(type(instance)).value, instance.id, instance.access_hash )) - self.save() + self.save() diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index c2955469..fe63ab8a 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -253,6 +253,7 @@ class TelegramBareClient: # TODO Shall we clear the _exported_sessions, or may be reused? self._first_request = True # On reconnect it will be first again + self.session.close() def __del__(self): self.disconnect() From 3b8365f8716add070490e7aca46b728eb8cbc43b Mon Sep 17 00:00:00 2001 From: frizzlywitch Date: Fri, 26 Jan 2018 16:38:13 +0300 Subject: [PATCH 074/108] Remove square braces from IPv6 addresses (#561) --- telethon/extensions/tcp_client.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index d01c2b13..d4c45776 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -56,12 +56,7 @@ class TcpClient: :param port: the port to connect to. """ if ':' in ip: # IPv6 - # The address needs to be surrounded by [] as discussed on PR#425 - if not ip.startswith('['): - ip = '[' + ip - if not ip.endswith(']'): - ip = ip + ']' - + ip = ip.replace('[', '').replace(']', '') mode, address = socket.AF_INET6, (ip, port, 0, 0) else: mode, address = socket.AF_INET, (ip, port) From 067006d24833e592fad6183fdd8a805182dd4b10 Mon Sep 17 00:00:00 2001 From: Matteo Date: Sat, 27 Jan 2018 15:29:38 -0500 Subject: [PATCH 075/108] Add batch_size and wait_time to get_message_history (#565) --- telethon/telegram_client.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index e0a90d23..c2a91de7 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -646,7 +646,8 @@ class TelegramClient(TelegramBareClient): return self(messages.DeleteMessagesRequest(message_ids, revoke=revoke)) def get_message_history(self, entity, limit=20, offset_date=None, - offset_id=0, max_id=0, min_id=0, add_offset=0): + offset_id=0, max_id=0, min_id=0, add_offset=0, + batch_size=100, wait_time=1): """ Gets the message history for the specified entity @@ -681,6 +682,15 @@ class TelegramClient(TelegramBareClient): Additional message offset (all of the specified offsets + this offset = older messages). + batch_size (:obj:`int`): + Number of messages to be returned by each Telegram API + "getHistory" request. Notice that Telegram has a hard limit + of 100 messages per API call. + + wait_time (:obj:`int`): + Wait time between different "getHistory" requests. Use this + parameter to avoid hitting the "FloodWaitError" (see note below). + Returns: A list of messages with extra attributes: @@ -689,6 +699,16 @@ class TelegramClient(TelegramBareClient): * ``.fwd_from.sender`` = if fwd_from, who sent it originally. * ``.fwd_from.channel`` = if fwd_from, original channel. * ``.to`` = entity to which the message was sent. + + Notes: + Telegram limit for "getHistory" requests seems to be 3000 messages + within 30 seconds. Therefore, please adjust "batch_size" and + "wait_time" parameters accordingly to avoid incurring into a + "FloodWaitError". For example, if you plan to retrieve more than 3000 + messages (i.e. limit=3000 or None) in batches of 100 messages + (i.e. batch_size=100) please make sure to select a wait time of at + least one second (i.e. wait_time=1). + """ entity = self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -705,7 +725,7 @@ class TelegramClient(TelegramBareClient): entities = {} while len(messages) < limit: # Telegram has a hard limit of 100 - real_limit = min(limit - len(messages), 100) + real_limit = min(limit - len(messages), min(batch_size,100)) result = self(GetHistoryRequest( peer=entity, limit=real_limit, @@ -738,8 +758,7 @@ class TelegramClient(TelegramBareClient): # batches of 100 messages each request (since the FloodWait was # of 30 seconds). If the limit is greater than that, we will # sleep 1s between each request. - if limit > 3000: - time.sleep(1) + time.sleep(wait_time) # Add a few extra attributes to the Message to make it friendlier. messages.total = total_messages From 700b4c3169a7dbf64c64191ec43756b044428a06 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 27 Jan 2018 21:37:57 +0100 Subject: [PATCH 076/108] Fix-up #565 with some rewording/behaviour changes Such as not waiting unless strictly needed and better wording. --- telethon/telegram_client.py | 39 +++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index c2a91de7..18602032 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -647,7 +647,7 @@ class TelegramClient(TelegramBareClient): def get_message_history(self, entity, limit=20, offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0, - batch_size=100, wait_time=1): + batch_size=100, wait_time=None): """ Gets the message history for the specified entity @@ -683,13 +683,15 @@ class TelegramClient(TelegramBareClient): this offset = older messages). batch_size (:obj:`int`): - Number of messages to be returned by each Telegram API - "getHistory" request. Notice that Telegram has a hard limit - of 100 messages per API call. + Messages will be returned in chunks of this size (100 is + the maximum). While it makes no sense to modify this value, + you are still free to do so. wait_time (:obj:`int`): - Wait time between different "getHistory" requests. Use this - parameter to avoid hitting the "FloodWaitError" (see note below). + Wait time between different ``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. Returns: A list of messages with extra attributes: @@ -701,13 +703,11 @@ class TelegramClient(TelegramBareClient): * ``.to`` = entity to which the message was sent. Notes: - Telegram limit for "getHistory" requests seems to be 3000 messages - within 30 seconds. Therefore, please adjust "batch_size" and - "wait_time" parameters accordingly to avoid incurring into a - "FloodWaitError". For example, if you plan to retrieve more than 3000 - messages (i.e. limit=3000 or None) in batches of 100 messages - (i.e. batch_size=100) please make sure to select a wait time of at - least one second (i.e. wait_time=1). + Telegram's flood wait limit for ``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 + you think may be good. """ entity = self.get_input_entity(entity) @@ -720,12 +720,16 @@ class TelegramClient(TelegramBareClient): )) return getattr(result, 'count', len(result.messages)), [], [] + if wait_time is None: + wait_time = 1 if limit > 3000 else 0 + + batch_size = min(max(batch_size, 1), 100) total_messages = 0 messages = UserList() entities = {} while len(messages) < limit: # Telegram has a hard limit of 100 - real_limit = min(limit - len(messages), min(batch_size,100)) + real_limit = min(limit - len(messages), batch_size) result = self(GetHistoryRequest( peer=entity, limit=real_limit, @@ -741,8 +745,6 @@ class TelegramClient(TelegramBareClient): ) total_messages = getattr(result, 'count', len(result.messages)) - # TODO We can potentially use self.session.database, but since - # it might be disabled, use a local dictionary. for u in result.users: entities[utils.get_peer_id(u)] = u for c in result.chats: @@ -753,11 +755,6 @@ class TelegramClient(TelegramBareClient): offset_id = result.messages[-1].id offset_date = result.messages[-1].date - - # Telegram limit seems to be 3000 messages within 30 seconds in - # batches of 100 messages each request (since the FloodWait was - # of 30 seconds). If the limit is greater than that, we will - # sleep 1s between each request. time.sleep(wait_time) # Add a few extra attributes to the Message to make it friendlier. From 7286f77008af13d959a7a553a45d202a0ac305f2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 28 Jan 2018 14:02:42 +0100 Subject: [PATCH 077/108] Sort keys and use Mozilla agent on error generator, update file --- telethon_generator/error_generator.py | 16 +++++++++++++--- telethon_generator/errors.json | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index 5b14f22e..a56d4b91 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -26,7 +26,9 @@ known_codes = { def fetch_errors(output, url=URL): print('Opening a connection to', url, '...') - r = urllib.request.urlopen(url) + r = urllib.request.urlopen(urllib.request.Request( + url, headers={'User-Agent' : 'Mozilla/5.0'} + )) print('Checking response...') data = json.loads( r.read().decode(r.info().get_param('charset') or 'utf-8') @@ -34,11 +36,11 @@ def fetch_errors(output, url=URL): if data.get('ok'): print('Response was okay, saving data') with open(output, 'w', encoding='utf-8') as f: - json.dump(data, f) + json.dump(data, f, sort_keys=True) return True else: print('The data received was not okay:') - print(json.dumps(data, indent=4)) + print(json.dumps(data, indent=4, sort_keys=True)) return False @@ -164,3 +166,11 @@ def generate_code(output, json_file, errors_desc): for pattern, name in patterns: f.write(' {}: {},\n'.format(repr(pattern), name)) f.write('}\n') + + +if __name__ == '__main__': + if input('generate (y/n)?: ').lower() == 'y': + generate_code('../telethon/errors/rpc_error_list.py', + 'errors.json', 'error_descriptions') + elif input('fetch (y/n)?: ').lower() == 'y': + fetch_errors('errors.json') diff --git a/telethon_generator/errors.json b/telethon_generator/errors.json index e807ff2d..31d31c0c 100644 --- a/telethon_generator/errors.json +++ b/telethon_generator/errors.json @@ -1 +1 @@ -{"ok": true, "result": {"400": {"account.updateProfile": ["ABOUT_TOO_LONG", "FIRSTNAME_INVALID"], "auth.importBotAuthorization": ["ACCESS_TOKEN_EXPIRED", "ACCESS_TOKEN_INVALID", "API_ID_INVALID"], "auth.sendCode": ["API_ID_INVALID", "INPUT_REQUEST_TOO_LONG", "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN", "PHONE_NUMBER_BANNED", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_PASSWORD_PROTECTED"], "messages.setInlineBotResults": ["ARTICLE_TITLE_EMPTY", "BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "MESSAGE_EMPTY", "QUERY_ID_INVALID", "REPLY_MARKUP_INVALID", "RESULT_TYPE_INVALID", "SEND_MESSAGE_TYPE_INVALID", "START_PARAM_INVALID"], "auth.importAuthorization": ["AUTH_BYTES_INVALID", "USER_ID_INVALID"], "invokeWithLayer": ["AUTH_BYTES_INVALID", "CDN_METHOD_INVALID", "CONNECTION_API_ID_INVALID", "CONNECTION_LANG_PACK_INVALID", "INPUT_LAYER_INVALID"], "channels.inviteToChannel": ["BOT_GROUPS_BLOCKED", "BOTS_TOO_MUCH", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_USER_DEACTIVATED", "USER_BANNED_IN_CHANNEL", "USER_BLOCKED", "USER_BOT", "USER_ID_INVALID", "USER_KICKED", "USER_NOT_MUTUAL_CONTACT", "USERS_TOO_MUCH"], "messages.getInlineBotResults": ["BOT_INLINE_DISABLED", "BOT_INVALID"], "messages.startBot": ["BOT_INVALID", "PEER_ID_INVALID", "START_PARAM_EMPTY", "START_PARAM_INVALID"], "messages.uploadMedia": ["BOT_MISSING", "PEER_ID_INVALID"], "stickers.addStickerToSet": ["BOT_MISSING", "STICKERSET_INVALID"], "stickers.changeStickerPosition": ["BOT_MISSING", "STICKER_INVALID"], "stickers.createStickerSet": ["BOT_MISSING", "PACK_SHORT_NAME_INVALID", "PEER_ID_INVALID", "STICKERS_EMPTY", "USER_ID_INVALID"], "stickers.removeStickerFromSet": ["BOT_MISSING", "STICKER_INVALID"], "messages.sendMessage": ["BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "ENTITY_MENTION_USER_INVALID", "INPUT_USER_DEACTIVATED", "MESSAGE_EMPTY", "MESSAGE_TOO_LONG", "PEER_ID_INVALID", "REPLY_MARKUP_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "phone.acceptCall": ["CALL_ALREADY_ACCEPTED", "CALL_ALREADY_DECLINED", "CALL_PEER_INVALID", "CALL_PROTOCOL_FLAGS_INVALID"], "phone.discardCall": ["CALL_ALREADY_ACCEPTED", "CALL_PEER_INVALID"], "phone.confirmCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.receivedCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.saveCallDebug": ["CALL_PEER_INVALID", "DATA_JSON_INVALID"], "phone.setCallRating": ["CALL_PEER_INVALID"], "phone.requestCall": ["CALL_PROTOCOL_FLAGS_INVALID", "PARTICIPANT_VERSION_OUTDATED", "USER_ID_INVALID"], "updates.getDifference": ["CDN_METHOD_INVALID", "DATE_EMPTY", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID"], "upload.getCdnFileHashes": ["CDN_METHOD_INVALID", "RSA_DECRYPT_FAILED"], "channels.checkUsername": ["CHANNEL_INVALID", "CHAT_ID_INVALID", "USERNAME_INVALID"], "channels.deleteChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteUserHistory": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.editAbout": ["CHANNEL_INVALID", "CHAT_ABOUT_NOT_MODIFIED", "CHAT_ABOUT_TOO_LONG", "CHAT_ADMIN_REQUIRED"], "channels.editAdmin": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_CREATOR", "USER_ID_INVALID", "USER_NOT_MUTUAL_CONTACT"], "channels.editBanned": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ADMIN_INVALID", "USER_ID_INVALID"], "channels.editPhoto": ["CHANNEL_INVALID"], "channels.editTitle": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.exportInvite": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "INVITE_HASH_EXPIRED"], "channels.exportMessageLink": ["CHANNEL_INVALID"], "channels.getAdminLog": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.getChannels": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getFullChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "MESSAGE_IDS_EMPTY"], "channels.getParticipant": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_ID_INVALID", "USER_NOT_PARTICIPANT"], "channels.getParticipants": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID"], "channels.joinChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHANNELS_TOO_MUCH"], "channels.leaveChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "USER_CREATOR", "USER_NOT_PARTICIPANT"], "channels.readHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.readMessageContents": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.reportSpam": ["CHANNEL_INVALID"], "channels.setStickers": ["CHANNEL_INVALID", "PARTICIPANTS_TOO_FEW"], "channels.toggleInvites": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.toggleSignatures": ["CHANNEL_INVALID"], "channels.updatePinnedMessage": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.updateUsername": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USERNAME_INVALID", "USERNAME_OCCUPIED"], "messages.editMessage": ["CHANNEL_INVALID", "MESSAGE_EDIT_TIME_EXPIRED", "MESSAGE_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED", "PEER_ID_INVALID"], "messages.forwardMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "MEDIA_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_IDS_EMPTY", "PEER_ID_INVALID", "RANDOM_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.getHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerSettings": ["CHANNEL_INVALID", "PEER_ID_INVALID"], "messages.sendMedia": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "FILE_PART_0_MISSING", "FILE_PART_LENGTH_INVALID", "FILE_PARTS_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_CAPTION_TOO_LONG", "MEDIA_EMPTY", "PEER_ID_INVALID", "PHOTO_EXT_INVALID", "USER_IS_BLOCKED", "USER_IS_BOT", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.setTyping": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "updates.getChannelDifference": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID", "RANGES_INVALID"], "messages.getMessagesViews": ["CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerDialogs": ["CHANNEL_PRIVATE", "PEER_ID_INVALID"], "messages.addChatUser": ["CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_ALREADY_PARTICIPANT", "USER_ID_INVALID", "USERS_TOO_MUCH"], "messages.discardEncryption": ["CHAT_ID_EMPTY", "ENCRYPTION_ALREADY_DECLINED", "ENCRYPTION_ID_INVALID"], "messages.acceptEncryption": ["CHAT_ID_INVALID", "ENCRYPTION_ALREADY_ACCEPTED", "ENCRYPTION_ALREADY_DECLINED"], "messages.deleteChatUser": ["CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_NOT_PARTICIPANT"], "messages.editChatAdmin": ["CHAT_ID_INVALID"], "messages.editChatPhoto": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.editChatTitle": ["CHAT_ID_INVALID"], "messages.exportChatInvite": ["CHAT_ID_INVALID"], "messages.forwardMessage": ["CHAT_ID_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID", "YOU_BLOCKED_USER"], "messages.getChats": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getFullChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.migrateChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.reportEncryptedSpam": ["CHAT_ID_INVALID"], "messages.sendEncrypted": ["CHAT_ID_INVALID", "DATA_INVALID", "MSG_WAIT_FAILED"], "messages.setEncryptedTyping": ["CHAT_ID_INVALID"], "messages.toggleChatAdmins": ["CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "channels.createChannel": ["CHAT_TITLE_EMPTY"], "auth.recoverPassword": ["CODE_EMPTY"], "account.confirmPhone": ["CODE_HASH_INVALID", "PHONE_CODE_EMPTY"], "initConnection": ["CONNECTION_LAYER_INVALID", "INPUT_FETCH_FAIL"], "contacts.block": ["CONTACT_ID_INVALID"], "contacts.deleteContact": ["CONTACT_ID_INVALID"], "contacts.unblock": ["CONTACT_ID_INVALID"], "messages.getBotCallbackAnswer": ["DATA_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID"], "messages.sendEncryptedService": ["DATA_INVALID", "MSG_WAIT_FAILED"], "auth.exportAuthorization": ["DC_ID_INVALID"], "messages.requestEncryption": ["DH_G_A_INVALID", "USER_ID_INVALID"], "auth.bindTempAuthKey": ["ENCRYPTED_MESSAGE_INVALID", "INPUT_REQUEST_TOO_LONG", "TEMP_AUTH_KEY_EMPTY"], "messages.setBotPrecheckoutResults": ["ERROR_TEXT_EMPTY"], "contacts.importCard": ["EXPORT_CARD_INVALID"], "upload.getFile": ["FILE_ID_INVALID", "LIMIT_INVALID", "LOCATION_INVALID", "OFFSET_INVALID"], "photos.uploadProfilePhoto": ["FILE_PART_0_MISSING", "FILE_PARTS_INVALID", "IMAGE_PROCESS_FAILED", "PHOTO_CROP_SIZE_SMALL", "PHOTO_EXT_INVALID"], "upload.saveBigFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "FILE_PART_SIZE_INVALID", "FILE_PARTS_INVALID"], "upload.saveFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID"], "auth.signUp": ["FIRSTNAME_INVALID", "PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_OCCUPIED"], "messages.saveGif": ["GIF_ID_INVALID"], "account.resetAuthorization": ["HASH_INVALID"], "account.sendConfirmPhoneCode": ["HASH_INVALID"], "messages.sendInlineBotResult": ["INLINE_RESULT_EXPIRED", "PEER_ID_INVALID", "QUERY_ID_EMPTY"], "messages.getDialogs": ["INPUT_CONSTRUCTOR_INVALID", "OFFSET_PEER_ID_INVALID"], "messages.search": ["INPUT_CONSTRUCTOR_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "PEER_ID_NOT_SUPPORTED", "SEARCH_QUERY_EMPTY", "USER_ID_INVALID"], "messages.checkChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID"], "messages.importChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID", "USER_ALREADY_PARTICIPANT", "USERS_TOO_MUCH"], "{}": ["INVITE_HASH_EXPIRED"], "langpack.getDifference": ["LANG_PACK_INVALID"], "langpack.getLangPack": ["LANG_PACK_INVALID"], "langpack.getLanguages": ["LANG_PACK_INVALID"], "langpack.getStrings": ["LANG_PACK_INVALID"], "upload.getWebFile": ["LOCATION_INVALID"], "photos.getUserPhotos": ["MAX_ID_INVALID", "USER_ID_INVALID"], "auth.sendInvites": ["MESSAGE_EMPTY"], "messages.editInlineBotMessage": ["MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED"], "messages.getInlineGameHighScores": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setInlineGameScore": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "payments.getPaymentForm": ["MESSAGE_ID_INVALID"], "payments.getPaymentReceipt": ["MESSAGE_ID_INVALID"], "payments.sendPaymentForm": ["MESSAGE_ID_INVALID"], "payments.validateRequestedInfo": ["MESSAGE_ID_INVALID"], "messages.readEncryptedHistory": ["MSG_WAIT_FAILED"], "messages.receivedQueue": ["MSG_WAIT_FAILED"], "messages.sendEncryptedFile": ["MSG_WAIT_FAILED"], "account.updatePasswordSettings": ["NEW_SALT_INVALID", "NEW_SETTINGS_INVALID", "PASSWORD_HASH_INVALID"], "auth.requestPasswordRecovery": ["PASSWORD_EMPTY"], "account.getPasswordSettings": ["PASSWORD_HASH_INVALID"], "account.getTmpPassword": ["PASSWORD_HASH_INVALID", "TMP_PASSWORD_DISABLED"], "auth.checkPassword": ["PASSWORD_HASH_INVALID"], "account.getNotifySettings": ["PEER_ID_INVALID"], "account.reportPeer": ["PEER_ID_INVALID"], "account.updateNotifySettings": ["PEER_ID_INVALID"], "contacts.resetTopPeerRating": ["PEER_ID_INVALID"], "messages.deleteHistory": ["PEER_ID_INVALID"], "messages.getGameHighScores": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getMessageEditData": ["PEER_ID_INVALID"], "messages.getUnreadMentions": ["PEER_ID_INVALID"], "messages.hideReportSpam": ["PEER_ID_INVALID"], "messages.readHistory": ["PEER_ID_INVALID"], "messages.reorderPinnedDialogs": ["PEER_ID_INVALID"], "messages.reportSpam": ["PEER_ID_INVALID"], "messages.saveDraft": ["PEER_ID_INVALID"], "messages.sendScreenshotNotification": ["PEER_ID_INVALID"], "messages.setGameScore": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.toggleDialogPin": ["PEER_ID_INVALID"], "auth.signIn": ["PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_UNOCCUPIED"], "auth.checkPhone": ["PHONE_NUMBER_BANNED", "PHONE_NUMBER_INVALID"], "account.changePhone": ["PHONE_NUMBER_INVALID"], "account.sendChangePhoneCode": ["PHONE_NUMBER_INVALID"], "auth.cancelCode": ["PHONE_NUMBER_INVALID"], "auth.resendCode": ["PHONE_NUMBER_INVALID"], "account.getPrivacy": ["PRIVACY_KEY_INVALID"], "account.setPrivacy": ["PRIVACY_KEY_INVALID"], "bots.answerWebhookJSONQuery": ["QUERY_ID_INVALID", "USER_BOT_INVALID"], "messages.setBotCallbackAnswer": ["QUERY_ID_INVALID"], "messages.setBotShippingResults": ["QUERY_ID_INVALID"], "contacts.search": ["QUERY_TOO_SHORT", "SEARCH_QUERY_EMPTY"], "messages.getDhConfig": ["RANDOM_LENGTH_INVALID"], "upload.reuploadCdnFile": ["RSA_DECRYPT_FAILED"], "messages.searchGifs": ["SEARCH_QUERY_EMPTY"], "messages.searchGlobal": ["SEARCH_QUERY_EMPTY"], "messages.getDocumentByHash": ["SHA256_HASH_INVALID"], "messages.faveSticker": ["STICKER_ID_INVALID"], "messages.saveRecentSticker": ["STICKER_ID_INVALID"], "messages.getStickerSet": ["STICKERSET_INVALID"], "messages.installStickerSet": ["STICKERSET_INVALID"], "messages.uninstallStickerSet": ["STICKERSET_INVALID"], "account.registerDevice": ["TOKEN_INVALID"], "account.unregisterDevice": ["TOKEN_INVALID"], "account.setAccountTTL": ["TTL_DAYS_INVALID"], "contacts.getTopPeers": ["TYPES_EMPTY"], "bots.sendCustomRequest": ["USER_BOT_INVALID"], "messages.getCommonChats": ["USER_ID_INVALID"], "users.getFullUser": ["USER_ID_INVALID"], "account.checkUsername": ["USERNAME_INVALID"], "account.updateUsername": ["USERNAME_INVALID", "USERNAME_NOT_MODIFIED", "USERNAME_OCCUPIED"], "contacts.resolveUsername": ["USERNAME_INVALID", "USERNAME_NOT_OCCUPIED"], "messages.createChat": ["USERS_TOO_FEW"]}, "401": {"contacts.resolveUsername": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "messages.getHistory": ["AUTH_KEY_PERM_EMPTY"], "auth.signIn": ["SESSION_PASSWORD_NEEDED"], "messages.getDialogs": ["SESSION_PASSWORD_NEEDED"], "updates.getDifference": ["SESSION_PASSWORD_NEEDED"], "updates.getState": ["SESSION_PASSWORD_NEEDED"], "upload.saveFilePart": ["SESSION_PASSWORD_NEEDED"], "users.getUsers": ["SESSION_PASSWORD_NEEDED"]}, "500": {"auth.sendCode": ["AUTH_RESTART"], "phone.acceptCall": ["CALL_OCCUPY_FAILED"], "messages.acceptEncryption": ["ENCRYPTION_OCCUPY_FAILED"], "updates.getChannelDifference": ["HISTORY_GET_FAILED", "PERSISTENT_TIMESTAMP_OUTDATED"], "users.getUsers": ["MEMBER_NO_LOCATION", "NEED_MEMBER_INVALID"], "auth.signUp": ["MEMBER_OCCUPY_PRIMARY_LOC_FAILED", "REG_ID_GENERATE_FAILED"], "channels.getChannels": ["NEED_CHAT_INVALID"], "messages.editChatTitle": ["NEED_CHAT_INVALID"], "contacts.deleteContacts": ["NEED_MEMBER_INVALID"], "contacts.importCard": ["NEED_MEMBER_INVALID"], "updates.getDifference": ["NEED_MEMBER_INVALID"], "phone.requestCall": ["PARTICIPANT_CALL_FAILED"], "messages.forwardMessages": ["PTS_CHANGE_EMPTY", "RANDOM_ID_DUPLICATE"], "messages.sendMessage": ["RANDOM_ID_DUPLICATE"], "messages.sendMedia": ["STORAGE_CHECK_FAILED"], "upload.getCdnFile": ["UNKNOWN_METHOD"]}, "403": {"channels.getFullChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "updates.getChannelDifference": ["CHANNEL_PUBLIC_GROUP_NA"], "channels.editAdmin": ["CHAT_ADMIN_INVITE_REQUIRED", "RIGHT_FORBIDDEN", "USER_PRIVACY_RESTRICTED"], "messages.migrateChat": ["CHAT_ADMIN_REQUIRED"], "messages.forwardMessages": ["CHAT_SEND_GIFS_FORBIDDEN", "CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_SEND_STICKERS_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "channels.inviteToChannel": ["CHAT_WRITE_FORBIDDEN", "USER_CHANNELS_TOO_MUCH", "USER_PRIVACY_RESTRICTED"], "messages.editMessage": ["CHAT_WRITE_FORBIDDEN", "MESSAGE_AUTHOR_REQUIRED"], "messages.sendMedia": ["CHAT_WRITE_FORBIDDEN"], "messages.sendMessage": ["CHAT_WRITE_FORBIDDEN"], "messages.setTyping": ["CHAT_WRITE_FORBIDDEN"], "messages.getMessageEditData": ["MESSAGE_AUTHOR_REQUIRED"], "channels.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.setInlineBotResults": ["USER_BOT_INVALID"], "phone.requestCall": ["USER_IS_BLOCKED", "USER_PRIVACY_RESTRICTED"], "messages.addChatUser": ["USER_NOT_MUTUAL_CONTACT", "USER_PRIVACY_RESTRICTED"], "channels.createChannel": ["USER_RESTRICTED"], "messages.createChat": ["USER_RESTRICTED"]}, "406": {"auth.checkPhone": ["PHONE_NUMBER_INVALID"], "auth.sendCode": ["PHONE_PASSWORD_FLOOD"]}, "-503": {"auth.resetAuthorizations": ["Timeout"], "contacts.deleteContacts": ["Timeout"], "messages.forwardMessages": ["Timeout"], "messages.getBotCallbackAnswer": ["Timeout"], "messages.getHistory": ["Timeout"], "messages.getInlineBotResults": ["Timeout"], "updates.getState": ["Timeout"]}}, "human_result": {"-429": ["Too many requests"], "ABOUT_TOO_LONG": ["The provided bio is too long"], "ACCESS_TOKEN_EXPIRED": ["Bot token expired"], "ACCESS_TOKEN_INVALID": ["The provided token is not valid"], "ACTIVE_USER_REQUIRED": ["The method is only available to already activated users"], "API_ID_INVALID": ["The api_id/api_hash combination is invalid"], "ARTICLE_TITLE_EMPTY": ["The title of the article is empty"], "AUTH_BYTES_INVALID": ["The provided authorization is invalid"], "AUTH_KEY_PERM_EMPTY": ["The temporary auth key must be binded to the permanent auth key to use these methods."], "AUTH_KEY_UNREGISTERED": ["The authorization key has expired"], "AUTH_RESTART": ["Restart the authorization process"], "BOT_GROUPS_BLOCKED": ["This bot can't be added to groups"], "BOT_INLINE_DISABLED": ["This bot can't be used in inline mode"], "BOT_INVALID": ["This is not a valid bot"], "BOT_METHOD_INVALID": ["This method cannot be run by a bot"], "BOT_MISSING": ["This method can only be run by a bot"], "BOTS_TOO_MUCH": ["There are too many bots in this chat/channel"], "BUTTON_DATA_INVALID": ["The provided button data is invalid"], "BUTTON_TYPE_INVALID": ["The type of one of the buttons you provided is invalid"], "BUTTON_URL_INVALID": ["Button URL invalid"], "CALL_ALREADY_ACCEPTED": ["The call was already accepted"], "CALL_ALREADY_DECLINED": ["The call was already declined"], "CALL_OCCUPY_FAILED": ["The call failed because the user is already making another call"], "CALL_PEER_INVALID": ["The provided call peer object is invalid"], "CALL_PROTOCOL_FLAGS_INVALID": ["Call protocol flags invalid"], "CDN_METHOD_INVALID": ["You can't call this method in a CDN DC"], "CHANNEL_INVALID": ["The provided channel is invalid"], "CHANNEL_PRIVATE": ["You haven't joined this channel/supergroup"], "CHANNEL_PUBLIC_GROUP_NA": ["channel/supergroup not available"], "CHANNELS_TOO_MUCH": ["You have joined too many channels/supergroups"], "CHAT_ABOUT_NOT_MODIFIED": ["About text has not changed"], "CHAT_ADMIN_INVITE_REQUIRED": ["You do not have the rights to do this"], "CHAT_ADMIN_REQUIRED": ["You must be an admin in this chat to do this"], "CHAT_FORBIDDEN": ["You cannot write in this chat"], "CHAT_ID_EMPTY": ["The provided chat ID is empty"], "CHAT_ID_INVALID": ["The provided chat id is invalid"], "CHAT_NOT_MODIFIED": ["The pinned message wasn't modified"], "CHAT_SEND_GIFS_FORBIDDEN": ["You can't send gifs in this chat"], "CHAT_SEND_MEDIA_FORBIDDEN": ["You can't send media in this chat"], "CHAT_SEND_STICKERS_FORBIDDEN": ["You can't send stickers in this chat."], "CHAT_TITLE_EMPTY": ["No chat title provided"], "CHAT_WRITE_FORBIDDEN": ["You can't write in this chat"], "CODE_EMPTY": ["The provided code is empty"], "CODE_HASH_INVALID": ["Code hash invalid"], "CONNECTION_API_ID_INVALID": ["The provided API id is invalid"], "CONNECTION_LANG_PACK_INVALID": ["Language pack invalid"], "CONNECTION_LAYER_INVALID": ["Layer invalid"], "CONTACT_ID_INVALID": ["The provided contact ID is invalid"], "DATA_INVALID": ["Encrypted data invalid"], "DATA_JSON_INVALID": ["The provided JSON data is invalid"], "DATE_EMPTY": ["Date empty"], "DC_ID_INVALID": ["The provided DC ID is invalid"], "DH_G_A_INVALID": ["g_a invalid"], "ENCRYPTED_MESSAGE_INVALID": ["Encrypted message invalid"], "ENCRYPTION_ALREADY_ACCEPTED": ["Secret chat already accepted"], "ENCRYPTION_ALREADY_DECLINED": ["The secret chat was already declined"], "ENCRYPTION_ID_INVALID": ["The provided secret chat ID is invalid"], "ENCRYPTION_OCCUPY_FAILED": ["Internal server error while accepting secret chat"], "ENTITY_MENTION_USER_INVALID": ["You can't use this entity"], "ERROR_TEXT_EMPTY": ["The provided error message is empty"], "EXPORT_CARD_INVALID": ["Provided card is invalid"], "FIELD_NAME_EMPTY": ["The field with the name FIELD_NAME is missing"], "FIELD_NAME_INVALID": ["The field with the name FIELD_NAME is invalid"], "FILE_ID_INVALID": ["The provided file id is invalid"], "FILE_PART_0_MISSING": ["File part 0 missing"], "FILE_PART_EMPTY": ["The provided file part is empty"], "FILE_PART_INVALID": ["The file part number is invalid"], "FILE_PART_LENGTH_INVALID": ["The length of a file part is invalid"], "FILE_PART_SIZE_INVALID": ["The provided file part size is invalid"], "FILE_PARTS_INVALID": ["The number of file parts is invalid"], "FIRSTNAME_INVALID": ["The first name is invalid"], "FLOOD_WAIT_666": ["Spooky af m8"], "GIF_ID_INVALID": ["The provided GIF ID is invalid"], "HASH_INVALID": ["The provided hash is invalid"], "HISTORY_GET_FAILED": ["Fetching of history failed"], "IMAGE_PROCESS_FAILED": ["Failure while processing image"], "INLINE_RESULT_EXPIRED": ["The inline query expired"], "INPUT_CONSTRUCTOR_INVALID": ["The provided constructor is invalid"], "INPUT_FETCH_ERROR": ["An error occurred while deserializing TL parameters"], "INPUT_FETCH_FAIL": ["Failed deserializing TL payload"], "INPUT_LAYER_INVALID": ["The provided layer is invalid"], "INPUT_METHOD_INVALID": ["The provided method is invalid"], "INPUT_REQUEST_TOO_LONG": ["The request is too big"], "INPUT_USER_DEACTIVATED": ["The specified user was deleted"], "INTERDC_1_CALL_ERROR": ["An error occurred while communicating with DC 1"], "INTERDC_1_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 1"], "INTERDC_2_CALL_ERROR": ["An error occurred while communicating with DC 2"], "INTERDC_2_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 2"], "INTERDC_3_CALL_ERROR": ["An error occurred while communicating with DC 3"], "INTERDC_3_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 3"], "INTERDC_4_CALL_ERROR": ["An error occurred while communicating with DC 4"], "INTERDC_4_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 4"], "INTERDC_5_CALL_ERROR": ["An error occurred while communicating with DC 5"], "INTERDC_5_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 5"], "INVITE_HASH_EMPTY": ["The invite hash is empty"], "INVITE_HASH_EXPIRED": ["The invite link has expired"], "INVITE_HASH_INVALID": ["The invite hash is invalid"], "LANG_PACK_INVALID": ["The provided language pack is invalid"], "LASTNAME_INVALID": ["The last name is invalid"], "LIMIT_INVALID": ["The provided limit is invalid"], "LOCATION_INVALID": ["The provided location is invalid"], "MAX_ID_INVALID": ["The provided max ID is invalid"], "MD5_CHECKSUM_INVALID": ["The MD5 checksums do not match"], "MEDIA_CAPTION_TOO_LONG": ["The caption is too long"], "MEDIA_EMPTY": ["The provided media object is invalid"], "MEMBER_OCCUPY_PRIMARY_LOC_FAILED": ["Occupation of primary member location failed"], "MESSAGE_AUTHOR_REQUIRED": ["Message author required"], "MESSAGE_DELETE_FORBIDDEN": ["You can't delete one of the messages you tried to delete, most likely because it is a service message."], "MESSAGE_EDIT_TIME_EXPIRED": ["You can't edit this message anymore, too much time has passed since its creation."], "MESSAGE_EMPTY": ["The provided message is empty"], "MESSAGE_ID_INVALID": ["The provided message id is invalid"], "MESSAGE_IDS_EMPTY": ["No message ids were provided"], "MESSAGE_NOT_MODIFIED": ["The message text has not changed"], "MESSAGE_TOO_LONG": ["The provided message is too long"], "MSG_WAIT_FAILED": ["A waiting call returned an error"], "NEED_CHAT_INVALID": ["The provided chat is invalid"], "NEED_MEMBER_INVALID": ["The provided member is invalid"], "NEW_SALT_INVALID": ["The new salt is invalid"], "NEW_SETTINGS_INVALID": ["The new settings are invalid"], "OFFSET_INVALID": ["The provided offset is invalid"], "OFFSET_PEER_ID_INVALID": ["The provided offset peer is invalid"], "PACK_SHORT_NAME_INVALID": ["Short pack name invalid"], "PARTICIPANT_CALL_FAILED": ["Failure while making call"], "PARTICIPANT_VERSION_OUTDATED": ["The other participant does not use an up to date telegram client with support for calls"], "PARTICIPANTS_TOO_FEW": ["Not enough participants"], "PASSWORD_EMPTY": ["The provided password is empty"], "PASSWORD_HASH_INVALID": ["The provided password hash is invalid"], "PEER_FLOOD": ["Too many requests"], "PEER_ID_INVALID": ["The provided peer id is invalid"], "PEER_ID_NOT_SUPPORTED": ["The provided peer ID is not supported"], "PERSISTENT_TIMESTAMP_EMPTY": ["Persistent timestamp empty"], "PERSISTENT_TIMESTAMP_INVALID": ["Persistent timestamp invalid"], "PERSISTENT_TIMESTAMP_OUTDATED": ["Persistent timestamp outdated"], "PHONE_CODE_EMPTY": ["phone_code is missing"], "PHONE_CODE_EXPIRED": ["The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)"], "PHONE_CODE_HASH_EMPTY": ["phone_code_hash is missing"], "PHONE_CODE_INVALID": ["The provided phone code is invalid"], "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN": [""], "PHONE_NUMBER_BANNED": ["The provided phone number is banned from telegram"], "PHONE_NUMBER_FLOOD": ["You asked for the code too many times."], "PHONE_NUMBER_INVALID": ["The phone number is invalid"], "PHONE_NUMBER_OCCUPIED": ["The phone number is already in use"], "PHONE_NUMBER_UNOCCUPIED": ["The phone number is not yet being used"], "PHONE_PASSWORD_FLOOD": ["You have tried logging in too many times"], "PHONE_PASSWORD_PROTECTED": ["This phone is password protected"], "PHOTO_CROP_SIZE_SMALL": ["Photo is too small"], "PHOTO_EXT_INVALID": ["The extension of the photo is invalid"], "PHOTO_INVALID_DIMENSIONS": ["The photo dimensions are invalid"], "PRIVACY_KEY_INVALID": ["The privacy key is invalid"], "PTS_CHANGE_EMPTY": ["No PTS change"], "QUERY_ID_EMPTY": ["The query ID is empty"], "QUERY_ID_INVALID": ["The query ID is invalid"], "QUERY_TOO_SHORT": ["The query string is too short"], "RANDOM_ID_DUPLICATE": ["You provided a random ID that was already used"], "RANDOM_ID_INVALID": ["A provided random ID is invalid"], "RANDOM_LENGTH_INVALID": ["Random length invalid"], "RANGES_INVALID": ["Invalid range provided"], "REG_ID_GENERATE_FAILED": ["Failure while generating registration ID"], "REPLY_MARKUP_INVALID": ["The provided reply markup is invalid"], "RESULT_TYPE_INVALID": ["Result type invalid"], "RIGHT_FORBIDDEN": ["Your admin rights do not allow you to do this"], "RPC_CALL_FAIL": ["Telegram is having internal issues, please try again later."], "RPC_MCGET_FAIL": ["Telegram is having internal issues, please try again later."], "RSA_DECRYPT_FAILED": ["Internal RSA decryption failed"], "SEARCH_QUERY_EMPTY": ["The search query is empty"], "SEND_MESSAGE_TYPE_INVALID": ["The message type is invalid"], "SESSION_PASSWORD_NEEDED": ["2FA is enabled, use a password to login"], "SHA256_HASH_INVALID": ["The provided SHA256 hash is invalid"], "START_PARAM_EMPTY": ["The start parameter is empty"], "START_PARAM_INVALID": ["Start parameter invalid"], "STICKER_ID_INVALID": ["The provided sticker ID is invalid"], "STICKER_INVALID": ["The provided sticker is invalid"], "STICKERS_EMPTY": ["No sticker provided"], "STICKERSET_INVALID": ["The provided sticker set is invalid"], "STORAGE_CHECK_FAILED": ["Server storage check failed"], "TEMP_AUTH_KEY_EMPTY": ["No temporary auth key provided"], "Timeout": ["A timeout occurred while fetching data from the bot"], "TMP_PASSWORD_DISABLED": ["The temporary password is disabled"], "TOKEN_INVALID": ["The provided token is invalid"], "TTL_DAYS_INVALID": ["The provided TTL is invalid"], "TYPE_CONSTRUCTOR_INVALID": ["The type constructor is invalid"], "TYPES_EMPTY": ["The types field is empty"], "UNKNOWN_METHOD": ["The method you tried to call cannot be called on non-CDN DCs"], "USER_ADMIN_INVALID": ["You're not an admin"], "USER_ALREADY_PARTICIPANT": ["The user is already in the group"], "USER_BANNED_IN_CHANNEL": ["You're banned from sending messages in supergroups/channels"], "USER_BLOCKED": ["User blocked"], "USER_BOT": ["Bots can only be admins in channels."], "USER_BOT_INVALID": ["This method can only be called by a bot"], "USER_BOT_REQUIRED": ["This method can only be called by a bot"], "USER_CHANNELS_TOO_MUCH": ["One of the users you tried to add is already in too many channels/supergroups"], "USER_CREATOR": ["You can't leave this channel, because you're its creator"], "USER_DEACTIVATED": ["The user was deactivated"], "USER_ID_INVALID": ["The provided user ID is invalid"], "USER_IS_BLOCKED": ["User is blocked"], "USER_IS_BOT": ["Bots can't send messages to other bots"], "USER_KICKED": ["This user was kicked from this supergroup/channel"], "USER_NOT_MUTUAL_CONTACT": ["The provided user is not a mutual contact"], "USER_NOT_PARTICIPANT": ["You're not a member of this supergroup/channel"], "USER_PRIVACY_RESTRICTED": ["The user's privacy settings do not allow you to do this"], "USER_RESTRICTED": ["You're spamreported, you can't create channels or chats."], "USERNAME_INVALID": ["The provided username is not valid"], "USERNAME_NOT_MODIFIED": ["The username was not modified"], "USERNAME_NOT_OCCUPIED": ["The provided username is not occupied"], "USERNAME_OCCUPIED": ["The provided username is already occupied"], "USERS_TOO_FEW": ["Not enough users (to create a chat, for example)"], "USERS_TOO_MUCH": ["The maximum number of users has been exceeded (to create a chat, for example)"], "WEBPAGE_CURL_FAILED": ["Failure while fetching the webpage with cURL"], "YOU_BLOCKED_USER": ["You blocked this user"]}} \ No newline at end of file +{"human_result": {"-429": ["Too many requests"], "ABOUT_TOO_LONG": ["The provided bio is too long"], "ACCESS_TOKEN_EXPIRED": ["Bot token expired"], "ACCESS_TOKEN_INVALID": ["The provided token is not valid"], "ACTIVE_USER_REQUIRED": ["The method is only available to already activated users"], "ADMINS_TOO_MUCH": ["Too many admins"], "API_ID_INVALID": ["The api_id/api_hash combination is invalid"], "API_ID_PUBLISHED_FLOOD": ["This API id was published somewhere, you can't use it now"], "ARTICLE_TITLE_EMPTY": ["The title of the article is empty"], "AUTH_BYTES_INVALID": ["The provided authorization is invalid"], "AUTH_KEY_PERM_EMPTY": ["The temporary auth key must be binded to the permanent auth key to use these methods."], "AUTH_KEY_UNREGISTERED": ["The authorization key has expired"], "AUTH_RESTART": ["Restart the authorization process"], "BOTS_TOO_MUCH": ["There are too many bots in this chat/channel"], "BOT_CHANNELS_NA": ["Bots can't edit admin privileges"], "BOT_GROUPS_BLOCKED": ["This bot can't be added to groups"], "BOT_INLINE_DISABLED": ["This bot can't be used in inline mode"], "BOT_INVALID": ["This is not a valid bot"], "BOT_METHOD_INVALID": ["This method cannot be run by a bot"], "BOT_MISSING": ["This method can only be run by a bot"], "BUTTON_DATA_INVALID": ["The provided button data is invalid"], "BUTTON_TYPE_INVALID": ["The type of one of the buttons you provided is invalid"], "BUTTON_URL_INVALID": ["Button URL invalid"], "CALL_ALREADY_ACCEPTED": ["The call was already accepted"], "CALL_ALREADY_DECLINED": ["The call was already declined"], "CALL_OCCUPY_FAILED": ["The call failed because the user is already making another call"], "CALL_PEER_INVALID": ["The provided call peer object is invalid"], "CALL_PROTOCOL_FLAGS_INVALID": ["Call protocol flags invalid"], "CDN_METHOD_INVALID": ["You can't call this method in a CDN DC"], "CHANNELS_ADMIN_PUBLIC_TOO_MUCH": ["You're admin of too many public channels, make some channels private to change the username of this channel"], "CHANNELS_TOO_MUCH": ["You have joined too many channels/supergroups"], "CHANNEL_INVALID": ["The provided channel is invalid"], "CHANNEL_PRIVATE": ["You haven't joined this channel/supergroup"], "CHANNEL_PUBLIC_GROUP_NA": ["channel/supergroup not available"], "CHAT_ABOUT_NOT_MODIFIED": ["About text has not changed"], "CHAT_ABOUT_TOO_LONG": ["Chat about too long"], "CHAT_ADMIN_INVITE_REQUIRED": ["You do not have the rights to do this"], "CHAT_ADMIN_REQUIRED": ["You must be an admin in this chat to do this"], "CHAT_FORBIDDEN": ["You cannot write in this chat"], "CHAT_ID_EMPTY": ["The provided chat ID is empty"], "CHAT_ID_INVALID": ["The provided chat id is invalid"], "CHAT_NOT_MODIFIED": ["The pinned message wasn't modified"], "CHAT_SEND_GIFS_FORBIDDEN": ["You can't send gifs in this chat"], "CHAT_SEND_MEDIA_FORBIDDEN": ["You can't send media in this chat"], "CHAT_SEND_STICKERS_FORBIDDEN": ["You can't send stickers in this chat."], "CHAT_TITLE_EMPTY": ["No chat title provided"], "CHAT_WRITE_FORBIDDEN": ["You can't write in this chat"], "CODE_EMPTY": ["The provided code is empty"], "CODE_HASH_INVALID": ["Code hash invalid"], "CONNECTION_API_ID_INVALID": ["The provided API id is invalid"], "CONNECTION_DEVICE_MODEL_EMPTY": ["Device model empty"], "CONNECTION_LANG_PACK_INVALID": ["Language pack invalid"], "CONNECTION_LAYER_INVALID": ["Layer invalid"], "CONNECTION_NOT_INITED": ["Connection not initialized"], "CONNECTION_SYSTEM_EMPTY": ["Connection system empty"], "CONTACT_ID_INVALID": ["The provided contact ID is invalid"], "DATA_INVALID": ["Encrypted data invalid"], "DATA_JSON_INVALID": ["The provided JSON data is invalid"], "DATE_EMPTY": ["Date empty"], "DC_ID_INVALID": ["The provided DC ID is invalid"], "DH_G_A_INVALID": ["g_a invalid"], "EMAIL_UNCONFIRMED": ["Email unconfirmed"], "ENCRYPTED_MESSAGE_INVALID": ["Encrypted message invalid"], "ENCRYPTION_ALREADY_ACCEPTED": ["Secret chat already accepted"], "ENCRYPTION_ALREADY_DECLINED": ["The secret chat was already declined"], "ENCRYPTION_DECLINED": ["The secret chat was declined"], "ENCRYPTION_ID_INVALID": ["The provided secret chat ID is invalid"], "ENCRYPTION_OCCUPY_FAILED": ["Internal server error while accepting secret chat"], "ENTITY_MENTION_USER_INVALID": ["You can't use this entity"], "ERROR_TEXT_EMPTY": ["The provided error message is empty"], "EXPORT_CARD_INVALID": ["Provided card is invalid"], "EXTERNAL_URL_INVALID": ["External URL invalid"], "FIELD_NAME_EMPTY": ["The field with the name FIELD_NAME is missing"], "FIELD_NAME_INVALID": ["The field with the name FIELD_NAME is invalid"], "FILE_ID_INVALID": ["The provided file id is invalid"], "FILE_PARTS_INVALID": ["The number of file parts is invalid"], "FILE_PART_0_MISSING": ["File part 0 missing"], "FILE_PART_122_MISSING": [""], "FILE_PART_154_MISSING": [""], "FILE_PART_458_MISSING": [""], "FILE_PART_468_MISSING": [""], "FILE_PART_504_MISSING": [""], "FILE_PART_6_MISSING": ["File part 6 missing"], "FILE_PART_72_MISSING": [""], "FILE_PART_94_MISSING": [""], "FILE_PART_EMPTY": ["The provided file part is empty"], "FILE_PART_INVALID": ["The file part number is invalid"], "FILE_PART_LENGTH_INVALID": ["The length of a file part is invalid"], "FILE_PART_SIZE_INVALID": ["The provided file part size is invalid"], "FIRSTNAME_INVALID": ["The first name is invalid"], "FLOOD_WAIT_666": ["Spooky af m8"], "GIF_ID_INVALID": ["The provided GIF ID is invalid"], "GROUPED_MEDIA_INVALID": ["Invalid grouped media"], "HASH_INVALID": ["The provided hash is invalid"], "HISTORY_GET_FAILED": ["Fetching of history failed"], "IMAGE_PROCESS_FAILED": ["Failure while processing image"], "INLINE_RESULT_EXPIRED": ["The inline query expired"], "INPUT_CONSTRUCTOR_INVALID": ["The provided constructor is invalid"], "INPUT_FETCH_ERROR": ["An error occurred while deserializing TL parameters"], "INPUT_FETCH_FAIL": ["Failed deserializing TL payload"], "INPUT_LAYER_INVALID": ["The provided layer is invalid"], "INPUT_METHOD_INVALID": ["The provided method is invalid"], "INPUT_REQUEST_TOO_LONG": ["The request is too big"], "INPUT_USER_DEACTIVATED": ["The specified user was deleted"], "INTERDC_1_CALL_ERROR": ["An error occurred while communicating with DC 1"], "INTERDC_1_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 1"], "INTERDC_2_CALL_ERROR": ["An error occurred while communicating with DC 2"], "INTERDC_2_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 2"], "INTERDC_3_CALL_ERROR": ["An error occurred while communicating with DC 3"], "INTERDC_3_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 3"], "INTERDC_4_CALL_ERROR": ["An error occurred while communicating with DC 4"], "INTERDC_4_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 4"], "INTERDC_5_CALL_ERROR": ["An error occurred while communicating with DC 5"], "INTERDC_5_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 5"], "INVITE_HASH_EMPTY": ["The invite hash is empty"], "INVITE_HASH_EXPIRED": ["The invite link has expired"], "INVITE_HASH_INVALID": ["The invite hash is invalid"], "LANG_PACK_INVALID": ["The provided language pack is invalid"], "LASTNAME_INVALID": ["The last name is invalid"], "LIMIT_INVALID": ["The provided limit is invalid"], "LOCATION_INVALID": ["The provided location is invalid"], "MAX_ID_INVALID": ["The provided max ID is invalid"], "MD5_CHECKSUM_INVALID": ["The MD5 checksums do not match"], "MEDIA_CAPTION_TOO_LONG": ["The caption is too long"], "MEDIA_EMPTY": ["The provided media object is invalid"], "MEDIA_INVALID": ["Media invalid"], "MEMBER_NO_LOCATION": ["An internal failure occurred while fetching user info (couldn't find location)"], "MEMBER_OCCUPY_PRIMARY_LOC_FAILED": ["Occupation of primary member location failed"], "MESSAGE_AUTHOR_REQUIRED": ["Message author required"], "MESSAGE_DELETE_FORBIDDEN": ["You can't delete one of the messages you tried to delete, most likely because it is a service message."], "MESSAGE_EDIT_TIME_EXPIRED": ["You can't edit this message anymore, too much time has passed since its creation."], "MESSAGE_EMPTY": ["The provided message is empty"], "MESSAGE_IDS_EMPTY": ["No message ids were provided"], "MESSAGE_ID_INVALID": ["The provided message id is invalid"], "MESSAGE_NOT_MODIFIED": ["The message text has not changed"], "MESSAGE_TOO_LONG": ["The provided message is too long"], "MSG_WAIT_FAILED": ["A waiting call returned an error"], "NEED_CHAT_INVALID": ["The provided chat is invalid"], "NEED_MEMBER_INVALID": ["The provided member is invalid"], "NEW_SALT_INVALID": ["The new salt is invalid"], "NEW_SETTINGS_INVALID": ["The new settings are invalid"], "OFFSET_INVALID": ["The provided offset is invalid"], "OFFSET_PEER_ID_INVALID": ["The provided offset peer is invalid"], "PACK_SHORT_NAME_INVALID": ["Short pack name invalid"], "PACK_SHORT_NAME_OCCUPIED": ["A stickerpack with this name already exists"], "PARTICIPANTS_TOO_FEW": ["Not enough participants"], "PARTICIPANT_CALL_FAILED": ["Failure while making call"], "PARTICIPANT_VERSION_OUTDATED": ["The other participant does not use an up to date telegram client with support for calls"], "PASSWORD_EMPTY": ["The provided password is empty"], "PASSWORD_HASH_INVALID": ["The provided password hash is invalid"], "PEER_FLOOD": ["Too many requests"], "PEER_ID_INVALID": ["The provided peer id is invalid"], "PEER_ID_NOT_SUPPORTED": ["The provided peer ID is not supported"], "PERSISTENT_TIMESTAMP_EMPTY": ["Persistent timestamp empty"], "PERSISTENT_TIMESTAMP_INVALID": ["Persistent timestamp invalid"], "PERSISTENT_TIMESTAMP_OUTDATED": ["Persistent timestamp outdated"], "PHONE_CODE_EMPTY": ["phone_code is missing"], "PHONE_CODE_EXPIRED": ["The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)"], "PHONE_CODE_HASH_EMPTY": ["phone_code_hash is missing"], "PHONE_CODE_INVALID": ["The provided phone code is invalid"], "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN": [""], "PHONE_NUMBER_BANNED": ["The provided phone number is banned from telegram"], "PHONE_NUMBER_FLOOD": ["You asked for the code too many times."], "PHONE_NUMBER_INVALID": ["The phone number is invalid"], "PHONE_NUMBER_OCCUPIED": ["The phone number is already in use"], "PHONE_NUMBER_UNOCCUPIED": ["The phone number is not yet being used"], "PHONE_PASSWORD_FLOOD": ["You have tried logging in too many times"], "PHONE_PASSWORD_PROTECTED": ["This phone is password protected"], "PHOTO_CROP_SIZE_SMALL": ["Photo is too small"], "PHOTO_EXT_INVALID": ["The extension of the photo is invalid"], "PHOTO_INVALID": ["Photo invalid"], "PHOTO_INVALID_DIMENSIONS": ["The photo dimensions are invalid"], "PRIVACY_KEY_INVALID": ["The privacy key is invalid"], "PTS_CHANGE_EMPTY": ["No PTS change"], "QUERY_ID_EMPTY": ["The query ID is empty"], "QUERY_ID_INVALID": ["The query ID is invalid"], "QUERY_TOO_SHORT": ["The query string is too short"], "RANDOM_ID_DUPLICATE": ["You provided a random ID that was already used"], "RANDOM_ID_INVALID": ["A provided random ID is invalid"], "RANDOM_LENGTH_INVALID": ["Random length invalid"], "RANGES_INVALID": ["Invalid range provided"], "REG_ID_GENERATE_FAILED": ["Failure while generating registration ID"], "REPLY_MARKUP_INVALID": ["The provided reply markup is invalid"], "RESULT_TYPE_INVALID": ["Result type invalid"], "RIGHT_FORBIDDEN": ["Your admin rights do not allow you to do this"], "RPC_CALL_FAIL": ["Telegram is having internal issues, please try again later."], "RPC_MCGET_FAIL": ["Telegram is having internal issues, please try again later."], "RSA_DECRYPT_FAILED": ["Internal RSA decryption failed"], "SEARCH_QUERY_EMPTY": ["The search query is empty"], "SEND_MESSAGE_TYPE_INVALID": ["The message type is invalid"], "SESSION_PASSWORD_NEEDED": ["2FA is enabled, use a password to login"], "SHA256_HASH_INVALID": ["The provided SHA256 hash is invalid"], "START_PARAM_EMPTY": ["The start parameter is empty"], "START_PARAM_INVALID": ["Start parameter invalid"], "STICKERSET_INVALID": ["The provided sticker set is invalid"], "STICKERS_EMPTY": ["No sticker provided"], "STICKER_EMOJI_INVALID": ["Sticker emoji invalid"], "STICKER_FILE_INVALID": ["Sticker file invalid"], "STICKER_ID_INVALID": ["The provided sticker ID is invalid"], "STICKER_INVALID": ["The provided sticker is invalid"], "STICKER_PNG_DIMENSIONS": ["Sticker png dimensions invalid"], "STORAGE_CHECK_FAILED": ["Server storage check failed"], "STORE_INVALID_SCALAR_TYPE": [""], "TEMP_AUTH_KEY_EMPTY": ["No temporary auth key provided"], "TMP_PASSWORD_DISABLED": ["The temporary password is disabled"], "TOKEN_INVALID": ["The provided token is invalid"], "TTL_DAYS_INVALID": ["The provided TTL is invalid"], "TYPES_EMPTY": ["The types field is empty"], "TYPE_CONSTRUCTOR_INVALID": ["The type constructor is invalid"], "Timeout": ["A timeout occurred while fetching data from the bot"], "UNKNOWN_METHOD": ["The method you tried to call cannot be called on non-CDN DCs"], "USERNAME_INVALID": ["The provided username is not valid"], "USERNAME_NOT_MODIFIED": ["The username was not modified"], "USERNAME_NOT_OCCUPIED": ["The provided username is not occupied"], "USERNAME_OCCUPIED": ["The provided username is already occupied"], "USERS_TOO_FEW": ["Not enough users (to create a chat, for example)"], "USERS_TOO_MUCH": ["The maximum number of users has been exceeded (to create a chat, for example)"], "USER_ADMIN_INVALID": ["You're not an admin"], "USER_ALREADY_PARTICIPANT": ["The user is already in the group"], "USER_BANNED_IN_CHANNEL": ["You're banned from sending messages in supergroups/channels"], "USER_BLOCKED": ["User blocked"], "USER_BOT": ["Bots can only be admins in channels."], "USER_BOT_INVALID": ["This method can only be called by a bot"], "USER_BOT_REQUIRED": ["This method can only be called by a bot"], "USER_CHANNELS_TOO_MUCH": ["One of the users you tried to add is already in too many channels/supergroups"], "USER_CREATOR": ["You can't leave this channel, because you're its creator"], "USER_DEACTIVATED": ["The user was deactivated"], "USER_ID_INVALID": ["The provided user ID is invalid"], "USER_IS_BLOCKED": ["User is blocked"], "USER_IS_BOT": ["Bots can't send messages to other bots"], "USER_KICKED": ["This user was kicked from this supergroup/channel"], "USER_NOT_MUTUAL_CONTACT": ["The provided user is not a mutual contact"], "USER_NOT_PARTICIPANT": ["You're not a member of this supergroup/channel"], "USER_PRIVACY_RESTRICTED": ["The user's privacy settings do not allow you to do this"], "USER_RESTRICTED": ["You're spamreported, you can't create channels or chats."], "WC_CONVERT_URL_INVALID": ["WC convert URL invalid"], "WEBPAGE_CURL_FAILED": ["Failure while fetching the webpage with cURL"], "WEBPAGE_MEDIA_EMPTY": ["Webpage media empty"], "YOU_BLOCKED_USER": ["You blocked this user"]}, "ok": true, "result": {"-503": {"auth.bindTempAuthKey": ["Timeout"], "auth.resetAuthorizations": ["Timeout"], "channels.getFullChannel": ["Timeout"], "channels.getParticipants": ["Timeout"], "contacts.deleteContacts": ["Timeout"], "contacts.search": ["Timeout"], "help.getCdnConfig": ["Timeout"], "help.getConfig": ["Timeout"], "messages.forwardMessages": ["Timeout"], "messages.getBotCallbackAnswer": ["Timeout"], "messages.getDialogs": ["Timeout"], "messages.getHistory": ["Timeout"], "messages.getInlineBotResults": ["Timeout"], "messages.readHistory": ["Timeout"], "messages.sendMedia": ["Timeout"], "messages.sendMessage": ["Timeout"], "updates.getChannelDifference": ["Timeout"], "updates.getDifference": ["Timeout"], "updates.getState": ["Timeout"], "upload.getFile": ["Timeout"], "users.getFullUser": ["Timeout"], "users.getUsers": ["Timeout"]}, "400": {"account.changePhone": ["PHONE_NUMBER_INVALID"], "account.checkUsername": ["USERNAME_INVALID"], "account.confirmPhone": ["CODE_HASH_INVALID", "PHONE_CODE_EMPTY"], "account.getNotifySettings": ["PEER_ID_INVALID"], "account.getPasswordSettings": ["PASSWORD_HASH_INVALID"], "account.getPrivacy": ["PRIVACY_KEY_INVALID"], "account.getTmpPassword": ["PASSWORD_HASH_INVALID", "TMP_PASSWORD_DISABLED"], "account.registerDevice": ["TOKEN_INVALID"], "account.reportPeer": ["PEER_ID_INVALID"], "account.resetAuthorization": ["HASH_INVALID"], "account.sendChangePhoneCode": ["PHONE_NUMBER_INVALID"], "account.sendConfirmPhoneCode": ["HASH_INVALID"], "account.setAccountTTL": ["TTL_DAYS_INVALID"], "account.setPrivacy": ["PRIVACY_KEY_INVALID"], "account.unregisterDevice": ["TOKEN_INVALID"], "account.updateNotifySettings": ["PEER_ID_INVALID"], "account.updatePasswordSettings": ["EMAIL_UNCONFIRMED", "NEW_SALT_INVALID", "NEW_SETTINGS_INVALID", "PASSWORD_HASH_INVALID"], "account.updateProfile": ["ABOUT_TOO_LONG", "FIRSTNAME_INVALID"], "account.updateUsername": ["USERNAME_INVALID", "USERNAME_NOT_MODIFIED", "USERNAME_OCCUPIED"], "auth.bindTempAuthKey": ["ENCRYPTED_MESSAGE_INVALID", "INPUT_REQUEST_TOO_LONG", "TEMP_AUTH_KEY_EMPTY"], "auth.cancelCode": ["PHONE_NUMBER_INVALID"], "auth.checkPassword": ["PASSWORD_HASH_INVALID"], "auth.checkPhone": ["PHONE_NUMBER_BANNED", "PHONE_NUMBER_INVALID"], "auth.exportAuthorization": ["DC_ID_INVALID"], "auth.importAuthorization": ["AUTH_BYTES_INVALID", "USER_ID_INVALID"], "auth.importBotAuthorization": ["ACCESS_TOKEN_EXPIRED", "ACCESS_TOKEN_INVALID", "API_ID_INVALID"], "auth.recoverPassword": ["CODE_EMPTY"], "auth.requestPasswordRecovery": ["PASSWORD_EMPTY"], "auth.resendCode": ["PHONE_NUMBER_INVALID"], "auth.sendCode": ["API_ID_INVALID", "API_ID_PUBLISHED_FLOOD", "INPUT_REQUEST_TOO_LONG", "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN", "PHONE_NUMBER_BANNED", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_PASSWORD_PROTECTED"], "auth.sendInvites": ["MESSAGE_EMPTY"], "auth.signIn": ["PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_UNOCCUPIED"], "auth.signUp": ["FIRSTNAME_INVALID", "PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_OCCUPIED"], "bots.answerWebhookJSONQuery": ["QUERY_ID_INVALID", "USER_BOT_INVALID"], "bots.sendCustomRequest": ["USER_BOT_INVALID"], "channels.checkUsername": ["CHANNEL_INVALID", "CHAT_ID_INVALID", "USERNAME_INVALID"], "channels.createChannel": ["CHAT_TITLE_EMPTY"], "channels.deleteChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteUserHistory": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.editAbout": ["CHANNEL_INVALID", "CHAT_ABOUT_NOT_MODIFIED", "CHAT_ABOUT_TOO_LONG", "CHAT_ADMIN_REQUIRED"], "channels.editAdmin": ["ADMINS_TOO_MUCH", "BOT_CHANNELS_NA", "CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_CREATOR", "USER_ID_INVALID", "USER_NOT_MUTUAL_CONTACT"], "channels.editBanned": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ADMIN_INVALID", "USER_ID_INVALID"], "channels.editPhoto": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "PHOTO_INVALID"], "channels.editTitle": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.exportInvite": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "INVITE_HASH_EXPIRED"], "channels.exportMessageLink": ["CHANNEL_INVALID"], "channels.getAdminLog": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED"], "channels.getChannels": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getFullChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "MESSAGE_IDS_EMPTY"], "channels.getParticipant": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ID_INVALID", "USER_NOT_PARTICIPANT"], "channels.getParticipants": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID"], "channels.inviteToChannel": ["BOT_GROUPS_BLOCKED", "BOTS_TOO_MUCH", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_USER_DEACTIVATED", "USER_BANNED_IN_CHANNEL", "USER_BLOCKED", "USER_BOT", "USER_ID_INVALID", "USER_KICKED", "USER_NOT_MUTUAL_CONTACT", "USERS_TOO_MUCH"], "channels.joinChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHANNELS_TOO_MUCH"], "channels.leaveChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "USER_CREATOR", "USER_NOT_PARTICIPANT"], "channels.readHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.readMessageContents": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.reportSpam": ["CHANNEL_INVALID", "INPUT_USER_DEACTIVATED"], "channels.setStickers": ["CHANNEL_INVALID", "PARTICIPANTS_TOO_FEW"], "channels.toggleInvites": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.toggleSignatures": ["CHANNEL_INVALID"], "channels.updatePinnedMessage": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "channels.updateUsername": ["CHANNEL_INVALID", "CHANNELS_ADMIN_PUBLIC_TOO_MUCH", "CHAT_ADMIN_REQUIRED", "USERNAME_INVALID", "USERNAME_OCCUPIED"], "contacts.block": ["CONTACT_ID_INVALID"], "contacts.deleteContact": ["CONTACT_ID_INVALID"], "contacts.getTopPeers": ["TYPES_EMPTY"], "contacts.importCard": ["EXPORT_CARD_INVALID"], "contacts.resetTopPeerRating": ["PEER_ID_INVALID"], "contacts.resolveUsername": ["USERNAME_INVALID", "USERNAME_NOT_OCCUPIED"], "contacts.search": ["QUERY_TOO_SHORT", "SEARCH_QUERY_EMPTY"], "contacts.unblock": ["CONTACT_ID_INVALID"], "initConnection": ["CONNECTION_LAYER_INVALID", "INPUT_FETCH_FAIL"], "invokeWithLayer": ["AUTH_BYTES_INVALID", "CDN_METHOD_INVALID", "CONNECTION_API_ID_INVALID", "CONNECTION_DEVICE_MODEL_EMPTY", "CONNECTION_LANG_PACK_INVALID", "CONNECTION_NOT_INITED", "CONNECTION_SYSTEM_EMPTY", "INPUT_LAYER_INVALID", "INVITE_HASH_EXPIRED"], "langpack.getDifference": ["LANG_PACK_INVALID"], "langpack.getLangPack": ["LANG_PACK_INVALID"], "langpack.getLanguages": ["LANG_PACK_INVALID"], "langpack.getStrings": ["LANG_PACK_INVALID"], "messages.acceptEncryption": ["CHAT_ID_INVALID", "ENCRYPTION_ALREADY_ACCEPTED", "ENCRYPTION_ALREADY_DECLINED"], "messages.addChatUser": ["CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "USER_ALREADY_PARTICIPANT", "USER_ID_INVALID", "USERS_TOO_MUCH"], "messages.checkChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID"], "messages.createChat": ["USERS_TOO_FEW"], "messages.deleteChatUser": ["CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_NOT_PARTICIPANT"], "messages.deleteHistory": ["PEER_ID_INVALID"], "messages.discardEncryption": ["CHAT_ID_EMPTY", "ENCRYPTION_ALREADY_DECLINED", "ENCRYPTION_ID_INVALID"], "messages.editChatAdmin": ["CHAT_ID_INVALID"], "messages.editChatPhoto": ["CHAT_ID_INVALID", "INPUT_CONSTRUCTOR_INVALID", "INPUT_FETCH_FAIL", "PEER_ID_INVALID", "PHOTO_EXT_INVALID"], "messages.editChatTitle": ["CHAT_ID_INVALID"], "messages.editInlineBotMessage": ["MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED"], "messages.editMessage": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "INPUT_USER_DEACTIVATED", "MESSAGE_EDIT_TIME_EXPIRED", "MESSAGE_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED", "PEER_ID_INVALID"], "messages.exportChatInvite": ["CHAT_ID_INVALID"], "messages.faveSticker": ["STICKER_ID_INVALID"], "messages.forwardMessage": ["CHAT_ID_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID", "YOU_BLOCKED_USER"], "messages.forwardMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "GROUPED_MEDIA_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_IDS_EMPTY", "PEER_ID_INVALID", "RANDOM_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.getBotCallbackAnswer": ["CHANNEL_INVALID", "DATA_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID"], "messages.getChats": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getCommonChats": ["USER_ID_INVALID"], "messages.getDhConfig": ["RANDOM_LENGTH_INVALID"], "messages.getDialogs": ["INPUT_CONSTRUCTOR_INVALID", "OFFSET_PEER_ID_INVALID"], "messages.getDocumentByHash": ["SHA256_HASH_INVALID"], "messages.getFullChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getGameHighScores": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getInlineBotResults": ["BOT_INLINE_DISABLED", "BOT_INVALID", "CHANNEL_PRIVATE"], "messages.getInlineGameHighScores": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getMessageEditData": ["PEER_ID_INVALID"], "messages.getMessagesViews": ["CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerDialogs": ["CHANNEL_PRIVATE", "PEER_ID_INVALID"], "messages.getPeerSettings": ["CHANNEL_INVALID", "PEER_ID_INVALID"], "messages.getStickerSet": ["STICKERSET_INVALID"], "messages.getUnreadMentions": ["PEER_ID_INVALID"], "messages.getWebPage": ["WC_CONVERT_URL_INVALID"], "messages.hideReportSpam": ["PEER_ID_INVALID"], "messages.importChatInvite": ["CHANNELS_TOO_MUCH", "INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID", "USER_ALREADY_PARTICIPANT", "USERS_TOO_MUCH"], "messages.installStickerSet": ["STICKERSET_INVALID"], "messages.migrateChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.readEncryptedHistory": ["MSG_WAIT_FAILED"], "messages.readHistory": ["PEER_ID_INVALID"], "messages.receivedQueue": ["MSG_WAIT_FAILED"], "messages.reorderPinnedDialogs": ["PEER_ID_INVALID"], "messages.reportEncryptedSpam": ["CHAT_ID_INVALID"], "messages.reportSpam": ["PEER_ID_INVALID"], "messages.requestEncryption": ["DH_G_A_INVALID", "USER_ID_INVALID"], "messages.saveDraft": ["PEER_ID_INVALID"], "messages.saveGif": ["GIF_ID_INVALID"], "messages.saveRecentSticker": ["STICKER_ID_INVALID"], "messages.search": ["CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "PEER_ID_NOT_SUPPORTED", "SEARCH_QUERY_EMPTY", "USER_ID_INVALID"], "messages.searchGifs": ["SEARCH_QUERY_EMPTY"], "messages.searchGlobal": ["SEARCH_QUERY_EMPTY"], "messages.sendEncrypted": ["CHAT_ID_INVALID", "DATA_INVALID", "ENCRYPTION_DECLINED", "MSG_WAIT_FAILED"], "messages.sendEncryptedFile": ["MSG_WAIT_FAILED"], "messages.sendEncryptedService": ["DATA_INVALID", "ENCRYPTION_DECLINED", "MSG_WAIT_FAILED"], "messages.sendInlineBotResult": ["INLINE_RESULT_EXPIRED", "PEER_ID_INVALID", "QUERY_ID_EMPTY", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.sendMedia": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "EXTERNAL_URL_INVALID", "FILE_PART_122_MISSING", "FILE_PART_458_MISSING", "FILE_PART_468_MISSING", "FILE_PART_504_MISSING", "FILE_PART_72_MISSING", "FILE_PART_94_MISSING", "FILE_PART_LENGTH_INVALID", "FILE_PARTS_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_CAPTION_TOO_LONG", "MEDIA_EMPTY", "PEER_ID_INVALID", "PHOTO_EXT_INVALID", "PHOTO_INVALID_DIMENSIONS", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.sendMessage": ["BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "ENTITY_MENTION_USER_INVALID", "INPUT_USER_DEACTIVATED", "MESSAGE_EMPTY", "MESSAGE_TOO_LONG", "PEER_ID_INVALID", "REPLY_MARKUP_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.sendScreenshotNotification": ["PEER_ID_INVALID"], "messages.setBotCallbackAnswer": ["QUERY_ID_INVALID"], "messages.setBotPrecheckoutResults": ["ERROR_TEXT_EMPTY"], "messages.setBotShippingResults": ["QUERY_ID_INVALID"], "messages.setEncryptedTyping": ["CHAT_ID_INVALID"], "messages.setGameScore": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setInlineBotResults": ["ARTICLE_TITLE_EMPTY", "BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "MESSAGE_EMPTY", "QUERY_ID_INVALID", "REPLY_MARKUP_INVALID", "RESULT_TYPE_INVALID", "SEND_MESSAGE_TYPE_INVALID", "START_PARAM_INVALID"], "messages.setInlineGameScore": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setTyping": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT"], "messages.startBot": ["BOT_INVALID", "PEER_ID_INVALID", "START_PARAM_EMPTY", "START_PARAM_INVALID"], "messages.toggleChatAdmins": ["CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "messages.toggleDialogPin": ["PEER_ID_INVALID"], "messages.uninstallStickerSet": ["STICKERSET_INVALID"], "messages.uploadMedia": ["BOT_MISSING", "MEDIA_INVALID", "PEER_ID_INVALID"], "payments.getPaymentForm": ["MESSAGE_ID_INVALID"], "payments.getPaymentReceipt": ["MESSAGE_ID_INVALID"], "payments.sendPaymentForm": ["MESSAGE_ID_INVALID"], "payments.validateRequestedInfo": ["MESSAGE_ID_INVALID"], "phone.acceptCall": ["CALL_ALREADY_ACCEPTED", "CALL_ALREADY_DECLINED", "CALL_PEER_INVALID", "CALL_PROTOCOL_FLAGS_INVALID"], "phone.confirmCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.discardCall": ["CALL_ALREADY_ACCEPTED", "CALL_PEER_INVALID"], "phone.receivedCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.requestCall": ["CALL_PROTOCOL_FLAGS_INVALID", "PARTICIPANT_VERSION_OUTDATED", "USER_ID_INVALID"], "phone.saveCallDebug": ["CALL_PEER_INVALID", "DATA_JSON_INVALID"], "phone.setCallRating": ["CALL_PEER_INVALID"], "photos.getUserPhotos": ["MAX_ID_INVALID", "USER_ID_INVALID"], "photos.uploadProfilePhoto": ["FILE_PARTS_INVALID", "IMAGE_PROCESS_FAILED", "PHOTO_CROP_SIZE_SMALL", "PHOTO_EXT_INVALID"], "stickers.addStickerToSet": ["BOT_MISSING", "STICKERSET_INVALID"], "stickers.changeStickerPosition": ["BOT_MISSING", "STICKER_INVALID"], "stickers.createStickerSet": ["BOT_MISSING", "PACK_SHORT_NAME_INVALID", "PACK_SHORT_NAME_OCCUPIED", "PEER_ID_INVALID", "STICKER_EMOJI_INVALID", "STICKER_FILE_INVALID", "STICKER_PNG_DIMENSIONS", "STICKERS_EMPTY", "USER_ID_INVALID"], "stickers.removeStickerFromSet": ["BOT_MISSING", "STICKER_INVALID"], "updates.getChannelDifference": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID", "RANGES_INVALID"], "updates.getDifference": ["CDN_METHOD_INVALID", "DATE_EMPTY", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID"], "upload.getCdnFileHashes": ["CDN_METHOD_INVALID", "RSA_DECRYPT_FAILED"], "upload.getFile": ["FILE_ID_INVALID", "INPUT_FETCH_FAIL", "LIMIT_INVALID", "LOCATION_INVALID", "OFFSET_INVALID"], "upload.getWebFile": ["LOCATION_INVALID"], "upload.reuploadCdnFile": ["RSA_DECRYPT_FAILED"], "upload.saveBigFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "FILE_PART_SIZE_INVALID", "FILE_PARTS_INVALID"], "upload.saveFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "INPUT_FETCH_FAIL"], "users.getFullUser": ["USER_ID_INVALID"], "{}": ["INVITE_HASH_EXPIRED"]}, "401": {"account.updateStatus": ["SESSION_PASSWORD_NEEDED"], "auth.signIn": ["SESSION_PASSWORD_NEEDED"], "contacts.resolveUsername": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "messages.getDialogs": ["SESSION_PASSWORD_NEEDED"], "messages.getHistory": ["AUTH_KEY_PERM_EMPTY"], "messages.importChatInvite": ["SESSION_PASSWORD_NEEDED"], "updates.getDifference": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "updates.getState": ["SESSION_PASSWORD_NEEDED"], "upload.saveFilePart": ["SESSION_PASSWORD_NEEDED"], "users.getUsers": ["SESSION_PASSWORD_NEEDED"]}, "403": {"channels.createChannel": ["USER_RESTRICTED"], "channels.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "channels.editAdmin": ["CHAT_ADMIN_INVITE_REQUIRED", "RIGHT_FORBIDDEN", "USER_PRIVACY_RESTRICTED"], "channels.getFullChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "channels.inviteToChannel": ["CHAT_WRITE_FORBIDDEN", "USER_CHANNELS_TOO_MUCH", "USER_PRIVACY_RESTRICTED"], "channels.leaveChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "invokeWithLayer": ["CHAT_WRITE_FORBIDDEN"], "messages.addChatUser": ["USER_NOT_MUTUAL_CONTACT", "USER_PRIVACY_RESTRICTED"], "messages.createChat": ["USER_RESTRICTED"], "messages.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.editMessage": ["CHAT_WRITE_FORBIDDEN", "MESSAGE_AUTHOR_REQUIRED"], "messages.forwardMessages": ["CHAT_SEND_GIFS_FORBIDDEN", "CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_SEND_STICKERS_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "messages.getMessageEditData": ["MESSAGE_AUTHOR_REQUIRED"], "messages.migrateChat": ["CHAT_ADMIN_REQUIRED"], "messages.sendEncryptedService": ["USER_IS_BLOCKED"], "messages.sendInlineBotResult": ["CHAT_WRITE_FORBIDDEN"], "messages.sendMedia": ["CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "messages.sendMessage": ["CHAT_WRITE_FORBIDDEN"], "messages.setInlineBotResults": ["USER_BOT_INVALID"], "messages.setTyping": ["CHAT_WRITE_FORBIDDEN"], "phone.requestCall": ["USER_IS_BLOCKED", "USER_PRIVACY_RESTRICTED"], "updates.getChannelDifference": ["CHANNEL_PUBLIC_GROUP_NA"]}, "500": {"auth.sendCode": ["AUTH_RESTART"], "auth.signUp": ["MEMBER_OCCUPY_PRIMARY_LOC_FAILED", "REG_ID_GENERATE_FAILED"], "channels.getChannels": ["NEED_CHAT_INVALID"], "contacts.deleteContacts": ["NEED_MEMBER_INVALID"], "contacts.importCard": ["NEED_MEMBER_INVALID"], "invokeWithLayer": ["NEED_MEMBER_INVALID"], "messages.acceptEncryption": ["ENCRYPTION_OCCUPY_FAILED"], "messages.editChatTitle": ["NEED_CHAT_INVALID"], "messages.forwardMessages": ["PTS_CHANGE_EMPTY", "RANDOM_ID_DUPLICATE"], "messages.sendMedia": ["RANDOM_ID_DUPLICATE", "STORAGE_CHECK_FAILED"], "messages.sendMessage": ["RANDOM_ID_DUPLICATE"], "phone.acceptCall": ["CALL_OCCUPY_FAILED"], "phone.requestCall": ["PARTICIPANT_CALL_FAILED"], "updates.getChannelDifference": ["HISTORY_GET_FAILED", "PERSISTENT_TIMESTAMP_OUTDATED"], "updates.getDifference": ["NEED_MEMBER_INVALID", "STORE_INVALID_SCALAR_TYPE"], "upload.getCdnFile": ["UNKNOWN_METHOD"], "users.getUsers": ["MEMBER_NO_LOCATION", "NEED_MEMBER_INVALID"]}}} \ No newline at end of file From a7888bfaf8f5e5c602619ce02bcfc24edbc5d209 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 30 Jan 2018 09:11:40 +0100 Subject: [PATCH 078/108] Fix tiny typo on the documentation --- readthedocs/extra/advanced-usage/sessions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index 7f1ded9b..ae2b03ad 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -4,7 +4,7 @@ Session Files ============== -The first parameter you pass the the constructor of the ``TelegramClient`` is +The first parameter you pass to the constructor of the ``TelegramClient`` is the ``session``, and defaults to be the session name (or full path). That is, if you create a ``TelegramClient('anon')`` instance and connect, an ``anon.session`` file will be created on the working directory. From bf56d3211828f34b024c7660accd963d933a369a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 30 Jan 2018 18:32:42 +0100 Subject: [PATCH 079/108] Add missing FutureSalts response special case (#81) --- telethon/network/mtproto_sender.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 0e960181..877611df 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -4,11 +4,9 @@ encrypting every packet, and relies on a valid AuthKey in the used Session. """ import gzip import logging -import struct from threading import Lock from .. import helpers as utils -from ..crypto import AES from ..errors import ( BadMessageError, InvalidChecksumError, BrokenAuthKeyError, rpc_message_to_error @@ -16,11 +14,11 @@ from ..errors import ( from ..extensions import BinaryReader from ..tl import TLMessage, MessageContainer, GzipPacked from ..tl.all_tlobjects import tlobjects +from ..tl.functions.auth import LogOutRequest from ..tl.types import ( - MsgsAck, Pong, BadServerSalt, BadMsgNotification, + MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts, MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo ) -from ..tl.functions.auth import LogOutRequest __log__ = logging.getLogger(__name__) @@ -244,6 +242,12 @@ class MtProtoSender: return True + if isinstance(obj, FutureSalts): + r = self._pop_request(obj.req_msg_id) + if r: + r.result = obj + r.confirm_received.set() + # If the object isn't any of the above, then it should be an Update. self.session.process_entities(obj) if state: From c8bbbe3e3ce2b9db683ce3085fca19841c26562a Mon Sep 17 00:00:00 2001 From: Birger Jarl Date: Wed, 31 Jan 2018 23:01:53 +0300 Subject: [PATCH 080/108] Save session data when migrating from JSON (#570) --- telethon/session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/session.py b/telethon/session.py index 266d732e..e168e559 100644 --- a/telethon/session.py +++ b/telethon/session.py @@ -159,6 +159,7 @@ class Session: 'insert or replace into entities values (?,?,?,?,?)', entities ) + self._update_session_table() c.close() self.save() From d5a91c727332b5c9c4466f4c6d55f67029d0d2d0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Feb 2018 09:39:00 +0100 Subject: [PATCH 081/108] Don't set session to None on .log_out() --- telethon/telegram_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 18602032..4ae7f642 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -418,7 +418,6 @@ class TelegramClient(TelegramBareClient): self.disconnect() self.session.delete() - self.session = None return True def get_me(self): From add122bfe7b376d0a4ad27786b6cd87186e17e1e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Feb 2018 10:12:46 +0100 Subject: [PATCH 082/108] Support signing up through .start() --- telethon/telegram_client.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4ae7f642..84c77b9c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -27,7 +27,8 @@ from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, - SessionPasswordNeededError, FileMigrateError + SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, + PhoneNumberOccupiedError ) from .network import ConnectionMode from .tl.custom import Draft, Dialog @@ -204,7 +205,8 @@ class TelegramClient(TelegramBareClient): def start(self, phone=lambda: input('Please enter your phone: '), password=None, bot_token=None, - force_sms=False, code_callback=None): + force_sms=False, code_callback=None, + first_name='New User', last_name=''): """ Convenience method to interactively connect and sign in if required, also taking into consideration that 2FA may be enabled in the account. @@ -236,6 +238,13 @@ class TelegramClient(TelegramBareClient): A callable that will be used to retrieve the Telegram login code. Defaults to `input()`. + first_name (:obj:`str`, optional): + The first name to be used if signing up. This has no + effect if the account already exists and you sign in. + + last_name (:obj:`str`, optional): + Similar to the first name, but for the last. Optional. + Returns: :obj:`TelegramClient`: This client, so initialization can be chained with `.start()`. @@ -276,19 +285,28 @@ class TelegramClient(TelegramBareClient): max_attempts = 3 two_step_detected = False - self.send_code_request(phone, force_sms=force_sms) + sent_code = self.send_code_request(phone, force_sms=force_sms) + sign_up = not sent_code.phone_registered while attempts < max_attempts: try: - # Raises SessionPasswordNeededError if 2FA enabled - me = self.sign_in(phone, code_callback()) + if sign_up: + me = self.sign_up(code_callback(), first_name, last_name) + else: + # Raises SessionPasswordNeededError if 2FA enabled + me = self.sign_in(phone, code_callback()) break except SessionPasswordNeededError: two_step_detected = True break + except PhoneNumberOccupiedError: + sign_up = False + except PhoneNumberUnoccupiedError: + sign_up = True except (PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError): print('Invalid code. Please try again.', file=sys.stderr) - attempts += 1 + + attempts += 1 else: raise RuntimeError( '{} consecutive sign-in attempts failed. Aborting' From fbd53e2126f8f46a86ed3bee824817a3dad793af Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 1 Feb 2018 12:10:03 +0100 Subject: [PATCH 083/108] Override TLObject's __eq__ and __ne__ methods --- telethon/tl/tlobject.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index db1982c4..b048158c 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -144,6 +144,12 @@ class TLObject: def on_response(self, reader): self.result = reader.tgread_object() + def __eq__(self, o): + return isinstance(o, type(self)) and self.to_dict() == o.to_dict() + + def __ne__(self, o): + return not isinstance(o, type(self)) or self.to_dict() != o.to_dict() + def __str__(self): return TLObject.pretty_format(self) From cf21808118737005beb3450e31a387b6c41b285f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 2 Feb 2018 17:23:28 +0100 Subject: [PATCH 084/108] Raise error on .get_entity() on non-joined invite link --- telethon/telegram_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 84c77b9c..9dda06af 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1716,10 +1716,10 @@ class TelegramClient(TelegramBareClient): if is_join_chat: invite = self(CheckChatInviteRequest(string)) if isinstance(invite, ChatInvite): - # If it's an invite to a chat, the user must join before - # for the link to be resolved and work, otherwise raise. - if invite.channel: - return invite.channel + raise ValueError( + 'Cannot get entity from a channel ' + '(or group) that you are not part of' + ) elif isinstance(invite, ChatInviteAlready): return invite.chat else: From 2ffe2b71dc019792c4b34c5ed070e9299021a461 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 11:39:15 +0100 Subject: [PATCH 085/108] Except OSError with errno.WSAEACCES when connecting "OSError: [WinError 10013] An attempt was made to access a socket in a way forbidden by its access permissions." --- telethon/extensions/tcp_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index d4c45776..a306302a 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -73,7 +73,8 @@ class TcpClient: # There are some errors that we know how to handle, and # the loop will allow us to retry if e.errno in (errno.EBADF, errno.ENOTSOCK, errno.EINVAL, - errno.ECONNREFUSED): + errno.ECONNREFUSED, # Windows-specific follow + getattr(errno, 'WSAEACCES', None)): # Bad file descriptor, i.e. socket was closed, set it # to none to recreate it on the next iteration self._socket = None From eefd37c2d78a48429f6fcceb3cfb4dbbc7affe5f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 12:15:38 +0100 Subject: [PATCH 086/108] Stop calling .disconnect() from .__del__() It was causing some strange behaviour with the synchronized Queue used by the UpdateState class. Calling .get() with any timeout would block forever. Perhaps something else got released when the script ended and then any call would block forever, thus the thread never joining. --- telethon/telegram_bare_client.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index fe63ab8a..bff6c1d3 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -255,9 +255,6 @@ class TelegramBareClient: self._first_request = True # On reconnect it will be first again self.session.close() - def __del__(self): - self.disconnect() - def _reconnect(self, new_dc=None): """If 'new_dc' is not set, only a call to .connect() will be made since it's assumed that the connection has been lost and the From 341fb38136418c9ba616b70103618be390d9d1cb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 15:39:37 +0100 Subject: [PATCH 087/108] Invoke getState after the server kicks us idling for updates For some reason, the server seems to kick us after 1024 items from the network are received. Tested with the following code, 1022 updates were received, after BadServerSalt, NewSessionCreated and MsgsAck: client = TelegramClient(..., spawn_read_thread=False) client.connect(_sync_updates=False) sender = client._sender client = None while True: try: sender.receive(None) except TimeoutError: pass except ConnectionResetError: sender.connect() If one were to run this code after being kicked no further items will be retrieved and it will always timeout. Invoking a ping has no effect either. Invoking some "high level" request like getState seems to do the trick. --- telethon/telegram_bare_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index bff6c1d3..5984bb2e 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -664,6 +664,14 @@ class TelegramBareClient: with self._reconnect_lock: while self._user_connected and not self._reconnect(): sleep(0.1) # Retry forever, this is instant messaging + + if self.is_connected(): + # Telegram seems to kick us every 1024 items received + # from the network not considering things like bad salt. + # We must execute some *high level* request (that's not + # a ping) if we want to receive updates again. + # TODO Test if getDifference works too (better alternative) + self._sender.send(GetStateRequest()) except: self._idling.clear() raise From fd08d5325393e1f650e682ef3cb9427507ea0d2b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 15:42:43 +0100 Subject: [PATCH 088/108] Trust the server will not send duplicates This change was also suggested by the test on the previous commit. --- telethon/update_state.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/telethon/update_state.py b/telethon/update_state.py index 9f308d89..f98c0c04 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -30,7 +30,6 @@ class UpdateState: self.handlers = [] self._updates_lock = RLock() self._updates = Queue() - self._latest_updates = deque(maxlen=10) # https://core.telegram.org/api/updates self._state = tl.updates.State(0, 0, datetime.now(), 0, 0) @@ -130,34 +129,12 @@ class UpdateState: self._state = update return # Nothing else to be done - pts = getattr(update, 'pts', self._state.pts) - if hasattr(update, 'pts') and pts <= self._state.pts: - __log__.info('Ignoring %s, already have it', update) - return # We already handled this update - - self._state.pts = pts - - # TODO There must be a better way to handle updates rather than - # keeping a queue with the latest updates only, and handling - # the 'pts' correctly should be enough. However some updates - # like UpdateUserStatus (even inside UpdateShort) will be called - # repeatedly very often if invoking anything inside an update - # handler. TODO Figure out why. - """ - client = TelegramClient('anon', api_id, api_hash, update_workers=1) - client.connect() - def handle(u): - client.get_me() - client.add_update_handler(handle) - input('Enter to exit.') - """ - data = pickle.dumps(update.to_dict()) - if data in self._latest_updates: - __log__.info('Ignoring %s, already have it', update) - return # Duplicated too - - self._latest_updates.append(data) + if hasattr(update, 'pts'): + self._state.pts = update.pts + # After running the script for over an hour and receiving over + # 1000 updates, the only duplicates received were users going + # online or offline. We can trust the server until new reports. if isinstance(update, tl.UpdateShort): self._updates.put(update.update) # Expand "Updates" into "Update", and pass these to callbacks. From 06bc761a5b794283c895e66460edd7bdeee187d7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 3 Feb 2018 16:03:17 +0100 Subject: [PATCH 089/108] Update to v0.17 --- readthedocs/extra/changelog.rst | 45 +++++++++++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 580ebe4b..2e11fc7d 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,51 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Trust the Server with Updates (v0.17) +===================================== + +*Published at 2018/02/03* + +The library trusts the server with updates again. The library will *not* +check for duplicates anymore, and when the server kicks us, it will run +``GetStateRequest`` so the server starts sending updates again (something +it wouldn't do unless you invoked something, it seems). But this update +also brings a few more changes! + +Additions +~~~~~~~~~ + +- ``TLObject``'s override ``__eq__`` and ``__ne__``, so you can compare them. +- Added some missing cases on ``.get_input_entity()`` and peer functions. +- ``obj.to_dict()`` now has a ``'_'`` key with the type used. +- ``.start()`` can also sign up now. +- More parameters for ``.get_message_history()``. +- Updated list of RPC errors. +- HTML parsing thanks to **@tulir**! It can be used similar to markdown: + ``client.send_message(..., parse_mode='html')``. + + +Enhancements +~~~~~~~~~~~~ + +- ``client.send_file()`` now accepts ``Message``'s and + ``MessageMedia``'s as the ``file`` parameter. +- Some documentation updates and fixed to clarify certain things. +- New exact match feature on https://lonamiwebs.github.io/Telethon. +- Return as early as possible from ``.get_input_entity()`` and similar, + to avoid penalizing you for doing this right. + +Bug fixes +~~~~~~~~~ + +- ``.download_media()`` wouldn't accept a ``Document`` as parameter. +- The SQLite is now closed properly on disconnection. +- IPv6 addresses shouldn't use square braces. +- Fix regarding ``.log_out()``. +- The time offset wasn't being used (so having wrong system time would + cause the library not to work at all). + + New ``.resolve()`` method (v0.16.2) =================================== diff --git a/telethon/version.py b/telethon/version.py index 28c39d24..675ca0af 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '0.16.2' +__version__ = '0.17' From f200369a9327f71c8c0260c07ebdaf66d8057526 Mon Sep 17 00:00:00 2001 From: tsujp Date: Tue, 6 Feb 2018 19:21:09 +0900 Subject: [PATCH 090/108] Add Heroku instructions to sessions documentation (#586) --- readthedocs/extra/advanced-usage/.DS_Store | Bin 0 -> 6148 bytes readthedocs/extra/advanced-usage/sessions.rst | 49 ++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 readthedocs/extra/advanced-usage/.DS_Store diff --git a/readthedocs/extra/advanced-usage/.DS_Store b/readthedocs/extra/advanced-usage/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0= 3.8.2). Heroku uses +SQLite 3.7.9 which does not support ``WITHOUT ROWID``. So, if you generated +your session file on a system with SQLite >= 3.8.2 your session file will not +work on Heroku's platform and will throw a corrupted schema error. + +There are multiple ways to solve this, the easiest of which is generating a +session file on your Heroku dyno itself. The most complicated is creating +a custom buildpack to install SQLite >= 3.8.2. + + +Generating Session File on a Heroku Dyno +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + Do not restart your application Dyno at any point prior to retrieving your + session file. Constantly creating new session files from Telegram's API + will result in a 24 hour rate limit ban. + +Due to Heroku's ephemeral filesystem all dynamically generated +files not part of your applications buildpack or codebase are destroyed upon +each restart. + +Using this scaffolded code we can start the authentication process: + + .. code-block:: python + + client = TelegramClient('login.session', api_id, api_hash).start() + +At this point your Dyno will crash because you cannot access stdin. Open your +Dyno's control panel on the Heroku website and "run console" from the "More" +dropdown at the top right. Enter ``bash`` and wait for it to load. + +You will automatically be placed into your applications working directory. +So run your application ``python app.py`` and now you can complete the input +requests such as "what is your phone number" etc. + +Once you're successfully authenticated exit your application script with +CTRL + C and ``ls`` to confirm ``login.session`` exists in your current +directory. Now you can create a git repo on your account and commit +``login.session`` to that repo. + +You cannot ``ssh`` into your Dyno instance because it has crashed, so unless +you programatically upload this file to a server host this is the only way to +get it off of your Dyno. From 4362c02e9293ef7d1f8430c0c6c12f66257fc301 Mon Sep 17 00:00:00 2001 From: tsujp Date: Tue, 6 Feb 2018 20:13:38 +0900 Subject: [PATCH 091/108] Add further Heroku instructions to session documentation (#588) --- readthedocs/extra/advanced-usage/sessions.rst | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/readthedocs/extra/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index 765641ab..fca7828e 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -59,8 +59,13 @@ session file on your Heroku dyno itself. The most complicated is creating a custom buildpack to install SQLite >= 3.8.2. -Generating Session File on a Heroku Dyno -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Generating a Session File on a Heroku Dyno +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + Due to Heroku's ephemeral filesystem all dynamically generated + files not part of your applications buildpack or codebase are destroyed + upon each restart. .. warning:: Do not restart your application Dyno at any point prior to retrieving your @@ -78,7 +83,7 @@ Using this scaffolded code we can start the authentication process: client = TelegramClient('login.session', api_id, api_hash).start() At this point your Dyno will crash because you cannot access stdin. Open your -Dyno's control panel on the Heroku website and "run console" from the "More" +Dyno's control panel on the Heroku website and "Run console" from the "More" dropdown at the top right. Enter ``bash`` and wait for it to load. You will automatically be placed into your applications working directory. @@ -93,3 +98,16 @@ directory. Now you can create a git repo on your account and commit You cannot ``ssh`` into your Dyno instance because it has crashed, so unless you programatically upload this file to a server host this is the only way to get it off of your Dyno. + +You now have a session file compatible with SQLite <= 3.8.2. Now you can +programatically fetch this file from an external host (Firebase, S3 etc.) +and login to your session using the following scaffolded code: + + .. code-block:: python + + fileName, headers = urllib.request.urlretrieve(file_url, 'login.session') + client = TelegramClient(os.path.abspath(fileName), api_id, api_hash).start() + +.. note:: + - ``urlretrieve`` will be depreciated, consider using ``requests``. + - ``file_url`` represents the location of your file. From 5ec984dd820a00d005306b6144b86b9b54346c99 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 10:41:58 +0100 Subject: [PATCH 092/108] Allow adding events with the client.on decorator --- telethon/events/__init__.py | 14 ++++++++++++++ telethon/telegram_client.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 telethon/events/__init__.py diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py new file mode 100644 index 00000000..d4d1e019 --- /dev/null +++ b/telethon/events/__init__.py @@ -0,0 +1,14 @@ +import abc +from ..tl import types, functions +from ..extensions import markdown +from .. import utils + + +class _EventBuilder(abc.ABC): + @abc.abstractmethod + def build(self, update): + """Builds an event for the given update if possible, or returns None""" + + @abc.abstractmethod + def resolve(self, client): + """Helper method to allow event builders to be resolved before usage""" diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 9dda06af..c4f5d722 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -160,6 +160,8 @@ class TelegramClient(TelegramBareClient): **kwargs ) + self._event_builders = [] + # Some fields to easy signing in. Let {phone: hash} be # a dictionary because the user may change their mind. self._phone_code_hash = {} @@ -1623,6 +1625,41 @@ class TelegramClient(TelegramBareClient): # endregion + # region Event handling + + def on(self, event): + """ + + Turns the given entity into a valid Telegram user or chat. + + Args: + event (:obj:`_EventBuilder` | :obj:`type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + """ + if isinstance(event, type): + event = event() + + event.resolve(self) + + def decorator(f): + self._event_builders.append((event, f)) + return f + + if self._on_handler not in self.updates.handlers: + self.add_update_handler(self._on_handler) + + return decorator + + def _on_handler(self, update): + for builder, callback in self._event_builders: + event = builder.build(update) + if event: + event._client = self + callback(event) + + # endregion + # region Small utilities to make users' life easier def get_entity(self, entity): From ef837b1a5325b080b4e282fa3a1aa3bc358ab565 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 10:42:40 +0100 Subject: [PATCH 093/108] Add a NewMessage event to handle incoming messages --- telethon/events/__init__.py | 240 ++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d4d1e019..683945a7 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -12,3 +12,243 @@ class _EventBuilder(abc.ABC): @abc.abstractmethod def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" + + +# Classes defined here are actually Event builders +# for their inner Event classes. Inner ._client is +# set later by the creator TelegramClient. +class NewMessage(_EventBuilder): + """ + Represents a new message event builder. + + Args: + incoming (:obj:`bool`, optional): + If set to ``True``, only **incoming** messages will be handled. + Mutually exclusive with ``outgoing`` (can only set one of either). + + outgoing (:obj:`bool`, optional): + If set to ``True``, only **outgoing** messages will be handled. + Mutually exclusive with ``incoming`` (can only set one of either). + + chats (:obj:`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + 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). + """ + def __init__(self, incoming=None, outgoing=None, + chats=None, blacklist_chats=False, + require_input=True): + if incoming and outgoing: + raise ValueError('Can only set either incoming or outgoing') + + self.incoming = incoming + self.outgoing = outgoing + self.chats = chats + self.blacklist_chats = blacklist_chats + + def resolve(self, client): + if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): + self.chats = set(utils.get_peer_id(x) + for x in client.get_input_entity(self.chats)) + elif self.chats is not None: + self.chats = {utils.get_peer_id( + client.get_input_entity(self.chats))} + + def build(self, update): + if isinstance(update, + (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + event = NewMessage.Event(update.message) + elif isinstance(update, types.UpdateShortMessage): + event = NewMessage.Event(types.Message( + out=update.out, + mentioned=update.mentioned, + media_unread=update.media_unread, + silent=update.silent, + id=update.id, + to_id=types.PeerUser(update.user_id), + message=update.message, + date=update.date, + fwd_from=update.fwd_from, + via_bot_id=update.via_bot_id, + reply_to_msg_id=update.reply_to_msg_id, + entities=update.entities + )) + else: + return + + # Short-circuit if we let pass all events + if all(x is None for x in (self.incoming, self.outgoing, self.chats)): + return event + + if self.incoming and event.message.out: + return + if self.outgoing and not event.message.out: + return + + if self.chats is not None: + inside = utils.get_peer_id(event.input_chat) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return + + # Tests passed so return the event + return event + + class Event: + """ + Represents the event of a new message. + + Members: + message (:obj:`Message`): + This is the original ``Message`` object. + + input_chat (:obj:`InputPeer`): + This is the input chat (private, group, megagroup or channel) + to which the message was sent. This doesn't have the title or + anything, but is useful if you don't need those to avoid + further requests. + + Note that this might not be available if the library can't + find the input chat. + + chat (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional): + This property will make an API call the first time to get the + most up to date version of the chat, so use with care as + there is no caching besides local caching yet. + + ``input_chat`` needs to be available (often the case). + + is_private (:obj:`bool`): + True if the message was sent as a private message. + + is_group (:obj:`bool`): + True if the message was sent on a group or megagroup. + + is_channel (:obj:`bool`): + True if the message was sent on a megagroup or channel. + + input_sender (:obj:`InputPeer`): + This 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. + + Note that this might not be available if the library can't + find the input chat. + + sender (:obj:`User`): + This property will make an API call the first time to get the + most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + + text (:obj:`str`): + The message text, markdown-formatted. + + raw_text (:obj:`str`): + The raw message text, ignoring any formatting. + + is_reply (:obj:`str`): + Whether the message is a reply to some other or not. + + reply_message (:obj:`Message`, optional): + This property will make an API call the first time to get the + full ``Message`` object that one was replying to, so use with + care as there is no caching besides local caching yet. + + forward (:obj:`MessageFwdHeader`, optional): + The unmodified ``MessageFwdHeader``, if present. + + out (:obj:`bool`): + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + """ + def __init__(self, message): + self._client = None + self.message = message + self._text = None + + self._chat = None + self._sender = None + + self.is_private = isinstance(message.to_id, types.PeerUser) + self.is_group = ( + isinstance(message.to_id, (types.PeerChat, types.PeerChannel)) + and not message.post + ) + self.is_channel = isinstance(message.to_id, types.PeerChannel) + + self.is_reply = bool(message.reply_to_msg_id) + self._reply_message = None + + def reply(self, message, as_reply=True): + """Replies to this message""" + self._client.send_message(self.message.to_id, message) + + @property + def input_chat(self): + # TODO If not found, getMessages to find the sender and chat + return self._client.get_input_entity(self.message.to_id) + + @property + def chat(self): + if self._chat is None: + # TODO Assert input entity is not None to avoid weird errors + self._chat = self._client.get_entity(self.input_chat) + return self._chat + + @property + def input_sender(self): + # TODO If not found, getMessages to find the sender and chat + return self._client.get_input_entity(self.message.from_id) + + @property + def sender(self): + if self._sender is None: + # TODO Assert input entity is not None to avoid weird errors + self._sender = self._client.get_entity(self.input_sender) + return self._sender + + @property + def text(self): + if self._text is None: + if not self.message.entities: + return self.message.message + self._text = markdown.unparse(self.message.message, + self.message.entities or []) + return self._text + + @property + def raw_text(self): + return self.message.message + + @property + def reply_message(self): + if not self.message.reply_to_msg_id: + return None + + if self._reply_message is None: + if isinstance(self.input_chat, types.InputPeerChannel): + r = self._client(functions.channels.GetMessagesRequest( + self.input_chat, [self.message.reply_to_msg_id] + )) + else: + r = self._client(functions.messages.GetMessagesRequest( + [self.message.reply_to_msg_id] + )) + if not isinstance(r, types.messages.MessagesNotModified): + self._reply_message = r.messages[0] + + return self._reply_message + + @property + def forward(self): + return self.message.fwd_from + + @property + def out(self): + return self.message.out From 9c09233b4fd0b207802622656f6c055d7333c81a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 13:45:17 +0100 Subject: [PATCH 094/108] Make NewMessage's input chat/sender actual Input* if possible --- telethon/events/__init__.py | 75 +++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 683945a7..d7012ad9 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,7 +1,10 @@ import abc -from ..tl import types, functions -from ..extensions import markdown +import itertools + from .. import utils +from ..errors import RPCError +from ..extensions import markdown +from ..tl import types, functions class _EventBuilder(abc.ABC): @@ -172,7 +175,9 @@ class NewMessage(_EventBuilder): self.message = message self._text = None + self._input_chat = None self._chat = None + self._input_sender = None self._sender = None self.is_private = isinstance(message.to_id, types.PeerUser) @@ -189,28 +194,76 @@ class NewMessage(_EventBuilder): """Replies to this message""" self._client.send_message(self.message.to_id, message) + def _get_input_entity(self, msg_id, entity_id, chat=None): + """ + Helper function to call GetMessages on the give msg_id and + return the input entity whose ID is the given entity ID. + """ + try: + if isinstance(chat, types.InputPeerChannel): + result = self._client( + functions.channels.GetMessagesRequest(chat, [msg_id]) + ) + else: + result = self._client( + functions.messages.GetMessagesRequest([msg_id]) + ) + except RPCError: + return + entity = { + utils.get_peer_id(x): x for x in itertools.chain( + getattr(result, 'chats', []), + getattr(result, 'users', [])) + }.get(entity_id) + if entity: + return utils.get_input_peer(entity) + @property def input_chat(self): - # TODO If not found, getMessages to find the sender and chat - return self._client.get_input_entity(self.message.to_id) + if self._input_chat is None: + try: + self._input_chat = self._client.get_input_entity( + self.message.to_id + ) + except (ValueError, TypeError): + # The library hasn't seen this chat, get the message + if not isinstance(self.message.to_id, types.PeerChannel): + # TODO For channels, getDifference? Maybe looking + # in the dialogs (which is already done) is enough. + self._input_chat = self._get_input_entity( + self.message.id, + utils.get_peer_id(self.message.to_id) + ) + return self._input_chat @property def chat(self): - if self._chat is None: - # TODO Assert input entity is not None to avoid weird errors - self._chat = self._client.get_entity(self.input_chat) + if self._chat is None and self.input_chat: + self._chat = self._client.get_entity(self._input_chat) return self._chat @property def input_sender(self): - # TODO If not found, getMessages to find the sender and chat + if self._input_sender is None: + try: + self._input_sender = self._client.get_input_entity( + self.message.from_id + ) + except (ValueError, TypeError): + if isinstance(self.message.to_id, types.PeerChannel): + # We can rely on self.input_chat for this + self._input_sender = self._get_input_entity( + self.message.id, + self.message.from_id, + chat=self.input_chat + ) + return self._client.get_input_entity(self.message.from_id) @property def sender(self): - if self._sender is None: - # TODO Assert input entity is not None to avoid weird errors - self._sender = self._client.get_entity(self.input_sender) + if self._sender is None and self.input_sender: + self._sender = self._client.get_entity(self._input_sender) return self._sender @property From dc43757cff0e9ce61b7fec2a4123ada61d0e4d5d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 13:55:25 +0100 Subject: [PATCH 095/108] Don't access NewMessage properties when building the event --- telethon/events/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d7012ad9..b5d7ae68 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -92,7 +92,7 @@ class NewMessage(_EventBuilder): return if self.chats is not None: - inside = utils.get_peer_id(event.input_chat) in self.chats + inside = utils.get_peer_id(event.message.to_id) in self.chats if inside == self.blacklist_chats: # If this chat matches but it's a blacklist ignore. # If it doesn't match but it's a whitelist ignore. From 2e0a8d6bce604c7949b1a1dd189edd4759cea4a4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 13:55:41 +0100 Subject: [PATCH 096/108] Add respond and reply methods to the NewMessage event --- telethon/events/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index b5d7ae68..4a6ae07f 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -190,9 +190,21 @@ class NewMessage(_EventBuilder): self.is_reply = bool(message.reply_to_msg_id) self._reply_message = None - def reply(self, message, as_reply=True): - """Replies to this message""" - self._client.send_message(self.message.to_id, message) + def respond(self, *args, **kwargs): + """ + Responds to the message (not as a reply). This is a shorthand for + ``client.send_message(event.chat, ...)``. + """ + return self._client.send_message(self.input_chat, *args, **kwargs) + + def reply(self, *args, **kwargs): + """ + Replies to the message (as a reply). This is a shorthand for + ``client.send_message(event.chat, ..., reply_to=event.message.id)``. + """ + return self._client.send_message(self.input_chat, + reply_to=self.message.id, + *args, **kwargs) def _get_input_entity(self, msg_id, entity_id, chat=None): """ From c79fbe451f70dbe83d61dbb5abcdd75dc670c69e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 7 Feb 2018 14:06:36 +0100 Subject: [PATCH 097/108] Fix NewMessage event not dropping MessageService --- telethon/events/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 4a6ae07f..5fdd257d 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -63,6 +63,8 @@ class NewMessage(_EventBuilder): def build(self, update): if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): + if not isinstance(update.message, types.Message): + return # We don't care about MessageService's here event = NewMessage.Event(update.message) elif isinstance(update, types.UpdateShortMessage): event = NewMessage.Event(types.Message( From 91ba50174a52d80ca62ca1e981612e929845d3b0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 8 Feb 2018 19:43:15 +0100 Subject: [PATCH 098/108] Provide easier access to media through NewMessage event --- telethon/events/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 5fdd257d..1477cc02 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -316,6 +316,24 @@ class NewMessage(_EventBuilder): def forward(self): return self.message.fwd_from + @property + def media(self): + return self.message.media + + @property + def photo(self): + if isinstance(self.message.media, types.MessageMediaPhoto): + photo = self.message.media.photo + if isinstance(photo, types.Photo): + return photo + + @property + def document(self): + if isinstance(self.message.media, types.MessageMediaDocument): + doc = self.message.media.document + if isinstance(doc, types.Document): + return doc + @property def out(self): return self.message.out From e15dd05975975139fee4ecb87e6c9525cde41e63 Mon Sep 17 00:00:00 2001 From: Kyle2142 Date: Fri, 9 Feb 2018 10:07:25 +0200 Subject: [PATCH 099/108] Corrected info in Admin Permissions example (#589) --- .../extra/examples/chats-and-channels.rst | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index 30b94178..44ee6112 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -169,14 +169,28 @@ Giving or revoking admin permissions can be done with the `EditAdminRequest`__: pin_messages=True, invite_link=None, edit_messages=None - ) + ) + # Equivalent to: + # rights = ChannelAdminRights( + # change_info=True, + # delete_messages=True, + # pin_messages=True + # ) - client(EditAdminRequest(channel, who, rights)) + # Once you have a ChannelAdminRights, invoke it + client(EditAdminRequest(channel, user, rights)) - -Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set -to ``True`` the ``post_messages`` and ``edit_messages`` fields. Those that -are ``None`` can be omitted (left here so you know `which are available`__. + # User will now be able to change group info, delete other people's + # messages and pin messages. + +| Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all +| parameters to ``True`` to give a user full permissions, as not all +| permissions are related to both broadcast channels/megagroups. +| +| E.g. trying to set ``post_messages=True`` in a megagroup will raise an +| error. It is recommended to always use keyword arguments, and to set only +| the permissions the user needs. If you don't need to change a permission, +| it can be omitted (full list `here`__). __ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html __ https://github.com/Kyle2142 From 510bbf0fc8a5988378321f8fb10ad0548c06ff1e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 11:36:41 +0100 Subject: [PATCH 100/108] Create a more reusable Event base class --- telethon/events/__init__.py | 148 ++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 59 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 1477cc02..d16a6529 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -17,6 +17,90 @@ class _EventBuilder(abc.ABC): """Helper method to allow event builders to be resolved before usage""" +class _EventCommon(abc.ABC): + """Intermediate class with common things to all events""" + + def __init__(self, chat_peer=None, msg_id=None, broadcast=False): + self._client = None + self._chat_peer = chat_peer + self._message_id = msg_id + self._input_chat = None + self._chat = None + + self.is_private = isinstance(chat_peer, types.PeerUser) + self.is_group = ( + isinstance(chat_peer, (types.PeerChat, types.PeerChannel)) + and not broadcast + ) + self.is_channel = isinstance(chat_peer, types.PeerChannel) + + def _get_input_entity(self, msg_id, entity_id, chat=None): + """ + Helper function to call 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. + """ + try: + if isinstance(chat, types.InputPeerChannel): + result = self._client( + functions.channels.GetMessagesRequest(chat, [msg_id]) + ) + else: + result = self._client( + functions.messages.GetMessagesRequest([msg_id]) + ) + except RPCError: + return + entity = { + utils.get_peer_id(x): x for x in itertools.chain( + getattr(result, 'chats', []), + getattr(result, 'users', [])) + }.get(entity_id) + if entity: + return utils.get_input_peer(entity) + + @property + def input_chat(self): + """ + The (:obj:`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. + + Note that this might be ``None`` if the library can't find it. + """ + + if self._input_chat is None and self._chat_peer is not None: + try: + self._input_chat = self._client.get_input_entity( + self._chat_peer + ) + except (ValueError, TypeError): + # The library hasn't seen this chat, get the message + if not isinstance(self._chat_peer, types.PeerChannel): + # TODO For channels, getDifference? Maybe looking + # in the dialogs (which is already done) is enough. + if self._message_id is not None: + self._input_chat = self._get_input_entity( + self._message_id, + utils.get_peer_id(self._chat_peer) + ) + return self._input_chat + + @property + def chat(self): + """ + The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which + the event occurred. This property will make an API call the first time + to get the most up to date version of the chat, so use with care as + there is no caching besides local caching yet. + """ + if self._chat is None and self.input_chat: + self._chat = self._client.get_entity(self._input_chat) + return self._chat + + # Classes defined here are actually Event builders # for their inner Event classes. Inner ._client is # set later by the creator TelegramClient. @@ -42,8 +126,7 @@ class NewMessage(_EventBuilder): it matches it will NOT be handled) or a whitelist (default). """ def __init__(self, incoming=None, outgoing=None, - chats=None, blacklist_chats=False, - require_input=True): + chats=None, blacklist_chats=False): if incoming and outgoing: raise ValueError('Can only set either incoming or outgoing') @@ -103,7 +186,7 @@ class NewMessage(_EventBuilder): # Tests passed so return the event return event - class Event: + class Event(_EventCommon): """ Represents the event of a new message. @@ -173,7 +256,9 @@ class NewMessage(_EventBuilder): another session) or incoming (i.e. someone else sent it). """ def __init__(self, message): - self._client = None + super().__init__(chat_peer=message.to_id, + msg_id=message.id, broadcast=bool(message.post)) + self.message = message self._text = None @@ -182,13 +267,6 @@ class NewMessage(_EventBuilder): self._input_sender = None self._sender = None - self.is_private = isinstance(message.to_id, types.PeerUser) - self.is_group = ( - isinstance(message.to_id, (types.PeerChat, types.PeerChannel)) - and not message.post - ) - self.is_channel = isinstance(message.to_id, types.PeerChannel) - self.is_reply = bool(message.reply_to_msg_id) self._reply_message = None @@ -208,54 +286,6 @@ class NewMessage(_EventBuilder): reply_to=self.message.id, *args, **kwargs) - def _get_input_entity(self, msg_id, entity_id, chat=None): - """ - Helper function to call GetMessages on the give msg_id and - return the input entity whose ID is the given entity ID. - """ - try: - if isinstance(chat, types.InputPeerChannel): - result = self._client( - functions.channels.GetMessagesRequest(chat, [msg_id]) - ) - else: - result = self._client( - functions.messages.GetMessagesRequest([msg_id]) - ) - except RPCError: - return - entity = { - utils.get_peer_id(x): x for x in itertools.chain( - getattr(result, 'chats', []), - getattr(result, 'users', [])) - }.get(entity_id) - if entity: - return utils.get_input_peer(entity) - - @property - def input_chat(self): - if self._input_chat is None: - try: - self._input_chat = self._client.get_input_entity( - self.message.to_id - ) - except (ValueError, TypeError): - # The library hasn't seen this chat, get the message - if not isinstance(self.message.to_id, types.PeerChannel): - # TODO For channels, getDifference? Maybe looking - # in the dialogs (which is already done) is enough. - self._input_chat = self._get_input_entity( - self.message.id, - utils.get_peer_id(self.message.to_id) - ) - return self._input_chat - - @property - def chat(self): - if self._chat is None and self.input_chat: - self._chat = self._client.get_entity(self._input_chat) - return self._chat - @property def input_sender(self): if self._input_sender is None: From 379c7755581afe3f43007523d88fe3be2cb9afe0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 11:37:17 +0100 Subject: [PATCH 101/108] Add a new ChatAction Event --- telethon/events/__init__.py | 235 ++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index d16a6529..31cf1f07 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -367,3 +367,238 @@ class NewMessage(_EventBuilder): @property def out(self): return self.message.out + + +class ChatAction(_EventBuilder): + """ + Represents an action in a chat (such as user joined, left, or new pin). + + Args: + chats (:obj:`entity`, optional): + May be one or more entities (username/peer/etc.). By default, + 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). + + """ + def __init__(self, chats=None, blacklist_chats=False): + # TODO This can probably be reused in all builders + self.chats = chats + self.blacklist_chats = blacklist_chats + + def resolve(self, client): + if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): + self.chats = set(utils.get_peer_id(x) + for x in client.get_input_entity(self.chats)) + elif self.chats is not None: + self.chats = {utils.get_peer_id( + client.get_input_entity(self.chats))} + + def build(self, update): + if isinstance(update, types.UpdateChannelPinnedMessage): + # Telegram sends UpdateChannelPinnedMessage and then + # UpdateNewChannelMessage with MessageActionPinMessage. + event = ChatAction.Event(types.PeerChannel(update.channel_id), + new_pin=update.id) + + elif isinstance(update, types.UpdateChatParticipantAdd): + event = ChatAction.Event(types.PeerChat(update.chat_id), + added_by=update.inviter_id or True, + users=update.user_id) + + elif isinstance(update, types.UpdateChatParticipantDelete): + event = ChatAction.Event(types.PeerChat(update.chat_id), + kicked_by=True, + users=update.user_id) + + elif (isinstance(update, ( + types.UpdateNewMessage, types.UpdateNewChannelMessage)) + and isinstance(update.message, types.MessageService)): + msg = update.message + action = update.message.action + if isinstance(action, types.MessageActionChatJoinedByLink): + event = ChatAction.Event(msg.to_id, + added_by=True, + users=msg.from_id) + elif isinstance(action, types.MessageActionChatAddUser): + event = ChatAction.Event(msg.to_id, + added_by=msg.from_id or True, + users=action.users) + elif isinstance(action, types.MessageActionChatDeleteUser): + event = ChatAction.Event(msg.to_id, + kicked_by=msg.from_id or True, + users=action.user_id) + elif isinstance(action, types.MessageActionChatCreate): + event = ChatAction.Event(msg.to_id, + users=action.users, + created=True, + new_title=action.title) + elif isinstance(action, types.MessageActionChannelCreate): + event = ChatAction.Event(msg.to_id, + created=True, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditTitle): + event = ChatAction.Event(msg.to_id, + new_title=action.title) + elif isinstance(action, types.MessageActionChatEditPhoto): + event = ChatAction.Event(msg.to_id, + new_photo=action.photo) + elif isinstance(action, types.MessageActionChatDeletePhoto): + event = ChatAction.Event(msg.to_id, + new_photo=True) + else: + return + else: + return + + if self.chats is None: + return event + else: + inside = utils.get_peer_id(event._chat_peer) in self.chats + if inside == self.blacklist_chats: + # If this chat matches but it's a blacklist ignore. + # If it doesn't match but it's a whitelist ignore. + return + + return event + + class Event(_EventCommon): + """ + Represents the event of a new chat action. + + Members: + new_pin (:obj:`bool`): + ``True`` if the pin has changed (new pin or removed). + + new_photo (:obj:`bool`): + ``True`` if there's a new chat photo (or it was removed). + + photo (:obj:`Photo`, optional): + The new photo (or ``None`` if it was removed). + + + user_added (:obj:`bool`): + ``True`` if the user was added by some other. + + user_joined (:obj:`bool`): + ``True`` if the user joined on their own. + + user_left (:obj:`bool`): + ``True`` if the user left on their own. + + user_kicked (:obj:`bool`): + ``True`` if the user was kicked by some other. + + created (:obj:`bool`, optional): + ``True`` if this chat was just created. + + new_title (:obj:`bool`, optional): + The new title string for the chat, if applicable. + """ + def __init__(self, chat_peer, new_pin=None, new_photo=None, + added_by=None, kicked_by=None, created=None, + users=None, new_title=None): + super().__init__(chat_peer=chat_peer, msg_id=new_pin) + + self.new_pin = isinstance(new_pin, int) + self._pinned_message = new_pin + + self.new_photo = new_photo is not None + self.photo = \ + new_photo if isinstance(new_photo, types.Photo) else None + + self._added_by = None + self._kicked_by = None + self.user_added, self.user_joined, self.user_left,\ + self.user_kicked = (False, False, False, False) + + if added_by is True: + self.user_joined = True + elif added_by: + self.user_added = True + self._added_by = added_by + + if kicked_by is True: + self.user_left = True + elif kicked_by: + self.user_kicked = True + self._kicked_by = kicked_by + + self.created = bool(created) + self._user_peers = users if isinstance(users, list) else [users] + self._users = None + self.new_title = new_title + + @property + def pinned_message(self): + """ + If ``new_pin`` is ``True``, this returns the (:obj:`Message`) + object that was pinned. + """ + if self._pinned_message == 0: + return None + + if isinstance(self._pinned_message, int) and self.input_chat: + r = self._client(functions.channels.GetMessagesRequest( + self._input_chat, [self._pinned_message] + )) + try: + self._pinned_message = next( + x for x in r.messages + if isinstance(x, types.Message) + and x.id == self._pinned_message + ) + except StopIteration: + pass + + if isinstance(self._pinned_message, types.Message): + return self._pinned_message + + @property + def added_by(self): + """ + The user who added ``users``, if applicable (``None`` otherwise). + """ + if self._added_by and not isinstance(self._added_by, types.User): + self._added_by = self._client.get_entity(self._added_by) + return self._added_by + + @property + def kicked_by(self): + """ + The user who kicked ``users``, if applicable (``None`` otherwise). + """ + if self._kicked_by and not isinstance(self._kicked_by, types.User): + self._kicked_by = self._client.get_entity(self._kicked_by) + return self._kicked_by + + @property + def user(self): + """ + The single user that takes part in this action (e.g. joined). + + Might be ``None`` if the information can't be retrieved or + there is no user taking part. + """ + try: + return next(self.users) + except (StopIteration, TypeError): + return None + + @property + def users(self): + """ + A list of users that take part in this action (e.g. joined). + + Might be empty if the information can't be retrieved or there + are no users taking part. + """ + if self._users is None and self._user_peers: + try: + self._users = self._client.get_entity(self._user_peers) + except (TypeError, ValueError): + self._users = [] + + return self._users From ffe826b35ff3fff12bbe2e16b3a7c7fb4a35b960 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 12:42:04 +0100 Subject: [PATCH 102/108] Add a new UserUpdate Event --- telethon/events/__init__.py | 151 ++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 31cf1f07..22665cf1 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -1,4 +1,5 @@ import abc +import datetime import itertools from .. import utils @@ -602,3 +603,153 @@ class ChatAction(_EventBuilder): self._users = [] return self._users + + +class UserUpdate(_EventBuilder): + """ + Represents an user update (gone online, offline, joined Telegram). + """ + + def build(self, update): + if isinstance(update, types.UpdateUserStatus): + event = UserUpdate.Event(update.user_id, + status=update.status) + else: + return + + return event + + def resolve(self, client): + pass + + class Event(_EventCommon): + """ + Represents the event of an user status update (last seen, joined). + + Members: + online (:obj:`bool`, optional): + ``True`` if the user is currently online, ``False`` otherwise. + Might be ``None`` if this information is not present. + + last_seen (:obj:`datetime`, optional): + Exact date when the user was last seen if known. + + until (:obj:`datetime`, optional): + Until when will the user remain online. + + within_months (:obj:`bool`): + ``True`` if the user was seen within 30 days. + + within_weeks (:obj:`bool`): + ``True`` if the user was seen within 7 days. + + recently (:obj:`bool`): + ``True`` if the user was seen within a day. + + action (:obj:`SendMessageAction`, optional): + The "typing" action if any the user is performing if any. + + cancel (:obj:`bool`): + ``True`` if the action was cancelling other actions. + + typing (:obj:`bool`): + ``True`` if the action is typing a message. + + recording (:obj:`bool`): + ``True`` if the action is recording something. + + uploading (:obj:`bool`): + ``True`` if the action is uploading something. + + playing (:obj:`bool`): + ``True`` if the action is playing a game. + + audio (:obj:`bool`): + ``True`` if what's being recorded/uploaded is an audio. + + round (:obj:`bool`): + ``True`` if what's being recorded/uploaded is a round video. + + video (:obj:`bool`): + ``True`` if what's being recorded/uploaded is an video. + + document (:obj:`bool`): + ``True`` if what's being uploaded is document. + + geo (:obj:`bool`): + ``True`` if what's being uploaded is a geo. + + photo (:obj:`bool`): + ``True`` if what's being uploaded is a photo. + + contact (:obj:`bool`): + ``True`` if what's being uploaded (selected) is a contact. + """ + def __init__(self, user_id, status=None, typing=None): + super().__init__(types.PeerUser(user_id)) + + self.online = None if status is None else \ + isinstance(status, types.UserStatusOnline) + + self.last_seen = status.was_online if \ + isinstance(status, types.UserStatusOffline) else None + + self.until = status.expires if \ + isinstance(status, types.UserStatusOnline) else None + + if self.last_seen: + diff = datetime.datetime.now() - self.last_seen + if diff < datetime.timedelta(days=30): + self.within_months = True + if diff < datetime.timedelta(days=7): + self.within_weeks = True + if diff < datetime.timedelta(days=1): + self.recently = True + else: + self.within_months = self.within_weeks = self.recently = False + if isinstance(status, (types.UserStatusOnline, + types.UserStatusRecently)): + self.within_months = self.within_weeks = True + self.recently = True + elif isinstance(status, types.UserStatusLastWeek): + self.within_months = self.within_weeks = True + elif isinstance(status, types.UserStatusLastMonth): + self.within_months = True + + self.action = typing + if typing: + self.cancel = self.typing = self.recording = self.uploading = \ + self.playing = False + self.audio = self.round = self.video = self.document = \ + self.geo = self.photo = self.contact = False + + if isinstance(typing, types.SendMessageCancelAction): + self.cancel = True + elif isinstance(typing, types.SendMessageTypingAction): + self.typing = True + elif isinstance(typing, types.SendMessageGamePlayAction): + self.playing = True + elif isinstance(typing, types.SendMessageGeoLocationAction): + self.geo = True + elif isinstance(typing, types.SendMessageRecordAudioAction): + self.recording = self.audio = True + elif isinstance(typing, types.SendMessageRecordRoundAction): + self.recording = self.round = True + elif isinstance(typing, types.SendMessageRecordVideoAction): + self.recording = self.video = True + elif isinstance(typing, types.SendMessageChooseContactAction): + self.uploading = self.contact = True + elif isinstance(typing, types.SendMessageUploadAudioAction): + self.uploading = self.audio = True + elif isinstance(typing, types.SendMessageUploadDocumentAction): + self.uploading = self.document = True + elif isinstance(typing, types.SendMessageUploadPhotoAction): + self.uploading = self.photo = True + elif isinstance(typing, types.SendMessageUploadRoundAction): + self.uploading = self.round = True + elif isinstance(typing, types.SendMessageUploadVideoAction): + self.uploading = self.video = True + + @property + def user(self): + return self.chat From 8786a5225777624dd41b5761f992c934de00319a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 13:05:34 +0100 Subject: [PATCH 103/108] Add a new MessageChanged Event --- telethon/events/__init__.py | 91 +++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 22665cf1..068ef031 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -753,3 +753,94 @@ class UserUpdate(_EventBuilder): @property def user(self): return self.chat + + +class MessageChanged(_EventBuilder): + """ + Represents a message changed (edited or deleted). + """ + + def build(self, update): + if isinstance(update, (types.UpdateEditMessage, + types.UpdateEditChannelMessage)): + event = MessageChanged.Event(edit_msg=update.message) + elif isinstance(update, (types.UpdateDeleteMessages, + types.UpdateDeleteChannelMessages)): + event = MessageChanged.Event( + deleted_ids=update.messages, + peer=types.PeerChannel(update.channel_id) + ) + else: + return + + return event + + def resolve(self, client): + pass + + class Event(_EventCommon): + """ + Represents the event of an user status update (last seen, joined). + + Members: + edited (:obj:`bool`): + ``True`` if the message was edited. + + message (:obj:`Message`, optional): + The new edited message, if any. + + deleted (:obj:`bool`): + ``True`` if the message IDs were deleted. + + deleted_ids (:obj:`List[int]`): + A list containing the IDs of the messages that were deleted. + + input_sender (:obj:`InputPeer`): + This is the input version of the user who edited the message. + Similarly to ``input_chat``, this doesn't have things like + username or similar, but still useful in some cases. + + Note that this might not be available if the library can't + find the input chat. + + sender (:obj:`User`): + This property will make an API call the first time to get the + most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + """ + def __init__(self, edit_msg=None, deleted_ids=None, peer=None): + super().__init__(peer if not edit_msg else edit_msg.to_id) + + self.edited = bool(edit_msg) + self.message = edit_msg + self.deleted = bool(deleted_ids) + self.deleted_ids = deleted_ids or [] + self._input_sender = None + self._sender = None + + @property + def input_sender(self): + # TODO Code duplication + if self._input_sender is None and self.message: + try: + self._input_sender = self._client.get_input_entity( + self.message.from_id + ) + except (ValueError, TypeError): + if isinstance(self.message.to_id, types.PeerChannel): + # We can rely on self.input_chat for this + self._input_sender = self._get_input_entity( + self.message.id, + self.message.from_id, + chat=self.input_chat + ) + + return self._client.get_input_entity(self.message.from_id) + + @property + def sender(self): + if self._sender is None and self.input_sender: + self._sender = self._client.get_entity(self._input_sender) + return self._sender From f5eda72329aa4ee20f348d5123841b6122ebf2eb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 13:08:09 +0100 Subject: [PATCH 104/108] Add a new Raw Event --- telethon/events/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 068ef031..697e8056 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -102,6 +102,17 @@ class _EventCommon(abc.ABC): return self._chat +class Raw(_EventBuilder): + """ + Represents a raw event. The event is the update itself. + """ + def resolve(self, client): + pass + + def build(self, update): + return update + + # Classes defined here are actually Event builders # for their inner Event classes. Inner ._client is # set later by the creator TelegramClient. From 51677543682a0be0b394fd29c90ee27d08154942 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 13:10:02 +0100 Subject: [PATCH 105/108] Fix input_sender events' property not using cached value --- telethon/events/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 697e8056..0697521f 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -314,7 +314,7 @@ class NewMessage(_EventBuilder): chat=self.input_chat ) - return self._client.get_input_entity(self.message.from_id) + return self._input_sender @property def sender(self): @@ -848,7 +848,7 @@ class MessageChanged(_EventBuilder): chat=self.input_chat ) - return self._client.get_input_entity(self.message.from_id) + return self._input_sender @property def sender(self): From 14389a0ef221c9335ed19163e476a69a422e5551 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 15:56:42 +0100 Subject: [PATCH 106/108] Better document the events module --- readthedocs/telethon.events.rst | 8 +++ readthedocs/telethon.rst | 8 +++ telethon/events/__init__.py | 109 ++++++++++++++++++-------------- 3 files changed, 76 insertions(+), 49 deletions(-) create mode 100644 readthedocs/telethon.events.rst diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst new file mode 100644 index 00000000..7cd062db --- /dev/null +++ b/readthedocs/telethon.events.rst @@ -0,0 +1,8 @@ +telethon\.events package +======================== + + +.. automodule:: telethon.events + :members: + :undoc-members: + :show-inheritance: diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst index e7a30c42..96becc9b 100644 --- a/readthedocs/telethon.rst +++ b/readthedocs/telethon.rst @@ -26,6 +26,14 @@ telethon\.telegram\_client module :undoc-members: :show-inheritance: +telethon\.events package +------------------------ + +.. toctree:: + + telethon.events + + telethon\.update\_state module ------------------------------ diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 0697521f..40678998 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -206,22 +206,6 @@ class NewMessage(_EventBuilder): message (:obj:`Message`): This is the original ``Message`` object. - input_chat (:obj:`InputPeer`): - This is the input chat (private, group, megagroup or channel) - to which the message was sent. This doesn't have the title or - anything, but is useful if you don't need those to avoid - further requests. - - Note that this might not be available if the library can't - find the input chat. - - chat (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional): - This property will make an API call the first time to get the - most up to date version of the chat, so use with care as - there is no caching besides local caching yet. - - ``input_chat`` needs to be available (often the case). - is_private (:obj:`bool`): True if the message was sent as a private message. @@ -231,41 +215,8 @@ class NewMessage(_EventBuilder): is_channel (:obj:`bool`): True if the message was sent on a megagroup or channel. - input_sender (:obj:`InputPeer`): - This 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. - - Note that this might not be available if the library can't - find the input chat. - - sender (:obj:`User`): - This property will make an API call the first time to get the - most up to date version of the sender, so use with care as - there is no caching besides local caching yet. - - ``input_sender`` needs to be available (often the case). - - text (:obj:`str`): - The message text, markdown-formatted. - - raw_text (:obj:`str`): - The raw message text, ignoring any formatting. - is_reply (:obj:`str`): Whether the message is a reply to some other or not. - - reply_message (:obj:`Message`, optional): - This property will make an API call the first time to get the - full ``Message`` object that one was replying to, so use with - care as there is no caching besides local caching yet. - - forward (:obj:`MessageFwdHeader`, optional): - The unmodified ``MessageFwdHeader``, if present. - - out (:obj:`bool`): - Whether the message is outgoing (i.e. you sent it from - another session) or incoming (i.e. someone else sent it). """ def __init__(self, message): super().__init__(chat_peer=message.to_id, @@ -300,6 +251,14 @@ class NewMessage(_EventBuilder): @property def input_sender(self): + """ + This (:obj:`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. + + Note that this might not be available if the library can't + find the input chat. + """ if self._input_sender is None: try: self._input_sender = self._client.get_input_entity( @@ -318,12 +277,22 @@ class NewMessage(_EventBuilder): @property def sender(self): + """ + This (:obj:`User`) will make an API call the first time to get + the most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + """ if self._sender is None and self.input_sender: self._sender = self._client.get_entity(self._input_sender) return self._sender @property def text(self): + """ + The message text, markdown-formatted. + """ if self._text is None: if not self.message.entities: return self.message.message @@ -333,10 +302,18 @@ class NewMessage(_EventBuilder): @property def raw_text(self): + """ + The raw message text, ignoring any formatting. + """ return self.message.message @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, + so use with care as there is no caching besides local caching yet. + """ if not self.message.reply_to_msg_id: return None @@ -356,14 +333,24 @@ class NewMessage(_EventBuilder): @property def forward(self): + """ + The unmodified (:obj:`MessageFwdHeader`, optional). + """ return self.message.fwd_from @property def media(self): + """ + The unmodified (:obj:`MessageMedia`, optional). + """ return self.message.media @property def photo(self): + """ + If the message media is a photo, + this returns the (:obj:`Photo`) object. + """ if isinstance(self.message.media, types.MessageMediaPhoto): photo = self.message.media.photo if isinstance(photo, types.Photo): @@ -371,6 +358,10 @@ class NewMessage(_EventBuilder): @property def document(self): + """ + If the message media is a document, + this returns the (:obj:`Document`) object. + """ if isinstance(self.message.media, types.MessageMediaDocument): doc = self.message.media.document if isinstance(doc, types.Document): @@ -378,6 +369,10 @@ class NewMessage(_EventBuilder): @property def out(self): + """ + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + """ return self.message.out @@ -763,6 +758,7 @@ class UserUpdate(_EventBuilder): @property def user(self): + """Alias around the chat (conversation).""" return self.chat @@ -833,6 +829,14 @@ class MessageChanged(_EventBuilder): @property def input_sender(self): + """ + This (:obj:`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. + + Note that this might not be available if the library can't + find the input chat. + """ # TODO Code duplication if self._input_sender is None and self.message: try: @@ -852,6 +856,13 @@ class MessageChanged(_EventBuilder): @property def sender(self): + """ + This (:obj:`User`) will make an API call the first time to get + the most up to date version of the sender, so use with care as + there is no caching besides local caching yet. + + ``input_sender`` needs to be available (often the case). + """ if self._sender is None and self.input_sender: self._sender = self._client.get_entity(self._input_sender) return self._sender From 10ebc442c9342262c0bfdb65155a19fdce9be9f4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 16:41:23 +0100 Subject: [PATCH 107/108] Add a friendlier introduction to events --- .../extra/advanced-usage/update-modes.rst | 144 +++++++++++++ .../extra/basic/working-with-updates.rst | 204 ++++++++---------- readthedocs/index.rst | 1 + readthedocs/telethon.events.rst | 4 - 4 files changed, 240 insertions(+), 113 deletions(-) create mode 100644 readthedocs/extra/advanced-usage/update-modes.rst diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst new file mode 100644 index 00000000..83495ef7 --- /dev/null +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -0,0 +1,144 @@ +.. _update-modes: + +============ +Update Modes +============ + + +The library can run in four distinguishable modes: + +- With no extra threads at all. +- With an extra thread that receives everything as soon as possible (default). +- With several worker threads that run your update handlers. +- A mix of the above. + +Since this section is about updates, we'll describe the simplest way to +work with them. + + +Using multiple workers +********************** + +When you create your client, simply pass a number to the +``update_workers`` parameter: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=2)`` + +You can set any amount of workers you want. The more you put, the more +update handlers that can be called "at the same time". One or two should +suffice most of the time, since setting more will not make things run +faster most of the times (actually, it could slow things down). + +The next thing you want to do is to add a method that will be called when +an `Update`__ arrives: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client.add_update_handler(callback) + # do more work here, or simply sleep! + +That's it! This is the old way to listen for raw updates, with no further +processing. If this feels annoying for you, remember that you can always +use :ref:`working-with-updates` but maybe use this for some other cases. + +Now let's do something more interesting. Every time an user talks to use, +let's reply to them with the same text reversed: + + .. code-block:: python + + from telethon.tl.types import UpdateShortMessage, PeerUser + + def replier(update): + if isinstance(update, UpdateShortMessage) and not update.out: + client.send_message(PeerUser(update.user_id), update.message[::-1]) + + + client.add_update_handler(replier) + input('Press enter to stop this!') + client.disconnect() + +We only ask you one thing: don't keep this running for too long, or your +contacts will go mad. + + +Spawning no worker at all +************************* + +All the workers do is loop forever and poll updates from a queue that is +filled from the ``ReadThread``, responsible for reading every item off +the network. If you only need a worker and the ``MainThread`` would be +doing no other job, this is the preferred way. You can easily do the same +as the workers like so: + + .. code-block:: python + + while True: + try: + update = client.updates.poll() + if not update: + continue + + print('I received', update) + except KeyboardInterrupt: + break + + client.disconnect() + +Note that ``poll`` accepts a ``timeout=`` parameter, and it will return +``None`` if other thread got the update before you could or if the timeout +expired, so it's important to check ``if not update``. + +This can coexist with the rest of ``N`` workers, or you can set it to ``0`` +additional workers: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` + +You **must** set it to ``0`` (or other number), as it defaults to ``None`` +and there is a different. ``None`` workers means updates won't be processed +*at all*, so you must set it to some value (``0`` or greater) if you want +``client.updates.poll()`` to work. + + +Using the main thread instead the ``ReadThread`` +************************************************ + +If you have no work to do on the ``MainThread`` and you were planning to have +a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary +``ReadThread`` at all like so: + + .. code-block:: python + + client = TelegramClient( + ... + spawn_read_thread=False + ) + +And then ``.idle()`` from the ``MainThread``: + + ``client.idle()`` + +You can stop it with :kbd:`Control+C`, and you can configure the signals +to be used in a similar fashion to `Python Telegram Bot`__. + +As a complete example: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client = TelegramClient('session', api_id, api_hash, + update_workers=1, spawn_read_thread=False) + + client.connect() + client.add_update_handler(callback) + client.idle() # ends with Ctrl+C + + +This is the preferred way to use if you're simply going to listen for updates. + +__ https://lonamiwebs.github.io/Telethon/types/update.html +__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst index 72155d86..a6c0a529 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -5,144 +5,130 @@ Working with Updates ==================== -.. note:: - - There are plans to make working with updates more friendly. Stay tuned! +The library comes with the :mod:`events` module. *Events* are an abstraction +over what Telegram calls `updates`__, and are meant to ease simple and common +usage when dealing with them, since there are many updates. Let's dive in! .. contents:: -The library can run in four distinguishable modes: - -- With no extra threads at all. -- With an extra thread that receives everything as soon as possible (default). -- With several worker threads that run your update handlers. -- A mix of the above. - -Since this section is about updates, we'll describe the simplest way to -work with them. - - -Using multiple workers -********************** - -When you create your client, simply pass a number to the -``update_workers`` parameter: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=4)`` - -4 workers should suffice for most cases (this is also the default on -`Python Telegram Bot`__). You can set this value to more, or even less -if you need. - -The next thing you want to do is to add a method that will be called when -an `Update`__ arrives: +Getting Started +*************** .. code-block:: python - def callback(update): - print('I received', update) + from telethon import TelegramClient, events - client.add_update_handler(callback) - # do more work here, or simply sleep! + client = TelegramClient(..., update_workers=1, spawn_read_thread=False) + client.start() -That's it! Now let's do something more interesting. -Every time an user talks to use, let's reply to them with the same -text reversed: + @client.on(events.NewMessage) + def my_event_handler(event): + if 'hello' in event.raw_text: + event.reply('hi!') + + client.idle() + + +Not much, but there might be some things unclear. What does this code do? .. code-block:: python - from telethon.tl.types import UpdateShortMessage, PeerUser + from telethon import TelegramClient, events - def replier(update): - if isinstance(update, UpdateShortMessage) and not update.out: - client.send_message(PeerUser(update.user_id), update.message[::-1]) + client = TelegramClient(..., update_workers=1, spawn_read_thread=False) + client.start() - client.add_update_handler(replier) - input('Press enter to stop this!') - client.disconnect() - -We only ask you one thing: don't keep this running for too long, or your -contacts will go mad. - - -Spawning no worker at all -************************* - -All the workers do is loop forever and poll updates from a queue that is -filled from the ``ReadThread``, responsible for reading every item off -the network. If you only need a worker and the ``MainThread`` would be -doing no other job, this is the preferred way. You can easily do the same -as the workers like so: +This is normal initialization (of course, pass session name, API ID and hash). +Nothing we don't know already. .. code-block:: python - while True: - try: - update = client.updates.poll() - if not update: - continue - - print('I received', update) - except KeyboardInterrupt: - break - - client.disconnect() - -Note that ``poll`` accepts a ``timeout=`` parameter, and it will return -``None`` if other thread got the update before you could or if the timeout -expired, so it's important to check ``if not update``. - -This can coexist with the rest of ``N`` workers, or you can set it to ``0`` -additional workers: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` - -You **must** set it to ``0`` (or other number), as it defaults to ``None`` -and there is a different. ``None`` workers means updates won't be processed -*at all*, so you must set it to some value (``0`` or greater) if you want -``client.updates.poll()`` to work. + @client.on(events.NewMessage) -Using the main thread instead the ``ReadThread`` -************************************************ - -If you have no work to do on the ``MainThread`` and you were planning to have -a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary -``ReadThread`` at all like so: +This Python decorator will attach itself to the ``my_event_handler`` +definition, and basically means that *on* a ``NewMessage`` *event*, +the callback function you're about to define will be called: .. code-block:: python - client = TelegramClient( - ... - spawn_read_thread=False - ) + def my_event_handler(event): + if 'hello' in event.raw_text: + event.reply('hi!') -And then ``.idle()`` from the ``MainThread``: - ``client.idle()`` - -You can stop it with :kbd:`Control+C`, and you can configure the signals -to be used in a similar fashion to `Python Telegram Bot`__. - -As a complete example: +If a ``NewMessage`` event occurs, and ``'hello'`` is in the text of the +message, we ``reply`` to the event with a ``'hi!'`` message. .. code-block:: python - def callback(update): - print('I received', update) - - client = TelegramClient('session', api_id, api_hash, - update_workers=1, spawn_read_thread=False) - - client.connect() - client.add_update_handler(callback) - client.idle() # ends with Ctrl+C - client.disconnect() + client.idle() + + +Finally, this tells the client that we're done with our code, and want +to listen for all these events to occur. Of course, you might want to +do other things instead idling. For this refer to :ref:`update-modes`. + + +More on events +************** + +The ``NewMessage`` event has much more than what was shown. You can access +the ``.sender`` of the message through that member, or even see if the message +had ``.media``, a ``.photo`` or a ``.document`` (which you could download with +for example ``client.download_media(event.photo)``. + +If you don't want to ``.reply`` as a reply, you can use the ``.respond()`` +method instead. Of course, there are more events such as ``ChatAction`` or +``UserUpdate``, and they're all used in the same way. Simply add the +``@client.on(events.XYZ)`` decorator on the top of your handler and you're +done! The event that will be passed always is of type ``XYZ.Event`` (for +instance, ``NewMessage.Event``), except for the ``Raw`` event which just +passes the ``Update`` object. + +You can put the same event on many handlers, and even different events on +the same handler. You can also have a handler work on only specific chats, +for example: + + + .. code-block:: python + + import ast + import random + + + @client.on(events.NewMessage(chats='TelethonOffTopic', incoming=True)) + def normal_handler(event): + if 'roll' in event.raw_text: + event.reply(str(random.randint(1, 6))) + + + @client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True)) + def admin_handler(event): + if event.raw_text.startswith('eval'): + expression = event.raw_text.replace('eval', '').strip() + event.reply(str(ast.literal_eval(expression))) + + +You can pass one or more chats to the ``chats`` parameter (as a list or tuple), +and only events from there will be processed. You can also specify whether you +want to handle incoming or outgoing messages (those you receive or those you +send). In this example, people can say ``'roll'`` and you will reply with a +random number, while if you say ``'eval 4+4'``, you will reply with the +solution. Try it! + + +Events module +************* + +.. automodule:: telethon.events + :members: + :undoc-members: + :show-inheritance: + -__ https://python-telegram-bot.org/ __ https://lonamiwebs.github.io/Telethon/types/update.html -__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 74c3b8e6..c1d2b6ec 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -49,6 +49,7 @@ heavy job for you, so you can focus on developing an application. extra/advanced-usage/accessing-the-full-api extra/advanced-usage/sessions + extra/advanced-usage/update-modes .. _Examples: diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst index 7cd062db..071a39bf 100644 --- a/readthedocs/telethon.events.rst +++ b/readthedocs/telethon.events.rst @@ -2,7 +2,3 @@ telethon\.events package ======================== -.. automodule:: telethon.events - :members: - :undoc-members: - :show-inheritance: From 6261affaa1bf1223705e967bd01f1d76b680208e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Feb 2018 17:16:28 +0100 Subject: [PATCH 108/108] Update to v0.17.1 --- readthedocs/extra/advanced-usage/.DS_Store | Bin 6148 -> 0 bytes readthedocs/extra/changelog.rst | 12 ++++++++++++ telethon/version.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) delete mode 100644 readthedocs/extra/advanced-usage/.DS_Store diff --git a/readthedocs/extra/advanced-usage/.DS_Store b/readthedocs/extra/advanced-usage/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0