# █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ # █▀█ █ █ █ █▀█ █▀▄ █ # © Copyright 2022 # https://t.me/hikariatama # # 🔒 Licensed under the GNU AGPLv3 # 🌐 https://www.gnu.org/licenses/agpl-3.0.html # scope: inline import ast import functools import logging from math import ceil from typing import Optional, Union, Any from telethon.tl.types import Message from .. import loader, utils, translations from ..inline.types import InlineCall logger = logging.getLogger(__name__) # Everywhere in this module, we use the following naming convention: # `obj_type` of non-core module = False # `obj_type` of core module = True # `obj_type` of library = "library" @loader.tds class HikkaConfigMod(loader.Module): """Interactive configurator for Hikka Userbot""" strings = { "name": "HikkaConfig", "choose_core": "🎚 Choose a category", "configure": "🎚 Choose a module to configure", "configure_lib": "🪴 Choose a library to configure", "configuring_mod": ( "🎚 Choose config option for mod {}\n\nCurrent" " options:\n\n{}" ), "configuring_lib": ( "🪴 Choose config option for library {}\n\nCurrent" " options:\n\n{}" ), "configuring_option": ( "🎚 Configuring option {} of mod" " {}\nℹ️ {}\n\nDefault: {}\n\nCurrent:" " {}\n\n{}" ), "configuring_option_lib": ( "🪴 Configuring option {} of library" " {}\nℹ️ {}\n\nDefault: {}\n\nCurrent:" " {}\n\n{}" ), "option_saved": ( "🎚 Option {} of module {}" " saved!\nCurrent: {}" ), "option_saved_lib": ( "🪴 Option {} of library {}" " saved!\nCurrent: {}" ), "option_reset": ( "♻️ Option {} of module {} has" " been reset to default\nCurrent: {}" ), "option_reset_lib": ( "♻️ Option {} of library {} has" " been reset to default\nCurrent: {}" ), "args": "🚫 You specified incorrect args", "no_mod": "🚫 Module doesn't exist", "no_option": "🚫 Configuration option doesn't exist", "validation_error": "🚫 You entered incorrect config value. \nError: {}", "try_again": "🔁 Try again", "typehint": "🕵️ Must be a{eng_art} {}", "set": "set", "set_default_btn": "♻️ Reset default", "enter_value_btn": "✍️ Enter value", "enter_value_desc": "✍️ Enter new configuration value for this option", "add_item_desc": "✍️ Enter item to add", "remove_item_desc": "✍️ Enter item to remove", "back_btn": "👈 Back", "close_btn": "🔻 Close", "add_item_btn": "➕ Add item", "remove_item_btn": "➖ Remove item", "show_hidden": "🚸 Show value", "hide_value": "🔒 Hide value", "builtin": "🛰 Built-in", "external": "🛸 External", "libraries": "🪴 Libraries", } strings_ru = { "choose_core": "🎚 Выбери категорию", "configure": "🎚 Выбери модуль для настройки", "configure_lib": "🪴 Выбери библиотеку для настройки", "configuring_mod": ( "🎚 Выбери параметр для модуля {}\n\nТекущие" " настройки:\n\n{}" ), "configuring_lib": ( "🪴 Выбери параметр для библиотеки {}\n\nТекущие" " настройки:\n\n{}" ), "configuring_option": ( "🎚 Управление параметром {} модуля" " {}\nℹ️ {}\n\nСтандартное:" " {}\n\nТекущее: {}\n\n{}" ), "configuring_option_lib": ( "🪴 Управление параметром {} библиотеки" " {}\nℹ️ {}\n\nСтандартное:" " {}\n\nТекущее: {}\n\n{}" ), "option_saved": ( "🎚 Параметр {} модуля {}" " сохранен!\nТекущее: {}" ), "option_saved_lib": ( "🪴 Параметр {} библиотеки {}" " сохранен!\nТекущее: {}" ), "option_reset": ( "♻️ Параметр {} модуля {}" " сброшен до значения по умолчанию\nТекущее: {}" ), "option_reset_lib": ( "♻️ Параметр {} библиотеки {}" " сброшен до значения по умолчанию\nТекущее: {}" ), "_cmd_doc_config": "Настройки модулей", "_cmd_doc_fconfig": ( "<имя модуля> <имя конфига> <значение> - Расшифровывается как ForceConfig -" " Принудительно устанавливает значение в конфиге, если это не удалось" " сделать через inline бота" ), "_cls_doc": "Интерактивный конфигуратор Hikka", "args": "🚫 Ты указал неверные аргументы", "no_mod": "🚫 Модуль не существует", "no_option": "🚫 У модуля нет такого значения конфига", "validation_error": ( "🚫 Введено некорректное значение конфига. \nОшибка: {}" ), "try_again": "🔁 Попробовать еще раз", "typehint": "🕵️ Должно быть {}", "set": "поставить", "set_default_btn": "♻️ Значение по умолчанию", "enter_value_btn": "✍️ Ввести значение", "enter_value_desc": "✍️ Введи новое значение этого параметра", "add_item_desc": "✍️ Введи элемент, который нужно добавить", "remove_item_desc": "✍️ Введи элемент, который нужно удалить", "back_btn": "👈 Назад", "close_btn": "🔻 Закрыть", "add_item_btn": "➕ Добавить элемент", "remove_item_btn": "➖ Удалить элемент", "show_hidden": "🚸 Показать значение", "hide_value": "🔒 Скрыть значение", "builtin": "🛰 Встроенные", "external": "🛸 Внешние", "libraries": "🪴 Библиотеки", } _row_size = 3 _num_rows = 5 @staticmethod def prep_value(value: Any) -> Any: if isinstance(value, str): return f"{utils.escape_html(value.strip())}" if isinstance(value, list) and value: return ( "[\n " + "\n ".join( [f"{utils.escape_html(str(item))}" for item in value] ) + "\n]" ) return f"{utils.escape_html(value)}" def hide_value(self, value: Any) -> str: if isinstance(value, list) and value: return self.prep_value(["*" * len(str(i)) for i in value]) return self.prep_value("*" * len(str(value))) async def inline__set_config( self, call: InlineCall, query: str, mod: str, option: str, inline_message_id: str, obj_type: Union[bool, str] = False, ): try: self.lookup(mod).config[option] = query except loader.validators.ValidationError as e: await call.edit( self.strings("validation_error").format(e.args[0]), reply_markup={ "text": self.strings("try_again"), "callback": self.inline__configure_option, "args": (mod, option), "kwargs": {"obj_type": obj_type}, }, ) return await call.edit( self.strings( "option_saved" if isinstance(obj_type, bool) else "option_saved_lib" ).format( utils.escape_html(option), utils.escape_html(mod), self.prep_value(self.lookup(mod).config[option]) if not self.lookup(mod).config._config[option].validator or self.lookup(mod).config._config[option].validator.internal_id != "Hidden" else self.hide_value(self.lookup(mod).config[option]), ), reply_markup=[ [ { "text": self.strings("back_btn"), "callback": self.inline__configure, "args": (mod,), "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ] ], inline_message_id=inline_message_id, ) async def inline__reset_default( self, call: InlineCall, mod: str, option: str, obj_type: Union[bool, str] = False, ): mod_instance = self.lookup(mod) mod_instance.config[option] = mod_instance.config.getdef(option) await call.edit( self.strings( "option_reset" if isinstance(obj_type, bool) else "option_reset_lib" ).format( utils.escape_html(option), utils.escape_html(mod), self.prep_value(self.lookup(mod).config[option]) if not self.lookup(mod).config._config[option].validator or self.lookup(mod).config._config[option].validator.internal_id != "Hidden" else self.hide_value(self.lookup(mod).config[option]), ), reply_markup=[ [ { "text": self.strings("back_btn"), "callback": self.inline__configure, "args": (mod,), "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ] ], ) async def inline__set_bool( self, call: InlineCall, mod: str, option: str, value: bool, obj_type: Union[bool, str] = False, ): try: self.lookup(mod).config[option] = value except loader.validators.ValidationError as e: await call.edit( self.strings("validation_error").format(e.args[0]), reply_markup={ "text": self.strings("try_again"), "callback": self.inline__configure_option, "args": (mod, option), "kwargs": {"obj_type": obj_type}, }, ) return validator = self.lookup(mod).config._config[option].validator doc = utils.escape_html( next( ( validator.doc[lang] for lang in self._db.get(translations.__name__, "lang", "en").split( " " ) if lang in validator.doc ), validator.doc["en"], ) ) await call.edit( self.strings( "configuring_option" if isinstance(obj_type, bool) else "configuring_option_lib" ).format( utils.escape_html(option), utils.escape_html(mod), utils.escape_html(self.lookup(mod).config.getdoc(option)), self.prep_value(self.lookup(mod).config.getdef(option)), self.prep_value(self.lookup(mod).config[option]) if not validator or validator.internal_id != "Hidden" else self.hide_value(self.lookup(mod).config[option]), self.strings("typehint").format( doc, eng_art="n" if doc.lower().startswith(tuple("euioay")) else "", ) if doc else "", ), reply_markup=self._generate_bool_markup(mod, option, obj_type), ) await call.answer("✅") def _generate_bool_markup( self, mod: str, option: str, obj_type: Union[bool, str] = False, ) -> list: return [ [ *( [ { "text": f"❌ {self.strings('set')} `False`", "callback": self.inline__set_bool, "args": (mod, option, False), "kwargs": {"obj_type": obj_type}, } ] if self.lookup(mod).config[option] else [ { "text": f"✅ {self.strings('set')} `True`", "callback": self.inline__set_bool, "args": (mod, option, True), "kwargs": {"obj_type": obj_type}, } ] ) ], [ *( [ { "text": self.strings("set_default_btn"), "callback": self.inline__reset_default, "args": (mod, option), "kwargs": {"obj_type": obj_type}, } ] if self.lookup(mod).config[option] != self.lookup(mod).config.getdef(option) else [] ) ], [ { "text": self.strings("back_btn"), "callback": self.inline__configure, "args": (mod,), "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ], ] async def inline__add_item( self, call: InlineCall, query: str, mod: str, option: str, inline_message_id: str, obj_type: Union[bool, str] = False, ): try: try: query = ast.literal_eval(query) except Exception: pass if isinstance(query, (set, tuple)): query = list(query) if not isinstance(query, list): query = [query] self.lookup(mod).config[option] = self.lookup(mod).config[option] + query except loader.validators.ValidationError as e: await call.edit( self.strings("validation_error").format(e.args[0]), reply_markup={ "text": self.strings("try_again"), "callback": self.inline__configure_option, "args": (mod, option), "kwargs": {"obj_type": obj_type}, }, ) return await call.edit( self.strings( "option_saved" if isinstance(obj_type, bool) else "option_saved_lib" ).format( utils.escape_html(option), utils.escape_html(mod), self.prep_value(self.lookup(mod).config[option]) if not self.lookup(mod).config._config[option].validator or self.lookup(mod).config._config[option].validator.internal_id != "Hidden" else self.hide_value(self.lookup(mod).config[option]), ), reply_markup=[ [ { "text": self.strings("back_btn"), "callback": self.inline__configure, "args": (mod,), "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ] ], inline_message_id=inline_message_id, ) async def inline__remove_item( self, call: InlineCall, query: str, mod: str, option: str, inline_message_id: str, obj_type: Union[bool, str] = False, ): try: try: query = ast.literal_eval(query) except Exception: pass if isinstance(query, (set, tuple)): query = list(query) if not isinstance(query, list): query = [query] query = list(map(str, query)) old_config_len = len(self.lookup(mod).config[option]) self.lookup(mod).config[option] = [ i for i in self.lookup(mod).config[option] if str(i) not in query ] if old_config_len == len(self.lookup(mod).config[option]): raise loader.validators.ValidationError( f"Nothing from passed value ({self.prep_value(query)}) is not in" " target list" ) except loader.validators.ValidationError as e: await call.edit( self.strings("validation_error").format(e.args[0]), reply_markup={ "text": self.strings("try_again"), "callback": self.inline__configure_option, "args": (mod, option), "kwargs": {"obj_type": obj_type}, }, ) return await call.edit( self.strings( "option_saved" if isinstance(obj_type, bool) else "option_saved_lib" ).format( utils.escape_html(option), utils.escape_html(mod), self.prep_value(self.lookup(mod).config[option]) if not self.lookup(mod).config._config[option].validator or self.lookup(mod).config._config[option].validator.internal_id != "Hidden" else self.hide_value(self.lookup(mod).config[option]), ), reply_markup=[ [ { "text": self.strings("back_btn"), "callback": self.inline__configure, "args": (mod,), "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ] ], inline_message_id=inline_message_id, ) def _generate_series_markup( self, call: InlineCall, mod: str, option: str, obj_type: Union[bool, str] = False, ) -> list: return [ [ { "text": self.strings("enter_value_btn"), "input": self.strings("enter_value_desc"), "handler": self.inline__set_config, "args": (mod, option, call.inline_message_id), "kwargs": {"obj_type": obj_type}, } ], [ *( [ { "text": self.strings("remove_item_btn"), "input": self.strings("remove_item_desc"), "handler": self.inline__remove_item, "args": (mod, option, call.inline_message_id), "kwargs": {"obj_type": obj_type}, }, { "text": self.strings("add_item_btn"), "input": self.strings("add_item_desc"), "handler": self.inline__add_item, "args": (mod, option, call.inline_message_id), "kwargs": {"obj_type": obj_type}, }, ] if self.lookup(mod).config[option] else [] ), ], [ *( [ { "text": self.strings("set_default_btn"), "callback": self.inline__reset_default, "args": (mod, option), "kwargs": {"obj_type": obj_type}, } ] if self.lookup(mod).config[option] != self.lookup(mod).config.getdef(option) else [] ) ], [ { "text": self.strings("back_btn"), "callback": self.inline__configure, "args": (mod,), "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ], ] async def inline__configure_option( self, call: InlineCall, mod: str, config_opt: str, force_hidden: Optional[bool] = False, obj_type: Union[bool, str] = False, ): module = self.lookup(mod) args = [ utils.escape_html(config_opt), utils.escape_html(mod), utils.escape_html(module.config.getdoc(config_opt)), self.prep_value(module.config.getdef(config_opt)), self.prep_value(module.config[config_opt]) if not module.config._config[config_opt].validator or module.config._config[config_opt].validator.internal_id != "Hidden" or force_hidden else self.hide_value(module.config[config_opt]), ] if ( module.config._config[config_opt].validator and module.config._config[config_opt].validator.internal_id == "Hidden" ): additonal_button_row = ( [ [ { "text": self.strings("hide_value"), "callback": self.inline__configure_option, "args": (mod, config_opt, False), "kwargs": {"obj_type": obj_type}, } ] ] if force_hidden else [ [ { "text": self.strings("show_hidden"), "callback": self.inline__configure_option, "args": (mod, config_opt, True), "kwargs": {"obj_type": obj_type}, } ] ] ) else: additonal_button_row = [] try: validator = module.config._config[config_opt].validator doc = utils.escape_html( next( ( validator.doc[lang] for lang in self._db.get( translations.__name__, "lang", "en" ).split(" ") if lang in validator.doc ), validator.doc["en"], ) ) except Exception: doc = None validator = None args += [""] else: args += [ self.strings("typehint").format( doc, eng_art="n" if doc.lower().startswith(tuple("euioay")) else "", ) ] if validator.internal_id == "Boolean": await call.edit( self.strings( "configuring_option" if isinstance(obj_type, bool) else "configuring_option_lib" ).format(*args), reply_markup=additonal_button_row + self._generate_bool_markup(mod, config_opt, obj_type), ) return if validator.internal_id == "Series": await call.edit( self.strings( "configuring_option" if isinstance(obj_type, bool) else "configuring_option_lib" ).format(*args), reply_markup=additonal_button_row + self._generate_series_markup(call, mod, config_opt, obj_type), ) return await call.edit( self.strings( "configuring_option" if isinstance(obj_type, bool) else "configuring_option_lib" ).format(*args), reply_markup=additonal_button_row + [ [ { "text": self.strings("enter_value_btn"), "input": self.strings("enter_value_desc"), "handler": self.inline__set_config, "args": (mod, config_opt, call.inline_message_id), "kwargs": {"obj_type": obj_type}, } ], [ { "text": self.strings("set_default_btn"), "callback": self.inline__reset_default, "args": (mod, config_opt), "kwargs": {"obj_type": obj_type}, } ], [ { "text": self.strings("back_btn"), "callback": self.inline__configure, "args": (mod,), "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ], ], ) async def inline__configure( self, call: InlineCall, mod: str, obj_type: Union[bool, str] = False, ): btns = [ { "text": param, "callback": self.inline__configure_option, "args": (mod, param), "kwargs": {"obj_type": obj_type}, } for param in self.lookup(mod).config ] await call.edit( self.strings( "configuring_mod" if isinstance(obj_type, bool) else "configuring_lib" ).format( utils.escape_html(mod), "\n".join( [ f"▫️ {utils.escape_html(key)}:" f" {self.prep_value(value) if not self.lookup(mod).config._config[key].validator or self.lookup(mod).config._config[key].validator.internal_id != 'Hidden' else self.hide_value(value)}" for key, value in self.lookup(mod).config.items() ] ), ), reply_markup=list(utils.chunks(btns, 2)) + [ [ { "text": self.strings("back_btn"), "callback": self.inline__global_config, "kwargs": {"obj_type": obj_type}, }, {"text": self.strings("close_btn"), "action": "close"}, ] ], ) async def inline__choose_category(self, call: Union[Message, InlineCall]): await utils.answer( call, self.strings("choose_core"), reply_markup=[ [ { "text": self.strings("builtin"), "callback": self.inline__global_config, "kwargs": {"obj_type": True}, }, { "text": self.strings("external"), "callback": self.inline__global_config, }, ], *( [ [ { "text": self.strings("libraries"), "callback": self.inline__global_config, "kwargs": {"obj_type": "library"}, } ] ] if self.allmodules.libraries and any(hasattr(lib, "config") for lib in self.allmodules.libraries) else [] ), [{"text": self.strings("close_btn"), "action": "close"}], ], ) async def inline__global_config( self, call: InlineCall, page: int = 0, obj_type: Union[bool, str] = False, ): if isinstance(obj_type, bool): to_config = [ mod.strings("name") for mod in self.allmodules.modules if hasattr(mod, "config") and callable(mod.strings) and (getattr(mod, "__origin__", None) == "" or not obj_type) and (getattr(mod, "__origin__", None) != "" or obj_type) ] else: to_config = [ lib.name for lib in self.allmodules.libraries if hasattr(lib, "config") ] to_config.sort() kb = [] for mod_row in utils.chunks( to_config[ page * self._num_rows * self._row_size : (page + 1) * self._num_rows * self._row_size ], 3, ): row = [ { "text": btn, "callback": self.inline__configure, "args": (btn,), "kwargs": {"obj_type": obj_type}, } for btn in mod_row ] kb += [row] if len(to_config) > self._num_rows * self._row_size: kb += self.inline.build_pagination( callback=functools.partial( self.inline__global_config, obj_type=obj_type ), total_pages=ceil(len(to_config) / (self._num_rows * self._row_size)), current_page=page + 1, ) kb += [ [ { "text": self.strings("back_btn"), "callback": self.inline__choose_category, }, {"text": self.strings("close_btn"), "action": "close"}, ] ] await call.edit( self.strings( "configure" if isinstance(obj_type, bool) else "configure_lib" ), reply_markup=kb, ) async def configcmd(self, message: Message): """Configure modules""" args = utils.get_args_raw(message) if self.lookup(args) and hasattr(self.lookup(args), "config"): form = await self.inline.form("🌘 Loading configuration", message) mod = self.lookup(args) if isinstance(mod, loader.Library): type_ = "library" else: type_ = getattr(mod, "__origin__", None) == "" await self.inline__configure(form, args, obj_type=type_) return await self.inline__choose_category(message) async def fconfigcmd(self, message: Message): """ - Stands for ForceConfig - Set the config value if it is not possible using default method""" args = utils.get_args_raw(message).split(maxsplit=2) if len(args) < 3: await utils.answer(message, self.strings("args")) return mod, option, value = args instance = self.lookup(mod) if not instance: await utils.answer(message, self.strings("no_mod")) return if option not in instance.config: await utils.answer(message, self.strings("no_option")) return instance.config[option] = value await utils.answer( message, self.strings( "option_saved" if isinstance(instance, loader.Module) else "option_saved_lib" ).format( utils.escape_html(option), utils.escape_html(mod), self.prep_value(instance.config[option]) if not instance.config._config[option].validator or instance.config._config[option].validator.internal_id != "Hidden" else self.hide_value(instance.config[option]), ), )