mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-05 20:50:22 +03:00
Merge branch 'master' into asyncio
This commit is contained in:
commit
6e854325a8
|
@ -207,6 +207,13 @@ def get_description(arg):
|
||||||
desc.append('This argument can be omitted.')
|
desc.append('This argument can be omitted.')
|
||||||
otherwise = True
|
otherwise = True
|
||||||
|
|
||||||
|
if arg.type in {'InputPeer', 'InputUser', 'InputChannel'}:
|
||||||
|
desc.append(
|
||||||
|
'Anything entity-like will work if the library can find its '
|
||||||
|
'<code>Input</code> version (e.g., usernames, <code>Peer</code>, '
|
||||||
|
'<code>User</code> or <code>Channel</code> objects, etc.).'
|
||||||
|
)
|
||||||
|
|
||||||
if arg.is_vector:
|
if arg.is_vector:
|
||||||
if arg.is_generic:
|
if arg.is_generic:
|
||||||
desc.append('A list of other Requests must be supplied.')
|
desc.append('A list of other Requests must be supplied.')
|
||||||
|
@ -221,7 +228,11 @@ def get_description(arg):
|
||||||
desc.insert(1, 'Otherwise,')
|
desc.insert(1, 'Otherwise,')
|
||||||
desc[-1] = desc[-1][:1].lower() + desc[-1][1:]
|
desc[-1] = desc[-1][:1].lower() + desc[-1][1:]
|
||||||
|
|
||||||
return ' '.join(desc)
|
return ' '.join(desc).replace(
|
||||||
|
'list',
|
||||||
|
'<span class="tooltip" title="Any iterable that supports len() '
|
||||||
|
'will work too">list</span>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def copy_replace(src, dst, replacements):
|
def copy_replace(src, dst, replacements):
|
||||||
|
|
|
@ -108,6 +108,10 @@ span.sh4 {
|
||||||
color: #06c;
|
color: #06c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.tooltip {
|
||||||
|
border-bottom: 1px dashed #444;
|
||||||
|
}
|
||||||
|
|
||||||
#searchBox {
|
#searchBox {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
@ -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()``.
|
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::
|
.. note::
|
||||||
If you want to use a **proxy**, you have to `install PySocks`__
|
If you want to use a **proxy**, you have to `install PySocks`__
|
||||||
|
|
|
@ -14,6 +14,41 @@ it can take advantage of new goodies!
|
||||||
.. contents:: List of All Versions
|
.. 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 <https://core.telegram.org/bots/api#formatting-options>`__).
|
||||||
|
|
||||||
|
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)
|
Updates as Events (v0.17.1)
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
|
|
|
@ -3,86 +3,90 @@ AES IGE implementation in Python. This module may use libssl if available.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import pyaes
|
import pyaes
|
||||||
from . import libssl
|
|
||||||
|
try:
|
||||||
|
import cryptg
|
||||||
|
except ImportError:
|
||||||
|
cryptg = None
|
||||||
|
|
||||||
|
|
||||||
if libssl.AES is not None:
|
class AES:
|
||||||
# Use libssl if available, since it will be faster
|
"""
|
||||||
AES = libssl.AES
|
Class that servers as an interface to encrypt and decrypt
|
||||||
else:
|
text through the AES IGE mode.
|
||||||
# Fallback to a pure Python implementation
|
"""
|
||||||
class AES:
|
@staticmethod
|
||||||
|
def decrypt_ige(cipher_text, key, iv):
|
||||||
"""
|
"""
|
||||||
Class that servers as an interface to encrypt and decrypt
|
Decrypts the given text in 16-bytes blocks by using the
|
||||||
text through the AES IGE mode.
|
given key and 32-bytes initialization vector.
|
||||||
"""
|
"""
|
||||||
@staticmethod
|
if cryptg:
|
||||||
def decrypt_ige(cipher_text, key, iv):
|
return cryptg.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:]
|
|
||||||
|
|
||||||
aes = pyaes.AES(key)
|
iv1 = iv[:len(iv) // 2]
|
||||||
|
iv2 = iv[len(iv) // 2:]
|
||||||
|
|
||||||
plain_text = []
|
aes = pyaes.AES(key)
|
||||||
blocks_count = len(cipher_text) // 16
|
|
||||||
|
|
||||||
cipher_text_block = [0] * 16
|
plain_text = []
|
||||||
for block_index in range(blocks_count):
|
blocks_count = len(cipher_text) // 16
|
||||||
for i in range(16):
|
|
||||||
cipher_text_block[i] = \
|
|
||||||
cipher_text[block_index * 16 + i] ^ iv2[i]
|
|
||||||
|
|
||||||
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 = aes.decrypt(cipher_text_block)
|
||||||
plain_text_block[i] ^= iv1[i]
|
|
||||||
|
|
||||||
iv1 = cipher_text[block_index * 16:block_index * 16 + 16]
|
for i in range(16):
|
||||||
iv2 = plain_text_block
|
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
|
return bytes(plain_text)
|
||||||
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
|
@staticmethod
|
||||||
if len(plain_text) % 16 != 0:
|
def encrypt_ige(plain_text, key, iv):
|
||||||
padding_count = 16 - len(plain_text) % 16
|
"""
|
||||||
plain_text += os.urandom(padding_count)
|
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]
|
if cryptg:
|
||||||
iv2 = iv[len(iv) // 2:]
|
return cryptg.encrypt_ige(plain_text, key, iv)
|
||||||
|
|
||||||
aes = pyaes.AES(key)
|
iv1 = iv[:len(iv) // 2]
|
||||||
|
iv2 = iv[len(iv) // 2:]
|
||||||
|
|
||||||
cipher_text = []
|
aes = pyaes.AES(key)
|
||||||
blocks_count = len(plain_text) // 16
|
|
||||||
|
|
||||||
for block_index in range(blocks_count):
|
cipher_text = []
|
||||||
plain_text_block = list(
|
blocks_count = len(plain_text) // 16
|
||||||
plain_text[block_index * 16:block_index * 16 + 16]
|
|
||||||
)
|
|
||||||
for i in range(16):
|
|
||||||
plain_text_block[i] ^= iv1[i]
|
|
||||||
|
|
||||||
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 = aes.encrypt(plain_text_block)
|
||||||
cipher_text_block[i] ^= iv2[i]
|
|
||||||
|
|
||||||
iv1 = cipher_text_block
|
for i in range(16):
|
||||||
iv2 = plain_text[block_index * 16:block_index * 16 + 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)
|
||||||
|
|
|
@ -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:
|
|
||||||
""" <aes.h>
|
|
||||||
# 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)
|
|
|
@ -4,7 +4,7 @@
|
||||||
class ReadCancelledError(Exception):
|
class ReadCancelledError(Exception):
|
||||||
"""Occurs when a read operation was cancelled."""
|
"""Occurs when a read operation was cancelled."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(self, 'The read operation was cancelled.')
|
super().__init__('The read operation was cancelled.')
|
||||||
|
|
||||||
|
|
||||||
class TypeNotFoundError(Exception):
|
class TypeNotFoundError(Exception):
|
||||||
|
@ -14,7 +14,7 @@ class TypeNotFoundError(Exception):
|
||||||
"""
|
"""
|
||||||
def __init__(self, invalid_constructor_id):
|
def __init__(self, invalid_constructor_id):
|
||||||
super().__init__(
|
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 '
|
'that was supposed to be read with ID {}. Most likely, a TLObject '
|
||||||
'was trying to be read when it should not be read.'
|
'was trying to be read when it should not be read.'
|
||||||
.format(hex(invalid_constructor_id)))
|
.format(hex(invalid_constructor_id)))
|
||||||
|
@ -29,7 +29,6 @@ class InvalidChecksumError(Exception):
|
||||||
"""
|
"""
|
||||||
def __init__(self, checksum, valid_checksum):
|
def __init__(self, checksum, valid_checksum):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
self,
|
|
||||||
'Invalid checksum ({} when {} was expected). '
|
'Invalid checksum ({} when {} was expected). '
|
||||||
'This packet should be skipped.'
|
'This packet should be skipped.'
|
||||||
.format(checksum, valid_checksum))
|
.format(checksum, valid_checksum))
|
||||||
|
@ -44,7 +43,6 @@ class BrokenAuthKeyError(Exception):
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
self,
|
|
||||||
'The authorization key is broken, and it must be reset.'
|
'The authorization key is broken, and it must be reset.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,7 +54,7 @@ class SecurityError(Exception):
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
if not args:
|
if not args:
|
||||||
args = ['A security check failed.']
|
args = ['A security check failed.']
|
||||||
super().__init__(self, *args)
|
super().__init__(*args)
|
||||||
|
|
||||||
|
|
||||||
class CdnFileTamperedError(SecurityError):
|
class CdnFileTamperedError(SecurityError):
|
||||||
|
|
|
@ -40,7 +40,7 @@ class ForbiddenError(RPCError):
|
||||||
message = 'FORBIDDEN'
|
message = 'FORBIDDEN'
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
super().__init__(self, message)
|
super().__init__(message)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class NotFoundError(RPCError):
|
||||||
message = 'NOT_FOUND'
|
message = 'NOT_FOUND'
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
super().__init__(self, message)
|
super().__init__(message)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class ServerError(RPCError):
|
||||||
message = 'INTERNAL'
|
message = 'INTERNAL'
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
super().__init__(self, message)
|
super().__init__(message)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ class BadMessageError(Exception):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, code):
|
def __init__(self, code):
|
||||||
super().__init__(self, self.ErrorMessages.get(
|
super().__init__(self.ErrorMessages.get(
|
||||||
code,
|
code,
|
||||||
'Unknown error code (this should not happen): {}.'.format(code)))
|
'Unknown error code (this should not happen): {}.'.format(code)))
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,11 @@ class NewMessage(_EventBuilder):
|
||||||
blacklist_chats (:obj:`bool`, optional):
|
blacklist_chats (:obj:`bool`, optional):
|
||||||
Whether to treat the the list of chats as a blacklist (if
|
Whether to treat the the list of chats as a blacklist (if
|
||||||
it matches it will NOT be handled) or a whitelist (default).
|
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,
|
def __init__(self, incoming=None, outgoing=None,
|
||||||
chats=None, blacklist_chats=False):
|
chats=None, blacklist_chats=False):
|
||||||
|
@ -149,8 +154,8 @@ class NewMessage(_EventBuilder):
|
||||||
|
|
||||||
async def resolve(self, client):
|
async def resolve(self, client):
|
||||||
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str):
|
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str):
|
||||||
self.chats = set(utils.get_peer_id(x)
|
self.chats = set(utils.get_peer_id(await client.get_input_entity(x))
|
||||||
for x in await client.get_input_entity(self.chats))
|
for x in self.chats)
|
||||||
elif self.chats is not None:
|
elif self.chats is not None:
|
||||||
self.chats = {utils.get_peer_id(
|
self.chats = {utils.get_peer_id(
|
||||||
await client.get_input_entity(self.chats))}
|
await client.get_input_entity(self.chats))}
|
||||||
|
@ -169,6 +174,7 @@ class NewMessage(_EventBuilder):
|
||||||
silent=update.silent,
|
silent=update.silent,
|
||||||
id=update.id,
|
id=update.id,
|
||||||
to_id=types.PeerUser(update.user_id),
|
to_id=types.PeerUser(update.user_id),
|
||||||
|
from_id=types.InputPeerSelf() if update.out else update.user_id,
|
||||||
message=update.message,
|
message=update.message,
|
||||||
date=update.date,
|
date=update.date,
|
||||||
fwd_from=update.fwd_from,
|
fwd_from=update.fwd_from,
|
||||||
|
@ -249,6 +255,32 @@ class NewMessage(_EventBuilder):
|
||||||
reply_to=self.message.id,
|
reply_to=self.message.id,
|
||||||
*args, **kwargs)
|
*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
|
@property
|
||||||
async def input_sender(self):
|
async def input_sender(self):
|
||||||
"""
|
"""
|
||||||
|
@ -257,21 +289,23 @@ class NewMessage(_EventBuilder):
|
||||||
things like username or similar, but still useful in some cases.
|
things like username or similar, but still useful in some cases.
|
||||||
|
|
||||||
Note that this might not be available if the library can't
|
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._input_sender is None:
|
||||||
|
if self.is_channel and not self.is_group:
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._input_sender = await self._client.get_input_entity(
|
self._input_sender = await self._client.get_input_entity(
|
||||||
self.message.from_id
|
self.message.from_id
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
if isinstance(self.message.to_id, types.PeerChannel):
|
# We can rely on self.input_chat for this
|
||||||
# We can rely on self.input_chat for this
|
self._input_sender = await self._get_input_entity(
|
||||||
self._input_sender = await self._get_input_entity(
|
self.message.id,
|
||||||
self.message.id,
|
self.message.from_id,
|
||||||
self.message.from_id,
|
chat=await self.input_chat
|
||||||
chat=await self.input_chat
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return self._input_sender
|
return self._input_sender
|
||||||
|
|
||||||
|
@ -397,8 +431,8 @@ class ChatAction(_EventBuilder):
|
||||||
|
|
||||||
async def resolve(self, client):
|
async def resolve(self, client):
|
||||||
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str):
|
if hasattr(self.chats, '__iter__') and not isinstance(self.chats, str):
|
||||||
self.chats = set(utils.get_peer_id(x)
|
self.chats = set(utils.get_peer_id(await client.get_input_entity(x))
|
||||||
for x in await client.get_input_entity(self.chats))
|
for x in self.chats)
|
||||||
elif self.chats is not None:
|
elif self.chats is not None:
|
||||||
self.chats = {utils.get_peer_id(
|
self.chats = {utils.get_peer_id(
|
||||||
await client.get_input_entity(self.chats))}
|
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.
|
things like username or similar, but still useful in some cases.
|
||||||
|
|
||||||
Note that this might not be available if the library can't
|
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
|
# 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:
|
try:
|
||||||
self._input_sender = await self._client.get_input_entity(
|
self._input_sender = await self._client.get_input_entity(
|
||||||
self.message.from_id
|
self.message.from_id
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
if isinstance(self.message.to_id, types.PeerChannel):
|
# We can rely on self.input_chat for this
|
||||||
# We can rely on self.input_chat for this
|
self._input_sender = await self._get_input_entity(
|
||||||
self._input_sender = await self._get_input_entity(
|
self.message.id,
|
||||||
self.message.id,
|
self.message.from_id,
|
||||||
self.message.from_id,
|
chat=await self.input_chat
|
||||||
chat=await self.input_chat
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return self._input_sender
|
return self._input_sender
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
"""
|
"""
|
||||||
Simple HTML -> Telegram entity parser.
|
Simple HTML -> Telegram entity parser.
|
||||||
"""
|
"""
|
||||||
|
import struct
|
||||||
|
from collections import deque
|
||||||
from html import escape, unescape
|
from html import escape, unescape
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
from ..tl.types import (
|
from ..tl.types import (
|
||||||
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
|
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('<HH', x.encode('utf-16le')))
|
||||||
|
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _del_surrogate(text):
|
||||||
|
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
|
||||||
|
|
||||||
|
|
||||||
class HTMLToTelegramParser(HTMLParser):
|
class HTMLToTelegramParser(HTMLParser):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -109,8 +122,8 @@ def parse(html):
|
||||||
:return: a tuple consisting of (clean message, [message entities]).
|
:return: a tuple consisting of (clean message, [message entities]).
|
||||||
"""
|
"""
|
||||||
parser = HTMLToTelegramParser()
|
parser = HTMLToTelegramParser()
|
||||||
parser.feed(html)
|
parser.feed(_add_surrogate(html))
|
||||||
return parser.text, parser.entities
|
return _del_surrogate(parser.text), parser.entities
|
||||||
|
|
||||||
|
|
||||||
def unparse(text, entities):
|
def unparse(text, entities):
|
||||||
|
@ -124,6 +137,8 @@ def unparse(text, entities):
|
||||||
"""
|
"""
|
||||||
if not entities:
|
if not entities:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
text = _add_surrogate(text)
|
||||||
html = []
|
html = []
|
||||||
last_offset = 0
|
last_offset = 0
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
|
@ -164,4 +179,4 @@ def unparse(text, entities):
|
||||||
skip_entity = True
|
skip_entity = True
|
||||||
last_offset = entity.offset + (0 if skip_entity else entity.length)
|
last_offset = entity.offset + (0 if skip_entity else entity.length)
|
||||||
html.append(text[last_offset:])
|
html.append(text[last_offset:])
|
||||||
return ''.join(html)
|
return _del_surrogate(''.join(html))
|
||||||
|
|
|
@ -152,6 +152,9 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
|
||||||
:param entities: the MessageEntity's applied to the text.
|
:param entities: the MessageEntity's applied to the text.
|
||||||
:return: a markdown-like text representing the combination of both inputs.
|
:return: a markdown-like text representing the combination of both inputs.
|
||||||
"""
|
"""
|
||||||
|
if not entities:
|
||||||
|
return text
|
||||||
|
|
||||||
if not delimiters:
|
if not delimiters:
|
||||||
if delimiters is not None:
|
if delimiters is not None:
|
||||||
return text
|
return text
|
||||||
|
|
|
@ -92,6 +92,11 @@ class MtProtoSender:
|
||||||
messages = [TLMessage(self.session, r) for r in requests]
|
messages = [TLMessage(self.session, r) for r in requests]
|
||||||
self._pending_receive.update({m.msg_id: m for m in messages})
|
self._pending_receive.update({m.msg_id: m for m in messages})
|
||||||
|
|
||||||
|
__log__.debug('Sending requests with IDs: %s', ', '.join(
|
||||||
|
'{}: {}'.format(m.request.__class__.__name__, m.msg_id)
|
||||||
|
for m in messages
|
||||||
|
))
|
||||||
|
|
||||||
if len(messages) == 1:
|
if len(messages) == 1:
|
||||||
message = messages[0]
|
message = messages[0]
|
||||||
else:
|
else:
|
||||||
|
@ -468,6 +473,7 @@ class MtProtoSender:
|
||||||
request_id = reader.read_long()
|
request_id = reader.read_long()
|
||||||
inner_code = reader.read_int(signed=False)
|
inner_code = reader.read_int(signed=False)
|
||||||
|
|
||||||
|
__log__.debug('Received response for request with ID %d', request_id)
|
||||||
request = self._pop_request(request_id)
|
request = self._pop_request(request_id)
|
||||||
|
|
||||||
if inner_code == 0x2144ca19: # RPC Error
|
if inner_code == 0x2144ca19: # RPC Error
|
||||||
|
@ -502,8 +508,18 @@ class MtProtoSender:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# If it's really a result for RPC from previous connection
|
# If it's really a result for RPC from previous connection
|
||||||
# session, it will be skipped by the handle_container()
|
# session, it will be skipped by the handle_container().
|
||||||
__log__.warning('Lost request will be skipped')
|
# For some reason this also seems to happen when downloading
|
||||||
|
# photos, where the server responds with FileJpeg().
|
||||||
|
try:
|
||||||
|
obj = reader.tgread_object()
|
||||||
|
except Exception as e:
|
||||||
|
obj = '(failed to read: %s)' % e
|
||||||
|
|
||||||
|
__log__.warning(
|
||||||
|
'Lost request (ID %d) with code %s will be skipped, contents: %s',
|
||||||
|
request_id, hex(inner_code), obj
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _handle_gzip_packed(self, msg_id, sequence, reader, state):
|
async def _handle_gzip_packed(self, msg_id, sequence, reader, state):
|
||||||
|
|
|
@ -101,8 +101,6 @@ class TelegramBareClient:
|
||||||
self.session = session
|
self.session = session
|
||||||
self.api_id = int(api_id)
|
self.api_id = int(api_id)
|
||||||
self.api_hash = api_hash
|
self.api_hash = api_hash
|
||||||
if self.api_id < 20: # official apps must use obfuscated
|
|
||||||
connection_mode = ConnectionMode.TCP_OBFUSCATED
|
|
||||||
|
|
||||||
# This is the main sender, which will be used from the thread
|
# This is the main sender, which will be used from the thread
|
||||||
# that calls .connect(). Every other thread will spawn a new
|
# that calls .connect(). Every other thread will spawn a new
|
||||||
|
@ -152,10 +150,17 @@ class TelegramBareClient:
|
||||||
|
|
||||||
self._recv_loop = None
|
self._recv_loop = None
|
||||||
self._ping_loop = None
|
self._ping_loop = None
|
||||||
|
self._state_loop = None
|
||||||
self._idling = asyncio.Event()
|
self._idling = asyncio.Event()
|
||||||
|
|
||||||
# Default PingRequest delay
|
# Default PingRequest delay
|
||||||
self._ping_delay = timedelta(minutes=1)
|
self._ping_delay = timedelta(minutes=1)
|
||||||
|
# Also have another delay for GetStateRequest.
|
||||||
|
#
|
||||||
|
# If the connection is kept alive for long without invoking any
|
||||||
|
# high level request the server simply stops sending updates.
|
||||||
|
# TODO maybe we can have ._last_request instead if any req works?
|
||||||
|
self._state_delay = timedelta(hours=1)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -570,6 +575,8 @@ class TelegramBareClient:
|
||||||
self._recv_loop = asyncio.ensure_future(self._recv_loop_impl(), loop=self._loop)
|
self._recv_loop = asyncio.ensure_future(self._recv_loop_impl(), loop=self._loop)
|
||||||
if self._ping_loop is None:
|
if self._ping_loop is None:
|
||||||
self._ping_loop = asyncio.ensure_future(self._ping_loop_impl(), loop=self._loop)
|
self._ping_loop = asyncio.ensure_future(self._ping_loop_impl(), loop=self._loop)
|
||||||
|
if self._state_loop is None:
|
||||||
|
self._state_loop = asyncio.ensure_future(self._state_loop_impl(), loop=self._loop)
|
||||||
|
|
||||||
async def _ping_loop_impl(self):
|
async def _ping_loop_impl(self):
|
||||||
while self._user_connected:
|
while self._user_connected:
|
||||||
|
@ -577,6 +584,11 @@ class TelegramBareClient:
|
||||||
await asyncio.sleep(self._ping_delay.seconds, loop=self._loop)
|
await asyncio.sleep(self._ping_delay.seconds, loop=self._loop)
|
||||||
self._ping_loop = None
|
self._ping_loop = None
|
||||||
|
|
||||||
|
async def _state_loop_impl(self):
|
||||||
|
while self._user_connected:
|
||||||
|
await asyncio.sleep(self._state_delay.seconds, loop=self._loop)
|
||||||
|
await self._sender.send(GetStateRequest())
|
||||||
|
|
||||||
async def _recv_loop_impl(self):
|
async def _recv_loop_impl(self):
|
||||||
__log__.info('Starting to wait for items from the network')
|
__log__.info('Starting to wait for items from the network')
|
||||||
self._idling.set()
|
self._idling.set()
|
||||||
|
|
|
@ -4,6 +4,7 @@ import io
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict, UserList
|
from collections import OrderedDict, UserList
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
@ -45,8 +46,8 @@ from .tl.functions.contacts import (
|
||||||
from .tl.functions.messages import (
|
from .tl.functions.messages import (
|
||||||
GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
|
GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
|
||||||
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
|
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
|
||||||
CheckChatInviteRequest, ReadMentionsRequest,
|
CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest,
|
||||||
SendMultiMediaRequest, UploadMediaRequest
|
UploadMediaRequest, EditMessageRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
from .tl.functions import channels
|
from .tl.functions import channels
|
||||||
|
@ -68,7 +69,9 @@ from .tl.types import (
|
||||||
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty,
|
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty,
|
||||||
ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf,
|
ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf,
|
||||||
InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig,
|
InputSingleMedia, InputMediaPhoto, InputPhoto, InputFile, InputFileBig,
|
||||||
InputDocument, InputMediaDocument, Document
|
InputDocument, InputMediaDocument, Document, MessageEntityTextUrl,
|
||||||
|
InputMessageEntityMentionName, DocumentAttributeVideo,
|
||||||
|
UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates
|
||||||
)
|
)
|
||||||
from .tl.types.messages import DialogsSlice
|
from .tl.types.messages import DialogsSlice
|
||||||
from .extensions import markdown, html
|
from .extensions import markdown, html
|
||||||
|
@ -414,7 +417,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
result = await self(SignUpRequest(
|
result = await self(SignUpRequest(
|
||||||
phone_number=self._phone,
|
phone_number=self._phone,
|
||||||
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
|
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
|
||||||
phone_code=code,
|
phone_code=str(code),
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
last_name=last_name
|
last_name=last_name
|
||||||
))
|
))
|
||||||
|
@ -561,13 +564,62 @@ class TelegramClient(TelegramBareClient):
|
||||||
msg_id = update.id
|
msg_id = update.id
|
||||||
break
|
break
|
||||||
|
|
||||||
for update in result.updates:
|
if isinstance(result, UpdateShort):
|
||||||
|
updates = [result.update]
|
||||||
|
elif isinstance(result, Updates):
|
||||||
|
updates = result.updates
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
for update in updates:
|
||||||
if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)):
|
if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)):
|
||||||
if update.message.id == msg_id:
|
if update.message.id == msg_id:
|
||||||
return update.message
|
return update.message
|
||||||
|
|
||||||
|
elif (isinstance(update, UpdateEditMessage) and
|
||||||
|
not isinstance(request.peer, InputPeerChannel)):
|
||||||
|
if request.id == update.message.id:
|
||||||
|
return update.message
|
||||||
|
|
||||||
|
elif (isinstance(update, UpdateEditChannelMessage) and
|
||||||
|
utils.get_peer_id(request.peer) ==
|
||||||
|
utils.get_peer_id(update.message.to_id)):
|
||||||
|
if request.id == update.message.id:
|
||||||
|
return update.message
|
||||||
|
|
||||||
|
def _parse_message_text(self, message, parse_mode):
|
||||||
|
"""
|
||||||
|
Returns a (parsed message, entities) tuple depending on parse_mode.
|
||||||
|
"""
|
||||||
|
if not parse_mode:
|
||||||
|
return message, []
|
||||||
|
|
||||||
|
parse_mode = parse_mode.lower()
|
||||||
|
if parse_mode in {'md', 'markdown'}:
|
||||||
|
message, msg_entities = markdown.parse(message)
|
||||||
|
elif parse_mode.startswith('htm'):
|
||||||
|
message, msg_entities = html.parse(message)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unknown parsing mode: {}'.format(parse_mode))
|
||||||
|
|
||||||
|
for i, e in enumerate(msg_entities):
|
||||||
|
if isinstance(e, MessageEntityTextUrl):
|
||||||
|
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
msg_entities[i] = InputMessageEntityMentionName(
|
||||||
|
e.offset, e.length, self.get_input_entity(
|
||||||
|
int(m.group(1)) if m.group(1) else e.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# Make no replacement
|
||||||
|
pass
|
||||||
|
|
||||||
|
return message, msg_entities
|
||||||
|
|
||||||
async def send_message(self, entity, message, reply_to=None,
|
async def send_message(self, entity, message, reply_to=None,
|
||||||
parse_mode=None, link_preview=True):
|
parse_mode='md', link_preview=True):
|
||||||
"""
|
"""
|
||||||
Sends the given message to the specified entity (user/chat/channel).
|
Sends the given message to the specified entity (user/chat/channel).
|
||||||
|
|
||||||
|
@ -583,8 +635,10 @@ class TelegramClient(TelegramBareClient):
|
||||||
it should be the ID of the message that it should reply to.
|
it should be the ID of the message that it should reply to.
|
||||||
|
|
||||||
parse_mode (:obj:`str`, optional):
|
parse_mode (:obj:`str`, optional):
|
||||||
Can be 'md' or 'markdown' for markdown-like parsing, in a similar
|
Can be 'md' or 'markdown' for markdown-like parsing (default),
|
||||||
fashion how official clients work.
|
or 'htm' or 'html' for HTML-like parsing. If ``None`` or any
|
||||||
|
other false-y value is provided, the message will be sent with
|
||||||
|
no formatting.
|
||||||
|
|
||||||
link_preview (:obj:`bool`, optional):
|
link_preview (:obj:`bool`, optional):
|
||||||
Should the link preview be shown?
|
Should the link preview be shown?
|
||||||
|
@ -593,23 +647,14 @@ class TelegramClient(TelegramBareClient):
|
||||||
the sent message
|
the sent message
|
||||||
"""
|
"""
|
||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
if parse_mode:
|
message, msg_entities = self._parse_message_text(message, parse_mode)
|
||||||
parse_mode = parse_mode.lower()
|
|
||||||
if parse_mode in {'md', 'markdown'}:
|
|
||||||
message, msg_entities = markdown.parse(message)
|
|
||||||
elif parse_mode.startswith('htm'):
|
|
||||||
message, msg_entities = html.parse(message)
|
|
||||||
else:
|
|
||||||
raise ValueError('Unknown parsing mode: {}'.format(parse_mode))
|
|
||||||
else:
|
|
||||||
msg_entities = []
|
|
||||||
|
|
||||||
request = SendMessageRequest(
|
request = SendMessageRequest(
|
||||||
peer=entity,
|
peer=entity,
|
||||||
message=message,
|
message=message,
|
||||||
entities=msg_entities,
|
entities=msg_entities,
|
||||||
no_webpage=not link_preview,
|
no_webpage=not link_preview,
|
||||||
reply_to_msg_id=self._get_reply_to(reply_to)
|
reply_to_msg_id=self._get_message_id(reply_to)
|
||||||
)
|
)
|
||||||
result = await self(request)
|
result = await self(request)
|
||||||
|
|
||||||
|
@ -626,6 +671,51 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
return self._get_response_message(request, result)
|
return self._get_response_message(request, result)
|
||||||
|
|
||||||
|
async def edit_message(self, entity, message_id, message=None,
|
||||||
|
parse_mode='md', link_preview=True):
|
||||||
|
"""
|
||||||
|
Edits the given message ID (to change its contents or disable preview).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity (:obj:`entity`):
|
||||||
|
From which chat to edit the message.
|
||||||
|
|
||||||
|
message_id (:obj:`str`):
|
||||||
|
The ID of the message (or ``Message`` itself) to be edited.
|
||||||
|
|
||||||
|
message (:obj:`str`, optional):
|
||||||
|
The new text of the message.
|
||||||
|
|
||||||
|
parse_mode (:obj:`str`, optional):
|
||||||
|
Can be 'md' or 'markdown' for markdown-like parsing (default),
|
||||||
|
or 'htm' or 'html' for HTML-like parsing. If ``None`` or any
|
||||||
|
other false-y value is provided, the message will be sent with
|
||||||
|
no formatting.
|
||||||
|
|
||||||
|
link_preview (:obj:`bool`, optional):
|
||||||
|
Should the link preview be shown?
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
``MessageAuthorRequiredError`` if you're not the author of the
|
||||||
|
message but try editing it anyway.
|
||||||
|
|
||||||
|
``MessageNotModifiedError`` if the contents of the message were
|
||||||
|
not modified at all.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the edited message
|
||||||
|
"""
|
||||||
|
message, msg_entities = self._parse_message_text(message, parse_mode)
|
||||||
|
request = EditMessageRequest(
|
||||||
|
peer=await self.get_input_entity(entity),
|
||||||
|
id=self._get_message_id(message_id),
|
||||||
|
message=message,
|
||||||
|
no_webpage=not link_preview,
|
||||||
|
entities=msg_entities
|
||||||
|
)
|
||||||
|
result = await self(request)
|
||||||
|
return self._get_response_message(request, result)
|
||||||
|
|
||||||
async def delete_messages(self, entity, message_ids, revoke=True):
|
async def delete_messages(self, entity, message_ids, revoke=True):
|
||||||
"""
|
"""
|
||||||
Deletes a message from a chat, optionally "for everyone".
|
Deletes a message from a chat, optionally "for everyone".
|
||||||
|
@ -849,22 +939,22 @@ class TelegramClient(TelegramBareClient):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_reply_to(reply_to):
|
def _get_message_id(message):
|
||||||
"""Sanitizes the 'reply_to' parameter a user may send"""
|
"""Sanitizes the 'reply_to' parameter a user may send"""
|
||||||
if reply_to is None:
|
if message is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(reply_to, int):
|
if isinstance(message, int):
|
||||||
return reply_to
|
return message
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if reply_to.SUBCLASS_OF_ID == 0x790009e3:
|
if message.SUBCLASS_OF_ID == 0x790009e3:
|
||||||
# hex(crc32(b'Message')) = 0x790009e3
|
# hex(crc32(b'Message')) = 0x790009e3
|
||||||
return reply_to.id
|
return message.id
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise TypeError('Invalid reply_to type: {}'.format(type(reply_to)))
|
raise TypeError('Invalid message type: {}'.format(type(message)))
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
@ -902,8 +992,8 @@ class TelegramClient(TelegramBareClient):
|
||||||
|
|
||||||
force_document (:obj:`bool`, optional):
|
force_document (:obj:`bool`, optional):
|
||||||
If left to ``False`` and the file is a path that ends with
|
If left to ``False`` and the file is a path that ends with
|
||||||
``.png``, ``.jpg`` and such, the file will be sent as a photo.
|
the extension of an image file or a video file, it will be
|
||||||
Otherwise always as a document.
|
sent as such. Otherwise always as a document.
|
||||||
|
|
||||||
progress_callback (:obj:`callable`, optional):
|
progress_callback (:obj:`callable`, optional):
|
||||||
A callback function accepting two parameters:
|
A callback function accepting two parameters:
|
||||||
|
@ -953,7 +1043,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
]
|
]
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
reply_to = self._get_reply_to(reply_to)
|
reply_to = self._get_message_id(reply_to)
|
||||||
|
|
||||||
if not isinstance(file, (str, bytes, io.IOBase)):
|
if not isinstance(file, (str, bytes, io.IOBase)):
|
||||||
# The user may pass a Message containing media (or the media,
|
# The user may pass a Message containing media (or the media,
|
||||||
|
@ -995,6 +1085,9 @@ class TelegramClient(TelegramBareClient):
|
||||||
# TODO If the input file is an audio, find out:
|
# TODO If the input file is an audio, find out:
|
||||||
# Performer and song title and add DocumentAttributeAudio
|
# Performer and song title and add DocumentAttributeAudio
|
||||||
}
|
}
|
||||||
|
if not force_document and utils.is_video(file):
|
||||||
|
attr_dict[DocumentAttributeVideo] = \
|
||||||
|
DocumentAttributeVideo(0, 0, 0)
|
||||||
else:
|
else:
|
||||||
attr_dict = {
|
attr_dict = {
|
||||||
DocumentAttributeFilename:
|
DocumentAttributeFilename:
|
||||||
|
@ -1063,7 +1156,7 @@ class TelegramClient(TelegramBareClient):
|
||||||
# cache only makes a difference for documents where the user may
|
# cache only makes a difference for documents where the user may
|
||||||
# want the attributes used on them to change. Caption's ignored.
|
# want the attributes used on them to change. Caption's ignored.
|
||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
reply_to = self._get_reply_to(reply_to)
|
reply_to = self._get_message_id(reply_to)
|
||||||
|
|
||||||
# Need to upload the media first, but only if they're not cached yet
|
# Need to upload the media first, but only if they're not cached yet
|
||||||
media = []
|
media = []
|
||||||
|
|
|
@ -3,10 +3,10 @@ Utilities for working with the Telegram API itself (such as handy methods
|
||||||
to convert between an entity like an User, Chat, etc. into its Input version)
|
to convert between an entity like an User, Chat, etc. into its Input version)
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
|
import mimetypes
|
||||||
import re
|
import re
|
||||||
from mimetypes import add_type, guess_extension
|
from mimetypes import add_type, guess_extension
|
||||||
|
|
||||||
from .tl.types.contacts import ResolvedPeer
|
|
||||||
from .tl.types import (
|
from .tl.types import (
|
||||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||||
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
||||||
|
@ -24,6 +24,7 @@ from .tl.types import (
|
||||||
InputMediaUploadedPhoto, DocumentAttributeFilename, photos,
|
InputMediaUploadedPhoto, DocumentAttributeFilename, photos,
|
||||||
TopPeer, InputNotifyPeer
|
TopPeer, InputNotifyPeer
|
||||||
)
|
)
|
||||||
|
from .tl.types.contacts import ResolvedPeer
|
||||||
|
|
||||||
USERNAME_RE = re.compile(
|
USERNAME_RE = re.compile(
|
||||||
r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
|
r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
|
||||||
|
@ -322,8 +323,12 @@ def get_input_media(media, user_caption=None, is_photo=False):
|
||||||
|
|
||||||
def is_image(file):
|
def is_image(file):
|
||||||
"""Returns True if the file extension looks like an image file"""
|
"""Returns True if the file extension looks like an image file"""
|
||||||
return (isinstance(file, str) and
|
return (mimetypes.guess_type(file)[0] or '').startswith('image/')
|
||||||
bool(re.search(r'\.(png|jpe?g|gif)$', file, re.IGNORECASE)))
|
|
||||||
|
|
||||||
|
def is_video(file):
|
||||||
|
"""Returns True if the file extension looks like a video file"""
|
||||||
|
return (mimetypes.guess_type(file)[0] or '').startswith('video/')
|
||||||
|
|
||||||
|
|
||||||
def parse_phone(phone):
|
def parse_phone(phone):
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# Versions should comply with PEP440.
|
# Versions should comply with PEP440.
|
||||||
# This line is parsed in setup.py:
|
# This line is parsed in setup.py:
|
||||||
__version__ = '0.17.1'
|
__version__ = '0.17.2'
|
||||||
|
|
|
@ -68,7 +68,7 @@ def write_error(f, code, name, desc, capture_name):
|
||||||
f.write(
|
f.write(
|
||||||
"self.{} = int(kwargs.get('capture', 0))\n ".format(capture_name)
|
"self.{} = int(kwargs.get('capture', 0))\n ".format(capture_name)
|
||||||
)
|
)
|
||||||
f.write('super(Exception, self).__init__(self, {}'.format(repr(desc)))
|
f.write('super(Exception, self).__init__({}'.format(repr(desc)))
|
||||||
if capture_name:
|
if capture_name:
|
||||||
f.write('.format(self.{})'.format(capture_name))
|
f.write('.format(self.{})'.format(capture_name))
|
||||||
f.write(')\n')
|
f.write(')\n')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user