Add known entities to all updates and use them in the events

This should reduce the amount of API calls made when getting the
full sender/chat on events (mostly on channels, where Telegram
seems to always send Updates instead only a normal Update).
This commit is contained in:
Lonami Exo 2018-03-14 10:28:21 +01:00
parent fd309f0407
commit 7e9d19d727
2 changed files with 70 additions and 18 deletions

View File

@ -71,6 +71,7 @@ class _EventCommon(abc.ABC):
"""Intermediate class with common things to all events""" """Intermediate class with common things to all events"""
def __init__(self, chat_peer=None, msg_id=None, broadcast=False): def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
self._entities = {}
self._client = None self._client = None
self._chat_peer = chat_peer self._chat_peer = chat_peer
self._message_id = msg_id self._message_id = msg_id
@ -104,6 +105,7 @@ class _EventCommon(abc.ABC):
) )
except RPCError: except RPCError:
return return
# TODO This could return a tuple to also have the full entity
entity = { entity = {
utils.get_peer_id(x): x for x in itertools.chain( utils.get_peer_id(x): x for x in itertools.chain(
getattr(result, 'chats', []), getattr(result, 'chats', []),
@ -148,12 +150,19 @@ class _EventCommon(abc.ABC):
def chat(self): def chat(self):
""" """
The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which The (:obj:`User` | :obj:`Chat` | :obj:`Channel`, optional) on which
the event occurred. This property will make an API call the first time the event occurred. This property may make an API call the first time
to get the most up to date version of the chat, so use with care as to get the most up to date version of the chat (mostly when the event
there is no caching besides local caching yet. doesn't belong to a channel), so keep that in mind.
""" """
if self._chat is None and self.input_chat: if not self.input_chat:
return None
if self._chat is None:
self._chat = self._entities.get(utils.get_peer_id(self._input_chat))
if self._chat is None:
self._chat = self._client.get_entity(self._input_chat) self._chat = self._client.get_entity(self._input_chat)
return self._chat return self._chat
@ -249,6 +258,7 @@ class NewMessage(_EventBuilder):
return return
# Short-circuit if we let pass all events # Short-circuit if we let pass all events
event._entities = update.entities
if all(x is None for x in (self.incoming, self.outgoing, self.chats, if all(x is None for x in (self.incoming, self.outgoing, self.chats,
self.pattern)): self.pattern)):
return event return event
@ -300,8 +310,6 @@ class NewMessage(_EventBuilder):
self.message = message self.message = message
self._text = None self._text = None
self._input_chat = None
self._chat = None
self._input_sender = None self._input_sender = None
self._sender = None self._sender = None
@ -395,14 +403,22 @@ class NewMessage(_EventBuilder):
@property @property
def sender(self): def sender(self):
""" """
This (:obj:`User`) will make an API call the first time to get This (:obj:`User`) may make an API call the first time to get
the most up to date version of the sender, so use with care as the most up to date version of the sender (mostly when the event
there is no caching besides local caching yet. doesn't belong to a channel), so keep that in mind.
``input_sender`` needs to be available (often the case). ``input_sender`` needs to be available (often the case).
""" """
if self._sender is None and self.input_sender: if not self.input_sender:
return None
if self._sender is None:
self._sender = \
self._entities.get(utils.get_peer_id(self._input_sender))
if self._sender is None:
self._sender = self._client.get_entity(self._input_sender) self._sender = self._client.get_entity(self._input_sender)
return self._sender return self._sender
@property @property
@ -621,6 +637,7 @@ class ChatAction(_EventBuilder):
else: else:
return return
event._entities = update.entities
return self._filter_event(event) return self._filter_event(event)
class Event(_EventCommon): class Event(_EventCommon):
@ -762,7 +779,12 @@ class ChatAction(_EventBuilder):
The user who added ``users``, if applicable (``None`` otherwise). The user who added ``users``, if applicable (``None`` otherwise).
""" """
if self._added_by and not isinstance(self._added_by, types.User): if self._added_by and not isinstance(self._added_by, types.User):
self._added_by = self._client.get_entity(self._added_by) self._added_by =\
self._entities.get(utils.get_peer_id(self._added_by))
if not self._added_by:
self._added_by = self._client.get_entity(self._added_by)
return self._added_by return self._added_by
@property @property
@ -771,7 +793,12 @@ class ChatAction(_EventBuilder):
The user who kicked ``users``, if applicable (``None`` otherwise). The user who kicked ``users``, if applicable (``None`` otherwise).
""" """
if self._kicked_by and not isinstance(self._kicked_by, types.User): if self._kicked_by and not isinstance(self._kicked_by, types.User):
self._kicked_by = self._client.get_entity(self._kicked_by) self._kicked_by =\
self._entities.get(utils.get_peer_id(self._kicked_by))
if not self._kicked_by:
self._kicked_by = self._client.get_entity(self._kicked_by)
return self._kicked_by return self._kicked_by
@property @property
@ -801,11 +828,24 @@ class ChatAction(_EventBuilder):
Might be empty if the information can't be retrieved or there Might be empty if the information can't be retrieved or there
are no users taking part. are no users taking part.
""" """
if self._users is None and self._user_peers: if not self._user_peers:
return []
if self._users is None:
have, missing = [], []
for peer in self._user_peers:
user = self._entities.get(utils.get_peer_id(peer))
if user:
have.append(user)
else:
missing.append(peer)
try: try:
self._users = self._client.get_entity(self._user_peers) missing = self._client.get_entity(missing)
except (TypeError, ValueError): except (TypeError, ValueError):
self._users = [] missing = []
self._user_peers = have + missing
return self._users return self._users
@ -837,6 +877,7 @@ class UserUpdate(_EventBuilder):
else: else:
return return
event._entities = update.entities
return self._filter_event(event) return self._filter_event(event)
class Event(_EventCommon): class Event(_EventCommon):
@ -984,6 +1025,7 @@ class MessageEdited(NewMessage):
else: else:
return return
event._entities = update.entities
return self._filter_event(event) return self._filter_event(event)
@ -1005,6 +1047,7 @@ class MessageDeleted(_EventBuilder):
else: else:
return return
event._entities = update.entities
return self._filter_event(event) return self._filter_event(event)
class Event(_EventCommon): class Event(_EventCommon):

View File

@ -1,10 +1,10 @@
import itertools
import logging import logging
import pickle
from collections import deque
from queue import Queue, Empty
from datetime import datetime from datetime import datetime
from queue import Queue, Empty
from threading import RLock, Thread from threading import RLock, Thread
from . import utils
from .tl import types as tl from .tl import types as tl
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
@ -127,14 +127,23 @@ class UpdateState:
# After running the script for over an hour and receiving over # After running the script for over an hour and receiving over
# 1000 updates, the only duplicates received were users going # 1000 updates, the only duplicates received were users going
# online or offline. We can trust the server until new reports. # online or offline. We can trust the server until new reports.
#
# TODO Note somewhere that all updates are modified to include
# .entities, which is a dictionary you can access but may be empty.
# This should only be used as read-only.
if isinstance(update, tl.UpdateShort): if isinstance(update, tl.UpdateShort):
update.update.entities = {}
self._updates.put(update.update) self._updates.put(update.update)
# Expand "Updates" into "Update", and pass these to callbacks. # Expand "Updates" into "Update", and pass these to callbacks.
# Since .users and .chats have already been processed, we # Since .users and .chats have already been processed, we
# don't need to care about those either. # don't need to care about those either.
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
entities = {utils.get_peer_id(x): x for x in
itertools.chain(update.users, update.chats)}
for u in update.updates: for u in update.updates:
u.entities = entities
self._updates.put(u) self._updates.put(u)
# TODO Handle "tl.UpdatesTooLong" # TODO Handle "tl.UpdatesTooLong"
else: else:
update.entities = {}
self._updates.put(update) self._updates.put(update)