# ÂŠī¸ Dan Gazizullin, 2021-2023 # This file is a part of Hikka Userbot # 🌐 https://github.com/hikariatama/Hikka # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 # 🔑 https://www.gnu.org/licenses/agpl-3.0.html # ÂŠī¸ Codrago, 2024-2025 # This file is a part of Heroku Userbot # 🌐 https://github.com/coddrago/Heroku # You can redistribute it and/or modify it under the terms of the GNU AGPLv3 # 🔑 https://www.gnu.org/licenses/agpl-3.0.html import ast import contextlib import functools import typing from math import ceil from herokutl.tl.types import Message from .. import loader, translations, utils from ..inline.types import InlineCall # 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" ROW_SIZE = 3 NUM_ROWS = 5 @loader.tds class HerokuConfigMod(loader.Module): """Interactive configurator for Heroku Userbot""" strings = {"name": "HerokuConfig"} def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( "cfg_emoji", "đŸĒ", "Change emoji when opening config", validator=loader.validators.String(), ), ) @staticmethod def prep_value(value: typing.Any) -> typing.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: typing.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))) def _get_value(self, mod: str, option: str) -> str: return ( 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]) ) async def inline__set_config( self, call: InlineCall, query: str, mod: str, option: str, inline_message_id: str, obj_type: typing.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._get_value(mod, 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: typing.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._get_value(mod, 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: typing.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: typing.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: typing.Union[bool, str] = False, ): try: with contextlib.suppress(Exception): query = ast.literal_eval(query) 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._get_value(mod, 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: typing.Union[bool, str] = False, ): try: with contextlib.suppress(Exception): query = ast.literal_eval(query) 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._get_value(mod, 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: typing.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 _choice_set_value( self, call: InlineCall, mod: str, option: str, value: bool, obj_type: typing.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 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._get_value(mod, 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"}, ] ], ) await call.answer("✅") async def _multi_choice_set_value( self, call: InlineCall, mod: str, option: str, value: bool, obj_type: typing.Union[bool, str] = False, ): try: if value in self.lookup(mod).config._config[option].value: self.lookup(mod).config._config[option].value.remove(value) else: self.lookup(mod).config._config[option].value += [value] self.lookup(mod).config.reload() 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 self.inline__configure_option(call, mod, option, False, obj_type) await call.answer("✅") def _generate_choice_markup( self, call: InlineCall, mod: str, option: str, obj_type: typing.Union[bool, str] = False, ) -> list: possible_values = list( self.lookup(mod) .config._config[option] .validator.validate.keywords["possible_values"] ) 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}, } ], *utils.chunks( [ { "text": ( f"{'â˜‘ī¸' if self.lookup(mod).config[option] == value else '🔘'} " f"{value if len(str(value)) < 20 else str(value)[:20]}" ), "callback": self._choice_set_value, "args": (mod, option, value, obj_type), } for value in possible_values ], 2, )[ : ( 6 if self.lookup(mod).config[option] != self.lookup(mod).config.getdef(option) else 7 ) ], [ *( [ { "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"}, ], ] def _generate_multi_choice_markup( self, call: InlineCall, mod: str, option: str, obj_type: typing.Union[bool, str] = False, ) -> list: possible_values = list( self.lookup(mod) .config._config[option] .validator.validate.keywords["possible_values"] ) 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}, } ], *utils.chunks( [ { "text": ( f"{'â˜‘ī¸' if value in self.lookup(mod).config[option] else 'â—ģī¸'} " f"{value if len(str(value)) < 20 else str(value)[:20]}" ), "callback": self._multi_choice_set_value, "args": (mod, option, value, obj_type), } for value in possible_values ], 2, )[ : ( 6 if self.lookup(mod).config[option] != self.lookup(mod).config.getdef(option) else 7 ) ], [ *( [ { "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: bool = False, obj_type: typing.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 if validator.internal_id == "Choice": 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_choice_markup(call, mod, config_opt, obj_type), ) return if validator.internal_id == "MultiChoice": 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_multi_choice_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: typing.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( [ "â–Ģī¸ {}: {}".format( utils.escape_html(key), self._get_value(mod, key), ) for key in self.lookup(mod).config ] ), ), 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: typing.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: typing.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 (mod.__origin__.startswith(" NUM_ROWS * ROW_SIZE: kb += self.inline.build_pagination( callback=functools.partial( self.inline__global_config, obj_type=obj_type ), total_pages=ceil(len(to_config) / (NUM_ROWS * 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, ) @loader.command(alias="cfg") async def configcmd(self, message: Message): args = utils.get_args_raw(message) args_s = args.split() if len(args_s) == 1 and self.lookup(args_s[0]) and hasattr(self.lookup(args_s[0]), 'config'): form = await self.inline.form(self.config["cfg_emoji"], message, silent=True) mod = self.lookup(args) if isinstance(mod, loader.Library): type_ = "library" else: type_ = mod.__origin__.startswith("