diff --git a/README.md b/README.md index 2320567..54bebcd 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ The bot includes the following plugins: - **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 - **dnsdumpster.py**: DNSDumpster domain reconnaissance - **exploitdb.py**: Exploit-DB search @@ -118,6 +119,7 @@ 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 - **karma.py**: Room karma tracking system (display names only, no Matrix IDs) - **lastfm.py**: Last.fm integration - **loadplugin.py**: Load/unload plugins at runtime diff --git a/plugins/dictionary.py b/plugins/dictionary.py new file mode 100644 index 0000000..a1e637e --- /dev/null +++ b/plugins/dictionary.py @@ -0,0 +1,95 @@ +""" +Plugin for looking up word definitions using the system's dict command. +""" + +import subprocess +import logging +import simplematrixbotlib as botlib + +async def handle_command(room, message, bot, prefix, config): + """ + Function to handle the !define command. + + Args: + room (Room): The Matrix room where the command was invoked. + message (RoomMessage): The message object containing the command. + bot (Bot): The bot object. + prefix (str): The command prefix. + config (dict): Configuration parameters. + + Returns: + None + """ + match = botlib.MessageMatch(room, message, bot, prefix) + if match.is_not_from_this_bot() and match.prefix() and match.command("define"): + args = match.args() + if not args: + await bot.api.send_text_message(room.room_id, "Usage: !define ") + return + + word = ' '.join(args) + logging.info(f"Looking up definition for: {word}") + + try: + # Use the WordNet dictionary by default for concise definitions + result = subprocess.run( + ['/usr/bin/dict', '-d', 'wn', word], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0 and result.stdout.strip(): + # Clean up the output for better presentation + definition = result.stdout.strip() + # Remove extra newlines and clean up formatting + lines = [line for line in definition.split('\n') if line.strip()] + formatted_definition = '\n'.join(lines) + + # Send as markdown for better formatting + await bot.api.send_markdown_message(room.room_id, f"**Definition of '{word}':**\n```\n{formatted_definition}\n```") + elif result.stderr and "No definitions found" in result.stderr: + # Try with all dictionaries if WordNet doesn't have it + result_all = subprocess.run( + ['/usr/bin/dict', '-d', 'all', word], + capture_output=True, + text=True, + timeout=10 + ) + + if result_all.returncode == 0 and result_all.stdout.strip(): + definition = result_all.stdout.strip() + lines = [line for line in definition.split('\n') if line.strip()] + # Limit output to prevent overly long messages + if len(lines) > 20: + formatted_definition = '\n'.join(lines[:20]) + f"\n... (truncated, {len(lines)} total lines)" + else: + formatted_definition = '\n'.join(lines) + + await bot.api.send_markdown_message(room.room_id, f"**Definition of '{word}':**\n```\n{formatted_definition}\n```") + else: + await bot.api.send_text_message(room.room_id, f"No definition found for '{word}'") + else: + await bot.api.send_text_message(room.room_id, f"No definition found for '{word}'") + + except subprocess.TimeoutExpired: + await bot.api.send_text_message(room.room_id, "Sorry, the dictionary lookup timed out. Please try again.") + except Exception as e: + logging.error(f"Error looking up definition: {e}") + await bot.api.send_text_message(room.room_id, f"Error looking up definition for '{word}': {str(e)}") + + +# --------------------------------------------------------------------------- +# Plugin Metadata +# --------------------------------------------------------------------------- + +__version__ = "1.0.0" +__author__ = "Funguy Bot" +__description__ = "Look up word definitions using system dictionary" +__help__ = """ +
+!define – Word definitions +

Look up definitions of words using multiple dictionaries including WordNet, Webster's, and others.
+Usage: !define

+
+""" \ No newline at end of file diff --git a/plugins/infermatic-text.py b/plugins/infermatic-text.py index 348f3ab..7da65e4 100644 --- a/plugins/infermatic-text.py +++ b/plugins/infermatic-text.py @@ -9,6 +9,7 @@ import json import simplematrixbotlib as botlib from asyncio import Queue from dotenv import load_dotenv +import re # Load environment variables from .env file in the parent directory plugin_dir = os.path.dirname(os.path.abspath(__file__)) @@ -65,7 +66,7 @@ async def handle_command(room, message, bot, prefix, config): try: # Extract options manually since argparse doesn't handle mixed positional/optional well temperature = 0.9 - max_tokens = 2048 + max_tokens = 512 custom_model = None prompt_parts = [] @@ -174,10 +175,11 @@ async def list_models(room, bot): except Exception as e: await bot.api.send_text_message(room.room_id, f"❌ Unexpected error: {str(e)}") +import re # add at the top of the file + async def generate_text(room, bot, prompt, model, temperature, max_tokens): """Generate text using the Infermatic AI API.""" try: - # Send initial processing message await bot.api.send_text_message(room.room_id, f"📝 Generating text...") url = f"{INFERMATIC_API_BASE}/chat/completions" @@ -205,14 +207,13 @@ async def generate_text(room, bot, prompt, model, temperature, max_tokens): await bot.api.send_text_message(room.room_id, "No response generated.") return - # Format the output with collapsible sections - output = f"
📝 Generated Text (Click to expand)" - output += f"Model: {model}

" - output += f"Prompt: {prompt}

" - output += f"Response:

" - output += f"{generated_text}" - output += f"
" + # ---- Clean up blank lines that break list rendering ---- + # Remove blank lines directly before a list item (number‐dot or hyphen). + generated_text = re.sub(r'\n\n(\d+\.)', r'\n\1', generated_text) + generated_text = re.sub(r'\n\n(- )', r'\n\1', generated_text) + # Build a pure Markdown message (no HTML) + output = f"**Model:** `{model}`\n\n**Prompt:** {prompt}\n\n**Response:**\n\n{generated_text}" await bot.api.send_markdown_message(room.room_id, output) except requests.exceptions.Timeout: @@ -227,7 +228,6 @@ async def generate_text(room, bot, prompt, model, temperature, max_tokens): except Exception as e: await bot.api.send_text_message(room.room_id, f"❌ Error generating text: {str(e)}") finally: - # Process next queued command if not command_queue.empty(): next_command = await command_queue.get() await handle_command(*next_command) @@ -237,9 +237,9 @@ async def generate_text(room, bot, prompt, model, temperature, max_tokens): # Plugin Metadata # --------------------------------------------------------------------------- -__version__ = "1.0.0" +__version__ = "1.0.2" __author__ = "Funguy Bot" -__description__ = "AI text generation via Infermatic API" +__description__ = "AI text generation via Infermatic API (pure Markdown output)" __help__ = """
!text – AI text generation (Infermatic) @@ -248,7 +248,7 @@ __help__ = """
  • !text --list-models – List available models
  • !text --use-model <model> <prompt> – Specific model
  • --temperature <0.0-1.0> – Set creativity (default 0.9)
  • -
  • --max-tokens <number> – Max output length (default 2048)
  • +
  • --max-tokens <number> – Max output length (default 512)
  • Requires INFERMATIC_API env var.

    diff --git a/plugins/joke.py b/plugins/joke.py new file mode 100644 index 0000000..a1bf4a7 --- /dev/null +++ b/plugins/joke.py @@ -0,0 +1,93 @@ +""" +Plugin for fetching jokes from the Official Joke API. +""" +# plugins/joke.py + +import logging +import simplematrixbotlib as botlib +import aiohttp + +async def handle_command(room, message, bot, prefix, config): + """ + Function to handle the !joke command. + + Args: + room (Room): The Matrix room where the command was invoked. + message (RoomMessage): The message object containing the command. + bot (Bot): The bot object. + prefix (str): The command prefix. + config (dict): Configuration parameters. + + Returns: + None + """ + match = botlib.MessageMatch(room, message, bot, prefix) + + # Handle !joke command + if match.is_not_from_this_bot() and match.prefix() and match.command("joke"): + args = match.args() + + # Check if user wants a specific category + category = "general" + if args: + category = args[0].lower() + if category not in ["general", "programming"]: + # If invalid category, use general + category = "general" + + logging.info(f"Fetching {category} joke") + + try: + # Fetch joke from API + if category == "programming": + url = "https://official-joke-api.appspot.com/jokes/programming/random" + else: + url = "https://official-joke-api.appspot.com/random_joke" + + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data = await response.json() + + # Handle different response formats + if isinstance(data, list) and len(data) > 0: + joke = data[0] + elif isinstance(data, dict): + joke = data + else: + await bot.api.send_text_message(room.room_id, "Sorry, couldn't fetch a joke right now.") + return + + # Extract joke parts + setup = joke.get("setup", "No setup available") + punchline = joke.get("punchline", "No punchline available") + + # Send the joke with a delay for better effect + await bot.api.send_text_message(room.room_id, setup) + # Add a small delay before the punchline for comedic timing + import asyncio + await asyncio.sleep(2) + await bot.api.send_text_message(room.room_id, f"... {punchline}") + else: + await bot.api.send_text_message(room.room_id, "Sorry, couldn't fetch a joke right now. Try again later.") + + except Exception as e: + logging.error(f"Error fetching joke: {e}") + await bot.api.send_text_message(room.room_id, f"Error fetching joke: {str(e)}") + + +# --------------------------------------------------------------------------- +# Plugin Metadata +# --------------------------------------------------------------------------- + +__version__ = "1.0.0" +__author__ = "Funguy Bot" +__description__ = "Get random jokes from the Official Joke API" +__help__ = """ +
    +!joke – Random jokes +

    Get random jokes from the Official Joke API.
    +Usage: !joke for a general joke
    +Usage: !joke programming for a programming joke

    +
    +""" \ No newline at end of file