Don't cache entities with min flag set, bump v1.10.2

Since layer 102, there are two access_hash. One with the min flag,
and one without it. This was causing channel invalid errors.

access_hash with min flag set can only be used to fetch files such
as profile pictures.

access_hash with min flag unset can be used under all circumstances.

Previously, the library did not distinguish between these, so it was
caching the hash that could hardly be used for anything.

With this change, only the "full" access_hash is stored, which will
work for any methods.

See also: https://core.telegram.org/api/min
This commit is contained in:
Lonami Exo 2019-09-12 19:17:32 +02:00
parent 5c72e1286e
commit 9c06f29aaf
4 changed files with 57 additions and 11 deletions

View File

@ -100,8 +100,10 @@ class MemorySession(Session):
p = utils.get_input_peer(e, allow_self=False) p = utils.get_input_peer(e, allow_self=False)
marked_id = utils.get_peer_id(p) marked_id = utils.get_peer_id(p)
except TypeError: except TypeError:
# Note: `get_input_peer` already checks for # Note: `get_input_peer` already checks for non-zero `access_hash`.
# non-zero `access_hash`. See issues #354 and #392. # See issues #354 and #392. It also checks that the entity
# is not `min`, because its `access_hash` cannot be used
# anywhere (since layer 102, there are two access hashes).
return return
if isinstance(p, (InputPeerUser, InputPeerChannel)): if isinstance(p, (InputPeerUser, InputPeerChannel)):

View File

@ -17,7 +17,7 @@ except ImportError as e:
sqlite3_err = type(e) sqlite3_err = type(e)
EXTENSION = '.session' EXTENSION = '.session'
CURRENT_VERSION = 5 # database version CURRENT_VERSION = 6 # database version
class SQLiteSession(MemorySession): class SQLiteSession(MemorySession):
@ -143,6 +143,12 @@ class SQLiteSession(MemorySession):
if old == 4: if old == 4:
old += 1 old += 1
c.execute("alter table sessions add column takeout_id integer") c.execute("alter table sessions add column takeout_id integer")
if old == 5:
# Not really any schema upgrade, but potentially all access
# hashes for User and Channel are wrong, so drop them off.
old += 1
c.execute('delete from entities')
c.close() c.close()
@staticmethod @staticmethod

View File

@ -138,12 +138,34 @@ def get_input_peer(entity, allow_self=True, check_hash=True):
Gets the input peer for the given "entity" (user, chat or channel). Gets the input peer for the given "entity" (user, chat or channel).
A ``TypeError`` is raised if the given entity isn't a supported type A ``TypeError`` is raised if the given entity isn't a supported type
or if ``check_hash is True`` but the entity's ``access_hash is None``. or if ``check_hash is True`` but the entity's ``access_hash is None``
*or* the entity contains ``min`` information. In this case, the hash
cannot be used for general purposes, and thus is not returned to avoid
any issues which can derive from invalid access hashes.
Note that ``check_hash`` **is ignored** if an input peer is already Note that ``check_hash`` **is ignored** if an input peer is already
passed since in that case we assume the user knows what they're doing. passed since in that case we assume the user knows what they're doing.
This is key to getting entities by explicitly passing ``hash = 0``. This is key to getting entities by explicitly passing ``hash = 0``.
""" """
# NOTE: It is important that this method validates the access hashes,
# because it is used when we *require* a valid general-purpose
# access hash. This includes caching, which relies on this method.
# Further, when resolving raw methods, they do e.g.,
# utils.get_input_channel(client.get_input_peer(...))
#
# ...which means that the client's method verifies the hashes.
#
# Excerpt from a conversation with official developers (slightly edited):
# > We send new access_hash for Channel with min flag since layer 102.
# > Previously, we omitted it.
# > That one works just to download the profile picture.
#
# < So, min hashes only work for getting files,
# < but the non-min hash is required for any other operation?
#
# > Yes.
#
# More information: https://core.telegram.org/api/min
try: try:
if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
return entity return entity
@ -159,19 +181,19 @@ def get_input_peer(entity, allow_self=True, check_hash=True):
if isinstance(entity, types.User): if isinstance(entity, types.User):
if entity.is_self and allow_self: if entity.is_self and allow_self:
return types.InputPeerSelf() return types.InputPeerSelf()
elif entity.access_hash is not None or not check_hash: elif (entity.access_hash is not None and not entity.min) or not check_hash:
return types.InputPeerUser(entity.id, entity.access_hash) return types.InputPeerUser(entity.id, entity.access_hash)
else: else:
raise TypeError('User without access_hash cannot be input') raise TypeError('User without access_hash or min info cannot be input')
if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)): if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)):
return types.InputPeerChat(entity.id) return types.InputPeerChat(entity.id)
if isinstance(entity, (types.Channel, types.ChannelForbidden)): if isinstance(entity, (types.Channel, types.ChannelForbidden)):
if entity.access_hash is not None or not check_hash: if (entity.access_hash is not None and not entity.min) or not check_hash:
return types.InputPeerChannel(entity.id, entity.access_hash) return types.InputPeerChannel(entity.id, entity.access_hash)
else: else:
raise TypeError('Channel without access_hash cannot be input') raise TypeError('Channel without access_hash or min info cannot be input')
if isinstance(entity, types.InputUser): if isinstance(entity, types.InputUser):
return types.InputPeerUser(entity.user_id, entity.access_hash) return types.InputPeerUser(entity.user_id, entity.access_hash)
@ -198,7 +220,15 @@ def get_input_peer(entity, allow_self=True, check_hash=True):
def get_input_channel(entity): def get_input_channel(entity):
"""Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone.""" """
Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone.
.. important::
This method does not validate for invalid general-purpose access
hashes, unlike `get_input_peer`. Consider using instead:
``get_input_channel(get_input_peer(channel))``.
"""
try: try:
if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel')
return entity return entity
@ -215,7 +245,15 @@ def get_input_channel(entity):
def get_input_user(entity): def get_input_user(entity):
"""Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone.""" """
Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone.
.. important::
This method does not validate for invalid general-purpose access
hashes, unlike `get_input_peer`. Consider using instead:
``get_input_channel(get_input_peer(channel))``.
"""
try: try:
if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'): if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'):
return entity return entity

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440. # Versions should comply with PEP440.
# This line is parsed in setup.py: # This line is parsed in setup.py:
__version__ = '1.10.1' __version__ = '1.10.2'