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('