config.py fixed for admin, updated .gitignore

This commit is contained in:
2026-05-07 16:18:45 -05:00
parent 10a6028037
commit 0de3ddbd9e
3 changed files with 137 additions and 84 deletions
+134 -78
View File
@@ -1,10 +1,13 @@
"""
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
@@ -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 subcommands 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 <config_option> <value>")
await bot.api.send_text_message(room.room_id,
"Usage: !set <config_option> <value>")
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 <config_option>")
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}")
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__ = """
<details>
<summary><strong>Admin Config</strong> (!set, !get, !saveconf, …)</summary>
<ul>
<li><code>!set &lt;option&gt; &lt;value&gt;</code> Set admin_user/prefix</li>
<li><code>!get &lt;option&gt;</code> Display config value</li>
<li><code>!show</code> Show current settings</li>
<li><code>!saveconf</code> / <code>!loadconf</code> Save/load config</li>
<li><code>!rehash</code> Reload configuration</li>
<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>Admin only.</p>
<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>
"""