""" 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 @dataclass class FunguyConfig(botlib.Config): """ Custom configuration class for the Funguy bot. Extends the base Config class to provide additional configuration options. Args: config_file (str): Path to the configuration file. """ def __init__(self, config_file="funguy.conf"): super().__init__() # 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= "" @property def admin_user(self): return self._admin_user @admin_user.setter def admin_user(self, value): self._admin_user = value @property def prefix(self): return self._prefix @prefix.setter def prefix(self, value): self._prefix = value @property def config_file(self): return self._config_file @config_file.setter def config_file(self, value): self._config_file = value def load_config(self, config_file): """Load configuration from a TOML file.""" self.load_toml(config_file) self._config_file = config_file logging.info(f"Loaded configuration from {config_file}") def save_config(self, config_file=None): """ Save configuration to a TOML file, **preserving** any extra sections (e.g., plugins.disabled) not managed by the base library. If config_file is not provided, the instance's stored config_file is used. """ 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): """ Handle commands related to bot configuration. All sub‑commands require the sender to be the configured admin_user. """ match = botlib.MessageMatch(room, message, bot, prefix) 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 ") return option, value = args if option == "admin_user": 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}`") else: await bot.api.send_text_message(room.room_id, "Invalid configuration option.") elif cmd == "get": if len(args) != 1: 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}") elif option == "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.") elif cmd == "show": await bot.api.send_text_message( room.room_id, f"Admin user: {config.admin_user}\nPrefix: {config.prefix}" ) 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 reloaded from file." ) elif cmd == "reset": config.prefix = "!" await bot.api.send_text_message( room.room_id, "Configuration reset to defaults (admin_user unchanged)." ) # --------------------------------------------------------------------------- # Plugin Metadata # --------------------------------------------------------------------------- __version__ = "1.0.2" __author__ = "Funguy Bot (hardened)" __description__ = "Admin-only configuration commands (preserves disabled plugins)" __help__ = """
Admin Config (!set, !get, !saveconf, …)
  • !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)

Changing admin_user via bot commands is blocked for safety.

The plugins.disabled section is now preserved when saving.

"""