From b722a78d21d9b3f31f91eeedbf1f4c9b32fa21ec Mon Sep 17 00:00:00 2001 From: Hash Borgir Date: Sat, 9 May 2026 12:08:38 -0500 Subject: [PATCH] various plugin refactors and fixes --- README.md | 38 +++-- funguy.py | 156 +++++++++++++++---- plugins/config.py | 352 +++++++++++++++++++++++++++++++----------- plugins/isup.py | 3 +- plugins/loadplugin.py | 128 --------------- plugins/utils.py | 59 ------- 6 files changed, 402 insertions(+), 334 deletions(-) delete mode 100644 plugins/loadplugin.py delete mode 100644 plugins/utils.py diff --git a/README.md b/README.md index f8bb088..94f6f31 100644 --- a/README.md +++ b/README.md @@ -104,16 +104,18 @@ systemctl start funguybot The bot includes the following plugins: -- **admin.py**: Full room moderation – multi‑word name support -- **arxiv.py**: arXiv academic paper search (with rate limiting and error reporting) +- **admin.py**: Full room moderation – multi-word name support +- **arxiv.py**: arXiv academic paper search - **bitcoin.py**: Current Bitcoin price +- **common.py**: Shared utilities for FunguyBot plugins. - **config.py**: Admin-only configuration commands (preserves disabled plugins) -- **cron.py**: In‑process cron scheduler (room‑aware, no system crontab) +- **cron.py**: In-process cron scheduler (room-aware, no system crontab) - **date.py**: Show current date and time -- **ddg.py**: DuckDuckGo search – collapsible results (ddgs library, no API key) -- **dictionary.py** Dictionary plugin to fetch word definitions from dict -- **dns.py**: DNS reconnaissance +- **ddg.py**: DuckDuckGo search plugin +- **dictionary.py**: Look up word definitions using system dictionary +- **dns.py**: DNS reconnaissance (SSRF-safe) - **dnsdumpster.py**: DNSDumpster domain reconnaissance +- **encode.py**: Comprehensive CyberChef-like encoding and analysis toolkit - **exploitdb.py**: Exploit-DB search - **fortune.py**: Random fortune message - **geo.py**: IP geolocation lookup @@ -124,27 +126,29 @@ The bot includes the following plugins: - **imdb.py**: IMDb lookup via OMDb API - **infermatic-text.py**: AI text generation via Infermatic API - **isup.py**: Check if a site is up -- **joke.py**: Get a random joke from the joke APIs +- **joke.py**: Get random jokes from the Official Joke API - **karma.py**: Room karma tracking system (display names only, no Matrix IDs) -- **lastfm.py**: Last.fm integration +- **lastfm.py**: Last.fm music stats with aligned code block output - **loadplugin.py**: Load/unload plugins at runtime - **news.py**: News headlines via GNews API -- **plugins.py**: List all loaded plugins +- **plugins.py**: List all loaded plugins with count - **proxy.py**: Working SOCKS5 proxy finder -- **quote.py**: Goodreads quotes via headless browser (Playwright) -- **roomstats.py**: Per‑user room statistics (Limnoria‑style), with multi‑word name support +- **quote.py**: Fetch Goodreads quotes +- **roomstats.py**: Per-user room statistics - **shodan.py**: Shodan.io reconnaissance - **sslscan.py**: SSL/TLS security scanner -- **stable-diffusion.py**: Stable Diffusion image generation +- **stable-diffusion.py**: Stable Diffusion image generation (LORA support) - **subdomains.py**: Subdomain enumeration via CertSpotter -- **sysinfo.py**: System information and monitoring -- **timezone.py**: World clock (no hardcoded cities) +- **subnet.py**: Subnet calculator +- **sysinfo.py**: System information plugin +- **timezone.py**: World clock (offline IANA zones + free geocoding) - **urbandictionary.py**: Urban Dictionary definitions -- **weather.py**: Weather forecast (OWM primary, Open‑Meteo fallback) +- **utils.py**: Security utilities for Funguy Bot plugins. +- **weather.py**: Weather data plugin - **welcome.py**: Room welcome message -- **whois.py**: WHOIS lookup +- **whois.py**: Domain WHOIS lookup - **wikipedia.py**: Wikipedia article summary -- **xkcd.py**: Random XKCD comic +- **xkcd.py**: Fetch random or specific xkcd comics - **youtube-search.py**: YouTube video search ## Configuration diff --git a/funguy.py b/funguy.py index 095b005..126cb85 100644 --- a/funguy.py +++ b/funguy.py @@ -20,7 +20,7 @@ from plugins.config import FunguyConfig # Rate limiter settings RATE_LIMIT_WINDOW = 5.0 # seconds -MAX_COMMANDS_PER_WINDOW = 5 +MAX_COMMANDS_PER_WINDOW = 3 class FunguyBot: @@ -118,22 +118,78 @@ class FunguyBot: toml.dump(existing_config, f) def _check_rate_limit(self, sender: str) -> bool: - """Return True if the sender is allowed to proceed, False if rate limited.""" + """Return True if the sender is allowed to proceed. + Admin is always allowed.""" + # Admin bypass + if sender == self.config.admin_user: + return True + now = time.monotonic() bucket = self._rate_limit_buckets[sender] # Prune old entries bucket = [t for t in bucket if now - t < RATE_LIMIT_WINDOW] self._rate_limit_buckets[sender] = bucket + if len(bucket) >= MAX_COMMANDS_PER_WINDOW: + logging.debug("Rate limit hit for %s", sender) return False + bucket.append(now) return True + # ------------------------------------------------------------------ + # New: load/unload a single plugin at runtime + # ------------------------------------------------------------------ + async def load_plugin(self, plugin_name: str) -> bool: + """Dynamically load a plugin module, add to PLUGINS, and call its setup().""" + if plugin_name in self.PLUGINS: + logging.info(f"Plugin '{plugin_name}' is already loaded.") + return False + try: + module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}") + self.PLUGINS[plugin_name] = module + logging.info(f"Loaded plugin: {plugin_name}") + # Call setup if the bot is already running + if self.bot is not None and hasattr(module, "setup") and callable(module.setup): + module.setup(self.bot) + logging.info(f"Setup called for newly loaded plugin: {plugin_name}") + return True + except Exception as e: + logging.error(f"Error loading plugin {plugin_name}: {e}") + return False + + async def unload_plugin(self, plugin_name: str) -> bool: + """Remove a plugin from PLUGINS and unload its module.""" + if plugin_name not in self.PLUGINS: + logging.info(f"Plugin '{plugin_name}' is not loaded.") + return False + try: + del self.PLUGINS[plugin_name] + module_path = f"{self.PLUGINS_DIR}.{plugin_name}" + if module_path in sys.modules: + del sys.modules[module_path] + logging.info(f"Unloaded plugin: {plugin_name}") + return True + except Exception as e: + logging.error(f"Error unloading plugin {plugin_name}: {e}") + return False + + # ------------------------------------------------------------------ + # New: restart the bot process + # ------------------------------------------------------------------ + async def restart_bot(self, room_id): + await self.bot.api.send_text_message(room_id, "🔄 Restarting bot...") + await asyncio.sleep(1) + logging.info("Restart command received – exiting.") + sys.exit(0) + async def handle_commands(self, room, message): match = botlib.MessageMatch(room, message, self.bot, self.config.prefix) - # Rate limit check (applies to all commands) sender = str(message.sender) + is_admin = (sender == self.config.admin_user) + + # Rate limit check (applies to all non‑admin commands) if not self._check_rate_limit(sender): await self.bot.api.send_text_message( room.room_id, @@ -143,45 +199,81 @@ class FunguyBot: # Admin commands if match.is_not_from_this_bot() and match.prefix() and match.command("reload"): - if sender == self.config.admin_user: + if is_admin: self.reload_plugins() - await self.bot.api.send_text_message(room.room_id, "Plugins reloaded successfully") + await self.bot.api.send_text_message(room.room_id, "All plugins reloaded successfully.") else: await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") return + if match.is_not_from_this_bot() and match.prefix() and match.command("load"): + if not is_admin: + await self.bot.api.send_text_message(room.room_id, "⛔ Admin only.") + return + args = match.args() + if len(args) != 1: + await self.bot.api.send_text_message(room.room_id, "Usage: !load ") + return + success = await self.load_plugin(args[0]) + msg = f"✅ Plugin '{args[0]}' loaded." if success else f"❌ Could not load '{args[0]}'. See logs for details." + await self.bot.api.send_text_message(room.room_id, msg) + return + + if match.is_not_from_this_bot() and match.prefix() and match.command("unload"): + if not is_admin: + await self.bot.api.send_text_message(room.room_id, "⛔ Admin only.") + return + args = match.args() + if len(args) != 1: + await self.bot.api.send_text_message(room.room_id, "Usage: !unload ") + return + success = await self.unload_plugin(args[0]) + msg = f"✅ Plugin '{args[0]}' unloaded." if success else f"❌ Could not unload '{args[0]}'. See logs for details." + await self.bot.api.send_text_message(room.room_id, msg) + return + if match.is_not_from_this_bot() and match.prefix() and match.command("disable"): - if sender == self.config.admin_user: - args = match.args() - if len(args) != 2: - await self.bot.api.send_text_message(room.room_id, "Usage: !disable ") - else: - plugin_name, room_id = args - await self.disable_plugin(room_id, plugin_name) - await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' disabled for room '{room_id}'") - else: - await self.bot.api.send_text_message(room.room_id, "You are not authorized to disable plugins.") + if not is_admin: + await self.bot.api.send_text_message(room.room_id, "⛔ Admin only.") + return + args = match.args() + if len(args) != 1: + await self.bot.api.send_text_message(room.room_id, "Usage: !disable ") + return + plugin_name = args[0] + room_id = room.room_id + await self.disable_plugin(room_id, plugin_name) + await self.bot.api.send_text_message(room.room_id, f"🚫 Plugin '{plugin_name}' disabled in this room.") return if match.is_not_from_this_bot() and match.prefix() and match.command("enable"): - if sender == self.config.admin_user: - args = match.args() - if len(args) != 2: - await self.bot.api.send_text_message(room.room_id, "Usage: !enable ") - else: - plugin_name, room_id = args - await self.enable_plugin(room_id, plugin_name) - await self.bot.api.send_text_message(room.room_id, f"Plugin '{plugin_name}' enabled for room '{room_id}'") - else: - await self.bot.api.send_text_message(room.room_id, "You are not authorized to enable plugins.") + if not is_admin: + await self.bot.api.send_text_message(room.room_id, "⛔ Admin only.") + return + args = match.args() + if len(args) != 1: + await self.bot.api.send_text_message(room.room_id, "Usage: !enable ") + return + plugin_name = args[0] + room_id = room.room_id + await self.enable_plugin(room_id, plugin_name) + await self.bot.api.send_text_message(room.room_id, f"✅ Plugin '{plugin_name}' enabled in this room.") + return + + if match.is_not_from_this_bot() and match.prefix() and match.command("restart"): + if not is_admin: + await self.bot.api.send_text_message(room.room_id, "⛔ Admin only.") + return + await self.restart_bot(room.room_id) return if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"): - if sender == self.config.admin_user: - self.rehash_config() - await self.bot.api.send_text_message(room.room_id, "Config rehashed") - else: - await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.") + if not is_admin: + await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload config.") + return + self.load_config() + self.load_disabled_plugins() + await self.bot.api.send_text_message(room.room_id, "🔄 Configuration rehashed.") return # Dispatch to active plugins @@ -192,10 +284,6 @@ class FunguyBot: except Exception as e: logging.error(f"Error in plugin {plugin_name}: {e}", exc_info=True) - def rehash_config(self): - del self.config - self.config = FunguyConfig() - async def disable_plugin(self, room_id, plugin_name): if room_id not in self.disabled_plugins: self.disabled_plugins[room_id] = [] diff --git a/plugins/config.py b/plugins/config.py index 8b70182..869bab5 100644 --- a/plugins/config.py +++ b/plugins/config.py @@ -2,6 +2,14 @@ Custom configuration class for the Funguy bot. Security‑hardened: only the configured admin user can read or change settings. Save operation preserves extra sections (plugins.disabled, etc.). +Commands: + !set