Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-05-30 19:02:55 +02:00
commit cb75092ba1
20 changed files with 711 additions and 173 deletions

View File

@ -11,10 +11,10 @@ Accessing the Full API
reason not to, like a method not existing or you wanting more control. reason not to, like a method not existing or you wanting more control.
The ``TelegramClient`` doesn't offer a method for every single request The `telethon.telegram_client.TelegramClient` doesn't offer a method for every
the Telegram API supports. However, it's very simple to *call* or *invoke* single request the Telegram API supports. However, it's very simple to *call*
any request. Whenever you need something, don't forget to `check the or *invoke* any request. Whenever you need something, don't forget to `check
documentation`__ and look for the `method you need`__. There you can go the documentation`__ and look for the `method you need`__. There you can go
through a sorted list of everything you can do. through a sorted list of everything you can do.
@ -30,9 +30,9 @@ You should also refer to the documentation to see what the objects
(constructors) Telegram returns look like. Every constructor inherits (constructors) Telegram returns look like. Every constructor inherits
from a common type, and that's the reason for this distinction. from a common type, and that's the reason for this distinction.
Say ``client.send_message()`` didn't exist, we could use the `search`__ Say `telethon.telegram_client.TelegramClient.send_message` didn't exist,
to look for "message". There we would find :tl:`SendMessageRequest`, we could use the `search`__ to look for "message". There we would find
which we can work with. :tl:`SendMessageRequest`, which we can work with.
Every request is a Python class, and has the parameters needed for you Every request is a Python class, and has the parameters needed for you
to invoke it. You can also call ``help(request)`` for information on to invoke it. You can also call ``help(request)`` for information on
@ -63,7 +63,7 @@ construct one, for instance:
peer = InputPeerUser(user_id, user_hash) peer = InputPeerUser(user_id, user_hash)
Or we call ``.get_input_entity()``: Or we call `telethon.telegram_client.TelegramClient.get_input_entity()`:
.. code-block:: python .. code-block:: python
@ -74,7 +74,7 @@ When you're going to invoke an API method, most require you to pass an
``.get_input_entity()`` is more straightforward (and often ``.get_input_entity()`` is more straightforward (and often
immediate, if you've seen the user before, know their ID, etc.). immediate, if you've seen the user before, know their ID, etc.).
If you also need to have information about the whole user, use If you also need to have information about the whole user, use
``.get_entity()`` instead: `telethon.telegram_client.TelegramClient.get_entity()` instead:
.. code-block:: python .. code-block:: python
@ -83,7 +83,7 @@ If you also need to have information about the whole user, use
In the later case, when you use the entity, the library will cast it to In the later case, when you use the entity, the library will cast it to
its "input" version for you. If you already have the complete user and its "input" version for you. If you already have the complete user and
want to cache its input version so the library doesn't have to do this want to cache its input version so the library doesn't have to do this
every time its used, simply call ``.get_input_peer``: every time its used, simply call `telethon.utils.get_input_peer`:
.. code-block:: python .. code-block:: python

View File

@ -7,7 +7,7 @@ Session Files
The first parameter you pass to the constructor of the ``TelegramClient`` is The first parameter you pass to the constructor of the ``TelegramClient`` is
the ``session``, and defaults to be the session name (or full path). That is, the ``session``, and defaults to be the session name (or full path). That is,
if you create a ``TelegramClient('anon')`` instance and connect, an if you create a ``TelegramClient('anon')`` instance and connect, an
``anon.session`` file will be created on the working directory. ``anon.session`` file will be created in the working directory.
Note that if you pass a string it will be a file in the current working Note that if you pass a string it will be a file in the current working
directory, although you can also pass absolute paths. directory, although you can also pass absolute paths.

View File

@ -26,7 +26,7 @@ That's it! This is the old way to listen for raw updates, with no further
processing. If this feels annoying for you, remember that you can always processing. If this feels annoying for you, remember that you can always
use :ref:`working-with-updates` but maybe use this for some other cases. use :ref:`working-with-updates` but maybe use this for some other cases.
Now let's do something more interesting. Every time an user talks to use, Now let's do something more interesting. Every time an user talks to us,
let's reply to them with the same text reversed: let's reply to them with the same text reversed:
.. code-block:: python .. code-block:: python

View File

@ -58,8 +58,7 @@ Manual Installation
5. Done! 5. Done!
To generate the `method documentation`__, ``cd docs`` and then To generate the `method documentation`__, ``python3 setup.py gen docs``.
``python3 generate.py`` (if some pages render bad do it twice).
Optional dependencies Optional dependencies

View File

@ -2,14 +2,15 @@
Deleted, Limited or Deactivated Accounts Deleted, Limited or Deactivated Accounts
======================================== ========================================
If you're from Iran or Russian, we have bad news for you. If you're from Iran or Russia, we have bad news for you. Telegram is much more
Telegram is much more likely to ban these numbers, likely to ban these numbers, as they are often used to spam other accounts,
as they are often used to spam other accounts, likely through the use of libraries like this one. The best advice we can
likely through the use of libraries like this one. give you is to not abuse the API, like calling many requests really quickly,
The best advice we can give you is to not abuse the API,
like calling many requests really quickly,
and to sign up with these phones through an official application. and to sign up with these phones through an official application.
We have also had reports from Kazakhstan and China, where connecting
would fail. To solve these connection problems, you should use a proxy.
Telegram may also ban virtual (VoIP) phone numbers, Telegram may also ban virtual (VoIP) phone numbers,
as again, they're likely to be used for spam. as again, they're likely to be used for spam.

View File

@ -29,5 +29,12 @@ class MessageDeleted(EventBuilder):
super().__init__( super().__init__(
chat_peer=peer, msg_id=(deleted_ids or [0])[0] chat_peer=peer, msg_id=(deleted_ids or [0])[0]
) )
if peer is None:
# If it's not a channel ID, then it was private/small group.
# We can't know which one was exactly unless we logged all
# messages, but we can indicate that it was maybe either of
# both by setting them both to True.
self.is_private = self.is_group = True
self.deleted_id = None if not deleted_ids else deleted_ids[0] self.deleted_id = None if not deleted_ids else deleted_ids[0]
self.deleted_ids = deleted_ids self.deleted_ids = deleted_ids

View File

@ -28,8 +28,16 @@ class NewMessage(EventBuilder):
""" """
def __init__(self, incoming=None, outgoing=None, def __init__(self, incoming=None, outgoing=None,
chats=None, blacklist_chats=False, pattern=None): chats=None, blacklist_chats=False, pattern=None):
if incoming is not None and outgoing is None:
outgoing = not incoming
elif outgoing is not None and incoming is None:
incoming = not incoming
if incoming and outgoing: if incoming and outgoing:
raise ValueError('Can only set either incoming or outgoing') self.incoming = self.outgoing = None # Same as no filter
elif all(x is not None and not x for x in (incoming, outgoing)):
raise ValueError("Don't create an event handler if you "
"don't want neither incoming or outgoing!")
super().__init__(chats=chats, blacklist_chats=blacklist_chats) super().__init__(chats=chats, blacklist_chats=blacklist_chats)
self.incoming = incoming self.incoming = incoming

View File

@ -11,8 +11,7 @@ from ..tl.types import (
ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail
) )
from .. import helpers as utils from .. import helpers as utils
from ..crypto import AES, AuthKey, Factorization from ..crypto import AES, AuthKey, Factorization, rsa
from ..crypto import rsa
from ..errors import SecurityError from ..errors import SecurityError
from ..extensions import BinaryReader from ..extensions import BinaryReader
from ..network import MtProtoPlainSender from ..network import MtProtoPlainSender

View File

@ -963,7 +963,7 @@ class TelegramClient(TelegramBareClient):
Raises: Raises:
``MessageAuthorRequiredError`` if you're not the author of the ``MessageAuthorRequiredError`` if you're not the author of the
message but try editing it anyway. message but tried editing it anyway.
``MessageNotModifiedError`` if the contents of the message were ``MessageNotModifiedError`` if the contents of the message were
not modified at all. not modified at all.
@ -1031,7 +1031,8 @@ class TelegramClient(TelegramBareClient):
async def iter_messages(self, entity, limit=20, offset_date=None, async def iter_messages(self, entity, limit=20, offset_date=None,
offset_id=0, max_id=0, min_id=0, add_offset=0, offset_id=0, max_id=0, min_id=0, add_offset=0,
search=None, filter=None, from_user=None, search=None, filter=None, from_user=None,
batch_size=100, wait_time=None, _total=None): batch_size=100, wait_time=None, ids=None,
_total=None):
""" """
Iterator over the message history for the specified entity. Iterator over the message history for the specified entity.
@ -1059,7 +1060,7 @@ class TelegramClient(TelegramBareClient):
max_id (`int`): max_id (`int`):
All the messages with a higher (newer) ID or equal to this will All the messages with a higher (newer) ID or equal to this will
be excluded be excluded.
min_id (`int`): min_id (`int`):
All the messages with a lower (older) ID or equal to this will All the messages with a lower (older) ID or equal to this will
@ -1091,6 +1092,15 @@ class TelegramClient(TelegramBareClient):
If left to ``None``, it will default to 1 second only if If left to ``None``, it will default to 1 second only if
the limit is higher than 3000. the limit is higher than 3000.
ids (`int`, `list`):
A single integer ID (or several IDs) for the message that
should be returned. This parameter takes precedence over
the rest (which will be ignored if this is set). This can
for instance be used to get the message with ID 123 from
a channel. Note that if the message doesn't exist, ``None``
will appear in its place, so that zipping the list of IDs
with the messages can match one-to-one.
_total (`list`, optional): _total (`list`, optional):
A single-item list to pass the total parameter by reference. A single-item list to pass the total parameter by reference.
@ -1110,6 +1120,23 @@ class TelegramClient(TelegramBareClient):
you think may be good. you think may be good.
""" """
entity = await self.get_input_entity(entity) entity = await self.get_input_entity(entity)
if ids:
if not utils.is_list_like(ids):
ids = (ids,)
async for x in self._iter_ids(entity, ids, total=_total):
await yield_(x)
return
# Telegram doesn't like min_id/max_id. If these IDs are low enough
# (starting from last_id - 100), the request will return nothing.
#
# We can emulate their behaviour locally by setting offset = max_id
# and simply stopping once we hit a message with ID <= min_id.
offset_id = max(offset_id, max_id)
if offset_id and min_id:
if offset_id - min_id <= 1:
return
limit = float('inf') if limit is None else int(limit) limit = float('inf') if limit is None else int(limit)
if search is not None or filter or from_user: if search is not None or filter or from_user:
if filter is None: if filter is None:
@ -1123,8 +1150,8 @@ class TelegramClient(TelegramBareClient):
offset_id=offset_id, offset_id=offset_id,
add_offset=add_offset, add_offset=add_offset,
limit=1, limit=1,
max_id=max_id, max_id=0,
min_id=min_id, min_id=0,
hash=0, hash=0,
from_id=self.get_input_entity(from_user) if from_user else None from_id=self.get_input_entity(from_user) if from_user else None
) )
@ -1134,8 +1161,8 @@ class TelegramClient(TelegramBareClient):
limit=1, limit=1,
offset_date=offset_date, offset_date=offset_date,
offset_id=offset_id, offset_id=offset_id,
min_id=min_id, min_id=0,
max_id=max_id, max_id=0,
add_offset=add_offset, add_offset=add_offset,
hash=0 hash=0
) )
@ -1166,6 +1193,9 @@ class TelegramClient(TelegramBareClient):
for x in itertools.chain(r.users, r.chats)} for x in itertools.chain(r.users, r.chats)}
for message in r.messages: for message in r.messages:
if message.id <= min_id:
return
if isinstance(message, MessageEmpty) or message.id >= last_id: if isinstance(message, MessageEmpty) or message.id >= last_id:
continue continue
@ -1175,27 +1205,7 @@ class TelegramClient(TelegramBareClient):
# IDs are returned in descending order. # IDs are returned in descending order.
last_id = message.id last_id = message.id
# Add a few extra attributes to the Message to be friendlier. self._make_message_friendly(message, entities)
# To make messages more friendly, always add message
# to service messages, and action to normal messages.
message.message = getattr(message, 'message', None)
message.action = getattr(message, 'action', None)
message.to = entities[utils.get_peer_id(message.to_id)]
message.sender = (
None if not message.from_id else
entities[utils.get_peer_id(message.from_id)]
)
if getattr(message, 'fwd_from', None):
message.fwd_from.sender = (
None if not message.fwd_from.from_id else
entities[utils.get_peer_id(message.fwd_from.from_id)]
)
message.fwd_from.channel = (
None if not message.fwd_from.channel_id else
entities[utils.get_peer_id(
PeerChannel(message.fwd_from.channel_id)
)]
)
await yield_(message) await yield_(message)
have += 1 have += 1
@ -1210,18 +1220,92 @@ class TelegramClient(TelegramBareClient):
await asyncio.sleep(max(wait_time - (time.time() - start), 0)) await asyncio.sleep(max(wait_time - (time.time() - start), 0))
@staticmethod
def _make_message_friendly(message, entities):
"""
Add a few extra attributes to the :tl:`Message` to be friendlier.
To make messages more friendly, always add message
to service messages, and action to normal messages.
"""
# TODO Create an actual friendlier class
message.message = getattr(message, 'message', None)
message.action = getattr(message, 'action', None)
message.to = entities[utils.get_peer_id(message.to_id)]
message.sender = (
None if not message.from_id else
entities[utils.get_peer_id(message.from_id)]
)
if getattr(message, 'fwd_from', None):
message.fwd_from.sender = (
None if not message.fwd_from.from_id else
entities[utils.get_peer_id(message.fwd_from.from_id)]
)
message.fwd_from.channel = (
None if not message.fwd_from.channel_id else
entities[utils.get_peer_id(
PeerChannel(message.fwd_from.channel_id)
)]
)
@async_generator
async def _iter_ids(self, entity, ids, total):
"""
Special case for `iter_messages` when it should only fetch some IDs.
"""
if total:
total[0] = len(ids)
if isinstance(entity, InputPeerChannel):
r = await self(channels.GetMessagesRequest(entity, ids))
else:
r = await self(messages.GetMessagesRequest(ids))
entities = {utils.get_peer_id(x): x
for x in itertools.chain(r.users, r.chats)}
# Telegram seems to return the messages in the order in which
# we asked them for, so we don't need to check it ourselves.
for message in r.messages:
if isinstance(message, MessageEmpty):
await yield_(None)
else:
self._make_message_friendly(message, entities)
await yield_(message)
async def get_messages(self, *args, **kwargs): async def get_messages(self, *args, **kwargs):
""" """
Same as :meth:`iter_messages`, but returns a list instead Same as :meth:`iter_messages`, but returns a list instead
with an additional ``.total`` attribute on the list. with an additional ``.total`` attribute on the list.
If the `limit` is not set, it will be 1 by default unless both
`min_id` **and** `max_id` are set (as *named* arguments), in
which case the entire range will be returned.
This is so because any integer limit would be rather arbitrary and
it's common to only want to fetch one message, but if a range is
specified it makes sense that it should return the entirety of it.
If `ids` is present in the *named* arguments and is not a list,
a single :tl:`Message` will be returned for convenience instead
of a list.
""" """
total = [0] total = [0]
kwargs['_total'] = total kwargs['_total'] = total
if len(args) == 1 and 'limit' not in kwargs:
if 'min_id' in kwargs and 'max_id' in kwargs:
kwargs['limit'] = None
else:
kwargs['limit'] = 1
msgs = UserList() msgs = UserList()
async for msg in self.iter_messages(*args, **kwargs): async for msg in self.iter_messages(*args, **kwargs):
msgs.append(msg) msgs.append(msg)
msgs.total = total[0] msgs.total = total[0]
if 'ids' in kwargs and not utils.is_list_like(kwargs['ids']):
return msgs[0]
return msgs return msgs
async def get_message_history(self, *args, **kwargs): async def get_message_history(self, *args, **kwargs):
@ -1666,7 +1750,7 @@ class TelegramClient(TelegramBareClient):
if m.has('duration') else 0) if m.has('duration') else 0)
) )
else: else:
doc = DocumentAttributeVideo(0, 0, 0, doc = DocumentAttributeVideo(0, 1, 1,
round_message=video_note) round_message=video_note)
attr_dict[DocumentAttributeVideo] = doc attr_dict[DocumentAttributeVideo] = doc
@ -2448,7 +2532,7 @@ class TelegramClient(TelegramBareClient):
async def catch_up(self): async def catch_up(self):
state = self.session.get_update_state(0) state = self.session.get_update_state(0)
if not state: if not state or not state.pts:
return return
self.session.catching_up = True self.session.catching_up = True

View File

@ -552,8 +552,13 @@ def resolve_id(marked_id):
if marked_id >= 0: if marked_id >= 0:
return marked_id, PeerUser return marked_id, PeerUser
if str(marked_id).startswith('-100'): # There have been report of chat IDs being 10000xyz, which means their
return int(str(marked_id)[4:]), PeerChannel # marked version is -10000xyz, which in turn looks like a channel but
# it becomes 00xyz (= xyz). Hence, we must assert that there are only
# two zeroes.
m = re.match(r'-100([^0]\d*)', str(marked_id))
if m:
return int(m.group(1)), PeerChannel
return -marked_id, PeerChat return -marked_id, PeerChat

View File

@ -3,7 +3,8 @@ from getpass import getpass
from telethon.utils import get_display_name from telethon.utils import get_display_name
from telethon import ConnectionMode, TelegramClient from telethon import TelegramClient, events
from telethon.network import ConnectionTcpAbridged
from telethon.errors import SessionPasswordNeededError from telethon.errors import SessionPasswordNeededError
from telethon.tl.types import ( from telethon.tl.types import (
PeerChat, UpdateShortChatMessage, UpdateShortMessage PeerChat, UpdateShortChatMessage, UpdateShortMessage
@ -70,11 +71,11 @@ class InteractiveTelegramClient(TelegramClient):
# These parameters should be passed always, session name and API # These parameters should be passed always, session name and API
session_user_id, api_id, api_hash, session_user_id, api_id, api_hash,
# You can optionally change the connection mode by using this enum. # You can optionally change the connection mode by passing a
# This changes how much data will be sent over the network with # type or an instance of it. This changes how the sent packets
# every request, and how it will be formatted. Default is # look (low-level concept you normally shouldn't worry about).
# ConnectionMode.TCP_FULL, and smallest is TCP_TCP_ABRIDGED. # Default is ConnectionTcpFull, smallest is ConnectionTcpAbridged.
connection_mode=ConnectionMode.TCP_ABRIDGED, connection=ConnectionTcpAbridged,
# If you're using a proxy, set it here. # If you're using a proxy, set it here.
proxy=proxy, proxy=proxy,
@ -126,10 +127,11 @@ class InteractiveTelegramClient(TelegramClient):
def run(self): def run(self):
"""Main loop of the TelegramClient, will wait for user action""" """Main loop of the TelegramClient, will wait for user action"""
# Once everything is ready, we can add an update handler. Every # Once everything is ready, we can add an event handler.
# update object will be passed to the self.update_handler method, #
# where we can process it as we need. # Events are an abstraction over Telegram's "Updates" and
self.add_update_handler(self.update_handler) # are much easier to use.
self.add_event_handler(self.message_handler, events.NewMessage)
# Enter a while loop to chat as long as the user wants # Enter a while loop to chat as long as the user wants
while True: while True:
@ -334,31 +336,29 @@ class InteractiveTelegramClient(TelegramClient):
bytes_to_string(total_bytes), downloaded_bytes / total_bytes) bytes_to_string(total_bytes), downloaded_bytes / total_bytes)
) )
def update_handler(self, update): def message_handler(self, event):
"""Callback method for received Updates""" """Callback method for received events.NewMessage"""
# We have full control over what we want to do with the updates. # Note that accessing ``.sender`` and ``.chat`` may be slow since
# In our case we only want to react to chat messages, so we use # these are not cached and must be queried always! However it lets
# isinstance() to behave accordingly on these cases. # us access the chat title and user name.
if isinstance(update, UpdateShortMessage): if event.is_group:
who = self.get_entity(update.user_id) if event.out:
if update.out: sprint('>> sent "{}" to chat {}'.format(
event.text, get_display_name(event.chat)
))
else:
sprint('<< {} @ {} sent "{}"'.format(
get_display_name(event.sender),
get_display_name(event.chat),
event.text
))
else:
if event.out:
sprint('>> "{}" to user {}'.format( sprint('>> "{}" to user {}'.format(
update.message, get_display_name(who) event.text, get_display_name(event.chat)
)) ))
else: else:
sprint('<< {} sent "{}"'.format( sprint('<< {} sent "{}"'.format(
get_display_name(who), update.message get_display_name(event.chat), event.text
))
elif isinstance(update, UpdateShortChatMessage):
which = self.get_entity(PeerChat(update.chat_id))
if update.out:
sprint('>> sent "{}" to chat {}'.format(
update.message, get_display_name(which)
))
else:
who = self.get_entity(update.from_id)
sprint('<< {} @ {} sent "{}"'.format(
get_display_name(which), get_display_name(who), update.message
)) ))

View File

@ -1,5 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# A simple script to print all updates received # A simple script to print all updates received
#
# NOTE: To run this script you MUST have 'TG_API_ID' and 'TG_API_HASH' in
# your environment variables. This is a good way to use these private
# values. See https://superuser.com/q/284342.
from os import environ from os import environ
@ -23,7 +27,7 @@ def main():
else: else:
client.start() client.start()
client.add_update_handler(update_handler) client.add_event_handler(update_handler)
print('(Press Ctrl+C to stop this)') print('(Press Ctrl+C to stop this)')
client.idle() client.idle()

View File

@ -2,9 +2,9 @@
""" """
A example script to automatically send messages based on certain triggers. A example script to automatically send messages based on certain triggers.
The script makes uses of environment variables to determine the API ID, NOTE: To run this script you MUST have 'TG_API_ID' and 'TG_API_HASH' in
hash, phone and such to be used. You may want to add these to your .bashrc your environment variables. This is a good way to use these private
file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION. values. See https://superuser.com/q/284342.
This script assumes that you have certain files on the working directory, This script assumes that you have certain files on the working directory,
such as "xfiles.m4a" or "anytime.png" for some of the automated replies. such as "xfiles.m4a" or "anytime.png" for some of the automated replies.

View File

@ -4,7 +4,36 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Telethon API</title> <title>Telethon API</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="css/docs.css" rel="stylesheet"> <link id="style" href="css/docs.light.css" rel="stylesheet">
<script>
(function () {
var style = document.getElementById('style');
// setTheme(<link />, 'light' / 'dark')
function setTheme(theme) {
document.cookie = 'css=' + theme + '; path=/';
return style.href = 'css/docs.' + theme + '.css';
}
// setThemeOnClick(<link />, 'light' / 'dark', <a />)
function setThemeOnClick(theme, button) {
return button.addEventListener('click', function (e) {
setTheme(theme);
e.preventDefault();
return false;
});
}
setTheme(document.cookie
.split(';')[0]
.split('=')[1] || 'light');
document.addEventListener('DOMContentLoaded', function () {
setThemeOnClick('light', document.getElementById('themeLight'));
setThemeOnClick('dark', document.getElementById('themeDark'));
});
})();
</script>
<link href="https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro" rel="stylesheet">
<style> <style>
body { body {
@ -21,7 +50,10 @@
on what the methods, constructors and types mean. Nevertheless, this on what the methods, constructors and types mean. Nevertheless, this
page aims to provide easy access to all the available methods, their page aims to provide easy access to all the available methods, their
definition and parameters.</p> definition and parameters.</p>
<p id="themeSelect">
<a href="#" id="themeLight">light</a> /
<a href="#" id="themeDark">dark</a> theme.
</p>
<p>Please note that when you see this:</p> <p>Please note that when you see this:</p>
<pre>---functions--- <pre>---functions---
users.getUsers#0d91a548 id:Vector&lt;InputUser&gt; = Vector&lt;User&gt;</pre> users.getUsers#0d91a548 id:Vector&lt;InputUser&gt; = Vector&lt;User&gt;</pre>

View File

@ -0,0 +1,185 @@
body {
font-family: 'Nunito', sans-serif;
color: #bbb;
background-color:#000;
font-size: 16px;
}
a {
color: #42aaed;
text-decoration: none;
}
pre {
font-family: 'Source Code Pro', monospace;
padding: 8px;
color: #567;
background: #080a0c;
border-radius: 0;
overflow-x: auto;
}
a:hover {
color: #64bbdd;
text-decoration: underline;
}
table {
width: 100%;
max-width: 100%;
}
table td {
border-top: 1px solid #111;
padding: 8px;
}
.horizontal {
margin-bottom: 16px;
list-style: none;
background: #080a0c;
border-radius: 4px;
padding: 8px 16px;
}
.horizontal li {
display: inline-block;
margin: 0 8px 0 0;
}
.horizontal img {
display: inline-block;
margin: 0 8px -2px 0;
}
h1, summary.title {
font-size: 24px;
}
h3 {
font-size: 20px;
}
#main_div {
padding: 20px 0;
max-width: 800px;
margin: 0 auto;
}
pre::-webkit-scrollbar {
visibility: visible;
display: block;
height: 12px;
}
pre::-webkit-scrollbar-track:horizontal {
background: #222;
border-radius: 0;
height: 12px;
}
pre::-webkit-scrollbar-thumb:horizontal {
background: #444;
border-radius: 0;
height: 12px;
}
:target {
border: 2px solid #149;
background: #246;
padding: 4px;
}
/* 'sh' stands for Syntax Highlight */
span.sh1 {
color: #f93;
}
span.tooltip {
border-bottom: 1px dashed #ddd;
}
#searchBox {
width: 100%;
border: none;
height: 20px;
padding: 8px;
font-size: 16px;
border-radius: 2px;
border: 2px solid #222;
background: #000;
color: #eee;
}
#searchBox:placeholder-shown {
color: #bbb;
font-style: italic;
}
button {
border-radius: 2px;
font-size: 16px;
padding: 8px;
color: #bbb;
background-color: #111;
border: 2px solid #146;
transition-duration: 300ms;
}
button:hover {
background-color: #146;
color: #fff;
}
/* https://www.w3schools.com/css/css_navbar.asp */
ul.together {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
ul.together li {
float: left;
}
ul.together li a {
display: block;
border-radius: 8px;
background: #111;
padding: 4px 8px;
margin: 8px;
}
/* https://stackoverflow.com/a/30810322 */
.invisible {
left: 0;
top: -99px;
padding: 0;
width: 2em;
height: 2em;
border: none;
outline: none;
position: fixed;
box-shadow: none;
color: transparent;
background: transparent;
}
@media (max-width: 640px) {
h1, summary.title {
font-size: 18px;
}
h3 {
font-size: 16px;
}
#dev_page_content_wrap {
padding-top: 12px;
}
#dev_page_title {
margin-top: 10px;
margin-bottom: 20px;
}
}

View File

@ -0,0 +1,229 @@
/* Begin of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css
*
* Hack typeface https://github.com/source-foundry/Hack
* License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md
*/
@font-face {
font-family: 'Hack';
src: url('fonts/hack-regular.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular.woff?sha=3114f1256') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-bold.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold.woff?sha=3114f1256') format('woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-italic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic.woff?sha=3114f1256') format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-bolditalic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic.woff?sha=3114f1256') format('woff');
font-weight: 700;
font-style: italic;
}
/* End of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css */
body {
font-family: 'Hack', monospace;
color: #0a0;
background-color: #000;
font-size: 16px;
}
::-moz-selection {
color: #000;
background: #0a0;
}
::selection {
color: #000;
background: #0a0;
}
a {
color: #0a0;
}
pre {
padding: 8px;
color: #0c0;
background: #010;
border-radius: 0;
overflow-x: auto;
}
a:hover {
color: #0f0;
text-decoration: underline;
}
table {
width: 100%;
max-width: 100%;
}
table td {
border-top: 1px solid #111;
padding: 8px;
}
.horizontal {
margin-bottom: 16px;
list-style: none;
background: #010;
border-radius: 4px;
padding: 8px 16px;
}
.horizontal li {
display: inline-block;
margin: 0 8px 0 0;
}
.horizontal img {
opacity: 0;
display: inline-block;
margin: 0 8px -2px 0;
}
h1, summary.title {
font-size: 24px;
}
h3 {
font-size: 20px;
}
#main_div {
padding: 20px 0;
max-width: 800px;
margin: 0 auto;
}
pre::-webkit-scrollbar {
visibility: visible;
display: block;
height: 12px;
}
pre::-webkit-scrollbar-track:horizontal {
background: #222;
border-radius: 0;
height: 12px;
}
pre::-webkit-scrollbar-thumb:horizontal {
background: #444;
border-radius: 0;
height: 12px;
}
:target {
border: 2px solid #0f0;
background: #010;
padding: 4px;
}
/* 'sh' stands for Syntax Highlight */
span.sh1 {
color: #0f0;
}
span.tooltip {
border-bottom: 1px dashed #ddd;
}
#searchBox {
width: 100%;
border: none;
height: 20px;
padding: 8px;
font-size: 16px;
border-radius: 2px;
border: 2px solid #222;
background: #000;
color: #0e0;
font-family: 'Hack', monospace;
}
#searchBox:placeholder-shown {
color: #0b0;
font-style: italic;
}
button {
font-size: 16px;
padding: 8px;
color: #0f0;
background-color: #071007;
border: 2px solid #131;
transition-duration: 300ms;
font-family: 'Hack', monospace;
}
button:hover {
background-color: #131;
}
/* https://www.w3schools.com/css/css_navbar.asp */
ul.together {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
}
ul.together li {
float: left;
}
ul.together li a {
display: block;
border-radius: 8px;
background: #121;
padding: 4px 8px;
margin: 8px;
}
/* https://stackoverflow.com/a/30810322 */
.invisible {
left: 0;
top: -99px;
padding: 0;
width: 2em;
height: 2em;
border: none;
outline: none;
position: fixed;
box-shadow: none;
color: transparent;
background: transparent;
}
@media (max-width: 640px) {
h1, summary.title {
font-size: 18px;
}
h3 {
font-size: 16px;
}
#dev_page_content_wrap {
padding-top: 12px;
}
#dev_page_title {
margin-top: 10px;
margin-bottom: 20px;
}
}

View File

@ -95,19 +95,6 @@ span.sh1 {
color: #f70; color: #f70;
} }
span.sh2 {
color: #0c7;
}
span.sh3 {
color: #aaa;
font-style: italic;
}
span.sh4 {
color: #06c;
}
span.tooltip { span.tooltip {
border-bottom: 1px dashed #444; border-bottom: 1px dashed #444;
} }

View File

@ -31,29 +31,32 @@ class DocsWriter:
self._script = '' self._script = ''
# High level writing # High level writing
def write_head(self, title, relative_css_path): def write_head(self, title, relative_css_path, default_css):
"""Writes the head part for the generated document, """Writes the head part for the generated document,
with the given title and CSS with the given title and CSS
""" """
self.write('''<!DOCTYPE html> self.write(
'''<!DOCTYPE html>
<html> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>''') <title>{title}</title>
self.write(title)
self.write('''</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="''') <link id="style" href="{rel_css}/docs.{def_css}.css" rel="stylesheet">
<script>
self.write(relative_css_path) document.getElementById("style").href = "{rel_css}/docs."
+ (document.cookie.split(";")[0].split("=")[1] || "{def_css}")
self.write('''" rel="stylesheet"> + ".css";
<link href="https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro" rel="stylesheet"> </script>
<link href="https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro"
rel="stylesheet">
</head> </head>
<body> <body>
<div id="main_div">''') <div id="main_div">''',
title=title,
rel_css=relative_css_path.rstrip('/'),
def_css=default_css
)
def set_menu_separator(self, relative_image_path): def set_menu_separator(self, relative_image_path):
"""Sets the menu separator. """Sets the menu separator.
@ -77,9 +80,7 @@ class DocsWriter:
self.write('<li>') self.write('<li>')
if link: if link:
self.write('<a href="') self.write('<a href="{}">', link)
self.write(link)
self.write('">')
# Write the real menu entry text # Write the real menu entry text
self.write(name) self.write(name)
@ -98,26 +99,21 @@ class DocsWriter:
"""Writes a title header in the document body, """Writes a title header in the document body,
with an optional depth level with an optional depth level
""" """
self.write('<h%d>' % level) self.write('<h{level}>{title}</h{level}>', title=title, level=level)
self.write(title)
self.write('</h%d>' % level)
def write_code(self, tlobject): def write_code(self, tlobject):
"""Writes the code for the given 'tlobject' properly """Writes the code for the given 'tlobject' properly
formatted with hyperlinks formatted with hyperlinks
""" """
self.write('<pre>---') self.write('<pre>---{}---\n',
self.write('functions' if tlobject.is_function else 'types') 'functions' if tlobject.is_function else 'types')
self.write('---\n')
# Write the function or type and its ID # Write the function or type and its ID
if tlobject.namespace: if tlobject.namespace:
self.write(tlobject.namespace) self.write(tlobject.namespace)
self.write('.') self.write('.')
self.write(tlobject.name) self.write('{}#{:08x}', tlobject.name, tlobject.id)
self.write('#')
self.write(hex(tlobject.id)[2:].rjust(8, '0'))
# Write all the arguments (or do nothing if there's none) # Write all the arguments (or do nothing if there's none)
for arg in tlobject.args: for arg in tlobject.args:
@ -134,20 +130,19 @@ class DocsWriter:
# "Opening" modifiers # "Opening" modifiers
if arg.is_flag: if arg.is_flag:
self.write('flags.%d?' % arg.flag_index) self.write('flags.{}?', arg.flag_index)
if arg.is_generic: if arg.is_generic:
self.write('!') self.write('!')
if arg.is_vector: if arg.is_vector:
self.write( self.write('<a href="{}">Vector</a>&lt;',
'<a href="%s">Vector</a>&lt;' % self.type_to_path('vector') self.type_to_path('vector'))
)
# Argument type # Argument type
if arg.type: if arg.type:
if add_link: if add_link:
self.write('<a href="%s">' % self.type_to_path(arg.type)) self.write('<a href="{}">', self.type_to_path(arg.type))
self.write(arg.type) self.write(arg.type)
if add_link: if add_link:
self.write('</a>') self.write('</a>')
@ -176,19 +171,14 @@ class DocsWriter:
# use a lower type name for it (see #81) # use a lower type name for it (see #81)
vector, inner = tlobject.result.split('<') vector, inner = tlobject.result.split('<')
inner = inner.strip('>') inner = inner.strip('>')
self.write('<a href="') self.write('<a href="{}">{}</a>&lt;',
self.write(self.type_to_path(vector)) self.type_to_path(vector), vector)
self.write('">%s</a>&lt;' % vector)
self.write('<a href="') self.write('<a href="{}">{}</a>&gt;',
self.write(self.type_to_path(inner)) self.type_to_path(inner), inner)
self.write('">%s</a>' % inner)
self.write('&gt;')
else: else:
self.write('<a href="') self.write('<a href="{}">{}</a>',
self.write(self.type_to_path(tlobject.result)) self.type_to_path(tlobject.result), tlobject.result)
self.write('">%s</a>' % tlobject.result)
self.write('</pre>') self.write('</pre>')
@ -209,17 +199,13 @@ class DocsWriter:
self.write('<td') self.write('<td')
if align: if align:
self.write(' style="text-align:') self.write(' style="text-align:{}"', align)
self.write(align)
self.write('"')
self.write('>') self.write('>')
if bold: if bold:
self.write('<b>') self.write('<b>')
if link: if link:
self.write('<a href="') self.write('<a href="{}">', link)
self.write(link)
self.write('">')
# Finally write the real table data, the given text # Finally write the real table data, the given text
self.write(text) self.write(text)
@ -244,9 +230,7 @@ class DocsWriter:
def write_text(self, text): def write_text(self, text):
"""Writes a paragraph of text""" """Writes a paragraph of text"""
self.write('<p>') self.write('<p>{}</p>', text)
self.write(text)
self.write('</p>')
def write_copy_button(self, text, text_to_copy): def write_copy_button(self, text, text_to_copy):
"""Writes a button with 'text' which can be used """Writes a button with 'text' which can be used
@ -273,16 +257,18 @@ class DocsWriter:
'c.select();' 'c.select();'
'try{document.execCommand("copy")}' 'try{document.execCommand("copy")}'
'catch(e){}}' 'catch(e){}}'
'</script>') '</script>'
)
self.write('</div>') self.write('</div>{}</body></html>', self._script)
self.write(self._script)
self.write('</body></html>')
# "Low" level writing # "Low" level writing
def write(self, s): def write(self, s, *args, **kwargs):
"""Wrapper around handle.write""" """Wrapper around handle.write"""
self.handle.write(s) if args or kwargs:
self.handle.write(s.format(*args, **kwargs))
else:
self.handle.write(s)
# With block # With block
def __enter__(self): def __enter__(self):

View File

@ -33,14 +33,15 @@ def get_import_code(tlobject):
.format(kind, ns, tlobject.class_name) .format(kind, ns, tlobject.class_name)
def _get_create_path_for(root, tlobject): def _get_create_path_for(root, tlobject, make=True):
"""Creates and returns the path for the given TLObject at root.""" """Creates and returns the path for the given TLObject at root."""
out_dir = 'methods' if tlobject.is_function else 'constructors' out_dir = 'methods' if tlobject.is_function else 'constructors'
if tlobject.namespace: if tlobject.namespace:
out_dir = os.path.join(out_dir, tlobject.namespace) out_dir = os.path.join(out_dir, tlobject.namespace)
out_dir = os.path.join(root, out_dir) out_dir = os.path.join(root, out_dir)
os.makedirs(out_dir, exist_ok=True) if make:
os.makedirs(out_dir, exist_ok=True)
return os.path.join(out_dir, _get_file_name(tlobject)) return os.path.join(out_dir, _get_file_name(tlobject))
@ -114,7 +115,9 @@ def _generate_index(folder, original_paths, root):
filename = os.path.join(folder, 'index.html') filename = os.path.join(folder, 'index.html')
with DocsWriter(filename, type_to_path=_get_path_for_type) as docs: with DocsWriter(filename, type_to_path=_get_path_for_type) as docs:
# Title should be the current folder name # Title should be the current folder name
docs.write_head(folder.title(), relative_css_path=paths['css']) docs.write_head(folder.title(),
relative_css_path=paths['css'],
default_css=original_paths['default_css'])
docs.set_menu_separator(paths['arrow']) docs.set_menu_separator(paths['arrow'])
_build_menu(docs, filename, root, _build_menu(docs, filename, root,
@ -206,7 +209,7 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
# * Generating the types documentation, showing available constructors. # * Generating the types documentation, showing available constructors.
# TODO Tried using 'defaultdict(list)' with strange results, make it work. # TODO Tried using 'defaultdict(list)' with strange results, make it work.
original_paths = { original_paths = {
'css': 'css/docs.css', 'css': 'css',
'arrow': 'img/arrow.svg', 'arrow': 'img/arrow.svg',
'search.js': 'js/search.js', 'search.js': 'js/search.js',
'404': '404.html', '404': '404.html',
@ -218,6 +221,7 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
original_paths = {k: os.path.join(output_dir, v) original_paths = {k: os.path.join(output_dir, v)
for k, v in original_paths.items()} for k, v in original_paths.items()}
original_paths['default_css'] = 'light' # docs.<name>.css, local path
type_to_constructors = {} type_to_constructors = {}
type_to_functions = {} type_to_functions = {}
for tlobject in tlobjects: for tlobject in tlobjects:
@ -251,7 +255,8 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
with DocsWriter(filename, type_to_path=path_for_type) as docs: with DocsWriter(filename, type_to_path=path_for_type) as docs:
docs.write_head(title=tlobject.class_name, docs.write_head(title=tlobject.class_name,
relative_css_path=paths['css']) relative_css_path=paths['css'],
default_css=original_paths['default_css'])
# Create the menu (path to the current TLObject) # Create the menu (path to the current TLObject)
docs.set_menu_separator(paths['arrow']) docs.set_menu_separator(paths['arrow'])
@ -392,9 +397,9 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
for k, v in original_paths.items()} for k, v in original_paths.items()}
with DocsWriter(filename, type_to_path=path_for_type) as docs: with DocsWriter(filename, type_to_path=path_for_type) as docs:
docs.write_head( docs.write_head(title=snake_to_camel_case(name),
title=snake_to_camel_case(name), relative_css_path=paths['css'],
relative_css_path=paths['css']) default_css=original_paths['default_css'])
docs.set_menu_separator(paths['arrow']) docs.set_menu_separator(paths['arrow'])
_build_menu(docs, filename, output_dir, _build_menu(docs, filename, output_dir,
@ -549,7 +554,7 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
type_names = fmt(types, formatter=lambda x: x) type_names = fmt(types, formatter=lambda x: x)
# Local URLs shouldn't rely on the output's root, so set empty root # Local URLs shouldn't rely on the output's root, so set empty root
create_path_for = functools.partial(_get_create_path_for, '') create_path_for = functools.partial(_get_create_path_for, '', make=False)
path_for_type = functools.partial(_get_path_for_type, '') path_for_type = functools.partial(_get_path_for_type, '')
request_urls = fmt(methods, create_path_for) request_urls = fmt(methods, create_path_for)
type_urls = fmt(types, path_for_type) type_urls = fmt(types, path_for_type)
@ -570,7 +575,8 @@ def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
def _copy_resources(res_dir, out_dir): def _copy_resources(res_dir, out_dir):
for dirname, files in [('css', ['docs.css']), ('img', ['arrow.svg'])]: for dirname, files in [('css', ['docs.light.css', 'docs.dark.css']),
('img', ['arrow.svg'])]:
dirpath = os.path.join(out_dir, dirname) dirpath = os.path.join(out_dir, dirname)
os.makedirs(dirpath, exist_ok=True) os.makedirs(dirpath, exist_ok=True)
for file in files: for file in files:

View File

@ -477,6 +477,12 @@ def _write_arg_to_bytes(builder, arg, args, name=None):
# Else it may be a custom type # Else it may be a custom type
builder.write('bytes({})', name) builder.write('bytes({})', name)
# If the type is not boxed (i.e. starts with lowercase) we should
# not serialize the constructor ID (so remove its first 4 bytes).
boxed = arg.type[arg.type.find('.') + 1].isupper()
if not boxed:
builder.write('[4:]')
if arg.is_flag: if arg.is_flag:
builder.write(')') builder.write(')')
if arg.is_vector: if arg.is_vector: