mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-25 02:43:45 +03:00
Make use of the new MessageBox
This commit is contained in:
parent
b5bfe5d9a1
commit
db09a92bc5
|
@ -15,6 +15,7 @@ from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
|
|||
from ..sessions import Session, SQLiteSession, MemorySession
|
||||
from ..tl import functions, types
|
||||
from ..tl.alltlobjects import LAYER
|
||||
from .._updates import MessageBox, EntityCache as MbEntityCache
|
||||
|
||||
DEFAULT_DC_ID = 2
|
||||
DEFAULT_IPV4_IP = '149.154.167.51'
|
||||
|
@ -376,18 +377,6 @@ class TelegramBaseClient(abc.ABC):
|
|||
proxy=init_proxy
|
||||
)
|
||||
|
||||
self._sender = MTProtoSender(
|
||||
self.session.auth_key,
|
||||
loggers=self._log,
|
||||
retries=self._connection_retries,
|
||||
delay=self._retry_delay,
|
||||
auto_reconnect=self._auto_reconnect,
|
||||
connect_timeout=self._timeout,
|
||||
auth_key_callback=self._auth_key_callback,
|
||||
update_callback=self._handle_update,
|
||||
auto_reconnect_callback=self._handle_auto_reconnect
|
||||
)
|
||||
|
||||
# Remember flood-waited requests to avoid making them again
|
||||
self._flood_waited_requests = {}
|
||||
|
||||
|
@ -396,18 +385,14 @@ class TelegramBaseClient(abc.ABC):
|
|||
self._borrow_sender_lock = asyncio.Lock()
|
||||
|
||||
self._updates_handle = None
|
||||
self._keepalive_handle = None
|
||||
self._last_request = time.time()
|
||||
self._channel_pts = {}
|
||||
self._no_updates = not receive_updates
|
||||
|
||||
if sequential_updates:
|
||||
self._updates_queue = asyncio.Queue()
|
||||
self._dispatching_updates_queue = asyncio.Event()
|
||||
else:
|
||||
# Use a set of pending instead of a queue so we can properly
|
||||
# terminate all pending updates on disconnect.
|
||||
self._updates_queue = set()
|
||||
self._dispatching_updates_queue = None
|
||||
# Used for non-sequential updates, in order to terminate all pending tasks on disconnect.
|
||||
self._sequential_updates = sequential_updates
|
||||
self._event_handler_tasks = set()
|
||||
|
||||
self._authorized = None # None = unknown, False = no, True = yes
|
||||
|
||||
|
@ -442,6 +427,26 @@ class TelegramBaseClient(abc.ABC):
|
|||
# A place to store if channels are a megagroup or not (see `edit_admin`)
|
||||
self._megagroup_cache = {}
|
||||
|
||||
# This is backported from v2 in a very ad-hoc way just to get proper update handling
|
||||
self._catch_up = True
|
||||
self._updates_queue = asyncio.Queue()
|
||||
self._message_box = 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._sender = MTProtoSender(
|
||||
self.session.auth_key,
|
||||
loggers=self._log,
|
||||
retries=self._connection_retries,
|
||||
delay=self._retry_delay,
|
||||
auto_reconnect=self._auto_reconnect,
|
||||
connect_timeout=self._timeout,
|
||||
auth_key_callback=self._auth_key_callback,
|
||||
updates_queue=self._updates_queue,
|
||||
auto_reconnect_callback=self._handle_auto_reconnect
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Properties
|
||||
|
@ -537,6 +542,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
))
|
||||
|
||||
self._updates_handle = self.loop.create_task(self._update_loop())
|
||||
self._keepalive_handle = self.loop.create_task(self._keepalive_loop())
|
||||
|
||||
def is_connected(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
|
@ -629,13 +635,12 @@ class TelegramBaseClient(abc.ABC):
|
|||
|
||||
# trio's nurseries would handle this for us, but this is asyncio.
|
||||
# All tasks spawned in the background should properly be terminated.
|
||||
if self._dispatching_updates_queue is None and self._updates_queue:
|
||||
for task in self._updates_queue:
|
||||
if self._event_handler_tasks:
|
||||
for task in self._event_handler_tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.wait(self._updates_queue)
|
||||
self._updates_queue.clear()
|
||||
|
||||
await asyncio.wait(self._event_handler_tasks)
|
||||
self._event_handler_tasks.clear()
|
||||
|
||||
await self.session.close()
|
||||
|
||||
|
@ -648,7 +653,8 @@ class TelegramBaseClient(abc.ABC):
|
|||
"""
|
||||
await self._sender.disconnect()
|
||||
await helpers._cancel(self._log[__name__],
|
||||
updates_handle=self._updates_handle)
|
||||
updates_handle=self._updates_handle,
|
||||
keepalive_handle=self._keepalive_handle)
|
||||
|
||||
async def _switch_dc(self: 'TelegramClient', new_dc):
|
||||
"""
|
||||
|
@ -845,10 +851,6 @@ class TelegramBaseClient(abc.ABC):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _handle_update(self: 'TelegramClient', update):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _update_loop(self: 'TelegramClient'):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -7,10 +7,12 @@ import time
|
|||
import traceback
|
||||
import typing
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
from .. import events, utils, errors
|
||||
from ..events.common import EventBuilder, EventCommon
|
||||
from ..tl import types, functions
|
||||
from .._updates import GapError
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
@ -237,106 +239,76 @@ class UpdateMethods:
|
|||
|
||||
await client.catch_up()
|
||||
"""
|
||||
pts, date = self._state_cache[None]
|
||||
if not pts:
|
||||
return
|
||||
|
||||
self.session.catching_up = True
|
||||
try:
|
||||
while True:
|
||||
d = await self(functions.updates.GetDifferenceRequest(
|
||||
pts, date, 0
|
||||
))
|
||||
if isinstance(d, (types.updates.DifferenceSlice,
|
||||
types.updates.Difference)):
|
||||
if isinstance(d, types.updates.Difference):
|
||||
state = d.state
|
||||
else:
|
||||
state = d.intermediate_state
|
||||
|
||||
pts, date = state.pts, state.date
|
||||
await self._handle_update(types.Updates(
|
||||
users=d.users,
|
||||
chats=d.chats,
|
||||
date=state.date,
|
||||
seq=state.seq,
|
||||
updates=d.other_updates + [
|
||||
types.UpdateNewMessage(m, 0, 0)
|
||||
for m in d.new_messages
|
||||
]
|
||||
))
|
||||
|
||||
# TODO Implement upper limit (max_pts)
|
||||
# We don't want to fetch updates we already know about.
|
||||
#
|
||||
# We may still get duplicates because the Difference
|
||||
# contains a lot of updates and presumably only has
|
||||
# the state for the last one, but at least we don't
|
||||
# unnecessarily fetch too many.
|
||||
#
|
||||
# updates.getDifference's pts_total_limit seems to mean
|
||||
# "how many pts is the request allowed to return", and
|
||||
# if there is more than that, it returns "too long" (so
|
||||
# there would be duplicate updates since we know about
|
||||
# some). This can be used to detect collisions (i.e.
|
||||
# it would return an update we have already seen).
|
||||
else:
|
||||
if isinstance(d, types.updates.DifferenceEmpty):
|
||||
date = d.date
|
||||
elif isinstance(d, types.updates.DifferenceTooLong):
|
||||
pts = d.pts
|
||||
break
|
||||
except (ConnectionError, asyncio.CancelledError):
|
||||
pass
|
||||
finally:
|
||||
# TODO Save new pts to session
|
||||
self._state_cache._pts_date = (pts, date)
|
||||
self.session.catching_up = False
|
||||
await self._updates_queue.put(types.UpdatesTooLong())
|
||||
|
||||
# endregion
|
||||
|
||||
# region Private methods
|
||||
|
||||
# It is important to not make _handle_update async because we rely on
|
||||
# the order that the updates arrive in to update the pts and date to
|
||||
# be always-increasing. There is also no need to make this async.
|
||||
async def _handle_update(self: 'TelegramClient', update):
|
||||
async def _update_loop(self: 'TelegramClient'):
|
||||
try:
|
||||
updates_to_dispatch = deque()
|
||||
|
||||
while self.is_connected():
|
||||
if updates_to_dispatch:
|
||||
if self._sequential_updates:
|
||||
await self._dispatch_update(updates_to_dispatch.popleft())
|
||||
else:
|
||||
while updates_to_dispatch:
|
||||
task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft()))
|
||||
self._event_handler_tasks.add(task)
|
||||
task.add_done_callback(lambda _: self._event_handler_tasks.discard(task))
|
||||
|
||||
continue
|
||||
|
||||
get_diff = self._message_box.get_difference()
|
||||
if get_diff:
|
||||
self._log[__name__].info('Getting difference for account updates')
|
||||
diff = await self(get_diff)
|
||||
updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache)
|
||||
updates_to_dispatch.extend(await self._preprocess_updates(updates, users, chats))
|
||||
continue
|
||||
|
||||
get_diff = self._message_box.get_channel_difference(self._mb_entity_cache)
|
||||
if get_diff:
|
||||
self._log[__name__].info('Getting difference for channel updates')
|
||||
diff = await self(get_diff)
|
||||
updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache)
|
||||
updates_to_dispatch.extend(await self._preprocess_updates(updates, users, chats))
|
||||
continue
|
||||
|
||||
deadline = self._message_box.check_deadlines()
|
||||
try:
|
||||
updates = await asyncio.wait_for(
|
||||
self._updates_queue.get(),
|
||||
deadline - asyncio.get_running_loop().time()
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
self._log[__name__].info('Timeout waiting for updates expired')
|
||||
continue
|
||||
|
||||
processed = []
|
||||
try:
|
||||
users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed)
|
||||
except GapError:
|
||||
continue # get(_channel)_difference will start returning requests
|
||||
|
||||
updates_to_dispatch.extend(await self._preprocess_updates(processed, users, chats))
|
||||
except Exception:
|
||||
self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)')
|
||||
|
||||
async def _preprocess_updates(self, updates, users, chats):
|
||||
await self.session.process_entities(update)
|
||||
self._entity_cache.add(update)
|
||||
|
||||
if isinstance(update, (types.Updates, types.UpdatesCombined)):
|
||||
entities = {utils.get_peer_id(x): x for x in
|
||||
itertools.chain(update.users, update.chats)}
|
||||
for u in update.updates:
|
||||
self._process_update(u, update.updates, entities=entities)
|
||||
elif isinstance(update, types.UpdateShort):
|
||||
self._process_update(update.update, None)
|
||||
else:
|
||||
self._process_update(update, None)
|
||||
self._mb_entity_cache.extend(users, chats)
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(users, chats)}
|
||||
for u in updates:
|
||||
u._entities = entities
|
||||
return updates
|
||||
|
||||
self._state_cache.update(update)
|
||||
|
||||
def _process_update(self: 'TelegramClient', update, others, entities=None):
|
||||
update._entities = entities or {}
|
||||
|
||||
# This part is somewhat hot so we don't bother patching
|
||||
# update with channel ID/its state. Instead we just pass
|
||||
# arguments which is faster.
|
||||
channel_id = self._state_cache.get_channel_id(update)
|
||||
args = (update, others, channel_id, self._state_cache[channel_id])
|
||||
if self._dispatching_updates_queue is None:
|
||||
task = self.loop.create_task(self._dispatch_update(*args))
|
||||
self._updates_queue.add(task)
|
||||
task.add_done_callback(lambda _: self._updates_queue.discard(task))
|
||||
else:
|
||||
self._updates_queue.put_nowait(args)
|
||||
if not self._dispatching_updates_queue.is_set():
|
||||
self._dispatching_updates_queue.set()
|
||||
self.loop.create_task(self._dispatch_queue_updates())
|
||||
|
||||
self._state_cache.update(update)
|
||||
|
||||
async def _update_loop(self: 'TelegramClient'):
|
||||
async def _keepalive_loop(self: 'TelegramClient'):
|
||||
# Pings' ID don't really need to be secure, just "random"
|
||||
rnd = lambda: random.randrange(-2**63, 2**63)
|
||||
while self.is_connected():
|
||||
|
@ -374,50 +346,9 @@ class UpdateMethods:
|
|||
# it every minute instead. No-op if there's nothing new.
|
||||
await self.session.save()
|
||||
|
||||
# We need to send some content-related request at least hourly
|
||||
# for Telegram to keep delivering updates, otherwise they will
|
||||
# just stop even if we're connected. Do so every 30 minutes.
|
||||
#
|
||||
# TODO Call getDifference instead since it's more relevant
|
||||
if time.time() - self._last_request > 30 * 60:
|
||||
if not await self.is_user_authorized():
|
||||
# What can be the user doing for so
|
||||
# long without being logged in...?
|
||||
continue
|
||||
|
||||
try:
|
||||
await self(functions.updates.GetStateRequest())
|
||||
except (ConnectionError, asyncio.CancelledError):
|
||||
return
|
||||
|
||||
async def _dispatch_queue_updates(self: 'TelegramClient'):
|
||||
while not self._updates_queue.empty():
|
||||
await self._dispatch_update(*self._updates_queue.get_nowait())
|
||||
|
||||
self._dispatching_updates_queue.clear()
|
||||
|
||||
async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, pts_date):
|
||||
if not self._entity_cache.ensure_cached(update):
|
||||
# We could add a lock to not fetch the same pts twice if we are
|
||||
# already fetching it. However this does not happen in practice,
|
||||
# which makes sense, because different updates have different pts.
|
||||
if self._state_cache.update(update, check_only=True):
|
||||
# If the update doesn't have pts, fetching won't do anything.
|
||||
# For example, UpdateUserStatus or UpdateChatUserTyping.
|
||||
try:
|
||||
await self._get_difference(update, channel_id, pts_date)
|
||||
except OSError:
|
||||
pass # We were disconnected, that's okay
|
||||
except errors.RPCError:
|
||||
# There's a high chance the request fails because we lack
|
||||
# the channel. Because these "happen sporadically" (#1428)
|
||||
# we should be okay (no flood waits) even if more occur.
|
||||
pass
|
||||
except ValueError:
|
||||
# There is a chance that GetFullChannelRequest and GetDifferenceRequest
|
||||
# inside the _get_difference() function will end up with
|
||||
# ValueError("Request was unsuccessful N time(s)") for whatever reasons.
|
||||
pass
|
||||
async def _dispatch_update(self: 'TelegramClient', update):
|
||||
# TODO only used for AlbumHack, and MessageBox is not really designed for this
|
||||
others = None
|
||||
|
||||
if not self._self_input_peer:
|
||||
# Some updates require our own ID, so we must make sure
|
||||
|
@ -523,67 +454,6 @@ class UpdateMethods:
|
|||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].exception('Unhandled exception on %s', name)
|
||||
|
||||
async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date):
|
||||
"""
|
||||
Get the difference for this `channel_id` if any, then load entities.
|
||||
|
||||
Calls :tl:`updates.getDifference`, which fills the entities cache
|
||||
(always done by `__call__`) and lets us know about the full entities.
|
||||
"""
|
||||
# Fetch since the last known pts/date before this update arrived,
|
||||
# in order to fetch this update at full, including its entities.
|
||||
self._log[__name__].debug('Getting difference for entities '
|
||||
'for %r', update.__class__)
|
||||
if channel_id:
|
||||
# There are reports where we somehow call get channel difference
|
||||
# with `InputPeerEmpty`. Check our assumptions to better debug
|
||||
# this when it happens.
|
||||
assert isinstance(channel_id, int), 'channel_id was {}, not int in {}'.format(type(channel_id), update)
|
||||
try:
|
||||
# Wrap the ID inside a peer to ensure we get a channel back.
|
||||
where = await self.get_input_entity(types.PeerChannel(channel_id))
|
||||
except ValueError:
|
||||
# There's a high chance that this fails, since
|
||||
# we are getting the difference to fetch entities.
|
||||
return
|
||||
|
||||
if not pts_date:
|
||||
# First-time, can't get difference. Get pts instead.
|
||||
result = await self(functions.channels.GetFullChannelRequest(
|
||||
utils.get_input_channel(where)
|
||||
))
|
||||
self._state_cache[channel_id] = result.full_chat.pts
|
||||
return
|
||||
|
||||
result = await self(functions.updates.GetChannelDifferenceRequest(
|
||||
channel=where,
|
||||
filter=types.ChannelMessagesFilterEmpty(),
|
||||
pts=pts_date, # just pts
|
||||
limit=100,
|
||||
force=True
|
||||
))
|
||||
else:
|
||||
if not pts_date[0]:
|
||||
# First-time, can't get difference. Get pts instead.
|
||||
result = await self(functions.updates.GetStateRequest())
|
||||
self._state_cache[None] = result.pts, result.date
|
||||
return
|
||||
|
||||
result = await self(functions.updates.GetDifferenceRequest(
|
||||
pts=pts_date[0],
|
||||
date=pts_date[1],
|
||||
qts=0
|
||||
))
|
||||
|
||||
if isinstance(result, (types.updates.Difference,
|
||||
types.updates.DifferenceSlice,
|
||||
types.updates.ChannelDifference,
|
||||
types.updates.ChannelDifferenceTooLong)):
|
||||
update._entities.update({
|
||||
utils.get_peer_id(x): x for x in
|
||||
itertools.chain(result.users, result.chats)
|
||||
})
|
||||
|
||||
async def _handle_auto_reconnect(self: 'TelegramClient'):
|
||||
# TODO Catch-up
|
||||
# For now we make a high-level request to let Telegram
|
||||
|
|
|
@ -44,7 +44,7 @@ class MTProtoSender:
|
|||
def __init__(self, auth_key, *, loggers,
|
||||
retries=5, delay=1, auto_reconnect=True, connect_timeout=None,
|
||||
auth_key_callback=None,
|
||||
update_callback=None, auto_reconnect_callback=None):
|
||||
updates_queue=None, auto_reconnect_callback=None):
|
||||
self._connection = None
|
||||
self._loggers = loggers
|
||||
self._log = loggers[__name__]
|
||||
|
@ -53,7 +53,7 @@ class MTProtoSender:
|
|||
self._auto_reconnect = auto_reconnect
|
||||
self._connect_timeout = connect_timeout
|
||||
self._auth_key_callback = auth_key_callback
|
||||
self._update_callback = update_callback
|
||||
self._updates_queue = updates_queue
|
||||
self._auto_reconnect_callback = auto_reconnect_callback
|
||||
self._connect_lock = asyncio.Lock()
|
||||
self._ping = None
|
||||
|
@ -645,8 +645,7 @@ class MTProtoSender:
|
|||
return
|
||||
|
||||
self._log.debug('Handling update %s', message.obj.__class__.__name__)
|
||||
if self._update_callback:
|
||||
await self._update_callback(message.obj)
|
||||
self._updates_queue.put_nowait(message.obj)
|
||||
|
||||
async def _handle_pong(self, message):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue
Block a user