diff --git a/funguy.conf b/funguy.conf new file mode 100644 index 0000000..5ef4f36 --- /dev/null +++ b/funguy.conf @@ -0,0 +1,11 @@ +[simplematrixbotlib.config] +timeout = 65536 +join_on_invite = true +encryption_enabled = false +emoji_verify = false +ignore_unverified_devices = true +store_path = "./store/" +allowlist = [] +blocklist = [] +admin_user = "@hashborgir:mozilla.org" +prefix = "!!" diff --git a/funguy.py b/funguy.py index 589d75c..5efcb25 100755 --- a/funguy.py +++ b/funguy.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# bot.py +# funguy.py import os import logging @@ -8,26 +8,12 @@ import simplematrixbotlib as botlib from dotenv import load_dotenv import time import sys - -# Load environment variables from .env file -load_dotenv() - -# Bot configuration settings -MATRIX_URL = os.getenv("MATRIX_URL") -MATRIX_USER = os.getenv("MATRIX_USER") -MATRIX_PASS = os.getenv("MATRIX_PASS") - -ADMIN_USER = "@hashborgir:mozilla.org" - -PREFIX = '!' - -creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS) -bot = botlib.Bot(creds) +from plugins.config import FunguyConfig # Setup logging logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO) -# Load plugins +# Load plugins (defined before plugin load/reload functions) PLUGINS_DIR = "plugins" PLUGINS = {} @@ -51,19 +37,53 @@ def reload_plugins(): del sys.modules[plugin_name] load_plugins() +def rehash_config(config): + del config + config = FunguyConfig() + + +# Load environment variables from .env file +load_dotenv() + +# Load plugins load_plugins() +# Bot configuration settings +MATRIX_URL = os.getenv("MATRIX_URL") +MATRIX_USER = os.getenv("MATRIX_USER") +MATRIX_PASS = os.getenv("MATRIX_PASS") + +# Get credentials from env +creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS) + +# Bot configuration +config = FunguyConfig() +bot = botlib.Bot(creds, config) + +PREFIX = config.prefix + @bot.listener.on_message_event async def handle_commands(room, message): match = botlib.MessageMatch(room, message, bot, PREFIX) if match.is_not_from_this_bot() and match.prefix() and match.command("reload"): - if str(message.sender) == ADMIN_USER: + if str(message.sender) == config.admin_user: reload_plugins() await bot.api.send_text_message(room.room_id, "Plugins reloaded successfully") else: await bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") for plugin_name, plugin_module in PLUGINS.items(): - await plugin_module.handle_command(room, message, bot, PREFIX) + await plugin_module.handle_command(room, message, bot, PREFIX, config) + + + if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"): + if str(message.sender) == config.admin_user: + rehash_config(config) + await bot.api.send_text_message(room.room_id, "Config rehashed") + else: + await bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") + + + bot.run() diff --git a/plugins/config.py b/plugins/config.py new file mode 100644 index 0000000..9de70b4 --- /dev/null +++ b/plugins/config.py @@ -0,0 +1,130 @@ +# plugins/config.py + +import os +import logging +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) + logging.info("Loaded configuration from funguy.conf") + + _admin_user: str = "" + _prefix: str = "" + + # Define getters and setters for custom configuration options + @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 + + # 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 + """ + self.__init__() + logging.info("Loaded configuration from funguy.conf") + + # Method to save configuration to file + def save_config(self, config_file): + """ + Save configuration options to a TOML file. + + Args: + config_file (str): Path to the configuration file. + + Returns: + None + """ + self.save_toml(config_file) + logging.info("Saved configuration to funguy.conf") + +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 + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("set"): + args = match.args() + if len(args) != 2: + await bot.api.send_text_message(room.room_id, "Usage: !set ") + return + option, value = args + if option == "admin_user": + config.admin_user = value + await bot.api.send_text_message(room.room_id, f"Admin user set to {value}") + 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 match.is_not_from_this_bot() and match.prefix() and match.command("get"): + args = match.args() + 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 match.is_not_from_this_bot() and match.prefix() and match.command("save"): + config.save_config("funguy.conf") + config.load_config("funguy.conf") + await bot.api.send_text_message(room.room_id, "Configuration saved & reloaded") + + 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}") + + elif match.is_not_from_this_bot() and match.prefix() and match.command("reset"): + config.admin_user = "" + config.prefix = "!" + await bot.api.send_text_message(room.room_id, "Configuration reset") + diff --git a/plugins/date.py b/plugins/date.py index 36d3a15..2cc65d9 100644 --- a/plugins/date.py +++ b/plugins/date.py @@ -1,10 +1,14 @@ +""" +This plugin provides a command to get the current date +""" + # plugins/date.py import datetime import logging import simplematrixbotlib as botlib -async def handle_command(room, message, bot, PREFIX): +async def handle_command(room, message, bot, PREFIX, config): """ Function to handle the !date command. diff --git a/plugins/ai-music.py b/plugins/disabled/ai-music.py similarity index 98% rename from plugins/ai-music.py rename to plugins/disabled/ai-music.py index 2004ee2..fe71674 100644 --- a/plugins/ai-music.py +++ b/plugins/disabled/ai-music.py @@ -1,3 +1,6 @@ +""" +This plugin provides a command to interact with the music knowledge A.I. +""" # plugins/ai-music.py import logging diff --git a/plugins/ai-tech.py b/plugins/disabled/ai-tech.py similarity index 98% rename from plugins/ai-tech.py rename to plugins/disabled/ai-tech.py index 4dee36e..4b614fd 100644 --- a/plugins/ai-tech.py +++ b/plugins/disabled/ai-tech.py @@ -1,3 +1,7 @@ +""" +This plugin provides a command to interact with the LLM for Tech/IT/Security/Selfhosting/Programming etc. +""" + # plugins/ai-tech.py import logging diff --git a/plugins/commands.py b/plugins/disabled/commands.py similarity index 96% rename from plugins/commands.py rename to plugins/disabled/commands.py index 502f45a..8f1ba7d 100644 --- a/plugins/commands.py +++ b/plugins/disabled/commands.py @@ -1,3 +1,7 @@ +""" +This plugin provides a command to display the list of available commands and their descriptions. +""" + # plugins/commands.py import logging diff --git a/plugins/disabled/config.py b/plugins/disabled/config.py new file mode 100644 index 0000000..5f68bef --- /dev/null +++ b/plugins/disabled/config.py @@ -0,0 +1,131 @@ +# plugins/config.py + +import os +import logging +import simplematrixbotlib as botlib + +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): + super().__init__() + + # Define custom configuration options here + self.admin_user = "" + self.prefix = "!" + + # Load configuration from file + self.load_toml(config_file) + logging.info("Loaded configuration from funguy.conf") + + # Define getters and setters for custom configuration options + @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 + + # 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 + """ + self.load_toml(config_file) + logging.info("Loaded configuration from funguy.conf") + + # Method to save configuration to file + def save_config(self, config_file): + """ + Save configuration options to a TOML file. + + Args: + config_file (str): Path to the configuration file. + + Returns: + None + """ + self.save_toml(config_file) + logging.info("Saved configuration to funguy.conf") + +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 + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("set"): + args = match.args() + if len(args) != 2: + await bot.api.send_text_message(room.room_id, "Usage: !set ") + return + option, value = args + if option == "admin_user": + config.admin_user = value + await bot.api.send_text_message(room.room_id, f"Admin user set to {value}") + 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 match.is_not_from_this_bot() and match.prefix() and match.command("get"): + args = match.args() + 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 match.is_not_from_this_bot() and match.prefix() and match.command("save"): + config.save_config("funguy.conf") + await bot.api.send_text_message(room.room_id, "Configuration saved") + + elif match.is_not_from_this_bot() and match.prefix() and match.command("load"): + config.load_config("funguy.conf") + await bot.api.send_text_message(room.room_id, "Configuration loaded") + + 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}") + + elif match.is_not_from_this_bot() and match.prefix() and match.command("reset"): + config.admin_user = "" + config.prefix = "!" + await bot.api.send_text_message(room.room_id, "Configuration reset") + diff --git a/plugins/disabled/date.py b/plugins/disabled/date.py new file mode 100644 index 0000000..743f8b1 --- /dev/null +++ b/plugins/disabled/date.py @@ -0,0 +1,33 @@ +""" +This plugin provides a command to get the current date +""" + +# plugins/date.py + +import datetime +import logging +import simplematrixbotlib as botlib + +async def handle_command(room, message, bot, config): + PREFIX = config.prefix + """ + Function to handle the !date command. + + Args: + room (Room): The Matrix room where the command was invoked. + message (RoomMessage): The message object containing the command. + + Returns: + None + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("date"): + logging.info("Fetching current date and time") + current_datetime = datetime.datetime.now() + day_of_week = current_datetime.strftime("%A") + month = current_datetime.strftime("%B") + year = current_datetime.strftime("%Y") + time = current_datetime.strftime("%I:%M:%S %p") + date_message = f"📅 Today is **{day_of_week}** of **{month}** in **{year}**. The time is **⏰ {time}**" + await bot.api.send_markdown_message(room.room_id, date_message) + logging.info("Sent current date and time to the room") diff --git a/plugins/fortune.py b/plugins/disabled/fortune.py similarity index 91% rename from plugins/fortune.py rename to plugins/disabled/fortune.py index add2a2c..474dd43 100644 --- a/plugins/fortune.py +++ b/plugins/disabled/fortune.py @@ -1,3 +1,6 @@ +""" +This plugin provides a command to get a random fortune message. +""" # plugins/fortune.py import subprocess diff --git a/plugins/isup.py b/plugins/disabled/isup.py similarity index 97% rename from plugins/isup.py rename to plugins/disabled/isup.py index f68ad17..b50ffe4 100644 --- a/plugins/isup.py +++ b/plugins/disabled/isup.py @@ -1,3 +1,7 @@ +""" +This plugin provides a command to check if a website or server is up. +""" + # plugins/isup.py import logging diff --git a/plugins/karma.py b/plugins/disabled/karma.py similarity index 98% rename from plugins/karma.py rename to plugins/disabled/karma.py index aa37cae..d74012d 100644 --- a/plugins/karma.py +++ b/plugins/disabled/karma.py @@ -1,3 +1,8 @@ +""" +This plugin provides a command to manage karma points for users. +""" + + # plugins/karma.py import sqlite3 diff --git a/plugins/disabled/load_plugin.py b/plugins/disabled/load_plugin.py new file mode 100644 index 0000000..dd3ad95 --- /dev/null +++ b/plugins/disabled/load_plugin.py @@ -0,0 +1,47 @@ +""" +This plugin provides a command to load individual plugins from local or remote sources +""" + +# plugins/load_plugin.py + +import os +import logging +import importlib +import simplematrixbotlib as botlib + +# Bot configuration settings +ADMIN_USER = "@hashborgir:mozilla.org" + +PLUGINS = {} + +async def load_plugin(plugin_name): + try: + module = importlib.import_module(f"{plugin_name}") + PLUGINS[plugin_name] = module + logging.info(f"Loaded plugin: {plugin_name}") + return True + except ModuleNotFoundError: + logging.error(f"Plugin module '{plugin_name}' not found") + except Exception as e: + logging.error(f"Error loading plugin '{plugin_name}': {e}") + return False + +async def handle_command(room, message, bot, PREFIX): + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("load"): + if str(message.sender) == ADMIN_USER: + args = match.args() + if len(args) != 1: + await bot.api.send_text_message(room.room_id, "Usage: !load ") + else: + plugin_name = args[0] + if plugin_name not in PLUGINS: + success = await load_plugin(plugin_name) + if success: + await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' loaded successfully") + else: + await bot.api.send_text_message(room.room_id, f"Failed to load plugin '{plugin_name}'") + else: + await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' is already loaded") + else: + await bot.api.send_text_message(room.room_id, "You are not authorized to load plugins.") diff --git a/plugins/disabled/plugins.py b/plugins/disabled/plugins.py new file mode 100644 index 0000000..e239574 --- /dev/null +++ b/plugins/disabled/plugins.py @@ -0,0 +1,57 @@ +""" +This plugin provides a command to list all loaded plugins along with their descriptions. +""" + +# plugins/plugins.py + +import os +import sys +import logging +import simplematrixbotlib as botlib + +async def handle_command(room, message, bot, PREFIX): + """ + Function to handle the !plugins command. + + Args: + room (Room): The Matrix room where the command was invoked. + message (RoomMessage): The message object containing the command. + + Returns: + None + """ + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"): + logging.info("Received !plugins command") + plugin_descriptions = get_plugin_descriptions() + + # Prepend custom string before the output + plugin_descriptions.insert(0, "# Plugins List: \n\n") + + plugins_message = "\n\n".join(plugin_descriptions) + await bot.api.send_markdown_message(room.room_id, plugins_message) + logging.info("Sent plugin list to the room") + + +def get_plugin_descriptions(): + """ + Function to get descriptions of all loaded plugin modules. + + Returns: + list: A list of plugin descriptions sorted alphabetically. + """ + plugin_descriptions = [] + for module_name, module in sys.modules.items(): + if module_name.startswith("plugins.") and hasattr(module, "__file__"): + plugin_path = module.__file__ + plugin_name = os.path.basename(plugin_path).split(".")[0] + try: + description = module.__doc__.strip().split("\n")[0] + except AttributeError: + description = "No description available" + plugin_descriptions.append(f"**[{plugin_name}.py]:** {description}") + + # Sort the plugin descriptions alphabetically by plugin name + plugin_descriptions.sort() + + return plugin_descriptions diff --git a/plugins/proxy.py b/plugins/disabled/proxy.py similarity index 98% rename from plugins/proxy.py rename to plugins/disabled/proxy.py index c498d90..a9b39ca 100644 --- a/plugins/proxy.py +++ b/plugins/disabled/proxy.py @@ -1,3 +1,7 @@ +""" +This plugin provides a command to get random SOCKS5 proxies. +""" + # plugins/proxy.py import os diff --git a/plugins/youtube.py b/plugins/disabled/youtube.py similarity index 95% rename from plugins/youtube.py rename to plugins/disabled/youtube.py index af0465f..8357b10 100644 --- a/plugins/youtube.py +++ b/plugins/disabled/youtube.py @@ -1,3 +1,7 @@ +""" +This plugin provides a command to fetch YouTube video information from links. +""" + # plugins/youtube.py import re diff --git a/plugins/load_plugin.py b/plugins/load_plugin.py new file mode 100644 index 0000000..2e5db54 --- /dev/null +++ b/plugins/load_plugin.py @@ -0,0 +1,41 @@ +# plugins/load_plugin.py + +import os +import logging +import importlib +import simplematrixbotlib as botlib + +# Bot configuration settings +ADMIN_USER = "@hashborgir:mozilla.org" + +PLUGINS = {} + +async def load_plugin(plugin_name): + try: + module = importlib.import_module(f"plugins.{plugin_name}") + PLUGINS[plugin_name] = module + logging.info(f"Loaded plugin: {plugin_name}") + return True + except Exception as e: + logging.error(f"Error loading plugin {plugin_name}: {e}") + return False + +async def handle_command(room, message, bot, PREFIX, config): + match = botlib.MessageMatch(room, message, bot, PREFIX) + if match.is_not_from_this_bot() and match.prefix() and match.command("load"): + if str(message.sender) == ADMIN_USER: + args = match.args() + if len(args) != 1: + await bot.api.send_text_message(room.room_id, "Usage: !load ") + else: + plugin_name = args[0] + if plugin_name not in PLUGINS: + success = await load_plugin(plugin_name) + if success: + await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' loaded successfully") + else: + await bot.api.send_text_message(room.room_id, f"Error loading plugin '{plugin_name}'") + else: + await bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' is already loaded") + else: + await bot.api.send_text_message(room.room_id, "You are not authorized to load plugins.")