#!/usr/bin/env python """ Funguy Bot Class """ # Importing necessary libraries and modules import os # Operating System functions import logging # Logging library for logging messages import importlib # Library for dynamically importing modules import simplematrixbotlib as botlib # Library for interacting with Matrix chat from dotenv import load_dotenv # Library for loading environment variables from a .env file import time # Time-related functions import sys # System-specific parameters and functions import toml # Library for parsing TOML configuration files # Importing FunguyConfig class from plugins.config module from plugins.config import FunguyConfig class FunguyBot: """ A bot class for managing plugins and handling commands in a Matrix chat environment. Methods: - __init__: Constructor method for initializing the bot. - load_dotenv: Method to load environment variables from a .env file. - setup_logging: Method to configure logging settings. - load_plugins: Method to load plugins from the specified directory. - reload_plugins: Method to reload all plugins. - load_config: Method to load configuration settings. - load_disabled_plugins: Method to load disabled plugins from configuration file. - save_disabled_plugins: Method to save disabled plugins to configuration file. - handle_commands: Method to handle incoming commands and dispatch them to appropriate plugins. - rehash_config: Method to rehash the configuration settings. - disable_plugin: Method to disable a plugin for a specific room. - enable_plugin: Method to enable a plugin for a specific room. - run: Method to initialize and run the bot. Properties: - PLUGINS_DIR: Directory where plugins are stored - PLUGINS: Dictionary to store loaded plugins - config: Configuration object - bot: Bot object - disabled_plugins: Dictionary to store disabled plugins for each room """ def __init__(self): """ Constructor method for FunguyBot class. """ # Setting up instance variables self.PLUGINS_DIR = "plugins" # Directory where plugins are stored self.PLUGINS = {} # Dictionary to store loaded plugins self.config = None # Configuration object self.bot = None # Bot object self.disabled_plugins = {} # Dictionary to store disabled plugins for each room self.load_dotenv() # Loading environment variables from .env file self.setup_logging() # Setting up logging configurations self.load_plugins() # Loading plugins self.load_config() # Loading bot configuration self.load_disabled_plugins() # Loading disabled plugins from configuration file def load_dotenv(self): """ Method to load environment variables from a .env file. """ load_dotenv() def setup_logging(self): """ Method to configure logging settings. """ # Basic configuration for logging messages to console logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO) logging.getLogger().setLevel(logging.INFO) def load_plugins(self): """ Method to load plugins from the specified directory. """ # Iterating through files in the plugins directory for plugin_file in os.listdir(self.PLUGINS_DIR): if plugin_file.endswith(".py"): # Checking if file is a Python file plugin_name = os.path.splitext(plugin_file)[0] # Extracting plugin name try: # Importing plugin module dynamically module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}") self.PLUGINS[plugin_name] = module # Storing loaded plugin module logging.info(f"Loaded plugin: {plugin_name}") # Logging successful plugin loading except Exception as e: logging.error(f"Error loading plugin {plugin_name}: {e}") # Logging error if plugin loading fails def reload_plugins(self): """ Method to reload all plugins. """ self.PLUGINS = {} # Clearing loaded plugins dictionary # Unloading modules from sys.modules for plugin_name in list(sys.modules.keys()): if plugin_name.startswith(self.PLUGINS_DIR + "."): del sys.modules[plugin_name] # Deleting plugin module from system modules self.load_plugins() # Reloading plugins def load_config(self): """ Method to load configuration settings. """ self.config = FunguyConfig() # Creating instance of FunguyConfig to load configuration def load_disabled_plugins(self): """ Method to load disabled plugins from configuration file. """ # Checking if configuration file exists if os.path.exists('funguy.conf'): # Loading configuration data from TOML file with open('funguy.conf', 'r') as f: config_data = toml.load(f) # Extracting disabled plugins from configuration data self.disabled_plugins = config_data.get('plugins', {}).get('disabled', {}) def save_disabled_plugins(self): """ Method to save disabled plugins to configuration file. """ existing_config = {} # Checking if configuration file exists if os.path.exists('funguy.conf'): # Loading existing configuration data with open('funguy.conf', 'r') as f: existing_config = toml.load(f) # Updating configuration data with disabled plugins existing_config['plugins'] = {'disabled': self.disabled_plugins} # Writing updated configuration data back to file with open('funguy.conf', 'w') as f: toml.dump(existing_config, f) async def handle_commands(self, room, message): """ Method to handle incoming commands and dispatch them to appropriate plugins. """ match = botlib.MessageMatch(room, message, self.bot, self.config.prefix) # Matching message against bot's prefix # Reloading plugins command if match.is_not_from_this_bot() and match.prefix() and match.command("reload"): if str(message.sender) == self.config.admin_user: # Checking if sender is admin user self.reload_plugins() # Reloading plugins await self.bot.api.send_text_message(room.room_id, "Plugins reloaded successfully") # Sending success message else: await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") # Sending unauthorized message # Disable plugin command if match.is_not_from_this_bot() and match.prefix() and match.command("disable"): if str(message.sender) == self.config.admin_user: # Checking if sender is admin user args = match.args() # Getting command arguments if len(args) != 2: # Checking if correct number of arguments provided await self.bot.api.send_text_message(room.room_id, "Usage: !disable ") # Sending usage message else: plugin_name, room_id = args # Extracting plugin name and room ID await self.disable_plugin(room_id, plugin_name) # Disabling plugin await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' disabled for room '{room_id}'") # Sending success message else: await self.bot.api.send_text_message(room.room_id, "You are not authorized to disable plugins.") # Sending unauthorized message # Enable plugin command if match.is_not_from_this_bot() and match.prefix() and match.command("enable"): if str(message.sender) == self.config.admin_user: # Checking if sender is admin user args = match.args() # Getting command arguments if len(args) != 2: # Checking if correct number of arguments provided await self.bot.api.send_text_message(room.room_id, "Usage: !enable ") # Sending usage message else: plugin_name, room_id = args # Extracting plugin name and room ID await self.enable_plugin(room_id, plugin_name) # Enabling plugin await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' enabled for room '{room_id}'") # Sending success message else: await self.bot.api.send_text_message(room.room_id, "You are not authorized to enable plugins.") # Sending unauthorized message # Dispatching commands to plugins for plugin_name, plugin_module in self.PLUGINS.items(): if plugin_name not in self.disabled_plugins.get(room.room_id, []): await plugin_module.handle_command(room, message, self.bot, self.config.prefix, self.config) # Rehash config command if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"): if str(message.sender) == self.config.admin_user: # Checking if sender is admin user self.rehash_config() # Rehashing configuration await self.bot.api.send_text_message(room.room_id, "Config rehashed") # Sending success message else: await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") # Sending unauthorized message def rehash_config(self): """ Method to rehash the configuration settings. """ del self.config # Deleting current configuration object self.config = FunguyConfig() # Creating new instance of FunguyConfig to load updated configuration async def disable_plugin(self, room_id, plugin_name): """ Method to disable a plugin for a specific room. """ if room_id not in self.disabled_plugins: self.disabled_plugins[room_id] = [] # Creating entry for room ID if not exist if plugin_name not in self.disabled_plugins[room_id]: self.disabled_plugins[room_id].append(plugin_name) # Adding plugin to list of disabled plugins for the room self.save_disabled_plugins() # Saving disabled plugins to configuration file async def enable_plugin(self, room_id, plugin_name): """ Method to enable a plugin for a specific room. """ if room_id in self.disabled_plugins and plugin_name in self.disabled_plugins[room_id]: self.disabled_plugins[room_id].remove(plugin_name) # Removing plugin from list of disabled plugins for the room self.save_disabled_plugins() # Saving disabled plugins to configuration file def run(self): """ Method to initialize and run the bot. """ # Retrieving Matrix credentials from environment variables MATRIX_URL = os.getenv("MATRIX_URL") MATRIX_USER = os.getenv("MATRIX_USER") MATRIX_PASS = os.getenv("MATRIX_PASS") creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS) # Creating credentials object self.bot = botlib.Bot(creds, self.config) # Creating bot instance # Defining listener for message events @self.bot.listener.on_message_event async def wrapper_handle_commands(room, message): await self.handle_commands(room, message) # Calling handle_commands method for incoming messages self.bot.run() # Running the bot if __name__ == "__main__": bot = FunguyBot() # Creating instance of FunguyBot bot.run() # Running the bot