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.
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:
-
-
How to create a TelegramClient.
-
Connecting to the Telegram servers and authorizing an user.
-
Retrieving a list of chats (dialogs).
-
Invoking a request without the built-in methods.
-
-
#!/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.
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…" />
+
+ Exact match:
+
+
+
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('