diff --git a/funguy.conf b/funguy.conf
index f0e9fb1..9afb5c1 100644
--- a/funguy.conf
+++ b/funguy.conf
@@ -14,3 +14,4 @@ config_file = "funguy.conf"
[plugins.disabled]
"!uFhErnfpYhhlauJsNK:matrix.org" = [ "youtube-preview", "ai", "proxy",]
"!vYcfWXpPvxeQvhlFdV:matrix.org" = []
+"!NXdVjDXPxXowPkrJJY:matrix.org" = [ "karma",]
diff --git a/funguy.py b/funguy.py
index 37842a3..0b5802a 100755
--- a/funguy.py
+++ b/funguy.py
@@ -17,6 +17,11 @@ import toml # Library for parsing TOML configuration files
# Importing FunguyConfig class from plugins.config module
from plugins.config import FunguyConfig
+# Whitelist of allowed plugins to prevent arbitrary code execution
+ALLOWED_PLUGINS = {'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma',
+ 'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion',
+ 'xkcd', 'youtube-preview', 'youtube-search'}
+
class FunguyBot:
"""
A bot class for managing plugins and handling commands in a Matrix chat environment.
@@ -78,17 +83,22 @@ class FunguyBot:
"""
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
+ # Iterating through whitelisted plugins only
+ for plugin_name in ALLOWED_PLUGINS:
+ plugin_file = os.path.join(self.PLUGINS_DIR, f"{plugin_name}.py")
+
+ # Verify that the plugin file exists
+ if not os.path.isfile(plugin_file):
+ logging.warning(f"Plugin file not found: {plugin_file}, skipping")
+ continue
+
+ try:
+ # Importing plugin module dynamically with validated plugin name
+ 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):
"""
@@ -233,3 +243,10 @@ class FunguyBot:
if __name__ == "__main__":
bot = FunguyBot() # Creating instance of FunguyBot
bot.run() # Running the bot
+
+ from plugins import cron # Import your cron plugin
+
+ # After bot starts running, periodically check for cron jobs
+ while True:
+ asyncio.sleep(60) # Check every minute (adjust as needed)
+ cron.run_cron_jobs(bot) # Check and execute cron jobs
diff --git a/plugins/ai.py b/plugins/ai.py
index 71fd023..72f2c18 100644
--- a/plugins/ai.py
+++ b/plugins/ai.py
@@ -61,7 +61,7 @@ async def handle_ai_command(room, bot, command, args, config):
data = {
"prompt": f"[INST]{config[command]['prompt']}{prompt}[/INST]",
- "max_tokens": 1024,
+ "max_tokens": 4096,
"temperature": config[command]["temperature"],
"top_p": config[command]["top_p"],
"top_k": config[command]["top_k"],
@@ -72,7 +72,7 @@ async def handle_ai_command(room, bot, command, args, config):
# Make HTTP request to the API endpoint
try:
- response = requests.post(url, headers=headers, json=data, verify=False)
+ response = requests.post(url, headers=headers, json=data, verify=False, timeout=300)
response.raise_for_status() # Raise HTTPError for bad responses
payload = response.json()
new_text = payload['choices'][0]['text']
diff --git a/plugins/cron.py b/plugins/cron.py
new file mode 100644
index 0000000..3f224f7
--- /dev/null
+++ b/plugins/cron.py
@@ -0,0 +1,64 @@
+# plugins/cron.py
+
+import sqlite3
+from crontab import CronTab
+import simplematrixbotlib as botlib
+
+# Database connection and cursor
+conn = sqlite3.connect('cron.db')
+cursor = conn.cursor()
+
+# Create table if not exists
+cursor.execute('''CREATE TABLE IF NOT EXISTS cron (
+ room_id TEXT,
+ cron_entry TEXT,
+ command TEXT
+ )''')
+conn.commit()
+
+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("cron"):
+ args = match.args()
+ if len(args) >= 4:
+ action = args[0]
+ room_id = args[1]
+ cron_entry = ' '.join(args[2:-1])
+ command = args[-1]
+ if action == "add":
+ add_cron(room_id, cron_entry, command)
+ await bot.api.send_text_message(room.room_id, f"Cron added successfully")
+ elif action == "remove":
+ remove_cron(room_id, command)
+ await bot.api.send_text_message(room.room_id, f"Cron removed successfully")
+ else:
+ await bot.api.send_text_message(room.room_id, "Usage: !cron add|remove room_id cron_entry command")
+
+def add_cron(room_id, cron_entry, command):
+ # Check if the cron entry already exists in the database for the given room_id and command
+ cursor.execute('SELECT * FROM cron WHERE room_id=? AND command=? AND cron_entry=?', (room_id, command, cron_entry))
+ existing_entry = cursor.fetchone()
+ if existing_entry:
+ return # Cron entry already exists, do not add duplicate
+
+ # Insert the cron entry into the database
+ cursor.execute('INSERT INTO cron (room_id, cron_entry, command) VALUES (?, ?, ?)', (room_id, cron_entry, command))
+ conn.commit()
+
+def remove_cron(room_id, command):
+ cursor.execute('DELETE FROM cron WHERE room_id=? AND command=?', (room_id, command))
+ conn.commit()
+
+async def run_cron_jobs(bot):
+ cron = CronTab()
+ for job in cron:
+ cron_entry = str(job)
+ for row in cursor.execute('SELECT * FROM cron WHERE cron_entry=?', (cron_entry,)):
+ room_id, _, command = row
+ room = await bot.api.get_room_by_id(room_id)
+ if room:
+ plugin_name = command.split()[0].replace(prefix, '') # Extract plugin name
+ plugin_module = bot.plugins.get(plugin_name)
+ if plugin_module:
+ await plugin_module.handle_command(room, None, bot, prefix, config)
+
diff --git a/plugins/help.py b/plugins/help.py
index 3bf6e46..7e0894e 100644
--- a/plugins/help.py
+++ b/plugins/help.py
@@ -65,6 +65,10 @@ async def handle_command(room, message, bot, prefix, config):
Disables a command. Use '!disable plugin room' to disable a specific command.
+Get random xkcd image.
+🧙♂️ Creator & Developer: Hash Borgir is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)
Join our Matrix Room: [Self-hosting | Security | Sysadmin | Homelab | Programming](https://matrix.to/#/#selfhosting:mozilla.org)
diff --git a/plugins/loadplugin.py b/plugins/loadplugin.py index fdaf95f..22dd1ab 100644 --- a/plugins/loadplugin.py +++ b/plugins/loadplugin.py @@ -11,6 +11,11 @@ import sys # Import sys module for unloading plugins # Dictionary to store loaded plugins PLUGINS = {} +# Whitelist of allowed plugins to prevent arbitrary code execution +ALLOWED_PLUGINS = {'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma', + 'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion', + 'xkcd', 'youtube-preview', 'youtube-search'} + async def load_plugin(plugin_name): """ Asynchronously loads a plugin. @@ -21,9 +26,46 @@ async def load_plugin(plugin_name): Returns: bool: True if the plugin is loaded successfully, False otherwise. """ + # Validate plugin name against whitelist + if plugin_name not in ALLOWED_PLUGINS: + logging.error(f"Plugin '{plugin_name}' is not whitelisted") + return False + + # Verify that the plugin file exists in the plugins directory + plugin_path = os.path.join("plugins", f"{plugin_name}.py") + if not os.path.isfile(plugin_path): + logging.error(f"Plugin file not found: {plugin_path}") + return False + try: - # Import the plugin module - module = importlib.import_module(f"plugins.{plugin_name}") + # Create a mapping of whitelisted plugins to their module paths + plugin_modules = { + 'ai': 'plugins.ai', + 'config': 'plugins.config', + 'cron': 'plugins.cron', + 'date': 'plugins.date', + 'fortune': 'plugins.fortune', + 'help': 'plugins.help', + 'isup': 'plugins.isup', + 'karma': 'plugins.karma', + 'loadplugin': 'plugins.loadplugin', + 'plugins': 'plugins.plugins', + 'proxy': 'plugins.proxy', + 'sd_text': 'plugins.sd_text', + 'stable-diffusion': 'plugins.stable-diffusion', + 'xkcd': 'plugins.xkcd', + 'youtube-preview': 'plugins.youtube-preview', + 'youtube-search': 'plugins.youtube-search', + } + + # Get the module path from the mapping + module_path = plugin_modules.get(plugin_name) + if not module_path: + logging.error(f"Plugin '{plugin_name}' not found in plugin mapping") + return False + + # Import the plugin module using the validated module path + module = importlib.import_module(module_path) # Add the plugin module to the PLUGINS dictionary PLUGINS[plugin_name] = module logging.info(f"Loaded plugin: {plugin_name}") @@ -112,4 +154,3 @@ async def handle_command(room, message, bot, prefix, config): else: # Send unauthorized message if the sender is not the admin await bot.api.send_text_message(room.room_id, "You are not authorized to unload plugins.") - diff --git a/plugins/proxy.py b/plugins/proxy.py index 546815a..e29b7ff 100644 --- a/plugins/proxy.py +++ b/plugins/proxy.py @@ -23,7 +23,7 @@ PROXY_LIST_FILENAME = 'socks5.txt' PROXY_LIST_EXPIRATION = timedelta(hours=8) MAX_THREADS = 128 PROXIES_DB_FILE = 'proxies.db' -MAX_PROXIES_IN_DB = 500 +MAX_PROXIES_IN_DB = 10 # Setup verbose logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') diff --git a/plugins/sd_text.py b/plugins/sd_text.py new file mode 100644 index 0000000..4352ebd --- /dev/null +++ b/plugins/sd_text.py @@ -0,0 +1,95 @@ +""" +Plugin for generating text using Ollama's Mistral 7B Instruct model and sending it to a Matrix chat room. +""" + +import requests +from asyncio import Queue +import simplematrixbotlib as botlib +import argparse + +# Queue to store pending commands +command_queue = Queue() + +API_URL = "http://localhost:11434/api/generate" +MODEL_NAME = "mistral:7b-instruct" + +async def process_command(room, message, bot, prefix, config): + """ + Queue and process !text commands sequentially. + """ + match = botlib.MessageMatch(room, message, bot, prefix) + if match.prefix() and match.command("text"): + if command_queue.empty(): + await handle_command(room, message, bot, prefix, config) + else: + await command_queue.put((room, message, bot, prefix, config)) + +async def handle_command(room, message, bot, prefix, config): + """ + Send the prompt to Ollama API and return the generated text. + """ + match = botlib.MessageMatch(room, message, bot, prefix) + if not (match.prefix() and match.command("text")): + return + + # Parse optional arguments + parser = argparse.ArgumentParser(description='Generate text using Ollama API') + parser.add_argument('--max_tokens', type=int, default=512, help='Maximum tokens to generate') + parser.add_argument('--temperature', type=float, default=0.7, help='Temperature for generation') + parser.add_argument('prompt', nargs='+', help='Prompt for the model') + + try: + args = parser.parse_args(message.body.split()[1:]) # Skip command itself + prompt = ' '.join(args.prompt).strip() + + if not prompt: + await bot.api.send_text_message(room.room_id, "Usage: !textGenerate text using Ollama's Mistral 7B Instruct model
+ +Usage:
+