Merge branch 'master' into sync

This commit is contained in:
Lonami Exo 2018-07-09 12:47:13 +02:00
commit 71d2907017
16 changed files with 218 additions and 52 deletions

View File

@ -1,5 +1,6 @@
import getpass import getpass
import hashlib import hashlib
import inspect
import os import os
import sys import sys

View File

@ -136,7 +136,6 @@ class MessageMethods(UploadMethods, MessageParseMethods):
offset_id = max(offset_id, min_id) offset_id = max(offset_id, min_id)
if offset_id and max_id: if offset_id and max_id:
if max_id - offset_id <= 1: if max_id - offset_id <= 1:
print('suck lol')
return return
if not max_id: if not max_id:
@ -404,10 +403,17 @@ class MessageMethods(UploadMethods, MessageParseMethods):
if reply_to is not None: if reply_to is not None:
reply_id = utils.get_message_id(reply_to) reply_id = utils.get_message_id(reply_to)
elif utils.get_peer_id(entity) == utils.get_peer_id(message.to_id):
reply_id = message.reply_to_msg_id
else: else:
reply_id = None if isinstance(entity, types.InputPeerSelf):
eid = utils.get_peer_id(self.get_me(input_peer=True))
else:
eid = utils.get_peer_id(entity)
if eid == utils.get_peer_id(message.to_id):
reply_id = message.reply_to_msg_id
else:
reply_id = None
request = functions.messages.SendMessageRequest( request = functions.messages.SendMessageRequest(
peer=entity, peer=entity,
message=message.message or '', message=message.message or '',
@ -447,7 +453,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
return self._get_response_message(request, result, entity) return self._get_response_message(request, result, entity)
def forward_messages(self, entity, messages, *, from_peer=None): def forward_messages(self, entity, messages, from_peer=None):
""" """
Forwards the given message(s) to the specified entity. Forwards the given message(s) to the specified entity.

View File

@ -72,7 +72,8 @@ class UploadMethods(MessageParseMethods, UserMethods):
:tl:`DocumentAttributeFilename` and so on. :tl:`DocumentAttributeFilename` and so on.
thumb (`str` | `bytes` | `file`, optional): thumb (`str` | `bytes` | `file`, optional):
Optional thumbnail (for videos). Optional JPEG thumbnail (for documents). **Telegram will
ignore this parameter** unless you pass a ``.jpg`` file!
allow_cache (`bool`, optional): allow_cache (`bool`, optional):
Whether to allow using the cached version stored in the Whether to allow using the cached version stored in the

View File

@ -273,6 +273,32 @@ class UserMethods(TelegramBaseClient):
.format(peer) .format(peer)
) )
def get_peer_id(self, peer, add_mark=True):
"""
Gets the ID for the given peer, which may be anything entity-like.
This method needs to be ``async`` because `peer` supports usernames,
invite-links, phone numbers, etc.
If ``add_mark is False``, then a positive ID will be returned
instead. By default, bot-API style IDs (signed) are returned.
"""
if isinstance(peer, int):
return utils.get_peer_id(peer, add_mark=add_mark)
try:
if peer.SUBCLASS_OF_ID in (0x2d45687, 0xc91c90b6):
# 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer'
return utils.get_peer_id(peer)
except AttributeError:
pass
peer = self.get_input_entity(peer)
if isinstance(peer, types.InputPeerSelf):
peer = self.get_me(input_peer=True)
return utils.get_peer_id(peer, add_mark=add_mark)
# endregion # endregion
# region Private methods # region Private methods
@ -334,4 +360,18 @@ class UserMethods(TelegramBaseClient):
'Cannot find any entity corresponding to "{}"'.format(string) 'Cannot find any entity corresponding to "{}"'.format(string)
) )
def _get_input_notify(self, notify):
"""
Returns a :tl:`InputNotifyPeer`. This is a bit tricky because
it may or not need access to the client to convert what's given
into an input entity.
"""
try:
if notify.SUBCLASS_OF_ID == 0x58981615:
if isinstance(notify, types.InputNotifyPeer):
notify.peer = self.get_input_entity(notify.peer)
return notify
except AttributeError:
return types.InputNotifyPeer(self.get_input_entity(notify))
# endregion # endregion

View File

@ -18,13 +18,13 @@ class StopPropagation(Exception):
>>> client = TelegramClient(...) >>> client = TelegramClient(...)
>>> >>>
>>> @client.on(events.NewMessage) >>> @client.on(events.NewMessage)
... def delete(event): ... async def delete(event):
... event.delete() ... await event.delete()
... # No other event handler will have a chance to handle this event ... # No other event handler will have a chance to handle this event
... raise StopPropagation ... raise StopPropagation
... ...
>>> @client.on(events.NewMessage) >>> @client.on(events.NewMessage)
... def _(event): ... async def _(event):
... # Will never be reached, because it is the second handler ... # Will never be reached, because it is the second handler
... pass ... pass
""" """

View File

@ -10,6 +10,7 @@ any sort, nor any other kind of errors such as connecting twice.
import errno import errno
import logging import logging
import socket import socket
import ssl
import threading import threading
from io import BytesIO from io import BytesIO
@ -28,6 +29,7 @@ try:
except ImportError: except ImportError:
socks = None socks = None
SSL_PORT = 443
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
@ -37,14 +39,17 @@ class TcpClient:
class SocketClosed(ConnectionError): class SocketClosed(ConnectionError):
pass pass
def __init__(self, *, timeout, proxy=None): def __init__(self, *, timeout, ssl=None, proxy=None):
""" """
Initializes the TCP client. Initializes the TCP client.
:param proxy: the proxy to be used, if any. :param proxy: the proxy to be used, if any.
:param timeout: the timeout for connect, read and write operations. :param timeout: the timeout for connect, read and write operations.
:param ssl: ssl.wrap_socket keyword arguments to use when connecting
if port == SSL_PORT, or do nothing if not present.
""" """
self.proxy = proxy self.proxy = proxy
self.ssl = ssl
self._socket = None self._socket = None
self._closed = threading.Event() self._closed = threading.Event()
@ -87,6 +92,8 @@ class TcpClient:
try: try:
if self._socket is None: if self._socket is None:
self._socket = self._create_socket(mode, self.proxy) self._socket = self._create_socket(mode, self.proxy)
if self.ssl and port == SSL_PORT:
self._socket = ssl.wrap_socket(self._socket, **self.ssl)
self._socket.settimeout(self.timeout) self._socket.settimeout(self.timeout)
self._socket.connect(address) self._socket.connect(address)

View File

@ -7,5 +7,5 @@ from .authenticator import do_authentication
from .mtprotosender import MTProtoSender from .mtprotosender import MTProtoSender
from .connection import ( from .connection import (
ConnectionTcpFull, ConnectionTcpAbridged, ConnectionTcpObfuscated, ConnectionTcpFull, ConnectionTcpAbridged, ConnectionTcpObfuscated,
ConnectionTcpIntermediate ConnectionTcpIntermediate, ConnectionHttp
) )

View File

@ -2,3 +2,4 @@ from .tcpfull import ConnectionTcpFull
from .tcpabridged import ConnectionTcpAbridged from .tcpabridged import ConnectionTcpAbridged
from .tcpobfuscated import ConnectionTcpObfuscated from .tcpobfuscated import ConnectionTcpObfuscated
from .tcpintermediate import ConnectionTcpIntermediate from .tcpintermediate import ConnectionTcpIntermediate
from .http import ConnectionHttp

View File

@ -0,0 +1,62 @@
import errno
import ssl
from .common import Connection
from ...extensions import TcpClient
class ConnectionHttp(Connection):
def __init__(self, *, loop, timeout, proxy=None):
super().__init__(loop=loop, timeout=timeout, proxy=proxy)
self.conn = TcpClient(
timeout=self._timeout, loop=self._loop, proxy=self._proxy,
ssl=dict(ssl_version=ssl.PROTOCOL_SSLv23, ciphers='ADH-AES256-SHA')
)
self.read = self.conn.read
self.write = self.conn.write
self._host = None
async def connect(self, ip, port):
self._host = '{}:{}'.format(ip, port)
try:
await self.conn.connect(ip, port)
except OSError as e:
if e.errno == errno.EISCONN:
return # Already connected, no need to re-set everything up
else:
raise
def get_timeout(self):
return self.conn.timeout
def is_connected(self):
return self.conn.is_connected
async def close(self):
self.conn.close()
async def recv(self):
while True:
line = await self._read_line()
if line.lower().startswith(b'content-length: '):
await self.read(2)
length = int(line[16:-2])
return await self.read(length)
async def _read_line(self):
newline = ord('\n')
line = await self.read(1)
while line[-1] != newline:
line += await self.read(1)
return line
async def send(self, message):
await self.write(
'POST /api HTTP/1.1\r\n'
'Host: {}\r\n'
'Content-Type: application/x-www-form-urlencoded\r\n'
'Connection: keep-alive\r\n'
'Keep-Alive: timeout=100000, max=10000000\r\n'
'Content-Length: {}\r\n\r\n'.format(self._host, len(message))
.encode('ascii') + message
)

View File

@ -513,7 +513,6 @@ class MTProtoSender:
rpc_result.req_msg_id) rpc_result.req_msg_id)
if rpc_result.error: if rpc_result.error:
# TODO Report errors if possible/enabled
error = rpc_message_to_error(rpc_result.error) error = rpc_message_to_error(rpc_result.error)
self._send_queue.put_nowait(self.state.create_message( self._send_queue.put_nowait(self.state.create_message(
MsgsAck([message.msg_id]) MsgsAck([message.msg_id])
@ -523,10 +522,13 @@ class MTProtoSender:
message.future.set_exception(error) message.future.set_exception(error)
return return
elif message: elif message:
# TODO Would be nice to avoid accessing a per-obj read_result
# Instead have a variable that indicated how the result should
# be read (an enum) and dispatch to read the result, mostly
# always it's just a normal TLObject.
with BinaryReader(rpc_result.body) as reader: with BinaryReader(rpc_result.body) as reader:
result = message.obj.read_result(reader) result = message.obj.read_result(reader)
# TODO Process entities
if not message.future.cancelled(): if not message.future.cancelled():
message.future.set_result(result) message.future.set_result(result)
return return
@ -753,6 +755,7 @@ class _ContainerQueue(queue.Queue):
isinstance(result.obj, MessageContainer): isinstance(result.obj, MessageContainer):
return result return result
size = result.size()
result = [result] result = [result]
while not self.empty(): while not self.empty():
# TODO Is this a bug in Python? For some reason get_nowait() # TODO Is this a bug in Python? For some reason get_nowait()
@ -773,11 +776,13 @@ class _ContainerQueue(queue.Queue):
if not isinstance(items, list): if not isinstance(items, list):
items = [items] items = [items]
for item in items: for item in items:
if item == _reconnect_sentinel or\ if (item == _reconnect_sentinel or
isinstance(item.obj, MessageContainer): isinstance(item.obj, MessageContainer)
or size + item.size() > MessageContainer.MAXIMUM_SIZE):
self.put_nowait(item) self.put_nowait(item)
break return result # break 2 levels
else: else:
size += item.size()
result.append(item) result.append(item)
return result return result

View File

@ -46,7 +46,8 @@ class MTProtoState:
msg_id=self._get_new_msg_id(), msg_id=self._get_new_msg_id(),
seq_no=self._get_seq_no(isinstance(obj, TLRequest)), seq_no=self._get_seq_no(isinstance(obj, TLRequest)),
obj=obj, obj=obj,
after_id=after.msg_id if after else None after_id=after.msg_id if after else None,
out=True # Pre-convert the request into bytes
) )
def update_message_id(self, message): def update_message_id(self, message):

View File

@ -10,6 +10,11 @@ __log__ = logging.getLogger(__name__)
class MessageContainer(TLObject): class MessageContainer(TLObject):
CONSTRUCTOR_ID = 0x73f1f8dc CONSTRUCTOR_ID = 0x73f1f8dc
# Maximum size in bytes for the inner payload of the container.
# Telegram will close the connection if the payload is bigger.
# The overhead of the container itself is subtracted.
MAXIMUM_SIZE = 1044456 - 8
def __init__(self, messages): def __init__(self, messages):
self.messages = messages self.messages = messages

View File

@ -21,9 +21,7 @@ class TLMessage(TLObject):
sent `TLMessage`, and this result can be represented as a `Future` sent `TLMessage`, and this result can be represented as a `Future`
that will eventually be set with either a result, error or cancelled. that will eventually be set with either a result, error or cancelled.
""" """
def __init__(self, msg_id, seq_no, obj=None, after_id=0): def __init__(self, msg_id, seq_no, obj, out=False, after_id=0):
self.msg_id = msg_id
self.seq_no = seq_no
self.obj = obj self.obj = obj
self.container_msg_id = None self.container_msg_id = None
self.future = concurrent.futures.Future() self.future = concurrent.futures.Future()
@ -31,23 +29,59 @@ class TLMessage(TLObject):
# After which message ID this one should run. We do this so # After which message ID this one should run. We do this so
# InvokeAfterMsgRequest is transparent to the user and we can # InvokeAfterMsgRequest is transparent to the user and we can
# easily invoke after while confirming the original request. # easily invoke after while confirming the original request.
# TODO Currently we don't update this if another message ID changes
self.after_id = after_id self.after_id = after_id
# There are two use-cases for the TLMessage, outgoing and incoming.
# Outgoing messages are meant to be serialized and sent across the
# network so it makes sense to pack them as early as possible and
# avoid this computation if it needs to be resent, and also shows
# serializing-errors as early as possible (foreground task).
#
# We assume obj won't change so caching the bytes is safe to do.
# Caching bytes lets us get the size in a fast way, necessary for
# knowing whether a container can be sent (<1MB) or not (too big).
#
# Incoming messages don't really need this body, but we save the
# msg_id and seq_no inside the body for consistency and raise if
# one tries to bytes()-ify the entire message (len == 12).
if not out:
self._body = struct.pack('<qi', msg_id, seq_no)
else:
if self.after_id is None:
body = GzipPacked.gzip_if_smaller(self.obj)
else:
body = GzipPacked.gzip_if_smaller(
InvokeAfterMsgRequest(self.after_id, self.obj))
self._body = struct.pack('<qii', msg_id, seq_no, len(body)) + body
def to_dict(self): def to_dict(self):
return { return {
'_': 'TLMessage', '_': 'TLMessage',
'msg_id': self.msg_id, 'msg_id': self.msg_id,
'seq_no': self.seq_no, 'seq_no': self.seq_no,
'obj': self.obj, 'obj': self.obj,
'container_msg_id': self.container_msg_id, 'container_msg_id': self.container_msg_id
'after_id': self.after_id
} }
def __bytes__(self): @property
if self.after_id is None: def msg_id(self):
body = GzipPacked.gzip_if_smaller(self.obj) return struct.unpack('<q', self._body[:8])[0]
else:
body = GzipPacked.gzip_if_smaller(
InvokeAfterMsgRequest(self.after_id, self.obj))
return struct.pack('<qii', self.msg_id, self.seq_no, len(body)) + body @msg_id.setter
def msg_id(self, value):
self._body = struct.pack('<q', value) + self._body[8:]
@property
def seq_no(self):
return struct.unpack('<i', self._body[8:12])[0]
def __bytes__(self):
if len(self._body) == 12: # msg_id, seqno
raise TypeError('Incoming messages should not be bytes()-ed')
return self._body
def size(self):
return len(self._body)

View File

@ -575,17 +575,6 @@ def parse_username(username):
return None, False return None, False
def _fix_peer_id(peer_id):
"""
Fixes the peer ID for chats and channels, in case the users
mix marking the ID with the :tl:`Peer` constructors.
"""
peer_id = abs(peer_id)
if str(peer_id).startswith('100'):
peer_id = str(peer_id)[3:]
return int(peer_id)
def get_inner_text(text, entities): def get_inner_text(text, entities):
""" """
Gets the inner text that's surrounded by the given entities. Gets the inner text that's surrounded by the given entities.
@ -605,7 +594,7 @@ def get_inner_text(text, entities):
return result return result
def get_peer_id(peer): def get_peer_id(peer, add_mark=True):
""" """
Finds the ID of the given peer, and converts it to the "bot api" format Finds the ID of the given peer, and converts it to the "bot api" format
so it the peer can be identified back. User ID is left unmodified, so it the peer can be identified back. User ID is left unmodified,
@ -616,7 +605,7 @@ def get_peer_id(peer):
""" """
# First we assert it's a Peer TLObject, or early return for integers # First we assert it's a Peer TLObject, or early return for integers
if isinstance(peer, int): if isinstance(peer, int):
return peer return peer if add_mark else resolve_id(peer)[0]
try: try:
if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6):
@ -634,9 +623,9 @@ def get_peer_id(peer):
elif isinstance(peer, (PeerChat, InputPeerChat)): elif isinstance(peer, (PeerChat, InputPeerChat)):
# Check in case the user mixed things up to avoid blowing up # Check in case the user mixed things up to avoid blowing up
if not (0 < peer.chat_id <= 0x7fffffff): if not (0 < peer.chat_id <= 0x7fffffff):
peer.chat_id = _fix_peer_id(peer.chat_id) peer.chat_id = resolve_id(peer.chat_id)[0]
return -peer.chat_id return -peer.chat_id if add_mark else peer.chat_id
elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)): elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)):
if isinstance(peer, ChannelFull): if isinstance(peer, ChannelFull):
# Special case: .get_input_peer can't return InputChannel from # Special case: .get_input_peer can't return InputChannel from
@ -647,15 +636,18 @@ def get_peer_id(peer):
# Check in case the user mixed things up to avoid blowing up # Check in case the user mixed things up to avoid blowing up
if not (0 < i <= 0x7fffffff): if not (0 < i <= 0x7fffffff):
i = _fix_peer_id(i) i = resolve_id(i)[0]
if isinstance(peer, ChannelFull): if isinstance(peer, ChannelFull):
peer.id = i peer.id = i
else: else:
peer.channel_id = i peer.channel_id = i
# Concat -100 through math tricks, .to_supergroup() on Madeline if add_mark:
# IDs will be strictly positive -> log works # Concat -100 through math tricks, .to_supergroup() on
return -(i + pow(10, math.floor(math.log10(i) + 3))) # Madeline IDs will be strictly positive -> log works.
return -(i + pow(10, math.floor(math.log10(i) + 3)))
else:
return i
_raise_cast_fail(peer, 'int') _raise_cast_fail(peer, 'int')

View File

@ -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__ = '1.0.3' __version__ = '1.0.4'

View File

@ -23,11 +23,16 @@ AUTO_CASTS = {
'InputDialogPeer': 'InputDialogPeer':
'utils.get_input_dialog(client.get_input_entity({}))', 'utils.get_input_dialog(client.get_input_entity({}))',
'InputNotifyPeer': 'client._get_input_notify({})',
'InputMedia': 'utils.get_input_media({})', 'InputMedia': 'utils.get_input_media({})',
'InputPhoto': 'utils.get_input_photo({})', 'InputPhoto': 'utils.get_input_photo({})',
'InputMessage': 'utils.get_input_message({})' 'InputMessage': 'utils.get_input_message({})'
} }
NAMED_AUTO_CASTS = {
('chat_id', 'int'): 'client.get_peer_id({}, add_mark=False)'
}
BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128',
'int256', 'double', 'Bool', 'true', 'date') 'int256', 'double', 'Bool', 'true', 'date')
@ -232,12 +237,18 @@ def _write_class_init(tlobject, kind, type_constructors, builder):
def _write_resolve(tlobject, builder): def _write_resolve(tlobject, builder):
if any(arg.type in AUTO_CASTS for arg in tlobject.real_args): if tlobject.is_function and any(
(arg.type in AUTO_CASTS
or ((arg.name, arg.type) in NAMED_AUTO_CASTS))
for arg in tlobject.real_args
):
builder.writeln('def resolve(self, client, utils):') builder.writeln('def resolve(self, client, utils):')
for arg in tlobject.real_args: for arg in tlobject.real_args:
ac = AUTO_CASTS.get(arg.type, None) ac = AUTO_CASTS.get(arg.type)
if not ac: if not ac:
continue ac = NAMED_AUTO_CASTS.get((arg.name, arg.type))
if not ac:
continue
if arg.is_flag: if arg.is_flag:
builder.writeln('if self.{}:', arg.name) builder.writeln('if self.{}:', arg.name)