From 0de3ddbd9ed3e39f3c40dbff0114cfb1c83aa779 Mon Sep 17 00:00:00 2001 From: Hash Borgir Date: Thu, 7 May 2026 16:18:45 -0500 Subject: [PATCH] config.py fixed for admin, updated .gitignore --- .gitignore | 9 +- plugins/config.py | 212 +++++++++++++++++++++++++++++----------------- roomstats.db | Bin 12288 -> 12288 bytes 3 files changed, 137 insertions(+), 84 deletions(-) diff --git a/.gitignore b/.gitignore index faccaf1..6707cd3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ .env -karma.db -proxies.db session.txt socks5.txt venv/ @@ -9,8 +7,7 @@ simplematrixbotlib*/ chromedriver store funguybot.service -stats.db -cron.db __pycache__/ -lastfm.db -venv.bak/ \ No newline at end of file +funguy.conf +*.db +plugins/disabled/ diff --git a/plugins/config.py b/plugins/config.py index 1ff1d6a..8b70182 100644 --- a/plugins/config.py +++ b/plugins/config.py @@ -1,10 +1,13 @@ """ Custom configuration class for the Funguy bot. +Security‑hardened: only the configured admin user can read or change settings. +Save operation preserves extra sections (plugins.disabled, etc.). """ # plugins/config.py import os import logging +import toml import simplematrixbotlib as botlib from dataclasses import dataclass @@ -23,13 +26,14 @@ class FunguyConfig(botlib.Config): # Load configuration from file self.load_toml(config_file) + # Store the actual file path used (so save_config can find it) + self._config_file = config_file logging.info(f"Loaded configuration from {config_file}") _admin_user: str = "" _prefix: str = "" _config_file: str= "" - # Define getters and setters for custom configuration options @property def admin_user(self): return self._admin_user @@ -54,123 +58,175 @@ class FunguyConfig(botlib.Config): def config_file(self, value): self._config_file = value - - # Method to load configuration from file def load_config(self, config_file): - """ - Load configuration options from a TOML file. - - Args: - config_file (str): Path to the configuration file. - - Returns: - None - """ + """Load configuration from a TOML file.""" self.load_toml(config_file) + self._config_file = config_file logging.info(f"Loaded configuration from {config_file}") - - # Method to save configuration to file - def save_config(self, config_file): + def save_config(self, config_file=None): """ - Save configuration options to a TOML file. + Save configuration to a TOML file, **preserving** any extra sections + (e.g., plugins.disabled) not managed by the base library. - Args: - config_file (str): Path to the configuration file. - - Returns: - None + If config_file is not provided, the instance's stored config_file is used. """ - self.save_toml(config_file) - logging.info(f"Saved configuration to {config_file}") + if config_file is None: + config_file = self._config_file + if not config_file: + raise ValueError("No config file path set for saving.") + + # 1. Let the library write its portion to a temporary file + tmp_file = config_file + ".tmp" + try: + self.save_toml(tmp_file) + + # 2. Read the temporary file (library's view of the config) + with open(tmp_file, 'r') as f: + new_config = toml.load(f) + + # 3. Read the current config file (if it exists) to preserve extra sections + original = {} + if os.path.exists(config_file): + with open(config_file, 'r') as f: + original = toml.load(f) + + # 4. Merge: keep everything from the original, then overlay + # the library's config table(s). This leaves any top-level + # sections not produced by the library exactly as they were. + merged = original.copy() + for key, value in new_config.items(): + merged[key] = value + + # 5. Write back the merged result + with open(config_file, 'w') as f: + toml.dump(merged, f) + + logging.info(f"Configuration saved to {config_file} (extra sections preserved)") + except Exception as e: + logging.error(f"Error saving config to {config_file}: {e}") + raise + finally: + # Always remove the temp file + if os.path.exists(tmp_file): + os.remove(tmp_file) + async def handle_command(room, message, bot, prefix, config): """ - Function to handle commands related to bot configuration. - - Args: - room (Room): The Matrix room where the command was invoked. - message (RoomMessage): The message object containing the command. - bot (Bot): The bot instance. - PREFIX (str): The bot command prefix. - config (FunguyConfig): The bot configuration instance. - - Returns: - None + Handle commands related to bot configuration. + All sub‑commands require the sender to be the configured admin_user. """ - # Check if the message matches the command pattern and is not from this bot match = botlib.MessageMatch(room, message, bot, prefix) - if match.is_not_from_this_bot() and match.prefix() and match.command("set"): - # If the command is 'set', check if it has exactly two arguments - args = match.args() + + if not match.is_not_from_this_bot() or not match.prefix(): + return + + cmd = match.command() + if cmd not in ("set", "get", "saveconf", "loadconf", "show", "reset"): + return + + sender = str(message.sender) + if sender != config.admin_user: + logging.warning( + "Unauthorized config command attempt by %s in room %s: %s", + sender, room.room_id, cmd + ) + await bot.api.send_text_message( + room.room_id, + "⛔ You are not authorized to use configuration commands." + ) + return + + args = match.args() + + if cmd == "set": if len(args) != 2: - await bot.api.send_text_message(room.room_id, "Usage: !set ") + await bot.api.send_text_message(room.room_id, + "Usage: !set ") return option, value = args - # Set the specified configuration option based on the provided value if option == "admin_user": - config.admin_user = value - await bot.api.send_text_message(room.room_id, f"Admin user set to {value}") + await bot.api.send_text_message( + room.room_id, + "❌ Changing 'admin_user' via !set is not allowed for security reasons." + ) + return elif option == "prefix": config.prefix = value - await bot.api.send_text_message(room.room_id, f"Prefix set to {value}") + await bot.api.send_text_message(room.room_id, + f"Prefix set to `{value}`") else: - await bot.api.send_text_message(room.room_id, "Invalid configuration option") + await bot.api.send_text_message(room.room_id, + "Invalid configuration option.") - # If the command is 'get', retrieve the value of the specified configuration option - elif match.is_not_from_this_bot() and match.prefix() and match.command("get"): - args = match.args() + elif cmd == "get": if len(args) != 1: - await bot.api.send_text_message(room.room_id, "Usage: !get ") + await bot.api.send_text_message(room.room_id, + "Usage: !get ") return option = args[0] if option == "admin_user": - await bot.api.send_text_message(room.room_id, f"Admin user: {config.admin_user}") + await bot.api.send_text_message(room.room_id, + f"Admin user: {config.admin_user}") elif option == "prefix": - await bot.api.send_text_message(room.room_id, f"Prefix: {config.prefix}") + await bot.api.send_text_message(room.room_id, + f"Prefix: {config.prefix}") else: - await bot.api.send_text_message(room.room_id, "Invalid configuration option") + await bot.api.send_text_message(room.room_id, + "Invalid configuration option.") - # If the command is 'saveconf', save the current configuration - elif match.is_not_from_this_bot() and match.prefix() and match.command("saveconf"): - config.save_config(config.config_file) - await bot.api.send_text_message(room.room_id, "Configuration saved") + elif cmd == "show": + await bot.api.send_text_message( + room.room_id, + f"Admin user: {config.admin_user}\nPrefix: {config.prefix}" + ) - # If the command is 'loadconf', load the saved configuration - elif match.is_not_from_this_bot() and match.prefix() and match.command("loadconf"): + elif cmd == "saveconf": + try: + config.save_config() # uses the stored config_file by default + await bot.api.send_text_message( + room.room_id, + "Configuration saved (including disabled plugins)." + ) + except Exception as e: + await bot.api.send_text_message( + room.room_id, + f"❌ Failed to save configuration: {e}" + ) + + elif cmd == "loadconf": config.load_config(config.config_file) - await bot.api.send_text_message(room.room_id, "Configuration loaded") + await bot.api.send_text_message( + room.room_id, + "Configuration reloaded from file." + ) - # If the command is 'show', display the current configuration - elif match.is_not_from_this_bot() and match.prefix() and match.command("show"): - admin_user = config.admin_user - prefix = config.prefix - await bot.api.send_text_message(room.room_id, f"Admin user: {admin_user}, Prefix: {prefix}") - - # If the command is 'reset', reset the configuration to default values - elif match.is_not_from_this_bot() and match.prefix() and match.command("reset"): - config.admin_user = "" + elif cmd == "reset": config.prefix = "!" - await bot.api.send_text_message(room.room_id, "Configuration reset") + await bot.api.send_text_message( + room.room_id, + "Configuration reset to defaults (admin_user unchanged)." + ) # --------------------------------------------------------------------------- # Plugin Metadata # --------------------------------------------------------------------------- - -__version__ = "1.0.0" -__author__ = "Funguy Bot" -__description__ = "Admin configuration commands" +__version__ = "1.0.2" +__author__ = "Funguy Bot (hardened)" +__description__ = "Admin-only configuration commands (preserves disabled plugins)" __help__ = """
Admin Config (!set, !get, !saveconf, …)
    -
  • !set <option> <value> – Set admin_user/prefix
  • -
  • !get <option> – Display config value
  • -
  • !show – Show current settings
  • -
  • !saveconf / !loadconf – Save/load config
  • -
  • !rehash – Reload configuration
  • +
  • !set prefix <value> – Change command prefix (admin only)
  • +
  • !get <option> – Display config value (admin only)
  • +
  • !show – Show current settings (admin only)
  • +
  • !saveconf / !loadconf – Save/load config (admin only)
  • +
  • !reset – Reset to defaults, preserving admin_user (admin only)
-

Admin only.

+

Changing admin_user via bot commands is blocked for safety.

+

The plugins.disabled section is now preserved when saving.

""" diff --git a/roomstats.db b/roomstats.db index daadc185b1d9563a70d6c86617fd7cfcf936d9bd..07cb7e02aee817a3968fc6fe47b3b7147221b59b 100644 GIT binary patch delta 257 zcmZojXh@hK&6qe*#+fm3W5Pmye*RPj7QW*Q{9^nY_>S`zY!(#Q&d2V{%;#y($TT@e zUPH4aBegsuvm~{oGCj4(DmSsDD6>K@zbKuFG2A)x&$lnWjC`Kfj7&_7lk?=&Ccn}b zQ?O!lVpL(t{L9$Jz`(!_#2{eD!sn^U0Ro(p3*=R#%2IQz@-nirbM%t)bAf83{g}XZ zGI0XUv6ED0Ud=E$^Do0yE}&i(eklh2WBi@`KKxQZ2XgVV7_xY3PVUuL;6pKnWpby! MiX<)vGXrJI0m0fxWdHyG delta 119 zcmZojXh@hK%_uTa#+gxMW5PmyZUzPhCjL|g{>}WUn*|j(`6o66u=_CadD=5FPCh56 zG5Nl{ga8u