mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-05 04:30:22 +03:00
Compare commits
No commits in common. "v1" and "v1.23.0" have entirely different histories.
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Create a report about a bug inside the library or issues with the documentation
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Checklist**
|
||||
* [ ] The error is in the library's code, and not in my own.
|
||||
* [ ] I have searched for this issue before posting it and there isn't a duplicate.
|
||||
* [ ] I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/master.zip` and triggered the bug in the latest version.
|
||||
|
||||
**Code that causes the issue**
|
||||
```python
|
||||
from telethon.sync import TelegramClient
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
**Traceback**
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "code.py", line 1, in <code>
|
||||
|
||||
```
|
96
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
96
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
@ -1,96 +0,0 @@
|
|||
name: Bug Report
|
||||
description: Create a report about a bug inside the library.
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: reproducing-example
|
||||
attributes:
|
||||
label: Code that causes the issue
|
||||
description: Provide a code example that reproduces the problem. Try to keep it short without other dependencies.
|
||||
placeholder: |
|
||||
```python
|
||||
from telethon.sync import TelegramClient
|
||||
...
|
||||
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Explain what you should expect to happen. Include reproduction steps.
|
||||
placeholder: |
|
||||
"I was doing... I was expecting the following to happen..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Explain what actually happens.
|
||||
placeholder: |
|
||||
"This happened instead..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: traceback
|
||||
attributes:
|
||||
label: Traceback
|
||||
description: |
|
||||
The traceback, if the problem is a crash.
|
||||
placeholder: |
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "code.py", line 1, in <code>
|
||||
|
||||
```
|
||||
|
||||
- type: input
|
||||
id: telethon-version
|
||||
attributes:
|
||||
label: Telethon version
|
||||
description: The output of `python -c "import telethon; print(telethon.__version__)"`.
|
||||
placeholder: "1.x"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: python-version
|
||||
attributes:
|
||||
label: Python version
|
||||
description: The output of `python --version`.
|
||||
placeholder: "3.x"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system (including distribution name and version)
|
||||
placeholder: Windows 11, macOS 13.4, Ubuntu 23.04...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments. Is it a server? Network condition?
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: The error is in the library's code, and not in my own.
|
||||
required: true
|
||||
- label: I have searched for this issue before posting it and there isn't an open duplicate.
|
||||
required: true
|
||||
- label: I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip` and triggered the bug in the latest version.
|
||||
required: true
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,4 +1,3 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ask questions in StackOverflow
|
||||
url: https://stackoverflow.com/questions/ask?tags=telethon
|
||||
|
|
22
.github/ISSUE_TEMPLATE/documentation-issue.yml
vendored
22
.github/ISSUE_TEMPLATE/documentation-issue.yml
vendored
|
@ -1,22 +0,0 @@
|
|||
name: Documentation Issue
|
||||
description: Report a problem with the documentation.
|
||||
labels: [documentation]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Describe the problem in detail.
|
||||
placeholder: This part is unclear...
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: This is a documentation problem, not a question or a bug report.
|
||||
required: true
|
||||
- label: I have searched for this issue before posting it and there isn't a duplicate.
|
||||
required: true
|
10
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Suggest ideas, changes or other enhancements for the library
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please describe your idea. Would you like another friendly method? Renaming them to something more appropriated? Changing the way something works?
|
22
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
22
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
@ -1,22 +0,0 @@
|
|||
name: Feature Request
|
||||
description: Suggest ideas, changes or other enhancements for the library.
|
||||
labels: [enhancement]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Describe your suggested feature
|
||||
description: Please describe your idea. Would you like another friendly method? Renaming them to something more appropriate? Changing the way something works?
|
||||
placeholder: "It should work like this..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched for this issue before posting it and there isn't a duplicate.
|
||||
required: true
|
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
|
@ -1,5 +0,0 @@
|
|||
<!--
|
||||
Thanks for the PR! Please keep in mind that v1 is *feature frozen*.
|
||||
New features very likely won't be merged, although fixes can be sent.
|
||||
All new development should happen in v2. Thanks!
|
||||
-->
|
|
@ -1,18 +0,0 @@
|
|||
# https://docs.readthedocs.io/en/stable/config-file/v2.html
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
sphinx:
|
||||
configuration: readthedocs/conf.py
|
||||
|
||||
formats:
|
||||
- pdf
|
||||
- epub
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: readthedocs/requirements.txt
|
|
@ -12,8 +12,6 @@ as a user or through a bot account (bot API alternative).
|
|||
|
||||
If you have code using Telethon before its 1.0 version, you must
|
||||
read `Compatibility and Convenience`_ to learn how to migrate.
|
||||
As with any third-party library for Telegram, be careful not to
|
||||
break `Telegram's ToS`_ or `Telegram can ban the account`_.
|
||||
|
||||
What is this?
|
||||
-------------
|
||||
|
@ -77,9 +75,7 @@ useful information.
|
|||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||
.. _MTProto: https://core.telegram.org/mtproto
|
||||
.. _Telegram: https://telegram.org
|
||||
.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html
|
||||
.. _Telegram's ToS: https://core.telegram.org/api/terms
|
||||
.. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library
|
||||
.. _Compatibility and Convenience: https://docs.telethon.dev/en/latest/misc/compatibility-and-convenience.html
|
||||
.. _Read The Docs: https://docs.telethon.dev
|
||||
|
||||
.. |logo| image:: logo.svg
|
||||
|
|
|
@ -3,4 +3,3 @@ pysocks
|
|||
python-socks[asyncio]
|
||||
hachoir
|
||||
pillow
|
||||
isal
|
||||
|
|
|
@ -25,7 +25,7 @@ you can run the following command instead:
|
|||
|
||||
.. code-block:: sh
|
||||
|
||||
python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip
|
||||
python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/master.zip
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ For that, you can use **events**.
|
|||
.. code-block:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s',
|
||||
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
|
||||
level=logging.WARNING)
|
||||
|
||||
|
||||
|
|
|
@ -40,22 +40,22 @@ because tasks are smaller than threads, which are smaller than processes.
|
|||
What are asyncio basics?
|
||||
========================
|
||||
|
||||
The code samples below assume that you have Python 3.7 or greater installed.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# First we need the asyncio library
|
||||
import asyncio
|
||||
|
||||
# Then we need a loop to work with
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# We also need something to run
|
||||
async def main():
|
||||
for char in 'Hello, world!\n':
|
||||
print(char, end='', flush=True)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
# Then, we can create a new asyncio loop and use it to run our coroutine.
|
||||
# The creation and tear-down of the loop is hidden away from us.
|
||||
asyncio.run(main())
|
||||
# Then, we need to run the loop with a task
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
What does telethon.sync do?
|
||||
|
@ -101,7 +101,7 @@ Instead of this:
|
|||
|
||||
# or, using asyncio's default loop (it's the same)
|
||||
import asyncio
|
||||
loop = asyncio.get_running_loop() # == client.loop
|
||||
loop = asyncio.get_event_loop() # == client.loop
|
||||
me = loop.run_until_complete(client.get_me())
|
||||
print(me.username)
|
||||
|
||||
|
@ -158,10 +158,13 @@ loops or use ``async with``:
|
|||
|
||||
print(message.sender.username)
|
||||
|
||||
asyncio.run(main())
|
||||
# ^ this will create a new asyncio loop behind the scenes and tear it down
|
||||
# once the function returns. It will run the loop untiil main finishes.
|
||||
# You should only use this function if there is no other loop running.
|
||||
loop = asyncio.get_event_loop()
|
||||
# ^ this assigns the default event loop from the main thread to a variable
|
||||
|
||||
loop.run_until_complete(main())
|
||||
# ^ this runs the *entire* loop until the main() function finishes.
|
||||
# While the main() function does not finish, the loop will be running.
|
||||
# While the loop is running, you can't run it again.
|
||||
|
||||
|
||||
The ``await`` keyword blocks the *current* task, and the loop can run
|
||||
|
@ -181,14 +184,14 @@ concurrently:
|
|||
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||
print('world') # eventually the loop finishes all tasks
|
||||
|
||||
async def main():
|
||||
asyncio.create_task(world(2)) # create the world task, passing 2 as delay
|
||||
asyncio.create_task(hello(delay=1)) # another task, but with delay 1
|
||||
await asyncio.sleep(3) # wait for three seconds before exiting
|
||||
|
||||
loop = asyncio.get_event_loop() # get the default loop for the main thread
|
||||
loop.create_task(world(2)) # create the world task, passing 2 as delay
|
||||
loop.create_task(hello(delay=1)) # another task, but with delay 1
|
||||
try:
|
||||
# create a new temporary asyncio loop and use it to run main
|
||||
asyncio.run(main())
|
||||
# run the event loop forever; ctrl+c to stop it
|
||||
# we could also run the loop for three seconds:
|
||||
# loop.run_until_complete(asyncio.sleep(3))
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
@ -206,15 +209,10 @@ The same example, but without the comment noise:
|
|||
await asyncio.sleep(delay)
|
||||
print('world')
|
||||
|
||||
async def main():
|
||||
asyncio.create_task(world(2))
|
||||
asyncio.create_task(hello(delay=1))
|
||||
await asyncio.sleep(3)
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(world(2))
|
||||
loop.create_task(hello(1))
|
||||
loop.run_until_complete(asyncio.sleep(3))
|
||||
|
||||
|
||||
Can I use threads?
|
||||
|
@ -252,9 +250,9 @@ You may have seen this error:
|
|||
|
||||
RuntimeError: There is no current event loop in thread 'Thread-1'.
|
||||
|
||||
It just means you didn't create a loop for that thread. Please refer to
|
||||
the ``asyncio`` documentation to correctly learn how to set the event loop
|
||||
for non-main threads.
|
||||
It just means you didn't create a loop for that thread, and if you don't
|
||||
pass a loop when creating the client, it uses ``asyncio.get_event_loop()``,
|
||||
which only works in the main thread.
|
||||
|
||||
|
||||
client.run_until_disconnected() blocks!
|
||||
|
|
|
@ -28,9 +28,6 @@ their own Telegram bots. Quoting their main page:
|
|||
Bot API is simply an HTTP endpoint which translates your requests to it into
|
||||
MTProto calls through tdlib_, their bot backend.
|
||||
|
||||
Configuration of your bot, such as its available commands and auto-completion,
|
||||
is configured through `@BotFather <https://t.me/BotFather>`_.
|
||||
|
||||
|
||||
What is MTProto?
|
||||
================
|
||||
|
@ -148,7 +145,7 @@ After using Telethon:
|
|||
|
||||
Key differences:
|
||||
|
||||
* The recommended way to do it imports fewer things.
|
||||
* The recommended way to do it imports less things.
|
||||
* All handlers trigger by default, so we need ``events.StopPropagation``.
|
||||
* Adding handlers, responding and running is a lot less verbose.
|
||||
* Telethon needs ``async def`` and ``await``.
|
||||
|
@ -299,7 +296,7 @@ After rewriting:
|
|||
|
||||
class Subbot(TelegramClient):
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, **kw)
|
||||
await super().__init__(*a, **kw)
|
||||
self.add_event_handler(self.on_update, events.NewMessage)
|
||||
|
||||
async def connect():
|
||||
|
|
|
@ -268,7 +268,7 @@ That means you can do this:
|
|||
.. code-block:: python
|
||||
|
||||
message.user_id
|
||||
await message.get_input_sender()
|
||||
await message.get_input_user()
|
||||
message.user
|
||||
# ...etc
|
||||
|
||||
|
|
|
@ -150,6 +150,6 @@ You can also except it and act as you prefer:
|
|||
VoIP numbers are very limited, and some countries are more limited too.
|
||||
|
||||
|
||||
.. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_generator/data/errors.csv
|
||||
.. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/master/telethon_generator/data/errors.csv
|
||||
.. _raw API page: https://tl.telethon.dev/
|
||||
.. _messages.sendMessage: https://tl.telethon.dev/methods/messages/send_message.html
|
||||
|
|
|
@ -10,20 +10,13 @@ The Full API
|
|||
methods listed on :ref:`client-ref` unless you have a better reason
|
||||
not to, like a method not existing or you wanting more control.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
The :ref:`telethon-client` doesn't offer a method for every single request
|
||||
the Telegram API supports. However, it's very simple to *call* or *invoke*
|
||||
any request defined in Telegram's API.
|
||||
any request. Whenever you need something, don't forget to `check the documentation`_
|
||||
and look for the `method you need`_. There you can go through a sorted list
|
||||
of everything you can do.
|
||||
|
||||
This section will teach you how to use what Telethon calls the `TL reference`_.
|
||||
The linked page contains a list and a way to search through *all* types
|
||||
generated from the definition of Telegram's API (in ``.tl`` file format,
|
||||
hence the name). These types include requests and constructors.
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -32,193 +25,10 @@ hence the name). These types include requests and constructors.
|
|||
as you type, and a "Copy import" button. If you like namespaces, you
|
||||
can also do ``from telethon.tl import types, functions``. Both work.
|
||||
|
||||
Telegram makes these ``.tl`` files public, which other implementations, such
|
||||
as Telethon, can also use to generate code. These files are versioned under
|
||||
what's called "layers". ``.tl`` files consist of thousands of definitions,
|
||||
and newer layers often add, change, or remove them. Each definition refers
|
||||
to either a Remote Procedure Call (RPC) function, or a type (which the
|
||||
`TL reference`_ calls "constructors", as they construct particular type
|
||||
instances).
|
||||
|
||||
As such, the `TL reference`_ is a good place to go to learn about all possible
|
||||
requests, types, and what they look like. If you're curious about what's been
|
||||
changed between layers, you can refer to the `TL diff`_ site.
|
||||
|
||||
|
||||
Navigating the TL reference
|
||||
===========================
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
"Functions" is the term used for the Remote Procedure Calls (RPC) that can be
|
||||
sent to Telegram to ask it to perform something (e.g. "send message"). These
|
||||
requests have an associated return type. These can be invoked ("called"):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(...)
|
||||
function_instance = SomeRequest(...)
|
||||
|
||||
# Invoke the request
|
||||
returned_type = await client(function_instance)
|
||||
|
||||
Whenever you find the type for a function in the `TL reference`_, the page
|
||||
will contain the following information:
|
||||
|
||||
* What type of account can use the method. This information is regenerated
|
||||
from time to time (by attempting to invoke the function under both account
|
||||
types and finding out where it fails). Some requests can only be used by
|
||||
bot accounts, others by user accounts, and others by both.
|
||||
* The TL definition. This helps you get a feel for the what the function
|
||||
looks like. This is not Python code. It just contains the definition in
|
||||
a concise manner.
|
||||
* "Copy import" button. Does what it says: it will copy the necessary Python
|
||||
code to import the function to your system's clipboard for easy access.
|
||||
* Returns. The returned type. When you invoke the function, this is what the
|
||||
result will be. It also includes which of the constructors can be returned
|
||||
inline, to save you a click.
|
||||
* Parameters. The parameters accepted by the function, including their type,
|
||||
whether they expect a list, and whether they're optional.
|
||||
* Known RPC errors. A best-effort list of known errors the request may cause.
|
||||
This list is not complete and may be out of date, but should provide an
|
||||
overview of what could go wrong.
|
||||
* Example. Autogenerated example, showcasing how you may want to call it.
|
||||
Bear in mind that this is *autogenerated*. It may be spitting out non-sense.
|
||||
The goal of this example is not to show you everything you can do with the
|
||||
request, only to give you a feel for what it looks like to use it.
|
||||
|
||||
It is very important to click through the links and navigate to get the full
|
||||
picture. A specific page will show you what the specific function returns and
|
||||
needs as input parameters. But it may reference other types, so you need to
|
||||
navigate to those to learn what those contain or need.
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
"Types" as understood by TL are not actually generated in Telethon.
|
||||
They would be the "abstract base class" of the constructors, but since Python
|
||||
is duck-typed, there is hardly any need to generate mostly unnecessary code.
|
||||
The page for a type contains:
|
||||
|
||||
* Constructors. Every type will have one or more constructors. These
|
||||
constructors *are* generated and can be immported and used.
|
||||
* Requests returning this type. A helpful way to find out "what requests can
|
||||
return this?". This is how you may learn what request you need to use to
|
||||
obtain a particular instance of a type.
|
||||
* Requests accepting this type as input. A helpful way to find out "what
|
||||
requests can use this type as one of their input parameters?". This is how
|
||||
you may learn where a type is used.
|
||||
* Other types containing this type. A helpful way to find out "where else
|
||||
does this type appear?". This is how you can walk back through nested
|
||||
objects.
|
||||
|
||||
Constructors
|
||||
------------
|
||||
|
||||
Constructors are used to create instances of a particular type, and are also
|
||||
returned when invoking requests. You will have to create instances yourself
|
||||
when invoking requests that need a particular type as input.
|
||||
The page for a constructor contains:
|
||||
|
||||
* Belongs to. The parent type. This is a link back to the types page for the
|
||||
specific constructor. It also contains the sibling constructors inline, to
|
||||
save you a click.
|
||||
* Members. Both the input parameters *and* fields the constructor contains.
|
||||
|
||||
|
||||
Using the TL reference
|
||||
======================
|
||||
|
||||
After you've found a request you want to send, a good start would be to simply
|
||||
copy and paste the autogenerated example into your script. Then you can simply
|
||||
tweak it to your needs.
|
||||
|
||||
If you want to do it from scratch, first, make sure to import the request into
|
||||
your code (either using the "Copy import" button near the top, or by manually
|
||||
spelling out the package under ``telethon.tl.functions.*``).
|
||||
|
||||
Then, start reading the parameters one by one. If the parameter cannot be
|
||||
omitted, you **will** need to specify it, so make sure to spell it out as
|
||||
an input parameter when constructing the request instance. Let's look at
|
||||
`PingRequest`_ for example. First, we copy the import:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions import PingRequest
|
||||
|
||||
Then, we look at the parameters:
|
||||
|
||||
ping_id - long
|
||||
|
||||
A single parameter, and it's a long (a integer number with a large range of
|
||||
values). It doesn't say it can be omitted, so we must provide it, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
PingRequest(
|
||||
ping_id=48641868471
|
||||
)
|
||||
|
||||
(In this case, the ping ID is a random number. You often have to guess what
|
||||
the parameter needs just by looking at the name.)
|
||||
|
||||
Now that we have our request, we can invoke it:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response = await client(PingRequest(
|
||||
ping_id=48641868471
|
||||
))
|
||||
|
||||
To find out what ``response`` looks like, we can do as the autogenerated
|
||||
example suggests and "stringify" the result as a pretty-printed string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(result.stringify())
|
||||
|
||||
This will print out the following:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Pong(
|
||||
msg_id=781875678118,
|
||||
ping_id=48641868471
|
||||
)
|
||||
|
||||
Which is a very easy way to get a feel for a response. You should nearly
|
||||
always print the stringified result, at least once, when trying out requests,
|
||||
to get a feel for what the response may look like.
|
||||
|
||||
But of course, you don't need to do that. Without writing any code, you could
|
||||
have navigated through the "Returns" link to learn ``PingRequest`` returns a
|
||||
``Pong``, which only has one constructor, and the constructor has two members,
|
||||
``msg_id`` and ``ping_id``.
|
||||
|
||||
If you wanted to create your own ``Pong``, you would use both members as input
|
||||
parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_pong = Pong(
|
||||
msg_id=781875678118,
|
||||
ping_id=48641868471
|
||||
)
|
||||
|
||||
(Yes, constructing object instances can use the same code that ``.stringify``
|
||||
would return!)
|
||||
|
||||
And if you wanted to access the ``msg_id`` member, you would simply access it
|
||||
like any other attribute access in Python:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(response.msg_id)
|
||||
|
||||
|
||||
Example walkthrough
|
||||
===================
|
||||
You should also refer to the documentation to see what the objects
|
||||
(constructors) Telegram returns look like. Every constructor inherits
|
||||
from a common type, and that's the reason for this distinction.
|
||||
|
||||
Say `client.send_message()
|
||||
<telethon.client.messages.MessageMethods.send_message>` didn't exist,
|
||||
|
@ -414,7 +224,6 @@ and still access the successful results:
|
|||
# The second request failed.
|
||||
second = e.exceptions[1]
|
||||
|
||||
.. _TL reference: https://tl.telethon.dev
|
||||
.. _TL diff: https://diff.telethon.dev
|
||||
.. _PingRequest: https://tl.telethon.dev/methods/ping.html
|
||||
.. _check the documentation: https://tl.telethon.dev
|
||||
.. _method you need: https://tl.telethon.dev/methods/index.html
|
||||
.. _use the search: https://tl.telethon.dev/?q=message&redirect=no
|
||||
|
|
|
@ -143,7 +143,7 @@ output (likely your terminal).
|
|||
.. warning::
|
||||
|
||||
**Keep this string safe!** Anyone with this string can use it
|
||||
to login into your account and do anything they want to.
|
||||
to login into your account and do anything they want to to do.
|
||||
|
||||
This is similar to leaking your ``*.session`` files online,
|
||||
but it is easier to leak a string than it is to leak a file.
|
||||
|
|
|
@ -191,7 +191,8 @@ so the code above and the following are equivalent:
|
|||
async def main():
|
||||
await client.disconnected
|
||||
|
||||
asyncio.run(main())
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
You could also run `client.disconnected
|
||||
|
@ -206,7 +207,7 @@ Notice that unlike `client.disconnected
|
|||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`,
|
||||
`client.run_until_disconnected
|
||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` will
|
||||
handle ``KeyboardInterrupt`` for you. This method is special and can
|
||||
handle ``KeyboardInterrupt`` with you. This method is special and can
|
||||
also be ran while the loop is running, so you can do this:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
@ -85,7 +85,7 @@ release = version
|
|||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = 'en'
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
|
|
|
@ -25,17 +25,13 @@ so don't store sensitive data here.
|
|||
|
||||
Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and
|
||||
``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would
|
||||
be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five
|
||||
times, in this case, ``22222`` so we can hardcode that:
|
||||
be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated six
|
||||
times, in this case, ``222222`` so we can hardcode that:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(None, api_id, api_hash)
|
||||
client.session.set_dc(2, '149.154.167.40', 80)
|
||||
client.start(
|
||||
phone='9996621234', code_callback=lambda: '22222'
|
||||
phone='9996621234', code_callback=lambda: '222222'
|
||||
)
|
||||
|
||||
Note that Telegram has changed the length of login codes multiple times in the
|
||||
past, so if ``dc_id`` repeated five times does not work, try repeating it six
|
||||
times.
|
||||
|
|
|
@ -84,10 +84,6 @@ use is very straightforward, or :tl:`InviteToChannelRequest` for channels:
|
|||
[users_to_add]
|
||||
))
|
||||
|
||||
Note that this method will only really work for friends or bot accounts.
|
||||
Trying to mass-add users with this approach will not work, and can put both
|
||||
your account and group to risk, possibly being flagged as spam and limited.
|
||||
|
||||
|
||||
Checking a link without joining
|
||||
===============================
|
||||
|
|
|
@ -25,7 +25,7 @@ you should use :tl:`GetFullUser`:
|
|||
# or even
|
||||
full = await client(GetFullUserRequest('username'))
|
||||
|
||||
bio = full.full_user.about
|
||||
bio = full.about
|
||||
|
||||
|
||||
See :tl:`UserFull` to know what other fields you can access.
|
||||
|
|
|
@ -2,12 +2,44 @@
|
|||
Working with messages
|
||||
=====================
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These examples assume you have read :ref:`full-api`.
|
||||
|
||||
This section has been `moved to the wiki`_, where it can be easily edited as new
|
||||
features arrive and the API changes. Please refer to the linked page to learn how
|
||||
to send spoilers, custom emoji, stickers, react to messages, and more things.
|
||||
.. contents::
|
||||
|
||||
.. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages
|
||||
|
||||
Sending stickers
|
||||
================
|
||||
|
||||
Stickers are nothing else than ``files``, and when you successfully retrieve
|
||||
the stickers for a certain sticker set, all you will have are ``handles`` to
|
||||
these files. Remember, the files Telegram holds on their servers can be
|
||||
referenced through this pair of ID/hash (unique per user), and you need to
|
||||
use this handle when sending a "document" message. This working example will
|
||||
send yourself the very first sticker you have:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Get all the sticker sets this user has
|
||||
from telethon.tl.functions.messages import GetAllStickersRequest
|
||||
sticker_sets = await client(GetAllStickersRequest(0))
|
||||
|
||||
# Choose a sticker set
|
||||
from telethon.tl.functions.messages import GetStickerSetRequest
|
||||
from telethon.tl.types import InputStickerSetID
|
||||
sticker_set = sticker_sets.sets[0]
|
||||
|
||||
# Get the stickers for this sticker set
|
||||
stickers = await client(GetStickerSetRequest(
|
||||
stickerset=InputStickerSetID(
|
||||
id=sticker_set.id, access_hash=sticker_set.access_hash
|
||||
)
|
||||
))
|
||||
|
||||
# Stickers are nothing more than files, so send that
|
||||
await client.send_file('me', stickers.documents[0])
|
||||
|
||||
|
||||
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
|
||||
|
|
|
@ -103,6 +103,7 @@ You can also use the menu on the left to quickly skip over sections.
|
|||
:caption: Miscellaneous
|
||||
|
||||
misc/changelog
|
||||
misc/wall-of-shame.rst
|
||||
misc/compatibility-and-convenience
|
||||
|
||||
.. toctree::
|
||||
|
|
|
@ -13,519 +13,6 @@ it can take advantage of new goodies!
|
|||
|
||||
.. contents:: List of All Versions
|
||||
|
||||
New layer (v1.40)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 201 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=199&to=201>`__.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* ``send_as`` and ``effect`` added to ``send_message`` and related methods.
|
||||
* :tl:`MessageMediaGeoLive` is now recognized for auto-input conversion.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Improved wording when using a likely unintended session file.
|
||||
* Improved behaviour for matching Markdown links.
|
||||
* A truly clean update-state is now fetched upon login. This was most notably important for bots.
|
||||
* Time offset is now updated more reliably after connecting. This should fix legitimate "message too old/new" issues.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* :tl:`ChannelParticipantLeft` is now skipped in ``iter_participants``.
|
||||
* ``spoiler`` flag was lost on :tl:`MessageMediaPhoto` auto-input conversion.
|
||||
* :tl:`KeyboardButtonCopy` is now recognized as an inline button.
|
||||
* Downloading web-documents should now work again. Note that this still fetches the file from the original server.
|
||||
|
||||
|
||||
New layer (v1.39)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 199 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=193&to=199>`__.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* ``drop_media_captions`` added to ``forward_messages``, and documented together with ``drop_author``.
|
||||
* :tl:`InputMediaDocumentExternal` is now recognized when sending albums.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* ``receive_updates=False`` now covers more cases, however, Telegram is still free to ignore it.
|
||||
* Better type-hints in several methods.
|
||||
* Markdown parsing of inline links should cover more cases.
|
||||
* ``range`` is now considered "list-like" and can be used on e.g. ``ids`` parameters.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Session is now saved after setting the DC.
|
||||
* Fixed rare crash in entity cache handling when iterating through dialogs.
|
||||
* Fixed IOError that could occur during automatic resizing of some photos.
|
||||
|
||||
|
||||
New layer (v1.38)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 193 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=188&to=193>`__.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Formatting entities misbehaved with albums.
|
||||
* Sending a Message object with a file did not use the new file.
|
||||
|
||||
|
||||
New layer (v1.37)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 188 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=181&to=188>`__.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* Support for CDN downloads should be back. Telethon still prefers no CDN by default.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* ``FloodWaitPremium`` should now be handled like any other floodwaits.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Fixed edge-case when using ``get_messages(..., reverse=True)``.
|
||||
* ``ConnectionError`` when using proxies should be raised properly.
|
||||
|
||||
|
||||
New layer (v1.36)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 181 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=178&to=181>`__.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Certain updates, such as :tl:`UpdateBotStopped`, should now be processed reliably.
|
||||
|
||||
|
||||
New layer (v1.35)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 178 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=173&to=178>`__.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* ``drop_author`` parameter now exposed in ``forward_messages``.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* "Custom secret support" should work with ``TcpMTProxy``.
|
||||
* Some type hints should now be more accurate.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Session path couldn't be a ``pathlib.Path`` or ``None``.
|
||||
* Python versions older than 3.9 should now be supported again.
|
||||
* Readthedocs should hopefully build the v1 documentation again.
|
||||
|
||||
|
||||
New layer (v1.34)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 173 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=167&to=173>`__.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* ``reply_to_chat`` and ``reply_to_sender`` are now in ``Message``.
|
||||
This is useful when you lack access to the chat, but Telegram still included some basic information.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* ``parse_mode`` with a custom instance containing both ``parse`` and ``unparse`` should now work.
|
||||
* Parsing and unparsing message entities should now behave better in certain corner-cases.
|
||||
|
||||
|
||||
New layer (v1.33)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 167 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=166&to=167>`__.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* ``webbrowser`` is now imported conditionally, to support niche environments.
|
||||
* Library should now retry on the suddenly-common ``TimedOutError``.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Sending photos which were automatically resized should work again (included in the v1.32 series).
|
||||
|
||||
|
||||
New layer (v1.32)
|
||||
=================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 166 |
|
||||
+------------------------+
|
||||
|
||||
`View new and changed raw API methods <https://diff.telethon.dev/?from=165&to=166>`__.
|
||||
|
||||
This enables you to use custom languages in preformatted blocks using HTML:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<pre>
|
||||
<code class='language-python'>from telethon import TelegramClient</code>
|
||||
</pre>
|
||||
|
||||
Note that Telethon v1's markdown is a custom format and won't support language tags.
|
||||
If you want to set a custom language, you have to use HTML or a custom formatter.
|
||||
|
||||
|
||||
Dropped imghdr support (v1.31)
|
||||
==============================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 165 |
|
||||
+------------------------+
|
||||
|
||||
This release contains a breaking change in preparation for Python 3.12.
|
||||
If you were sending photos from in-memory ``bytes`` or ``BytesIO`` containing images,
|
||||
you should now use ``BytesIO`` and set the ``.name`` property to a dummy name.
|
||||
This will allow Telethon to detect the correct extension (and file type).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# before
|
||||
image_data = b'...'
|
||||
client.send_file(chat, image_data)
|
||||
|
||||
# after
|
||||
from io import BytesIO
|
||||
image_data = BytesIO(b'...')
|
||||
image_data.name = 'a.jpg' # any name, only the extension matters
|
||||
client.send_file(chat, image_data)
|
||||
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Code generation wasn't working under PyPy.
|
||||
* Obtaining markdown or HTML from message text could produce unexpected results sometimes.
|
||||
* Other fixes for bugs from the previous version, which were already fixed in patch versions.
|
||||
|
||||
Breaking Changes
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* ``imghdr`` is deprecated in newer Python versions, so Telethon no longer uses it.
|
||||
This means there might be some cases where Telethon fails to infer the file extension for buffers containing images.
|
||||
If you were relying on this, add ``.name = 'a.jpg'`` (or other extension) to the ``BytesIO`` buffers you upload.
|
||||
|
||||
Layer bump and small changes (v1.30)
|
||||
====================================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 162 |
|
||||
+------------------------+
|
||||
|
||||
Some of the bug fixes were already present in patch versions of ``v1.29``, but
|
||||
the new layer necessitated a minor bump.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Removed client-side checks for editing messages.
|
||||
This only affects ``Message.edit``, as ``client.edit_message`` already had
|
||||
no checks.
|
||||
* Library should not understand more server-side errors during update handling
|
||||
which should reduce crashes.
|
||||
* Client-side image compression should behave better now.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Some updates such as ``UpdateChatParticipant`` were being missed due to the
|
||||
order in which Telegram sent them. The library now more carefully checks for
|
||||
the sequence and pts contained in them to avoid dropping them.
|
||||
* Fixed ``is_inline`` check for :tl:`KeyboardButtonWebView`.
|
||||
* Fixed some issues getting entity from cache by ID.
|
||||
* ``reply_to`` should now work when sending albums.
|
||||
|
||||
|
||||
More bug fixing (v1.29)
|
||||
=======================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 160 |
|
||||
+------------------------+
|
||||
|
||||
This layer introduces the necessary raw API methods to work with stories.
|
||||
|
||||
The library is aiming to be "feature-frozen" for as long as v1 is active,
|
||||
so friendly client methods are not implemented, but example code to use
|
||||
stories can be found in the GitHub wiki of the project.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Removed client-side checks for methods dealing with chat permissions.
|
||||
In particular, this means you can now ban channels.
|
||||
* Improved some error messages and added new classes for more RPC errors.
|
||||
* The client-side check for valid usernames has been loosened, so that
|
||||
very short premium usernames are no longer considered invalid.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Attempting to download a thumbnail from documnets without one would fail,
|
||||
rather than do nothing (since nothing can be downloaded if there is no thumb).
|
||||
* More errors are caught in the update handling loop.
|
||||
* HTML ``.text`` should now "unparse" any message contents correctly.
|
||||
* Fixed some problems related to logging.
|
||||
* ``comment_to`` should now work as expected with albums.
|
||||
* ``asyncio.CancelledError`` should now correctly propagate from the update loop.
|
||||
* Removed some absolute imports in favour of relative imports.
|
||||
* ``UserUpdate.last_seen`` should now behave correctly.
|
||||
* Fixed a rare ``ValueError`` during ``connect`` if the session cache was bad.
|
||||
|
||||
|
||||
New Layer and housekeeping (v1.28)
|
||||
==================================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 155 |
|
||||
+------------------------+
|
||||
|
||||
Plenty of stale issues closed, as well as improvements for some others.
|
||||
|
||||
Additions
|
||||
~~~~~~~~~
|
||||
|
||||
* New ``entity_cache_limit`` parameter in the ``TelegramClient`` constructor.
|
||||
This should help a bit in keeping memory usage in check.
|
||||
|
||||
Enhancements
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* ``progress_callback`` is now called when dealing with albums. See the
|
||||
documentation on `client.send_file() <telethon.client.uploads.UploadMethods.send_file>`
|
||||
for details.
|
||||
* Update state and entities are now periodically saved, so that the information
|
||||
isn't lost in the case of crash or unexpected script terminations. You should
|
||||
still be calling ``disconnect`` or using the context-manager, though.
|
||||
* The client should no longer unnecessarily call ``get_me`` every time it's started.
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Messages obtained via raw API could not be used in ``forward_messages``.
|
||||
* ``force_sms`` and ``sign_up`` have been deprecated. See `issue 4050`_ for details.
|
||||
It is no longer possible for third-party applications, such as those made with
|
||||
Telethon, to use those features.
|
||||
* ``events.ChatAction`` should now work in more cases in groups with hidden members.
|
||||
* Errors that occur at the connection level should now be properly propagated, so that
|
||||
you can actually have a chance to handle them.
|
||||
* Update handling should be more resilient.
|
||||
* ``PhoneCodeExpiredError`` will correctly clear the stored hash if it occurs in ``sign_in``.
|
||||
* In patch ``v1.28.2``, :tl:`InputBotInlineMessageID64` can now be used
|
||||
to edit inline messages.
|
||||
|
||||
|
||||
.. _issue 4050: https://github.com/LonamiWebs/Telethon/issues/4050
|
||||
|
||||
|
||||
New Layer and some Bug fixes (v1.27)
|
||||
====================================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 152 |
|
||||
+------------------------+
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* When the account is logged-out, the library should now correctly propagate
|
||||
an error through ``run_until_disconnected`` to let you handle it.
|
||||
* The library no longer uses ``asyncio.get_event_loop()`` in newer Python
|
||||
versions, which should get rid of some deprecation warnings.
|
||||
* It could happen that bots would receive messages sent by themselves,
|
||||
very often right after they deleted a message. This should happen far
|
||||
less often now (but might still happen with unlucky timings).
|
||||
* Maximum photo size for automatic image resizing is now larger.
|
||||
* The initial request is now correctly wrapped in ``invokeWithoutUpdates``
|
||||
when updates are disabled after constructing the client instance.
|
||||
* Using a ``pathlib.Path`` to download contacts and web documents should
|
||||
now work correctly.
|
||||
|
||||
New Layer and some Bug fixes (v1.26)
|
||||
====================================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 149 |
|
||||
+------------------------+
|
||||
|
||||
This new layer includes things such as emoji status, more admin log events,
|
||||
forum topics and message reactions, among other things. You can access these
|
||||
using raw API. It also contains a few bug fixes.
|
||||
|
||||
These were fixed in the v1.25 series:
|
||||
|
||||
* ``client.edit_admin`` did not work on small group chats.
|
||||
* ``client.get_messages`` could stop early in some channels.
|
||||
* ``client.download_profile_photo`` now should work even if ``User.min``.
|
||||
* ``client.disconnect`` should no longer hang when being called from within
|
||||
an event handlers.
|
||||
* ``client.get_dialogs`` now initializes the update state for channels.
|
||||
* The message sender should not need to be fetched in more cases.
|
||||
* Lowered the severity of some log messages to be less spammy.
|
||||
|
||||
These are new to v1.26.0:
|
||||
|
||||
* Layer update.
|
||||
* New documented RPC errors.
|
||||
* Sometimes the first message update to a channel could be missed if said
|
||||
message was read immediately.
|
||||
* ``client.get_dialogs`` would fail when the total count evenly divided
|
||||
the chunk size of 100.
|
||||
* ``client.get_messages`` could get stuck during a global search.
|
||||
* Potentially fixed some issues when sending certain videos.
|
||||
* Update handling should be more resilient.
|
||||
* The client should handle having its auth key destroyed more gracefully.
|
||||
* Fixed some issues when logging certain messages.
|
||||
|
||||
|
||||
Bug fixes (v1.25.1)
|
||||
===================
|
||||
|
||||
This version should fix some of the problems that came with the revamped
|
||||
update handling.
|
||||
|
||||
* Some inline URLs were not parsing correctly with markdown.
|
||||
* ``events.Raw`` was handling :tl:`UpdateShort` which it shouldn't do.
|
||||
* ``events.Album`` should now work again.
|
||||
* ``CancelledError`` was being incorrectly logged as a fatal error.
|
||||
* Some fixes to update handling primarly aimed for bot accounts.
|
||||
* Update handling now can deal with more errors without crashing.
|
||||
* Unhandled errors from update handling will now be propagated through
|
||||
``client.run_until_disconnected``.
|
||||
* Invite links with ``+`` are now recognized.
|
||||
* Added new known RPC errors.
|
||||
* ``telethon.types`` could not be used as a module.
|
||||
* 0-length message entities are now stripped to avoid errors.
|
||||
* ``client.send_message`` was not returning a message with ``reply_to``
|
||||
in some cases.
|
||||
* ``aggressive`` in ``client.iter_participants`` now does nothing (it did
|
||||
not really work anymore anyway, and this should prevent other errors).
|
||||
* ``client.iter_participants`` was failing in some groups.
|
||||
* Text with HTML URLs could sometimes fail to parse.
|
||||
* Added a hard timeout during disconnect in order to prevent the program
|
||||
from freezing.
|
||||
|
||||
Please be sure to report issues with update handling if you still encounter
|
||||
some errors!
|
||||
|
||||
|
||||
Update handling overhaul (v1.25)
|
||||
================================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 144 |
|
||||
+------------------------+
|
||||
|
||||
I had plans to release v2 way earlier, but my motivation drained off, so that
|
||||
didn't happen. The reason for another v1 release is that there was a clear
|
||||
need to fix some things regarding update handling (which were present in v2).
|
||||
I did not want to make this release. But with the release date for v2 still
|
||||
being unclear, I find it necessary to release another v1 version. I apologize
|
||||
for the delay (I should've done this a lot sooner but didn't because in my
|
||||
head I would've pushed through and finished v2, but I underestimated how much
|
||||
work that was and I probably experienced burn-out).
|
||||
|
||||
I still don't intend to make new additions to the v1 series (beyond updating
|
||||
the Telegram layer being used). I still have plans to finish v2 some day.
|
||||
But in the meantime, new features, such as reactions, will have to be used
|
||||
through raw API.
|
||||
|
||||
This update also backports the update overhaul from v2. If you experience
|
||||
issues with updates, please report them on the GitHub page for the project.
|
||||
However, this new update handling should be more reliable, and ``catch_up``
|
||||
should actually work properly.
|
||||
|
||||
Breaking Changes
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* In order for ``catch_up`` to work (new flag in the ``TelegramClient``
|
||||
constructor), sessions need to impleemnt the new ``get_update_states``.
|
||||
Third-party session storages won't have this implemented by the time
|
||||
this version released, so ``catch_up`` may not work with those.
|
||||
|
||||
Rushed release to fix login (v1.24)
|
||||
===================================
|
||||
|
||||
+------------------------+
|
||||
| Scheme layer used: 133 |
|
||||
+------------------------+
|
||||
|
||||
This is a rushed release. It contains a layer recent enough to not fail with
|
||||
``UPDATE_APP_TO_LOGIN``, but still not the latest, to avoid breaking more
|
||||
than necessary.
|
||||
|
||||
Breaking Changes
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* The biggest change is user identifiers (and chat identifiers, and others)
|
||||
**now use up to 64 bits**, rather than 32. If you were storing them in some
|
||||
storage with fixed size, you may need to update (such as database tables
|
||||
storing only integers).
|
||||
|
||||
There have been other changes which I currently don't have the time to document.
|
||||
You can refer to the following link to see them early:
|
||||
https://github.com/LonamiWebs/Telethon/compare/v1.23.0...v1.24.0
|
||||
|
||||
|
||||
New schema and bug fixes (v1.23)
|
||||
================================
|
||||
|
||||
|
@ -2479,7 +1966,7 @@ the scenes! This means you're now able to do both of the following:
|
|||
async def main():
|
||||
await client.send_message('me', 'Hello!')
|
||||
|
||||
asyncio.run(main())
|
||||
asyncio.get_event_loop().run_until_complete(main())
|
||||
|
||||
# ...can be rewritten as:
|
||||
|
||||
|
|
|
@ -161,17 +161,19 @@ just get rid of ``telethon.sync`` and work inside an ``async def``:
|
|||
|
||||
await client.run_until_disconnected()
|
||||
|
||||
asyncio.run(main())
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
The ``telethon.sync`` magic module essentially wraps every method behind:
|
||||
The ``telethon.sync`` magic module simply wraps every method behind:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
asyncio.run(main())
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
|
||||
With some other tricks, so that you don't have to write it yourself every time.
|
||||
That's the overhead you pay if you import it, and what you save if you don't.
|
||||
So that you don't have to write it yourself every time. That's the
|
||||
overhead you pay if you import it, and what you save if you don't.
|
||||
|
||||
Learning
|
||||
========
|
||||
|
|
65
readthedocs/misc/wall-of-shame.rst
Normal file
65
readthedocs/misc/wall-of-shame.rst
Normal file
|
@ -0,0 +1,65 @@
|
|||
=============
|
||||
Wall of Shame
|
||||
=============
|
||||
|
||||
|
||||
This project has an
|
||||
`issues <https://github.com/LonamiWebs/Telethon/issues>`__ section for
|
||||
you to file **issues** whenever you encounter any when working with the
|
||||
library. Said section is **not** for issues on *your* program but rather
|
||||
issues with Telethon itself.
|
||||
|
||||
If you have not made the effort to 1. read through the docs and 2.
|
||||
`look for the method you need <https://tl.telethon.dev/>`__,
|
||||
you will end up on the `Wall of
|
||||
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
|
||||
i.e. all issues labeled
|
||||
`"RTFM" <http://www.urbandictionary.com/define.php?term=RTFM>`__:
|
||||
|
||||
**rtfm**
|
||||
Literally "Read The F--king Manual"; a term showing the
|
||||
frustration of being bothered with questions so trivial that the asker
|
||||
could have quickly figured out the answer on their own with minimal
|
||||
effort, usually by reading readily-available documents. People who
|
||||
say"RTFM!" might be considered rude, but the true rude ones are the
|
||||
annoying people who take absolutely no self-responibility and expect to
|
||||
have all the answers handed to them personally.
|
||||
|
||||
*"Damn, that's the twelveth time that somebody posted this question
|
||||
to the messageboard today! RTFM, already!"*
|
||||
|
||||
*by Bill M. July 27, 2004*
|
||||
|
||||
If you have indeed read the docs, and have tried looking for the method,
|
||||
and yet you didn't find what you need, **that's fine**. Telegram's API
|
||||
can have some obscure names at times, and for this reason, there is a
|
||||
`"question"
|
||||
label <https://github.com/LonamiWebs/Telethon/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20label%3Aquestion%20>`__
|
||||
with questions that are okay to ask. Just state what you've tried so
|
||||
that we know you've made an effort, or you'll go to the Wall of Shame.
|
||||
|
||||
Of course, if the issue you're going to open is not even a question but
|
||||
a real issue with the library (thankfully, most of the issues have been
|
||||
that!), you won't end up here. Don't worry.
|
||||
|
||||
Current winner
|
||||
--------------
|
||||
|
||||
The current winner is `issue
|
||||
213 <https://github.com/LonamiWebs/Telethon/issues/213>`__:
|
||||
|
||||
**Issue:**
|
||||
|
||||
.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg
|
||||
|
||||
:alt: Winner issue
|
||||
|
||||
Winner issue
|
||||
|
||||
**Answer:**
|
||||
|
||||
.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg
|
||||
|
||||
:alt: Winner issue answer
|
||||
|
||||
Winner issue answer
|
|
@ -32,6 +32,7 @@ Auth
|
|||
send_code_request
|
||||
sign_in
|
||||
qr_login
|
||||
sign_up
|
||||
log_out
|
||||
edit_2fa
|
||||
|
||||
|
@ -168,7 +169,6 @@ Updates
|
|||
remove_event_handler
|
||||
list_event_handlers
|
||||
catch_up
|
||||
set_receive_updates
|
||||
|
||||
Bots
|
||||
----
|
||||
|
|
|
@ -20,7 +20,7 @@ To enable logging, at the following code to the top of your main file:
|
|||
.. code-block:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s',
|
||||
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
|
||||
level=logging.WARNING)
|
||||
|
||||
You can change the logging level to be something different, from less to more information:
|
||||
|
@ -60,16 +60,6 @@ And except them as such:
|
|||
My account was deleted/limited when using the library
|
||||
=====================================================
|
||||
|
||||
First and foremost, **this is not a problem exclusive to Telethon.
|
||||
Any third-party library is prone to cause the accounts to appear banned.**
|
||||
Even official applications can make Telegram ban an account under certain
|
||||
circumstances. Third-party libraries such as Telethon are a lot easier to
|
||||
use, and as such, they are misused to spam, which causes Telegram to learn
|
||||
certain patterns and ban suspicious activity.
|
||||
|
||||
There is no point in Telethon trying to circumvent this. Even if it succeeded,
|
||||
spammers would then abuse the library again, and the cycle would repeat.
|
||||
|
||||
The library will only do things that you tell it to do. If you use
|
||||
the library with bad intentions, Telegram will hopefully ban you.
|
||||
|
||||
|
@ -77,7 +67,8 @@ However, you may also be part of a limited country, such as Iran or Russia.
|
|||
In that case, we have bad news for you. Telegram is much more likely to ban
|
||||
these numbers, as they are often used to spam other accounts, likely through
|
||||
the use of libraries like this one. The best advice we can give you is to not
|
||||
abuse the API, like calling many requests really quickly.
|
||||
abuse the API, like calling many requests really quickly, and to sign up with
|
||||
these phones through an official application.
|
||||
|
||||
We have also had reports from Kazakhstan and China, where connecting
|
||||
would fail. To solve these connection problems, you should use a proxy.
|
||||
|
@ -85,16 +76,6 @@ would fail. To solve these connection problems, you should use a proxy.
|
|||
Telegram may also ban virtual (VoIP) phone numbers,
|
||||
as again, they're likely to be used for spam.
|
||||
|
||||
More recently (year 2023 onwards), Telegram has started putting a lot more
|
||||
measures to prevent spam (with even additions such as anonymous participants
|
||||
in groups or the inability to fetch group members at all). This means some
|
||||
of the anti-spam measures have gotten more aggressive.
|
||||
|
||||
The recommendation has usually been to use the library only on well-established
|
||||
accounts (and not an account you just created), and to not perform actions that
|
||||
could be seen as abuse. Telegram decides what those actions are, and they're
|
||||
free to change how they operate at any time.
|
||||
|
||||
If you want to check if your account has been limited,
|
||||
simply send a private message to `@SpamBot`_ through Telegram itself.
|
||||
You should notice this by getting errors like ``PeerFloodError``,
|
||||
|
@ -198,137 +179,6 @@ won't do unnecessary work unless you need to:
|
|||
sender = await event.get_sender()
|
||||
|
||||
|
||||
File download is slow or sending files takes too long
|
||||
=====================================================
|
||||
|
||||
The communication with Telegram is encrypted. Encryption requires a lot of
|
||||
math, and doing it in pure Python is very slow. ``cryptg`` is a library which
|
||||
containns the encryption functions used by Telethon. If it is installed (via
|
||||
``pip install cryptg``), it will automatically be used and should provide
|
||||
a considerable speed boost. You can know whether it's used by configuring
|
||||
``logging`` (at ``INFO`` level or lower) *before* importing ``telethon``.
|
||||
|
||||
Note that the library does *not* download or upload files in parallel, which
|
||||
can also help with the speed of downloading or uploading a single file. There
|
||||
are snippets online implementing that. The reason why this is not built-in
|
||||
is because the limiting factor in the long run are ``FloodWaitError``, and
|
||||
using parallel download or uploads only makes them occur sooner.
|
||||
|
||||
|
||||
What does "Server sent a very new message with ID" mean?
|
||||
========================================================
|
||||
|
||||
You may also see this error as "Server sent a very old message with ID".
|
||||
|
||||
This is a security feature from Telethon that cannot be disabled and is
|
||||
meant to protect you against replay attacks.
|
||||
|
||||
When this message is incorrectly reported as a "bug",
|
||||
the most common patterns seem to be:
|
||||
|
||||
* Your system time is incorrect.
|
||||
* The proxy you're using may be interfering somehow.
|
||||
* The Telethon session is being used or has been used from somewhere else.
|
||||
Make sure that you created the session from Telethon, and are not using the
|
||||
same session anywhere else. If you need to use the same account from
|
||||
multiple places, login and use a different session for each place you need.
|
||||
|
||||
|
||||
What does "Server replied with a wrong session ID" mean?
|
||||
========================================================
|
||||
|
||||
This is a security feature from Telethon that cannot be disabled and is
|
||||
meant to protect you against unwanted session reuse.
|
||||
|
||||
When this message is reported as a "bug", the most common patterns seem to be:
|
||||
|
||||
* The proxy you're using may be interfering somehow.
|
||||
* The Telethon session is being used or has been used from somewhere else.
|
||||
Make sure that you created the session from Telethon, and are not using the
|
||||
same session anywhere else. If you need to use the same account from
|
||||
multiple places, login and use a different session for each place you need.
|
||||
* You may be using multiple connections to the Telegram server, which seems
|
||||
to confuse Telegram.
|
||||
|
||||
Most of the time it should be safe to ignore this warning. If the library
|
||||
still doesn't behave correctly, make sure to check if any of the above bullet
|
||||
points applies in your case and try to work around it.
|
||||
|
||||
If the issue persists and there is a way to reliably reproduce this error,
|
||||
please add a comment with any additional details you can provide to
|
||||
`issue 3759`_, and perhaps some additional investigation can be done
|
||||
(but it's unlikely, as Telegram *is* sending unexpected data).
|
||||
|
||||
|
||||
What does "Could not find a matching Constructor ID for the TLObject" mean?
|
||||
===========================================================================
|
||||
|
||||
Telegram uses "layers", which you can think of as "versions" of the API they
|
||||
offer. When Telethon reads responses that the Telegram servers send, these
|
||||
need to be deserialized (into what Telethon calls "TLObjects").
|
||||
|
||||
Every Telethon version understands a single Telegram layer. When Telethon
|
||||
connects to Telegram, both agree on the layer to use. If the layers don't
|
||||
match, Telegram may send certain objects which Telethon no longer understands.
|
||||
|
||||
When this message is reported as a "bug", the most common patterns seem to be
|
||||
that the Telethon session is being used or has been used from somewhere else.
|
||||
Make sure that you created the session from Telethon, and are not using the
|
||||
same session anywhere else. If you need to use the same account from
|
||||
multiple places, login and use a different session for each place you need.
|
||||
|
||||
|
||||
What does "Task was destroyed but it is pending" mean?
|
||||
======================================================
|
||||
|
||||
Your script likely finished abruptly, the ``asyncio`` event loop got
|
||||
destroyed, and the library did not get a chance to properly close the
|
||||
connection and close the session.
|
||||
|
||||
Make sure you're either using the context manager for the client or always
|
||||
call ``await client.disconnect()`` (by e.g. using a ``try/finally``).
|
||||
|
||||
|
||||
What does "The asyncio event loop must not change after connection" mean?
|
||||
=========================================================================
|
||||
|
||||
Telethon uses ``asyncio``, and makes use of things like tasks and queues
|
||||
internally to manage the connection to the server and match responses to the
|
||||
requests you make. Most of them are initialized after the client is connected.
|
||||
|
||||
For example, if the library expects a result to a request made in loop A, but
|
||||
you attempt to get that result in loop B, you will very likely find a deadlock.
|
||||
To avoid a deadlock, the library checks to make sure the loop in use is the
|
||||
same as the one used to initialize everything, and if not, it throws an error.
|
||||
|
||||
The most common cause is ``asyncio.run``, since it creates a new event loop.
|
||||
If you ``asyncio.run`` a function to create the client and set it up, and then
|
||||
you ``asyncio.run`` another function to do work, things won't work, so the
|
||||
library throws an error early to let you know something is wrong.
|
||||
|
||||
Instead, it's often a good idea to have a single ``async def main`` and simply
|
||||
``asyncio.run()`` it and do all the work there. From it, you're also able to
|
||||
call other ``async def`` without having to touch ``asyncio.run`` again:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# It's fine to create the client outside as long as you don't connect
|
||||
client = TelegramClient(...)
|
||||
|
||||
async def main():
|
||||
# Now the client will connect, so the loop must not change from now on.
|
||||
# But as long as you do all the work inside main, including calling
|
||||
# other async functions, things will work.
|
||||
async with client:
|
||||
....
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
||||
Be sure to read the ``asyncio`` documentation if you want a better
|
||||
understanding of event loop, tasks, and what functions you can use.
|
||||
|
||||
|
||||
What does "bases ChatGetter" mean?
|
||||
==================================
|
||||
|
||||
|
@ -354,36 +204,6 @@ Telegram has a lot to offer, and inheritance helps the library reduce
|
|||
boilerplate, so it's important to know this concept. For newcomers,
|
||||
this may be a problem, so we explain what it means here in the FAQ.
|
||||
|
||||
Can I send files by ID?
|
||||
=======================
|
||||
|
||||
When people talk about IDs, they often refer to one of two things:
|
||||
the integer ID inside media, and a random-looking long string.
|
||||
|
||||
You cannot use the integer ID to send media. Generally speaking, sending media
|
||||
requires a combination of ID, ``access_hash`` and ``file_reference``.
|
||||
The first two are integers, while the last one is a random ``bytes`` sequence.
|
||||
|
||||
* The integer ``id`` will always be the same for every account, so every user
|
||||
or bot looking at a particular media file, will see a consistent ID.
|
||||
* The ``access_hash`` will always be the same for a given account, but
|
||||
different accounts will each see their own, different ``access_hash``.
|
||||
This makes it impossible to get media object from one account and use it in
|
||||
another. The other account must fetch the media object itself.
|
||||
* The ``file_reference`` is random for everyone and will only work for a few
|
||||
hours before it expires. It must be refetched before the media can be used
|
||||
(to either resend the media or download it).
|
||||
|
||||
The second type of "`file ID <https://core.telegram.org/bots/api#inputfile>`_"
|
||||
people refer to is a concept from the HTTP Bot API. It's a custom format which
|
||||
encodes enough information to use the media.
|
||||
|
||||
Telethon provides an old version of these HTTP Bot API-style file IDs via
|
||||
``message.file.id``, however, this feature is no longer maintained, so it may
|
||||
not work. It will be removed in future versions. Nonetheless, it is possible
|
||||
to find a different Python package (or write your own) to parse these file IDs
|
||||
and construct the necessary input file objects to send or download the media.
|
||||
|
||||
|
||||
Can I use Flask with the library?
|
||||
=================================
|
||||
|
@ -419,5 +239,4 @@ file and run that, or use the normal ``python`` interpreter.
|
|||
.. _logging: https://docs.python.org/3/library/logging.html
|
||||
.. _@SpamBot: https://t.me/SpamBot
|
||||
.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297
|
||||
.. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759
|
||||
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy
|
||||
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples#quart_loginpy
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
./
|
||||
sphinx-rtd-theme~=1.3.0
|
||||
telethon
|
22
setup.py
22
setup.py
|
@ -16,7 +16,6 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
|
@ -44,8 +43,6 @@ class TempWorkDir:
|
|||
os.chdir(self.original)
|
||||
|
||||
|
||||
API_REF_URL = 'https://tl.telethon.dev/'
|
||||
|
||||
GENERATOR_DIR = Path('telethon_generator')
|
||||
LIBRARY_DIR = Path('telethon')
|
||||
|
||||
|
@ -158,31 +155,14 @@ def main(argv):
|
|||
generate(argv[2:], argv[1])
|
||||
|
||||
elif len(argv) >= 2 and argv[1] == 'pypi':
|
||||
# Make sure tl.telethon.dev is up-to-date first
|
||||
with urllib.request.urlopen(API_REF_URL) as resp:
|
||||
html = resp.read()
|
||||
m = re.search(br'layer\s+(\d+)', html)
|
||||
if not m:
|
||||
print('Failed to check that the API reference is up to date:', API_REF_URL)
|
||||
return
|
||||
|
||||
from telethon_generator.parsers import find_layer
|
||||
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
|
||||
published_layer = int(m[1])
|
||||
if published_layer != layer:
|
||||
print('Published layer', published_layer, 'does not match current layer', layer, '.')
|
||||
print('Make sure to update the API reference site first:', API_REF_URL)
|
||||
return
|
||||
|
||||
# (Re)generate the code to make sure we don't push without it
|
||||
generate(['tl', 'errors'])
|
||||
|
||||
# Try importing the telethon module to assert it has no errors
|
||||
try:
|
||||
import telethon
|
||||
except Exception as e:
|
||||
except:
|
||||
print('Packaging for PyPi aborted, importing the module failed.')
|
||||
print(e)
|
||||
return
|
||||
|
||||
remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info']
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from .client.telegramclient import TelegramClient
|
||||
from .network import connection
|
||||
from .tl import types, functions, custom
|
||||
from .tl.custom import Button
|
||||
from .tl import patched as _ # import for its side-effects
|
||||
from . import version, events, utils, errors, types, functions, custom
|
||||
from . import version, events, utils, errors
|
||||
|
||||
__version__ = version.__version__
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
from .entitycache import EntityCache
|
||||
from .messagebox import MessageBox, GapError, PrematureEndReason
|
||||
from .session import SessionState, ChannelState, Entity, EntityType
|
|
@ -1,59 +0,0 @@
|
|||
from .session import EntityType, Entity
|
||||
|
||||
|
||||
_sentinel = object()
|
||||
|
||||
|
||||
class EntityCache:
|
||||
def __init__(
|
||||
self,
|
||||
hash_map: dict = _sentinel,
|
||||
self_id: int = None,
|
||||
self_bot: bool = None
|
||||
):
|
||||
self.hash_map = {} if hash_map is _sentinel else hash_map
|
||||
self.self_id = self_id
|
||||
self.self_bot = self_bot
|
||||
|
||||
def set_self_user(self, id, bot, hash):
|
||||
self.self_id = id
|
||||
self.self_bot = bot
|
||||
if hash:
|
||||
self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER)
|
||||
|
||||
def get(self, id):
|
||||
try:
|
||||
hash, ty = self.hash_map[id]
|
||||
return Entity(ty, id, hash)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def extend(self, users, chats):
|
||||
# See https://core.telegram.org/api/min for "issues" with "min constructors".
|
||||
self.hash_map.update(
|
||||
(u.id, (
|
||||
u.access_hash,
|
||||
EntityType.BOT if u.bot else EntityType.USER,
|
||||
))
|
||||
for u in users
|
||||
if getattr(u, 'access_hash', None) and not u.min
|
||||
)
|
||||
self.hash_map.update(
|
||||
(c.id, (
|
||||
c.access_hash,
|
||||
EntityType.MEGAGROUP if c.megagroup else (
|
||||
EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL
|
||||
),
|
||||
))
|
||||
for c in chats
|
||||
if getattr(c, 'access_hash', None) and not getattr(c, 'min', None)
|
||||
)
|
||||
|
||||
def put(self, entity):
|
||||
self.hash_map[entity.id] = (entity.hash, entity.ty)
|
||||
|
||||
def retain(self, filter):
|
||||
self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)}
|
||||
|
||||
def __len__(self):
|
||||
return len(self.hash_map)
|
|
@ -1,825 +0,0 @@
|
|||
"""
|
||||
This module deals with correct handling of updates, including gaps, and knowing when the code
|
||||
should "get difference" (the set of updates that the client should know by now minus the set
|
||||
of updates that it actually knows).
|
||||
|
||||
Each chat has its own [`Entry`] in the [`MessageBox`] (this `struct` is the "entry point").
|
||||
At any given time, the message box may be either getting difference for them (entry is in
|
||||
[`MessageBox::getting_diff_for`]) or not. If not getting difference, a possible gap may be
|
||||
found for the updates (entry is in [`MessageBox::possible_gaps`]). Otherwise, the entry is
|
||||
on its happy path.
|
||||
|
||||
Gaps are cleared when they are either resolved on their own (by waiting for a short time)
|
||||
or because we got the difference for the corresponding entry.
|
||||
|
||||
While there are entries for which their difference must be fetched,
|
||||
[`MessageBox::check_deadlines`] will always return [`Instant::now`], since "now" is the time
|
||||
to get the difference.
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import time
|
||||
import logging
|
||||
from enum import Enum
|
||||
from .session import SessionState, ChannelState
|
||||
from ..tl import types as tl, functions as fn
|
||||
from ..helpers import get_running_loop
|
||||
|
||||
|
||||
# Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too.
|
||||
NO_SEQ = 0
|
||||
|
||||
# See https://core.telegram.org/method/updates.getChannelDifference.
|
||||
BOT_CHANNEL_DIFF_LIMIT = 100000
|
||||
USER_CHANNEL_DIFF_LIMIT = 100
|
||||
|
||||
# > It may be useful to wait up to 0.5 seconds
|
||||
POSSIBLE_GAP_TIMEOUT = 0.5
|
||||
|
||||
# After how long without updates the client will "timeout".
|
||||
#
|
||||
# When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the
|
||||
# updates that arrive in the meantime. After all updates are fetched when this happens, the
|
||||
# client will resume normal operation, and the timeout will reset.
|
||||
#
|
||||
# Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates).
|
||||
NO_UPDATES_TIMEOUT = 15 * 60
|
||||
|
||||
# object() but with a tag to make it easier to debug
|
||||
class Sentinel:
|
||||
__slots__ = ('tag',)
|
||||
|
||||
def __init__(self, tag=None):
|
||||
self.tag = tag or '_'
|
||||
|
||||
def __repr__(self):
|
||||
return self.tag
|
||||
|
||||
# Entry "enum".
|
||||
# Account-wide `pts` includes private conversations (one-to-one) and small group chats.
|
||||
ENTRY_ACCOUNT = Sentinel('ACCOUNT')
|
||||
# Account-wide `qts` includes only "secret" one-to-one chats.
|
||||
ENTRY_SECRET = Sentinel('SECRET')
|
||||
# Integers will be Channel-specific `pts`, and includes "megagroup", "broadcast" and "supergroup" channels.
|
||||
|
||||
# Python's logging doesn't define a TRACE level. Pick halfway between DEBUG and NOTSET.
|
||||
# We don't define a name for this as libraries shouldn't do that though.
|
||||
LOG_LEVEL_TRACE = (logging.DEBUG - logging.NOTSET) // 2
|
||||
|
||||
_sentinel = Sentinel()
|
||||
|
||||
def next_updates_deadline():
|
||||
return get_running_loop().time() + NO_UPDATES_TIMEOUT
|
||||
|
||||
def epoch():
|
||||
return datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
class GapError(ValueError):
|
||||
def __repr__(self):
|
||||
return 'GapError()'
|
||||
|
||||
|
||||
class PrematureEndReason(Enum):
|
||||
TEMPORARY_SERVER_ISSUES = 'tmp'
|
||||
BANNED = 'ban'
|
||||
|
||||
|
||||
# Represents the information needed to correctly handle a specific `tl::enums::Update`.
|
||||
class PtsInfo:
|
||||
__slots__ = ('pts', 'pts_count', 'entry')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pts: int,
|
||||
pts_count: int,
|
||||
entry: object
|
||||
):
|
||||
self.pts = pts
|
||||
self.pts_count = pts_count
|
||||
self.entry = entry
|
||||
|
||||
@classmethod
|
||||
def from_update(cls, update):
|
||||
pts = getattr(update, 'pts', None)
|
||||
if pts:
|
||||
pts_count = getattr(update, 'pts_count', None) or 0
|
||||
try:
|
||||
entry = update.message.peer_id.channel_id
|
||||
except AttributeError:
|
||||
entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT
|
||||
return cls(pts=pts, pts_count=pts_count, entry=entry)
|
||||
|
||||
qts = getattr(update, 'qts', None)
|
||||
if qts:
|
||||
return cls(pts=qts, pts_count=1, entry=ENTRY_SECRET)
|
||||
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={self.entry})'
|
||||
|
||||
|
||||
# The state of a particular entry in the message box.
|
||||
class State:
|
||||
__slots__ = ('pts', 'deadline')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# Current local persistent timestamp.
|
||||
pts: int,
|
||||
# Next instant when we would get the update difference if no updates arrived before then.
|
||||
deadline: float
|
||||
):
|
||||
self.pts = pts
|
||||
self.deadline = deadline
|
||||
|
||||
def __repr__(self):
|
||||
return f'State(pts={self.pts}, deadline={self.deadline})'
|
||||
|
||||
|
||||
# > ### Recovering gaps
|
||||
# > […] Manually obtaining updates is also required in the following situations:
|
||||
# > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above).
|
||||
# > It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update
|
||||
# > arrives, that fills the gap.
|
||||
#
|
||||
# This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because
|
||||
# the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone).
|
||||
class PossibleGap:
|
||||
__slots__ = ('deadline', 'updates')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
deadline: float,
|
||||
# Pending updates (those with a larger PTS, producing the gap which may later be filled).
|
||||
updates: list # of updates
|
||||
):
|
||||
self.deadline = deadline
|
||||
self.updates = updates
|
||||
|
||||
def __repr__(self):
|
||||
return f'PossibleGap(deadline={self.deadline}, update_count={len(self.updates)})'
|
||||
|
||||
|
||||
# Represents a "message box" (event `pts` for a specific entry).
|
||||
#
|
||||
# See https://core.telegram.org/api/updates#message-related-event-sequences.
|
||||
class MessageBox:
|
||||
__slots__ = ('_log', 'map', 'date', 'seq', 'next_deadline', 'possible_gaps', 'getting_diff_for')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log,
|
||||
# Map each entry to their current state.
|
||||
map: dict = _sentinel, # entry -> state
|
||||
|
||||
# Additional fields beyond PTS needed by `ENTRY_ACCOUNT`.
|
||||
date: datetime.datetime = epoch() + datetime.timedelta(seconds=1),
|
||||
seq: int = NO_SEQ,
|
||||
|
||||
# Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline).
|
||||
next_deadline: object = None, # entry
|
||||
|
||||
# Which entries have a gap and may soon trigger a need to get difference.
|
||||
#
|
||||
# If a gap is found, stores the required information to resolve it (when should it timeout and what updates
|
||||
# should be held in case the gap is resolved on its own).
|
||||
#
|
||||
# Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have
|
||||
# a gap in them).
|
||||
possible_gaps: dict = _sentinel, # entry -> possiblegap
|
||||
|
||||
# For which entries are we currently getting difference.
|
||||
getting_diff_for: set = _sentinel, # entry
|
||||
):
|
||||
self._log = log
|
||||
self.map = {} if map is _sentinel else map
|
||||
self.date = date
|
||||
self.seq = seq
|
||||
self.next_deadline = next_deadline
|
||||
self.possible_gaps = {} if possible_gaps is _sentinel else possible_gaps
|
||||
self.getting_diff_for = set() if getting_diff_for is _sentinel else getting_diff_for
|
||||
|
||||
if __debug__:
|
||||
self._trace('MessageBox initialized')
|
||||
|
||||
def _trace(self, msg, *args, **kwargs):
|
||||
# Calls to trace can't really be removed beforehand without some dark magic.
|
||||
# So every call to trace is prefixed with `if __debug__`` instead, to remove
|
||||
# it when using `python -O`. Probably unnecessary, but it's nice to avoid
|
||||
# paying the cost for something that is not used.
|
||||
self._log.log(LOG_LEVEL_TRACE, 'Current MessageBox state: seq = %r, date = %s, map = %r',
|
||||
self.seq, self.date.isoformat(), self.map)
|
||||
self._log.log(LOG_LEVEL_TRACE, msg, *args, **kwargs)
|
||||
|
||||
# region Creation, querying, and setting base state.
|
||||
|
||||
def load(self, session_state, channel_states):
|
||||
"""
|
||||
Create a [`MessageBox`] from a previously known update state.
|
||||
"""
|
||||
if __debug__:
|
||||
self._trace('Loading MessageBox with session_state = %r, channel_states = %r', session_state, channel_states)
|
||||
|
||||
deadline = next_updates_deadline()
|
||||
|
||||
self.map.clear()
|
||||
if session_state.pts != NO_SEQ:
|
||||
self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline)
|
||||
if session_state.qts != NO_SEQ:
|
||||
self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline)
|
||||
self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states)
|
||||
|
||||
self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc)
|
||||
self.seq = session_state.seq
|
||||
self.next_deadline = ENTRY_ACCOUNT
|
||||
|
||||
def session_state(self):
|
||||
"""
|
||||
Return the current state.
|
||||
|
||||
This should be used for persisting the state.
|
||||
"""
|
||||
return dict(
|
||||
pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ,
|
||||
qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,
|
||||
date=self.date,
|
||||
seq=self.seq,
|
||||
), {id: state.pts for id, state in self.map.items() if isinstance(id, int)}
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""
|
||||
Return true if the message box is empty and has no state yet.
|
||||
"""
|
||||
return ENTRY_ACCOUNT not in self.map
|
||||
|
||||
def check_deadlines(self):
|
||||
"""
|
||||
Return the next deadline when receiving updates should timeout.
|
||||
|
||||
If a deadline expired, the corresponding entries will be marked as needing to get its difference.
|
||||
While there are entries pending of getting their difference, this method returns the current instant.
|
||||
"""
|
||||
now = get_running_loop().time()
|
||||
|
||||
if self.getting_diff_for:
|
||||
return now
|
||||
|
||||
deadline = next_updates_deadline()
|
||||
|
||||
# Most of the time there will be zero or one gap in flight so finding the minimum is cheap.
|
||||
if self.possible_gaps:
|
||||
deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values()))
|
||||
elif self.next_deadline in self.map:
|
||||
deadline = min(deadline, self.map[self.next_deadline].deadline)
|
||||
|
||||
# asyncio's loop time precision only seems to be about 3 decimal places, so it's possible that
|
||||
# we find the same number again on repeated calls. Without the "or equal" part we would log the
|
||||
# timeout for updates several times (it also makes sense to get difference if now is the deadline).
|
||||
if now >= deadline:
|
||||
# Check all expired entries and add them to the list that needs getting difference.
|
||||
self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now >= gap.deadline)
|
||||
self.getting_diff_for.update(entry for entry, state in self.map.items() if now >= state.deadline)
|
||||
|
||||
if __debug__:
|
||||
self._trace('Deadlines met, now getting diff for %r', self.getting_diff_for)
|
||||
|
||||
# When extending `getting_diff_for`, it's important to have the moral equivalent of
|
||||
# `begin_get_diff` (that is, clear possible gaps if we're now getting difference).
|
||||
for entry in self.getting_diff_for:
|
||||
self.possible_gaps.pop(entry, None)
|
||||
|
||||
return deadline
|
||||
|
||||
# Reset the deadline for the periods without updates for the given entries.
|
||||
#
|
||||
# It also updates the next deadline time to reflect the new closest deadline.
|
||||
def reset_deadlines(self, entries, deadline):
|
||||
if not entries:
|
||||
return
|
||||
for entry in entries:
|
||||
if entry not in self.map:
|
||||
raise RuntimeError('Called reset_deadline on an entry for which we do not have state')
|
||||
self.map[entry].deadline = deadline
|
||||
|
||||
if self.next_deadline in entries:
|
||||
# If the updated deadline was the closest one, recalculate the new minimum.
|
||||
self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0]
|
||||
elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline:
|
||||
# If the updated deadline is smaller than the next deadline, change the next deadline to be the new one.
|
||||
# Any entry will do, so the one from the last iteration is fine.
|
||||
self.next_deadline = entry
|
||||
# else an unrelated deadline was updated, so the closest one remains unchanged.
|
||||
|
||||
# Convenience to reset a channel's deadline, with optional timeout.
|
||||
def reset_channel_deadline(self, channel_id, timeout):
|
||||
self.reset_deadlines({channel_id}, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT))
|
||||
|
||||
# Sets the update state.
|
||||
#
|
||||
# Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable
|
||||
# updates will be fetched.
|
||||
def set_state(self, state, reset=True):
|
||||
if __debug__:
|
||||
self._trace('Setting state %s', state)
|
||||
|
||||
deadline = next_updates_deadline()
|
||||
|
||||
if state.pts != NO_SEQ or not reset:
|
||||
self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline)
|
||||
else:
|
||||
self.map.pop(ENTRY_ACCOUNT, None)
|
||||
|
||||
# Telegram seems to use the `qts` for bot accounts, but while applying difference,
|
||||
# it might be reset back to 0. See issue #3873 for more details.
|
||||
#
|
||||
# During login, a value of zero would mean the `pts` is unknown,
|
||||
# so the map shouldn't contain that entry.
|
||||
# But while applying difference, if the value is zero, it (probably)
|
||||
# truly means that's what should be used (hence the `reset` flag).
|
||||
if state.qts != NO_SEQ or not reset:
|
||||
self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline)
|
||||
else:
|
||||
self.map.pop(ENTRY_SECRET, None)
|
||||
|
||||
self.date = state.date
|
||||
self.seq = state.seq
|
||||
|
||||
# Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs.
|
||||
#
|
||||
# The update state will only be updated if no entry was known previously.
|
||||
def try_set_channel_state(self, id, pts):
|
||||
if __debug__:
|
||||
self._trace('Trying to set channel state for %r: %r', id, pts)
|
||||
|
||||
if id not in self.map:
|
||||
self.map[id] = State(pts=pts, deadline=next_updates_deadline())
|
||||
|
||||
# Try to begin getting difference for the given entry.
|
||||
# Fails if the entry does not have a previously-known state that can be used to get its difference.
|
||||
#
|
||||
# Clears any previous gaps.
|
||||
def try_begin_get_diff(self, entry, reason):
|
||||
if entry not in self.map:
|
||||
# Won't actually be able to get difference for this entry if we don't have a pts to start off from.
|
||||
if entry in self.possible_gaps:
|
||||
raise RuntimeError('Should not have a possible_gap for an entry not in the state map')
|
||||
|
||||
if __debug__:
|
||||
self._trace('Should get difference for %r because %s but cannot due to missing hash', entry, reason)
|
||||
return
|
||||
|
||||
if __debug__:
|
||||
self._trace('Marking %r as needing difference because %s', entry, reason)
|
||||
self.getting_diff_for.add(entry)
|
||||
self.possible_gaps.pop(entry, None)
|
||||
|
||||
# Finish getting difference for the given entry.
|
||||
#
|
||||
# It also resets the deadline.
|
||||
def end_get_diff(self, entry):
|
||||
try:
|
||||
self.getting_diff_for.remove(entry)
|
||||
except KeyError:
|
||||
raise RuntimeError('Called end_get_diff on an entry which was not getting diff for')
|
||||
|
||||
self.reset_deadlines({entry}, next_updates_deadline())
|
||||
assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference"
|
||||
|
||||
# endregion Creation, querying, and setting base state.
|
||||
|
||||
# region "Normal" updates flow (processing and detection of gaps).
|
||||
|
||||
# Process an update and return what should be done with it.
|
||||
#
|
||||
# Updates corresponding to entries for which their difference is currently being fetched
|
||||
# will be ignored. While according to the [updates' documentation]:
|
||||
#
|
||||
# > Implementations [have] to postpone updates received via the socket while
|
||||
# > filling gaps in the event and `Update` sequences, as well as avoid filling
|
||||
# > gaps in the same sequence.
|
||||
#
|
||||
# In practice, these updates should have also been retrieved through getting difference.
|
||||
#
|
||||
# [updates documentation] https://core.telegram.org/api/updates
|
||||
def process_updates(
|
||||
self,
|
||||
updates,
|
||||
chat_hashes,
|
||||
result, # out list of updates; returns list of user, chat, or raise if gap
|
||||
):
|
||||
|
||||
# v1 has never sent updates produced by the client itself to the handlers.
|
||||
# However proper update handling requires those to be processed.
|
||||
# This is an ugly workaround for that.
|
||||
self_outgoing = getattr(updates, '_self_outgoing', False)
|
||||
real_result = result
|
||||
result = []
|
||||
|
||||
date = getattr(updates, 'date', None)
|
||||
seq = getattr(updates, 'seq', None)
|
||||
seq_start = getattr(updates, 'seq_start', None)
|
||||
users = getattr(updates, 'users', None) or []
|
||||
chats = getattr(updates, 'chats', None) or []
|
||||
|
||||
if __debug__:
|
||||
self._trace('Processing updates with seq = %r, seq_start = %r, date = %s: %s',
|
||||
seq, seq_start, date.isoformat() if date else None, updates)
|
||||
|
||||
if date is None:
|
||||
# updatesTooLong is the only one with no date (we treat it as a gap)
|
||||
self.try_begin_get_diff(ENTRY_ACCOUNT, 'received updatesTooLong')
|
||||
raise GapError
|
||||
if seq is None:
|
||||
seq = NO_SEQ
|
||||
if seq_start is None:
|
||||
seq_start = seq
|
||||
|
||||
# updateShort is the only update which cannot be dispatched directly but doesn't have 'updates' field
|
||||
updates = getattr(updates, 'updates', None) or [updates.update if isinstance(updates, tl.UpdateShort) else updates]
|
||||
|
||||
for u in updates:
|
||||
u._self_outgoing = self_outgoing
|
||||
|
||||
# > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors
|
||||
# > there is no need to check `seq` or change a local state.
|
||||
if seq_start != NO_SEQ:
|
||||
if self.seq + 1 > seq_start:
|
||||
# Skipping updates that were already handled
|
||||
if __debug__:
|
||||
self._trace('Skipping updates as they should have already been handled')
|
||||
return (users, chats)
|
||||
elif self.seq + 1 < seq_start:
|
||||
# Gap detected
|
||||
self.try_begin_get_diff(ENTRY_ACCOUNT, 'detected gap')
|
||||
raise GapError
|
||||
# else apply
|
||||
|
||||
def _sort_gaps(update):
|
||||
pts = PtsInfo.from_update(update)
|
||||
return pts.pts - pts.pts_count if pts else 0
|
||||
|
||||
reset_deadlines = set() # temporary buffer
|
||||
|
||||
result.extend(filter(None, (
|
||||
self.apply_pts_info(u, reset_deadlines=reset_deadlines)
|
||||
# Telegram can send updates out of order (e.g. ReadChannelInbox first
|
||||
# and then NewChannelMessage, both with the same pts, but the count is
|
||||
# 0 and 1 respectively), so we sort them first.
|
||||
for u in sorted(updates, key=_sort_gaps))))
|
||||
|
||||
self.reset_deadlines(reset_deadlines, next_updates_deadline())
|
||||
|
||||
if self.possible_gaps:
|
||||
if __debug__:
|
||||
self._trace('Trying to re-apply %r possible gaps', len(self.possible_gaps))
|
||||
|
||||
# For each update in possible gaps, see if the gap has been resolved already.
|
||||
for key in list(self.possible_gaps.keys()):
|
||||
self.possible_gaps[key].updates.sort(key=_sort_gaps)
|
||||
|
||||
for _ in range(len(self.possible_gaps[key].updates)):
|
||||
update = self.possible_gaps[key].updates.pop(0)
|
||||
|
||||
# If this fails to apply, it will get re-inserted at the end.
|
||||
# All should fail, so the order will be preserved (it would've cycled once).
|
||||
update = self.apply_pts_info(update, reset_deadlines=None)
|
||||
if update:
|
||||
result.append(update)
|
||||
if __debug__:
|
||||
self._trace('Resolved gap with %r: %s', PtsInfo.from_update(update), update)
|
||||
|
||||
# Clear now-empty gaps.
|
||||
self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates}
|
||||
|
||||
real_result.extend(u for u in result if not u._self_outgoing)
|
||||
|
||||
if result and not self.possible_gaps:
|
||||
# > If the updates were applied, local *Updates* state must be updated
|
||||
# > with `seq` (unless it's 0) and `date` from the constructor.
|
||||
if __debug__:
|
||||
self._trace('Updating seq as all updates were applied')
|
||||
if date != epoch():
|
||||
self.date = date
|
||||
if seq != NO_SEQ:
|
||||
self.seq = seq
|
||||
|
||||
return (users, chats)
|
||||
|
||||
# Tries to apply the input update if its `PtsInfo` follows the correct order.
|
||||
#
|
||||
# If the update can be applied, it is returned; otherwise, the update is stored in a
|
||||
# possible gap (unless it was already handled or would be handled through getting
|
||||
# difference) and `None` is returned.
|
||||
def apply_pts_info(
|
||||
self,
|
||||
update,
|
||||
*,
|
||||
reset_deadlines,
|
||||
):
|
||||
# This update means we need to call getChannelDifference to get the updates from the channel
|
||||
if isinstance(update, tl.UpdateChannelTooLong):
|
||||
self.try_begin_get_diff(update.channel_id, 'received updateChannelTooLong')
|
||||
return None
|
||||
|
||||
pts = PtsInfo.from_update(update)
|
||||
if not pts:
|
||||
# No pts means that the update can be applied in any order.
|
||||
if __debug__:
|
||||
self._trace('No pts in update, so it can be applied in any order: %s', update)
|
||||
return update
|
||||
|
||||
# As soon as we receive an update of any form related to messages (has `PtsInfo`),
|
||||
# the "no updates" period for that entry is reset.
|
||||
#
|
||||
# Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry.
|
||||
#
|
||||
# By the time this method returns, self.map will have an entry for which we can reset its deadline.
|
||||
if reset_deadlines:
|
||||
reset_deadlines.add(pts.entry)
|
||||
|
||||
if pts.entry in self.getting_diff_for:
|
||||
# Note: early returning here also prevents gap from being inserted (which they should
|
||||
# not be while getting difference).
|
||||
if __debug__:
|
||||
self._trace('Skipping update with %r as its difference is being fetched', pts)
|
||||
return None
|
||||
|
||||
if pts.entry in self.map:
|
||||
local_pts = self.map[pts.entry].pts
|
||||
if local_pts + pts.pts_count > pts.pts:
|
||||
# Ignore
|
||||
if __debug__:
|
||||
self._trace('Skipping update since local pts %r > %r: %s', local_pts, pts, update)
|
||||
return None
|
||||
elif local_pts + pts.pts_count < pts.pts:
|
||||
# Possible gap
|
||||
# TODO store chats too?
|
||||
if __debug__:
|
||||
self._trace('Possible gap since local pts %r < %r: %s', local_pts, pts, update)
|
||||
if pts.entry not in self.possible_gaps:
|
||||
self.possible_gaps[pts.entry] = PossibleGap(
|
||||
deadline=get_running_loop().time() + POSSIBLE_GAP_TIMEOUT,
|
||||
updates=[]
|
||||
)
|
||||
|
||||
self.possible_gaps[pts.entry].updates.append(update)
|
||||
return None
|
||||
else:
|
||||
# Apply
|
||||
if __debug__:
|
||||
self._trace('Applying update pts since local pts %r = %r: %s', local_pts, pts, update)
|
||||
|
||||
# In a channel, we may immediately receive:
|
||||
# * ReadChannelInbox (pts = X, pts_count = 0)
|
||||
# * NewChannelMessage (pts = X, pts_count = 1)
|
||||
#
|
||||
# Notice how both `pts` are the same. If they were to be applied out of order, the first
|
||||
# one however would've triggered a gap because `local_pts` + `pts_count` of 0 would be
|
||||
# less than `remote_pts`. So there is no risk by setting the `local_pts` to match the
|
||||
# `remote_pts` here of missing the new message.
|
||||
#
|
||||
# The message would however be lost if we initialized the pts with the first one, since
|
||||
# the second one would appear "already handled". To prevent this we set the pts to be
|
||||
# one less when the count is 0 (which might be wrong and trigger a gap later on, but is
|
||||
# unlikely). This will prevent us from losing updates in the unlikely scenario where these
|
||||
# two updates arrive in different packets (and therefore couldn't be sorted beforehand).
|
||||
if pts.entry in self.map:
|
||||
self.map[pts.entry].pts = pts.pts
|
||||
else:
|
||||
# When a chat is migrated to a megagroup, the first update can be a `ReadChannelInbox`
|
||||
# with `pts = 1, pts_count = 0` followed by a `NewChannelMessage` with `pts = 2, pts_count=1`.
|
||||
# Note how the `pts` for the message is 2 and not 1 unlike the case described before!
|
||||
# This is likely because the `pts` cannot be 0 (or it would fail with PERSISTENT_TIMESTAMP_EMPTY),
|
||||
# which forces the first update to be 1. But if we got difference with 1 and the second update
|
||||
# also used 1, we would miss it, so Telegram probably uses 2 to work around that.
|
||||
self.map[pts.entry] = State(
|
||||
pts=(pts.pts - (0 if pts.pts_count else 1)) or 1,
|
||||
deadline=next_updates_deadline()
|
||||
)
|
||||
|
||||
return update
|
||||
|
||||
# endregion "Normal" updates flow (processing and detection of gaps).
|
||||
|
||||
# region Getting and applying account difference.
|
||||
|
||||
# Return the request that needs to be made to get the difference, if any.
|
||||
def get_difference(self):
|
||||
for entry in (ENTRY_ACCOUNT, ENTRY_SECRET):
|
||||
if entry in self.getting_diff_for:
|
||||
if entry not in self.map:
|
||||
raise RuntimeError('Should not try to get difference for an entry without known state')
|
||||
|
||||
gd = fn.updates.GetDifferenceRequest(
|
||||
pts=self.map[ENTRY_ACCOUNT].pts,
|
||||
pts_total_limit=None,
|
||||
date=self.date,
|
||||
qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,
|
||||
)
|
||||
if __debug__:
|
||||
self._trace('Requesting account difference %s', gd)
|
||||
return gd
|
||||
|
||||
return None
|
||||
|
||||
# Similar to [`MessageBox::process_updates`], but using the result from getting difference.
|
||||
def apply_difference(
|
||||
self,
|
||||
diff,
|
||||
chat_hashes,
|
||||
):
|
||||
if __debug__:
|
||||
self._trace('Applying account difference %s', diff)
|
||||
|
||||
finish = None
|
||||
result = None
|
||||
|
||||
if isinstance(diff, tl.updates.DifferenceEmpty):
|
||||
finish = True
|
||||
self.date = diff.date
|
||||
self.seq = diff.seq
|
||||
result = [], [], []
|
||||
elif isinstance(diff, tl.updates.Difference):
|
||||
finish = True
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
result = self.apply_difference_type(diff, chat_hashes)
|
||||
elif isinstance(diff, tl.updates.DifferenceSlice):
|
||||
finish = False
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
result = self.apply_difference_type(diff, chat_hashes)
|
||||
elif isinstance(diff, tl.updates.DifferenceTooLong):
|
||||
finish = True
|
||||
self.map[ENTRY_ACCOUNT].pts = diff.pts # the deadline will be reset once the diff ends
|
||||
result = [], [], []
|
||||
|
||||
if finish:
|
||||
account = ENTRY_ACCOUNT in self.getting_diff_for
|
||||
secret = ENTRY_SECRET in self.getting_diff_for
|
||||
|
||||
if not account and not secret:
|
||||
raise RuntimeError('Should not be applying the difference when neither account or secret was diff was active')
|
||||
|
||||
# Both may be active if both expired at the same time.
|
||||
if account:
|
||||
self.end_get_diff(ENTRY_ACCOUNT)
|
||||
if secret:
|
||||
self.end_get_diff(ENTRY_SECRET)
|
||||
|
||||
return result
|
||||
|
||||
def apply_difference_type(
|
||||
self,
|
||||
diff,
|
||||
chat_hashes,
|
||||
):
|
||||
state = getattr(diff, 'intermediate_state', None) or diff.state
|
||||
self.set_state(state, reset=False)
|
||||
|
||||
# diff.other_updates can contain things like UpdateChannelTooLong and UpdateNewChannelMessage.
|
||||
# We need to process those as if they were socket updates to discard any we have already handled.
|
||||
updates = []
|
||||
self.process_updates(tl.Updates(
|
||||
updates=diff.other_updates,
|
||||
users=diff.users,
|
||||
chats=diff.chats,
|
||||
date=epoch(),
|
||||
seq=NO_SEQ, # this way date is not used
|
||||
), chat_hashes, updates)
|
||||
|
||||
updates.extend(tl.UpdateNewMessage(
|
||||
message=m,
|
||||
pts=NO_SEQ,
|
||||
pts_count=NO_SEQ,
|
||||
) for m in diff.new_messages)
|
||||
updates.extend(tl.UpdateNewEncryptedMessage(
|
||||
message=m,
|
||||
qts=NO_SEQ,
|
||||
) for m in diff.new_encrypted_messages)
|
||||
|
||||
return updates, diff.users, diff.chats
|
||||
|
||||
def end_difference(self):
|
||||
if __debug__:
|
||||
self._trace('Ending account difference')
|
||||
|
||||
account = ENTRY_ACCOUNT in self.getting_diff_for
|
||||
secret = ENTRY_SECRET in self.getting_diff_for
|
||||
|
||||
if not account and not secret:
|
||||
raise RuntimeError('Should not be ending get difference when neither account or secret was diff was active')
|
||||
|
||||
# Both may be active if both expired at the same time.
|
||||
if account:
|
||||
self.end_get_diff(ENTRY_ACCOUNT)
|
||||
if secret:
|
||||
self.end_get_diff(ENTRY_SECRET)
|
||||
|
||||
# endregion Getting and applying account difference.
|
||||
|
||||
# region Getting and applying channel difference.
|
||||
|
||||
# Return the request that needs to be made to get a channel's difference, if any.
|
||||
def get_channel_difference(
|
||||
self,
|
||||
chat_hashes,
|
||||
):
|
||||
entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None)
|
||||
if not entry:
|
||||
return None
|
||||
|
||||
packed = chat_hashes.get(entry)
|
||||
if not packed:
|
||||
# Cannot get channel difference as we're missing its hash
|
||||
# TODO we should probably log this
|
||||
self.end_get_diff(entry)
|
||||
# Remove the outdated `pts` entry from the map so that the next update can correct
|
||||
# it. Otherwise, it will spam that the access hash is missing.
|
||||
self.map.pop(entry, None)
|
||||
return None
|
||||
|
||||
state = self.map.get(entry)
|
||||
if not state:
|
||||
raise RuntimeError('Should not try to get difference for an entry without known state')
|
||||
|
||||
gd = fn.updates.GetChannelDifferenceRequest(
|
||||
force=False,
|
||||
channel=tl.InputChannel(packed.id, packed.hash),
|
||||
filter=tl.ChannelMessagesFilterEmpty(),
|
||||
pts=state.pts,
|
||||
limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT
|
||||
)
|
||||
if __debug__:
|
||||
self._trace('Requesting channel difference %s', gd)
|
||||
return gd
|
||||
|
||||
# Similar to [`MessageBox::process_updates`], but using the result from getting difference.
|
||||
def apply_channel_difference(
|
||||
self,
|
||||
request,
|
||||
diff,
|
||||
chat_hashes,
|
||||
):
|
||||
entry = request.channel.channel_id
|
||||
if __debug__:
|
||||
self._trace('Applying channel difference for %r: %s', entry, diff)
|
||||
|
||||
self.possible_gaps.pop(entry, None)
|
||||
|
||||
if isinstance(diff, tl.updates.ChannelDifferenceEmpty):
|
||||
assert diff.final
|
||||
self.end_get_diff(entry)
|
||||
self.map[entry].pts = diff.pts
|
||||
return [], [], []
|
||||
elif isinstance(diff, tl.updates.ChannelDifferenceTooLong):
|
||||
assert diff.final
|
||||
self.map[entry].pts = diff.dialog.pts
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
self.reset_channel_deadline(entry, diff.timeout)
|
||||
# This `diff` has the "latest messages and corresponding chats", but it would
|
||||
# be strange to give the user only partial changes of these when they would
|
||||
# expect all updates to be fetched. Instead, nothing is returned.
|
||||
return [], [], []
|
||||
elif isinstance(diff, tl.updates.ChannelDifference):
|
||||
if diff.final:
|
||||
self.end_get_diff(entry)
|
||||
|
||||
self.map[entry].pts = diff.pts
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
|
||||
updates = []
|
||||
self.process_updates(tl.Updates(
|
||||
updates=diff.other_updates,
|
||||
users=diff.users,
|
||||
chats=diff.chats,
|
||||
date=epoch(),
|
||||
seq=NO_SEQ, # this way date is not used
|
||||
), chat_hashes, updates)
|
||||
|
||||
updates.extend(tl.UpdateNewChannelMessage(
|
||||
message=m,
|
||||
pts=NO_SEQ,
|
||||
pts_count=NO_SEQ,
|
||||
) for m in diff.new_messages)
|
||||
self.reset_channel_deadline(entry, None)
|
||||
|
||||
return updates, diff.users, diff.chats
|
||||
|
||||
def end_channel_difference(self, request, reason: PrematureEndReason, chat_hashes):
|
||||
entry = request.channel.channel_id
|
||||
if __debug__:
|
||||
self._trace('Ending channel difference for %r because %s', entry, reason)
|
||||
|
||||
if reason == PrematureEndReason.TEMPORARY_SERVER_ISSUES:
|
||||
# Temporary issues. End getting difference without updating the pts so we can retry later.
|
||||
self.possible_gaps.pop(entry, None)
|
||||
self.end_get_diff(entry)
|
||||
elif reason == PrematureEndReason.BANNED:
|
||||
# Banned in the channel. Forget its state since we can no longer fetch updates from it.
|
||||
self.possible_gaps.pop(entry, None)
|
||||
self.end_get_diff(entry)
|
||||
del self.map[entry]
|
||||
else:
|
||||
raise RuntimeError('Unknown reason to end channel difference')
|
||||
|
||||
# endregion Getting and applying channel difference.
|
|
@ -1,195 +0,0 @@
|
|||
from typing import Optional, Tuple
|
||||
from enum import IntEnum
|
||||
from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel
|
||||
import struct
|
||||
|
||||
class SessionState:
|
||||
"""
|
||||
Stores the information needed to fetch updates and about the current user.
|
||||
|
||||
* user_id: 64-bit number representing the user identifier.
|
||||
* dc_id: 32-bit number relating to the datacenter identifier where the user is.
|
||||
* bot: is the logged-in user a bot?
|
||||
* pts: 64-bit number holding the state needed to fetch updates.
|
||||
* qts: alternative 64-bit number holding the state needed to fetch updates.
|
||||
* date: 64-bit number holding the date needed to fetch updates.
|
||||
* seq: 64-bit-number holding the sequence number needed to fetch updates.
|
||||
* takeout_id: 64-bit-number holding the identifier of the current takeout session.
|
||||
|
||||
Note that some of the numbers will only use 32 out of the 64 available bits.
|
||||
However, for future-proofing reasons, we recommend you pretend they are 64-bit long.
|
||||
"""
|
||||
__slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: int,
|
||||
dc_id: int,
|
||||
bot: bool,
|
||||
pts: int,
|
||||
qts: int,
|
||||
date: int,
|
||||
seq: int,
|
||||
takeout_id: Optional[int]
|
||||
):
|
||||
self.user_id = user_id
|
||||
self.dc_id = dc_id
|
||||
self.bot = bot
|
||||
self.pts = pts
|
||||
self.qts = qts
|
||||
self.date = date
|
||||
self.seq = seq
|
||||
self.takeout_id = takeout_id
|
||||
|
||||
def __repr__(self):
|
||||
return repr({k: getattr(self, k) for k in self.__slots__})
|
||||
|
||||
|
||||
class ChannelState:
|
||||
"""
|
||||
Stores the information needed to fetch updates from a channel.
|
||||
|
||||
* channel_id: 64-bit number representing the channel identifier.
|
||||
* pts: 64-bit number holding the state needed to fetch updates.
|
||||
"""
|
||||
__slots__ = ('channel_id', 'pts')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channel_id: int,
|
||||
pts: int,
|
||||
):
|
||||
self.channel_id = channel_id
|
||||
self.pts = pts
|
||||
|
||||
def __repr__(self):
|
||||
return repr({k: getattr(self, k) for k in self.__slots__})
|
||||
|
||||
|
||||
class EntityType(IntEnum):
|
||||
"""
|
||||
You can rely on the type value to be equal to the ASCII character one of:
|
||||
|
||||
* 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``.
|
||||
* 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``.
|
||||
* 'G' (71): this entity belongs to a small group :tl:`Chat`.
|
||||
* 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`.
|
||||
* 'M' (77): this entity belongs to a megagroup :tl:`Channel`.
|
||||
* 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`.
|
||||
"""
|
||||
USER = ord('U')
|
||||
BOT = ord('B')
|
||||
GROUP = ord('G')
|
||||
CHANNEL = ord('C')
|
||||
MEGAGROUP = ord('M')
|
||||
GIGAGROUP = ord('E')
|
||||
|
||||
def canonical(self):
|
||||
"""
|
||||
Return the canonical version of this type.
|
||||
"""
|
||||
return _canon_entity_types[self]
|
||||
|
||||
|
||||
_canon_entity_types = {
|
||||
EntityType.USER: EntityType.USER,
|
||||
EntityType.BOT: EntityType.USER,
|
||||
EntityType.GROUP: EntityType.GROUP,
|
||||
EntityType.CHANNEL: EntityType.CHANNEL,
|
||||
EntityType.MEGAGROUP: EntityType.CHANNEL,
|
||||
EntityType.GIGAGROUP: EntityType.CHANNEL,
|
||||
}
|
||||
|
||||
|
||||
class Entity:
|
||||
"""
|
||||
Stores the information needed to use a certain user, chat or channel with the API.
|
||||
|
||||
* ty: 8-bit number indicating the type of the entity (of type `EntityType`).
|
||||
* id: 64-bit number uniquely identifying the entity among those of the same type.
|
||||
* hash: 64-bit signed number needed to use this entity with the API.
|
||||
|
||||
The string representation of this class is considered to be stable, for as long as
|
||||
Telegram doesn't need to add more fields to the entities. It can also be converted
|
||||
to bytes with ``bytes(entity)``, for a more compact representation.
|
||||
"""
|
||||
__slots__ = ('ty', 'id', 'hash')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ty: EntityType,
|
||||
id: int,
|
||||
hash: int
|
||||
):
|
||||
self.ty = ty
|
||||
self.id = id
|
||||
self.hash = hash
|
||||
|
||||
@property
|
||||
def is_user(self):
|
||||
"""
|
||||
``True`` if the entity is either a user or a bot.
|
||||
"""
|
||||
return self.ty in (EntityType.USER, EntityType.BOT)
|
||||
|
||||
@property
|
||||
def is_group(self):
|
||||
"""
|
||||
``True`` if the entity is a small group chat or `megagroup`_.
|
||||
|
||||
.. _megagroup: https://telegram.org/blog/supergroups5k
|
||||
"""
|
||||
return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP)
|
||||
|
||||
@property
|
||||
def is_broadcast(self):
|
||||
"""
|
||||
``True`` if the entity is a broadcast channel or `broadcast group`_.
|
||||
|
||||
.. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members
|
||||
"""
|
||||
return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP)
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, string: str):
|
||||
"""
|
||||
Convert the string into an `Entity`.
|
||||
"""
|
||||
try:
|
||||
ty, id, hash = string.split('.')
|
||||
ty, id, hash = ord(ty), int(id), int(hash)
|
||||
except AttributeError:
|
||||
raise TypeError(f'expected str, got {string!r}') from None
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None
|
||||
|
||||
return cls(EntityType(ty), id, hash)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, blob):
|
||||
"""
|
||||
Convert the bytes into an `Entity`.
|
||||
"""
|
||||
try:
|
||||
ty, id, hash = struct.unpack('<Bqq', blob)
|
||||
except struct.error:
|
||||
raise ValueError(f'malformed entity data, got {blob!r}') from None
|
||||
|
||||
return cls(EntityType(ty), id, hash)
|
||||
|
||||
def __str__(self):
|
||||
return f'{chr(self.ty)}.{self.id}.{self.hash}'
|
||||
|
||||
def __bytes__(self):
|
||||
return struct.pack('<Bqq', self.ty, self.id, self.hash)
|
||||
|
||||
def _as_input_peer(self):
|
||||
if self.is_user:
|
||||
return InputPeerUser(self.id, self.hash)
|
||||
elif self.ty == EntityType.GROUP:
|
||||
return InputPeerChat(self.id)
|
||||
else:
|
||||
return InputPeerChannel(self.id, self.hash)
|
||||
|
||||
def __repr__(self):
|
||||
return repr({k: getattr(self, k) for k in self.__slots__})
|
|
@ -7,7 +7,6 @@ import warnings
|
|||
|
||||
from .. import utils, helpers, errors, password as pwd_mod
|
||||
from ..tl import types, functions, custom
|
||||
from .._updates import SessionState
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
@ -19,8 +18,8 @@ class AuthMethods:
|
|||
|
||||
def start(
|
||||
self: 'TelegramClient',
|
||||
phone: typing.Union[typing.Callable[[], str], str] = lambda: input('Please enter your phone (or bot token): '),
|
||||
password: typing.Union[typing.Callable[[], str], str] = lambda: getpass.getpass('Please enter your password: '),
|
||||
phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '),
|
||||
password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '),
|
||||
*,
|
||||
bot_token: str = None,
|
||||
force_sms: bool = False,
|
||||
|
@ -34,6 +33,12 @@ class AuthMethods:
|
|||
By default, this method will be interactive (asking for
|
||||
user input if needed), and will handle 2FA if enabled too.
|
||||
|
||||
If the phone doesn't belong to an existing account (and will hence
|
||||
`sign_up` for a new one), **you are agreeing to Telegram's
|
||||
Terms of Service. This is required and your account
|
||||
will be banned otherwise.** See https://telegram.org/tos
|
||||
and https://core.telegram.org/api/terms.
|
||||
|
||||
If the event loop is already running, this method returns a
|
||||
coroutine that you should await on your own code; otherwise
|
||||
the loop is ran until said coroutine completes.
|
||||
|
@ -147,16 +152,14 @@ class AuthMethods:
|
|||
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; '
|
||||
'if you were expecting a different user, check whether '
|
||||
'you are accidentally reusing an existing session'
|
||||
'not login to the bot account using the provided '
|
||||
'bot_token (it may not be using the user you expect)'
|
||||
)
|
||||
elif phone and not callable(phone) and utils.parse_phone(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; '
|
||||
'if you were expecting a different user, check whether '
|
||||
'you are accidentally reusing an existing session'
|
||||
'not login to the user account using the provided '
|
||||
'phone (it may not be using the user you expect)'
|
||||
)
|
||||
|
||||
return self
|
||||
|
@ -184,6 +187,7 @@ class AuthMethods:
|
|||
two_step_detected = False
|
||||
|
||||
await self.send_code_request(phone, force_sms=force_sms)
|
||||
sign_up = False # assume login
|
||||
while attempts < max_attempts:
|
||||
try:
|
||||
value = code_callback()
|
||||
|
@ -196,12 +200,19 @@ class AuthMethods:
|
|||
if not value:
|
||||
raise errors.PhoneCodeEmptyError(request=None)
|
||||
|
||||
if sign_up:
|
||||
me = await self.sign_up(value, first_name, last_name)
|
||||
else:
|
||||
# Raises SessionPasswordNeededError if 2FA enabled
|
||||
me = await self.sign_in(phone, code=value)
|
||||
break
|
||||
except errors.SessionPasswordNeededError:
|
||||
two_step_detected = True
|
||||
break
|
||||
except errors.PhoneNumberOccupiedError:
|
||||
sign_up = False
|
||||
except errors.PhoneNumberUnoccupiedError:
|
||||
sign_up = True
|
||||
except (errors.PhoneCodeEmptyError,
|
||||
errors.PhoneCodeExpiredError,
|
||||
errors.PhoneCodeHashEmptyError,
|
||||
|
@ -241,13 +252,12 @@ class AuthMethods:
|
|||
|
||||
# We won't reach here if any step failed (exit by exception)
|
||||
signed, name = 'Signed in successfully as', utils.get_display_name(me)
|
||||
tos = '; remember to not break the ToS or you will risk an account ban!'
|
||||
try:
|
||||
print(signed, name, tos, sep='')
|
||||
print(signed, name)
|
||||
except UnicodeEncodeError:
|
||||
# Some terminals don't support certain characters
|
||||
print(signed, name.encode('utf-8', errors='ignore')
|
||||
.decode('ascii', errors='ignore'), tos, sep='')
|
||||
.decode('ascii', errors='ignore'))
|
||||
|
||||
return self
|
||||
|
||||
|
@ -355,18 +365,13 @@ class AuthMethods:
|
|||
'and a password only if an RPCError was raised before.'
|
||||
)
|
||||
|
||||
try:
|
||||
result = await self(request)
|
||||
except errors.PhoneCodeExpiredError:
|
||||
self._phone_code_hash.pop(phone, None)
|
||||
raise
|
||||
|
||||
if isinstance(result, types.auth.AuthorizationSignUpRequired):
|
||||
# Emulate pre-layer 104 behaviour
|
||||
self._tos = result.terms_of_service
|
||||
raise errors.PhoneNumberUnoccupiedError(request=request)
|
||||
|
||||
return await self._on_login(result.user)
|
||||
return self._on_login(result.user)
|
||||
|
||||
async def sign_up(
|
||||
self: 'TelegramClient',
|
||||
|
@ -377,41 +382,109 @@ class AuthMethods:
|
|||
phone: str = None,
|
||||
phone_code_hash: str = None) -> 'types.User':
|
||||
"""
|
||||
This method can no longer be used, and will immediately raise a ``ValueError``.
|
||||
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
|
||||
"""
|
||||
raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details')
|
||||
Signs up to Telegram as a new user account.
|
||||
|
||||
async def _on_login(self, user):
|
||||
Use this if you don't have an account yet.
|
||||
|
||||
You must call `send_code_request` first.
|
||||
|
||||
**By using this method you're agreeing to Telegram's
|
||||
Terms of Service. This is required and your account
|
||||
will be banned otherwise.** See https://telegram.org/tos
|
||||
and https://core.telegram.org/api/terms.
|
||||
|
||||
Arguments
|
||||
code (`str` | `int`):
|
||||
The code sent by Telegram
|
||||
|
||||
first_name (`str`):
|
||||
The first name to be used by the new account.
|
||||
|
||||
last_name (`str`, optional)
|
||||
Optional last name.
|
||||
|
||||
phone (`str` | `int`, optional):
|
||||
The phone to sign up. This will be the last phone used by
|
||||
default (you normally don't need to set this).
|
||||
|
||||
phone_code_hash (`str`, optional):
|
||||
The hash returned by `send_code_request`. This can be left as
|
||||
`None` to use the last hash known for the phone to be used.
|
||||
|
||||
Returns
|
||||
The new created :tl:`User`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
phone = '+34 123 123 123'
|
||||
await client.send_code_request(phone)
|
||||
|
||||
code = input('enter code: ')
|
||||
await client.sign_up(code, first_name='Anna', last_name='Banana')
|
||||
"""
|
||||
me = await self.get_me()
|
||||
if me:
|
||||
return me
|
||||
|
||||
# To prevent abuse, one has to try to sign in before signing up. This
|
||||
# is the current way in which Telegram validates the code to sign up.
|
||||
#
|
||||
# `sign_in` will set `_tos`, so if it's set we don't need to call it
|
||||
# because the user already tried to sign in.
|
||||
#
|
||||
# We're emulating pre-layer 104 behaviour so except the right error:
|
||||
if not self._tos:
|
||||
try:
|
||||
return await self.sign_in(
|
||||
phone=phone,
|
||||
code=code,
|
||||
phone_code_hash=phone_code_hash,
|
||||
)
|
||||
except errors.PhoneNumberUnoccupiedError:
|
||||
pass # code is correct and was used, now need to sign in
|
||||
|
||||
if self._tos and self._tos.text:
|
||||
if self.parse_mode:
|
||||
t = self.parse_mode.unparse(self._tos.text, self._tos.entities)
|
||||
else:
|
||||
t = self._tos.text
|
||||
sys.stderr.write("{}\n".format(t))
|
||||
sys.stderr.flush()
|
||||
|
||||
phone, phone_code_hash = \
|
||||
self._parse_phone_and_hash(phone, phone_code_hash)
|
||||
|
||||
result = await self(functions.auth.SignUpRequest(
|
||||
phone_number=phone,
|
||||
phone_code_hash=phone_code_hash,
|
||||
first_name=first_name,
|
||||
last_name=last_name
|
||||
))
|
||||
|
||||
if self._tos:
|
||||
await self(
|
||||
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
|
||||
|
||||
return self._on_login(result.user)
|
||||
|
||||
def _on_login(self, user):
|
||||
"""
|
||||
Callback called whenever the login or sign up process completes.
|
||||
|
||||
Returns the input user parameter.
|
||||
"""
|
||||
self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash)
|
||||
self._bot = bool(user.bot)
|
||||
self._self_input_peer = utils.get_input_peer(user, allow_self=False)
|
||||
self._authorized = True
|
||||
|
||||
state = await self(functions.updates.GetStateRequest())
|
||||
# the server may send an old qts in getState
|
||||
difference = await self(functions.updates.GetDifferenceRequest(pts=state.pts, date=state.date, qts=state.qts))
|
||||
|
||||
if isinstance(difference, types.updates.Difference):
|
||||
state = difference.state
|
||||
elif isinstance(difference, types.updates.DifferenceSlice):
|
||||
state = difference.intermediate_state
|
||||
elif isinstance(difference, types.updates.DifferenceTooLong):
|
||||
state.pts = difference.pts
|
||||
|
||||
self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), [])
|
||||
|
||||
return user
|
||||
|
||||
async def send_code_request(
|
||||
self: 'TelegramClient',
|
||||
phone: str,
|
||||
*,
|
||||
force_sms: bool = False,
|
||||
_retry_count: int = 0) -> 'types.auth.SentCode':
|
||||
force_sms: bool = False) -> 'types.auth.SentCode':
|
||||
"""
|
||||
Sends the Telegram code needed to login to the given phone number.
|
||||
|
||||
|
@ -420,8 +493,7 @@ class AuthMethods:
|
|||
The phone to which the code will be sent.
|
||||
|
||||
force_sms (`bool`, optional):
|
||||
Whether to force sending as SMS. This has been deprecated.
|
||||
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
|
||||
Whether to force sending as SMS.
|
||||
|
||||
Returns
|
||||
An instance of :tl:`SentCode`.
|
||||
|
@ -433,10 +505,6 @@ class AuthMethods:
|
|||
sent = await client.send_code_request(phone)
|
||||
print(sent)
|
||||
"""
|
||||
if force_sms:
|
||||
warnings.warn('force_sms has been deprecated and no longer works')
|
||||
force_sms = False
|
||||
|
||||
result = None
|
||||
phone = utils.parse_phone(phone) or self._phone
|
||||
phone_hash = self._phone_code_hash.get(phone)
|
||||
|
@ -446,14 +514,7 @@ class AuthMethods:
|
|||
result = await self(functions.auth.SendCodeRequest(
|
||||
phone, self.api_id, self.api_hash, types.CodeSettings()))
|
||||
except errors.AuthRestartError:
|
||||
if _retry_count > 2:
|
||||
raise
|
||||
return await self.send_code_request(
|
||||
phone, force_sms=force_sms, _retry_count=_retry_count+1)
|
||||
|
||||
# TODO figure out when/if/how this can happen
|
||||
if isinstance(result, types.auth.SentCodeSuccess):
|
||||
raise RuntimeError('logged in right after sending the code')
|
||||
return await self.send_code_request(phone, force_sms=force_sms)
|
||||
|
||||
# If we already sent a SMS, do not resend the code (hash may be empty)
|
||||
if isinstance(result.type, types.auth.SentCodeTypeSms):
|
||||
|
@ -468,21 +529,8 @@ class AuthMethods:
|
|||
self._phone = phone
|
||||
|
||||
if force_sms:
|
||||
try:
|
||||
result = await self(
|
||||
functions.auth.ResendCodeRequest(phone, phone_hash))
|
||||
except errors.PhoneCodeExpiredError:
|
||||
if _retry_count > 2:
|
||||
raise
|
||||
self._phone_code_hash.pop(phone, None)
|
||||
self._log[__name__].info(
|
||||
"Phone code expired in ResendCodeRequest, requesting a new code"
|
||||
)
|
||||
return await self.send_code_request(
|
||||
phone, force_sms=False, _retry_count=_retry_count+1)
|
||||
|
||||
if isinstance(result, types.auth.SentCodeSuccess):
|
||||
raise RuntimeError('logged in right after resending the code')
|
||||
|
||||
self._phone_code_hash[phone] = result.phone_code_hash
|
||||
|
||||
|
@ -520,9 +568,6 @@ class AuthMethods:
|
|||
|
||||
# Important! You need to wait for the login to complete!
|
||||
await qr_login.wait()
|
||||
|
||||
# If you have 2FA enabled, `wait` will raise `telethon.errors.SessionPasswordNeededError`.
|
||||
# You should except that error and call `sign_in` with the password if this happens.
|
||||
"""
|
||||
qr_login = custom.QRLogin(self, ignored_ids or [])
|
||||
await qr_login.recreate()
|
||||
|
@ -532,8 +577,6 @@ class AuthMethods:
|
|||
"""
|
||||
Logs out Telegram and deletes the current ``*.session`` file.
|
||||
|
||||
The client is unusable after logging out and a new instance should be created.
|
||||
|
||||
Returns
|
||||
`True` if the operation was successful.
|
||||
|
||||
|
@ -548,12 +591,13 @@ class AuthMethods:
|
|||
except errors.RPCError:
|
||||
return False
|
||||
|
||||
self._mb_entity_cache.set_self_user(None, None, None)
|
||||
self._bot = None
|
||||
self._self_input_peer = None
|
||||
self._authorized = False
|
||||
self._state_cache.reset()
|
||||
|
||||
await self.disconnect()
|
||||
await utils.maybe_async(self.session.delete())
|
||||
self.session = None
|
||||
self.session.delete()
|
||||
return True
|
||||
|
||||
async def edit_2fa(
|
||||
|
|
|
@ -7,8 +7,8 @@ from ..tl import types, custom
|
|||
class ButtonMethods:
|
||||
@staticmethod
|
||||
def build_reply_markup(
|
||||
buttons: 'typing.Optional[hints.MarkupLike]'
|
||||
) -> 'typing.Optional[types.TypeReplyMarkup]':
|
||||
buttons: 'typing.Optional[hints.MarkupLike]',
|
||||
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
|
||||
"""
|
||||
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
|
||||
the given buttons.
|
||||
|
@ -26,6 +26,9 @@ class ButtonMethods:
|
|||
The button, list of buttons, array of buttons or markup
|
||||
to convert into a markup.
|
||||
|
||||
inline_only (`bool`, optional):
|
||||
Whether the buttons **must** be inline buttons only or not.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -39,8 +42,8 @@ class ButtonMethods:
|
|||
return None
|
||||
|
||||
try:
|
||||
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: # crc32(b'ReplyMarkup'):
|
||||
return buttons
|
||||
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
|
||||
return buttons # crc32(b'ReplyMarkup'):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
@ -54,8 +57,6 @@ class ButtonMethods:
|
|||
resize = None
|
||||
single_use = None
|
||||
selective = None
|
||||
persistent = None
|
||||
placeholder = None
|
||||
|
||||
rows = []
|
||||
for row in buttons:
|
||||
|
@ -68,10 +69,6 @@ class ButtonMethods:
|
|||
single_use = button.single_use
|
||||
if button.selective is not None:
|
||||
selective = button.selective
|
||||
if button.persistent is not None:
|
||||
persistent = button.persistent
|
||||
if button.placeholder is not None:
|
||||
placeholder = button.placeholder
|
||||
|
||||
button = button.button
|
||||
elif isinstance(button, custom.MessageButton):
|
||||
|
@ -81,21 +78,19 @@ class ButtonMethods:
|
|||
is_inline |= inline
|
||||
is_normal |= not inline
|
||||
|
||||
if button.SUBCLASS_OF_ID == 0xbad74a3: # crc32(b'KeyboardButton')
|
||||
if button.SUBCLASS_OF_ID == 0xbad74a3:
|
||||
# 0xbad74a3 == crc32(b'KeyboardButton')
|
||||
current.append(button)
|
||||
|
||||
if current:
|
||||
rows.append(types.KeyboardButtonRow(current))
|
||||
|
||||
if is_inline and is_normal:
|
||||
if inline_only and is_normal:
|
||||
raise ValueError('You cannot use non-inline buttons here')
|
||||
elif is_inline == is_normal and is_normal:
|
||||
raise ValueError('You cannot mix inline with normal buttons')
|
||||
elif is_inline:
|
||||
return types.ReplyInlineMarkup(rows)
|
||||
# elif is_normal:
|
||||
return types.ReplyKeyboardMarkup(
|
||||
rows=rows,
|
||||
resize=resize,
|
||||
single_use=single_use,
|
||||
selective=selective,
|
||||
persistent=persistent,
|
||||
placeholder=placeholder
|
||||
)
|
||||
rows, resize=resize, single_use=single_use, selective=selective)
|
||||
|
|
|
@ -22,7 +22,6 @@ class _ChatAction:
|
|||
'contact': types.SendMessageChooseContactAction(),
|
||||
'game': types.SendMessageGamePlayAction(),
|
||||
'location': types.SendMessageGeoLocationAction(),
|
||||
'sticker': types.SendMessageChooseStickerAction(),
|
||||
|
||||
'record-audio': types.SendMessageRecordAudioAction(),
|
||||
'record-voice': types.SendMessageRecordAudioAction(), # alias
|
||||
|
@ -97,7 +96,7 @@ class _ChatAction:
|
|||
|
||||
|
||||
class _ParticipantsIter(RequestIter):
|
||||
async def _init(self, entity, filter, search):
|
||||
async def _init(self, entity, filter, search, aggressive):
|
||||
if isinstance(filter, type):
|
||||
if filter in (types.ChannelParticipantsBanned,
|
||||
types.ChannelParticipantsKicked,
|
||||
|
@ -122,8 +121,7 @@ class _ParticipantsIter(RequestIter):
|
|||
self.filter_entity = lambda ent: True
|
||||
|
||||
# Only used for channels, but we should always set the attribute
|
||||
# Called `requests` even though it's just one for legacy purposes.
|
||||
self.requests = None
|
||||
self.requests = []
|
||||
|
||||
if ty == helpers._EntityType.CHANNEL:
|
||||
if self.limit <= 0:
|
||||
|
@ -134,13 +132,22 @@ class _ParticipantsIter(RequestIter):
|
|||
raise StopAsyncIteration
|
||||
|
||||
self.seen = set()
|
||||
self.requests = functions.channels.GetParticipantsRequest(
|
||||
if aggressive and not filter:
|
||||
self.requests.extend(functions.channels.GetParticipantsRequest(
|
||||
channel=entity,
|
||||
filter=types.ChannelParticipantsSearch(x),
|
||||
offset=0,
|
||||
limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
|
||||
hash=0
|
||||
) for x in (search or string.ascii_lowercase))
|
||||
else:
|
||||
self.requests.append(functions.channels.GetParticipantsRequest(
|
||||
channel=entity,
|
||||
filter=filter or types.ChannelParticipantsSearch(search),
|
||||
offset=0,
|
||||
limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
|
||||
hash=0
|
||||
)
|
||||
))
|
||||
|
||||
elif ty == helpers._EntityType.CHAT:
|
||||
full = await self.client(
|
||||
|
@ -155,10 +162,7 @@ class _ParticipantsIter(RequestIter):
|
|||
|
||||
users = {user.id: user for user in full.users}
|
||||
for participant in full.full_chat.participants.participants:
|
||||
if isinstance(participant, types.ChannelParticipantLeft):
|
||||
# See issue #3231 to learn why this is ignored.
|
||||
continue
|
||||
elif isinstance(participant, types.ChannelParticipantBanned):
|
||||
if isinstance(participant, types.ChannelParticipantBanned):
|
||||
user_id = participant.peer.user_id
|
||||
else:
|
||||
user_id = participant.user_id
|
||||
|
@ -185,14 +189,21 @@ class _ParticipantsIter(RequestIter):
|
|||
if not self.requests:
|
||||
return True
|
||||
|
||||
self.requests.limit = min(self.limit - self.requests.offset, _MAX_PARTICIPANTS_CHUNK_SIZE)
|
||||
# Only care about the limit for the first request
|
||||
# (small amount of people, won't be aggressive).
|
||||
#
|
||||
# Most people won't care about getting exactly 12,345
|
||||
# members so it doesn't really matter not to be 100%
|
||||
# precise with being out of the offset/limit here.
|
||||
self.requests[0].limit = min(
|
||||
self.limit - self.requests[0].offset, _MAX_PARTICIPANTS_CHUNK_SIZE)
|
||||
|
||||
if self.requests.offset > self.limit:
|
||||
if self.requests[0].offset > self.limit:
|
||||
return True
|
||||
|
||||
if self.total is None:
|
||||
f = self.requests.filter
|
||||
if (
|
||||
f = self.requests[0].filter
|
||||
if len(self.requests) > 1 or (
|
||||
not isinstance(f, types.ChannelParticipantsRecent)
|
||||
and (not isinstance(f, types.ChannelParticipantsSearch) or f.q)
|
||||
):
|
||||
|
@ -200,31 +211,28 @@ class _ParticipantsIter(RequestIter):
|
|||
# if there's a filter which would reduce the real total number.
|
||||
# getParticipants is cheaper than getFull.
|
||||
self.total = (await self.client(functions.channels.GetParticipantsRequest(
|
||||
channel=self.requests.channel,
|
||||
channel=self.requests[0].channel,
|
||||
filter=types.ChannelParticipantsRecent(),
|
||||
offset=0,
|
||||
limit=1,
|
||||
hash=0
|
||||
))).count
|
||||
|
||||
participants = await self.client(self.requests)
|
||||
results = await self.client(self.requests)
|
||||
for i in reversed(range(len(self.requests))):
|
||||
participants = results[i]
|
||||
if self.total is None:
|
||||
# Will only get here if there was one request with a filter that matched all users.
|
||||
self.total = participants.count
|
||||
if not participants.users:
|
||||
self.requests = None
|
||||
return
|
||||
self.requests.pop(i)
|
||||
continue
|
||||
|
||||
self.requests.offset += len(participants.participants)
|
||||
self.requests[i].offset += len(participants.participants)
|
||||
users = {user.id: user for user in participants.users}
|
||||
for participant in participants.participants:
|
||||
if isinstance(participant, types.ChannelParticipantLeft):
|
||||
# See issue #3231 to learn why this is ignored.
|
||||
continue
|
||||
elif isinstance(participant, types.ChannelParticipantBanned):
|
||||
if not isinstance(participant.peer, types.PeerUser):
|
||||
# May have the entire channel banned. See #3105.
|
||||
continue
|
||||
|
||||
if isinstance(participant, types.ChannelParticipantBanned):
|
||||
user_id = participant.peer.user_id
|
||||
else:
|
||||
user_id = participant.user_id
|
||||
|
@ -419,6 +427,9 @@ class ChatMethods:
|
|||
search (`str`, optional):
|
||||
Look for participants with this string in name/username.
|
||||
|
||||
If ``aggressive is True``, the symbols from this string will
|
||||
be used.
|
||||
|
||||
filter (:tl:`ChannelParticipantsFilter`, optional):
|
||||
The filter to be used, if you want e.g. only admins
|
||||
Note that you might not have permissions for some filter.
|
||||
|
@ -431,11 +442,14 @@ class ChatMethods:
|
|||
use :tl:`ChannelParticipantsKicked` instead.
|
||||
|
||||
aggressive (`bool`, optional):
|
||||
Does nothing. This is kept for backwards-compatibility.
|
||||
Aggressively looks for all participants in the chat.
|
||||
|
||||
There have been several changes to Telegram's API that limits
|
||||
the amount of members that can be retrieved, and this was a
|
||||
hack that no longer works.
|
||||
This is useful for channels since 20 July 2018,
|
||||
Telegram added a server-side limit where only the
|
||||
first 200 members can be retrieved. With this flag
|
||||
set, more than 200 will be often be retrieved.
|
||||
|
||||
This has no effect if a ``filter`` is given.
|
||||
|
||||
Yields
|
||||
The :tl:`User` objects returned by :tl:`GetParticipantsRequest`
|
||||
|
@ -464,7 +478,8 @@ class ChatMethods:
|
|||
limit,
|
||||
entity=entity,
|
||||
filter=filter,
|
||||
search=search
|
||||
search=search,
|
||||
aggressive=aggressive
|
||||
)
|
||||
|
||||
async def get_participants(
|
||||
|
@ -755,7 +770,6 @@ class ChatMethods:
|
|||
* ``'contact'``: choosing a contact.
|
||||
* ``'game'``: playing a game.
|
||||
* ``'location'``: choosing a geo location.
|
||||
* ``'sticker'``: choosing a sticker.
|
||||
* ``'record-audio'``: recording a voice note.
|
||||
You may use ``'record-voice'`` as alias.
|
||||
* ``'record-round'``: recording a round video.
|
||||
|
@ -928,6 +942,9 @@ class ChatMethods:
|
|||
"""
|
||||
entity = await self.get_input_entity(entity)
|
||||
user = await self.get_input_entity(user)
|
||||
ty = helpers._entity_type(user)
|
||||
if ty != helpers._EntityType.USER:
|
||||
raise ValueError('You must pass a user entity')
|
||||
|
||||
perm_names = (
|
||||
'change_info', 'post_messages', 'edit_messages', 'delete_messages',
|
||||
|
@ -965,7 +982,7 @@ class ChatMethods:
|
|||
is_admin = any(locals()[x] for x in perm_names)
|
||||
|
||||
return await self(functions.messages.EditChatAdminRequest(
|
||||
entity.chat_id, user, is_admin=is_admin))
|
||||
entity, user, is_admin=is_admin))
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
|
@ -1114,6 +1131,12 @@ class ChatMethods:
|
|||
))
|
||||
|
||||
user = await self.get_input_entity(user)
|
||||
ty = helpers._entity_type(user)
|
||||
if ty != helpers._EntityType.USER:
|
||||
raise ValueError('You must pass a user entity')
|
||||
|
||||
if isinstance(user, types.InputPeerSelf):
|
||||
raise ValueError('You cannot restrict yourself')
|
||||
|
||||
return await self(functions.channels.EditBannedRequest(
|
||||
channel=entity,
|
||||
|
@ -1160,6 +1183,8 @@ class ChatMethods:
|
|||
"""
|
||||
entity = await self.get_input_entity(entity)
|
||||
user = await self.get_input_entity(user)
|
||||
if helpers._entity_type(user) != helpers._EntityType.USER:
|
||||
raise ValueError('You must pass a user entity')
|
||||
|
||||
ty = helpers._entity_type(entity)
|
||||
if ty == helpers._EntityType.CHAT:
|
||||
|
@ -1229,13 +1254,15 @@ class ChatMethods:
|
|||
if isinstance(entity, types.Channel):
|
||||
FullChat = await self(functions.channels.GetFullChannelRequest(entity))
|
||||
elif isinstance(entity, types.Chat):
|
||||
FullChat = await self(functions.messages.GetFullChatRequest(entity.id))
|
||||
FullChat = await self(functions.messages.GetFullChatRequest(entity))
|
||||
else:
|
||||
return
|
||||
return FullChat.chats[0].default_banned_rights
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
user = await self.get_input_entity(user)
|
||||
if helpers._entity_type(user) != helpers._EntityType.USER:
|
||||
raise ValueError('You must pass a user entity')
|
||||
if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:
|
||||
participant = await self(functions.channels.GetParticipantRequest(
|
||||
entity,
|
||||
|
@ -1244,7 +1271,7 @@ class ChatMethods:
|
|||
return custom.ParticipantPermissions(participant.participant, False)
|
||||
elif helpers._entity_type(entity) == helpers._EntityType.CHAT:
|
||||
chat = await self(functions.messages.GetFullChatRequest(
|
||||
entity.chat_id
|
||||
entity
|
||||
))
|
||||
if isinstance(user, types.InputPeerSelf):
|
||||
user = await self.get_me(input_peer=True)
|
||||
|
|
|
@ -58,8 +58,6 @@ class _DialogsIter(RequestIter):
|
|||
for x in itertools.chain(r.users, r.chats)
|
||||
if not isinstance(x, (types.UserEmpty, types.ChatEmpty))}
|
||||
|
||||
self.client._mb_entity_cache.extend(r.users, r.chats)
|
||||
|
||||
messages = {}
|
||||
for m in r.messages:
|
||||
m._finish_init(self.client, entities, None)
|
||||
|
@ -84,16 +82,14 @@ class _DialogsIter(RequestIter):
|
|||
|
||||
cd = custom.Dialog(self.client, d, entities, message)
|
||||
if cd.dialog.pts:
|
||||
self.client._message_box.try_set_channel_state(
|
||||
utils.get_peer_id(d.peer, add_mark=False), cd.dialog.pts)
|
||||
self.client._channel_pts[cd.id] = cd.dialog.pts
|
||||
|
||||
if not self.ignore_migrated or getattr(
|
||||
cd.entity, 'migrated_to', None) is None:
|
||||
self.buffer.append(cd)
|
||||
|
||||
if not self.buffer or len(r.dialogs) < self.request.limit\
|
||||
if len(r.dialogs) < self.request.limit\
|
||||
or not isinstance(r, types.messages.DialogsSlice):
|
||||
# Buffer being empty means all returned dialogs were skipped (due to offsets).
|
||||
# Less than we requested means we reached the end, or
|
||||
# we didn't get a DialogsSlice which means we got all.
|
||||
return True
|
||||
|
@ -486,16 +482,6 @@ class DialogMethods:
|
|||
Creates a `Conversation <telethon.tl.custom.conversation.Conversation>`
|
||||
with the given entity.
|
||||
|
||||
.. note::
|
||||
|
||||
This Conversation API has certain shortcomings, such as lacking
|
||||
persistence, poor interaction with other event handlers, and
|
||||
overcomplicated usage for anything beyond the simplest case.
|
||||
|
||||
If you plan to interact with a bot without handlers, this works
|
||||
fine, but when running a bot yourself, you may instead prefer
|
||||
to follow the advice from https://stackoverflow.com/a/62246569/.
|
||||
|
||||
This is not the same as just sending a message to create a "dialog"
|
||||
with them, but rather a way to easily send messages and await for
|
||||
responses or other reactions. Refer to its documentation for more.
|
||||
|
|
|
@ -27,22 +27,12 @@ MAX_CHUNK_SIZE = 512 * 1024
|
|||
# 2021-01-15, users reported that `errors.TimeoutError` can occur while downloading files.
|
||||
TIMED_OUT_SLEEP = 1
|
||||
|
||||
|
||||
class _CdnRedirect(Exception):
|
||||
def __init__(self, cdn_redirect=None):
|
||||
self.cdn_redirect = cdn_redirect
|
||||
|
||||
|
||||
class _DirectDownloadIter(RequestIter):
|
||||
async def _init(
|
||||
self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data, cdn_redirect=None):
|
||||
self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data
|
||||
):
|
||||
self.request = functions.upload.GetFileRequest(
|
||||
file, offset=offset, limit=request_size)
|
||||
self._client = self.client
|
||||
self._cdn_redirect = cdn_redirect
|
||||
if cdn_redirect is not None:
|
||||
self.request = functions.upload.GetCdnFileRequest(cdn_redirect.file_token, offset=offset, limit=request_size)
|
||||
self._client = await self.client._get_cdn_client(cdn_redirect)
|
||||
|
||||
self.total = file_size
|
||||
self._stride = stride
|
||||
|
@ -51,7 +41,7 @@ class _DirectDownloadIter(RequestIter):
|
|||
self._msg_data = msg_data
|
||||
self._timed_out = False
|
||||
|
||||
self._exported = dc_id and self._client.session.dc_id != dc_id
|
||||
self._exported = dc_id and self.client.session.dc_id != dc_id
|
||||
if not self._exported:
|
||||
# The used sender will also change if ``FileMigrateError`` occurs
|
||||
self._sender = self.client._sender
|
||||
|
@ -63,12 +53,9 @@ class _DirectDownloadIter(RequestIter):
|
|||
config = await self.client(functions.help.GetConfigRequest())
|
||||
for option in config.dc_options:
|
||||
if option.ip_address == self.client.session.server_address:
|
||||
await utils.maybe_async(
|
||||
self.client.session.set_dc(
|
||||
option.id, option.ip_address, option.port
|
||||
)
|
||||
)
|
||||
await utils.maybe_async(self.client.session.save())
|
||||
option.id, option.ip_address, option.port)
|
||||
self.client.session.save()
|
||||
break
|
||||
|
||||
# TODO Figure out why the session may have the wrong DC ID
|
||||
|
@ -86,20 +73,14 @@ class _DirectDownloadIter(RequestIter):
|
|||
|
||||
async def _request(self):
|
||||
try:
|
||||
result = await self._client._call(self._sender, self.request)
|
||||
result = await self.client._call(self._sender, self.request)
|
||||
self._timed_out = False
|
||||
if isinstance(result, types.upload.FileCdnRedirect):
|
||||
if self.client._mb_entity_cache.self_bot:
|
||||
raise ValueError('FileCdnRedirect but the GetCdnFileRequest API access for bot users is restricted. Try to change api_id to avoid FileCdnRedirect')
|
||||
raise _CdnRedirect(result)
|
||||
if isinstance(result, types.upload.CdnFileReuploadNeeded):
|
||||
await self.client._call(self.client._sender, functions.upload.ReuploadCdnFileRequest(file_token=self._cdn_redirect.file_token, request_token=result.request_token))
|
||||
result = await self._client._call(self._sender, self.request)
|
||||
return result.bytes
|
||||
raise NotImplementedError # TODO Implement
|
||||
else:
|
||||
return result.bytes
|
||||
|
||||
except errors.TimedOutError as e:
|
||||
except errors.TimeoutError as e:
|
||||
if self._timed_out:
|
||||
self.client._log[__name__].warning('Got two timeouts in a row while downloading file')
|
||||
raise
|
||||
|
@ -115,7 +96,7 @@ class _DirectDownloadIter(RequestIter):
|
|||
self._exported = True
|
||||
return await self._request()
|
||||
|
||||
except (errors.FilerefUpgradeNeededError, errors.FileReferenceExpiredError) as e:
|
||||
except errors.FilerefUpgradeNeededError as e:
|
||||
# Only implemented for documents which are the ones that may take that long to download
|
||||
if not self._msg_data \
|
||||
or not isinstance(self.request.location, types.InputDocumentFileLocation) \
|
||||
|
@ -242,8 +223,7 @@ class DownloadMethods:
|
|||
The output file path, directory, or stream-like object.
|
||||
If the path exists and is a file, it will be overwritten.
|
||||
If file is the type `bytes`, it will be downloaded in-memory
|
||||
and returned as a bytestring (i.e. ``file=bytes``, without
|
||||
parentheses or quotes).
|
||||
as a bytestring (e.g. ``file=bytes``).
|
||||
|
||||
download_big (`bool`, optional):
|
||||
Whether to use the big version of the available photos.
|
||||
|
@ -292,9 +272,7 @@ class DownloadMethods:
|
|||
if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)):
|
||||
dc_id = photo.dc_id
|
||||
loc = types.InputPeerPhotoFileLocation(
|
||||
# min users can be used to download profile photos
|
||||
# self.get_input_entity would otherwise not accept those
|
||||
peer=utils.get_input_peer(entity, check_hash=False),
|
||||
peer=await self.get_input_entity(entity),
|
||||
photo_id=photo.photo_id,
|
||||
big=download_big
|
||||
)
|
||||
|
@ -353,8 +331,7 @@ class DownloadMethods:
|
|||
The output file path, directory, or stream-like object.
|
||||
If the path exists and is a file, it will be overwritten.
|
||||
If file is the type `bytes`, it will be downloaded in-memory
|
||||
and returned as a bytestring (i.e. ``file=bytes``, without
|
||||
parentheses or quotes).
|
||||
as a bytestring (e.g. ``file=bytes``).
|
||||
|
||||
progress_callback (`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
|
@ -397,9 +374,6 @@ class DownloadMethods:
|
|||
path = await message.download_media()
|
||||
await message.download_media(filename)
|
||||
|
||||
# Downloading to memory
|
||||
blob = await client.download_media(message, bytes)
|
||||
|
||||
# Printing download progress
|
||||
def callback(current, total):
|
||||
print('Downloaded', current, 'out of', total,
|
||||
|
@ -535,9 +509,7 @@ class DownloadMethods:
|
|||
dc_id: int = None,
|
||||
key: bytes = None,
|
||||
iv: bytes = None,
|
||||
msg_data: tuple = None,
|
||||
cdn_redirect: types.upload.FileCdnRedirect = None
|
||||
) -> typing.Optional[bytes]:
|
||||
msg_data: tuple = None) -> typing.Optional[bytes]:
|
||||
if not part_size_kb:
|
||||
if not file_size:
|
||||
part_size_kb = 64 # Reasonable default
|
||||
|
@ -564,7 +536,7 @@ class DownloadMethods:
|
|||
|
||||
try:
|
||||
async for chunk in self._iter_download(
|
||||
input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data, cdn_redirect=cdn_redirect):
|
||||
input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data):
|
||||
if iv and key:
|
||||
chunk = AES.decrypt_ige(chunk, key, iv)
|
||||
r = f.write(chunk)
|
||||
|
@ -582,20 +554,6 @@ class DownloadMethods:
|
|||
|
||||
if in_memory:
|
||||
return f.getvalue()
|
||||
except _CdnRedirect as e:
|
||||
self._log[__name__].info('FileCdnRedirect to CDN data center %s', e.cdn_redirect.dc_id)
|
||||
return await self._download_file(
|
||||
input_location=input_location,
|
||||
file=file,
|
||||
part_size_kb=part_size_kb,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback,
|
||||
dc_id=e.cdn_redirect.dc_id,
|
||||
key=e.cdn_redirect.encryption_key,
|
||||
iv=e.cdn_redirect.encryption_iv,
|
||||
msg_data=msg_data,
|
||||
cdn_redirect=e.cdn_redirect
|
||||
)
|
||||
finally:
|
||||
if isinstance(file, str) or in_memory:
|
||||
f.close()
|
||||
|
@ -717,8 +675,7 @@ class DownloadMethods:
|
|||
request_size: int = MAX_CHUNK_SIZE,
|
||||
file_size: int = None,
|
||||
dc_id: int = None,
|
||||
msg_data: tuple = None,
|
||||
cdn_redirect: types.upload.FileCdnRedirect = None
|
||||
msg_data: tuple = None
|
||||
):
|
||||
info = utils._get_file_info(file)
|
||||
if info.dc_id is not None:
|
||||
|
@ -769,7 +726,6 @@ class DownloadMethods:
|
|||
request_size=request_size,
|
||||
file_size=file_size,
|
||||
msg_data=msg_data,
|
||||
cdn_redirect=cdn_redirect
|
||||
)
|
||||
|
||||
# endregion
|
||||
|
@ -778,9 +734,6 @@ class DownloadMethods:
|
|||
|
||||
@staticmethod
|
||||
def _get_thumb(thumbs, thumb):
|
||||
if not thumbs:
|
||||
return None
|
||||
|
||||
# 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.
|
||||
|
@ -923,9 +876,6 @@ class DownloadMethods:
|
|||
else:
|
||||
file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
|
||||
size = self._get_thumb(document.thumbs, thumb)
|
||||
if not size or isinstance(size, types.PhotoSizeEmpty):
|
||||
return
|
||||
|
||||
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
|
||||
return self._download_cached_photo_size(size, file)
|
||||
|
||||
|
@ -966,19 +916,22 @@ class DownloadMethods:
|
|||
'END:VCARD\n'
|
||||
).format(f=first_name, l=last_name, p=phone_number).encode('utf-8')
|
||||
|
||||
if file is bytes:
|
||||
return result
|
||||
elif isinstance(file, str):
|
||||
file = cls._get_proper_filename(
|
||||
file, 'contact', '.vcard',
|
||||
possible_names=[first_name, phone_number, last_name]
|
||||
)
|
||||
if file is bytes:
|
||||
return result
|
||||
f = file if hasattr(file, 'write') else open(file, 'wb')
|
||||
f = open(file, 'wb')
|
||||
else:
|
||||
f = file
|
||||
|
||||
try:
|
||||
f.write(result)
|
||||
finally:
|
||||
# Only close the stream if we opened it
|
||||
if f != file:
|
||||
if isinstance(file, str):
|
||||
f.close()
|
||||
|
||||
return file
|
||||
|
@ -995,20 +948,21 @@ class DownloadMethods:
|
|||
)
|
||||
|
||||
# TODO Better way to get opened handles of files and auto-close
|
||||
in_memory = file is bytes
|
||||
if in_memory:
|
||||
f = io.BytesIO()
|
||||
elif isinstance(file, str):
|
||||
kind, possible_names = cls._get_kind_and_names(web.attributes)
|
||||
file = cls._get_proper_filename(
|
||||
file, kind, utils.get_extension(web),
|
||||
possible_names=possible_names
|
||||
)
|
||||
if file is bytes:
|
||||
f = io.BytesIO()
|
||||
elif hasattr(file, 'write'):
|
||||
f = file
|
||||
else:
|
||||
f = open(file, 'wb')
|
||||
else:
|
||||
f = file
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
with aiohttp.ClientSession() as session:
|
||||
# TODO Use progress_callback; get content length from response
|
||||
# https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319
|
||||
async with session.get(web.url) as response:
|
||||
|
@ -1018,10 +972,10 @@ class DownloadMethods:
|
|||
break
|
||||
f.write(chunk)
|
||||
finally:
|
||||
if f != file:
|
||||
if isinstance(file, str) or file is bytes:
|
||||
f.close()
|
||||
|
||||
return f.getvalue() if file is bytes else file
|
||||
return f.getvalue() if in_memory else file
|
||||
|
||||
@staticmethod
|
||||
def _get_proper_filename(file, kind, extension,
|
||||
|
|
|
@ -90,12 +90,7 @@ class MessageParseMethods:
|
|||
|
||||
for i in reversed(range(len(msg_entities))):
|
||||
e = msg_entities[i]
|
||||
if not e.length:
|
||||
# 0-length MessageEntity is no longer valid #3884.
|
||||
# Because the user can provide their own parser (with reasonable 0-length
|
||||
# entities), strip them here rather than fixing the built-in parsers.
|
||||
del msg_entities[i]
|
||||
elif isinstance(e, types.MessageEntityTextUrl):
|
||||
if isinstance(e, types.MessageEntityTextUrl):
|
||||
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
||||
if m:
|
||||
user = int(m.group(1)) if m.group(1) else e.url
|
||||
|
|
|
@ -19,8 +19,7 @@ class _MessagesIter(RequestIter):
|
|||
"""
|
||||
async def _init(
|
||||
self, entity, offset_id, min_id, max_id,
|
||||
from_user, offset_date, add_offset, filter, search, reply_to,
|
||||
scheduled
|
||||
from_user, offset_date, add_offset, filter, search, reply_to
|
||||
):
|
||||
# Note that entity being `None` will perform a global search.
|
||||
if entity:
|
||||
|
@ -85,11 +84,6 @@ class _MessagesIter(RequestIter):
|
|||
offset_id=offset_id,
|
||||
limit=1
|
||||
)
|
||||
elif scheduled:
|
||||
self.request = functions.messages.GetScheduledHistoryRequest(
|
||||
peer=entity,
|
||||
hash=0
|
||||
)
|
||||
elif reply_to is not None:
|
||||
self.request = functions.messages.GetRepliesRequest(
|
||||
peer=self.entity,
|
||||
|
@ -204,24 +198,7 @@ class _MessagesIter(RequestIter):
|
|||
message._finish_init(self.client, entities, self.entity)
|
||||
self.buffer.append(message)
|
||||
|
||||
# Not a slice (using offset would return the same, with e.g. SearchGlobal).
|
||||
if isinstance(r, types.messages.Messages):
|
||||
return True
|
||||
|
||||
# Some channels are "buggy" and may return less messages than
|
||||
# requested (apparently, the messages excluded are, for example,
|
||||
# "not displayable due to local laws").
|
||||
#
|
||||
# This means it's not safe to rely on `len(r.messages) < req.limit` as
|
||||
# the stop condition. Unfortunately more requests must be made.
|
||||
#
|
||||
# However we can still check if the highest ID is equal to or lower
|
||||
# than the limit, in which case there won't be any more messages
|
||||
# because the lowest message ID is 1.
|
||||
#
|
||||
# We also assume the API will always return, at least, one message if
|
||||
# there is more to fetch.
|
||||
if not r.messages or (not self.reverse and r.messages[0].id <= self.request.limit):
|
||||
if len(r.messages) < self.request.limit:
|
||||
return True
|
||||
|
||||
# Get the last message that's not empty (in some rare cases
|
||||
|
@ -359,8 +336,7 @@ class MessageMethods:
|
|||
wait_time: float = None,
|
||||
ids: 'typing.Union[int, typing.Sequence[int]]' = None,
|
||||
reverse: bool = False,
|
||||
reply_to: int = None,
|
||||
scheduled: bool = False
|
||||
reply_to: int = None
|
||||
) -> 'typing.Union[_MessagesIter, _IDsIter]':
|
||||
"""
|
||||
Iterator over the messages for the given chat.
|
||||
|
@ -487,10 +463,6 @@ class MessageMethods:
|
|||
a message and replies to it itself, that reply will not
|
||||
be included in the results.
|
||||
|
||||
scheduled (`bool`, optional):
|
||||
If set to `True`, messages which are scheduled will be returned.
|
||||
All other parameter will be ignored for this, except `entity`.
|
||||
|
||||
Yields
|
||||
Instances of `Message <telethon.tl.custom.message.Message>`.
|
||||
|
||||
|
@ -549,13 +521,10 @@ class MessageMethods:
|
|||
add_offset=add_offset,
|
||||
filter=filter,
|
||||
search=search,
|
||||
reply_to=reply_to,
|
||||
scheduled=scheduled
|
||||
reply_to=reply_to
|
||||
)
|
||||
|
||||
async def get_messages(
|
||||
self: 'TelegramClient', *args, **kwargs
|
||||
) -> typing.Union['hints.TotalList', typing.Optional['types.Message']]:
|
||||
async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':
|
||||
"""
|
||||
Same as `iter_messages()`, but returns a
|
||||
`TotalList <telethon.helpers.TotalList>` instead.
|
||||
|
@ -619,7 +588,7 @@ class MessageMethods:
|
|||
peer=entity,
|
||||
msg_id=utils.get_message_id(message)
|
||||
))
|
||||
m = min(r.messages, key=lambda msg: msg.id)
|
||||
m = r.messages[0]
|
||||
chat = next(c for c in r.chats if c.id == m.peer_id.channel_id)
|
||||
return utils.get_input_peer(chat), m.id
|
||||
|
||||
|
@ -637,15 +606,11 @@ class MessageMethods:
|
|||
thumb: 'hints.FileLike' = None,
|
||||
force_document: bool = False,
|
||||
clear_draft: bool = False,
|
||||
buttons: typing.Optional['hints.MarkupLike'] = None,
|
||||
buttons: 'hints.MarkupLike' = None,
|
||||
silent: bool = None,
|
||||
background: bool = None,
|
||||
supports_streaming: bool = False,
|
||||
schedule: 'hints.DateLike' = None,
|
||||
comment_to: 'typing.Union[int, types.Message]' = None,
|
||||
nosound_video: bool = None,
|
||||
send_as: typing.Optional['hints.EntityLike'] = None,
|
||||
message_effect_id: typing.Optional[int] = None
|
||||
comment_to: 'typing.Union[int, types.Message]' = None
|
||||
) -> 'types.Message':
|
||||
"""
|
||||
Sends a message to the specified user, chat or channel.
|
||||
|
@ -736,9 +701,6 @@ class MessageMethods:
|
|||
channel or not. Defaults to `False`, which means it will
|
||||
notify them. Set it to `True` to alter this behaviour.
|
||||
|
||||
background (`bool`, optional):
|
||||
Whether the message should be send in background.
|
||||
|
||||
supports_streaming (`bool`, optional):
|
||||
Whether the sent video supports streaming or not. Note that
|
||||
Telegram only recognizes as streamable some formats like MP4,
|
||||
|
@ -759,25 +721,6 @@ class MessageMethods:
|
|||
This parameter takes precedence over ``reply_to``. If there is
|
||||
no linked chat, `telethon.errors.sgIdInvalidError` is raised.
|
||||
|
||||
nosound_video (`bool`, optional):
|
||||
Only applicable when sending a video file without an audio
|
||||
track. If set to ``True``, the video will be displayed in
|
||||
Telegram as a video. If set to ``False``, Telegram will attempt
|
||||
to display the video as an animated gif. (It may still display
|
||||
as a video due to other factors.) The value is ignored if set
|
||||
on non-video files. This is set to ``True`` for albums, as gifs
|
||||
cannot be sent in albums.
|
||||
|
||||
send_as (`entity`):
|
||||
Unique identifier (int) or username (str) of the chat or channel to send the message as.
|
||||
You can use this to send the message on behalf of a chat or channel where you have appropriate permissions.
|
||||
Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat,
|
||||
This setting applies to the current message and will remain effective for future messages unless explicitly changed.
|
||||
To set this behavior permanently for all messages, use SaveDefaultSendAs.
|
||||
|
||||
message_effect_id (`int`, optional):
|
||||
Unique identifier of the message effect to be added to the message; for private chats only
|
||||
|
||||
Returns
|
||||
The sent `custom.Message <telethon.tl.custom.message.Message>`.
|
||||
|
||||
|
@ -838,9 +781,6 @@ class MessageMethods:
|
|||
await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5))
|
||||
"""
|
||||
if file is not None:
|
||||
if isinstance(message, types.Message):
|
||||
formatting_entities = formatting_entities or message.entities
|
||||
message = message.message
|
||||
return await self.send_file(
|
||||
entity, file, caption=message, reply_to=reply_to,
|
||||
attributes=attributes, parse_mode=parse_mode,
|
||||
|
@ -848,16 +788,12 @@ class MessageMethods:
|
|||
buttons=buttons, clear_draft=clear_draft, silent=silent,
|
||||
schedule=schedule, supports_streaming=supports_streaming,
|
||||
formatting_entities=formatting_entities,
|
||||
comment_to=comment_to, background=background,
|
||||
nosound_video=nosound_video,
|
||||
send_as=send_as, message_effect_id=message_effect_id
|
||||
comment_to=comment_to
|
||||
)
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
if comment_to is not None:
|
||||
entity, reply_to = await self._get_comment_data(entity, comment_to)
|
||||
else:
|
||||
reply_to = utils.get_message_id(reply_to)
|
||||
|
||||
if isinstance(message, types.Message):
|
||||
if buttons is None:
|
||||
|
@ -875,29 +811,23 @@ class MessageMethods:
|
|||
message.media,
|
||||
caption=message.message,
|
||||
silent=silent,
|
||||
background=background,
|
||||
reply_to=reply_to,
|
||||
buttons=markup,
|
||||
formatting_entities=message.entities,
|
||||
parse_mode=None, # explicitly disable parse_mode to force using even empty formatting_entities
|
||||
schedule=schedule,
|
||||
send_as=send_as, message_effect_id=message_effect_id
|
||||
schedule=schedule
|
||||
)
|
||||
|
||||
request = functions.messages.SendMessageRequest(
|
||||
peer=entity,
|
||||
message=message.message or '',
|
||||
silent=silent,
|
||||
background=background,
|
||||
reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),
|
||||
reply_to_msg_id=utils.get_message_id(reply_to),
|
||||
reply_markup=markup,
|
||||
entities=message.entities,
|
||||
clear_draft=clear_draft,
|
||||
no_webpage=not isinstance(
|
||||
message.media, types.MessageMediaWebPage),
|
||||
schedule_date=schedule,
|
||||
send_as=await self.get_input_entity(send_as) if send_as else None,
|
||||
effect=message_effect_id
|
||||
schedule_date=schedule
|
||||
)
|
||||
message = message.message
|
||||
else:
|
||||
|
@ -913,14 +843,11 @@ class MessageMethods:
|
|||
message=message,
|
||||
entities=formatting_entities,
|
||||
no_webpage=not link_preview,
|
||||
reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),
|
||||
reply_to_msg_id=utils.get_message_id(reply_to),
|
||||
clear_draft=clear_draft,
|
||||
silent=silent,
|
||||
background=background,
|
||||
reply_markup=self.build_reply_markup(buttons),
|
||||
schedule_date=schedule,
|
||||
send_as=await self.get_input_entity(send_as) if send_as else None,
|
||||
effect=message_effect_id
|
||||
schedule_date=schedule
|
||||
)
|
||||
|
||||
result = await self(request)
|
||||
|
@ -934,8 +861,7 @@ class MessageMethods:
|
|||
media=result.media,
|
||||
entities=result.entities,
|
||||
reply_markup=request.reply_markup,
|
||||
ttl_period=result.ttl_period,
|
||||
reply_to=request.reply_to
|
||||
ttl_period=result.ttl_period
|
||||
)
|
||||
message._finish_init(self, {}, entity)
|
||||
return message
|
||||
|
@ -948,13 +874,9 @@ class MessageMethods:
|
|||
messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]',
|
||||
from_peer: 'hints.EntityLike' = None,
|
||||
*,
|
||||
background: bool = None,
|
||||
with_my_score: bool = None,
|
||||
silent: bool = None,
|
||||
as_album: bool = None,
|
||||
schedule: 'hints.DateLike' = None,
|
||||
drop_author: bool = None,
|
||||
drop_media_captions: bool = None,
|
||||
schedule: 'hints.DateLike' = None
|
||||
) -> 'typing.Sequence[types.Message]':
|
||||
"""
|
||||
Forwards the given messages to the specified entity.
|
||||
|
@ -984,12 +906,6 @@ class MessageMethods:
|
|||
the person has the chat muted). Set it to `True` to alter
|
||||
this behaviour.
|
||||
|
||||
background (`bool`, optional):
|
||||
Whether the message should be forwarded in background.
|
||||
|
||||
with_my_score (`bool`, optional):
|
||||
Whether forwarded should contain your game score.
|
||||
|
||||
as_album (`bool`, optional):
|
||||
This flag no longer has any effect.
|
||||
|
||||
|
@ -998,12 +914,6 @@ class MessageMethods:
|
|||
instead they will be scheduled to be automatically sent
|
||||
at a later time.
|
||||
|
||||
drop_author (`bool`, optional):
|
||||
Whether to forward messages without quoting the original author.
|
||||
|
||||
drop_media_captions (`bool`, optional):
|
||||
Whether to strip captions from media. Setting this to `True` requires that `drop_author` also be set to `True`.
|
||||
|
||||
Returns
|
||||
The list of forwarded `Message <telethon.tl.custom.message.Message>`,
|
||||
or a single one if a list wasn't provided as input.
|
||||
|
@ -1062,7 +972,7 @@ class MessageMethods:
|
|||
if isinstance(chunk[0], int):
|
||||
chat = from_peer
|
||||
else:
|
||||
chat = from_peer or await self.get_input_entity(chunk[0].peer_id)
|
||||
chat = await chunk[0].get_input_chat()
|
||||
chunk = [m.id for m in chunk]
|
||||
|
||||
req = functions.messages.ForwardMessagesRequest(
|
||||
|
@ -1070,11 +980,7 @@ class MessageMethods:
|
|||
id=chunk,
|
||||
to_peer=entity,
|
||||
silent=silent,
|
||||
background=background,
|
||||
with_my_score=with_my_score,
|
||||
schedule_date=schedule,
|
||||
drop_author=drop_author,
|
||||
drop_media_captions=drop_media_captions
|
||||
schedule_date=schedule
|
||||
)
|
||||
result = await self(req)
|
||||
sent.extend(self._get_response_message(req, result, entity))
|
||||
|
@ -1084,7 +990,7 @@ class MessageMethods:
|
|||
async def edit_message(
|
||||
self: 'TelegramClient',
|
||||
entity: 'typing.Union[hints.EntityLike, types.Message]',
|
||||
message: 'typing.Union[int, types.Message, types.InputMessageID, str]' = None,
|
||||
message: 'hints.MessageLike' = None,
|
||||
text: str = None,
|
||||
*,
|
||||
parse_mode: str = (),
|
||||
|
@ -1094,7 +1000,7 @@ class MessageMethods:
|
|||
file: 'hints.FileLike' = None,
|
||||
thumb: 'hints.FileLike' = None,
|
||||
force_document: bool = False,
|
||||
buttons: typing.Optional['hints.MarkupLike'] = None,
|
||||
buttons: 'hints.MarkupLike' = None,
|
||||
supports_streaming: bool = False,
|
||||
schedule: 'hints.DateLike' = None
|
||||
) -> 'types.Message':
|
||||
|
@ -1110,11 +1016,11 @@ class MessageMethods:
|
|||
from it, so the next parameter will be assumed to be the
|
||||
message text.
|
||||
|
||||
You may also pass a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`,
|
||||
You may also pass a :tl:`InputBotInlineMessageID`,
|
||||
which is the only way to edit messages that were sent
|
||||
after the user selects an inline query result.
|
||||
|
||||
message (`int` | `Message <telethon.tl.custom.message.Message>` | :tl:`InputMessageID` | `str`):
|
||||
message (`int` | `Message <telethon.tl.custom.message.Message>` | `str`):
|
||||
The ID of the message (or `Message
|
||||
<telethon.tl.custom.message.Message>` itself) to be edited.
|
||||
If the `entity` was a `Message
|
||||
|
@ -1182,7 +1088,7 @@ class MessageMethods:
|
|||
|
||||
Returns
|
||||
The edited `Message <telethon.tl.custom.message.Message>`,
|
||||
unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which
|
||||
unless `entity` was a :tl:`InputBotInlineMessageID` in which
|
||||
case this method returns a boolean.
|
||||
|
||||
Raises
|
||||
|
@ -1208,7 +1114,7 @@ class MessageMethods:
|
|||
# or
|
||||
await client.edit_message(message, 'hello!!!')
|
||||
"""
|
||||
if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
|
||||
if isinstance(entity, types.InputBotInlineMessageID):
|
||||
text = text or message
|
||||
message = entity
|
||||
elif isinstance(entity, types.Message):
|
||||
|
@ -1224,7 +1130,7 @@ class MessageMethods:
|
|||
attributes=attributes,
|
||||
force_document=force_document)
|
||||
|
||||
if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
|
||||
if isinstance(entity, types.InputBotInlineMessageID):
|
||||
request = functions.messages.EditInlineBotMessageRequest(
|
||||
id=entity,
|
||||
message=text,
|
||||
|
@ -1342,8 +1248,7 @@ class MessageMethods:
|
|||
message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None,
|
||||
*,
|
||||
max_id: int = None,
|
||||
clear_mentions: bool = False,
|
||||
clear_reactions: bool = False) -> bool:
|
||||
clear_mentions: bool = False) -> bool:
|
||||
"""
|
||||
Marks messages as read and optionally clears mentions.
|
||||
|
||||
|
@ -1377,13 +1282,6 @@ class MessageMethods:
|
|||
If no message is provided, this will be the only action
|
||||
taken.
|
||||
|
||||
clear_reactions (`bool`):
|
||||
Whether the reactions badge should be cleared (so that
|
||||
there are no more reaction notifications) or not for the given entity.
|
||||
|
||||
If no message is provided, this will be the only action
|
||||
taken.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -1406,10 +1304,6 @@ class MessageMethods:
|
|||
entity = await self.get_input_entity(entity)
|
||||
if clear_mentions:
|
||||
await self(functions.messages.ReadMentionsRequest(entity))
|
||||
if max_id is None and not clear_reactions:
|
||||
return True
|
||||
if clear_reactions:
|
||||
await self(functions.messages.ReadReactionsRequest(entity))
|
||||
if max_id is None:
|
||||
return True
|
||||
|
||||
|
@ -1510,11 +1404,12 @@ class MessageMethods:
|
|||
)
|
||||
result = await self(request)
|
||||
|
||||
# Unpinning does not produce a service message.
|
||||
# Pinning a message that was already pinned also produces no service message.
|
||||
# Pinning a message in your own chat does not produce a service message,
|
||||
# but pinning on a private conversation with someone else does.
|
||||
if unpin or not result.updates:
|
||||
# Unpinning does not produce a service message
|
||||
if unpin:
|
||||
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
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import abc
|
||||
import inspect
|
||||
import re
|
||||
import asyncio
|
||||
import collections
|
||||
|
@ -7,17 +6,16 @@ import logging
|
|||
import platform
|
||||
import time
|
||||
import typing
|
||||
import datetime
|
||||
import pathlib
|
||||
|
||||
from .. import utils, version, helpers, __name__ as __base_name__
|
||||
from .. import version, helpers, __name__ as __base_name__
|
||||
from ..crypto import rsa
|
||||
from ..entitycache import EntityCache
|
||||
from ..extensions import markdown
|
||||
from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
|
||||
from ..sessions import Session, SQLiteSession, MemorySession
|
||||
from ..statecache import StateCache
|
||||
from ..tl import functions, types
|
||||
from ..tl.alltlobjects import LAYER
|
||||
from .._updates import MessageBox, EntityCache as MbEntityCache, SessionState, ChannelState, Entity, EntityType
|
||||
|
||||
DEFAULT_DC_ID = 2
|
||||
DEFAULT_IPV4_IP = '149.154.167.51'
|
||||
|
@ -193,7 +191,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
Defaults to `lang_code`.
|
||||
|
||||
loop (`asyncio.AbstractEventLoop`, optional):
|
||||
Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`.
|
||||
Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`.
|
||||
This argument is ignored.
|
||||
|
||||
base_logger (`str` | `logging.Logger`, optional):
|
||||
|
@ -201,29 +199,6 @@ class TelegramBaseClient(abc.ABC):
|
|||
If a `str` is given, it'll be passed to `logging.getLogger()`. If a
|
||||
`logging.Logger` is given, it'll be used directly. If something
|
||||
else or nothing is given, the default logger will be used.
|
||||
|
||||
receive_updates (`bool`, optional):
|
||||
Whether the client will receive updates or not. By default, updates
|
||||
will be received from Telegram as they occur.
|
||||
|
||||
Turning this off means that Telegram will not send updates at all
|
||||
so event handlers, conversations, and QR login will not work.
|
||||
However, certain scripts don't need updates, so this will reduce
|
||||
the amount of bandwidth used.
|
||||
|
||||
entity_cache_limit (`int`, optional):
|
||||
How many users, chats and channels to keep in the in-memory cache
|
||||
at most. This limit is checked against when processing updates.
|
||||
|
||||
When this limit is reached or exceeded, all entities that are not
|
||||
required for update handling will be flushed to the session file.
|
||||
|
||||
Note that this implies that there is a lower bound to the amount
|
||||
of entities that must be kept in memory.
|
||||
|
||||
Setting this limit too low will cause the library to attempt to
|
||||
flush entities to the session file even if no entities can be
|
||||
removed from the in-memory cache, which will degrade performance.
|
||||
"""
|
||||
|
||||
# Current TelegramClient version
|
||||
|
@ -237,7 +212,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
|
||||
def __init__(
|
||||
self: 'TelegramClient',
|
||||
session: 'typing.Union[str, pathlib.Path, Session]',
|
||||
session: 'typing.Union[str, Session]',
|
||||
api_id: int,
|
||||
api_hash: str,
|
||||
*,
|
||||
|
@ -259,11 +234,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
lang_code: str = 'en',
|
||||
system_lang_code: str = 'en',
|
||||
loop: asyncio.AbstractEventLoop = None,
|
||||
base_logger: typing.Union[str, logging.Logger] = None,
|
||||
receive_updates: bool = True,
|
||||
catch_up: bool = False,
|
||||
entity_cache_limit: int = 5000
|
||||
):
|
||||
base_logger: typing.Union[str, logging.Logger] = None):
|
||||
if not api_id or not api_hash:
|
||||
raise ValueError(
|
||||
"Your API ID or Hash cannot be empty or None. "
|
||||
|
@ -286,9 +257,9 @@ class TelegramBaseClient(abc.ABC):
|
|||
self._log = _Loggers()
|
||||
|
||||
# Determine what session object we have
|
||||
if isinstance(session, (str, pathlib.Path)):
|
||||
if isinstance(session, str) or session is None:
|
||||
try:
|
||||
session = SQLiteSession(str(session))
|
||||
session = SQLiteSession(session)
|
||||
except ImportError:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
|
@ -299,17 +270,24 @@ class TelegramBaseClient(abc.ABC):
|
|||
'you use another session storage'
|
||||
)
|
||||
session = MemorySession()
|
||||
elif session is None:
|
||||
session = MemorySession()
|
||||
elif not isinstance(session, Session):
|
||||
raise TypeError(
|
||||
'The given session must be a str or a Session instance.'
|
||||
)
|
||||
|
||||
# ':' in session.server_address is True if it's an IPv6 address
|
||||
if (not session.server_address or
|
||||
(':' in session.server_address) != use_ipv6):
|
||||
session.set_dc(
|
||||
DEFAULT_DC_ID,
|
||||
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
|
||||
DEFAULT_PORT
|
||||
)
|
||||
|
||||
self.flood_sleep_threshold = flood_sleep_threshold
|
||||
|
||||
# TODO Use AsyncClassWrapper(session)
|
||||
# ChatGetter and SenderGetter can use the in-memory _mb_entity_cache
|
||||
# ChatGetter and SenderGetter can use the in-memory _entity_cache
|
||||
# to avoid network access and the need for await in session files.
|
||||
#
|
||||
# The session files only wants the entities to persist
|
||||
|
@ -317,6 +295,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
# TODO Session should probably return all cached
|
||||
# info of entities, not just the input versions
|
||||
self.session = session
|
||||
self._entity_cache = EntityCache()
|
||||
self.api_id = int(api_id)
|
||||
self.api_hash = api_hash
|
||||
|
||||
|
@ -387,27 +366,45 @@ class TelegramBaseClient(abc.ABC):
|
|||
proxy=init_proxy
|
||||
)
|
||||
|
||||
self._sender = MTProtoSender(
|
||||
self.session.auth_key,
|
||||
loggers=self._log,
|
||||
retries=self._connection_retries,
|
||||
delay=self._retry_delay,
|
||||
auto_reconnect=self._auto_reconnect,
|
||||
connect_timeout=self._timeout,
|
||||
auth_key_callback=self._auth_key_callback,
|
||||
update_callback=self._handle_update,
|
||||
auto_reconnect_callback=self._handle_auto_reconnect
|
||||
)
|
||||
|
||||
# Remember flood-waited requests to avoid making them again
|
||||
self._flood_waited_requests = {}
|
||||
|
||||
# Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders
|
||||
self._borrowed_senders = {}
|
||||
self._borrow_sender_lock = asyncio.Lock()
|
||||
self._exported_sessions = {}
|
||||
|
||||
self._loop = None # only used as a sanity check
|
||||
self._updates_error = None
|
||||
self._updates_handle = None
|
||||
self._keepalive_handle = None
|
||||
self._last_request = time.time()
|
||||
self._no_updates = not receive_updates
|
||||
self._channel_pts = {}
|
||||
|
||||
# Used for non-sequential updates, in order to terminate all pending tasks on disconnect.
|
||||
self._sequential_updates = sequential_updates
|
||||
self._event_handler_tasks = set()
|
||||
if sequential_updates:
|
||||
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.
|
||||
self._updates_queue = set()
|
||||
self._dispatching_updates_queue = None
|
||||
|
||||
self._authorized = None # None = unknown, False = no, True = yes
|
||||
|
||||
# Update state (for catching up after a disconnection)
|
||||
# TODO Get state from channels too
|
||||
self._state_cache = StateCache(
|
||||
self.session.get_update_state(0), self._log)
|
||||
|
||||
# Some further state for subclasses
|
||||
self._event_builders = []
|
||||
|
||||
|
@ -432,29 +429,13 @@ class TelegramBaseClient(abc.ABC):
|
|||
self._phone = None
|
||||
self._tos = None
|
||||
|
||||
# Sometimes we need to know who we are, cache the self peer
|
||||
self._self_input_peer = None
|
||||
self._bot = None
|
||||
|
||||
# A place to store if channels are a megagroup or not (see `edit_admin`)
|
||||
self._megagroup_cache = {}
|
||||
|
||||
# This is backported from v2 in a very ad-hoc way just to get proper update handling
|
||||
self._catch_up = catch_up
|
||||
self._updates_queue = asyncio.Queue()
|
||||
self._message_box = MessageBox(self._log['messagebox'])
|
||||
self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference)
|
||||
self._entity_cache_limit = entity_cache_limit
|
||||
|
||||
self._sender = MTProtoSender(
|
||||
self.session.auth_key,
|
||||
loggers=self._log,
|
||||
retries=self._connection_retries,
|
||||
delay=self._retry_delay,
|
||||
auto_reconnect=self._auto_reconnect,
|
||||
connect_timeout=self._timeout,
|
||||
auth_key_callback=self._auth_key_callback,
|
||||
updates_queue=self._updates_queue,
|
||||
auto_reconnect_callback=self._handle_auto_reconnect
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Properties
|
||||
|
@ -476,7 +457,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
# Join the task (wait for it to complete)
|
||||
await task
|
||||
"""
|
||||
return helpers.get_running_loop()
|
||||
return asyncio.get_event_loop()
|
||||
|
||||
@property
|
||||
def disconnected(self: 'TelegramClient') -> asyncio.Future:
|
||||
|
@ -529,26 +510,6 @@ class TelegramBaseClient(abc.ABC):
|
|||
except OSError:
|
||||
print('Failed to connect')
|
||||
"""
|
||||
if self.session is None:
|
||||
raise ValueError('TelegramClient instance cannot be reused after logging out')
|
||||
|
||||
if self._loop is None:
|
||||
self._loop = helpers.get_running_loop()
|
||||
elif self._loop != helpers.get_running_loop():
|
||||
raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')
|
||||
|
||||
# ':' in session.server_address is True if it's an IPv6 address
|
||||
if (not self.session.server_address or
|
||||
(':' in self.session.server_address) != self._use_ipv6):
|
||||
await utils.maybe_async(
|
||||
self.session.set_dc(
|
||||
DEFAULT_DC_ID,
|
||||
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
|
||||
DEFAULT_PORT
|
||||
)
|
||||
)
|
||||
await utils.maybe_async(self.session.save())
|
||||
|
||||
if not await self._sender.connect(self._connection(
|
||||
self.session.server_address,
|
||||
self.session.port,
|
||||
|
@ -561,54 +522,15 @@ class TelegramBaseClient(abc.ABC):
|
|||
return
|
||||
|
||||
self.session.auth_key = self._sender.auth_key
|
||||
await utils.maybe_async(self.session.save())
|
||||
|
||||
try:
|
||||
# See comment when saving entities to understand this hack
|
||||
self_entity = await utils.maybe_async(self.session.get_input_entity(0))
|
||||
self_id = self_entity.access_hash
|
||||
self_user = await utils.maybe_async(self.session.get_input_entity(self_id))
|
||||
self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if self._catch_up:
|
||||
ss = SessionState(0, 0, False, 0, 0, 0, 0, None)
|
||||
cs = []
|
||||
|
||||
update_states = await utils.maybe_async(self.session.get_update_states())
|
||||
for entity_id, state in update_states:
|
||||
if entity_id == 0:
|
||||
# TODO current session doesn't store self-user info but adding that is breaking on downstream session impls
|
||||
ss = SessionState(0, 0, False, state.pts, state.qts, int(state.date.timestamp()), state.seq, None)
|
||||
else:
|
||||
cs.append(ChannelState(entity_id, state.pts))
|
||||
|
||||
self._message_box.load(ss, cs)
|
||||
for state in cs:
|
||||
try:
|
||||
entity = await utils.maybe_async(self.session.get_input_entity(state.channel_id))
|
||||
except ValueError:
|
||||
self._log[__name__].warning(
|
||||
'No access_hash in cache for channel %s, will not catch up', state.channel_id)
|
||||
else:
|
||||
self._mb_entity_cache.put(Entity(EntityType.CHANNEL, entity.channel_id, entity.access_hash))
|
||||
self.session.save()
|
||||
|
||||
self._init_request.query = functions.help.GetConfigRequest()
|
||||
|
||||
req = self._init_request
|
||||
if self._no_updates:
|
||||
req = functions.InvokeWithoutUpdatesRequest(req)
|
||||
|
||||
await self._sender.send(functions.InvokeWithLayerRequest(LAYER, req))
|
||||
|
||||
if self._message_box.is_empty():
|
||||
me = await self.get_me()
|
||||
if me:
|
||||
await self._on_login(me) # also calls GetState to initialize the MessageBox
|
||||
await self._sender.send(functions.InvokeWithLayerRequest(
|
||||
LAYER, self._init_request
|
||||
))
|
||||
|
||||
self._updates_handle = self.loop.create_task(self._update_loop())
|
||||
self._keepalive_handle = self.loop.create_task(self._keepalive_loop())
|
||||
|
||||
def is_connected(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
|
@ -633,12 +555,6 @@ class TelegramBaseClient(abc.ABC):
|
|||
coroutine that you should await on your own code; otherwise
|
||||
the loop is ran until said coroutine completes.
|
||||
|
||||
Event handlers which are currently running will be cancelled before
|
||||
this function returns (in order to properly clean-up their tasks).
|
||||
In particular, this means that using ``disconnect`` in a handler
|
||||
will cause code after the ``disconnect`` to never run. If this is
|
||||
needed, consider spawning a separate task to do the remaining work.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -646,11 +562,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
await client.disconnect()
|
||||
"""
|
||||
if self.loop.is_running():
|
||||
# Disconnect may be called from an event handler, which would
|
||||
# cancel itself during itself and never actually complete the
|
||||
# disconnection. Shield the task to prevent disconnect itself
|
||||
# from being cancelled. See issue #3942 for more details.
|
||||
return asyncio.shield(self.loop.create_task(self._disconnect_coro()))
|
||||
return self._disconnect_coro()
|
||||
else:
|
||||
try:
|
||||
self.loop.run_until_complete(self._disconnect_coro())
|
||||
|
@ -691,32 +603,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
else:
|
||||
connection._proxy = proxy
|
||||
|
||||
async def _save_states_and_entities(self: 'TelegramClient'):
|
||||
# As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``.
|
||||
# This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved.
|
||||
# Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities.
|
||||
# It doesn't matter if we put users in the list of chats.
|
||||
if self._mb_entity_cache.self_id:
|
||||
await utils.maybe_async(
|
||||
self.session.process_entities(
|
||||
types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], [])
|
||||
)
|
||||
)
|
||||
|
||||
ss, cs = self._message_box.session_state()
|
||||
await utils.maybe_async(self.session.set_update_state(0, types.updates.State(**ss, unread_count=0)))
|
||||
now = datetime.datetime.now() # any datetime works; channels don't need it
|
||||
for channel_id, pts in cs.items():
|
||||
await utils.maybe_async(
|
||||
self.session.set_update_state(
|
||||
channel_id, types.updates.State(pts, 0, now, 0, unread_count=0)
|
||||
)
|
||||
)
|
||||
|
||||
async def _disconnect_coro(self: 'TelegramClient'):
|
||||
if self.session is None:
|
||||
return # already logged out and disconnected
|
||||
|
||||
await self._disconnect()
|
||||
|
||||
# Also clean-up all exported senders because we're done with them
|
||||
|
@ -736,16 +623,24 @@ class TelegramBaseClient(abc.ABC):
|
|||
|
||||
# trio's nurseries would handle this for us, but this is asyncio.
|
||||
# All tasks spawned in the background should properly be terminated.
|
||||
if self._event_handler_tasks:
|
||||
for task in self._event_handler_tasks:
|
||||
if self._dispatching_updates_queue is None and self._updates_queue:
|
||||
for task in self._updates_queue:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.wait(self._event_handler_tasks)
|
||||
self._event_handler_tasks.clear()
|
||||
await asyncio.wait(self._updates_queue)
|
||||
self._updates_queue.clear()
|
||||
|
||||
await self._save_states_and_entities()
|
||||
pts, date = self._state_cache[None]
|
||||
if pts and date:
|
||||
self.session.set_update_state(0, types.updates.State(
|
||||
pts=pts,
|
||||
qts=0,
|
||||
date=date,
|
||||
seq=0,
|
||||
unread_count=0
|
||||
))
|
||||
|
||||
await utils.maybe_async(self.session.close())
|
||||
self.session.close()
|
||||
|
||||
async def _disconnect(self: 'TelegramClient'):
|
||||
"""
|
||||
|
@ -756,8 +651,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
"""
|
||||
await self._sender.disconnect()
|
||||
await helpers._cancel(self._log[__name__],
|
||||
updates_handle=self._updates_handle,
|
||||
keepalive_handle=self._keepalive_handle)
|
||||
updates_handle=self._updates_handle)
|
||||
|
||||
async def _switch_dc(self: 'TelegramClient', new_dc):
|
||||
"""
|
||||
|
@ -766,22 +660,22 @@ class TelegramBaseClient(abc.ABC):
|
|||
self._log[__name__].info('Reconnecting to new data center %s', new_dc)
|
||||
dc = await self._get_dc(new_dc)
|
||||
|
||||
await utils.maybe_async(self.session.set_dc(dc.id, dc.ip_address, dc.port))
|
||||
self.session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
# auth_key's are associated with a server, which has now changed
|
||||
# so it's not valid anymore. Set to None to force recreating it.
|
||||
self._sender.auth_key.key = None
|
||||
self.session.auth_key = None
|
||||
await utils.maybe_async(self.session.save())
|
||||
self.session.save()
|
||||
await self._disconnect()
|
||||
return await self.connect()
|
||||
|
||||
async def _auth_key_callback(self: 'TelegramClient', auth_key):
|
||||
def _auth_key_callback(self: 'TelegramClient', auth_key):
|
||||
"""
|
||||
Callback from the sender whenever it needed to generate a
|
||||
new authorization key. This means we are not authorized.
|
||||
"""
|
||||
self.session.auth_key = auth_key
|
||||
await utils.maybe_async(self.session.save())
|
||||
self.session.save()
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -796,8 +690,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
if cdn and not self._cdn_config:
|
||||
cls._cdn_config = await self(functions.help.GetCdnConfigRequest())
|
||||
for pk in cls._cdn_config.public_keys:
|
||||
if pk.dc_id == dc_id:
|
||||
rsa.add_key(pk.public_key, old=False)
|
||||
rsa.add_key(pk.public_key)
|
||||
|
||||
try:
|
||||
return next(
|
||||
|
@ -810,13 +703,10 @@ class TelegramBaseClient(abc.ABC):
|
|||
'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check',
|
||||
dc_id, cdn, self._use_ipv6
|
||||
)
|
||||
try:
|
||||
return next(
|
||||
dc for dc in cls._config.dc_options
|
||||
if dc.id == dc_id and bool(dc.cdn) == cdn
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(f'Failed to get DC {dc_id} (cdn = {cdn})')
|
||||
|
||||
async def _create_exported_sender(self: 'TelegramClient', dc_id):
|
||||
"""
|
||||
|
@ -904,30 +794,28 @@ class TelegramBaseClient(abc.ABC):
|
|||
|
||||
async def _get_cdn_client(self: 'TelegramClient', cdn_redirect):
|
||||
"""Similar to ._borrow_exported_client, but for CDNs"""
|
||||
# TODO Implement
|
||||
raise NotImplementedError
|
||||
session = self._exported_sessions.get(cdn_redirect.dc_id)
|
||||
if not session:
|
||||
dc = await self._get_dc(cdn_redirect.dc_id, cdn=True)
|
||||
session = await utils.maybe_async(self.session.clone())
|
||||
await utils.maybe_async(session.set_dc(dc.id, dc.ip_address, dc.port))
|
||||
session = self.session.clone()
|
||||
await session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
self._exported_sessions[cdn_redirect.dc_id] = session
|
||||
|
||||
self._log[__name__].info('Creating new CDN client')
|
||||
client = self.__class__(
|
||||
client = TelegramBaseClient(
|
||||
session, self.api_id, self.api_hash,
|
||||
proxy=self._proxy,
|
||||
timeout=self._timeout,
|
||||
loop=self.loop
|
||||
proxy=self._sender.connection.conn.proxy,
|
||||
timeout=self._sender.connection.get_timeout()
|
||||
)
|
||||
|
||||
session.auth_key = self._sender.auth_key
|
||||
await client._sender.connect(self._connection(
|
||||
session.server_address,
|
||||
session.port,
|
||||
session.dc_id,
|
||||
loggers=self._log,
|
||||
proxy=self._proxy,
|
||||
local_addr=self._local_addr
|
||||
))
|
||||
# This will make use of the new RSA keys for this specific CDN.
|
||||
#
|
||||
# We won't be calling GetConfigRequest because it's only called
|
||||
# when needed by ._get_dc, and also it's static so it's likely
|
||||
# set already. Avoid invoking non-CDN methods by not syncing updates.
|
||||
client.connect(_sync_updates=False)
|
||||
return client
|
||||
|
||||
# endregion
|
||||
|
@ -949,17 +837,16 @@ class TelegramBaseClient(abc.ABC):
|
|||
executed sequentially on the server. They run in arbitrary
|
||||
order by default.
|
||||
|
||||
flood_sleep_threshold (`int` | `None`, optional):
|
||||
The flood sleep threshold to use for this request. This overrides
|
||||
the default value stored in
|
||||
`client.flood_sleep_threshold <telethon.client.telegrambaseclient.TelegramBaseClient.flood_sleep_threshold>`
|
||||
|
||||
Returns:
|
||||
The result of the request (often a `TLObject`) or a list of
|
||||
results if more than one request was given.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _handle_update(self: 'TelegramClient', update):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _update_loop(self: 'TelegramClient'):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -7,24 +7,15 @@ import time
|
|||
import traceback
|
||||
import typing
|
||||
import logging
|
||||
import warnings
|
||||
from collections import deque
|
||||
import sqlite3
|
||||
|
||||
from .. import events, utils, errors
|
||||
from ..events.common import EventBuilder, EventCommon
|
||||
from ..tl import types, functions
|
||||
from .._updates import GapError, PrematureEndReason
|
||||
from ..helpers import get_running_loop
|
||||
from ..version import __version__
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
Callback = typing.Callable[[typing.Any], typing.Any]
|
||||
|
||||
class UpdateMethods:
|
||||
|
||||
# region Public methods
|
||||
|
@ -33,34 +24,18 @@ class UpdateMethods:
|
|||
try:
|
||||
# Make a high-level request to notify that we want updates
|
||||
await self(functions.updates.GetStateRequest())
|
||||
result = await self.disconnected
|
||||
if self._updates_error is not None:
|
||||
raise self._updates_error
|
||||
return result
|
||||
return await self.disconnected
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
await self.disconnect()
|
||||
|
||||
async def set_receive_updates(self: 'TelegramClient', receive_updates):
|
||||
"""
|
||||
Change the value of `receive_updates`.
|
||||
|
||||
This is an `async` method, because in order for Telegram to start
|
||||
sending updates again, a request must be made.
|
||||
"""
|
||||
self._no_updates = not receive_updates
|
||||
if receive_updates:
|
||||
await self(functions.updates.GetStateRequest())
|
||||
|
||||
def run_until_disconnected(self: 'TelegramClient'):
|
||||
"""
|
||||
Runs the event loop until the library is disconnected.
|
||||
|
||||
It also notifies Telegram that we want to receive updates
|
||||
as described in https://core.telegram.org/api/updates.
|
||||
If an unexpected error occurs during update handling,
|
||||
the client will disconnect and said error will be raised.
|
||||
|
||||
Manual disconnections can be made by calling `disconnect()
|
||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`
|
||||
|
@ -129,7 +104,7 @@ class UpdateMethods:
|
|||
|
||||
def add_event_handler(
|
||||
self: 'TelegramClient',
|
||||
callback: Callback,
|
||||
callback: callable,
|
||||
event: EventBuilder = None):
|
||||
"""
|
||||
Registers a new event handler callback.
|
||||
|
@ -178,7 +153,7 @@ class UpdateMethods:
|
|||
|
||||
def remove_event_handler(
|
||||
self: 'TelegramClient',
|
||||
callback: Callback,
|
||||
callback: callable,
|
||||
event: EventBuilder = None) -> int:
|
||||
"""
|
||||
Inverse operation of `add_event_handler()`.
|
||||
|
@ -216,7 +191,7 @@ class UpdateMethods:
|
|||
return found
|
||||
|
||||
def list_event_handlers(self: 'TelegramClient')\
|
||||
-> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]':
|
||||
-> 'typing.Sequence[typing.Tuple[callable, EventBuilder]]':
|
||||
"""
|
||||
Lists all registered event handlers.
|
||||
|
||||
|
@ -249,240 +224,106 @@ class UpdateMethods:
|
|||
|
||||
await client.catch_up()
|
||||
"""
|
||||
await self._updates_queue.put(types.UpdatesTooLong())
|
||||
pts, date = self._state_cache[None]
|
||||
if not pts:
|
||||
return
|
||||
|
||||
self.session.catching_up = True
|
||||
try:
|
||||
while True:
|
||||
d = await self(functions.updates.GetDifferenceRequest(
|
||||
pts, date, 0
|
||||
))
|
||||
if isinstance(d, (types.updates.DifferenceSlice,
|
||||
types.updates.Difference)):
|
||||
if isinstance(d, types.updates.Difference):
|
||||
state = d.state
|
||||
else:
|
||||
state = d.intermediate_state
|
||||
|
||||
pts, date = state.pts, state.date
|
||||
self._handle_update(types.Updates(
|
||||
users=d.users,
|
||||
chats=d.chats,
|
||||
date=state.date,
|
||||
seq=state.seq,
|
||||
updates=d.other_updates + [
|
||||
types.UpdateNewMessage(m, 0, 0)
|
||||
for m in d.new_messages
|
||||
]
|
||||
))
|
||||
|
||||
# TODO Implement upper limit (max_pts)
|
||||
# We don't want to fetch updates we already know about.
|
||||
#
|
||||
# We may still get duplicates because the Difference
|
||||
# contains a lot of updates and presumably only has
|
||||
# the state for the last one, but at least we don't
|
||||
# unnecessarily fetch too many.
|
||||
#
|
||||
# updates.getDifference's pts_total_limit seems to mean
|
||||
# "how many pts is the request allowed to return", and
|
||||
# if there is more than that, it returns "too long" (so
|
||||
# there would be duplicate updates since we know about
|
||||
# some). This can be used to detect collisions (i.e.
|
||||
# it would return an update we have already seen).
|
||||
else:
|
||||
if isinstance(d, types.updates.DifferenceEmpty):
|
||||
date = d.date
|
||||
elif isinstance(d, types.updates.DifferenceTooLong):
|
||||
pts = d.pts
|
||||
break
|
||||
except (ConnectionError, asyncio.CancelledError):
|
||||
pass
|
||||
finally:
|
||||
# TODO Save new pts to session
|
||||
self._state_cache._pts_date = (pts, date)
|
||||
self.session.catching_up = False
|
||||
|
||||
# endregion
|
||||
|
||||
# region Private methods
|
||||
|
||||
# It is important to not make _handle_update async because we rely on
|
||||
# the order that the updates arrive in to update the pts and date to
|
||||
# be always-increasing. There is also no need to make this async.
|
||||
def _handle_update(self: 'TelegramClient', update):
|
||||
self.session.process_entities(update)
|
||||
self._entity_cache.add(update)
|
||||
|
||||
if isinstance(update, (types.Updates, types.UpdatesCombined)):
|
||||
entities = {utils.get_peer_id(x): x for x in
|
||||
itertools.chain(update.users, update.chats)}
|
||||
for u in update.updates:
|
||||
self._process_update(u, update.updates, entities=entities)
|
||||
elif isinstance(update, types.UpdateShort):
|
||||
self._process_update(update.update, None)
|
||||
else:
|
||||
self._process_update(update, None)
|
||||
|
||||
self._state_cache.update(update)
|
||||
|
||||
def _process_update(self: 'TelegramClient', update, others, entities=None):
|
||||
update._entities = entities or {}
|
||||
|
||||
# This part is somewhat hot so we don't bother patching
|
||||
# update with channel ID/its state. Instead we just pass
|
||||
# arguments which is faster.
|
||||
channel_id = self._state_cache.get_channel_id(update)
|
||||
args = (update, others, channel_id, self._state_cache[channel_id])
|
||||
if self._dispatching_updates_queue is None:
|
||||
task = self.loop.create_task(self._dispatch_update(*args))
|
||||
self._updates_queue.add(task)
|
||||
task.add_done_callback(lambda _: self._updates_queue.discard(task))
|
||||
else:
|
||||
self._updates_queue.put_nowait(args)
|
||||
if not self._dispatching_updates_queue.is_set():
|
||||
self._dispatching_updates_queue.set()
|
||||
self.loop.create_task(self._dispatch_queue_updates())
|
||||
|
||||
self._state_cache.update(update)
|
||||
|
||||
async def _update_loop(self: 'TelegramClient'):
|
||||
# If the MessageBox is not empty, the account had to be logged-in to fill in its state.
|
||||
# This flag is used to propagate the "you got logged-out" error up (but getting logged-out
|
||||
# can only happen if it was once logged-in).
|
||||
was_once_logged_in = self._authorized is True or not self._message_box.is_empty()
|
||||
|
||||
self._updates_error = None
|
||||
try:
|
||||
if self._catch_up:
|
||||
# User wants to catch up as soon as the client is up and running,
|
||||
# so this is the best place to do it.
|
||||
await self.catch_up()
|
||||
|
||||
updates_to_dispatch = deque()
|
||||
|
||||
while self.is_connected():
|
||||
if updates_to_dispatch:
|
||||
if self._sequential_updates:
|
||||
await self._dispatch_update(updates_to_dispatch.popleft())
|
||||
else:
|
||||
while updates_to_dispatch:
|
||||
# TODO if _dispatch_update fails for whatever reason, it's not logged! this should be fixed
|
||||
task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft()))
|
||||
self._event_handler_tasks.add(task)
|
||||
task.add_done_callback(self._event_handler_tasks.discard)
|
||||
|
||||
continue
|
||||
|
||||
if len(self._mb_entity_cache) >= self._entity_cache_limit:
|
||||
self._log[__name__].info(
|
||||
'In-memory entity cache limit reached (%s/%s), flushing to session',
|
||||
len(self._mb_entity_cache),
|
||||
self._entity_cache_limit
|
||||
)
|
||||
await self._save_states_and_entities()
|
||||
self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map)
|
||||
if len(self._mb_entity_cache) >= self._entity_cache_limit:
|
||||
warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit')
|
||||
|
||||
self._log[__name__].info(
|
||||
'In-memory entity cache at %s/%s after flushing to session',
|
||||
len(self._mb_entity_cache),
|
||||
self._entity_cache_limit
|
||||
)
|
||||
|
||||
|
||||
get_diff = self._message_box.get_difference()
|
||||
if get_diff:
|
||||
self._log[__name__].debug('Getting difference for account updates')
|
||||
try:
|
||||
diff = await self(get_diff)
|
||||
except (
|
||||
errors.ServerError,
|
||||
errors.TimedOutError,
|
||||
errors.FloodWaitError,
|
||||
ValueError
|
||||
) as e:
|
||||
# Telegram is having issues
|
||||
self._log[__name__].info('Cannot get difference since Telegram is having issues: %s', type(e).__name__)
|
||||
self._message_box.end_difference()
|
||||
continue
|
||||
except (errors.UnauthorizedError, errors.AuthKeyError) as e:
|
||||
# Not logged in or broken authorization key, can't get difference
|
||||
self._log[__name__].info('Cannot get difference since the account is not logged in: %s', type(e).__name__)
|
||||
self._message_box.end_difference()
|
||||
if was_once_logged_in:
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
continue
|
||||
except (errors.TypeNotFoundError, sqlite3.OperationalError) as e:
|
||||
# User is likely doing weird things with their account or session and Telegram gets confused as to what layer they use
|
||||
self._log[__name__].warning('Cannot get difference since the account is likely misusing the session: %s', e)
|
||||
self._message_box.end_difference()
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
except OSError as e:
|
||||
# Network is likely down, but it's unclear for how long.
|
||||
# If disconnect is called this task will be cancelled along with the sleep.
|
||||
# If disconnect is not called, getting difference should be retried after a few seconds.
|
||||
self._log[__name__].info('Cannot get difference since the network is down: %s: %s', type(e).__name__, e)
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache)
|
||||
if updates:
|
||||
self._log[__name__].info('Got difference for account updates')
|
||||
|
||||
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(updates, users, chats))
|
||||
updates_to_dispatch.extend(_preprocess_updates)
|
||||
continue
|
||||
|
||||
get_diff = self._message_box.get_channel_difference(self._mb_entity_cache)
|
||||
if get_diff:
|
||||
self._log[__name__].debug('Getting difference for channel %s updates', get_diff.channel.channel_id)
|
||||
try:
|
||||
diff = await self(get_diff)
|
||||
except (errors.UnauthorizedError, errors.AuthKeyError) as e:
|
||||
# Not logged in or broken authorization key, can't get difference
|
||||
self._log[__name__].warning(
|
||||
'Cannot get difference for channel %s since the account is not logged in: %s',
|
||||
get_diff.channel.channel_id, type(e).__name__
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
if was_once_logged_in:
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
continue
|
||||
except (errors.TypeNotFoundError, sqlite3.OperationalError) as e:
|
||||
self._log[__name__].warning(
|
||||
'Cannot get difference for channel %s since the account is likely misusing the session: %s',
|
||||
get_diff.channel.channel_id, e
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
except (
|
||||
errors.PersistentTimestampOutdatedError,
|
||||
errors.PersistentTimestampInvalidError,
|
||||
errors.ServerError,
|
||||
errors.TimedOutError,
|
||||
errors.FloodWaitError,
|
||||
ValueError
|
||||
) as e:
|
||||
# According to Telegram's docs:
|
||||
# "Channel internal replication issues, try again later (treat this like an RPC_CALL_FAIL)."
|
||||
# We can treat this as "empty difference" and not update the local pts.
|
||||
# Then this same call will be retried when another gap is detected or timeout expires.
|
||||
#
|
||||
# Another option would be to literally treat this like an RPC_CALL_FAIL and retry after a few
|
||||
# seconds, but if Telegram is having issues it's probably best to wait for it to send another
|
||||
# update (hinting it may be okay now) and retry then.
|
||||
#
|
||||
# This is a bit hacky because MessageBox doesn't really have a way to "not update" the pts.
|
||||
# Instead we manually extract the previously-known pts and use that.
|
||||
#
|
||||
# For PersistentTimestampInvalidError:
|
||||
# Somehow our pts is either too new or the server does not know about this.
|
||||
# We treat this as PersistentTimestampOutdatedError for now.
|
||||
# TODO investigate why/when this happens and if this is the proper solution
|
||||
self._log[__name__].warning(
|
||||
'Getting difference for channel updates %s caused %s;'
|
||||
' ending getting difference prematurely until server issues are resolved',
|
||||
get_diff.channel.channel_id, type(e).__name__
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
continue
|
||||
except (errors.ChannelPrivateError, errors.ChannelInvalidError):
|
||||
# Timeout triggered a get difference, but we have been banned in the channel since then.
|
||||
# Because we can no longer fetch updates from this channel, we should stop keeping track
|
||||
# of it entirely.
|
||||
self._log[__name__].info(
|
||||
'Account is now banned in %d so we can no longer fetch updates from it',
|
||||
get_diff.channel.channel_id
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.BANNED,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
continue
|
||||
except OSError as e:
|
||||
self._log[__name__].info(
|
||||
'Cannot get difference for channel %d since the network is down: %s: %s',
|
||||
get_diff.channel.channel_id, type(e).__name__, e
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
|
||||
updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache)
|
||||
if updates:
|
||||
self._log[__name__].info('Got difference for channel %d updates', get_diff.channel.channel_id)
|
||||
|
||||
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(updates, users, chats))
|
||||
updates_to_dispatch.extend(_preprocess_updates)
|
||||
continue
|
||||
|
||||
deadline = self._message_box.check_deadlines()
|
||||
deadline_delay = deadline - get_running_loop().time()
|
||||
if deadline_delay > 0:
|
||||
# Don't bother sleeping and timing out if the delay is already 0 (pollutes the logs).
|
||||
try:
|
||||
updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay)
|
||||
except asyncio.TimeoutError:
|
||||
self._log[__name__].debug('Timeout waiting for updates expired')
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
processed = []
|
||||
try:
|
||||
users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed)
|
||||
except GapError:
|
||||
continue # get(_channel)_difference will start returning requests
|
||||
|
||||
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(processed, users, chats))
|
||||
updates_to_dispatch.extend(_preprocess_updates)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)')
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
|
||||
async def _preprocess_updates(self, updates, users, chats):
|
||||
self._mb_entity_cache.extend(users, chats)
|
||||
await utils.maybe_async(self.session.process_entities(types.contacts.ResolvedPeer(None, users, chats)))
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(users, chats)}
|
||||
for u in updates:
|
||||
u._entities = entities
|
||||
return updates
|
||||
|
||||
async def _keepalive_loop(self: 'TelegramClient'):
|
||||
# Pings' ID don't really need to be secure, just "random"
|
||||
rnd = lambda: random.randrange(-2**63, 2**63)
|
||||
while self.is_connected():
|
||||
|
@ -518,18 +359,57 @@ class UpdateMethods:
|
|||
# inserted because this is a rather expensive operation
|
||||
# (default's sqlite3 takes ~0.1s to commit changes). Do
|
||||
# it every minute instead. No-op if there's nothing new.
|
||||
await self._save_states_and_entities()
|
||||
self.session.save()
|
||||
|
||||
await utils.maybe_async(self.session.save())
|
||||
# We need to send some content-related request at least hourly
|
||||
# for Telegram to keep delivering updates, otherwise they will
|
||||
# just stop even if we're connected. Do so every 30 minutes.
|
||||
#
|
||||
# TODO Call getDifference instead since it's more relevant
|
||||
if time.time() - self._last_request > 30 * 60:
|
||||
if not await self.is_user_authorized():
|
||||
# What can be the user doing for so
|
||||
# long without being logged in...?
|
||||
continue
|
||||
|
||||
async def _dispatch_update(self: 'TelegramClient', update):
|
||||
# TODO only used for AlbumHack, and MessageBox is not really designed for this
|
||||
others = None
|
||||
try:
|
||||
await self(functions.updates.GetStateRequest())
|
||||
except (ConnectionError, asyncio.CancelledError):
|
||||
return
|
||||
|
||||
if not self._mb_entity_cache.self_id:
|
||||
async def _dispatch_queue_updates(self: 'TelegramClient'):
|
||||
while not self._updates_queue.empty():
|
||||
await self._dispatch_update(*self._updates_queue.get_nowait())
|
||||
|
||||
self._dispatching_updates_queue.clear()
|
||||
|
||||
async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, pts_date):
|
||||
if not self._entity_cache.ensure_cached(update):
|
||||
# We could add a lock to not fetch the same pts twice if we are
|
||||
# already fetching it. However this does not happen in practice,
|
||||
# which makes sense, because different updates have different pts.
|
||||
if self._state_cache.update(update, check_only=True):
|
||||
# If the update doesn't have pts, fetching won't do anything.
|
||||
# For example, UpdateUserStatus or UpdateChatUserTyping.
|
||||
try:
|
||||
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
|
||||
except ValueError:
|
||||
# There is a chance that GetFullChannelRequest and GetDifferenceRequest
|
||||
# inside the _get_difference() function will end up with
|
||||
# ValueError("Request was unsuccessful N time(s)") for whatever reasons.
|
||||
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._mb_entity_cache`.
|
||||
# `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.
|
||||
|
@ -630,6 +510,67 @@ class UpdateMethods:
|
|||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].exception('Unhandled exception on %s', name)
|
||||
|
||||
async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date):
|
||||
"""
|
||||
Get the difference for this `channel_id` if any, then load entities.
|
||||
|
||||
Calls :tl:`updates.getDifference`, which fills the entities cache
|
||||
(always done by `__call__`) and lets us know about the full entities.
|
||||
"""
|
||||
# Fetch since the last known pts/date before this update arrived,
|
||||
# in order to fetch this update at full, including its entities.
|
||||
self._log[__name__].debug('Getting difference for entities '
|
||||
'for %r', update.__class__)
|
||||
if channel_id:
|
||||
# There are reports where we somehow call get channel difference
|
||||
# with `InputPeerEmpty`. Check our assumptions to better debug
|
||||
# this when it happens.
|
||||
assert isinstance(channel_id, int), 'channel_id was {}, not int in {}'.format(type(channel_id), update)
|
||||
try:
|
||||
# Wrap the ID inside a peer to ensure we get a channel back.
|
||||
where = await self.get_input_entity(types.PeerChannel(channel_id))
|
||||
except ValueError:
|
||||
# There's a high chance that this fails, since
|
||||
# we are getting the difference to fetch entities.
|
||||
return
|
||||
|
||||
if not pts_date:
|
||||
# First-time, can't get difference. Get pts instead.
|
||||
result = await self(functions.channels.GetFullChannelRequest(
|
||||
utils.get_input_channel(where)
|
||||
))
|
||||
self._state_cache[channel_id] = result.full_chat.pts
|
||||
return
|
||||
|
||||
result = await self(functions.updates.GetChannelDifferenceRequest(
|
||||
channel=where,
|
||||
filter=types.ChannelMessagesFilterEmpty(),
|
||||
pts=pts_date, # just pts
|
||||
limit=100,
|
||||
force=True
|
||||
))
|
||||
else:
|
||||
if not pts_date[0]:
|
||||
# First-time, can't get difference. Get pts instead.
|
||||
result = await self(functions.updates.GetStateRequest())
|
||||
self._state_cache[None] = result.pts, result.date
|
||||
return
|
||||
|
||||
result = await self(functions.updates.GetDifferenceRequest(
|
||||
pts=pts_date[0],
|
||||
date=pts_date[1],
|
||||
qts=0
|
||||
))
|
||||
|
||||
if isinstance(result, (types.updates.Difference,
|
||||
types.updates.DifferenceSlice,
|
||||
types.updates.ChannelDifference,
|
||||
types.updates.ChannelDifferenceTooLong)):
|
||||
update._entities.update({
|
||||
utils.get_peer_id(x): x for x in
|
||||
itertools.chain(result.users, result.chats)
|
||||
})
|
||||
|
||||
async def _handle_auto_reconnect(self: 'TelegramClient'):
|
||||
# TODO Catch-up
|
||||
# For now we make a high-level request to let Telegram
|
||||
|
|
|
@ -18,6 +18,7 @@ try:
|
|||
except ImportError:
|
||||
PIL = None
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
@ -35,7 +36,7 @@ class _CacheType:
|
|||
|
||||
|
||||
def _resize_photo_if_needed(
|
||||
file, is_image, width=2560, height=2560, background=(255, 255, 255)):
|
||||
file, is_image, width=1280, height=1280, background=(255, 255, 255)):
|
||||
|
||||
# https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254
|
||||
if (not is_image
|
||||
|
@ -46,17 +47,7 @@ def _resize_photo_if_needed(
|
|||
if isinstance(file, bytes):
|
||||
file = io.BytesIO(file)
|
||||
|
||||
if isinstance(file, io.IOBase):
|
||||
# Pillow seeks to 0 unconditionally later anyway
|
||||
old_pos = file.tell()
|
||||
file.seek(0, io.SEEK_END)
|
||||
before = file.tell()
|
||||
elif isinstance(file, str) and os.path.exists(file):
|
||||
# Check if file exists as a path and if so, get its size on disk
|
||||
before = os.path.getsize(file)
|
||||
else:
|
||||
# Would be weird...
|
||||
before = None
|
||||
before = file.tell() if isinstance(file, io.IOBase) else None
|
||||
|
||||
try:
|
||||
# Don't use a `with` block for `image`, or `file` would be closed.
|
||||
|
@ -67,43 +58,34 @@ def _resize_photo_if_needed(
|
|||
except KeyError:
|
||||
kwargs = {}
|
||||
|
||||
if image.mode == 'RGB':
|
||||
# Check if image is within acceptable bounds, if so, check if the image is at or below 10 MB, or assume it isn't if size is None or 0
|
||||
if image.width <= width and image.height <= height and (before <= 10000000 if before else False):
|
||||
if image.width <= width and image.height <= height:
|
||||
return file
|
||||
|
||||
# If the image is already RGB, don't convert it
|
||||
# certain modes such as 'P' have no alpha index but can't be saved as JPEG directly
|
||||
image.thumbnail((width, height), PIL.Image.LANCZOS)
|
||||
image.thumbnail((width, height), PIL.Image.ANTIALIAS)
|
||||
|
||||
alpha_index = image.mode.find('A')
|
||||
if alpha_index == -1:
|
||||
# If the image mode doesn't have alpha
|
||||
# channel then don't bother masking it away.
|
||||
result = image
|
||||
else:
|
||||
# We could save the resized image with the original format, but
|
||||
# JPEG often compresses better -> smaller size -> faster upload
|
||||
# We need to mask away the alpha channel ([3]), since otherwise
|
||||
# IOError is raised when trying to save alpha channels in JPEG.
|
||||
image.thumbnail((width, height), PIL.Image.LANCZOS)
|
||||
result = PIL.Image.new('RGB', image.size, background)
|
||||
mask = None
|
||||
|
||||
if image.has_transparency_data:
|
||||
if image.mode == 'RGBA':
|
||||
mask = image.getchannel('A')
|
||||
else:
|
||||
mask = image.convert('RGBA').getchannel('A')
|
||||
|
||||
result.paste(image, mask=mask)
|
||||
result.paste(image, mask=image.split()[alpha_index])
|
||||
|
||||
buffer = io.BytesIO()
|
||||
result.save(buffer, 'JPEG', progressive=True, **kwargs)
|
||||
result.save(buffer, 'JPEG', **kwargs)
|
||||
buffer.seek(0)
|
||||
buffer.name = 'a.jpg'
|
||||
return buffer
|
||||
|
||||
except IOError:
|
||||
return file
|
||||
finally:
|
||||
# The original position might matter
|
||||
if isinstance(file, io.IOBase):
|
||||
file.seek(old_pos)
|
||||
if before is not None:
|
||||
file.seek(before, io.SEEK_SET)
|
||||
|
||||
|
||||
class UploadMethods:
|
||||
|
@ -117,7 +99,6 @@ class UploadMethods:
|
|||
*,
|
||||
caption: typing.Union[str, typing.Sequence[str]] = None,
|
||||
force_document: bool = False,
|
||||
mime_type: str = None,
|
||||
file_size: int = None,
|
||||
clear_draft: bool = False,
|
||||
progress_callback: 'hints.ProgressCallback' = None,
|
||||
|
@ -126,24 +107,15 @@ class UploadMethods:
|
|||
thumb: 'hints.FileLike' = None,
|
||||
allow_cache: bool = True,
|
||||
parse_mode: str = (),
|
||||
formatting_entities: typing.Optional[
|
||||
typing.Union[
|
||||
typing.List[types.TypeMessageEntity], typing.List[typing.List[types.TypeMessageEntity]]
|
||||
]
|
||||
] = None,
|
||||
formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
|
||||
voice_note: bool = False,
|
||||
video_note: bool = False,
|
||||
buttons: typing.Optional['hints.MarkupLike'] = None,
|
||||
buttons: 'hints.MarkupLike' = None,
|
||||
silent: bool = None,
|
||||
background: bool = None,
|
||||
supports_streaming: bool = False,
|
||||
schedule: 'hints.DateLike' = None,
|
||||
comment_to: 'typing.Union[int, types.Message]' = None,
|
||||
ttl: int = None,
|
||||
nosound_video: bool = None,
|
||||
send_as: typing.Optional['hints.EntityLike'] = None,
|
||||
message_effect_id: typing.Optional[int] = None,
|
||||
**kwargs) -> typing.Union[typing.List[typing.Any], typing.Any]:
|
||||
**kwargs) -> 'types.Message':
|
||||
"""
|
||||
Sends message with the given file to the specified entity.
|
||||
|
||||
|
@ -210,13 +182,6 @@ class UploadMethods:
|
|||
the extension of an image file or a video file, it will be
|
||||
sent as such. Otherwise always as a document.
|
||||
|
||||
mime_type (`str`, optional):
|
||||
Custom mime type to use for the file to be sent (for example,
|
||||
``audio/mpeg``, ``audio/x-vorbis+ogg``, etc.).
|
||||
It can change the type of files displayed.
|
||||
If not set to any value, the mime type will be determined
|
||||
automatically based on the file's extension.
|
||||
|
||||
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.
|
||||
|
@ -263,11 +228,7 @@ class UploadMethods:
|
|||
default.
|
||||
|
||||
formatting_entities (`list`, optional):
|
||||
Optional formatting entities for the sent media message. When sending an album,
|
||||
`formatting_entities` can be a list of lists, where each inner list contains
|
||||
`types.TypeMessageEntity`. Each inner list will be assigned to the corresponding
|
||||
file in a pairwise manner with the caption. If provided, the ``parse_mode``
|
||||
parameter will be ignored.
|
||||
A list of message formatting entities. When provided, the ``parse_mode`` is ignored.
|
||||
|
||||
voice_note (`bool`, optional):
|
||||
If `True` the audio will be sent as a voice note.
|
||||
|
@ -288,9 +249,6 @@ class UploadMethods:
|
|||
the person has the chat muted). Set it to `True` to alter
|
||||
this behaviour.
|
||||
|
||||
background (`bool`, optional):
|
||||
Whether the message should be send in background.
|
||||
|
||||
supports_streaming (`bool`, optional):
|
||||
Whether the sent video supports streaming or not. Note that
|
||||
Telegram only recognizes as streamable some formats like MP4,
|
||||
|
@ -311,37 +269,6 @@ class UploadMethods:
|
|||
This parameter takes precedence over ``reply_to``. If there is
|
||||
no linked chat, `telethon.errors.sgIdInvalidError` is raised.
|
||||
|
||||
ttl (`int`. optional):
|
||||
The Time-To-Live of the file (also known as "self-destruct timer"
|
||||
or "self-destructing media"). If set, files can only be viewed for
|
||||
a short period of time before they disappear from the message
|
||||
history automatically.
|
||||
|
||||
The value must be at least 1 second, and at most 60 seconds,
|
||||
otherwise Telegram will ignore this parameter.
|
||||
|
||||
Not all types of media can be used with this parameter, such
|
||||
as text documents, which will fail with ``TtlMediaInvalidError``.
|
||||
|
||||
nosound_video (`bool`, optional):
|
||||
Only applicable when sending a video file without an audio
|
||||
track. If set to ``True``, the video will be displayed in
|
||||
Telegram as a video. If set to ``False``, Telegram will attempt
|
||||
to display the video as an animated gif. (It may still display
|
||||
as a video due to other factors.) The value is ignored if set
|
||||
on non-video files. This is set to ``True`` for albums, as gifs
|
||||
cannot be sent in albums.
|
||||
|
||||
send_as (`entity`):
|
||||
Unique identifier (int) or username (str) of the chat or channel to send the message as.
|
||||
You can use this to send the message on behalf of a chat or channel where you have appropriate permissions.
|
||||
Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat,
|
||||
This setting applies to the current message and will remain effective for future messages unless explicitly changed.
|
||||
To set this behavior permanently for all messages, use SaveDefaultSendAs.
|
||||
|
||||
message_effect_id (`int`, optional):
|
||||
Unique identifier of the message effect to be added to the message; for private chats only
|
||||
|
||||
Returns
|
||||
The `Message <telethon.tl.custom.message.Message>` (or messages)
|
||||
containing the sent file, or messages if a list of them was passed.
|
||||
|
@ -399,9 +326,6 @@ class UploadMethods:
|
|||
if not caption:
|
||||
caption = ''
|
||||
|
||||
if not formatting_entities:
|
||||
formatting_entities = []
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
if comment_to is not None:
|
||||
entity, reply_to = await self._get_comment_data(entity, comment_to)
|
||||
|
@ -411,46 +335,38 @@ class UploadMethods:
|
|||
# First check if the user passed an iterable, in which case
|
||||
# we may want to send grouped.
|
||||
if utils.is_list_like(file):
|
||||
sent_count = 0
|
||||
used_callback = None if not progress_callback else (
|
||||
lambda s, t: progress_callback(sent_count + s, len(file))
|
||||
)
|
||||
|
||||
if utils.is_list_like(caption):
|
||||
captions = caption
|
||||
else:
|
||||
captions = [caption]
|
||||
|
||||
# Check that formatting_entities list is valid
|
||||
if all(utils.is_list_like(obj) for obj in formatting_entities):
|
||||
formatting_entities = formatting_entities
|
||||
elif utils.is_list_like(formatting_entities):
|
||||
formatting_entities = [formatting_entities]
|
||||
else:
|
||||
raise TypeError('The formatting_entities argument must be a list or a sequence of lists')
|
||||
|
||||
# Check that all entities in all lists are of the correct type
|
||||
if not all(isinstance(ent, types.TypeMessageEntity) for sublist in formatting_entities for ent in sublist):
|
||||
raise TypeError('All entities must be instances of <types.TypeMessageEntity>')
|
||||
|
||||
result = []
|
||||
while file:
|
||||
result += await self._send_album(
|
||||
entity, file[:10], caption=captions[:10], formatting_entities=formatting_entities[:10],
|
||||
progress_callback=used_callback, reply_to=reply_to,
|
||||
entity, file[:10], caption=captions[:10],
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
parse_mode=parse_mode, silent=silent, schedule=schedule,
|
||||
supports_streaming=supports_streaming, clear_draft=clear_draft,
|
||||
force_document=force_document, background=background,
|
||||
send_as=send_as, message_effect_id=message_effect_id
|
||||
force_document=force_document
|
||||
)
|
||||
file = file[10:]
|
||||
captions = captions[10:]
|
||||
formatting_entities = formatting_entities[10:]
|
||||
sent_count += 10
|
||||
|
||||
for doc, cap in zip(file, captions):
|
||||
result.append(await self.send_file(
|
||||
entity, doc, allow_cache=allow_cache,
|
||||
caption=cap, force_document=force_document,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
attributes=attributes, thumb=thumb, voice_note=voice_note,
|
||||
video_note=video_note, buttons=buttons, silent=silent,
|
||||
supports_streaming=supports_streaming, schedule=schedule,
|
||||
clear_draft=clear_draft,
|
||||
**kwargs
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
if formatting_entities:
|
||||
if formatting_entities is not None:
|
||||
msg_entities = formatting_entities
|
||||
else:
|
||||
caption, msg_entities =\
|
||||
|
@ -458,13 +374,11 @@ class UploadMethods:
|
|||
|
||||
file_handle, media, image = await self._file_to_media(
|
||||
file, force_document=force_document,
|
||||
mime_type=mime_type,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback,
|
||||
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
|
||||
voice_note=voice_note, video_note=video_note,
|
||||
supports_streaming=supports_streaming, ttl=ttl,
|
||||
nosound_video=nosound_video,
|
||||
supports_streaming=supports_streaming
|
||||
)
|
||||
|
||||
# e.g. invalid cast from :tl:`MessageMediaWebPage`
|
||||
|
@ -472,25 +386,18 @@ class UploadMethods:
|
|||
raise TypeError('Cannot use {!r} as file'.format(file))
|
||||
|
||||
markup = self.build_reply_markup(buttons)
|
||||
reply_to = None if reply_to is None else types.InputReplyToMessage(reply_to)
|
||||
request = functions.messages.SendMediaRequest(
|
||||
entity, media, reply_to=reply_to, message=caption,
|
||||
entity, media, reply_to_msg_id=reply_to, message=caption,
|
||||
entities=msg_entities, reply_markup=markup, silent=silent,
|
||||
schedule_date=schedule, clear_draft=clear_draft,
|
||||
background=background,
|
||||
send_as=await self.get_input_entity(send_as) if send_as else None,
|
||||
effect=message_effect_id
|
||||
schedule_date=schedule, clear_draft=clear_draft
|
||||
)
|
||||
return self._get_response_message(request, await self(request), entity)
|
||||
|
||||
async def _send_album(self: 'TelegramClient', entity, files, caption='',
|
||||
formatting_entities=None,
|
||||
progress_callback=None, reply_to=None,
|
||||
parse_mode=(), silent=None, schedule=None,
|
||||
supports_streaming=None, clear_draft=None,
|
||||
force_document=False, background=None, ttl=None,
|
||||
send_as: typing.Optional['hints.EntityLike'] = None,
|
||||
message_effect_id: typing.Optional[int] = None):
|
||||
force_document=False):
|
||||
"""Specialized version of .send_file for albums"""
|
||||
# We don't care if the user wants to avoid cache, we will use it
|
||||
# anyway. Why? The cached version will be exactly the same thing
|
||||
|
@ -498,51 +405,36 @@ class UploadMethods:
|
|||
# cache only makes a difference for documents where the user may
|
||||
# want the attributes used on them to change.
|
||||
#
|
||||
# In theory documents can be sent inside the albums, but they appear
|
||||
# In theory documents can be sent inside the albums but they appear
|
||||
# as different messages (not inside the album), and the logic to set
|
||||
# the attributes/avoid cache is already written in .send_file().
|
||||
entity = await self.get_input_entity(entity)
|
||||
if not utils.is_list_like(caption):
|
||||
caption = (caption,)
|
||||
if not all(isinstance(obj, list) for obj in formatting_entities):
|
||||
formatting_entities = (formatting_entities,)
|
||||
|
||||
captions = []
|
||||
# If the formatting_entities argument is provided, we don't use parse_mode
|
||||
if formatting_entities:
|
||||
# Pop from the end (so reverse)
|
||||
capt_with_ent = itertools.zip_longest(reversed(caption), reversed(formatting_entities), fillvalue=None)
|
||||
for msg_caption, msg_entities in capt_with_ent:
|
||||
captions.append((msg_caption, msg_entities))
|
||||
else:
|
||||
for c in reversed(caption): # Pop from the end (so reverse)
|
||||
captions.append(await self._parse_message_text(c or '', parse_mode))
|
||||
|
||||
reply_to = utils.get_message_id(reply_to)
|
||||
|
||||
used_callback = None if not progress_callback else (
|
||||
# use an integer when sent matches total, to easily determine a file has been fully sent
|
||||
lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files))
|
||||
)
|
||||
|
||||
# Need to upload the media first, but only if they're not cached yet
|
||||
media = []
|
||||
for sent_count, file in enumerate(files):
|
||||
for file in files:
|
||||
# Albums want :tl:`InputMedia` which, in theory, includes
|
||||
# :tl:`InputMediaUploadedPhoto`. However, using that will
|
||||
# :tl:`InputMediaUploadedPhoto`. However using that will
|
||||
# make it `raise MediaInvalidError`, so we need to upload
|
||||
# it as media and then convert that to :tl:`InputMediaPhoto`.
|
||||
fh, fm, _ = await self._file_to_media(
|
||||
file, supports_streaming=supports_streaming,
|
||||
force_document=force_document, ttl=ttl,
|
||||
progress_callback=used_callback, nosound_video=True)
|
||||
force_document=force_document)
|
||||
if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)):
|
||||
r = await self(functions.messages.UploadMediaRequest(
|
||||
entity, media=fm
|
||||
))
|
||||
|
||||
fm = utils.get_input_media(r.photo)
|
||||
elif isinstance(fm, (types.InputMediaUploadedDocument, types.InputMediaDocumentExternal)):
|
||||
elif isinstance(fm, types.InputMediaUploadedDocument):
|
||||
r = await self(functions.messages.UploadMediaRequest(
|
||||
entity, media=fm
|
||||
))
|
||||
|
@ -563,11 +455,8 @@ class UploadMethods:
|
|||
|
||||
# Now we can construct the multi-media request
|
||||
request = functions.messages.SendMultiMediaRequest(
|
||||
entity, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), multi_media=media,
|
||||
silent=silent, schedule_date=schedule, clear_draft=clear_draft,
|
||||
background=background,
|
||||
send_as=await self.get_input_entity(send_as) if send_as else None,
|
||||
effect=message_effect_id
|
||||
entity, reply_to_msg_id=reply_to, multi_media=media,
|
||||
silent=silent, schedule_date=schedule, clear_draft=clear_draft
|
||||
)
|
||||
result = await self(request)
|
||||
|
||||
|
@ -638,13 +527,6 @@ class UploadMethods:
|
|||
A callback function accepting two parameters:
|
||||
``(sent bytes, total)``.
|
||||
|
||||
When sending an album, the callback will receive a number
|
||||
between 0 and the amount of files as the "sent" parameter,
|
||||
and the amount of files as the "total". Note that the first
|
||||
parameter will be a floating point number to indicate progress
|
||||
within a file (e.g. ``2.5`` means it has sent 50% of the third
|
||||
file, because it's between 2 and 3).
|
||||
|
||||
Returns
|
||||
:tl:`InputFileBig` if the file size is larger than 10MB,
|
||||
`InputSizedFile <telethon.tl.custom.inputsizedfile.InputSizedFile>`
|
||||
|
@ -766,8 +648,7 @@ class UploadMethods:
|
|||
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,
|
||||
ttl=None, nosound_video=None):
|
||||
supports_streaming=False, mime_type=None, as_image=None):
|
||||
if not file:
|
||||
return None, None, None
|
||||
|
||||
|
@ -796,8 +677,7 @@ class UploadMethods:
|
|||
force_document=force_document,
|
||||
voice_note=voice_note,
|
||||
video_note=video_note,
|
||||
supports_streaming=supports_streaming,
|
||||
ttl=ttl
|
||||
supports_streaming=supports_streaming
|
||||
), as_image)
|
||||
except TypeError:
|
||||
# Can't turn whatever was given into media
|
||||
|
@ -816,13 +696,13 @@ class UploadMethods:
|
|||
)
|
||||
elif re.match('https?://', file):
|
||||
if as_image:
|
||||
media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl)
|
||||
media = types.InputMediaPhotoExternal(file)
|
||||
else:
|
||||
media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl)
|
||||
media = types.InputMediaDocumentExternal(file)
|
||||
else:
|
||||
bot_file = utils.resolve_bot_file_id(file)
|
||||
if bot_file:
|
||||
media = utils.get_input_media(bot_file, ttl=ttl)
|
||||
media = utils.get_input_media(bot_file)
|
||||
|
||||
if media:
|
||||
pass # Already have media, don't check the rest
|
||||
|
@ -832,7 +712,7 @@ class UploadMethods:
|
|||
'an HTTP URL or a valid bot-API-like file ID'.format(file)
|
||||
)
|
||||
elif as_image:
|
||||
media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl)
|
||||
media = types.InputMediaUploadedPhoto(file_handle)
|
||||
else:
|
||||
attributes, mime_type = utils.get_attributes(
|
||||
file,
|
||||
|
@ -852,18 +732,12 @@ class UploadMethods:
|
|||
thumb = str(thumb.absolute())
|
||||
thumb = await self.upload_file(thumb, file_size=file_size)
|
||||
|
||||
# setting `nosound_video` to `True` doesn't affect videos with sound
|
||||
# instead it prevents sending silent videos as GIFs
|
||||
nosound_video = nosound_video if mime_type.split("/")[0] == 'video' else None
|
||||
|
||||
media = types.InputMediaUploadedDocument(
|
||||
file=file_handle,
|
||||
mime_type=mime_type,
|
||||
attributes=attributes,
|
||||
thumb=thumb,
|
||||
force_file=force_document and not is_image,
|
||||
ttl_seconds=ttl,
|
||||
nosound_video=nosound_video
|
||||
force_file=force_document and not is_image
|
||||
)
|
||||
return file_handle, media, as_image
|
||||
|
||||
|
|
|
@ -26,19 +26,12 @@ def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta):
|
|||
|
||||
|
||||
class UserMethods:
|
||||
async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None):
|
||||
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, flood_sleep_threshold=None):
|
||||
if self._loop is not None and self._loop != helpers.get_running_loop():
|
||||
raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')
|
||||
# if the loop is None it will fail with a connection error later on
|
||||
|
||||
if flood_sleep_threshold is None:
|
||||
flood_sleep_threshold = self.flood_sleep_threshold
|
||||
requests = list(request) if utils.is_list_like(request) else [request]
|
||||
request = list(request) if utils.is_list_like(request) else request
|
||||
for i, r in enumerate(requests):
|
||||
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):
|
||||
raise _NOT_A_REQUEST()
|
||||
await r.resolve(self, utils)
|
||||
|
@ -49,20 +42,13 @@ class UserMethods:
|
|||
diff = round(due - time.time())
|
||||
if diff <= 3: # Flood waits below 3 seconds are "ignored"
|
||||
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
|
||||
elif diff <= flood_sleep_threshold:
|
||||
elif diff <= self.flood_sleep_threshold:
|
||||
self._log[__name__].info(*_fmt_flood(diff, r, early=True))
|
||||
await asyncio.sleep(diff)
|
||||
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
|
||||
else:
|
||||
raise errors.FloodWaitError(request=r, capture=diff)
|
||||
|
||||
if self._no_updates:
|
||||
if utils.is_list_like(request):
|
||||
request[i] = functions.InvokeWithoutUpdatesRequest(r)
|
||||
else:
|
||||
# This should only run once as requests should be a list of 1 item
|
||||
request = functions.InvokeWithoutUpdatesRequest(r)
|
||||
|
||||
request_index = 0
|
||||
last_error = None
|
||||
self._last_request = time.time()
|
||||
|
@ -80,7 +66,8 @@ class UserMethods:
|
|||
exceptions.append(e)
|
||||
results.append(None)
|
||||
continue
|
||||
await utils.maybe_async(self.session.process_entities(result))
|
||||
self.session.process_entities(result)
|
||||
self._entity_cache.add(result)
|
||||
exceptions.append(None)
|
||||
results.append(result)
|
||||
request_index += 1
|
||||
|
@ -90,11 +77,11 @@ class UserMethods:
|
|||
return results
|
||||
else:
|
||||
result = await future
|
||||
await utils.maybe_async(self.session.process_entities(result))
|
||||
self.session.process_entities(result)
|
||||
self._entity_cache.add(result)
|
||||
return result
|
||||
except (errors.ServerError, errors.RpcCallFailError,
|
||||
errors.RpcMcgetFailError, errors.InterdcCallErrorError,
|
||||
errors.TimedOutError,
|
||||
errors.InterdcCallRichErrorError) as e:
|
||||
last_error = e
|
||||
self._log[__name__].warning(
|
||||
|
@ -102,8 +89,7 @@ class UserMethods:
|
|||
e.__class__.__name__, e)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
except (errors.FloodWaitError, errors.FloodPremiumWaitError,
|
||||
errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e:
|
||||
except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e:
|
||||
last_error = e
|
||||
if utils.is_list_like(request):
|
||||
request = request[request_index]
|
||||
|
@ -163,17 +149,20 @@ class UserMethods:
|
|||
me = await client.get_me()
|
||||
print(me.username)
|
||||
"""
|
||||
if input_peer and self._mb_entity_cache.self_id:
|
||||
return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer()
|
||||
if input_peer and self._self_input_peer:
|
||||
return self._self_input_peer
|
||||
|
||||
try:
|
||||
me = (await self(
|
||||
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
|
||||
|
||||
if not self._mb_entity_cache.self_id:
|
||||
self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash)
|
||||
self._bot = me.bot
|
||||
if not self._self_input_peer:
|
||||
self._self_input_peer = utils.get_input_peer(
|
||||
me, allow_self=False
|
||||
)
|
||||
|
||||
return utils.get_input_peer(me, allow_self=False) if input_peer else me
|
||||
return self._self_input_peer if input_peer else me
|
||||
except errors.UnauthorizedError:
|
||||
return None
|
||||
|
||||
|
@ -185,7 +174,7 @@ class UserMethods:
|
|||
This property is used in every update, and some like `updateLoginToken`
|
||||
occur prior to login, so it gracefully handles when no ID is known yet.
|
||||
"""
|
||||
return self._mb_entity_cache.self_id
|
||||
return self._self_input_peer.user_id if self._self_input_peer else None
|
||||
|
||||
async def is_bot(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
|
@ -199,10 +188,10 @@ class UserMethods:
|
|||
else:
|
||||
print('Hello')
|
||||
"""
|
||||
if self._mb_entity_cache.self_bot is None:
|
||||
await self.get_me(input_peer=True)
|
||||
if self._bot is None:
|
||||
self._bot = (await self.get_me()).bot
|
||||
|
||||
return self._mb_entity_cache.self_bot
|
||||
return self._bot
|
||||
|
||||
async def is_user_authorized(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
|
@ -228,7 +217,7 @@ class UserMethods:
|
|||
|
||||
async def get_entity(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike') -> typing.Union['hints.Entity', typing.List['hints.Entity']]:
|
||||
entity: 'hints.EntitiesLike') -> 'hints.Entity':
|
||||
"""
|
||||
Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`
|
||||
or :tl:`Channel`. You can also pass a list or iterable of entities,
|
||||
|
@ -327,9 +316,7 @@ class UserMethods:
|
|||
|
||||
# Merge users, chats and channels into a single dictionary
|
||||
id_entity = {
|
||||
# `get_input_entity` might've guessed the type from a non-marked ID,
|
||||
# so the only way to match that with the input is by not using marks here.
|
||||
utils.get_peer_id(x, add_mark=False): x
|
||||
utils.get_peer_id(x): x
|
||||
for x in itertools.chain(users, chats, channels)
|
||||
}
|
||||
|
||||
|
@ -342,7 +329,7 @@ class UserMethods:
|
|||
if isinstance(x, str):
|
||||
result.append(await self._get_entity_from_string(x))
|
||||
elif not isinstance(x, types.InputPeerSelf):
|
||||
result.append(id_entity[utils.get_peer_id(x, add_mark=False)])
|
||||
result.append(id_entity[utils.get_peer_id(x)])
|
||||
else:
|
||||
result.append(next(
|
||||
u for u in id_entity.values()
|
||||
|
@ -425,8 +412,8 @@ class UserMethods:
|
|||
try:
|
||||
# 0x2d45687 == crc32(b'Peer')
|
||||
if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687:
|
||||
return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer()
|
||||
except AttributeError:
|
||||
return self._entity_cache[peer]
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
# Then come known strings that take precedence
|
||||
|
@ -435,8 +422,7 @@ class UserMethods:
|
|||
|
||||
# No InputPeer, cached peer, or known string. Fetch from disk cache
|
||||
try:
|
||||
input_entity = await utils.maybe_async(self.session.get_input_entity(peer))
|
||||
return input_entity
|
||||
return self.session.get_input_entity(peer)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
@ -474,7 +460,7 @@ class UserMethods:
|
|||
|
||||
raise ValueError(
|
||||
'Could not find the input entity for {} ({}). Please read https://'
|
||||
'docs.telethon.dev/en/stable/concepts/entities.html to'
|
||||
'docs.telethon.dev/en/latest/concepts/entities.html to'
|
||||
' find out more details.'
|
||||
.format(peer, type(peer).__name__)
|
||||
)
|
||||
|
@ -575,8 +561,8 @@ class UserMethods:
|
|||
pass
|
||||
try:
|
||||
# Nobody with this username, maybe it's an exact name/title
|
||||
input_entity = await utils.maybe_async(self.session.get_input_entity(string))
|
||||
return await self.get_entity(input_entity)
|
||||
return await self.get_entity(
|
||||
self.session.get_input_entity(string))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
from .tl.custom import *
|
147
telethon/entitycache.py
Normal file
147
telethon/entitycache.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
import inspect
|
||||
import itertools
|
||||
|
||||
from . import utils
|
||||
from .tl import types
|
||||
|
||||
# Which updates have the following fields?
|
||||
_has_field = {
|
||||
('user_id', int): [],
|
||||
('chat_id', int): [],
|
||||
('channel_id', int): [],
|
||||
('peer', 'TypePeer'): [],
|
||||
('peer', 'TypeDialogPeer'): [],
|
||||
('message', 'TypeMessage'): [],
|
||||
}
|
||||
|
||||
# Note: We don't bother checking for some rare:
|
||||
# * `UpdateChatParticipantAdd.inviter_id` integer.
|
||||
# * `UpdateNotifySettings.peer` dialog peer.
|
||||
# * `UpdatePinnedDialogs.order` list of dialog peers.
|
||||
# * `UpdateReadMessagesContents.messages` list of messages.
|
||||
# * `UpdateChatParticipants.participants` list of participants.
|
||||
#
|
||||
# There are also some uninteresting `update.message` of type string.
|
||||
|
||||
|
||||
def _fill():
|
||||
for name in dir(types):
|
||||
update = getattr(types, name)
|
||||
if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e:
|
||||
cid = update.CONSTRUCTOR_ID
|
||||
sig = inspect.signature(update.__init__)
|
||||
for param in sig.parameters.values():
|
||||
vec = _has_field.get((param.name, param.annotation))
|
||||
if vec is not None:
|
||||
vec.append(cid)
|
||||
|
||||
# Future-proof check: if the documentation format ever changes
|
||||
# then we won't be able to pick the update types we are interested
|
||||
# in, so we must make sure we have at least an update for each field
|
||||
# which likely means we are doing it right.
|
||||
if not all(_has_field.values()):
|
||||
raise RuntimeError('FIXME: Did the init signature or updates change?')
|
||||
|
||||
|
||||
# We use a function to avoid cluttering the globals (with name/update/cid/doc)
|
||||
_fill()
|
||||
|
||||
|
||||
class EntityCache:
|
||||
"""
|
||||
In-memory input entity cache, defaultdict-like behaviour.
|
||||
"""
|
||||
def add(self, entities):
|
||||
"""
|
||||
Adds the given entities to the cache, if they weren't saved before.
|
||||
"""
|
||||
if not utils.is_list_like(entities):
|
||||
# Invariant: all "chats" and "users" are always iterables,
|
||||
# and "user" never is (so we wrap it inside a list).
|
||||
entities = itertools.chain(
|
||||
getattr(entities, 'chats', []),
|
||||
getattr(entities, 'users', []),
|
||||
(hasattr(entities, 'user') and [entities.user]) or []
|
||||
)
|
||||
|
||||
for entity in entities:
|
||||
try:
|
||||
pid = utils.get_peer_id(entity)
|
||||
if pid not in self.__dict__:
|
||||
# Note: `get_input_peer` already checks for `access_hash`
|
||||
self.__dict__[pid] = utils.get_input_peer(entity)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
Gets the corresponding :tl:`InputPeer` for the given ID or peer,
|
||||
or raises ``KeyError`` on any error (i.e. cannot be found).
|
||||
"""
|
||||
if not isinstance(item, int) or item < 0:
|
||||
try:
|
||||
return self.__dict__[utils.get_peer_id(item)]
|
||||
except TypeError:
|
||||
raise KeyError('Invalid key will not have entity') from None
|
||||
|
||||
for cls in (types.PeerUser, types.PeerChat, types.PeerChannel):
|
||||
result = self.__dict__.get(utils.get_peer_id(cls(item)))
|
||||
if result:
|
||||
return result
|
||||
|
||||
raise KeyError('No cached entity for the given key')
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear the entity cache.
|
||||
"""
|
||||
self.__dict__.clear()
|
||||
|
||||
def ensure_cached(
|
||||
self,
|
||||
update,
|
||||
has_user_id=frozenset(_has_field[('user_id', int)]),
|
||||
has_chat_id=frozenset(_has_field[('chat_id', int)]),
|
||||
has_channel_id=frozenset(_has_field[('channel_id', int)]),
|
||||
has_peer=frozenset(_has_field[('peer', 'TypePeer')] + _has_field[('peer', 'TypeDialogPeer')]),
|
||||
has_message=frozenset(_has_field[('message', 'TypeMessage')])
|
||||
):
|
||||
"""
|
||||
Ensures that all the relevant entities in the given update are cached.
|
||||
"""
|
||||
# This method is called pretty often and we want it to have the lowest
|
||||
# overhead possible. For that, we avoid `isinstance` and constantly
|
||||
# getting attributes out of `types.` by "caching" the constructor IDs
|
||||
# in sets inside the arguments, and using local variables.
|
||||
dct = self.__dict__
|
||||
cid = update.CONSTRUCTOR_ID
|
||||
if cid in has_user_id and \
|
||||
update.user_id not in dct:
|
||||
return False
|
||||
|
||||
if cid in has_chat_id and \
|
||||
utils.get_peer_id(types.PeerChat(update.chat_id)) not in dct:
|
||||
return False
|
||||
|
||||
if cid in has_channel_id and \
|
||||
utils.get_peer_id(types.PeerChannel(update.channel_id)) not in dct:
|
||||
return False
|
||||
|
||||
if cid in has_peer and \
|
||||
utils.get_peer_id(update.peer) not in dct:
|
||||
return False
|
||||
|
||||
if cid in has_message:
|
||||
x = update.message
|
||||
y = getattr(x, 'peer_id', None) # handle MessageEmpty
|
||||
if y and utils.get_peer_id(y) not in dct:
|
||||
return False
|
||||
|
||||
y = getattr(x, 'from_id', None)
|
||||
if y and utils.get_peer_id(y) not in dct:
|
||||
return False
|
||||
|
||||
# We don't quite worry about entities anywhere else.
|
||||
# This is enough.
|
||||
|
||||
return True
|
|
@ -6,7 +6,7 @@ import re
|
|||
|
||||
from .common import (
|
||||
ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
|
||||
InvalidBufferError, AuthKeyNotFound, SecurityError, CdnFileTamperedError,
|
||||
InvalidBufferError, SecurityError, CdnFileTamperedError,
|
||||
AlreadyInConversationError, BadMessageError, MultiError
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
"""Errors not related to the Telegram API itself"""
|
||||
import struct
|
||||
import textwrap
|
||||
|
||||
from ..tl import TLRequest
|
||||
|
||||
|
@ -19,8 +18,8 @@ class TypeNotFoundError(Exception):
|
|||
def __init__(self, invalid_constructor_id, remaining):
|
||||
super().__init__(
|
||||
'Could not find a matching Constructor ID for the TLObject '
|
||||
'that was supposed to be read with ID {:08x}. See the FAQ '
|
||||
'for more details. '
|
||||
'that was supposed to be read with ID {:08x}. Most likely, '
|
||||
'a TLObject was trying to be read when it should not be read. '
|
||||
'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining))
|
||||
|
||||
self.invalid_constructor_id = invalid_constructor_id
|
||||
|
@ -59,22 +58,6 @@ class InvalidBufferError(BufferError):
|
|||
'Invalid response buffer (too short {})'.format(self.payload))
|
||||
|
||||
|
||||
class AuthKeyNotFound(Exception):
|
||||
"""
|
||||
The server claims it doesn't know about the authorization key (session
|
||||
file) currently being used. This might be because it either has never
|
||||
seen this authorization key, or it used to know about the authorization
|
||||
key but has forgotten it, either temporarily or permanently (possibly
|
||||
due to server errors).
|
||||
|
||||
If the issue persists, you may need to recreate the session file and login
|
||||
again. This is not done automatically because it is not possible to know
|
||||
if the issue is temporary or permanent.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(textwrap.dedent(self.__class__.__doc__))
|
||||
|
||||
|
||||
class SecurityError(Exception):
|
||||
"""
|
||||
Generic security error, mostly used when generating a new AuthKey.
|
||||
|
|
|
@ -1,15 +1,3 @@
|
|||
from ..tl import functions
|
||||
|
||||
_NESTS_QUERY = (
|
||||
functions.InvokeAfterMsgRequest,
|
||||
functions.InvokeAfterMsgsRequest,
|
||||
functions.InitConnectionRequest,
|
||||
functions.InvokeWithLayerRequest,
|
||||
functions.InvokeWithoutUpdatesRequest,
|
||||
functions.InvokeWithMessagesRangeRequest,
|
||||
functions.InvokeWithTakeoutRequest,
|
||||
)
|
||||
|
||||
class RPCError(Exception):
|
||||
"""Base class for all Remote Procedure Call errors."""
|
||||
code = None
|
||||
|
@ -25,15 +13,7 @@ class RPCError(Exception):
|
|||
|
||||
@staticmethod
|
||||
def _fmt_request(request):
|
||||
n = 0
|
||||
reason = ''
|
||||
while isinstance(request, _NESTS_QUERY):
|
||||
n += 1
|
||||
reason += request.__class__.__name__ + '('
|
||||
request = request.query
|
||||
reason += request.__class__.__name__ + ')' * n
|
||||
|
||||
return ' (caused by {})'.format(reason)
|
||||
return ' (caused by {})'.format(request.__class__.__name__)
|
||||
|
||||
def __reduce__(self):
|
||||
return type(self), (self.request, self.message, self.code)
|
||||
|
|
|
@ -97,10 +97,8 @@ class Album(EventBuilder):
|
|||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
# TODO normally we'd only check updates if they come with other updates
|
||||
# but MessageBox is not designed for this so others will always be None.
|
||||
# In essence we always rely on AlbumHack rather than returning early if not others.
|
||||
others = [update]
|
||||
if not others:
|
||||
return # We only care about albums which come inside the same Updates
|
||||
|
||||
if isinstance(update,
|
||||
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
||||
|
@ -152,15 +150,23 @@ class Album(EventBuilder):
|
|||
"""
|
||||
def __init__(self, messages):
|
||||
message = messages[0]
|
||||
super().__init__(chat_peer=message.peer_id,
|
||||
if not message.out and isinstance(message.peer_id, types.PeerUser):
|
||||
# Incoming message (e.g. from a bot) has peer_id=us, and
|
||||
# from_id=bot (the actual "chat" from a user's perspective).
|
||||
chat_peer = message.from_id
|
||||
else:
|
||||
chat_peer = message.peer_id
|
||||
|
||||
super().__init__(chat_peer=chat_peer,
|
||||
msg_id=message.id, broadcast=bool(message.post))
|
||||
|
||||
SenderGetter.__init__(self, message.sender_id)
|
||||
self.messages = messages
|
||||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
self.sender_id, self._entities, client._entity_cache)
|
||||
|
||||
for msg in self.messages:
|
||||
msg._finish_init(client, self._entities, None)
|
||||
|
|
|
@ -151,7 +151,7 @@ class CallbackQuery(EventBuilder):
|
|||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
self.sender_id, self._entities, client._entity_cache)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
|
@ -208,9 +208,8 @@ class CallbackQuery(EventBuilder):
|
|||
if not getattr(self._input_sender, 'access_hash', True):
|
||||
# getattr with True to handle the InputPeerSelf() case
|
||||
try:
|
||||
self._input_sender = self._client._mb_entity_cache.get(
|
||||
utils.resolve_id(self._sender_id)[0])._as_input_peer()
|
||||
except AttributeError:
|
||||
self._input_sender = self._client._entity_cache[self._sender_id]
|
||||
except KeyError:
|
||||
m = await self.get_message()
|
||||
if m:
|
||||
self._sender = m._sender
|
||||
|
@ -300,7 +299,7 @@ class CallbackQuery(EventBuilder):
|
|||
"""
|
||||
Edits the message. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.edit_message` with
|
||||
the ``entity`` set to the correct :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`.
|
||||
the ``entity`` set to the correct :tl:`InputBotInlineMessageID`.
|
||||
|
||||
Returns `True` if the edit was successful.
|
||||
|
||||
|
@ -313,7 +312,7 @@ class CallbackQuery(EventBuilder):
|
|||
since the message object is normally not present.
|
||||
"""
|
||||
self._client.loop.create_task(self.answer())
|
||||
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
|
||||
if isinstance(self.query.msg_id, types.InputBotInlineMessageID):
|
||||
return await self._client.edit_message(
|
||||
self.query.msg_id, *args, **kwargs
|
||||
)
|
||||
|
@ -338,8 +337,6 @@ class CallbackQuery(EventBuilder):
|
|||
This method will likely fail if `via_inline` is `True`.
|
||||
"""
|
||||
self._client.loop.create_task(self.answer())
|
||||
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
|
||||
raise TypeError('Inline messages cannot be deleted as there is no API request available to do so')
|
||||
return await self._client.delete_messages(
|
||||
await self.get_input_chat(), [self.query.msg_id],
|
||||
*args, **kwargs
|
||||
|
|
|
@ -11,7 +11,6 @@ class ChatAction(EventBuilder):
|
|||
* Whenever a new chat is created.
|
||||
* Whenever a chat's title or photo is changed or removed.
|
||||
* Whenever a new message is pinned.
|
||||
* Whenever a user scores in a game.
|
||||
* Whenever a user joins or is added to the group.
|
||||
* Whenever a user is removed or leaves a group if it has
|
||||
less than 50 members or the removed user was a bot.
|
||||
|
@ -30,7 +29,6 @@ class ChatAction(EventBuilder):
|
|||
if event.user_joined:
|
||||
await event.reply('Welcome to the group!')
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
# Rely on specific pin updates for unpins, but otherwise ignore them
|
||||
|
@ -104,20 +102,6 @@ class ChatAction(EventBuilder):
|
|||
elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to:
|
||||
return cls.Event(msg,
|
||||
pin_ids=[msg.reply_to_msg_id])
|
||||
elif isinstance(action, types.MessageActionGameScore):
|
||||
return cls.Event(msg,
|
||||
new_score=action.score)
|
||||
|
||||
elif isinstance(update, types.UpdateChannelParticipant) \
|
||||
and bool(update.new_participant) != bool(update.prev_participant):
|
||||
# If members are hidden, bots will receive this update instead,
|
||||
# as there won't be a service message. Promotions and demotions
|
||||
# seem to have both new and prev participant, which are ignored
|
||||
# by this event.
|
||||
return cls.Event(types.PeerChannel(update.channel_id),
|
||||
users=update.user_id,
|
||||
added_by=update.actor_id if update.new_participant else None,
|
||||
kicked_by=update.actor_id if update.prev_participant else None)
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
|
@ -154,16 +138,12 @@ class ChatAction(EventBuilder):
|
|||
new_title (`str`, optional):
|
||||
The new title string for the chat, if applicable.
|
||||
|
||||
new_score (`str`, optional):
|
||||
The new score string for the game, if applicable.
|
||||
|
||||
unpin (`bool`):
|
||||
`True` if the existing pin gets unpinned.
|
||||
"""
|
||||
|
||||
def __init__(self, where, new_photo=None,
|
||||
added_by=None, kicked_by=None, created=None,
|
||||
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
|
||||
users=None, new_title=None, pin_ids=None, pin=None):
|
||||
if isinstance(where, types.MessageService):
|
||||
self.action_message = where
|
||||
where = where.peer_id
|
||||
|
@ -213,7 +193,6 @@ class ChatAction(EventBuilder):
|
|||
self._users = None
|
||||
self._input_users = None
|
||||
self.new_title = new_title
|
||||
self.new_score = new_score
|
||||
self.unpin = not pin
|
||||
|
||||
def _set_client(self, client):
|
||||
|
@ -425,10 +404,9 @@ class ChatAction(EventBuilder):
|
|||
|
||||
# If missing, try from the entity cache
|
||||
try:
|
||||
self._input_users.append(self._client._mb_entity_cache.get(
|
||||
utils.resolve_id(user_id)[0])._as_input_peer())
|
||||
self._input_users.append(self._client._entity_cache[user_id])
|
||||
continue
|
||||
except AttributeError:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return self._input_users or []
|
||||
|
|
|
@ -154,7 +154,7 @@ class EventCommon(ChatGetter, abc.ABC):
|
|||
self._client = client
|
||||
if self._chat_peer:
|
||||
self._chat, self._input_chat = utils._get_entity_pair(
|
||||
self.chat_id, self._entities, client._mb_entity_cache)
|
||||
self.chat_id, self._entities, client._entity_cache)
|
||||
else:
|
||||
self._chat = self._input_chat = None
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import re
|
|||
import asyncio
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils, helpers
|
||||
from .. import utils
|
||||
from ..tl import types, functions, custom
|
||||
from ..tl.custom.sendergetter import SenderGetter
|
||||
|
||||
|
@ -99,7 +99,7 @@ class InlineQuery(EventBuilder):
|
|||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
self.sender_id, self._entities, client._entity_cache)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
|
@ -130,7 +130,7 @@ class InlineQuery(EventBuilder):
|
|||
and the user's device is able to send it, this will return
|
||||
the :tl:`GeoPoint` with the position of the user.
|
||||
"""
|
||||
return self.query.geo
|
||||
return
|
||||
|
||||
@property
|
||||
def builder(self):
|
||||
|
@ -242,6 +242,6 @@ class InlineQuery(EventBuilder):
|
|||
if inspect.isawaitable(obj):
|
||||
return asyncio.ensure_future(obj)
|
||||
|
||||
f = helpers.get_running_loop().create_future()
|
||||
f = asyncio.get_event_loop().create_future()
|
||||
f.set_result(obj)
|
||||
return f
|
||||
|
|
|
@ -14,7 +14,7 @@ from ..tl.custom.sendergetter import SenderGetter
|
|||
# in a single place will make it annoying to use (since
|
||||
# the user needs to check for the existence of `None`).
|
||||
#
|
||||
# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser
|
||||
# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUserPhoto
|
||||
|
||||
def _requires_action(function):
|
||||
@functools.wraps(function)
|
||||
|
@ -95,7 +95,7 @@ class UserUpdate(EventBuilder):
|
|||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
self.sender_id, self._entities, client._entity_cache)
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
|
@ -136,7 +136,6 @@ class UserUpdate(EventBuilder):
|
|||
"""
|
||||
return isinstance(self.action, (
|
||||
types.SendMessageChooseContactAction,
|
||||
types.SendMessageChooseStickerAction,
|
||||
types.SendMessageUploadAudioAction,
|
||||
types.SendMessageUploadDocumentAction,
|
||||
types.SendMessageUploadPhotoAction,
|
||||
|
@ -229,14 +228,6 @@ class UserUpdate(EventBuilder):
|
|||
"""
|
||||
return isinstance(self.action, types.SendMessageUploadDocumentAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def sticker(self):
|
||||
"""
|
||||
`True` if what's being uploaded is a sticker.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageChooseStickerAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def photo(self):
|
||||
|
@ -246,7 +237,7 @@ class UserUpdate(EventBuilder):
|
|||
return isinstance(self.action, types.SendMessageUploadPhotoAction)
|
||||
|
||||
@property
|
||||
@_requires_status
|
||||
@_requires_action
|
||||
def last_seen(self):
|
||||
"""
|
||||
Exact `datetime.datetime` when the user was last seen if known.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"""
|
||||
This module contains the BinaryReader utility class.
|
||||
"""
|
||||
import struct
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from io import BytesIO
|
||||
from struct import unpack
|
||||
|
||||
from ..errors import TypeNotFoundError
|
||||
from ..tl.alltlobjects import tlobjects
|
||||
|
@ -19,8 +21,7 @@ class BinaryReader:
|
|||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
self.stream = data or b''
|
||||
self.position = 0
|
||||
self.stream = BytesIO(data)
|
||||
self._last = None # Should come in handy to spot -404 errors
|
||||
|
||||
# region Reading
|
||||
|
@ -29,35 +30,23 @@ class BinaryReader:
|
|||
# https://core.telegram.org/mtproto
|
||||
def read_byte(self):
|
||||
"""Reads a single byte value."""
|
||||
value, = struct.unpack_from("<B", self.stream, self.position)
|
||||
self.position += 1
|
||||
return value
|
||||
return self.read(1)[0]
|
||||
|
||||
def read_int(self, signed=True):
|
||||
"""Reads an integer (4 bytes) value."""
|
||||
fmt = '<i' if signed else '<I'
|
||||
value, = struct.unpack_from(fmt, self.stream, self.position)
|
||||
self.position += 4
|
||||
return value
|
||||
return int.from_bytes(self.read(4), byteorder='little', signed=signed)
|
||||
|
||||
def read_long(self, signed=True):
|
||||
"""Reads a long integer (8 bytes) value."""
|
||||
fmt = '<q' if signed else '<Q'
|
||||
value, = struct.unpack_from(fmt, self.stream, self.position)
|
||||
self.position += 8
|
||||
return value
|
||||
return int.from_bytes(self.read(8), byteorder='little', signed=signed)
|
||||
|
||||
def read_float(self):
|
||||
"""Reads a real floating point (4 bytes) value."""
|
||||
value, = struct.unpack_from("<f", self.stream, self.position)
|
||||
self.position += 4
|
||||
return value
|
||||
return unpack('<f', self.read(4))[0]
|
||||
|
||||
def read_double(self):
|
||||
"""Reads a real floating point (8 bytes) value."""
|
||||
value, = struct.unpack_from("<d", self.stream, self.position)
|
||||
self.position += 8
|
||||
return value
|
||||
return unpack('<d', self.read(8))[0]
|
||||
|
||||
def read_large_int(self, bits, signed=True):
|
||||
"""Reads a n-bits long integer value."""
|
||||
|
@ -66,12 +55,7 @@ class BinaryReader:
|
|||
|
||||
def read(self, length=-1):
|
||||
"""Read the given amount of bytes, or -1 to read all remaining."""
|
||||
if length >= 0:
|
||||
result = self.stream[self.position:self.position + length]
|
||||
self.position += length
|
||||
else:
|
||||
result = self.stream[self.position:]
|
||||
self.position += len(result)
|
||||
result = self.stream.read(length)
|
||||
if (length >= 0) and (len(result) != length):
|
||||
raise BufferError(
|
||||
'No more data left to read (need {}, got {}: {}); last read {}'
|
||||
|
@ -83,7 +67,7 @@ class BinaryReader:
|
|||
|
||||
def get_bytes(self):
|
||||
"""Gets the byte array representing the current buffer as a whole."""
|
||||
return self.stream
|
||||
return self.stream.getvalue()
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -169,24 +153,24 @@ class BinaryReader:
|
|||
|
||||
def close(self):
|
||||
"""Closes the reader, freeing the BytesIO stream."""
|
||||
self.stream = b''
|
||||
self.stream.close()
|
||||
|
||||
# region Position related
|
||||
|
||||
def tell_position(self):
|
||||
"""Tells the current position on the stream."""
|
||||
return self.position
|
||||
return self.stream.tell()
|
||||
|
||||
def set_position(self, position):
|
||||
"""Sets the current position on the stream."""
|
||||
self.position = position
|
||||
self.stream.seek(position)
|
||||
|
||||
def seek(self, offset):
|
||||
"""
|
||||
Seeks the stream position given an offset from the current position.
|
||||
The offset may be negative.
|
||||
"""
|
||||
self.position += offset
|
||||
self.stream.seek(offset, os.SEEK_CUR)
|
||||
|
||||
# endregion
|
||||
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
"""
|
||||
Simple HTML -> Telegram entity parser.
|
||||
"""
|
||||
import struct
|
||||
from collections import deque
|
||||
from html import escape
|
||||
from html.parser import HTMLParser
|
||||
from typing import Iterable, Tuple, List
|
||||
from typing import Iterable, Optional, Tuple, List
|
||||
|
||||
from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text
|
||||
from ..tl import TLObject
|
||||
from .. import helpers
|
||||
from ..tl.types import (
|
||||
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
|
||||
MessageEntityPre, MessageEntityEmail, MessageEntityUrl,
|
||||
MessageEntityTextUrl, MessageEntityMentionName,
|
||||
MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote,
|
||||
MessageEntityCustomEmoji, TypeMessageEntity
|
||||
TypeMessageEntity
|
||||
)
|
||||
|
||||
|
||||
# Helpers from markdown.py
|
||||
def _add_surrogate(text):
|
||||
return ''.join(
|
||||
''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))
|
||||
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
|
||||
)
|
||||
|
||||
|
||||
def _del_surrogate(text):
|
||||
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
|
||||
|
||||
|
||||
class HTMLToTelegramParser(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
@ -74,18 +86,10 @@ class HTMLToTelegramParser(HTMLParser):
|
|||
EntityType = MessageEntityUrl
|
||||
else:
|
||||
EntityType = MessageEntityTextUrl
|
||||
args['url'] = del_surrogate(url)
|
||||
args['url'] = url
|
||||
url = None
|
||||
self._open_tags_meta.popleft()
|
||||
self._open_tags_meta.appendleft(url)
|
||||
elif tag == 'tg-emoji':
|
||||
try:
|
||||
emoji_id = int(attrs['emoji-id'])
|
||||
except (KeyError, ValueError):
|
||||
return
|
||||
|
||||
EntityType = MessageEntityCustomEmoji
|
||||
args['document_id'] = emoji_id
|
||||
|
||||
if EntityType and tag not in self._building_entities:
|
||||
self._building_entities[tag] = EntityType(
|
||||
|
@ -129,36 +133,13 @@ def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
|
|||
return html, []
|
||||
|
||||
parser = HTMLToTelegramParser()
|
||||
parser.feed(add_surrogate(html))
|
||||
text = strip_text(parser.text, parser.entities)
|
||||
parser.entities.reverse()
|
||||
parser.entities.sort(key=lambda entity: entity.offset)
|
||||
return del_surrogate(text), parser.entities
|
||||
parser.feed(_add_surrogate(html))
|
||||
text = helpers.strip_text(parser.text, parser.entities)
|
||||
return _del_surrogate(text), parser.entities
|
||||
|
||||
|
||||
ENTITY_TO_FORMATTER = {
|
||||
MessageEntityBold: ('<strong>', '</strong>'),
|
||||
MessageEntityItalic: ('<em>', '</em>'),
|
||||
MessageEntityCode: ('<code>', '</code>'),
|
||||
MessageEntityUnderline: ('<u>', '</u>'),
|
||||
MessageEntityStrike: ('<del>', '</del>'),
|
||||
MessageEntityBlockquote: ('<blockquote>', '</blockquote>'),
|
||||
MessageEntityPre: lambda e, _: (
|
||||
"<pre>\n"
|
||||
" <code class='language-{}'>\n"
|
||||
" ".format(e.language), "{}\n"
|
||||
" </code>\n"
|
||||
"</pre>"
|
||||
),
|
||||
MessageEntityEmail: lambda _, t: ('<a href="mailto:{}">'.format(t), '</a>'),
|
||||
MessageEntityUrl: lambda _, t: ('<a href="{}">'.format(t), '</a>'),
|
||||
MessageEntityTextUrl: lambda e, _: ('<a href="{}">'.format(escape(e.url)), '</a>'),
|
||||
MessageEntityMentionName: lambda e, _: ('<a href="tg://user?id={}">'.format(e.user_id), '</a>'),
|
||||
MessageEntityCustomEmoji: lambda e, _: ('<tg-emoji emoji-id="{}">'.format(e.document_id), '</tg-emoji>'),
|
||||
}
|
||||
|
||||
|
||||
def unparse(text: str, entities: Iterable[TypeMessageEntity]) -> str:
|
||||
def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
|
||||
_length: Optional[int] = None) -> str:
|
||||
"""
|
||||
Performs the reverse operation to .parse(), effectively returning HTML
|
||||
given a normal text and its MessageEntity's.
|
||||
|
@ -172,32 +153,77 @@ def unparse(text: str, entities: Iterable[TypeMessageEntity]) -> str:
|
|||
elif not entities:
|
||||
return escape(text)
|
||||
|
||||
if isinstance(entities, TLObject):
|
||||
entities = (entities,)
|
||||
|
||||
text = add_surrogate(text)
|
||||
insert_at = []
|
||||
text = _add_surrogate(text)
|
||||
if _length is None:
|
||||
_length = len(text)
|
||||
html = []
|
||||
last_offset = 0
|
||||
for i, entity in enumerate(entities):
|
||||
s = entity.offset
|
||||
e = entity.offset + entity.length
|
||||
delimiter = ENTITY_TO_FORMATTER.get(type(entity), None)
|
||||
if delimiter:
|
||||
if callable(delimiter):
|
||||
delimiter = delimiter(entity, text[s:e])
|
||||
insert_at.append((s, i, delimiter[0]))
|
||||
insert_at.append((e, -i, delimiter[1]))
|
||||
if entity.offset >= _offset + _length:
|
||||
break
|
||||
relative_offset = entity.offset - _offset
|
||||
if relative_offset > last_offset:
|
||||
html.append(escape(text[last_offset:relative_offset]))
|
||||
elif relative_offset < last_offset:
|
||||
continue
|
||||
|
||||
insert_at.sort(key=lambda t: (t[0], t[1]))
|
||||
next_escape_bound = len(text)
|
||||
while insert_at:
|
||||
# Same logic as markdown.py
|
||||
at, _, what = insert_at.pop()
|
||||
while within_surrogate(text, at):
|
||||
at += 1
|
||||
skip_entity = False
|
||||
length = entity.length
|
||||
|
||||
text = text[:at] + what + escape(text[at:next_escape_bound]) + text[next_escape_bound:]
|
||||
next_escape_bound = at
|
||||
# If we are in the middle of a surrogate nudge the position by +1.
|
||||
# Otherwise we would end up with malformed text and fail to encode.
|
||||
# For example of bad input: "Hi \ud83d\ude1c"
|
||||
# https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF
|
||||
while helpers.within_surrogate(text, relative_offset, length=_length):
|
||||
relative_offset += 1
|
||||
|
||||
text = escape(text[:next_escape_bound]) + text[next_escape_bound:]
|
||||
while helpers.within_surrogate(text, relative_offset + length, length=_length):
|
||||
length += 1
|
||||
|
||||
return del_surrogate(text)
|
||||
entity_text = unparse(text=text[relative_offset:relative_offset + length],
|
||||
entities=entities[i + 1:],
|
||||
_offset=entity.offset, _length=length)
|
||||
entity_type = type(entity)
|
||||
|
||||
if entity_type == MessageEntityBold:
|
||||
html.append('<strong>{}</strong>'.format(entity_text))
|
||||
elif entity_type == MessageEntityItalic:
|
||||
html.append('<em>{}</em>'.format(entity_text))
|
||||
elif entity_type == MessageEntityCode:
|
||||
html.append('<code>{}</code>'.format(entity_text))
|
||||
elif entity_type == MessageEntityUnderline:
|
||||
html.append('<u>{}</u>'.format(entity_text))
|
||||
elif entity_type == MessageEntityStrike:
|
||||
html.append('<del>{}</del>'.format(entity_text))
|
||||
elif entity_type == MessageEntityBlockquote:
|
||||
html.append('<blockquote>{}</blockquote>'.format(entity_text))
|
||||
elif entity_type == MessageEntityPre:
|
||||
if entity.language:
|
||||
html.append(
|
||||
"<pre>\n"
|
||||
" <code class='language-{}'>\n"
|
||||
" {}\n"
|
||||
" </code>\n"
|
||||
"</pre>".format(entity.language, entity_text))
|
||||
else:
|
||||
html.append('<pre><code>{}</code></pre>'
|
||||
.format(entity_text))
|
||||
elif entity_type == MessageEntityEmail:
|
||||
html.append('<a href="mailto:{0}">{0}</a>'.format(entity_text))
|
||||
elif entity_type == MessageEntityUrl:
|
||||
html.append('<a href="{0}">{0}</a>'.format(entity_text))
|
||||
elif entity_type == MessageEntityTextUrl:
|
||||
html.append('<a href="{}">{}</a>'
|
||||
.format(escape(entity.url), entity_text))
|
||||
elif entity_type == MessageEntityMentionName:
|
||||
html.append('<a href="tg://user?id={}">{}</a>'
|
||||
.format(entity.user_id, entity_text))
|
||||
else:
|
||||
skip_entity = True
|
||||
last_offset = relative_offset + (0 if skip_entity else length)
|
||||
|
||||
while helpers.within_surrogate(text, last_offset, length=_length):
|
||||
last_offset += 1
|
||||
|
||||
html.append(escape(text[last_offset:]))
|
||||
return _del_surrogate(''.join(html))
|
||||
|
|
|
@ -22,10 +22,14 @@ DEFAULT_DELIMITERS = {
|
|||
'```': MessageEntityPre
|
||||
}
|
||||
|
||||
DEFAULT_URL_RE = re.compile(r'\[([^]]*?)\]\(([\s\S]*?)\)')
|
||||
DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)')
|
||||
DEFAULT_URL_FORMAT = '[{0}]({1})'
|
||||
|
||||
|
||||
def overlap(a, b, x, y):
|
||||
return max(a, x) < min(b, y)
|
||||
|
||||
|
||||
def parse(message, delimiters=None, url_re=None):
|
||||
"""
|
||||
Parses the given markdown message and returns its stripped representation
|
||||
|
@ -86,8 +90,8 @@ def parse(message, delimiters=None, url_re=None):
|
|||
for ent in result:
|
||||
# If the end is after our start, it is affected
|
||||
if ent.offset + ent.length > i:
|
||||
# If the old start is before ours and the old end is after ours, we are fully enclosed
|
||||
if ent.offset <= i and ent.offset + ent.length >= end + len(delim):
|
||||
# If the old start is also before ours, it is fully enclosed
|
||||
if ent.offset <= i:
|
||||
ent.length -= len(delim) * 2
|
||||
else:
|
||||
ent.length -= len(delim)
|
||||
|
@ -115,7 +119,7 @@ def parse(message, delimiters=None, url_re=None):
|
|||
message[m.end():]
|
||||
))
|
||||
|
||||
delim_size = m.end() - m.start() - len(m.group(1))
|
||||
delim_size = m.end() - m.start() - len(m.group())
|
||||
for ent in result:
|
||||
# If the end is after our start, it is affected
|
||||
if ent.offset + ent.length > m.start():
|
||||
|
@ -160,13 +164,13 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
|
|||
text = add_surrogate(text)
|
||||
delimiters = {v: k for k, v in delimiters.items()}
|
||||
insert_at = []
|
||||
for i, entity in enumerate(entities):
|
||||
for entity in entities:
|
||||
s = entity.offset
|
||||
e = entity.offset + entity.length
|
||||
delimiter = delimiters.get(type(entity), None)
|
||||
if delimiter:
|
||||
insert_at.append((s, i, delimiter))
|
||||
insert_at.append((e, -i, delimiter))
|
||||
insert_at.append((s, delimiter))
|
||||
insert_at.append((e, delimiter))
|
||||
else:
|
||||
url = None
|
||||
if isinstance(entity, MessageEntityTextUrl):
|
||||
|
@ -174,12 +178,12 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
|
|||
elif isinstance(entity, MessageEntityMentionName):
|
||||
url = 'tg://user?id={}'.format(entity.user_id)
|
||||
if url:
|
||||
insert_at.append((s, i, '['))
|
||||
insert_at.append((e, -i, ']({})'.format(url)))
|
||||
insert_at.append((s, '['))
|
||||
insert_at.append((e, ']({})'.format(url)))
|
||||
|
||||
insert_at.sort(key=lambda t: (t[0], t[1]))
|
||||
insert_at.sort(key=lambda t: t[0])
|
||||
while insert_at:
|
||||
at, _, what = insert_at.pop()
|
||||
at, what = insert_at.pop()
|
||||
|
||||
# If we are in the middle of a surrogate nudge the position by -1.
|
||||
# Otherwise we would end up with malformed text and fail to encode.
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
from .tl.functions import *
|
|
@ -7,7 +7,6 @@ import struct
|
|||
import inspect
|
||||
import logging
|
||||
import functools
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from hashlib import sha1
|
||||
|
||||
|
@ -58,81 +57,47 @@ def within_surrogate(text, index, *, length=None):
|
|||
|
||||
return (
|
||||
1 < index < len(text) and # in bounds
|
||||
'\ud800' <= text[index - 1] <= '\udbff' and # previous is
|
||||
'\ud800' <= text[index - 1] <= '\udfff' and # previous is
|
||||
'\ud800' <= text[index] <= '\udfff' # current is
|
||||
)
|
||||
|
||||
|
||||
def strip_text(text, entities):
|
||||
"""
|
||||
Strips whitespace from the given surrogated text modifying the provided
|
||||
entities, also removing any empty (0-length) entities.
|
||||
Strips whitespace from the given text modifying the provided entities.
|
||||
|
||||
This assumes that the length of entities is greater or equal to 0, and
|
||||
that no entity is out of bounds.
|
||||
This assumes that there are no overlapping entities, that their length
|
||||
is greater or equal to one, and that their length is not out of bounds.
|
||||
"""
|
||||
if not entities:
|
||||
return text.strip()
|
||||
|
||||
len_ori = len(text)
|
||||
text = text.lstrip()
|
||||
left_offset = len_ori - len(text)
|
||||
text = text.rstrip()
|
||||
len_final = len(text)
|
||||
while text and text[-1].isspace():
|
||||
e = entities[-1]
|
||||
if e.offset + e.length == len(text):
|
||||
if e.length == 1:
|
||||
del entities[-1]
|
||||
if not entities:
|
||||
return text.strip()
|
||||
else:
|
||||
e.length -= 1
|
||||
text = text[:-1]
|
||||
|
||||
while text and text[0].isspace():
|
||||
for i in reversed(range(len(entities))):
|
||||
e = entities[i]
|
||||
if e.length == 0:
|
||||
del entities[i]
|
||||
if e.offset != 0:
|
||||
e.offset -= 1
|
||||
continue
|
||||
|
||||
if e.offset + e.length > left_offset:
|
||||
if e.offset >= left_offset:
|
||||
# 0 1|2 3 4 5 | 0 1|2 3 4 5
|
||||
# ^ ^ | ^
|
||||
# lo(2) o(5) | o(2)/lo(2)
|
||||
e.offset -= left_offset
|
||||
# |0 1 2 3 | |0 1 2 3
|
||||
# ^ | ^
|
||||
# o=o-lo(3=5-2) | o=o-lo(0=2-2)
|
||||
if e.length == 1:
|
||||
del entities[0]
|
||||
if not entities:
|
||||
return text.lstrip()
|
||||
else:
|
||||
# e.offset < left_offset and e.offset + e.length > left_offset
|
||||
# 0 1 2 3|4 5 6 7 8 9 10
|
||||
# ^ ^ ^
|
||||
# o(1) lo(4) o+l(1+9)
|
||||
e.length = e.offset + e.length - left_offset
|
||||
e.offset = 0
|
||||
# |0 1 2 3 4 5 6
|
||||
# ^ ^
|
||||
# o(0) o+l=0+o+l-lo(6=0+6=0+1+9-4)
|
||||
else:
|
||||
# e.offset + e.length <= left_offset
|
||||
# 0 1 2 3|4 5
|
||||
# ^ ^
|
||||
# o(0) o+l(4)
|
||||
# lo(4)
|
||||
del entities[i]
|
||||
continue
|
||||
e.length -= 1
|
||||
|
||||
if e.offset + e.length <= len_final:
|
||||
# |0 1 2 3 4 5 6 7 8 9
|
||||
# ^ ^
|
||||
# o(1) o+l(1+9)/lf(10)
|
||||
continue
|
||||
if e.offset >= len_final:
|
||||
# |0 1 2 3 4
|
||||
# ^
|
||||
# o(5)/lf(5)
|
||||
del entities[i]
|
||||
else:
|
||||
# e.offset < len_final and e.offset + e.length > len_final
|
||||
# |0 1 2 3 4 5 (6) (7) (8) (9)
|
||||
# ^ ^ ^
|
||||
# o(1) lf(6) o+l(1+8)
|
||||
e.length = len_final - e.offset
|
||||
# |0 1 2 3 4 5
|
||||
# ^ ^
|
||||
# o(1) o+l=o+lf-o=lf(6=1+5=1+6-1)
|
||||
text = text[1:]
|
||||
|
||||
return text
|
||||
|
||||
|
@ -348,22 +313,21 @@ class _FileStream(io.IOBase):
|
|||
self._size = os.path.getsize(self._file)
|
||||
self._stream = open(self._file, 'rb')
|
||||
self._close_stream = True
|
||||
return self
|
||||
|
||||
if isinstance(self._file, bytes):
|
||||
elif isinstance(self._file, bytes):
|
||||
self._size = len(self._file)
|
||||
self._stream = io.BytesIO(self._file)
|
||||
self._close_stream = True
|
||||
return self
|
||||
|
||||
if not callable(getattr(self._file, 'read', None)):
|
||||
elif not callable(getattr(self._file, 'read', None)):
|
||||
raise TypeError('file description should have a `read` method')
|
||||
|
||||
elif self._size is not None:
|
||||
self._name = getattr(self._file, 'name', None)
|
||||
self._stream = self._file
|
||||
self._close_stream = False
|
||||
|
||||
if self._size is None:
|
||||
else:
|
||||
if callable(getattr(self._file, 'seekable', None)):
|
||||
seekable = await _maybe_await(self._file.seekable())
|
||||
else:
|
||||
|
@ -374,6 +338,8 @@ class _FileStream(io.IOBase):
|
|||
await _maybe_await(self._file.seek(0, os.SEEK_END))
|
||||
self._size = await _maybe_await(self._file.tell())
|
||||
await _maybe_await(self._file.seek(pos, os.SEEK_SET))
|
||||
self._stream = self._file
|
||||
self._close_stream = False
|
||||
else:
|
||||
_log.warning(
|
||||
'Could not determine file size beforehand so the entire '
|
||||
|
@ -423,12 +389,3 @@ class _FileStream(io.IOBase):
|
|||
pass
|
||||
|
||||
# endregion
|
||||
|
||||
def get_running_loop():
|
||||
if sys.version_info >= (3, 7):
|
||||
try:
|
||||
return asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return asyncio.get_event_loop_policy().get_event_loop()
|
||||
else:
|
||||
return asyncio.get_event_loop()
|
||||
|
|
|
@ -44,12 +44,7 @@ FileLike = typing.Union[
|
|||
typing.BinaryIO,
|
||||
types.TypeMessageMedia,
|
||||
types.TypeInputFile,
|
||||
types.TypeInputFileLocation,
|
||||
types.TypeInputMedia,
|
||||
types.TypePhoto,
|
||||
types.TypeInputPhoto,
|
||||
types.TypeDocument,
|
||||
types.TypeInputDocument
|
||||
types.TypeInputFileLocation
|
||||
]
|
||||
|
||||
# Can't use `typing.Type` in Python 3.5.2
|
||||
|
|
|
@ -13,7 +13,7 @@ try:
|
|||
except ImportError:
|
||||
python_socks = None
|
||||
|
||||
from ...errors import InvalidChecksumError, InvalidBufferError
|
||||
from ...errors import InvalidChecksumError
|
||||
from ... import helpers
|
||||
|
||||
|
||||
|
@ -116,15 +116,9 @@ class Connection(abc.ABC):
|
|||
# python_socks internal errors are not inherited from
|
||||
# builtin IOError (just from Exception). Instead of adding those
|
||||
# in exceptions clauses everywhere through the code, we
|
||||
# rather monkey-patch them in place. Keep in mind that
|
||||
# ProxyError takes error_code as keyword argument.
|
||||
# rather monkey-patch them in place.
|
||||
|
||||
class ConnectionErrorExtra(ConnectionError):
|
||||
def __init__(self, message, error_code=None):
|
||||
super().__init__(message)
|
||||
self.error_code = error_code
|
||||
|
||||
python_socks._errors.ProxyError = ConnectionErrorExtra
|
||||
python_socks._errors.ProxyError = ConnectionError
|
||||
python_socks._errors.ProxyConnectionError = ConnectionError
|
||||
python_socks._errors.ProxyTimeoutError = ConnectionError
|
||||
|
||||
|
@ -161,7 +155,7 @@ class Connection(abc.ABC):
|
|||
|
||||
# Actual TCP connection is performed here.
|
||||
await asyncio.wait_for(
|
||||
helpers.get_running_loop().sock_connect(sock=sock, address=address),
|
||||
asyncio.get_event_loop().sock_connect(sock=sock, address=address),
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
|
@ -196,7 +190,7 @@ class Connection(abc.ABC):
|
|||
|
||||
# Actual TCP connection and negotiation performed here.
|
||||
await asyncio.wait_for(
|
||||
helpers.get_running_loop().sock_connect(sock=sock, address=address),
|
||||
asyncio.get_event_loop().sock_connect(sock=sock, address=address),
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
|
@ -250,7 +244,7 @@ class Connection(abc.ABC):
|
|||
await self._connect(timeout=timeout, ssl=ssl)
|
||||
self._connected = True
|
||||
|
||||
loop = helpers.get_running_loop()
|
||||
loop = asyncio.get_event_loop()
|
||||
self._send_task = loop.create_task(self._send_loop())
|
||||
self._recv_task = loop.create_task(self._recv_loop())
|
||||
|
||||
|
@ -259,9 +253,6 @@ class Connection(abc.ABC):
|
|||
Disconnects from the server, and clears
|
||||
pending outgoing and incoming messages.
|
||||
"""
|
||||
if not self._connected:
|
||||
return
|
||||
|
||||
self._connected = False
|
||||
|
||||
await helpers._cancel(
|
||||
|
@ -274,12 +265,7 @@ class Connection(abc.ABC):
|
|||
self._writer.close()
|
||||
if sys.version_info >= (3, 7):
|
||||
try:
|
||||
await asyncio.wait_for(self._writer.wait_closed(), timeout=10)
|
||||
except asyncio.TimeoutError:
|
||||
# See issue #3917. For some users, this line was hanging indefinitely.
|
||||
# The hard timeout is not ideal (connection won't be properly closed),
|
||||
# but the code will at least be able to procceed.
|
||||
self._log.warning('Graceful disconnection timed out, forcibly ignoring cleanup')
|
||||
await self._writer.wait_closed()
|
||||
except Exception as e:
|
||||
# Disconnecting should never raise. Seen:
|
||||
# * OSError: No route to host and
|
||||
|
@ -305,10 +291,8 @@ class Connection(abc.ABC):
|
|||
This method returns a coroutine.
|
||||
"""
|
||||
while self._connected:
|
||||
result, err = await self._recv_queue.get()
|
||||
if err:
|
||||
raise err
|
||||
if result:
|
||||
result = await self._recv_queue.get()
|
||||
if result: # None = sentinel value = keep trying
|
||||
return result
|
||||
|
||||
raise ConnectionError('Not connected')
|
||||
|
@ -335,31 +319,34 @@ class Connection(abc.ABC):
|
|||
"""
|
||||
This loop is constantly putting items on the queue as they're read.
|
||||
"""
|
||||
try:
|
||||
while self._connected:
|
||||
try:
|
||||
data = await self._recv()
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except (IOError, asyncio.IncompleteReadError) as e:
|
||||
self._log.warning('Server closed the connection: %s', e)
|
||||
await self._recv_queue.put((None, e))
|
||||
await self.disconnect()
|
||||
except InvalidChecksumError as e:
|
||||
self._log.warning('Server response had invalid checksum: %s', e)
|
||||
await self._recv_queue.put((None, e))
|
||||
except InvalidBufferError as e:
|
||||
self._log.warning('Server response had invalid buffer: %s', e)
|
||||
await self._recv_queue.put((None, e))
|
||||
except Exception as e:
|
||||
self._log.exception('Unexpected exception in the receive loop')
|
||||
await self._recv_queue.put((None, e))
|
||||
await self.disconnect()
|
||||
if isinstance(e, (IOError, asyncio.IncompleteReadError)):
|
||||
msg = 'The server closed the connection'
|
||||
self._log.info(msg)
|
||||
elif isinstance(e, InvalidChecksumError):
|
||||
msg = 'The server response had an invalid checksum'
|
||||
self._log.info(msg)
|
||||
else:
|
||||
await self._recv_queue.put((data, None))
|
||||
finally:
|
||||
msg = 'Unexpected exception in the receive loop'
|
||||
self._log.exception(msg)
|
||||
|
||||
await self.disconnect()
|
||||
|
||||
# Add a sentinel value to unstuck recv
|
||||
if self._recv_queue.empty():
|
||||
self._recv_queue.put_nowait(None)
|
||||
|
||||
break
|
||||
|
||||
try:
|
||||
await self._recv_queue.put(data)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
def _init_conn(self):
|
||||
"""
|
||||
|
|
|
@ -2,7 +2,7 @@ import struct
|
|||
from zlib import crc32
|
||||
|
||||
from .connection import Connection, PacketCodec
|
||||
from ...errors import InvalidChecksumError, InvalidBufferError
|
||||
from ...errors import InvalidChecksumError
|
||||
|
||||
|
||||
class FullPacketCodec(PacketCodec):
|
||||
|
@ -24,18 +24,6 @@ class FullPacketCodec(PacketCodec):
|
|||
async def read_packet(self, reader):
|
||||
packet_len_seq = await reader.readexactly(8) # 4 and 4
|
||||
packet_len, seq = struct.unpack('<ii', packet_len_seq)
|
||||
if packet_len < 0 and seq < 0:
|
||||
# It has been observed that the length and seq can be -429,
|
||||
# followed by the body of 4 bytes also being -429.
|
||||
# See https://github.com/LonamiWebs/Telethon/issues/4042.
|
||||
body = await reader.readexactly(4)
|
||||
raise InvalidBufferError(body)
|
||||
elif packet_len < 8:
|
||||
# Currently unknown why packet_len may be less than 8 but not negative.
|
||||
# Attempting to `readexactly` with less than 0 fails without saying what
|
||||
# the number was which is less helpful.
|
||||
raise InvalidBufferError(packet_len_seq)
|
||||
|
||||
body = await reader.readexactly(packet_len - 8)
|
||||
checksum = struct.unpack('<I', body[-4:])[0]
|
||||
body = body[:-4]
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import base64
|
||||
import os
|
||||
|
||||
from .connection import ObfuscatedConnection
|
||||
|
@ -99,7 +98,7 @@ class TcpMTProxy(ObfuscatedConnection):
|
|||
def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None):
|
||||
# connect to proxy's host and port instead of telegram's ones
|
||||
proxy_host, proxy_port = self.address_info(proxy)
|
||||
self._secret = self.normalize_secret(proxy[2])
|
||||
self._secret = bytes.fromhex(proxy[2])
|
||||
super().__init__(
|
||||
proxy_host, proxy_port, dc_id, loggers=loggers)
|
||||
|
||||
|
@ -131,18 +130,6 @@ class TcpMTProxy(ObfuscatedConnection):
|
|||
raise ValueError("No proxy info specified for MTProxy connection")
|
||||
return proxy_info[:2]
|
||||
|
||||
@staticmethod
|
||||
def normalize_secret(secret):
|
||||
if secret[:2] in ("ee", "dd"): # Remove extra bytes
|
||||
secret = secret[2:]
|
||||
|
||||
try:
|
||||
secret_bytes = bytes.fromhex(secret)
|
||||
except ValueError:
|
||||
secret = secret + '=' * (-len(secret) % 4)
|
||||
secret_bytes = base64.b64decode(secret.encode())
|
||||
|
||||
return secret_bytes[:16] # Remove the domain from the secret (until domain support is added)
|
||||
|
||||
class ConnectionTcpMTProxyAbridged(TcpMTProxy):
|
||||
"""
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import asyncio
|
||||
import collections
|
||||
import struct
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from . import authenticator
|
||||
from ..extensions.messagepacker import MessagePacker
|
||||
|
@ -12,20 +10,18 @@ from .mtprotostate import MTProtoState
|
|||
from ..tl.tlobject import TLRequest
|
||||
from .. import helpers, utils
|
||||
from ..errors import (
|
||||
BadMessageError, InvalidBufferError, AuthKeyNotFound, SecurityError,
|
||||
BadMessageError, InvalidBufferError, SecurityError,
|
||||
TypeNotFoundError, rpc_message_to_error
|
||||
)
|
||||
from ..extensions import BinaryReader
|
||||
from ..tl.core import RpcResult, MessageContainer, GzipPacked
|
||||
from ..tl.functions.auth import LogOutRequest
|
||||
from ..tl.functions import PingRequest, DestroySessionRequest, DestroyAuthKeyRequest
|
||||
from ..tl.functions import PingRequest, DestroySessionRequest
|
||||
from ..tl.types import (
|
||||
MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts,
|
||||
MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq,
|
||||
MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload, DestroySessionOk, DestroySessionNone,
|
||||
DestroyAuthKeyOk, DestroyAuthKeyNone, DestroyAuthKeyFail
|
||||
)
|
||||
from ..tl import types as _tl
|
||||
from ..crypto import AuthKey
|
||||
from ..helpers import retry_range
|
||||
|
||||
|
@ -48,7 +44,7 @@ class MTProtoSender:
|
|||
def __init__(self, auth_key, *, loggers,
|
||||
retries=5, delay=1, auto_reconnect=True, connect_timeout=None,
|
||||
auth_key_callback=None,
|
||||
updates_queue=None, auto_reconnect_callback=None):
|
||||
update_callback=None, auto_reconnect_callback=None):
|
||||
self._connection = None
|
||||
self._loggers = loggers
|
||||
self._log = loggers[__name__]
|
||||
|
@ -57,7 +53,7 @@ class MTProtoSender:
|
|||
self._auto_reconnect = auto_reconnect
|
||||
self._connect_timeout = connect_timeout
|
||||
self._auth_key_callback = auth_key_callback
|
||||
self._updates_queue = updates_queue
|
||||
self._update_callback = update_callback
|
||||
self._auto_reconnect_callback = auto_reconnect_callback
|
||||
self._connect_lock = asyncio.Lock()
|
||||
self._ping = None
|
||||
|
@ -70,7 +66,7 @@ class MTProtoSender:
|
|||
# pending futures should be cancelled.
|
||||
self._user_connected = False
|
||||
self._reconnecting = False
|
||||
self._disconnected = helpers.get_running_loop().create_future()
|
||||
self._disconnected = asyncio.get_event_loop().create_future()
|
||||
self._disconnected.set_result(None)
|
||||
|
||||
# We need to join the loops upon disconnection
|
||||
|
@ -112,11 +108,8 @@ class MTProtoSender:
|
|||
MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten,
|
||||
MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten,
|
||||
MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all,
|
||||
DestroySessionOk.CONSTRUCTOR_ID: self._handle_destroy_session,
|
||||
DestroySessionNone.CONSTRUCTOR_ID: self._handle_destroy_session,
|
||||
DestroyAuthKeyOk.CONSTRUCTOR_ID: self._handle_destroy_auth_key,
|
||||
DestroyAuthKeyNone.CONSTRUCTOR_ID: self._handle_destroy_auth_key,
|
||||
DestroyAuthKeyFail.CONSTRUCTOR_ID: self._handle_destroy_auth_key,
|
||||
DestroySessionOk: self._handle_destroy_session,
|
||||
DestroySessionNone: self._handle_destroy_session,
|
||||
}
|
||||
|
||||
# Public API
|
||||
|
@ -263,7 +256,7 @@ class MTProtoSender:
|
|||
await self._disconnect(error=e)
|
||||
raise e
|
||||
|
||||
loop = helpers.get_running_loop()
|
||||
loop = asyncio.get_event_loop()
|
||||
self._log.debug('Starting send loop')
|
||||
self._send_loop_handle = loop.create_task(self._send_loop())
|
||||
|
||||
|
@ -302,7 +295,7 @@ class MTProtoSender:
|
|||
# notify whenever we change it. This is crucial when we
|
||||
# switch to different data centers.
|
||||
if self._auth_key_callback:
|
||||
await self._auth_key_callback(self.auth_key)
|
||||
self._auth_key_callback(self.auth_key)
|
||||
|
||||
self._log.debug('auth_key generation success!')
|
||||
return True
|
||||
|
@ -384,8 +377,11 @@ class MTProtoSender:
|
|||
except BufferError as e:
|
||||
# TODO there should probably only be one place to except all these errors
|
||||
if isinstance(e, InvalidBufferError) and e.code == 404:
|
||||
self._log.info('Server does not know about the current auth key; the session may need to be recreated')
|
||||
last_error = AuthKeyNotFound()
|
||||
self._log.info('Broken authorization key; resetting')
|
||||
self.auth_key.key = None
|
||||
if self._auth_key_callback:
|
||||
self._auth_key_callback(None)
|
||||
|
||||
ok = False
|
||||
break
|
||||
else:
|
||||
|
@ -402,7 +398,7 @@ class MTProtoSender:
|
|||
self._pending_state.clear()
|
||||
|
||||
if self._auto_reconnect_callback:
|
||||
helpers.get_running_loop().create_task(self._auto_reconnect_callback())
|
||||
asyncio.get_event_loop().create_task(self._auto_reconnect_callback())
|
||||
|
||||
break
|
||||
else:
|
||||
|
@ -427,7 +423,7 @@ class MTProtoSender:
|
|||
# gets stuck.
|
||||
# TODO It still gets stuck? Investigate where and why.
|
||||
self._reconnecting = True
|
||||
helpers.get_running_loop().create_task(self._reconnect(error))
|
||||
asyncio.get_event_loop().create_task(self._reconnect(error))
|
||||
|
||||
def _keepalive_ping(self, rnd_id):
|
||||
"""
|
||||
|
@ -505,29 +501,13 @@ class MTProtoSender:
|
|||
self._log.debug('Receiving items from the network...')
|
||||
try:
|
||||
body = await self._connection.recv()
|
||||
except asyncio.CancelledError:
|
||||
raise # bypass except Exception
|
||||
except (IOError, asyncio.IncompleteReadError) as e:
|
||||
self._log.info('Connection closed while receiving data: %s', e)
|
||||
self._start_reconnect(e)
|
||||
return
|
||||
except InvalidBufferError as e:
|
||||
if e.code == 429:
|
||||
self._log.warning('Server indicated flood error at transport level: %s', e)
|
||||
await self._disconnect(error=e)
|
||||
else:
|
||||
self._log.exception('Server sent invalid buffer')
|
||||
self._start_reconnect(e)
|
||||
return
|
||||
except Exception as e:
|
||||
self._log.exception('Unhandled error while receiving data')
|
||||
except IOError as e:
|
||||
self._log.info('Connection closed while receiving data')
|
||||
self._start_reconnect(e)
|
||||
return
|
||||
|
||||
try:
|
||||
message = self._state.decrypt_message_data(body)
|
||||
if message is None:
|
||||
continue # this message is to be ignored
|
||||
except TypeNotFoundError as e:
|
||||
# Received object which we don't know how to deserialize
|
||||
self._log.info('Type %08x not found, remaining data %r',
|
||||
|
@ -541,14 +521,18 @@ class MTProtoSender:
|
|||
continue
|
||||
except BufferError as e:
|
||||
if isinstance(e, InvalidBufferError) and e.code == 404:
|
||||
self._log.info('Server does not know about the current auth key; the session may need to be recreated')
|
||||
await self._disconnect(error=AuthKeyNotFound())
|
||||
self._log.info('Broken authorization key; resetting')
|
||||
self.auth_key.key = None
|
||||
if self._auth_key_callback:
|
||||
self._auth_key_callback(None)
|
||||
|
||||
await self._disconnect(error=e)
|
||||
else:
|
||||
self._log.warning('Invalid buffer %s', e)
|
||||
self._start_reconnect(e)
|
||||
return
|
||||
except Exception as e:
|
||||
self._log.exception('Unhandled error while decrypting data')
|
||||
self._log.exception('Unhandled error while receiving data')
|
||||
self._start_reconnect(e)
|
||||
return
|
||||
|
||||
|
@ -612,14 +596,6 @@ class MTProtoSender:
|
|||
# However receiving a File() with empty bytes is "common".
|
||||
# See #658, #759 and #958. They seem to happen in a container
|
||||
# which contain the real response right after.
|
||||
#
|
||||
# But, it might also happen that we get an *error* for no parent request.
|
||||
# If that's the case attempting to read from body which is None would fail with:
|
||||
# "BufferError: No more data left to read (need 4, got 0: b''); last read None".
|
||||
# This seems to be particularly common for "RpcError(error_code=-500, error_message='No workers running')".
|
||||
if rpc_result.error:
|
||||
self._log.info('Received error without parent request: %s', rpc_result.error)
|
||||
else:
|
||||
try:
|
||||
with BinaryReader(rpc_result.body) as reader:
|
||||
if not isinstance(reader.tgread_object(), upload.File):
|
||||
|
@ -644,7 +620,6 @@ class MTProtoSender:
|
|||
if not state.future.cancelled():
|
||||
state.future.set_exception(e)
|
||||
else:
|
||||
self._store_own_updates(result)
|
||||
if not state.future.cancelled():
|
||||
state.future.set_result(result)
|
||||
|
||||
|
@ -673,54 +648,12 @@ class MTProtoSender:
|
|||
try:
|
||||
assert message.obj.SUBCLASS_OF_ID == 0x8af52aac # crc32(b'Updates')
|
||||
except AssertionError:
|
||||
self._log.warning(
|
||||
'Note: %s is not an update, not dispatching it %s',
|
||||
message.obj.__class__.__name__,
|
||||
message.obj
|
||||
)
|
||||
self._log.warning('Note: %s is not an update, not dispatching it %s', message.obj)
|
||||
return
|
||||
|
||||
self._log.debug('Handling update %s', message.obj.__class__.__name__)
|
||||
self._updates_queue.put_nowait(message.obj)
|
||||
|
||||
def _store_own_updates(self, obj, *, _update_ids=frozenset((
|
||||
_tl.UpdateShortMessage.CONSTRUCTOR_ID,
|
||||
_tl.UpdateShortChatMessage.CONSTRUCTOR_ID,
|
||||
_tl.UpdateShort.CONSTRUCTOR_ID,
|
||||
_tl.UpdatesCombined.CONSTRUCTOR_ID,
|
||||
_tl.Updates.CONSTRUCTOR_ID,
|
||||
_tl.UpdateShortSentMessage.CONSTRUCTOR_ID,
|
||||
)), _update_like_ids=frozenset((
|
||||
_tl.messages.AffectedHistory.CONSTRUCTOR_ID,
|
||||
_tl.messages.AffectedMessages.CONSTRUCTOR_ID,
|
||||
_tl.messages.AffectedFoundMessages.CONSTRUCTOR_ID,
|
||||
))):
|
||||
try:
|
||||
if obj.CONSTRUCTOR_ID in _update_ids:
|
||||
obj._self_outgoing = True # flag to only process, but not dispatch these
|
||||
self._updates_queue.put_nowait(obj)
|
||||
elif obj.CONSTRUCTOR_ID in _update_like_ids:
|
||||
# Ugly "hack" (?) - otherwise bots reliably detect gaps when deleting messages.
|
||||
#
|
||||
# Note: the `date` being `None` is used to check for `updatesTooLong`, so epoch
|
||||
# is used instead. It is still not read, because `updateShort` has no `seq`.
|
||||
#
|
||||
# Some requests, such as `readHistory`, also return these types. But the `pts_count`
|
||||
# seems to be zero, so while this will produce some bogus `updateDeleteMessages`,
|
||||
# it's still one of the "cleaner" approaches to handling the new `pts`.
|
||||
# `updateDeleteMessages` is probably the "least-invasive" update that can be used.
|
||||
upd = _tl.UpdateShort(
|
||||
_tl.UpdateDeleteMessages([], obj.pts, obj.pts_count),
|
||||
datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc)
|
||||
)
|
||||
upd._self_outgoing = True
|
||||
self._updates_queue.put_nowait(upd)
|
||||
elif obj.CONSTRUCTOR_ID == _tl.messages.InvitedUsers.CONSTRUCTOR_ID:
|
||||
obj.updates._self_outgoing = True
|
||||
self._updates_queue.put_nowait(obj.updates)
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
if self._update_callback:
|
||||
self._update_callback(message.obj)
|
||||
|
||||
async def _handle_pong(self, message):
|
||||
"""
|
||||
|
@ -893,26 +826,3 @@ class MTProtoSender:
|
|||
del self._pending_state[msg_id]
|
||||
if not state.future.cancelled():
|
||||
state.future.set_result(message.obj)
|
||||
|
||||
async def _handle_destroy_auth_key(self, message):
|
||||
"""
|
||||
Handles :tl:`DestroyAuthKeyFail`, :tl:`DestroyAuthKeyNone`, and :tl:`DestroyAuthKeyOk`.
|
||||
|
||||
:tl:`DestroyAuthKey` is not intended for users to use, but they still
|
||||
might, and the response won't come in `rpc_result`, so thhat's worked
|
||||
around here.
|
||||
"""
|
||||
self._log.debug('Handling destroy auth key %s', message.obj)
|
||||
for msg_id, state in list(self._pending_state.items()):
|
||||
if isinstance(state.request, DestroyAuthKeyRequest):
|
||||
del self._pending_state[msg_id]
|
||||
if not state.future.cancelled():
|
||||
state.future.set_result(message.obj)
|
||||
|
||||
# If the auth key has been destroyed, that pretty much means the
|
||||
# library can't continue as our auth key will no longer be found
|
||||
# on the server.
|
||||
# Even if the library didn't disconnect, the server would (and then
|
||||
# the library would reconnect and learn about auth key being invalid).
|
||||
if isinstance(message.obj, DestroyAuthKeyOk):
|
||||
await self._disconnect(error=AuthKeyNotFound())
|
||||
|
|
|
@ -2,7 +2,6 @@ import os
|
|||
import struct
|
||||
import time
|
||||
from hashlib import sha256
|
||||
from collections import deque
|
||||
|
||||
from ..crypto import AES
|
||||
from ..errors import SecurityError, InvalidBufferError
|
||||
|
@ -11,17 +10,6 @@ from ..tl.core import TLMessage
|
|||
from ..tl.tlobject import TLRequest
|
||||
from ..tl.functions import InvokeAfterMsgRequest
|
||||
from ..tl.core.gzippacked import GzipPacked
|
||||
from ..tl.types import BadServerSalt, BadMsgNotification
|
||||
|
||||
|
||||
# N is not specified in https://core.telegram.org/mtproto/security_guidelines#checking-msg-id, but 500 is reasonable
|
||||
MAX_RECENT_MSG_IDS = 500
|
||||
|
||||
MSG_TOO_NEW_DELTA = 30
|
||||
MSG_TOO_OLD_DELTA = 300
|
||||
|
||||
# Something must be wrong if we ignore too many messages at the same time
|
||||
MAX_CONSECUTIVE_IGNORED = 10
|
||||
|
||||
|
||||
class _OpaqueRequest(TLRequest):
|
||||
|
@ -66,9 +54,6 @@ class MTProtoState:
|
|||
self.salt = 0
|
||||
|
||||
self.id = self._sequence = self._last_msg_id = None
|
||||
self._recent_remote_ids = deque(maxlen=MAX_RECENT_MSG_IDS)
|
||||
self._highest_remote_id = 0
|
||||
self._ignore_count = 0
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
|
@ -79,9 +64,6 @@ class MTProtoState:
|
|||
self.id = struct.unpack('q', os.urandom(8))[0]
|
||||
self._sequence = 0
|
||||
self._last_msg_id = 0
|
||||
self._recent_remote_ids.clear()
|
||||
self._highest_remote_id = 0
|
||||
self._ignore_count = 0
|
||||
|
||||
def update_message_id(self, message):
|
||||
"""
|
||||
|
@ -152,8 +134,6 @@ class MTProtoState:
|
|||
"""
|
||||
Inverse of `encrypt_message_data` for incoming server messages.
|
||||
"""
|
||||
now = time.time() # get the time as early as possible, even if other checks make it go unused
|
||||
|
||||
if len(body) < 8:
|
||||
raise InvalidBufferError(body)
|
||||
|
||||
|
@ -176,19 +156,9 @@ class MTProtoState:
|
|||
reader = BinaryReader(body)
|
||||
reader.read_long() # remote_salt
|
||||
if reader.read_long() != self.id:
|
||||
raise SecurityError('Server replied with a wrong session ID (see FAQ for details)')
|
||||
raise SecurityError('Server replied with a wrong session ID')
|
||||
|
||||
remote_msg_id = reader.read_long()
|
||||
|
||||
if remote_msg_id % 2 != 1:
|
||||
raise SecurityError('Server sent an even msg_id')
|
||||
|
||||
# Only perform the (somewhat expensive) check of duplicate if we did receive a lower ID
|
||||
if remote_msg_id <= self._highest_remote_id and remote_msg_id in self._recent_remote_ids:
|
||||
self._log.warning('Server resent the older message %d, ignoring', remote_msg_id)
|
||||
self._count_ignored()
|
||||
return None
|
||||
|
||||
remote_sequence = reader.read_int()
|
||||
reader.read_int() # msg_len for the inner object, padding ignored
|
||||
|
||||
|
@ -197,45 +167,8 @@ class MTProtoState:
|
|||
# reader isn't used for anything else after this, it's unnecessary.
|
||||
obj = reader.tgread_object()
|
||||
|
||||
# "Certain client-to-server service messages containing data sent by the client to the
|
||||
# server (for example, msg_id of a recent client query) may, nonetheless, be processed
|
||||
# on the client even if the time appears to be "incorrect". This is especially true of
|
||||
# messages to change server_salt and notifications about invalid time on the client."
|
||||
#
|
||||
# This means we skip the time check for certain types of messages.
|
||||
if obj.CONSTRUCTOR_ID in (BadServerSalt.CONSTRUCTOR_ID, BadMsgNotification.CONSTRUCTOR_ID):
|
||||
if not self._highest_remote_id and not self.time_offset:
|
||||
# If the first message we receive is a bad notification, take this opportunity
|
||||
# to adjust the time offset. Assume it will remain stable afterwards. Updating
|
||||
# the offset unconditionally would make the next checks pointless.
|
||||
self.update_time_offset(remote_msg_id)
|
||||
else:
|
||||
remote_msg_time = remote_msg_id >> 32
|
||||
time_delta = (now + self.time_offset) - remote_msg_time
|
||||
|
||||
if time_delta > MSG_TOO_OLD_DELTA:
|
||||
self._log.warning('Server sent a very old message with ID %d, ignoring (see FAQ for details)', remote_msg_id)
|
||||
self._count_ignored()
|
||||
return None
|
||||
|
||||
if -time_delta > MSG_TOO_NEW_DELTA:
|
||||
self._log.warning('Server sent a very new message with ID %d, ignoring (see FAQ for details)', remote_msg_id)
|
||||
self._count_ignored()
|
||||
return None
|
||||
|
||||
self._recent_remote_ids.append(remote_msg_id)
|
||||
self._highest_remote_id = remote_msg_id
|
||||
self._ignore_count = 0
|
||||
|
||||
return TLMessage(remote_msg_id, remote_sequence, obj)
|
||||
|
||||
def _count_ignored(self):
|
||||
# It's possible that ignoring a message "bricks" the connection,
|
||||
# but this should not happen unless there's something else wrong.
|
||||
self._ignore_count += 1
|
||||
if self._ignore_count >= MAX_CONSECUTIVE_IGNORED:
|
||||
raise SecurityError('Too many messages had to be ignored consecutively')
|
||||
|
||||
def _get_new_msg_id(self):
|
||||
"""
|
||||
Generates a new unique message ID based on the current
|
||||
|
|
|
@ -98,11 +98,6 @@ class Session(ABC):
|
|||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_update_states(self):
|
||||
"""
|
||||
Returns an iterable over all known pairs of ``(entity ID, update state)``.
|
||||
"""
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Called on client disconnection. Should be used to
|
||||
|
|
|
@ -77,9 +77,6 @@ class MemorySession(Session):
|
|||
def set_update_state(self, entity_id, state):
|
||||
self._update_states[entity_id] = state
|
||||
|
||||
def get_update_states(self):
|
||||
return self._update_states.items()
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
@ -133,8 +130,6 @@ class MemorySession(Session):
|
|||
entities = []
|
||||
if hasattr(tlo, 'user'):
|
||||
entities.append(tlo.user)
|
||||
if hasattr(tlo, 'chat'):
|
||||
entities.append(tlo.chat)
|
||||
if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats):
|
||||
entities.extend(tlo.chats)
|
||||
if hasattr(tlo, 'users') and utils.is_list_like(tlo.users):
|
||||
|
@ -174,7 +169,7 @@ class MemorySession(Session):
|
|||
def get_entity_rows_by_id(self, id, exact=True):
|
||||
try:
|
||||
if exact:
|
||||
return next((found_id, hash) for found_id, hash, _, _, _
|
||||
return next((id, hash) for found_id, hash, _, _, _
|
||||
in self._entities if found_id == id)
|
||||
else:
|
||||
ids = (
|
||||
|
@ -182,7 +177,7 @@ class MemorySession(Session):
|
|||
utils.get_peer_id(PeerChat(id)),
|
||||
utils.get_peer_id(PeerChannel(id))
|
||||
)
|
||||
return next((found_id, hash) for found_id, hash, _, _, _
|
||||
return next((id, hash) for found_id, hash, _, _, _
|
||||
in self._entities if found_id in ids)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
|
|
@ -2,7 +2,7 @@ import datetime
|
|||
import os
|
||||
import time
|
||||
|
||||
from ..tl import types
|
||||
from telethon.tl import types
|
||||
from .memory import MemorySession, _SentFileType
|
||||
from .. import utils
|
||||
from ..crypto import AuthKey
|
||||
|
@ -215,20 +215,6 @@ class SQLiteSession(MemorySession):
|
|||
entity_id, state.pts, state.qts,
|
||||
state.date.timestamp(), state.seq)
|
||||
|
||||
def get_update_states(self):
|
||||
c = self._cursor()
|
||||
try:
|
||||
rows = c.execute('select id, pts, qts, date, seq from update_state').fetchall()
|
||||
return ((row[0], types.updates.State(
|
||||
pts=row[1],
|
||||
qts=row[2],
|
||||
date=datetime.datetime.fromtimestamp(row[3], tz=datetime.timezone.utc),
|
||||
seq=row[4],
|
||||
unread_count=0)
|
||||
) for row in rows)
|
||||
finally:
|
||||
c.close()
|
||||
|
||||
def save(self):
|
||||
"""Saves the current session object as session_user_id.session"""
|
||||
# This is a no-op if there are no changes to commit, so there's
|
||||
|
|
164
telethon/statecache.py
Normal file
164
telethon/statecache.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
import inspect
|
||||
|
||||
from .tl import types
|
||||
|
||||
|
||||
# Which updates have the following fields?
|
||||
_has_channel_id = []
|
||||
|
||||
|
||||
# TODO EntityCache does the same. Reuse?
|
||||
def _fill():
|
||||
for name in dir(types):
|
||||
update = getattr(types, name)
|
||||
if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e:
|
||||
cid = update.CONSTRUCTOR_ID
|
||||
sig = inspect.signature(update.__init__)
|
||||
for param in sig.parameters.values():
|
||||
if param.name == 'channel_id' and param.annotation == int:
|
||||
_has_channel_id.append(cid)
|
||||
|
||||
if not _has_channel_id:
|
||||
raise RuntimeError('FIXME: Did the init signature or updates change?')
|
||||
|
||||
|
||||
# We use a function to avoid cluttering the globals (with name/update/cid/doc)
|
||||
_fill()
|
||||
|
||||
|
||||
class StateCache:
|
||||
"""
|
||||
In-memory update state cache, defaultdict-like behaviour.
|
||||
"""
|
||||
def __init__(self, initial, loggers):
|
||||
# We only care about the pts and the date. By using a tuple which
|
||||
# is lightweight and immutable we can easily copy them around to
|
||||
# each update in case they need to fetch missing entities.
|
||||
self._logger = loggers[__name__]
|
||||
if initial:
|
||||
self._pts_date = initial.pts, initial.date
|
||||
else:
|
||||
self._pts_date = None, None
|
||||
|
||||
def reset(self):
|
||||
self.__dict__.clear()
|
||||
self._pts_date = None, None
|
||||
|
||||
# TODO Call this when receiving responses too...?
|
||||
def update(
|
||||
self,
|
||||
update,
|
||||
*,
|
||||
channel_id=None,
|
||||
has_pts=frozenset(x.CONSTRUCTOR_ID for x in (
|
||||
types.UpdateNewMessage,
|
||||
types.UpdateDeleteMessages,
|
||||
types.UpdateReadHistoryInbox,
|
||||
types.UpdateReadHistoryOutbox,
|
||||
types.UpdateWebPage,
|
||||
types.UpdateReadMessagesContents,
|
||||
types.UpdateEditMessage,
|
||||
types.updates.State,
|
||||
types.updates.DifferenceTooLong,
|
||||
types.UpdateShortMessage,
|
||||
types.UpdateShortChatMessage,
|
||||
types.UpdateShortSentMessage
|
||||
)),
|
||||
has_date=frozenset(x.CONSTRUCTOR_ID for x in (
|
||||
types.UpdateUserPhoto,
|
||||
types.UpdateEncryption,
|
||||
types.UpdateEncryptedMessagesRead,
|
||||
types.UpdateChatParticipantAdd,
|
||||
types.updates.DifferenceEmpty,
|
||||
types.UpdateShortMessage,
|
||||
types.UpdateShortChatMessage,
|
||||
types.UpdateShort,
|
||||
types.UpdatesCombined,
|
||||
types.Updates,
|
||||
types.UpdateShortSentMessage,
|
||||
)),
|
||||
has_channel_pts=frozenset(x.CONSTRUCTOR_ID for x in (
|
||||
types.UpdateChannelTooLong,
|
||||
types.UpdateNewChannelMessage,
|
||||
types.UpdateDeleteChannelMessages,
|
||||
types.UpdateEditChannelMessage,
|
||||
types.UpdateChannelWebPage,
|
||||
types.updates.ChannelDifferenceEmpty,
|
||||
types.updates.ChannelDifferenceTooLong,
|
||||
types.updates.ChannelDifference
|
||||
)),
|
||||
check_only=False
|
||||
):
|
||||
"""
|
||||
Update the state with the given update.
|
||||
"""
|
||||
cid = update.CONSTRUCTOR_ID
|
||||
if check_only:
|
||||
return cid in has_pts or cid in has_date or cid in has_channel_pts
|
||||
|
||||
if cid in has_pts:
|
||||
if cid in has_date:
|
||||
self._pts_date = update.pts, update.date
|
||||
else:
|
||||
self._pts_date = update.pts, self._pts_date[1]
|
||||
elif cid in has_date:
|
||||
self._pts_date = self._pts_date[0], update.date
|
||||
|
||||
if cid in has_channel_pts:
|
||||
if channel_id is None:
|
||||
channel_id = self.get_channel_id(update)
|
||||
|
||||
if channel_id is None:
|
||||
self._logger.info(
|
||||
'Failed to retrieve channel_id from %s', update)
|
||||
else:
|
||||
self.__dict__[channel_id] = update.pts
|
||||
|
||||
def get_channel_id(
|
||||
self,
|
||||
update,
|
||||
has_channel_id=frozenset(_has_channel_id),
|
||||
# Hardcoded because only some with message are for channels
|
||||
has_message=frozenset(x.CONSTRUCTOR_ID for x in (
|
||||
types.UpdateNewChannelMessage,
|
||||
types.UpdateEditChannelMessage
|
||||
))
|
||||
):
|
||||
"""
|
||||
Gets the **unmarked** channel ID from this update, if it has any.
|
||||
|
||||
Fails for ``*difference`` updates, where ``channel_id``
|
||||
is supposedly already known from the outside.
|
||||
"""
|
||||
cid = update.CONSTRUCTOR_ID
|
||||
if cid in has_channel_id:
|
||||
return update.channel_id
|
||||
elif cid in has_message:
|
||||
if update.message.peer_id is None:
|
||||
# Telegram sometimes sends empty messages to give a newer pts:
|
||||
# UpdateNewChannelMessage(message=MessageEmpty(id), pts=pts, pts_count=1)
|
||||
# Not sure why, but it's safe to ignore them.
|
||||
self._logger.debug('Update has None peer_id %s', update)
|
||||
else:
|
||||
return update.message.peer_id.channel_id
|
||||
|
||||
return None
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
If `item` is `None`, returns the default ``(pts, date)``.
|
||||
|
||||
If it's an **unmarked** channel ID, returns its ``pts``.
|
||||
|
||||
If no information is known, ``pts`` will be `None`.
|
||||
"""
|
||||
if item is None:
|
||||
return self._pts_date
|
||||
else:
|
||||
return self.__dict__.get(item)
|
||||
|
||||
def __setitem__(self, where, value):
|
||||
if where is None:
|
||||
self._pts_date = value
|
||||
else:
|
||||
self.__dict__[where] = value
|
|
@ -14,7 +14,7 @@ import asyncio
|
|||
import functools
|
||||
import inspect
|
||||
|
||||
from . import events, errors, utils, connection, helpers
|
||||
from . import events, errors, utils, connection
|
||||
from .client.account import _TakeoutClient
|
||||
from .client.telegramclient import TelegramClient
|
||||
from .tl import types, functions, custom
|
||||
|
@ -32,7 +32,7 @@ def _syncify_wrap(t, method_name):
|
|||
@functools.wraps(method)
|
||||
def syncified(*args, **kwargs):
|
||||
coro = method(*args, **kwargs)
|
||||
loop = helpers.get_running_loop()
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
return coro
|
||||
else:
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
try:
|
||||
from isal import igzip as gzip
|
||||
except ImportError:
|
||||
import gzip
|
||||
import struct
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class Button:
|
|||
instances instead making them yourself (i.e. don't do ``Button(...)``
|
||||
but instead use methods line `Button.inline(...) <inline>` etc.
|
||||
|
||||
You can use `inline`, `switch_inline`, `url`, `auth`, `buy` and `game`
|
||||
You can use `inline`, `switch_inline`, `url` and `auth`
|
||||
together to create inline buttons (under the message).
|
||||
|
||||
You can use `text`, `request_location`, `request_phone` and `request_poll`
|
||||
|
@ -37,14 +37,11 @@ class Button:
|
|||
to 128 characters and add the ellipsis (…) character as
|
||||
the 129.
|
||||
"""
|
||||
def __init__(self, button, *, resize, single_use, selective,
|
||||
persistent, placeholder):
|
||||
def __init__(self, button, *, resize, single_use, selective):
|
||||
self.button = button
|
||||
self.resize = resize
|
||||
self.single_use = single_use
|
||||
self.selective = selective
|
||||
self.persistent = persistent
|
||||
self.placeholder = placeholder
|
||||
|
||||
@staticmethod
|
||||
def _is_inline(button):
|
||||
|
@ -52,14 +49,10 @@ class Button:
|
|||
Returns `True` if the button belongs to an inline keyboard.
|
||||
"""
|
||||
return isinstance(button, (
|
||||
types.KeyboardButtonCopy,
|
||||
types.KeyboardButtonBuy,
|
||||
types.KeyboardButtonCallback,
|
||||
types.KeyboardButtonGame,
|
||||
types.KeyboardButtonSwitchInline,
|
||||
types.KeyboardButtonUrl,
|
||||
types.InputKeyboardButtonUrlAuth,
|
||||
types.KeyboardButtonWebView,
|
||||
types.InputKeyboardButtonUrlAuth
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
|
@ -171,15 +164,11 @@ class Button:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def text(cls, text, *, resize=None, single_use=None, selective=None,
|
||||
persistent=None, placeholder=None):
|
||||
def text(cls, text, *, resize=None, single_use=None, selective=None):
|
||||
"""
|
||||
Creates a new keyboard button with the given text.
|
||||
|
||||
Args:
|
||||
text (`str`):
|
||||
The title of the button.
|
||||
|
||||
resize (`bool`):
|
||||
If present, the entire keyboard will be reconfigured to
|
||||
be resized and be smaller if there are not many buttons.
|
||||
|
@ -194,77 +183,48 @@ class Button:
|
|||
users. It will target users that are @mentioned in the text
|
||||
of the message or to the sender of the message you reply to.
|
||||
|
||||
persistent (`bool`):
|
||||
If present, always show the keyboard when the regular keyboard
|
||||
is hidden. Defaults to false, in which case the custom keyboard
|
||||
can be hidden and revealed via the keyboard icon.
|
||||
|
||||
placeholder (`str`):
|
||||
The placeholder to be shown in the input field when the keyboard is active;
|
||||
1-64 characters
|
||||
|
||||
When the user clicks this button, a text message with the same text
|
||||
as the button will be sent, and can be handled with `events.NewMessage
|
||||
<telethon.events.newmessage.NewMessage>`. You cannot distinguish
|
||||
between a button press and the user typing and sending exactly the
|
||||
same text on their own.
|
||||
"""
|
||||
return cls(
|
||||
types.KeyboardButton(text),
|
||||
resize=resize,
|
||||
single_use=single_use,
|
||||
selective=selective,
|
||||
persistent=persistent,
|
||||
placeholder=placeholder
|
||||
)
|
||||
return cls(types.KeyboardButton(text),
|
||||
resize=resize, single_use=single_use, selective=selective)
|
||||
|
||||
@classmethod
|
||||
def request_location(cls, text, *, resize=None, single_use=None, selective=None,
|
||||
persistent=None, placeholder=None):
|
||||
def request_location(cls, text, *,
|
||||
resize=None, single_use=None, selective=None):
|
||||
"""
|
||||
Creates a new keyboard button to request the user's location on click.
|
||||
|
||||
``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
|
||||
are documented in `text`.
|
||||
``resize``, ``single_use`` and ``selective`` are documented in `text`.
|
||||
|
||||
When the user clicks this button, a confirmation box will be shown
|
||||
to the user asking whether they want to share their location with the
|
||||
bot, and if confirmed a message with geo media will be sent.
|
||||
"""
|
||||
return cls(
|
||||
types.KeyboardButtonRequestGeoLocation(text),
|
||||
resize=resize,
|
||||
single_use=single_use,
|
||||
selective=selective,
|
||||
persistent=persistent,
|
||||
placeholder=placeholder
|
||||
)
|
||||
return cls(types.KeyboardButtonRequestGeoLocation(text),
|
||||
resize=resize, single_use=single_use, selective=selective)
|
||||
|
||||
@classmethod
|
||||
def request_phone(cls, text, *, resize=None, single_use=None,
|
||||
selective=None, persistent=None, placeholder=None):
|
||||
def request_phone(cls, text, *,
|
||||
resize=None, single_use=None, selective=None):
|
||||
"""
|
||||
Creates a new keyboard button to request the user's phone on click.
|
||||
|
||||
``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
|
||||
are documented in `text`.
|
||||
``resize``, ``single_use`` and ``selective`` are documented in `text`.
|
||||
|
||||
When the user clicks this button, a confirmation box will be shown
|
||||
to the user asking whether they want to share their phone with the
|
||||
bot, and if confirmed a message with contact media will be sent.
|
||||
"""
|
||||
return cls(
|
||||
types.KeyboardButtonRequestPhone(text),
|
||||
resize=resize,
|
||||
single_use=single_use,
|
||||
selective=selective,
|
||||
placeholder=placeholder,
|
||||
persistent=persistent
|
||||
)
|
||||
return cls(types.KeyboardButtonRequestPhone(text),
|
||||
resize=resize, single_use=single_use, selective=selective)
|
||||
|
||||
@classmethod
|
||||
def request_poll(cls, text, *, force_quiz=False, resize=None, single_use=None,
|
||||
selective=None, persistent=None, placeholder=None):
|
||||
def request_poll(cls, text, *, force_quiz=False,
|
||||
resize=None, single_use=None, selective=None):
|
||||
"""
|
||||
Creates a new keyboard button to request the user to create a poll.
|
||||
|
||||
|
@ -276,71 +236,26 @@ class Button:
|
|||
the votes cannot be retracted. Otherwise, users can vote and retract
|
||||
the vote, and the pol might be multiple choice.
|
||||
|
||||
``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
|
||||
are documented in `text`.
|
||||
``resize``, ``single_use`` and ``selective`` are documented in `text`.
|
||||
|
||||
When the user clicks this button, a screen letting the user create a
|
||||
poll will be shown, and if they do create one, the poll will be sent.
|
||||
"""
|
||||
return cls(
|
||||
types.KeyboardButtonRequestPoll(text, quiz=force_quiz),
|
||||
resize=resize,
|
||||
single_use=single_use,
|
||||
selective=selective,
|
||||
persistent=persistent,
|
||||
placeholder=placeholder
|
||||
)
|
||||
return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz),
|
||||
resize=resize, single_use=single_use, selective=selective)
|
||||
|
||||
@staticmethod
|
||||
def clear(selective=None):
|
||||
def clear():
|
||||
"""
|
||||
Clears all keyboard buttons after sending a message with this markup.
|
||||
When used, no other button should be present or it will be ignored.
|
||||
|
||||
``selective`` is as documented in `text`.
|
||||
|
||||
"""
|
||||
return types.ReplyKeyboardHide(selective=selective)
|
||||
return types.ReplyKeyboardHide()
|
||||
|
||||
@staticmethod
|
||||
def force_reply(single_use=None, selective=None, placeholder=None):
|
||||
def force_reply():
|
||||
"""
|
||||
Forces a reply to the message with this markup. If used,
|
||||
no other button should be present or it will be ignored.
|
||||
|
||||
``single_use``, ``selective`` and ``placeholder`` are as documented in `text`.
|
||||
|
||||
"""
|
||||
return types.ReplyKeyboardForceReply(
|
||||
single_use=single_use,
|
||||
selective=selective,
|
||||
placeholder=placeholder)
|
||||
|
||||
@staticmethod
|
||||
def buy(text):
|
||||
"""
|
||||
Creates a new inline button to buy a product.
|
||||
|
||||
This can only be used when sending files of type
|
||||
:tl:`InputMediaInvoice`, and must be the first button.
|
||||
|
||||
If the button is not specified, Telegram will automatically
|
||||
add the button to the message. See the
|
||||
`Payments API <https://core.telegram.org/api/payments>`__
|
||||
documentation for more information.
|
||||
"""
|
||||
return types.KeyboardButtonBuy(text)
|
||||
|
||||
@staticmethod
|
||||
def game(text):
|
||||
"""
|
||||
Creates a new inline button to start playing a game.
|
||||
|
||||
This should be used when sending files of type
|
||||
:tl:`InputMediaGame`, and must be the first button.
|
||||
|
||||
See the
|
||||
`Games <https://core.telegram.org/api/bots/games>`__
|
||||
documentation for more information on using games.
|
||||
"""
|
||||
return types.KeyboardButtonGame(text)
|
||||
return types.ReplyKeyboardForceReply()
|
||||
|
|
|
@ -66,9 +66,8 @@ class ChatGetter(abc.ABC):
|
|||
"""
|
||||
if self._input_chat is None and self._chat_peer and self._client:
|
||||
try:
|
||||
self._input_chat = self._client._mb_entity_cache.get(
|
||||
utils.get_peer_id(self._chat_peer, add_mark=False))._as_input_peer()
|
||||
except AttributeError:
|
||||
self._input_chat = self._client._entity_cache[self._chat_peer]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return self._input_chat
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import datetime
|
||||
|
||||
from .. import TLObject, types
|
||||
from .. import TLObject
|
||||
from ..functions.messages import SaveDraftRequest
|
||||
from ..types import DraftMessage
|
||||
from ...errors import RPCError
|
||||
from ...extensions import markdown
|
||||
from ...utils import get_input_peer, get_peer, get_peer_id
|
||||
from ...utils import get_input_peer, get_peer
|
||||
|
||||
|
||||
class Draft:
|
||||
|
@ -37,7 +37,7 @@ class Draft:
|
|||
self._raw_text = draft.message
|
||||
self.date = draft.date
|
||||
self.link_preview = not draft.no_webpage
|
||||
self.reply_to_msg_id = draft.reply_to.reply_to_msg_id if isinstance(draft.reply_to, types.InputReplyToMessage) else None
|
||||
self.reply_to_msg_id = draft.reply_to_msg_id
|
||||
|
||||
@property
|
||||
def entity(self):
|
||||
|
@ -53,9 +53,8 @@ class Draft:
|
|||
"""
|
||||
if not self._input_entity:
|
||||
try:
|
||||
self._input_entity = self._client._mb_entity_cache.get(
|
||||
get_peer_id(self._peer, add_mark=False))._as_input_peer()
|
||||
except AttributeError:
|
||||
self._input_entity = self._client._entity_cache[self._peer]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return self._input_entity
|
||||
|
@ -139,7 +138,7 @@ class Draft:
|
|||
peer=self._peer,
|
||||
message=raw_text,
|
||||
no_webpage=not link_preview,
|
||||
reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),
|
||||
reply_to_msg_id=reply_to,
|
||||
entities=entities
|
||||
))
|
||||
|
||||
|
|
|
@ -21,12 +21,7 @@ class File:
|
|||
@property
|
||||
def id(self):
|
||||
"""
|
||||
The old bot-API style ``file_id`` representing this file.
|
||||
|
||||
.. warning::
|
||||
|
||||
This feature has not been maintained for a long time and
|
||||
may not work. It will be removed in future versions.
|
||||
The bot-API style ``file_id`` representing this file.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
@ -36,12 +36,12 @@ class Forward(ChatGetter, SenderGetter):
|
|||
if ty == helpers._EntityType.USER:
|
||||
sender_id = utils.get_peer_id(original.from_id)
|
||||
sender, input_sender = utils._get_entity_pair(
|
||||
sender_id, entities, client._mb_entity_cache)
|
||||
sender_id, entities, client._entity_cache)
|
||||
|
||||
elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL):
|
||||
peer = original.from_id
|
||||
chat, input_chat = utils._get_entity_pair(
|
||||
utils.get_peer_id(peer), entities, client._mb_entity_cache)
|
||||
utils.get_peer_id(peer), entities, client._entity_cache)
|
||||
|
||||
# This call resets the client
|
||||
ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat)
|
||||
|
|
|
@ -391,7 +391,7 @@ class InlineBuilder:
|
|||
'text geo contact game'.split(), args) if x[1]) or 'none')
|
||||
)
|
||||
|
||||
markup = self._client.build_reply_markup(buttons)
|
||||
markup = self._client.build_reply_markup(buttons, inline_only=True)
|
||||
if text is not None:
|
||||
text, msg_entities = await self._client._parse_message_text(
|
||||
text, parse_mode
|
||||
|
|
|
@ -102,9 +102,8 @@ class InlineResult:
|
|||
elif isinstance(self.result, types.BotInlineMediaResult):
|
||||
return self.result.document
|
||||
|
||||
async def click(self, entity=None, reply_to=None, comment_to=None,
|
||||
silent=False, clear_draft=False, hide_via=False,
|
||||
background=None):
|
||||
async def click(self, entity=None, reply_to=None,
|
||||
silent=False, clear_draft=False, hide_via=False):
|
||||
"""
|
||||
Clicks this result and sends the associated `message`.
|
||||
|
||||
|
@ -115,11 +114,6 @@ class InlineResult:
|
|||
reply_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):
|
||||
If present, the sent message will reply to this ID or message.
|
||||
|
||||
comment_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):
|
||||
Similar to ``reply_to``, but replies in the linked group of a
|
||||
broadcast channel instead (effectively leaving a "comment to"
|
||||
the specified message).
|
||||
|
||||
silent (`bool`, optional):
|
||||
Whether the message should notify people with sound or not.
|
||||
Defaults to `False` (send with a notification sound unless
|
||||
|
@ -133,10 +127,6 @@ class InlineResult:
|
|||
hide_via (`bool`, optional):
|
||||
Whether the "via @bot" should be hidden or not.
|
||||
Only works with certain bots (like @bing or @gif).
|
||||
|
||||
background (`bool`, optional):
|
||||
Whether the message should be send in background.
|
||||
|
||||
"""
|
||||
if entity:
|
||||
entity = await self._client.get_input_entity(entity)
|
||||
|
@ -145,20 +135,15 @@ class InlineResult:
|
|||
else:
|
||||
raise ValueError('You must provide the entity where the result should be sent to')
|
||||
|
||||
if comment_to:
|
||||
entity, reply_id = await self._client._get_comment_data(entity, comment_to)
|
||||
else:
|
||||
reply_id = None if reply_to is None else utils.get_message_id(reply_to)
|
||||
|
||||
req = functions.messages.SendInlineBotResultRequest(
|
||||
peer=entity,
|
||||
query_id=self._query_id,
|
||||
id=self.result.id,
|
||||
silent=silent,
|
||||
background=background,
|
||||
clear_draft=clear_draft,
|
||||
hide_via=hide_via,
|
||||
reply_to=None if reply_id is None else types.InputReplyToMessage(reply_id)
|
||||
reply_to_msg_id=reply_id
|
||||
)
|
||||
return self._client._get_response_message(
|
||||
req, await self._client(req), entity)
|
||||
|
|
|
@ -65,15 +65,6 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
pinned (`bool`):
|
||||
Whether this message is currently pinned or not.
|
||||
|
||||
noforwards (`bool`):
|
||||
Whether this message can be forwarded or not.
|
||||
|
||||
invert_media (`bool`):
|
||||
Whether the media in this message should be inverted.
|
||||
|
||||
offline (`bool`):
|
||||
Whether the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message.
|
||||
|
||||
id (`int`):
|
||||
The ID of this message. This field is *always* present.
|
||||
Any other member is optional and may be `None`.
|
||||
|
@ -96,7 +87,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
The ID of the bot used to send this message
|
||||
through its inline mode (e.g. "via @like").
|
||||
|
||||
reply_to (:tl:`MessageReplyHeader` | :tl:`MessageReplyStoryHeader`):
|
||||
reply_to (:tl:`MessageReplyHeader`):
|
||||
The original reply header if this message is replying to another.
|
||||
|
||||
date (`datetime`):
|
||||
|
@ -150,9 +141,6 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
(photo albums or video albums), all of them will
|
||||
have the same value here.
|
||||
|
||||
reactions (:tl:`MessageReactions`)
|
||||
Reactions to this message.
|
||||
|
||||
restriction_reason (List[:tl:`RestrictionReason`])
|
||||
An optional list of reasons why this message was restricted.
|
||||
If the list is `None`, this message has not been restricted.
|
||||
|
@ -166,68 +154,53 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
action (:tl:`MessageAction`):
|
||||
The message action object of the message for :tl:`MessageService`
|
||||
instances, which will be `None` for other types of messages.
|
||||
|
||||
saved_peer_id (:tl:`Peer`)
|
||||
"""
|
||||
|
||||
# region Initialization
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
peer_id: types.TypePeer,
|
||||
# Common to all
|
||||
self, id: int,
|
||||
|
||||
# Common to Message and MessageService (mandatory)
|
||||
peer_id: types.TypePeer = None,
|
||||
date: Optional[datetime] = None,
|
||||
message: Optional[str] = None,
|
||||
# Copied from Message.__init__ signature
|
||||
|
||||
# Common to Message and MessageService (flags)
|
||||
out: Optional[bool] = None,
|
||||
mentioned: Optional[bool] = None,
|
||||
media_unread: Optional[bool] = None,
|
||||
silent: Optional[bool] = None,
|
||||
post: Optional[bool] = None,
|
||||
from_scheduled: Optional[bool] = None,
|
||||
legacy: Optional[bool] = None,
|
||||
edit_hide: Optional[bool] = None,
|
||||
pinned: Optional[bool] = None,
|
||||
noforwards: Optional[bool] = None,
|
||||
invert_media: Optional[bool] = None,
|
||||
offline: Optional[bool] = None,
|
||||
video_processing_pending: Optional[bool] = None,
|
||||
paid_suggested_post_stars: Optional[bool] = None,
|
||||
paid_suggested_post_ton: Optional[bool] = None,
|
||||
from_id: Optional[types.TypePeer] = None,
|
||||
from_boosts_applied: Optional[int] = None,
|
||||
saved_peer_id: Optional[types.TypePeer] = None,
|
||||
reply_to: Optional[types.TypeMessageReplyHeader] = None,
|
||||
ttl_period: Optional[int] = None,
|
||||
|
||||
# For Message (mandatory)
|
||||
message: Optional[str] = None,
|
||||
|
||||
# For Message (flags)
|
||||
fwd_from: Optional[types.TypeMessageFwdHeader] = None,
|
||||
via_bot_id: Optional[int] = None,
|
||||
via_business_bot_id: Optional[int] = None,
|
||||
reply_to: Optional[types.TypeMessageReplyHeader] = None,
|
||||
media: Optional[types.TypeMessageMedia] = None,
|
||||
reply_markup: Optional[types.TypeReplyMarkup] = None,
|
||||
entities: Optional[List[types.TypeMessageEntity]] = None,
|
||||
views: Optional[int] = None,
|
||||
forwards: Optional[int] = None,
|
||||
replies: Optional[types.TypeMessageReplies] = None,
|
||||
edit_date: Optional[datetime] = None,
|
||||
post_author: Optional[str] = None,
|
||||
grouped_id: Optional[int] = None,
|
||||
reactions: Optional[types.TypeMessageReactions] = None,
|
||||
restriction_reason: Optional[List[types.TypeRestrictionReason]] = None,
|
||||
ttl_period: Optional[int] = None,
|
||||
quick_reply_shortcut_id: Optional[int] = None,
|
||||
effect: Optional[int] = None,
|
||||
factcheck: Optional[types.TypeFactCheck] = None,
|
||||
report_delivery_until_date: Optional[datetime] = None,
|
||||
paid_message_stars: Optional[int] = None,
|
||||
suggested_post: Optional[types.TypeSuggestedPost] = None,
|
||||
# Copied from MessageService.__init__ signature
|
||||
action: Optional[types.TypeMessageAction] = None,
|
||||
reactions_are_possible: Optional[bool] = None,
|
||||
from_scheduled: Optional[bool] = None,
|
||||
legacy: Optional[bool] = None,
|
||||
edit_hide: Optional[bool] = None,
|
||||
pinned: Optional[bool] = None,
|
||||
restriction_reason: Optional[types.TypeRestrictionReason] = None,
|
||||
forwards: Optional[int] = None,
|
||||
replies: Optional[types.TypeMessageReplies] = None,
|
||||
|
||||
# For MessageAction (mandatory)
|
||||
action: Optional[types.TypeMessageAction] = None
|
||||
):
|
||||
# Copied from Message.__init__ body
|
||||
self.id = id
|
||||
self.peer_id = peer_id
|
||||
self.date = date
|
||||
self.message = message
|
||||
# Common properties to messages, then to service (in the order they're defined in the `.tl`)
|
||||
self.out = bool(out)
|
||||
self.mentioned = mentioned
|
||||
self.media_unread = media_unread
|
||||
|
@ -236,20 +209,14 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
self.from_scheduled = from_scheduled
|
||||
self.legacy = legacy
|
||||
self.edit_hide = edit_hide
|
||||
self.pinned = pinned
|
||||
self.noforwards = noforwards
|
||||
self.invert_media = invert_media
|
||||
self.offline = offline
|
||||
self.video_processing_pending = video_processing_pending
|
||||
self.paid_suggested_post_stars = paid_suggested_post_stars
|
||||
self.paid_suggested_post_ton = paid_suggested_post_ton
|
||||
self.id = id
|
||||
self.from_id = from_id
|
||||
self.from_boosts_applied = from_boosts_applied
|
||||
self.saved_peer_id = saved_peer_id
|
||||
self.peer_id = peer_id
|
||||
self.fwd_from = fwd_from
|
||||
self.via_bot_id = via_bot_id
|
||||
self.via_business_bot_id = via_business_bot_id
|
||||
self.reply_to = reply_to
|
||||
self.date = date
|
||||
self.message = message
|
||||
self.media = None if isinstance(media, types.MessageMediaEmpty) else media
|
||||
self.reply_markup = reply_markup
|
||||
self.entities = entities
|
||||
|
@ -257,20 +224,12 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
self.forwards = forwards
|
||||
self.replies = replies
|
||||
self.edit_date = edit_date
|
||||
self.pinned = pinned
|
||||
self.post_author = post_author
|
||||
self.grouped_id = grouped_id
|
||||
self.reactions = reactions
|
||||
self.restriction_reason = restriction_reason
|
||||
self.ttl_period = ttl_period
|
||||
self.quick_reply_shortcut_id = quick_reply_shortcut_id
|
||||
self.effect = effect
|
||||
self.factcheck = factcheck
|
||||
self.report_delivery_until_date = report_delivery_until_date
|
||||
self.paid_message_stars = paid_message_stars
|
||||
self.suggested_post = suggested_post
|
||||
# Copied from MessageService.__init__ body
|
||||
self.action = action
|
||||
self.reactions_are_possible = reactions_are_possible
|
||||
|
||||
# Convenient storage for custom functions
|
||||
# TODO This is becoming a bit of bloat
|
||||
|
@ -302,8 +261,6 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
SenderGetter.__init__(self, sender_id)
|
||||
|
||||
self._forward = None
|
||||
self._reply_to_chat = None
|
||||
self._reply_to_sender = None
|
||||
|
||||
def _finish_init(self, client, entities, input_chat):
|
||||
"""
|
||||
|
@ -318,7 +275,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from:
|
||||
self.out = True
|
||||
|
||||
cache = client._mb_entity_cache
|
||||
cache = client._entity_cache
|
||||
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, entities, cache)
|
||||
|
@ -357,14 +314,6 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
self._linked_chat = entities.get(utils.get_peer_id(
|
||||
types.PeerChannel(self.replies.channel_id)))
|
||||
|
||||
if isinstance(self.reply_to, types.MessageReplyHeader):
|
||||
if self.reply_to.reply_to_peer_id:
|
||||
self._reply_to_chat = entities.get(utils.get_peer_id(self.reply_to.reply_to_peer_id))
|
||||
if self.reply_to.reply_from:
|
||||
if self.reply_to.reply_from.from_id:
|
||||
self._reply_to_sender = entities.get(utils.get_peer_id(self.reply_to.reply_from.from_id))
|
||||
|
||||
|
||||
|
||||
# endregion Initialization
|
||||
|
||||
|
@ -424,11 +373,10 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
@property
|
||||
def is_reply(self):
|
||||
"""
|
||||
`True` if the message is a reply to some other message or story.
|
||||
`True` if the message is a reply to some other message.
|
||||
|
||||
Remember that if the replied-to is a message,
|
||||
you can access the ID of the message this one is
|
||||
replying to through `reply_to.reply_to_msg_id`,
|
||||
Remember that you can access the ID of the message
|
||||
this one is replying to through `reply_to.reply_to_msg_id`,
|
||||
and the `Message` object with `get_reply_message()`.
|
||||
"""
|
||||
return self.reply_to is not None
|
||||
|
@ -441,22 +389,6 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
"""
|
||||
return self._forward
|
||||
|
||||
@property
|
||||
def reply_to_chat(self):
|
||||
"""
|
||||
The :tl:`Channel` in which the replied-to message was sent,
|
||||
if this message is a reply in another chat
|
||||
"""
|
||||
return self._reply_to_chat
|
||||
|
||||
@property
|
||||
def reply_to_sender(self):
|
||||
"""
|
||||
The :tl:`User`, :tl:`Channel`, or whatever other entity that
|
||||
sent the replied-to message, if this message is a reply in another chat.
|
||||
"""
|
||||
return self._reply_to_sender
|
||||
|
||||
@property
|
||||
def buttons(self):
|
||||
"""
|
||||
|
@ -717,11 +649,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
Returns the message ID this message is replying to, if any.
|
||||
This is equivalent to accessing ``.reply_to.reply_to_msg_id``.
|
||||
"""
|
||||
return (
|
||||
self.reply_to.reply_to_msg_id
|
||||
if isinstance(self.reply_to, types.MessageReplyHeader)
|
||||
else None
|
||||
)
|
||||
return self.reply_to.reply_to_msg_id if self.reply_to else None
|
||||
|
||||
@property
|
||||
def to_id(self):
|
||||
|
@ -787,7 +715,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
The result will be cached after its first use.
|
||||
"""
|
||||
if self._reply_message is None and self._client:
|
||||
if not isinstance(self.reply_to, types.MessageReplyHeader):
|
||||
if not self.reply_to:
|
||||
return None
|
||||
|
||||
# Bots cannot access other bots' messages by their ID.
|
||||
|
@ -846,26 +774,12 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
|
||||
async def edit(self, *args, **kwargs):
|
||||
"""
|
||||
Edits the message if it's outgoing. Shorthand for
|
||||
Edits the message iff it's outgoing. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.edit_message`
|
||||
with both ``entity`` and ``message`` already set.
|
||||
|
||||
Returns
|
||||
The edited `Message <telethon.tl.custom.message.Message>`,
|
||||
unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which
|
||||
case this method returns a boolean.
|
||||
|
||||
Raises
|
||||
``MessageAuthorRequiredError`` if you're not the author of the
|
||||
message but tried editing it anyway.
|
||||
|
||||
``MessageNotModifiedError`` if the contents of the message were
|
||||
not modified at all.
|
||||
|
||||
``MessageIdInvalidError`` if the ID of the message is invalid
|
||||
(the ID itself may be correct, but the message with that ID
|
||||
cannot be edited). For example, when trying to edit messages
|
||||
with a reply markup (or clear markup) this error will be raised.
|
||||
Returns `None` if the message was incoming,
|
||||
or the edited `Message` otherwise.
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -879,6 +793,9 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
This is generally the most desired and convenient behaviour,
|
||||
and will work for link previews and message buttons.
|
||||
"""
|
||||
if self.fwd_from or not self.out or not self._client:
|
||||
return None # We assume self.out was patched for our chat
|
||||
|
||||
if 'link_preview' not in kwargs:
|
||||
kwargs['link_preview'] = bool(self.web_preview)
|
||||
|
||||
|
@ -921,7 +838,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
|
||||
async def click(self, i=None, j=None,
|
||||
*, text=None, filter=None, data=None, share_phone=None,
|
||||
share_geo=None, password=None, open_url=None):
|
||||
share_geo=None, password=None):
|
||||
"""
|
||||
Calls :tl:`SendVote` with the specified poll option
|
||||
or `button.click <telethon.tl.custom.messagebutton.MessageButton.click>`
|
||||
|
@ -1006,12 +923,6 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
you need to provide your account's password. Otherwise,
|
||||
`teltehon.errors.PasswordHashInvalidError` is raised.
|
||||
|
||||
open_url (`bool`):
|
||||
When clicking on an inline keyboard URL button :tl:`KeyboardButtonUrl`
|
||||
By default it will return URL of the button, passing ``click(open_url=True)``
|
||||
will lunch the default browser with given URL of the button and
|
||||
return `True` on success.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -1041,7 +952,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
|
||||
but = types.KeyboardButtonCallback('', data)
|
||||
return await MessageButton(self._client, but, chat, None, self.id).click(
|
||||
share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url)
|
||||
share_phone=share_phone, share_geo=share_geo, password=password)
|
||||
|
||||
if sum(int(x is not None) for x in (i, text, filter)) >= 2:
|
||||
raise ValueError('You can only set either of i, text or filter')
|
||||
|
@ -1114,7 +1025,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
button = find_button()
|
||||
if button:
|
||||
return await button.click(
|
||||
share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url)
|
||||
share_phone=share_phone, share_geo=share_geo, password=password)
|
||||
|
||||
async def mark_read(self):
|
||||
"""
|
||||
|
@ -1217,9 +1128,8 @@ class Message(ChatGetter, SenderGetter, TLObject):
|
|||
return bot
|
||||
else:
|
||||
try:
|
||||
return self._client._mb_entity_cache.get(
|
||||
utils.resolve_id(self.via_bot_id)[0])._as_input_peer()
|
||||
except AttributeError:
|
||||
return self._client._entity_cache[self.via_bot_id]
|
||||
except KeyError:
|
||||
raise ValueError('No input sender') from None
|
||||
|
||||
def _document_by_attribute(self, kind, condition=None):
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
from .. import types, functions
|
||||
from ... import password as pwd_mod
|
||||
from ...errors import BotResponseTimeoutError
|
||||
try:
|
||||
import webbrowser
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
|
@ -65,7 +61,7 @@ class MessageButton:
|
|||
if isinstance(self.button, types.KeyboardButtonUrl):
|
||||
return self.button.url
|
||||
|
||||
async def click(self, share_phone=None, share_geo=None, *, password=None, open_url=None):
|
||||
async def click(self, share_phone=None, share_geo=None, *, password=None):
|
||||
"""
|
||||
Emulates the behaviour of clicking this button.
|
||||
|
||||
|
@ -79,8 +75,7 @@ class MessageButton:
|
|||
:tl:`StartBotRequest` will be invoked and the resulting updates
|
||||
returned.
|
||||
|
||||
If it's a :tl:`KeyboardButtonUrl`, the ``URL`` of the button will
|
||||
be returned. If you pass ``open_url=True`` the URL of the button will
|
||||
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
|
||||
|
@ -117,10 +112,7 @@ class MessageButton:
|
|||
bot=self._bot, peer=self._chat, start_param=self.button.query
|
||||
))
|
||||
elif isinstance(self.button, types.KeyboardButtonUrl):
|
||||
if open_url:
|
||||
if "webbrowser" in sys.modules:
|
||||
return webbrowser.open(self.button.url)
|
||||
return self.button.url
|
||||
elif isinstance(self.button, types.KeyboardButtonGame):
|
||||
req = functions.messages.GetBotCallbackAnswerRequest(
|
||||
peer=self._chat, msg_id=self._msg_id, game=True
|
||||
|
|
|
@ -92,12 +92,9 @@ class ParticipantPermissions:
|
|||
Whether the administrator can add new administrators with the same or
|
||||
less permissions than them.
|
||||
"""
|
||||
if not self.is_admin:
|
||||
if not self.is_admin or (self.is_chat and not self.is_creator):
|
||||
return False
|
||||
|
||||
if self.is_chat:
|
||||
return self.is_creator
|
||||
|
||||
return self.participant.admin_rights.add_admins
|
||||
|
||||
ban_users = property(**_admin_prop('ban_users', """
|
||||
|
|
|
@ -113,7 +113,7 @@ class QRLogin:
|
|||
|
||||
if isinstance(resp, types.auth.LoginTokenSuccess):
|
||||
user = resp.authorization.user
|
||||
await self._client._on_login(user)
|
||||
self._client._on_login(user)
|
||||
return user
|
||||
|
||||
raise TypeError('Login token response was unexpected: {}'.format(resp))
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import abc
|
||||
|
||||
from ... import utils
|
||||
|
||||
|
||||
class SenderGetter(abc.ABC):
|
||||
"""
|
||||
|
@ -48,9 +46,6 @@ class SenderGetter(abc.ABC):
|
|||
# cached information, they may use the property instead.
|
||||
if (self._sender is None or getattr(self._sender, 'min', None)) \
|
||||
and await self.get_input_sender():
|
||||
# self.get_input_sender may refresh in which case the sender may no longer be min
|
||||
# However it could still incur a cost so the cheap check is done twice instead.
|
||||
if self._sender is None or getattr(self._sender, 'min', None):
|
||||
try:
|
||||
self._sender =\
|
||||
await self._client.get_entity(self._input_sender)
|
||||
|
@ -71,9 +66,9 @@ class SenderGetter(abc.ABC):
|
|||
"""
|
||||
if self._input_sender is None and self._sender_id and self._client:
|
||||
try:
|
||||
self._input_sender = self._client._mb_entity_cache.get(
|
||||
utils.resolve_id(self._sender_id)[0])._as_input_peer()
|
||||
except AttributeError:
|
||||
self._input_sender = \
|
||||
self._client._entity_cache[self._sender_id]
|
||||
except KeyError:
|
||||
pass
|
||||
return self._input_sender
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
from .tl.types import *
|
|
@ -4,6 +4,7 @@ to convert between an entity like a User, Chat, etc. into its Input version)
|
|||
"""
|
||||
import base64
|
||||
import binascii
|
||||
import imghdr
|
||||
import inspect
|
||||
import io
|
||||
import itertools
|
||||
|
@ -14,7 +15,6 @@ import os
|
|||
import pathlib
|
||||
import re
|
||||
import struct
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from mimetypes import guess_extension
|
||||
from types import GeneratorType
|
||||
|
@ -54,14 +54,20 @@ 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/)?'
|
||||
r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|joinchat/)?'
|
||||
)
|
||||
TG_JOIN_RE = re.compile(
|
||||
r'tg://(join)\?invite='
|
||||
)
|
||||
|
||||
# The only shorter-than-five-characters usernames are those used for some
|
||||
# special, very well known bots. This list may be incomplete though:
|
||||
# "[...] @gif, @vid, @pic, @bing, @wiki, @imdb and @bold [...]"
|
||||
#
|
||||
# See https://telegram.org/blog/inline-bots#how-does-it-work
|
||||
VALID_USERNAME_RE = re.compile(
|
||||
r'^[a-z](?:(?!__)\w){1,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
|
||||
)
|
||||
|
||||
|
@ -96,8 +102,7 @@ def get_display_name(entity):
|
|||
else:
|
||||
return ''
|
||||
|
||||
elif isinstance(entity, (
|
||||
types.Chat, types.ChatForbidden, types.Channel, types.ChannelForbidden)):
|
||||
elif isinstance(entity, (types.Chat, types.ChatForbidden, types.Channel)):
|
||||
return entity.title
|
||||
|
||||
return ''
|
||||
|
@ -424,8 +429,7 @@ def get_input_geo(geo):
|
|||
def get_input_media(
|
||||
media, *,
|
||||
is_photo=False, attributes=None, force_document=False,
|
||||
voice_note=False, video_note=False, supports_streaming=False,
|
||||
ttl=None
|
||||
voice_note=False, video_note=False, supports_streaming=False
|
||||
):
|
||||
"""
|
||||
Similar to :meth:`get_input_peer`, but for media.
|
||||
|
@ -438,40 +442,37 @@ def get_input_media(
|
|||
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia')
|
||||
return media
|
||||
elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
|
||||
return types.InputMediaPhoto(media, ttl_seconds=ttl, spoiler=media.spoiler)
|
||||
return types.InputMediaPhoto(media)
|
||||
elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument')
|
||||
return types.InputMediaDocument(media, ttl_seconds=ttl, spoiler=media.spoiler)
|
||||
return types.InputMediaDocument(media)
|
||||
except AttributeError:
|
||||
_raise_cast_fail(media, 'InputMedia')
|
||||
|
||||
if isinstance(media, types.MessageMediaPhoto):
|
||||
return types.InputMediaPhoto(
|
||||
id=get_input_photo(media.photo),
|
||||
spoiler=media.spoiler,
|
||||
ttl_seconds=ttl or media.ttl_seconds
|
||||
ttl_seconds=media.ttl_seconds
|
||||
)
|
||||
|
||||
if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)):
|
||||
return types.InputMediaPhoto(
|
||||
id=get_input_photo(media),
|
||||
ttl_seconds=ttl
|
||||
id=get_input_photo(media)
|
||||
)
|
||||
|
||||
if isinstance(media, types.MessageMediaDocument):
|
||||
return types.InputMediaDocument(
|
||||
id=get_input_document(media.document),
|
||||
ttl_seconds=ttl or media.ttl_seconds
|
||||
ttl_seconds=media.ttl_seconds
|
||||
)
|
||||
|
||||
if isinstance(media, (types.Document, types.DocumentEmpty)):
|
||||
return types.InputMediaDocument(
|
||||
id=get_input_document(media),
|
||||
ttl_seconds=ttl
|
||||
id=get_input_document(media)
|
||||
)
|
||||
|
||||
if isinstance(media, (types.InputFile, types.InputFileBig)):
|
||||
if is_photo:
|
||||
return types.InputMediaUploadedPhoto(file=media, ttl_seconds=ttl)
|
||||
return types.InputMediaUploadedPhoto(file=media)
|
||||
else:
|
||||
attrs, mime = get_attributes(
|
||||
media,
|
||||
|
@ -482,8 +483,7 @@ def get_input_media(
|
|||
supports_streaming=supports_streaming
|
||||
)
|
||||
return types.InputMediaUploadedDocument(
|
||||
file=media, mime_type=mime, attributes=attrs, force_file=force_document,
|
||||
ttl_seconds=ttl)
|
||||
file=media, mime_type=mime, attributes=attrs, force_file=force_document)
|
||||
|
||||
if isinstance(media, types.MessageMediaGame):
|
||||
return types.InputMediaGame(id=types.InputGameID(
|
||||
|
@ -502,14 +502,6 @@ def get_input_media(
|
|||
if isinstance(media, types.MessageMediaGeo):
|
||||
return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo))
|
||||
|
||||
if isinstance(media, types.MessageMediaGeoLive):
|
||||
return types.InputMediaGeoLive(
|
||||
geo_point=get_input_geo(media.geo),
|
||||
period=media.period,
|
||||
heading=media.heading,
|
||||
proximity_notification_radius=media.proximity_notification_radius,
|
||||
)
|
||||
|
||||
if isinstance(media, types.MessageMediaVenue):
|
||||
return types.InputMediaVenue(
|
||||
geo_point=get_input_geo(media.geo),
|
||||
|
@ -530,7 +522,7 @@ def get_input_media(
|
|||
return types.InputMediaEmpty()
|
||||
|
||||
if isinstance(media, types.Message):
|
||||
return get_input_media(media.media, is_photo=is_photo, ttl=ttl)
|
||||
return get_input_media(media.media, is_photo=is_photo)
|
||||
|
||||
if isinstance(media, types.MessageMediaPoll):
|
||||
if media.poll.quiz:
|
||||
|
@ -587,14 +579,11 @@ def _get_entity_pair(entity_id, entities, cache,
|
|||
"""
|
||||
Returns ``(entity, input_entity)`` for the given entity ID.
|
||||
"""
|
||||
if not entity_id:
|
||||
return None, None
|
||||
|
||||
entity = entities.get(entity_id)
|
||||
try:
|
||||
input_entity = cache.get(resolve_id(entity_id)[0])._as_input_peer()
|
||||
except AttributeError:
|
||||
# AttributeError is unlikely, so another TypeError won't hurt
|
||||
input_entity = cache[entity_id]
|
||||
except KeyError:
|
||||
# KeyError is unlikely, so another TypeError won't hurt
|
||||
try:
|
||||
input_entity = get_input_peer(entity)
|
||||
except TypeError:
|
||||
|
@ -611,9 +600,6 @@ def get_message_id(message):
|
|||
if isinstance(message, int):
|
||||
return message
|
||||
|
||||
if isinstance(message, types.InputMessageID):
|
||||
return message.id
|
||||
|
||||
try:
|
||||
if message.SUBCLASS_OF_ID == 0x790009e3:
|
||||
# hex(crc32(b'Message')) = 0x790009e3
|
||||
|
@ -770,10 +756,7 @@ def sanitize_parse_mode(mode):
|
|||
if not mode:
|
||||
return None
|
||||
|
||||
if (all(hasattr(mode, x) for x in ('parse', 'unparse'))
|
||||
and all(callable(x) for x in (mode.parse, mode.unparse))):
|
||||
return mode
|
||||
elif callable(mode):
|
||||
if callable(mode):
|
||||
class CustomMode:
|
||||
@staticmethod
|
||||
def unparse(text, entities):
|
||||
|
@ -781,6 +764,9 @@ def sanitize_parse_mode(mode):
|
|||
|
||||
CustomMode.parse = mode
|
||||
return CustomMode
|
||||
elif (all(hasattr(mode, x) for x in ('parse', 'unparse'))
|
||||
and all(callable(x) for x in (mode.parse, mode.unparse))):
|
||||
return mode
|
||||
elif isinstance(mode, str):
|
||||
try:
|
||||
return {
|
||||
|
@ -848,6 +834,12 @@ def _get_extension(file):
|
|||
return os.path.splitext(file)[-1]
|
||||
elif isinstance(file, pathlib.Path):
|
||||
return file.suffix
|
||||
elif isinstance(file, bytes):
|
||||
kind = imghdr.what(io.BytesIO(file))
|
||||
return ('.' + kind) if kind else ''
|
||||
elif isinstance(file, io.IOBase) and not isinstance(file, io.TextIOBase) and file.seekable():
|
||||
kind = imghdr.what(file)
|
||||
return ('.' + kind) if kind is not None else ''
|
||||
elif getattr(file, 'name', None):
|
||||
# Note: ``file.name`` works for :tl:`InputFile` and some `IOBase`
|
||||
return _get_extension(file.name)
|
||||
|
@ -910,7 +902,7 @@ def is_list_like(obj):
|
|||
enough. Things like ``open()`` are also iterable (and probably many
|
||||
other things), so just support the commonly known list-like objects.
|
||||
"""
|
||||
return isinstance(obj, (list, tuple, set, dict, range, GeneratorType))
|
||||
return isinstance(obj, (list, tuple, set, dict, GeneratorType))
|
||||
|
||||
|
||||
def parse_phone(phone):
|
||||
|
@ -1033,13 +1025,13 @@ def get_peer_id(peer, add_mark=True):
|
|||
return peer.user_id
|
||||
elif isinstance(peer, types.PeerChat):
|
||||
# Check in case the user mixed things up to avoid blowing up
|
||||
if not (0 < peer.chat_id <= 9999999999):
|
||||
if not (0 < peer.chat_id <= 0x7fffffff):
|
||||
peer.chat_id = resolve_id(peer.chat_id)[0]
|
||||
|
||||
return -peer.chat_id if add_mark else peer.chat_id
|
||||
else: # if isinstance(peer, types.PeerChannel):
|
||||
# Check in case the user mixed things up to avoid blowing up
|
||||
if not (0 < peer.channel_id <= 9999999999):
|
||||
if not (0 < peer.channel_id <= 0x7fffffff):
|
||||
peer.channel_id = resolve_id(peer.channel_id)[0]
|
||||
|
||||
if not add_mark:
|
||||
|
@ -1345,8 +1337,11 @@ def get_appropriated_part_size(file_size):
|
|||
return 128
|
||||
if file_size <= 786432000: # 750MB
|
||||
return 256
|
||||
if file_size <= 2097152000: # 2000MB
|
||||
return 512
|
||||
|
||||
raise ValueError('File size too large')
|
||||
|
||||
|
||||
def encode_waveform(waveform):
|
||||
"""
|
||||
|
@ -1558,11 +1553,3 @@ def _photo_size_byte_count(size):
|
|||
return max(size.sizes)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
async def maybe_async(coro):
|
||||
result = coro
|
||||
if inspect.isawaitable(result):
|
||||
warnings.warn('Using async sessions support is an experimental feature')
|
||||
result = await result
|
||||
return result
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '1.40.0'
|
||||
__version__ = '1.23.0'
|
||||
|
|
|
@ -136,7 +136,7 @@ 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/v1/telethon_examples/payment.py)
|
||||
### [`payment.py`](https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/payment.py)
|
||||
|
||||
* Usable as: **bot**.
|
||||
* Difficulty: **medium**.
|
||||
|
@ -150,18 +150,18 @@ It makes use of the ["raw API"](https://tl.telethon.dev) (that is, no friendly `
|
|||
|
||||
|
||||
[Telethon]: https://github.com/LonamiWebs/Telethon
|
||||
[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_examples/LICENSE
|
||||
[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/LICENSE
|
||||
[@BotFather]: https://t.me/BotFather
|
||||
[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/assistant.py
|
||||
[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/quart_login.py
|
||||
[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/gui.py
|
||||
[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/interactive_telegram_client.py
|
||||
[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_messages.py
|
||||
[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_updates.py
|
||||
[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/replier.py
|
||||
[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/assistant.py
|
||||
[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/quart_login.py
|
||||
[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/gui.py
|
||||
[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/interactive_telegram_client.py
|
||||
[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_messages.py
|
||||
[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_updates.py
|
||||
[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/replier.py
|
||||
[@TelethonianBot]: https://t.me/TelethonianBot
|
||||
[official Telethon's chat]: https://t.me/TelethonChat
|
||||
[`asyncio`]: https://docs.python.org/3/library/asyncio.html
|
||||
[`tkinter`]: https://docs.python.org/3/library/tkinter.html
|
||||
[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/screenshot-gui.jpg
|
||||
[`events.NewMessage`]: https://docs.telethon.dev/en/stable/modules/events.html#telethon.events.newmessage.NewMessage
|
||||
[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/screenshot-gui.jpg
|
||||
[`events.NewMessage`]: https://docs.telethon.dev/en/latest/modules/events.html#telethon.events.newmessage.NewMessage
|
||||
|
|
|
@ -53,7 +53,7 @@ def callback(func):
|
|||
def wrapped(*args, **kwargs):
|
||||
result = func(*args, **kwargs)
|
||||
if inspect.iscoroutine(result):
|
||||
asyncio.create_task(result)
|
||||
aio_loop.create_task(result)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
@ -369,4 +369,10 @@ async def main(interval=0.05):
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
# Some boilerplate code to set up the main method
|
||||
aio_loop = asyncio.get_event_loop()
|
||||
try:
|
||||
aio_loop.run_until_complete(main())
|
||||
finally:
|
||||
if not aio_loop.is_closed():
|
||||
aio_loop.close()
|
||||
|
|
|
@ -9,6 +9,9 @@ from telethon.errors import SessionPasswordNeededError
|
|||
from telethon.network import ConnectionTcpAbridged
|
||||
from telethon.utils import get_display_name
|
||||
|
||||
# Create a global variable to hold the loop we will be using
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
def sprint(string, *args, **kwargs):
|
||||
"""Safe Print (handle UnicodeEncodeErrors on some terminals)"""
|
||||
|
@ -47,7 +50,7 @@ async def async_input(prompt):
|
|||
let the loop run while we wait for input.
|
||||
"""
|
||||
print(prompt, end='', flush=True)
|
||||
return (await asyncio.get_running_loop().run_in_executor(None, sys.stdin.readline)).rstrip()
|
||||
return (await loop.run_in_executor(None, sys.stdin.readline)).rstrip()
|
||||
|
||||
|
||||
def get_env(name, message, cast=str):
|
||||
|
@ -106,34 +109,34 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
# media known the message ID, for every message having media.
|
||||
self.found_media = {}
|
||||
|
||||
async def init(self):
|
||||
# Calling .connect() may raise a connection error False, so you need
|
||||
# to except those before continuing. Otherwise you may want to retry
|
||||
# as done here.
|
||||
print('Connecting to Telegram servers...')
|
||||
try:
|
||||
await self.connect()
|
||||
loop.run_until_complete(self.connect())
|
||||
except IOError:
|
||||
# We handle IOError and not ConnectionError because
|
||||
# PySocks' errors do not subclass ConnectionError
|
||||
# (so this will work with and without proxies).
|
||||
print('Initial connection failed. Retrying...')
|
||||
await self.connect()
|
||||
loop.run_until_complete(self.connect())
|
||||
|
||||
# If the user hasn't called .sign_in() yet, they won't
|
||||
# If the user hasn't called .sign_in() or .sign_up() yet, they won't
|
||||
# be authorized. The first thing you must do is authorize. Calling
|
||||
# .sign_in() should only be done once as the information is saved on
|
||||
# the *.session file so you don't need to enter the code every time.
|
||||
if not await self.is_user_authorized():
|
||||
if not loop.run_until_complete(self.is_user_authorized()):
|
||||
print('First run. Sending code request...')
|
||||
user_phone = input('Enter your phone: ')
|
||||
await self.sign_in(user_phone)
|
||||
loop.run_until_complete(self.sign_in(user_phone))
|
||||
|
||||
self_user = None
|
||||
while self_user is None:
|
||||
code = input('Enter the code you just received: ')
|
||||
try:
|
||||
self_user = await self.sign_in(code=code)
|
||||
self_user =\
|
||||
loop.run_until_complete(self.sign_in(code=code))
|
||||
|
||||
# Two-step verification may be enabled, and .sign_in will
|
||||
# raise this error. If that's the case ask for the password.
|
||||
|
@ -143,7 +146,8 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
pw = getpass('Two step verification is enabled. '
|
||||
'Please enter your password: ')
|
||||
|
||||
self_user = await self.sign_in(password=pw)
|
||||
self_user =\
|
||||
loop.run_until_complete(self.sign_in(password=pw))
|
||||
|
||||
async def run(self):
|
||||
"""Main loop of the TelegramClient, will wait for user action"""
|
||||
|
@ -393,14 +397,9 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
))
|
||||
|
||||
|
||||
async def main():
|
||||
if __name__ == '__main__':
|
||||
SESSION = os.environ.get('TG_SESSION', 'interactive')
|
||||
API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int)
|
||||
API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ')
|
||||
client = InteractiveTelegramClient(SESSION, API_ID, API_HASH)
|
||||
await client.init()
|
||||
await client.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run()
|
||||
loop.run_until_complete(client.run())
|
||||
|
|
|
@ -7,6 +7,8 @@ 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
|
||||
|
||||
|
@ -83,9 +85,9 @@ async def payment_received_handler(event):
|
|||
payment: types.MessageActionPaymentSentMe = event.message.action
|
||||
# do something after payment was received
|
||||
if payment.payload.decode('UTF-8') == 'product A':
|
||||
await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying 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.peer_id.user_id, 'Thank you for buying product B!')
|
||||
await bot.send_message(event.message.from_id, 'Thank you for buying product B!')
|
||||
raise events.StopPropagation
|
||||
|
||||
|
||||
|
@ -178,4 +180,4 @@ if __name__ == '__main__':
|
|||
if not provider_token:
|
||||
logger.error("No provider token supplied.")
|
||||
exit(1)
|
||||
asyncio.run(main())
|
||||
loop.run_until_complete(main())
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import base64
|
||||
import os
|
||||
|
||||
import hypercorn.asyncio
|
||||
from quart import Quart, render_template_string, request
|
||||
|
||||
from telethon import TelegramClient, utils
|
||||
|
@ -81,8 +82,6 @@ async def format_message(message):
|
|||
# Connect the client before we start serving with Quart
|
||||
@app.before_serving
|
||||
async def startup():
|
||||
# After connecting, the client will create additional asyncio tasks that run until it's disconnected again.
|
||||
# Be careful to not mix different asyncio loops during a client's lifetime, or things won't work properly!
|
||||
await client.connect()
|
||||
|
||||
|
||||
|
@ -130,11 +129,24 @@ async def root():
|
|||
return await render_template_string(BASE_TEMPLATE, content=CODE_FORM)
|
||||
|
||||
|
||||
async def main():
|
||||
await hypercorn.asyncio.serve(app, hypercorn.Config())
|
||||
|
||||
|
||||
# By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio
|
||||
# event loop. If we had connected the `TelegramClient` before, `telethon` will
|
||||
# use `asyncio.get_running_loop()` to create some additional tasks. If these
|
||||
# loops are different, it won't work.
|
||||
# event loop. If we create the `TelegramClient` before, `telethon` will
|
||||
# use `asyncio.get_event_loop()`, which is the implicit loop in the main
|
||||
# thread. These two loops are different, and it won't work.
|
||||
#
|
||||
# To keep things simple, be sure to not create multiple asyncio loops!
|
||||
# So, we have to manually pass the same `loop` to both applications to
|
||||
# make 100% sure it works and to avoid headaches.
|
||||
#
|
||||
# To run Quart inside `async def`, we must use `hypercorn.asyncio.serve()`
|
||||
# directly.
|
||||
#
|
||||
# This example creates a global client outside of Quart handlers.
|
||||
# If you create the client inside the handlers (common case), you
|
||||
# won't have to worry about any of this, but it's still good to be
|
||||
# explicit about the event loop.
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
client.loop.run_until_complete(main())
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,15 +5,14 @@ ACCESS_TOKEN_EXPIRED,400,Bot token expired
|
|||
ACCESS_TOKEN_INVALID,400,The provided token is not valid
|
||||
ACTIVE_USER_REQUIRED,401,The method is only available to already activated users
|
||||
ADMINS_TOO_MUCH,400,Too many admins
|
||||
ADMIN_ID_INVALID,400,The specified admin ID is invalid
|
||||
ADMIN_RANK_EMOJI_NOT_ALLOWED,400,Emoji are not allowed in admin titles or ranks
|
||||
ADMIN_RANK_INVALID,400,The given admin title or rank was invalid (possibly larger than 16 characters)
|
||||
ALBUM_PHOTOS_TOO_MANY,400,Too many photos were included in the album
|
||||
API_ID_INVALID,400,The api_id/api_hash combination is invalid
|
||||
API_ID_PUBLISHED_FLOOD,400,"This API id was published somewhere, you can't use it now"
|
||||
ARTICLE_TITLE_EMPTY,400,The title of the article is empty
|
||||
AUDIO_CONTENT_URL_EMPTY,400,The remote URL specified in the content field is empty
|
||||
AUDIO_TITLE_EMPTY,400,The title attribute of the audio must be non-empty
|
||||
AUDIO_CONTENT_URL_EMPTY,400,
|
||||
AUTH_BYTES_INVALID,400,The provided authorization is invalid
|
||||
AUTH_KEY_DUPLICATED,406,"The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions"
|
||||
AUTH_KEY_INVALID,401,The key is invalid
|
||||
|
@ -21,19 +20,17 @@ AUTH_KEY_PERM_EMPTY,401,"The method is unavailable for temporary authorization k
|
|||
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_EXCEPTION,400,An error occurred while importing the auth token
|
||||
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
|
||||
AUTH_TOKEN_INVALID2,400,An invalid authorization token was provided
|
||||
AUTH_TOKEN_INVALIDX,400,The specified auth token is invalid
|
||||
AUTOARCHIVE_NOT_AVAILABLE,400,You cannot use this feature yet
|
||||
BANK_CARD_NUMBER_INVALID,400,Incorrect credit card number
|
||||
BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default"
|
||||
BASE_PORT_LOC_INVALID,400,Base port location invalid
|
||||
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_ONESIDE_NOT_AVAIL,400,
|
||||
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_COMMAND_INVALID,400,The specified command is invalid
|
||||
BOT_COMMAND_INVALID,400,
|
||||
BOT_DOMAIN_INVALID,400,The domain used for the auth button does not match the one configured in @BotFather
|
||||
BOT_GAMES_DISABLED,400,Bot games cannot be used in this type of chat
|
||||
BOT_GROUPS_BLOCKED,400,This bot can't be added to groups
|
||||
|
@ -41,100 +38,69 @@ BOT_INLINE_DISABLED,400,This bot can't be used in inline mode
|
|||
BOT_INVALID,400,This is not a valid bot
|
||||
BOT_METHOD_INVALID,400,The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot
|
||||
BOT_MISSING,400,This method can only be run by a bot
|
||||
BOT_ONESIDE_NOT_AVAIL,400,Bots can't pin messages in PM just for themselves
|
||||
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
|
||||
BOT_SCORE_NOT_MODIFIED,400,The score wasn't modified
|
||||
BROADCAST_CALLS_DISABLED,400,
|
||||
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_TEXT_INVALID,400,The specified button text is invalid
|
||||
BUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid
|
||||
BUTTON_URL_INVALID,400,Button URL invalid
|
||||
BUTTON_USER_PRIVACY_RESTRICTED,400,The privacy setting of the user specified in a [inputKeyboardButtonUserProfile](/constructor/inputKeyboardButtonUserProfile) button do not allow creating such a button
|
||||
CALL_ALREADY_ACCEPTED,400,The call was already accepted
|
||||
CALL_ALREADY_DECLINED,400,The call was already declined
|
||||
CALL_OCCUPY_FAILED,500,The call failed because the user is already making another call
|
||||
CALL_PEER_INVALID,400,The provided call peer object is invalid
|
||||
CALL_PROTOCOL_FLAGS_INVALID,400,Call protocol flags invalid
|
||||
CDN_METHOD_INVALID,400,This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods
|
||||
CDN_UPLOAD_TIMEOUT,500,A server-side timeout occurred while reuploading the file to the CDN DC
|
||||
CHANNELS_ADMIN_LOCATED_TOO_MUCH,400,The user has reached the limit of public geogroups
|
||||
CHANNELS_ADMIN_PUBLIC_TOO_MUCH,400,"You're admin of too many public channels, make some channels private to change the username of this channel"
|
||||
CHANNELS_TOO_MUCH,400,You have joined too many channels/supergroups
|
||||
CHANNEL_BANNED,400,The channel is banned
|
||||
CHANNEL_FORUM_MISSING,400,
|
||||
CHANNEL_ID_INVALID,400,The specified supergroup ID is invalid
|
||||
CHANNEL_INVALID,400,"Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited"
|
||||
CHANNEL_PARICIPANT_MISSING,400,The current user is not in the channel
|
||||
CHANNEL_PRIVATE,400 406,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it
|
||||
CHANNEL_PRIVATE,400,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it
|
||||
CHANNEL_PUBLIC_GROUP_NA,403,channel/supergroup not available
|
||||
CHANNEL_TOO_BIG,400,
|
||||
CHANNEL_TOO_LARGE,400 406,Channel is too large to be deleted; this error is issued when trying to delete channels with more than 1000 members (subject to change)
|
||||
CHAT_ABOUT_NOT_MODIFIED,400,About text has not changed
|
||||
CHAT_ABOUT_TOO_LONG,400,Chat about too long
|
||||
CHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this
|
||||
CHAT_ADMIN_REQUIRED,400 403,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group"
|
||||
CHAT_DISCUSSION_UNALLOWED,400,
|
||||
CHAT_ADMIN_REQUIRED,400,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group"
|
||||
CHAT_FORBIDDEN,403,You cannot write in this chat
|
||||
CHAT_FORWARDS_RESTRICTED,400 406,You can't forward messages from a protected chat
|
||||
CHAT_GET_FAILED,500,
|
||||
CHAT_GUEST_SEND_FORBIDDEN,403,"You join the discussion group before commenting, see [here](/api/discussion#requiring-users-to-join-the-group) for more info"
|
||||
CHAT_ID_EMPTY,400,The provided chat ID is empty
|
||||
CHAT_ID_GENERATE_FAILED,500,Failure while generating the chat ID
|
||||
CHAT_ID_INVALID,400,"Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead"
|
||||
CHAT_INVALID,400,The chat is invalid for this request
|
||||
CHAT_INVITE_PERMANENT,400,You can't set an expiration date on permanent invite links
|
||||
CHAT_LINK_EXISTS,400,The chat is linked to a channel and cannot be used in that request
|
||||
CHAT_NOT_MODIFIED,400,"The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)"
|
||||
CHAT_RESTRICTED,400,The chat is restricted and cannot be used in that request
|
||||
CHAT_REVOKE_DATE_UNSUPPORTED,400,`min_date` and `max_date` are not available for using with non-user peers
|
||||
CHAT_SEND_GAME_FORBIDDEN,403,You can't send a game to this chat
|
||||
CHAT_SEND_GIFS_FORBIDDEN,403,You can't send gifs in this chat
|
||||
CHAT_SEND_INLINE_FORBIDDEN,400 403,You cannot send inline results in this chat
|
||||
CHAT_SEND_INLINE_FORBIDDEN,400,You cannot send inline results in this chat
|
||||
CHAT_SEND_MEDIA_FORBIDDEN,403,You can't send media in this chat
|
||||
CHAT_SEND_POLL_FORBIDDEN,403,You can't send polls in this chat
|
||||
CHAT_SEND_STICKERS_FORBIDDEN,403,You can't send stickers in this chat
|
||||
CHAT_TITLE_EMPTY,400,No chat title provided
|
||||
CHAT_TOO_BIG,400,"This method is not available for groups with more than `chat_read_mark_size_threshold` members, [see client configuration](https://core.telegram.org/api/config#client-configuration)"
|
||||
CHAT_WRITE_FORBIDDEN,403,You can't write in this chat
|
||||
CHP_CALL_FAIL,500,The statistics cannot be retrieved at this time
|
||||
CODE_EMPTY,400,The provided code is empty
|
||||
CODE_HASH_INVALID,400,Code hash invalid
|
||||
CODE_INVALID,400,Code invalid (i.e. from email)
|
||||
CONNECTION_API_ID_INVALID,400,The provided API id is invalid
|
||||
CONNECTION_APP_VERSION_EMPTY,400,App version is empty
|
||||
CONNECTION_DEVICE_MODEL_EMPTY,400,Device model empty
|
||||
CONNECTION_LANG_PACK_INVALID,400,"The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty"
|
||||
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_ADD_MISSING,400,Contact to add is missing
|
||||
CONTACT_ID_INVALID,400,The provided contact ID is invalid
|
||||
CONTACT_NAME_EMPTY,400,The provided contact name cannot be empty
|
||||
CONTACT_REQ_MISSING,400,Missing contact request
|
||||
CREATE_CALL_FAILED,400,An error occurred while creating the call
|
||||
CURRENCY_TOTAL_AMOUNT_INVALID,400,The total amount of all prices is invalid
|
||||
CURRENCY_TOTAL_AMOUNT_INVALID,400,
|
||||
DATA_INVALID,400,Encrypted data invalid
|
||||
DATA_JSON_INVALID,400,The provided JSON data is invalid
|
||||
DATA_TOO_LONG,400,Data too long
|
||||
DATE_EMPTY,400,Date empty
|
||||
DC_ID_INVALID,400,This occurs when an authorization is tried to be exported for the same data center one is currently connected to
|
||||
DH_G_A_INVALID,400,g_a invalid
|
||||
DOCUMENT_INVALID,400,The document file was invalid and can't be used in inline mode
|
||||
EDIT_BOT_INVITE_FORBIDDEN,403,Normal users can't edit invites that were created by bots
|
||||
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,400,Email unconfirmed
|
||||
EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}"
|
||||
EMAIL_VERIFY_EXPIRED,400,The verification email has expired
|
||||
EMOJI_INVALID,400,The specified theme emoji is valid
|
||||
EMOJI_NOT_MODIFIED,400,The theme wasn't changed
|
||||
EMOTICON_EMPTY,400,The emoticon field cannot be empty
|
||||
EMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon
|
||||
EMOTICON_STICKERPACK_MISSING,400,The emoticon sticker pack you are trying to get is missing
|
||||
|
@ -145,18 +111,15 @@ ENCRYPTION_DECLINED,400,The secret chat was declined
|
|||
ENCRYPTION_ID_INVALID,400,The provided secret chat ID is invalid
|
||||
ENCRYPTION_OCCUPY_FAILED,500,TDLib developer claimed it is not an error while accepting secret chats and 500 is used instead of 420
|
||||
ENTITIES_TOO_LONG,400,It is no longer possible to send such long data inside entity tags (for example inline text URLs)
|
||||
ENTITY_BOUNDS_INVALID,400,Some of provided entities have invalid bounds (length is zero or out of the boundaries of the string)
|
||||
ENTITY_MENTION_USER_INVALID,400,You can't use this entity
|
||||
ERROR_TEXT_EMPTY,400,The provided error message is empty
|
||||
EXPIRE_DATE_INVALID,400,The specified expiration date is invalid
|
||||
EXPIRE_FORBIDDEN,400,
|
||||
EXPORT_CARD_INVALID,400,Provided card is invalid
|
||||
EXTERNAL_URL_INVALID,400,External URL invalid
|
||||
FIELD_NAME_EMPTY,400,The field with the name FIELD_NAME is missing
|
||||
FIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid
|
||||
FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again
|
||||
FILE_CONTENT_TYPE_INVALID,400,File content-type is invalid
|
||||
FILE_EMTPY,400,An empty file was provided
|
||||
FILE_CONTENT_TYPE_INVALID,400,
|
||||
FILE_ID_INVALID,400,"The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)"
|
||||
FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc}
|
||||
FILE_PARTS_INVALID,400,The number of file parts is invalid
|
||||
|
@ -166,51 +129,37 @@ FILE_PART_INVALID,400,The file part number is invalid
|
|||
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_TOO_BIG,400,The uploaded file part is too big
|
||||
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
|
||||
FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message
|
||||
FILE_TITLE_EMPTY,400,An empty file title was specified
|
||||
FILTER_ID_INVALID,400,The specified filter ID is invalid
|
||||
FILTER_INCLUDE_EMPTY,400,The include_peers vector of the filter is empty
|
||||
FILTER_NOT_SUPPORTED,400,The specified filter cannot be used in this context
|
||||
FILTER_TITLE_EMPTY,400,The title field of the filter is empty
|
||||
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
|
||||
FLOOD_WAIT_X,420,A wait of {seconds} seconds is required
|
||||
FLOOD_PREMIUM_WAIT_X,420,A wait of {seconds} seconds is required in non-premium accounts
|
||||
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 406,Recently logged-in users cannot add or change admins
|
||||
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
|
||||
FROM_MESSAGE_BOT_DISABLED,400,Bots can't use fromMessage min constructors
|
||||
FROM_PEER_INVALID,400,The given from_user peer cannot be used for the parameter
|
||||
GAME_BOT_INVALID,400,You cannot send that game with the current bot
|
||||
GEO_POINT_INVALID,400,Invalid geoposition provided
|
||||
GIF_CONTENT_TYPE_INVALID,400,GIF content-type invalid
|
||||
GIF_CONTENT_TYPE_INVALID,400,
|
||||
GIF_ID_INVALID,400,The provided GIF ID is invalid
|
||||
GRAPH_EXPIRED_RELOAD,400,"This graph has expired, please obtain a new graph token"
|
||||
GRAPH_INVALID_RELOAD,400,"Invalid graph token provided, please reload the stats and provide the updated token"
|
||||
GRAPH_INVALID_RELOAD,400,
|
||||
GRAPH_OUTDATED_RELOAD,400,"Data can't be used for the channel statistics, graphs outdated"
|
||||
GROUPCALL_ADD_PARTICIPANTS_FAILED,500,
|
||||
GROUPCALL_ALREADY_DISCARDED,400,The group call was already discarded
|
||||
GROUPCALL_ALREADY_STARTED,403,"The groupcall has already started, you can join directly using [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)"
|
||||
GROUPCALL_FORBIDDEN,403,The group call has already ended
|
||||
GROUPCALL_INVALID,400,The specified group call is invalid
|
||||
GROUPCALL_JOIN_MISSING,400,You haven't joined this group call
|
||||
GROUPCALL_NOT_MODIFIED,400,Group call settings weren't modified
|
||||
GROUPCALL_SSRC_DUPLICATE_MUCH,400,The app needs to retry joining the group call with a new SSRC value
|
||||
GROUPCALL_ALREADY_DISCARDED,400,
|
||||
GROUPCALL_FORBIDDEN,403,
|
||||
GROUPCALL_JOIN_MISSING,400,
|
||||
GROUPCALL_SSRC_DUPLICATE_MUCH,400,
|
||||
GROUPCALL_NOT_MODIFIED,400,
|
||||
GROUPED_MEDIA_INVALID,400,Invalid grouped media
|
||||
GROUP_CALL_INVALID,400,Group call invalid
|
||||
HASH_INVALID,400,The provided hash is invalid
|
||||
HIDE_REQUESTER_MISSING,400,The join request was missing or was already handled
|
||||
HISTORY_GET_FAILED,500,Fetching of history failed
|
||||
IMAGE_PROCESS_FAILED,400,Failure while processing image
|
||||
IMPORT_FILE_INVALID,400,The file is too large to be imported
|
||||
IMPORT_FORMAT_UNRECOGNIZED,400,Unknown import format
|
||||
IMPORT_ID_INVALID,400,The specified import ID is invalid
|
||||
IMPORT_ID_INVALID,400,
|
||||
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
|
||||
|
@ -220,32 +169,23 @@ INPUT_FILTER_INVALID,400,The search query filter is invalid
|
|||
INPUT_LAYER_INVALID,400,The provided layer is invalid
|
||||
INPUT_METHOD_INVALID,400,The invoked method does not exist anymore or has never existed
|
||||
INPUT_REQUEST_TOO_LONG,400,The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message)
|
||||
INPUT_TEXT_EMPTY,400,The specified text is empty
|
||||
INPUT_USER_DEACTIVATED,400,The specified user was deleted
|
||||
INTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc}
|
||||
INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc}
|
||||
INVITE_FORBIDDEN_WITH_JOINAS,400,"If the user has anonymously joined a group call as a channel, they can't invite other users to the group call because that would cause deanonymization, because the invite would be sent using the original user ID, not the anonymized channel ID"
|
||||
INVITE_HASH_EMPTY,400,The invite hash is empty
|
||||
INVITE_HASH_EXPIRED,400 406,The chat the user tried to join has expired and is not valid anymore
|
||||
INVITE_HASH_EXPIRED,400,The chat the user tried to join has expired and is not valid anymore
|
||||
INVITE_HASH_INVALID,400,The invite hash is invalid
|
||||
INVITE_REQUEST_SENT,400,You have successfully requested to join this chat or channel
|
||||
INVITE_REVOKED_MISSING,400,The specified invite link was already revoked or is invalid
|
||||
INVOICE_PAYLOAD_INVALID,400,The specified invoice payload is invalid
|
||||
JOIN_AS_PEER_INVALID,400,The specified peer cannot be used to join a group call
|
||||
LANG_CODE_INVALID,400,The specified language code is invalid
|
||||
LANG_CODE_NOT_SUPPORTED,400,The specified language code is not supported
|
||||
LANG_PACK_INVALID,400,The provided language pack is invalid
|
||||
LASTNAME_INVALID,400,The last name is invalid
|
||||
LIMIT_INVALID,400,An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files
|
||||
LINK_NOT_MODIFIED,400,The channel is already linked to this group
|
||||
LOCATION_INVALID,400,The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files
|
||||
MAX_DATE_INVALID,400,The specified maximum date is invalid
|
||||
MAX_ID_INVALID,400,The provided max ID is invalid
|
||||
MAX_QTS_INVALID,400,The provided QTS were invalid
|
||||
MD5_CHECKSUM_INVALID,400,The MD5 check-sums do not match
|
||||
MEDIA_CAPTION_TOO_LONG,400,The caption is too long
|
||||
MEDIA_EMPTY,400,The provided media object is invalid or the current account may not be able to send it (such as games as users)
|
||||
MEDIA_GROUPED_INVALID,400,You tried to send media of different types in an album
|
||||
MEDIA_GROUPED_INVALID,400,
|
||||
MEDIA_INVALID,400,Media invalid
|
||||
MEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as stickers or voice notes)
|
||||
MEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes)
|
||||
|
@ -260,15 +200,13 @@ MESSAGE_DELETE_FORBIDDEN,403,"You can't delete one of the messages you tried to
|
|||
MESSAGE_EDIT_TIME_EXPIRED,400,"You can't edit this message anymore, too much time has passed since its creation."
|
||||
MESSAGE_EMPTY,400,Empty or invalid UTF-8 message was sent
|
||||
MESSAGE_IDS_EMPTY,400,No message ids were provided
|
||||
MESSAGE_ID_INVALID,400,The specified message ID is invalid or you can't do that operation on such message
|
||||
MESSAGE_ID_INVALID,400,"The specified message ID is invalid or you can't do that operation on such message"
|
||||
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
|
||||
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
|
||||
MIN_DATE_INVALID,400,The specified minimum date is invalid
|
||||
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_TOO_OLD,400,"[`chat_read_mark_expire_period` seconds](https://core.telegram.org/api/config#chat-read-mark-expire-period) have passed since the message was sent, read receipts were deleted"
|
||||
MSG_WAIT_FAILED,400,A waiting call returned an error
|
||||
MT_SEND_QUEUE_TOO_LONG,500,
|
||||
MULTI_MEDIA_TOO_LONG,400,Too many media files were included in the same album
|
||||
|
@ -276,32 +214,25 @@ NEED_CHAT_INVALID,500,The provided chat is invalid
|
|||
NEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size)
|
||||
NETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc}
|
||||
NEW_SALT_INVALID,400,The new salt is invalid
|
||||
NEW_SETTINGS_EMPTY,400,"No password is set on the current account, and no new password was specified in `new_settings`"
|
||||
NEW_SETTINGS_INVALID,400,The new settings are invalid
|
||||
NEXT_OFFSET_INVALID,400,The value for next_offset is invalid. Check that it has normal characters and is not too long
|
||||
NOT_ALLOWED,403,
|
||||
OFFSET_INVALID,400,"The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files"
|
||||
OFFSET_PEER_ID_INVALID,400,The provided offset peer is invalid
|
||||
OPTIONS_TOO_MUCH,400,You defined too many options for the poll
|
||||
OPTION_INVALID,400,The option specified is invalid and does not exist in the target poll
|
||||
PACK_SHORT_NAME_INVALID,400,"Invalid sticker pack name. It must begin with a letter, can't contain consecutive underscores and must end in ""_by_<bot username>""."
|
||||
PACK_SHORT_NAME_OCCUPIED,400,A stickerpack with this name already exists
|
||||
PACK_TITLE_INVALID,400,The stickerpack title is invalid
|
||||
PARTICIPANTS_TOO_FEW,400,Not enough participants
|
||||
PARTICIPANT_CALL_FAILED,500,Failure while making call
|
||||
PARTICIPANT_ID_INVALID,400,The specified participant ID is invalid
|
||||
PARTICIPANT_JOIN_MISSING,400 403,"Trying to enable a presentation, when the user hasn't joined the Video Chat with [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)"
|
||||
PARTICIPANT_JOIN_MISSING,403,
|
||||
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_RECOVERY_EXPIRED,400,The recovery code has expired
|
||||
PASSWORD_RECOVERY_NA,400,"No email was set, can't recover password via email"
|
||||
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,400,Too many requests
|
||||
PEER_HISTORY_EMPTY,400,
|
||||
PEER_ID_INVALID,400,"An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)"
|
||||
PEER_ID_NOT_SUPPORTED,400,The provided peer ID is not supported
|
||||
PERSISTENT_TIMESTAMP_EMPTY,400,Persistent timestamp empty
|
||||
|
@ -311,9 +242,7 @@ PHONE_CODE_EMPTY,400,The phone code is missing
|
|||
PHONE_CODE_EXPIRED,400,The confirmation code has expired
|
||||
PHONE_CODE_HASH_EMPTY,400,The phone code hash is missing
|
||||
PHONE_CODE_INVALID,400,The phone code entered was invalid
|
||||
PHONE_HASH_EXPIRED,400,An invalid or expired `phone_code_hash` was provided
|
||||
PHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc}
|
||||
PHONE_NOT_OCCUPIED,400,No user is associated to the specified phone number
|
||||
PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app
|
||||
PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam
|
||||
PHONE_NUMBER_FLOOD,400,You asked for the code too many times.
|
||||
|
@ -322,81 +251,59 @@ PHONE_NUMBER_OCCUPIED,400,The phone number is already in use
|
|||
PHONE_NUMBER_UNOCCUPIED,400,The phone number is not yet being used
|
||||
PHONE_PASSWORD_FLOOD,406,You have tried logging in too many times
|
||||
PHONE_PASSWORD_PROTECTED,400,This phone is password protected
|
||||
PHOTO_CONTENT_TYPE_INVALID,400,Photo mime-type invalid
|
||||
PHOTO_CONTENT_TYPE_INVALID,400,
|
||||
PHOTO_CONTENT_URL_EMPTY,400,The content from the URL used as a photo appears to be empty or has caused another HTTP error
|
||||
PHOTO_CROP_FILE_MISSING,400,Photo crop file missing
|
||||
PHOTO_CROP_SIZE_SMALL,400,Photo is too small
|
||||
PHOTO_EXT_INVALID,400,The extension of the photo is invalid
|
||||
PHOTO_FILE_MISSING,400,Profile photo file missing
|
||||
PHOTO_ID_INVALID,400,Photo id is invalid
|
||||
PHOTO_INVALID,400,Photo invalid
|
||||
PHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images)
|
||||
PHOTO_SAVE_FILE_INVALID,400,The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally
|
||||
PHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error
|
||||
PINNED_DIALOGS_TOO_MUCH,400,Too many pinned dialogs
|
||||
PIN_RESTRICTED,400,You can't pin messages in private chats with other people
|
||||
POLL_ANSWERS_INVALID,400,The poll did not have enough answers or had too many
|
||||
POLL_ANSWER_INVALID,400,One of the poll answers is not acceptable
|
||||
POLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll
|
||||
POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too long)
|
||||
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
|
||||
POLL_VOTE_REQUIRED,403,Cast a vote in the poll before calling this method
|
||||
POSTPONED_TIMEOUT,500,The postponed call has timed out
|
||||
PREMIUM_ACCOUNT_REQUIRED,403,A premium account is required to execute this action
|
||||
PREMIUM_CURRENTLY_UNAVAILABLE,406,
|
||||
PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,"Similar to a flood wait, must wait {minutes} minutes"
|
||||
PRIVACY_KEY_INVALID,400,The privacy key is invalid
|
||||
PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request
|
||||
PRIVACY_VALUE_INVALID,400,The privacy value is invalid
|
||||
PTS_CHANGE_EMPTY,500,No PTS change
|
||||
PUBLIC_CHANNEL_MISSING,403,You can only export group call invite links for public chats or channels
|
||||
PUBLIC_KEY_REQUIRED,400,A public key is required
|
||||
QUERY_ID_EMPTY,400,The query ID is empty
|
||||
QUERY_ID_INVALID,400,The query ID is invalid
|
||||
QUERY_TOO_SHORT,400,The query string is too short
|
||||
QUIZ_ANSWER_MISSING,400,You can forward a quiz while hiding the original author only after choosing an option in the quiz
|
||||
QUIZ_CORRECT_ANSWERS_EMPTY,400,A quiz must specify one correct answer
|
||||
QUIZ_CORRECT_ANSWERS_TOO_MUCH,400,There can only be one correct answer
|
||||
QUIZ_CORRECT_ANSWER_INVALID,400,The correct answer is not an existing answer
|
||||
QUIZ_MULTIPLE_INVALID,400,A poll cannot be both multiple choice and quiz
|
||||
RANDOM_ID_DUPLICATE,500,You provided a random ID that was already used
|
||||
RANDOM_ID_EMPTY,400,Random ID empty
|
||||
RANDOM_ID_INVALID,400,A provided random ID is invalid
|
||||
RANDOM_LENGTH_INVALID,400,Random length invalid
|
||||
RANGES_INVALID,400,Invalid range provided
|
||||
REACTIONS_TOO_MANY,400,"The message already has exactly `reactions_uniq_max` reaction emojis, you can't react with a new emoji, see [the docs for more info](/api/config#client-configuration)"
|
||||
REACTION_EMPTY,400,No reaction provided
|
||||
REACTION_INVALID,400,Invalid reaction provided (only emoji are allowed)
|
||||
REFLECTOR_NOT_AVAILABLE,400,Invalid call reflector server
|
||||
REG_ID_GENERATE_FAILED,500,Failure while generating registration ID
|
||||
REPLY_MARKUP_BUY_EMPTY,400,Reply markup for buy button empty
|
||||
REPLY_MARKUP_GAME_EMPTY,400,The provided reply markup for the game is empty
|
||||
REPLY_MARKUP_INVALID,400,The provided reply markup is invalid
|
||||
REPLY_MARKUP_TOO_LONG,400,The data embedded in the reply markup buttons was too much
|
||||
RESET_REQUEST_MISSING,400,No password reset is in progress
|
||||
RESULTS_TOO_MUCH,400,"You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit"
|
||||
RESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs
|
||||
RESULT_ID_EMPTY,400,Result ID empty
|
||||
RESULT_ID_INVALID,400,The given result cannot be used to send the selection to the bot
|
||||
RESULT_TYPE_INVALID,400,Result type invalid
|
||||
REVOTE_NOT_ALLOWED,400,You cannot change your vote
|
||||
RIGHTS_NOT_MODIFIED,400,"The new admin rights are equal to the old rights, no change was made"
|
||||
RIGHT_FORBIDDEN,403,Either your admin rights do not allow you to do this or you passed the wrong rights combination (some rights only apply to channels and vice versa)
|
||||
RPC_CALL_FAIL,500,"Telegram is having internal issues, please try again later."
|
||||
RPC_MCGET_FAIL,500,"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_INVALID,400,Invalid schedule date provided
|
||||
SCHEDULE_DATE_INVALID,400,
|
||||
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)
|
||||
SCORE_INVALID,400,The specified game score is invalid
|
||||
SEARCH_QUERY_EMPTY,400,The search query is empty
|
||||
SEARCH_WITH_LINK_NOT_SUPPORTED,400,You cannot provide a search query and an invite link at the same time
|
||||
SECONDS_INVALID,400,"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)"
|
||||
SEND_AS_PEER_INVALID,400,You can't send messages as the specified peer
|
||||
SEND_CODE_UNAVAILABLE,406,"Returned when all available options for this type of number were already used (e.g. flash-call, then SMS, then this error might be returned to trigger a second resend)"
|
||||
SEND_MESSAGE_MEDIA_INVALID,400,The message media was invalid or not specified
|
||||
SEND_MESSAGE_TYPE_INVALID,400,The message type is invalid
|
||||
SENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed at this time
|
||||
|
@ -404,99 +311,68 @@ 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
|
||||
SETTINGS_INVALID,400,Invalid settings were provided
|
||||
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
|
||||
SHORT_NAME_INVALID,400,The specified short name is invalid
|
||||
SHORT_NAME_OCCUPIED,400,The specified short name is already in use
|
||||
SIGN_IN_FAILED,500,Failure while signing in
|
||||
SLOWMODE_MULTI_MSGS_DISABLED,400,"Slowmode is enabled, you cannot forward multiple messages to this group"
|
||||
SHORT_NAME_OCCUPIED,400,
|
||||
SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat
|
||||
SMS_CODE_CREATE_FAILED,400,An error occurred while creating the SMS code
|
||||
SRP_ID_INVALID,400,Invalid SRP ID provided
|
||||
SRP_PASSWORD_CHANGED,400,Password has changed
|
||||
SRP_ID_INVALID,400,
|
||||
START_PARAM_EMPTY,400,The start parameter is empty
|
||||
START_PARAM_INVALID,400,Start parameter invalid
|
||||
START_PARAM_TOO_LONG,400,Start parameter is too long
|
||||
STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc}
|
||||
STICKERPACK_STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more"
|
||||
STICKERSET_INVALID,400 406,The provided sticker set is invalid
|
||||
STICKERSET_INVALID,400,The provided sticker set is invalid
|
||||
STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins
|
||||
STICKERS_EMPTY,400,No sticker provided
|
||||
STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more"
|
||||
STICKER_DOCUMENT_INVALID,400,"The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)"
|
||||
STICKER_EMOJI_INVALID,400,Sticker emoji invalid
|
||||
STICKER_FILE_INVALID,400,Sticker file invalid
|
||||
STICKER_GIF_DIMENSIONS,400,The specified video sticker has invalid dimensions
|
||||
STICKER_ID_INVALID,400,The provided sticker ID is invalid
|
||||
STICKER_INVALID,400,The provided sticker is invalid
|
||||
STICKER_MIME_INVALID,400,Make sure to pass a valid image file for the right InputFile parameter
|
||||
STICKER_PNG_DIMENSIONS,400,Sticker png dimensions invalid
|
||||
STICKER_PNG_NOPNG,400,Stickers must be a png file but the used image was not a png
|
||||
STICKER_TGS_NODOC,400,You must send the animated sticker as a document
|
||||
STICKER_TGS_NODOC,400,
|
||||
STICKER_TGS_NOTGS,400,Stickers must be a tgs file but the used file was not a tgs
|
||||
STICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used file was not png
|
||||
STICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs
|
||||
STICKER_VIDEO_BIG,400,The specified video sticker is too big
|
||||
STICKER_VIDEO_NODOC,400,You must send the video sticker as a document
|
||||
STICKER_VIDEO_NOWEBM,400,The specified video sticker is not in webm format
|
||||
STORAGE_CHECK_FAILED,500,Server storage check failed
|
||||
STORE_INVALID_SCALAR_TYPE,500,
|
||||
SWITCH_PM_TEXT_EMPTY,400,The switch_pm.text field was empty
|
||||
TAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout
|
||||
TAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session
|
||||
TAKEOUT_REQUIRED,400 403,You must initialize a takeout request first
|
||||
TEMP_AUTH_KEY_ALREADY_BOUND,400,The passed temporary key is already bound to another **perm_auth_key_id**
|
||||
TAKEOUT_REQUIRED,400,You must initialize a takeout request first
|
||||
TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided
|
||||
THEME_FILE_INVALID,400,Invalid theme file provided
|
||||
THEME_FORMAT_INVALID,400,Invalid theme format provided
|
||||
TIMEOUT,500,A timeout occurred while fetching data from the worker
|
||||
TITLE_INVALID,400,
|
||||
THEME_INVALID,400,Theme invalid
|
||||
THEME_MIME_INVALID,400,"You cannot create this theme, the mime-type is invalid"
|
||||
THEME_TITLE_INVALID,400,The specified theme title is invalid
|
||||
TIMEOUT,500,A timeout occurred while fetching data from the worker
|
||||
TITLE_INVALID,400,The specified stickerpack title is invalid
|
||||
TMP_PASSWORD_DISABLED,400,The temporary password is disabled
|
||||
TMP_PASSWORD_INVALID,400,Password auth needs to be regenerated
|
||||
TOKEN_INVALID,400,The provided token is invalid
|
||||
TOPIC_DELETED,400,The topic was deleted
|
||||
TO_LANG_INVALID,400,The specified destination language is invalid
|
||||
TTL_DAYS_INVALID,400,The provided TTL is invalid
|
||||
TTL_MEDIA_INVALID,400,The provided media cannot be used with a TTL
|
||||
TTL_PERIOD_INVALID,400,The provided TTL Period is invalid
|
||||
TYPES_EMPTY,400,The types field is empty
|
||||
TYPE_CONSTRUCTOR_INVALID,400,The type constructor is invalid
|
||||
Timedout,-503,Timeout while fetching data
|
||||
Timeout,-503,Timeout while fetching data
|
||||
UNKNOWN_ERROR,400,
|
||||
UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs
|
||||
UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None)
|
||||
UPDATE_APP_TO_LOGIN,406,
|
||||
URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL)
|
||||
USAGE_LIMIT_INVALID,400,The specified usage limit is invalid
|
||||
USER_VOLUME_INVALID,400,
|
||||
USERNAME_INVALID,400,"Nobody is using this username, or the username is unacceptable. If the latter, it must match r""[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"""
|
||||
USERNAME_NOT_MODIFIED,400,The username is not different from the current username
|
||||
USERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet
|
||||
USERNAME_OCCUPIED,400,The username is already taken
|
||||
USERNAME_PURCHASE_AVAILABLE,400,
|
||||
USERPIC_PRIVACY_REQUIRED,406,You need to disable privacy settings for your profile picture in order to make your geolocation public
|
||||
USERPIC_UPLOAD_REQUIRED,400 406,You must have a profile picture before using this method
|
||||
USERS_TOO_FEW,400,"Not enough users (to create a chat, for example)"
|
||||
USERS_TOO_MUCH,400,"The maximum number of users has been exceeded (to create a chat, for example)"
|
||||
USER_ADMIN_INVALID,400,Either you're not an admin or you tried to ban an admin that you didn't promote
|
||||
USER_ALREADY_INVITED,400,You have already invited this user
|
||||
USER_ALREADY_PARTICIPANT,400,The authenticated user is already a participant of the chat
|
||||
USER_BANNED_IN_CHANNEL,400,You're banned from sending messages in supergroups/channels
|
||||
USER_BLOCKED,400,User blocked
|
||||
USER_BOT,400,Bots can only be admins in channels.
|
||||
USER_BOT_INVALID,400 403,This method can only be called by a bot
|
||||
USER_BOT_REQUIRED,400,This method can only be called by a bot
|
||||
USER_CHANNELS_TOO_MUCH,400 403,One of the users you tried to add is already in too many channels/supergroups
|
||||
USER_CHANNELS_TOO_MUCH,403,One of the users you tried to add is already in too many channels/supergroups
|
||||
USER_CREATOR,400,"You can't leave this channel, because you're its creator"
|
||||
USER_DEACTIVATED,401,The user has been deleted/deactivated
|
||||
USER_DEACTIVATED_BAN,401,The user has been deleted/deactivated
|
||||
USER_DELETED,403,You can't send this secret message because the other participant deleted their account
|
||||
USER_ID_INVALID,400,"Invalid object ID for a user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited"
|
||||
USER_INVALID,400 403,The given user was invalid
|
||||
USER_INVALID,400,The given user was invalid
|
||||
USER_IS_BLOCKED,400 403,User is blocked
|
||||
USER_IS_BOT,400,Bots can't send messages to other bots
|
||||
USER_KICKED,400,This user was kicked from this supergroup/channel
|
||||
|
@ -504,26 +380,18 @@ USER_MIGRATE_X,303,The user whose identity is being used to execute queries is a
|
|||
USER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact
|
||||
USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel
|
||||
USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this
|
||||
USER_RESTRICTED,403 406,"You're spamreported, you can't create channels or chats."
|
||||
USER_VOLUME_INVALID,400,The specified user volume is invalid
|
||||
USER_RESTRICTED,403,"You're spamreported, you can't create channels or chats."
|
||||
USERPIC_UPLOAD_REQUIRED,400,You must have a profile picture before using this method
|
||||
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
|
||||
VIDEO_TITLE_EMPTY,400,The specified video title is empty
|
||||
VOICE_MESSAGES_FORBIDDEN,400,This user's privacy settings forbid you from sending voice messages
|
||||
VIDEO_TITLE_EMPTY,400,
|
||||
WALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper
|
||||
WALLPAPER_INVALID,400,The input wallpaper was not valid
|
||||
WALLPAPER_MIME_INVALID,400,The specified wallpaper MIME type is invalid
|
||||
WALLPAPER_MIME_INVALID,400,
|
||||
WC_CONVERT_URL_INVALID,400,WC convert URL invalid
|
||||
WEBDOCUMENT_INVALID,400,Invalid webdocument URL provided
|
||||
WEBDOCUMENT_MIME_INVALID,400,Invalid webdocument mime type provided
|
||||
WEBDOCUMENT_SIZE_TOO_BIG,400,Webdocument is too big!
|
||||
WEBDOCUMENT_MIME_INVALID,400,
|
||||
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
|
||||
WEBPUSH_AUTH_INVALID,400,The specified web push authentication secret is invalid
|
||||
WEBPUSH_KEY_INVALID,400,The specified web push elliptic curve Diffie-Hellman public key is invalid
|
||||
WEBPUSH_TOKEN_INVALID,400,The specified web push token is invalid
|
||||
WORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately
|
||||
YOU_BLOCKED_USER,400,You blocked this user
|
||||
FROZEN_METHOD_INVALID,420,You tried to use a method that is not available for frozen accounts
|
||||
FROZEN_PARTICIPANT_MISSING,400,Your account is frozen and can't access the chat
|
||||
|
|
|
|
@ -1,6 +1,7 @@
|
|||
ns,friendly,raw
|
||||
account.AccountMethods,takeout,invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession
|
||||
auth.AuthMethods,sign_in,auth.signIn auth.importBotAuthorization
|
||||
auth.AuthMethods,sign_up,auth.signUp
|
||||
auth.AuthMethods,send_code_request,auth.sendCode auth.resendCode
|
||||
auth.AuthMethods,log_out,auth.logOut
|
||||
auth.AuthMethods,edit_2fa,account.updatePasswordSettings
|
||||
|
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user