Merge branch 'v1' into 64inlinemsgid

This commit is contained in:
Lonami 2023-04-07 17:02:37 +02:00 committed by GitHub
commit 50489719fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 269 additions and 239 deletions

View File

@ -13,6 +13,49 @@ it can take advantage of new goodies!
.. contents:: List of All Versions .. contents:: List of All Versions
New Layer and housekeeping (v1.28)
==================================
+------------------------+
| Scheme layer used: 155 |
+------------------------+
Plenty of stale issues closed, as well as improvements for some others.
Additions
~~~~~~~~~
* New ``entity_cache_limit`` parameter in the ``TelegramClient`` constructor.
This should help a bit in keeping memory usage in check.
Enhancements
~~~~~~~~~~~~
* ``progress_callback`` is now called when dealing with albums. See the
documentation on `client.send_file() <telethon.client.uploads.UploadMethods.send_file>`
for details.
* Update state and entities are now periodically saved, so that the information
isn't lost in the case of crash or unexpected script terminations. You should
still be calling ``disconnect`` or using the context-manager, though.
* The client should no longer unnecessarily call ``get_me`` every time it's started.
Bug fixes
~~~~~~~~~
* Messages obtained via raw API could not be used in ``forward_messages``.
* ``force_sms`` and ``sign_up`` have been deprecated. See `issue 4050`_ for details.
It is no longer possible for third-party applications, such as those made with
Telethon, to use those features.
* ``events.ChatAction`` should now work in more cases in groups with hidden members.
* Errors that occur at the connection level should now be properly propagated, so that
you can actually have a chance to handle them.
* Update handling should be more resilient.
* ``PhoneCodeExpiredError`` will correctly clear the stored hash if it occurs in ``sign_in``.
.. _issue 4050: https://github.com/LonamiWebs/Telethon/issues/4050
New Layer and some Bug fixes (v1.27) New Layer and some Bug fixes (v1.27)
==================================== ====================================

View File

@ -178,6 +178,69 @@ won't do unnecessary work unless you need to:
sender = await event.get_sender() sender = await event.get_sender()
What does "Server sent a very new message with ID" mean?
========================================================
You may also see this error as "Server sent a very old message with ID".
This is a security feature from Telethon that cannot be disabled and is
meant to protect you against replay attacks.
When this message is incorrectly reported as a "bug",
the most common patterns seem to be:
* Your system time is incorrect.
* The proxy you're using may be interfering somehow.
* The Telethon session is being used or has been used from somewhere else.
Make sure that you created the session from Telethon, and are not using the
same session anywhere else. If you need to use the same account from
multiple places, login and use a different session for each place you need.
What does "Server replied with a wrong session ID" mean?
========================================================
This is a security feature from Telethon that cannot be disabled and is
meant to protect you against unwanted session reuse.
When this message is reported as a "bug", the most common patterns seem to be:
* The proxy you're using may be interfering somehow.
* The Telethon session is being used or has been used from somewhere else.
Make sure that you created the session from Telethon, and are not using the
same session anywhere else. If you need to use the same account from
multiple places, login and use a different session for each place you need.
* You may be using multiple connections to the Telegram server, which seems
to confuse Telegram.
Most of the time it should be safe to ignore this warning. If the library
still doesn't behave correctly, make sure to check if any of the above bullet
points applies in your case and try to work around it.
If the issue persists and there is a way to reliably reproduce this error,
please add a comment with any additional details you can provide to
`issue 3759`_, and perhaps some additional investigation can be done
(but it's unlikely, as Telegram *is* sending unexpected data).
What does "Could not find a matching Constructor ID for the TLObject" mean?
===========================================================================
Telegram uses "layers", which you can think of as "versions" of the API they
offer. When Telethon reads responses that the Telegram servers send, these
need to be deserialized (into what Telethon calls "TLObjects").
Every Telethon version understands a single Telegram layer. When Telethon
connects to Telegram, both agree on the layer to use. If the layers don't
match, Telegram may send certain objects which Telethon no longer understands.
When this message is reported as a "bug", the most common patterns seem to be
that he Telethon session is being used or has been used from somewhere else.
Make sure that you created the session from Telethon, and are not using the
same session anywhere else. If you need to use the same account from
multiple places, login and use a different session for each place you need.
What does "bases ChatGetter" mean? What does "bases ChatGetter" mean?
================================== ==================================
@ -268,4 +331,5 @@ file and run that, or use the normal ``python`` interpreter.
.. _logging: https://docs.python.org/3/library/logging.html .. _logging: https://docs.python.org/3/library/logging.html
.. _@SpamBot: https://t.me/SpamBot .. _@SpamBot: https://t.me/SpamBot
.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297 .. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297
.. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy .. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy

View File

@ -9,15 +9,17 @@ class EntityCache:
self, self,
hash_map: dict = _sentinel, hash_map: dict = _sentinel,
self_id: int = None, self_id: int = None,
self_bot: bool = False self_bot: bool = None
): ):
self.hash_map = {} if hash_map is _sentinel else hash_map self.hash_map = {} if hash_map is _sentinel else hash_map
self.self_id = self_id self.self_id = self_id
self.self_bot = self_bot self.self_bot = self_bot
def set_self_user(self, id, bot): def set_self_user(self, id, bot, hash):
self.self_id = id self.self_id = id
self.self_bot = bot self.self_bot = bot
if hash:
self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER)
def get(self, id): def get(self, id):
try: try:
@ -52,3 +54,9 @@ class EntityCache:
def put(self, entity): def put(self, entity):
self.hash_map[entity.id] = (entity.hash, entity.ty) self.hash_map[entity.id] = (entity.hash, entity.ty)
def retain(self, filter):
self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)}
def __len__(self):
return len(self.hash_map)

View File

@ -232,7 +232,7 @@ class MessageBox:
self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline) self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline)
self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states) self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states)
self.date = datetime.datetime.fromtimestamp(session_state.date).replace(tzinfo=datetime.timezone.utc) self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc)
self.seq = session_state.seq self.seq = session_state.seq
self.next_deadline = ENTRY_ACCOUNT self.next_deadline = ENTRY_ACCOUNT

View File

@ -352,7 +352,12 @@ class AuthMethods:
'and a password only if an RPCError was raised before.' 'and a password only if an RPCError was raised before.'
) )
try:
result = await self(request) result = await self(request)
except errors.PhoneCodeExpiredError:
self._phone_code_hash.pop(phone, None)
raise
if isinstance(result, types.auth.AuthorizationSignUpRequired): if isinstance(result, types.auth.AuthorizationSignUpRequired):
# Emulate pre-layer 104 behaviour # Emulate pre-layer 104 behaviour
self._tos = result.terms_of_service self._tos = result.terms_of_service
@ -380,8 +385,7 @@ class AuthMethods:
Returns the input user parameter. Returns the input user parameter.
""" """
self._bot = bool(user.bot) self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash)
self._self_input_peer = utils.get_input_peer(user, allow_self=False)
self._authorized = True self._authorized = True
state = await self(functions.updates.GetStateRequest()) state = await self(functions.updates.GetStateRequest())
@ -531,8 +535,7 @@ class AuthMethods:
except errors.RPCError: except errors.RPCError:
return False return False
self._bot = None self._mb_entity_cache.set_self_user(None, None, None)
self._self_input_peer = None
self._authorized = False self._authorized = False
await self.disconnect() await self.disconnect()

View File

@ -10,7 +10,6 @@ import datetime
from .. import version, helpers, __name__ as __base_name__ from .. import version, helpers, __name__ as __base_name__
from ..crypto import rsa from ..crypto import rsa
from ..entitycache import EntityCache
from ..extensions import markdown from ..extensions import markdown
from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
from ..sessions import Session, SQLiteSession, MemorySession from ..sessions import Session, SQLiteSession, MemorySession
@ -209,6 +208,20 @@ class TelegramBaseClient(abc.ABC):
so event handlers, conversations, and QR login will not work. so event handlers, conversations, and QR login will not work.
However, certain scripts don't need updates, so this will reduce However, certain scripts don't need updates, so this will reduce
the amount of bandwidth used. the amount of bandwidth used.
entity_cache_limit (`int`, optional):
How many users, chats and channels to keep in the in-memory cache
at most. This limit is checked against when processing updates.
When this limit is reached or exceeded, all entities that are not
required for update handling will be flushed to the session file.
Note that this implies that there is a lower bound to the amount
of entities that must be kept in memory.
Setting this limit too low will cause the library to attempt to
flush entities to the session file even if no entities can be
removed from the in-memory cache, which will degrade performance.
""" """
# Current TelegramClient version # Current TelegramClient version
@ -246,7 +259,8 @@ class TelegramBaseClient(abc.ABC):
loop: asyncio.AbstractEventLoop = None, loop: asyncio.AbstractEventLoop = None,
base_logger: typing.Union[str, logging.Logger] = None, base_logger: typing.Union[str, logging.Logger] = None,
receive_updates: bool = True, receive_updates: bool = True,
catch_up: bool = False catch_up: bool = False,
entity_cache_limit: int = 5000
): ):
if not api_id or not api_hash: if not api_id or not api_hash:
raise ValueError( raise ValueError(
@ -300,7 +314,7 @@ class TelegramBaseClient(abc.ABC):
self.flood_sleep_threshold = flood_sleep_threshold self.flood_sleep_threshold = flood_sleep_threshold
# TODO Use AsyncClassWrapper(session) # TODO Use AsyncClassWrapper(session)
# ChatGetter and SenderGetter can use the in-memory _entity_cache # ChatGetter and SenderGetter can use the in-memory _mb_entity_cache
# to avoid network access and the need for await in session files. # to avoid network access and the need for await in session files.
# #
# The session files only wants the entities to persist # The session files only wants the entities to persist
@ -308,7 +322,6 @@ class TelegramBaseClient(abc.ABC):
# TODO Session should probably return all cached # TODO Session should probably return all cached
# info of entities, not just the input versions # info of entities, not just the input versions
self.session = session self.session = session
self._entity_cache = EntityCache()
self.api_id = int(api_id) self.api_id = int(api_id)
self.api_hash = api_hash self.api_hash = api_hash
@ -422,10 +435,6 @@ class TelegramBaseClient(abc.ABC):
self._phone = None self._phone = None
self._tos = None self._tos = None
# Sometimes we need to know who we are, cache the self peer
self._self_input_peer = None
self._bot = None
# A place to store if channels are a megagroup or not (see `edit_admin`) # A place to store if channels are a megagroup or not (see `edit_admin`)
self._megagroup_cache = {} self._megagroup_cache = {}
@ -433,8 +442,8 @@ class TelegramBaseClient(abc.ABC):
self._catch_up = catch_up self._catch_up = catch_up
self._updates_queue = asyncio.Queue() self._updates_queue = asyncio.Queue()
self._message_box = MessageBox(self._log['messagebox']) self._message_box = MessageBox(self._log['messagebox'])
# This entity cache is tailored for the messagebox and is not used for absolutely everything like _entity_cache
self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference) self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference)
self._entity_cache_limit = entity_cache_limit
self._sender = MTProtoSender( self._sender = MTProtoSender(
self.session.auth_key, self.session.auth_key,
@ -540,6 +549,14 @@ class TelegramBaseClient(abc.ABC):
self.session.auth_key = self._sender.auth_key self.session.auth_key = self._sender.auth_key
self.session.save() self.session.save()
try:
# See comment when saving entities to understand this hack
self_id = self.session.get_input_entity(0).access_hash
self_user = self.session.get_input_entity(self_id)
self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash)
except ValueError:
pass
if self._catch_up: if self._catch_up:
ss = SessionState(0, 0, False, 0, 0, 0, 0, None) ss = SessionState(0, 0, False, 0, 0, 0, 0, None)
cs = [] cs = []
@ -654,6 +671,24 @@ class TelegramBaseClient(abc.ABC):
else: else:
connection._proxy = proxy connection._proxy = proxy
def _save_states_and_entities(self: 'TelegramClient'):
entities = self._mb_entity_cache.get_all_entities()
# Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities.
# It doesn't matter if we put users in the list of chats.
self.session.process_entities(types.contacts.ResolvedPeer(None, [e._as_input_peer() for e in entities], []))
# As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``.
# This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved.
if self._mb_entity_cache.self_id:
self.session.process_entities(types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], []))
ss, cs = self._message_box.session_state()
self.session.set_update_state(0, types.updates.State(**ss, unread_count=0))
now = datetime.datetime.now() # any datetime works; channels don't need it
for channel_id, pts in cs.items():
self.session.set_update_state(channel_id, types.updates.State(pts, 0, now, 0, unread_count=0))
async def _disconnect_coro(self: 'TelegramClient'): async def _disconnect_coro(self: 'TelegramClient'):
if self.session is None: if self.session is None:
return # already logged out and disconnected return # already logged out and disconnected
@ -684,17 +719,7 @@ class TelegramBaseClient(abc.ABC):
await asyncio.wait(self._event_handler_tasks) await asyncio.wait(self._event_handler_tasks)
self._event_handler_tasks.clear() self._event_handler_tasks.clear()
entities = self._mb_entity_cache.get_all_entities() self._save_states_and_entities()
# Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities.
# It doesn't matter if we put users in the list of chats.
self.session.process_entities(types.contacts.ResolvedPeer(None, [e._as_input_peer() for e in entities], []))
ss, cs = self._message_box.session_state()
self.session.set_update_state(0, types.updates.State(**ss, unread_count=0))
now = datetime.datetime.now() # any datetime works; channels don't need it
for channel_id, pts in cs.items():
self.session.set_update_state(channel_id, types.updates.State(pts, 0, now, 0, unread_count=0))
self.session.close() self.session.close()

View File

@ -7,6 +7,7 @@ import time
import traceback import traceback
import typing import typing
import logging import logging
import warnings
from collections import deque from collections import deque
from .. import events, utils, errors from .. import events, utils, errors
@ -14,6 +15,7 @@ from ..events.common import EventBuilder, EventCommon
from ..tl import types, functions from ..tl import types, functions
from .._updates import GapError, PrematureEndReason from .._updates import GapError, PrematureEndReason
from ..helpers import get_running_loop from ..helpers import get_running_loop
from ..version import __version__
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -280,6 +282,24 @@ class UpdateMethods:
continue continue
if len(self._mb_entity_cache) >= self._entity_cache_limit:
self._log[__name__].info(
'In-memory entity cache limit reached (%s/%s), flushing to session',
len(self._mb_entity_cache),
self._entity_cache_limit
)
self._save_states_and_entities()
self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map)
if len(self._mb_entity_cache) >= self._entity_cache_limit:
warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit')
self._log[__name__].info(
'In-memory entity cache at %s/%s after flushing to session',
len(self._mb_entity_cache),
self._entity_cache_limit
)
get_diff = self._message_box.get_difference() get_diff = self._message_box.get_difference()
if get_diff: if get_diff:
self._log[__name__].debug('Getting difference for account updates') self._log[__name__].debug('Getting difference for account updates')
@ -419,7 +439,7 @@ class UpdateMethods:
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except Exception as e: except Exception as e:
self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)') self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)')
self._updates_error = e self._updates_error = e
await self.disconnect() await self.disconnect()
@ -467,16 +487,18 @@ class UpdateMethods:
# inserted because this is a rather expensive operation # inserted because this is a rather expensive operation
# (default's sqlite3 takes ~0.1s to commit changes). Do # (default's sqlite3 takes ~0.1s to commit changes). Do
# it every minute instead. No-op if there's nothing new. # it every minute instead. No-op if there's nothing new.
self._save_states_and_entities()
self.session.save() self.session.save()
async def _dispatch_update(self: 'TelegramClient', update): async def _dispatch_update(self: 'TelegramClient', update):
# TODO only used for AlbumHack, and MessageBox is not really designed for this # TODO only used for AlbumHack, and MessageBox is not really designed for this
others = None others = None
if not self._self_input_peer: if not self._mb_entity_cache.self_id:
# Some updates require our own ID, so we must make sure # Some updates require our own ID, so we must make sure
# that the event builder has offline access to it. Calling # that the event builder has offline access to it. Calling
# `get_me()` will cache it under `self._self_input_peer`. # `get_me()` will cache it under `self._mb_entity_cache`.
# #
# It will return `None` if we haven't logged in yet which is # It will return `None` if we haven't logged in yet which is
# fine, we will just retry next time anyway. # fine, we will just retry next time anyway.

View File

@ -352,6 +352,11 @@ class UploadMethods:
# First check if the user passed an iterable, in which case # First check if the user passed an iterable, in which case
# we may want to send grouped. # we may want to send grouped.
if utils.is_list_like(file): if utils.is_list_like(file):
sent_count = 0
used_callback = None if not progress_callback else (
lambda s, t: progress_callback(sent_count + s, len(file))
)
if utils.is_list_like(caption): if utils.is_list_like(caption):
captions = caption captions = caption
else: else:
@ -361,25 +366,14 @@ class UploadMethods:
while file: while file:
result += await self._send_album( result += await self._send_album(
entity, file[:10], caption=captions[:10], entity, file[:10], caption=captions[:10],
progress_callback=progress_callback, reply_to=reply_to, progress_callback=used_callback, reply_to=reply_to,
parse_mode=parse_mode, silent=silent, schedule=schedule, parse_mode=parse_mode, silent=silent, schedule=schedule,
supports_streaming=supports_streaming, clear_draft=clear_draft, supports_streaming=supports_streaming, clear_draft=clear_draft,
force_document=force_document, background=background, force_document=force_document, background=background,
) )
file = file[10:] file = file[10:]
captions = captions[10:] captions = captions[10:]
sent_count += 10
for doc, cap in zip(file, captions):
result.append(await self.send_file(
entity, doc, allow_cache=allow_cache,
caption=cap, force_document=force_document,
progress_callback=progress_callback, reply_to=reply_to,
attributes=attributes, thumb=thumb, voice_note=voice_note,
video_note=video_note, buttons=buttons, silent=silent,
supports_streaming=supports_streaming, schedule=schedule,
clear_draft=clear_draft, background=background,
**kwargs
))
return result return result
@ -436,16 +430,22 @@ class UploadMethods:
reply_to = utils.get_message_id(reply_to) reply_to = utils.get_message_id(reply_to)
used_callback = None if not progress_callback else (
# use an integer when sent matches total, to easily determine a file has been fully sent
lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files))
)
# Need to upload the media first, but only if they're not cached yet # Need to upload the media first, but only if they're not cached yet
media = [] media = []
for file in files: for sent_count, file in enumerate(files):
# Albums want :tl:`InputMedia` which, in theory, includes # Albums want :tl:`InputMedia` which, in theory, includes
# :tl:`InputMediaUploadedPhoto`. However using that will # :tl:`InputMediaUploadedPhoto`. However using that will
# make it `raise MediaInvalidError`, so we need to upload # make it `raise MediaInvalidError`, so we need to upload
# it as media and then convert that to :tl:`InputMediaPhoto`. # it as media and then convert that to :tl:`InputMediaPhoto`.
fh, fm, _ = await self._file_to_media( fh, fm, _ = await self._file_to_media(
file, supports_streaming=supports_streaming, file, supports_streaming=supports_streaming,
force_document=force_document, ttl=ttl) force_document=force_document, ttl=ttl,
progress_callback=used_callback)
if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)): if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)):
r = await self(functions.messages.UploadMediaRequest( r = await self(functions.messages.UploadMediaRequest(
entity, media=fm entity, media=fm
@ -546,6 +546,13 @@ class UploadMethods:
A callback function accepting two parameters: A callback function accepting two parameters:
``(sent bytes, total)``. ``(sent bytes, total)``.
When sending an album, the callback will receive a number
between 0 and the amount of files as the "sent" parameter,
and the amount of files as the "total". Note that the first
parameter will be a floating point number to indicate progress
within a file (e.g. ``2.5`` means it has sent 50% of the third
file, because it's between 2 and 3).
Returns Returns
:tl:`InputFileBig` if the file size is larger than 10MB, :tl:`InputFileBig` if the file size is larger than 10MB,
`InputSizedFile <telethon.tl.custom.inputsizedfile.InputSizedFile>` `InputSizedFile <telethon.tl.custom.inputsizedfile.InputSizedFile>`

View File

@ -72,7 +72,6 @@ class UserMethods:
results.append(None) results.append(None)
continue continue
self.session.process_entities(result) self.session.process_entities(result)
self._entity_cache.add(result)
exceptions.append(None) exceptions.append(None)
results.append(result) results.append(result)
request_index += 1 request_index += 1
@ -83,7 +82,6 @@ class UserMethods:
else: else:
result = await future result = await future
self.session.process_entities(result) self.session.process_entities(result)
self._entity_cache.add(result)
return result return result
except (errors.ServerError, errors.RpcCallFailError, except (errors.ServerError, errors.RpcCallFailError,
errors.RpcMcgetFailError, errors.InterdcCallErrorError, errors.RpcMcgetFailError, errors.InterdcCallErrorError,
@ -154,20 +152,17 @@ class UserMethods:
me = await client.get_me() me = await client.get_me()
print(me.username) print(me.username)
""" """
if input_peer and self._self_input_peer: if input_peer and self._mb_entity_cache.self_id:
return self._self_input_peer return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer()
try: try:
me = (await self( me = (await self(
functions.users.GetUsersRequest([types.InputUserSelf()])))[0] functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
self._bot = me.bot if not self._mb_entity_cache.self_id:
if not self._self_input_peer: self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash)
self._self_input_peer = utils.get_input_peer(
me, allow_self=False
)
return self._self_input_peer if input_peer else me return utils.get_input_peer(me, allow_self=False) if input_peer else me
except errors.UnauthorizedError: except errors.UnauthorizedError:
return None return None
@ -179,7 +174,7 @@ class UserMethods:
This property is used in every update, and some like `updateLoginToken` This property is used in every update, and some like `updateLoginToken`
occur prior to login, so it gracefully handles when no ID is known yet. occur prior to login, so it gracefully handles when no ID is known yet.
""" """
return self._self_input_peer.user_id if self._self_input_peer else None return self._mb_entity_cache.self_id
async def is_bot(self: 'TelegramClient') -> bool: async def is_bot(self: 'TelegramClient') -> bool:
""" """
@ -193,10 +188,10 @@ class UserMethods:
else: else:
print('Hello') print('Hello')
""" """
if self._bot is None: if self._mb_entity_cache.self_bot is None:
self._bot = (await self.get_me()).bot await self.get_me(input_peer=True)
return self._bot return self._mb_entity_cache.self_bot
async def is_user_authorized(self: 'TelegramClient') -> bool: async def is_user_authorized(self: 'TelegramClient') -> bool:
""" """
@ -417,8 +412,8 @@ class UserMethods:
try: try:
# 0x2d45687 == crc32(b'Peer') # 0x2d45687 == crc32(b'Peer')
if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687:
return self._entity_cache[peer] return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer()
except (AttributeError, KeyError): except AttributeError:
pass pass
# Then come known strings that take precedence # Then come known strings that take precedence

View File

@ -1,147 +0,0 @@
import inspect
import itertools
from . import utils
from .tl import types
# Which updates have the following fields?
_has_field = {
('user_id', int): [],
('chat_id', int): [],
('channel_id', int): [],
('peer', 'TypePeer'): [],
('peer', 'TypeDialogPeer'): [],
('message', 'TypeMessage'): [],
}
# Note: We don't bother checking for some rare:
# * `UpdateChatParticipantAdd.inviter_id` integer.
# * `UpdateNotifySettings.peer` dialog peer.
# * `UpdatePinnedDialogs.order` list of dialog peers.
# * `UpdateReadMessagesContents.messages` list of messages.
# * `UpdateChatParticipants.participants` list of participants.
#
# There are also some uninteresting `update.message` of type string.
def _fill():
for name in dir(types):
update = getattr(types, name)
if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e:
cid = update.CONSTRUCTOR_ID
sig = inspect.signature(update.__init__)
for param in sig.parameters.values():
vec = _has_field.get((param.name, param.annotation))
if vec is not None:
vec.append(cid)
# Future-proof check: if the documentation format ever changes
# then we won't be able to pick the update types we are interested
# in, so we must make sure we have at least an update for each field
# which likely means we are doing it right.
if not all(_has_field.values()):
raise RuntimeError('FIXME: Did the init signature or updates change?')
# We use a function to avoid cluttering the globals (with name/update/cid/doc)
_fill()
class EntityCache:
"""
In-memory input entity cache, defaultdict-like behaviour.
"""
def add(self, entities):
"""
Adds the given entities to the cache, if they weren't saved before.
"""
if not utils.is_list_like(entities):
# Invariant: all "chats" and "users" are always iterables,
# and "user" never is (so we wrap it inside a list).
entities = itertools.chain(
getattr(entities, 'chats', []),
getattr(entities, 'users', []),
(hasattr(entities, 'user') and [entities.user]) or []
)
for entity in entities:
try:
pid = utils.get_peer_id(entity)
if pid not in self.__dict__:
# Note: `get_input_peer` already checks for `access_hash`
self.__dict__[pid] = utils.get_input_peer(entity)
except TypeError:
pass
def __getitem__(self, item):
"""
Gets the corresponding :tl:`InputPeer` for the given ID or peer,
or raises ``KeyError`` on any error (i.e. cannot be found).
"""
if not isinstance(item, int) or item < 0:
try:
return self.__dict__[utils.get_peer_id(item)]
except TypeError:
raise KeyError('Invalid key will not have entity') from None
for cls in (types.PeerUser, types.PeerChat, types.PeerChannel):
result = self.__dict__.get(utils.get_peer_id(cls(item)))
if result:
return result
raise KeyError('No cached entity for the given key')
def clear(self):
"""
Clear the entity cache.
"""
self.__dict__.clear()
def ensure_cached(
self,
update,
has_user_id=frozenset(_has_field[('user_id', int)]),
has_chat_id=frozenset(_has_field[('chat_id', int)]),
has_channel_id=frozenset(_has_field[('channel_id', int)]),
has_peer=frozenset(_has_field[('peer', 'TypePeer')] + _has_field[('peer', 'TypeDialogPeer')]),
has_message=frozenset(_has_field[('message', 'TypeMessage')])
):
"""
Ensures that all the relevant entities in the given update are cached.
"""
# This method is called pretty often and we want it to have the lowest
# overhead possible. For that, we avoid `isinstance` and constantly
# getting attributes out of `types.` by "caching" the constructor IDs
# in sets inside the arguments, and using local variables.
dct = self.__dict__
cid = update.CONSTRUCTOR_ID
if cid in has_user_id and \
update.user_id not in dct:
return False
if cid in has_chat_id and \
utils.get_peer_id(types.PeerChat(update.chat_id)) not in dct:
return False
if cid in has_channel_id and \
utils.get_peer_id(types.PeerChannel(update.channel_id)) not in dct:
return False
if cid in has_peer and \
utils.get_peer_id(update.peer) not in dct:
return False
if cid in has_message:
x = update.message
y = getattr(x, 'peer_id', None) # handle MessageEmpty
if y and utils.get_peer_id(y) not in dct:
return False
y = getattr(x, 'from_id', None)
if y and utils.get_peer_id(y) not in dct:
return False
# We don't quite worry about entities anywhere else.
# This is enough.
return True

View File

@ -19,8 +19,8 @@ class TypeNotFoundError(Exception):
def __init__(self, invalid_constructor_id, remaining): def __init__(self, invalid_constructor_id, remaining):
super().__init__( super().__init__(
'Could not find a matching Constructor ID for the TLObject ' 'Could not find a matching Constructor ID for the TLObject '
'that was supposed to be read with ID {:08x}. Most likely, ' 'that was supposed to be read with ID {:08x}. See the FAQ '
'a TLObject was trying to be read when it should not be read. ' 'for more details. '
'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining)) 'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining))
self.invalid_constructor_id = invalid_constructor_id self.invalid_constructor_id = invalid_constructor_id

View File

@ -160,7 +160,7 @@ class Album(EventBuilder):
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair( self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache) self.sender_id, self._entities, client._mb_entity_cache)
for msg in self.messages: for msg in self.messages:
msg._finish_init(client, self._entities, None) msg._finish_init(client, self._entities, None)

View File

@ -151,7 +151,7 @@ class CallbackQuery(EventBuilder):
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair( self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache) self.sender_id, self._entities, client._mb_entity_cache)
@property @property
def id(self): def id(self):
@ -208,8 +208,9 @@ class CallbackQuery(EventBuilder):
if not getattr(self._input_sender, 'access_hash', True): if not getattr(self._input_sender, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case # getattr with True to handle the InputPeerSelf() case
try: try:
self._input_sender = self._client._entity_cache[self._sender_id] self._input_sender = self._client._mb_entity_cache.get(
except KeyError: utils.resolve_id(self._sender_id)[0])._as_input_peer()
except AttributeError:
m = await self.get_message() m = await self.get_message()
if m: if m:
self._sender = m._sender self._sender = m._sender

View File

@ -425,9 +425,10 @@ class ChatAction(EventBuilder):
# If missing, try from the entity cache # If missing, try from the entity cache
try: try:
self._input_users.append(self._client._entity_cache[user_id]) self._input_users.append(self._client._mb_entity_cache.get(
utils.resolve_id(user_id)[0])._as_input_peer())
continue continue
except KeyError: except AttributeError:
pass pass
return self._input_users or [] return self._input_users or []

View File

@ -154,7 +154,7 @@ class EventCommon(ChatGetter, abc.ABC):
self._client = client self._client = client
if self._chat_peer: if self._chat_peer:
self._chat, self._input_chat = utils._get_entity_pair( self._chat, self._input_chat = utils._get_entity_pair(
self.chat_id, self._entities, client._entity_cache) self.chat_id, self._entities, client._mb_entity_cache)
else: else:
self._chat = self._input_chat = None self._chat = self._input_chat = None

View File

@ -99,7 +99,7 @@ class InlineQuery(EventBuilder):
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair( self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache) self.sender_id, self._entities, client._mb_entity_cache)
@property @property
def id(self): def id(self):

View File

@ -95,7 +95,7 @@ class UserUpdate(EventBuilder):
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair( self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache) self.sender_id, self._entities, client._mb_entity_cache)
@property @property
def user(self): def user(self):

View File

@ -176,7 +176,7 @@ class MTProtoState:
reader = BinaryReader(body) reader = BinaryReader(body)
reader.read_long() # remote_salt reader.read_long() # remote_salt
if reader.read_long() != self.id: if reader.read_long() != self.id:
raise SecurityError('Server replied with a wrong session ID') raise SecurityError('Server replied with a wrong session ID (see FAQ for details)')
remote_msg_id = reader.read_long() remote_msg_id = reader.read_long()
@ -208,12 +208,12 @@ class MTProtoState:
time_delta = now - remote_msg_time time_delta = now - remote_msg_time
if time_delta > MSG_TOO_OLD_DELTA: if time_delta > MSG_TOO_OLD_DELTA:
self._log.warning('Server sent a very old message with ID %d, ignoring', remote_msg_id) self._log.warning('Server sent a very old message with ID %d, ignoring (see FAQ for details)', remote_msg_id)
self._count_ignored() self._count_ignored()
return None return None
if -time_delta > MSG_TOO_NEW_DELTA: if -time_delta > MSG_TOO_NEW_DELTA:
self._log.warning('Server sent a very new message with ID %d, ignoring', remote_msg_id) self._log.warning('Server sent a very new message with ID %d, ignoring (see FAQ for details)', remote_msg_id)
self._count_ignored() self._count_ignored()
return None return None

View File

@ -66,8 +66,9 @@ class ChatGetter(abc.ABC):
""" """
if self._input_chat is None and self._chat_peer and self._client: if self._input_chat is None and self._chat_peer and self._client:
try: try:
self._input_chat = self._client._entity_cache[self._chat_peer] self._input_chat = self._client._mb_entity_cache.get(
except KeyError: utils.get_peer_id(self._chat_peer, add_mark=False))._as_input_peer()
except AttributeError:
pass pass
return self._input_chat return self._input_chat

View File

@ -5,7 +5,7 @@ from ..functions.messages import SaveDraftRequest
from ..types import DraftMessage from ..types import DraftMessage
from ...errors import RPCError from ...errors import RPCError
from ...extensions import markdown from ...extensions import markdown
from ...utils import get_input_peer, get_peer from ...utils import get_input_peer, get_peer, get_peer_id
class Draft: class Draft:
@ -53,8 +53,9 @@ class Draft:
""" """
if not self._input_entity: if not self._input_entity:
try: try:
self._input_entity = self._client._entity_cache[self._peer] self._input_entity = self._client._mb_entity_cache.get(
except KeyError: get_peer_id(self._peer, add_mark=False))._as_input_peer()
except AttributeError:
pass pass
return self._input_entity return self._input_entity

View File

@ -36,12 +36,12 @@ class Forward(ChatGetter, SenderGetter):
if ty == helpers._EntityType.USER: if ty == helpers._EntityType.USER:
sender_id = utils.get_peer_id(original.from_id) sender_id = utils.get_peer_id(original.from_id)
sender, input_sender = utils._get_entity_pair( sender, input_sender = utils._get_entity_pair(
sender_id, entities, client._entity_cache) sender_id, entities, client._mb_entity_cache)
elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL): elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL):
peer = original.from_id peer = original.from_id
chat, input_chat = utils._get_entity_pair( chat, input_chat = utils._get_entity_pair(
utils.get_peer_id(peer), entities, client._entity_cache) utils.get_peer_id(peer), entities, client._mb_entity_cache)
# This call resets the client # This call resets the client
ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat) ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat)

View File

@ -285,7 +285,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from: if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from:
self.out = True self.out = True
cache = client._entity_cache cache = client._mb_entity_cache
self._sender, self._input_sender = utils._get_entity_pair( self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, entities, cache) self.sender_id, entities, cache)
@ -1138,8 +1138,9 @@ class Message(ChatGetter, SenderGetter, TLObject):
return bot return bot
else: else:
try: try:
return self._client._entity_cache[self.via_bot_id] return self._client._mb_entity_cache.get(
except KeyError: utils.resolve_id(self.via_bot_id)[0])._as_input_peer()
except AttributeError:
raise ValueError('No input sender') from None raise ValueError('No input sender') from None
def _document_by_attribute(self, kind, condition=None): def _document_by_attribute(self, kind, condition=None):

View File

@ -1,5 +1,7 @@
import abc import abc
from ... import utils
class SenderGetter(abc.ABC): class SenderGetter(abc.ABC):
""" """
@ -69,9 +71,9 @@ class SenderGetter(abc.ABC):
""" """
if self._input_sender is None and self._sender_id and self._client: if self._input_sender is None and self._sender_id and self._client:
try: try:
self._input_sender = \ self._input_sender = self._client._mb_entity_cache.get(
self._client._entity_cache[self._sender_id] utils.resolve_id(self._sender_id)[0])._as_input_peer()
except KeyError: except AttributeError:
pass pass
return self._input_sender return self._input_sender

View File

@ -583,11 +583,14 @@ def _get_entity_pair(entity_id, entities, cache,
""" """
Returns ``(entity, input_entity)`` for the given entity ID. Returns ``(entity, input_entity)`` for the given entity ID.
""" """
if not entity_id:
return None, None
entity = entities.get(entity_id) entity = entities.get(entity_id)
try: try:
input_entity = cache[entity_id] input_entity = cache.get(resolve_id(entity_id)[0])._as_input_peer()
except KeyError: except AttributeError:
# KeyError is unlikely, so another TypeError won't hurt # AttributeError is unlikely, so another TypeError won't hurt
try: try:
input_entity = get_input_peer(entity) input_entity = get_input_peer(entity)
except TypeError: except TypeError: