233 lines
7.8 KiB
Python
233 lines
7.8 KiB
Python
"""
|
||
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 <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 <value></code> – Change command prefix (admin only)</li>
|
||
<li><code>!get <option></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>
|
||
"""
|