@@ -5,7 +5,6 @@
import pathlib
import re
import typing
import inspect
from io import BytesIO

from ..crypto import AES
@@ -95,6 +94,7 @@ async def send_file(
*,
caption: typing.Union[str, typing.Sequence[str]] = None,
force_document: bool = False,
file_size: int = None,
clear_draft: bool = False,
progress_callback: 'hints.ProgressCallback' = None,
reply_to: 'hints.MessageIDLike' = None,
@@ -175,6 +175,13 @@ async def send_file(
the extension of an image file or a video file, it will be
sent as such. Otherwise always as a document.
file_size (`int`, optional):
The size of the file to be uploaded if it needs to be uploaded,
which will be determined automatically if not specified.
If the file size can't be determined beforehand, the entire
file will be read in-memory to find out how large it is.
clear_draft (`bool`, optional):
Whether the existing draft should be cleared or not.
@@ -358,6 +365,7 @@ def callback(current, total):

file_handle, media, image = await self._file_to_media(
file, force_document=force_document,
file_size=file_size,
progress_callback=progress_callback,
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
voice_note=voice_note, video_note=video_note,
@@ -449,6 +457,7 @@ async def upload_file(
file: 'hints.FileLike',
*,
part_size_kb: float = None,
file_size: int = None,
file_name: str = None,
use_cache: type = None,
key: bytes = None,
@@ -480,6 +489,13 @@ async def upload_file(
Chunk size when uploading files. The larger, the less
requests will be made (up to 512KB maximum).
file_size (`int`, optional):
The size of the file to be uploaded, which will be determined
automatically if not specified.
If the file size can't be determined beforehand, the entire
file will be read in-memory to find out how large it is.
file_name (`str`, optional):
The file name which will be used on the resulting InputFile.
If not specified, the name will be taken from the ``file``
@@ -527,34 +543,42 @@ async def upload_file(
if not file_name and getattr(file, 'name', None):
file_name = file.name

if isinstance(file, str):
if file_size is not None:
pass # do nothing as it's already kwown
elif isinstance(file, str):
file_size = os.path.getsize(file)
stream = open(file, 'rb')
close_stream = True
elif isinstance(file, bytes):
file_size = len(file)
stream = io.BytesIO(file)
close_stream = True
else:
# `aiofiles` shouldn't base `IOBase` because they change the
# methods' definition. `seekable` would be `async` but since
# we won't get to check that, there's no need to maybe-await.
if isinstance(file, io.IOBase) and file.seekable():
pos = file.tell()
if not callable(getattr(file, 'read', None)):
raise TypeError('file description should have a `read` method')

if callable(getattr(file, 'seekable', None)):
seekable = await helpers._maybe_await(file.seekable())
else:
pos = None
seekable = False

# TODO Don't load the entire file in memory always
data = file.read()
if inspect.isawaitable(data):
data = await data
if seekable:
pos = await helpers._maybe_await(file.tell())
await helpers._maybe_await(file.seek(0, os.SEEK_END))
file_size = await helpers._maybe_await(file.tell())
await helpers._maybe_await(file.seek(pos, os.SEEK_SET))

if pos is not None:
file.seek(pos)
stream = file
close_stream = False
else:
self._log[__name__].warning(
'Could not determine file size beforehand so the entire '
'file will be read in-memory')

if not isinstance(data, bytes):
raise TypeError(
'file descriptor returned {}, not bytes (you must '
'open the file in bytes mode)'.format(type(data)))

file = data
file_size = len(file)
data = await helpers._maybe_await(file.read())
stream = io.BytesIO(data)
close_stream = True
file_size = len(data)

# File will now either be a string or bytes
if not part_size_kb:
@@ -584,35 +608,46 @@ async def upload_file(

# Determine whether the file is too big (over 10MB) or not
# Telegram does make a distinction between smaller or larger files
is_large = file_size > 10 * 1024 * 1024
is_big = file_size > 10 * 1024 * 1024
hash_md5 = hashlib.md5()
if not is_large:
# Calculate the MD5 hash before anything else.
# As this needs to be done always for small files,
# might as well do it before anything else and
# check the cache.
if isinstance(file, str):
with open(file, 'rb') as stream:
file = stream.read()
hash_md5.update(file)

part_count = (file_size + part_size - 1) // part_size
self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d',
file_size, part_count, part_size)

with open(file, 'rb') if isinstance(file, str) else BytesIO(file)\
as stream:
pos = 0
try:
for part_index in range(part_count):
# Read the file by in chunks of size part_size
part = stream.read(part_size)
part = await helpers._maybe_await(stream.read(part_size))

if not isinstance(part, bytes):
raise TypeError(
'file descriptor returned {}, not bytes (you must '
'open the file in bytes mode)'.format(type(part)))

# `file_size` could be wrong in which case `part` may not be
# `part_size` before reaching the end.
if len(part) != part_size and part_index < part_count - 1:
raise ValueError(
'read less than {} before reaching the end; either '
'`file_size` or `read` are wrong'.format(part_size))

pos += len(part)

if not is_big:
# Bit odd that MD5 is only needed for small files and not
# big ones with more chance for corruption, but that's
# what Telegram wants.
hash_md5.update(part)

# encryption part if needed
# Encryption part if needed
if key and iv:
part = AES.encrypt_ige(part, key, iv)

# The SavePartRequest is different depending on whether
# the file is too large or not (over or less than 10MB)
if is_large:
if is_big:
request = functions.upload.SaveBigFilePartRequest(
file_id, part_index, part_count, part)
else:
@@ -624,14 +659,15 @@ async def upload_file(
self._log[__name__].debug('Uploaded %d/%d',
part_index + 1, part_count)
if progress_callback:
r = progress_callback(stream.tell(), file_size)
if inspect.isawaitable(r):
await r
await helpers._maybe_await(progress_callback(pos, file_size))
else:
raise RuntimeError(
'Failed to upload file part {}.'.format(part_index))
finally:
if close_stream:
await helpers._maybe_await(stream.close())

if is_large:
if is_big:
return types.InputFileBig(file_id, part_count, file_name)
else:
return custom.InputSizedFile(
@@ -641,7 +677,7 @@ async def upload_file(
# endregion

async def _file_to_media(
self, file, force_document=False,
self, file, force_document=False, file_size=None,
progress_callback=None, attributes=None, thumb=None,
allow_cache=True, voice_note=False, video_note=False,
supports_streaming=False, mime_type=None, as_image=None):
@@ -686,13 +722,12 @@ async def _file_to_media(
elif not isinstance(file, str) or os.path.isfile(file):
file_handle = await self.upload_file(
_resize_photo_if_needed(file, as_image),
file_size=file_size,
progress_callback=progress_callback
)
elif re.match('https?://', file):
if as_image:
media = types.InputMediaPhotoExternal(file)
elif not force_document and utils.is_gif(file):
media = types.InputMediaGifExternal(file, '')
else:
media = types.InputMediaDocumentExternal(file)
else:
@@ -725,13 +760,14 @@ async def _file_to_media(
else:
if isinstance(thumb, pathlib.Path):
thumb = str(thumb.absolute())
thumb = await self.upload_file(thumb)
thumb = await self.upload_file(thumb, file_size=file_size)

media = types.InputMediaUploadedDocument(
file=file_handle,
mime_type=mime_type,
attributes=attributes,
thumb=thumb
thumb=thumb,
force_file=force_document
)
return file_handle, media, as_image

@@ -44,7 +44,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False):
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
elif diff <= self.flood_sleep_threshold:
self._log[__name__].info(*_fmt_flood(diff, r, early=True))
await asyncio.sleep(diff, loop=self._loop)
await asyncio.sleep(diff)
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
else:
raise errors.FloodWaitError(request=r, capture=diff)
@@ -99,7 +99,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False):

if e.seconds <= self.flood_sleep_threshold:
self._log[__name__].info(*_fmt_flood(e.seconds, request))
await asyncio.sleep(e.seconds, loop=self._loop)
await asyncio.sleep(e.seconds)
else:
raise
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
@@ -92,7 +92,7 @@ async def resolve(self, client):
return

if not self._resolve_lock:
self._resolve_lock = asyncio.Lock(loop=client.loop)
self._resolve_lock = asyncio.Lock()

async with self._resolve_lock:
if not self.resolved:
@@ -206,10 +206,9 @@ async def handler(event):
return

if results:
futures = [self._as_future(x, self._client.loop)
for x in results]
futures = [self._as_future(x) for x in results]

await asyncio.wait(futures, loop=self._client.loop)
await asyncio.wait(futures)

# All futures will be in the `done` *set* that `wait` returns.
#
@@ -236,10 +235,10 @@ async def handler(event):
)

@staticmethod
def _as_future(obj, loop):
def _as_future(obj):
if inspect.isawaitable(obj):
return asyncio.ensure_future(obj, loop=loop)
return asyncio.ensure_future(obj)

f = loop.create_future()
f = asyncio.get_event_loop().create_future()
f.set_result(obj)
return f
@@ -22,11 +22,10 @@ class MessagePacker:
point where outgoing requests are put, and where ready-messages are get.
"""

def __init__(self, state, loop, loggers):
def __init__(self, state, loggers):
self._state = state
self._loop = loop
self._deque = collections.deque()
self._ready = asyncio.Event(loop=loop)
self._ready = asyncio.Event()
self._log = loggers[__name__]

def append(self, state):
@@ -3,6 +3,7 @@
import enum
import os
import struct
import inspect
from hashlib import sha1


@@ -107,6 +108,13 @@ def retry_range(retries):
yield 1 + attempt


async def _maybe_await(value):
if inspect.isawaitable(value):
return await value
else:
return value


async def _cancel(log, **tasks):
"""
Helper to cancel one or more tasks gracefully, logging exceptions.
@@ -28,11 +28,10 @@ class Connection(abc.ABC):
# should be one of `PacketCodec` implementations
packet_codec = None

def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
def __init__(self, ip, port, dc_id, *, loggers, proxy=None):
self._ip = ip
self._port = port
self._dc_id = dc_id # only for MTProxy, it's an abstraction leak
self._loop = loop
self._log = loggers[__name__]
self._proxy = proxy
self._reader = None
@@ -48,9 +47,8 @@ def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
async def _connect(self, timeout=None, ssl=None):
if not self._proxy:
self._reader, self._writer = await asyncio.wait_for(
asyncio.open_connection(
self._ip, self._port, loop=self._loop, ssl=ssl),
loop=self._loop, timeout=timeout
asyncio.open_connection(self._ip, self._port, ssl=ssl),
timeout=timeout
)
else:
import socks
@@ -67,9 +65,8 @@ async def _connect(self, timeout=None, ssl=None):

s.settimeout(timeout)
await asyncio.wait_for(
self._loop.sock_connect(s, address),
timeout=timeout,
loop=self._loop
asyncio.get_event_loop().sock_connect(s, address),
timeout=timeout
)
if ssl:
if ssl_mod is None:
@@ -87,8 +84,7 @@ async def _connect(self, timeout=None, ssl=None):

s.setblocking(False)

self._reader, self._writer = \
await asyncio.open_connection(sock=s, loop=self._loop)
self._reader, self._writer = await asyncio.open_connection(sock=s)

self._codec = self.packet_codec(self)
self._init_conn()
@@ -101,8 +97,9 @@ async def connect(self, timeout=None, ssl=None):
await self._connect(timeout=timeout, ssl=ssl)
self._connected = True

self._send_task = self._loop.create_task(self._send_loop())
self._recv_task = self._loop.create_task(self._recv_loop())
loop = asyncio.get_event_loop()
self._send_task = loop.create_task(self._send_loop())
self._recv_task = loop.create_task(self._recv_loop())

async def disconnect(self):
"""
@@ -95,12 +95,12 @@ class TcpMTProxy(ObfuscatedConnection):
obfuscated_io = MTProxyIO

# noinspection PyUnusedLocal
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
def __init__(self, ip, port, dc_id, *, loggers, proxy=None):
# connect to proxy's host and port instead of telegram's ones
proxy_host, proxy_port = self.address_info(proxy)
self._secret = bytes.fromhex(proxy[2])
super().__init__(
proxy_host, proxy_port, dc_id, loop=loop, loggers=loggers)
proxy_host, proxy_port, dc_id, loggers=loggers)

async def _connect(self, timeout=None, ssl=None):
await super()._connect(timeout=timeout, ssl=ssl)
@@ -40,12 +40,11 @@ class MTProtoSender:
A new authorization key will be generated on connection if no other
key exists yet.
"""
def __init__(self, auth_key, loop, *, loggers,
def __init__(self, auth_key, *, loggers,
retries=5, delay=1, auto_reconnect=True, connect_timeout=None,
auth_key_callback=None,
update_callback=None, auto_reconnect_callback=None):
self._connection = None
self._loop = loop
self._loggers = loggers
self._log = loggers[__name__]
self._retries = retries
@@ -55,7 +54,7 @@ def __init__(self, auth_key, loop, *, loggers,
self._auth_key_callback = auth_key_callback
self._update_callback = update_callback
self._auto_reconnect_callback = auto_reconnect_callback
self._connect_lock = asyncio.Lock(loop=loop)
self._connect_lock = asyncio.Lock()

# Whether the user has explicitly connected or disconnected.
#
@@ -65,7 +64,7 @@ def __init__(self, auth_key, loop, *, loggers,
# pending futures should be cancelled.
self._user_connected = False
self._reconnecting = False
self._disconnected = self._loop.create_future()
self._disconnected = asyncio.get_event_loop().create_future()
self._disconnected.set_result(None)

# We need to join the loops upon disconnection
@@ -78,8 +77,7 @@ def __init__(self, auth_key, loop, *, loggers,

# Outgoing messages are put in a queue and sent in a batch.
# Note that here we're also storing their ``_RequestState``.
self._send_queue = MessagePacker(self._state, self._loop,
loggers=self._loggers)
self._send_queue = MessagePacker(self._state, loggers=self._loggers)

# Sent states are remembered until a response is received.
self._pending_state = {}
@@ -171,7 +169,7 @@ async def method():

if not utils.is_list_like(request):
try:
state = RequestState(request, self._loop)
state = RequestState(request)
except struct.error as e:
# "struct.error: required argument is not an integer" is not
# very helpful; log the request to find out what wasn't int.
@@ -186,7 +184,7 @@ async def method():
state = None
for req in request:
try:
state = RequestState(req, self._loop, after=ordered and state)
state = RequestState(req, after=ordered and state)
except struct.error as e:
self._log.error('Request caused struct.error: %s: %s', e, request)
raise
@@ -206,7 +204,7 @@ def disconnected(self):
Note that it may resolve in either a ``ConnectionError``
or any other unexpected error that could not be handled.
"""
return asyncio.shield(self._disconnected, loop=self._loop)
return asyncio.shield(self._disconnected)

# Private methods

@@ -241,7 +239,7 @@ async def _connect(self):
# reconnect cleanly after.
await self._connection.disconnect()
connected = False
await asyncio.sleep(self._delay, loop=self._loop)
await asyncio.sleep(self._delay)
continue # next iteration we will try to reconnect

break # all steps done, break retry loop
@@ -253,17 +251,18 @@ async def _connect(self):
await self._disconnect(error=e)
raise e

loop = asyncio.get_event_loop()
self._log.debug('Starting send loop')
self._send_loop_handle = self._loop.create_task(self._send_loop())
self._send_loop_handle = loop.create_task(self._send_loop())

self._log.debug('Starting receive loop')
self._recv_loop_handle = self._loop.create_task(self._recv_loop())
self._recv_loop_handle = loop.create_task(self._recv_loop())

# _disconnected only completes after manual disconnection
# or errors after which the sender cannot continue such
# as failing to reconnect or any unexpected error.
if self._disconnected.done():
self._disconnected = self._loop.create_future()
self._disconnected = loop.create_future()

self._log.info('Connection to %s complete!', self._connection)

@@ -378,7 +377,7 @@ async def _reconnect(self, last_error):
self._pending_state.clear()

if self._auto_reconnect_callback:
self._loop.create_task(self._auto_reconnect_callback())
asyncio.get_event_loop().create_task(self._auto_reconnect_callback())

break
else:
@@ -398,7 +397,7 @@ def _start_reconnect(self, error):
# gets stuck.
# TODO It still gets stuck? Investigate where and why.
self._reconnecting = True
self._loop.create_task(self._reconnect(error))
asyncio.get_event_loop().create_task(self._reconnect(error))

# Loops

@@ -411,7 +410,7 @@ async def _send_loop(self):
"""
while self._user_connected and not self._reconnecting:
if self._pending_ack:
ack = RequestState(MsgsAck(list(self._pending_ack)), self._loop)
ack = RequestState(MsgsAck(list(self._pending_ack)))
self._send_queue.append(ack)
self._last_acks.append(ack)
self._pending_ack.clear()
@@ -564,7 +563,7 @@ async def _handle_rpc_result(self, message):
if rpc_result.error:
error = rpc_message_to_error(rpc_result.error, state.request)
self._send_queue.append(
RequestState(MsgsAck([state.msg_id]), loop=self._loop))
RequestState(MsgsAck([state.msg_id])))

if not state.future.cancelled():
state.future.set_exception(error)
@@ -751,8 +750,8 @@ async def _handle_state_forgotten(self, message):
enqueuing a :tl:`MsgsStateInfo` to be sent at a later point.
"""
self._send_queue.append(RequestState(MsgsStateInfo(
req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids)),
loop=self._loop))
req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids)
)))

async def _handle_msg_all(self, message):
"""
@@ -10,10 +10,10 @@ class RequestState:
"""
__slots__ = ('container_id', 'msg_id', 'request', 'data', 'future', 'after')

def __init__(self, request, loop, after=None):
def __init__(self, request, after=None):
self.container_id = None
self.msg_id = None
self.request = request
self.data = bytes(request)
self.future = asyncio.Future(loop=loop)
self.future = asyncio.Future()
self.after = after
@@ -65,8 +65,7 @@ async def __anext__(self):
# asyncio will handle times <= 0 to sleep 0 seconds
if self.wait_time:
await asyncio.sleep(
self.wait_time - (time.time() - self.last_load),
loop=self.client.loop
self.wait_time - (time.time() - self.last_load)
)
self.last_load = time.time()

@@ -445,8 +445,7 @@ def _get_result(self, future, start_time, timeout, pending, target_id):
# cleared when their futures are set to a result.
return asyncio.wait_for(
future,
timeout=None if due == float('inf') else due - time.time(),
loop=self._client.loop
timeout=None if due == float('inf') else due - time.time()
)

def _cancel_all(self, exception=None):
@@ -483,7 +483,7 @@ def get_input_media(
supports_streaming=supports_streaming
)
return types.InputMediaUploadedDocument(
file=media, mime_type=mime, attributes=attrs)
file=media, mime_type=mime, attributes=attrs, force_file=force_document)

if isinstance(media, types.MessageMediaGame):
return types.InputMediaGame(id=types.InputGameID(
@@ -1235,7 +1235,7 @@ def get_appropriated_part_size(file_size):
return 128
if file_size <= 786432000: # 750MB
return 256
if file_size <= 1572864000: # 1500MB
if file_size <= 2097152000: # 2000MB
return 512

raise ValueError('File size too large')
@@ -1,3 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
__version__ = '1.15.0'
__version__ = '1.16.0'
@@ -341,8 +341,8 @@ async def check_chat(self, event=None):
self.chat.configure(bg='yellow')


async def main(loop, interval=0.05):
client = TelegramClient(SESSION, API_ID, API_HASH, loop=loop)
async def main(interval=0.05):
client = TelegramClient(SESSION, API_ID, API_HASH)
try:
await client.connect()
except Exception as e:
@@ -372,7 +372,7 @@ async def main(loop, interval=0.05):
# Some boilerplate code to set up the main method
aio_loop = asyncio.get_event_loop()
try:
aio_loop.run_until_complete(main(aio_loop))
aio_loop.run_until_complete(main())
finally:
if not aio_loop.is_closed():
aio_loop.close()
@@ -62,10 +62,9 @@ inputMediaUploadedPhoto#1e287d04 flags:# file:InputFile stickers:flags.0?Vector<
inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMedia;
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia;
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia;
inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;
inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
@@ -75,7 +74,7 @@ inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> s
inputMediaDice#e66fbf7b emoticon:string = InputMedia;

inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
inputChatUploadedPhoto#c642724e flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = InputChatPhoto;
inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;

inputGeoPointEmpty#e4c123d6 = InputGeoPoint;
@@ -113,7 +112,7 @@ userEmpty#200250ba id:int = User;
user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User;

userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto;
userProfilePhoto#ecd75d8c photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto;
userProfilePhoto#69d3ab26 flags:# has_video:flags.0?true photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto;

userStatusEmpty#9d05049 = UserStatus;
userStatusOnline#edb93949 expires:int = UserStatus;
@@ -139,7 +138,7 @@ chatParticipantsForbidden#fc900c2b flags:# chat_id:int self_participant:flags.0?
chatParticipants#3f460fed chat_id:int participants:Vector<ChatParticipant> version:int = ChatParticipants;

chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#475cdbd5 photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;
chatPhoto#d20b9f3c flags:# has_video:flags.0?true photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto;

messageEmpty#83e5de54 id:int = Message;
message#452c0e65 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector<RestrictionReason> = Message;
@@ -187,7 +186,7 @@ dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer t
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;

photoEmpty#2331b22d id:long = Photo;
photo#d07504a5 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> dc_id:int = Photo;
photo#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> video_sizes:flags.1?Vector<VideoSize> dc_id:int = Photo;

photoSizeEmpty#e17e23c type:string = PhotoSize;
photoSize#77bfb61b type:string location:FileLocation w:int h:int size:int = PhotoSize;
@@ -213,7 +212,7 @@ inputPeerNotifySettings#9c3d198e flags:# show_previews:flags.0?Bool silent:flags

peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = PeerNotifySettings;

peerSettings#818426cd flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true = PeerSettings;
peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true geo_distance:flags.6?int = PeerSettings;

wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper;
wallPaperNoFile#8af40b25 flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;
@@ -358,6 +357,7 @@ updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update;
updateDialogFilterOrder#a5d72105 order:Vector<int> = Update;
updateDialogFilters#3504914f = Update;
updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update;
updateChannelParticipant#65d2b464 flags:# channel_id:int date:int user_id:int prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant qts:int = Update;

updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;

@@ -395,7 +395,7 @@ help.inviteText#18cb9f78 message:string = help.InviteText;

encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat;
encryptedChatWaiting#3bf703dc id:int access_hash:long date:int admin_id:int participant_id:int = EncryptedChat;
encryptedChatRequested#c878527e id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat;
encryptedChatRequested#62718a82 flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat;
encryptedChat#fa56ce36 id:int access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long = EncryptedChat;
encryptedChatDiscarded#13d6dd27 id:int = EncryptedChat;

@@ -529,6 +529,7 @@ chatInviteExported#fc2e05bc link:string = ExportedChatInvite;

chatInviteAlready#5a686d7c chat:Chat = ChatInvite;
chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector<User> = ChatInvite;
chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite;

inputStickerSetEmpty#ffb62b95 = InputStickerSet;
inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet;
@@ -619,11 +620,6 @@ channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector

help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector<MessageEntity> min_age_confirm:flags.1?int = help.TermsOfService;

foundGif#162ecc1f url:string thumb_url:string content_url:string content_type:string w:int h:int = FoundGif;
foundGifCached#9c750409 url:string photo:Photo document:Document = FoundGif;

messages.foundGifs#450a1c0a next_offset:int results:Vector<FoundGif> = messages.FoundGifs;

messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs;
messages.savedGifs#2e0709a5 hash:int gifs:Vector<Document> = messages.SavedGifs;

@@ -1141,7 +1137,17 @@ stats.broadcastStats#bdf78394 period:StatsDateRangeDays followers:StatsAbsValueA
help.promoDataEmpty#98f6ac75 expires:int = help.PromoData;
help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector<Chat> users:Vector<User> psa_type:flags.1?string psa_message:flags.2?string = help.PromoData;

videoSize#435bb987 type:string location:FileLocation w:int h:int size:int = VideoSize;
videoSize#e831c556 flags:# type:string location:FileLocation w:int h:int size:int video_start_ts:flags.0?double = VideoSize;

statsGroupTopPoster#18f3d0f7 user_id:int messages:int avg_chars:int = StatsGroupTopPoster;

statsGroupTopAdmin#6014f412 user_id:int deleted:int kicked:int banned:int = StatsGroupTopAdmin;

statsGroupTopInviter#31962a4c user_id:int invitations:int = StatsGroupTopInviter;

stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector<StatsGroupTopPoster> top_admins:Vector<StatsGroupTopAdmin> top_inviters:Vector<StatsGroupTopInviter> users:Vector<User> = stats.MegagroupStats;

globalPrivacySettings#bea2f424 flags:# archive_and_mute_new_noncontact_peers:flags.0?Bool = GlobalPrivacySettings;

---functions---

@@ -1237,6 +1243,8 @@ account.getThemes#285946f8 format:string hash:int = account.Themes;
account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool;
account.getContentSettings#8b9b4dae = account.ContentSettings;
account.getMultiWallPapers#65ad71dc wallpapers:Vector<InputWallPaper> = Vector<WallPaper>;
account.getGlobalPrivacySettings#eb2b4cf6 = GlobalPrivacySettings;
account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = GlobalPrivacySettings;

users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
@@ -1312,7 +1320,6 @@ messages.migrateChat#15a3b8e3 chat_id:int = Updates;
messages.searchGlobal#bf7225a4 flags:# folder_id:flags.0?int q:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
messages.reorderStickerSets#78337739 flags:# masks:flags.0?true order:Vector<long> = Bool;
messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document;
messages.searchGifs#bf9a776b q:string offset:int = messages.FoundGifs;
messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs;
messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;
messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;
@@ -1392,7 +1399,7 @@ updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;

photos.updateProfilePhoto#f0bb5152 id:InputPhoto = UserProfilePhoto;
photos.uploadProfilePhoto#4f32c098 file:InputFile = photos.Photo;
photos.uploadProfilePhoto#89f30f69 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double = photos.Photo;
photos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;
photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;

@@ -1425,6 +1432,7 @@ help.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo;
help.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector<MessageEntity> = help.UserInfo;
help.getPromoData#c0977421 = help.PromoData;
help.hidePromoData#1e251c95 peer:InputPeer = Bool;
help.dismissSuggestion#77fa99f suggestion:string = Bool;

channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
@@ -1501,5 +1509,6 @@ folders.deleteFolder#1c295881 folder_id:int = Updates;

stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;

// LAYER 114
// LAYER 116
@@ -32,6 +32,7 @@ BOT_POLLS_DISABLED,400,You cannot create polls under a bot account
BOT_RESPONSE_TIMEOUT,400,The bot did not answer to the callback query in time
BROADCAST_ID_INVALID,400,The channel is invalid
BROADCAST_PUBLIC_VOTERS_FORBIDDEN,400,You cannot broadcast polls where the voters are public
BROADCAST_REQUIRED,400,The request can only be used with a broadcast channel
BUTTON_DATA_INVALID,400,The provided button data is invalid
BUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid
BUTTON_URL_INVALID,400,Button URL invalid
@@ -154,6 +155,7 @@ MEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as
MEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes)
MEGAGROUP_ID_INVALID,400,The group is invalid
MEGAGROUP_PREHISTORY_HIDDEN,400,You can't set this discussion group because it's history is hidden
MEGAGROUP_REQUIRED,400,The request can only be used with a megagroup channel
MEMBER_NO_LOCATION,500,An internal failure occurred while fetching user info (couldn't find location)
MEMBER_OCCUPY_PRIMARY_LOC_FAILED,500,Occupation of primary member location failed
MESSAGE_AUTHOR_REQUIRED,403,Message author required
@@ -223,6 +225,7 @@ POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too lon
POLL_QUESTION_INVALID,400,The poll question was either empty or too long
POLL_UNSUPPORTED,400,This layer does not support polls in the issued method
PRIVACY_KEY_INVALID,400,The privacy key is invalid
PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request
PTS_CHANGE_EMPTY,500,No PTS change
QUERY_ID_EMPTY,400,The query ID is empty
QUERY_ID_INVALID,400,The query ID is invalid
@@ -316,6 +319,7 @@ USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagr
USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this
USER_RESTRICTED,403,"You're spamreported, you can't create channels or chats."
VIDEO_CONTENT_TYPE_INVALID,400,The video content type is not supported with the given parameters (i.e. supports_streaming)
VIDEO_FILE_INVALID,400,The given video cannot be used
WALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper
WALLPAPER_INVALID,400,The input wallpaper was not valid
WC_CONVERT_URL_INVALID,400,WC convert URL invalid
@@ -50,7 +50,7 @@ account.sendVerifyPhoneCode,user,
account.setAccountTTL,user,TTL_DAYS_INVALID
account.setContactSignUpNotification,user,
account.setContentSettings,user,
account.setPrivacy,user,PRIVACY_KEY_INVALID
account.setPrivacy,user,PRIVACY_KEY_INVALID PRIVACY_TOO_LONG
account.unregisterDevice,user,TOKEN_INVALID
account.updateDeviceLocked,user,
account.updateNotifySettings,user,PEER_ID_INVALID
@@ -312,14 +312,15 @@ phone.setCallRating,user,CALL_PEER_INVALID
photos.deletePhotos,user,
photos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID
photos.updateProfilePhoto,user,
photos.uploadProfilePhoto,user,FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID
photos.uploadProfilePhoto,user,FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID VIDEO_FILE_INVALID
ping,both,
reqDHParams,both,
reqPq,both,
reqPqMulti,both,
rpcDropAnswer,both,
setClientDHParams,both,
stats.getBroadcastStats,user,
stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED STATS_MIGRATE_X
stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X
stats.loadAsyncGraph,user,
stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID
stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID
@@ -0,0 +1,13 @@
#!/bin/bash

python setup.py gen docs
rm -rf /tmp/docs
mv docs/ /tmp/docs
git checkout gh-pages
# there's probably better ways but we know none has spaces
rm -rf $(ls /tmp/docs)
mv /tmp/docs/* .
git add constructors/ types/ methods/ index.html js/search.js css/ img/
git commit --amend -m "Update documentation"
git push --force
git checkout master