Merge branch 'master' into v2

This commit is contained in:
Lonami Exo 2019-06-16 11:27:19 +02:00
commit b3f0c3d2ea
29 changed files with 382 additions and 227 deletions

View File

@ -85,7 +85,7 @@ manually.
Thanks to `@bb010g`_ for writing down this nice list. Thanks to `@bb010g`_ for writing down this nice list.
.. _cryptg: https://github.com/Lonami/cryptg .. _cryptg: https://github.com/cher-nov/cryptg
.. _pyaes: https://github.com/ricmoo/pyaes .. _pyaes: https://github.com/ricmoo/pyaes
.. _pillow: https://python-pillow.org .. _pillow: https://python-pillow.org
.. _aiohttp: https://docs.aiohttp.org .. _aiohttp: https://docs.aiohttp.org

View File

@ -140,3 +140,54 @@ where the following keys are allowed:
} }
.. __: https://github.com/nibrag/aiosocks .. __: https://github.com/nibrag/aiosocks
Using MTProto Proxies
=====================
MTProto Proxies are Telegram's alternative to normal proxies,
and work a bit differently. The following protocols are available:
* ``ConnectionTcpMTProxyAbridged``
* ``ConnectionTcpMTProxyIntermediate``
* ``ConnectionTcpMTProxyRandomizedIntermediate`` (preferred)
For now, you need to manually specify these special connection modes
if you want to use a MTProto Proxy. Your code would look like this:
.. code-block:: python
from telethon import TelegramClient, connection
# we need to change the connection ^^^^^^^^^^
client = TelegramClient(
'anon',
api_id,
api_hash,
# Use one of the available connection modes.
# Normally, this one works with most proxies.
connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,
# Then, pass the proxy details as a tuple:
# (host name, port, proxy secret)
#
# If the proxy has no secret, the secret must be:
# '00000000000000000000000000000000'
proxy=('mtproxy.example.com', 2002, 'secret')
)
In future updates, we may make it easier to use MTProto Proxies
(such as avoiding the need to manually pass ``connection=``).
In short, the same code above but without comments to make it clearer:
.. code-block:: python
from telethon import TelegramClient, connection
client = TelegramClient(
'anon', api_id, api_hash,
connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,
proxy=('mtproxy.example.com', 2002, 'secret')
)

View File

@ -79,7 +79,7 @@ with a ``'hi!'`` message.
.. note:: .. note::
Event handlers **must** be ``async def``. After all, Event handlers **must** be ``async def``. After all,
Telethon is an asynchronous library based on asyncio_, Telethon is an asynchronous library based on `asyncio`,
which is a safer and often faster approach to threads. which is a safer and often faster approach to threads.
You **must** ``await`` all method calls that use You **must** ``await`` all method calls that use
@ -157,5 +157,3 @@ Make sure you understand the code seen here before continuing!
As a rule of thumb, remember that new message events behave just As a rule of thumb, remember that new message events behave just
like message objects, so you can do with them everything you can like message objects, so you can do with them everything you can
do with a message object. do with a message object.
.. _asyncio: https://docs.python.org/3/library/asyncio.html

View File

@ -10,11 +10,11 @@ Mastering asyncio
What's asyncio? What's asyncio?
=============== ===============
asyncio_ is a Python 3's built-in library. This means it's already installed if `asyncio` is a Python 3's built-in library. This means it's already installed if
you have Python 3. Since Python 3.5, it is convenient to work with asynchronous you have Python 3. Since Python 3.5, it is convenient to work with asynchronous
code. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do. code. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do.
asyncio_ stands for *Asynchronous Input Output*. This is a very powerful `asyncio` stands for *Asynchronous Input Output*. This is a very powerful
concept to use whenever you work IO. Interacting with the web or external concept to use whenever you work IO. Interacting with the web or external
APIs such as Telegram's makes a lot of sense this way. APIs such as Telegram's makes a lot of sense this way.
@ -24,7 +24,7 @@ Why asyncio?
Asynchronous IO makes a lot of sense in a library like Telethon. Asynchronous IO makes a lot of sense in a library like Telethon.
You send a request to the server (such as "get some message"), and You send a request to the server (such as "get some message"), and
thanks to asyncio_, your code won't block while a response arrives. thanks to `asyncio`, your code won't block while a response arrives.
The alternative would be to spawn a thread for each update so that The alternative would be to spawn a thread for each update so that
other code can run while the response arrives. That is *a lot* more other code can run while the response arrives. That is *a lot* more
@ -234,7 +234,7 @@ the client:
Generally, **you don't need threads** unless you know what you're doing. Generally, **you don't need threads** unless you know what you're doing.
Just create another task, as shown above. If you're using the Telethon Just create another task, as shown above. If you're using the Telethon
with a library that uses threads, you must be careful to use ``threading.Lock`` with a library that uses threads, you must be careful to use `threading.Lock`
whenever you use the client, or enable the compatible mode. For that, see whenever you use the client, or enable the compatible mode. For that, see
:ref:`compatibility-and-convenience`. :ref:`compatibility-and-convenience`.
@ -254,7 +254,7 @@ client.run_until_disconnected() blocks!
All of what `client.run_until_disconnected() All of what `client.run_until_disconnected()
<telethon.client.updates.UpdateMethods.run_until_disconnected>` does is <telethon.client.updates.UpdateMethods.run_until_disconnected>` does is
run the asyncio_'s event loop until the client is disconnected. That means run the `asyncio`'s event loop until the client is disconnected. That means
*the loop is running*. And if the loop is running, it will run all the tasks *the loop is running*. And if the loop is running, it will run all the tasks
in it. So if you want to run *other* code, create tasks for it: in it. So if you want to run *other* code, create tasks for it:
@ -274,9 +274,10 @@ in it. So if you want to run *other* code, create tasks for it:
This creates a task for a clock that prints the time every second. This creates a task for a clock that prints the time every second.
You don't need to use `client.run_until_disconnected() You don't need to use `client.run_until_disconnected()
<telethon.client.updates.UpdateMethods.run_until_disconnected>` either! <telethon.client.updates.UpdateMethods.run_until_disconnected>` either!
You just need to make the loop is running, somehow. ``asyncio.run_forever`` You just need to make the loop is running, somehow. `loop.run_forever()
and ``asyncio.run_until_complete`` can also be used to run the loop, and <asyncio.loop.run_forever()>` and `loop.run_until_complete()
Telethon will be happy with any approach. <asyncio.loop.run_until_complete>` can also be used to run
the loop, and Telethon will be happy with any approach.
Of course, there are better tools to run code hourly or daily, see below. Of course, there are better tools to run code hourly or daily, see below.
@ -285,7 +286,7 @@ What else can asyncio do?
========================= =========================
Asynchronous IO is a really powerful tool, as we've seen. There are plenty Asynchronous IO is a really powerful tool, as we've seen. There are plenty
of other useful libraries that also use asyncio_ and that you can integrate of other useful libraries that also use `asyncio` and that you can integrate
with Telethon. with Telethon.
* `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous * `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous
@ -314,7 +315,7 @@ you can run requests in parallel:
This code will get the 10 last messages from `@TelethonChat This code will get the 10 last messages from `@TelethonChat
<https://t.me/TelethonChat>`_, send one to `@TelethonOfftopic <https://t.me/TelethonChat>`_, send one to `@TelethonOfftopic
<https://t.me/TelethonOfftopic>`_, and also download the profile <https://t.me/TelethonOfftopic>`_, and also download the profile
photo of the main group. asyncio_ will run all these three tasks photo of the main group. `asyncio` will run all these three tasks
at the same time. You can run all the tasks you want this way. at the same time. You can run all the tasks you want this way.
A different way would be: A different way would be:
@ -355,8 +356,6 @@ Where can I read more?
====================== ======================
`Check out my blog post `Check out my blog post
<https://lonamiwebs.github.io/blog/asyncio/>`_ about asyncio_, which <https://lonamiwebs.github.io/blog/asyncio/>`_ about `asyncio`, which
has some more examples and pictures to help you understand what happens has some more examples and pictures to help you understand what happens
when the loop runs. when the loop runs.
.. _asyncio: https://docs.python.org/3/library/asyncio.html

View File

@ -158,12 +158,12 @@ Understanding asyncio
===================== =====================
With ``asyncio``, the library has several tasks running in the background. With `asyncio`, the library has several tasks running in the background.
One task is used for sending requests, another task is used to receive them, One task is used for sending requests, another task is used to receive them,
and a third one is used to handle updates. and a third one is used to handle updates.
To handle updates, you must keep your script running. You can do this in To handle updates, you must keep your script running. You can do this in
several ways. For instance, if you are *not* running ``asyncio``'s event several ways. For instance, if you are *not* running `asyncio`'s event
loop, you should use `client.run_until_disconnected loop, you should use `client.run_until_disconnected
<telethon.client.updates.UpdateMethods.run_until_disconnected>`: <telethon.client.updates.UpdateMethods.run_until_disconnected>`:

View File

@ -40,9 +40,14 @@ tl_ref_url = 'https://tl.telethon.dev'
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.autosummary', 'sphinx.ext.autosummary',
'sphinx.ext.intersphinx',
'custom_roles' 'custom_roles'
] ]
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None)
}
# Change the default role so we can avoid prefixing everything with :obj: # Change the default role so we can avoid prefixing everything with :obj:
default_role = "py:obj" default_role = "py:obj"

View File

@ -195,11 +195,11 @@ banned rights of a user through :tl:`EditBannedRequest` and its parameter
client(EditBannedRequest(channel, user, rights)) client(EditBannedRequest(channel, user, rights))
You can also use a ``datetime`` object for ``until_date=``, or even a You can use a `datetime.datetime` object for ``until_date=``,
Unix timestamp. Note that if you ban someone for less than 30 seconds a `datetime.timedelta` or even a Unix timestamp. Note that if you ban
or for more than 366 days, Telegram will consider the ban to actually someone for less than 30 seconds or for more than 366 days, Telegram
last forever. This is officially documented under will consider the ban to actually last forever. This is officially
https://core.telegram.org/bots/api#restrictchatmember. documented under https://core.telegram.org/bots/api#restrictchatmember.
Kicking a member Kicking a member

View File

@ -1152,7 +1152,7 @@ reasons. But there's one more surprise!
There is a new magic ``telethon.sync`` module to let you use **all** the There is a new magic ``telethon.sync`` module to let you use **all** the
methods in the :ref:`TelegramClient <telethon-client>` (and the types returned methods in the :ref:`TelegramClient <telethon-client>` (and the types returned
from its functions) in a synchronous way, while using ``asyncio`` behind from its functions) in a synchronous way, while using `asyncio` behind
the scenes! This means you're now able to do both of the following: the scenes! This means you're now able to do both of the following:
.. code-block:: python .. code-block:: python
@ -1238,7 +1238,7 @@ Bug fixes
- "User joined" event was being treated as "User was invited". - "User joined" event was being treated as "User was invited".
- SQLite's cursor should not be closed properly after usage. - SQLite's cursor should not be closed properly after usage.
- ``await`` the updates task upon disconnection. - ``await`` the updates task upon disconnection.
- Some bug in Python 3.5.2's ``asyncio`` causing 100% CPU load if you - Some bug in Python 3.5.2's `asyncio` causing 100% CPU load if you
forgot to call `client.disconnect() forgot to call `client.disconnect()
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`. <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`.
The method is called for you on object destruction, but you still should The method is called for you on object destruction, but you still should
@ -1373,7 +1373,7 @@ Enhancements
- ``pathlib.Path`` is now supported for downloading and uploading media. - ``pathlib.Path`` is now supported for downloading and uploading media.
- Messages you send to yourself are now considered outgoing, unless they - Messages you send to yourself are now considered outgoing, unless they
are forwarded. are forwarded.
- The documentation has been updated with a brand new ``asyncio`` crash - The documentation has been updated with a brand new `asyncio` crash
course to encourage you use it. You can still use the threaded version course to encourage you use it. You can still use the threaded version
if you want though. if you want though.
- ``.name`` property is now properly supported when sending and downloading - ``.name`` property is now properly supported when sending and downloading

View File

@ -4,7 +4,7 @@
Compatibility and Convenience Compatibility and Convenience
============================= =============================
Telethon is an ``asyncio`` library. Compatibility is an important concern, Telethon is an `asyncio` library. Compatibility is an important concern,
and while it can't always be kept and mistakes happens, the :ref:`changelog` and while it can't always be kept and mistakes happens, the :ref:`changelog`
is there to tell you when these important changes happen. is there to tell you when these important changes happen.
@ -16,7 +16,7 @@ Compatibility
Some decisions when developing will inevitable be proven wrong in the future. Some decisions when developing will inevitable be proven wrong in the future.
One of these decisions was using threads. Now that Python 3.4 is reaching EOL One of these decisions was using threads. Now that Python 3.4 is reaching EOL
and using ``asyncio`` is usable as of Python 3.5 it makes sense for a library and using `asyncio` is usable as of Python 3.5 it makes sense for a library
like Telethon to make a good use of it. like Telethon to make a good use of it.
If you have old code, **just use old versions** of the library! There is If you have old code, **just use old versions** of the library! There is
@ -34,7 +34,7 @@ and clean-ups. Using an older version is the right way to go.
Sometimes, other small decisions are made. These all will be reflected in the Sometimes, other small decisions are made. These all will be reflected in the
:ref:`changelog` which you should read when upgrading. :ref:`changelog` which you should read when upgrading.
If you want to jump the ``asyncio`` boat, here are some of the things you will If you want to jump the `asyncio` boat, here are some of the things you will
need to start migrating really old code: need to start migrating really old code:
.. code-block:: python .. code-block:: python
@ -91,7 +91,7 @@ Convenience
This makes the examples shorter and easier to think about. This makes the examples shorter and easier to think about.
For quick scripts that don't need updates, it's a lot more convenient to For quick scripts that don't need updates, it's a lot more convenient to
forget about ``asyncio`` and just work with sequential code. This can prove forget about `asyncio` and just work with sequential code. This can prove
to be a powerful hybrid for running under the Python REPL too. to be a powerful hybrid for running under the Python REPL too.
.. code-block:: python .. code-block:: python
@ -178,10 +178,10 @@ overhead you pay if you import it, and what you save if you don't.
Learning Learning
======== ========
You know the library uses ``asyncio`` everywhere, and you want to learn You know the library uses `asyncio` everywhere, and you want to learn
how to do things right. Even though ``asyncio`` is its own topic, the how to do things right. Even though `asyncio` is its own topic, the
documentation wants you to learn how to use Telethon correctly, and for documentation wants you to learn how to use Telethon correctly, and for
that, you need to use ``asyncio`` correctly too. For this reason, there that, you need to use `asyncio` correctly too. For this reason, there
is a section called :ref:`mastering-asyncio` that will introduce you to is a section called :ref:`mastering-asyncio` that will introduce you to
the ``asyncio`` world, with links to more resources for learning how to the `asyncio` world, with links to more resources for learning how to
use it. Feel free to check that section out once you have read the rest. use it. Feel free to check that section out once you have read the rest.

View File

@ -19,34 +19,79 @@ its **attributes** (the properties will be shown here).
.. contents:: .. contents::
CallbackQuery NewMessage
==========
Occurs whenever a new text message or a message with media arrives.
.. note::
The new message event **should be treated as** a
normal `Message <telethon.tl.custom.message.Message>`, with
the following exceptions:
* ``pattern_match`` is the match object returned by ``pattern=``.
* ``message`` is **not** the message string. It's the `Message
<telethon.tl.custom.message.Message>` object.
Remember, this event is just a proxy over the message, so while
you won't see its attributes and properties, you can still access
them. Please see the full documentation for examples.
Full documentation for the `NewMessage
<telethon.events.newmessage.NewMessage>`.
MessageEdited
============= =============
Full documentation for the `CallbackQuery Occurs whenever a message is edited. Just like `NewMessage
<telethon.events.callbackquery.CallbackQuery>`. <telethon.events.newmessage.NewMessage>`, you should treat
this event as a `Message <telethon.tl.custom.message.Message>`.
.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event Full documentation for the `MessageEdited
<telethon.events.messageedited.MessageEdited>`.
MessageDeleted
==============
Occurs whenever a message is deleted. Note that this event isn't 100%
reliable, since Telegram doesn't always notify the clients that a message
was deleted.
It only has the ``deleted_id`` and ``deleted_ids`` attributes
(in addition to the chat if the deletion happened in a channel).
Full documentation for the `MessageDeleted
<telethon.events.messagedeleted.MessageDeleted>`.
MessageRead
===========
Occurs whenever one or more messages are read in a chat.
Full documentation for the `MessageRead
<telethon.events.messageread.MessageRead>`.
.. currentmodule:: telethon.events.messageread.MessageRead.Event
.. autosummary:: .. autosummary::
:nosignatures: :nosignatures:
id inbox
message_id message_ids
data
chat_instance
via_inline
respond get_messages
reply is_read
edit
delete
answer
get_message
ChatAction ChatAction
========== ==========
Occurs whenever a user joins or leaves a chat, or a message is pinned.
Full documentation for the `ChatAction Full documentation for the `ChatAction
<telethon.events.chataction.ChatAction>`. <telethon.events.chataction.ChatAction>`.
@ -76,9 +121,63 @@ Full documentation for the `ChatAction
get_input_users get_input_users
UserUpdate
==========
Occurs whenever a user goes online, starts typing, etc.
A lot of fields are attributes and not properties, so they
are not shown here. Please refer to its full documentation.
Full documentation for the `UserUpdate
<telethon.events.userupdate.UserUpdate>`.
.. currentmodule:: telethon.events.userupdate.UserUpdate.Event
.. autosummary::
:nosignatures:
user
input_user
user_id
get_user
get_input_user
CallbackQuery
=============
Occurs whenever you sign in as a bot and a user
clicks one of the inline buttons on your messages.
Full documentation for the `CallbackQuery
<telethon.events.callbackquery.CallbackQuery>`.
.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event
.. autosummary::
:nosignatures:
id
message_id
data
chat_instance
via_inline
respond
reply
edit
delete
answer
get_message
InlineQuery InlineQuery
=========== ===========
Occurs whenever you sign in as a bot and a user
sends an inline query such as ``@bot query``.
Full documentation for the `InlineQuery Full documentation for the `InlineQuery
<telethon.events.inlinequery.InlineQuery>`. <telethon.events.inlinequery.InlineQuery>`.
@ -95,90 +194,9 @@ Full documentation for the `InlineQuery
answer answer
MessageDeleted
==============
Full documentation for the `MessageDeleted
<telethon.events.messagedeleted.MessageDeleted>`.
It only has the ``deleted_id`` and ``deleted_ids`` attributes
(in addition to the chat if the deletion happened in a channel).
MessageEdited
=============
Full documentation for the `MessageEdited
<telethon.events.messageedited.MessageEdited>`.
This event is the same as `NewMessage
<telethon.events.newmessage.NewMessage>`,
but occurs only when an edit happens.
MessageRead
===========
Full documentation for the `MessageRead
<telethon.events.messageread.MessageRead>`.
.. currentmodule:: telethon.events.messageread.MessageRead.Event
.. autosummary::
:nosignatures:
inbox
message_ids
get_messages
is_read
NewMessage
==========
Full documentation for the `NewMessage
<telethon.events.newmessage.NewMessage>`.
Note that the new message event **should be treated as** a
normal `Message <telethon.tl.custom.message.Message>`, with
the following exceptions:
* ``pattern_match`` is the match object returned by ``pattern=``.
* ``message`` is **not** the message string. It's the `Message
<telethon.tl.custom.message.Message>` object.
Remember, this event is just a proxy over the message, so while
you won't see its attributes and properties, you can still access
them.
Raw Raw
=== ===
Raw events are not actual events. Instead, they are the raw Raw events are not actual events. Instead, they are the raw
:tl:`Update` object that Telegram sends. You normally shouldn't :tl:`Update` object that Telegram sends. You normally shouldn't
need these. need these.
UserUpdate
==========
Full documentation for the `UserUpdate
<telethon.events.userupdate.UserUpdate>`.
A lot of fields are attributes and not properties, so they
are not shown here.
.. currentmodule:: telethon.events.userupdate.UserUpdate.Event
.. autosummary::
:nosignatures:
user
input_user
user_id
get_user
get_input_user

View File

@ -249,7 +249,8 @@ InlineResult
The `InlineResult <telethon.tl.custom.inlineresult.InlineResult>` object The `InlineResult <telethon.tl.custom.inlineresult.InlineResult>` object
is returned inside a list by the `client.inline_query() is returned inside a list by the `client.inline_query()
<telethon.client.bots.BotMethods.inline_query>` method to make an inline <telethon.client.bots.BotMethods.inline_query>` method to make an inline
query to a bot that supports being used in inline mode, such as ``@like``. query to a bot that supports being used in inline mode, such as
`@like <https://t.me/like>`_.
Note that the list returned is in fact a *subclass* of a list called Note that the list returned is in fact a *subclass* of a list called
`InlineResults <telethon.tl.custom.inlineresults.InlineResults>`, which, `InlineResults <telethon.tl.custom.inlineresults.InlineResults>`, which,

View File

@ -282,11 +282,12 @@ class AuthMethods(MessageParseMethods, UserMethods):
password (`str`): password (`str`):
2FA password, should be used if a previous call raised 2FA password, should be used if a previous call raised
SessionPasswordNeededError. ``SessionPasswordNeededError``.
bot_token (`str`): bot_token (`str`):
Used to sign in as a bot. Not all requests will be available. Used to sign in as a bot. Not all requests will be available.
This should be the hash the @BotFather gave you. This should be the hash the `@BotFather <https://t.me/BotFather>`_
gave you.
phone (`str` | `int`): phone (`str` | `int`):
By default, the library remembers the phone passed to By default, the library remembers the phone passed to

View File

@ -15,7 +15,7 @@ if typing.TYPE_CHECKING:
class _DialogsIter(RequestIter): class _DialogsIter(RequestIter):
async def _init( async def _init(
self, offset_date, offset_id, offset_peer, ignore_migrated, folder self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder
): ):
self.request = functions.messages.GetDialogsRequest( self.request = functions.messages.GetDialogsRequest(
offset_date=offset_date, offset_date=offset_date,
@ -23,6 +23,7 @@ class _DialogsIter(RequestIter):
offset_peer=offset_peer, offset_peer=offset_peer,
limit=1, limit=1,
hash=0, hash=0,
exclude_pinned=ignore_pinned,
folder_id=folder folder_id=folder
) )
@ -76,15 +77,19 @@ class _DialogsIter(RequestIter):
# we didn't get a DialogsSlice which means we got all. # we didn't get a DialogsSlice which means we got all.
return True return True
# Don't set `request.offset_id` to the last message ID. # We can't use `messages[-1]` as the offset ID / date.
# Why? It seems to cause plenty of dialogs to be skipped. # Why? Because pinned dialogs will mess with the order
# # in this list. Instead, we find the last dialog which
# By leaving it to 0 after the first iteration, even if # has a message, and use it as an offset.
# the user originally passed another ID, we ensure that last_message = next((
# it will work correctly. messages[d.top_message]
self.request.offset_id = 0 for d in reversed(r.dialogs)
if d.top_message in messages
), None)
self.request.exclude_pinned = True self.request.exclude_pinned = True
self.request.offset_date = r.messages[-1].date self.request.offset_id = last_message.id if last_message else 0
self.request.offset_date = last_message.date if last_message else None
self.request.offset_peer =\ self.request.offset_peer =\
entities[utils.get_peer_id(r.dialogs[-1].peer)] entities[utils.get_peer_id(r.dialogs[-1].peer)]
@ -110,6 +115,7 @@ class DialogMethods(UserMethods):
offset_date: 'hints.DateLike' = None, offset_date: 'hints.DateLike' = None,
offset_id: int = 0, offset_id: int = 0,
offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(), offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),
ignore_pinned: bool = False,
ignore_migrated: bool = False, ignore_migrated: bool = False,
folder: int = None, folder: int = None,
archived: bool = None archived: bool = None
@ -134,11 +140,15 @@ class DialogMethods(UserMethods):
offset_peer (:tl:`InputPeer`, optional): offset_peer (:tl:`InputPeer`, optional):
The peer to be used as an offset. The peer to be used as an offset.
ignore_pinned (`bool`, optional):
Whether pinned dialogs should be ignored or not.
When set to ``True``, these won't be yielded at all.
ignore_migrated (`bool`, optional): ignore_migrated (`bool`, optional):
Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel` Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel`
should be included or not. By default all the chats in your should be included or not. By default all the chats in your
dialogs are returned, but setting this to ``True`` will hide dialogs are returned, but setting this to ``True`` will ignore
them in the same way official applications do. (i.e. skip) them in the same way official applications do.
folder (`int`, optional): folder (`int`, optional):
The folder from which the dialogs should be retrieved. The folder from which the dialogs should be retrieved.
@ -178,6 +188,7 @@ class DialogMethods(UserMethods):
offset_date=offset_date, offset_date=offset_date,
offset_id=offset_id, offset_id=offset_id,
offset_peer=offset_peer, offset_peer=offset_peer,
ignore_pinned=ignore_pinned,
ignore_migrated=ignore_migrated, ignore_migrated=ignore_migrated,
folder=folder folder=folder
) )

View File

@ -1,5 +1,6 @@
import hashlib import hashlib
import io import io
import itertools
import os import os
import pathlib import pathlib
import re import re
@ -78,9 +79,9 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods):
async def send_file( async def send_file(
self: 'TelegramClient', self: 'TelegramClient',
entity: 'hints.EntityLike', entity: 'hints.EntityLike',
file: 'hints.FileLike', file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]',
*, *,
caption: str = None, caption: typing.Union[str, typing.Sequence[str]] = None,
force_document: bool = False, force_document: bool = False,
progress_callback: 'hints.ProgressCallback' = None, progress_callback: 'hints.ProgressCallback' = None,
reply_to: 'hints.MessageIDLike' = None, reply_to: 'hints.MessageIDLike' = None,
@ -244,31 +245,41 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods):
# First check if the user passed an iterable, in which case # First check if the user passed an iterable, in which case
# we may want to send as an album if all are photo files. # we may want to send as an album if all are photo files.
if utils.is_list_like(file): if utils.is_list_like(file):
image_captions = []
document_captions = []
if utils.is_list_like(caption):
captions = caption
else:
captions = [caption]
# TODO Fix progress_callback # TODO Fix progress_callback
images = [] images = []
if force_document: if force_document:
documents = file documents = file
else: else:
documents = [] documents = []
for x in file: for doc, cap in itertools.zip_longest(file, captions):
if utils.is_image(x): if utils.is_image(doc):
images.append(x) images.append(doc)
image_captions.append(cap)
else: else:
documents.append(x) documents.append(doc)
document_captions.append(cap)
result = [] result = []
while images: while images:
result += await self._send_album( result += await self._send_album(
entity, images[:10], caption=caption, entity, images[:10], caption=image_captions[:10],
progress_callback=progress_callback, reply_to=reply_to, progress_callback=progress_callback, reply_to=reply_to,
parse_mode=parse_mode, silent=silent parse_mode=parse_mode, silent=silent
) )
images = images[10:] images = images[10:]
image_captions = image_captions[10:]
for x in documents: for doc, cap in zip(documents, captions):
result.append(await self.send_file( result.append(await self.send_file(
entity, x, entity, doc,
caption=caption, force_document=force_document, caption=cap, force_document=force_document,
progress_callback=progress_callback, reply_to=reply_to, progress_callback=progress_callback, reply_to=reply_to,
attributes=attributes, thumb=thumb, voice_note=voice_note, attributes=attributes, thumb=thumb, voice_note=voice_note,
video_note=video_note, buttons=buttons, silent=silent, video_note=video_note, buttons=buttons, silent=silent,

View File

@ -3,13 +3,28 @@ Helper module around the system's libssl library if available for IGE mode.
""" """
import ctypes import ctypes
import ctypes.util import ctypes.util
try:
import ctypes.macholib.dyld
except ImportError:
pass
import logging import logging
import os import os
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
def _find_ssl_lib():
lib = ctypes.util.find_library('ssl') lib = ctypes.util.find_library('ssl')
if not lib:
raise OSError('no library called "ssl" found')
# First, let ctypes try to handle it itself.
try:
libssl = ctypes.cdll.LoadLibrary(lib)
except OSError:
pass
else:
return libssl
# This is a best-effort attempt at finding the full real path of lib. # This is a best-effort attempt at finding the full real path of lib.
# #
@ -17,9 +32,8 @@ lib = ctypes.util.find_library('ssl')
# so we have to do that ourselves. # so we have to do that ourselves.
try: try:
# This is not documented, so it could fail. Be on the safe side. # This is not documented, so it could fail. Be on the safe side.
import ctypes.macholib.dyld
paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK
except (ImportError, AttributeError): except AttributeError:
paths = [ paths = [
os.path.expanduser("~/lib"), os.path.expanduser("~/lib"),
"/usr/local/lib", "/usr/local/lib",
@ -34,18 +48,17 @@ for path in paths:
# Manually follow symbolic links on *nix systems. # Manually follow symbolic links on *nix systems.
# Fix for https://github.com/LonamiWebs/Telethon/issues/1167 # Fix for https://github.com/LonamiWebs/Telethon/issues/1167
lib = os.path.realpath(os.path.join(root, lib)) lib = os.path.realpath(os.path.join(root, lib))
break return ctypes.cdll.LoadLibrary(lib)
else:
raise OSError('no absolute path for "%s" and cannot load by name' % lib)
try: try:
if not lib: _libssl = _find_ssl_lib()
raise OSError('no library called "ssl" found')
_libssl = ctypes.cdll.LoadLibrary(lib)
except OSError as e: except OSError as e:
# See https://github.com/LonamiWebs/Telethon/issues/1167 # See https://github.com/LonamiWebs/Telethon/issues/1167
# Sometimes `find_library` returns improper filenames. # Sometimes `find_library` returns improper filenames.
__log__.info('Failed to load %s: %s (%s)', lib, type(e), e) __log__.info('Failed to load SSL library: %s (%s)', type(e), e)
_libssl = None _libssl = None
if not _libssl: if not _libssl:

View File

@ -10,7 +10,8 @@ from ..tl.custom.sendergetter import SenderGetter
@name_inner_event @name_inner_event
class CallbackQuery(EventBuilder): class CallbackQuery(EventBuilder):
""" """
Represents a callback query event (when an inline button is clicked). Occurs whenever you sign in as a bot and a user
clicks one of the inline buttons on your messages.
Note that the `chats` parameter will **not** work with normal Note that the `chats` parameter will **not** work with normal
IDs or peers if the clicked inline button comes from a "via bot" IDs or peers if the clicked inline button comes from a "via bot"

View File

@ -6,7 +6,7 @@ from ..tl import types, functions
@name_inner_event @name_inner_event
class ChatAction(EventBuilder): class ChatAction(EventBuilder):
""" """
Represents an action in a chat (such as user joined, left, or new pin). Occurs whenever a user joins or leaves a chat, or a message is pinned.
""" """
@classmethod @classmethod
def build(cls, update): def build(cls, update):

View File

@ -12,7 +12,8 @@ from ..tl.custom.sendergetter import SenderGetter
@name_inner_event @name_inner_event
class InlineQuery(EventBuilder): class InlineQuery(EventBuilder):
""" """
Represents an inline query event (when someone writes ``'@my_bot query'``). Occurs whenever you sign in as a bot and a user
sends an inline query such as ``@bot query``.
Args: Args:
users (`entity`, optional): users (`entity`, optional):

View File

@ -5,7 +5,9 @@ from ..tl import types
@name_inner_event @name_inner_event
class MessageDeleted(EventBuilder): class MessageDeleted(EventBuilder):
""" """
Event fired when one or more messages are deleted. Occurs whenever a message is deleted. Note that this event isn't 100%
reliable, since Telegram doesn't always notify the clients that a message
was deleted.
.. important:: .. important::

View File

@ -6,7 +6,9 @@ from ..tl import types
@name_inner_event @name_inner_event
class MessageEdited(NewMessage): class MessageEdited(NewMessage):
""" """
Event fired when a message has been edited. Occurs whenever a message is edited. Just like `NewMessage
<telethon.events.newmessage.NewMessage>`, you should treat
this event as a `Message <telethon.tl.custom.message.Message>`.
.. warning:: .. warning::

View File

@ -6,7 +6,7 @@ from ..tl import types
@name_inner_event @name_inner_event
class MessageRead(EventBuilder): class MessageRead(EventBuilder):
""" """
Event fired when one or more messages have been read. Occurs whenever one or more messages are read in a chat.
Args: Args:
inbox (`bool`, optional): inbox (`bool`, optional):

View File

@ -8,7 +8,7 @@ from ..tl import types
@name_inner_event @name_inner_event
class NewMessage(EventBuilder): class NewMessage(EventBuilder):
""" """
Represents a new message event builder. Occurs whenever a new text message or a message with media arrives.
Args: Args:
incoming (`bool`, optional): incoming (`bool`, optional):

View File

@ -4,7 +4,9 @@ from .. import utils
class Raw(EventBuilder): class Raw(EventBuilder):
""" """
Represents a raw event. The event is the update itself. Raw events are not actual events. Instead, they are the raw
:tl:`Update` object that Telegram sends. You normally shouldn't
need these.
Args: Args:
types (`list` | `tuple` | `type`, optional): types (`list` | `tuple` | `type`, optional):

View File

@ -9,7 +9,7 @@ from ..tl.custom.sendergetter import SenderGetter
@name_inner_event @name_inner_event
class UserUpdate(EventBuilder): class UserUpdate(EventBuilder):
""" """
Represents a user update (gone online, offline, joined Telegram). Occurs whenever a user goes online, starts typing, etc.
""" """
@classmethod @classmethod
def build(cls, update): def build(cls, update):
@ -32,7 +32,8 @@ class UserUpdate(EventBuilder):
class Event(EventCommon, SenderGetter): class Event(EventCommon, SenderGetter):
""" """
Represents the event of a user status update (last seen, joined). Represents the event of a user update
such as gone online, started typing, etc.
Members: Members:
online (`bool`, optional): online (`bool`, optional):

View File

@ -544,6 +544,12 @@ class MTProtoSender:
await self._process_message(message) await self._process_message(message)
async def _handle_update(self, message): async def _handle_update(self, message):
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)
return
self._log.debug('Handling update %s', message.obj.__class__.__name__) self._log.debug('Handling update %s', message.obj.__class__.__name__)
if self._update_callback: if self._update_callback:
self._update_callback(message.obj) self._update_callback(message.obj)

View File

@ -285,11 +285,12 @@ class Conversation(ChatGetter):
return await self._get_result(future, start_time, timeout, self._custom, counter) return await self._get_result(future, start_time, timeout, self._custom, counter)
async def _check_custom(self, built): async def _check_custom(self, built):
for i, (ev, fut) in self._custom.items(): for key, (ev, fut) in list(self._custom.items()):
ev_type = type(ev) ev_type = type(ev)
inst = built[ev_type] inst = built[ev_type]
if inst and ev.filter(inst): if inst and ev.filter(inst):
fut.set_result(inst) fut.set_result(inst)
del self._custom[key]
def _on_new_message(self, response): def _on_new_message(self, response):
response = response.message response = response.message
@ -302,22 +303,28 @@ class Conversation(ChatGetter):
self._incoming.append(response) self._incoming.append(response)
# Note: we don't remove from pending here, that's done on get result # Most of the time, these dictionaries will contain just one item
for msg_id, future in self._pending_responses.items(): # TODO In fact, why not make it be that way? Force one item only.
# How often will people want to wait for two responses at
# the same time? It's impossible, first one will arrive
# and then another, so they can do that.
for msg_id, future in list(self._pending_responses.items()):
self._response_indices[msg_id] = len(self._incoming) self._response_indices[msg_id] = len(self._incoming)
future.set_result(response) future.set_result(response)
del self._pending_responses[msg_id]
for msg_id, future in self._pending_replies.items(): for msg_id, future in list(self._pending_replies.items()):
if msg_id == response.reply_to_msg_id: if msg_id == response.reply_to_msg_id:
self._reply_indices[msg_id] = len(self._incoming) self._reply_indices[msg_id] = len(self._incoming)
future.set_result(response) future.set_result(response)
del self._pending_replies[msg_id]
def _on_edit(self, message): def _on_edit(self, message):
message = message.message message = message.message
if message.chat_id != self.chat_id or message.out: if message.chat_id != self.chat_id or message.out:
return return
for msg_id, future in self._pending_edits.items(): for msg_id, future in list(self._pending_edits.items()):
if msg_id < message.id: if msg_id < message.id:
edit_ts = message.edit_date.timestamp() edit_ts = message.edit_date.timestamp()
@ -330,6 +337,7 @@ class Conversation(ChatGetter):
self._edit_dates[msg_id] = message.edit_date.timestamp() self._edit_dates[msg_id] = message.edit_date.timestamp()
future.set_result(message) future.set_result(message)
del self._pending_edits[msg_id]
def _on_read(self, event): def _on_read(self, event):
if event.chat_id != self.chat_id or event.inbox: if event.chat_id != self.chat_id or event.inbox:
@ -338,10 +346,11 @@ class Conversation(ChatGetter):
self._last_read = event.max_id self._last_read = event.max_id
remove_reads = [] remove_reads = []
for msg_id, pending in self._pending_reads.items(): for msg_id, pending in list(self._pending_reads.items()):
if msg_id >= self._last_read: if msg_id >= self._last_read:
remove_reads.append(msg_id) remove_reads.append(msg_id)
pending.set_result(True) pending.set_result(True)
del self._pending_reads[msg_id]
for to_remove in remove_reads: for to_remove in remove_reads:
del self._pending_reads[to_remove] del self._pending_reads[to_remove]
@ -365,14 +374,16 @@ class Conversation(ChatGetter):
if timeout is not None: if timeout is not None:
due = min(due, start_time + timeout) due = min(due, start_time + timeout)
try: # NOTE: We can't try/finally to pop from pending here because
# the event loop needs to get back to us, but it might
# dispatch another update before, and in that case a
# response could be set twice. So responses must be
# cleared when their futures are set to a result.
return await asyncio.wait_for( return await asyncio.wait_for(
future, future,
timeout=None if due == float('inf') else due - time.time(), timeout=None if due == float('inf') else due - time.time(),
loop=self._client.loop loop=self._client.loop
) )
finally:
del pending[target_id]
def _cancel_all(self, exception=None): def _cancel_all(self, exception=None):
self._cancelled = True self._cancelled = True

View File

@ -855,6 +855,16 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC):
else: else:
return await self._buttons[i][j].click() return await self._buttons[i][j].click()
async def mark_read(self):
"""
Marks the message as read. Shorthand for
`client.send_read_acknowledge()
<telethon.client.messages.MessageMethods.send_read_acknowledge>`
with both ``entity`` and ``message`` already set.
"""
await self._client.send_read_acknowledge(
await self.get_input_chat(), max_id=self.id)
async def pin(self, *, notify=False): async def pin(self, *, notify=False):
""" """
Pins the message. Shorthand for Pins the message. Shorthand for

View File

@ -106,8 +106,12 @@ def get_extension(media):
"""Gets the corresponding extension for any Telegram media.""" """Gets the corresponding extension for any Telegram media."""
# Photos are always compressed as .jpg by Telegram # Photos are always compressed as .jpg by Telegram
if isinstance(media, (types.UserProfilePhoto, try:
types.ChatPhoto, types.MessageMediaPhoto)): get_input_photo(media)
return '.jpg'
except TypeError:
# These cases are not handled by input photo because it can't
if isinstance(media, (types.UserProfilePhoto, types.ChatPhoto)):
return '.jpg' return '.jpg'
# Documents will come with a mime type # Documents will come with a mime type
@ -290,7 +294,10 @@ def get_input_photo(photo):
except AttributeError: except AttributeError:
_raise_cast_fail(photo, 'InputPhoto') _raise_cast_fail(photo, 'InputPhoto')
if isinstance(photo, types.photos.Photo): if isinstance(photo, types.Message):
photo = photo.media
if isinstance(photo, (types.photos.Photo, types.MessageMediaPhoto)):
photo = photo.photo photo = photo.photo
if isinstance(photo, types.Photo): if isinstance(photo, types.Photo):
@ -302,6 +309,7 @@ def get_input_photo(photo):
if isinstance(photo, types.messages.ChatFull): if isinstance(photo, types.messages.ChatFull):
photo = photo.full_chat photo = photo.full_chat
if isinstance(photo, types.ChannelFull): if isinstance(photo, types.ChannelFull):
return get_input_photo(photo.chat_photo) return get_input_photo(photo.chat_photo)
elif isinstance(photo, types.UserFull): elif isinstance(photo, types.UserFull):
@ -678,7 +686,8 @@ def _get_extension(file):
# Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase`
return _get_extension(file.name) return _get_extension(file.name)
else: else:
return '' # Maybe it's a Telegram media
return get_extension(file)
def is_image(file): def is_image(file):

View File

@ -171,8 +171,22 @@ function updateSearch(event) {
var foundTypes = getSearchArray(types, typesu, query); var foundTypes = getSearchArray(types, typesu, query);
var foundConstructors = getSearchArray(constructors, constructorsu, query); var foundConstructors = getSearchArray(constructors, constructorsu, query);
var original = requests.concat(constructors);
var originalu = requestsu.concat(constructorsu);
var destination = [];
var destinationu = [];
for (var i = 0; i < original.length; ++i) {
if (original[i].toLowerCase().replace("request", "") == query) {
destination.push(original[i]);
destinationu.push(originalu[i]);
}
}
if (event && event.keyCode == 13) { if (event && event.keyCode == 13) {
if (methodsDetails.open && foundRequests[1].length) { if (destination.length != 0) {
window.location = destinationu[0];
} else if (methodsDetails.open && foundRequests[1].length) {
window.location = foundRequests[1][0]; window.location = foundRequests[1][0];
} else if (typesDetails.open && foundTypes[1].length) { } else if (typesDetails.open && foundTypes[1].length) {
window.location = foundTypes[1][0]; window.location = foundTypes[1][0];
@ -187,18 +201,6 @@ function updateSearch(event) {
buildList(constructorsCount, constructorsList, foundConstructors); buildList(constructorsCount, constructorsList, foundConstructors);
// Now look for exact matches // Now look for exact matches
var original = requests.concat(constructors);
var originalu = requestsu.concat(constructorsu);
var destination = [];
var destinationu = [];
for (var i = 0; i < original.length; ++i) {
if (original[i].toLowerCase().replace("request", "") == query) {
destination.push(original[i]);
destinationu.push(originalu[i]);
}
}
if (destination.length == 0) { if (destination.length == 0) {
exactMatch.style.display = "none"; exactMatch.style.display = "none";
} else { } else {