Files
FunguyBot/plugins/config.py
T

233 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Custom configuration class for the Funguy bot.
Securityhardened: 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 subcommands 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 <config_option> <value>")
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 <config_option>")
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__ = """
<details>
<summary><strong>Admin Config</strong> (!set, !get, !saveconf, …)</summary>
<ul>
<li><code>!set prefix &lt;value&gt;</code> Change command prefix (admin only)</li>
<li><code>!get &lt;option&gt;</code> Display config value (admin only)</li>
<li><code>!show</code> Show current settings (admin only)</li>
<li><code>!saveconf</code> / <code>!loadconf</code> Save/load config (admin only)</li>
<li><code>!reset</code> Reset to defaults, preserving admin_user (admin only)</li>
</ul>
<p>Changing <code>admin_user</code> via bot commands is blocked for safety.</p>
<p>The <code>plugins.disabled</code> section is now preserved when saving.</p>
</details>
"""