From 1911fdba67130596763b762f7557374dd35103e9 Mon Sep 17 00:00:00 2001 From: hikariatama Date: Mon, 10 Oct 2022 13:45:33 +0000 Subject: [PATCH] 1.5.1 - Fix `--no-web` arg - Fix `tglog_level` config option of module `Tester` - Fix duplicated monkey on login page - Fix shit modules with uppercase commands - Add physical `Enter` button to login page on mobile devices - Add `--proxy-pass` arg - Add `utils.invite_inline_bot` method - Add `utils.iter_attrs` method - Add `@loader.raw_handler` decorator - Add `invite_bot` parameter to `utils.asset_channel` - Add support for `String` validator's `min_len` and `max_len` parameters --- CHANGELOG.md | 5 +- hikka/dispatcher.py | 11 +++ hikka/loader.py | 182 +++++++++++++++++++++++++++++++++++--------- hikka/main.py | 5 ++ hikka/types.py | 2 +- hikka/utils.py | 9 +++ 6 files changed, 175 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d89ad5..3973c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,13 @@ - Fix `--no-web` arg - Fix `tglog_level` config option of module `Tester` - Fix duplicated monkey on login page +- Fix shit modules with uppercase commands - Add physical `Enter` button to login page on mobile devices - Add `--proxy-pass` arg - Add `utils.invite_inline_bot` method -- Add `invite_bot` method to `utils.asset_channel` +- Add `utils.iter_attrs` method +- Add `@loader.raw_handler` decorator +- Add `invite_bot` parameter to `utils.asset_channel` - Add support for `String` validator's `min_len` and `max_len` parameters ## 🌑 Hikka 1.5.0 diff --git a/hikka/dispatcher.py b/hikka/dispatcher.py index ecc49f1..05ee078 100755 --- a/hikka/dispatcher.py +++ b/hikka/dispatcher.py @@ -108,6 +108,8 @@ class CommandDispatcher: else str(self._client.hikka_me.id) ) + self.raw_handlers = [] + async def _handle_ratelimit(self, message: Message, func: callable) -> bool: if await self.security.check( message, @@ -370,6 +372,15 @@ class CommandDispatcher: return message, prefix, txt, func + async def handle_raw(self, event: events.Raw): + """Handle raw events.""" + for handler in self.raw_handlers: + if isinstance(event, tuple(handler.updates)): + try: + await handler(event) + except Exception as e: + logger.exception("Error in raw handler %s: %s", handler.id, e) + async def handle_command( self, event: typing.Union[events.NewMessage, events.MessageDeleted], diff --git a/hikka/loader.py b/hikka/loader.py index 946636a..a92b605 100644 --- a/hikka/loader.py +++ b/hikka/loader.py @@ -15,6 +15,7 @@ import logging import os import re import sys +from uuid import uuid4 import requests import copy @@ -23,6 +24,8 @@ import importlib.util import importlib.machinery from functools import partial, wraps +from telethon.utils import is_list_like +from telethon.tl.tlobject import TLObject from telethon.tl.types import Message, InputPeerNotifySettings, Channel from telethon.tl.functions.account import UpdateNotifySettingsRequest from telethon.hints import EntityLike @@ -500,6 +503,26 @@ def callback_handler(*args, **kwargs): return _mark_method("is_callback_handler", *args, **kwargs) +def raw_handler(updates: typing.Union[TLObject, typing.List[TLObject]]): + """ + Decorator that marks function as raw telethon events handler + Use it to prevent zombie-event-handlers, left by unloaded modules + :param updates: Update(-s) to handle + ⚠️ Do not try to simulate behavior of this decorator by yourself! + ⚠️ This feature won't work, if you dynamically declare method with decorator! + """ + if not is_list_like(updates): + updates = [updates] + + def inner(func: callable): + func.is_raw_handler = True + func.updates = updates + func.id = uuid4().hex + return func + + return inner + + class Modules: """Stores all registered modules""" @@ -698,6 +721,18 @@ class Modules: for alias, cmd in aliases.items(): self.add_alias(alias, cmd) + def register_raw_handlers(self, instance: Module): + """Register event handlers for a module""" + for name, handler in utils.iter_attrs(instance): + if getattr(handler, "is_raw_handler", False): + self.client.dispatcher.raw_handlers.append(handler) + logger.debug( + "Registered raw handler %s for %s. ID: %s", + name, + instance.__class__.__name__, + handler.id, + ) + def register_commands(self, instance: Module): """Register commands from instance""" with contextlib.suppress(AttributeError): @@ -708,11 +743,6 @@ class Modules: map(lambda x: x.lower(), list(instance.hikka_commands)) ) - for name, cmd in self.commands.copy().items(): - if cmd.__self__.__class__.__name__ == instance.__class__.__name__: - logger.debug("Removing command %s for update", name) - del self.commands[name] - for _command, cmd in instance.hikka_commands.items(): # Restrict overwriting core modules' commands if ( @@ -730,23 +760,31 @@ class Modules: if cmd in instance.hikka_commands: self.add_alias(alias, cmd) + self.register_inline_stuff(instance) + + def register_inline_stuff(self, instance: Module): for name, func in instance.hikka_inline_handlers.copy().items(): if name.lower() in self.inline_handlers: if ( hasattr(func, "__self__") and hasattr(self.inline_handlers[name], "__self__") - and func.__self__.__class__.__name__ - != self.inline_handlers[name].__self__.__class__.__name__ + and ( + func.__self__.__class__.__name__ + != self.inline_handlers[name].__self__.__class__.__name__ + ) ): - logger.debug("Duplicate inline_handler %s", name) + logger.debug( + "Duplicate inline_handler %s of %s", + name, + instance.__class__.__name__, + ) logger.debug( - "Replacing inline_handler for %s", self.inline_handlers[name] + "Replacing inline_handler %s for %s", + self.inline_handlers[name], + instance.__class__.__name__, ) - if not func.__doc__: - logger.debug("Missing docs for %s", name) - self.inline_handlers.update({name.lower(): func}) for name, func in instance.hikka_callback_handlers.copy().items(): @@ -756,11 +794,46 @@ class Modules: and func.__self__.__class__.__name__ != self.callback_handlers[name].__self__.__class__.__name__ ): - logger.debug("Duplicate callback_handler %s", name) + logger.debug( + "Duplicate callback_handler %s of %s", + name, + instance.__class__.__name__, + ) self.callback_handlers.update({name.lower(): func}) - def register_watcher(self, instance: Module): + def unregister_inline_stuff(self, instance: Module, purpose: str): + for name, func in instance.hikka_inline_handlers.copy().items(): + if name.lower() in self.inline_handlers and ( + hasattr(func, "__self__") + and hasattr(self.inline_handlers[name], "__self__") + and func.__self__.__class__.__name__ + == self.inline_handlers[name].__self__.__class__.__name__ + ): + del self.inline_handlers[name.lower()] + logger.debug( + "Unregistered inline_handler %s of %s for %s", + name, + instance.__class__.__name__, + purpose, + ) + + for name, func in instance.hikka_callback_handlers.copy().items(): + if name.lower() in self.callback_handlers and ( + hasattr(func, "__self__") + and hasattr(self.callback_handlers[name], "__self__") + and func.__self__.__class__.__name__ + == self.callback_handlers[name].__self__.__class__.__name__ + ): + del self.callback_handlers[name.lower()] + logger.debug( + "Unregistered callback_handler %s of %s for %s", + name, + instance.__class__.__name__, + purpose, + ) + + def register_watchers(self, instance: Module): """Register watcher from instance""" with contextlib.suppress(AttributeError): _hikka_client_id_logging_tag = copy.copy(self.client.tg_id) @@ -1416,8 +1489,12 @@ class Modules: self.modules.remove(mod) raise + self.unregister_commands(mod, "update") + self.unregister_raw_handlers(mod, "update") + self.register_commands(mod) - self.register_watcher(mod) + self.register_watchers(mod) + self.register_raw_handlers(mod) def get_classname(self, name: str) -> str: return next( @@ -1461,32 +1538,63 @@ class Modules: await module.on_unload() - for method in dir(module): - if isinstance(getattr(module, method), InfiniteLoop): - getattr(module, method).stop() - logger.debug( - "Stopped loop in module %s, method %s", module, method - ) - - for name, cmd in self.commands.copy().items(): - if cmd.__self__.__class__.__name__ == module.__class__.__name__: - logger.debug("Removing command %s for unload", name) - del self.commands[name] - for alias, _command in self.aliases.copy().items(): - if _command == name: - del self.aliases[alias] - - for _watcher in self.watchers.copy(): - if ( - _watcher.__self__.__class__.__name__ - == module.__class__.__name__ - ): - logger.debug("Removing watcher %s for unload", _watcher) - self.watchers.remove(_watcher) + self.unregister_raw_handlers(module, "unload") + self.unregister_loops(module, "unload") + self.unregister_commands(module, "unload") + self.unregister_watchers(module, "unload") + self.unregister_inline_stuff(module, "unload") logger.debug("Worked: %s", worked) return worked + def unregister_loops(self, instance: Module, purpose: str): + for name, method in utils.iter_attrs(instance): + if isinstance(method, InfiniteLoop): + logger.debug( + "Stopping loop for %s in module %s, method %s", + purpose, + instance.__class__.__name__, + name, + ) + method.stop() + + def unregister_commands(self, instance: Module, purpose: str): + for name, cmd in self.commands.copy().items(): + if cmd.__self__.__class__.__name__ == instance.__class__.__name__: + logger.debug( + "Removing command %s of module %s for %s", + name, + instance.__class__.__name__, + purpose, + ) + del self.commands[name] + for alias, _command in self.aliases.copy().items(): + if _command == name: + del self.aliases[alias] + + def unregister_watchers(self, instance: Module, purpose: str): + for _watcher in self.watchers.copy(): + if _watcher.__self__.__class__.__name__ == instance.__class__.__name__: + logger.debug( + "Removing watcher %s of module %s for %s", + _watcher, + instance.__class__.__name__, + purpose, + ) + self.watchers.remove(_watcher) + + def unregister_raw_handlers(self, instance: Module, purpose: str): + """Unregister event handlers for a module""" + for handler in self.client.dispatcher.raw_handlers: + if handler.__self__.__class__.__name__ == instance.__class__.__name__: + self.client.dispatcher.raw_handlers.remove(handler) + logger.debug( + "Unregistered raw handler of module %s for %s. ID: %s", + instance.__class__.__name__, + purpose, + handler.id, + ) + def add_alias(self, alias: str, cmd: str) -> bool: """Make an alias""" if cmd not in self.commands: diff --git a/hikka/main.py b/hikka/main.py index 90a150d..77f61dd 100755 --- a/hikka/main.py +++ b/hikka/main.py @@ -620,6 +620,11 @@ class Hikka: events.MessageEdited(), ) + client.add_event_handler( + dispatcher.handle_raw, + events.Raw(), + ) + async def amain(self, first: bool, client: CustomTelegramClient): """Entrypoint for async init, run once for each user""" client.parse_mode = "HTML" diff --git a/hikka/types.py b/hikka/types.py index 65432d6..994e381 100644 --- a/hikka/types.py +++ b/hikka/types.py @@ -370,7 +370,7 @@ def _get_members( method_name.rsplit(ending, maxsplit=1)[0] if (method_name == ending if strict else method_name.endswith(ending)) else method_name - ): getattr(mod, method_name) + ).lower(): getattr(mod, method_name) for method_name in dir(mod) if callable(getattr(mod, method_name)) and ( diff --git a/hikka/utils.py b/hikka/utils.py index a0fe382..da45a41 100755 --- a/hikka/utils.py +++ b/hikka/utils.py @@ -1394,6 +1394,15 @@ def validate_html(html: str) -> str: return telethon.extensions.html.unparse(escape_html(text), entities) +def iter_attrs(obj: typing.Any, /) -> typing.Iterator[typing.Tuple[str, typing.Any]]: + """ + Iterates over attributes of object + :param obj: Object to iterate over + :return: Iterator of attributes and their values + """ + return ((attr, getattr(obj, attr)) for attr in dir(obj)) + + init_ts = time.perf_counter()