mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-03 11:40:11 +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.')
|
||||
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_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',
|
||||
'<span class="tooltip" title="Any iterable that supports len() '
|
||||
'will work too">list</span>'
|
||||
)
|
||||
|
||||
|
||||
def copy_replace(src, dst, replacements):
|
||||
|
|
|
@ -108,6 +108,10 @@ span.sh4 {
|
|||
color: #06c;
|
||||
}
|
||||
|
||||
span.tooltip {
|
||||
border-bottom: 1px dashed #444;
|
||||
}
|
||||
|
||||
#searchBox {
|
||||
width: 100%;
|
||||
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()``.
|
||||
|
||||
.. 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`__
|
||||
|
|
|
@ -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 <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)
|
||||
===========================
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
"""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):
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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('<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):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
@ -109,8 +122,8 @@ def parse(html):
|
|||
:return: a tuple consisting of (clean message, [message entities]).
|
||||
"""
|
||||
parser = HTMLToTelegramParser()
|
||||
parser.feed(html)
|
||||
return parser.text, parser.entities
|
||||
parser.feed(_add_surrogate(html))
|
||||
return _del_surrogate(parser.text), parser.entities
|
||||
|
||||
|
||||
def unparse(text, entities):
|
||||
|
@ -124,6 +137,8 @@ def unparse(text, entities):
|
|||
"""
|
||||
if not entities:
|
||||
return text
|
||||
|
||||
text = _add_surrogate(text)
|
||||
html = []
|
||||
last_offset = 0
|
||||
for entity in entities:
|
||||
|
@ -164,4 +179,4 @@ def unparse(text, entities):
|
|||
skip_entity = True
|
||||
last_offset = entity.offset + (0 if skip_entity else entity.length)
|
||||
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.
|
||||
:return: a markdown-like text representing the combination of both inputs.
|
||||
"""
|
||||
if not entities:
|
||||
return text
|
||||
|
||||
if not delimiters:
|
||||
if delimiters is not None:
|
||||
return text
|
||||
|
|
|
@ -92,6 +92,11 @@ class MtProtoSender:
|
|||
messages = [TLMessage(self.session, r) for r in requests]
|
||||
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:
|
||||
message = messages[0]
|
||||
else:
|
||||
|
@ -468,6 +473,7 @@ class MtProtoSender:
|
|||
request_id = reader.read_long()
|
||||
inner_code = reader.read_int(signed=False)
|
||||
|
||||
__log__.debug('Received response for request with ID %d', request_id)
|
||||
request = self._pop_request(request_id)
|
||||
|
||||
if inner_code == 0x2144ca19: # RPC Error
|
||||
|
@ -502,8 +508,18 @@ class MtProtoSender:
|
|||
return True
|
||||
|
||||
# If it's really a result for RPC from previous connection
|
||||
# session, it will be skipped by the handle_container()
|
||||
__log__.warning('Lost request will be skipped')
|
||||
# session, it will be skipped by the handle_container().
|
||||
# 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
|
||||
|
||||
async def _handle_gzip_packed(self, msg_id, sequence, reader, state):
|
||||
|
|
|
@ -101,8 +101,6 @@ class TelegramBareClient:
|
|||
self.session = session
|
||||
self.api_id = int(api_id)
|
||||
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
|
||||
# that calls .connect(). Every other thread will spawn a new
|
||||
|
@ -152,10 +150,17 @@ class TelegramBareClient:
|
|||
|
||||
self._recv_loop = None
|
||||
self._ping_loop = None
|
||||
self._state_loop = None
|
||||
self._idling = asyncio.Event()
|
||||
|
||||
# Default PingRequest delay
|
||||
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
|
||||
|
||||
|
@ -570,6 +575,8 @@ class TelegramBareClient:
|
|||
self._recv_loop = asyncio.ensure_future(self._recv_loop_impl(), loop=self._loop)
|
||||
if self._ping_loop is None:
|
||||
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):
|
||||
while self._user_connected:
|
||||
|
@ -577,6 +584,11 @@ class TelegramBareClient:
|
|||
await asyncio.sleep(self._ping_delay.seconds, loop=self._loop)
|
||||
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):
|
||||
__log__.info('Starting to wait for items from the network')
|
||||
self._idling.set()
|
||||
|
|
|
@ -4,6 +4,7 @@ import io
|
|||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict, UserList
|
||||
from datetime import datetime, timedelta
|
||||
|
@ -45,8 +46,8 @@ from .tl.functions.contacts import (
|
|||
from .tl.functions.messages import (
|
||||
GetDialogsRequest, GetHistoryRequest, SendMediaRequest,
|
||||
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
|
||||
CheckChatInviteRequest, ReadMentionsRequest,
|
||||
SendMultiMediaRequest, UploadMediaRequest
|
||||
CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest,
|
||||
UploadMediaRequest, EditMessageRequest
|
||||
)
|
||||
|
||||
from .tl.functions import channels
|
||||
|
@ -68,7 +69,9 @@ from .tl.types import (
|
|||
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty,
|
||||
ChatInvite, ChatInviteAlready, PeerChannel, Photo, InputPeerSelf,
|
||||
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 .extensions import markdown, html
|
||||
|
@ -414,7 +417,7 @@ class TelegramClient(TelegramBareClient):
|
|||
result = await self(SignUpRequest(
|
||||
phone_number=self._phone,
|
||||
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
|
||||
phone_code=code,
|
||||
phone_code=str(code),
|
||||
first_name=first_name,
|
||||
last_name=last_name
|
||||
))
|
||||
|
@ -561,13 +564,62 @@ class TelegramClient(TelegramBareClient):
|
|||
msg_id = update.id
|
||||
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 update.message.id == msg_id:
|
||||
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,
|
||||
parse_mode=None, link_preview=True):
|
||||
parse_mode='md', link_preview=True):
|
||||
"""
|
||||
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.
|
||||
|
||||
parse_mode (:obj:`str`, optional):
|
||||
Can be 'md' or 'markdown' for markdown-like parsing, in a similar
|
||||
fashion how official clients work.
|
||||
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?
|
||||
|
@ -593,23 +647,14 @@ class TelegramClient(TelegramBareClient):
|
|||
the sent message
|
||||
"""
|
||||
entity = await self.get_input_entity(entity)
|
||||
if 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 = []
|
||||
message, msg_entities = self._parse_message_text(message, parse_mode)
|
||||
|
||||
request = SendMessageRequest(
|
||||
peer=entity,
|
||||
message=message,
|
||||
entities=msg_entities,
|
||||
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)
|
||||
|
||||
|
@ -626,6 +671,51 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
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):
|
||||
"""
|
||||
Deletes a message from a chat, optionally "for everyone".
|
||||
|
@ -849,22 +939,22 @@ class TelegramClient(TelegramBareClient):
|
|||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_reply_to(reply_to):
|
||||
def _get_message_id(message):
|
||||
"""Sanitizes the 'reply_to' parameter a user may send"""
|
||||
if reply_to is None:
|
||||
if message is None:
|
||||
return None
|
||||
|
||||
if isinstance(reply_to, int):
|
||||
return reply_to
|
||||
if isinstance(message, int):
|
||||
return message
|
||||
|
||||
try:
|
||||
if reply_to.SUBCLASS_OF_ID == 0x790009e3:
|
||||
if message.SUBCLASS_OF_ID == 0x790009e3:
|
||||
# hex(crc32(b'Message')) = 0x790009e3
|
||||
return reply_to.id
|
||||
return message.id
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
raise TypeError('Invalid reply_to type: {}'.format(type(reply_to)))
|
||||
raise TypeError('Invalid message type: {}'.format(type(message)))
|
||||
|
||||
# endregion
|
||||
|
||||
|
@ -902,8 +992,8 @@ class TelegramClient(TelegramBareClient):
|
|||
|
||||
force_document (:obj:`bool`, optional):
|
||||
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.
|
||||
Otherwise always as a document.
|
||||
the extension of an image file or a video file, it will be
|
||||
sent as such. Otherwise always as a document.
|
||||
|
||||
progress_callback (:obj:`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
|
@ -953,7 +1043,7 @@ class TelegramClient(TelegramBareClient):
|
|||
]
|
||||
|
||||
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)):
|
||||
# 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:
|
||||
# Performer and song title and add DocumentAttributeAudio
|
||||
}
|
||||
if not force_document and utils.is_video(file):
|
||||
attr_dict[DocumentAttributeVideo] = \
|
||||
DocumentAttributeVideo(0, 0, 0)
|
||||
else:
|
||||
attr_dict = {
|
||||
DocumentAttributeFilename:
|
||||
|
@ -1063,7 +1156,7 @@ class TelegramClient(TelegramBareClient):
|
|||
# cache only makes a difference for documents where the user may
|
||||
# want the attributes used on them to change. Caption's ignored.
|
||||
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
|
||||
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)
|
||||
"""
|
||||
import math
|
||||
import mimetypes
|
||||
import re
|
||||
from mimetypes import add_type, guess_extension
|
||||
|
||||
from .tl.types.contacts import ResolvedPeer
|
||||
from .tl.types import (
|
||||
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
|
||||
ChatPhoto, InputPeerChannel, InputPeerChat, InputPeerUser, InputPeerEmpty,
|
||||
|
@ -24,6 +24,7 @@ from .tl.types import (
|
|||
InputMediaUploadedPhoto, DocumentAttributeFilename, photos,
|
||||
TopPeer, InputNotifyPeer
|
||||
)
|
||||
from .tl.types.contacts import ResolvedPeer
|
||||
|
||||
USERNAME_RE = re.compile(
|
||||
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):
|
||||
"""Returns True if the file extension looks like an image file"""
|
||||
return (isinstance(file, str) and
|
||||
bool(re.search(r'\.(png|jpe?g|gif)$', file, re.IGNORECASE)))
|
||||
return (mimetypes.guess_type(file)[0] or '').startswith('image/')
|
||||
|
||||
|
||||
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):
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# 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(
|
||||
"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:
|
||||
f.write('.format(self.{})'.format(capture_name))
|
||||
f.write(')\n')
|
||||
|
|
Loading…
Reference in New Issue
Block a user