diff --git a/setup.py b/setup.py index 2058924f..3f8ee7a6 100755 --- a/setup.py +++ b/setup.py @@ -15,16 +15,11 @@ Extra supported commands are: from codecs import open from sys import argv import os +import re # Always prefer setuptools over distutils from setuptools import find_packages, setup -try: - from telethon import TelegramClient -except Exception as e: - print('Failed to import TelegramClient due to', e) - TelegramClient = None - class TempWorkDir: """Switches the working directory to be the one on which this file lives, @@ -94,21 +89,15 @@ def main(): fetch_errors(ERRORS_JSON) else: - if not TelegramClient: - gen_tl() - from telethon import TelegramClient as TgClient - version = TgClient.__version__ - else: - version = TelegramClient.__version__ - # Get the long description from the README file with open('README.rst', encoding='utf-8') as f: long_description = f.read() + with open('telethon/version.py', encoding='utf-8') as f: + version = re.search(r"^__version__\s+=\s+'(.*)'$", + f.read(), flags=re.MULTILINE).group(1) setup( name='Telethon', - - # Versions should comply with PEP440. version=version, description="Full-featured Telegram client library for Python 3", long_description=long_description, diff --git a/telethon/__init__.py b/telethon/__init__.py index c8593168..2f984bf1 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -1,4 +1,9 @@ +import logging from .telegram_bare_client import TelegramBareClient from .telegram_client import TelegramClient from .network import ConnectionMode -from . import tl +from . import tl, version + + +__version__ = version.__version__ +logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/telethon/crypto/factorization.py b/telethon/crypto/factorization.py index 69c8dcc9..359887d3 100644 --- a/telethon/crypto/factorization.py +++ b/telethon/crypto/factorization.py @@ -1,71 +1,45 @@ from random import randint -try: - import sympy.ntheory -except ImportError: - sympy = None class Factorization: - @staticmethod - def find_small_multiplier_lopatin(what): - """Finds the small multiplier by using Lopatin's method""" - g = 0 - for i in range(3): - q = (randint(0, 127) & 15) + 17 - x = randint(0, 1000000000) + 1 - y = x - lim = 1 << (i + 18) - for j in range(1, lim): - a, b, c = x, x, q - while b != 0: - if (b & 1) != 0: - c += a - if c >= what: - c -= what - a += a - if a >= what: - a -= what - b >>= 1 + @classmethod + def factorize(cls, pq): + if pq % 2 == 0: + return 2, pq // 2 - x = c - z = y - x if x < y else x - y - g = Factorization.gcd(z, what) - if g != 1: + y, c, m = randint(1, pq - 1), randint(1, pq - 1), randint(1, pq - 1) + g = r = q = 1 + x = ys = 0 + + while g == 1: + x = y + for i in range(r): + y = (pow(y, 2, pq) + c) % pq + + k = 0 + while k < r and g == 1: + ys = y + for i in range(min(m, r - k)): + y = (pow(y, 2, pq) + c) % pq + q = q * (abs(x - y)) % pq + + g = cls.gcd(q, pq) + k += m + + r *= 2 + + if g == pq: + while True: + ys = (pow(ys, 2, pq) + c) % pq + g = cls.gcd(abs(x - ys), pq) + if g > 1: break - if (j & (j - 1)) == 0: - y = x - - if g > 1: - break - - p = what // g - return min(p, g) + return g, pq // g @staticmethod def gcd(a, b): - """Calculates the greatest common divisor""" - while a != 0 and b != 0: - while b & 1 == 0: - b >>= 1 + while b: + a, b = b, a % b - while a & 1 == 0: - a >>= 1 - - if a > b: - a -= b - else: - b -= a - - return a if b == 0 else b - - @staticmethod - def factorize(pq): - """Factorizes the given number and returns both - the divisor and the number divided by the divisor - """ - if sympy: - return tuple(sympy.ntheory.factorint(pq).keys()) - else: - divisor = Factorization.find_small_multiplier_lopatin(pq) - return divisor, pq // divisor + return a diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py new file mode 100644 index 00000000..3cdf95f7 --- /dev/null +++ b/telethon/extensions/markdown.py @@ -0,0 +1,123 @@ +""" +Simple markdown parser which does not support nesting. Intended primarily +for use within the library, which attempts to handle emojies correctly, +since they seem to count as two characters and it's a bit strange. +""" +import re +from ..tl.types import ( + MessageEntityBold, MessageEntityItalic, MessageEntityCode, + MessageEntityPre, MessageEntityTextUrl +) + + +DEFAULT_DELIMITERS = { + '**': MessageEntityBold, + '__': MessageEntityItalic, + '`': MessageEntityCode, + '```': MessageEntityPre +} + +# Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)', +# reason why there's '\0' after every match-literal character. +DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0') + + +def parse(message, delimiters=None, url_re=None): + """ + Parses the given message and returns the stripped message and a list + of MessageEntity* using the specified delimiters dictionary (or default + if None). The dictionary should be a mapping {delimiter: entity class}. + + The url_re(gex) must contain two matching groups: the text to be + clickable and the URL itself, and be utf-16le encoded. + """ + if url_re is None: + url_re = DEFAULT_URL_RE + elif url_re: + if isinstance(url_re, bytes): + url_re = re.compile(url_re) + + if not delimiters: + if delimiters is not None: + return message, [] + delimiters = DEFAULT_DELIMITERS + + delimiters = {k.encode('utf-16le'): v for k, v in delimiters.items()} + + # Cannot use a for loop because we need to skip some indices + i = 0 + result = [] + current = None + end_delimiter = None + + # Work on byte level with the utf-16le encoding to get the offsets right. + # The offset will just be half the index we're at. + message = message.encode('utf-16le') + while i < len(message): + if url_re and current is None: + # If we're not inside a previous match since Telegram doesn't allow + # nested message entities, try matching the URL from the i'th pos. + url_match = url_re.match(message, pos=i) + if url_match: + # Replace the whole match with only the inline URL text. + message = b''.join(( + message[:url_match.start()], + url_match.group(1), + message[url_match.end():] + )) + + result.append(MessageEntityTextUrl( + offset=i // 2, length=len(url_match.group(1)) // 2, + url=url_match.group(2).decode('utf-16le') + )) + i += len(url_match.group(1)) + # Next loop iteration, don't check delimiters, since + # a new inline URL might be right after this one. + continue + + if end_delimiter is None: + # We're not expecting any delimiter, so check them all + for d, m in delimiters.items(): + # Slice the string at the current i'th position to see if + # it matches the current delimiter d, otherwise skip it. + if message[i:i + len(d)] != d: + continue + + if message[i + len(d):i + 2 * len(d)] == d: + # The same delimiter can't be right afterwards, if + # this were the case we would match empty strings + # like `` which we don't want to. + continue + + # Get rid of the delimiter by slicing it away + message = message[:i] + message[i + len(d):] + if m == MessageEntityPre: + # Special case, also has 'lang' + current = m(i // 2, None, '') + else: + current = m(i // 2, None) + + end_delimiter = d # We expect the same delimiter. + break + + elif message[i:i + len(end_delimiter)] == end_delimiter: + message = message[:i] + message[i + len(end_delimiter):] + current.length = (i // 2) - current.offset + result.append(current) + current, end_delimiter = None, None + # Don't increment i here as we matched a delimiter, + # and there may be a new one right after. This is + # different than when encountering the first delimiter, + # as we already know there won't be the same right after. + continue + + # Next iteration, utf-16 encoded characters need 2 bytes. + i += 2 + + # We may have found some a delimiter but not its ending pair. + # If this is the case, we want to insert the delimiter character back. + if current is not None: + message = \ + message[:current.offset] + end_delimiter + message[current.offset:] + + return message.decode('utf-16le'), result diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index a26d71b1..54bc16bc 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -1,12 +1,12 @@ import logging import os import asyncio -from datetime import timedelta, datetime +from datetime import timedelta from hashlib import md5 from io import BytesIO from asyncio import Lock -from . import helpers as utils +from . import helpers as utils, version from .crypto import rsa, CdnDecrypter from .errors import ( RPCError, BrokenAuthKeyError, ServerError, @@ -57,7 +57,7 @@ class TelegramBareClient: """ # Current TelegramClient version - __version__ = '0.15.3' + __version__ = version.__version__ # TODO Make this thread-safe, all connections share the same DC _config = None # Server configuration (with .dc_options) @@ -188,12 +188,10 @@ class TelegramBareClient: self.disconnect() return await self.connect(_sync_updates=_sync_updates) - except (RPCError, ConnectionError) as error: + except (RPCError, ConnectionError): # Probably errors from the previous session, ignore them self.disconnect() - self._logger.debug( - 'Could not stabilise initial connection: {}'.format(error) - ) + self._logger.exception('Could not stabilise initial connection.') return False def is_connected(self): @@ -232,7 +230,6 @@ class TelegramBareClient: # Assume we are disconnected due to some error, so connect again try: await self._reconnect_lock.acquire() - # Another thread may have connected again, so check that first if self.is_connected(): return True @@ -383,6 +380,12 @@ class TelegramBareClient: if result is not None: return result + await asyncio.sleep(retry + 1, loop=self._loop) + self._logger.debug('RPC failed. Attempting reconnection.') + if not self._reconnect_lock.locked(): + with await self._reconnect_lock: + self._reconnect() + raise ValueError('Number of retries reached 0.') # Let people use client.invoke(SomeRequest()) instead client(...) @@ -436,18 +439,13 @@ class TelegramBareClient: pass # We will just retry except ConnectionResetError: - if not self._user_connected or self._reconnect_lock.locked(): - # Only attempt reconnecting if the user called connect and not - # reconnecting already. + if self._user_connected: + # Server disconnected us, __call__ will try reconnecting. + return None + else: + # User never called .connect(), so raise this error. raise - self._logger.debug('Server disconnected us. Reconnecting and ' - 'resending request... (%d)' % retry) - await self._reconnect() - if not self.is_connected(): - await asyncio.sleep(retry + 1, loop=self._loop) - return None - if init_connection: # We initialized the connection successfully, even if # a request had an RPC error we have invoked it fine. @@ -740,11 +738,10 @@ class TelegramBareClient: self._logger.debug(error) need_reconnect = True await asyncio.sleep(1, loop=self._loop) - except Exception as error: + except Exception: # Unknown exception, pass it to the main thread - self._logger.debug( - '[ERROR] Unknown error on the read loop, please report', - error + self._logger.exception( + 'Unknown error on the read loop, please report.' ) try: @@ -762,10 +759,6 @@ class TelegramBareClient: except ImportError: "Not using PySocks, so it can't be a socket error" - # If something strange happens we don't want to enter an - # infinite loop where all we do is raise an exception, so - # add a little sleep to avoid the CPU usage going mad. - await asyncio.sleep(0.1, loop=self._loop) break self._recv_loop = None diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index a51032f2..4d84ec0c 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -1,7 +1,10 @@ import os +import time from datetime import datetime, timedelta from mimetypes import guess_type +import asyncio + try: import socks except ImportError: @@ -22,15 +25,16 @@ from .tl.functions.account import ( ) from .tl.functions.auth import ( CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest, - SignUpRequest, ImportBotAuthorizationRequest + SignUpRequest, ResendCodeRequest, ImportBotAuthorizationRequest ) from .tl.functions.contacts import ( GetContactsRequest, ResolveUsernameRequest ) from .tl.functions.messages import ( GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest, - SendMessageRequest, GetChatsRequest, - GetAllDraftsRequest) + SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, + CheckChatInviteRequest +) from .tl.functions import channels from .tl.functions import messages @@ -48,8 +52,12 @@ from .tl.types import ( Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto, InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID, UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage, - PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel) + PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty, + ChatInvite, ChatInviteAlready, PeerChannel +) from .tl.types.messages import DialogsSlice +from .extensions import markdown + class TelegramClient(TelegramBareClient): """Full featured TelegramClient meant to extend the basic functionality""" @@ -101,16 +109,29 @@ class TelegramClient(TelegramBareClient): # region Authorization requests - async def send_code_request(self, phone): + async def send_code_request(self, phone, force_sms=False): """Sends a code request to the specified phone number. - :param str | int phone: The phone to which the code will be sent. - :return auth.SentCode: Information about the result of the request. + :param str | int phone: + The phone to which the code will be sent. + :param bool force_sms: + Whether to force sending as SMS. You should call it at least + once before without this set to True first. + :return auth.SentCode: + Information about the result of the request. """ phone = EntityDatabase.parse_phone(phone) or self._phone - result = await self(SendCodeRequest(phone, self.api_id, self.api_hash)) + if force_sms: + if not self._phone_code_hash: + raise ValueError( + 'You must call this method without force_sms at least once.' + ) + result = await self(ResendCodeRequest(phone, self._phone_code_hash)) + else: + result = await self(SendCodeRequest(phone, self.api_id, self.api_hash)) + self._phone_code_hash = result.phone_code_hash + self._phone = phone - self._phone_code_hash = result.phone_code_hash return result async def sign_in(self, phone=None, code=None, @@ -251,22 +272,21 @@ class TelegramClient(TelegramBareClient): The peer to be used as an offset. :return: A tuple of lists ([dialogs], [entities]). """ - if limit is None: - limit = float('inf') + limit = float('inf') if limit is None else int(limit) + if limit == 0: + return [], [] dialogs = {} # Use peer id as identifier to avoid dupes messages = {} # Used later for sorting TODO also return these? entities = {} while len(dialogs) < limit: - need = limit - len(dialogs) + real_limit = min(limit - len(dialogs), 100) r = await self(GetDialogsRequest( offset_date=offset_date, offset_id=offset_id, offset_peer=offset_peer, - limit=need if need < float('inf') else 0 + limit=real_limit )) - if not r.dialogs: - break for d in r.dialogs: dialogs[utils.get_peer_id(d.peer, True)] = d @@ -279,8 +299,9 @@ class TelegramClient(TelegramBareClient): for c in r.chats: entities[c.id] = c - if not isinstance(r, DialogsSlice): - # Don't enter next iteration if we already got all + if len(r.dialogs) < real_limit or not isinstance(r, DialogsSlice): + # Less than we requested means we reached the end, or + # we didn't get a DialogsSlice which means we got all. break offset_date = r.messages[-1].date @@ -324,21 +345,39 @@ class TelegramClient(TelegramBareClient): entity, message, reply_to=None, + parse_mode=None, link_preview=True): """ Sends the given message to the specified entity (user/chat/channel). - :param str | int | User | Chat | Channel entity: To who will it be sent. - :param str message: The message to be sent. - :param int | Message reply_to: Whether to reply to a message or not. - :param link_preview: Should the link preview be shown? + :param str | int | User | Chat | Channel entity: + To who will it be sent. + :param str message: + The message to be sent. + :param int | Message reply_to: + Whether to reply to a message or not. + :param str parse_mode: + Can be 'md' or 'markdown' for markdown-like parsing, in a similar + fashion how official clients work. + :param link_preview: + Should the link preview be shown? + :return Message: the sent message """ entity = await self.get_input_entity(entity) + if parse_mode: + parse_mode = parse_mode.lower() + if parse_mode in {'md', 'markdown'}: + message, msg_entities = markdown.parse(message) + else: + raise ValueError('Unknown parsing mode', parse_mode) + else: + msg_entities = [] + request = SendMessageRequest( peer=entity, message=message, - entities=[], + entities=msg_entities, no_webpage=not link_preview, reply_to_msg_id=self._get_reply_to(reply_to) ) @@ -415,43 +454,100 @@ class TelegramClient(TelegramBareClient): """ Gets the message history for the specified entity - :param entity: The entity from whom to retrieve the message history - :param limit: Number of messages to be retrieved - :param offset_date: Offset date (messages *previous* to this date will be retrieved) - :param offset_id: Offset message ID (only messages *previous* to the given ID will be retrieved) - :param max_id: All the messages with a higher (newer) ID or equal to this will be excluded - :param min_id: All the messages with a lower (older) ID or equal to this will be excluded - :param add_offset: Additional message offset (all of the specified offsets + this offset = older messages) + :param entity: + The entity from whom to retrieve the message history. + :param limit: + Number of messages to be retrieved. Due to limitations with the API + retrieving more than 3000 messages will take longer than half a + minute (or even more based on previous calls). The limit may also + be None, which would eventually return the whole history. + :param offset_date: + Offset date (messages *previous* to this date will be retrieved). + :param offset_id: + Offset message ID (only messages *previous* to the given ID will + be retrieved). + :param max_id: + All the messages with a higher (newer) ID or equal to this will + be excluded + :param min_id: + All the messages with a lower (older) ID or equal to this will + be excluded. + :param add_offset: + Additional message offset + (all of the specified offsets + this offset = older messages). :return: A tuple containing total message count and two more lists ([messages], [senders]). Note that the sender can be null if it was not found! """ - result = await self(GetHistoryRequest( - peer=await self.get_input_entity(entity), - limit=limit, - offset_date=offset_date, - offset_id=offset_id, - max_id=max_id, - min_id=min_id, - add_offset=add_offset - )) + entity = await self.get_input_entity(entity) + limit = float('inf') if limit is None else int(limit) + if limit == 0: + # No messages, but we still need to know the total message count + result = await self(GetHistoryRequest( + peer=entity, limit=1, + offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0 + )) + return getattr(result, 'count', len(result.messages)), [], [] - # The result may be a messages slice (not all messages were retrieved) - # or simply a messages TLObject. In the later case, no "count" - # attribute is specified, so the total messages count is simply - # the count of retrieved messages - total_messages = getattr(result, 'count', len(result.messages)) + total_messages = 0 + messages = [] + entities = {} + while len(messages) < limit: + # Telegram has a hard limit of 100 + real_limit = min(limit - len(messages), 100) + result = await self(GetHistoryRequest( + peer=entity, + limit=real_limit, + offset_date=offset_date, + offset_id=offset_id, + max_id=max_id, + min_id=min_id, + add_offset=add_offset + )) + messages.extend( + m for m in result.messages if not isinstance(m, MessageEmpty) + ) + total_messages = getattr(result, 'count', len(result.messages)) - # Iterate over all the messages and find the sender User - entities = [ - utils.find_user_or_chat(m.from_id, result.users, result.chats) - if m.from_id is not None else - utils.find_user_or_chat(m.to_id, result.users, result.chats) + # TODO We can potentially use self.session.database, but since + # it might be disabled, use a local dictionary. + for u in result.users: + entities[utils.get_peer_id(u, add_mark=True)] = u + for c in result.chats: + entities[utils.get_peer_id(c, add_mark=True)] = c - for m in result.messages - ] + if len(result.messages) < real_limit: + break - return total_messages, result.messages, entities + offset_id = result.messages[-1].id + offset_date = result.messages[-1].date + + # Telegram limit seems to be 3000 messages within 30 seconds in + # batches of 100 messages each request (since the FloodWait was + # of 30 seconds). If the limit is greater than that, we will + # sleep 1s between each request. + if limit > 3000: + await asyncio.sleep(1, loop=self._loop) + + # In a new list with the same length as the messages append + # their senders, so people can zip(messages, senders). + senders = [] + for m in messages: + if m.from_id: + who = entities[utils.get_peer_id(m.from_id, add_mark=True)] + elif getattr(m, 'fwd_from', None): + # .from_id is optional, so this is the sanest fallback. + who = entities[utils.get_peer_id( + m.fwd_from.from_id or PeerChannel(m.fwd_from.channel_id), + add_mark=True + )] + else: + # If there's not even a FwdHeader, fallback to the sender + # being where the message was sent. + who = entities[utils.get_peer_id(m.to_id, add_mark=True)] + senders.append(who) + + return total_messages, messages, senders async def send_read_acknowledge(self, entity, messages=None, max_id=None): """ @@ -938,8 +1034,18 @@ class TelegramClient(TelegramBareClient): entity = phone await self(GetContactsRequest(0)) else: - entity = string.strip('@').lower() - await self(ResolveUsernameRequest(entity)) + entity, is_join_chat = EntityDatabase.parse_username(string) + if is_join_chat: + invite = await self(CheckChatInviteRequest(entity)) + if isinstance(invite, ChatInvite): + # If it's an invite to a chat, the user must join before + # for the link to be resolved and work, otherwise raise. + if invite.channel: + return invite.channel + elif isinstance(invite, ChatInviteAlready): + return invite.chat + else: + await self(ResolveUsernameRequest(entity)) # MtProtoSender will call .process_entities on the requests made try: diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index c333649b..296383b2 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -8,6 +8,11 @@ from ..tl.types import ( from .. import utils # Keep this line the last to maybe fix #357 +USERNAME_RE = re.compile( + r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?' +) + + class EntityDatabase: def __init__(self, input_list=None, enabled=True, enabled_full=True): """Creates a new entity database with an initial load of "Input" @@ -21,7 +26,12 @@ class EntityDatabase: self._entities = {} # marked_id: user|chat|channel if input_list: - self._input_entities = {k: v for k, v in input_list} + # TODO For compatibility reasons some sessions were saved with + # 'access_hash': null in the JSON session file. Drop these, as + # it means we don't have access to such InputPeers. Issue #354. + self._input_entities = { + k: v for k, v in input_list if v is not None + } else: self._input_entities = {} # marked_id: hash @@ -66,10 +76,22 @@ class EntityDatabase: try: p = utils.get_input_peer(e, allow_self=False) - new_input[utils.get_peer_id(p, add_mark=True)] = \ - getattr(p, 'access_hash', 0) # chats won't have hash + marked_id = utils.get_peer_id(p, add_mark=True) - if self.enabled_full: + has_hash = False + if isinstance(p, InputPeerChat): + # Chats don't have a hash + new_input[marked_id] = 0 + has_hash = True + elif p.access_hash: + # Some users and channels seem to be returned without + # an 'access_hash', meaning Telegram doesn't want you + # to access them. This is the reason behind ensuring + # that the 'access_hash' is non-zero. See issue #354. + new_input[marked_id] = p.access_hash + has_hash = True + + if self.enabled_full and has_hash: if isinstance(e, (User, Chat, Channel)): new.append(e) except ValueError: @@ -114,7 +136,7 @@ class EntityDatabase: phone = getattr(entity, 'phone', None) if phone: - self._username_id[phone] = marked_id + self._phone_id[phone] = marked_id def _parse_key(self, key): """Parses the given string, integer or TLObject key into a @@ -132,7 +154,8 @@ class EntityDatabase: if phone: return self._phone_id[phone] else: - return self._username_id[key.lstrip('@').lower()] + username, _ = EntityDatabase.parse_username(key) + return self._username_id[username.lower()] except KeyError as e: raise ValueError() from e @@ -185,6 +208,19 @@ class EntityDatabase: if phone.isdigit(): return phone + @staticmethod + def parse_username(username): + """Parses the given username or channel access hash, given + a string, username or URL. Returns a tuple consisting of + both the stripped username and whether it is a joinchat/ hash. + """ + username = username.strip() + m = USERNAME_RE.match(username) + if m: + return username[m.end():], bool(m.group(1)) + else: + return username, False + def get_input_entity(self, peer): try: i = utils.get_peer_id(peer, add_mark=True) diff --git a/telethon/utils.py b/telethon/utils.py index afb24b16..3259c8e2 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -19,7 +19,7 @@ from .tl.types import ( DocumentEmpty, InputDocumentEmpty, Message, GeoPoint, InputGeoPoint, GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty, InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, - FileLocationUnavailable, InputMediaUploadedDocument, + FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull, InputMediaUploadedPhoto, DocumentAttributeFilename, photos ) @@ -84,13 +84,13 @@ def get_input_peer(entity, allow_self=True): if entity.is_self and allow_self: return InputPeerSelf() else: - return InputPeerUser(entity.id, entity.access_hash) + return InputPeerUser(entity.id, entity.access_hash or 0) if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)): return InputPeerChat(entity.id) if isinstance(entity, (Channel, ChannelForbidden)): - return InputPeerChannel(entity.id, entity.access_hash) + return InputPeerChannel(entity.id, entity.access_hash or 0) # Less common cases if isinstance(entity, UserEmpty): @@ -99,6 +99,9 @@ def get_input_peer(entity, allow_self=True): if isinstance(entity, InputUser): return InputPeerUser(entity.user_id, entity.access_hash) + if isinstance(entity, InputUserSelf): + return InputPeerSelf() + if isinstance(entity, UserFull): return get_input_peer(entity.user) @@ -120,7 +123,7 @@ def get_input_channel(entity): return entity if isinstance(entity, (Channel, ChannelForbidden)): - return InputChannel(entity.id, entity.access_hash) + return InputChannel(entity.id, entity.access_hash or 0) if isinstance(entity, InputPeerChannel): return InputChannel(entity.channel_id, entity.access_hash) @@ -140,7 +143,7 @@ def get_input_user(entity): if entity.is_self: return InputUserSelf() else: - return InputUser(entity.id, entity.access_hash) + return InputUser(entity.id, entity.access_hash or 0) if isinstance(entity, InputPeerSelf): return InputUserSelf() @@ -322,8 +325,13 @@ def get_peer_id(peer, add_mark=False): return peer.user_id elif isinstance(peer, (PeerChat, InputPeerChat)): return -peer.chat_id if add_mark else peer.chat_id - elif isinstance(peer, (PeerChannel, InputPeerChannel)): - i = peer.channel_id + elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)): + if isinstance(peer, ChannelFull): + # Special case: .get_input_peer can't return InputChannel from + # ChannelFull since it doesn't have an .access_hash attribute. + i = peer.id + else: + i = peer.channel_id if add_mark: # Concat -100 through math tricks, .to_supergroup() on Madeline # IDs will be strictly positive -> log works diff --git a/telethon/version.py b/telethon/version.py new file mode 100644 index 00000000..bafdcc72 --- /dev/null +++ b/telethon/version.py @@ -0,0 +1,3 @@ +# Versions should comply with PEP440. +# This line is parsed in setup.py: +__version__ = '0.15.4' diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index ee179a42..52c2c356 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -354,7 +354,7 @@ class InteractiveTelegramClient(TelegramClient): update.message, get_display_name(who) )) else: - sprint('<< {} sent "{}"]'.format( + sprint('<< {} sent "{}"'.format( get_display_name(who), update.message )) diff --git a/telethon_generator/error_descriptions b/telethon_generator/error_descriptions index 500504d7..65894ba1 100644 --- a/telethon_generator/error_descriptions +++ b/telethon_generator/error_descriptions @@ -45,7 +45,7 @@ PHONE_NUMBER_OCCUPIED=The phone number is already in use PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid -USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]" +USERNAME_INVALID=Nobody is using this username, or the username is unacceptable. If the latter, it must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]" USERNAME_NOT_MODIFIED=The username is not different from the current username USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet USERNAME_OCCUPIED=The username is already taken diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 5e949239..2ecb31b4 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -159,14 +159,17 @@ inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers: inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia; -inputMediaUploadedDocument#e39621fd flags:# file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector caption:string stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; +inputMediaUploadedDocument#e39621fd flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector caption:string stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia; -inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = 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#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia; +inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia; + +inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto; @@ -218,11 +221,11 @@ userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#9ba2d800 id:int = Chat; chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat; chatForbidden#7328bdb id:int title:string = Chat; -channel#cb44b1c flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights = Chat; +channel#450b7115 flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights participants_count:flags.17?int = Chat; channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector = ChatFull; -channelFull#17f45fcf flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet = ChatFull; +channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull; chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; chatParticipantCreator#da13538a user_id:int = ChatParticipant; @@ -235,7 +238,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto; messageEmpty#83e5de54 id:int = Message; -message#90dddc11 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?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 views:flags.10?int edit_date:flags.15?int post_author:flags.16?string = Message; +message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?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 views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message; messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -245,9 +248,10 @@ messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:str messageMediaUnsupported#9f84f49e = MessageMedia; messageMediaDocument#7c4414d3 flags:# document:flags.0?Document caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; -messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = MessageMedia; +messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia; +messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#a6638b9a title:string users:Vector = MessageAction; @@ -267,6 +271,7 @@ messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long pa messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction; messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; messageActionScreenshotTaken#4792929b = MessageAction; +messageActionCustomAction#fae69f56 message:string = MessageAction; dialog#e4def5db flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog; @@ -363,6 +368,8 @@ inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFil inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter; inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; +inputMessagesFilterContacts#e062db83 = MessagesFilter; +inputMessagesFilterGeo#e7026d0d = MessagesFilter; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update; @@ -429,6 +436,7 @@ updateLangPack#56022f4d difference:LangPackDifference = Update; updateFavedStickers#e511996d = Update; updateChannelReadMessagesContents#89893b45 channel_id:int messages:Vector = Update; updateContactsReset#7084a7be = Update; +updateChannelAvailableMessages#70db6837 channel_id:int available_min_id:int = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -455,7 +463,7 @@ upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption; -config#8df376a4 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector = Config; +config#9c840964 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector = Config; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; @@ -665,6 +673,7 @@ channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter; channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter; channels.channelParticipants#f56ee2a8 count:int participants:Vector users:Vector = channels.ChannelParticipants; +channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector = channels.ChannelParticipant; @@ -680,7 +689,7 @@ messages.savedGifs#2e0709a5 hash:int gifs:Vector = messages.SavedGifs; inputBotInlineMessageMediaAuto#292fed13 flags:# caption:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaGeo#f4a59de1 flags:# geo_point:InputGeoPoint reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaVenue#aaafadc8 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#2daf01a7 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; @@ -692,18 +701,18 @@ inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:Input botInlineMessageMediaAuto#a74b15b flags:# caption:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaGeo#3a8fd8b8 flags:# geo:GeoPoint reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaVenue#4366232e flags:# geo:GeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaContact#35edb4d4 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineResult#9bebaeb9 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb_url:flags.4?string content_url:flags.5?string content_type:flags.5?string w:flags.6?int h:flags.6?int duration:flags.7?int send_message:BotInlineMessage = BotInlineResult; botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult; -messages.botResults#ccd3563d flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector cache_time:int = messages.BotResults; +messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector cache_time:int users:Vector = messages.BotResults; exportedMessageLink#1f486803 link:string = ExportedMessageLink; -messageFwdHeader#fadff4ac flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string = MessageFwdHeader; +messageFwdHeader#559ebe6d flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int = MessageFwdHeader; auth.codeTypeSms#72a3158c = auth.CodeType; auth.codeTypeCall#741cd3e3 = auth.CodeType; @@ -903,6 +912,7 @@ channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticip channelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction; +channelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent; @@ -917,6 +927,14 @@ cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash; messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; messages.favedStickers#f37f2f16 hash:int packs:Vector stickers:Vector = messages.FavedStickers; +help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; + +recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl; +recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl; +recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; +recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; +recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1001,7 +1019,7 @@ messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool; messages.sendMessage#fa88427a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; messages.sendMedia#c8f16791 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia random_id:long reply_markup:flags.2?ReplyMarkup = Updates; -messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer = Updates; +messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.hideReportSpam#a8f1709b peer:InputPeer = Bool; messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings; @@ -1048,7 +1066,7 @@ messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_p messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool; messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#ce91e4ca flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; +messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector geo_point:flags.13?InputGeoPoint = Updates; messages.editInlineBotMessage#130c2c85 flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; @@ -1080,6 +1098,9 @@ messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages; +messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; +messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1108,13 +1129,14 @@ help.getAppChangelog#9010ef6f prev_app_version:string = Updates; help.getTermsOfService#350170f3 = help.TermsOfService; help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool; help.getCdnConfig#52029342 = CdnConfig; +help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; channels.getMessages#93d7b347 channel:InputChannel id:Vector = messages.Messages; -channels.getParticipants#24d98f92 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int = channels.ChannelParticipants; +channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants; channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant; channels.getChannels#a7f6bbb id:Vector = messages.Chats; channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; @@ -1139,6 +1161,8 @@ channels.editBanned#bfd915cd channel:InputChannel user_id:InputUser banned_right channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector max_id:long min_id:long limit:int = channels.AdminLogResults; channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool; channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector = Bool; +channels.deleteHistory#af369d42 channel:InputChannel max_id:int = Bool; +channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -1169,4 +1193,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector = Vector; -// LAYER 71 +// LAYER 73