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
+3 -6
View File
@@ -1,6 +1,4 @@
.env .env
karma.db
proxies.db
session.txt session.txt
socks5.txt socks5.txt
venv/ venv/
@@ -9,8 +7,7 @@ simplematrixbotlib*/
chromedriver chromedriver
store store
funguybot.service funguybot.service
stats.db
cron.db
__pycache__/ __pycache__/
lastfm.db funguy.conf
venv.bak/ *.db
plugins/disabled/
+134 -78
View File
@@ -1,10 +1,13 @@
""" """
Custom configuration class for the Funguy bot. 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 # plugins/config.py
import os import os
import logging import logging
import toml
import simplematrixbotlib as botlib import simplematrixbotlib as botlib
from dataclasses import dataclass from dataclasses import dataclass
@@ -23,13 +26,14 @@ class FunguyConfig(botlib.Config):
# Load configuration from file # Load configuration from file
self.load_toml(config_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}") logging.info(f"Loaded configuration from {config_file}")
_admin_user: str = "" _admin_user: str = ""
_prefix: str = "" _prefix: str = ""
_config_file: str= "" _config_file: str= ""
# Define getters and setters for custom configuration options
@property @property
def admin_user(self): def admin_user(self):
return self._admin_user return self._admin_user
@@ -54,123 +58,175 @@ class FunguyConfig(botlib.Config):
def config_file(self, value): def config_file(self, value):
self._config_file = value self._config_file = value
# Method to load configuration from file
def load_config(self, config_file): def load_config(self, config_file):
""" """Load configuration from a TOML file."""
Load configuration options from a TOML file.
Args:
config_file (str): Path to the configuration file.
Returns:
None
"""
self.load_toml(config_file) self.load_toml(config_file)
self._config_file = config_file
logging.info(f"Loaded configuration from {config_file}") logging.info(f"Loaded configuration from {config_file}")
def save_config(self, config_file=None):
# Method to save configuration to file
def save_config(self, config_file):
""" """
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: If config_file is not provided, the instance's stored config_file is used.
config_file (str): Path to the configuration file.
Returns:
None
""" """
self.save_toml(config_file) if config_file is None:
logging.info(f"Saved configuration to {config_file}") 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): async def handle_command(room, message, bot, prefix, config):
""" """
Function to handle commands related to bot configuration. Handle commands related to bot configuration.
All subcommands require the sender to be the configured admin_user.
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
""" """
# Check if the message matches the command pattern and is not from this bot
match = botlib.MessageMatch(room, message, bot, prefix) 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 if not match.is_not_from_this_bot() or not match.prefix():
args = match.args() 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: 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 return
option, value = args option, value = args
# Set the specified configuration option based on the provided value
if option == "admin_user": if option == "admin_user":
config.admin_user = value await bot.api.send_text_message(
await bot.api.send_text_message(room.room_id, f"Admin user set to {value}") room.room_id,
"❌ Changing 'admin_user' via !set is not allowed for security reasons."
)
return
elif option == "prefix": elif option == "prefix":
config.prefix = value 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: 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 cmd == "get":
elif match.is_not_from_this_bot() and match.prefix() and match.command("get"):
args = match.args()
if len(args) != 1: 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 return
option = args[0] option = args[0]
if option == "admin_user": 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": 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: 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 cmd == "show":
elif match.is_not_from_this_bot() and match.prefix() and match.command("saveconf"): await bot.api.send_text_message(
config.save_config(config.config_file) room.room_id,
await bot.api.send_text_message(room.room_id, "Configuration saved") f"Admin user: {config.admin_user}\nPrefix: {config.prefix}"
)
# If the command is 'loadconf', load the saved configuration elif cmd == "saveconf":
elif match.is_not_from_this_bot() and match.prefix() and match.command("loadconf"): 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) 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 cmd == "reset":
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 = ""
config.prefix = "!" 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 # Plugin Metadata
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
__version__ = "1.0.2"
__version__ = "1.0.0" __author__ = "Funguy Bot (hardened)"
__author__ = "Funguy Bot" __description__ = "Admin-only configuration commands (preserves disabled plugins)"
__description__ = "Admin configuration commands"
__help__ = """ __help__ = """
<details> <details>
<summary><strong>Admin Config</strong> (!set, !get, !saveconf, …)</summary> <summary><strong>Admin Config</strong> (!set, !get, !saveconf, …)</summary>
<ul> <ul>
<li><code>!set &lt;option&gt; &lt;value&gt;</code> Set admin_user/prefix</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</li> <li><code>!get &lt;option&gt;</code> Display config value (admin only)</li>
<li><code>!show</code> Show current settings</li> <li><code>!show</code> Show current settings (admin only)</li>
<li><code>!saveconf</code> / <code>!loadconf</code> Save/load config</li> <li><code>!saveconf</code> / <code>!loadconf</code> Save/load config (admin only)</li>
<li><code>!rehash</code> Reload configuration</li> <li><code>!reset</code> Reset to defaults, preserving admin_user (admin only)</li>
</ul> </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> </details>
""" """
BIN
View File
Binary file not shown.