diff --git a/README.rst b/README.rst index 14fc52a5..3b7f7d55 100755 --- a/README.rst +++ b/README.rst @@ -5,14 +5,24 @@ Telethon ⭐️ Thanks **everyone** who has starred the project, it means a lot! **Telethon** is Telegram client implementation in **Python 3** which uses -the latest available API of Telegram. Remember to use **pip3** to install! +the latest available API of Telegram. + + +What is this? +------------- + +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 ---------- .. code:: sh - pip install telethon + pip3 install telethon Creating a client @@ -27,14 +37,10 @@ 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) async def main(): - await client.connect() - # Skip this if you already have a previous 'session_name.session' file - await client.sign_in(phone_number) - me = await client.sign_in(code=input('Code: ')) + await client.start() asyncio.get_event_loop().run_until_complete(main()) diff --git a/docs/docs_writer.py b/docs/docs_writer.py index 9eec6cd7..82241a48 100644 --- a/docs/docs_writer.py +++ b/docs/docs_writer.py @@ -28,6 +28,7 @@ class DocsWriter: self.table_columns = 0 self.table_columns_left = None self.write_copy_script = False + self._script = '' # High level writing def write_head(self, title, relative_css_path): @@ -254,6 +255,12 @@ class DocsWriter: self.write('' .format(text_to_copy, text)) + def add_script(self, src='', relative_src=None): + if relative_src: + self._script += ''.format(relative_src) + elif src: + self._script += ''.format(src) + def end_body(self): """Ends the whole document. This should be called the last""" if self.write_copy_script: @@ -268,7 +275,9 @@ class DocsWriter: 'catch(e){}}' '') - self.write('') + self.write('') + self.write(self._script) + self.write('') # "Low" level writing def write(self, s): diff --git a/docs/generate.py b/docs/generate.py index 4feb1518..ae2bd43c 100755 --- a/docs/generate.py +++ b/docs/generate.py @@ -224,6 +224,16 @@ def get_description(arg): return ' '.join(desc) +def copy_replace(src, dst, replacements): + """Copies the src file into dst applying the replacements dict""" + with open(src) as infile, open(dst, 'w') as outfile: + outfile.write(re.sub( + '|'.join(re.escape(k) for k in replacements), + lambda m: str(replacements[m.group(0)]), + infile.read() + )) + + def generate_documentation(scheme_file): """Generates the documentation HTML files from from scheme.tl to /methods and /constructors, etc. @@ -231,6 +241,7 @@ def generate_documentation(scheme_file): original_paths = { 'css': 'css/docs.css', 'arrow': 'img/arrow.svg', + 'search.js': 'js/search.js', '404': '404.html', 'index_all': 'index.html', 'index_types': 'types/index.html', @@ -366,6 +377,10 @@ def generate_documentation(scheme_file): else: docs.write_text('This type has no members.') + # TODO Bit hacky, make everything like this? (prepending '../') + depth = '../' * (2 if tlobject.namespace else 1) + docs.add_script(src='prependPath = "{}";'.format(depth)) + docs.add_script(relative_src=paths['search.js']) docs.end_body() # Find all the available types (which are not the same as the constructors) @@ -540,36 +555,31 @@ def generate_documentation(scheme_file): type_urls = fmt(types, get_path_for_type) constructor_urls = fmt(constructors, get_create_path_for) - replace_dict = { - 'type_count': len(types), - 'method_count': len(methods), - 'constructor_count': len(tlobjects) - len(methods), - 'layer': layer, - - 'request_names': request_names, - 'type_names': type_names, - 'constructor_names': constructor_names, - 'request_urls': request_urls, - 'type_urls': type_urls, - 'constructor_urls': constructor_urls - } - shutil.copy('../res/404.html', original_paths['404']) - - with open('../res/core.html') as infile,\ - open(original_paths['index_all'], 'w') as outfile: - text = infile.read() - for key, value in replace_dict.items(): - text = text.replace('{' + key + '}', str(value)) - - outfile.write(text) + copy_replace('../res/core.html', original_paths['index_all'], { + '{type_count}': len(types), + '{method_count}': len(methods), + '{constructor_count}': len(tlobjects) - len(methods), + '{layer}': layer, + }) + os.makedirs(os.path.abspath(os.path.join( + original_paths['search.js'], os.path.pardir + )), exist_ok=True) + copy_replace('../res/js/search.js', original_paths['search.js'], { + '{request_names}': request_names, + '{type_names}': type_names, + '{constructor_names}': constructor_names, + '{request_urls}': request_urls, + '{type_urls}': type_urls, + '{constructor_urls}': constructor_urls + }) # Everything done print('Documentation generated.') def copy_resources(): - for d in ['css', 'img']: + for d in ('css', 'img'): os.makedirs(d, exist_ok=True) shutil.copy('../res/img/arrow.svg', 'img') diff --git a/docs/res/core.html b/docs/res/core.html index bc5c04b3..25295494 100644 --- a/docs/res/core.html +++ b/docs/res/core.html @@ -14,29 +14,7 @@
- - - -
- -
Methods (0) -
    -
-
- -
Types (0) -
    -
-
- -
Constructors (0) -
    -
-
-
- -
+

Telethon API

This documentation was generated straight from the scheme.tl provided by Telegram. However, there is no official documentation per se @@ -44,8 +22,15 @@ page aims to provide easy access to all the available methods, their definition and parameters.

-

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

+

Please note that when you see this:

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

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

Index

Full example

-

The following example demonstrates:

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

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

- -

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

+

Documentation for this is now + here. +

- -
- + diff --git a/docs/res/js/search.js b/docs/res/js/search.js new file mode 100644 index 00000000..c63672e7 --- /dev/null +++ b/docs/res/js/search.js @@ -0,0 +1,172 @@ +root = document.getElementById("main_div"); +root.innerHTML = ` + + + +
+ + +
Methods (0) + +
+ +
Types (0) + +
+ +
Constructors (0) + +
+
+
+` + root.innerHTML + "
"; + +// HTML modified, now load documents +contentDiv = document.getElementById("contentDiv"); +searchDiv = document.getElementById("searchDiv"); +searchBox = document.getElementById("searchBox"); + +// Search lists +methodsList = document.getElementById("methodsList"); +methodsCount = document.getElementById("methodsCount"); + +typesList = document.getElementById("typesList"); +typesCount = document.getElementById("typesCount"); + +constructorsList = document.getElementById("constructorsList"); +constructorsCount = document.getElementById("constructorsCount"); + +// Exact match +exactMatch = document.getElementById("exactMatch"); +exactList = document.getElementById("exactList"); + +try { + requests = [{request_names}]; + types = [{type_names}]; + constructors = [{constructor_names}]; + + requestsu = [{request_urls}]; + typesu = [{type_urls}]; + constructorsu = [{constructor_urls}]; +} catch (e) { + requests = []; + types = []; + constructors = []; + requestsu = []; + typesu = []; + constructorsu = []; +} + +if (typeof prependPath !== 'undefined') { + for (var i = 0; i != requestsu.length; ++i) { + requestsu[i] = prependPath + requestsu[i]; + } + for (var i = 0; i != typesu.length; ++i) { + typesu[i] = prependPath + typesu[i]; + } + for (var i = 0; i != constructorsu.length; ++i) { + constructorsu[i] = prependPath + constructorsu[i]; + } +} + +// Given two input arrays "original" and "original urls" and a query, +// return a pair of arrays with matching "query" elements from "original". +// +// TODO Perhaps return an array of pairs instead a pair of arrays (for cache). +function getSearchArray(original, originalu, query) { + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().indexOf(query) != -1) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + return [destination, destinationu]; +} + +// Modify "countSpan" and "resultList" accordingly based on the elements +// given as [[elements], [element urls]] (both with the same length) +function buildList(countSpan, resultList, foundElements) { + var result = ""; + for (var i = 0; i < foundElements[0].length; ++i) { + result += '
  • '; + result += ''; + result += foundElements[0][i]; + result += '
  • '; + } + + if (countSpan) { + countSpan.innerHTML = "" + foundElements[0].length; + } + resultList.innerHTML = result; +} + +function updateSearch() { + if (searchBox.value) { + contentDiv.style.display = "none"; + searchDiv.style.display = ""; + + var query = searchBox.value.toLowerCase(); + + var foundRequests = getSearchArray(requests, requestsu, query); + var foundTypes = getSearchArray(types, typesu, query); + var foundConstructors = getSearchArray( + constructors, constructorsu, query + ); + + buildList(methodsCount, methodsList, foundRequests); + buildList(typesCount, typesList, foundTypes); + buildList(constructorsCount, constructorsList, foundConstructors); + + // Now look for exact matches + var original = requests.concat(constructors); + var originalu = requestsu.concat(constructorsu); + var destination = []; + var destinationu = []; + + for (var i = 0; i < original.length; ++i) { + if (original[i].toLowerCase().replace("request", "") == query) { + destination.push(original[i]); + destinationu.push(originalu[i]); + } + } + + if (destination.length == 0) { + exactMatch.style.display = "none"; + } else { + exactMatch.style.display = ""; + buildList(null, exactList, [destination, destinationu]); + } + } else { + contentDiv.style.display = ""; + searchDiv.style.display = "none"; + } +} + +function getQuery(name) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i != vars.length; ++i) { + var pair = vars[i].split("="); + if (pair[0] == name) + return pair[1]; + } +} + +var query = getQuery('q'); +if (query) { + searchBox.value = query; +} + +updateSearch(); 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/advanced-usage/sessions.rst b/readthedocs/extra/advanced-usage/sessions.rst index 7f1ded9b..fca7828e 100644 --- a/readthedocs/extra/advanced-usage/sessions.rst +++ b/readthedocs/extra/advanced-usage/sessions.rst @@ -4,7 +4,7 @@ Session Files ============== -The first parameter you pass the the constructor of the ``TelegramClient`` is +The first parameter you pass to the constructor of the ``TelegramClient`` is the ``session``, and defaults to be the session name (or full path). That is, if you create a ``TelegramClient('anon')`` instance and connect, an ``anon.session`` file will be created on the working directory. @@ -44,3 +44,70 @@ methods. For example, you could save it on a database: You should read the ````session.py```` source file to know what "relevant data" you need to keep track of. + + +Sessions and Heroku +------------------- + +You probably have a newer version of SQLite installed (>= 3.8.2). Heroku uses +SQLite 3.7.9 which does not support ``WITHOUT ROWID``. So, if you generated +your session file on a system with SQLite >= 3.8.2 your session file will not +work on Heroku's platform and will throw a corrupted schema error. + +There are multiple ways to solve this, the easiest of which is generating a +session file on your Heroku dyno itself. The most complicated is creating +a custom buildpack to install SQLite >= 3.8.2. + + +Generating a Session File on a Heroku Dyno +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + Due to Heroku's ephemeral filesystem all dynamically generated + files not part of your applications buildpack or codebase are destroyed + upon each restart. + +.. warning:: + Do not restart your application Dyno at any point prior to retrieving your + session file. Constantly creating new session files from Telegram's API + will result in a 24 hour rate limit ban. + +Due to Heroku's ephemeral filesystem all dynamically generated +files not part of your applications buildpack or codebase are destroyed upon +each restart. + +Using this scaffolded code we can start the authentication process: + + .. code-block:: python + + client = TelegramClient('login.session', api_id, api_hash).start() + +At this point your Dyno will crash because you cannot access stdin. Open your +Dyno's control panel on the Heroku website and "Run console" from the "More" +dropdown at the top right. Enter ``bash`` and wait for it to load. + +You will automatically be placed into your applications working directory. +So run your application ``python app.py`` and now you can complete the input +requests such as "what is your phone number" etc. + +Once you're successfully authenticated exit your application script with +CTRL + C and ``ls`` to confirm ``login.session`` exists in your current +directory. Now you can create a git repo on your account and commit +``login.session`` to that repo. + +You cannot ``ssh`` into your Dyno instance because it has crashed, so unless +you programatically upload this file to a server host this is the only way to +get it off of your Dyno. + +You now have a session file compatible with SQLite <= 3.8.2. Now you can +programatically fetch this file from an external host (Firebase, S3 etc.) +and login to your session using the following scaffolded code: + + .. code-block:: python + + fileName, headers = urllib.request.urlretrieve(file_url, 'login.session') + client = TelegramClient(os.path.abspath(fileName), api_id, api_hash).start() + +.. note:: + - ``urlretrieve`` will be depreciated, consider using ``requests``. + - ``file_url`` represents the location of your file. diff --git a/readthedocs/extra/advanced-usage/update-modes.rst b/readthedocs/extra/advanced-usage/update-modes.rst new file mode 100644 index 00000000..83495ef7 --- /dev/null +++ b/readthedocs/extra/advanced-usage/update-modes.rst @@ -0,0 +1,144 @@ +.. _update-modes: + +============ +Update Modes +============ + + +The library can run in four distinguishable modes: + +- With no extra threads at all. +- With an extra thread that receives everything as soon as possible (default). +- With several worker threads that run your update handlers. +- A mix of the above. + +Since this section is about updates, we'll describe the simplest way to +work with them. + + +Using multiple workers +********************** + +When you create your client, simply pass a number to the +``update_workers`` parameter: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=2)`` + +You can set any amount of workers you want. The more you put, the more +update handlers that can be called "at the same time". One or two should +suffice most of the time, since setting more will not make things run +faster most of the times (actually, it could slow things down). + +The next thing you want to do is to add a method that will be called when +an `Update`__ arrives: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client.add_update_handler(callback) + # do more work here, or simply sleep! + +That's it! This is the old way to listen for raw updates, with no further +processing. If this feels annoying for you, remember that you can always +use :ref:`working-with-updates` but maybe use this for some other cases. + +Now let's do something more interesting. Every time an user talks to use, +let's reply to them with the same text reversed: + + .. code-block:: python + + from telethon.tl.types import UpdateShortMessage, PeerUser + + def replier(update): + if isinstance(update, UpdateShortMessage) and not update.out: + client.send_message(PeerUser(update.user_id), update.message[::-1]) + + + client.add_update_handler(replier) + input('Press enter to stop this!') + client.disconnect() + +We only ask you one thing: don't keep this running for too long, or your +contacts will go mad. + + +Spawning no worker at all +************************* + +All the workers do is loop forever and poll updates from a queue that is +filled from the ``ReadThread``, responsible for reading every item off +the network. If you only need a worker and the ``MainThread`` would be +doing no other job, this is the preferred way. You can easily do the same +as the workers like so: + + .. code-block:: python + + while True: + try: + update = client.updates.poll() + if not update: + continue + + print('I received', update) + except KeyboardInterrupt: + break + + client.disconnect() + +Note that ``poll`` accepts a ``timeout=`` parameter, and it will return +``None`` if other thread got the update before you could or if the timeout +expired, so it's important to check ``if not update``. + +This can coexist with the rest of ``N`` workers, or you can set it to ``0`` +additional workers: + + ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` + +You **must** set it to ``0`` (or other number), as it defaults to ``None`` +and there is a different. ``None`` workers means updates won't be processed +*at all*, so you must set it to some value (``0`` or greater) if you want +``client.updates.poll()`` to work. + + +Using the main thread instead the ``ReadThread`` +************************************************ + +If you have no work to do on the ``MainThread`` and you were planning to have +a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary +``ReadThread`` at all like so: + + .. code-block:: python + + client = TelegramClient( + ... + spawn_read_thread=False + ) + +And then ``.idle()`` from the ``MainThread``: + + ``client.idle()`` + +You can stop it with :kbd:`Control+C`, and you can configure the signals +to be used in a similar fashion to `Python Telegram Bot`__. + +As a complete example: + + .. code-block:: python + + def callback(update): + print('I received', update) + + client = TelegramClient('session', api_id, api_hash, + update_workers=1, spawn_read_thread=False) + + client.connect() + client.add_update_handler(callback) + client.idle() # ends with Ctrl+C + + +This is the preferred way to use if you're simply going to listen for updates. + +__ https://lonamiwebs.github.io/Telethon/types/update.html +__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 81e19c83..bf565bb0 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 @@ -76,6 +76,26 @@ 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. 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. + +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`__ (via pip or manual) and then set the appropriated parameters: @@ -113,6 +133,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/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 88a6247c..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 @@ -11,7 +9,7 @@ Getting Started Simple Installation ******************* - ``pip install telethon`` + ``pip3 install telethon`` **More details**: :ref:`installation` @@ -27,14 +25,9 @@ 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.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` @@ -44,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 b4fb1ac2..e74cdae6 100644 --- a/readthedocs/extra/basic/installation.rst +++ b/readthedocs/extra/basic/installation.rst @@ -10,27 +10,28 @@ 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/ # 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 @@ -39,7 +40,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`` @@ -50,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 @@ -63,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..a6c0a529 100644 --- a/readthedocs/extra/basic/working-with-updates.rst +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -4,139 +4,131 @@ Working with Updates ==================== + +The library comes with the :mod:`events` module. *Events* are an abstraction +over what Telegram calls `updates`__, and are meant to ease simple and common +usage when dealing with them, since there are many updates. Let's dive in! + + .. contents:: -The library can run in four distinguishable modes: - -- With no extra threads at all. -- With an extra thread that receives everything as soon as possible (default). -- With several worker threads that run your update handlers. -- A mix of the above. - -Since this section is about updates, we'll describe the simplest way to -work with them. - - -Using multiple workers -********************** - -When you create your client, simply pass a number to the -``update_workers`` parameter: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=4)`` - -4 workers should suffice for most cases (this is also the default on -`Python Telegram Bot`__). You can set this value to more, or even less -if you need. - -The next thing you want to do is to add a method that will be called when -an `Update`__ arrives: +Getting Started +*************** .. code-block:: python - def callback(update): - print('I received', update) + from telethon import TelegramClient, events - client.add_update_handler(callback) - # do more work here, or simply sleep! + client = TelegramClient(..., update_workers=1, spawn_read_thread=False) + client.start() -That's it! Now let's do something more interesting. -Every time an user talks to use, let's reply to them with the same -text reversed: + @client.on(events.NewMessage) + def my_event_handler(event): + if 'hello' in event.raw_text: + event.reply('hi!') + + client.idle() + + +Not much, but there might be some things unclear. What does this code do? .. code-block:: python - from telethon.tl.types import UpdateShortMessage, PeerUser + from telethon import TelegramClient, events - def replier(update): - if isinstance(update, UpdateShortMessage) and not update.out: - client.send_message(PeerUser(update.user_id), update.message[::-1]) + client = TelegramClient(..., update_workers=1, spawn_read_thread=False) + client.start() - client.add_update_handler(replier) - input('Press enter to stop this!') - client.disconnect() - -We only ask you one thing: don't keep this running for too long, or your -contacts will go mad. - - -Spawning no worker at all -************************* - -All the workers do is loop forever and poll updates from a queue that is -filled from the ``ReadThread``, responsible for reading every item off -the network. If you only need a worker and the ``MainThread`` would be -doing no other job, this is the preferred way. You can easily do the same -as the workers like so: +This is normal initialization (of course, pass session name, API ID and hash). +Nothing we don't know already. .. code-block:: python - while True: - try: - update = client.updates.poll() - if not update: - continue - - print('I received', update) - except KeyboardInterrupt: - break - - client.disconnect() - -Note that ``poll`` accepts a ``timeout=`` parameter, and it will return -``None`` if other thread got the update before you could or if the timeout -expired, so it's important to check ``if not update``. - -This can coexist with the rest of ``N`` workers, or you can set it to ``0`` -additional workers: - - ``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` - -You **must** set it to ``0`` (or other number), as it defaults to ``None`` -and there is a different. ``None`` workers means updates won't be processed -*at all*, so you must set it to some value (``0`` or greater) if you want -``client.updates.poll()`` to work. + @client.on(events.NewMessage) -Using the main thread instead the ``ReadThread`` -************************************************ - -If you have no work to do on the ``MainThread`` and you were planning to have -a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary -``ReadThread`` at all like so: +This Python decorator will attach itself to the ``my_event_handler`` +definition, and basically means that *on* a ``NewMessage`` *event*, +the callback function you're about to define will be called: .. code-block:: python - client = TelegramClient( - ... - spawn_read_thread=False - ) + def my_event_handler(event): + if 'hello' in event.raw_text: + event.reply('hi!') -And then ``.idle()`` from the ``MainThread``: - ``client.idle()`` - -You can stop it with :kbd:`Control+C`, and you can configure the signals -to be used in a similar fashion to `Python Telegram Bot`__. - -As a complete example: +If a ``NewMessage`` event occurs, and ``'hello'`` is in the text of the +message, we ``reply`` to the event with a ``'hi!'`` message. .. code-block:: python - def callback(update): - print('I received', update) - - client = TelegramClient('session', api_id, api_hash, - update_workers=1, spawn_read_thread=False) - - client.connect() - client.add_update_handler(callback) - client.idle() # ends with Ctrl+C - client.disconnect() + client.idle() + + +Finally, this tells the client that we're done with our code, and want +to listen for all these events to occur. Of course, you might want to +do other things instead idling. For this refer to :ref:`update-modes`. + + +More on events +************** + +The ``NewMessage`` event has much more than what was shown. You can access +the ``.sender`` of the message through that member, or even see if the message +had ``.media``, a ``.photo`` or a ``.document`` (which you could download with +for example ``client.download_media(event.photo)``. + +If you don't want to ``.reply`` as a reply, you can use the ``.respond()`` +method instead. Of course, there are more events such as ``ChatAction`` or +``UserUpdate``, and they're all used in the same way. Simply add the +``@client.on(events.XYZ)`` decorator on the top of your handler and you're +done! The event that will be passed always is of type ``XYZ.Event`` (for +instance, ``NewMessage.Event``), except for the ``Raw`` event which just +passes the ``Update`` object. + +You can put the same event on many handlers, and even different events on +the same handler. You can also have a handler work on only specific chats, +for example: + + + .. code-block:: python + + import ast + import random + + + @client.on(events.NewMessage(chats='TelethonOffTopic', incoming=True)) + def normal_handler(event): + if 'roll' in event.raw_text: + event.reply(str(random.randint(1, 6))) + + + @client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True)) + def admin_handler(event): + if event.raw_text.startswith('eval'): + expression = event.raw_text.replace('eval', '').strip() + event.reply(str(ast.literal_eval(expression))) + + +You can pass one or more chats to the ``chats`` parameter (as a list or tuple), +and only events from there will be processed. You can also specify whether you +want to handle incoming or outgoing messages (those you receive or those you +send). In this example, people can say ``'roll'`` and you will reply with a +random number, while if you say ``'eval 4+4'``, you will reply with the +solution. Try it! + + +Events module +************* + +.. automodule:: telethon.events + :members: + :undoc-members: + :show-inheritance: + -__ https://python-telegram-bot.org/ __ https://lonamiwebs.github.io/Telethon/types/update.html -__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst new file mode 100644 index 00000000..57b11bec --- /dev/null +++ b/readthedocs/extra/changelog.rst @@ -0,0 +1,1463 @@ +.. _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 + + +Updates as Events (v0.17.1) +=========================== + +*Published at 2018/02/09* + +Of course there was more work to be done regarding updates, and it's here! +The library comes with a new ``events`` module (which you will often import +as ``from telethon import TelegramClient, events``). This are pretty much +all the additions that come with this version change, but they are a nice +addition. Refer to :ref:`working-with-updates` to get started with events. + + +Trust the Server with Updates (v0.17) +===================================== + +*Published at 2018/02/03* + +The library trusts the server with updates again. The library will *not* +check for duplicates anymore, and when the server kicks us, it will run +``GetStateRequest`` so the server starts sending updates again (something +it wouldn't do unless you invoked something, it seems). But this update +also brings a few more changes! + +Additions +~~~~~~~~~ + +- ``TLObject``'s override ``__eq__`` and ``__ne__``, so you can compare them. +- Added some missing cases on ``.get_input_entity()`` and peer functions. +- ``obj.to_dict()`` now has a ``'_'`` key with the type used. +- ``.start()`` can also sign up now. +- More parameters for ``.get_message_history()``. +- Updated list of RPC errors. +- HTML parsing thanks to **@tulir**! It can be used similar to markdown: + ``client.send_message(..., parse_mode='html')``. + + +Enhancements +~~~~~~~~~~~~ + +- ``client.send_file()`` now accepts ``Message``'s and + ``MessageMedia``'s as the ``file`` parameter. +- Some documentation updates and fixed to clarify certain things. +- New exact match feature on https://lonamiwebs.github.io/Telethon. +- Return as early as possible from ``.get_input_entity()`` and similar, + to avoid penalizing you for doing this right. + +Bug fixes +~~~~~~~~~ + +- ``.download_media()`` wouldn't accept a ``Document`` as parameter. +- The SQLite is now closed properly on disconnection. +- IPv6 addresses shouldn't use square braces. +- Fix regarding ``.log_out()``. +- The time offset wasn't being used (so having wrong system time would + cause the library not to work at all). + + +New ``.resolve()`` method (v0.16.2) +=================================== + +*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) +===================== + +*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) +==================================== + +*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. + +Additions +~~~~~~~~~ + +- 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. + +Internal 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. + +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. + +.. 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 +~~~~~~~~~~~~~~~~ + +- 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. + + +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. + +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! + +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 + 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). +- 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. + +Bug fixes +~~~~~~~~~ + +- ``.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* + +Bug fixes +~~~~~~~~~ + +- **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. + +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) +================================ + +*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. + +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. + + +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: + +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 + ``*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. + +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. +- ``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: + +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 + 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: + +Additions +~~~~~~~~~ + +- ``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: + +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. +- ``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``). + +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: + +Bug fixes +--------- + +- **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. + +Bug 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**. + +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. + +Breaking changes +---------------- + +- 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. + +Additions +--------- + +- 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. + +.. note:: + + 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 fixes +~~~~~~~~~~ + +- 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``. + +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**! + +Breaking changes +---------------- + +- 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. + +Additions +--------- + +- 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. + +Internal changes +---------------- + +- 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. + +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**. +- 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. + +Enhancements +~~~~~~~~~~~~ + +- **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. + +Enhancements +~~~~~~~~~~~~ + +- 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 | ++-----------------------+ + +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**. + +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``). + +Internal changes +~~~~~~~~~~~~~~~~ + +- ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. + +Bot login and proxy support (v0.8) +================================== + +*Published at 2017/04/14* + +Additions +~~~~~~~~~ + +- **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. + +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/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/developing/understanding-the-type-language.rst b/readthedocs/extra/developing/understanding-the-type-language.rst index c82063ef..8e5259a7 100644 --- a/readthedocs/extra/developing/understanding-the-type-language.rst +++ b/readthedocs/extra/developing/understanding-the-type-language.rst @@ -10,9 +10,7 @@ what other programming languages commonly call classes or structs. Every definition is written as follows for a Telegram object is defined as follows: -.. code:: tl - - name#id argument_name:argument_type = CommonType + ``name#id argument_name:argument_type = CommonType`` This means that in a single line you know what the ``TLObject`` name is. You know it's unique ID, and you know what arguments it has. It really 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 1bafec80..44ee6112 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 ************************* @@ -41,7 +46,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')) @@ -61,7 +66,7 @@ which use is very straightforward: client(AddChatUserRequest( chat_id, user_to_add, - fwd_limit=10 # allow the user to see the 10 last messages + fwd_limit=10 # Allow the user to see the 10 last messages )) @@ -70,7 +75,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 +85,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) @@ -107,8 +111,9 @@ a fixed limit: all_participants = [] while True: - participants = client.invoke(GetParticipantsRequest( - channel, ChannelParticipantsSearch(''), offset, limit + participants = client(GetParticipantsRequest( + channel, ChannelParticipantsSearch(''), offset, limit, + hash=0 )) if not participants.users: break @@ -164,14 +169,28 @@ Giving or revoking admin permissions can be done with the `EditAdminRequest`__: pin_messages=True, invite_link=None, edit_messages=None - ) + ) + # Equivalent to: + # rights = ChannelAdminRights( + # change_info=True, + # delete_messages=True, + # pin_messages=True + # ) - client(EditAdminRequest(channel, who, rights)) + # Once you have a ChannelAdminRights, invoke it + client(EditAdminRequest(channel, user, rights)) - -Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set -to ``True`` the ``post_messages`` and ``edit_messages`` fields. Those that -are ``None`` can be omitted (left here so you know `which are available`__. + # User will now be able to change group info, delete other people's + # messages and pin messages. + +| Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all +| parameters to ``True`` to give a user full permissions, as not all +| permissions are related to both broadcast channels/megagroups. +| +| E.g. trying to set ``post_messages=True`` in a megagroup will raise an +| error. It is recommended to always use keyword arguments, and to set only +| the permissions the user needs. If you don't need to change a permission, +| it can be omitted (full list `here`__). __ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html __ https://github.com/Kyle2142 diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index 880bac6f..ab38788c 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 ******************* @@ -42,12 +47,26 @@ into issues_. A valid example would be: .. code-block:: python + from telethon.tl.functions.messages import SearchRequest + from telethon.tl.types import InputMessagesFilterEmpty + + filter = InputMessagesFilterEmpty() result = client(SearchRequest( - entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100 + peer=peer, # On which chat/conversation + q='query', # What to search for + filter=filter, # Filter to use (maybe filter for media) + min_date=None, # Minimum date + max_date=None, # Maximum date + offset_id=0, # ID of the message to use as offset + add_offset=0, # Additional offset + limit=10, # How many results + max_id=0, # Maximum message ID + min_id=0, # Minimum message ID + from_id=None # Who must have sent the message (peer) )) -It's important to note that the optional parameter ``from_id`` has been left -omitted and thus defaults to ``None``. Changing it to InputUserEmpty_, as one +It's important to note that the optional parameter ``from_id`` could have +been omitted (defaulting to ``None``). Changing it to InputUserEmpty_, as one could think to specify "no user", won't work because this parameter is a flag, and it being unspecified has a different meaning. diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst index 55a21d7b..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). @@ -17,11 +18,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..4f7b5660 100644 --- a/readthedocs/extra/wall-of-shame.rst +++ b/readthedocs/extra/wall-of-shame.rst @@ -9,27 +9,28 @@ 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" `__: -> > **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. -If you have indeed read the wiki, and have tried looking for the method, + *"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 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/readthedocs/index.rst b/readthedocs/index.rst index 8e5c6053..c1d2b6ec 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -10,7 +10,21 @@ 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 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? +************* + +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: @@ -35,6 +49,7 @@ Please follow the links below to get you started. extra/advanced-usage/accessing-the-full-api extra/advanced-usage/sessions + extra/advanced-usage/update-modes .. _Examples: @@ -75,19 +90,20 @@ Please follow the links below to get you started. 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.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 ======================== diff --git a/readthedocs/telethon.events.rst b/readthedocs/telethon.events.rst new file mode 100644 index 00000000..071a39bf --- /dev/null +++ b/readthedocs/telethon.events.rst @@ -0,0 +1,4 @@ +telethon\.events package +======================== + + diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst index 2d3c269c..96becc9b 100644 --- a/readthedocs/telethon.rst +++ b/readthedocs/telethon.rst @@ -26,6 +26,14 @@ telethon\.telegram\_client module :undoc-members: :show-inheritance: +telethon\.events package +------------------------ + +.. toctree:: + + telethon.events + + telethon\.update\_state module ------------------------------ @@ -42,6 +50,13 @@ telethon\.utils module :undoc-members: :show-inheritance: +telethon\.session module +------------------------ + +.. automodule:: telethon.session + :members: + :undoc-members: + :show-inheritance: telethon\.cryto package ------------------------ @@ -58,21 +73,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/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() 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(' Telegram entity parser. +""" +from html import escape, unescape +from html.parser import HTMLParser +from collections import deque + +from ..tl.types import ( + MessageEntityBold, MessageEntityItalic, MessageEntityCode, + MessageEntityPre, MessageEntityEmail, MessageEntityUrl, + MessageEntityTextUrl +) + + +class HTMLToTelegramParser(HTMLParser): + def __init__(self): + super().__init__() + self.text = '' + self.entities = [] + self._building_entities = {} + self._open_tags = deque() + self._open_tags_meta = deque() + + def handle_starttag(self, tag, attrs): + self._open_tags.appendleft(tag) + self._open_tags_meta.appendleft(None) + + attrs = dict(attrs) + EntityType = None + args = {} + if tag == 'strong' or tag == 'b': + EntityType = MessageEntityBold + elif tag == 'em' or tag == 'i': + EntityType = MessageEntityItalic + elif tag == 'code': + try: + # If we're in the middle of a
     tag, this  tag is
    +                # probably intended for syntax highlighting.
    +                #
    +                # Syntax highlighting is set with
    +                #     codeblock
    +                # inside 
     tags
    +                pre = self._building_entities['pre']
    +                try:
    +                    pre.language = attrs['class'][len('language-'):]
    +                except KeyError:
    +                    pass
    +            except KeyError:
    +                EntityType = MessageEntityCode
    +        elif tag == 'pre':
    +            EntityType = MessageEntityPre
    +            args['language'] = ''
    +        elif tag == 'a':
    +            try:
    +                url = attrs['href']
    +            except KeyError:
    +                return
    +            if url.startswith('mailto:'):
    +                url = url[len('mailto:'):]
    +                EntityType = MessageEntityEmail
    +            else:
    +                if self.get_starttag_text() == url:
    +                    EntityType = MessageEntityUrl
    +                else:
    +                    EntityType = MessageEntityTextUrl
    +                    args['url'] = url
    +                    url = None
    +            self._open_tags_meta.popleft()
    +            self._open_tags_meta.appendleft(url)
    +
    +        if EntityType and tag not in self._building_entities:
    +            self._building_entities[tag] = EntityType(
    +                offset=len(self.text),
    +                # The length will be determined when closing the tag.
    +                length=0,
    +                **args)
    +
    +    def handle_data(self, text):
    +        text = unescape(text)
    +
    +        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
    +        if previous_tag == 'a':
    +            url = self._open_tags_meta[0]
    +            if url:
    +                text = url
    +
    +        for tag, entity in self._building_entities.items():
    +            entity.length += len(text.strip('\n'))
    +
    +        self.text += text
    +
    +    def handle_endtag(self, tag):
    +        try:
    +            self._open_tags.popleft()
    +            self._open_tags_meta.popleft()
    +        except IndexError:
    +            pass
    +        entity = self._building_entities.pop(tag, None)
    +        if entity:
    +            self.entities.append(entity)
    +
    +
    +def parse(html):
    +    """
    +    Parses the given HTML message and returns its stripped representation
    +    plus a list of the MessageEntity's that were found.
    +
    +    :param message: the message with HTML to be parsed.
    +    :return: a tuple consisting of (clean message, [message entities]).
    +    """
    +    parser = HTMLToTelegramParser()
    +    parser.feed(html)
    +    return parser.text, parser.entities
    +
    +
    +def unparse(text, entities):
    +    """
    +    Performs the reverse operation to .parse(), effectively returning HTML
    +    given a normal text and its MessageEntity's.
    +
    +    :param text: the text to be reconverted into HTML.
    +    :param entities: the MessageEntity's applied to the text.
    +    :return: a HTML representation of the combination of both inputs.
    +    """
    +    if not entities:
    +        return text
    +    html = []
    +    last_offset = 0
    +    for entity in entities:
    +        if entity.offset > last_offset:
    +            html.append(escape(text[last_offset:entity.offset]))
    +        elif entity.offset < last_offset:
    +            continue
    +
    +        skip_entity = False
    +        entity_text = escape(text[entity.offset:entity.offset + entity.length])
    +        entity_type = type(entity)
    +
    +        if entity_type == MessageEntityBold:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityItalic:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityCode:
    +            html.append('{}'.format(entity_text))
    +        elif entity_type == MessageEntityPre:
    +            if entity.language:
    +                html.append(
    +                    "
    \n"
    +                    "    \n"
    +                    "        {}\n"
    +                    "    \n"
    +                    "
    ".format(entity.language, entity_text)) + else: + html.append('
    {}
    ' + .format(entity_text)) + elif entity_type == MessageEntityEmail: + html.append('{0}'.format(entity_text)) + elif entity_type == MessageEntityUrl: + html.append('{0}'.format(entity_text)) + elif entity_type == MessageEntityTextUrl: + html.append('{}' + .format(escape(entity.url), entity_text)) + else: + skip_entity = True + last_offset = entity.offset + (0 if skip_entity else entity.length) + html.append(text[last_offset:]) + return ''.join(html) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 2826bdaa..c1839397 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(' SQL entities = self._check_migrate_json() - self._conn = sqlite3.connect(self.filename, check_same_thread=False) - c = self._conn.cursor() + self._conn = None + c = self._cursor() c.execute("select name from sqlite_master " "where type='table' and name='version'") if c.fetchone(): @@ -95,48 +113,47 @@ 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() 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""" + type integer, + id integer, + hash integer, + primary key(md5_digest, file_size, type) + )""" ) + c.execute("insert into version values (?)", (CURRENT_VERSION,)) # Migrating from JSON -> new table and may have entities if entities: c.executemany( 'insert or replace into entities values (?,?,?,?,?)', entities ) + self._update_session_table() c.close() self.save() @@ -151,30 +168,46 @@ 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) 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 def _upgrade_database(self, old): - 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""" - ) - old = 2 + c = self._cursor() + # old == 1 doesn't have the old sent_files so no need to drop + if old == 2: + # Old cache from old sent_files lasts then a day anyway, drop + 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() + + @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". + 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 @@ -185,11 +218,10 @@ class Session: self._update_session_table() # Fetch the auth_key corresponding to this data center - c = self._conn.cursor() + c = self._cursor() c.execute('select auth_key from sessions') tuple_ = c.fetchone() if tuple_: - from ..crypto import AuthKey self._auth_key = AuthKey(data=tuple_[0]) else: self._auth_key = None @@ -213,7 +245,13 @@ class Session: self._update_session_table() def _update_session_table(self): - c = self._conn.cursor() + c = self._cursor() + # While we can save multiple rows into the sessions table + # currently we only want to keep ONE as the tables don't + # tell us which auth_key's are usable and will work. Needs + # 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, @@ -226,6 +264,19 @@ class Session: """Saves the current session object as session_user_id.session""" self._conn.commit() + def _cursor(self): + """Asserts that the connection is open and returns a cursor""" + if self._conn is None: + self._conn = sqlite3.connect(self.filename) + return self._conn.cursor() + + def close(self): + """Closes the connection unless we're working in-memory""" + if self.filename != ':memory:': + if self._conn is not None: + self._conn.close() + self._conn = None + def delete(self): """Deletes the current session file""" if self.filename == ':memory:': @@ -265,7 +316,7 @@ class Session: now = time.time() nanoseconds = int((now - int(now)) * 1e+9) # "message identifiers are divisible by 4" - new_msg_id = (int(now) << 32) | (nanoseconds << 2) + new_msg_id = ((int(now) + self.time_offset) << 32) | (nanoseconds << 2) if self._last_msg_id >= new_msg_id: new_msg_id = self._last_msg_id + 4 @@ -313,12 +364,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 @@ -330,7 +388,7 @@ class Session: if not rows: return - self._conn.executemany( + self._cursor().executemany( 'insert or replace into entities values (?,?,?,?,?)', rows ) self.save() @@ -346,15 +404,19 @@ class Session: Raises ValueError if it cannot be found. """ - if isinstance(key, TLObject): - try: - # Try to early return if this key can be casted as input peer - return utils.get_input_peer(key) - except TypeError: - # Otherwise, get the ID of the peer + try: + if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): + # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) + # We already have an Input version, so nothing else required + return key + # Try to early return if this key can be casted as input peer + return utils.get_input_peer(key) + except (AttributeError, TypeError): + # Not a TLObject or can't be cast into InputPeer + if isinstance(key, TLObject): key = utils.get_peer_id(key) - c = self._conn.cursor() + c = self._cursor() if isinstance(key, str): phone = utils.parse_phone(key) if phone: @@ -384,15 +446,24 @@ 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._cursor().execute( + 'select id, hash from sent_files ' + 'where md5_digest = ? and file_size = ? and type = ?', + (md5_digest, file_size, _SentFileType.from_type(cls).value) ).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, file_id, part_count): - self._conn.execute( - 'insert into sent_files values (?,?,?,?)', - (md5_digest, file_size, file_id, part_count) - ) + def cache_file(self, md5_digest, file_size, instance): + if not isinstance(instance, (InputDocument, InputPhoto)): + raise TypeError('Cannot cache %s instance' % type(instance)) + + self._cursor().execute( + 'insert or replace into sent_files values (?,?,?,?,?)', ( + md5_digest, file_size, + _SentFileType.from_type(type(instance)).value, + instance.id, instance.access_hash + )) self.save() diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 748e05d2..9ed706ee 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -1,20 +1,19 @@ +import asyncio import logging import os -import asyncio -from datetime import timedelta -from hashlib import md5 -from io import BytesIO from asyncio import Lock +from datetime import timedelta -from . import helpers as utils, version -from .crypto import rsa, CdnDecrypter +from . import version, utils +from .crypto import rsa from .errors import ( - RPCError, BrokenAuthKeyError, ServerError, - FloodWaitError, 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 +from .session import Session +from .tl import TLObject from .tl.all_tlobjects import LAYER from .tl.functions import ( InitConnectionRequest, InvokeWithLayerRequest, PingRequest @@ -26,15 +25,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' @@ -83,7 +75,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 @@ -160,6 +152,7 @@ class TelegramBareClient: self._recv_loop = None self._ping_loop = None + self._idling = asyncio.Event() # Default PingRequest delay self._ping_delay = timedelta(minutes=1) @@ -241,9 +234,7 @@ class TelegramBareClient: self._sender.disconnect() # TODO Shall we clear the _exported_sessions, or may be reused? self._first_request = True # On reconnect it will be first again - - def __del__(self): - self.disconnect() + self.session.close() async def _reconnect(self, new_dc=None): """If 'new_dc' is not set, only a call to .connect() will be made @@ -264,7 +255,7 @@ class TelegramBareClient: __log__.info('Attempting reconnection...') return await self.connect() - except ConnectionResetError: + except ConnectionResetError as e: __log__.warning('Reconnection failed due to %s', e) return False finally: @@ -408,6 +399,9 @@ class TelegramBareClient: x.content_related for x in requests): raise TypeError('You can only invoke requests, not types!') + for request in requests: + await request.resolve(self, utils) + # For logging purposes if len(requests) == 1: which = type(requests[0]).__name__ @@ -416,12 +410,8 @@ class TelegramBareClient: len(requests), [type(x).__name__ for x in requests]) __log__.debug('Invoking %s', which) - - # 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 = self._recv_loop is None + call_receive = \ + not self._idling.is_set() or self._reconnect_lock.locked() for retry in range(retries): result = await self._invoke(call_receive, retry, *requests) @@ -435,7 +425,7 @@ class TelegramBareClient: await asyncio.sleep(retry + 1, loop=self._loop) if not self._reconnect_lock.locked(): with await self._reconnect_lock: - self._reconnect() + await self._reconnect() raise RuntimeError('Number of retries reached 0.') @@ -482,17 +472,26 @@ class TelegramBareClient: __log__.error('Authorization key seems broken and was invalid!') self.session.auth_key = None + except TypeNotFoundError as e: + # Only occurs when we call receive. May happen when + # we need to reconnect to another DC on login and + # Telegram somehow sends old objects (like configOld) + self._first_request = True + __log__.warning('Read unknown TLObject code ({}). ' + 'Setting again first_request flag.' + .format(hex(e.invalid_constructor_id))) + except TimeoutError: __log__.warning('Invoking timed out') # We will just retry - 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 @@ -514,13 +513,13 @@ class TelegramBareClient: UserMigrateError) as e: await self._reconnect(new_dc=e.new_dc) - return None + return await self._invoke(call_receive, retry, *requests) except ServerError as e: # 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 @@ -535,211 +534,12 @@ class TelegramBareClient: (code request sent and confirmed)?""" return self._authorized - # endregion - - # region Uploading media - - async def upload_file(self, - file, - part_size_kb=None, - file_name=None, - 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) + def get_input_entity(self, peer): """ - 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) - tuple_ = self.session.get_file(hash_md5.digest(), file_size) - if tuple_: - __log__.info('File was already cached, not uploading again') - return InputFile(name=file_name, - md5_checksum=tuple_[0], id=tuple_[2], parts=tuple_[3]) - 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 = await 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: - 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()) - - # endregion - - # region Downloading media - - async 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. + Stub method, no functionality so that calling + ``.get_input_entity()`` from ``.resolve()`` doesn't fail. """ - 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 = await cdn_decrypter.get_file() - else: - result = await client(GetFileRequest( - input_location, offset, part_size - )) - - if isinstance(result, FileCdnRedirect): - __log__.info('File lives in a CDN') - cdn_decrypter, result = \ - await CdnDecrypter.prepare_decrypter( - client, - await self._get_cdn_client(result), - result - ) - - except FileMigrateError as e: - __log__.info('File lives in another DC') - client = await 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() + return peer # endregion @@ -782,6 +582,7 @@ class TelegramBareClient: async def _recv_loop_impl(self): __log__.info('Starting to wait for items from the network') + self._idling.set() need_reconnect = False while self._user_connected: try: @@ -792,37 +593,27 @@ class TelegramBareClient: # Retry forever, this is instant messaging await asyncio.sleep(0.1, loop=self._loop) + # Telegram seems to kick us every 1024 items received + # from the network not considering things like bad salt. + # We must execute some *high level* request (that's not + # a ping) if we want to receive updates again. + # TODO Test if getDifference works too (better alternative) + await self._sender.send(GetStateRequest()) + __log__.debug('Receiving items from the network...') await self._sender.receive(update_state=self.updates) except TimeoutError: # No problem. - __log__.info('Receiving items from the network timed out') - except ConnectionError as error: + __log__.debug('Receiving items from the network timed out') + except ConnectionError: need_reconnect = True __log__.error('Connection was reset while receiving items') await asyncio.sleep(1, loop=self._loop) - except Exception as error: - # Unknown exception, pass it to the main thread - __log__.exception('Unknown exception in the read thread! ' - 'Disconnecting and leaving it to main thread') + except: + self._idling.clear() + raise - try: - import socks - if isinstance(error, ( - socks.GeneralProxyError, - socks.ProxyConnectionError - )): - # This is a known error, and it's not related to - # Telegram but rather to the proxy. Disconnect and - # hand it over to the main thread. - self._background_error = error - self.disconnect() - break - except ImportError: - "Not using PySocks, so it can't be a socket error" - - break - - self._recv_loop = None + self._idling.clear() + __log__.info('Connection closed by the user, not reading anymore') # endregion diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 5d1431e4..d119c194 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,11 +1,21 @@ +import asyncio +import hashlib +import io import itertools +import logging import os -import time +import sys from collections import OrderedDict, UserList from datetime import datetime, timedelta +from io import BytesIO from mimetypes import guess_type -import asyncio +from .crypto import CdnDecrypter +from .tl.custom import InputSizedFile +from .tl.functions.upload import ( + SaveBigFilePartRequest, SaveFilePartRequest, GetFileRequest +) +from .tl.types.upload import FileCdnRedirect try: import socks @@ -16,10 +26,11 @@ from . import TelegramBareClient from . import helpers, utils from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, - PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError + PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, + SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, + PhoneNumberOccupiedError ) from .network import ConnectionMode -from .tl import TLObject from .tl.custom import Draft, Dialog from .tl.functions.account import ( GetPasswordRequest @@ -34,7 +45,8 @@ from .tl.functions.contacts import ( from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, SendMediaRequest, SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, - CheckChatInviteRequest + CheckChatInviteRequest, ReadMentionsRequest, + SendMultiMediaRequest, UploadMediaRequest ) from .tl.functions import channels @@ -54,10 +66,14 @@ 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, + InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig, + InputDocument, InputMediaDocument, Document ) from .tl.types.messages import DialogsSlice -from .extensions import markdown +from .extensions import markdown, html + +__log__ = logging.getLogger(__name__) class TelegramClient(TelegramBareClient): @@ -142,8 +158,11 @@ class TelegramClient(TelegramBareClient): **kwargs ) - # Some fields to easy signing in - self._phone_code_hash = None + self._event_builders = [] + + # Some fields to easy signing in. Let {phone: hash} be + # a dictionary because the user may change their mind. + self._phone_code_hash = {} self._phone = None # endregion @@ -167,21 +186,145 @@ 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 = await 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 = await self(ResendCodeRequest(phone, self._phone_code_hash)) - self._phone_code_hash = result.phone_code_hash + result = await self(ResendCodeRequest(phone, phone_hash)) + self._phone_code_hash[phone] = result.phone_code_hash return result + async def start(self, + phone=lambda: input('Please enter your phone: '), + password=None, bot_token=None, + force_sms=False, code_callback=None, + first_name='New User', last_name=''): + """ + Convenience method to interactively connect and sign in if required, + also taking into consideration that 2FA may be enabled in the account. + + Example usage: + >>> client = await 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` | :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). + 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()`. + + first_name (:obj:`str`, optional): + The first name to be used if signing up. This has no + effect if the account already exists and you sign in. + + last_name (:obj:`str`, optional): + Similar to the first name, but for the last. Optional. + + Returns: + :obj:`TelegramClient`: + This client, so initialization can be chained with `.start()`. + """ + + 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 not phone and not bot_token: + raise ValueError('No phone number or bot token provided.') + + 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(): + await self.connect() + + if self.is_user_authorized(): + return self + + if bot_token: + await 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 + two_step_detected = False + + sent_code = await self.send_code_request(phone, force_sms=force_sms) + sign_up = not sent_code.phone_registered + while attempts < max_attempts: + try: + if sign_up: + me = await self.sign_up(code_callback(), first_name, last_name) + else: + # Raises SessionPasswordNeededError if 2FA enabled + me = await self.sign_in(phone, code_callback()) + break + except SessionPasswordNeededError: + two_step_detected = True + break + except PhoneNumberOccupiedError: + sign_up = False + except PhoneNumberUnoccupiedError: + sign_up = True + except (PhoneCodeEmptyError, PhoneCodeExpiredError, + PhoneCodeHashEmptyError, PhoneCodeInvalidError): + print('Invalid code. Please try again.', file=sys.stderr) + + attempts += 1 + 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 = await 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 + async def sign_in(self, phone=None, code=None, password=None, bot_token=None, phone_code_hash=None): """ @@ -214,11 +357,13 @@ class TelegramClient(TelegramBareClient): :meth:`.send_code_request()`. """ - if phone and not code: + if phone and not code and not password: return await 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.' @@ -226,17 +371,11 @@ 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 = await self(SignInRequest(phone, phone_code_hash, code)) - - except (PhoneCodeEmptyError, PhoneCodeExpiredError, - PhoneCodeHashEmptyError, PhoneCodeInvalidError): - return None + # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, + # PhoneCodeHashEmptyError or PhoneCodeInvalidError. + result = await self(SignInRequest(phone, phone_code_hash, str(code))) elif password: - salt = await self(GetPasswordRequest()).current_salt + salt = (await self(GetPasswordRequest())).current_salt result = await self(CheckPasswordRequest( helpers.get_password_hash(password, salt) )) @@ -274,7 +413,7 @@ class TelegramClient(TelegramBareClient): """ result = await 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 @@ -285,7 +424,7 @@ class TelegramClient(TelegramBareClient): async 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. @@ -297,7 +436,6 @@ class TelegramClient(TelegramBareClient): self.disconnect() self.session.delete() - self.session = None return True async def get_me(self): @@ -306,7 +444,7 @@ class TelegramClient(TelegramBareClient): or None if the request fails (hence, not authenticated). Returns: - Your own user. + :obj:`User`: Your own user. """ try: return (await self(GetUsersRequest([InputUserSelf()])))[0] @@ -407,15 +545,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)): @@ -453,6 +597,8 @@ class TelegramClient(TelegramBareClient): parse_mode = parse_mode.lower() if parse_mode in {'md', 'markdown'}: message, msg_entities = markdown.parse(message) + elif parse_mode.startswith('htm'): + message, msg_entities = html.parse(message) else: raise ValueError('Unknown parsing mode: {}'.format(parse_mode)) else: @@ -518,8 +664,8 @@ class TelegramClient(TelegramBareClient): return await self(messages.DeleteMessagesRequest(message_ids, revoke=revoke)) async def get_message_history(self, entity, limit=20, offset_date=None, - offset_id=0, max_id=0, min_id=0, - add_offset=0): + offset_id=0, max_id=0, min_id=0, add_offset=0, + batch_size=100, wait_time=None): """ Gets the message history for the specified entity @@ -554,6 +700,17 @@ class TelegramClient(TelegramBareClient): Additional message offset (all of the specified offsets + this offset = older messages). + batch_size (:obj:`int`): + Messages will be returned in chunks of this size (100 is + the maximum). While it makes no sense to modify this value, + you are still free to do so. + + wait_time (:obj:`int`): + Wait time between different ``GetHistoryRequest``. Use this + parameter to avoid hitting the ``FloodWaitError`` as needed. + If left to ``None``, it will default to 1 second only if + the limit is higher than 3000. + Returns: A list of messages with extra attributes: @@ -562,6 +719,13 @@ class TelegramClient(TelegramBareClient): * ``.fwd_from.sender`` = if fwd_from, who sent it originally. * ``.fwd_from.channel`` = if fwd_from, original channel. * ``.to`` = entity to which the message was sent. + + Notes: + Telegram's flood wait limit for ``GetHistoryRequest`` seems to + be around 30 seconds per 3000 messages, therefore a sleep of 1 + second is the default for this limit (or above). You may need + an higher limit, so you're free to set the ``batch_size`` that + you think may be good. """ entity = await self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) @@ -573,12 +737,16 @@ class TelegramClient(TelegramBareClient): )) return getattr(result, 'count', len(result.messages)), [], [] + if wait_time is None: + wait_time = 1 if limit > 3000 else 0 + + batch_size = min(max(batch_size, 1), 100) total_messages = 0 messages = UserList() entities = {} while len(messages) < limit: # Telegram has a hard limit of 100 - real_limit = min(limit - len(messages), 100) + real_limit = min(limit - len(messages), batch_size) result = await self(GetHistoryRequest( peer=entity, limit=real_limit, @@ -594,8 +762,6 @@ class TelegramClient(TelegramBareClient): ) total_messages = getattr(result, 'count', len(result.messages)) - # TODO We can potentially use self.session.database, but since - # it might be disabled, use a local dictionary. for u in result.users: entities[utils.get_peer_id(u)] = u for c in result.chats: @@ -606,18 +772,15 @@ class TelegramClient(TelegramBareClient): offset_id = result.messages[-1].id offset_date = result.messages[-1].date - - # Telegram limit seems to be 3000 messages within 30 seconds in - # batches of 100 messages each request (since the FloodWait was - # of 30 seconds). If the limit is greater than that, we will - # sleep 1s between each request. - if limit > 3000: - await asyncio.sleep(1, loop=self._loop) + await asyncio.sleep(wait_time) # 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? + # 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)]) @@ -637,7 +800,8 @@ class TelegramClient(TelegramBareClient): return messages - async def send_read_acknowledge(self, entity, message=None, max_id=None): + async 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"). @@ -652,22 +816,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 = await self.get_input_entity(entity) - if entity == InputPeerChannel: - return await self(channels.ReadHistoryRequest(entity, max_id=max_id)) - else: - return await self(messages.ReadHistoryRequest(entity, max_id=max_id)) + if clear_mentions: + await self(ReadMentionsRequest(entity)) + if max_id is None: + return True + + if max_id is not None: + if isinstance(entity, InputPeerChannel): + return await self(channels.ReadHistoryRequest(entity, max_id=max_id)) + else: + return await self(messages.ReadHistoryRequest(entity, max_id=max_id)) + + return False @staticmethod def _get_reply_to(reply_to): @@ -678,10 +857,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))) @@ -694,6 +875,7 @@ class TelegramClient(TelegramBareClient): reply_to=None, attributes=None, thumb=None, + allow_cache=True, **kwargs): """ Sends a file to the specified entity. @@ -702,7 +884,7 @@ class TelegramClient(TelegramBareClient): entity (:obj:`entity`): Who will receive the file. - file (:obj:`str` | :obj:`bytes` | :obj:`file`): + file (:obj:`str` | :obj:`bytes` | :obj:`file` | :obj:`media`): The path of the file, byte array, or stream that will be sent. Note that if a byte array or a stream is given, a filename or its type won't be inferred, and it will be sent as an @@ -711,6 +893,10 @@ class TelegramClient(TelegramBareClient): Subsequent calls with the very same file will result in immediate uploads, unless ``.clear_file_cache()`` is called. + Furthermore the file may be any media (a message, document, + photo or similar) so that it can be resent without the need + to download and re-upload it again. + caption (:obj:`str`, optional): Optional caption for the sent media message. @@ -730,29 +916,72 @@ 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 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 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__') 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): + return await self._send_album( + entity, file, caption=caption, + progress_callback=progress_callback, reply_to=reply_to + ) + # Not all are images, so send all the files one by one + return [ + await 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 + ] + entity = await self.get_input_entity(entity) + reply_to = self._get_reply_to(reply_to) + + if not isinstance(file, (str, bytes, io.IOBase)): + # The user may pass a Message containing media (or the media, + # or anything similar) that should be treated as a file. Try + # getting the input media for whatever they passed and send it. + try: + media = utils.get_input_media(file, user_caption=caption) + except TypeError: + pass # Can't turn whatever was given into media + else: + request = SendMediaRequest(entity, media, + reply_to_msg_id=reply_to) + return self._get_response_message(request, await self(request)) + + as_image = utils.is_image(file) and not force_document + use_cache = InputPhoto if as_image else InputDocument file_handle = await self.upload_file( - file, progress_callback=progress_callback + file, progress_callback=progress_callback, + use_cache=use_cache if allow_cache else None ) - if as_photo 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 @@ -762,14 +991,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: @@ -803,23 +1032,193 @@ class TelegramClient(TelegramBareClient): # Once the media type is properly specified and the file uploaded, # send the media message to the desired entity. - request = SendMediaRequest( - peer=await self.get_input_entity(entity), - media=media, - reply_to_msg_id=self._get_reply_to(reply_to) - ) - result = await self(request) + request = SendMediaRequest(entity, media, reply_to_msg_id=reply_to) + msg = self._get_response_message(request, await self(request)) + 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. + 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 self._get_response_message(request, result) + return msg - async def send_voice_note(self, entity, file, caption='', upload_progress=None, - reply_to=None): + async def send_voice_note(self, entity, file, caption='', + progress_callback=None, reply_to=None): """Wrapper method around .send_file() with is_voice_note=()""" return await 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 + async def _send_album(self, entity, files, caption='', + 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 = await self.get_input_entity(entity) + reply_to = self._get_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 = await self.upload_file(file, use_cache=InputPhoto) + if not isinstance(fh, InputPhoto): + input_photo = utils.get_input_photo((await 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 = await 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) + ] + + async def upload_file(self, file, part_size_kb=None, file_name=None, + use_cache=None, 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"``. + + 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: + ``InputFileBig`` if the file size is larger than 10MB, + ``InputSizedFile`` (subclass of ``InputFile``) otherwise. + """ + 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 + 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, + # 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.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', + 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 = await 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 InputSizedFile( + file_id, part_count, file_name, md5=hash_md5, size=file_size + ) + # endregion # region Downloading media requests @@ -845,9 +1244,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: @@ -937,7 +1341,7 @@ class TelegramClient(TelegramBareClient): return await self._download_photo( media, file, date, progress_callback ) - elif isinstance(media, MessageMediaDocument): + elif isinstance(media, (MessageMediaDocument, Document)): return await self._download_document( media, file, date, progress_callback ) @@ -948,7 +1352,6 @@ class TelegramClient(TelegramBareClient): async def _download_photo(self, photo, file, date, progress_callback): """Specialized version of .download_media() for photos""" - # Determine the photo and its largest size if isinstance(photo, MessageMediaPhoto): photo = photo.photo @@ -974,9 +1377,13 @@ class TelegramClient(TelegramBareClient): ) return file - async def _download_document(self, mm_doc, file, date, progress_callback): + async def _download_document(self, document, file, date, progress_callback): """Specialized version of .download_media() for documents""" - document = mm_doc.document + if isinstance(document, MessageMediaDocument): + document = document.document + if not isinstance(document, Document): + return + file_size = document.size possible_names = [] @@ -990,7 +1397,7 @@ class TelegramClient(TelegramBareClient): )) file = self._get_proper_filename( - file, 'document', utils.get_extension(mm_doc), + file, 'document', utils.get_extension(document), date=date, possible_names=possible_names ) @@ -1101,10 +1508,149 @@ class TelegramClient(TelegramBareClient): return result i += 1 + async 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 = await cdn_decrypter.get_file() + else: + result = await client(GetFileRequest( + input_location, offset, part_size + )) + + if isinstance(result, FileCdnRedirect): + __log__.info('File lives in a CDN') + cdn_decrypter, result = \ + await CdnDecrypter.prepare_decrypter( + client, + await self._get_cdn_client(result), + result + ) + + except FileMigrateError as e: + __log__.info('File lives in another DC') + client = await 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 + # region Event handling + + def on(self, event): + """ + + Turns the given entity into a valid Telegram user or chat. + + Args: + event (:obj:`_EventBuilder` | :obj:`type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + """ + if isinstance(event, type): + event = event() + + event.resolve(self) + + def decorator(f): + self._event_builders.append((event, f)) + return f + + if self._on_handler not in self.updates.handlers: + self.add_update_handler(self._on_handler) + + return decorator + + def _on_handler(self, update): + for builder, callback in self._event_builders: + event = builder.build(update) + if event: + event._client = self + callback(event) + + # endregion + # region Small utilities to make users' life easier async def get_entity(self, entity): @@ -1130,7 +1676,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 @@ -1196,13 +1742,15 @@ class TelegramClient(TelegramBareClient): if is_join_chat: invite = await self(CheckChatInviteRequest(string)) if isinstance(invite, ChatInvite): - # If it's an invite to a chat, the user must join before - # for the link to be resolved and work, otherwise raise. - if invite.channel: - return invite.channel + raise ValueError( + 'Cannot get entity from a channel ' + '(or group) that you are not part of' + ) elif isinstance(invite, ChatInviteAlready): return invite.chat else: + if string in ('me', 'self'): + return await self.get_me() result = await self(ResolveUsernameRequest(string)) for entity in itertools.chain(result.users, result.chats): if entity.username.lower() == string: @@ -1236,20 +1784,21 @@ class TelegramClient(TelegramBareClient): pass if isinstance(peer, str): + if peer in ('me', 'self'): + return InputPeerSelf() return utils.get_input_peer(await self._get_entity_from_string(peer)) is_peer = False 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/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 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/dialog.py b/telethon/tl/custom/dialog.py index 2cce89a3..61065bdf 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) async def send_message(self, *args, **kwargs): """ diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index c3e354fc..78358999 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 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 diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index f07d4bb9..1940580f 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -6,6 +6,7 @@ class TLObject: def __init__(self): self.confirm_received = None self.rpc_error = None + self.result = None # These should be overrode self.content_related = False # Only requests/functions/queries are @@ -18,14 +19,12 @@ class TLObject: """ if indent is None: if isinstance(obj, TLObject): - return '{}({})'.format(type(obj).__name__, ', '.join( - '{}={}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.to_dict(recursive=False).items() - )) + obj = obj.to_dict() + if isinstance(obj, dict): - return '{{{}}}'.format(', '.join( - '{}: {}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() + return '{}({})'.format(obj.get('_', 'dict'), ', '.join( + '{}={}'.format(k, TLObject.pretty_format(v)) + for k, v in obj.items() if k != '_' )) elif isinstance(obj, str) or isinstance(obj, bytes): return repr(obj) @@ -41,30 +40,28 @@ class TLObject: return repr(obj) else: result = [] - if isinstance(obj, TLObject) or isinstance(obj, dict): - if isinstance(obj, dict): - d = obj - start, end, sep = '{', '}', ': ' - else: - d = obj.to_dict(recursive=False) - start, end, sep = '(', ')', '=' - result.append(type(obj).__name__) + if isinstance(obj, TLObject): + obj = obj.to_dict() - result.append(start) - if d: + if isinstance(obj, dict): + result.append(obj.get('_', 'dict')) + result.append('(') + if obj: result.append('\n') indent += 1 - for k, v in d.items(): + for k, v in obj.items(): + if k == '_': + continue result.append('\t' * indent) result.append(k) - result.append(sep) + result.append('=') result.append(TLObject.pretty_format(v, indent)) result.append(',\n') result.pop() # last ',\n' indent -= 1 result.append('\n') result.append('\t' * indent) - result.append(end) + result.append(')') elif isinstance(obj, str) or isinstance(obj, bytes): result.append(repr(obj)) @@ -142,8 +139,27 @@ 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 __eq__(self, o): + return isinstance(o, type(self)) and self.to_dict() == o.to_dict() + + def __ne__(self, o): + return not isinstance(o, type(self)) or self.to_dict() != o.to_dict() + + def __str__(self): + return TLObject.pretty_format(self) + + def stringify(self): + return TLObject.pretty_format(self, indent=0) + # These should be overrode - def to_dict(self, recursive=True): + async def resolve(self, client, utils): + pass + + def to_dict(self): return {} def __bytes__(self): diff --git a/telethon/update_state.py b/telethon/update_state.py index ccceb60d..cc14e258 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -17,11 +17,8 @@ class UpdateState: def __init__(self, loop=None): self.handlers = [] - self._latest_updates = deque(maxlen=10) self._loop = loop if loop else asyncio.get_event_loop() - self._logger = logging.getLogger(__name__) - # https://core.telegram.org/api/updates self._state = tl.updates.State(0, 0, datetime.now(), 0, 0) @@ -38,44 +35,20 @@ class UpdateState: self._state = update return # Nothing else to be done - pts = getattr(update, 'pts', self._state.pts) - if hasattr(update, 'pts') and pts <= self._state.pts: - __log__.info('Ignoring %s, already have it', update) - return # We already handled this update - - self._state.pts = pts - - # TODO There must be a better way to handle updates rather than - # keeping a queue with the latest updates only, and handling - # the 'pts' correctly should be enough. However some updates - # like UpdateUserStatus (even inside UpdateShort) will be called - # repeatedly very often if invoking anything inside an update - # handler. TODO Figure out why. - """ - client = TelegramClient('anon', api_id, api_hash, update_workers=1) - client.connect() - def handle(u): - client.get_me() - client.add_update_handler(handle) - input('Enter to exit.') - """ - data = pickle.dumps(update.to_dict()) - if data in self._latest_updates: - __log__.info('Ignoring %s, already have it', update) - return # Duplicated too - - self._latest_updates.append(data) + if hasattr(update, 'pts'): + self._state.pts = update.pts + # After running the script for over an hour and receiving over + # 1000 updates, the only duplicates received were users going + # online or offline. We can trust the server until new reports. + if isinstance(update, tl.UpdateShort): + self.handle_update(update.update) # Expand "Updates" into "Update", and pass these to callbacks. # Since .users and .chats have already been processed, we # don't need to care about those either. - if isinstance(update, tl.UpdateShort): - self.handle_update(update.update) - elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): - for upd in update.updates: - self.handle_update(upd) - - # TODO Handle "Updates too long" + for u in update.updates: + self.handle_update(u) + # TODO Handle "tl.UpdatesTooLong" else: self.handle_update(update) diff --git a/telethon/utils.py b/telethon/utils.py index 48c867d1..3e310d3d 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -3,11 +3,10 @@ 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.contacts import ResolvedPeer from .tl.types import ( Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty, @@ -22,10 +21,10 @@ from .tl.types import ( GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull, - InputMediaUploadedPhoto, DocumentAttributeFilename, photos + InputMediaUploadedPhoto, DocumentAttributeFilename, photos, + TopPeer, InputNotifyPeer ) - USERNAME_RE = re.compile( r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' ) @@ -62,13 +61,13 @@ def get_extension(media): # Documents will come with a mime type if isinstance(media, MessageMediaDocument): - if isinstance(media.document, Document): - if media.document.mime_type == 'application/octet-stream': - # Octet stream are just bytes, which have no default extension - return '' - else: - extension = guess_extension(media.document.mime_type) - return extension if extension else '' + media = media.document + if isinstance(media, Document): + if media.mime_type == 'application/octet-stream': + # Octet stream are just bytes, which have no default extension + return '' + else: + return guess_extension(media.mime_type) or '' return '' @@ -81,12 +80,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() @@ -100,15 +99,18 @@ def get_input_peer(entity, allow_self=True): return InputPeerChannel(entity.id, entity.access_hash or 0) # Less common cases - if isinstance(entity, UserEmpty): - return InputPeerEmpty() - if isinstance(entity, InputUser): return InputPeerUser(entity.user_id, entity.access_hash) + if isinstance(entity, InputChannel): + return InputPeerChannel(entity.channel_id, entity.access_hash) + if isinstance(entity, InputUserSelf): return InputPeerSelf() + if isinstance(entity, UserEmpty): + return InputPeerEmpty() + if isinstance(entity, UserFull): return get_input_peer(entity.user) @@ -123,12 +125,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 +142,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 +171,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 +194,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 +214,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,24 +241,26 @@ 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), - 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): @@ -278,9 +282,10 @@ def get_input_media(media, user_caption=None, is_photo=False): if isinstance(media, (ChatPhoto, UserProfilePhoto)): if isinstance(media.photo_big, FileLocationUnavailable): - return get_input_media(media.photo_small, is_photo=True) + media = media.photo_small else: - return get_input_media(media.photo_big, is_photo=True) + media = media.photo_big + return get_input_media(media, user_caption=user_caption, is_photo=True) if isinstance(media, MessageMediaContact): return InputMediaContact( @@ -298,7 +303,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, ( @@ -307,11 +313,19 @@ def get_input_media(media, user_caption=None, is_photo=False): return InputMediaEmpty() if isinstance(media, Message): - return get_input_media(media.media) + return get_input_media( + media.media, user_caption=user_caption, is_photo=is_photo + ) _raise_cast_fail(media, 'InputMedia') +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): @@ -348,15 +362,18 @@ 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): + if isinstance(peer, (ResolvedPeer, InputNotifyPeer, TopPeer)): + peer = peer.peer + else: + # Not a Peer or an InputPeer, so first get its Input version + peer = get_input_peer(peer, allow_self=False) + except AttributeError: + _raise_cast_fail(peer, 'int') # Set the right ID/kind, or raise if the TLObject is not recognised if isinstance(peer, (PeerUser, InputPeerUser)): diff --git a/telethon/version.py b/telethon/version.py index e7fcc442..6cf35eba 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.17.1' 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): 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..a56d4b91 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -26,7 +26,9 @@ known_codes = { def fetch_errors(output, url=URL): print('Opening a connection to', url, '...') - r = urllib.request.urlopen(url) + r = urllib.request.urlopen(urllib.request.Request( + url, headers={'User-Agent' : 'Mozilla/5.0'} + )) print('Checking response...') data = json.loads( r.read().decode(r.info().get_param('charset') or 'utf-8') @@ -34,11 +36,11 @@ def fetch_errors(output, url=URL): if data.get('ok'): print('Response was okay, saving data') with open(output, 'w', encoding='utf-8') as f: - json.dump(data, f) + json.dump(data, f, sort_keys=True) return True else: print('The data received was not okay:') - print(json.dumps(data, indent=4)) + print(json.dumps(data, indent=4, sort_keys=True)) return False @@ -79,7 +81,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 +122,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', @@ -161,3 +166,11 @@ def generate_code(output, json_file, errors_desc): for pattern, name in patterns: f.write(' {}: {},\n'.format(repr(pattern), name)) f.write('}\n') + + +if __name__ == '__main__': + if input('generate (y/n)?: ').lower() == 'y': + generate_code('../telethon/errors/rpc_error_list.py', + 'errors.json', 'error_descriptions') + elif input('fetch (y/n)?: ').lower() == 'y': + fetch_errors('errors.json') diff --git a/telethon_generator/errors.json b/telethon_generator/errors.json index e807ff2d..31d31c0c 100644 --- a/telethon_generator/errors.json +++ b/telethon_generator/errors.json @@ -1 +1 @@ -{"ok": true, "result": {"400": {"account.updateProfile": ["ABOUT_TOO_LONG", "FIRSTNAME_INVALID"], "auth.importBotAuthorization": ["ACCESS_TOKEN_EXPIRED", "ACCESS_TOKEN_INVALID", "API_ID_INVALID"], "auth.sendCode": ["API_ID_INVALID", "INPUT_REQUEST_TOO_LONG", "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN", "PHONE_NUMBER_BANNED", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_PASSWORD_PROTECTED"], "messages.setInlineBotResults": ["ARTICLE_TITLE_EMPTY", "BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "MESSAGE_EMPTY", "QUERY_ID_INVALID", "REPLY_MARKUP_INVALID", "RESULT_TYPE_INVALID", "SEND_MESSAGE_TYPE_INVALID", "START_PARAM_INVALID"], "auth.importAuthorization": ["AUTH_BYTES_INVALID", "USER_ID_INVALID"], "invokeWithLayer": ["AUTH_BYTES_INVALID", "CDN_METHOD_INVALID", "CONNECTION_API_ID_INVALID", "CONNECTION_LANG_PACK_INVALID", "INPUT_LAYER_INVALID"], "channels.inviteToChannel": ["BOT_GROUPS_BLOCKED", "BOTS_TOO_MUCH", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_USER_DEACTIVATED", "USER_BANNED_IN_CHANNEL", "USER_BLOCKED", "USER_BOT", "USER_ID_INVALID", "USER_KICKED", "USER_NOT_MUTUAL_CONTACT", "USERS_TOO_MUCH"], "messages.getInlineBotResults": ["BOT_INLINE_DISABLED", "BOT_INVALID"], "messages.startBot": ["BOT_INVALID", "PEER_ID_INVALID", "START_PARAM_EMPTY", "START_PARAM_INVALID"], "messages.uploadMedia": ["BOT_MISSING", "PEER_ID_INVALID"], "stickers.addStickerToSet": ["BOT_MISSING", "STICKERSET_INVALID"], "stickers.changeStickerPosition": ["BOT_MISSING", "STICKER_INVALID"], "stickers.createStickerSet": ["BOT_MISSING", "PACK_SHORT_NAME_INVALID", "PEER_ID_INVALID", "STICKERS_EMPTY", "USER_ID_INVALID"], "stickers.removeStickerFromSet": ["BOT_MISSING", "STICKER_INVALID"], "messages.sendMessage": ["BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "ENTITY_MENTION_USER_INVALID", "INPUT_USER_DEACTIVATED", "MESSAGE_EMPTY", "MESSAGE_TOO_LONG", "PEER_ID_INVALID", "REPLY_MARKUP_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "phone.acceptCall": ["CALL_ALREADY_ACCEPTED", "CALL_ALREADY_DECLINED", "CALL_PEER_INVALID", "CALL_PROTOCOL_FLAGS_INVALID"], "phone.discardCall": ["CALL_ALREADY_ACCEPTED", "CALL_PEER_INVALID"], "phone.confirmCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.receivedCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.saveCallDebug": ["CALL_PEER_INVALID", "DATA_JSON_INVALID"], "phone.setCallRating": ["CALL_PEER_INVALID"], "phone.requestCall": ["CALL_PROTOCOL_FLAGS_INVALID", "PARTICIPANT_VERSION_OUTDATED", "USER_ID_INVALID"], "updates.getDifference": ["CDN_METHOD_INVALID", "DATE_EMPTY", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID"], "upload.getCdnFileHashes": ["CDN_METHOD_INVALID", "RSA_DECRYPT_FAILED"], "channels.checkUsername": ["CHANNEL_INVALID", "CHAT_ID_INVALID", "USERNAME_INVALID"], "channels.deleteChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteUserHistory": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.editAbout": ["CHANNEL_INVALID", "CHAT_ABOUT_NOT_MODIFIED", "CHAT_ABOUT_TOO_LONG", "CHAT_ADMIN_REQUIRED"], "channels.editAdmin": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_CREATOR", "USER_ID_INVALID", "USER_NOT_MUTUAL_CONTACT"], "channels.editBanned": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ADMIN_INVALID", "USER_ID_INVALID"], "channels.editPhoto": ["CHANNEL_INVALID"], "channels.editTitle": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.exportInvite": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "INVITE_HASH_EXPIRED"], "channels.exportMessageLink": ["CHANNEL_INVALID"], "channels.getAdminLog": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.getChannels": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getFullChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "MESSAGE_IDS_EMPTY"], "channels.getParticipant": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_ID_INVALID", "USER_NOT_PARTICIPANT"], "channels.getParticipants": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID"], "channels.joinChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHANNELS_TOO_MUCH"], "channels.leaveChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "USER_CREATOR", "USER_NOT_PARTICIPANT"], "channels.readHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.readMessageContents": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.reportSpam": ["CHANNEL_INVALID"], "channels.setStickers": ["CHANNEL_INVALID", "PARTICIPANTS_TOO_FEW"], "channels.toggleInvites": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.toggleSignatures": ["CHANNEL_INVALID"], "channels.updatePinnedMessage": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.updateUsername": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USERNAME_INVALID", "USERNAME_OCCUPIED"], "messages.editMessage": ["CHANNEL_INVALID", "MESSAGE_EDIT_TIME_EXPIRED", "MESSAGE_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED", "PEER_ID_INVALID"], "messages.forwardMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "MEDIA_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_IDS_EMPTY", "PEER_ID_INVALID", "RANDOM_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.getHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerSettings": ["CHANNEL_INVALID", "PEER_ID_INVALID"], "messages.sendMedia": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "FILE_PART_0_MISSING", "FILE_PART_LENGTH_INVALID", "FILE_PARTS_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_CAPTION_TOO_LONG", "MEDIA_EMPTY", "PEER_ID_INVALID", "PHOTO_EXT_INVALID", "USER_IS_BLOCKED", "USER_IS_BOT", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.setTyping": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "updates.getChannelDifference": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID", "RANGES_INVALID"], "messages.getMessagesViews": ["CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerDialogs": ["CHANNEL_PRIVATE", "PEER_ID_INVALID"], "messages.addChatUser": ["CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_ALREADY_PARTICIPANT", "USER_ID_INVALID", "USERS_TOO_MUCH"], "messages.discardEncryption": ["CHAT_ID_EMPTY", "ENCRYPTION_ALREADY_DECLINED", "ENCRYPTION_ID_INVALID"], "messages.acceptEncryption": ["CHAT_ID_INVALID", "ENCRYPTION_ALREADY_ACCEPTED", "ENCRYPTION_ALREADY_DECLINED"], "messages.deleteChatUser": ["CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_NOT_PARTICIPANT"], "messages.editChatAdmin": ["CHAT_ID_INVALID"], "messages.editChatPhoto": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.editChatTitle": ["CHAT_ID_INVALID"], "messages.exportChatInvite": ["CHAT_ID_INVALID"], "messages.forwardMessage": ["CHAT_ID_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID", "YOU_BLOCKED_USER"], "messages.getChats": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getFullChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.migrateChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.reportEncryptedSpam": ["CHAT_ID_INVALID"], "messages.sendEncrypted": ["CHAT_ID_INVALID", "DATA_INVALID", "MSG_WAIT_FAILED"], "messages.setEncryptedTyping": ["CHAT_ID_INVALID"], "messages.toggleChatAdmins": ["CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "channels.createChannel": ["CHAT_TITLE_EMPTY"], "auth.recoverPassword": ["CODE_EMPTY"], "account.confirmPhone": ["CODE_HASH_INVALID", "PHONE_CODE_EMPTY"], "initConnection": ["CONNECTION_LAYER_INVALID", "INPUT_FETCH_FAIL"], "contacts.block": ["CONTACT_ID_INVALID"], "contacts.deleteContact": ["CONTACT_ID_INVALID"], "contacts.unblock": ["CONTACT_ID_INVALID"], "messages.getBotCallbackAnswer": ["DATA_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID"], "messages.sendEncryptedService": ["DATA_INVALID", "MSG_WAIT_FAILED"], "auth.exportAuthorization": ["DC_ID_INVALID"], "messages.requestEncryption": ["DH_G_A_INVALID", "USER_ID_INVALID"], "auth.bindTempAuthKey": ["ENCRYPTED_MESSAGE_INVALID", "INPUT_REQUEST_TOO_LONG", "TEMP_AUTH_KEY_EMPTY"], "messages.setBotPrecheckoutResults": ["ERROR_TEXT_EMPTY"], "contacts.importCard": ["EXPORT_CARD_INVALID"], "upload.getFile": ["FILE_ID_INVALID", "LIMIT_INVALID", "LOCATION_INVALID", "OFFSET_INVALID"], "photos.uploadProfilePhoto": ["FILE_PART_0_MISSING", "FILE_PARTS_INVALID", "IMAGE_PROCESS_FAILED", "PHOTO_CROP_SIZE_SMALL", "PHOTO_EXT_INVALID"], "upload.saveBigFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "FILE_PART_SIZE_INVALID", "FILE_PARTS_INVALID"], "upload.saveFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID"], "auth.signUp": ["FIRSTNAME_INVALID", "PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_OCCUPIED"], "messages.saveGif": ["GIF_ID_INVALID"], "account.resetAuthorization": ["HASH_INVALID"], "account.sendConfirmPhoneCode": ["HASH_INVALID"], "messages.sendInlineBotResult": ["INLINE_RESULT_EXPIRED", "PEER_ID_INVALID", "QUERY_ID_EMPTY"], "messages.getDialogs": ["INPUT_CONSTRUCTOR_INVALID", "OFFSET_PEER_ID_INVALID"], "messages.search": ["INPUT_CONSTRUCTOR_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "PEER_ID_NOT_SUPPORTED", "SEARCH_QUERY_EMPTY", "USER_ID_INVALID"], "messages.checkChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID"], "messages.importChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID", "USER_ALREADY_PARTICIPANT", "USERS_TOO_MUCH"], "{}": ["INVITE_HASH_EXPIRED"], "langpack.getDifference": ["LANG_PACK_INVALID"], "langpack.getLangPack": ["LANG_PACK_INVALID"], "langpack.getLanguages": ["LANG_PACK_INVALID"], "langpack.getStrings": ["LANG_PACK_INVALID"], "upload.getWebFile": ["LOCATION_INVALID"], "photos.getUserPhotos": ["MAX_ID_INVALID", "USER_ID_INVALID"], "auth.sendInvites": ["MESSAGE_EMPTY"], "messages.editInlineBotMessage": ["MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED"], "messages.getInlineGameHighScores": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setInlineGameScore": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "payments.getPaymentForm": ["MESSAGE_ID_INVALID"], "payments.getPaymentReceipt": ["MESSAGE_ID_INVALID"], "payments.sendPaymentForm": ["MESSAGE_ID_INVALID"], "payments.validateRequestedInfo": ["MESSAGE_ID_INVALID"], "messages.readEncryptedHistory": ["MSG_WAIT_FAILED"], "messages.receivedQueue": ["MSG_WAIT_FAILED"], "messages.sendEncryptedFile": ["MSG_WAIT_FAILED"], "account.updatePasswordSettings": ["NEW_SALT_INVALID", "NEW_SETTINGS_INVALID", "PASSWORD_HASH_INVALID"], "auth.requestPasswordRecovery": ["PASSWORD_EMPTY"], "account.getPasswordSettings": ["PASSWORD_HASH_INVALID"], "account.getTmpPassword": ["PASSWORD_HASH_INVALID", "TMP_PASSWORD_DISABLED"], "auth.checkPassword": ["PASSWORD_HASH_INVALID"], "account.getNotifySettings": ["PEER_ID_INVALID"], "account.reportPeer": ["PEER_ID_INVALID"], "account.updateNotifySettings": ["PEER_ID_INVALID"], "contacts.resetTopPeerRating": ["PEER_ID_INVALID"], "messages.deleteHistory": ["PEER_ID_INVALID"], "messages.getGameHighScores": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getMessageEditData": ["PEER_ID_INVALID"], "messages.getUnreadMentions": ["PEER_ID_INVALID"], "messages.hideReportSpam": ["PEER_ID_INVALID"], "messages.readHistory": ["PEER_ID_INVALID"], "messages.reorderPinnedDialogs": ["PEER_ID_INVALID"], "messages.reportSpam": ["PEER_ID_INVALID"], "messages.saveDraft": ["PEER_ID_INVALID"], "messages.sendScreenshotNotification": ["PEER_ID_INVALID"], "messages.setGameScore": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.toggleDialogPin": ["PEER_ID_INVALID"], "auth.signIn": ["PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_UNOCCUPIED"], "auth.checkPhone": ["PHONE_NUMBER_BANNED", "PHONE_NUMBER_INVALID"], "account.changePhone": ["PHONE_NUMBER_INVALID"], "account.sendChangePhoneCode": ["PHONE_NUMBER_INVALID"], "auth.cancelCode": ["PHONE_NUMBER_INVALID"], "auth.resendCode": ["PHONE_NUMBER_INVALID"], "account.getPrivacy": ["PRIVACY_KEY_INVALID"], "account.setPrivacy": ["PRIVACY_KEY_INVALID"], "bots.answerWebhookJSONQuery": ["QUERY_ID_INVALID", "USER_BOT_INVALID"], "messages.setBotCallbackAnswer": ["QUERY_ID_INVALID"], "messages.setBotShippingResults": ["QUERY_ID_INVALID"], "contacts.search": ["QUERY_TOO_SHORT", "SEARCH_QUERY_EMPTY"], "messages.getDhConfig": ["RANDOM_LENGTH_INVALID"], "upload.reuploadCdnFile": ["RSA_DECRYPT_FAILED"], "messages.searchGifs": ["SEARCH_QUERY_EMPTY"], "messages.searchGlobal": ["SEARCH_QUERY_EMPTY"], "messages.getDocumentByHash": ["SHA256_HASH_INVALID"], "messages.faveSticker": ["STICKER_ID_INVALID"], "messages.saveRecentSticker": ["STICKER_ID_INVALID"], "messages.getStickerSet": ["STICKERSET_INVALID"], "messages.installStickerSet": ["STICKERSET_INVALID"], "messages.uninstallStickerSet": ["STICKERSET_INVALID"], "account.registerDevice": ["TOKEN_INVALID"], "account.unregisterDevice": ["TOKEN_INVALID"], "account.setAccountTTL": ["TTL_DAYS_INVALID"], "contacts.getTopPeers": ["TYPES_EMPTY"], "bots.sendCustomRequest": ["USER_BOT_INVALID"], "messages.getCommonChats": ["USER_ID_INVALID"], "users.getFullUser": ["USER_ID_INVALID"], "account.checkUsername": ["USERNAME_INVALID"], "account.updateUsername": ["USERNAME_INVALID", "USERNAME_NOT_MODIFIED", "USERNAME_OCCUPIED"], "contacts.resolveUsername": ["USERNAME_INVALID", "USERNAME_NOT_OCCUPIED"], "messages.createChat": ["USERS_TOO_FEW"]}, "401": {"contacts.resolveUsername": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "messages.getHistory": ["AUTH_KEY_PERM_EMPTY"], "auth.signIn": ["SESSION_PASSWORD_NEEDED"], "messages.getDialogs": ["SESSION_PASSWORD_NEEDED"], "updates.getDifference": ["SESSION_PASSWORD_NEEDED"], "updates.getState": ["SESSION_PASSWORD_NEEDED"], "upload.saveFilePart": ["SESSION_PASSWORD_NEEDED"], "users.getUsers": ["SESSION_PASSWORD_NEEDED"]}, "500": {"auth.sendCode": ["AUTH_RESTART"], "phone.acceptCall": ["CALL_OCCUPY_FAILED"], "messages.acceptEncryption": ["ENCRYPTION_OCCUPY_FAILED"], "updates.getChannelDifference": ["HISTORY_GET_FAILED", "PERSISTENT_TIMESTAMP_OUTDATED"], "users.getUsers": ["MEMBER_NO_LOCATION", "NEED_MEMBER_INVALID"], "auth.signUp": ["MEMBER_OCCUPY_PRIMARY_LOC_FAILED", "REG_ID_GENERATE_FAILED"], "channels.getChannels": ["NEED_CHAT_INVALID"], "messages.editChatTitle": ["NEED_CHAT_INVALID"], "contacts.deleteContacts": ["NEED_MEMBER_INVALID"], "contacts.importCard": ["NEED_MEMBER_INVALID"], "updates.getDifference": ["NEED_MEMBER_INVALID"], "phone.requestCall": ["PARTICIPANT_CALL_FAILED"], "messages.forwardMessages": ["PTS_CHANGE_EMPTY", "RANDOM_ID_DUPLICATE"], "messages.sendMessage": ["RANDOM_ID_DUPLICATE"], "messages.sendMedia": ["STORAGE_CHECK_FAILED"], "upload.getCdnFile": ["UNKNOWN_METHOD"]}, "403": {"channels.getFullChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "updates.getChannelDifference": ["CHANNEL_PUBLIC_GROUP_NA"], "channels.editAdmin": ["CHAT_ADMIN_INVITE_REQUIRED", "RIGHT_FORBIDDEN", "USER_PRIVACY_RESTRICTED"], "messages.migrateChat": ["CHAT_ADMIN_REQUIRED"], "messages.forwardMessages": ["CHAT_SEND_GIFS_FORBIDDEN", "CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_SEND_STICKERS_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "channels.inviteToChannel": ["CHAT_WRITE_FORBIDDEN", "USER_CHANNELS_TOO_MUCH", "USER_PRIVACY_RESTRICTED"], "messages.editMessage": ["CHAT_WRITE_FORBIDDEN", "MESSAGE_AUTHOR_REQUIRED"], "messages.sendMedia": ["CHAT_WRITE_FORBIDDEN"], "messages.sendMessage": ["CHAT_WRITE_FORBIDDEN"], "messages.setTyping": ["CHAT_WRITE_FORBIDDEN"], "messages.getMessageEditData": ["MESSAGE_AUTHOR_REQUIRED"], "channels.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.setInlineBotResults": ["USER_BOT_INVALID"], "phone.requestCall": ["USER_IS_BLOCKED", "USER_PRIVACY_RESTRICTED"], "messages.addChatUser": ["USER_NOT_MUTUAL_CONTACT", "USER_PRIVACY_RESTRICTED"], "channels.createChannel": ["USER_RESTRICTED"], "messages.createChat": ["USER_RESTRICTED"]}, "406": {"auth.checkPhone": ["PHONE_NUMBER_INVALID"], "auth.sendCode": ["PHONE_PASSWORD_FLOOD"]}, "-503": {"auth.resetAuthorizations": ["Timeout"], "contacts.deleteContacts": ["Timeout"], "messages.forwardMessages": ["Timeout"], "messages.getBotCallbackAnswer": ["Timeout"], "messages.getHistory": ["Timeout"], "messages.getInlineBotResults": ["Timeout"], "updates.getState": ["Timeout"]}}, "human_result": {"-429": ["Too many requests"], "ABOUT_TOO_LONG": ["The provided bio is too long"], "ACCESS_TOKEN_EXPIRED": ["Bot token expired"], "ACCESS_TOKEN_INVALID": ["The provided token is not valid"], "ACTIVE_USER_REQUIRED": ["The method is only available to already activated users"], "API_ID_INVALID": ["The api_id/api_hash combination is invalid"], "ARTICLE_TITLE_EMPTY": ["The title of the article is empty"], "AUTH_BYTES_INVALID": ["The provided authorization is invalid"], "AUTH_KEY_PERM_EMPTY": ["The temporary auth key must be binded to the permanent auth key to use these methods."], "AUTH_KEY_UNREGISTERED": ["The authorization key has expired"], "AUTH_RESTART": ["Restart the authorization process"], "BOT_GROUPS_BLOCKED": ["This bot can't be added to groups"], "BOT_INLINE_DISABLED": ["This bot can't be used in inline mode"], "BOT_INVALID": ["This is not a valid bot"], "BOT_METHOD_INVALID": ["This method cannot be run by a bot"], "BOT_MISSING": ["This method can only be run by a bot"], "BOTS_TOO_MUCH": ["There are too many bots in this chat/channel"], "BUTTON_DATA_INVALID": ["The provided button data is invalid"], "BUTTON_TYPE_INVALID": ["The type of one of the buttons you provided is invalid"], "BUTTON_URL_INVALID": ["Button URL invalid"], "CALL_ALREADY_ACCEPTED": ["The call was already accepted"], "CALL_ALREADY_DECLINED": ["The call was already declined"], "CALL_OCCUPY_FAILED": ["The call failed because the user is already making another call"], "CALL_PEER_INVALID": ["The provided call peer object is invalid"], "CALL_PROTOCOL_FLAGS_INVALID": ["Call protocol flags invalid"], "CDN_METHOD_INVALID": ["You can't call this method in a CDN DC"], "CHANNEL_INVALID": ["The provided channel is invalid"], "CHANNEL_PRIVATE": ["You haven't joined this channel/supergroup"], "CHANNEL_PUBLIC_GROUP_NA": ["channel/supergroup not available"], "CHANNELS_TOO_MUCH": ["You have joined too many channels/supergroups"], "CHAT_ABOUT_NOT_MODIFIED": ["About text has not changed"], "CHAT_ADMIN_INVITE_REQUIRED": ["You do not have the rights to do this"], "CHAT_ADMIN_REQUIRED": ["You must be an admin in this chat to do this"], "CHAT_FORBIDDEN": ["You cannot write in this chat"], "CHAT_ID_EMPTY": ["The provided chat ID is empty"], "CHAT_ID_INVALID": ["The provided chat id is invalid"], "CHAT_NOT_MODIFIED": ["The pinned message wasn't modified"], "CHAT_SEND_GIFS_FORBIDDEN": ["You can't send gifs in this chat"], "CHAT_SEND_MEDIA_FORBIDDEN": ["You can't send media in this chat"], "CHAT_SEND_STICKERS_FORBIDDEN": ["You can't send stickers in this chat."], "CHAT_TITLE_EMPTY": ["No chat title provided"], "CHAT_WRITE_FORBIDDEN": ["You can't write in this chat"], "CODE_EMPTY": ["The provided code is empty"], "CODE_HASH_INVALID": ["Code hash invalid"], "CONNECTION_API_ID_INVALID": ["The provided API id is invalid"], "CONNECTION_LANG_PACK_INVALID": ["Language pack invalid"], "CONNECTION_LAYER_INVALID": ["Layer invalid"], "CONTACT_ID_INVALID": ["The provided contact ID is invalid"], "DATA_INVALID": ["Encrypted data invalid"], "DATA_JSON_INVALID": ["The provided JSON data is invalid"], "DATE_EMPTY": ["Date empty"], "DC_ID_INVALID": ["The provided DC ID is invalid"], "DH_G_A_INVALID": ["g_a invalid"], "ENCRYPTED_MESSAGE_INVALID": ["Encrypted message invalid"], "ENCRYPTION_ALREADY_ACCEPTED": ["Secret chat already accepted"], "ENCRYPTION_ALREADY_DECLINED": ["The secret chat was already declined"], "ENCRYPTION_ID_INVALID": ["The provided secret chat ID is invalid"], "ENCRYPTION_OCCUPY_FAILED": ["Internal server error while accepting secret chat"], "ENTITY_MENTION_USER_INVALID": ["You can't use this entity"], "ERROR_TEXT_EMPTY": ["The provided error message is empty"], "EXPORT_CARD_INVALID": ["Provided card is invalid"], "FIELD_NAME_EMPTY": ["The field with the name FIELD_NAME is missing"], "FIELD_NAME_INVALID": ["The field with the name FIELD_NAME is invalid"], "FILE_ID_INVALID": ["The provided file id is invalid"], "FILE_PART_0_MISSING": ["File part 0 missing"], "FILE_PART_EMPTY": ["The provided file part is empty"], "FILE_PART_INVALID": ["The file part number is invalid"], "FILE_PART_LENGTH_INVALID": ["The length of a file part is invalid"], "FILE_PART_SIZE_INVALID": ["The provided file part size is invalid"], "FILE_PARTS_INVALID": ["The number of file parts is invalid"], "FIRSTNAME_INVALID": ["The first name is invalid"], "FLOOD_WAIT_666": ["Spooky af m8"], "GIF_ID_INVALID": ["The provided GIF ID is invalid"], "HASH_INVALID": ["The provided hash is invalid"], "HISTORY_GET_FAILED": ["Fetching of history failed"], "IMAGE_PROCESS_FAILED": ["Failure while processing image"], "INLINE_RESULT_EXPIRED": ["The inline query expired"], "INPUT_CONSTRUCTOR_INVALID": ["The provided constructor is invalid"], "INPUT_FETCH_ERROR": ["An error occurred while deserializing TL parameters"], "INPUT_FETCH_FAIL": ["Failed deserializing TL payload"], "INPUT_LAYER_INVALID": ["The provided layer is invalid"], "INPUT_METHOD_INVALID": ["The provided method is invalid"], "INPUT_REQUEST_TOO_LONG": ["The request is too big"], "INPUT_USER_DEACTIVATED": ["The specified user was deleted"], "INTERDC_1_CALL_ERROR": ["An error occurred while communicating with DC 1"], "INTERDC_1_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 1"], "INTERDC_2_CALL_ERROR": ["An error occurred while communicating with DC 2"], "INTERDC_2_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 2"], "INTERDC_3_CALL_ERROR": ["An error occurred while communicating with DC 3"], "INTERDC_3_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 3"], "INTERDC_4_CALL_ERROR": ["An error occurred while communicating with DC 4"], "INTERDC_4_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 4"], "INTERDC_5_CALL_ERROR": ["An error occurred while communicating with DC 5"], "INTERDC_5_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 5"], "INVITE_HASH_EMPTY": ["The invite hash is empty"], "INVITE_HASH_EXPIRED": ["The invite link has expired"], "INVITE_HASH_INVALID": ["The invite hash is invalid"], "LANG_PACK_INVALID": ["The provided language pack is invalid"], "LASTNAME_INVALID": ["The last name is invalid"], "LIMIT_INVALID": ["The provided limit is invalid"], "LOCATION_INVALID": ["The provided location is invalid"], "MAX_ID_INVALID": ["The provided max ID is invalid"], "MD5_CHECKSUM_INVALID": ["The MD5 checksums do not match"], "MEDIA_CAPTION_TOO_LONG": ["The caption is too long"], "MEDIA_EMPTY": ["The provided media object is invalid"], "MEMBER_OCCUPY_PRIMARY_LOC_FAILED": ["Occupation of primary member location failed"], "MESSAGE_AUTHOR_REQUIRED": ["Message author required"], "MESSAGE_DELETE_FORBIDDEN": ["You can't delete one of the messages you tried to delete, most likely because it is a service message."], "MESSAGE_EDIT_TIME_EXPIRED": ["You can't edit this message anymore, too much time has passed since its creation."], "MESSAGE_EMPTY": ["The provided message is empty"], "MESSAGE_ID_INVALID": ["The provided message id is invalid"], "MESSAGE_IDS_EMPTY": ["No message ids were provided"], "MESSAGE_NOT_MODIFIED": ["The message text has not changed"], "MESSAGE_TOO_LONG": ["The provided message is too long"], "MSG_WAIT_FAILED": ["A waiting call returned an error"], "NEED_CHAT_INVALID": ["The provided chat is invalid"], "NEED_MEMBER_INVALID": ["The provided member is invalid"], "NEW_SALT_INVALID": ["The new salt is invalid"], "NEW_SETTINGS_INVALID": ["The new settings are invalid"], "OFFSET_INVALID": ["The provided offset is invalid"], "OFFSET_PEER_ID_INVALID": ["The provided offset peer is invalid"], "PACK_SHORT_NAME_INVALID": ["Short pack name invalid"], "PARTICIPANT_CALL_FAILED": ["Failure while making call"], "PARTICIPANT_VERSION_OUTDATED": ["The other participant does not use an up to date telegram client with support for calls"], "PARTICIPANTS_TOO_FEW": ["Not enough participants"], "PASSWORD_EMPTY": ["The provided password is empty"], "PASSWORD_HASH_INVALID": ["The provided password hash is invalid"], "PEER_FLOOD": ["Too many requests"], "PEER_ID_INVALID": ["The provided peer id is invalid"], "PEER_ID_NOT_SUPPORTED": ["The provided peer ID is not supported"], "PERSISTENT_TIMESTAMP_EMPTY": ["Persistent timestamp empty"], "PERSISTENT_TIMESTAMP_INVALID": ["Persistent timestamp invalid"], "PERSISTENT_TIMESTAMP_OUTDATED": ["Persistent timestamp outdated"], "PHONE_CODE_EMPTY": ["phone_code is missing"], "PHONE_CODE_EXPIRED": ["The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)"], "PHONE_CODE_HASH_EMPTY": ["phone_code_hash is missing"], "PHONE_CODE_INVALID": ["The provided phone code is invalid"], "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN": [""], "PHONE_NUMBER_BANNED": ["The provided phone number is banned from telegram"], "PHONE_NUMBER_FLOOD": ["You asked for the code too many times."], "PHONE_NUMBER_INVALID": ["The phone number is invalid"], "PHONE_NUMBER_OCCUPIED": ["The phone number is already in use"], "PHONE_NUMBER_UNOCCUPIED": ["The phone number is not yet being used"], "PHONE_PASSWORD_FLOOD": ["You have tried logging in too many times"], "PHONE_PASSWORD_PROTECTED": ["This phone is password protected"], "PHOTO_CROP_SIZE_SMALL": ["Photo is too small"], "PHOTO_EXT_INVALID": ["The extension of the photo is invalid"], "PHOTO_INVALID_DIMENSIONS": ["The photo dimensions are invalid"], "PRIVACY_KEY_INVALID": ["The privacy key is invalid"], "PTS_CHANGE_EMPTY": ["No PTS change"], "QUERY_ID_EMPTY": ["The query ID is empty"], "QUERY_ID_INVALID": ["The query ID is invalid"], "QUERY_TOO_SHORT": ["The query string is too short"], "RANDOM_ID_DUPLICATE": ["You provided a random ID that was already used"], "RANDOM_ID_INVALID": ["A provided random ID is invalid"], "RANDOM_LENGTH_INVALID": ["Random length invalid"], "RANGES_INVALID": ["Invalid range provided"], "REG_ID_GENERATE_FAILED": ["Failure while generating registration ID"], "REPLY_MARKUP_INVALID": ["The provided reply markup is invalid"], "RESULT_TYPE_INVALID": ["Result type invalid"], "RIGHT_FORBIDDEN": ["Your admin rights do not allow you to do this"], "RPC_CALL_FAIL": ["Telegram is having internal issues, please try again later."], "RPC_MCGET_FAIL": ["Telegram is having internal issues, please try again later."], "RSA_DECRYPT_FAILED": ["Internal RSA decryption failed"], "SEARCH_QUERY_EMPTY": ["The search query is empty"], "SEND_MESSAGE_TYPE_INVALID": ["The message type is invalid"], "SESSION_PASSWORD_NEEDED": ["2FA is enabled, use a password to login"], "SHA256_HASH_INVALID": ["The provided SHA256 hash is invalid"], "START_PARAM_EMPTY": ["The start parameter is empty"], "START_PARAM_INVALID": ["Start parameter invalid"], "STICKER_ID_INVALID": ["The provided sticker ID is invalid"], "STICKER_INVALID": ["The provided sticker is invalid"], "STICKERS_EMPTY": ["No sticker provided"], "STICKERSET_INVALID": ["The provided sticker set is invalid"], "STORAGE_CHECK_FAILED": ["Server storage check failed"], "TEMP_AUTH_KEY_EMPTY": ["No temporary auth key provided"], "Timeout": ["A timeout occurred while fetching data from the bot"], "TMP_PASSWORD_DISABLED": ["The temporary password is disabled"], "TOKEN_INVALID": ["The provided token is invalid"], "TTL_DAYS_INVALID": ["The provided TTL is invalid"], "TYPE_CONSTRUCTOR_INVALID": ["The type constructor is invalid"], "TYPES_EMPTY": ["The types field is empty"], "UNKNOWN_METHOD": ["The method you tried to call cannot be called on non-CDN DCs"], "USER_ADMIN_INVALID": ["You're not an admin"], "USER_ALREADY_PARTICIPANT": ["The user is already in the group"], "USER_BANNED_IN_CHANNEL": ["You're banned from sending messages in supergroups/channels"], "USER_BLOCKED": ["User blocked"], "USER_BOT": ["Bots can only be admins in channels."], "USER_BOT_INVALID": ["This method can only be called by a bot"], "USER_BOT_REQUIRED": ["This method can only be called by a bot"], "USER_CHANNELS_TOO_MUCH": ["One of the users you tried to add is already in too many channels/supergroups"], "USER_CREATOR": ["You can't leave this channel, because you're its creator"], "USER_DEACTIVATED": ["The user was deactivated"], "USER_ID_INVALID": ["The provided user ID is invalid"], "USER_IS_BLOCKED": ["User is blocked"], "USER_IS_BOT": ["Bots can't send messages to other bots"], "USER_KICKED": ["This user was kicked from this supergroup/channel"], "USER_NOT_MUTUAL_CONTACT": ["The provided user is not a mutual contact"], "USER_NOT_PARTICIPANT": ["You're not a member of this supergroup/channel"], "USER_PRIVACY_RESTRICTED": ["The user's privacy settings do not allow you to do this"], "USER_RESTRICTED": ["You're spamreported, you can't create channels or chats."], "USERNAME_INVALID": ["The provided username is not valid"], "USERNAME_NOT_MODIFIED": ["The username was not modified"], "USERNAME_NOT_OCCUPIED": ["The provided username is not occupied"], "USERNAME_OCCUPIED": ["The provided username is already occupied"], "USERS_TOO_FEW": ["Not enough users (to create a chat, for example)"], "USERS_TOO_MUCH": ["The maximum number of users has been exceeded (to create a chat, for example)"], "WEBPAGE_CURL_FAILED": ["Failure while fetching the webpage with cURL"], "YOU_BLOCKED_USER": ["You blocked this user"]}} \ No newline at end of file +{"human_result": {"-429": ["Too many requests"], "ABOUT_TOO_LONG": ["The provided bio is too long"], "ACCESS_TOKEN_EXPIRED": ["Bot token expired"], "ACCESS_TOKEN_INVALID": ["The provided token is not valid"], "ACTIVE_USER_REQUIRED": ["The method is only available to already activated users"], "ADMINS_TOO_MUCH": ["Too many admins"], "API_ID_INVALID": ["The api_id/api_hash combination is invalid"], "API_ID_PUBLISHED_FLOOD": ["This API id was published somewhere, you can't use it now"], "ARTICLE_TITLE_EMPTY": ["The title of the article is empty"], "AUTH_BYTES_INVALID": ["The provided authorization is invalid"], "AUTH_KEY_PERM_EMPTY": ["The temporary auth key must be binded to the permanent auth key to use these methods."], "AUTH_KEY_UNREGISTERED": ["The authorization key has expired"], "AUTH_RESTART": ["Restart the authorization process"], "BOTS_TOO_MUCH": ["There are too many bots in this chat/channel"], "BOT_CHANNELS_NA": ["Bots can't edit admin privileges"], "BOT_GROUPS_BLOCKED": ["This bot can't be added to groups"], "BOT_INLINE_DISABLED": ["This bot can't be used in inline mode"], "BOT_INVALID": ["This is not a valid bot"], "BOT_METHOD_INVALID": ["This method cannot be run by a bot"], "BOT_MISSING": ["This method can only be run by a bot"], "BUTTON_DATA_INVALID": ["The provided button data is invalid"], "BUTTON_TYPE_INVALID": ["The type of one of the buttons you provided is invalid"], "BUTTON_URL_INVALID": ["Button URL invalid"], "CALL_ALREADY_ACCEPTED": ["The call was already accepted"], "CALL_ALREADY_DECLINED": ["The call was already declined"], "CALL_OCCUPY_FAILED": ["The call failed because the user is already making another call"], "CALL_PEER_INVALID": ["The provided call peer object is invalid"], "CALL_PROTOCOL_FLAGS_INVALID": ["Call protocol flags invalid"], "CDN_METHOD_INVALID": ["You can't call this method in a CDN DC"], "CHANNELS_ADMIN_PUBLIC_TOO_MUCH": ["You're admin of too many public channels, make some channels private to change the username of this channel"], "CHANNELS_TOO_MUCH": ["You have joined too many channels/supergroups"], "CHANNEL_INVALID": ["The provided channel is invalid"], "CHANNEL_PRIVATE": ["You haven't joined this channel/supergroup"], "CHANNEL_PUBLIC_GROUP_NA": ["channel/supergroup not available"], "CHAT_ABOUT_NOT_MODIFIED": ["About text has not changed"], "CHAT_ABOUT_TOO_LONG": ["Chat about too long"], "CHAT_ADMIN_INVITE_REQUIRED": ["You do not have the rights to do this"], "CHAT_ADMIN_REQUIRED": ["You must be an admin in this chat to do this"], "CHAT_FORBIDDEN": ["You cannot write in this chat"], "CHAT_ID_EMPTY": ["The provided chat ID is empty"], "CHAT_ID_INVALID": ["The provided chat id is invalid"], "CHAT_NOT_MODIFIED": ["The pinned message wasn't modified"], "CHAT_SEND_GIFS_FORBIDDEN": ["You can't send gifs in this chat"], "CHAT_SEND_MEDIA_FORBIDDEN": ["You can't send media in this chat"], "CHAT_SEND_STICKERS_FORBIDDEN": ["You can't send stickers in this chat."], "CHAT_TITLE_EMPTY": ["No chat title provided"], "CHAT_WRITE_FORBIDDEN": ["You can't write in this chat"], "CODE_EMPTY": ["The provided code is empty"], "CODE_HASH_INVALID": ["Code hash invalid"], "CONNECTION_API_ID_INVALID": ["The provided API id is invalid"], "CONNECTION_DEVICE_MODEL_EMPTY": ["Device model empty"], "CONNECTION_LANG_PACK_INVALID": ["Language pack invalid"], "CONNECTION_LAYER_INVALID": ["Layer invalid"], "CONNECTION_NOT_INITED": ["Connection not initialized"], "CONNECTION_SYSTEM_EMPTY": ["Connection system empty"], "CONTACT_ID_INVALID": ["The provided contact ID is invalid"], "DATA_INVALID": ["Encrypted data invalid"], "DATA_JSON_INVALID": ["The provided JSON data is invalid"], "DATE_EMPTY": ["Date empty"], "DC_ID_INVALID": ["The provided DC ID is invalid"], "DH_G_A_INVALID": ["g_a invalid"], "EMAIL_UNCONFIRMED": ["Email unconfirmed"], "ENCRYPTED_MESSAGE_INVALID": ["Encrypted message invalid"], "ENCRYPTION_ALREADY_ACCEPTED": ["Secret chat already accepted"], "ENCRYPTION_ALREADY_DECLINED": ["The secret chat was already declined"], "ENCRYPTION_DECLINED": ["The secret chat was declined"], "ENCRYPTION_ID_INVALID": ["The provided secret chat ID is invalid"], "ENCRYPTION_OCCUPY_FAILED": ["Internal server error while accepting secret chat"], "ENTITY_MENTION_USER_INVALID": ["You can't use this entity"], "ERROR_TEXT_EMPTY": ["The provided error message is empty"], "EXPORT_CARD_INVALID": ["Provided card is invalid"], "EXTERNAL_URL_INVALID": ["External URL invalid"], "FIELD_NAME_EMPTY": ["The field with the name FIELD_NAME is missing"], "FIELD_NAME_INVALID": ["The field with the name FIELD_NAME is invalid"], "FILE_ID_INVALID": ["The provided file id is invalid"], "FILE_PARTS_INVALID": ["The number of file parts is invalid"], "FILE_PART_0_MISSING": ["File part 0 missing"], "FILE_PART_122_MISSING": [""], "FILE_PART_154_MISSING": [""], "FILE_PART_458_MISSING": [""], "FILE_PART_468_MISSING": [""], "FILE_PART_504_MISSING": [""], "FILE_PART_6_MISSING": ["File part 6 missing"], "FILE_PART_72_MISSING": [""], "FILE_PART_94_MISSING": [""], "FILE_PART_EMPTY": ["The provided file part is empty"], "FILE_PART_INVALID": ["The file part number is invalid"], "FILE_PART_LENGTH_INVALID": ["The length of a file part is invalid"], "FILE_PART_SIZE_INVALID": ["The provided file part size is invalid"], "FIRSTNAME_INVALID": ["The first name is invalid"], "FLOOD_WAIT_666": ["Spooky af m8"], "GIF_ID_INVALID": ["The provided GIF ID is invalid"], "GROUPED_MEDIA_INVALID": ["Invalid grouped media"], "HASH_INVALID": ["The provided hash is invalid"], "HISTORY_GET_FAILED": ["Fetching of history failed"], "IMAGE_PROCESS_FAILED": ["Failure while processing image"], "INLINE_RESULT_EXPIRED": ["The inline query expired"], "INPUT_CONSTRUCTOR_INVALID": ["The provided constructor is invalid"], "INPUT_FETCH_ERROR": ["An error occurred while deserializing TL parameters"], "INPUT_FETCH_FAIL": ["Failed deserializing TL payload"], "INPUT_LAYER_INVALID": ["The provided layer is invalid"], "INPUT_METHOD_INVALID": ["The provided method is invalid"], "INPUT_REQUEST_TOO_LONG": ["The request is too big"], "INPUT_USER_DEACTIVATED": ["The specified user was deleted"], "INTERDC_1_CALL_ERROR": ["An error occurred while communicating with DC 1"], "INTERDC_1_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 1"], "INTERDC_2_CALL_ERROR": ["An error occurred while communicating with DC 2"], "INTERDC_2_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 2"], "INTERDC_3_CALL_ERROR": ["An error occurred while communicating with DC 3"], "INTERDC_3_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 3"], "INTERDC_4_CALL_ERROR": ["An error occurred while communicating with DC 4"], "INTERDC_4_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 4"], "INTERDC_5_CALL_ERROR": ["An error occurred while communicating with DC 5"], "INTERDC_5_CALL_RICH_ERROR": ["A rich error occurred while communicating with DC 5"], "INVITE_HASH_EMPTY": ["The invite hash is empty"], "INVITE_HASH_EXPIRED": ["The invite link has expired"], "INVITE_HASH_INVALID": ["The invite hash is invalid"], "LANG_PACK_INVALID": ["The provided language pack is invalid"], "LASTNAME_INVALID": ["The last name is invalid"], "LIMIT_INVALID": ["The provided limit is invalid"], "LOCATION_INVALID": ["The provided location is invalid"], "MAX_ID_INVALID": ["The provided max ID is invalid"], "MD5_CHECKSUM_INVALID": ["The MD5 checksums do not match"], "MEDIA_CAPTION_TOO_LONG": ["The caption is too long"], "MEDIA_EMPTY": ["The provided media object is invalid"], "MEDIA_INVALID": ["Media invalid"], "MEMBER_NO_LOCATION": ["An internal failure occurred while fetching user info (couldn't find location)"], "MEMBER_OCCUPY_PRIMARY_LOC_FAILED": ["Occupation of primary member location failed"], "MESSAGE_AUTHOR_REQUIRED": ["Message author required"], "MESSAGE_DELETE_FORBIDDEN": ["You can't delete one of the messages you tried to delete, most likely because it is a service message."], "MESSAGE_EDIT_TIME_EXPIRED": ["You can't edit this message anymore, too much time has passed since its creation."], "MESSAGE_EMPTY": ["The provided message is empty"], "MESSAGE_IDS_EMPTY": ["No message ids were provided"], "MESSAGE_ID_INVALID": ["The provided message id is invalid"], "MESSAGE_NOT_MODIFIED": ["The message text has not changed"], "MESSAGE_TOO_LONG": ["The provided message is too long"], "MSG_WAIT_FAILED": ["A waiting call returned an error"], "NEED_CHAT_INVALID": ["The provided chat is invalid"], "NEED_MEMBER_INVALID": ["The provided member is invalid"], "NEW_SALT_INVALID": ["The new salt is invalid"], "NEW_SETTINGS_INVALID": ["The new settings are invalid"], "OFFSET_INVALID": ["The provided offset is invalid"], "OFFSET_PEER_ID_INVALID": ["The provided offset peer is invalid"], "PACK_SHORT_NAME_INVALID": ["Short pack name invalid"], "PACK_SHORT_NAME_OCCUPIED": ["A stickerpack with this name already exists"], "PARTICIPANTS_TOO_FEW": ["Not enough participants"], "PARTICIPANT_CALL_FAILED": ["Failure while making call"], "PARTICIPANT_VERSION_OUTDATED": ["The other participant does not use an up to date telegram client with support for calls"], "PASSWORD_EMPTY": ["The provided password is empty"], "PASSWORD_HASH_INVALID": ["The provided password hash is invalid"], "PEER_FLOOD": ["Too many requests"], "PEER_ID_INVALID": ["The provided peer id is invalid"], "PEER_ID_NOT_SUPPORTED": ["The provided peer ID is not supported"], "PERSISTENT_TIMESTAMP_EMPTY": ["Persistent timestamp empty"], "PERSISTENT_TIMESTAMP_INVALID": ["Persistent timestamp invalid"], "PERSISTENT_TIMESTAMP_OUTDATED": ["Persistent timestamp outdated"], "PHONE_CODE_EMPTY": ["phone_code is missing"], "PHONE_CODE_EXPIRED": ["The phone code you provided has expired, this may happen if it was sent to any chat on telegram (if the code is sent through a telegram chat (not the official account) to avoid it append or prepend to the code some chars)"], "PHONE_CODE_HASH_EMPTY": ["phone_code_hash is missing"], "PHONE_CODE_INVALID": ["The provided phone code is invalid"], "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN": [""], "PHONE_NUMBER_BANNED": ["The provided phone number is banned from telegram"], "PHONE_NUMBER_FLOOD": ["You asked for the code too many times."], "PHONE_NUMBER_INVALID": ["The phone number is invalid"], "PHONE_NUMBER_OCCUPIED": ["The phone number is already in use"], "PHONE_NUMBER_UNOCCUPIED": ["The phone number is not yet being used"], "PHONE_PASSWORD_FLOOD": ["You have tried logging in too many times"], "PHONE_PASSWORD_PROTECTED": ["This phone is password protected"], "PHOTO_CROP_SIZE_SMALL": ["Photo is too small"], "PHOTO_EXT_INVALID": ["The extension of the photo is invalid"], "PHOTO_INVALID": ["Photo invalid"], "PHOTO_INVALID_DIMENSIONS": ["The photo dimensions are invalid"], "PRIVACY_KEY_INVALID": ["The privacy key is invalid"], "PTS_CHANGE_EMPTY": ["No PTS change"], "QUERY_ID_EMPTY": ["The query ID is empty"], "QUERY_ID_INVALID": ["The query ID is invalid"], "QUERY_TOO_SHORT": ["The query string is too short"], "RANDOM_ID_DUPLICATE": ["You provided a random ID that was already used"], "RANDOM_ID_INVALID": ["A provided random ID is invalid"], "RANDOM_LENGTH_INVALID": ["Random length invalid"], "RANGES_INVALID": ["Invalid range provided"], "REG_ID_GENERATE_FAILED": ["Failure while generating registration ID"], "REPLY_MARKUP_INVALID": ["The provided reply markup is invalid"], "RESULT_TYPE_INVALID": ["Result type invalid"], "RIGHT_FORBIDDEN": ["Your admin rights do not allow you to do this"], "RPC_CALL_FAIL": ["Telegram is having internal issues, please try again later."], "RPC_MCGET_FAIL": ["Telegram is having internal issues, please try again later."], "RSA_DECRYPT_FAILED": ["Internal RSA decryption failed"], "SEARCH_QUERY_EMPTY": ["The search query is empty"], "SEND_MESSAGE_TYPE_INVALID": ["The message type is invalid"], "SESSION_PASSWORD_NEEDED": ["2FA is enabled, use a password to login"], "SHA256_HASH_INVALID": ["The provided SHA256 hash is invalid"], "START_PARAM_EMPTY": ["The start parameter is empty"], "START_PARAM_INVALID": ["Start parameter invalid"], "STICKERSET_INVALID": ["The provided sticker set is invalid"], "STICKERS_EMPTY": ["No sticker provided"], "STICKER_EMOJI_INVALID": ["Sticker emoji invalid"], "STICKER_FILE_INVALID": ["Sticker file invalid"], "STICKER_ID_INVALID": ["The provided sticker ID is invalid"], "STICKER_INVALID": ["The provided sticker is invalid"], "STICKER_PNG_DIMENSIONS": ["Sticker png dimensions invalid"], "STORAGE_CHECK_FAILED": ["Server storage check failed"], "STORE_INVALID_SCALAR_TYPE": [""], "TEMP_AUTH_KEY_EMPTY": ["No temporary auth key provided"], "TMP_PASSWORD_DISABLED": ["The temporary password is disabled"], "TOKEN_INVALID": ["The provided token is invalid"], "TTL_DAYS_INVALID": ["The provided TTL is invalid"], "TYPES_EMPTY": ["The types field is empty"], "TYPE_CONSTRUCTOR_INVALID": ["The type constructor is invalid"], "Timeout": ["A timeout occurred while fetching data from the bot"], "UNKNOWN_METHOD": ["The method you tried to call cannot be called on non-CDN DCs"], "USERNAME_INVALID": ["The provided username is not valid"], "USERNAME_NOT_MODIFIED": ["The username was not modified"], "USERNAME_NOT_OCCUPIED": ["The provided username is not occupied"], "USERNAME_OCCUPIED": ["The provided username is already occupied"], "USERS_TOO_FEW": ["Not enough users (to create a chat, for example)"], "USERS_TOO_MUCH": ["The maximum number of users has been exceeded (to create a chat, for example)"], "USER_ADMIN_INVALID": ["You're not an admin"], "USER_ALREADY_PARTICIPANT": ["The user is already in the group"], "USER_BANNED_IN_CHANNEL": ["You're banned from sending messages in supergroups/channels"], "USER_BLOCKED": ["User blocked"], "USER_BOT": ["Bots can only be admins in channels."], "USER_BOT_INVALID": ["This method can only be called by a bot"], "USER_BOT_REQUIRED": ["This method can only be called by a bot"], "USER_CHANNELS_TOO_MUCH": ["One of the users you tried to add is already in too many channels/supergroups"], "USER_CREATOR": ["You can't leave this channel, because you're its creator"], "USER_DEACTIVATED": ["The user was deactivated"], "USER_ID_INVALID": ["The provided user ID is invalid"], "USER_IS_BLOCKED": ["User is blocked"], "USER_IS_BOT": ["Bots can't send messages to other bots"], "USER_KICKED": ["This user was kicked from this supergroup/channel"], "USER_NOT_MUTUAL_CONTACT": ["The provided user is not a mutual contact"], "USER_NOT_PARTICIPANT": ["You're not a member of this supergroup/channel"], "USER_PRIVACY_RESTRICTED": ["The user's privacy settings do not allow you to do this"], "USER_RESTRICTED": ["You're spamreported, you can't create channels or chats."], "WC_CONVERT_URL_INVALID": ["WC convert URL invalid"], "WEBPAGE_CURL_FAILED": ["Failure while fetching the webpage with cURL"], "WEBPAGE_MEDIA_EMPTY": ["Webpage media empty"], "YOU_BLOCKED_USER": ["You blocked this user"]}, "ok": true, "result": {"-503": {"auth.bindTempAuthKey": ["Timeout"], "auth.resetAuthorizations": ["Timeout"], "channels.getFullChannel": ["Timeout"], "channels.getParticipants": ["Timeout"], "contacts.deleteContacts": ["Timeout"], "contacts.search": ["Timeout"], "help.getCdnConfig": ["Timeout"], "help.getConfig": ["Timeout"], "messages.forwardMessages": ["Timeout"], "messages.getBotCallbackAnswer": ["Timeout"], "messages.getDialogs": ["Timeout"], "messages.getHistory": ["Timeout"], "messages.getInlineBotResults": ["Timeout"], "messages.readHistory": ["Timeout"], "messages.sendMedia": ["Timeout"], "messages.sendMessage": ["Timeout"], "updates.getChannelDifference": ["Timeout"], "updates.getDifference": ["Timeout"], "updates.getState": ["Timeout"], "upload.getFile": ["Timeout"], "users.getFullUser": ["Timeout"], "users.getUsers": ["Timeout"]}, "400": {"account.changePhone": ["PHONE_NUMBER_INVALID"], "account.checkUsername": ["USERNAME_INVALID"], "account.confirmPhone": ["CODE_HASH_INVALID", "PHONE_CODE_EMPTY"], "account.getNotifySettings": ["PEER_ID_INVALID"], "account.getPasswordSettings": ["PASSWORD_HASH_INVALID"], "account.getPrivacy": ["PRIVACY_KEY_INVALID"], "account.getTmpPassword": ["PASSWORD_HASH_INVALID", "TMP_PASSWORD_DISABLED"], "account.registerDevice": ["TOKEN_INVALID"], "account.reportPeer": ["PEER_ID_INVALID"], "account.resetAuthorization": ["HASH_INVALID"], "account.sendChangePhoneCode": ["PHONE_NUMBER_INVALID"], "account.sendConfirmPhoneCode": ["HASH_INVALID"], "account.setAccountTTL": ["TTL_DAYS_INVALID"], "account.setPrivacy": ["PRIVACY_KEY_INVALID"], "account.unregisterDevice": ["TOKEN_INVALID"], "account.updateNotifySettings": ["PEER_ID_INVALID"], "account.updatePasswordSettings": ["EMAIL_UNCONFIRMED", "NEW_SALT_INVALID", "NEW_SETTINGS_INVALID", "PASSWORD_HASH_INVALID"], "account.updateProfile": ["ABOUT_TOO_LONG", "FIRSTNAME_INVALID"], "account.updateUsername": ["USERNAME_INVALID", "USERNAME_NOT_MODIFIED", "USERNAME_OCCUPIED"], "auth.bindTempAuthKey": ["ENCRYPTED_MESSAGE_INVALID", "INPUT_REQUEST_TOO_LONG", "TEMP_AUTH_KEY_EMPTY"], "auth.cancelCode": ["PHONE_NUMBER_INVALID"], "auth.checkPassword": ["PASSWORD_HASH_INVALID"], "auth.checkPhone": ["PHONE_NUMBER_BANNED", "PHONE_NUMBER_INVALID"], "auth.exportAuthorization": ["DC_ID_INVALID"], "auth.importAuthorization": ["AUTH_BYTES_INVALID", "USER_ID_INVALID"], "auth.importBotAuthorization": ["ACCESS_TOKEN_EXPIRED", "ACCESS_TOKEN_INVALID", "API_ID_INVALID"], "auth.recoverPassword": ["CODE_EMPTY"], "auth.requestPasswordRecovery": ["PASSWORD_EMPTY"], "auth.resendCode": ["PHONE_NUMBER_INVALID"], "auth.sendCode": ["API_ID_INVALID", "API_ID_PUBLISHED_FLOOD", "INPUT_REQUEST_TOO_LONG", "PHONE_NUMBER_APP_SIGNUP_FORBIDDEN", "PHONE_NUMBER_BANNED", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_PASSWORD_PROTECTED"], "auth.sendInvites": ["MESSAGE_EMPTY"], "auth.signIn": ["PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_UNOCCUPIED"], "auth.signUp": ["FIRSTNAME_INVALID", "PHONE_CODE_EMPTY", "PHONE_CODE_EXPIRED", "PHONE_CODE_INVALID", "PHONE_NUMBER_FLOOD", "PHONE_NUMBER_INVALID", "PHONE_NUMBER_OCCUPIED"], "bots.answerWebhookJSONQuery": ["QUERY_ID_INVALID", "USER_BOT_INVALID"], "bots.sendCustomRequest": ["USER_BOT_INVALID"], "channels.checkUsername": ["CHANNEL_INVALID", "CHAT_ID_INVALID", "USERNAME_INVALID"], "channels.createChannel": ["CHAT_TITLE_EMPTY"], "channels.deleteChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.deleteUserHistory": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED"], "channels.editAbout": ["CHANNEL_INVALID", "CHAT_ABOUT_NOT_MODIFIED", "CHAT_ABOUT_TOO_LONG", "CHAT_ADMIN_REQUIRED"], "channels.editAdmin": ["ADMINS_TOO_MUCH", "BOT_CHANNELS_NA", "CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "USER_CREATOR", "USER_ID_INVALID", "USER_NOT_MUTUAL_CONTACT"], "channels.editBanned": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ADMIN_INVALID", "USER_ID_INVALID"], "channels.editPhoto": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "PHOTO_INVALID"], "channels.editTitle": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.exportInvite": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "INVITE_HASH_EXPIRED"], "channels.exportMessageLink": ["CHANNEL_INVALID"], "channels.getAdminLog": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED"], "channels.getChannels": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getFullChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.getMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "MESSAGE_IDS_EMPTY"], "channels.getParticipant": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "USER_ID_INVALID", "USER_NOT_PARTICIPANT"], "channels.getParticipants": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID"], "channels.inviteToChannel": ["BOT_GROUPS_BLOCKED", "BOTS_TOO_MUCH", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "INPUT_USER_DEACTIVATED", "USER_BANNED_IN_CHANNEL", "USER_BLOCKED", "USER_BOT", "USER_ID_INVALID", "USER_KICKED", "USER_NOT_MUTUAL_CONTACT", "USERS_TOO_MUCH"], "channels.joinChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHANNELS_TOO_MUCH"], "channels.leaveChannel": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "USER_CREATOR", "USER_NOT_PARTICIPANT"], "channels.readHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.readMessageContents": ["CHANNEL_INVALID", "CHANNEL_PRIVATE"], "channels.reportSpam": ["CHANNEL_INVALID", "INPUT_USER_DEACTIVATED"], "channels.setStickers": ["CHANNEL_INVALID", "PARTICIPANTS_TOO_FEW"], "channels.toggleInvites": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_NOT_MODIFIED"], "channels.toggleSignatures": ["CHANNEL_INVALID"], "channels.updatePinnedMessage": ["CHANNEL_INVALID", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "channels.updateUsername": ["CHANNEL_INVALID", "CHANNELS_ADMIN_PUBLIC_TOO_MUCH", "CHAT_ADMIN_REQUIRED", "USERNAME_INVALID", "USERNAME_OCCUPIED"], "contacts.block": ["CONTACT_ID_INVALID"], "contacts.deleteContact": ["CONTACT_ID_INVALID"], "contacts.getTopPeers": ["TYPES_EMPTY"], "contacts.importCard": ["EXPORT_CARD_INVALID"], "contacts.resetTopPeerRating": ["PEER_ID_INVALID"], "contacts.resolveUsername": ["USERNAME_INVALID", "USERNAME_NOT_OCCUPIED"], "contacts.search": ["QUERY_TOO_SHORT", "SEARCH_QUERY_EMPTY"], "contacts.unblock": ["CONTACT_ID_INVALID"], "initConnection": ["CONNECTION_LAYER_INVALID", "INPUT_FETCH_FAIL"], "invokeWithLayer": ["AUTH_BYTES_INVALID", "CDN_METHOD_INVALID", "CONNECTION_API_ID_INVALID", "CONNECTION_DEVICE_MODEL_EMPTY", "CONNECTION_LANG_PACK_INVALID", "CONNECTION_NOT_INITED", "CONNECTION_SYSTEM_EMPTY", "INPUT_LAYER_INVALID", "INVITE_HASH_EXPIRED"], "langpack.getDifference": ["LANG_PACK_INVALID"], "langpack.getLangPack": ["LANG_PACK_INVALID"], "langpack.getLanguages": ["LANG_PACK_INVALID"], "langpack.getStrings": ["LANG_PACK_INVALID"], "messages.acceptEncryption": ["CHAT_ID_INVALID", "ENCRYPTION_ALREADY_ACCEPTED", "ENCRYPTION_ALREADY_DECLINED"], "messages.addChatUser": ["CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "USER_ALREADY_PARTICIPANT", "USER_ID_INVALID", "USERS_TOO_MUCH"], "messages.checkChatInvite": ["INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID"], "messages.createChat": ["USERS_TOO_FEW"], "messages.deleteChatUser": ["CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_NOT_PARTICIPANT"], "messages.deleteHistory": ["PEER_ID_INVALID"], "messages.discardEncryption": ["CHAT_ID_EMPTY", "ENCRYPTION_ALREADY_DECLINED", "ENCRYPTION_ID_INVALID"], "messages.editChatAdmin": ["CHAT_ID_INVALID"], "messages.editChatPhoto": ["CHAT_ID_INVALID", "INPUT_CONSTRUCTOR_INVALID", "INPUT_FETCH_FAIL", "PEER_ID_INVALID", "PHOTO_EXT_INVALID"], "messages.editChatTitle": ["CHAT_ID_INVALID"], "messages.editInlineBotMessage": ["MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED"], "messages.editMessage": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "INPUT_USER_DEACTIVATED", "MESSAGE_EDIT_TIME_EXPIRED", "MESSAGE_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_NOT_MODIFIED", "PEER_ID_INVALID"], "messages.exportChatInvite": ["CHAT_ID_INVALID"], "messages.faveSticker": ["STICKER_ID_INVALID"], "messages.forwardMessage": ["CHAT_ID_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID", "YOU_BLOCKED_USER"], "messages.forwardMessages": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "GROUPED_MEDIA_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_EMPTY", "MESSAGE_ID_INVALID", "MESSAGE_IDS_EMPTY", "PEER_ID_INVALID", "RANDOM_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.getBotCallbackAnswer": ["CHANNEL_INVALID", "DATA_INVALID", "MESSAGE_ID_INVALID", "PEER_ID_INVALID"], "messages.getChats": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getCommonChats": ["USER_ID_INVALID"], "messages.getDhConfig": ["RANDOM_LENGTH_INVALID"], "messages.getDialogs": ["INPUT_CONSTRUCTOR_INVALID", "OFFSET_PEER_ID_INVALID"], "messages.getDocumentByHash": ["SHA256_HASH_INVALID"], "messages.getFullChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getGameHighScores": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getHistory": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getInlineBotResults": ["BOT_INLINE_DISABLED", "BOT_INVALID", "CHANNEL_PRIVATE"], "messages.getInlineGameHighScores": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.getMessageEditData": ["PEER_ID_INVALID"], "messages.getMessagesViews": ["CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.getPeerDialogs": ["CHANNEL_PRIVATE", "PEER_ID_INVALID"], "messages.getPeerSettings": ["CHANNEL_INVALID", "PEER_ID_INVALID"], "messages.getStickerSet": ["STICKERSET_INVALID"], "messages.getUnreadMentions": ["PEER_ID_INVALID"], "messages.getWebPage": ["WC_CONVERT_URL_INVALID"], "messages.hideReportSpam": ["PEER_ID_INVALID"], "messages.importChatInvite": ["CHANNELS_TOO_MUCH", "INVITE_HASH_EMPTY", "INVITE_HASH_EXPIRED", "INVITE_HASH_INVALID", "USER_ALREADY_PARTICIPANT", "USERS_TOO_MUCH"], "messages.installStickerSet": ["STICKERSET_INVALID"], "messages.migrateChat": ["CHAT_ID_INVALID", "PEER_ID_INVALID"], "messages.readEncryptedHistory": ["MSG_WAIT_FAILED"], "messages.readHistory": ["PEER_ID_INVALID"], "messages.receivedQueue": ["MSG_WAIT_FAILED"], "messages.reorderPinnedDialogs": ["PEER_ID_INVALID"], "messages.reportEncryptedSpam": ["CHAT_ID_INVALID"], "messages.reportSpam": ["PEER_ID_INVALID"], "messages.requestEncryption": ["DH_G_A_INVALID", "USER_ID_INVALID"], "messages.saveDraft": ["PEER_ID_INVALID"], "messages.saveGif": ["GIF_ID_INVALID"], "messages.saveRecentSticker": ["STICKER_ID_INVALID"], "messages.search": ["CHAT_ADMIN_REQUIRED", "INPUT_CONSTRUCTOR_INVALID", "INPUT_USER_DEACTIVATED", "PEER_ID_INVALID", "PEER_ID_NOT_SUPPORTED", "SEARCH_QUERY_EMPTY", "USER_ID_INVALID"], "messages.searchGifs": ["SEARCH_QUERY_EMPTY"], "messages.searchGlobal": ["SEARCH_QUERY_EMPTY"], "messages.sendEncrypted": ["CHAT_ID_INVALID", "DATA_INVALID", "ENCRYPTION_DECLINED", "MSG_WAIT_FAILED"], "messages.sendEncryptedFile": ["MSG_WAIT_FAILED"], "messages.sendEncryptedService": ["DATA_INVALID", "ENCRYPTION_DECLINED", "MSG_WAIT_FAILED"], "messages.sendInlineBotResult": ["INLINE_RESULT_EXPIRED", "PEER_ID_INVALID", "QUERY_ID_EMPTY", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.sendMedia": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "EXTERNAL_URL_INVALID", "FILE_PART_122_MISSING", "FILE_PART_458_MISSING", "FILE_PART_468_MISSING", "FILE_PART_504_MISSING", "FILE_PART_72_MISSING", "FILE_PART_94_MISSING", "FILE_PART_LENGTH_INVALID", "FILE_PARTS_INVALID", "INPUT_USER_DEACTIVATED", "MEDIA_CAPTION_TOO_LONG", "MEDIA_EMPTY", "PEER_ID_INVALID", "PHOTO_EXT_INVALID", "PHOTO_INVALID_DIMENSIONS", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "WEBPAGE_CURL_FAILED", "WEBPAGE_MEDIA_EMPTY"], "messages.sendMessage": ["BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ADMIN_REQUIRED", "CHAT_ID_INVALID", "ENTITY_MENTION_USER_INVALID", "INPUT_USER_DEACTIVATED", "MESSAGE_EMPTY", "MESSAGE_TOO_LONG", "PEER_ID_INVALID", "REPLY_MARKUP_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT", "YOU_BLOCKED_USER"], "messages.sendScreenshotNotification": ["PEER_ID_INVALID"], "messages.setBotCallbackAnswer": ["QUERY_ID_INVALID"], "messages.setBotPrecheckoutResults": ["ERROR_TEXT_EMPTY"], "messages.setBotShippingResults": ["QUERY_ID_INVALID"], "messages.setEncryptedTyping": ["CHAT_ID_INVALID"], "messages.setGameScore": ["PEER_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setInlineBotResults": ["ARTICLE_TITLE_EMPTY", "BUTTON_DATA_INVALID", "BUTTON_TYPE_INVALID", "BUTTON_URL_INVALID", "MESSAGE_EMPTY", "QUERY_ID_INVALID", "REPLY_MARKUP_INVALID", "RESULT_TYPE_INVALID", "SEND_MESSAGE_TYPE_INVALID", "START_PARAM_INVALID"], "messages.setInlineGameScore": ["MESSAGE_ID_INVALID", "USER_BOT_REQUIRED"], "messages.setTyping": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "CHAT_ID_INVALID", "PEER_ID_INVALID", "USER_BANNED_IN_CHANNEL", "USER_IS_BLOCKED", "USER_IS_BOT"], "messages.startBot": ["BOT_INVALID", "PEER_ID_INVALID", "START_PARAM_EMPTY", "START_PARAM_INVALID"], "messages.toggleChatAdmins": ["CHAT_ID_INVALID", "CHAT_NOT_MODIFIED"], "messages.toggleDialogPin": ["PEER_ID_INVALID"], "messages.uninstallStickerSet": ["STICKERSET_INVALID"], "messages.uploadMedia": ["BOT_MISSING", "MEDIA_INVALID", "PEER_ID_INVALID"], "payments.getPaymentForm": ["MESSAGE_ID_INVALID"], "payments.getPaymentReceipt": ["MESSAGE_ID_INVALID"], "payments.sendPaymentForm": ["MESSAGE_ID_INVALID"], "payments.validateRequestedInfo": ["MESSAGE_ID_INVALID"], "phone.acceptCall": ["CALL_ALREADY_ACCEPTED", "CALL_ALREADY_DECLINED", "CALL_PEER_INVALID", "CALL_PROTOCOL_FLAGS_INVALID"], "phone.confirmCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.discardCall": ["CALL_ALREADY_ACCEPTED", "CALL_PEER_INVALID"], "phone.receivedCall": ["CALL_ALREADY_DECLINED", "CALL_PEER_INVALID"], "phone.requestCall": ["CALL_PROTOCOL_FLAGS_INVALID", "PARTICIPANT_VERSION_OUTDATED", "USER_ID_INVALID"], "phone.saveCallDebug": ["CALL_PEER_INVALID", "DATA_JSON_INVALID"], "phone.setCallRating": ["CALL_PEER_INVALID"], "photos.getUserPhotos": ["MAX_ID_INVALID", "USER_ID_INVALID"], "photos.uploadProfilePhoto": ["FILE_PARTS_INVALID", "IMAGE_PROCESS_FAILED", "PHOTO_CROP_SIZE_SMALL", "PHOTO_EXT_INVALID"], "stickers.addStickerToSet": ["BOT_MISSING", "STICKERSET_INVALID"], "stickers.changeStickerPosition": ["BOT_MISSING", "STICKER_INVALID"], "stickers.createStickerSet": ["BOT_MISSING", "PACK_SHORT_NAME_INVALID", "PACK_SHORT_NAME_OCCUPIED", "PEER_ID_INVALID", "STICKER_EMOJI_INVALID", "STICKER_FILE_INVALID", "STICKER_PNG_DIMENSIONS", "STICKERS_EMPTY", "USER_ID_INVALID"], "stickers.removeStickerFromSet": ["BOT_MISSING", "STICKER_INVALID"], "updates.getChannelDifference": ["CHANNEL_INVALID", "CHANNEL_PRIVATE", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID", "RANGES_INVALID"], "updates.getDifference": ["CDN_METHOD_INVALID", "DATE_EMPTY", "PERSISTENT_TIMESTAMP_EMPTY", "PERSISTENT_TIMESTAMP_INVALID"], "upload.getCdnFileHashes": ["CDN_METHOD_INVALID", "RSA_DECRYPT_FAILED"], "upload.getFile": ["FILE_ID_INVALID", "INPUT_FETCH_FAIL", "LIMIT_INVALID", "LOCATION_INVALID", "OFFSET_INVALID"], "upload.getWebFile": ["LOCATION_INVALID"], "upload.reuploadCdnFile": ["RSA_DECRYPT_FAILED"], "upload.saveBigFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "FILE_PART_SIZE_INVALID", "FILE_PARTS_INVALID"], "upload.saveFilePart": ["FILE_PART_EMPTY", "FILE_PART_INVALID", "INPUT_FETCH_FAIL"], "users.getFullUser": ["USER_ID_INVALID"], "{}": ["INVITE_HASH_EXPIRED"]}, "401": {"account.updateStatus": ["SESSION_PASSWORD_NEEDED"], "auth.signIn": ["SESSION_PASSWORD_NEEDED"], "contacts.resolveUsername": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "messages.getDialogs": ["SESSION_PASSWORD_NEEDED"], "messages.getHistory": ["AUTH_KEY_PERM_EMPTY"], "messages.importChatInvite": ["SESSION_PASSWORD_NEEDED"], "updates.getDifference": ["AUTH_KEY_PERM_EMPTY", "SESSION_PASSWORD_NEEDED"], "updates.getState": ["SESSION_PASSWORD_NEEDED"], "upload.saveFilePart": ["SESSION_PASSWORD_NEEDED"], "users.getUsers": ["SESSION_PASSWORD_NEEDED"]}, "403": {"channels.createChannel": ["USER_RESTRICTED"], "channels.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "channels.editAdmin": ["CHAT_ADMIN_INVITE_REQUIRED", "RIGHT_FORBIDDEN", "USER_PRIVACY_RESTRICTED"], "channels.getFullChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "channels.inviteToChannel": ["CHAT_WRITE_FORBIDDEN", "USER_CHANNELS_TOO_MUCH", "USER_PRIVACY_RESTRICTED"], "channels.leaveChannel": ["CHANNEL_PUBLIC_GROUP_NA"], "invokeWithLayer": ["CHAT_WRITE_FORBIDDEN"], "messages.addChatUser": ["USER_NOT_MUTUAL_CONTACT", "USER_PRIVACY_RESTRICTED"], "messages.createChat": ["USER_RESTRICTED"], "messages.deleteMessages": ["MESSAGE_DELETE_FORBIDDEN"], "messages.editMessage": ["CHAT_WRITE_FORBIDDEN", "MESSAGE_AUTHOR_REQUIRED"], "messages.forwardMessages": ["CHAT_SEND_GIFS_FORBIDDEN", "CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_SEND_STICKERS_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "messages.getMessageEditData": ["MESSAGE_AUTHOR_REQUIRED"], "messages.migrateChat": ["CHAT_ADMIN_REQUIRED"], "messages.sendEncryptedService": ["USER_IS_BLOCKED"], "messages.sendInlineBotResult": ["CHAT_WRITE_FORBIDDEN"], "messages.sendMedia": ["CHAT_SEND_MEDIA_FORBIDDEN", "CHAT_WRITE_FORBIDDEN"], "messages.sendMessage": ["CHAT_WRITE_FORBIDDEN"], "messages.setInlineBotResults": ["USER_BOT_INVALID"], "messages.setTyping": ["CHAT_WRITE_FORBIDDEN"], "phone.requestCall": ["USER_IS_BLOCKED", "USER_PRIVACY_RESTRICTED"], "updates.getChannelDifference": ["CHANNEL_PUBLIC_GROUP_NA"]}, "500": {"auth.sendCode": ["AUTH_RESTART"], "auth.signUp": ["MEMBER_OCCUPY_PRIMARY_LOC_FAILED", "REG_ID_GENERATE_FAILED"], "channels.getChannels": ["NEED_CHAT_INVALID"], "contacts.deleteContacts": ["NEED_MEMBER_INVALID"], "contacts.importCard": ["NEED_MEMBER_INVALID"], "invokeWithLayer": ["NEED_MEMBER_INVALID"], "messages.acceptEncryption": ["ENCRYPTION_OCCUPY_FAILED"], "messages.editChatTitle": ["NEED_CHAT_INVALID"], "messages.forwardMessages": ["PTS_CHANGE_EMPTY", "RANDOM_ID_DUPLICATE"], "messages.sendMedia": ["RANDOM_ID_DUPLICATE", "STORAGE_CHECK_FAILED"], "messages.sendMessage": ["RANDOM_ID_DUPLICATE"], "phone.acceptCall": ["CALL_OCCUPY_FAILED"], "phone.requestCall": ["PARTICIPANT_CALL_FAILED"], "updates.getChannelDifference": ["HISTORY_GET_FAILED", "PERSISTENT_TIMESTAMP_OUTDATED"], "updates.getDifference": ["NEED_MEMBER_INVALID", "STORE_INVALID_SCALAR_TYPE"], "upload.getCdnFile": ["UNKNOWN_METHOD"], "users.getUsers": ["MEMBER_NO_LOCATION", "NEED_MEMBER_INVALID"]}}} \ No newline at end of file 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 3116003a..b2706487 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(await client.get_input_entity({}))', + 'InputChannel': 'utils.get_input_channel(await client.get_input_entity({}))', + 'InputUser': 'utils.get_input_user(await 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 @@ -137,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. @@ -257,22 +257,56 @@ 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('async 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: - builder.writeln('return {') - else: - builder.write('return {') + builder.writeln('def to_dict(self):') + builder.writeln('return {') builder.current_indent += 1 base_types = ('string', 'bytes', 'int', 'long', 'int128', 'int256', 'double', 'Bool', 'true', 'date') + builder.write("'_': '{}'".format(tlobject.class_name())) for arg in args: + builder.writeln(',') builder.write("'{}': ".format(arg.name)) if arg.type in base_types: if arg.is_vector: @@ -283,17 +317,17 @@ class TLGenerator: else: if arg.is_vector: builder.write( - '([] if self.{0} is None else [None' - ' if x is None else x.to_dict() for x in self.{0}]' - ') if recursive else self.{0}'.format(arg.name) + '[] if self.{0} is None else [None ' + 'if x is None else x.to_dict() for x in self.{0}]' + .format(arg.name) ) else: builder.write( - '(None if self.{0} is None else self.{0}.to_dict())' - ' if recursive else self.{0}'.format(arg.name) + 'None if self.{0} is None else self.{0}.to_dict()' + .format(arg.name) ) - builder.writeln(',') + builder.writeln() builder.current_indent -= 1 builder.writeln("}") @@ -351,78 +385,43 @@ 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 _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)) + 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_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 @@ -695,13 +694,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)]' diff --git a/telethon_tests/crypto_test.py b/telethon_tests/crypto_test.py index e11704a4..17453f62 100644 --- a/telethon_tests/crypto_test.py +++ b/telethon_tests/crypto_test.py @@ -53,6 +53,7 @@ class CryptoTests(unittest.TestCase): @staticmethod def test_calc_key(): + # TODO Upgrade test for MtProto 2.0 shared_key = b'\xbc\xd2m\xb7\xcav\xf4][\x88\x83\' \xf3\x11\x8as\xd04\x941\xae' \ b'*O\x03\x86\x9a/H#\x1a\x8c\xb5j\xe9$\xe0IvCm^\xe70\x1a5C\t\x16' \ b'\x03\xd2\x9d\xa9\x89\xd6\xce\x08P\x0fdr\xa0\xb3\xeb\xfecv\x1a' \ @@ -98,13 +99,6 @@ class CryptoTests(unittest.TestCase): assert iv == expected_iv, 'Invalid IV (expected ("{}"), got ("{}"))'.format( expected_iv, iv) - @staticmethod - def test_calc_msg_key(): - value = utils.calc_msg_key(b'Some random message') - expected = b'\xdfAa\xfc\x10\xab\x89\xd2\xfe\x19C\xf1\xdd~\xbf\x81' - assert value == expected, 'Value ("{}") does not equal expected ("{}")'.format( - value, expected) - @staticmethod def test_generate_key_data_from_nonce(): server_nonce = int.from_bytes(b'The 16-bit nonce', byteorder='little')