diff --git a/DexScript/github/installer.py b/DexScript/github/installer.py index ab50579..7a2a4fe 100644 --- a/DexScript/github/installer.py +++ b/DexScript/github/installer.py @@ -35,7 +35,7 @@ class InstallerConfig: """ github = ["Dotsian/DexScript", "dev"] - files = ["__init__.py", "cog.py", "commands.py", "parser.py", "utils.py"] + files = ["__init__.py", "cog.py", "commands.py", "parser.py", "utils.py", "config.toml"] appearance = { "logo": f"{ASSET_PATH}/DexScriptLogo.png", "logo_error": f"{ASSET_PATH}/DexScriptLogoError.png", @@ -71,15 +71,10 @@ def __init__(self, installer, embed_type="setup"): self.installer = installer - match embed_type: - case "setup": - self.setup() - case "error": - self.error() - case "installed": - self.installed() - case "uninstalled": - self.uninstalled() + if not hasattr(self, embed_type): + return + + getattr(self, embed_type)() def setup(self): self.title = "DexScript Installation" @@ -139,6 +134,15 @@ def uninstalled(self): self.set_thumbnail(url=config.appearance["logo"]) + def config(self): + with open(f"{config.path}/config.toml") as file: + file_contents = file.read() + + self.title = "DexScript Configuration" + self.description = f"```toml\n{file_contents}\n```" + self.color = discord.Color.from_str("#03BAFC") + self.timestamp = datetime.now() + class InstallerView(discord.ui.View): def __init__(self, installer): @@ -177,11 +181,153 @@ async def uninstall_button(self, interaction: discord.Interaction, _: discord.ui await interaction.message.edit(**self.installer.interface.fields) await interaction.response.defer() + @discord.ui.button( + style=discord.ButtonStyle.secondary, + label="Config", + disabled=not os.path.isfile(f"{config.path}/config.toml") + ) + async def config_button(self, interaction: discord.Interaction, _: discord.ui.Button): + self.installer.interface.embed = InstallerEmbed(self.installer, "config") + self.installer.interface.view = ConfigView(self.installer) + + await interaction.message.edit(**self.installer.interface.fields) + await interaction.response.defer() + @discord.ui.button(style=discord.ButtonStyle.red, label="Exit") async def quit_button(self, interaction: discord.Interaction, _: discord.ui.Button): - self.install_button.disabled = True - self.uninstall_button.disabled = True - self.quit_button.disabled = True + for item in self.children: + item.disabled = True + + await interaction.message.edit(**self.installer.interface.fields) + await interaction.response.defer() + + +class ConfigModal(discord.ui.Modal): + def __init__(self, installer, setting: str): + self.installer = installer + self.setting = setting + + super().__init__(title=f"Editing `{setting}`") + + value = discord.ui.TextInput(label="New value", required=True) + + async def on_submit(self, interaction: discord.Interaction): + with open(f"{config.path}/config.toml") as file: + lines = [x.strip() for x in file.readlines()] + new_lines = [] + + for line in lines: + if not line.startswith(self.setting): + new_lines.append(line + "\n") + continue + + full_value = self.value.value + new_value = f'"{full_value}"' + + if full_value.lower() in ["true", "false"]: + new_value = full_value.lower() + elif full_value.startswith("[") and full_value.endswith("]"): + new_value = full_value + + new_lines.append(f"{self.setting} = {new_value}\n") + + with open(f"{config.path}/config.toml", "w") as write_file: + write_file.writelines(new_lines) + + self.installer.interface.embed = InstallerEmbed(self.installer, "config") + + await interaction.message.edit(**self.installer.interface.fields) + + await interaction.response.send_message( + f"Updated `{self.setting}` to `{self.value.value}`!", + ephemeral=True + ) + + +class ConfigSelect(discord.ui.Select): + def __init__(self, installer): + self.installer = installer + + options = [] + + with open(f"{config.path}/config.toml") as file: + description = "" + + for line in file.readlines(): + if line.rstrip() in ["\n", "", "]"] or line.startswith(" "): + continue + + if line.startswith("#"): + description = line[2:] + continue + + name = line.split(" ")[0] + + options.append( + discord.SelectOption(label=name, value=name, description=description) + ) + + description = "" + + super().__init__(placeholder="Edit setting", max_values=1, min_values=1, options=options) + + async def callback(self, interaction: discord.Interaction): + await interaction.response.send_modal(ConfigModal(self.installer, self.values[0])) + + +class ConfigView(discord.ui.View): + def __init__(self, installer): + super().__init__() + self.installer = installer + + back_button = discord.ui.Button(label="Back", style=discord.ButtonStyle.primary) + reset_button = discord.ui.Button(label="Reset", style=discord.ButtonStyle.grey) + quit_button = discord.ui.Button(label="Exit", style=discord.ButtonStyle.red) + + back_button.callback = self.back_button + reset_button.callback = self.reset_button + quit_button.callback = self.quit_button + + self.add_item(back_button) + self.add_item(ConfigSelect(installer)) + self.add_item(reset_button) + self.add_item(quit_button) + + async def back_button(self, interaction: discord.Interaction): + self.installer.interface.embed = InstallerEmbed(self.installer, "setup") + self.installer.interface.view = InstallerView(self.installer) + + await interaction.message.edit(**self.installer.interface.fields) + await interaction.response.defer() + + async def reset_button(self, interaction: discord.Interaction): + request = requests.get( + f"https://api.github.com/repos/{config.github[0]}/" + "contents/DexScript/package/config.toml", + {"ref": config.github[1]} + ) + + if request.status_code != requests.codes.ok: + await interaction.response.send_message( + f"Failed to reset config file `({request.status_code})`", ephemeral=True + ) + return + + request = request.json() + content = b64decode(request["content"]) + + with open(f"{config.path}/config.toml", "w") as opened_file: + opened_file.write(content.decode()) + + self.installer.interface.embed = InstallerEmbed(self.installer, "config") + + await interaction.message.edit(**self.installer.interface.fields) + + await interaction.response.send_message("Successfully reset config file", ephemeral=True) + + async def quit_button(self, interaction: discord.Interaction): + for item in self.children: + item.disabled = True await interaction.message.edit(**self.installer.interface.fields) await interaction.response.defer() @@ -274,11 +420,15 @@ async def install(self): await bot.remove_cog("DexScript") # type: ignore - link = f"https://api.github.com/repos/{config.github[0]}/contents/" + link = f"https://api.github.com/repos/{config.github[0]}/contents" os.makedirs(config.path, exist_ok=True) for file in config.files: + if file.endswith(".toml") and os.path.isfile(f"{config.path}/{file}"): + logger.log(f"{file} already exists, skipping", "INFO") + continue + logger.log(f"Fetching {file} from '{link}/DexScript/package'", "INFO") request = requests.get(f"{link}/DexScript/package/{file}", {"ref": config.github[1]}) @@ -293,7 +443,7 @@ async def install(self): content = b64decode(request["content"]) with open(f"{config.path}/{file}", "w") as opened_file: - opened_file.write(content.decode("UTF-8")) + opened_file.write(content.decode()) logger.log(f"Installed {file} from '{link}/DexScript/package'", "INFO") @@ -334,7 +484,7 @@ def latest_version(self): if pyproject_request.status_code != requests.codes.ok: return - toml_content = b64decode(pyproject_request.json()["content"]).decode("UTF-8") + toml_content = b64decode(pyproject_request.json()["content"]).decode() new_version = re.search(r'version\s*=\s*"(.*?)"', toml_content) if not new_version: diff --git a/DexScript/package/cog.py b/DexScript/package/cog.py index ecd9e5d..6e273b8 100644 --- a/DexScript/package/cog.py +++ b/DexScript/package/cog.py @@ -15,6 +15,9 @@ ASSET_PATH = "https://raw.githubusercontent.com/Dotsian/DexScript/refs/heads/main/assets" +def check_dexscript_user(ctx): + return ctx.message.author.id in config.dexscript_user_ids + class DexScript(commands.Cog): """ DexScript commands. @@ -25,12 +28,12 @@ def __init__(self, bot): @staticmethod def check_version(): - if not config.versioncheck: + if not config.version_warning: return None request = requests.get( "https://api.github.com/repos/Dotsian/DexScript/contents/pyproject.toml", - {"ref": config.reference}, + {"ref": config.branch}, ) if request.status_code != requests.codes.ok: @@ -50,6 +53,7 @@ def check_version(): @commands.command() @commands.is_owner() + @commands.check(check_dexscript_user) async def run(self, ctx: commands.Context, *, code: str): """ Executes DexScript code. @@ -84,6 +88,7 @@ async def run(self, ctx: commands.Context, *, code: str): @commands.command() @commands.is_owner() + @commands.check(check_dexscript_user) async def about(self, ctx: commands.Context): """ Displays information about DexScript. @@ -120,6 +125,7 @@ async def about(self, ctx: commands.Context): @commands.command() @commands.is_owner() + @commands.check(check_dexscript_user) async def installer(self, ctx: commands.Context, reference: str = "main"): """ Displays the DexScript installer. @@ -148,32 +154,3 @@ async def installer(self, ctx: commands.Context, reference: str = "main"): case _: await ctx.send(f"Request raised error code `{request.status_code}`.") - - @commands.command() - @commands.is_owner() - async def setting(self, ctx: commands.Context, setting: str, value: str | None = None): - """ - Changes a setting based on the value provided. - - Parameters - ---------- - setting: str - The setting you want to toggle. - value: str | None - The value you want to set the setting to. - """ - setting = setting.lower() - - if setting not in vars(config): - await ctx.send(f"`{setting}` is not a valid setting.") - return - - setting_value = vars(config)[setting] - new_value = value - - if isinstance(setting_value, bool): - new_value = bool(value) if value else not setting_value - - setattr(config, setting, new_value) - - await ctx.send(f"`{setting}` has been set to `{new_value}`") diff --git a/DexScript/package/config.toml b/DexScript/package/config.toml new file mode 100644 index 0000000..1306048 --- /dev/null +++ b/DexScript/package/config.toml @@ -0,0 +1,14 @@ +# Whether or not DexScript will warn you for running an outdated version. +version-warning = true + +# The command groups that DexScript will load. +command-groups = ["Global", "Emoji", "Eval", "File", "Filter", "Template"] + +# A list of user IDs that will be able to execute DexScript commands. +dexscript-user-ids = [] + +# Displays additional information for error handling. +debug = false + +# The GitHub branch that will be used for version checking and other misc features. +branch = "main" diff --git a/DexScript/package/parser.py b/DexScript/package/parser.py index f70edd2..c54d06f 100644 --- a/DexScript/package/parser.py +++ b/DexScript/package/parser.py @@ -38,13 +38,13 @@ def __init__(self, ctx, bot): and issubclass(o, commands.DexCommand) and not issubclass(o, commands.Global) and o.__name__ != "DexCommand" - and o.__name__ in config.modules + and o.__name__ in config.command_groups ), ) self.global_methods = [x for x in dir(commands.Global) if not x.startswith("__")] - if "Global" not in config.modules: + if "Global" not in config.command_groups: self.global_methods = [] def create_value(self, line): @@ -62,7 +62,7 @@ def create_value(self, line): Types.BOOLEAN: lower in ["true", "false"], Types.HEX: lower.startswith("#"), Types.ARRAY: lower.startswith("[") and lower.endswith("]"), - Types.NONE: lower == "NIL" + Types.NONE: lower == "nil" } for key, operation in type_dict.items(): diff --git a/DexScript/package/utils.py b/DexScript/package/utils.py index e563ddd..300024e 100644 --- a/DexScript/package/utils.py +++ b/DexScript/package/utils.py @@ -3,7 +3,8 @@ import inspect import os import re -from dataclasses import dataclass, field +import tomllib +from dataclasses import dataclass from difflib import get_close_matches from enum import Enum from io import StringIO @@ -58,20 +59,31 @@ class Settings: Settings class for DexScript. """ - debug: bool = False - versioncheck: bool = False - reference: str = "main" - modules: list[str] = field(default_factory=lambda: [ - "Global", - "Emoji", - "Eval", - "File", - "Filter", - "Template" - ]) + def __init__(self, path): + with open(path, "rb") as f: + data = tomllib.load(f) + + if data is None: + return + + self.version_warning = data.get("version-warning", True) + + self.command_groups = data.get("command-groups", [ + "Global", + "Emoji", + "Eval", + "File", + "Filter", + "Template" + ]) + + self.dexscript_user_ids = data.get("dexscript-user-ids", []) + + self.debug = data.get("debug", False) + self.branch = data.get("branch", "main") -config = Settings() +config = Settings(Path(os.path.dirname(os.path.abspath(__file__)), "./config.toml")) @dataclass diff --git a/README.md b/README.md index 1232b01..496878f 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ To install DexScript, you must have the following: ## DexScript Setup -The DexScript installer is a intuitive menu that can allow you to easily update, install, and uninstall DexScript. To bring up the DexScript installer, all you have to do is run one eval command! +The DexScript installer is a intuitive menu that can allow you to easily update, install, configure, and uninstall DexScript. To bring up the DexScript installer, all you have to do is run one eval command! ### Versions @@ -60,6 +60,7 @@ Once you have ran the eval command, the DexScript installer should appear. There * Install [or] Update * Uninstall +* Config * Exit > [!IMPORTANT] @@ -73,6 +74,10 @@ If you are installing DexScript for the first time, you will see a button called If you already have DexScript, you will see a button called "Update". When you click that button, DexScript will update to the latest version. This will instantly update DexScript, which means you don't have to restart your bot. +#### Configuration + +If you have a `config.toml` file already installed within DexScript, the "Config" button will appear. This button will allow you to access the configuration menu, which will let you modify DexScript's internal settings. + #### Uninstalling If you already have DexScript, you will see a button called "Uninstall". Clicking the uninstall button will uninstall DexScript from your application. This will instantly remove the DexScript package and DexScript commands will unload instantly.