diff --git a/docs/generate.py b/docs/generate.py index ae2bd43c..75ab3091 100755 --- a/docs/generate.py +++ b/docs/generate.py @@ -207,6 +207,13 @@ def get_description(arg): desc.append('This argument can be omitted.') otherwise = True + if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}: + desc.append( + 'Anything entity-like will work if the library can find its ' + 'Input version (e.g., usernames, Peer, ' + 'User or Channel objects, etc.).' + ) + if arg.is_vector: if arg.is_generic: desc.append('A list of other Requests must be supplied.') @@ -221,7 +228,11 @@ def get_description(arg): desc.insert(1, 'Otherwise,') desc[-1] = desc[-1][:1].lower() + desc[-1][1:] - return ' '.join(desc) + return ' '.join(desc).replace( + 'list', + 'list' + ) def copy_replace(src, dst, replacements): diff --git a/docs/res/css/docs.css b/docs/res/css/docs.css index 05c61c9f..cd67af70 100644 --- a/docs/res/css/docs.css +++ b/docs/res/css/docs.css @@ -108,6 +108,10 @@ span.sh4 { color: #06c; } +span.tooltip { + border-bottom: 1px dashed #444; +} + #searchBox { width: 100%; border: none; diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index bf565bb0..e68f170b 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -95,6 +95,11 @@ is just a matter of taste, and how much control you need. Remember that you can get yourself at any time with ``client.get_me()``. +.. warning:: + Please note that if you fail to login around 5 times (or change the first + parameter of the ``TelegramClient``, which is the session name) you will + receive a ``FloodWaitError`` of around 22 hours, so be careful not to mess + this up! This shouldn't happen if you're doing things as explained, though. .. note:: If you want to use a **proxy**, you have to `install PySocks`__ diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 57b11bec..34609615 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,41 @@ it can take advantage of new goodies! .. contents:: List of All Versions +New small convenience functions (v0.17.2) +========================================= + +*Published at 2018/02/15* + +Primarily bug fixing and a few welcomed additions. + +Additions +~~~~~~~~~ + +- New convenience ``.edit_message()`` method on the ``TelegramClient``. +- New ``.edit()`` and ``.delete()`` shorthands on the ``NewMessage`` event. +- Default to markdown parsing when sending and editing messages. +- Support for inline mentions when sending and editing messages. They work + like inline urls (e.g. ``[text](@username)``) and also support the Bot-API + style (see `here `__). + +Bug fixes +~~~~~~~~~ + +- Periodically send ``GetStateRequest`` automatically to keep the server + sending updates even if you're not invoking any request yourself. +- HTML parsing was failing due to not handling surrogates properly. +- ``.sign_up`` was not accepting ``int`` codes. +- Whitelisting more than one chat on ``events`` wasn't working. +- Video files are sent as a video by default unless ``force_document``. + +Internal changes +~~~~~~~~~~~~~~~~ + +- More ``logging`` calls to help spot some bugs in the future. +- Some more logic to retrieve input entities on events. +- Clarified a few parts of the documentation. + + Updates as Events (v0.17.1) =========================== diff --git a/telethon/crypto/aes.py b/telethon/crypto/aes.py index 191cde15..8f13b5f0 100644 --- a/telethon/crypto/aes.py +++ b/telethon/crypto/aes.py @@ -3,86 +3,90 @@ AES IGE implementation in Python. This module may use libssl if available. """ import os import pyaes -from . import libssl + +try: + import cryptg +except ImportError: + cryptg = None -if libssl.AES is not None: - # Use libssl if available, since it will be faster - AES = libssl.AES -else: - # Fallback to a pure Python implementation - class AES: +class AES: + """ + Class that servers as an interface to encrypt and decrypt + text through the AES IGE mode. + """ + @staticmethod + def decrypt_ige(cipher_text, key, iv): """ - Class that servers as an interface to encrypt and decrypt - text through the AES IGE mode. + Decrypts the given text in 16-bytes blocks by using the + given key and 32-bytes initialization vector. """ - @staticmethod - def decrypt_ige(cipher_text, key, iv): - """ - Decrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - iv1 = iv[:len(iv) // 2] - iv2 = iv[len(iv) // 2:] + if cryptg: + return cryptg.decrypt_ige(cipher_text, key, iv) - aes = pyaes.AES(key) + iv1 = iv[:len(iv) // 2] + iv2 = iv[len(iv) // 2:] - plain_text = [] - blocks_count = len(cipher_text) // 16 + aes = pyaes.AES(key) - cipher_text_block = [0] * 16 - for block_index in range(blocks_count): - for i in range(16): - cipher_text_block[i] = \ - cipher_text[block_index * 16 + i] ^ iv2[i] + plain_text = [] + blocks_count = len(cipher_text) // 16 - plain_text_block = aes.decrypt(cipher_text_block) + cipher_text_block = [0] * 16 + for block_index in range(blocks_count): + for i in range(16): + cipher_text_block[i] = \ + cipher_text[block_index * 16 + i] ^ iv2[i] - for i in range(16): - plain_text_block[i] ^= iv1[i] + plain_text_block = aes.decrypt(cipher_text_block) - iv1 = cipher_text[block_index * 16:block_index * 16 + 16] - iv2 = plain_text_block + for i in range(16): + plain_text_block[i] ^= iv1[i] - plain_text.extend(plain_text_block) + iv1 = cipher_text[block_index * 16:block_index * 16 + 16] + iv2 = plain_text_block - return bytes(plain_text) + plain_text.extend(plain_text_block) - @staticmethod - def encrypt_ige(plain_text, key, iv): - """ - Encrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ + return bytes(plain_text) - # Add random padding iff it's not evenly divisible by 16 already - if len(plain_text) % 16 != 0: - padding_count = 16 - len(plain_text) % 16 - plain_text += os.urandom(padding_count) + @staticmethod + def encrypt_ige(plain_text, key, iv): + """ + Encrypts the given text in 16-bytes blocks by using the + given key and 32-bytes initialization vector. + """ + # Add random padding iff it's not evenly divisible by 16 already + if len(plain_text) % 16 != 0: + padding_count = 16 - len(plain_text) % 16 + plain_text += os.urandom(padding_count) - iv1 = iv[:len(iv) // 2] - iv2 = iv[len(iv) // 2:] + if cryptg: + return cryptg.encrypt_ige(plain_text, key, iv) - aes = pyaes.AES(key) + iv1 = iv[:len(iv) // 2] + iv2 = iv[len(iv) // 2:] - cipher_text = [] - blocks_count = len(plain_text) // 16 + aes = pyaes.AES(key) - for block_index in range(blocks_count): - plain_text_block = list( - plain_text[block_index * 16:block_index * 16 + 16] - ) - for i in range(16): - plain_text_block[i] ^= iv1[i] + cipher_text = [] + blocks_count = len(plain_text) // 16 - cipher_text_block = aes.encrypt(plain_text_block) + for block_index in range(blocks_count): + plain_text_block = list( + plain_text[block_index * 16:block_index * 16 + 16] + ) + for i in range(16): + plain_text_block[i] ^= iv1[i] - for i in range(16): - cipher_text_block[i] ^= iv2[i] + cipher_text_block = aes.encrypt(plain_text_block) - iv1 = cipher_text_block - iv2 = plain_text[block_index * 16:block_index * 16 + 16] + for i in range(16): + cipher_text_block[i] ^= iv2[i] - cipher_text.extend(cipher_text_block) + iv1 = cipher_text_block + iv2 = plain_text[block_index * 16:block_index * 16 + 16] - return bytes(cipher_text) + cipher_text.extend(cipher_text_block) + + return bytes(cipher_text) diff --git a/telethon/crypto/libssl.py b/telethon/crypto/libssl.py deleted file mode 100644 index b4735112..00000000 --- a/telethon/crypto/libssl.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -This module holds an AES IGE class, if libssl is available on the system. -""" -import os -import ctypes -from ctypes.util import find_library - -lib = find_library('ssl') -if not lib: - AES = None -else: - """ - # define AES_ENCRYPT 1 - # define AES_DECRYPT 0 - # define AES_MAXNR 14 - struct aes_key_st { - # ifdef AES_LONG - unsigned long rd_key[4 * (AES_MAXNR + 1)]; - # else - unsigned int rd_key[4 * (AES_MAXNR + 1)]; - # endif - int rounds; - }; - typedef struct aes_key_st AES_KEY; - - int AES_set_encrypt_key(const unsigned char *userKey, const int bits, - AES_KEY *key); - int AES_set_decrypt_key(const unsigned char *userKey, const int bits, - AES_KEY *key); - void AES_ige_encrypt(const unsigned char *in, unsigned char *out, - size_t length, const AES_KEY *key, - unsigned char *ivec, const int enc); - """ - _libssl = ctypes.cdll.LoadLibrary(lib) - - AES_MAXNR = 14 - AES_ENCRYPT = ctypes.c_int(1) - AES_DECRYPT = ctypes.c_int(0) - - class AES_KEY(ctypes.Structure): - """Helper class representing an AES key""" - _fields_ = [ - ('rd_key', ctypes.c_uint32 * (4*(AES_MAXNR + 1))), - ('rounds', ctypes.c_uint), - ] - - class AES: - """ - Class that servers as an interface to encrypt and decrypt - text through the AES IGE mode, using the system's libssl. - """ - @staticmethod - def decrypt_ige(cipher_text, key, iv): - """ - Decrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - aeskey = AES_KEY() - ckey = (ctypes.c_ubyte * len(key))(*key) - cklen = ctypes.c_int(len(key)*8) - cin = (ctypes.c_ubyte * len(cipher_text))(*cipher_text) - ctlen = ctypes.c_size_t(len(cipher_text)) - cout = (ctypes.c_ubyte * len(cipher_text))() - civ = (ctypes.c_ubyte * len(iv))(*iv) - - _libssl.AES_set_decrypt_key(ckey, cklen, ctypes.byref(aeskey)) - _libssl.AES_ige_encrypt( - ctypes.byref(cin), - ctypes.byref(cout), - ctlen, - ctypes.byref(aeskey), - ctypes.byref(civ), - AES_DECRYPT - ) - - return bytes(cout) - - @staticmethod - def encrypt_ige(plain_text, key, iv): - """ - Encrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - # Add random padding iff it's not evenly divisible by 16 already - if len(plain_text) % 16 != 0: - padding_count = 16 - len(plain_text) % 16 - plain_text += os.urandom(padding_count) - - aeskey = AES_KEY() - ckey = (ctypes.c_ubyte * len(key))(*key) - cklen = ctypes.c_int(len(key)*8) - cin = (ctypes.c_ubyte * len(plain_text))(*plain_text) - ctlen = ctypes.c_size_t(len(plain_text)) - cout = (ctypes.c_ubyte * len(plain_text))() - civ = (ctypes.c_ubyte * len(iv))(*iv) - - _libssl.AES_set_encrypt_key(ckey, cklen, ctypes.byref(aeskey)) - _libssl.AES_ige_encrypt( - ctypes.byref(cin), - ctypes.byref(cout), - ctlen, - ctypes.byref(aeskey), - ctypes.byref(civ), - AES_ENCRYPT - ) - - return bytes(cout) diff --git a/telethon/errors/common.py b/telethon/errors/common.py index 46b0b52e..0c03aee6 100644 --- a/telethon/errors/common.py +++ b/telethon/errors/common.py @@ -4,7 +4,7 @@ class ReadCancelledError(Exception): """Occurs when a read operation was cancelled.""" def __init__(self): - super().__init__(self, 'The read operation was cancelled.') + super().__init__('The read operation was cancelled.') class TypeNotFoundError(Exception): @@ -14,7 +14,7 @@ class TypeNotFoundError(Exception): """ def __init__(self, invalid_constructor_id): super().__init__( - self, 'Could not find a matching Constructor ID for the TLObject ' + 'Could not find a matching Constructor ID for the TLObject ' 'that was supposed to be read with ID {}. Most likely, a TLObject ' 'was trying to be read when it should not be read.' .format(hex(invalid_constructor_id))) @@ -29,7 +29,6 @@ class InvalidChecksumError(Exception): """ def __init__(self, checksum, valid_checksum): super().__init__( - self, 'Invalid checksum ({} when {} was expected). ' 'This packet should be skipped.' .format(checksum, valid_checksum)) @@ -44,7 +43,6 @@ class BrokenAuthKeyError(Exception): """ def __init__(self): super().__init__( - self, 'The authorization key is broken, and it must be reset.' ) @@ -56,7 +54,7 @@ class SecurityError(Exception): def __init__(self, *args): if not args: args = ['A security check failed.'] - super().__init__(self, *args) + super().__init__(*args) class CdnFileTamperedError(SecurityError): diff --git a/telethon/errors/rpc_base_errors.py b/telethon/errors/rpc_base_errors.py index 9e6eed1a..467b256c 100644 --- a/telethon/errors/rpc_base_errors.py +++ b/telethon/errors/rpc_base_errors.py @@ -40,7 +40,7 @@ class ForbiddenError(RPCError): message = 'FORBIDDEN' def __init__(self, message): - super().__init__(self, message) + super().__init__(message) self.message = message @@ -52,7 +52,7 @@ class NotFoundError(RPCError): message = 'NOT_FOUND' def __init__(self, message): - super().__init__(self, message) + super().__init__(message) self.message = message @@ -77,7 +77,7 @@ class ServerError(RPCError): message = 'INTERNAL' def __init__(self, message): - super().__init__(self, message) + super().__init__(message) self.message = message @@ -121,7 +121,7 @@ class BadMessageError(Exception): } def __init__(self, code): - super().__init__(self, self.ErrorMessages.get( + super().__init__(self.ErrorMessages.get( code, 'Unknown error code (this should not happen): {}.'.format(code))) diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 5bc08679..0af94658 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -136,6 +136,11 @@ class NewMessage(_EventBuilder): blacklist_chats (:obj:`bool`, optional): Whether to treat the the list of chats as a blacklist (if it matches it will NOT be handled) or a whitelist (default). + + Notes: + The ``message.from_id`` might not only be an integer or ``None``, + but also ``InputPeerSelf()`` for short private messages (the API + would not return such thing, this is a custom modification). """ def __init__(self, incoming=None, outgoing=None, chats=None, blacklist_chats=False): @@ -149,8 +154,8 @@ class NewMessage(_EventBuilder): async def resolve(self, client): if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(x) - for x in await client.get_input_entity(self.chats)) + self.chats = set(utils.get_peer_id(await client.get_input_entity(x)) + for x in self.chats) elif self.chats is not None: self.chats = {utils.get_peer_id( await client.get_input_entity(self.chats))} @@ -169,6 +174,7 @@ class NewMessage(_EventBuilder): silent=update.silent, id=update.id, to_id=types.PeerUser(update.user_id), + from_id=types.InputPeerSelf() if update.out else update.user_id, message=update.message, date=update.date, fwd_from=update.fwd_from, @@ -249,6 +255,32 @@ class NewMessage(_EventBuilder): reply_to=self.message.id, *args, **kwargs) + async def edit(self, *args, **kwargs): + """ + Edits the message iff it's outgoing. This is a shorthand for + ``client.edit_message(event.chat, event.message, ...)``. + + Returns ``None`` if the message was incoming, + or the edited message otherwise. + """ + if not self.message.out: + return None + + return await self._client.edit_message(self.input_chat, + self.message, + *args, **kwargs) + + async def delete(self, *args, **kwargs): + """ + Deletes the message. You're responsible for checking whether you + have the permission to do so, or to except the error otherwise. + This is a shorthand for + ``client.delete_messages(event.chat, event.message, ...)``. + """ + return await self._client.delete_messages(self.input_chat, + [self.message], + *args, **kwargs) + @property async def input_sender(self): """ @@ -257,21 +289,23 @@ class NewMessage(_EventBuilder): things like username or similar, but still useful in some cases. Note that this might not be available if the library can't - find the input chat. + find the input chat, or if the message a broadcast on a channel. """ if self._input_sender is None: + if self.is_channel and not self.is_group: + return None + try: self._input_sender = await self._client.get_input_entity( self.message.from_id ) except (ValueError, TypeError): - if isinstance(self.message.to_id, types.PeerChannel): - # We can rely on self.input_chat for this - self._input_sender = await self._get_input_entity( - self.message.id, - self.message.from_id, - chat=await self.input_chat - ) + # We can rely on self.input_chat for this + self._input_sender = await self._get_input_entity( + self.message.id, + self.message.from_id, + chat=await self.input_chat + ) return self._input_sender @@ -397,8 +431,8 @@ class ChatAction(_EventBuilder): async def resolve(self, client): if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str): - self.chats = set(utils.get_peer_id(x) - for x in await client.get_input_entity(self.chats)) + self.chats = set(utils.get_peer_id(await client.get_input_entity(x)) + for x in self.chats) elif self.chats is not None: self.chats = {utils.get_peer_id( await client.get_input_entity(self.chats))} @@ -835,22 +869,24 @@ class MessageChanged(_EventBuilder): things like username or similar, but still useful in some cases. Note that this might not be available if the library can't - find the input chat. + find the input chat, or if the message a broadcast on a channel. """ # TODO Code duplication - if self._input_sender is None and self.message: + if self._input_sender is None: + if self.is_channel and not self.is_group: + return None + try: self._input_sender = await self._client.get_input_entity( self.message.from_id ) except (ValueError, TypeError): - if isinstance(self.message.to_id, types.PeerChannel): - # We can rely on self.input_chat for this - self._input_sender = await self._get_input_entity( - self.message.id, - self.message.from_id, - chat=await self.input_chat - ) + # We can rely on self.input_chat for this + self._input_sender = await self._get_input_entity( + self.message.id, + self.message.from_id, + chat=await self.input_chat + ) return self._input_sender diff --git a/telethon/extensions/html.py b/telethon/extensions/html.py index 8cd170cb..bcbd13cc 100644 --- a/telethon/extensions/html.py +++ b/telethon/extensions/html.py @@ -1,9 +1,10 @@ """ Simple HTML -> Telegram entity parser. """ +import struct +from collections import deque from html import escape, unescape from html.parser import HTMLParser -from collections import deque from ..tl.types import ( MessageEntityBold, MessageEntityItalic, MessageEntityCode, @@ -12,6 +13,18 @@ from ..tl.types import ( ) +# Helpers from markdown.py +def _add_surrogate(text): + return ''.join( + ''.join(chr(y) for y in struct.unpack('