2018-06-13 17:20:15 +03:00
|
|
|
import asyncio
|
2020-05-16 10:58:37 +03:00
|
|
|
import inspect
|
2018-06-14 23:51:57 +03:00
|
|
|
import itertools
|
2018-06-18 14:22:25 +03:00
|
|
|
import random
|
2021-01-30 15:47:28 +03:00
|
|
|
import sys
|
2018-06-18 14:22:25 +03:00
|
|
|
import time
|
2021-01-30 15:47:28 +03:00
|
|
|
import traceback
|
2019-05-03 22:37:27 +03:00
|
|
|
import typing
|
2021-01-30 15:47:28 +03:00
|
|
|
import logging
|
2022-01-28 16:12:32 +03:00
|
|
|
import inspect
|
|
|
|
import bisect
|
|
|
|
import warnings
|
2022-01-22 15:27:00 +03:00
|
|
|
from collections import deque
|
2018-06-10 14:58:21 +03:00
|
|
|
|
2021-09-24 21:07:34 +03:00
|
|
|
from ..errors._rpcbase import RpcError
|
2021-09-24 22:11:50 +03:00
|
|
|
from .._events.raw import Raw
|
2022-01-28 16:12:32 +03:00
|
|
|
from .._events.base import StopPropagation, EventBuilder, EventHandler
|
2022-02-04 13:46:38 +03:00
|
|
|
from .._events.filters import make_filter, NotResolved
|
2021-09-26 20:58:42 +03:00
|
|
|
from .._misc import utils
|
|
|
|
from .. import _tl
|
2019-05-03 22:37:27 +03:00
|
|
|
|
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
from .telegramclient import TelegramClient
|
2018-06-10 14:58:21 +03:00
|
|
|
|
|
|
|
|
2021-08-03 19:33:46 +03:00
|
|
|
Callback = typing.Callable[[typing.Any], typing.Any]
|
|
|
|
|
2018-06-10 14:58:21 +03:00
|
|
|
|
2021-09-11 14:33:27 +03:00
|
|
|
async def set_receive_updates(self: 'TelegramClient', receive_updates):
|
|
|
|
self._no_updates = not receive_updates
|
|
|
|
if receive_updates:
|
2021-09-12 13:16:02 +03:00
|
|
|
await self(_tl.fn.updates.GetState())
|
2018-06-25 14:32:31 +03:00
|
|
|
|
2021-09-11 15:05:24 +03:00
|
|
|
async def run_until_disconnected(self: 'TelegramClient'):
|
2021-09-18 16:41:04 +03:00
|
|
|
# Make a high-level request to notify that we want updates
|
|
|
|
await self(_tl.fn.updates.GetState())
|
2022-01-16 15:59:43 +03:00
|
|
|
await self._sender.wait_disconnected()
|
2021-09-11 14:33:27 +03:00
|
|
|
|
2022-01-28 16:12:32 +03:00
|
|
|
def on(self: 'TelegramClient', *events, priority=0, **filters):
|
2021-09-11 14:33:27 +03:00
|
|
|
def decorator(f):
|
2022-01-28 16:12:32 +03:00
|
|
|
for event in events:
|
|
|
|
self.add_event_handler(f, event, priority=priority, **filters)
|
2021-09-11 14:33:27 +03:00
|
|
|
return f
|
2019-05-20 12:38:26 +03:00
|
|
|
|
2021-09-11 14:33:27 +03:00
|
|
|
return decorator
|
2019-05-20 12:38:26 +03:00
|
|
|
|
2021-09-11 14:33:27 +03:00
|
|
|
def add_event_handler(
|
|
|
|
self: 'TelegramClient',
|
2022-01-28 16:12:32 +03:00
|
|
|
callback=None,
|
|
|
|
event=None,
|
|
|
|
priority=0,
|
|
|
|
**filters
|
|
|
|
):
|
|
|
|
if callback is None:
|
|
|
|
return functools.partial(add_event_handler, self, event=event, priority=priority, **filters)
|
|
|
|
|
|
|
|
if event is None:
|
|
|
|
for param in inspect.signature(callback).parameters.values():
|
2022-02-04 13:46:08 +03:00
|
|
|
event = None if param.annotation is inspect.Signature.empty else param.annotation
|
2022-01-28 16:12:32 +03:00
|
|
|
break # only check the first parameter
|
|
|
|
|
|
|
|
if event is None:
|
|
|
|
event = Raw
|
|
|
|
|
2022-01-28 22:21:15 +03:00
|
|
|
if not inspect.iscoroutinefunction(callback):
|
|
|
|
raise TypeError(f'callback was not an async def function: {callback!r}')
|
|
|
|
|
|
|
|
if not isinstance(event, type):
|
|
|
|
raise TypeError(f'event type was not a type (an instance of something was probably used): {event!r}')
|
|
|
|
|
|
|
|
if not isinstance(priority, int):
|
|
|
|
raise TypeError(f'priority was not an integer: {priority!r}')
|
|
|
|
|
|
|
|
if not issubclass(event, EventBuilder):
|
|
|
|
try:
|
|
|
|
if event.SUBCLASS_OF_ID != 0x9f89304e:
|
|
|
|
raise TypeError(f'invalid raw update type for the event handler: {event!r}')
|
|
|
|
|
|
|
|
if 'types' in filters:
|
|
|
|
warnings.warn('"types" filter is ignored when the event type already is a raw update')
|
|
|
|
|
|
|
|
filters['types'] = event
|
|
|
|
event = Raw
|
|
|
|
except AttributeError:
|
|
|
|
raise TypeError(f'unrecognized event handler type: {param.annotation!r}')
|
|
|
|
|
2022-01-28 16:12:32 +03:00
|
|
|
handler = EventHandler(event, callback, priority, make_filter(**filters))
|
2022-01-28 22:21:15 +03:00
|
|
|
|
|
|
|
if self._dispatching_update_handlers:
|
|
|
|
# Now that there's a copy, we're no longer dispatching from the old update_handlers,
|
|
|
|
# so we can modify it. This is why we can turn the flag off.
|
|
|
|
self._update_handlers = self._update_handlers[:]
|
|
|
|
self._dispatching_update_handlers = False
|
|
|
|
|
2022-01-28 16:12:32 +03:00
|
|
|
bisect.insort(self._update_handlers, handler)
|
|
|
|
return handler
|
2019-05-20 12:38:26 +03:00
|
|
|
|
2021-09-11 14:33:27 +03:00
|
|
|
def remove_event_handler(
|
|
|
|
self: 'TelegramClient',
|
2022-02-24 12:59:52 +03:00
|
|
|
callback=None,
|
|
|
|
event=None,
|
|
|
|
*,
|
|
|
|
priority=None,
|
2022-01-28 16:12:32 +03:00
|
|
|
):
|
|
|
|
if callback is None and event is None and priority is None:
|
|
|
|
raise ValueError('must specify at least one of callback, event or priority')
|
|
|
|
|
|
|
|
if not self._update_handlers:
|
|
|
|
return [] # won't be removing anything (some code paths rely on non-empty lists)
|
|
|
|
|
2022-01-28 22:21:15 +03:00
|
|
|
if self._dispatching_update_handlers:
|
|
|
|
# May be an unnecessary copy if nothing was removed, but that's not a big deal.
|
|
|
|
self._update_handlers = self._update_handlers[:]
|
|
|
|
self._dispatching_update_handlers = False
|
|
|
|
|
2022-01-28 16:12:32 +03:00
|
|
|
if isinstance(callback, EventHandler):
|
|
|
|
if event is not None or priority is not None:
|
|
|
|
warnings.warn('event and priority are ignored when removing EventHandler instances')
|
|
|
|
|
|
|
|
index = bisect.bisect_left(self._update_handlers, callback)
|
|
|
|
try:
|
|
|
|
if self._update_handlers[index] == callback:
|
|
|
|
return [self._update_handlers.pop(index)]
|
|
|
|
except IndexError:
|
|
|
|
pass
|
|
|
|
return []
|
|
|
|
|
|
|
|
if priority is not None:
|
|
|
|
# can binary-search (using a dummy EventHandler)
|
|
|
|
index = bisect.bisect_right(self._update_handlers, EventHandler(None, None, priority, None))
|
|
|
|
try:
|
|
|
|
while self._update_handlers[index].priority == priority:
|
|
|
|
index += 1
|
|
|
|
except IndexError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
removed = []
|
|
|
|
while index > 0 and self._update_handlers[index - 1].priority == priority:
|
|
|
|
index -= 1
|
|
|
|
if callback is not None and self._update_handlers[index].callback != callback:
|
|
|
|
continue
|
|
|
|
if event is not None and self._update_handlers[index].event != event:
|
|
|
|
continue
|
|
|
|
removed.append(self._update_handlers.pop(index))
|
|
|
|
|
|
|
|
return removed
|
|
|
|
|
|
|
|
# slow-path, remove all matching
|
|
|
|
removed = []
|
|
|
|
for index, handler in reversed(enumerate(self._update_handlers)):
|
|
|
|
if callback is not None and handler.callback != callback:
|
|
|
|
continue
|
|
|
|
if event is not None and handler.event != event:
|
|
|
|
continue
|
|
|
|
removed.append(self._update_handlers.pop(index))
|
|
|
|
|
|
|
|
return removed
|
2021-09-11 14:33:27 +03:00
|
|
|
|
|
|
|
def list_event_handlers(self: 'TelegramClient')\
|
|
|
|
-> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]':
|
2022-01-28 16:12:32 +03:00
|
|
|
return self._update_handlers[:]
|
2019-04-23 21:15:27 +03:00
|
|
|
|
2021-09-11 14:33:27 +03:00
|
|
|
async def catch_up(self: 'TelegramClient'):
|
2022-01-23 21:53:48 +03:00
|
|
|
# The update loop is probably blocked on either timeout or an update to arrive.
|
|
|
|
# Unblock the loop by pushing a dummy update which will always trigger a gap.
|
|
|
|
# This, in return, causes the update loop to catch up.
|
|
|
|
await self._updates_queue.put(_tl.UpdatesTooLong())
|
2021-09-11 14:33:27 +03:00
|
|
|
|
|
|
|
async def _update_loop(self: 'TelegramClient'):
|
2022-01-22 15:27:00 +03:00
|
|
|
try:
|
|
|
|
updates_to_dispatch = deque()
|
2022-02-16 13:24:20 +03:00
|
|
|
while self.is_connected:
|
2022-01-22 15:27:00 +03:00
|
|
|
if updates_to_dispatch:
|
2022-02-15 13:57:55 +03:00
|
|
|
await _dispatch(self, *updates_to_dispatch.popleft())
|
2022-01-22 15:27:00 +03:00
|
|
|
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._entity_cache)
|
2022-02-15 13:57:55 +03:00
|
|
|
updates_to_dispatch.extend(_preprocess_updates(self, updates, users, chats))
|
2022-01-22 15:27:00 +03:00
|
|
|
continue
|
|
|
|
|
|
|
|
get_diff = self._message_box.get_channel_difference(self._entity_cache)
|
|
|
|
if get_diff:
|
|
|
|
self._log[__name__].info('Getting difference for channel updates')
|
|
|
|
diff = await self(get_diff)
|
2022-01-23 14:43:41 +03:00
|
|
|
updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._entity_cache)
|
2022-02-15 13:57:55 +03:00
|
|
|
updates_to_dispatch.extend(_preprocess_updates(self, updates, users, chats))
|
2022-01-22 15:27:00 +03:00
|
|
|
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 = []
|
|
|
|
users, chats = self._message_box.process_updates(updates, self._entity_cache, processed)
|
2022-02-15 13:57:55 +03:00
|
|
|
updates_to_dispatch.extend(_preprocess_updates(self, processed, users, chats))
|
2022-01-22 15:27:00 +03:00
|
|
|
except Exception:
|
|
|
|
self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)')
|
2022-01-28 22:21:15 +03:00
|
|
|
|
|
|
|
|
2022-02-15 13:57:55 +03:00
|
|
|
def _preprocess_updates(self, updates, users, chats):
|
|
|
|
self._entity_cache.extend(users, chats)
|
|
|
|
entities = Entities(self, users, chats)
|
|
|
|
return ((u, entities) for u in updates)
|
2022-01-28 22:21:15 +03:00
|
|
|
|
|
|
|
|
2022-02-15 13:57:55 +03:00
|
|
|
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 = {}
|
|
|
|
for handler in self._update_handlers:
|
|
|
|
event, entities = event_cache.get(handler._event)
|
|
|
|
if not event:
|
|
|
|
# build can fail if we're missing an access hash; we want this to crash
|
|
|
|
event_cache[handler._event] = event = handler._event._build(self, update, entities)
|
|
|
|
|
|
|
|
while True:
|
|
|
|
# filters can be modified at any time, and there can be any amount of them which are not yet resolved
|
2022-01-28 22:21:15 +03:00
|
|
|
try:
|
2022-02-15 13:57:55 +03:00
|
|
|
if handler._filter(event):
|
|
|
|
try:
|
|
|
|
await handler._callback(event)
|
|
|
|
except StopPropagation:
|
|
|
|
return
|
|
|
|
except Exception:
|
|
|
|
name = getattr(handler._callback, '__name__', repr(handler._callback))
|
|
|
|
self._log[__name__].exception('Unhandled exception on %s (this is likely a bug in your code)', name)
|
|
|
|
except NotResolved as nr:
|
|
|
|
try:
|
|
|
|
await nr.unresolved.resolve()
|
|
|
|
continue
|
|
|
|
except Exception as e:
|
|
|
|
# we cannot really do much about this; it might be a temporary network issue
|
|
|
|
warnings.warn(f'failed to resolve filter, handler will be skipped: {e}: {nr.unresolved!r}')
|
2022-01-28 23:06:31 +03:00
|
|
|
except Exception as e:
|
2022-02-15 13:57:55 +03:00
|
|
|
# invalid filter (e.g. types when types were not used as input)
|
|
|
|
warnings.warn(f'invalid filter applied, handler will be skipped: {e}: {e.filter!r}')
|
2022-01-28 22:21:15 +03:00
|
|
|
|
2022-02-15 13:57:55 +03:00
|
|
|
# we only want to continue on unresolved filter (to check if there are more unresolved)
|
|
|
|
break
|
|
|
|
finally:
|
|
|
|
self._dispatching_update_handlers = False
|