diff --git a/readthedocs/basic/installation.rst b/readthedocs/basic/installation.rst index f5941df1..b9c8b399 100644 --- a/readthedocs/basic/installation.rst +++ b/readthedocs/basic/installation.rst @@ -85,7 +85,7 @@ manually. 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 .. _pillow: https://python-pillow.org .. _aiohttp: https://docs.aiohttp.org diff --git a/readthedocs/basic/signing-in.rst b/readthedocs/basic/signing-in.rst index 46184955..e0c6e6db 100644 --- a/readthedocs/basic/signing-in.rst +++ b/readthedocs/basic/signing-in.rst @@ -140,3 +140,54 @@ where the following keys are allowed: } .. __: 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') + ) diff --git a/readthedocs/basic/updates.rst b/readthedocs/basic/updates.rst index 7e3a5e71..6e22ae64 100644 --- a/readthedocs/basic/updates.rst +++ b/readthedocs/basic/updates.rst @@ -79,7 +79,7 @@ with a ``'hi!'`` message. .. note:: 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. 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 like message objects, so you can do with them everything you can do with a message object. - -.. _asyncio: https://docs.python.org/3/library/asyncio.html diff --git a/readthedocs/concepts/asyncio.rst b/readthedocs/concepts/asyncio.rst index 182a5d6f..8500e55e 100644 --- a/readthedocs/concepts/asyncio.rst +++ b/readthedocs/concepts/asyncio.rst @@ -10,11 +10,11 @@ Mastering 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 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 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. 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 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. 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 :ref:`compatibility-and-convenience`. @@ -254,7 +254,7 @@ client.run_until_disconnected() blocks! All of what `client.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 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. You don't need to use `client.run_until_disconnected() ` either! -You just need to make the loop is running, somehow. ``asyncio.run_forever`` -and ``asyncio.run_until_complete`` can also be used to run the loop, and -Telethon will be happy with any approach. +You just need to make the loop is running, somehow. `loop.run_forever() +` and `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. @@ -285,7 +286,7 @@ What else can asyncio do? ========================= 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. * `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 `_, send one to `@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. A different way would be: @@ -355,8 +356,6 @@ Where can I read more? ====================== `Check out my blog post -`_ about asyncio_, which +`_ about `asyncio`, which has some more examples and pictures to help you understand what happens when the loop runs. - -.. _asyncio: https://docs.python.org/3/library/asyncio.html diff --git a/readthedocs/concepts/updates.rst b/readthedocs/concepts/updates.rst index aec75094..a5b978de 100644 --- a/readthedocs/concepts/updates.rst +++ b/readthedocs/concepts/updates.rst @@ -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, and a third one is used to handle updates. 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 `: diff --git a/readthedocs/conf.py b/readthedocs/conf.py index f7de1d4b..f4841dd5 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -40,9 +40,14 @@ tl_ref_url = 'https://tl.telethon.dev' extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', 'custom_roles' ] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None) +} + # Change the default role so we can avoid prefixing everything with :obj: default_role = "py:obj" diff --git a/readthedocs/examples/chats-and-channels.rst b/readthedocs/examples/chats-and-channels.rst index 98bfdc07..fa485c59 100644 --- a/readthedocs/examples/chats-and-channels.rst +++ b/readthedocs/examples/chats-and-channels.rst @@ -195,11 +195,11 @@ banned rights of a user through :tl:`EditBannedRequest` and its parameter client(EditBannedRequest(channel, user, rights)) -You can also use a ``datetime`` object for ``until_date=``, or even a -Unix timestamp. Note that if you ban someone for less than 30 seconds -or for more than 366 days, Telegram will consider the ban to actually -last forever. This is officially documented under -https://core.telegram.org/bots/api#restrictchatmember. +You can use a `datetime.datetime` object for ``until_date=``, +a `datetime.timedelta` or even a Unix timestamp. Note that if you ban +someone for less than 30 seconds or for more than 366 days, Telegram +will consider the ban to actually last forever. This is officially +documented under https://core.telegram.org/bots/api#restrictchatmember. Kicking a member diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index 9a3b6074..44fc2b4b 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -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 methods in the :ref:`TelegramClient ` (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: .. code-block:: python @@ -1238,7 +1238,7 @@ Bug fixes - "User joined" event was being treated as "User was invited". - SQLite's cursor should not be closed properly after usage. - ``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() `. 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. - Messages you send to yourself are now considered outgoing, unless they 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 if you want though. - ``.name`` property is now properly supported when sending and downloading diff --git a/readthedocs/misc/compatibility-and-convenience.rst b/readthedocs/misc/compatibility-and-convenience.rst index 9d6e8257..2d0769d4 100644 --- a/readthedocs/misc/compatibility-and-convenience.rst +++ b/readthedocs/misc/compatibility-and-convenience.rst @@ -4,7 +4,7 @@ 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` 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. 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. 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 :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: .. code-block:: python @@ -91,7 +91,7 @@ Convenience 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 -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. .. code-block:: python @@ -178,10 +178,10 @@ overhead you pay if you import it, and what you save if you don't. Learning ======== -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 +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 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 -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. diff --git a/readthedocs/quick-references/events-reference.rst b/readthedocs/quick-references/events-reference.rst index ead94a15..cba224cd 100644 --- a/readthedocs/quick-references/events-reference.rst +++ b/readthedocs/quick-references/events-reference.rst @@ -19,34 +19,79 @@ its **attributes** (the properties will be shown here). .. 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 `, with + the following exceptions: + + * ``pattern_match`` is the match object returned by ``pattern=``. + * ``message`` is **not** the message string. It's the `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 +`. + + +MessageEdited ============= -Full documentation for the `CallbackQuery -`. +Occurs whenever a message is edited. Just like `NewMessage +`, you should treat +this event as a `Message `. -.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event +Full documentation for the `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 +`. + + +MessageRead +=========== + +Occurs whenever one or more messages are read in a chat. + +Full documentation for the `MessageRead +`. + +.. currentmodule:: telethon.events.messageread.MessageRead.Event .. autosummary:: :nosignatures: - id - message_id - data - chat_instance - via_inline + inbox + message_ids - respond - reply - edit - delete - answer - get_message + get_messages + is_read ChatAction ========== +Occurs whenever a user joins or leaves a chat, or a message is pinned. + Full documentation for the `ChatAction `. @@ -76,9 +121,63 @@ Full documentation for the `ChatAction 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 +`. + +.. 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 +`. + +.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event + +.. autosummary:: + :nosignatures: + + id + message_id + data + chat_instance + via_inline + + respond + reply + edit + delete + answer + get_message + 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 `. @@ -95,90 +194,9 @@ Full documentation for the `InlineQuery answer - -MessageDeleted -============== - -Full documentation for the `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 -`. - -This event is the same as `NewMessage -`, -but occurs only when an edit happens. - - -MessageRead -=========== - -Full documentation for the `MessageRead -`. - -.. currentmodule:: telethon.events.messageread.MessageRead.Event - -.. autosummary:: - :nosignatures: - - inbox - message_ids - - get_messages - is_read - - -NewMessage -========== - -Full documentation for the `NewMessage -`. - -Note that the new message event **should be treated as** a -normal `Message `, with -the following exceptions: - -* ``pattern_match`` is the match object returned by ``pattern=``. -* ``message`` is **not** the message string. It's the `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 events are not actual events. Instead, they are the raw :tl:`Update` object that Telegram sends. You normally shouldn't need these. - - -UserUpdate -========== - -Full documentation for the `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 diff --git a/readthedocs/quick-references/objects-reference.rst b/readthedocs/quick-references/objects-reference.rst index 4a09ff74..c8b2e876 100644 --- a/readthedocs/quick-references/objects-reference.rst +++ b/readthedocs/quick-references/objects-reference.rst @@ -249,7 +249,8 @@ InlineResult The `InlineResult ` object is returned inside a list by the `client.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 `_. Note that the list returned is in fact a *subclass* of a list called `InlineResults `, which, diff --git a/telethon/client/auth.py b/telethon/client/auth.py index 18670c79..9dd9ae37 100644 --- a/telethon/client/auth.py +++ b/telethon/client/auth.py @@ -282,11 +282,12 @@ class AuthMethods(MessageParseMethods, UserMethods): password (`str`): 2FA password, should be used if a previous call raised - SessionPasswordNeededError. + ``SessionPasswordNeededError``. bot_token (`str`): 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 `_ + gave you. phone (`str` | `int`): By default, the library remembers the phone passed to diff --git a/telethon/client/dialogs.py b/telethon/client/dialogs.py index d6552e16..75f59fef 100644 --- a/telethon/client/dialogs.py +++ b/telethon/client/dialogs.py @@ -15,7 +15,7 @@ if typing.TYPE_CHECKING: class _DialogsIter(RequestIter): 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( offset_date=offset_date, @@ -23,6 +23,7 @@ class _DialogsIter(RequestIter): offset_peer=offset_peer, limit=1, hash=0, + exclude_pinned=ignore_pinned, folder_id=folder ) @@ -76,15 +77,19 @@ class _DialogsIter(RequestIter): # we didn't get a DialogsSlice which means we got all. return True - # Don't set `request.offset_id` to the last message ID. - # Why? It seems to cause plenty of dialogs to be skipped. - # - # By leaving it to 0 after the first iteration, even if - # the user originally passed another ID, we ensure that - # it will work correctly. - self.request.offset_id = 0 + # We can't use `messages[-1]` as the offset ID / date. + # Why? Because pinned dialogs will mess with the order + # in this list. Instead, we find the last dialog which + # has a message, and use it as an offset. + last_message = next(( + messages[d.top_message] + for d in reversed(r.dialogs) + if d.top_message in messages + ), None) + 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 =\ entities[utils.get_peer_id(r.dialogs[-1].peer)] @@ -110,6 +115,7 @@ class DialogMethods(UserMethods): offset_date: 'hints.DateLike' = None, offset_id: int = 0, offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(), + ignore_pinned: bool = False, ignore_migrated: bool = False, folder: int = None, archived: bool = None @@ -134,11 +140,15 @@ class DialogMethods(UserMethods): offset_peer (:tl:`InputPeer`, optional): 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): Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel` should be included or not. By default all the chats in your - dialogs are returned, but setting this to ``True`` will hide - them in the same way official applications do. + dialogs are returned, but setting this to ``True`` will ignore + (i.e. skip) them in the same way official applications do. folder (`int`, optional): The folder from which the dialogs should be retrieved. @@ -178,6 +188,7 @@ class DialogMethods(UserMethods): offset_date=offset_date, offset_id=offset_id, offset_peer=offset_peer, + ignore_pinned=ignore_pinned, ignore_migrated=ignore_migrated, folder=folder ) diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py index 98596003..63048dc8 100644 --- a/telethon/client/uploads.py +++ b/telethon/client/uploads.py @@ -1,5 +1,6 @@ import hashlib import io +import itertools import os import pathlib import re @@ -78,9 +79,9 @@ class UploadMethods(ButtonMethods, MessageParseMethods, UserMethods): async def send_file( self: 'TelegramClient', 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, progress_callback: 'hints.ProgressCallback' = 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 # we may want to send as an album if all are photo files. 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 images = [] if force_document: documents = file else: documents = [] - for x in file: - if utils.is_image(x): - images.append(x) + for doc, cap in itertools.zip_longest(file, captions): + if utils.is_image(doc): + images.append(doc) + image_captions.append(cap) else: - documents.append(x) + documents.append(doc) + document_captions.append(cap) result = [] while images: 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, parse_mode=parse_mode, silent=silent ) 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( - entity, x, - caption=caption, force_document=force_document, + entity, doc, + 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, diff --git a/telethon/crypto/libssl.py b/telethon/crypto/libssl.py index c50a0c98..f1421e57 100644 --- a/telethon/crypto/libssl.py +++ b/telethon/crypto/libssl.py @@ -3,49 +3,62 @@ Helper module around the system's libssl library if available for IGE mode. """ import ctypes import ctypes.util +try: + import ctypes.macholib.dyld +except ImportError: + pass import logging import os __log__ = logging.getLogger(__name__) -lib = ctypes.util.find_library('ssl') - -# This is a best-effort attempt at finding the full real path of lib. -# -# Unfortunately ctypes doesn't tell us *where* it finds the library, -# so we have to do that ourselves. -try: - # This is not documented, so it could fail. Be on the safe side. - import ctypes.macholib.dyld - paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK -except (ImportError, AttributeError): - paths = [ - os.path.expanduser("~/lib"), - "/usr/local/lib", - "/lib", - "/usr/lib", - ] - -for path in paths: - if os.path.isdir(path): - for root, _, files in os.walk(path): - if lib in files: - # Manually follow symbolic links on *nix systems. - # Fix for https://github.com/LonamiWebs/Telethon/issues/1167 - lib = os.path.realpath(os.path.join(root, lib)) - break - - -try: +def _find_ssl_lib(): + lib = ctypes.util.find_library('ssl') if not lib: raise OSError('no library called "ssl" found') - _libssl = ctypes.cdll.LoadLibrary(lib) + # 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. + # + # Unfortunately ctypes doesn't tell us *where* it finds the library, + # so we have to do that ourselves. + try: + # This is not documented, so it could fail. Be on the safe side. + paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK + except AttributeError: + paths = [ + os.path.expanduser("~/lib"), + "/usr/local/lib", + "/lib", + "/usr/lib", + ] + + for path in paths: + if os.path.isdir(path): + for root, _, files in os.walk(path): + if lib in files: + # Manually follow symbolic links on *nix systems. + # Fix for https://github.com/LonamiWebs/Telethon/issues/1167 + lib = os.path.realpath(os.path.join(root, lib)) + return ctypes.cdll.LoadLibrary(lib) + else: + raise OSError('no absolute path for "%s" and cannot load by name' % lib) + + +try: + _libssl = _find_ssl_lib() except OSError as e: # See https://github.com/LonamiWebs/Telethon/issues/1167 # 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 if not _libssl: diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py index e33fdf8f..99f0d147 100644 --- a/telethon/events/callbackquery.py +++ b/telethon/events/callbackquery.py @@ -10,7 +10,8 @@ from ..tl.custom.sendergetter import SenderGetter @name_inner_event 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 IDs or peers if the clicked inline button comes from a "via bot" diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index 6359cad6..75734e05 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -6,7 +6,7 @@ from ..tl import types, functions @name_inner_event 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 def build(cls, update): diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py index 1ae3e00d..794ca857 100644 --- a/telethon/events/inlinequery.py +++ b/telethon/events/inlinequery.py @@ -12,7 +12,8 @@ from ..tl.custom.sendergetter import SenderGetter @name_inner_event 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: users (`entity`, optional): diff --git a/telethon/events/messagedeleted.py b/telethon/events/messagedeleted.py index 7312c163..dc86997d 100644 --- a/telethon/events/messagedeleted.py +++ b/telethon/events/messagedeleted.py @@ -5,7 +5,9 @@ from ..tl import types @name_inner_event 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:: diff --git a/telethon/events/messageedited.py b/telethon/events/messageedited.py index 6a8138a0..83a71dd6 100644 --- a/telethon/events/messageedited.py +++ b/telethon/events/messageedited.py @@ -6,7 +6,9 @@ from ..tl import types @name_inner_event class MessageEdited(NewMessage): """ - Event fired when a message has been edited. + Occurs whenever a message is edited. Just like `NewMessage + `, you should treat + this event as a `Message `. .. warning:: diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py index 1b782db1..00ac6073 100644 --- a/telethon/events/messageread.py +++ b/telethon/events/messageread.py @@ -6,7 +6,7 @@ from ..tl import types @name_inner_event 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: inbox (`bool`, optional): diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 62d5deee..c1396f72 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -8,7 +8,7 @@ from ..tl import types @name_inner_event class NewMessage(EventBuilder): """ - Represents a new message event builder. + Occurs whenever a new text message or a message with media arrives. Args: incoming (`bool`, optional): diff --git a/telethon/events/raw.py b/telethon/events/raw.py index 4f25e769..cd4c80a9 100644 --- a/telethon/events/raw.py +++ b/telethon/events/raw.py @@ -4,7 +4,9 @@ from .. import utils 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: types (`list` | `tuple` | `type`, optional): diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py index 119af3d9..742c8034 100644 --- a/telethon/events/userupdate.py +++ b/telethon/events/userupdate.py @@ -9,7 +9,7 @@ from ..tl.custom.sendergetter import SenderGetter @name_inner_event class UserUpdate(EventBuilder): """ - Represents a user update (gone online, offline, joined Telegram). + Occurs whenever a user goes online, starts typing, etc. """ @classmethod def build(cls, update): @@ -32,7 +32,8 @@ class UserUpdate(EventBuilder): 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: online (`bool`, optional): diff --git a/telethon/network/mtprotosender.py b/telethon/network/mtprotosender.py index 5464f48c..d9aafad6 100644 --- a/telethon/network/mtprotosender.py +++ b/telethon/network/mtprotosender.py @@ -544,6 +544,12 @@ class MTProtoSender: await self._process_message(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__) if self._update_callback: self._update_callback(message.obj) diff --git a/telethon/tl/custom/conversation.py b/telethon/tl/custom/conversation.py index 9df1bf58..5ff92bb6 100644 --- a/telethon/tl/custom/conversation.py +++ b/telethon/tl/custom/conversation.py @@ -285,11 +285,12 @@ class Conversation(ChatGetter): return await self._get_result(future, start_time, timeout, self._custom, counter) 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) inst = built[ev_type] if inst and ev.filter(inst): fut.set_result(inst) + del self._custom[key] def _on_new_message(self, response): response = response.message @@ -302,22 +303,28 @@ class Conversation(ChatGetter): self._incoming.append(response) - # Note: we don't remove from pending here, that's done on get result - for msg_id, future in self._pending_responses.items(): + # Most of the time, these dictionaries will contain just one item + # 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) 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: self._reply_indices[msg_id] = len(self._incoming) future.set_result(response) + del self._pending_replies[msg_id] def _on_edit(self, message): message = message.message if message.chat_id != self.chat_id or message.out: 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: edit_ts = message.edit_date.timestamp() @@ -330,6 +337,7 @@ class Conversation(ChatGetter): self._edit_dates[msg_id] = message.edit_date.timestamp() future.set_result(message) + del self._pending_edits[msg_id] def _on_read(self, event): if event.chat_id != self.chat_id or event.inbox: @@ -338,10 +346,11 @@ class Conversation(ChatGetter): self._last_read = event.max_id 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: remove_reads.append(msg_id) pending.set_result(True) + del self._pending_reads[msg_id] for to_remove in remove_reads: del self._pending_reads[to_remove] @@ -365,14 +374,16 @@ class Conversation(ChatGetter): if timeout is not None: due = min(due, start_time + timeout) - try: - return await asyncio.wait_for( - future, - timeout=None if due == float('inf') else due - time.time(), - loop=self._client.loop - ) - finally: - del pending[target_id] + # 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( + future, + timeout=None if due == float('inf') else due - time.time(), + loop=self._client.loop + ) def _cancel_all(self, exception=None): self._cancelled = True diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 373feec5..643c76f9 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -855,6 +855,16 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): else: return await self._buttons[i][j].click() + async def mark_read(self): + """ + Marks the message as read. Shorthand for + `client.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): """ Pins the message. Shorthand for diff --git a/telethon/utils.py b/telethon/utils.py index f7509227..7121602a 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -106,9 +106,13 @@ def get_extension(media): """Gets the corresponding extension for any Telegram media.""" # Photos are always compressed as .jpg by Telegram - if isinstance(media, (types.UserProfilePhoto, - types.ChatPhoto, types.MessageMediaPhoto)): + try: + 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' # Documents will come with a mime type if isinstance(media, types.MessageMediaDocument): @@ -290,7 +294,10 @@ def get_input_photo(photo): except AttributeError: _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 if isinstance(photo, types.Photo): @@ -302,6 +309,7 @@ def get_input_photo(photo): if isinstance(photo, types.messages.ChatFull): photo = photo.full_chat + if isinstance(photo, types.ChannelFull): return get_input_photo(photo.chat_photo) elif isinstance(photo, types.UserFull): @@ -678,7 +686,8 @@ def _get_extension(file): # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` return _get_extension(file.name) else: - return '' + # Maybe it's a Telegram media + return get_extension(file) def is_image(file): diff --git a/telethon_generator/data/html/js/search.js b/telethon_generator/data/html/js/search.js index cb739c07..a67ffabc 100644 --- a/telethon_generator/data/html/js/search.js +++ b/telethon_generator/data/html/js/search.js @@ -171,8 +171,22 @@ function updateSearch(event) { var foundTypes = getSearchArray(types, typesu, 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 (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]; } else if (typesDetails.open && foundTypes[1].length) { window.location = foundTypes[1][0]; @@ -187,18 +201,6 @@ function updateSearch(event) { buildList(constructorsCount, constructorsList, foundConstructors); // Now look for exact matches - var original = requests.concat(constructors); - var originalu = requestsu.concat(constructorsu); - var destination = []; - var destinationu = []; - - for (var i = 0; i < original.length; ++i) { - if (original[i].toLowerCase().replace("request", "") == query) { - destination.push(original[i]); - destinationu.push(originalu[i]); - } - } - if (destination.length == 0) { exactMatch.style.display = "none"; } else {