From c40b8e016e5a045dbdb5ec1834401f6e61ef1509 Mon Sep 17 00:00:00 2001 From: "hikari.ftg" Date: Thu, 31 Mar 2022 18:24:00 +0000 Subject: [PATCH] 1.0.13: Include command prefix in inline `info`. Fix `--no-web`. Suggest to save modules to filesystem --- hikka/compat/geek.py | 17 ++-- hikka/loader.py | 71 +++++++++-------- hikka/main.py | 28 +++---- hikka/modules/hikka_info.py | 1 + hikka/modules/hikka_settings.py | 40 ++++++++++ hikka/modules/loader.py | 134 ++++++++++++++++++++++++++++---- hikka/utils.py | 9 ++- hikka/version.py | 2 +- 8 files changed, 225 insertions(+), 77 deletions(-) diff --git a/hikka/compat/geek.py b/hikka/compat/geek.py index 08af21c..1f0bd26 100644 --- a/hikka/compat/geek.py +++ b/hikka/compat/geek.py @@ -18,15 +18,20 @@ def compat(code: str) -> str: r"^( *)from \.\.inline import (.+)$", r"\1from ..inline.types import \2", re.sub( - r"^( *)from \.\.inline import rand, ?(.+)$", - r"\1from ..inline.types import \2\n\1from ..utils import rand", + r"^( *)from \.\.inline import rand[^,]*$", + "\1from ..utils import rand", re.sub( - r"^( *)from \.\.inline import (.+), ?rand[^,]+$", + r"^( *)from \.\.inline import rand, ?(.+)$", r"\1from ..inline.types import \2\n\1from ..utils import rand", re.sub( - r"^( *)from \.\.inline import (.+), ?rand, ?(.+)$", - r"\1from ..inline.types import \2, \3\n\1from ..utils import rand", - line.replace("GeekInlineQuery", "InlineQuery"), + r"^( *)from \.\.inline import (.+), ?rand[^,]*$", + r"\1from ..inline.types import \2\n\1from ..utils import rand", + re.sub( + r"^( *)from \.\.inline import (.+), ?rand, ?(.+)$", + r"\1from ..inline.types import \2, \3\n\1from ..utils import rand", + line.replace("GeekInlineQuery", "InlineQuery"), + flags=re.M, + ), flags=re.M, ), flags=re.M, diff --git a/hikka/loader.py b/hikka/loader.py index ddf26b9..1476a11 100755 --- a/hikka/loader.py +++ b/hikka/loader.py @@ -34,22 +34,12 @@ import inspect import logging import os import sys -import json from . import utils, security from .translations.dynamic import Strings from .inline.core import InlineManager from .types import Module, LoadError, ModuleConfig # noqa: F401 - - -def use_fs_for_modules(): - try: - with open("config.json", "r") as f: - config = json.loads(f.read()) - except Exception: - return False - - return config.get("use_fs_for_modules", False) +from importlib.machinery import ModuleSpec def test(*args, **kwargs): @@ -179,33 +169,43 @@ class Modules: and x[-3:] == ".py" and x[0] != "_" and ("OKTETO" in os.environ or x != "okteto.py") - and (not db.get("hikka", "disable_quickstart", False) or x != "quickstart.py") + and ( + not db.get("hikka", "disable_quickstart", False) + or x != "quickstart.py" + ) ), os.listdir(os.path.join(utils.get_base_dir(), MODULES_NAME)), ) ] - if use_fs_for_modules(): - mods += [ - os.path.join(LOADED_MODULES_DIR, mod) - for mod in filter( - lambda x: (len(x) > 3 and x[-3:] == ".py" and x[0] != "_"), - os.listdir(LOADED_MODULES_DIR), - ) - ] + mods += [ + os.path.join(LOADED_MODULES_DIR, mod) + for mod in filter( + lambda x: (len(x) > 3 and x[-3:] == ".py" and x[0] != "_"), + os.listdir(LOADED_MODULES_DIR), + ) + ] logging.debug(mods) for mod in mods: try: - module_name = f"{__package__}.{MODULES_NAME}.{os.path.basename(mod)[:-3]}" + module_name = ( + f"{__package__}.{MODULES_NAME}.{os.path.basename(mod)[:-3]}" + ) logging.debug(module_name) spec = importlib.util.spec_from_file_location(module_name, mod) self.register_module(spec, module_name) except BaseException as e: logging.exception(f"Failed to load module %s due to {e}:", mod) - def register_module(self, spec, module_name, origin=""): + def register_module( + self, + spec: ModuleSpec, + module_name: str, + origin: str = "", + save_fs: bool = False, + ) -> Module: """Register single module from importlib spec""" from .compat import uniborg @@ -232,7 +232,7 @@ class Modules: cls_name = ret.__class__.__name__ - if use_fs_for_modules(): + if save_fs: path = os.path.join(LOADED_MODULES_DIR, f"{cls_name}.py") if not os.path.isfile(path) and origin == "": @@ -243,7 +243,7 @@ class Modules: return ret - def register_commands(self, instance): + def register_commands(self, instance: Module) -> None: """Register commands from instance""" for command in instance.commands.copy(): # Verify that command does not already exist, or, @@ -264,7 +264,7 @@ class Modules: self.commands.update({command.lower(): instance.commands[command]}) - def register_watcher(self, instance): + def register_watcher(self, instance: Module) -> None: """Register watcher from instance""" try: if instance.watcher: @@ -280,7 +280,7 @@ class Modules: except AttributeError: pass - def complete_registration(self, instance): + def complete_registration(self, instance: Module) -> None: """Complete registration of instance""" instance.allmodules = self instance.hikka = True @@ -299,7 +299,7 @@ class Modules: self.modules += [instance] - def dispatch(self, command): + def dispatch(self, command: str) -> tuple: """Dispatch command to appropriate module""" change = str.maketrans(ru_keys + en_keys, en_keys + ru_keys) try: @@ -319,13 +319,13 @@ class Modules: except KeyError: return command, None - def send_config(self, db, babel, skip_hook=False): + def send_config(self, db, babel, skip_hook: bool = False) -> None: """Configure modules""" for mod in self.modules: self.send_config_one(mod, db, babel, skip_hook) @staticmethod - def send_config_one(mod, db, babel=None, skip_hook=False): + def send_config_one(mod, db, babel=None, skip_hook: bool = False) -> None: """Send config to single instance""" if hasattr(mod, "config"): modcfg = db.get(mod.__module__, "__config__", {}) @@ -413,7 +413,7 @@ class Modules: if not self._initial_registration and self.added_modules: await self.added_modules(self) - def get_classname(self, name): + def get_classname(self, name: str) -> str: return next( ( module.__class__.__module__ @@ -423,7 +423,7 @@ class Modules: name, ) - def unload_module(self, classname): + def unload_module(self, classname: str) -> bool: """Remove module and all stuff from it""" worked = [] to_remove = [] @@ -433,12 +433,11 @@ class Modules: worked += [module.__module__] name = module.__class__.__name__ - if use_fs_for_modules(): - path = os.path.join(LOADED_MODULES_DIR, f"{name}.py") + path = os.path.join(LOADED_MODULES_DIR, f"{name}.py") - if os.path.isfile(path): - os.remove(path) - logging.debug(f"Removed {name} file") + if os.path.isfile(path): + os.remove(path) + logging.debug(f"Removed {name} file") logging.debug("Removing module for unload %r", module) self.modules.remove(module) diff --git a/hikka/main.py b/hikka/main.py index 759613a..e2e871a 100755 --- a/hikka/main.py +++ b/hikka/main.py @@ -105,9 +105,6 @@ def save_config_key(key, value): return True -save_config_key("use_fs_for_modules", get_config_key("use_fs_for_modules")) - - def gen_port(): if "OKTETO" in os.environ: return 8080 @@ -327,19 +324,16 @@ class Hikka: def _init_web(self) -> None: """Initialize web""" - if web_available: - self.web = ( - core.Web( - data_root=self.arguments.data_root, - api_token=self.api_token, - proxy=self.proxy, - connection=self.conn, - ) - if getattr(self.arguments, "web", True) - else None - ) - else: + if web_available and not getattr(self.arguments, "disable_web", False): self.web = None + return + + self.web = core.Web( + data_root=self.arguments.data_root, + api_token=self.api_token, + proxy=self.proxy, + connection=self.conn, + ) def _get_token(self) -> None: while self.api_token is None: @@ -430,10 +424,6 @@ class Hikka: client.start( phone=raise_auth if self.web - and ( - not hasattr(self.arguments, "web") - or self.arguments.web is not False - ) else lambda: input("Phone: ") ) client.phone = phone diff --git a/hikka/modules/hikka_info.py b/hikka/modules/hikka_info.py index 2862d0a..cf51f9a 100755 --- a/hikka/modules/hikka_info.py +++ b/hikka/modules/hikka_info.py @@ -81,6 +81,7 @@ class HikkaInfoMod(loader.Module): f'๐Ÿคด Owner: {utils.escape_html(get_display_name(self._me))}\n\n' f"๐Ÿ”ฎ Version: {'.'.join(list(map(str, list(main.__version__))))}\n" f"๐Ÿงฑ Build: {ver[:8] or 'Unknown'}\n" + f"๐Ÿ“ผ Command prefix: ยซ{utils.escape_html((self._db.get(main.__name__, 'command_prefix', False) or '.')[0] )}ยป\n" f"{upd}\n" f"{utils.get_named_platform()}\n" ), diff --git a/hikka/modules/hikka_settings.py b/hikka/modules/hikka_settings.py index eea10c5..24b987f 100755 --- a/hikka/modules/hikka_settings.py +++ b/hikka/modules/hikka_settings.py @@ -338,6 +338,46 @@ class HikkaSettingsMod(loader.Module): } ), ], + [ + ( + { + "text": "โœ… Suggest FS for modules", + "callback": self.inline__setting, + "args": ( + "disable_modules_fs", + True, + ), + } + if not self._db.get(main.__name__, "disable_modules_fs", False) + else { + "text": "๐Ÿšซ Suggest FS for modules", + "callback": self.inline__setting, + "args": ( + "disable_modules_fs", + False, + ), + } + ), + ( + { + "text": "โœ… Always use FS for modules", + "callback": self.inline__setting, + "args": ( + "permanent_modules_fs", + False, + ), + } + if self._db.get(main.__name__, "permanent_modules_fs", False) + else { + "text": "๐Ÿšซ Always use FS for modules", + "callback": self.inline__setting, + "args": ( + "permanent_modules_fs", + True, + ), + } + ), + ], [ { "text": "๐Ÿ”„ Restart", diff --git a/hikka/modules/loader.py b/hikka/modules/loader.py index 3c0de99..7c7278a 100755 --- a/hikka/modules/loader.py +++ b/hikka/modules/loader.py @@ -26,6 +26,8 @@ # ๐Ÿ”’ Licensed under the GNU GPLv3 # ๐ŸŒ https://www.gnu.org/licenses/agpl-3.0.html +# scope: inline + import asyncio import importlib import inspect @@ -40,7 +42,8 @@ from importlib.machinery import ModuleSpec import telethon from telethon.tl.types import Message import functools -from typing import Any +from typing import Any, Union +from aiogram.types import CallbackQuery import requests @@ -169,6 +172,12 @@ class LoaderMod(loader.Module): "version_incompatible": "๐Ÿšซ This module requires Hikka {}+\nPlease, update with .update", "ffmpeg_required": "๐Ÿšซ This module requires FFMPEG, which is not installed", "developer": "\n\n๐Ÿง‘โ€๐Ÿ’ป Developer: {}", + "module_fs": "๐Ÿ’ฟ Would you like to save this module to filesystem, so it won't get unloaded after restart?", + "save": "๐Ÿ’ฟ Save", + "no_save": "๐Ÿšซ Don't save", + "save_for_all": "๐Ÿ’ฝ Always save to fs", + "never_save": "๐Ÿšซ Never save to fs", + "will_save_fs": "๐Ÿ’ฝ Now all modules, loaded with .loadmod will be saved to filesystem", } def __init__(self): @@ -268,6 +277,30 @@ class LoaderMod(loader.Module): except Exception: logger.exception(f"Failed to load {module_name}") + async def _inline__load( + self, + call: CallbackQuery, + doc: str, + path_: Union[str, None], + mode: str, + ) -> None: + save = False + if mode == "all_yes": + self._db.set(main.__name__, "permanent_modules_fs", True) + self._db.set(main.__name__, "disable_modules_fs", False) + await call.answer(self.strings("will_save_fs")) + save = True + elif mode == "all_no": + self._db.set(main.__name__, "disable_modules_fs", True) + self._db.set(main.__name__, "permanent_modules_fs", False) + elif mode == "once": + save = True + + if path_ is not None: + await self.load_module(doc, call, origin=path_, save_fs=save) + else: + await self.load_module(doc, call, save_fs=save) + @loader.owner async def loadmodcmd(self, message: Message) -> None: """Loads the module file""" @@ -291,20 +324,79 @@ class LoaderMod(loader.Module): logger.debug("Loading external module...") + if message.file: + await message.edit("") + message = await message.respond("๐Ÿ‘ฉโ€๐ŸŽค") + try: doc = doc.decode("utf-8") except UnicodeDecodeError: await utils.answer(message, self.strings("bad_unicode", message)) return + if ( + not self._db.get(main.__name__, "disable_modules_fs", False) + and not self._db.get(main.__name__, "permanent_modules_fs", False) + ): + await self.inline.form( + self.strings("module_fs"), + message=message, + reply_markup=[ + [ + { + "text": self.strings("save"), + "callback": self._inline__load, + "args": (doc, path_, "once"), + }, + { + "text": self.strings("no_save"), + "callback": self._inline__load, + "args": (doc, path_, "no"), + }, + ], + [ + { + "text": self.strings("save_for_all"), + "callback": self._inline__load, + "args": (doc, path_, "all_yes"), + } + ], + [ + { + "text": self.strings("never_save"), + "callback": self._inline__load, + "args": (doc, path_, "all_no"), + } + ], + ], + ) + return + if path_ is not None: - await self.load_module(doc, message, origin=path_) + await self.load_module( + doc, + message, + origin=path_, + save_fs=self._db.get(main.__name__, "permanent_modules_fs", False) + and not self._db.get(main.__name__, "disable_modules_fs", False), + ) else: - await self.load_module(doc, message) + await self.load_module( + doc, + message, + save_fs=self._db.get(main.__name__, "permanent_modules_fs", False) + and not self._db.get(main.__name__, "disable_modules_fs", False), + ) async def load_module( - self, doc, message, name=None, origin="", did_requirements=False - ): + self, + doc: str, + message: Message, + name: Union[str, None] = None, + origin: str = "", + did_requirements: bool = False, + save_fs: bool = False, + ) -> None: if any( line.replace(" ", "") == "#scope:ffmpeg" for line in doc.splitlines() ) and os.system("ffmpeg -version"): @@ -322,12 +414,14 @@ class LoaderMod(loader.Module): if re.search(r"# ?scope: ?hikka_min", doc): ver = re.search( - r"# ?scope: ?hikka_min ([0-9]+\.[0-9]+\.[0-9]+)", doc + r"# ?scope: ?hikka_min ([0-9]+\.[0-9]+\.[0-9]+)", + doc, ).group(1) ver_ = tuple(map(int, ver.split("."))) if main.__version__ < ver_: await utils.answer( - message, self.strings("version_incompatible").format(ver) + message, + self.strings("version_incompatible").format(ver), ) return @@ -347,7 +441,7 @@ class LoaderMod(loader.Module): try: try: spec = ModuleSpec(module_name, StringLoader(doc, origin), origin=origin) - instance = self.allmodules.register_module(spec, module_name, origin) + instance = self.allmodules.register_module(spec, module_name, origin, save_fs=save_fs) except ImportError as e: logger.info( "Module loading failed, attemping dependency installation", @@ -359,7 +453,8 @@ class LoaderMod(loader.Module): filter( lambda x: x and x[0] not in ("-", "_", "."), map( - str.strip, VALID_PIP_PACKAGES.search(doc)[1].split(" ") + str.strip, + VALID_PIP_PACKAGES.search(doc)[1].split(" "), ), ) ) @@ -374,14 +469,16 @@ class LoaderMod(loader.Module): if did_requirements: if message is not None: await utils.answer( - message, self.strings("requirements_restart", message) + message, + self.strings("requirements_restart", message), ) return True # save to database despite failure, so it will work after restart if message is not None: await utils.answer( - message, self.strings("requirements_installing", message) + message, + self.strings("requirements_installing", message), ) pip = await asyncio.create_subprocess_exec( @@ -402,7 +499,8 @@ class LoaderMod(loader.Module): if rc != 0: if message is not None: await utils.answer( - message, self.strings("requirements_failed", message) + message, + self.strings("requirements_failed", message), ) return False @@ -410,7 +508,12 @@ class LoaderMod(loader.Module): importlib.invalidate_caches() return await self.load_module( - doc, message, name, origin, True + doc, + message, + name, + origin, + True, + save_fs, ) # Try again except loader.LoadError as e: if message: @@ -447,7 +550,10 @@ class LoaderMod(loader.Module): try: self.allmodules.send_config_one(instance, self._db, self.babel) await self.allmodules.send_ready_one( - instance, self._client, self._db, self.allclients + instance, + self._client, + self._db, + self.allclients, ) except loader.LoadError as e: if message: diff --git a/hikka/utils.py b/hikka/utils.py index 9a94790..cd4354b 100755 --- a/hikka/utils.py +++ b/hikka/utils.py @@ -48,6 +48,10 @@ from telethon.tl.types import ( Chat, ) +from aiogram.types import CallbackQuery + +from .inline.types import InlineCall + import random from typing import Tuple, Union, List, Any @@ -228,8 +232,11 @@ def relocate_entities( return entities -async def answer(message: Message, response: str, **kwargs) -> list: +async def answer(message: Union[Message, CallbackQuery], response: str, **kwargs) -> list: """Use this to give the response to a command""" + if isinstance(message, (CallbackQuery, InlineCall)): + return await message.edit(response) + if isinstance(message, list): delete_job = asyncio.ensure_future( message[0].client.delete_messages(message[0].input_chat, message[1:]) diff --git a/hikka/version.py b/hikka/version.py index 27c468f..bad9ab7 100644 --- a/hikka/version.py +++ b/hikka/version.py @@ -1 +1 @@ -__version__ = (1, 0, 12) +__version__ = (1, 0, 13)