Begin updating the way updates are built

This commit is contained in:
Lonami Exo 2022-02-15 11:57:55 +01:00
parent c914a92dcf
commit 483e2aadf1
18 changed files with 337 additions and 284 deletions

View File

@ -170,7 +170,7 @@ async def _update_loop(self: 'TelegramClient'):
updates_to_dispatch = deque() updates_to_dispatch = deque()
while self.is_connected(): while self.is_connected():
if updates_to_dispatch: if updates_to_dispatch:
await _dispatch(self, updates_to_dispatch.popleft()) await _dispatch(self, *updates_to_dispatch.popleft())
continue continue
get_diff = self._message_box.get_difference() get_diff = self._message_box.get_difference()
@ -178,8 +178,7 @@ async def _update_loop(self: 'TelegramClient'):
self._log[__name__].info('Getting difference for account updates') self._log[__name__].info('Getting difference for account updates')
diff = await self(get_diff) diff = await self(get_diff)
updates, users, chats = self._message_box.apply_difference(diff, self._entity_cache) updates, users, chats = self._message_box.apply_difference(diff, self._entity_cache)
self._entity_cache.extend(users, chats) updates_to_dispatch.extend(_preprocess_updates(self, updates, users, chats))
updates_to_dispatch.extend(updates)
continue continue
get_diff = self._message_box.get_channel_difference(self._entity_cache) get_diff = self._message_box.get_channel_difference(self._entity_cache)
@ -187,8 +186,7 @@ async def _update_loop(self: 'TelegramClient'):
self._log[__name__].info('Getting difference for channel updates') self._log[__name__].info('Getting difference for channel updates')
diff = await self(get_diff) diff = await self(get_diff)
updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._entity_cache) updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._entity_cache)
self._entity_cache.extend(users, chats) updates_to_dispatch.extend(_preprocess_updates(self, updates, users, chats))
updates_to_dispatch.extend(updates)
continue continue
deadline = self._message_box.check_deadlines() deadline = self._message_box.check_deadlines()
@ -203,21 +201,50 @@ async def _update_loop(self: 'TelegramClient'):
processed = [] processed = []
users, chats = self._message_box.process_updates(updates, self._entity_cache, processed) users, chats = self._message_box.process_updates(updates, self._entity_cache, processed)
self._entity_cache.extend(users, chats) updates_to_dispatch.extend(_preprocess_updates(self, processed, users, chats))
updates_to_dispatch.extend(processed)
except Exception: except Exception:
self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)') self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)')
async def _dispatch(self, update): def _preprocess_updates(self, updates, users, chats):
self._dispatching_update_handlers = True self._entity_cache.extend(users, chats)
entities = Entities(self, users, chats)
return ((u, entities) for u in updates)
class Entities:
def __init__(self, client, users, chats):
self.self_id = client._session_state.user_id
self._entities = {e.id: e for e in itertools.chain(
(User(client, u) for u in users),
(Chat(client, c) for u in chats),
)}
def get(self, client, peer):
if not peer:
return None
id = utils.get_peer_id(peer)
try:
return self._entities[id]
except KeyError:
entity = client._entity_cache.get(query.user_id)
if not entity:
raise RuntimeError('Update is missing a hash but did not trigger a gap')
self._entities[entity.id] = User(client, entity) if entity.is_user else Chat(client, entity)
return self._entities[entity.id]
async def _dispatch(self, update, entities):
self._dispatching_update_handlers = True
try:
event_cache = {} event_cache = {}
for handler in self._update_handlers: for handler in self._update_handlers:
event = event_cache.get(handler._event) event, entities = event_cache.get(handler._event)
if not event: if not event:
event_cache[handler._event] = event = handler._event._build( # build can fail if we're missing an access hash; we want this to crash
update, [], self._session_state.user_id, {}, self) event_cache[handler._event] = event = handler._event._build(self, update, entities)
while True: while True:
# filters can be modified at any time, and there can be any amount of them which are not yet resolved # filters can be modified at any time, and there can be any amount of them which are not yet resolved
@ -226,7 +253,6 @@ async def _dispatch(self, update):
try: try:
await handler._callback(event) await handler._callback(event)
except StopPropagation: except StopPropagation:
self._dispatching_update_handlers = False
return return
except Exception: except Exception:
name = getattr(handler._callback, '__name__', repr(handler._callback)) name = getattr(handler._callback, '__name__', repr(handler._callback))
@ -244,5 +270,5 @@ async def _dispatch(self, update):
# we only want to continue on unresolved filter (to check if there are more unresolved) # we only want to continue on unresolved filter (to check if there are more unresolved)
break break
finally:
self._dispatching_update_handlers = False self._dispatching_update_handlers = False

View File

@ -107,7 +107,7 @@ class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.Se
_custom.sendergetter.SenderGetter.__init__(self, message.sender_id) _custom.sendergetter.SenderGetter.__init__(self, message.sender_id)
self.messages = messages self.messages = messages
def _build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, client, update, entities):
if not others: if not others:
return # We only care about albums which come inside the same Updates return # We only care about albums which come inside the same Updates
@ -146,6 +146,12 @@ class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.Se
and u.message.grouped_id == group) and u.message.grouped_id == group)
]) ])
self = cls.__new__(cls)
self._client = client
self._sender = entities.get(_tl.PeerUser(update.user_id))
self._chat = entities.get(_tl.PeerUser(update.user_id))
return self
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_id, self._entities) self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)

View File

@ -34,15 +34,12 @@ class StopPropagation(Exception):
class EventBuilder(abc.ABC): class EventBuilder(abc.ABC):
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
def _build(cls, update, others, self_id, entities, client): def _build(cls, client, update, entities):
""" """
Builds an event for the given update if possible, or returns None. Builds an event for the given update if possible, or returns None.
`others` are the rest of updates that came in the same container `entities` must have `get(Peer) -> User|Chat` and `self_id`,
as the current `update`. which must be the current user's ID.
`self_id` should be the current user's ID, since it is required
for some events which lack this information but still need it.
""" """

View File

@ -69,29 +69,32 @@ class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.senderg
Button.inline('Nope', b'no') Button.inline('Nope', b'no')
]) ])
""" """
def __init__(self, query, peer, msg_id): @classmethod
_custom.chatgetter.ChatGetter.__init__(self, peer) def _build(cls, client, update, entities):
_custom.sendergetter.SenderGetter.__init__(self, query.user_id) query = update
if isinstance(update, _tl.UpdateBotCallbackQuery):
peer = update.peer
msg_id = update.msg_id
elif isinstance(update, _tl.UpdateInlineBotCallbackQuery):
# See https://github.com/LonamiWebs/Telethon/pull/1005
# The long message ID is actually just msg_id + peer_id
msg_id, pid = struct.unpack('<ii', struct.pack('<q', update.msg_id.id))
peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid)
else:
return None
self = cls.__new__(cls)
self._client = client
self._sender = entities.get(_tl.PeerUser(query.user_id))
self._chat = entities.get(peer)
self.query = query self.query = query
self.data_match = None self.data_match = None
self.pattern_match = None self.pattern_match = None
self._message = None self._message = None
self._answered = False self._answered = False
@classmethod return self
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateBotCallbackQuery):
return cls.Event(update, update.peer, update.msg_id)
elif isinstance(update, _tl.UpdateInlineBotCallbackQuery):
# See https://github.com/LonamiWebs/Telethon/pull/1005
# The long message ID is actually just msg_id + peer_id
mid, pid = struct.unpack('<ii', struct.pack('<q', update.msg_id.id))
peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid)
return cls.Event(update, peer, mid)
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
@property @property
def id(self): def id(self):

View File

@ -72,18 +72,110 @@ class ChatAction(EventBuilder):
await event.reply('Welcome to the group!') await event.reply('Welcome to the group!')
""" """
def __init__(self, where, new_photo=None, @classmethod
added_by=None, kicked_by=None, created=None, from_approval=None, def _build(cls, client, update, entities):
users=None, new_title=None, pin_ids=None, pin=None, new_score=None): where = None
new_photo = None
added_by = None
kicked_by = None
created = None
from_approval = None
users = None
new_title = None
pin_ids = None
pin = None
new_score = None
# Rely on specific pin updates for unpins, but otherwise ignore them
# for new pins (we'd rather handle the new service message with pin,
# so that we can act on that message').
if isinstance(update, _tl.UpdatePinnedChannelMessages) and not update.pinned:
where = _tl.PeerChannel(update.channel_id)
pin_ids = update.messages
pin = update.pinned
elif isinstance(update, _tl.UpdatePinnedMessages) and not update.pinned:
where = update.peer
pin_ids = update.messages
pin = update.pinned
elif isinstance(update, _tl.UpdateChatParticipantAdd):
where = _tl.PeerChat(update.chat_id)
added_by = update.inviter_id or True
users = update.user_id
elif isinstance(update, _tl.UpdateChatParticipantDelete):
where = _tl.PeerChat(update.chat_id)
kicked_by = True
users = update.user_id
# UpdateChannel is sent if we leave a channel, and the update._entities
# set by _process_update would let us make some guesses. However it's
# better not to rely on this. Rely only in MessageActionChatDeleteUser.
elif (isinstance(update, (
_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage))
and isinstance(update.message, _tl.MessageService)):
msg = update.message
action = update.message.action
if isinstance(action, _tl.MessageActionChatJoinedByLink):
where = msg
added_by = True
users = msg.from_id
elif isinstance(action, _tl.MessageActionChatAddUser):
# If a user adds itself, it means they joined via the public chat username
added_by = ([msg.sender_id] == action.users) or msg.from_id
where = msg
added_by = added_by
users = action.users
elif isinstance(action, _tl.MessageActionChatJoinedByRequest):
# user joined from join request (after getting admin approval)
where = msg
from_approval = True
users = msg.from_id
elif isinstance(action, _tl.MessageActionChatDeleteUser):
where = msg
kicked_by = utils.get_peer_id(msg.from_id) if msg.from_id else True
users = action.user_id
elif isinstance(action, _tl.MessageActionChatCreate):
where = msg
users = action.users
created = True
new_title = action.title
elif isinstance(action, _tl.MessageActionChannelCreate):
where = msg
created = True
users = msg.from_id
new_title = action.title
elif isinstance(action, _tl.MessageActionChatEditTitle):
where = msg
users = msg.from_id
new_title = action.title
elif isinstance(action, _tl.MessageActionChatEditPhoto):
where = msg
users = msg.from_id
new_photo = action.photo
elif isinstance(action, _tl.MessageActionChatDeletePhoto):
where = msg
users = msg.from_id
new_photo = True
elif isinstance(action, _tl.MessageActionPinMessage) and msg.reply_to:
where = msg
pin_ids=[msg.reply_to_msg_id]
elif isinstance(action, _tl.MessageActionGameScore):
where = msg
new_score = action.score
self = cls.__new__(cls)
self._client = client
if isinstance(where, _tl.MessageService): if isinstance(where, _tl.MessageService):
self.action_message = where self.action_message = where
where = where.peer_id where = where.peer_id
else: else:
self.action_message = None self.action_message = None
# TODO needs some testing (can there be more than one id, and do they follow pin order?) self._chat = entities.get(where)
# same in get_pinned_message
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
self.new_pin = pin_ids is not None self.new_pin = pin_ids is not None
self._pin_ids = pin_ids self._pin_ids = pin_ids
@ -128,87 +220,7 @@ class ChatAction(EventBuilder):
self.new_score = new_score self.new_score = new_score
self.unpin = not pin self.unpin = not pin
@classmethod return self
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
# Rely on specific pin updates for unpins, but otherwise ignore them
# for new pins (we'd rather handle the new service message with pin,
# so that we can act on that message').
if isinstance(update, _tl.UpdatePinnedChannelMessages) and not update.pinned:
return cls.Event(_tl.PeerChannel(update.channel_id),
pin_ids=update.messages,
pin=update.pinned)
elif isinstance(update, _tl.UpdatePinnedMessages) and not update.pinned:
return cls.Event(update.peer,
pin_ids=update.messages,
pin=update.pinned)
elif isinstance(update, _tl.UpdateChatParticipantAdd):
return cls.Event(_tl.PeerChat(update.chat_id),
added_by=update.inviter_id or True,
users=update.user_id)
elif isinstance(update, _tl.UpdateChatParticipantDelete):
return cls.Event(_tl.PeerChat(update.chat_id),
kicked_by=True,
users=update.user_id)
# UpdateChannel is sent if we leave a channel, and the update._entities
# set by _process_update would let us make some guesses. However it's
# better not to rely on this. Rely only in MessageActionChatDeleteUser.
elif (isinstance(update, (
_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage))
and isinstance(update.message, _tl.MessageService)):
msg = update.message
action = update.message.action
if isinstance(action, _tl.MessageActionChatJoinedByLink):
return cls.Event(msg,
added_by=True,
users=msg.from_id)
elif isinstance(action, _tl.MessageActionChatAddUser):
# If a user adds itself, it means they joined via the public chat username
added_by = ([msg.sender_id] == action.users) or msg.from_id
return cls.Event(msg,
added_by=added_by,
users=action.users)
elif isinstance(action, _tl.MessageActionChatJoinedByRequest):
# user joined from join request (after getting admin approval)
return cls.Event(msg,
from_approval=True,
users=msg.from_id)
elif isinstance(action, _tl.MessageActionChatDeleteUser):
return cls.Event(msg,
kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True,
users=action.user_id)
elif isinstance(action, _tl.MessageActionChatCreate):
return cls.Event(msg,
users=action.users,
created=True,
new_title=action.title)
elif isinstance(action, _tl.MessageActionChannelCreate):
return cls.Event(msg,
created=True,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, _tl.MessageActionChatEditTitle):
return cls.Event(msg,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, _tl.MessageActionChatEditPhoto):
return cls.Event(msg,
users=msg.from_id,
new_photo=action.photo)
elif isinstance(action, _tl.MessageActionChatDeletePhoto):
return cls.Event(msg,
users=msg.from_id,
new_photo=True)
elif isinstance(action, _tl.MessageActionPinMessage) and msg.reply_to:
return cls.Event(msg,
pin_ids=[msg.reply_to_msg_id])
elif isinstance(action, _tl.MessageActionGameScore):
return cls.Event(msg,
new_score=action.score)
async def respond(self, *args, **kwargs): async def respond(self, *args, **kwargs):
""" """

View File

@ -40,21 +40,19 @@ class InlineQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.senderget
builder.article('lowercase', text=event.text.lower()), builder.article('lowercase', text=event.text.lower()),
]) ])
""" """
def __init__(self, query): @classmethod
_custom.chatgetter.ChatGetter.__init__(self, _tl.PeerUser(query.user_id)) def _build(cls, client, update, entities):
_custom.sendergetter.SenderGetter.__init__(self, query.user_id) if not isinstance(update, _tl.UpdateBotInlineQuery):
self.query = query return None
self = cls.__new__(cls)
self._client = client
self._sender = entities.get(_tl.PeerUser(update.user_id))
self._chat = entities.get(_tl.PeerUser(update.user_id))
self.query = update
self.pattern_match = None self.pattern_match = None
self._answered = False self._answered = False
return self
@classmethod
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateBotInlineQuery):
return cls.Event(update)
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
@property @property
def id(self): def id(self):

View File

@ -35,20 +35,18 @@ class MessageDeleted(EventBuilder, _custom.chatgetter.ChatGetter):
for msg_id in event.deleted_ids: for msg_id in event.deleted_ids:
print('Message', msg_id, 'was deleted in', event.chat_id) print('Message', msg_id, 'was deleted in', event.chat_id)
""" """
def __init__(self, deleted_ids, peer):
_custom.chatgetter.ChatGetter.__init__(self, chat_peer=peer)
self.deleted_id = None if not deleted_ids else deleted_ids[0]
self.deleted_ids = deleted_ids
@classmethod @classmethod
def _build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, client, update, entities):
if isinstance(update, _tl.UpdateDeleteMessages): if isinstance(update, _tl.UpdateDeleteMessages):
return cls.Event( peer = None
deleted_ids=update.messages,
peer=None
)
elif isinstance(update, _tl.UpdateDeleteChannelMessages): elif isinstance(update, _tl.UpdateDeleteChannelMessages):
return cls.Event( peer = _tl.PeerChannel(update.channel_id)
deleted_ids=update.messages, else:
peer=_tl.PeerChannel(update.channel_id) return None
)
self = cls.__new__(cls)
self._client = client
self._chat = entities.get(peer)
self.deleted_id = None if not update.messages else update.messages[0]
self.deleted_ids = update.messages
return self

View File

@ -41,7 +41,7 @@ class MessageEdited(EventBuilder):
print('Message', event.id, 'changed at', event.date) print('Message', event.id, 'changed at', event.date)
""" """
@classmethod @classmethod
def _build(cls, update, others, self_id, entities, client): def _build(cls, client, update, entities):
if isinstance(update, (_tl.UpdateEditMessage, if isinstance(update, (_tl.UpdateEditMessage,
_tl.UpdateEditChannelMessage)): _tl.UpdateEditChannelMessage)):
return cls._new(client, update.message, entities, None) return cls._new(client, update.message, entities, None)

View File

@ -45,24 +45,39 @@ class MessageRead(EventBuilder):
super().__init__(peer, self.max_id) super().__init__(peer, self.max_id)
@classmethod @classmethod
def _build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, client, update, entities):
out = False
contents = False
message_ids = None
if isinstance(update, _tl.UpdateReadHistoryInbox): if isinstance(update, _tl.UpdateReadHistoryInbox):
return cls.Event(update.peer, update.max_id, False) peer = update.peer
max_id = update.max_id
out = False
elif isinstance(update, _tl.UpdateReadHistoryOutbox): elif isinstance(update, _tl.UpdateReadHistoryOutbox):
return cls.Event(update.peer, update.max_id, True) peer = update.peer
max_id = update.max_id
out = True
elif isinstance(update, _tl.UpdateReadChannelInbox): elif isinstance(update, _tl.UpdateReadChannelInbox):
return cls.Event(_tl.PeerChannel(update.channel_id), peer = _tl.PeerChannel(update.channel_id)
update.max_id, False) max_id = update.max_id
out = False
elif isinstance(update, _tl.UpdateReadChannelOutbox): elif isinstance(update, _tl.UpdateReadChannelOutbox):
return cls.Event(_tl.PeerChannel(update.channel_id), peer = _tl.PeerChannel(update.channel_id)
update.max_id, True) max_id = update.max_id
out = True
elif isinstance(update, _tl.UpdateReadMessagesContents): elif isinstance(update, _tl.UpdateReadMessagesContents):
return cls.Event(message_ids=update.messages, peer = None
contents=True) message_ids = update.messages
contents = True
elif isinstance(update, _tl.UpdateChannelReadMessagesContents): elif isinstance(update, _tl.UpdateChannelReadMessagesContents):
return cls.Event(_tl.PeerChannel(update.channel_id), peer = _tl.PeerChannel(update.channel_id)
message_ids=update.messages, message_ids = update.messages
contents=True) contents = True
self = cls.__new__(cls)
self._client = client
self._chat = entities.get(peer)
return self
@property @property
def inbox(self): def inbox(self):

View File

@ -57,16 +57,8 @@ class NewMessage(EventBuilder, _custom.Message):
await asyncio.sleep(5) await asyncio.sleep(5)
await client.delete_messages(event.chat_id, [event.id, m.id]) await client.delete_messages(event.chat_id, [event.id, m.id])
""" """
def __init__(self, message):
self.__dict__['_init'] = False
super().__init__(chat_peer=message.peer_id,
msg_id=message.id, broadcast=bool(message.post))
self.pattern_match = None
self.message = message
@classmethod @classmethod
def _build(cls, update, others, self_id, entities, client): def _build(cls, client, update, entities):
if isinstance(update, if isinstance(update,
(_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)): (_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)):
if not isinstance(update.message, _tl.Message): if not isinstance(update.message, _tl.Message):

View File

@ -19,5 +19,5 @@ class Raw(EventBuilder):
print(update.stringify()) print(update.stringify())
""" """
@classmethod @classmethod
def _build(cls, update, others=None, self_id=None, *todo, **todo2): def _build(cls, client, update, entities):
return update return update

View File

@ -62,33 +62,35 @@ class UserUpdate(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergett
if event.uploading: if event.uploading:
await client.send_message(event.user_id, 'What are you sending?') await client.send_message(event.user_id, 'What are you sending?')
""" """
def __init__(self, peer, *, status=None, chat_peer=None, typing=None): @classmethod
_custom.chatgetter.ChatGetter.__init__(self, chat_peer or peer) def _build(cls, client, update, entities):
_custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) chat_peer = None
status = None
if isinstance(update, _tl.UpdateUserStatus):
peer = _tl.PeerUser(update.user_id)
status = update.status
typing = None
elif isinstance(update, _tl.UpdateChannelUserTyping):
peer = update.from_id
chat_peer = _tl.PeerChannel(update.channel_id)
typing = update.action
elif isinstance(update, _tl.UpdateChatUserTyping):
peer = update.from_id
chat_peer = _tl.PeerChat(update.chat_id)
typing = update.action
elif isinstance(update, _tl.UpdateUserTyping):
peer = update.user_id
typing = update.action
else:
return None
self = cls.__new__(cls)
self._client = client
self._sender = entities.get(peer)
self._chat = entities.get(chat_peer or peer)
self.status = status self.status = status
self.action = typing self.action = typing
return self
@classmethod
def _build(cls, update, others=None, self_id=None, *todo, **todo2):
if isinstance(update, _tl.UpdateUserStatus):
return UserUpdateEvent(_tl.PeerUser(update.user_id),
status=update.status)
elif isinstance(update, _tl.UpdateChannelUserTyping):
return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChannel(update.channel_id),
typing=update.action)
elif isinstance(update, _tl.UpdateChatUserTyping):
return UserUpdateEvent(update.from_id,
chat_peer=_tl.PeerChat(update.chat_id),
typing=update.action)
elif isinstance(update, _tl.UpdateUserTyping):
return UserUpdateEvent(update.user_id,
typing=update.action)
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities)
@property @property
def user(self): def user(self):

View File

@ -578,20 +578,6 @@ def get_input_group_call(call):
_raise_cast_fail(call, 'InputGroupCall') _raise_cast_fail(call, 'InputGroupCall')
def _get_entity_pair(entity_id, entities,
get_input_peer=get_input_peer):
"""
Returns ``(entity, input_entity)`` for the given entity ID.
"""
entity = entities.get(entity_id)
try:
input_entity = get_input_peer(entity)
except TypeError:
input_entity = None
return entity, input_entity
def get_message_id(message): def get_message_id(message):
"""Similar to :meth:`get_input_peer`, but for message IDs.""" """Similar to :meth:`get_input_peer`, but for message IDs."""
if message is None: if message is None:
@ -977,27 +963,13 @@ def get_peer(peer):
def get_peer_id(peer): def get_peer_id(peer):
""" """
Extract the integer ID from the given peer. Extract the integer ID from the given :tl:`Peer`.
""" """
# First we assert it's a Peer TLObject, or early return for integers pid = getattr(peer, 'user_id', None) or getattr(peer, 'channel_id', None) or getattr(peer, 'chat_id', None)
if isinstance(peer, int): if not isisintance(pid, int):
return peer
# Tell the user to use their client to resolve InputPeerSelf if we got one
if isinstance(peer, _tl.InputPeerSelf):
_raise_cast_fail(peer, 'int (you might want to use client.get_peer_id)')
try:
peer = get_peer(peer)
except TypeError:
_raise_cast_fail(peer, 'int') _raise_cast_fail(peer, 'int')
if isinstance(peer, _tl.PeerUser): return pid
return peer.user_id
elif isinstance(peer, _tl.PeerChat):
return peer.chat_id
else: # if isinstance(peer, _tl.PeerChannel):
return peer.channel_id
def _rle_decode(data): def _rle_decode(data):

View File

@ -1,5 +1,6 @@
from typing import Optional, List, TYPE_CHECKING from typing import Optional, List, TYPE_CHECKING
from datetime import datetime from datetime import datetime
from dataclasses import dataclass
import mimetypes import mimetypes
from .chatgetter import ChatGetter from .chatgetter import ChatGetter
from .sendergetter import SenderGetter from .sendergetter import SenderGetter
@ -27,17 +28,21 @@ def _fwd(field, doc):
return property(fget, fset, None, doc) return property(fget, fset, None, doc)
class _InputChat: @dataclass(frozen=True)
""" class _TinyChat:
Input channels and peer chats use a different name for "id" which breaks the property forwarding.
This class simply holds the two fields with proper names.
"""
__slots__ = ('id', 'access_hash') __slots__ = ('id', 'access_hash')
def __init__(self, input): id: int
self.id = getattr(input, 'channel_id', None) or input.chat_id access_hash: int
self.access_hash = getattr(input, 'access_hash', None)
@dataclass(frozen=True)
class _TinyChannel:
__slots__ = ('id', 'access_hash', 'megagroup')
id: int
access_hash: int
megagroup: bool # gigagroup is not present in channelForbidden but megagroup is
class Chat: class Chat:
@ -159,7 +164,11 @@ class Chat:
except AttributeError: except AttributeError:
migrated = None migrated = None
return Chat(_InputChat(migrated), self._client) if migrated else None if migrated is None:
return migrated
# Small chats don't migrate to other small chats, nor do they migrate to broadcast channels
return type(self)._new(self._client, _TinyChannel(migrated.channel_id, migrated.access_hash, True))
def __init__(self): def __init__(self):
raise TypeError('You cannot create Chat instances by hand!') raise TypeError('You cannot create Chat instances by hand!')
@ -168,7 +177,18 @@ class Chat:
def _new(cls, client, chat): def _new(cls, client, chat):
self = cls.__new__(cls) self = cls.__new__(cls)
self._client = client self._client = client
self._chat = chat self._chat = chat
if isinstance(cls, Entity):
if chat.is_user:
raise TypeError('Tried to construct Chat with non-chat Entity')
elif chat.ty == EntityType.GROUP:
self._chat = _TinyChat(chat.id)
else:
self._chat = _TinyChannel(chat.id, chat.hash, chat.is_group)
else:
self._chat = chat
self._full = None self._full = None
return self return self
@ -237,7 +257,7 @@ class Chat:
.. _megagroup: https://telegram.org/blog/supergroups5k .. _megagroup: https://telegram.org/blog/supergroups5k
""" """
return True return isinstance(self._chat, (_tl.Chat, _TinyChat, _tl.ChatForbidden, _tl.ChatEmpty)) or self._chat.megagroup
@property @property
def is_broadcast(self): def is_broadcast(self):
@ -253,7 +273,7 @@ class Chat:
.. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members .. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members
""" """
return True return not self.is_group
@property @property
def full_name(self): def full_name(self):

View File

@ -7,11 +7,9 @@ from ... import errors, _tl
class ChatGetter(abc.ABC): class ChatGetter(abc.ABC):
""" """
Helper base class that introduces the chat-related properties and methods. Helper base class that introduces the chat-related properties and methods.
"""
def __init__(self, chat, client):
self._chat = chat
self._client = client
The parent class must set both ``_chat`` and ``_client``.
"""
@property @property
def chat(self): def chat(self):
""" """
@ -60,7 +58,7 @@ class ChatGetter(abc.ABC):
# Here there's no need to fetch the chat - get_messages already did # Here there's no need to fetch the chat - get_messages already did
print(message.chat.stringify()) print(message.chat.stringify())
""" """
raise RuntimeError('TODO') raise RuntimeError('TODO fetch if it is tiny')
@property @property
def chat_id(self): def chat_id(self):

View File

@ -428,10 +428,10 @@ class Message(ChatGetter, SenderGetter):
if message.from_id is not None: if message.from_id is not None:
sender_id = utils.get_peer_id(message.from_id) sender_id = utils.get_peer_id(message.from_id)
# Note that these calls would reset the client self = cls.__new__(cls)
ChatGetter.__init__(self, message.peer_id, broadcast=message.post)
SenderGetter.__init__(self, sender_id)
self._client = client self._client = client
self._sender = entities.get(_tl.PeerUser(update.user_id))
self._chat = entities.get(_tl.PeerUser(update.user_id))
self._message = message self._message = message
# Convenient storage for custom functions # Convenient storage for custom functions

View File

@ -4,11 +4,9 @@ import abc
class SenderGetter(abc.ABC): class SenderGetter(abc.ABC):
""" """
Helper base class that introduces the sender-related properties and methods. Helper base class that introduces the sender-related properties and methods.
"""
def __init__(self, sender, client):
self._sender = sender
self._client = client
The parent class must set both ``_sender`` and ``_client``.
"""
@property @property
def sender(self): def sender(self):
""" """
@ -57,7 +55,7 @@ class SenderGetter(abc.ABC):
# Here there's no need to fetch the sender - get_messages already did # Here there's no need to fetch the sender - get_messages already did
print(message.sender.stringify()) print(message.sender.stringify())
""" """
raise RuntimeError('TODO') raise RuntimeError('TODO fetch if it is tiny')
@property @property
def sender_id(self): def sender_id(self):

View File

@ -1,5 +1,6 @@
from typing import Optional, List, TYPE_CHECKING from typing import Optional, List, TYPE_CHECKING
from datetime import datetime from datetime import datetime
from dataclasses import dataclass
import mimetypes import mimetypes
from .chatgetter import ChatGetter from .chatgetter import ChatGetter
from .sendergetter import SenderGetter from .sendergetter import SenderGetter
@ -67,6 +68,14 @@ class BotInfo:
self._user = user self._user = user
@dataclass(frozen=True)
class _TinyUser:
__slots__ = ('id', 'access_hash')
id: int
access_hash: int
class User: class User:
""" """
Represents a :tl:`User` (or :tl:`UserEmpty`, or :tl:`UserFull`) from the API. Represents a :tl:`User` (or :tl:`UserEmpty`, or :tl:`UserFull`) from the API.
@ -161,9 +170,16 @@ class User:
@classmethod @classmethod
def _new(cls, client, user): def _new(cls, client, user):
self = cls.__new__(cls) self = cls.__new__(cls)
self._client = client self._client = client
if isinstance(user, Entity):
if not user.is_user:
raise TypeError('Tried to construct User with non-user Entity')
self._user = _TinyUser(user.id, user.hash)
else:
self._user = user self._user = user
self._full = None self._full = None
raise RuntimeError('self._i_need_to_include_participant_info') raise RuntimeError('self._i_need_to_include_participant_info')