Heroku/heroku/modules/heroku_config.py

1045 lines
35 KiB
Python

# ©️ 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"</b><code>{utils.escape_html(value.strip())}</code><b>"
if isinstance(value, list) and value:
return (
"</b><code>[</code>\n "
+ "\n ".join(
[f"<code>{utils.escape_html(str(item))}</code>" for item in value]
)
+ "\n<code>]</code><b>"
)
return f"</b><code>{utils.escape_html(value)}</code><b>"
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(
[
"▫️ <code>{}</code>: <b>{}</b>".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("<core") or not obj_type)
and (not mod.__origin__.startswith("<core") 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 * NUM_ROWS * ROW_SIZE : (page + 1) * NUM_ROWS * 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) > 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("<core")
await self.inline__configure(form, args, obj_type=type_)
return
if len(args_s) == 2 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_s[0])
if isinstance(mod, loader.Library):
type_ = "library"
else:
type_ = mod.__origin__.startswith("<core")
if args_s[1] in mod.config.keys():
await self.inline__configure_option(form, args_s[0], args_s[1], obj_type=type_)
else:
await self.inline__configure(form, args, obj_type=type_)
return
await self.inline__choose_category(message)
@loader.command(alias="fcfg")
async def fconfig(self, message: Message):
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
if not (instance := self.lookup(mod)):
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._get_value(mod, option),
),
)