mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 12:05:38 +00:00
212 lines
8.5 KiB
Python
212 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Plugin loader for dynamic command discovery and loading
|
|
Handles scanning, loading, and registering command plugins
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import importlib
|
|
import importlib.util
|
|
from pathlib import Path
|
|
from typing import Dict, List, Any, Optional, Type
|
|
import logging
|
|
|
|
from .commands.base_command import BaseCommand
|
|
|
|
|
|
class PluginLoader:
|
|
"""Handles dynamic loading and discovery of command plugins"""
|
|
|
|
def __init__(self, bot, commands_dir: str = None):
|
|
self.bot = bot
|
|
self.logger = bot.logger
|
|
self.commands_dir = commands_dir or os.path.join(os.path.dirname(__file__), 'commands')
|
|
self.loaded_plugins: Dict[str, BaseCommand] = {}
|
|
self.plugin_metadata: Dict[str, Dict[str, Any]] = {}
|
|
self.keyword_mappings: Dict[str, str] = {} # keyword -> plugin_name
|
|
|
|
def discover_plugins(self) -> List[str]:
|
|
"""Discover all Python files in the commands directory that could be plugins"""
|
|
plugin_files = []
|
|
commands_path = Path(self.commands_dir)
|
|
|
|
if not commands_path.exists():
|
|
self.logger.error(f"Commands directory does not exist: {self.commands_dir}")
|
|
return plugin_files
|
|
|
|
# Scan for Python files (excluding __init__.py and base_command.py)
|
|
for file_path in commands_path.glob("*.py"):
|
|
if file_path.name not in ["__init__.py", "base_command.py", "plugin_loader.py"]:
|
|
plugin_files.append(file_path.stem)
|
|
|
|
self.logger.info(f"Discovered {len(plugin_files)} potential plugin files: {plugin_files}")
|
|
return plugin_files
|
|
|
|
def load_plugin(self, plugin_name: str) -> Optional[BaseCommand]:
|
|
"""Load a single plugin by name"""
|
|
try:
|
|
# Construct the full module path
|
|
module_path = f"modules.commands.{plugin_name}"
|
|
|
|
# Check if module is already loaded
|
|
if module_path in sys.modules:
|
|
module = sys.modules[module_path]
|
|
else:
|
|
# Import the module
|
|
module = importlib.import_module(module_path)
|
|
|
|
# Find the command class (should be the only class that inherits from BaseCommand)
|
|
command_class = None
|
|
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
if (issubclass(obj, BaseCommand) and
|
|
obj != BaseCommand and
|
|
obj.__module__ == module_path):
|
|
command_class = obj
|
|
break
|
|
|
|
if not command_class:
|
|
self.logger.warning(f"No valid command class found in {plugin_name}")
|
|
return None
|
|
|
|
# Instantiate the command
|
|
plugin_instance = command_class(self.bot)
|
|
|
|
# Validate plugin metadata
|
|
metadata = plugin_instance.get_metadata()
|
|
if not metadata.get('name'):
|
|
# Use the class name as the plugin name if not specified
|
|
metadata['name'] = command_class.__name__.lower().replace('command', '')
|
|
plugin_instance.name = metadata['name']
|
|
|
|
self.logger.info(f"Successfully loaded plugin: {metadata['name']} from {plugin_name}")
|
|
return plugin_instance
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to load plugin {plugin_name}: {e}")
|
|
return None
|
|
|
|
def load_all_plugins(self) -> Dict[str, BaseCommand]:
|
|
"""Load all discovered plugins"""
|
|
plugin_files = self.discover_plugins()
|
|
loaded_plugins = {}
|
|
|
|
for plugin_file in plugin_files:
|
|
plugin_instance = self.load_plugin(plugin_file)
|
|
if plugin_instance:
|
|
metadata = plugin_instance.get_metadata()
|
|
plugin_name = metadata['name']
|
|
loaded_plugins[plugin_name] = plugin_instance
|
|
self.plugin_metadata[plugin_name] = metadata
|
|
|
|
# Build keyword mappings
|
|
self._build_keyword_mappings(plugin_name, metadata)
|
|
|
|
self.loaded_plugins = loaded_plugins
|
|
self.logger.info(f"Loaded {len(loaded_plugins)} plugins: {list(loaded_plugins.keys())}")
|
|
return loaded_plugins
|
|
|
|
def _build_keyword_mappings(self, plugin_name: str, metadata: Dict[str, Any]):
|
|
"""Build keyword to plugin name mappings"""
|
|
# Map keywords to plugin name
|
|
for keyword in metadata.get('keywords', []):
|
|
self.keyword_mappings[keyword.lower()] = plugin_name
|
|
|
|
# Map aliases to plugin name
|
|
for alias in metadata.get('aliases', []):
|
|
self.keyword_mappings[alias.lower()] = plugin_name
|
|
|
|
def get_plugin_by_keyword(self, keyword: str) -> Optional[BaseCommand]:
|
|
"""Get a plugin instance by keyword"""
|
|
plugin_name = self.keyword_mappings.get(keyword.lower())
|
|
if plugin_name:
|
|
return self.loaded_plugins.get(plugin_name)
|
|
return None
|
|
|
|
def get_plugin_by_name(self, name: str) -> Optional[BaseCommand]:
|
|
"""Get a plugin instance by name"""
|
|
return self.loaded_plugins.get(name)
|
|
|
|
def get_all_plugins(self) -> Dict[str, BaseCommand]:
|
|
"""Get all loaded plugins"""
|
|
return self.loaded_plugins.copy()
|
|
|
|
def get_plugin_metadata(self, plugin_name: str = None) -> Dict[str, Any]:
|
|
"""Get metadata for a specific plugin or all plugins"""
|
|
if plugin_name:
|
|
return self.plugin_metadata.get(plugin_name, {})
|
|
return self.plugin_metadata.copy()
|
|
|
|
def get_plugins_by_category(self, category: str) -> Dict[str, BaseCommand]:
|
|
"""Get all plugins in a specific category"""
|
|
return {
|
|
name: plugin for name, plugin in self.loaded_plugins.items()
|
|
if plugin.category == category
|
|
}
|
|
|
|
def reload_plugin(self, plugin_name: str) -> bool:
|
|
"""Reload a specific plugin"""
|
|
try:
|
|
# Remove from loaded plugins
|
|
if plugin_name in self.loaded_plugins:
|
|
del self.loaded_plugins[plugin_name]
|
|
|
|
# Remove from metadata
|
|
if plugin_name in self.plugin_metadata:
|
|
del self.plugin_metadata[plugin_name]
|
|
|
|
# Remove keyword mappings
|
|
keywords_to_remove = []
|
|
for keyword, mapped_name in self.keyword_mappings.items():
|
|
if mapped_name == plugin_name:
|
|
keywords_to_remove.append(keyword)
|
|
|
|
for keyword in keywords_to_remove:
|
|
del self.keyword_mappings[keyword]
|
|
|
|
# Reload the plugin
|
|
plugin_instance = self.load_plugin(plugin_name)
|
|
if plugin_instance:
|
|
metadata = plugin_instance.get_metadata()
|
|
self.loaded_plugins[plugin_name] = plugin_instance
|
|
self.plugin_metadata[plugin_name] = metadata
|
|
self._build_keyword_mappings(plugin_name, metadata)
|
|
self.logger.info(f"Successfully reloaded plugin: {plugin_name}")
|
|
return True
|
|
else:
|
|
self.logger.error(f"Failed to reload plugin: {plugin_name}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error reloading plugin {plugin_name}: {e}")
|
|
return False
|
|
|
|
def validate_plugin(self, plugin_instance: BaseCommand) -> List[str]:
|
|
"""Validate a plugin instance and return any issues"""
|
|
issues = []
|
|
metadata = plugin_instance.get_metadata()
|
|
|
|
# Check required metadata
|
|
if not metadata.get('name'):
|
|
issues.append("Plugin missing 'name' metadata")
|
|
|
|
if not metadata.get('description'):
|
|
issues.append("Plugin missing 'description' metadata")
|
|
|
|
# Check if execute method is implemented
|
|
if not hasattr(plugin_instance, 'execute'):
|
|
issues.append("Plugin missing 'execute' method")
|
|
|
|
# Check for keyword conflicts
|
|
for keyword in metadata.get('keywords', []):
|
|
if keyword.lower() in self.keyword_mappings:
|
|
existing_plugin = self.keyword_mappings[keyword.lower()]
|
|
if existing_plugin != metadata['name']:
|
|
issues.append(f"Keyword '{keyword}' conflicts with plugin '{existing_plugin}'")
|
|
|
|
return issues
|
|
|
|
|
|
# Import inspect at module level for the load_plugin method
|
|
import inspect
|