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.
The ``TelegramClient`` doesn't offer a method for every single request
the Telegram API supports. However, it's very simple to *call* or *invoke*
any request. Whenever you need something, don't forget to `check the
documentation`__ and look for the `method you need`__. There you can go
The `telethon.telegram_client.TelegramClient` doesn't offer a method for every
single request the Telegram API supports. However, it's very simple to *call*
or *invoke* any request. Whenever you need something, don't forget to `check
the documentation`__ and look for the `method you need`__. There you can go
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
from a common type, and that's the reason for this distinction.
Say ``client.send_message()`` didn't exist, we could use the `search`__
to look for "message". There we would find :tl:`SendMessageRequest`,
which we can work with.
Say `telethon.telegram_client.TelegramClient.send_message` didn't exist,
we could use the `search`__ to look for "message". There we would find
:tl:`SendMessageRequest`, which we can work with.
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
@ -63,7 +63,7 @@ construct one, for instance:
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
@ -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
immediate, if you've seen the user before, know their ID, etc.).
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
@ -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
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
every time its used, simply call ``.get_input_peer``:
every time its used, simply call `telethon.utils.get_input_peer`:
.. code-block:: python

View File

@ -7,7 +7,7 @@ Session Files
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,
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
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
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:
.. code-block:: python

View File

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

View File

@ -2,14 +2,15 @@
Deleted, Limited or Deactivated Accounts
========================================
If you're from Iran or Russian, we have bad news for you.
Telegram is much more likely to ban these numbers,
as they are often used to spam other accounts,
likely through the use of libraries like this one.
The best advice we can give you is to not abuse the API,
like calling many requests really quickly,
If you're from Iran or Russia, we have bad news for you. Telegram is much more
likely to ban these numbers, as they are often used to spam other accounts,
likely through the use of libraries like this one. 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.
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,
as again, they're likely to be used for spam.

View File

@ -29,5 +29,12 @@ class MessageDeleted(EventBuilder):
super().__init__(
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_ids = deleted_ids

View File

@ -28,8 +28,16 @@ class NewMessage(EventBuilder):
"""
def __init__(self, incoming=None, outgoing=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:
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)
self.incoming = incoming

View File

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

View File

@ -963,7 +963,7 @@ class TelegramClient(TelegramBareClient):
Raises:
``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
not modified at all.
@ -1031,7 +1031,8 @@ class TelegramClient(TelegramBareClient):
async def iter_messages(self, entity, limit=20, offset_date=None,
offset_id=0, max_id=0, min_id=0, add_offset=0,
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.
@ -1059,7 +1060,7 @@ class TelegramClient(TelegramBareClient):
max_id (`int`):
All the messages with a higher (newer) ID or equal to this will
be excluded
be excluded.
min_id (`int`):
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
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):
A single-item list to pass the total parameter by reference.
@ -1110,6 +1120,23 @@ class TelegramClient(TelegramBareClient):
you think may be good.
"""
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)
if search is not None or filter or from_user:
if filter is None:
@ -1123,8 +1150,8 @@ class TelegramClient(TelegramBareClient):
offset_id=offset_id,
add_offset=add_offset,
limit=1,
max_id=max_id,
min_id=min_id,
max_id=0,
min_id=0,
hash=0,
from_id=self.get_input_entity(from_user) if from_user else None
)
@ -1134,8 +1161,8 @@ class TelegramClient(TelegramBareClient):
limit=1,
offset_date=offset_date,
offset_id=offset_id,
min_id=min_id,
max_id=max_id,
min_id=0,
max_id=0,
add_offset=add_offset,
hash=0
)
@ -1166,6 +1193,9 @@ class TelegramClient(TelegramBareClient):
for x in itertools.chain(r.users, r.chats)}
for message in r.messages:
if message.id <= min_id:
return
if isinstance(message, MessageEmpty) or message.id >= last_id:
continue
@ -1175,27 +1205,7 @@ class TelegramClient(TelegramBareClient):
# IDs are returned in descending order.
last_id = message.id
# Add a few extra attributes to the Message to be friendlier.
# 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)
)]
)
self._make_message_friendly(message, entities)
await yield_(message)
have += 1
@ -1210,18 +1220,92 @@ class TelegramClient(TelegramBareClient):
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):
"""
Same as :meth:`iter_messages`, but returns a list instead
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]
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()
async for msg in self.iter_messages(*args, **kwargs):
msgs.append(msg)
msgs.total = total[0]
if 'ids' in kwargs and not utils.is_list_like(kwargs['ids']):
return msgs[0]
return msgs
async def get_message_history(self, *args, **kwargs):
@ -1666,7 +1750,7 @@ class TelegramClient(TelegramBareClient):
if m.has('duration') else 0)
)
else:
doc = DocumentAttributeVideo(0, 0, 0,
doc = DocumentAttributeVideo(0, 1, 1,
round_message=video_note)
attr_dict[DocumentAttributeVideo] = doc
@ -2448,7 +2532,7 @@ class TelegramClient(TelegramBareClient):
async def catch_up(self):
state = self.session.get_update_state(0)
if not state:
if not state or not state.pts:
return
self.session.catching_up = True

View File

@ -552,8 +552,13 @@ def resolve_id(marked_id):
if marked_id >= 0:
return marked_id, PeerUser
if str(marked_id).startswith('-100'):
return int(str(marked_id)[4:]), PeerChannel
# There have been report of chat IDs being 10000xyz, which means their
# 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

View File

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

View File

@ -1,5 +1,9 @@
#!/usr/bin/env python3
# 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
@ -23,7 +27,7 @@ def main():
else:
client.start()
client.add_update_handler(update_handler)
client.add_event_handler(update_handler)
print('(Press Ctrl+C to stop this)')
client.idle()

View File

@ -2,9 +2,9 @@
"""
A example script to automatically send messages based on certain triggers.
The script makes uses of environment variables to determine the API ID,
hash, phone and such to be used. You may want to add these to your .bashrc
file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION.
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.
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.

View File

@ -4,7 +4,36 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Telethon API</title>
<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">
<style>
body {
@ -21,7 +50,10 @@
on what the methods, constructors and types mean. Nevertheless, this
page aims to provide easy access to all the available methods, their
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>
<pre>---functions---
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;
}
span.sh2 {
color: #0c7;
}
span.sh3 {
color: #aaa;
font-style: italic;
}
span.sh4 {
color: #06c;
}
span.tooltip {
border-bottom: 1px dashed #444;
}

View File

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

View File

@ -33,14 +33,15 @@ def get_import_code(tlobject):
.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."""
out_dir = 'methods' if tlobject.is_function else 'constructors'
if tlobject.namespace:
out_dir = os.path.join(out_dir, tlobject.namespace)
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))
@ -114,7 +115,9 @@ def _generate_index(folder, original_paths, root):
filename = os.path.join(folder, 'index.html')
with DocsWriter(filename, type_to_path=_get_path_for_type) as docs:
# 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'])
_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.
# TODO Tried using 'defaultdict(list)' with strange results, make it work.
original_paths = {
'css': 'css/docs.css',
'css': 'css',
'arrow': 'img/arrow.svg',
'search.js': 'js/search.js',
'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)
for k, v in original_paths.items()}
original_paths['default_css'] = 'light' # docs.<name>.css, local path
type_to_constructors = {}
type_to_functions = {}
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:
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)
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()}
with DocsWriter(filename, type_to_path=path_for_type) as docs:
docs.write_head(
title=snake_to_camel_case(name),
relative_css_path=paths['css'])
docs.write_head(title=snake_to_camel_case(name),
relative_css_path=paths['css'],
default_css=original_paths['default_css'])
docs.set_menu_separator(paths['arrow'])
_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)
# 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, '')
request_urls = fmt(methods, create_path_for)
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):
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)
os.makedirs(dirpath, exist_ok=True)
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
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:
builder.write(')')
if arg.is_vector: