mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-02 03:00:15 +03:00
Sync upstream
This commit is contained in:
commit
51842d4fda
|
@ -41,7 +41,7 @@ use these if possible.
|
|||
# ...to your contacts
|
||||
await client.send_message('+34600123123', 'Hello, friend!')
|
||||
# ...or even to any username
|
||||
await client.send_message('TelethonChat', 'Hello, Telethon!')
|
||||
await client.send_message('username', 'Testing Telethon!')
|
||||
|
||||
# You can, of course, use markdown in your messages:
|
||||
message = await client.send_message(
|
||||
|
|
|
@ -99,7 +99,7 @@ You will still need an API ID and hash, but the process is very similar:
|
|||
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
bot_token = '12345:0123456789abcdef0123456789abcdef
|
||||
bot_token = '12345:0123456789abcdef0123456789abcdef'
|
||||
|
||||
# We have to manually call "start" if we want an explicit bot token
|
||||
bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token)
|
||||
|
|
|
@ -219,19 +219,21 @@ Can I use threads?
|
|||
==================
|
||||
|
||||
Yes, you can, but you must understand that the loops themselves are
|
||||
not thread safe. and you must be sure to know what is happening. You
|
||||
may want to create a loop in a new thread and make sure to pass it to
|
||||
the client:
|
||||
not thread safe. and you must be sure to know what is happening. The
|
||||
easiest and cleanest option is to use `asyncio.run` to create and manage
|
||||
the new event loop for you:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
def go():
|
||||
loop = asyncio.new_event_loop()
|
||||
async def actual_work():
|
||||
client = TelegramClient(..., loop=loop)
|
||||
...
|
||||
... # can use `await` here
|
||||
|
||||
def go():
|
||||
asyncio.run(actual_work())
|
||||
|
||||
threading.Thread(target=go).start()
|
||||
|
||||
|
@ -308,27 +310,26 @@ you can run requests in parallel:
|
|||
|
||||
async def main():
|
||||
last, sent, download_path = await asyncio.gather(
|
||||
client.get_messages('TelethonChat', 10),
|
||||
client.send_message('TelethonOfftopic', 'Hey guys!'),
|
||||
client.download_profile_photo('TelethonChat')
|
||||
client.get_messages('telegram', 10),
|
||||
client.send_message('me', 'Using asyncio!'),
|
||||
client.download_profile_photo('telegram')
|
||||
)
|
||||
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
This code will get the 10 last messages from `@TelethonChat
|
||||
<https://t.me/TelethonChat>`_, send one to `@TelethonOfftopic
|
||||
<https://t.me/TelethonOfftopic>`_, and also download the profile
|
||||
photo of the main group. `asyncio` will run all these three tasks
|
||||
at the same time. You can run all the tasks you want this way.
|
||||
This code will get the 10 last messages from `@telegram
|
||||
<https://t.me/telegram>`_, send one to the chat with yourself, and also
|
||||
download the profile photo of the channel. `asyncio` will run all these
|
||||
three tasks at the same time. You can run all the tasks you want this way.
|
||||
|
||||
A different way would be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
loop.create_task(client.get_messages('TelethonChat', 10))
|
||||
loop.create_task(client.send_message('TelethonOfftopic', 'Hey guys!'))
|
||||
loop.create_task(client.download_profile_photo('TelethonChat'))
|
||||
loop.create_task(client.get_messages('telegram', 10))
|
||||
loop.create_task(client.send_message('me', 'Using asyncio!'))
|
||||
loop.create_task(client.download_profile_photo('telegram'))
|
||||
|
||||
They will run in the background as long as the loop is running too.
|
||||
|
||||
|
|
|
@ -178,7 +178,7 @@ exist, which just have the ID. You cannot get the hash out of them since
|
|||
you should not be needing it. The library probably has cached it before.
|
||||
|
||||
Peers are enough to identify an entity, but they are not enough to make
|
||||
a request with them use them. You need to know their hash before you can
|
||||
a request with them. You need to know their hash before you can
|
||||
"use them", and to know the hash you need to "encounter" them, let it
|
||||
be in your dialogs, participants, message forwards, etc.
|
||||
|
||||
|
@ -296,10 +296,10 @@ applications"? Now do the same with the library. Use what applies:
|
|||
await client.get_dialogs()
|
||||
|
||||
# Are they participant of some group? Get them.
|
||||
await client.get_participants('TelethonChat')
|
||||
await client.get_participants('username')
|
||||
|
||||
# Is the entity the original sender of a forwarded message? Get it.
|
||||
await client.get_messages('TelethonChat', 100)
|
||||
await client.get_messages('username', 100)
|
||||
|
||||
# NOW you can use the ID, anywhere!
|
||||
await client.send_message(123456, 'Hi!')
|
||||
|
|
|
@ -100,6 +100,9 @@ There are other community-maintained implementations available:
|
|||
* `Redis <https://github.com/ezdev128/telethon-session-redis>`_:
|
||||
stores all sessions in a single Redis data store.
|
||||
|
||||
* `MongoDB <https://github.com/watzon/telethon-session-mongo>`_:
|
||||
stores the current session in a MongoDB database.
|
||||
|
||||
|
||||
Creating your Own Storage
|
||||
=========================
|
||||
|
|
|
@ -10,7 +10,7 @@ understand the official Telegram documentation) on several languages
|
|||
(even more Python too), listed below:
|
||||
|
||||
C
|
||||
*
|
||||
=
|
||||
|
||||
Possibly the most well-known unofficial open source implementation out
|
||||
there by `@vysheng <https://github.com/vysheng>`__,
|
||||
|
@ -29,10 +29,10 @@ published `here <https://core.telegram.org/tdlib/docs/>`__.
|
|||
JavaScript
|
||||
==========
|
||||
|
||||
`@zerobias <https://github.com/zerobias>`__ is working on
|
||||
`telegram-mtproto <https://github.com/zerobias/telegram-mtproto>`__,
|
||||
a work-in-progress JavaScript library installable via
|
||||
`npm <https://www.npmjs.com/>`__.
|
||||
`Ali Gasymov <https://github.com/alik0211>`__ made the `@mtproto/core <https://github.com/alik0211/mtproto-core>`__ library for the browser and nodejs installable via `npm <https://www.npmjs.com/package/@mtproto/core>`__.
|
||||
|
||||
`painor <https://github.com/painor>`__ is the primary author of `gramjs <https://github.com/gram-js/gramjs>`__,
|
||||
a Telegram client implementation in JavaScript.
|
||||
|
||||
Kotlin
|
||||
======
|
||||
|
@ -45,6 +45,11 @@ languages for
|
|||
`@badoualy <https://github.com/badoualy>`__, currently as a beta–
|
||||
yet working.
|
||||
|
||||
Language-Agnostic
|
||||
=================
|
||||
|
||||
`Taas <https://www.t-a-a-s.ru/>`__ is a service that lets you use Telegram API with any HTTP client via API. Using tdlib under the hood, Taas is commercial service, but allows free access if you use under 5000 requests per month.
|
||||
|
||||
PHP
|
||||
===
|
||||
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
.. _telethon_projects:
|
||||
|
||||
=======================
|
||||
Projects using Telethon
|
||||
=======================
|
||||
|
||||
This page lists some **interesting and useful** real world
|
||||
examples showcasing what can be built with the library.
|
||||
|
||||
.. note::
|
||||
|
||||
Do you have an interesting project that uses the library or know of any
|
||||
that's not listed here? Feel free to leave a comment at
|
||||
`issue 744 <https://github.com/LonamiWebs/Telethon/issues/744>`_
|
||||
so it can be included in the next revision of the documentation!
|
||||
|
||||
You can also advertise your bot and its features, in the issue, although
|
||||
it should be a big project which can be useful for others before being
|
||||
included here, so please don't feel offended if it can't be here!
|
||||
|
||||
|
||||
.. _projects-telegram-export:
|
||||
|
||||
telethon_examples/
|
||||
==================
|
||||
|
||||
`telethon_examples <https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples>`_ /
|
||||
`Lonami's site <https://lonami.dev>`_
|
||||
|
||||
This documentation is not the only place where you can find useful code
|
||||
snippets using the library. The main repository also has a folder with
|
||||
some cool examples (even a Tkinter GUI!) which you can download, edit
|
||||
and run to learn and play with them.
|
||||
|
||||
@TelethonSnippets
|
||||
=================
|
||||
|
||||
`@TelethonSnippets <https://t.me/TelethonSnippets>`_
|
||||
|
||||
You can find useful short snippets for Telethon here.
|
||||
|
||||
telegram-export
|
||||
===============
|
||||
|
||||
`telegram-export <https://github.com/expectocode/telegram-export>`_ /
|
||||
`expectocode's GitHub <https://github.com/expectocode>`_
|
||||
|
||||
A tool to download Telegram data (users, chats, messages, and media)
|
||||
into a database (and display the saved data).
|
||||
|
||||
.. _projects-mautrix-telegram:
|
||||
|
||||
mautrix-telegram
|
||||
================
|
||||
|
||||
`mautrix-telegram <https://github.com/tulir/mautrix-telegram>`_ /
|
||||
`maunium's site <https://maunium.net/>`_
|
||||
|
||||
A Matrix-Telegram hybrid puppeting/relaybot bridge.
|
||||
|
||||
.. _projects-telegramtui:
|
||||
|
||||
TelegramTUI
|
||||
===========
|
||||
|
||||
`TelegramTUI <https://github.com/bad-day/TelegramTUI>`_ /
|
||||
`bad-day's GitHub <https://github.com/bad-day>`_
|
||||
|
||||
A Telegram client on your terminal.
|
||||
|
||||
tgcloud
|
||||
=======
|
||||
|
||||
`tgcloud <https://github.com/SlavikMIPT/tgcloud>`_ /
|
||||
`tgcloud's site <https://dev.tgcloud.xyz/>`_
|
||||
|
||||
Opensource Telegram based cloud storage.
|
||||
|
||||
tgmount
|
||||
=======
|
||||
|
||||
`tgmount <https://github.com/nktknshn/tgmount>`_ /
|
||||
`nktknshn's GitHub <https://github.com/nktknshn>`_
|
||||
|
||||
Mount Telegram dialogs and channels as a Virtual File System.
|
||||
|
||||
garnet
|
||||
======
|
||||
|
||||
`garnet <https://github.com/uwinx/pomegranate>`_ /
|
||||
`uwinx's GitHub <https://github.com/uwinx>`_
|
||||
|
||||
Pomegranate (or ``garnet`` for short) is a small telethon add-on which
|
||||
features persistent conversations based on Finite State Machines (FSM),
|
||||
a new ``Filter`` to define handlers more conveniently and utilities to
|
||||
run code on start and finish of the client. Be sure to check the project
|
||||
to learn about its latest features, since this description may be out of
|
||||
date.
|
|
@ -13,4 +13,5 @@ Full API **will** break between different minor versions of the library,
|
|||
since Telegram changes very often. The friendly methods will be kept
|
||||
compatible between major versions.
|
||||
|
||||
If you need to see real-world examples, please refer to :ref:`telethon_projects`.
|
||||
If you need to see real-world examples, please refer to the
|
||||
`wiki page of projects using Telethon <https://github.com/LonamiWebs/Telethon/wiki/Projects-using-Telethon>`__.
|
||||
|
|
|
@ -83,7 +83,6 @@ You can also use the menu on the left to quickly skip over sections.
|
|||
examples/chats-and-channels
|
||||
examples/users
|
||||
examples/working-with-messages
|
||||
examples/projects-using-telethon
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
|
|
@ -13,6 +13,198 @@ it can take advantage of new goodies!
|
|||
|
||||
.. contents:: List of All Versions
|
||||
|
||||
Bug Fixes (v1.16.1)
|
||||
===================
|
||||
|
||||
The last release added support to ``force_file`` on any media, including
|
||||
things that were not possible before like ``.webp`` files. However, the
|
||||
``force_document`` toggle commonly used for photos was applied "twice"
|
||||
(one told the library to send it as a document, and then to send that
|
||||
document as file), which prevented Telegram for analyzing the images. Long
|
||||
story short, sending files to the stickers bot stopped working, but that's
|
||||
been fixed now, and sending photos as documents include the size attribute
|
||||
again as long as Telegram adds it.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* When trying to `client.start() <telethon.client.auth.AuthMethods.start>` to
|
||||
another account if you were previously logged in, the library will now warn
|
||||
you because this is probably not intended. To avoid the warning, make sure
|
||||
you're logging in to the right account or logout from the other first.
|
||||
* Sending a copy of messages with polls will now work when possible.
|
||||
* The library now automatically retries on inter-dc call errors (which occur
|
||||
when Telegram has internal issues).
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* The aforementioned issue with ``force_document``.
|
||||
* Square brackets removed from IPv6 addresses. This may fix IPv6 support.
|
||||
|
||||
|
||||
Channel Statistics (v1.16)
|
||||
==========================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 116 |
|
||||
+------------------------+
|
||||
|
||||
The newest Telegram update has a new method to also retrieve megagroup
|
||||
statistics, which can now be used with `client.get_stats()
|
||||
<telethon.client.chats.ChatMethods.get_stats>`. This way you'll be able
|
||||
to access the raw data about your channel or megagroup statistics.
|
||||
|
||||
The maximum file size limit has also been increased to 2GB on the server,
|
||||
so you can send even larger files.
|
||||
|
||||
Breaking Changes
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* Besides the obvious layer change, the ``loop`` argument **is now ignored**.
|
||||
It has been deprecated since Python 3.8 and will be removed in Python 3.10,
|
||||
and also caused some annoying warning messages when using certain parts of
|
||||
the library. If you were (incorrectly) relying on using a different loop
|
||||
from the one that was set, things may break.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* `client.upload_file() <telethon.client.uploads.UploadMethods.upload_file>`
|
||||
now works better when streaming files (anything that has a ``.read()``),
|
||||
instead of reading it all into memory when possible.
|
||||
|
||||
|
||||
QR login (v1.15)
|
||||
================
|
||||
|
||||
*Published at 2020/07/04*
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 114 |
|
||||
+------------------------+
|
||||
|
||||
The library now has a friendly method to perform QR-login, as detailed in
|
||||
https://core.telegram.org/api/qr-login. It won't generate QR images, but it
|
||||
provides a way for you to easily do so with any other library of your choice.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* New `client.qr_login() <telethon.client.auth.AuthMethods.qr_login>`.
|
||||
* `message.click <telethon.tl.custom.message.Message.click>` now lets you
|
||||
click on buttons requesting phone or location.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Updated documentation and list of known errors.
|
||||
* `events.Album <telethon.events.album.Album>` should now handle albums from
|
||||
different data centers more gracefully.
|
||||
* `client.download_file()
|
||||
<telethon.client.downloads.DownloadMethods.download_file>` now supports
|
||||
`pathlib.Path` as the destination.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* No longer crash on updates received prior to logging in.
|
||||
* Server-side changes caused clicking on inline buttons to trigger a different
|
||||
error, which is now handled correctly.
|
||||
|
||||
|
||||
Minor quality of life improvements (v1.14)
|
||||
==========================================
|
||||
|
||||
*Published at 2020/05/26*
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 113 |
|
||||
+------------------------+
|
||||
|
||||
Some nice things that were missing, along with the usual bug-fixes.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* New `Message.dice <telethon.tl.custom.message.Message.dice>` property.
|
||||
* The ``func=`` parameter of events can now be an ``async`` function.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Fixed `client.action() <telethon.client.chats.ChatMethods.action>`
|
||||
having an alias wrong.
|
||||
* Fixed incorrect formatting of some errors.
|
||||
* Probably more reliable detection of pin events in small groups.
|
||||
* Fixed send methods on `client.conversation()
|
||||
<telethon.client.dialogs.DialogMethods.conversation>` were not honoring
|
||||
cancellation.
|
||||
* Flood waits of zero seconds are handled better.
|
||||
* Getting the pinned message in a chat was failing.
|
||||
* Fixed the return value when forwarding messages if some were missing
|
||||
and also the return value of albums.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* ``.tgs`` files are now recognised as animated stickers.
|
||||
* The service message produced by `Message.pin()
|
||||
<telethon.tl.custom.message.Message.pin>` is now returned.
|
||||
* Sending a file with `client.send_file()
|
||||
<telethon.client.uploads.UploadMethods.send_file>` now works fine when
|
||||
you pass an existing dice media (e.g. sending a message copy).
|
||||
* `client.edit_permissions() <telethon.client.chats.ChatMethods.edit_permissions>`
|
||||
now has the ``embed_links`` parameter which was missing.
|
||||
|
||||
Bug Fixes (v1.13)
|
||||
=================
|
||||
|
||||
*Published at 2020/04/25*
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 112 |
|
||||
+------------------------+
|
||||
|
||||
Bug fixes and layer bump.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Passing ``None`` as the entity to `client.delete_messages()
|
||||
<telethon.client.messages.MessageMethods.delete_messages>` would fail.
|
||||
* When downloading a thumbnail, the name inferred was wrong.
|
||||
|
||||
Bug Fixes (v1.12)
|
||||
=================
|
||||
|
||||
*Published at 2020/04/20*
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 111 |
|
||||
+------------------------+
|
||||
|
||||
Once again nothing major, but a few bug fixes and primarily the new layer
|
||||
deserves a new minor release.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
These were already included in the ``v1.11.3`` patch:
|
||||
|
||||
* ``libssl`` check was failing on macOS.
|
||||
* Getting input users would sometimes fail on `events.ChatAction
|
||||
<telethon.events.chataction.ChatAction>`.
|
||||
|
||||
These bug fixes are available in this release and beyond:
|
||||
|
||||
* Avoid another occurrence of `MemoryError`.
|
||||
* Sending large files in albums would fail because it tried to cache them.
|
||||
* The ``thumb`` was being ignored when sending files from :tl:`InputFile`.
|
||||
* Fixed editing inline messages from callback queries in some cases.
|
||||
* Proxy connection is now blocking which should help avoid some errors.
|
||||
|
||||
|
||||
Bug Fixes (v1.11)
|
||||
=================
|
||||
|
||||
|
|
|
@ -136,6 +136,15 @@ MessageButton
|
|||
:show-inheritance:
|
||||
|
||||
|
||||
QRLogin
|
||||
=======
|
||||
|
||||
.. automodule:: telethon.qrlogin
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
SenderGetter
|
||||
============
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ Auth
|
|||
start
|
||||
send_code_request
|
||||
sign_in
|
||||
qr_login
|
||||
sign_up
|
||||
log_out
|
||||
edit_2fa
|
||||
|
@ -138,6 +139,7 @@ Chats
|
|||
get_profile_photos
|
||||
edit_admin
|
||||
edit_permissions
|
||||
get_stats
|
||||
action
|
||||
|
||||
Parse Mode
|
||||
|
|
|
@ -3,9 +3,10 @@ import inspect
|
|||
import os
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from .. import utils, helpers, errors, password as pwd_mod
|
||||
from ..tl import types, functions
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
@ -138,7 +139,29 @@ class AuthMethods:
|
|||
if not self.is_connected():
|
||||
await self.connect()
|
||||
|
||||
if await self.is_user_authorized():
|
||||
# Rather than using `is_user_authorized`, use `get_me`. While this is
|
||||
# more expensive and needs to retrieve more data from the server, it
|
||||
# enables the library to warn users trying to login to a different
|
||||
# account. See #1172.
|
||||
me = await self.get_me()
|
||||
if me is not None:
|
||||
# The warnings here are on a best-effort and may fail.
|
||||
if bot_token:
|
||||
# bot_token's first part has the bot ID, but it may be invalid
|
||||
# so don't try to parse as int (instead cast our ID to string).
|
||||
if bot_token[:bot_token.find(':')] != str(me.id):
|
||||
warnings.warn(
|
||||
'the session already had an authorized user so it did '
|
||||
'not login to the bot account using the provided '
|
||||
'bot_token (it may not be using the user you expect)'
|
||||
)
|
||||
elif not callable(phone) and phone != me.phone:
|
||||
warnings.warn(
|
||||
'the session already had an authorized user so it did '
|
||||
'not login to the user account using the provided '
|
||||
'phone (it may not be using the user you expect)'
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
if not bot_token:
|
||||
|
@ -496,6 +519,43 @@ class AuthMethods:
|
|||
|
||||
return result
|
||||
|
||||
async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin:
|
||||
"""
|
||||
Initiates the QR login procedure.
|
||||
|
||||
Note that you must be connected before invoking this, as with any
|
||||
other request.
|
||||
|
||||
It is up to the caller to decide how to present the code to the user,
|
||||
whether it's the URL, using the token bytes directly, or generating
|
||||
a QR code and displaying it by other means.
|
||||
|
||||
See the documentation for `QRLogin` to see how to proceed after this.
|
||||
|
||||
Arguments
|
||||
ignored_ids (List[`int`]):
|
||||
List of already logged-in user IDs, to prevent logging in
|
||||
twice with the same user.
|
||||
|
||||
Returns
|
||||
An instance of `QRLogin`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
def display_url_as_qr(url):
|
||||
pass # do whatever to show url as a qr to the user
|
||||
|
||||
qr_login = await client.qr_login()
|
||||
display_url_as_qr(qr_login.url)
|
||||
|
||||
# Important! You need to wait for the login to complete!
|
||||
await qr_login.wait()
|
||||
"""
|
||||
qr_login = custom.QRLogin(self, ignored_ids or [])
|
||||
await qr_login.recreate()
|
||||
return qr_login
|
||||
|
||||
async def log_out(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
Logs out Telegram and deletes the current ``*.session`` file.
|
||||
|
|
|
@ -4,7 +4,7 @@ import itertools
|
|||
import string
|
||||
import typing
|
||||
|
||||
from .. import helpers, utils, hints
|
||||
from .. import helpers, utils, hints, errors
|
||||
from ..requestiter import RequestIter
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
|
@ -30,13 +30,13 @@ class _ChatAction:
|
|||
|
||||
'audio': types.SendMessageUploadAudioAction(1),
|
||||
'voice': types.SendMessageUploadAudioAction(1), # alias
|
||||
'song': types.SendMessageUploadAudioAction(1), # alias
|
||||
'round': types.SendMessageUploadRoundAction(1),
|
||||
'video': types.SendMessageUploadVideoAction(1),
|
||||
|
||||
'photo': types.SendMessageUploadPhotoAction(1),
|
||||
'document': types.SendMessageUploadDocumentAction(1),
|
||||
'file': types.SendMessageUploadDocumentAction(1), # alias
|
||||
'song': types.SendMessageUploadDocumentAction(1), # alias
|
||||
|
||||
'cancel': types.SendMessageCancelAction()
|
||||
}
|
||||
|
@ -337,9 +337,25 @@ class _ProfilePhotoIter(RequestIter):
|
|||
else:
|
||||
self.request.offset += len(result.photos)
|
||||
else:
|
||||
self.total = getattr(result, 'count', None)
|
||||
if self.total == 0 and isinstance(result, types.messages.ChannelMessages):
|
||||
# There are some broadcast channels that have a photo but this
|
||||
# request doesn't retrieve it for some reason. Work around this
|
||||
# issue by fetching the channel.
|
||||
#
|
||||
# We can't use the normal entity because that gives `ChatPhoto`
|
||||
# but we want a proper `Photo`, so fetch full channel instead.
|
||||
channel = await self.client(functions.channels.GetFullChannelRequest(self.request.peer))
|
||||
photo = channel.full_chat.chat_photo
|
||||
if isinstance(photo, types.Photo):
|
||||
self.buffer = [photo]
|
||||
self.total = 1
|
||||
|
||||
self.left = len(self.buffer)
|
||||
return
|
||||
|
||||
self.buffer = [x.action.photo for x in result.messages
|
||||
if isinstance(x.action, types.MessageActionChatEditPhoto)]
|
||||
self.total = getattr(result, 'count', None)
|
||||
if len(result.messages) < self.request.limit:
|
||||
self.left = len(self.buffer)
|
||||
elif result.messages:
|
||||
|
@ -922,6 +938,7 @@ class ChatMethods:
|
|||
send_gifs: bool = True,
|
||||
send_games: bool = True,
|
||||
send_inline: bool = True,
|
||||
embed_link_previews: bool = True,
|
||||
send_polls: bool = True,
|
||||
change_info: bool = True,
|
||||
invite_users: bool = True,
|
||||
|
@ -986,6 +1003,12 @@ class ChatMethods:
|
|||
send_inline (`bool`, optional):
|
||||
Whether the user is able to use inline bots or not.
|
||||
|
||||
embed_link_previews (`bool`, optional):
|
||||
Whether the user is able to enable the link preview in the
|
||||
messages they send. Note that the user will still be able to
|
||||
send messages with links if this permission is removed, but
|
||||
these links won't display a link preview.
|
||||
|
||||
send_polls (`bool`, optional):
|
||||
Whether the user is able to send polls or not.
|
||||
|
||||
|
@ -1031,6 +1054,7 @@ class ChatMethods:
|
|||
send_gifs=not send_gifs,
|
||||
send_games=not send_games,
|
||||
send_inline=not send_inline,
|
||||
embed_links=not embed_link_previews,
|
||||
send_polls=not send_polls,
|
||||
change_info=not change_info,
|
||||
invite_users=not invite_users,
|
||||
|
@ -1115,4 +1139,67 @@ class ChatMethods:
|
|||
else:
|
||||
raise ValueError('You must pass either a channel or a chat')
|
||||
|
||||
async def get_stats(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
):
|
||||
"""
|
||||
Retrieves statistics from the given megagroup or broadcast channel.
|
||||
|
||||
Note that some restrictions apply before being able to fetch statistics,
|
||||
in particular the channel must have enough members (for megagroups, this
|
||||
requires `at least 500 members`_).
|
||||
|
||||
Arguments
|
||||
entity (`entity`):
|
||||
The channel from which to get statistics.
|
||||
|
||||
Raises
|
||||
If the given entity is not a channel (broadcast or megagroup),
|
||||
a `TypeError` is raised.
|
||||
|
||||
If there are not enough members (poorly named) errors such as
|
||||
``telethon.errors.ChatAdminRequiredError`` will appear.
|
||||
|
||||
Returns
|
||||
Either :tl:`BroadcastStats` or :tl:`MegagroupStats`, depending on
|
||||
whether the input belonged to a broadcast channel or megagroup.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Some megagroup or channel username or ID to fetch
|
||||
channel = -100123
|
||||
stats = await client.get_stats(channel)
|
||||
print('Stats from', stats.period.min_date, 'to', stats.period.max_date, ':')
|
||||
print(stats.stringify())
|
||||
|
||||
.. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more
|
||||
"""
|
||||
entity = await self.get_input_entity(entity)
|
||||
if helpers._entity_type(entity) != helpers._EntityType.CHANNEL:
|
||||
raise TypeError('You must pass a user entity')
|
||||
|
||||
# Don't bother fetching the Channel entity (costs a request), instead
|
||||
# try to guess and if it fails we know it's the other one (best case
|
||||
# no extra request, worst just one).
|
||||
try:
|
||||
req = functions.stats.GetBroadcastStatsRequest(entity)
|
||||
return await self(req)
|
||||
except errors.StatsMigrateError as e:
|
||||
dc = e.dc
|
||||
except errors.BroadcastRequiredError:
|
||||
req = functions.stats.GetMegagroupStatsRequest(entity)
|
||||
try:
|
||||
return await self(req)
|
||||
except errors.StatsMigrateError as e:
|
||||
dc = e.dc
|
||||
|
||||
sender = await self._borrow_exported_sender(dc)
|
||||
try:
|
||||
# req will be resolved to use the right types inside by now
|
||||
return await sender.send(req)
|
||||
finally:
|
||||
await self._return_exported_sender(sender)
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -245,7 +245,7 @@ class DialogMethods:
|
|||
|
||||
# Getting only archived dialogs (both equivalent)
|
||||
archived = await client.get_dialogs(folder=1)
|
||||
non_archived = await client.get_dialogs(archived=True)
|
||||
archived = await client.get_dialogs(archived=True)
|
||||
"""
|
||||
return await self.iter_dialogs(*args, **kwargs).collect()
|
||||
|
||||
|
@ -378,7 +378,7 @@ class DialogMethods:
|
|||
entities = [await self.get_input_entity(entity)]
|
||||
else:
|
||||
entities = await asyncio.gather(
|
||||
*(self.get_input_entity(x) for x in entity), loop=self.loop)
|
||||
*(self.get_input_entity(x) for x in entity))
|
||||
|
||||
if folder is None:
|
||||
raise ValueError('You must specify a folder')
|
||||
|
|
|
@ -5,6 +5,8 @@ import pathlib
|
|||
import typing
|
||||
import inspect
|
||||
|
||||
from ..crypto import AES
|
||||
|
||||
from .. import utils, helpers, errors, hints
|
||||
from ..requestiter import RequestIter
|
||||
from ..tl import TLObject, types, functions
|
||||
|
@ -17,7 +19,6 @@ except ImportError:
|
|||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
# Chunk sizes for upload.getFile must be multiples of the smallest size
|
||||
MIN_CHUNK_SIZE = 4096
|
||||
MAX_CHUNK_SIZE = 512 * 1024
|
||||
|
@ -67,7 +68,7 @@ class _DirectDownloadIter(RequestIter):
|
|||
|
||||
async def _request(self):
|
||||
try:
|
||||
result = await self._sender.send(self.request)
|
||||
result = await self.client._call(self._sender, self.request)
|
||||
if isinstance(result, types.upload.FileCdnRedirect):
|
||||
raise NotImplementedError # TODO Implement
|
||||
else:
|
||||
|
@ -309,13 +310,20 @@ class DownloadMethods:
|
|||
The parameter should be an integer index between ``0`` and
|
||||
``len(sizes)``. ``0`` will download the smallest thumbnail,
|
||||
and ``len(sizes) - 1`` will download the largest thumbnail.
|
||||
You can also use negative indices.
|
||||
You can also use negative indices, which work the same as
|
||||
they do in Python's `list`.
|
||||
|
||||
You can also pass the :tl:`PhotoSize` instance to use.
|
||||
Alternatively, the thumb size type `str` may be used.
|
||||
|
||||
In short, use ``thumb=0`` if you want the smallest thumbnail
|
||||
and ``thumb=-1`` if you want the largest thumbnail.
|
||||
|
||||
.. note::
|
||||
The largest thumbnail may be a video instead of a photo,
|
||||
as they are available since layer 116 and are bigger than
|
||||
any of the photos.
|
||||
|
||||
Returns
|
||||
`None` if no media was provided, or if it was Empty. On success
|
||||
the file path is returned since it may differ from the one given.
|
||||
|
@ -328,6 +336,13 @@ class DownloadMethods:
|
|||
# or
|
||||
path = await message.download_media()
|
||||
await message.download_media(filename)
|
||||
|
||||
# Printing download progress
|
||||
def callback(current, total):
|
||||
print('Downloaded', current, 'out of', total,
|
||||
'bytes: {:.2%}'.format(current / total))
|
||||
|
||||
await client.download_media(message, progress_callback=callback)
|
||||
"""
|
||||
# TODO This won't work for messageService
|
||||
if isinstance(message, types.Message):
|
||||
|
@ -369,10 +384,17 @@ class DownloadMethods:
|
|||
part_size_kb: float = None,
|
||||
file_size: int = None,
|
||||
progress_callback: 'hints.ProgressCallback' = None,
|
||||
dc_id: int = None) -> typing.Optional[bytes]:
|
||||
dc_id: int = None,
|
||||
key: bytes = None,
|
||||
iv: bytes = None) -> typing.Optional[bytes]:
|
||||
"""
|
||||
Low-level method to download files from their input location.
|
||||
|
||||
.. note::
|
||||
|
||||
Generally, you should instead use `download_media`.
|
||||
This method is intended to be a bit more low-level.
|
||||
|
||||
Arguments
|
||||
input_location (:tl:`InputFileLocation`):
|
||||
The file location from which the file will be downloaded.
|
||||
|
@ -403,6 +425,13 @@ class DownloadMethods:
|
|||
The data center the library should connect to in order
|
||||
to download the file. You shouldn't worry about this.
|
||||
|
||||
key ('bytes', optional):
|
||||
In case of an encrypted upload (secret chats) a key is supplied
|
||||
|
||||
iv ('bytes', optional):
|
||||
In case of an encrypted upload (secret chats) an iv is supplied
|
||||
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -421,6 +450,9 @@ class DownloadMethods:
|
|||
raise ValueError(
|
||||
'The part size must be evenly divisible by 4096.')
|
||||
|
||||
if isinstance(file, pathlib.Path):
|
||||
file = str(file.absolute())
|
||||
|
||||
in_memory = file is None or file is bytes
|
||||
if in_memory:
|
||||
f = io.BytesIO()
|
||||
|
@ -434,6 +466,8 @@ class DownloadMethods:
|
|||
try:
|
||||
async for chunk in self.iter_download(
|
||||
input_location, request_size=part_size, dc_id=dc_id):
|
||||
if iv and key:
|
||||
chunk = AES.decrypt_ige(chunk, key, iv)
|
||||
r = f.write(chunk)
|
||||
if inspect.isawaitable(r):
|
||||
await r
|
||||
|
@ -535,25 +569,18 @@ class DownloadMethods:
|
|||
# Streaming `media` to an output file
|
||||
# After the iteration ends, the sender is cleaned up
|
||||
with open('photo.jpg', 'wb') as fd:
|
||||
async for chunk client.iter_download(media):
|
||||
async for chunk in client.iter_download(media):
|
||||
fd.write(chunk)
|
||||
|
||||
# Fetching only the header of a file (32 bytes)
|
||||
# You should manually close the iterator in this case.
|
||||
#
|
||||
# telethon.sync must be imported for this to work,
|
||||
# and you must not be inside an "async def".
|
||||
# "stream" is a common name for asynchronous generators,
|
||||
# and iter_download will yield `bytes` (chunks of the file).
|
||||
stream = client.iter_download(media, request_size=32)
|
||||
header = next(stream)
|
||||
stream.close()
|
||||
header = await stream.__anext__() # "manual" version of `async for`
|
||||
await stream.close()
|
||||
assert len(header) == 32
|
||||
|
||||
# Fetching only the header, inside of an ``async def``
|
||||
async def main():
|
||||
stream = client.iter_download(media, request_size=32)
|
||||
header = await stream.__anext__()
|
||||
await stream.close()
|
||||
assert len(header) == 32
|
||||
"""
|
||||
info = utils._get_file_info(file)
|
||||
if info.dc_id is not None:
|
||||
|
@ -610,12 +637,32 @@ class DownloadMethods:
|
|||
|
||||
@staticmethod
|
||||
def _get_thumb(thumbs, thumb):
|
||||
# Seems Telegram has changed the order and put `PhotoStrippedSize`
|
||||
# last while this is the smallest (layer 116). Ensure we have the
|
||||
# sizes sorted correctly with a custom function.
|
||||
def sort_thumbs(thumb):
|
||||
if isinstance(thumb, types.PhotoStrippedSize):
|
||||
return 1, len(thumb.bytes)
|
||||
if isinstance(thumb, types.PhotoCachedSize):
|
||||
return 1, len(thumb.bytes)
|
||||
if isinstance(thumb, types.PhotoSize):
|
||||
return 1, thumb.size
|
||||
if isinstance(thumb, types.VideoSize):
|
||||
return 2, thumb.size
|
||||
|
||||
# Empty size or invalid should go last
|
||||
return 0, 0
|
||||
|
||||
thumbs = list(sorted(thumbs, key=sort_thumbs))
|
||||
|
||||
if thumb is None:
|
||||
return thumbs[-1]
|
||||
elif isinstance(thumb, int):
|
||||
return thumbs[thumb]
|
||||
elif isinstance(thumb, str):
|
||||
return next((t for t in thumbs if t.type == thumb), None)
|
||||
elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize,
|
||||
types.PhotoStrippedSize)):
|
||||
types.PhotoStrippedSize, types.VideoSize)):
|
||||
return thumb
|
||||
else:
|
||||
return None
|
||||
|
@ -650,11 +697,16 @@ class DownloadMethods:
|
|||
if not isinstance(photo, types.Photo):
|
||||
return
|
||||
|
||||
size = self._get_thumb(photo.sizes, thumb)
|
||||
# Include video sizes here (but they may be None so provide an empty list)
|
||||
size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb)
|
||||
if not size or isinstance(size, types.PhotoSizeEmpty):
|
||||
return
|
||||
|
||||
file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
|
||||
if isinstance(size, types.VideoSize):
|
||||
file = self._get_proper_filename(file, 'video', '.mp4', date=date)
|
||||
else:
|
||||
file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
|
||||
|
||||
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
|
||||
return self._download_cached_photo_size(size, file)
|
||||
|
||||
|
@ -703,15 +755,15 @@ class DownloadMethods:
|
|||
if not isinstance(document, types.Document):
|
||||
return
|
||||
|
||||
kind, possible_names = self._get_kind_and_names(document.attributes)
|
||||
file = self._get_proper_filename(
|
||||
file, kind, utils.get_extension(document),
|
||||
date=date, possible_names=possible_names
|
||||
)
|
||||
|
||||
if thumb is None:
|
||||
kind, possible_names = self._get_kind_and_names(document.attributes)
|
||||
file = self._get_proper_filename(
|
||||
file, kind, utils.get_extension(document),
|
||||
date=date, possible_names=possible_names
|
||||
)
|
||||
size = None
|
||||
else:
|
||||
file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
|
||||
size = self._get_thumb(document.thumbs, thumb)
|
||||
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
|
||||
return self._download_cached_photo_size(size, file)
|
||||
|
|
|
@ -131,7 +131,18 @@ class MessageParseMethods:
|
|||
elif isinstance(update, (
|
||||
types.UpdateNewChannelMessage, types.UpdateNewMessage)):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
id_to_message[update.message.id] = update.message
|
||||
|
||||
# Pinning a message with `updatePinnedMessage` seems to
|
||||
# always produce a service message we can't map so return
|
||||
# it directly.
|
||||
#
|
||||
# It could also be a list (e.g. when sending albums).
|
||||
#
|
||||
# TODO this method is getting messier and messier as time goes on
|
||||
if hasattr(request, 'random_id') or utils.is_list_like(request):
|
||||
id_to_message[update.message.id] = update.message
|
||||
else:
|
||||
return update.message
|
||||
|
||||
elif (isinstance(update, types.UpdateEditMessage)
|
||||
and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL):
|
||||
|
@ -204,13 +215,18 @@ class MessageParseMethods:
|
|||
# deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at
|
||||
# Telegram), in which case we get some "missing" message mappings.
|
||||
# Log them with the hope that we can better work around them.
|
||||
#
|
||||
# This also happens when trying to forward messages that can't
|
||||
# be forwarded because they don't exist (0, service, deleted)
|
||||
# among others which could be (like deleted or existing).
|
||||
self._log[__name__].warning(
|
||||
'Request %s had missing message mappings %s', request, result)
|
||||
|
||||
return [
|
||||
mapping.get(random_to_id.get(rnd))
|
||||
or opposite.get(random_to_id.get(rnd))
|
||||
for rnd in random_to_id
|
||||
(mapping.get(random_to_id[rnd]) or opposite.get(random_to_id[rnd]))
|
||||
if rnd in random_to_id
|
||||
else None
|
||||
for rnd in random_id
|
||||
]
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -1025,14 +1025,25 @@ class MessageMethods:
|
|||
force_document=force_document)
|
||||
|
||||
if isinstance(entity, types.InputBotInlineMessageID):
|
||||
return await self(functions.messages.EditInlineBotMessageRequest(
|
||||
request = functions.messages.EditInlineBotMessageRequest(
|
||||
id=entity,
|
||||
message=text,
|
||||
no_webpage=not link_preview,
|
||||
entities=msg_entities,
|
||||
media=media,
|
||||
reply_markup=self.build_reply_markup(buttons)
|
||||
))
|
||||
)
|
||||
# Invoke `messages.editInlineBotMessage` from the right datacenter.
|
||||
# Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing.
|
||||
exported = self.session.dc_id != entity.dc_id
|
||||
if exported:
|
||||
try:
|
||||
sender = await self._borrow_exported_sender(entity.dc_id)
|
||||
return await self._call(sender, request)
|
||||
finally:
|
||||
await self._return_exported_sender(sender)
|
||||
else:
|
||||
return await self(request)
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
request = functions.messages.EditMessageRequest(
|
||||
|
@ -1046,7 +1057,6 @@ class MessageMethods:
|
|||
schedule_date=schedule
|
||||
)
|
||||
msg = self._get_response_message(request, await self(request), entity)
|
||||
await self._cache_media(msg, file, file_handle, image=image)
|
||||
return msg
|
||||
|
||||
async def delete_messages(
|
||||
|
@ -1108,8 +1118,14 @@ class MessageMethods:
|
|||
else int(m) for m in message_ids
|
||||
)
|
||||
|
||||
entity = await self.get_input_entity(entity) if entity else None
|
||||
if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:
|
||||
if entity:
|
||||
entity = await self.get_input_entity(entity)
|
||||
ty = helpers._entity_type(entity)
|
||||
else:
|
||||
# no entity (None), set a value that's not a channel for private delete
|
||||
ty = helpers._EntityType.USER
|
||||
|
||||
if ty == helpers._EntityType.CHANNEL:
|
||||
return await self([functions.channels.DeleteMessagesRequest(
|
||||
entity, list(c)) for c in utils.chunks(message_ids)])
|
||||
else:
|
||||
|
@ -1136,6 +1152,10 @@ class MessageMethods:
|
|||
If neither message nor maximum ID are provided, all messages will be
|
||||
marked as read by assuming that ``max_id = 0``.
|
||||
|
||||
If a message or maximum ID is provided, all the messages up to and
|
||||
including such ID will be marked as read (for all messages whose ID
|
||||
≤ max_id).
|
||||
|
||||
See also `Message.mark_read() <telethon.tl.custom.message.Message.mark_read>`.
|
||||
|
||||
Arguments
|
||||
|
@ -1146,8 +1166,8 @@ class MessageMethods:
|
|||
Either a list of messages or a single message.
|
||||
|
||||
max_id (`int`):
|
||||
Overrides messages, until which message should the
|
||||
acknowledge should be sent.
|
||||
Until which message should the read acknowledge be sent for.
|
||||
This has priority over the ``message`` parameter.
|
||||
|
||||
clear_mentions (`bool`):
|
||||
Whether the mention badge should be cleared (so that
|
||||
|
@ -1226,11 +1246,24 @@ class MessageMethods:
|
|||
"""
|
||||
message = utils.get_message_id(message) or 0
|
||||
entity = await self.get_input_entity(entity)
|
||||
await self(functions.messages.UpdatePinnedMessageRequest(
|
||||
request = functions.messages.UpdatePinnedMessageRequest(
|
||||
peer=entity,
|
||||
id=message,
|
||||
silent=not notify
|
||||
))
|
||||
)
|
||||
result = await self(request)
|
||||
|
||||
# Unpinning does not produce a service message, and technically
|
||||
# users can pass negative IDs which seem to behave as unpinning too.
|
||||
if message <= 0:
|
||||
return
|
||||
|
||||
# Pinning in User chats (just with yourself really) does not produce a service message
|
||||
if helpers._entity_type(entity) == helpers._EntityType.USER:
|
||||
return
|
||||
|
||||
# Pinning a message that doesn't exist would RPC-error earlier
|
||||
return self._get_response_message(request, result, entity)
|
||||
|
||||
# endregion
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import asyncio
|
|||
import collections
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
|
||||
|
@ -14,12 +13,12 @@ from ..extensions import markdown
|
|||
from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
|
||||
from ..sessions import Session, SQLiteSession, MemorySession
|
||||
from ..statecache import StateCache
|
||||
from ..tl import TLObject, functions, types
|
||||
from ..tl import functions, types
|
||||
from ..tl.alltlobjects import LAYER
|
||||
|
||||
DEFAULT_DC_ID = 2
|
||||
DEFAULT_IPV4_IP = '149.154.167.51'
|
||||
DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]'
|
||||
DEFAULT_IPV6_IP = '2001:67c:4e8:f002::a'
|
||||
DEFAULT_PORT = 443
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
@ -29,6 +28,41 @@ __default_log__ = logging.getLogger(__base_name__)
|
|||
__default_log__.addHandler(logging.NullHandler())
|
||||
|
||||
|
||||
# In seconds, how long to wait before disconnecting a exported sender.
|
||||
_DISCONNECT_EXPORTED_AFTER = 60
|
||||
|
||||
|
||||
class _ExportState:
|
||||
def __init__(self):
|
||||
# ``n`` is the amount of borrows a given sender has;
|
||||
# once ``n`` reaches ``0``, disconnect the sender after a while.
|
||||
self._n = 0
|
||||
self._zero_ts = 0
|
||||
self._connected = False
|
||||
|
||||
def add_borrow(self):
|
||||
self._n += 1
|
||||
self._connected = True
|
||||
|
||||
def add_return(self):
|
||||
self._n -= 1
|
||||
assert self._n >= 0, 'returned sender more than it was borrowed'
|
||||
if self._n == 0:
|
||||
self._zero_ts = time.time()
|
||||
|
||||
def should_disconnect(self):
|
||||
return (self._n == 0
|
||||
and self._connected
|
||||
and (time.time() - self._zero_ts) > _DISCONNECT_EXPORTED_AFTER)
|
||||
|
||||
def need_connect(self):
|
||||
return not self._connected
|
||||
|
||||
def mark_disconnected(self):
|
||||
assert self.should_disconnect(), 'marked as disconnected when it was borrowed'
|
||||
self._connected = False
|
||||
|
||||
|
||||
# TODO How hard would it be to support both `trio` and `asyncio`?
|
||||
class TelegramBaseClient(abc.ABC):
|
||||
"""
|
||||
|
@ -146,7 +180,8 @@ class TelegramBaseClient(abc.ABC):
|
|||
Defaults to `lang_code`.
|
||||
|
||||
loop (`asyncio.AbstractEventLoop`, optional):
|
||||
Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`
|
||||
Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`.
|
||||
This argument is ignored.
|
||||
|
||||
base_logger (`str` | `logging.Logger`, optional):
|
||||
Base logger name or instance to use.
|
||||
|
@ -193,7 +228,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
"Refer to telethon.rtfd.io for more information.")
|
||||
|
||||
self._use_ipv6 = use_ipv6
|
||||
self._loop = loop or asyncio.get_event_loop()
|
||||
self._loop = asyncio.get_event_loop()
|
||||
|
||||
if isinstance(base_logger, str):
|
||||
base_logger = logging.getLogger(base_logger)
|
||||
|
@ -300,7 +335,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
)
|
||||
|
||||
self._sender = MTProtoSender(
|
||||
self.session.auth_key, self._loop,
|
||||
self.session.auth_key,
|
||||
loggers=self._log,
|
||||
retries=self._connection_retries,
|
||||
delay=self._retry_delay,
|
||||
|
@ -314,19 +349,17 @@ class TelegramBaseClient(abc.ABC):
|
|||
# Remember flood-waited requests to avoid making them again
|
||||
self._flood_waited_requests = {}
|
||||
|
||||
# Cache ``{dc_id: (n, MTProtoSender)}`` for all borrowed senders,
|
||||
# being ``n`` the amount of borrows a given sender has; once ``n``
|
||||
# reaches ``0`` it should be disconnected and removed.
|
||||
# Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders
|
||||
self._borrowed_senders = {}
|
||||
self._borrow_sender_lock = asyncio.Lock(loop=self._loop)
|
||||
self._borrow_sender_lock = asyncio.Lock()
|
||||
|
||||
self._updates_handle = None
|
||||
self._last_request = time.time()
|
||||
self._channel_pts = {}
|
||||
|
||||
if sequential_updates:
|
||||
self._updates_queue = asyncio.Queue(loop=self._loop)
|
||||
self._dispatching_updates_queue = asyncio.Event(loop=self._loop)
|
||||
self._updates_queue = asyncio.Queue()
|
||||
self._dispatching_updates_queue = asyncio.Event()
|
||||
else:
|
||||
# Use a set of pending instead of a queue so we can properly
|
||||
# terminate all pending updates on disconnect.
|
||||
|
@ -346,6 +379,15 @@ class TelegramBaseClient(abc.ABC):
|
|||
# {chat_id: {Conversation}}
|
||||
self._conversations = collections.defaultdict(set)
|
||||
|
||||
# Hack to workaround the fact Telegram may send album updates as
|
||||
# different Updates when being sent from a different data center.
|
||||
# {grouped_id: AlbumHack}
|
||||
#
|
||||
# FIXME: We don't bother cleaning this up because it's not really
|
||||
# worth it, albums are pretty rare and this only holds them
|
||||
# for a second at most.
|
||||
self._albums = {}
|
||||
|
||||
# Default parse mode
|
||||
self._parse_mode = markdown
|
||||
|
||||
|
@ -375,7 +417,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
.. code-block:: python
|
||||
|
||||
# Download media in the background
|
||||
task = client.loop_create_task(message.download_media())
|
||||
task = client.loop.create_task(message.download_media())
|
||||
|
||||
# Do some work
|
||||
...
|
||||
|
@ -440,7 +482,6 @@ class TelegramBaseClient(abc.ABC):
|
|||
self.session.server_address,
|
||||
self.session.port,
|
||||
self.session.dc_id,
|
||||
loop=self._loop,
|
||||
loggers=self._log,
|
||||
proxy=self._proxy
|
||||
)):
|
||||
|
@ -500,13 +541,22 @@ class TelegramBaseClient(abc.ABC):
|
|||
async def _disconnect_coro(self: 'TelegramClient'):
|
||||
await self._disconnect()
|
||||
|
||||
# Also clean-up all exported senders because we're done with them
|
||||
async with self._borrow_sender_lock:
|
||||
for state, sender in self._borrowed_senders.values():
|
||||
if state.should_disconnect():
|
||||
# disconnect should never raise
|
||||
await sender.disconnect()
|
||||
|
||||
self._borrowed_senders.clear()
|
||||
|
||||
# trio's nurseries would handle this for us, but this is asyncio.
|
||||
# All tasks spawned in the background should properly be terminated.
|
||||
if self._dispatching_updates_queue is None and self._updates_queue:
|
||||
for task in self._updates_queue:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.wait(self._updates_queue, loop=self._loop)
|
||||
await asyncio.wait(self._updates_queue)
|
||||
self._updates_queue.clear()
|
||||
|
||||
pts, date = self._state_cache[None]
|
||||
|
@ -589,17 +639,15 @@ class TelegramBaseClient(abc.ABC):
|
|||
#
|
||||
# If one were to do that, Telegram would reset the connection
|
||||
# with no further clues.
|
||||
sender = MTProtoSender(None, self._loop, loggers=self._log)
|
||||
sender = MTProtoSender(None, loggers=self._log)
|
||||
await sender.connect(self._connection(
|
||||
dc.ip_address,
|
||||
dc.port,
|
||||
dc.id,
|
||||
loop=self._loop,
|
||||
loggers=self._log,
|
||||
proxy=self._proxy
|
||||
))
|
||||
self._log[__name__].info('Exporting authorization for data center %s',
|
||||
dc)
|
||||
self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc)
|
||||
auth = await self(functions.auth.ExportAuthorizationRequest(dc_id))
|
||||
req = self._init_with(functions.auth.ImportAuthorizationRequest(
|
||||
id=auth.id, bytes=auth.bytes
|
||||
|
@ -616,24 +664,27 @@ class TelegramBaseClient(abc.ABC):
|
|||
Once its job is over it should be `_return_exported_sender`.
|
||||
"""
|
||||
async with self._borrow_sender_lock:
|
||||
n, sender = self._borrowed_senders.get(dc_id, (0, None))
|
||||
if not sender:
|
||||
self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id)
|
||||
state, sender = self._borrowed_senders.get(dc_id, (None, None))
|
||||
|
||||
if state is None:
|
||||
state = _ExportState()
|
||||
sender = await self._create_exported_sender(dc_id)
|
||||
sender.dc_id = dc_id
|
||||
elif not n:
|
||||
self._borrowed_senders[dc_id] = (state, sender)
|
||||
|
||||
elif state.need_connect():
|
||||
dc = await self._get_dc(dc_id)
|
||||
await sender.connect(self._connection(
|
||||
dc.ip_address,
|
||||
dc.port,
|
||||
dc.id,
|
||||
loop=self._loop,
|
||||
loggers=self._log,
|
||||
proxy=self._proxy
|
||||
))
|
||||
|
||||
self._borrowed_senders[dc_id] = (n + 1, sender)
|
||||
|
||||
return sender
|
||||
state.add_borrow()
|
||||
return sender
|
||||
|
||||
async def _return_exported_sender(self: 'TelegramClient', sender):
|
||||
"""
|
||||
|
@ -641,14 +692,23 @@ class TelegramBaseClient(abc.ABC):
|
|||
been returned, the sender is cleanly disconnected.
|
||||
"""
|
||||
async with self._borrow_sender_lock:
|
||||
dc_id = sender.dc_id
|
||||
n, _ = self._borrowed_senders[dc_id]
|
||||
n -= 1
|
||||
self._borrowed_senders[dc_id] = (n, sender)
|
||||
if not n:
|
||||
self._log[__name__].info(
|
||||
'Disconnecting borrowed sender for DC %d', dc_id)
|
||||
await sender.disconnect()
|
||||
self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id)
|
||||
state, _ = self._borrowed_senders[sender.dc_id]
|
||||
state.add_return()
|
||||
|
||||
async def _clean_exported_senders(self: 'TelegramClient'):
|
||||
"""
|
||||
Cleans-up all unused exported senders by disconnecting them.
|
||||
"""
|
||||
async with self._borrow_sender_lock:
|
||||
for dc_id, (state, sender) in self._borrowed_senders.items():
|
||||
if state.should_disconnect():
|
||||
self._log[__name__].info(
|
||||
'Disconnecting borrowed sender for DC %d', dc_id)
|
||||
|
||||
# Disconnect should never raise
|
||||
await sender.disconnect()
|
||||
state.mark_disconnected()
|
||||
|
||||
async def _get_cdn_client(self: 'TelegramClient', cdn_redirect):
|
||||
"""Similar to ._borrow_exported_client, but for CDNs"""
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import itertools
|
||||
import random
|
||||
import time
|
||||
|
@ -325,7 +326,7 @@ class UpdateMethods:
|
|||
while self.is_connected():
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self.disconnected, timeout=60, loop=self._loop
|
||||
self.disconnected, timeout=60
|
||||
)
|
||||
continue # We actually just want to act upon timeout
|
||||
except asyncio.TimeoutError:
|
||||
|
@ -335,6 +336,9 @@ class UpdateMethods:
|
|||
except Exception:
|
||||
continue # Any disconnected exception should be ignored
|
||||
|
||||
# Check if we have any exported senders to clean-up periodically
|
||||
await self._clean_exported_senders()
|
||||
|
||||
# Don't bother sending pings until the low-level connection is
|
||||
# ready, otherwise a lot of pings will be batched to be sent upon
|
||||
# reconnect, when we really don't care about that.
|
||||
|
@ -388,11 +392,19 @@ class UpdateMethods:
|
|||
await self._get_difference(update, channel_id, pts_date)
|
||||
except OSError:
|
||||
pass # We were disconnected, that's okay
|
||||
except errors.RPCError:
|
||||
# There's a high chance the request fails because we lack
|
||||
# the channel. Because these "happen sporadically" (#1428)
|
||||
# we should be okay (no flood waits) even if more occur.
|
||||
pass
|
||||
|
||||
if not self._self_input_peer:
|
||||
# Some updates require our own ID, so we must make sure
|
||||
# that the event builder has offline access to it. Calling
|
||||
# `get_me()` will cache it under `self._self_input_peer`.
|
||||
#
|
||||
# It will return `None` if we haven't logged in yet which is
|
||||
# fine, we will just retry next time anyway.
|
||||
await self.get_me(input_peer=True)
|
||||
|
||||
built = EventBuilderDict(self, update, others)
|
||||
|
@ -421,7 +433,50 @@ class UpdateMethods:
|
|||
if not builder.resolved:
|
||||
await builder.resolve(self)
|
||||
|
||||
if not builder.filter(event):
|
||||
filter = builder.filter(event)
|
||||
if inspect.isawaitable(filter):
|
||||
filter = await filter
|
||||
if not filter:
|
||||
continue
|
||||
|
||||
try:
|
||||
await callback(event)
|
||||
except errors.AlreadyInConversationError:
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].debug(
|
||||
'Event handler "%s" already has an open conversation, '
|
||||
'ignoring new one', name)
|
||||
except events.StopPropagation:
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].debug(
|
||||
'Event handler "%s" stopped chain of propagation '
|
||||
'for event %s.', name, type(event).__name__
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
if not isinstance(e, asyncio.CancelledError) or self.is_connected():
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].exception('Unhandled exception on %s',
|
||||
name)
|
||||
|
||||
async def _dispatch_event(self: 'TelegramClient', event):
|
||||
"""
|
||||
Dispatches a single, out-of-order event. Used by `AlbumHack`.
|
||||
"""
|
||||
# We're duplicating a most logic from `_dispatch_update`, but all in
|
||||
# the name of speed; we don't want to make it worse for all updates
|
||||
# just because albums may need it.
|
||||
for builder, callback in self._event_builders:
|
||||
if not isinstance(event, builder.Event):
|
||||
continue
|
||||
|
||||
if not builder.resolved:
|
||||
await builder.resolve(self)
|
||||
|
||||
filter = builder.filter(event)
|
||||
if inspect.isawaitable(filter):
|
||||
filter = await filter
|
||||
if not filter:
|
||||
continue
|
||||
|
||||
try:
|
||||
|
@ -559,8 +614,15 @@ class EventBuilderDict:
|
|||
try:
|
||||
return self.__dict__[builder]
|
||||
except KeyError:
|
||||
# Updates may arrive before login (like updateLoginToken) and we
|
||||
# won't have our self ID yet (anyway only new messages need it).
|
||||
self_id = (
|
||||
self.client._self_input_peer.user_id
|
||||
if self.client._self_input_peer
|
||||
else None
|
||||
)
|
||||
event = self.__dict__[builder] = builder.build(
|
||||
self.update, self.others, self.client._self_input_peer.user_id)
|
||||
self.update, self.others, self_id)
|
||||
|
||||
if isinstance(event, EventCommon):
|
||||
event.original_update = self.update
|
||||
|
|
|
@ -5,9 +5,10 @@ import os
|
|||
import pathlib
|
||||
import re
|
||||
import typing
|
||||
import inspect
|
||||
from io import BytesIO
|
||||
|
||||
from ..crypto import AES
|
||||
|
||||
from .. import utils, helpers, hints
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
|
@ -93,6 +94,7 @@ class UploadMethods:
|
|||
*,
|
||||
caption: typing.Union[str, typing.Sequence[str]] = None,
|
||||
force_document: bool = False,
|
||||
file_size: int = None,
|
||||
clear_draft: bool = False,
|
||||
progress_callback: 'hints.ProgressCallback' = None,
|
||||
reply_to: 'hints.MessageIDLike' = None,
|
||||
|
@ -153,6 +155,10 @@ class UploadMethods:
|
|||
|
||||
* A handle to an uploaded file (from `upload_file`).
|
||||
|
||||
* A :tl:`InputMedia` instance. For example, if you want to
|
||||
send a dice use :tl:`InputMediaDice`, or if you want to
|
||||
send a contact use :tl:`InputMediaContact`.
|
||||
|
||||
To send an album, you should provide a list in this parameter.
|
||||
|
||||
If a list or similar is provided, the files in it will be
|
||||
|
@ -169,6 +175,13 @@ class UploadMethods:
|
|||
the extension of an image file or a video file, it will be
|
||||
sent as such. Otherwise always as a document.
|
||||
|
||||
file_size (`int`, optional):
|
||||
The size of the file to be uploaded if it needs to be uploaded,
|
||||
which will be determined automatically if not specified.
|
||||
|
||||
If the file size can't be determined beforehand, the entire
|
||||
file will be read in-memory to find out how large it is.
|
||||
|
||||
clear_draft (`bool`, optional):
|
||||
Whether the existing draft should be cleared or not.
|
||||
|
||||
|
@ -261,6 +274,26 @@ class UploadMethods:
|
|||
'/my/photos/holiday2.jpg',
|
||||
'/my/drawings/portrait.png'
|
||||
])
|
||||
|
||||
# Printing upload progress
|
||||
def callback(current, total):
|
||||
print('Uploaded', current, 'out of', total,
|
||||
'bytes: {:.2%}'.format(current / total))
|
||||
|
||||
await client.send_file(chat, file, progress_callback=callback)
|
||||
|
||||
# Dices, including dart and other future emoji
|
||||
from telethon.tl import types
|
||||
await client.send_file(chat, types.InputMediaDice(''))
|
||||
await client.send_file(chat, types.InputMediaDice('🎯'))
|
||||
|
||||
# Contacts
|
||||
await client.send_file(chat, types.InputMediaContact(
|
||||
phone_number='+34 123 456 789',
|
||||
first_name='Example',
|
||||
last_name='',
|
||||
vcard=''
|
||||
))
|
||||
"""
|
||||
# TODO Properly implement allow_cache to reuse the sha256 of the file
|
||||
# i.e. `None` was used
|
||||
|
@ -332,6 +365,7 @@ class UploadMethods:
|
|||
|
||||
file_handle, media, image = await self._file_to_media(
|
||||
file, force_document=force_document,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback,
|
||||
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
|
||||
voice_note=voice_note, video_note=video_note,
|
||||
|
@ -348,10 +382,7 @@ class UploadMethods:
|
|||
entities=msg_entities, reply_markup=markup, silent=silent,
|
||||
schedule_date=schedule, clear_draft=clear_draft
|
||||
)
|
||||
msg = self._get_response_message(request, await self(request), entity)
|
||||
await self._cache_media(msg, file, file_handle, image=image)
|
||||
|
||||
return msg
|
||||
return self._get_response_message(request, await self(request), entity)
|
||||
|
||||
async def _send_album(self: 'TelegramClient', entity, files, caption='',
|
||||
progress_callback=None, reply_to=None,
|
||||
|
@ -390,16 +421,12 @@ class UploadMethods:
|
|||
r = await self(functions.messages.UploadMediaRequest(
|
||||
entity, media=fm
|
||||
))
|
||||
self.session.cache_file(
|
||||
fh.md5, fh.size, utils.get_input_photo(r.photo))
|
||||
|
||||
fm = utils.get_input_media(r.photo)
|
||||
elif isinstance(fm, types.InputMediaUploadedDocument):
|
||||
r = await self(functions.messages.UploadMediaRequest(
|
||||
entity, media=fm
|
||||
))
|
||||
self.session.cache_file(
|
||||
fh.md5, fh.size, utils.get_input_document(r.document))
|
||||
|
||||
fm = utils.get_input_media(
|
||||
r.document, supports_streaming=supports_streaming)
|
||||
|
@ -430,12 +457,19 @@ class UploadMethods:
|
|||
file: 'hints.FileLike',
|
||||
*,
|
||||
part_size_kb: float = None,
|
||||
file_size: int = None,
|
||||
file_name: str = None,
|
||||
use_cache: type = None,
|
||||
key: bytes = None,
|
||||
iv: bytes = None,
|
||||
progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile':
|
||||
"""
|
||||
Uploads a file to Telegram's servers, without sending it.
|
||||
|
||||
.. note::
|
||||
|
||||
Generally, you want to use `send_file` instead.
|
||||
|
||||
This method returns a handle (an instance of :tl:`InputFile` or
|
||||
:tl:`InputFileBig`, as required) which can be later used before
|
||||
it expires (they are usable during less than a day).
|
||||
|
@ -455,6 +489,13 @@ class UploadMethods:
|
|||
Chunk size when uploading files. The larger, the less
|
||||
requests will be made (up to 512KB maximum).
|
||||
|
||||
file_size (`int`, optional):
|
||||
The size of the file to be uploaded, which will be determined
|
||||
automatically if not specified.
|
||||
|
||||
If the file size can't be determined beforehand, the entire
|
||||
file will be read in-memory to find out how large it is.
|
||||
|
||||
file_name (`str`, optional):
|
||||
The file name which will be used on the resulting InputFile.
|
||||
If not specified, the name will be taken from the ``file``
|
||||
|
@ -465,6 +506,12 @@ class UploadMethods:
|
|||
backward-compatibility (and it may get its use back in
|
||||
the future).
|
||||
|
||||
key ('bytes', optional):
|
||||
In case of an encrypted upload (secret chats) a key is supplied
|
||||
|
||||
iv ('bytes', optional):
|
||||
In case of an encrypted upload (secret chats) an iv is supplied
|
||||
|
||||
progress_callback (`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
``(sent bytes, total)``.
|
||||
|
@ -496,34 +543,42 @@ class UploadMethods:
|
|||
if not file_name and getattr(file, 'name', None):
|
||||
file_name = file.name
|
||||
|
||||
if isinstance(file, str):
|
||||
if file_size is not None:
|
||||
pass # do nothing as it's already kwown
|
||||
elif isinstance(file, str):
|
||||
file_size = os.path.getsize(file)
|
||||
stream = open(file, 'rb')
|
||||
close_stream = True
|
||||
elif isinstance(file, bytes):
|
||||
file_size = len(file)
|
||||
stream = io.BytesIO(file)
|
||||
close_stream = True
|
||||
else:
|
||||
# `aiofiles` shouldn't base `IOBase` because they change the
|
||||
# methods' definition. `seekable` would be `async` but since
|
||||
# we won't get to check that, there's no need to maybe-await.
|
||||
if isinstance(file, io.IOBase) and file.seekable():
|
||||
pos = file.tell()
|
||||
if not callable(getattr(file, 'read', None)):
|
||||
raise TypeError('file description should have a `read` method')
|
||||
|
||||
if callable(getattr(file, 'seekable', None)):
|
||||
seekable = await helpers._maybe_await(file.seekable())
|
||||
else:
|
||||
pos = None
|
||||
seekable = False
|
||||
|
||||
# TODO Don't load the entire file in memory always
|
||||
data = file.read()
|
||||
if inspect.isawaitable(data):
|
||||
data = await data
|
||||
if seekable:
|
||||
pos = await helpers._maybe_await(file.tell())
|
||||
await helpers._maybe_await(file.seek(0, os.SEEK_END))
|
||||
file_size = await helpers._maybe_await(file.tell())
|
||||
await helpers._maybe_await(file.seek(pos, os.SEEK_SET))
|
||||
|
||||
if pos is not None:
|
||||
file.seek(pos)
|
||||
stream = file
|
||||
close_stream = False
|
||||
else:
|
||||
self._log[__name__].warning(
|
||||
'Could not determine file size beforehand so the entire '
|
||||
'file will be read in-memory')
|
||||
|
||||
if not isinstance(data, bytes):
|
||||
raise TypeError(
|
||||
'file descriptor returned {}, not bytes (you must '
|
||||
'open the file in bytes mode)'.format(type(data)))
|
||||
|
||||
file = data
|
||||
file_size = len(file)
|
||||
data = await helpers._maybe_await(file.read())
|
||||
stream = io.BytesIO(data)
|
||||
close_stream = True
|
||||
file_size = len(data)
|
||||
|
||||
# File will now either be a string or bytes
|
||||
if not part_size_kb:
|
||||
|
@ -553,31 +608,46 @@ class UploadMethods:
|
|||
|
||||
# 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
|
||||
is_big = 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)
|
||||
|
||||
part_count = (file_size + part_size - 1) // part_size
|
||||
self._log[__name__].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:
|
||||
pos = 0
|
||||
try:
|
||||
for part_index in range(part_count):
|
||||
# Read the file by in chunks of size part_size
|
||||
part = stream.read(part_size)
|
||||
part = await helpers._maybe_await(stream.read(part_size))
|
||||
|
||||
if not isinstance(part, bytes):
|
||||
raise TypeError(
|
||||
'file descriptor returned {}, not bytes (you must '
|
||||
'open the file in bytes mode)'.format(type(part)))
|
||||
|
||||
# `file_size` could be wrong in which case `part` may not be
|
||||
# `part_size` before reaching the end.
|
||||
if len(part) != part_size and part_index < part_count - 1:
|
||||
raise ValueError(
|
||||
'read less than {} before reaching the end; either '
|
||||
'`file_size` or `read` are wrong'.format(part_size))
|
||||
|
||||
pos += len(part)
|
||||
|
||||
if not is_big:
|
||||
# Bit odd that MD5 is only needed for small files and not
|
||||
# big ones with more chance for corruption, but that's
|
||||
# what Telegram wants.
|
||||
hash_md5.update(part)
|
||||
|
||||
# Encryption part if needed
|
||||
if key and iv:
|
||||
part = AES.encrypt_ige(part, key, iv)
|
||||
|
||||
# The SavePartRequest is different depending on whether
|
||||
# the file is too large or not (over or less than 10MB)
|
||||
if is_large:
|
||||
if is_big:
|
||||
request = functions.upload.SaveBigFilePartRequest(
|
||||
file_id, part_index, part_count, part)
|
||||
else:
|
||||
|
@ -589,14 +659,15 @@ class UploadMethods:
|
|||
self._log[__name__].debug('Uploaded %d/%d',
|
||||
part_index + 1, part_count)
|
||||
if progress_callback:
|
||||
r = progress_callback(stream.tell(), file_size)
|
||||
if inspect.isawaitable(r):
|
||||
await r
|
||||
await helpers._maybe_await(progress_callback(pos, file_size))
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Failed to upload file part {}.'.format(part_index))
|
||||
finally:
|
||||
if close_stream:
|
||||
await helpers._maybe_await(stream.close())
|
||||
|
||||
if is_large:
|
||||
if is_big:
|
||||
return types.InputFileBig(file_id, part_count, file_name)
|
||||
else:
|
||||
return custom.InputSizedFile(
|
||||
|
@ -606,7 +677,7 @@ class UploadMethods:
|
|||
# endregion
|
||||
|
||||
async def _file_to_media(
|
||||
self, file, force_document=False,
|
||||
self, file, force_document=False, file_size=None,
|
||||
progress_callback=None, attributes=None, thumb=None,
|
||||
allow_cache=True, voice_note=False, video_note=False,
|
||||
supports_streaming=False, mime_type=None, as_image=None):
|
||||
|
@ -616,12 +687,14 @@ class UploadMethods:
|
|||
if isinstance(file, pathlib.Path):
|
||||
file = str(file.absolute())
|
||||
|
||||
is_image = utils.is_image(file)
|
||||
if as_image is None:
|
||||
as_image = utils.is_image(file) and not force_document
|
||||
as_image = is_image and not force_document
|
||||
|
||||
# `aiofiles` do not base `io.IOBase` but do have `read`, so we
|
||||
# just check for the read attribute to see if it's file-like.
|
||||
if not isinstance(file, (str, bytes)) and not hasattr(file, 'read'):
|
||||
if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig))\
|
||||
and not hasattr(file, 'read'):
|
||||
# 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.
|
||||
|
@ -644,16 +717,18 @@ class UploadMethods:
|
|||
|
||||
media = None
|
||||
file_handle = None
|
||||
if not isinstance(file, str) or os.path.isfile(file):
|
||||
|
||||
if isinstance(file, (types.InputFile, types.InputFileBig)):
|
||||
file_handle = file
|
||||
elif not isinstance(file, str) or os.path.isfile(file):
|
||||
file_handle = await self.upload_file(
|
||||
_resize_photo_if_needed(file, as_image),
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
elif re.match('https?://', file):
|
||||
if as_image:
|
||||
media = types.InputMediaPhotoExternal(file)
|
||||
elif not force_document and utils.is_gif(file):
|
||||
media = types.InputMediaGifExternal(file, '')
|
||||
else:
|
||||
media = types.InputMediaDocumentExternal(file)
|
||||
else:
|
||||
|
@ -675,36 +750,26 @@ class UploadMethods:
|
|||
file,
|
||||
mime_type=mime_type,
|
||||
attributes=attributes,
|
||||
force_document=force_document,
|
||||
force_document=force_document and not is_image,
|
||||
voice_note=voice_note,
|
||||
video_note=video_note,
|
||||
supports_streaming=supports_streaming
|
||||
)
|
||||
|
||||
input_kw = {}
|
||||
if thumb:
|
||||
if not thumb:
|
||||
thumb = None
|
||||
else:
|
||||
if isinstance(thumb, pathlib.Path):
|
||||
thumb = str(thumb.absolute())
|
||||
input_kw['thumb'] = await self.upload_file(thumb)
|
||||
thumb = await self.upload_file(thumb, file_size=file_size)
|
||||
|
||||
media = types.InputMediaUploadedDocument(
|
||||
file=file_handle,
|
||||
mime_type=mime_type,
|
||||
attributes=attributes,
|
||||
**input_kw
|
||||
thumb=thumb,
|
||||
force_file=force_document and not is_image
|
||||
)
|
||||
return file_handle, media, as_image
|
||||
|
||||
async def _cache_media(self: 'TelegramClient', msg, file, file_handle, image):
|
||||
if file and msg and isinstance(file_handle,
|
||||
custom.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 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)
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -27,6 +27,9 @@ def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta):
|
|||
|
||||
class UserMethods:
|
||||
async def __call__(self: 'TelegramClient', request, ordered=False):
|
||||
return await self._call(self._sender, request, ordered=ordered)
|
||||
|
||||
async def _call(self: 'TelegramClient', sender, request, ordered=False):
|
||||
requests = (request if utils.is_list_like(request) else (request,))
|
||||
for r in requests:
|
||||
if not isinstance(r, TLRequest):
|
||||
|
@ -41,7 +44,7 @@ class UserMethods:
|
|||
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
|
||||
elif diff <= self.flood_sleep_threshold:
|
||||
self._log[__name__].info(*_fmt_flood(diff, r, early=True))
|
||||
await asyncio.sleep(diff, loop=self._loop)
|
||||
await asyncio.sleep(diff)
|
||||
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
|
||||
else:
|
||||
raise errors.FloodWaitError(request=r, capture=diff)
|
||||
|
@ -50,7 +53,7 @@ class UserMethods:
|
|||
self._last_request = time.time()
|
||||
for attempt in retry_range(self._request_retries):
|
||||
try:
|
||||
future = self._sender.send(request, ordered=ordered)
|
||||
future = sender.send(request, ordered=ordered)
|
||||
if isinstance(future, list):
|
||||
results = []
|
||||
exceptions = []
|
||||
|
@ -76,7 +79,8 @@ class UserMethods:
|
|||
self._entity_cache.add(result)
|
||||
return result
|
||||
except (errors.ServerError, errors.RpcCallFailError,
|
||||
errors.RpcMcgetFailError) as e:
|
||||
errors.RpcMcgetFailError, errors.InterdcCallErrorError,
|
||||
errors.InterdcCallRichErrorError) as e:
|
||||
self._log[__name__].warning(
|
||||
'Telegram is having internal issues %s: %s',
|
||||
e.__class__.__name__, e)
|
||||
|
@ -89,9 +93,14 @@ class UserMethods:
|
|||
self._flood_waited_requests\
|
||||
[request.CONSTRUCTOR_ID] = time.time() + e.seconds
|
||||
|
||||
# In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
|
||||
# such a short amount will cause retries very fast leading to issues.
|
||||
if e.seconds == 0:
|
||||
e.seconds = 1
|
||||
|
||||
if e.seconds <= self.flood_sleep_threshold:
|
||||
self._log[__name__].info(*_fmt_flood(e.seconds, request))
|
||||
await asyncio.sleep(e.seconds, loop=self._loop)
|
||||
await asyncio.sleep(e.seconds)
|
||||
else:
|
||||
raise
|
||||
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import asyncio
|
||||
import time
|
||||
import weakref
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils
|
||||
|
@ -14,6 +16,54 @@ _IGNORE_MAX_AGE = 5 # seconds
|
|||
_IGNORE_DICT = {}
|
||||
|
||||
|
||||
_HACK_DELAY = 0.5
|
||||
|
||||
|
||||
class AlbumHack:
|
||||
"""
|
||||
When receiving an album from a different data-center, they will come in
|
||||
separate `Updates`, so we need to temporarily remember them for a while
|
||||
and only after produce the event.
|
||||
|
||||
Of course events are not designed for this kind of wizardy, so this is
|
||||
a dirty hack that gets the job done.
|
||||
|
||||
When cleaning up the code base we may want to figure out a better way
|
||||
to do this, or just leave the album problem to the users; the update
|
||||
handling code is bad enough as it is.
|
||||
"""
|
||||
def __init__(self, client, event):
|
||||
# It's probably silly to use a weakref here because this object is
|
||||
# very short-lived but might as well try to do "the right thing".
|
||||
self._client = weakref.ref(client)
|
||||
self._event = event # parent event
|
||||
self._due = client.loop.time() + _HACK_DELAY
|
||||
|
||||
client.loop.create_task(self.deliver_event())
|
||||
|
||||
def extend(self, messages):
|
||||
client = self._client()
|
||||
if client: # weakref may be dead
|
||||
self._event.messages.extend(messages)
|
||||
self._due = client.loop.time() + _HACK_DELAY
|
||||
|
||||
async def deliver_event(self):
|
||||
while True:
|
||||
client = self._client()
|
||||
if client is None:
|
||||
return # weakref is dead, nothing to deliver
|
||||
|
||||
diff = self._due - client.loop.time()
|
||||
if diff <= 0:
|
||||
# We've hit our due time, deliver event. It won't respect
|
||||
# sequential updates but fixing that would just worsen this.
|
||||
await client._dispatch_event(self._event)
|
||||
return
|
||||
|
||||
del client # Clear ref and sleep until our due time
|
||||
await asyncio.sleep(diff)
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class Album(EventBuilder):
|
||||
"""
|
||||
|
@ -66,6 +116,7 @@ class Album(EventBuilder):
|
|||
return
|
||||
|
||||
# Check if the ignore list is too big, and if it is clean it
|
||||
# TODO time could technically go backwards; time is not monotonic
|
||||
now = time.time()
|
||||
if len(_IGNORE_DICT) > _IGNORE_MAX_SIZE:
|
||||
for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]:
|
||||
|
@ -84,6 +135,11 @@ class Album(EventBuilder):
|
|||
and u.message.grouped_id == group)
|
||||
])
|
||||
|
||||
def filter(self, event):
|
||||
# Albums with less than two messages require a few hacks to work.
|
||||
if len(event.messages) > 1:
|
||||
return super().filter(event)
|
||||
|
||||
class Event(EventCommon, SenderGetter):
|
||||
"""
|
||||
Represents the event of a new album.
|
||||
|
@ -115,6 +171,14 @@ class Album(EventBuilder):
|
|||
for msg in self.messages:
|
||||
msg._finish_init(client, self._entities, None)
|
||||
|
||||
if len(self.messages) == 1:
|
||||
# This will require hacks to be a proper album event
|
||||
hack = client._albums.get(self.grouped_id)
|
||||
if hack is None:
|
||||
client._albums[self.grouped_id] = AlbumHack(client, self)
|
||||
else:
|
||||
hack.extend(self.messages)
|
||||
|
||||
@property
|
||||
def grouped_id(self):
|
||||
"""
|
||||
|
@ -259,7 +323,7 @@ class Album(EventBuilder):
|
|||
`telethon.client.messages.MessageMethods.pin_message`
|
||||
with both ``entity`` and ``message`` already set.
|
||||
"""
|
||||
await self.messages[0].pin(notify=notify)
|
||||
return await self.messages[0].pin(notify=notify)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
|
|
|
@ -51,7 +51,7 @@ class CallbackQuery(EventBuilder):
|
|||
# Send a message with buttons users can click
|
||||
async def main():
|
||||
await client.send_message(user, 'Yes or no?', buttons=[
|
||||
Button.inline('Yes!', b'yes')
|
||||
Button.inline('Yes!', b'yes'),
|
||||
Button.inline('Nope', b'no')
|
||||
])
|
||||
"""
|
||||
|
@ -118,8 +118,10 @@ class CallbackQuery(EventBuilder):
|
|||
elif event.query.data != self.match:
|
||||
return
|
||||
|
||||
if not self.func or self.func(event):
|
||||
return event
|
||||
if self.func:
|
||||
# Return the result of func directly as it may need to be awaited
|
||||
return self.func(event)
|
||||
return True
|
||||
|
||||
class Event(EventCommon, SenderGetter):
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils
|
||||
from ..tl import types, functions
|
||||
from ..tl import types
|
||||
|
||||
|
||||
@name_inner_event
|
||||
|
@ -38,6 +38,10 @@ class ChatAction(EventBuilder):
|
|||
return cls.Event(types.PeerChannel(update.channel_id),
|
||||
unpin=True)
|
||||
|
||||
elif isinstance(update, types.UpdateChatPinnedMessage) and update.id == 0:
|
||||
return cls.Event(types.PeerChat(update.chat_id),
|
||||
unpin=True)
|
||||
|
||||
elif isinstance(update, types.UpdateChatParticipantAdd):
|
||||
return cls.Event(types.PeerChat(update.chat_id),
|
||||
added_by=update.inviter_id or True,
|
||||
|
@ -104,8 +108,9 @@ class ChatAction(EventBuilder):
|
|||
return cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_photo=True)
|
||||
elif isinstance(action, types.MessageActionPinMessage):
|
||||
# Telegram always sends this service message for new pins
|
||||
elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to_msg_id:
|
||||
# Seems to not be reliable on unpins, but when pinning
|
||||
# we prefer this because we know who caused it.
|
||||
return cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_pin=msg.reply_to_msg_id)
|
||||
|
@ -256,17 +261,8 @@ class ChatAction(EventBuilder):
|
|||
|
||||
if isinstance(self._pinned_message, int)\
|
||||
and await self.get_input_chat():
|
||||
r = await self._client(functions.channels.GetMessagesRequest(
|
||||
self._input_chat, [self._pinned_message]
|
||||
))
|
||||
try:
|
||||
self._pinned_message = next(
|
||||
x for x in r.messages
|
||||
if isinstance(x, types.Message)
|
||||
and x.id == self._pinned_message
|
||||
)
|
||||
except StopIteration:
|
||||
pass
|
||||
self._pinned_message = await self._client.get_messages(
|
||||
self._input_chat, ids=self._pinned_message)
|
||||
|
||||
if isinstance(self._pinned_message, types.Message):
|
||||
return self._pinned_message
|
||||
|
@ -316,7 +312,7 @@ class ChatAction(EventBuilder):
|
|||
@property
|
||||
def user(self):
|
||||
"""
|
||||
The first user that takes part in this action (e.g. joined).
|
||||
The first user that takes part in this action. For example, who joined.
|
||||
|
||||
Might be `None` if the information can't be retrieved or
|
||||
there is no user taking part.
|
||||
|
@ -357,7 +353,7 @@ class ChatAction(EventBuilder):
|
|||
@property
|
||||
def users(self):
|
||||
"""
|
||||
A list of users that take part in this action (e.g. joined).
|
||||
A list of users that take part in this action. For example, who joined.
|
||||
|
||||
Might be empty if the information can't be retrieved or there
|
||||
are no users taking part.
|
||||
|
@ -381,7 +377,8 @@ class ChatAction(EventBuilder):
|
|||
if not self._user_ids:
|
||||
return []
|
||||
|
||||
if self._users is None or len(self._users) != len(self._user_ids):
|
||||
# Note: we access the property first so that it fills if needed
|
||||
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
|
||||
await self.action_message._reload_message()
|
||||
self._users = [
|
||||
u for u in self.action_message.action_entities
|
||||
|
@ -397,19 +394,31 @@ class ChatAction(EventBuilder):
|
|||
if self._input_users is None and self._user_ids:
|
||||
self._input_users = []
|
||||
for user_id in self._user_ids:
|
||||
# First try to get it from our entities
|
||||
try:
|
||||
self._input_users.append(utils.get_input_peer(self._entities[user_id]))
|
||||
continue
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
# If missing, try from the entity cache
|
||||
try:
|
||||
self._input_users.append(self._client._entity_cache[user_id])
|
||||
continue
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return self._input_users or []
|
||||
|
||||
async def get_input_users(self):
|
||||
"""
|
||||
Returns `input_users` but will make an API call if necessary.
|
||||
"""
|
||||
self._input_users = None
|
||||
if self._input_users is None:
|
||||
await self.action_message._reload_message()
|
||||
if not self._user_ids:
|
||||
return []
|
||||
|
||||
# Note: we access the property first so that it fills if needed
|
||||
if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:
|
||||
self._input_users = [
|
||||
utils.get_input_peer(u)
|
||||
for u in self.action_message.action_entities
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import abc
|
||||
import asyncio
|
||||
import itertools
|
||||
import warnings
|
||||
|
||||
from .. import utils
|
||||
from ..tl import TLObject, types, functions
|
||||
from ..tl import TLObject, types
|
||||
from ..tl.custom.chatgetter import ChatGetter
|
||||
|
||||
|
||||
|
@ -55,7 +54,7 @@ class EventBuilder(abc.ABC):
|
|||
which will be ignored if ``blacklist_chats=True``.
|
||||
|
||||
func (`callable`, optional):
|
||||
A callable function that should accept the event as input
|
||||
A callable (async or not) function that should accept the event as input
|
||||
parameter, and return a value indicating whether the event
|
||||
should be dispatched or not (any truthy value will do, it
|
||||
does not need to be a `bool`). It works like a custom filter:
|
||||
|
@ -93,7 +92,7 @@ class EventBuilder(abc.ABC):
|
|||
return
|
||||
|
||||
if not self._resolve_lock:
|
||||
self._resolve_lock = asyncio.Lock(loop=client.loop)
|
||||
self._resolve_lock = asyncio.Lock()
|
||||
|
||||
async with self._resolve_lock:
|
||||
if not self.resolved:
|
||||
|
@ -105,13 +104,13 @@ class EventBuilder(abc.ABC):
|
|||
|
||||
def filter(self, event):
|
||||
"""
|
||||
If the ID of ``event._chat_peer`` isn't in the chats set (or it is
|
||||
but the set is a blacklist) returns `None`, otherwise the event.
|
||||
Returns a truthy value if the event passed the filter and should be
|
||||
used, or falsy otherwise. The return value may need to be awaited.
|
||||
|
||||
The events must have been resolved before this can be called.
|
||||
"""
|
||||
if not self.resolved:
|
||||
return None
|
||||
return
|
||||
|
||||
if self.chats is not None:
|
||||
# Note: the `event.chat_id` property checks if it's `None` for us
|
||||
|
@ -119,10 +118,13 @@ class EventBuilder(abc.ABC):
|
|||
if inside == self.blacklist_chats:
|
||||
# If this chat matches but it's a blacklist ignore.
|
||||
# If it doesn't match but it's a whitelist ignore.
|
||||
return None
|
||||
return
|
||||
|
||||
if not self.func or self.func(event):
|
||||
return event
|
||||
if not self.func:
|
||||
return True
|
||||
|
||||
# Return the result of func directly as it may need to be awaited
|
||||
return self.func(event)
|
||||
|
||||
|
||||
class EventCommon(ChatGetter, abc.ABC):
|
||||
|
|
|
@ -79,11 +79,11 @@ class InlineQuery(EventBuilder):
|
|||
Represents the event of a new callback query.
|
||||
|
||||
Members:
|
||||
query (:tl:`UpdateBotCallbackQuery`):
|
||||
The original :tl:`UpdateBotCallbackQuery`.
|
||||
query (:tl:`UpdateBotInlineQuery`):
|
||||
The original :tl:`UpdateBotInlineQuery`.
|
||||
|
||||
Make sure to access the `text` of the query if
|
||||
that's what you want instead working with this.
|
||||
Make sure to access the `text` property of the query if
|
||||
you want the text rather than the actual query object.
|
||||
|
||||
pattern_match (`obj`, optional):
|
||||
The resulting object from calling the passed ``pattern``
|
||||
|
@ -206,10 +206,9 @@ class InlineQuery(EventBuilder):
|
|||
return
|
||||
|
||||
if results:
|
||||
futures = [self._as_future(x, self._client.loop)
|
||||
for x in results]
|
||||
futures = [self._as_future(x) for x in results]
|
||||
|
||||
await asyncio.wait(futures, loop=self._client.loop)
|
||||
await asyncio.wait(futures)
|
||||
|
||||
# All futures will be in the `done` *set* that `wait` returns.
|
||||
#
|
||||
|
@ -236,10 +235,10 @@ class InlineQuery(EventBuilder):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def _as_future(obj, loop):
|
||||
def _as_future(obj):
|
||||
if inspect.isawaitable(obj):
|
||||
return asyncio.ensure_future(obj, loop=loop)
|
||||
return asyncio.ensure_future(obj)
|
||||
|
||||
f = loop.create_future()
|
||||
f = asyncio.get_event_loop().create_future()
|
||||
f.set_result(obj)
|
||||
return f
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import asyncio
|
||||
import re
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set
|
||||
|
|
|
@ -29,12 +29,12 @@ class Raw(EventBuilder):
|
|||
self.types = None
|
||||
elif not utils.is_list_like(types):
|
||||
if not isinstance(types, type):
|
||||
raise TypeError('Invalid input type given %s', types)
|
||||
raise TypeError('Invalid input type given: {}'.format(types))
|
||||
|
||||
self.types = types
|
||||
else:
|
||||
if not all(isinstance(x, type) for x in types):
|
||||
raise TypeError('Invalid input types given %s', types)
|
||||
raise TypeError('Invalid input types given: {}'.format(types))
|
||||
|
||||
self.types = tuple(types)
|
||||
|
||||
|
@ -46,6 +46,8 @@ class Raw(EventBuilder):
|
|||
return update
|
||||
|
||||
def filter(self, event):
|
||||
if ((not self.types or isinstance(event, self.types))
|
||||
and (not self.func or self.func(event))):
|
||||
if not self.types or isinstance(event, self.types):
|
||||
if self.func:
|
||||
# Return the result of func directly as it may need to be awaited
|
||||
return self.func(event)
|
||||
return event
|
||||
|
|
|
@ -22,11 +22,10 @@ class MessagePacker:
|
|||
point where outgoing requests are put, and where ready-messages are get.
|
||||
"""
|
||||
|
||||
def __init__(self, state, loop, loggers):
|
||||
def __init__(self, state, loggers):
|
||||
self._state = state
|
||||
self._loop = loop
|
||||
self._deque = collections.deque()
|
||||
self._ready = asyncio.Event(loop=loop)
|
||||
self._ready = asyncio.Event()
|
||||
self._log = loggers[__name__]
|
||||
|
||||
def append(self, state):
|
||||
|
|
|
@ -3,6 +3,7 @@ import asyncio
|
|||
import enum
|
||||
import os
|
||||
import struct
|
||||
import inspect
|
||||
from hashlib import sha1
|
||||
|
||||
|
||||
|
@ -107,6 +108,13 @@ def retry_range(retries):
|
|||
yield 1 + attempt
|
||||
|
||||
|
||||
async def _maybe_await(value):
|
||||
if inspect.isawaitable(value):
|
||||
return await value
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
async def _cancel(log, **tasks):
|
||||
"""
|
||||
Helper to cancel one or more tasks gracefully, logging exceptions.
|
||||
|
@ -123,6 +131,8 @@ async def _cancel(log, **tasks):
|
|||
except RuntimeError:
|
||||
# Probably: RuntimeError: await wasn't used with future
|
||||
#
|
||||
# See: https://github.com/python/cpython/blob/12d3061c7819a73d891dcce44327410eaf0e1bc2/Lib/asyncio/futures.py#L265
|
||||
#
|
||||
# Happens with _asyncio.Task instances (in "Task cancelling" state)
|
||||
# trying to SIGINT the program right during initial connection, on
|
||||
# _recv_loop coroutine (but we're creating its task explicitly with
|
||||
|
@ -131,6 +141,12 @@ async def _cancel(log, **tasks):
|
|||
# Since we're aware of this error there's no point in logging it.
|
||||
# *May* be https://bugs.python.org/issue37172
|
||||
pass
|
||||
except AssertionError as e:
|
||||
# In Python 3.6, the above RuntimeError is an AssertionError
|
||||
# See https://github.com/python/cpython/blob/7df32f844efed33ca781a016017eab7050263b90/Lib/asyncio/futures.py#L328
|
||||
if e.args != ("yield from wasn't used with future",):
|
||||
log.exception('Unhandled exception from %s after cancelling '
|
||||
'%s (%s)', name, type(task), task)
|
||||
except Exception:
|
||||
log.exception('Unhandled exception from %s after cancelling '
|
||||
'%s (%s)', name, type(task), task)
|
||||
|
|
|
@ -28,11 +28,10 @@ class Connection(abc.ABC):
|
|||
# should be one of `PacketCodec` implementations
|
||||
packet_codec = None
|
||||
|
||||
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||
def __init__(self, ip, port, dc_id, *, loggers, proxy=None):
|
||||
self._ip = ip
|
||||
self._port = port
|
||||
self._dc_id = dc_id # only for MTProxy, it's an abstraction leak
|
||||
self._loop = loop
|
||||
self._log = loggers[__name__]
|
||||
self._proxy = proxy
|
||||
self._reader = None
|
||||
|
@ -48,9 +47,8 @@ class Connection(abc.ABC):
|
|||
async def _connect(self, timeout=None, ssl=None):
|
||||
if not self._proxy:
|
||||
self._reader, self._writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(
|
||||
self._ip, self._port, loop=self._loop, ssl=ssl),
|
||||
loop=self._loop, timeout=timeout
|
||||
asyncio.open_connection(self._ip, self._port, ssl=ssl),
|
||||
timeout=timeout
|
||||
)
|
||||
else:
|
||||
import socks
|
||||
|
@ -65,11 +63,10 @@ class Connection(abc.ABC):
|
|||
else:
|
||||
s.set_proxy(*self._proxy)
|
||||
|
||||
s.setblocking(False)
|
||||
s.settimeout(timeout)
|
||||
await asyncio.wait_for(
|
||||
self._loop.sock_connect(s, address),
|
||||
timeout=timeout,
|
||||
loop=self._loop
|
||||
asyncio.get_event_loop().sock_connect(s, address),
|
||||
timeout=timeout
|
||||
)
|
||||
if ssl:
|
||||
if ssl_mod is None:
|
||||
|
@ -78,17 +75,16 @@ class Connection(abc.ABC):
|
|||
'without the SSL module being available'
|
||||
)
|
||||
|
||||
s.settimeout(timeout)
|
||||
s = ssl_mod.wrap_socket(
|
||||
s,
|
||||
do_handshake_on_connect=True,
|
||||
ssl_version=ssl_mod.PROTOCOL_SSLv23,
|
||||
ciphers='ADH-AES256-SHA'
|
||||
)
|
||||
s.setblocking(False)
|
||||
|
||||
s.setblocking(False)
|
||||
|
||||
self._reader, self._writer = \
|
||||
await asyncio.open_connection(sock=s, loop=self._loop)
|
||||
self._reader, self._writer = await asyncio.open_connection(sock=s)
|
||||
|
||||
self._codec = self.packet_codec(self)
|
||||
self._init_conn()
|
||||
|
@ -101,8 +97,9 @@ class Connection(abc.ABC):
|
|||
await self._connect(timeout=timeout, ssl=ssl)
|
||||
self._connected = True
|
||||
|
||||
self._send_task = self._loop.create_task(self._send_loop())
|
||||
self._recv_task = self._loop.create_task(self._recv_loop())
|
||||
loop = asyncio.get_event_loop()
|
||||
self._send_task = loop.create_task(self._send_loop())
|
||||
self._recv_task = loop.create_task(self._recv_loop())
|
||||
|
||||
async def disconnect(self):
|
||||
"""
|
||||
|
|
|
@ -95,12 +95,12 @@ class TcpMTProxy(ObfuscatedConnection):
|
|||
obfuscated_io = MTProxyIO
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
|
||||
def __init__(self, ip, port, dc_id, *, loggers, proxy=None):
|
||||
# connect to proxy's host and port instead of telegram's ones
|
||||
proxy_host, proxy_port = self.address_info(proxy)
|
||||
self._secret = bytes.fromhex(proxy[2])
|
||||
super().__init__(
|
||||
proxy_host, proxy_port, dc_id, loop=loop, loggers=loggers)
|
||||
proxy_host, proxy_port, dc_id, loggers=loggers)
|
||||
|
||||
async def _connect(self, timeout=None, ssl=None):
|
||||
await super()._connect(timeout=timeout, ssl=ssl)
|
||||
|
|
|
@ -40,12 +40,11 @@ class MTProtoSender:
|
|||
A new authorization key will be generated on connection if no other
|
||||
key exists yet.
|
||||
"""
|
||||
def __init__(self, auth_key, loop, *, loggers,
|
||||
def __init__(self, auth_key, *, loggers,
|
||||
retries=5, delay=1, auto_reconnect=True, connect_timeout=None,
|
||||
auth_key_callback=None,
|
||||
update_callback=None, auto_reconnect_callback=None):
|
||||
self._connection = None
|
||||
self._loop = loop
|
||||
self._loggers = loggers
|
||||
self._log = loggers[__name__]
|
||||
self._retries = retries
|
||||
|
@ -55,7 +54,7 @@ class MTProtoSender:
|
|||
self._auth_key_callback = auth_key_callback
|
||||
self._update_callback = update_callback
|
||||
self._auto_reconnect_callback = auto_reconnect_callback
|
||||
self._connect_lock = asyncio.Lock(loop=loop)
|
||||
self._connect_lock = asyncio.Lock()
|
||||
|
||||
# Whether the user has explicitly connected or disconnected.
|
||||
#
|
||||
|
@ -65,7 +64,7 @@ class MTProtoSender:
|
|||
# pending futures should be cancelled.
|
||||
self._user_connected = False
|
||||
self._reconnecting = False
|
||||
self._disconnected = self._loop.create_future()
|
||||
self._disconnected = asyncio.get_event_loop().create_future()
|
||||
self._disconnected.set_result(None)
|
||||
|
||||
# We need to join the loops upon disconnection
|
||||
|
@ -78,8 +77,7 @@ class MTProtoSender:
|
|||
|
||||
# Outgoing messages are put in a queue and sent in a batch.
|
||||
# Note that here we're also storing their ``_RequestState``.
|
||||
self._send_queue = MessagePacker(self._state, self._loop,
|
||||
loggers=self._loggers)
|
||||
self._send_queue = MessagePacker(self._state, loggers=self._loggers)
|
||||
|
||||
# Sent states are remembered until a response is received.
|
||||
self._pending_state = {}
|
||||
|
@ -171,7 +169,7 @@ class MTProtoSender:
|
|||
|
||||
if not utils.is_list_like(request):
|
||||
try:
|
||||
state = RequestState(request, self._loop)
|
||||
state = RequestState(request)
|
||||
except struct.error as e:
|
||||
# "struct.error: required argument is not an integer" is not
|
||||
# very helpful; log the request to find out what wasn't int.
|
||||
|
@ -186,7 +184,7 @@ class MTProtoSender:
|
|||
state = None
|
||||
for req in request:
|
||||
try:
|
||||
state = RequestState(req, self._loop, after=ordered and state)
|
||||
state = RequestState(req, after=ordered and state)
|
||||
except struct.error as e:
|
||||
self._log.error('Request caused struct.error: %s: %s', e, request)
|
||||
raise
|
||||
|
@ -206,7 +204,7 @@ class MTProtoSender:
|
|||
Note that it may resolve in either a ``ConnectionError``
|
||||
or any other unexpected error that could not be handled.
|
||||
"""
|
||||
return asyncio.shield(self._disconnected, loop=self._loop)
|
||||
return asyncio.shield(self._disconnected)
|
||||
|
||||
# Private methods
|
||||
|
||||
|
@ -241,7 +239,7 @@ class MTProtoSender:
|
|||
# reconnect cleanly after.
|
||||
await self._connection.disconnect()
|
||||
connected = False
|
||||
await asyncio.sleep(self._delay, loop=self._loop)
|
||||
await asyncio.sleep(self._delay)
|
||||
continue # next iteration we will try to reconnect
|
||||
|
||||
break # all steps done, break retry loop
|
||||
|
@ -253,17 +251,18 @@ class MTProtoSender:
|
|||
await self._disconnect(error=e)
|
||||
raise e
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
self._log.debug('Starting send loop')
|
||||
self._send_loop_handle = self._loop.create_task(self._send_loop())
|
||||
self._send_loop_handle = loop.create_task(self._send_loop())
|
||||
|
||||
self._log.debug('Starting receive loop')
|
||||
self._recv_loop_handle = self._loop.create_task(self._recv_loop())
|
||||
self._recv_loop_handle = loop.create_task(self._recv_loop())
|
||||
|
||||
# _disconnected only completes after manual disconnection
|
||||
# or errors after which the sender cannot continue such
|
||||
# as failing to reconnect or any unexpected error.
|
||||
if self._disconnected.done():
|
||||
self._disconnected = self._loop.create_future()
|
||||
self._disconnected = loop.create_future()
|
||||
|
||||
self._log.info('Connection to %s complete!', self._connection)
|
||||
|
||||
|
@ -378,7 +377,7 @@ class MTProtoSender:
|
|||
self._pending_state.clear()
|
||||
|
||||
if self._auto_reconnect_callback:
|
||||
self._loop.create_task(self._auto_reconnect_callback())
|
||||
asyncio.get_event_loop().create_task(self._auto_reconnect_callback())
|
||||
|
||||
break
|
||||
else:
|
||||
|
@ -398,7 +397,7 @@ class MTProtoSender:
|
|||
# gets stuck.
|
||||
# TODO It still gets stuck? Investigate where and why.
|
||||
self._reconnecting = True
|
||||
self._loop.create_task(self._reconnect(error))
|
||||
asyncio.get_event_loop().create_task(self._reconnect(error))
|
||||
|
||||
# Loops
|
||||
|
||||
|
@ -411,7 +410,7 @@ class MTProtoSender:
|
|||
"""
|
||||
while self._user_connected and not self._reconnecting:
|
||||
if self._pending_ack:
|
||||
ack = RequestState(MsgsAck(list(self._pending_ack)), self._loop)
|
||||
ack = RequestState(MsgsAck(list(self._pending_ack)))
|
||||
self._send_queue.append(ack)
|
||||
self._last_acks.append(ack)
|
||||
self._pending_ack.clear()
|
||||
|
@ -564,7 +563,7 @@ class MTProtoSender:
|
|||
if rpc_result.error:
|
||||
error = rpc_message_to_error(rpc_result.error, state.request)
|
||||
self._send_queue.append(
|
||||
RequestState(MsgsAck([state.msg_id]), loop=self._loop))
|
||||
RequestState(MsgsAck([state.msg_id])))
|
||||
|
||||
if not state.future.cancelled():
|
||||
state.future.set_exception(error)
|
||||
|
@ -751,8 +750,8 @@ class MTProtoSender:
|
|||
enqueuing a :tl:`MsgsStateInfo` to be sent at a later point.
|
||||
"""
|
||||
self._send_queue.append(RequestState(MsgsStateInfo(
|
||||
req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids)),
|
||||
loop=self._loop))
|
||||
req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids)
|
||||
)))
|
||||
|
||||
async def _handle_msg_all(self, message):
|
||||
"""
|
||||
|
|
|
@ -10,10 +10,10 @@ class RequestState:
|
|||
"""
|
||||
__slots__ = ('container_id', 'msg_id', 'request', 'data', 'future', 'after')
|
||||
|
||||
def __init__(self, request, loop, after=None):
|
||||
def __init__(self, request, after=None):
|
||||
self.container_id = None
|
||||
self.msg_id = None
|
||||
self.request = request
|
||||
self.data = bytes(request)
|
||||
self.future = asyncio.Future(loop=loop)
|
||||
self.future = asyncio.Future()
|
||||
self.after = after
|
||||
|
|
|
@ -162,7 +162,6 @@ def compute_check(request: types.account.Password, password: str):
|
|||
|
||||
def generate_and_check_random():
|
||||
random_size = 256
|
||||
import time
|
||||
while True:
|
||||
random = os.urandom(random_size)
|
||||
a = int.from_bytes(random, 'big')
|
||||
|
|
|
@ -65,8 +65,7 @@ class RequestIter(abc.ABC):
|
|||
# asyncio will handle times <= 0 to sleep 0 seconds
|
||||
if self.wait_time:
|
||||
await asyncio.sleep(
|
||||
self.wait_time - (time.time() - self.last_load),
|
||||
loop=self.client.loop
|
||||
self.wait_time - (time.time() - self.last_load)
|
||||
)
|
||||
self.last_load = time.time()
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import datetime
|
||||
import inspect
|
||||
|
||||
from .tl import types
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import gzip
|
||||
import struct
|
||||
|
||||
from .. import TLObject, TLRequest
|
||||
from .. import TLObject
|
||||
|
||||
|
||||
class GzipPacked(TLObject):
|
||||
|
|
|
@ -10,3 +10,4 @@ from .inlinebuilder import InlineBuilder
|
|||
from .inlineresult import InlineResult
|
||||
from .inlineresults import InlineResults
|
||||
from .conversation import Conversation
|
||||
from .qrlogin import QRLogin
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import itertools
|
||||
import time
|
||||
|
||||
|
@ -11,6 +13,16 @@ from ... import helpers, utils, errors
|
|||
_EDIT_COLLISION_DELTA = 0.001
|
||||
|
||||
|
||||
def _checks_cancelled(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self._cancelled:
|
||||
raise asyncio.CancelledError('The conversation was cancelled before')
|
||||
|
||||
return f(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class Conversation(ChatGetter):
|
||||
"""
|
||||
Represents a conversation inside an specific chat.
|
||||
|
@ -66,6 +78,7 @@ class Conversation(ChatGetter):
|
|||
|
||||
self._edit_dates = {}
|
||||
|
||||
@_checks_cancelled
|
||||
async def send_message(self, *args, **kwargs):
|
||||
"""
|
||||
Sends a message in the context of this conversation. Shorthand
|
||||
|
@ -81,6 +94,7 @@ class Conversation(ChatGetter):
|
|||
self._last_outgoing = ms[-1].id
|
||||
return sent
|
||||
|
||||
@_checks_cancelled
|
||||
async def send_file(self, *args, **kwargs):
|
||||
"""
|
||||
Sends a file in the context of this conversation. Shorthand
|
||||
|
@ -96,6 +110,7 @@ class Conversation(ChatGetter):
|
|||
self._last_outgoing = ms[-1].id
|
||||
return sent
|
||||
|
||||
@_checks_cancelled
|
||||
def mark_read(self, message=None):
|
||||
"""
|
||||
Marks as read the latest received message if ``message is None``.
|
||||
|
@ -117,7 +132,8 @@ class Conversation(ChatGetter):
|
|||
|
||||
def get_response(self, message=None, *, timeout=None):
|
||||
"""
|
||||
Gets the next message that responds to a previous one.
|
||||
Gets the next message that responds to a previous one. This is
|
||||
the method you need most of the time, along with `get_edit`.
|
||||
|
||||
Args:
|
||||
message (`Message <telethon.tl.custom.message.Message>` | `int`, optional):
|
||||
|
@ -127,6 +143,16 @@ class Conversation(ChatGetter):
|
|||
timeout (`int` | `float`, optional):
|
||||
If present, this `timeout` (in seconds) will override the
|
||||
per-action timeout defined for the conversation.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with client.conversation(...) as conv:
|
||||
await conv.send_message('Hey, what is your name?')
|
||||
|
||||
response = await conv.get_response()
|
||||
name = response.text
|
||||
|
||||
await conv.send_message('Nice to meet you, {}!'.format(name))
|
||||
"""
|
||||
return self._get_message(
|
||||
message, self._response_indices, self._pending_responses, timeout,
|
||||
|
@ -257,23 +283,41 @@ class Conversation(ChatGetter):
|
|||
|
||||
.. note::
|
||||
|
||||
Only use this if there isn't another method available!
|
||||
**Only use this if there isn't another method available!**
|
||||
For example, don't use `wait_event` for new messages,
|
||||
since `get_response` already exists, etc.
|
||||
|
||||
Unless you're certain that your code will run fast enough,
|
||||
generally you should get a "handle" of this special coroutine
|
||||
before acting. Generally, you should do this:
|
||||
before acting. In this example you will see how to wait for a user
|
||||
to join a group with proper use of `wait_event`:
|
||||
|
||||
>>> from telethon import TelegramClient, events
|
||||
>>>
|
||||
>>> client = TelegramClient(...)
|
||||
>>>
|
||||
>>> async def main():
|
||||
>>> async with client.conversation(...) as conv:
|
||||
>>> response = conv.wait_event(events.NewMessage(incoming=True))
|
||||
>>> await conv.send_message('Hi')
|
||||
>>> response = await response
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
client = TelegramClient(...)
|
||||
group_id = ...
|
||||
|
||||
async def main():
|
||||
# Could also get the user id from an event; this is just an example
|
||||
user_id = ...
|
||||
|
||||
async with client.conversation(user_id) as conv:
|
||||
# Get a handle to the future event we'll wait for
|
||||
handle = conv.wait_event(events.ChatAction(
|
||||
group_id,
|
||||
func=lambda e: e.user_joined and e.user_id == user_id
|
||||
))
|
||||
|
||||
# Perform whatever action in between
|
||||
await conv.send_message('Please join this group before speaking to me!')
|
||||
|
||||
# Wait for the event we registered above to fire
|
||||
event = await handle
|
||||
|
||||
# Continue with the conversation
|
||||
await conv.send_message('Thanks!')
|
||||
|
||||
This way your event can be registered before acting,
|
||||
since the response may arrive before your event was
|
||||
|
@ -298,9 +342,15 @@ class Conversation(ChatGetter):
|
|||
for key, (ev, fut) in list(self._custom.items()):
|
||||
ev_type = type(ev)
|
||||
inst = built[ev_type]
|
||||
if inst and ev.filter(inst):
|
||||
fut.set_result(inst)
|
||||
del self._custom[key]
|
||||
|
||||
if inst:
|
||||
filter = ev.filter(inst)
|
||||
if inspect.isawaitable(filter):
|
||||
filter = await filter
|
||||
|
||||
if filter:
|
||||
fut.set_result(inst)
|
||||
del self._custom[key]
|
||||
|
||||
def _on_new_message(self, response):
|
||||
response = response.message
|
||||
|
@ -379,10 +429,8 @@ class Conversation(ChatGetter):
|
|||
else:
|
||||
raise ValueError('No message was sent previously')
|
||||
|
||||
@_checks_cancelled
|
||||
def _get_result(self, future, start_time, timeout, pending, target_id):
|
||||
if self._cancelled:
|
||||
raise asyncio.CancelledError('The conversation was cancelled before')
|
||||
|
||||
due = self._total_due
|
||||
if timeout is None:
|
||||
timeout = self._timeout
|
||||
|
@ -397,8 +445,7 @@ class Conversation(ChatGetter):
|
|||
# cleared when their futures are set to a result.
|
||||
return asyncio.wait_for(
|
||||
future,
|
||||
timeout=None if due == float('inf') else due - time.time(),
|
||||
loop=self._client.loop
|
||||
timeout=None if due == float('inf') else due - time.time()
|
||||
)
|
||||
|
||||
def _cancel_all(self, exception=None):
|
||||
|
|
|
@ -552,6 +552,14 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
|
|||
if isinstance(self.media, types.MessageMediaVenue):
|
||||
return self.media
|
||||
|
||||
@property
|
||||
def dice(self):
|
||||
"""
|
||||
The :tl:`MessageMediaDice` in this message, if it's a dice roll.
|
||||
"""
|
||||
if isinstance(self.media, types.MessageMediaDice):
|
||||
return self.media
|
||||
|
||||
@property
|
||||
def action_entities(self):
|
||||
"""
|
||||
|
@ -756,7 +764,8 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
|
|||
return await self._client.download_media(self, *args, **kwargs)
|
||||
|
||||
async def click(self, i=None, j=None,
|
||||
*, text=None, filter=None, data=None):
|
||||
*, text=None, filter=None, data=None, share_phone=None,
|
||||
share_geo=None):
|
||||
"""
|
||||
Calls `button.click <telethon.tl.custom.messagebutton.MessageButton.click>`
|
||||
on the specified button.
|
||||
|
@ -805,6 +814,28 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
|
|||
that if the message does not have this data, it will
|
||||
``raise DataInvalidError``.
|
||||
|
||||
share_phone (`bool` | `str` | tl:`InputMediaContact`):
|
||||
When clicking on a keyboard button requesting a phone number
|
||||
(:tl:`KeyboardButtonRequestPhone`), this argument must be
|
||||
explicitly set to avoid accidentally sharing the number.
|
||||
|
||||
It can be `True` to automatically share the current user's
|
||||
phone, a string to share a specific phone number, or a contact
|
||||
media to specify all details.
|
||||
|
||||
If the button is pressed without this, `ValueError` is raised.
|
||||
|
||||
share_geo (`tuple` | `list` | tl:`InputMediaGeoPoint`):
|
||||
When clicking on a keyboard button requesting a geo location
|
||||
(:tl:`KeyboardButtonRequestGeoLocation`), this argument must
|
||||
be explicitly set to avoid accidentally sharing the location.
|
||||
|
||||
It must be a `tuple` of `float` as ``(longitude, latitude)``,
|
||||
or a :tl:`InputGeoPoint` instance to avoid accidentally using
|
||||
the wrong roder.
|
||||
|
||||
If the button is pressed without this, `ValueError` is raised.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -820,6 +851,9 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
|
|||
|
||||
# Click by data
|
||||
await message.click(data=b'payload')
|
||||
|
||||
# Click on a button requesting a phone
|
||||
await message.click(0, share_phone=True)
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
|
@ -836,7 +870,7 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
|
|||
data=data
|
||||
)
|
||||
)
|
||||
except errors.BotTimeout:
|
||||
except errors.BotResponseTimeoutError:
|
||||
return None
|
||||
|
||||
if sum(int(x is not None) for x in (i, text, filter)) >= 2:
|
||||
|
@ -845,29 +879,35 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
|
|||
if not await self.get_buttons():
|
||||
return # Accessing the property sets self._buttons[_flat]
|
||||
|
||||
if text is not None:
|
||||
if callable(text):
|
||||
def find_button():
|
||||
nonlocal i
|
||||
if text is not None:
|
||||
if callable(text):
|
||||
for button in self._buttons_flat:
|
||||
if text(button.text):
|
||||
return button
|
||||
else:
|
||||
for button in self._buttons_flat:
|
||||
if button.text == text:
|
||||
return button
|
||||
return
|
||||
|
||||
if filter is not None:
|
||||
for button in self._buttons_flat:
|
||||
if text(button.text):
|
||||
return await button.click()
|
||||
if filter(button):
|
||||
return button
|
||||
return
|
||||
|
||||
if i is None:
|
||||
i = 0
|
||||
if j is None:
|
||||
return self._buttons_flat[i]
|
||||
else:
|
||||
for button in self._buttons_flat:
|
||||
if button.text == text:
|
||||
return await button.click()
|
||||
return
|
||||
return self._buttons[i][j]
|
||||
|
||||
if filter is not None:
|
||||
for button in self._buttons_flat:
|
||||
if filter(button):
|
||||
return await button.click()
|
||||
return
|
||||
|
||||
if i is None:
|
||||
i = 0
|
||||
if j is None:
|
||||
return await self._buttons_flat[i].click()
|
||||
else:
|
||||
return await self._buttons[i][j].click()
|
||||
button = find_button()
|
||||
if button:
|
||||
return await button.click(share_phone=share_phone, share_geo=share_geo)
|
||||
|
||||
async def mark_read(self):
|
||||
"""
|
||||
|
@ -890,7 +930,7 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
|
|||
# maybe just make it illegal to call messages from raw API?
|
||||
# That or figure out a way to always set it directly.
|
||||
if self._client:
|
||||
await self._client.pin_message(
|
||||
return await self._client.pin_message(
|
||||
await self.get_input_chat(), self.id, notify=notify)
|
||||
|
||||
# endregion Public Methods
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from .. import types, functions
|
||||
from ...errors import BotTimeout
|
||||
from ...errors import BotResponseTimeoutError
|
||||
import webbrowser
|
||||
|
||||
|
||||
|
@ -59,7 +59,7 @@ class MessageButton:
|
|||
if isinstance(self.button, types.KeyboardButtonUrl):
|
||||
return self.button.url
|
||||
|
||||
async def click(self):
|
||||
async def click(self, share_phone=None, share_geo=None):
|
||||
"""
|
||||
Emulates the behaviour of clicking this button.
|
||||
|
||||
|
@ -75,6 +75,19 @@ class MessageButton:
|
|||
|
||||
If it's a :tl:`KeyboardButtonUrl`, the URL of the button will
|
||||
be passed to ``webbrowser.open`` and return `True` on success.
|
||||
|
||||
If it's a :tl:`KeyboardButtonRequestPhone`, you must indicate that you
|
||||
want to ``share_phone=True`` in order to share it. Sharing it is not a
|
||||
default because it is a privacy concern and could happen accidentally.
|
||||
|
||||
You may also use ``share_phone=phone`` to share a specific number, in
|
||||
which case either `str` or :tl:`InputMediaContact` should be used.
|
||||
|
||||
If it's a :tl:`KeyboardButtonRequestGeoLocation`, you must pass a
|
||||
tuple in ``share_geo=(longitude, latitude)``. Note that Telegram seems
|
||||
to have some heuristics to determine impossible locations, so changing
|
||||
this value a lot quickly may not work as expected. You may also pass a
|
||||
:tl:`InputGeoPoint` if you find the order confusing.
|
||||
"""
|
||||
if isinstance(self.button, types.KeyboardButton):
|
||||
return await self._client.send_message(
|
||||
|
@ -85,7 +98,7 @@ class MessageButton:
|
|||
)
|
||||
try:
|
||||
return await self._client(req)
|
||||
except BotTimeout:
|
||||
except BotResponseTimeoutError:
|
||||
return None
|
||||
elif isinstance(self.button, types.KeyboardButtonSwitchInline):
|
||||
return await self._client(functions.messages.StartBotRequest(
|
||||
|
@ -99,5 +112,28 @@ class MessageButton:
|
|||
)
|
||||
try:
|
||||
return await self._client(req)
|
||||
except BotTimeout:
|
||||
except BotResponseTimeoutError:
|
||||
return None
|
||||
elif isinstance(self.button, types.KeyboardButtonRequestPhone):
|
||||
if not share_phone:
|
||||
raise ValueError('cannot click on phone buttons unless share_phone=True')
|
||||
|
||||
if share_phone == True or isinstance(share_phone, str):
|
||||
me = await self._client.get_me()
|
||||
share_phone = types.InputMediaContact(
|
||||
phone_number=me.phone if share_phone == True else share_phone,
|
||||
first_name=me.first_name or '',
|
||||
last_name=me.last_name or '',
|
||||
vcard=''
|
||||
)
|
||||
|
||||
return await self._client.send_file(self._chat, share_phone)
|
||||
elif isinstance(self.button, types.KeyboardButtonRequestGeoLocation):
|
||||
if not share_geo:
|
||||
raise ValueError('cannot click on geo buttons unless share_geo=(longitude, latitude)')
|
||||
|
||||
if isinstance(share_geo, (tuple, list)):
|
||||
long, lat = share_geo
|
||||
share_geo = types.InputMediaGeoPoint(types.InputGeoPoint(lat=lat, long=long))
|
||||
|
||||
return await self._client.send_file(self._chat, share_geo)
|
||||
|
|
119
telethon/tl/custom/qrlogin.py
Normal file
119
telethon/tl/custom/qrlogin.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
import asyncio
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
from .. import types, functions
|
||||
from ... import events
|
||||
|
||||
|
||||
class QRLogin:
|
||||
"""
|
||||
QR login information.
|
||||
|
||||
Most of the time, you will present the `url` as a QR code to the user,
|
||||
and while it's being shown, call `wait`.
|
||||
"""
|
||||
def __init__(self, client, ignored_ids):
|
||||
self._client = client
|
||||
self._request = functions.auth.ExportLoginTokenRequest(
|
||||
self._client.api_id, self._client.api_hash, ignored_ids)
|
||||
self._resp = None
|
||||
|
||||
async def recreate(self):
|
||||
"""
|
||||
Generates a new token and URL for a new QR code, useful if the code
|
||||
has expired before it was imported.
|
||||
"""
|
||||
self._resp = await self._client(self._request)
|
||||
|
||||
@property
|
||||
def token(self) -> bytes:
|
||||
"""
|
||||
The binary data representing the token.
|
||||
|
||||
It can be used by a previously-authorized client in a call to
|
||||
:tl:`auth.importLoginToken` to log the client that originally
|
||||
requested the QR login.
|
||||
"""
|
||||
return self._resp.token
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""
|
||||
The ``tg://login`` URI with the token. When opened by a Telegram
|
||||
application where the user is logged in, it will import the login
|
||||
token.
|
||||
|
||||
If you want to display a QR code to the user, this is the URL that
|
||||
should be launched when the QR code is scanned (the URL that should
|
||||
be contained in the QR code image you generate).
|
||||
|
||||
Whether you generate the QR code image or not is up to you, and the
|
||||
library can't do this for you due to the vast ways of generating and
|
||||
displaying the QR code that exist.
|
||||
|
||||
The URL simply consists of `token` base64-encoded.
|
||||
"""
|
||||
return 'tg://login?token={}'.format(base64.b64encode(self._resp.token).decode('utf-8'))
|
||||
|
||||
@property
|
||||
def expires(self) -> datetime.datetime:
|
||||
"""
|
||||
The `datetime` at which the QR code will expire.
|
||||
|
||||
If you want to try again, you will need to call `recreate`.
|
||||
"""
|
||||
return self._resp.expires
|
||||
|
||||
async def wait(self, timeout: float = None):
|
||||
"""
|
||||
Waits for the token to be imported by a previously-authorized client,
|
||||
either by scanning the QR, launching the URL directly, or calling the
|
||||
import method.
|
||||
|
||||
This method **must** be called before the QR code is scanned, and
|
||||
must be executing while the QR code is being scanned. Otherwise, the
|
||||
login will not complete.
|
||||
|
||||
Will raise `asyncio.TimeoutError` if the login doesn't complete on
|
||||
time.
|
||||
|
||||
Arguments
|
||||
timeout (float):
|
||||
The timeout, in seconds, to wait before giving up. By default
|
||||
the library will wait until the token expires, which is often
|
||||
what you want.
|
||||
|
||||
Returns
|
||||
On success, an instance of :tl:`User`. On failure it will raise.
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds()
|
||||
|
||||
event = asyncio.Event()
|
||||
|
||||
async def handler(_update):
|
||||
event.set()
|
||||
|
||||
self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken))
|
||||
|
||||
try:
|
||||
# Will raise timeout error if it doesn't complete quick enough,
|
||||
# which we want to let propagate
|
||||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||||
finally:
|
||||
self._client.remove_event_handler(handler)
|
||||
|
||||
# We got here without it raising timeout error, so we can proceed
|
||||
resp = await self._client(self._request)
|
||||
if isinstance(resp, types.auth.LoginTokenMigrateTo):
|
||||
await self._client._switch_dc(resp.dc_id)
|
||||
resp = await self._client(functions.auth.ImportLoginTokenRequest(resp.token))
|
||||
# resp should now be auth.loginTokenSuccess
|
||||
|
||||
if isinstance(resp, types.auth.LoginTokenSuccess):
|
||||
user = resp.authorization.user
|
||||
self._client._on_login(user)
|
||||
return user
|
||||
|
||||
raise TypeError('Login token response was unexpected: {}'.format(resp))
|
|
@ -186,6 +186,19 @@ class TLObject:
|
|||
return json.dumps(d, default=default, **kwargs)
|
||||
|
||||
def __bytes__(self):
|
||||
try:
|
||||
return self._bytes()
|
||||
except AttributeError:
|
||||
# If a type is wrong (e.g. expected `TLObject` but `int` was
|
||||
# provided) it will try to access `._bytes()`, which will fail
|
||||
# with `AttributeError`. This occurs in fact because the type
|
||||
# was wrong, so raise the correct error type.
|
||||
raise TypeError('a TLObject was expected but found something else')
|
||||
|
||||
# Custom objects will call `(...)._bytes()` and not `bytes(...)` so that
|
||||
# if the wrong type is used (e.g. `int`) we won't try allocating a huge
|
||||
# amount of data, which would cause a `MemoryError`.
|
||||
def _bytes(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -51,6 +51,8 @@ mimetypes.add_type('audio/aac', '.aac')
|
|||
mimetypes.add_type('audio/ogg', '.ogg')
|
||||
mimetypes.add_type('audio/flac', '.flac')
|
||||
|
||||
mimetypes.add_type('application/x-tgsticker', '.tgs')
|
||||
|
||||
USERNAME_RE = re.compile(
|
||||
r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|joinchat/)?'
|
||||
)
|
||||
|
@ -64,7 +66,7 @@ TG_JOIN_RE = re.compile(
|
|||
#
|
||||
# See https://telegram.org/blog/inline-bots#how-does-it-work
|
||||
VALID_USERNAME_RE = re.compile(
|
||||
r'^([a-z]((?!__)[\w\d]){3,30}[a-z\d]'
|
||||
r'^([a-z](?:(?!__)\w){3,30}[a-z\d]'
|
||||
r'|gif|vid|pic|bing|wiki|imdb|bold|vote|like|coub)$',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
@ -481,7 +483,7 @@ def get_input_media(
|
|||
supports_streaming=supports_streaming
|
||||
)
|
||||
return types.InputMediaUploadedDocument(
|
||||
file=media, mime_type=mime, attributes=attrs)
|
||||
file=media, mime_type=mime, attributes=attrs, force_file=force_document)
|
||||
|
||||
if isinstance(media, types.MessageMediaGame):
|
||||
return types.InputMediaGame(id=types.InputGameID(
|
||||
|
@ -510,6 +512,9 @@ def get_input_media(
|
|||
venue_type=''
|
||||
)
|
||||
|
||||
if isinstance(media, types.MessageMediaDice):
|
||||
return types.InputMediaDice(media.emoticon)
|
||||
|
||||
if isinstance(media, (
|
||||
types.MessageMediaEmpty, types.MessageMediaUnsupported,
|
||||
types.ChatPhotoEmpty, types.UserProfilePhotoEmpty,
|
||||
|
@ -520,6 +525,27 @@ def get_input_media(
|
|||
if isinstance(media, types.Message):
|
||||
return get_input_media(media.media, is_photo=is_photo)
|
||||
|
||||
if isinstance(media, types.MessageMediaPoll):
|
||||
if media.poll.quiz:
|
||||
if not media.results.results:
|
||||
# A quiz has correct answers, which we don't know until answered.
|
||||
# If the quiz hasn't been answered we can't reconstruct it properly.
|
||||
raise TypeError('Cannot cast unanswered quiz to any kind of InputMedia.')
|
||||
|
||||
correct_answers = [r.option for r in media.results.results if r.correct]
|
||||
else:
|
||||
correct_answers = None
|
||||
|
||||
return types.InputMediaPoll(
|
||||
poll=media.poll,
|
||||
correct_answers=correct_answers,
|
||||
solution=media.results.solution,
|
||||
solution_entities=media.results.solution_entities,
|
||||
)
|
||||
|
||||
if isinstance(media, types.Poll):
|
||||
return types.InputMediaPoll(media)
|
||||
|
||||
_raise_cast_fail(media, 'InputMedia')
|
||||
|
||||
|
||||
|
@ -1230,7 +1256,7 @@ def get_appropriated_part_size(file_size):
|
|||
return 128
|
||||
if file_size <= 786432000: # 750MB
|
||||
return 256
|
||||
if file_size <= 1572864000: # 1500MB
|
||||
if file_size <= 2097152000: # 2000MB
|
||||
return 512
|
||||
|
||||
raise ValueError('File size too large')
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '1.11.2'
|
||||
__version__ = '1.16.2'
|
||||
|
|
|
@ -130,6 +130,18 @@ assumes some [`asyncio`] knowledge, but otherwise is easy to follow.
|
|||
|
||||
![Screenshot of the tkinter GUI][tkinter GUI]
|
||||
|
||||
### [`payment.py`](https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/payment.py)
|
||||
|
||||
* Usable as: **bot**.
|
||||
* Difficulty: **medium**.
|
||||
|
||||
This example shows how to make invoices (Telegram's way of requesting payments) via a bot account. The example does not include how to add shipping information, though.
|
||||
|
||||
You'll need to obtain a "provider token" to use this example, so please read [Telegram's guide on payments](https://core.telegram.org/bots/payments) before using this example.
|
||||
|
||||
|
||||
It makes use of the ["raw API"](https://tl.telethon.dev) (that is, no friendly `client.` methods), which can be helpful in understanding how it works and how it can be used.
|
||||
|
||||
|
||||
[Telethon]: https://github.com/LonamiWebs/Telethon
|
||||
[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/LICENSE
|
||||
|
|
|
@ -341,8 +341,8 @@ class App(tkinter.Tk):
|
|||
self.chat.configure(bg='yellow')
|
||||
|
||||
|
||||
async def main(loop, interval=0.05):
|
||||
client = TelegramClient(SESSION, API_ID, API_HASH, loop=loop)
|
||||
async def main(interval=0.05):
|
||||
client = TelegramClient(SESSION, API_ID, API_HASH)
|
||||
try:
|
||||
await client.connect()
|
||||
except Exception as e:
|
||||
|
@ -372,7 +372,7 @@ if __name__ == "__main__":
|
|||
# Some boilerplate code to set up the main method
|
||||
aio_loop = asyncio.get_event_loop()
|
||||
try:
|
||||
aio_loop.run_until_complete(main(aio_loop))
|
||||
aio_loop.run_until_complete(main())
|
||||
finally:
|
||||
if not aio_loop.is_closed():
|
||||
aio_loop.close()
|
||||
|
|
183
telethon_examples/payment.py
Normal file
183
telethon_examples/payment.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
from telethon import TelegramClient, events, types, functions
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import tracemalloc
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
"""
|
||||
Provider token can be obtained via @BotFather. more info at https://core.telegram.org/bots/payments#getting-a-token
|
||||
|
||||
If you are using test token, set test=True in generate_invoice function,
|
||||
If you are using real token, set test=False
|
||||
"""
|
||||
provider_token = ''
|
||||
|
||||
tracemalloc.start()
|
||||
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.WARNING)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_env(name, message, cast=str):
|
||||
if name in os.environ:
|
||||
return os.environ[name]
|
||||
while True:
|
||||
value = input(message)
|
||||
try:
|
||||
return cast(value)
|
||||
except ValueError as e:
|
||||
print(e, file=sys.stderr)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
bot = TelegramClient(
|
||||
os.environ.get('TG_SESSION', 'payment'),
|
||||
get_env('TG_API_ID', 'Enter your API ID: ', int),
|
||||
get_env('TG_API_HASH', 'Enter your API hash: '),
|
||||
proxy=None
|
||||
)
|
||||
|
||||
|
||||
# That event is handled when customer enters his card/etc, on final pre-checkout
|
||||
# If we don't `SetBotPrecheckoutResultsRequest`, money won't be charged from buyer, and nothing will happen next.
|
||||
@bot.on(events.Raw(types.UpdateBotPrecheckoutQuery))
|
||||
async def payment_pre_checkout_handler(event: types.UpdateBotPrecheckoutQuery):
|
||||
if event.payload.decode('UTF-8') == 'product A':
|
||||
# so we have to confirm payment
|
||||
await bot(
|
||||
functions.messages.SetBotPrecheckoutResultsRequest(
|
||||
query_id=event.query_id,
|
||||
success=True,
|
||||
error=None
|
||||
)
|
||||
)
|
||||
elif event.payload.decode('UTF-8') == 'product B':
|
||||
# same for another
|
||||
await bot(
|
||||
functions.messages.SetBotPrecheckoutResultsRequest(
|
||||
query_id=event.query_id,
|
||||
success=True,
|
||||
error=None
|
||||
)
|
||||
)
|
||||
else:
|
||||
# for example, something went wrong (whatever reason). We can tell customer about that:
|
||||
await bot(
|
||||
functions.messages.SetBotPrecheckoutResultsRequest(
|
||||
query_id=event.query_id,
|
||||
success=False,
|
||||
error='Something went wrong'
|
||||
)
|
||||
)
|
||||
|
||||
raise events.StopPropagation
|
||||
|
||||
|
||||
# That event is handled at the end, when customer payed.
|
||||
@bot.on(events.Raw(types.UpdateNewMessage))
|
||||
async def payment_received_handler(event):
|
||||
if isinstance(event.message.action, types.MessageActionPaymentSentMe):
|
||||
payment: types.MessageActionPaymentSentMe = event.message.action
|
||||
# do something after payment was recieved
|
||||
if payment.payload.decode('UTF-8') == 'product A':
|
||||
await bot.send_message(event.message.from_id, 'Thank you for buying product A!')
|
||||
elif payment.payload.decode('UTF-8') == 'product B':
|
||||
await bot.send_message(event.message.from_id, 'Thank you for buying product B!')
|
||||
raise events.StopPropagation
|
||||
|
||||
|
||||
# let's put it in one function for more easier way
|
||||
def generate_invoice(price_label: str, price_amount: int, currency: str, title: str,
|
||||
description: str, payload: str, start_param: str) -> types.InputMediaInvoice:
|
||||
price = types.LabeledPrice(label=price_label, amount=price_amount) # label - just a text, amount=10000 means 100.00
|
||||
invoice = types.Invoice(
|
||||
currency=currency, # currency like USD
|
||||
prices=[price], # there could be a couple of prices.
|
||||
test=True, # if you're working with test token, else set test=False.
|
||||
# More info at https://core.telegram.org/bots/payments
|
||||
|
||||
# params for requesting specific fields
|
||||
name_requested=False,
|
||||
phone_requested=False,
|
||||
email_requested=False,
|
||||
shipping_address_requested=False,
|
||||
|
||||
# if price changes depending on shipping
|
||||
flexible=False,
|
||||
|
||||
# send data to provider
|
||||
phone_to_provider=False,
|
||||
email_to_provider=False
|
||||
)
|
||||
return types.InputMediaInvoice(
|
||||
title=title,
|
||||
description=description,
|
||||
invoice=invoice,
|
||||
payload=payload.encode('UTF-8'), # payload, which will be sent to next 2 handlers
|
||||
provider=provider_token,
|
||||
|
||||
provider_data=types.DataJSON('{}'),
|
||||
# data about the invoice, which will be shared with the payment provider. A detailed description of
|
||||
# required fields should be provided by the payment provider.
|
||||
|
||||
start_param=start_param,
|
||||
# Unique deep-linking parameter. May also be used in UpdateBotPrecheckoutQuery
|
||||
# see: https://core.telegram.org/bots#deep-linking
|
||||
# it may be the empty string if not needed
|
||||
|
||||
)
|
||||
|
||||
|
||||
@bot.on(events.NewMessage(pattern='/start'))
|
||||
async def start_handler(event: events.NewMessage.Event):
|
||||
await event.respond('/product_a - product A\n/product_b - product B\n/product_c - product, shall cause an error')
|
||||
|
||||
|
||||
@bot.on(events.NewMessage(pattern='/product_a'))
|
||||
async def start_handler(event: events.NewMessage.Event):
|
||||
await bot.send_message(
|
||||
event.chat_id, 'Sending invoice A',
|
||||
file=generate_invoice(
|
||||
price_label='Pay', price_amount=10000, currency='RUB', title='Title A', description='description A',
|
||||
payload='product A', start_param='abc'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@bot.on(events.NewMessage(pattern='/product_b'))
|
||||
async def start_handler(event: events.NewMessage.Event):
|
||||
await bot.send_message(
|
||||
event.chat_id, 'Sending invoice B',
|
||||
file=generate_invoice(
|
||||
price_label='Pay', price_amount=20000, currency='RUB', title='Title B', description='description B',
|
||||
payload='product B', start_param='abc'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@bot.on(events.NewMessage(pattern='/product_c'))
|
||||
async def start_handler(event: events.NewMessage.Event):
|
||||
await bot.send_message(
|
||||
event.chat_id, 'Sending invoice C',
|
||||
file=generate_invoice(
|
||||
price_label='Pay', price_amount=50000, currency='RUB', title='Title C',
|
||||
description='description c - shall cause an error', payload='product C', start_param='abc'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
await bot.start()
|
||||
await bot.run_until_disconnected()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not provider_token:
|
||||
logger.error("No provider token supplied.")
|
||||
exit(1)
|
||||
loop.run_until_complete(main())
|
|
@ -62,19 +62,19 @@ inputMediaUploadedPhoto#1e287d04 flags:# file:InputFile stickers:flags.0?Vector<
|
|||
inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
|
||||
inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia;
|
||||
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
|
||||
inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;
|
||||
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
|
||||
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
|
||||
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
|
||||
inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
|
||||
inputMediaGeoLive#ce4e82fd flags:# stopped:flags.0?true geo_point:InputGeoPoint period:flags.1?int = InputMedia;
|
||||
inputMediaPoll#abe9ca25 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> = InputMedia;
|
||||
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
|
||||
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
|
||||
|
||||
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
|
||||
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
|
||||
inputChatUploadedPhoto#c642724e flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = InputChatPhoto;
|
||||
inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;
|
||||
|
||||
inputGeoPointEmpty#e4c123d6 = InputGeoPoint;
|
||||
|
@ -109,10 +109,10 @@ storage.fileMp4#b3cea0e4 = storage.FileType;
|
|||
storage.fileWebp#1081464c = storage.FileType;
|
||||
|
||||
userEmpty#200250ba id:int = User;
|
||||
user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User;
|
||||
user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User;
|
||||
|
||||
userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto;
|
||||
userProfilePhoto#ecd75d8c photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto;
|
||||
userProfilePhoto#69d3ab26 flags:# has_video:flags.0?true photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto;
|
||||
|
||||
userStatusEmpty#9d05049 = UserStatus;
|
||||
userStatusOnline#edb93949 expires:int = UserStatus;
|
||||
|
@ -128,7 +128,7 @@ channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.
|
|||
channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat;
|
||||
|
||||
chatFull#1b7c9db3 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector<BotInfo> pinned_msg_id:flags.6?int folder_id:flags.11?int = ChatFull;
|
||||
channelFull#2d895c74 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true can_set_location:flags.16?true has_scheduled:flags.19?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int pts:int = ChatFull;
|
||||
channelFull#f0e6672a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int = ChatFull;
|
||||
|
||||
chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant;
|
||||
chatParticipantCreator#da13538a user_id:int = ChatParticipant;
|
||||
|
@ -138,7 +138,7 @@ chatParticipantsForbidden#fc900c2b flags:# chat_id:int self_participant:flags.0?
|
|||
chatParticipants#3f460fed chat_id:int participants:Vector<ChatParticipant> version:int = ChatParticipants;
|
||||
|
||||
chatPhotoEmpty#37c1011c = ChatPhoto;
|
||||
chatPhoto#475cdbd5 photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;
|
||||
chatPhoto#d20b9f3c flags:# has_video:flags.0?true photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;
|
||||
|
||||
messageEmpty#83e5de54 id:int = Message;
|
||||
message#452c0e65 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector<RestrictionReason> = Message;
|
||||
|
@ -156,6 +156,7 @@ messageMediaGame#fdb19008 game:Game = MessageMedia;
|
|||
messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia;
|
||||
messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia;
|
||||
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
|
||||
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
|
||||
|
||||
messageActionEmpty#b6aef7b0 = MessageAction;
|
||||
messageActionChatCreate#a6638b9a title:string users:Vector<int> = MessageAction;
|
||||
|
@ -185,7 +186,7 @@ dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer t
|
|||
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
|
||||
|
||||
photoEmpty#2331b22d id:long = Photo;
|
||||
photo#d07504a5 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> dc_id:int = Photo;
|
||||
photo#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> video_sizes:flags.1?Vector<VideoSize> dc_id:int = Photo;
|
||||
|
||||
photoSizeEmpty#e17e23c type:string = PhotoSize;
|
||||
photoSize#77bfb61b type:string location:FileLocation w:int h:int size:int = PhotoSize;
|
||||
|
@ -211,7 +212,7 @@ inputPeerNotifySettings#9c3d198e flags:# show_previews:flags.0?Bool silent:flags
|
|||
|
||||
peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = PeerNotifySettings;
|
||||
|
||||
peerSettings#818426cd flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true = PeerSettings;
|
||||
peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true geo_distance:flags.6?int = PeerSettings;
|
||||
|
||||
wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper;
|
||||
wallPaperNoFile#8af40b25 flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;
|
||||
|
@ -224,7 +225,7 @@ inputReportReasonOther#e1746d0a text:string = ReportReason;
|
|||
inputReportReasonCopyright#9b89f93a = ReportReason;
|
||||
inputReportReasonGeoIrrelevant#dbd4feed = ReportReason;
|
||||
|
||||
userFull#edf17c12 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int = UserFull;
|
||||
userFull#edf17c12 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int = UserFull;
|
||||
|
||||
contact#f911c994 user_id:int mutual:Bool = Contact;
|
||||
|
||||
|
@ -352,6 +353,11 @@ updateTheme#8216fba3 theme:Theme = Update;
|
|||
updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update;
|
||||
updateLoginToken#564fe691 = Update;
|
||||
updateMessagePollVote#42f88f2c poll_id:long user_id:int options:Vector<bytes> = Update;
|
||||
updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update;
|
||||
updateDialogFilterOrder#a5d72105 order:Vector<int> = Update;
|
||||
updateDialogFilters#3504914f = Update;
|
||||
updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update;
|
||||
updateChannelParticipant#65d2b464 flags:# channel_id:int date:int user_id:int prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant qts:int = Update;
|
||||
|
||||
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
|
||||
|
||||
|
@ -389,7 +395,7 @@ help.inviteText#18cb9f78 message:string = help.InviteText;
|
|||
|
||||
encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat;
|
||||
encryptedChatWaiting#3bf703dc id:int access_hash:long date:int admin_id:int participant_id:int = EncryptedChat;
|
||||
encryptedChatRequested#c878527e id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat;
|
||||
encryptedChatRequested#62718a82 flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat;
|
||||
encryptedChat#fa56ce36 id:int access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long = EncryptedChat;
|
||||
encryptedChatDiscarded#13d6dd27 id:int = EncryptedChat;
|
||||
|
||||
|
@ -416,7 +422,7 @@ inputDocumentEmpty#72f0eaae = InputDocument;
|
|||
inputDocument#1abfb575 id:long access_hash:long file_reference:bytes = InputDocument;
|
||||
|
||||
documentEmpty#36f8c871 id:long = Document;
|
||||
document#9ba29cc1 flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:int thumbs:flags.0?Vector<PhotoSize> dc_id:int attributes:Vector<DocumentAttribute> = Document;
|
||||
document#1e87342b flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:int thumbs:flags.0?Vector<PhotoSize> video_thumbs:flags.1?Vector<VideoSize> dc_id:int attributes:Vector<DocumentAttribute> = Document;
|
||||
|
||||
help.support#17c6b5f6 phone_number:string user:User = help.Support;
|
||||
|
||||
|
@ -502,7 +508,7 @@ messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMess
|
|||
webPageEmpty#eb1477e8 id:long = WebPage;
|
||||
webPagePending#c586da1c id:long date:int = WebPage;
|
||||
webPage#e89c45b2 flags:# id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector<WebPageAttribute> = WebPage;
|
||||
webPageNotModified#85849473 = WebPage;
|
||||
webPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage;
|
||||
|
||||
authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization;
|
||||
|
||||
|
@ -523,11 +529,13 @@ chatInviteExported#fc2e05bc link:string = ExportedChatInvite;
|
|||
|
||||
chatInviteAlready#5a686d7c chat:Chat = ChatInvite;
|
||||
chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector<User> = ChatInvite;
|
||||
chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite;
|
||||
|
||||
inputStickerSetEmpty#ffb62b95 = InputStickerSet;
|
||||
inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet;
|
||||
inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet;
|
||||
inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet;
|
||||
inputStickerSetDice#e67f520e emoticon:string = InputStickerSet;
|
||||
|
||||
stickerSet#eeb46f27 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumb:flags.4?PhotoSize thumb_dc_id:flags.4?int count:int hash:int = StickerSet;
|
||||
|
||||
|
@ -612,11 +620,6 @@ channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector
|
|||
|
||||
help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector<MessageEntity> min_age_confirm:flags.1?int = help.TermsOfService;
|
||||
|
||||
foundGif#162ecc1f url:string thumb_url:string content_url:string content_type:string w:int h:int = FoundGif;
|
||||
foundGifCached#9c750409 url:string photo:Photo document:Document = FoundGif;
|
||||
|
||||
messages.foundGifs#450a1c0a next_offset:int results:Vector<FoundGif> = messages.FoundGifs;
|
||||
|
||||
messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs;
|
||||
messages.savedGifs#2e0709a5 hash:int gifs:Vector<Document> = messages.SavedGifs;
|
||||
|
||||
|
@ -645,7 +648,7 @@ messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_off
|
|||
|
||||
exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink;
|
||||
|
||||
messageFwdHeader#ec338270 flags:# from_id:flags.0?int from_name:flags.5?string date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int = MessageFwdHeader;
|
||||
messageFwdHeader#353a686b flags:# from_id:flags.0?int from_name:flags.5?string date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int psa_type:flags.6?string = MessageFwdHeader;
|
||||
|
||||
auth.codeTypeSms#72a3158c = auth.CodeType;
|
||||
auth.codeTypeCall#741cd3e3 = auth.CodeType;
|
||||
|
@ -686,8 +689,8 @@ contacts.topPeersDisabled#b52c939d = contacts.TopPeers;
|
|||
draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage;
|
||||
draftMessage#fd8e711f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int message:string entities:flags.3?Vector<MessageEntity> date:int = DraftMessage;
|
||||
|
||||
messages.featuredStickersNotModified#4ede3cf = messages.FeaturedStickers;
|
||||
messages.featuredStickers#f89d88e5 hash:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;
|
||||
messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers;
|
||||
messages.featuredStickers#b6abc341 hash:int count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;
|
||||
|
||||
messages.recentStickersNotModified#b17f890 = messages.RecentStickers;
|
||||
messages.recentStickers#22f3afb3 hash:int packs:Vector<StickerPack> stickers:Vector<Document> dates:Vector<int> = messages.RecentStickers;
|
||||
|
@ -815,15 +818,16 @@ inputStickerSetItem#ffa0a496 flags:# document:InputDocument emoji:string mask_co
|
|||
inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall;
|
||||
|
||||
phoneCallEmpty#5366c915 id:long = PhoneCall;
|
||||
phoneCallWaiting#1b8f4ad1 flags:# video:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall;
|
||||
phoneCallRequested#87eabb53 flags:# video:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall;
|
||||
phoneCallAccepted#997c454a flags:# video:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int g_b:bytes protocol:PhoneCallProtocol = PhoneCall;
|
||||
phoneCall#8742ae7f flags:# p2p_allowed:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector<PhoneConnection> start_date:int = PhoneCall;
|
||||
phoneCallDiscarded#50ca4de1 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.5?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = PhoneCall;
|
||||
phoneCallWaiting#1b8f4ad1 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall;
|
||||
phoneCallRequested#87eabb53 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall;
|
||||
phoneCallAccepted#997c454a flags:# video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int g_b:bytes protocol:PhoneCallProtocol = PhoneCall;
|
||||
phoneCall#8742ae7f flags:# p2p_allowed:flags.5?true video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector<PhoneConnection> start_date:int = PhoneCall;
|
||||
phoneCallDiscarded#50ca4de1 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.6?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = PhoneCall;
|
||||
|
||||
phoneConnection#9d4c17c0 id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection;
|
||||
phoneConnectionWebrtc#635fe375 flags:# turn:flags.0?true stun:flags.1?true id:long ip:string ipv6:string port:int username:string password:string = PhoneConnection;
|
||||
|
||||
phoneCallProtocol#a2bb35cb flags:# udp_p2p:flags.0?true udp_reflector:flags.1?true min_layer:int max_layer:int = PhoneCallProtocol;
|
||||
phoneCallProtocol#fc878fc8 flags:# udp_p2p:flags.0?true udp_reflector:flags.1?true min_layer:int max_layer:int library_versions:Vector<string> = PhoneCallProtocol;
|
||||
|
||||
phone.phoneCall#ec82e140 phone_call:PhoneCall users:Vector<User> = phone.PhoneCall;
|
||||
|
||||
|
@ -906,9 +910,6 @@ fileHash#6242c773 offset:int limit:int hash:bytes = FileHash;
|
|||
|
||||
inputClientProxy#75588b3f address:string port:int = InputClientProxy;
|
||||
|
||||
help.proxyDataEmpty#e09e1fb8 expires:int = help.ProxyData;
|
||||
help.proxyDataPromo#2bf7ee23 expires:int peer:Peer chats:Vector<Chat> users:Vector<User> = help.ProxyData;
|
||||
|
||||
help.termsOfServiceUpdateEmpty#e3309f7f expires:int = help.TermsOfServiceUpdate;
|
||||
help.termsOfServiceUpdate#28ecf961 expires:int terms_of_service:help.TermsOfService = help.TermsOfServiceUpdate;
|
||||
|
||||
|
@ -1009,7 +1010,7 @@ pageListOrderedItemBlocks#98dd8936 num:string blocks:Vector<PageBlock> = PageLis
|
|||
|
||||
pageRelatedArticle#b390dc08 flags:# url:string webpage_id:long title:flags.0?string description:flags.1?string photo_id:flags.2?long author:flags.3?string published_date:flags.4?int = PageRelatedArticle;
|
||||
|
||||
page#ae891bec flags:# part:flags.0?true rtl:flags.1?true v2:flags.2?true url:string blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> = Page;
|
||||
page#98657f0d flags:# part:flags.0?true rtl:flags.1?true v2:flags.2?true url:string blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> views:flags.3?int = Page;
|
||||
|
||||
help.supportName#8c05f1c9 name:string = help.SupportName;
|
||||
|
||||
|
@ -1018,11 +1019,11 @@ help.userInfo#1eb3758 message:string entities:Vector<MessageEntity> author:strin
|
|||
|
||||
pollAnswer#6ca9c2e9 text:string option:bytes = PollAnswer;
|
||||
|
||||
poll#d5529d06 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector<PollAnswer> = Poll;
|
||||
poll#86e18161 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector<PollAnswer> close_period:flags.4?int close_date:flags.5?int = Poll;
|
||||
|
||||
pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters;
|
||||
|
||||
pollResults#c87024a2 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<int> = PollResults;
|
||||
pollResults#badcc1a3 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<int> solution:flags.4?string solution_entities:flags.4?Vector<MessageEntity> = PollResults;
|
||||
|
||||
chatOnlines#f041e250 onlines:int = ChatOnlines;
|
||||
|
||||
|
@ -1116,11 +1117,44 @@ bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl;
|
|||
|
||||
payments.bankCardData#3e24e573 title:string open_urls:Vector<BankCardOpenUrl> = payments.BankCardData;
|
||||
|
||||
dialogFilter#7438f7e8 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string pinned_peers:Vector<InputPeer> include_peers:Vector<InputPeer> exclude_peers:Vector<InputPeer> = DialogFilter;
|
||||
|
||||
dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested;
|
||||
|
||||
statsDateRangeDays#b637edaf min_date:int max_date:int = StatsDateRangeDays;
|
||||
|
||||
statsAbsValueAndPrev#cb43acde current:double previous:double = StatsAbsValueAndPrev;
|
||||
|
||||
statsPercentValue#cbce2fe0 part:double total:double = StatsPercentValue;
|
||||
|
||||
statsGraphAsync#4a27eb2d token:string = StatsGraph;
|
||||
statsGraphError#bedc9822 error:string = StatsGraph;
|
||||
statsGraph#8ea464b6 flags:# json:DataJSON zoom_token:flags.0?string = StatsGraph;
|
||||
|
||||
messageInteractionCounters#ad4fc9bd msg_id:int views:int forwards:int = MessageInteractionCounters;
|
||||
|
||||
stats.broadcastStats#bdf78394 period:StatsDateRangeDays followers:StatsAbsValueAndPrev views_per_post:StatsAbsValueAndPrev shares_per_post:StatsAbsValueAndPrev enabled_notifications:StatsPercentValue growth_graph:StatsGraph followers_graph:StatsGraph mute_graph:StatsGraph top_hours_graph:StatsGraph interactions_graph:StatsGraph iv_interactions_graph:StatsGraph views_by_source_graph:StatsGraph new_followers_by_source_graph:StatsGraph languages_graph:StatsGraph recent_message_interactions:Vector<MessageInteractionCounters> = stats.BroadcastStats;
|
||||
|
||||
help.promoDataEmpty#98f6ac75 expires:int = help.PromoData;
|
||||
help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector<Chat> users:Vector<User> psa_type:flags.1?string psa_message:flags.2?string = help.PromoData;
|
||||
|
||||
videoSize#e831c556 flags:# type:string location:FileLocation w:int h:int size:int video_start_ts:flags.0?double = VideoSize;
|
||||
|
||||
statsGroupTopPoster#18f3d0f7 user_id:int messages:int avg_chars:int = StatsGroupTopPoster;
|
||||
|
||||
statsGroupTopAdmin#6014f412 user_id:int deleted:int kicked:int banned:int = StatsGroupTopAdmin;
|
||||
|
||||
statsGroupTopInviter#31962a4c user_id:int invitations:int = StatsGroupTopInviter;
|
||||
|
||||
stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector<StatsGroupTopPoster> top_admins:Vector<StatsGroupTopAdmin> top_inviters:Vector<StatsGroupTopInviter> users:Vector<User> = stats.MegagroupStats;
|
||||
|
||||
globalPrivacySettings#bea2f424 flags:# archive_and_mute_new_noncontact_peers:flags.0?Bool = GlobalPrivacySettings;
|
||||
|
||||
---functions---
|
||||
|
||||
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
|
||||
invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;
|
||||
initConnection#785188b8 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy query:!X = X;
|
||||
initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;
|
||||
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
|
||||
invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;
|
||||
invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X;
|
||||
|
@ -1210,6 +1244,8 @@ account.getThemes#285946f8 format:string hash:int = account.Themes;
|
|||
account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool;
|
||||
account.getContentSettings#8b9b4dae = account.ContentSettings;
|
||||
account.getMultiWallPapers#65ad71dc wallpapers:Vector<InputWallPaper> = Vector<WallPaper>;
|
||||
account.getGlobalPrivacySettings#eb2b4cf6 = GlobalPrivacySettings;
|
||||
account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = GlobalPrivacySettings;
|
||||
|
||||
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
||||
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
|
||||
|
@ -1285,7 +1321,6 @@ messages.migrateChat#15a3b8e3 chat_id:int = Updates;
|
|||
messages.searchGlobal#bf7225a4 flags:# folder_id:flags.0?int q:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||
messages.reorderStickerSets#78337739 flags:# masks:flags.0?true order:Vector<long> = Bool;
|
||||
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
|
||||
messages.searchGifs#bf9a776b q:string offset:int = messages.FoundGifs;
|
||||
messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs;
|
||||
messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;
|
||||
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
|
||||
|
@ -1354,13 +1389,18 @@ messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates;
|
|||
messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector<int> = Updates;
|
||||
messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList;
|
||||
messages.toggleStickerSets#b5052fea flags:# uninstall:flags.0?true archive:flags.1?true unarchive:flags.2?true stickersets:Vector<InputStickerSet> = Bool;
|
||||
messages.getDialogFilters#f19ed96d = Vector<DialogFilter>;
|
||||
messages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;
|
||||
messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;
|
||||
messages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool;
|
||||
messages.getOldFeaturedStickers#5fe7025b offset:int limit:int hash:int = messages.FeaturedStickers;
|
||||
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||
|
||||
photos.updateProfilePhoto#f0bb5152 id:InputPhoto = UserProfilePhoto;
|
||||
photos.uploadProfilePhoto#4f32c098 file:InputFile = photos.Photo;
|
||||
photos.updateProfilePhoto#72d4742c id:InputPhoto = photos.Photo;
|
||||
photos.uploadProfilePhoto#89f30f69 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo;
|
||||
photos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
|
||||
photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;
|
||||
|
||||
|
@ -1382,7 +1422,6 @@ help.getAppChangelog#9010ef6f prev_app_version:string = Updates;
|
|||
help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool;
|
||||
help.getCdnConfig#52029342 = CdnConfig;
|
||||
help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls;
|
||||
help.getProxyData#3d7758e1 = help.ProxyData;
|
||||
help.getTermsOfServiceUpdate#2ca51fd1 = help.TermsOfServiceUpdate;
|
||||
help.acceptTermsOfService#ee72f79a id:DataJSON = Bool;
|
||||
help.getDeepLinkInfo#3fedc75f path:string = help.DeepLinkInfo;
|
||||
|
@ -1392,6 +1431,9 @@ help.getPassportConfig#c661ad08 hash:int = help.PassportConfig;
|
|||
help.getSupportName#d360e72c = help.SupportName;
|
||||
help.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo;
|
||||
help.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector<MessageEntity> = help.UserInfo;
|
||||
help.getPromoData#c0977421 = help.PromoData;
|
||||
help.hidePromoData#1e251c95 peer:InputPeer = Bool;
|
||||
help.dismissSuggestion#77fa99f suggestion:string = Bool;
|
||||
|
||||
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
|
||||
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
|
||||
|
@ -1431,6 +1473,7 @@ channels.getInactiveChannels#11e831ee = messages.InactiveChats;
|
|||
|
||||
bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;
|
||||
bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;
|
||||
bots.setBotCommands#805d46f6 commands:Vector<BotCommand> = Bool;
|
||||
|
||||
payments.getPaymentForm#99f09745 msg_id:int = payments.PaymentForm;
|
||||
payments.getPaymentReceipt#a092a980 msg_id:int = payments.PaymentReceipt;
|
||||
|
@ -1440,10 +1483,11 @@ payments.getSavedInfo#227d824b = payments.SavedInfo;
|
|||
payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool;
|
||||
payments.getBankCardData#2e79d779 number:string = payments.BankCardData;
|
||||
|
||||
stickers.createStickerSet#9bd86e6a flags:# masks:flags.0?true user_id:InputUser title:string short_name:string stickers:Vector<InputStickerSetItem> = messages.StickerSet;
|
||||
stickers.createStickerSet#f1036780 flags:# masks:flags.0?true animated:flags.1?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector<InputStickerSetItem> = messages.StickerSet;
|
||||
stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet;
|
||||
stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet;
|
||||
stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet;
|
||||
stickers.setStickerSetThumb#9a364e30 stickerset:InputStickerSet thumb:InputDocument = messages.StickerSet;
|
||||
|
||||
phone.getCallConfig#55451fa9 = DataJSON;
|
||||
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||
|
@ -1453,6 +1497,7 @@ phone.receivedCall#17d54f61 peer:InputPhoneCall = Bool;
|
|||
phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall duration:int reason:PhoneCallDiscardReason connection_id:long = Updates;
|
||||
phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates;
|
||||
phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;
|
||||
phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool;
|
||||
|
||||
langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference;
|
||||
langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<string> = Vector<LangPackString>;
|
||||
|
@ -1463,4 +1508,8 @@ langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLangua
|
|||
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
|
||||
folders.deleteFolder#1c295881 folder_id:int = Updates;
|
||||
|
||||
// LAYER 110
|
||||
stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
|
||||
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
|
||||
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;
|
||||
|
||||
// LAYER 117
|
||||
|
|
|
@ -15,9 +15,13 @@ AUTH_KEY_INVALID,401,The key is invalid
|
|||
AUTH_KEY_PERM_EMPTY,401,"The method is unavailable for temporary authorization key, not bound to permanent"
|
||||
AUTH_KEY_UNREGISTERED,401,The key is not registered in the system
|
||||
AUTH_RESTART,500,Restart the authorization process
|
||||
AUTH_TOKEN_ALREADY_ACCEPTED,400,The authorization token was already used
|
||||
AUTH_TOKEN_EXPIRED,400,The provided authorization token has expired and the updated QR-code must be re-scanned
|
||||
AUTH_TOKEN_INVALID,400,An invalid authorization token was provided
|
||||
BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default"
|
||||
BOTS_TOO_MUCH,400,There are too many bots in this chat/channel
|
||||
BOT_CHANNELS_NA,400,Bots can't edit admin privileges
|
||||
BOT_COMMAND_DESCRIPTION_INVALID,400,"The command description was empty, too long or had invalid characters used"
|
||||
BOT_GROUPS_BLOCKED,400,This bot can't be added to groups
|
||||
BOT_INLINE_DISABLED,400,This bot can't be used in inline mode
|
||||
BOT_INVALID,400,This is not a valid bot
|
||||
|
@ -25,8 +29,11 @@ BOT_METHOD_INVALID,400,The API access for bot users is restricted. The method yo
|
|||
BOT_MISSING,400,This method can only be run by a bot
|
||||
BOT_PAYMENTS_DISABLED,400,This method can only be run by a bot
|
||||
BOT_POLLS_DISABLED,400,You cannot create polls under a bot account
|
||||
BOT_RESPONSE_TIMEOUT,400,The bot did not answer to the callback query in time
|
||||
BROADCAST_FORBIDDEN,403,The request cannot be used in broadcast channels
|
||||
BROADCAST_ID_INVALID,400,The channel is invalid
|
||||
BROADCAST_PUBLIC_VOTERS_FORBIDDEN,400,You cannot broadcast polls where the voters are public
|
||||
BROADCAST_REQUIRED,400,The request can only be used with a broadcast channel
|
||||
BUTTON_DATA_INVALID,400,The provided button data is invalid
|
||||
BUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid
|
||||
BUTTON_URL_INVALID,400,Button URL invalid
|
||||
|
@ -67,7 +74,9 @@ CONNECTION_LANG_PACK_INVALID,400,"The specified language pack is not valid. This
|
|||
CONNECTION_LAYER_INVALID,400,The very first request must always be InvokeWithLayerRequest
|
||||
CONNECTION_NOT_INITED,400,Connection not initialized
|
||||
CONNECTION_SYSTEM_EMPTY,400,Connection system empty
|
||||
CONNECTION_SYSTEM_LANG_CODE_EMPTY,400,The system language string was empty during connection
|
||||
CONTACT_ID_INVALID,400,The provided contact ID is invalid
|
||||
CONTACT_NAME_EMPTY,400,The provided contact name cannot be empty
|
||||
DATA_INVALID,400,Encrypted data invalid
|
||||
DATA_JSON_INVALID,400,The provided JSON data is invalid
|
||||
DATE_EMPTY,400,Date empty
|
||||
|
@ -77,6 +86,7 @@ EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it
|
|||
EMAIL_INVALID,400,The given email is invalid
|
||||
EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}"
|
||||
EMOTICON_EMPTY,400,The emoticon field cannot be empty
|
||||
EMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon
|
||||
ENCRYPTED_MESSAGE_INVALID,400,Encrypted message invalid
|
||||
ENCRYPTION_ALREADY_ACCEPTED,400,Secret chat already accepted
|
||||
ENCRYPTION_ALREADY_DECLINED,400,The secret chat was already declined
|
||||
|
@ -100,6 +110,8 @@ FILE_PART_LENGTH_INVALID,400,The length of a file part is invalid
|
|||
FILE_PART_SIZE_CHANGED,400,The file part size (chunk size) cannot change during upload
|
||||
FILE_PART_SIZE_INVALID,400,The provided file part size is invalid
|
||||
FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage
|
||||
FILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty
|
||||
FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent
|
||||
FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again
|
||||
FIRSTNAME_INVALID,400,The first name is invalid
|
||||
FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers
|
||||
|
@ -107,6 +119,7 @@ FLOOD_WAIT_X,420,A wait of {seconds} seconds is required
|
|||
FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty
|
||||
FOLDER_ID_INVALID,400,The folder you tried to use was not valid
|
||||
FRESH_CHANGE_ADMINS_FORBIDDEN,400,Recently logged-in users cannot add or change admins
|
||||
FRESH_CHANGE_PHONE_FORBIDDEN,406,Recently logged-in users cannot use this request
|
||||
FRESH_RESET_AUTHORISATION_FORBIDDEN,406,The current session is too new and cannot be used to reset other authorisations yet
|
||||
GAME_BOT_INVALID,400,You cannot send that game with the current bot
|
||||
GIF_ID_INVALID,400,The provided GIF ID is invalid
|
||||
|
@ -114,6 +127,7 @@ GROUPED_MEDIA_INVALID,400,Invalid grouped media
|
|||
HASH_INVALID,400,The provided hash is invalid
|
||||
HISTORY_GET_FAILED,500,Fetching of history failed
|
||||
IMAGE_PROCESS_FAILED,400,Failure while processing image
|
||||
INLINE_BOT_REQUIRED,403,The action must be performed through an inline bot callback
|
||||
INLINE_RESULT_EXPIRED,400,The inline query expired
|
||||
INPUT_CONSTRUCTOR_INVALID,400,The provided constructor is invalid
|
||||
INPUT_FETCH_ERROR,,An error occurred while deserializing TL parameters
|
||||
|
@ -142,6 +156,7 @@ MEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as
|
|||
MEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes)
|
||||
MEGAGROUP_ID_INVALID,400,The group is invalid
|
||||
MEGAGROUP_PREHISTORY_HIDDEN,400,You can't set this discussion group because it's history is hidden
|
||||
MEGAGROUP_REQUIRED,400,The request can only be used with a megagroup channel
|
||||
MEMBER_NO_LOCATION,500,An internal failure occurred while fetching user info (couldn't find location)
|
||||
MEMBER_OCCUPY_PRIMARY_LOC_FAILED,500,Occupation of primary member location failed
|
||||
MESSAGE_AUTHOR_REQUIRED,403,Message author required
|
||||
|
@ -153,6 +168,7 @@ MESSAGE_ID_INVALID,400,"The specified message ID is invalid or you can't do that
|
|||
MESSAGE_NOT_MODIFIED,400,Content of the message was not modified
|
||||
MESSAGE_POLL_CLOSED,400,The poll was closed and can no longer be voted on
|
||||
MESSAGE_TOO_LONG,400,Message was too long. Current maximum length is 4096 UTF-8 characters
|
||||
METHOD_INVALID,400,The API method is invalid and cannot be used
|
||||
MSGID_DECREASE_RETRY,500,The request should be retried with a lower message ID
|
||||
MSG_ID_INVALID,400,The message ID used in the peer was invalid
|
||||
MSG_WAIT_FAILED,400,A waiting call returned an error
|
||||
|
@ -173,7 +189,9 @@ PARTICIPANT_CALL_FAILED,500,Failure while making call
|
|||
PARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to date telegram client with support for calls
|
||||
PASSWORD_EMPTY,400,The provided password is empty
|
||||
PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid
|
||||
PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a password) before this method can be used
|
||||
PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used
|
||||
PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method
|
||||
PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid
|
||||
PEER_FLOOD,,Too many requests
|
||||
PEER_ID_INVALID,400,An invalid Peer was used. Make sure to pass the right peer type
|
||||
|
@ -208,6 +226,7 @@ POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too lon
|
|||
POLL_QUESTION_INVALID,400,The poll question was either empty or too long
|
||||
POLL_UNSUPPORTED,400,This layer does not support polls in the issued method
|
||||
PRIVACY_KEY_INVALID,400,The privacy key is invalid
|
||||
PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request
|
||||
PTS_CHANGE_EMPTY,500,No PTS change
|
||||
QUERY_ID_EMPTY,400,The query ID is empty
|
||||
QUERY_ID_INVALID,400,The query ID is invalid
|
||||
|
@ -234,6 +253,7 @@ RPC_MCGET_FAIL,,"Telegram is having internal issues, please try again later."
|
|||
RSA_DECRYPT_FAILED,400,Internal RSA decryption failed
|
||||
SCHEDULE_BOT_NOT_ALLOWED,400,Bots are not allowed to schedule messages
|
||||
SCHEDULE_DATE_TOO_LATE,400,The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours)
|
||||
SCHEDULE_STATUS_PRIVATE,400,You cannot schedule a message until the person comes online if their privacy does not show this information
|
||||
SCHEDULE_TOO_MUCH,400,You cannot schedule more messages in this chat (last known limit of 100 per chat)
|
||||
SEARCH_QUERY_EMPTY,400,The search query is empty
|
||||
SECONDS_INVALID,400,"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)"
|
||||
|
@ -242,11 +262,13 @@ SEND_MESSAGE_TYPE_INVALID,400,The message type is invalid
|
|||
SESSION_EXPIRED,401,The authorization has expired
|
||||
SESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required
|
||||
SESSION_REVOKED,401,"The authorization has been invalidated, because of the user terminating all sessions"
|
||||
SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method
|
||||
SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid
|
||||
SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name
|
||||
SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat
|
||||
START_PARAM_EMPTY,400,The start parameter is empty
|
||||
START_PARAM_INVALID,400,Start parameter invalid
|
||||
STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc}
|
||||
STICKERSET_INVALID,400,The provided sticker set is invalid
|
||||
STICKERS_EMPTY,400,No sticker provided
|
||||
STICKER_EMOJI_INVALID,400,Sticker emoji invalid
|
||||
|
@ -298,9 +320,11 @@ USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagr
|
|||
USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this
|
||||
USER_RESTRICTED,403,"You're spamreported, you can't create channels or chats."
|
||||
VIDEO_CONTENT_TYPE_INVALID,400,The video content type is not supported with the given parameters (i.e. supports_streaming)
|
||||
VIDEO_FILE_INVALID,400,The given video cannot be used
|
||||
WALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper
|
||||
WALLPAPER_INVALID,400,The input wallpaper was not valid
|
||||
WC_CONVERT_URL_INVALID,400,WC convert URL invalid
|
||||
WEBDOCUMENT_URL_INVALID,400,The given URL cannot be used
|
||||
WEBPAGE_CURL_FAILED,400,Failure while fetching the webpage with cURL
|
||||
WEBPAGE_MEDIA_EMPTY,400,Webpage media empty
|
||||
WORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately
|
||||
|
|
Can't render this file because it has a wrong number of fields in line 103.
|
|
@ -5,24 +5,31 @@ account.changePhone,user,PHONE_NUMBER_INVALID
|
|||
account.checkUsername,user,USERNAME_INVALID
|
||||
account.confirmPasswordEmail,user,
|
||||
account.confirmPhone,user,CODE_HASH_INVALID PHONE_CODE_EMPTY
|
||||
account.createTheme,user,
|
||||
account.deleteSecureValue,user,
|
||||
account.finishTakeoutSession,user,
|
||||
account.getAccountTTL,user,
|
||||
account.getAllSecureValues,user,
|
||||
account.getAuthorizationForm,user,
|
||||
account.getAuthorizations,user,
|
||||
account.getAutoDownloadSettings,user,
|
||||
account.getContactSignUpNotification,user,
|
||||
account.getContentSettings,user,
|
||||
account.getMultiWallPapers,user,
|
||||
account.getNotifyExceptions,user,
|
||||
account.getNotifySettings,user,PEER_ID_INVALID
|
||||
account.getPassword,user,
|
||||
account.getPasswordSettings,user,PASSWORD_HASH_INVALID
|
||||
account.getPrivacy,user,PRIVACY_KEY_INVALID
|
||||
account.getSecureValue,user,
|
||||
account.getTheme,user,
|
||||
account.getThemes,user,
|
||||
account.getTmpPassword,user,PASSWORD_HASH_INVALID TMP_PASSWORD_DISABLED
|
||||
account.getWallPaper,user,WALLPAPER_INVALID
|
||||
account.getWallPapers,user,
|
||||
account.getWebAuthorizations,user,
|
||||
account.initTakeoutSession,user,
|
||||
account.installTheme,user,
|
||||
account.installWallPaper,user,WALLPAPER_INVALID
|
||||
account.registerDevice,user,TOKEN_INVALID
|
||||
account.reportPeer,user,PEER_ID_INVALID
|
||||
|
@ -32,32 +39,40 @@ account.resetNotifySettings,user,
|
|||
account.resetWallPapers,user,
|
||||
account.resetWebAuthorization,user,
|
||||
account.resetWebAuthorizations,user,
|
||||
account.saveAutoDownloadSettings,user,
|
||||
account.saveSecureValue,user,PASSWORD_REQUIRED
|
||||
account.saveTheme,user,
|
||||
account.saveWallPaper,user,WALLPAPER_INVALID
|
||||
account.sendChangePhoneCode,user,PHONE_NUMBER_INVALID
|
||||
account.sendChangePhoneCode,user,FRESH_CHANGE_PHONE_FORBIDDEN PHONE_NUMBER_INVALID
|
||||
account.sendConfirmPhoneCode,user,HASH_INVALID
|
||||
account.sendVerifyEmailCode,user,EMAIL_INVALID
|
||||
account.sendVerifyPhoneCode,user,
|
||||
account.setAccountTTL,user,TTL_DAYS_INVALID
|
||||
account.setContactSignUpNotification,user,
|
||||
account.setPrivacy,user,PRIVACY_KEY_INVALID
|
||||
account.setContentSettings,user,
|
||||
account.setPrivacy,user,PRIVACY_KEY_INVALID PRIVACY_TOO_LONG
|
||||
account.unregisterDevice,user,TOKEN_INVALID
|
||||
account.updateDeviceLocked,user,
|
||||
account.updateNotifySettings,user,PEER_ID_INVALID
|
||||
account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_X NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID
|
||||
account.updateProfile,user,ABOUT_TOO_LONG FIRSTNAME_INVALID
|
||||
account.updateStatus,user,SESSION_PASSWORD_NEEDED
|
||||
account.updateTheme,user,
|
||||
account.updateUsername,user,USERNAME_INVALID USERNAME_NOT_MODIFIED USERNAME_OCCUPIED
|
||||
account.uploadTheme,user,
|
||||
account.uploadWallPaper,user,WALLPAPER_FILE_INVALID
|
||||
account.verifyEmail,user,EMAIL_INVALID
|
||||
account.verifyPhone,user,
|
||||
auth.acceptLoginToken,user,
|
||||
auth.bindTempAuthKey,both,ENCRYPTED_MESSAGE_INVALID INPUT_REQUEST_TOO_LONG TEMP_AUTH_KEY_EMPTY Timeout
|
||||
auth.cancelCode,user,PHONE_NUMBER_INVALID
|
||||
auth.checkPassword,user,PASSWORD_HASH_INVALID
|
||||
auth.dropTempAuthKeys,both,
|
||||
auth.exportAuthorization,both,DC_ID_INVALID
|
||||
auth.exportLoginToken,user,
|
||||
auth.importAuthorization,both,AUTH_BYTES_INVALID USER_ID_INVALID
|
||||
auth.importBotAuthorization,both,ACCESS_TOKEN_EXPIRED ACCESS_TOKEN_INVALID API_ID_INVALID
|
||||
auth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_TOKEN_INVALID
|
||||
auth.logOut,both,
|
||||
auth.recoverPassword,user,CODE_EMPTY
|
||||
auth.requestPasswordRecovery,user,PASSWORD_EMPTY
|
||||
|
@ -68,6 +83,7 @@ auth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NU
|
|||
auth.signUp,user,FIRSTNAME_INVALID MEMBER_OCCUPY_PRIMARY_LOC_FAILED PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_NUMBER_OCCUPIED REG_ID_GENERATE_FAILED
|
||||
bots.answerWebhookJSONQuery,bot,QUERY_ID_INVALID USER_BOT_INVALID
|
||||
bots.sendCustomRequest,bot,USER_BOT_INVALID
|
||||
bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID
|
||||
channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID
|
||||
channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED
|
||||
channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE
|
||||
|
@ -76,6 +92,8 @@ channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORB
|
|||
channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED
|
||||
channels.editAdmin,both,ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED
|
||||
channels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID
|
||||
channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_X SESSION_TOO_FRESH_X
|
||||
channels.editLocation,user,
|
||||
channels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED PHOTO_INVALID
|
||||
channels.editTitle,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED CHAT_NOT_MODIFIED
|
||||
channels.exportMessageLink,user,CHANNEL_INVALID
|
||||
|
@ -83,6 +101,8 @@ channels.getAdminLog,user,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED
|
|||
channels.getAdminedPublicChannels,user,
|
||||
channels.getChannels,both,CHANNEL_INVALID CHANNEL_PRIVATE NEED_CHAT_INVALID
|
||||
channels.getFullChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA Timeout
|
||||
channels.getGroupsForDiscussion,user,
|
||||
channels.getInactiveChannels,user,
|
||||
channels.getLeftChannels,user,
|
||||
channels.getMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_IDS_EMPTY
|
||||
channels.getParticipant,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ID_INVALID USER_NOT_PARTICIPANT
|
||||
|
@ -99,13 +119,15 @@ channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS
|
|||
channels.toggleSignatures,user,CHANNEL_INVALID
|
||||
channels.toggleSlowMode,user,SECONDS_INVALID
|
||||
channels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED
|
||||
contacts.acceptContact,user,
|
||||
contacts.addContact,user,CONTACT_NAME_EMPTY
|
||||
contacts.block,user,CONTACT_ID_INVALID
|
||||
contacts.deleteByPhones,user,
|
||||
contacts.deleteContact,user,CONTACT_ID_INVALID
|
||||
contacts.deleteContacts,user,NEED_MEMBER_INVALID Timeout
|
||||
contacts.getBlocked,user,
|
||||
contacts.getContactIDs,user,
|
||||
contacts.getContacts,user,
|
||||
contacts.getLocated,user,
|
||||
contacts.getSaved,user,TAKEOUT_REQUIRED
|
||||
contacts.getStatuses,user,
|
||||
contacts.getTopPeers,user,TYPES_EMPTY
|
||||
|
@ -116,9 +138,9 @@ contacts.resolveUsername,both,AUTH_KEY_PERM_EMPTY SESSION_PASSWORD_NEEDED USERNA
|
|||
contacts.search,user,QUERY_TOO_SHORT SEARCH_QUERY_EMPTY Timeout
|
||||
contacts.toggleTopPeers,user,
|
||||
contacts.unblock,user,CONTACT_ID_INVALID
|
||||
contest.saveDeveloperInfo,both,
|
||||
folders.deleteFolder,user,FOLDER_ID_EMPTY
|
||||
folders.editPeerFolders,user,FOLDER_ID_INVALID
|
||||
getFutureSalts,both,
|
||||
help.acceptTermsOfService,user,
|
||||
help.editUserInfo,user,USER_INVALID
|
||||
help.getAppChangelog,user,
|
||||
|
@ -151,6 +173,7 @@ langpack.getLanguage,user,
|
|||
langpack.getLanguages,user,LANG_PACK_INVALID
|
||||
langpack.getStrings,user,LANG_PACK_INVALID
|
||||
messages.acceptEncryption,user,CHAT_ID_INVALID ENCRYPTION_ALREADY_ACCEPTED ENCRYPTION_ALREADY_DECLINED ENCRYPTION_OCCUPY_FAILED
|
||||
messages.acceptUrlAuth,user,
|
||||
messages.addChatUser,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID USERS_TOO_MUCH USER_ALREADY_PARTICIPANT USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED
|
||||
messages.checkChatInvite,user,INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID
|
||||
messages.clearAllDrafts,user,
|
||||
|
@ -159,6 +182,7 @@ messages.createChat,user,USERS_TOO_FEW USER_RESTRICTED
|
|||
messages.deleteChatUser,both,CHAT_ID_INVALID PEER_ID_INVALID USER_NOT_PARTICIPANT
|
||||
messages.deleteHistory,user,PEER_ID_INVALID
|
||||
messages.deleteMessages,both,MESSAGE_DELETE_FORBIDDEN
|
||||
messages.deleteScheduledMessages,user,
|
||||
messages.discardEncryption,user,CHAT_ID_EMPTY ENCRYPTION_ALREADY_DECLINED ENCRYPTION_ID_INVALID
|
||||
messages.editChatAbout,both,
|
||||
messages.editChatAdmin,user,CHAT_ID_INVALID
|
||||
|
@ -166,8 +190,8 @@ messages.editChatDefaultBannedRights,both,BANNED_RIGHTS_INVALID
|
|||
messages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETCH_FAIL PEER_ID_INVALID PHOTO_EXT_INVALID
|
||||
messages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID
|
||||
messages.editInlineBotMessage,both,MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED
|
||||
messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INPUT_USER_DEACTIVATED MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID
|
||||
messages.exportChatInvite,user,CHAT_ID_INVALID
|
||||
messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID
|
||||
messages.exportChatInvite,both,CHAT_ID_INVALID
|
||||
messages.faveSticker,user,STICKER_ID_INVALID
|
||||
messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
|
||||
messages.getAllChats,user,
|
||||
|
@ -175,13 +199,18 @@ messages.getAllDrafts,user,
|
|||
messages.getAllStickers,user,
|
||||
messages.getArchivedStickers,user,
|
||||
messages.getAttachedStickers,user,
|
||||
messages.getBotCallbackAnswer,user,CHANNEL_INVALID DATA_INVALID MESSAGE_ID_INVALID PEER_ID_INVALID Timeout
|
||||
messages.getBotCallbackAnswer,user,BOT_RESPONSE_TIMEOUT CHANNEL_INVALID DATA_INVALID MESSAGE_ID_INVALID PEER_ID_INVALID Timeout
|
||||
messages.getChats,both,CHAT_ID_INVALID PEER_ID_INVALID
|
||||
messages.getCommonChats,user,USER_ID_INVALID
|
||||
messages.getDhConfig,user,RANDOM_LENGTH_INVALID
|
||||
messages.getDialogFilters,user,
|
||||
messages.getDialogUnreadMarks,user,
|
||||
messages.getDialogs,user,INPUT_CONSTRUCTOR_INVALID OFFSET_PEER_ID_INVALID SESSION_PASSWORD_NEEDED Timeout
|
||||
messages.getDocumentByHash,both,SHA256_HASH_INVALID
|
||||
messages.getEmojiKeywords,user,
|
||||
messages.getEmojiKeywordsDifference,user,
|
||||
messages.getEmojiKeywordsLanguages,user,
|
||||
messages.getEmojiURL,user,
|
||||
messages.getFavedStickers,user,
|
||||
messages.getFeaturedStickers,user,
|
||||
messages.getFullChat,both,CHAT_ID_INVALID PEER_ID_INVALID
|
||||
|
@ -198,17 +227,22 @@ messages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID
|
|||
messages.getPeerSettings,user,CHANNEL_INVALID PEER_ID_INVALID
|
||||
messages.getPinnedDialogs,user,
|
||||
messages.getPollResults,user,
|
||||
messages.getPollVotes,user,BROADCAST_FORBIDDEN
|
||||
messages.getRecentLocations,user,
|
||||
messages.getRecentStickers,user,
|
||||
messages.getSavedGifs,user,
|
||||
messages.getScheduledHistory,user,
|
||||
messages.getScheduledMessages,user,
|
||||
messages.getSearchCounters,user,
|
||||
messages.getSplitRanges,user,
|
||||
messages.getStatsURL,user,
|
||||
messages.getStickerSet,both,STICKERSET_INVALID
|
||||
messages.getStickers,user,EMOTICON_EMPTY
|
||||
messages.getSuggestedDialogFilters,user,
|
||||
messages.getUnreadMentions,user,PEER_ID_INVALID
|
||||
messages.getWebPage,user,WC_CONVERT_URL_INVALID
|
||||
messages.getWebPagePreview,user,
|
||||
messages.hideReportSpam,user,PEER_ID_INVALID
|
||||
messages.hidePeerSettingsBar,user,
|
||||
messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT
|
||||
messages.installStickerSet,user,STICKERSET_INVALID
|
||||
messages.markDialogUnread,user,
|
||||
|
@ -226,37 +260,42 @@ messages.report,user,
|
|||
messages.reportEncryptedSpam,user,CHAT_ID_INVALID
|
||||
messages.reportSpam,user,PEER_ID_INVALID
|
||||
messages.requestEncryption,user,DH_G_A_INVALID USER_ID_INVALID
|
||||
messages.requestUrlAuth,user,
|
||||
messages.saveDraft,user,PEER_ID_INVALID
|
||||
messages.saveGif,user,GIF_ID_INVALID
|
||||
messages.saveRecentSticker,user,STICKER_ID_INVALID
|
||||
messages.search,user,CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID PEER_ID_NOT_SUPPORTED SEARCH_QUERY_EMPTY USER_ID_INVALID
|
||||
messages.searchGifs,user,SEARCH_QUERY_EMPTY
|
||||
messages.searchGifs,user,METHOD_INVALID SEARCH_QUERY_EMPTY
|
||||
messages.searchGlobal,user,SEARCH_QUERY_EMPTY
|
||||
messages.searchStickerSets,user,
|
||||
messages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED
|
||||
messages.sendEncryptedFile,user,MSG_WAIT_FAILED
|
||||
messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED
|
||||
messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
|
||||
messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
|
||||
messages.sendMessage,both,AUTH_KEY_DUPLICATED BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
|
||||
messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
|
||||
messages.sendMessage,both,AUTH_KEY_DUPLICATED BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
|
||||
messages.sendMultiMedia,both,SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH
|
||||
messages.sendReaction,User,REACTION_INVALID
|
||||
messages.sendScheduledMessages,user,
|
||||
messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID
|
||||
messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID
|
||||
messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY
|
||||
messages.setBotShippingResults,both,QUERY_ID_INVALID
|
||||
messages.setEncryptedTyping,user,CHAT_ID_INVALID
|
||||
messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED
|
||||
messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID USER_BOT_INVALID
|
||||
messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID USER_BOT_INVALID WEBDOCUMENT_URL_INVALID
|
||||
messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED
|
||||
messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT
|
||||
messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID
|
||||
messages.toggleDialogPin,user,PEER_ID_INVALID
|
||||
messages.toggleStickerSets,user,
|
||||
messages.uninstallStickerSet,user,STICKERSET_INVALID
|
||||
messages.updateDialogFilter,user,
|
||||
messages.updateDialogFiltersOrder,user,
|
||||
messages.updatePinnedMessage,both,
|
||||
messages.uploadEncryptedFile,user,
|
||||
messages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID
|
||||
payments.clearSavedInfo,user,
|
||||
payments.getBankCardData,user,
|
||||
payments.getPaymentForm,user,MESSAGE_ID_INVALID
|
||||
payments.getPaymentReceipt,user,MESSAGE_ID_INVALID
|
||||
payments.getSavedInfo,user,
|
||||
|
@ -273,17 +312,21 @@ phone.setCallRating,user,CALL_PEER_INVALID
|
|||
photos.deletePhotos,user,
|
||||
photos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID
|
||||
photos.updateProfilePhoto,user,
|
||||
photos.uploadProfilePhoto,user,FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID
|
||||
photos.uploadProfilePhoto,user,FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID VIDEO_FILE_INVALID
|
||||
ping,both,
|
||||
reqDHParams,both,
|
||||
reqPq,both,
|
||||
reqPqMulti,both,
|
||||
rpcDropAnswer,both,
|
||||
setClientDHParams,both,
|
||||
stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED STATS_MIGRATE_X
|
||||
stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X
|
||||
stats.loadAsyncGraph,user,
|
||||
stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID
|
||||
stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID
|
||||
stickers.createStickerSet,bot,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG USER_ID_INVALID
|
||||
stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID
|
||||
stickers.setStickerSetThumb,bot,
|
||||
updates.getChannelDifference,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA HISTORY_GET_FAILED PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID PERSISTENT_TIMESTAMP_OUTDATED RANGES_INVALID Timeout
|
||||
updates.getDifference,both,AUTH_KEY_PERM_EMPTY CDN_METHOD_INVALID DATE_EMPTY NEED_MEMBER_INVALID PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID SESSION_PASSWORD_NEEDED STORE_INVALID_SCALAR_TYPE Timeout
|
||||
updates.getState,both,AUTH_KEY_DUPLICATED MSGID_DECREASE_RETRY SESSION_PASSWORD_NEEDED Timeout
|
||||
|
|
|
|
@ -330,7 +330,7 @@ def _write_to_dict(tlobject, builder):
|
|||
|
||||
|
||||
def _write_to_bytes(tlobject, builder):
|
||||
builder.writeln('def __bytes__(self):')
|
||||
builder.writeln('def _bytes(self):')
|
||||
|
||||
# Some objects require more than one flag parameter to be set
|
||||
# at the same time. In this case, add an assertion.
|
||||
|
@ -509,7 +509,7 @@ def _write_arg_to_bytes(builder, arg, args, name=None):
|
|||
|
||||
else:
|
||||
# Else it may be a custom type
|
||||
builder.write('bytes({})', name)
|
||||
builder.write('{}._bytes()', name)
|
||||
|
||||
# If the type is not boxed (i.e. starts with lowercase) we should
|
||||
# not serialize the constructor ID (so remove its first 4 bytes).
|
||||
|
|
|
@ -36,7 +36,8 @@ KNOWN_NAMED_EXAMPLES = {
|
|||
('lang_pack', 'string'): "''",
|
||||
('lang_code', 'string'): "'en'",
|
||||
('chat_id', 'int'): '478614198',
|
||||
('client_id', 'long'): 'random.randrange(-2**63, 2**63)'
|
||||
('client_id', 'long'): 'random.randrange(-2**63, 2**63)',
|
||||
('video', 'InputFile'): "client.upload_file('/path/to/file.mp4')",
|
||||
}
|
||||
|
||||
KNOWN_TYPED_EXAMPLES = {
|
||||
|
|
0
tests/telethon/crypto/__init__.py
Normal file
0
tests/telethon/crypto/__init__.py
Normal file
0
tests/telethon/events/__init__.py
Normal file
0
tests/telethon/events/__init__.py
Normal file
67
tests/telethon/events/test_chataction.py
Normal file
67
tests/telethon/events/test_chataction.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import pytest
|
||||
|
||||
from telethon import TelegramClient, events, types, utils
|
||||
|
||||
|
||||
def get_client():
|
||||
return TelegramClient(None, 1, '1')
|
||||
|
||||
|
||||
def get_user_456():
|
||||
return types.User(
|
||||
id=456,
|
||||
access_hash=789,
|
||||
first_name='User 123'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_input_users_no_action_message_no_entities():
|
||||
event = events.ChatAction.build(types.UpdateChatParticipantDelete(
|
||||
chat_id=123,
|
||||
user_id=456,
|
||||
version=1
|
||||
))
|
||||
event._set_client(get_client())
|
||||
|
||||
assert await event.get_input_users() == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_input_users_no_action_message():
|
||||
user = get_user_456()
|
||||
event = events.ChatAction.build(types.UpdateChatParticipantDelete(
|
||||
chat_id=123,
|
||||
user_id=456,
|
||||
version=1
|
||||
))
|
||||
event._set_client(get_client())
|
||||
event._entities[user.id] = user
|
||||
|
||||
assert await event.get_input_users() == [utils.get_input_peer(user)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_no_action_message_no_entities():
|
||||
event = events.ChatAction.build(types.UpdateChatParticipantDelete(
|
||||
chat_id=123,
|
||||
user_id=456,
|
||||
version=1
|
||||
))
|
||||
event._set_client(get_client())
|
||||
|
||||
assert await event.get_users() == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_users_no_action_message():
|
||||
user = get_user_456()
|
||||
event = events.ChatAction.build(types.UpdateChatParticipantDelete(
|
||||
chat_id=123,
|
||||
user_id=456,
|
||||
version=1
|
||||
))
|
||||
event._set_client(get_client())
|
||||
event._entities[user.id] = user
|
||||
|
||||
assert await event.get_users() == [user]
|
0
tests/telethon/extensions/__init__.py
Normal file
0
tests/telethon/extensions/__init__.py
Normal file
|
@ -1,6 +1,8 @@
|
|||
import io
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
from telethon import utils
|
||||
from telethon.tl.types import (
|
||||
MessageMediaGame, Game, PhotoEmpty
|
||||
|
|
0
tests/telethon/tl/__init__.py
Normal file
0
tests/telethon/tl/__init__.py
Normal file
13
tests/telethon/tl/test_serialization.py
Normal file
13
tests/telethon/tl/test_serialization.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import pytest
|
||||
|
||||
from telethon.tl import types, functions
|
||||
|
||||
|
||||
def test_nested_invalid_serialization():
|
||||
large_long = 2**62
|
||||
request = functions.account.SetPrivacyRequest(
|
||||
key=types.InputPrivacyKeyChatInvite(),
|
||||
rules=[types.InputPrivacyValueDisallowUsers(users=[large_long])]
|
||||
)
|
||||
with pytest.raises(TypeError):
|
||||
bytes(request)
|
13
update-docs.sh
Normal file
13
update-docs.sh
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
python setup.py gen docs
|
||||
rm -rf /tmp/docs
|
||||
mv docs/ /tmp/docs
|
||||
git checkout gh-pages
|
||||
# there's probably better ways but we know none has spaces
|
||||
rm -rf $(ls /tmp/docs)
|
||||
mv /tmp/docs/* .
|
||||
git add constructors/ types/ methods/ index.html js/search.js css/ img/
|
||||
git commit --amend -m "Update documentation"
|
||||
git push --force
|
||||
git checkout master
|
Loading…
Reference in New Issue
Block a user