Refactor help plugin, and included metadata to all plugins

This commit is contained in:
2026-05-07 02:27:27 -05:00
parent abb4b5e245
commit dba205685b
38 changed files with 935 additions and 827 deletions
+5
View File
@@ -391,6 +391,11 @@ class FunguyBot:
self.setup_plugins() self.setup_plugins()
logging.info("✓ Plugin setup complete") logging.info("✓ Plugin setup complete")
# ----- NEW: Expose plugins dictionary on bot object -----
self.bot.plugins = self.PLUGINS
logging.info("✓ Plugin dictionary exposed on bot.plugins")
# --------------------------------------------------------
# Defining listener for message events # Defining listener for message events
@self.bot.listener.on_message_event @self.bot.listener.on_message_event
async def wrapper_handle_commands(room, message): async def wrapper_handle_commands(room, message):
+14
View File
@@ -320,3 +320,17 @@ def setup(bot):
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "Funguy Bot" __author__ = "Funguy Bot"
__description__ = "arXiv academic paper search" __description__ = "arXiv academic paper search"
__help__ = """
<details>
<summary><strong>!arxiv</strong> Search academic papers on arXiv</summary>
<ul>
<li><code>!arxiv &lt;query&gt;</code> Search papers (shows abstracts)</li>
<li><code>!arxiv list &lt;query&gt;</code> List without abstracts</li>
<li><code>!arxiv category &lt;category&gt;</code> Browse recent papers by category</li>
<li><code>!arxiv recent [category]</code> Most recent papers (7 days)</li>
<li><code>!arxiv random</code> Random paper</li>
<li><code>!arxiv &lt;id&gt;</code> Get paper by arXiv ID (e.g., 2101.00101)</li>
</ul>
<p><strong>Categories:</strong> ai, ml, security, crypto, cv, nlp, math, physics, quantum, bio, software</p>
</details>
"""
+15
View File
@@ -107,3 +107,18 @@ async def handle_command(room, message, bot, prefix, config):
"An unexpected error occurred while fetching Bitcoin price." "An unexpected error occurred while fetching Bitcoin price."
) )
logging.error(f"Unexpected error in Bitcoin plugin: {e}", exc_info=True) logging.error(f"Unexpected error in Bitcoin plugin: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Current Bitcoin price"
__help__ = """
<details>
<summary><strong>!btc</strong> Current Bitcoin price</summary>
<p>Fetches the latest BTC/USD price from bitcointicker.co.</p>
</details>
"""
+22
View File
@@ -152,3 +152,25 @@ async def handle_command(room, message, bot, prefix, config):
config.admin_user = "" config.admin_user = ""
config.prefix = "!" config.prefix = "!"
await bot.api.send_text_message(room.room_id, "Configuration reset") await bot.api.send_text_message(room.room_id, "Configuration reset")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Admin configuration commands"
__help__ = """
<details>
<summary><strong>Admin Config</strong> (!set, !get, !saveconf, …)</summary>
<ul>
<li><code>!set &lt;option&gt; &lt;value&gt;</code> Set admin_user/prefix</li>
<li><code>!get &lt;option&gt;</code> Display config value</li>
<li><code>!show</code> Show current settings</li>
<li><code>!saveconf</code> / <code>!loadconf</code> Save/load config</li>
<li><code>!rehash</code> Reload configuration</li>
</ul>
<p>Admin only.</p>
</details>
"""
+17
View File
@@ -62,3 +62,20 @@ async def run_cron_jobs(bot):
if plugin_module: if plugin_module:
await plugin_module.handle_command(room, None, bot, prefix, config) await plugin_module.handle_command(room, None, bot, prefix, config)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Cron job scheduler"
__help__ = """
<details>
<summary><strong>!cron</strong> Schedule commands via cron syntax</summary>
<ul>
<li><code>!cron add &lt;room_id&gt; &lt;cron_entry&gt; &lt;command&gt;</code> Add job</li>
<li><code>!cron remove &lt;room_id&gt; &lt;command&gt;</code> Remove job</li>
</ul>
<p>Admin only.</p>
</details>
"""
+14
View File
@@ -59,3 +59,17 @@ def get_ordinal_suffix(day_of_month):
return "nd" return "nd"
else: else:
return "th" return "th"
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Show current date and time"
__help__ = """
<details>
<summary><strong>!date</strong> Current date and time</summary>
<p>Displays the day of the week, ordinal date, and AM/PM time.</p>
</details>
"""
+28
View File
@@ -480,3 +480,31 @@ async def ddg_weather(room, bot, location):
) )
except Exception as e: except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error getting weather: {str(e)}") await bot.api.send_text_message(room.room_id, f"Error getting weather: {str(e)}")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "DuckDuckGo search"
__help__ = """
<details>
<summary><strong>!ddg</strong> DuckDuckGo search and instant answers</summary>
<ul>
<li><code>!ddg &lt;query&gt;</code> Instant answer (default)</li>
<li><code>!ddg search &lt;query&gt;</code> Web search results</li>
<li><code>!ddg instant &lt;query&gt;</code> Detailed instant answer</li>
<li><code>!ddg image &lt;query&gt;</code> Image search</li>
<li><code>!ddg news &lt;query&gt;</code> News search</li>
<li><code>!ddg video &lt;query&gt;</code> Video search</li>
<li><code>!ddg bang &lt;!bang query&gt;</code> Use DuckDuckGo bangs</li>
<li><code>!ddg define &lt;word&gt;</code> Word definition</li>
<li><code>!ddg calc &lt;expression&gt;</code> Calculator</li>
<li><code>!ddg weather [location]</code> Weather information</li>
<li><code>!ddg help</code> Show detailed help</li>
</ul>
<p>No API key required.</p>
</details>
"""
+15
View File
@@ -207,3 +207,18 @@ async def handle_command(room, message, bot, prefix, config):
f"An error occurred while performing DNS lookup: {str(e)}" f"An error occurred while performing DNS lookup: {str(e)}"
) )
logging.error(f"Error in DNS plugin for {domain}: {e}", exc_info=True) logging.error(f"Error in DNS plugin for {domain}: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "DNS reconnaissance"
__help__ = """
<details>
<summary><strong>!dns</strong> DNS reconnaissance</summary>
<p><code>!dns &lt;domain&gt;</code> Queries A, AAAA, MX, NS, TXT, CNAME, SOA, SRV, PTR records.</p>
</details>
"""
+19
View File
@@ -268,3 +268,22 @@ async def format_dnsdumpster_report(domain, data):
output = f"<details><summary><strong>🔍 DNSDumpster Report: {domain} (Click to expand)</strong></summary>{output}</details>" output = f"<details><summary><strong>🔍 DNSDumpster Report: {domain} (Click to expand)</strong></summary>{output}</details>"
return output return output
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "DNSDumpster domain reconnaissance"
__help__ = """
<details>
<summary><strong>!dnsdumpster</strong> Comprehensive DNS mapping via DNSDumpster</summary>
<ul>
<li><code>!dnsdumpster &lt;domain&gt;</code> Full recon (A, NS, MX, CNAME, TXT, etc.)</li>
<li><code>!dnsdumpster test</code> Test API connection</li>
</ul>
<p>Requires <strong>DNSDUMPSTER_KEY</strong> env var. Rate limit: 1 req/2 sec.</p>
</details>
"""
+16
View File
@@ -236,3 +236,19 @@ async def handle_command(room, message, bot, prefix, config):
f"An error occurred while searching Exploit-DB: {str(e)}" f"An error occurred while searching Exploit-DB: {str(e)}"
) )
logging.error(f"Error in exploitdb plugin: {e}", exc_info=True) logging.error(f"Error in exploitdb plugin: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Exploit-DB search"
__help__ = """
<details>
<summary><strong>!exploitdb</strong> Search Exploit Database</summary>
<p><code>!exploitdb &lt;search term&gt; [max_results]</code> Search for exploits (title, EDB-ID, type, platform, author, link).<br>
Example: <code>!exploitdb wordpress 5</code></p>
<p>Fetches from the official Exploit-DB CSV. Falls back to search links if unavailable.</p>
</details>
"""
+15
View File
@@ -24,3 +24,18 @@ async def handle_command(room, message, bot, prefix, config):
fortune_output = "🃏 " + subprocess.run(['/usr/games/fortune'], capture_output=True).stdout.decode('UTF-8') fortune_output = "🃏 " + subprocess.run(['/usr/games/fortune'], capture_output=True).stdout.decode('UTF-8')
await bot.api.send_markdown_message(room.room_id, fortune_output) await bot.api.send_markdown_message(room.room_id, fortune_output)
logging.info("Sent fortune to the room") logging.info("Sent fortune to the room")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Random fortune message"
__help__ = """
<details>
<summary><strong>!fortune</strong> Random fortune</summary>
<p>Runs the <code>/usr/games/fortune</code> utility and posts a random quote.</p>
</details>
"""
+19
View File
@@ -267,3 +267,22 @@ async def handle_command(room, message, bot, prefix, config):
f"An error occurred during geolocation lookup for {query}. Please try again later." f"An error occurred during geolocation lookup for {query}. Please try again later."
) )
logging.error(f"Error in geo plugin for {query}: {e}", exc_info=True) logging.error(f"Error in geo plugin for {query}: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "IP geolocation lookup"
__help__ = """
<details>
<summary><strong>!geo</strong> IP / domain geolocation</summary>
<ul>
<li><code>!geo &lt;ip&gt;</code> Locate an IP address</li>
<li><code>!geo &lt;domain&gt;</code> Resolves domain then locates</li>
</ul>
<p>Shows country, region, city, coordinates, ISP, ASN. Uses ip-api.com / ipapi.co.</p>
</details>
"""
+13
View File
@@ -388,3 +388,16 @@ def setup(bot):
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "Funguy Bot" __author__ = "Funguy Bot"
__description__ = "Hacker News integration" __description__ = "Hacker News integration"
__help__ = """
<details>
<summary><strong>!hn</strong> Hacker News stories</summary>
<ul>
<li><code>!hn</code> Top 5 stories</li>
<li><code>!hn top|new|best|ask|show|job</code> Story type</li>
<li><code>!hn story &lt;id&gt;</code> Story details</li>
<li><code>!hn comments &lt;id&gt;</code> Show comments</li>
<li><code>!hn search &lt;query&gt;</code> Search via Algolia</li>
</ul>
<p>Free, no API key needed.</p>
</details>
"""
+15
View File
@@ -384,3 +384,18 @@ async def handle_command(room, message, bot, prefix, config):
f"Error identifying hash: {str(e)}" f"Error identifying hash: {str(e)}"
) )
logging.error(f"Error in hashid command: {e}", exc_info=True) logging.error(f"Error in hashid command: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Hash type identifier"
__help__ = """
<details>
<summary><strong>!hashid</strong> Identify hash type</summary>
<p><code>!hashid &lt;hash&gt;</code> Recognises 100+ hash formats (MD5, SHA, bcrypt, etc.).<br>
Shows confidence level, Hashcat mode, and John the Ripper format.</p>
</details>
"""
+16
View File
@@ -385,3 +385,19 @@ async def format_header_analysis(results):
output = f"<details><summary><strong>🔒 Security Headers Analysis: {results['url']}</strong></summary>{output}</details>" output = f"<details><summary><strong>🔒 Security Headers Analysis: {results['url']}</strong></summary>{output}</details>"
return output return output
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "HTTP security header analysis"
__help__ = """
<details>
<summary><strong>!headers</strong> HTTP security header scanner</summary>
<p><code>!headers &lt;url&gt;</code> Checks HSTS, CSP, X-Frame-Options, etc.<br>
Provides security score (0-100) and recommendations. Also shows SSL certificate info.</p>
</details>
"""
+54 -652
View File
@@ -1,673 +1,75 @@
""" """
Plugin for providing a command to display the list of available commands and their descriptions. Plugin for dynamically aggregating help from all loaded plugins.
""" """
import logging import logging
import simplematrixbotlib as botlib import simplematrixbotlib as botlib
async def handle_command(room, message, bot, prefix, config): async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !help command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (MatrixBot): The Matrix bot instance.
prefix (str): The command prefix.
config (dict): The bot's configuration.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix) match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("help"): if not (match.is_not_from_this_bot() and match.prefix() and match.command("help")):
logging.info("Fetching command help documentation") return
commands_message = """
<details><summary><strong>🍄 Funguy Bot Commands 🍄</strong></summary>
<p>
<details><summary>📖 <strong>!help</strong></summary>
<p>Displays comprehensive help documentation for all available commands with usage examples.</p>
</details>
<details><summary>🔌 <strong>!plugins</strong></summary> args = match.args()
<p>Lists all loaded plugins along with their descriptions in alphabetical order.</p> plugins = getattr(bot, "plugins", {})
</details>
<details><summary>🃏 <strong>!fortune</strong></summary> if not plugins:
<p>Returns a random fortune message. Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room.</p> await bot.api.send_text_message(room.room_id, "No plugins are currently loaded.")
</details> return
<details><summary>⏰ <strong>!date</strong></summary> # If a specific plugin is requested
<p>Displays the current date and time. Fetches the current date and time using Python's `datetime` module and sends it in a formatted message with proper ordinal suffixes to the chat room.</p> if args:
</details> plugin_name = args[0].strip()
if plugin_name in plugins:
plugin = plugins[plugin_name]
help_html = getattr(plugin, "__help__", None)
if help_html:
await bot.api.send_markdown_message(room.room_id, help_html)
else:
# Fallback: docstring first line
doc = getattr(plugin, "__doc__", "No description available.")
first_line = doc.strip().split("\n")[0] if doc else "No description available."
await bot.api.send_text_message(
room.room_id, f"No detailed help for '{plugin_name}'. {first_line}"
)
return
else:
await bot.api.send_text_message(
room.room_id, f"Plugin '{plugin_name}' not found. Available: {', '.join(sorted(plugins.keys()))}"
)
return
<details><summary>💻 <strong>!proxy</strong></summary> # Aggregate help from all plugins
<p>Retrieves a tested/working random SOCKS5 proxy. Fetches a list of SOCKS5 proxies from public sources, tests their availability, and sends the first working proxy with latency information to the chat room. Caches working proxies for faster access.</p> help_parts = []
</details> for pname in sorted(plugins.keys()):
plugin = plugins[pname]
help_html = getattr(plugin, "__help__", None)
if help_html:
help_parts.append(help_html)
else:
# Minimal fallback using docstring
doc = getattr(plugin, "__doc__", "No description.")
first_line = doc.strip().split("\n")[0] if doc else "No description."
help_parts.append(
f"<details><summary><strong>!{pname}</strong></summary><p>{first_line}</p></details>"
)
<details><summary>📶 <strong>!isup [domain/ip]</strong></summary> # Append bot credits
<p>Checks if the specified domain or IP address is reachable. Performs DNS resolution and checks HTTP/HTTPS service availability. Reports successful DNS resolution and service status.</p> credits = """
</details> <details><summary><strong>🌟 Funguy Bot Credits</strong></summary>
<details><summary>☯ <strong>!karma [user]</strong></summary>
<p>Retrieves the karma points for the specified user. Retrieves the karma points for the specified user from a SQLite database and sends them as a message to the chat room.</p>
</details>
<details><summary>⇧ <strong>!karma [user] up</strong></summary>
<p>Increases the karma points for the specified user by 1. Increases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room. Users cannot modify their own karma.</p>
</details>
<details><summary>⇩ <strong>!karma [user] down</strong></summary>
<p>Decreases the karma points for the specified user by 1. Decreases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room. Users cannot modify their own karma.</p>
</details>
<details><summary>🌧️ <strong>!weather [location]</strong></summary>
<p>Fetches current weather information for any location using OpenWeatherMap API. Shows temperature (Celsius/Fahrenheit), conditions, humidity, wind speed, and weather emojis. Requires OPENWEATHER_API_KEY environment variable.</p>
</details>
<details><summary>📖 <strong>!ud [term] [index]</strong></summary>
<p>Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index number. Shows definition, example, author, votes, and permalink.</p>
</details>
<details><summary>🔍 <strong>!dns [domain]</strong></summary>
<p>Performs comprehensive DNS reconnaissance on a domain. Queries multiple DNS record types including A, AAAA, MX, NS, TXT, CNAME, SOA, and SRV records. Validates domain format and provides formatted results.</p>
</details>
<details><summary>💰 <strong>!btc</strong></summary>
<p>Fetches the current Bitcoin price in USD from bitcointicker.co API. Shows real-time BTC/USD price with proper formatting. Includes error handling for API timeouts and data parsing issues.</p>
</details>
<details><summary>🌐 <strong>!whois &lt;domain/ip&gt;</strong></summary>
<p>Perform comprehensive WHOIS lookups for domains and IP addresses. Retrieves registrar information, registration dates, name servers, and contact details from WHOIS databases.</p>
<p><strong>Usage:</strong></p>
<ul>
<li><code>!whois &lt;domain&gt;</code> - Query domain registration information</li>
<li><code>!whois &lt;ip&gt;</code> - Query IP address allocation details</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!whois example.com</code></li>
<li><code>!whois google.com</code></li>
<li><code>!whois 8.8.8.8</code></li>
<li><code>!whois 1.1.1.1</code></li>
</ul>
<p><strong>Output includes:</strong> Domain/IP information, registrar, WHOIS server, creation/expiration dates, name servers, and contact details.</p>
</details>
<details><summary>🔍 <strong>!subdomains <domain></strong></summary>
<p>Enumerate subdomains using SSL certificate transparency logs. Discovers associated subdomains by querying the CertSpotter API for SSL certificates issued for a domain.</p>
<p><strong>Usage:</strong></p>
<ul>
<li><code>!subdomains <domain></code> - Enumerate subdomains for any domain</li>
<li><code>!subdomains example.com</code> - Example subdomain enumeration</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
<li>Discovers subdomains through SSL certificate transparency logs</li>
<li>Uses the free CertSpotter API for enumeration</li>
<li>No rate limiting or API key required</li>
<li>Identifies subdomains through certificate SAN (Subject Alternative Name) enumeration</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!subdomains google.com</code></li>
<li><code>!subdomains github.com</code></li>
<li><code>!subdomains example.com</code></li>
</ul>
<p><em>Essential for reconnaissance and subdomain enumeration in penetration testing</em></p>
</details>
<details><summary>📍 <strong>!geo [ip/domain]</strong></summary>
<p>Perform IP geolocation lookups with detailed geographic information. Resolves domains to IP addresses and provides location data including country, region, city, coordinates, and ISP information.</p>
<p><strong>Usage:</strong></p>
<ul>
<li><code>!geo <ip_address></code> - Geolocate an IP address</li>
<li><code>!geo <domain></code> - Geolocate a domain (automatically resolves to IP)</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
<li>Uses ip-api.com as primary geolocation service with ipapi.co fallback</li>
<li>Automatic domain to IP resolution</li>
<li>Comprehensive geographic information</li>
<li>No API key required for basic usage</li>
<li>Supports both IPv4 and IPv6 addresses</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!geo 8.8.8.8</code></li>
<li><code>!geo example.com</code></li>
<li><code>!geo google.com</code></li>
</ul>
<p><strong>Information provided:</strong></p>
<ul>
<li>Country and country code</li>
<li>Region/State</li>
<li>City</li>
<li>Postal code</li>
<li>Latitude/Longitude coordinates</li>
<li>Timezone</li>
<li>ISP/Organization</li>
<li>Autonomous System Number (ASN)</li>
</ul>
<p><em>Essential for network reconnaissance and IP investigation</em></p>
</details>
<details><summary>🔍 <strong>!shodan [command] [query]</strong></summary>
<p>Shodan.io integration for security reconnaissance and threat intelligence.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!shodan ip &lt;ip_address&gt;</code> - Detailed IP information (services, ports, banners)</li>
<li><code>!shodan search &lt;query&gt;</code> - Search Shodan database with filters</li>
<li><code>!shodan host &lt;domain&gt;</code> - Host information and subdomain enumeration</li>
<li><code>!shodan count &lt;query&gt;</code> - Count results with geographic/organization breakdown</li>
<li><code>!shodan test</code> - Test API connection and debug queries</li>
</ul>
<p><strong>Search Examples:</strong></p>
<ul>
<li><code>!shodan search apache</code></li>
<li><code>!shodan search "port:22 country:US"</code></li>
<li><code>!shodan search "product:nginx"</code></li>
<li><code>!shodan search "net:192.168.1.0/24"</code></li>
<li><code>!shodan search "http.title:'admin'"</code></li>
</ul>
<p><strong>Common Filters:</strong> country, city, port, product, os, org, net, has_ssl, http.title</p>
<p><em>Requires SHODAN_KEY environment variable</em></p>
</details>
<details><summary>🌐 <strong>!dnsdumpster [domain]</strong></summary>
<p>Comprehensive DNS reconnaissance and attack surface mapping using DNSDumpster.com API.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!dnsdumpster &lt;domain&gt;</code> - Complete DNS reconnaissance for any domain</li>
<li><code>!dnsdumpster test</code> - Test API connection and key validity</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
<li>A Records - All IPv4 addresses with geographic and ASN information</li>
<li>NS Records - Complete name server information with IP locations</li>
<li>MX Records - All mail servers with geographic data</li>
<li>CNAME Records - Full alias chain mappings</li>
<li>TXT Records - All text records including SPF, DKIM, verification</li>
<li>Additional Records - AAAA, SRV, SOA, PTR records when available</li>
<li>Web Services - HTTP/HTTPS service detection with banner information</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!dnsdumpster google.com</code></li>
<li><code>!dnsdumpster github.com</code></li>
<li><code>!dnsdumpster example.com</code></li>
</ul>
<p><em>Requires DNSDUMPSTER_KEY environment variable</em><br>
<em>Rate Limit: 1 request per 2 seconds</em></p>
</details>
<details>
<summary><strong>💣 !exploitdb - Search Exploit Database</strong></summary>
<br>
<strong>Description:</strong><br>
Search Exploit-DB for security vulnerabilities and exploits. Returns detailed information about exploits including EDB-ID, type, platform, author, and direct links to exploit code.<br>
<br>
<strong>Usage:</strong><br>
<code>!exploitdb &lt;search_term&gt; [max_results]</code><br>
<br>
<strong>Parameters:</strong><br>
• <strong>search_term</strong> (required) - Software name, CVE number, or vulnerability type<br>
• <strong>max_results</strong> (optional) - Number of results to return (1-10, default: 5)<br>
<br>
<strong>Examples:</strong><br>
<code>!exploitdb wordpress</code> - Search for WordPress exploits<br>
<code>!exploitdb apache 3</code> - Get 3 Apache exploits<br>
<code>!exploitdb windows privilege escalation</code> - Search for Windows privesc<br>
<code>!exploitdb CVE-2021-44228</code> - Search by CVE number<br>
<code>!exploitdb linux kernel 10</code> - Get 10 Linux kernel exploits<br>
<code>!exploitdb sql injection</code> - Search for SQL injection exploits<br>
<br>
<strong>Output Includes:</strong><br>
• Exploit title and description<br>
• EDB-ID (Exploit Database ID)<br>
• Exploit type (webapps, local, remote, etc.)<br>
• Platform/OS (PHP, Linux, Windows, etc.)<br>
• Author name<br>
• Publication date<br>
• Direct link to full exploit code<br>
<br>
<strong>Notes:</strong><br>
• Searches the official Exploit-DB CSV database<br>
• May take a few seconds on first use (downloads database)<br>
• Falls back to search links if database unavailable<br>
<br>
<em>⚠️ Use responsibly and only on systems you have permission to test.</em>
</details>
<details><summary>🛡️ <strong>!headers &lt;url&gt;</strong></summary>
<p>Comprehensive HTTP security header analysis with security scoring and recommendations.</p>
<p><strong>Features:</strong></p>
<ul>
<li>Security scoring (0-100) with color-coded ratings</li>
<li>Critical security header validation and configuration checking</li>
<li>HTTP to HTTPS redirect chain analysis</li>
<li>SSL certificate information for HTTPS sites</li>
<li>Information disclosure header detection</li>
<li>Actionable security recommendations</li>
</ul>
<p><strong>Security Headers Analyzed:</strong></p>
<ul>
<li><code>Strict-Transport-Security</code> - HSTS enforcement</li>
<li><code>Content-Security-Policy</code> - XSS protection</li>
<li><code>X-Frame-Options</code> - Clickjacking protection</li>
<li><code>X-Content-Type-Options</code> - MIME sniffing prevention</li>
<li><code>Referrer-Policy</code> - Referrer control</li>
<li><code>Feature-Policy</code> - Browser feature restrictions</li>
<li>Server information headers</li>
</ul>
<p><strong>Security Ratings:</strong></p>
<ul>
<li>🟢 <strong>Excellent (80-100)</strong> - Strong configuration</li>
<li>🟡 <strong>Good (60-79)</strong> - Moderate, needs improvement</li>
<li>🟠 <strong>Fair (40-59)</strong> - Basic, significant improvements needed</li>
<li>🔴 <strong>Poor (0-39)</strong> - Weak configuration</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!headers example.com</code></li>
<li><code>!headers https://github.com</code></li>
<li><code>!headers localhost:3000</code></li>
<li><code>!headers subdomain.target.com</code></li>
</ul>
<p><em>Provides enterprise-grade security analysis for penetration testers and developers</em></p>
</details>
<details><summary>🔄 <strong>!hashid &lt;hash&gt;</strong></summary>
<p>Advanced hash type identification with confidence scoring and tool recommendations.</p>
<p><strong>Features:</strong></p>
<ul>
<li>100+ hash types including modern, legacy, and exotic algorithms</li>
<li>Color-coded confidence scoring (🟢 Very High to 🔴 Low)</li>
<li>Hashcat mode numbers and John the Ripper format names</li>
<li>Context-aware parsing for various hash formats</li>
</ul>
<p><strong>Supported Categories:</strong></p>
<ul>
<li><strong>Modern:</strong> yescrypt, scrypt, Argon2, bcrypt</li>
<li><strong>Unix/Linux:</strong> SHA-512/256 Crypt, MD5 Crypt, apr1</li>
<li><strong>Raw Hashes:</strong> MD5, SHA family, SHA-3, NTLM, LM</li>
<li><strong>Databases:</strong> MySQL, PostgreSQL, Oracle, MSSQL</li>
<li><strong>Web/CMS:</strong> WordPress, Drupal, phpBB3, Django</li>
<li><strong>LDAP:</strong> SSHA, SMD5, LDAP crypt formats</li>
<li><strong>Network:</strong> NetNTLMv1/v2, Kerberos</li>
<li><strong>Exotic:</strong> Whirlpool, RIPEMD, GOST, BLAKE2</li>
</ul>
<p><strong>Tool Integration:</strong></p>
<ul>
<li><strong>Hashcat:</strong> Mode numbers for <code>-m</code> parameter</li>
<li><strong>John:</strong> Format names for <code>--format=</code> parameter</li>
<li>Multi-tool compatibility</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!hashid 5d41402abc4b2a76b9719d911017c592</code> (MD5)</li>
<li><code>!hashid aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d</code> (SHA-1)</li>
<li><code>!hashid $6$rounds=5000$salt$hash...</code> (SHA-512 Crypt)</li>
<li><code>!hashid $y$j9T$...</code> (yescrypt - modern Linux)</li>
<li><code>!hashid 8846f7eaee8fb117ad06bdd830b7586c</code> (NTLM)</li>
<li><code>!hashid *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19</code> (MySQL)</li>
</ul>
<p><strong>Confidence Legend:</strong></p>
<ul>
<li>🟢 Very High (90-100%) - Single definitive match</li>
<li>🟡 High (80-89%) - Strong match with minor alternatives</li>
<li>🟠 Medium (60-79%) - Multiple plausible matches</li>
<li>🔴 Low (0-59%) - Uncertain, needs context</li>
</ul>
<p><em>Essential for penetration testers, forensic analysts, and password cracking</em></p>
</details>
<details><summary>🔐 <strong>!sslscan &lt;domain[:port]&gt;</strong></summary>
<p>Comprehensive SSL/TLS security scanning and analysis with vulnerability detection.</p>
<p><strong>Features:</strong></p>
<ul>
<li>TLS 1.0-1.3 protocol support testing with security scoring</li>
<li>Certificate chain validation, expiration, and signature analysis</li>
<li>25+ cipher suite testing with strength classification</li>
<li>Vulnerability detection (POODLE, weak ciphers, protocol issues)</li>
<li>0-100 security rating with color-coded assessment</li>
<li>PCI DSS and modern security standards compliance checking</li>
</ul>
<p><strong>Security Checks:</strong></p>
<ul>
<li><strong>Protocol Security:</strong> TLS 1.2/1.3 enforcement, insecure protocol detection</li>
<li><strong>Certificate Health:</strong> Expiration monitoring, signature validation</li>
<li><strong>Cipher Security:</strong> RC4, DES, 3DES, NULL cipher detection</li>
<li><strong>Modern Standards:</strong> Forward Secrecy, strong encryption</li>
</ul>
<p><strong>Security Ratings:</strong></p>
<ul>
<li>🟢 <strong>Excellent (90-100)</strong> - Modern TLS with strong security</li>
<li>🟡 <strong>Good (80-89)</strong> - Good security, minor improvements needed</li>
<li>🟠 <strong>Fair (60-79)</strong> - Moderate security, significant improvements</li>
<li>🔴 <strong>Poor (0-59)</strong> - Critical issues requiring immediate attention</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!sslscan example.com</code></li>
<li><code>!sslscan github.com:443</code></li>
<li><code>!sslscan localhost:8443</code></li>
<li><code>!sslscan 192.168.1.1:443</code></li>
</ul>
<p><em>Essential for security teams, system administrators, and developers ensuring TLS compliance</em><br>
<em>Note: SSLv2/SSLv3 testing limited by Python security features</em></p>
</details>
<details><summary>🎵 <strong>Last.fm Integration</strong></summary>
<p>Comprehensive Last.fm integration with 30+ commands for music analytics and social features.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!register <username></code> - Register your Last.fm username</li>
<li><code>!np [user]</code> - Show currently playing track</li>
<li><code>!recent [user] [limit]</code> - Show recent tracks (default 10, max 50)</li>
<li><code>!toptracks [user] [period]</code> - Show top tracks (overall/7day/1month/3month/6month/12month)</li>
<li><code>!topartists [user] [period]</code> - Show top artists</li>
<li><code>!topalbums [user] [period]</code> - Show top albums</li>
<li><code>!loved [user]</code> - Show recently loved tracks</li>
<li><code>!profile [user]</code> - Detailed user profile</li>
<li><code>!playcount [user]</code> - Total scrobbles</li>
<li><code>!scrobbles [user]</code> - Detailed scrobbling statistics</li>
<li><code>!compare <user1> <user2></code> - Compare musical tastes</li>
<li><code>!taste [user]</code> - Top artists with taste-o-meter</li>
<li><code>!friends [user]</code> - Show Last.fm friends</li>
<li><code>!recommend [user]</code> - Artist recommendations</li>
<li><code>!similar <artist></code> - Find similar artists</li>
<li><code>!tag <tag></code> - Top artists for a tag/genre</li>
<li><code>!charts</code> - Global top tracks chart</li>
<li><code>!tagcloud [user]</code> - Top genre tags</li>
<li><code>!now</code> - What are registered users playing?</li>
<li><code>!decades [user]</code> - Favorite decades analysis</li>
<li><code>!genres [user]</code> - Top genres/tags</li>
<li><code>!era <year></code> - Popular tracks from a year</li>
<li><code>!weekly [user]</code> - Weekly listening report</li>
<li><code>!monthly [user]</code> - Monthly listening report</li>
<li><code>!yearly [user] [year]</code> - Yearly listening report</li>
<li><code>!first <artist> [user]</code> - Find first scrobble of an artist</li>
<li><code>!concerts [user]</code> - Upcoming concerts for top artists</li>
<li><code>!radio <artist></code> - Generate playlist based on artist</li>
<li><code>!mashup <artist1> <artist2></code> - Musical connections between artists</li>
<li><code>!collage [user] [size]</code> - Top album art URLs</li>
<li><code>!listening [user]</code> - Currently listening with album art</li>
<li><code>!awards [user]</code> - Milestone achievements</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
<li>Register your Matrix ID with your Last.fm username</li>
<li>Display currently playing tracks with artist and album information</li>
<li>Compare musical tastes between users</li>
<li>Discover similar artists and genres</li>
<li>Get personalized artist recommendations</li>
<li>View detailed listening statistics and reports</li>
<li>Find upcoming concerts for your favorite artists</li>
<li>Generate playlists based on your musical preferences</li>
<li>View milestone achievements and listening habits</li>
<li>Uses SQLite database to store user associations</li>
<li>Requires LASTFM_API_KEY environment variable</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!register your_lastfm_username</code> - Register your Last.fm username</li>
<li><code>!np</code> - Show your currently playing track</li>
<li><code>!recent 20</code> - Show your 20 most recent tracks</li>
<li><code>!topartists 7day</code> - Show your top artists from the last 7 days</li>
<li><code>!compare user1 user2</code> - Compare musical tastes between two users</li>
<li><code>!similar radiohead</code> - Find artists similar to Radiohead</li>
<li><code>!tag electronic</code> - Show top electronic artists</li>
<li><code>!era 1994</code> - Show popular tracks from 1994</li>
<li><code>!radio metallica</code> - Generate a playlist based on Metallica</li>
<li><code>!mashup metallica megadeth</code> - Find musical connections between Metallica and Megadeth</li>
</ul>
<p><strong>Requirements:</strong></p>
<li>Last.fm account at last.fm</li>
<li>LASTFM_API_KEY in .env file</li>
<li>YOUTUBE_API_KEY in .env file (for YouTube integration)</li>
</ul>
</details>
<details><summary>📸 <strong>!sd [prompt]</strong></summary>
<p>Generates images using self-hosted Stable Diffusion. Supports options: --steps, --cfg, --h, --w, --neg, --sampler. Uses queuing system to handle multiple requests. See available options using just '!sd'.</p>
</details>
<details><summary>📄 <strong>!text [prompt]</strong></summary>
<p>Generates text using the Infermatic AI API. Supports multiple models, configurable parameters, and model listing. Uses queuing system for sequential processing.</p>
<p><strong>Usage:</strong></p>
<ul>
<li><code>!text &lt;prompt&gt;</code> - Generate text using the default model</li>
<li><code>!text --list-models</code> - List all available models from Infermatic AI</li>
<li><code>!text --use-model &lt;model_name&gt; &lt;prompt&gt;</code> - Use a specific model instead of the default</li>
<li><code>!text --temperature &lt;value&gt; &lt;prompt&gt;</code> - Set temperature (0.0-1.0, default: 0.9)</li>
<li><code>!text --max-tokens &lt;value&gt; &lt;prompt&gt;</code> - Set maximum tokens to generate (default: 2048)</li>
</ul>
<p><strong>Configuration:</strong></p>
<ul>
<li>Requires <code>INFERMATIC_API</code> environment variable set to your API key</li>
<li>Requires <code>INFERMATIC_MODEL</code> environment variable for default model (default: Sao10K-L3.1-70B-Hanami-x1)</li>
</ul>
<p><strong>Model Management:</strong></p>
<ul>
<li>Use <code>!text --list-models</code> to see all available models</li>
<li>Models support different capabilities and context lengths</li>
<li>Costs and token limits vary by model</li>
</ul>
<p><strong>Examples:</strong></p>
<ul>
<li><code>!text write a python function to calculate fibonacci</code></li>
<li><code>!text --list-models</code></li>
<li><code>!text --use-model llama-v3-8b-instruct explain quantum computing</code></li>
<li><code>!text --temperature 0.7 --max-tokens 500 write a haiku about AI</code></li>
</ul>
</details>
<details><summary>📰 <strong>!xkcd</strong></summary>
<p>Fetches and displays a random XKCD comic. Downloads comic image and sends it directly to the chat room.</p>
</details>
<details><summary>🎬 <strong>YouTube Features</strong></summary>
<p>Automatic preview when YouTube links are posted. Shows video info, description, and attempts to fetch lyrics. Also supports !yt [search terms] for direct YouTube searching.</p>
</details>
<details><summary>📘 <strong>!wp <search term></strong></summary>
<p>Fetches Wikipedia summaries and main images for search terms using MediaWiki APIs. No HTML scraping or BeautifulSoup required.<br>
<strong>Usage:</strong><br>
<code>!wp <search term></code> - Fetch Wikipedia summary for any search term<br>
<code>!wp help</code> - Show usage instructions<br>
<br>
<strong>Examples:</strong><br>
<code>!wp artificial intelligence</code><br>
<code>!wp machine learning</code><br>
<code>!wp python programming</code>
</p>
</details>
<details><summary>📘 <strong>!time <location></strong></summary>
<p>Fetches current time information for locations using the TimeAPI.io service.<br>
<strong>Usage:</strong><br>
<code>!time <location></code> - Get time for a location<br>
<code>!time help</code> - Show usage instructions<br>
<br>
<strong>Examples:</strong><br>
<code>!time London</code> - Get time in London<br>
<code>!time Tokyo</code> - Get time in Tokyo<br>
<code>!time New York</code> - Get time in New York<br>
<br>
<strong>Supported locations:</strong> Major cities worldwide including New York, London, Tokyo, Sydney, Paris, Berlin, etc.
</p>
</details>
<details><summary>📚 <strong>!arxiv [query]</strong></summary>
<p>Search academic papers on arXiv.org. Categories include AI, ML, Security, Physics, Math, and more. No API key required.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!arxiv <query></code> - Search for papers (shows abstracts)</li>
<li><code>!arxiv list <query></code> - List papers without abstracts</li>
<li><code>!arxiv category <category></code> - Browse recent papers by category</li>
<li><code>!ar0; 10px; color: #3b3a30; font-family: monospace; font-size: 12px; background: #f8f8f8; border: 1px solid #c8c8c8; border-radius: 3px; padding: 0 5px; }"
}
},
"code": {
"background": "white",
"color": "#3b3a30",
"font-family": "monospace",
"font-size": "12px",
"border": "1px solid #c8c8c8",
"border-radius": "3px",
"padding": "0 5px"
}
}
</style>
<div class="codeblock" style="white-space: pre-line; padding: 10px; border: 1px solid #c8c8c8; border-radius: 3px; background: #f8f8f8; color: #3b3a30; font-family: monospace; font-size: 12px;">
</style>
<div class="codeblock">
<summary>📚 <strong>!arxiv [query]</strong></summary>
<p>Search academic papers on arXiv.org. Categories include AI, ML, Security, Physics, Math, and more. No API key required.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!arxiv <query></code> - Search for papers (shows abstracts)</li>
<li><code>!arxiv list <query></code> - List papers without abstracts</li>
<li><code>!arxiv category <category></code> - Browse recent papers by category</li>
<li><code>!arxiv recent <category></code> - Most recent papers in category</li>
<li><code>!arxiv random</code> - Get a random paper</li>
<li><code>!arxiv <id></code> - Get paper by arXiv ID (e.g., 2101.00101)</li>
</ul>
<p><strong>Categories:</strong> ai, ml, security, crypto, cv, nlp, math, physics, quantum, bio, software</p>
</details>
<details><summary>📰 <strong>!news [category/query]</strong></summary>
<p>Fetch latest headlines from various news categories using GNews API. Requires GNEWS_API_KEY environment variable.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!news</code> - Get top headlines (default)</li>
<li><code>!news top</code> - Top headlines</li>
<li><code>!news world</code> - World news</li>
<li><code>!news tech</code> - Technology news</li>
<li><code>!news business</code> - Business news</li>
<li><code>!news science</code> - Science news</li>
<li><code>!news health</code> - Health news</li>
<li><code>!news crypto</code> - Cryptocurrency news</li>
<li><code>!news search <query></code> - Search for specific news</li>
</ul>
</details>
<details><summary>🔥 <strong>!hn [command]</strong></summary>
<p>Fetch top stories from Hacker News using Firebase API. No API key required.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!hn</code> - Show top 5 stories (default)</li>
<li><code>!hn top</code> - Top stories</li>
<li><code>!hn new</code> - Newest stories</li>
<li><code>!hn best</code> - Best stories</li>
<li><code>!hn ask</code> - Ask HN threads</li>
<li><code>!hn show</code> - Show HN posts</li>
<li><code>!hn job</code> - Job postings</li>
<li><code>!hn story <id></code> - Get details of a specific story</li>
<li><code>!hn comments <id></code> - Show comments for a story</li>
<li><code>!hn search <query></code> - Search stories (via Algolia)</li>
</ul>
</details>
<details><summary>☯ <strong>!karma [user]</strong></summary>
<p>Track karma points for users with leaderboards and statistics. Supports display names and Matrix IDs.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!karma <user></code> - Show karma for a user</li>
<li><code>!karma++ <user></code> - Give +1 karma to a user</li>
<li><code>!karma-- <user></code> - Give -1 karma to a user</li>
<li><code>!karma top [n]</code> - Show top karma entries</li>
<li><code>!karma bottom [n]</code> - Show bottom karma entries</li>
<li><code>!karma rank <user></code> - Show rank of user</li>
<li><code>!karma stats</code> - Show overall statistics</li>
<li><code>!karma history <user></code> - Show recent karma history</li>
<li><code>!++ <user></code> - Shortcut for !karma++</li>
<li><code>!-- <user></code> - Shortcut for !karma--</li>
<li><code><user>++</code> - Inline karma (message contains ++)</li>
<li><code><user>--</code> - Inline karma (message contains --)</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
<li>Supports display names and Matrix IDs</li>
<li>Room-specific karma tracking</li>
<li>Rate limiting to prevent spam</li>
<li>Karma history tracking</li>
<li>Leaderboards and statistics</li>
</ul>
</details>
<details><summary>🔥 <strong>!hn [command]</strong></summary>
<p>Fetch top stories from Hacker News using Firebase API. No API key required.</p>
<p><strong>Commands:</strong></p>
<ul>
<li><code>!hn</code> - Show top 5 stories (default)</li>
<li><code>!hn top</code> - Top stories</li>
<li><code>!hn new</code> - Newest stories</li>
<li><code>!hn best</code> - Best stories</li>
<li><code>!hn ask</code> - Ask HN threads</li>
<li><code>!hn show</code> - Show HN posts</li>
<li><code>!hn job</code> - Job postings</li>
<li><code>!hn story <id></code> - Get details of a specific story</li>
<li><code>!hn comments <id></code> - Show comments for a story</li>
<li><code>!hn search <query></code> - Search stories (via Algolia)</li>
</ul>
</details>
<details><summary>⏱️ <strong>!cron [add|remove] [room_id] [cron_entry] [command]</strong></summary>
<p>Schedule automated commands using cron syntax. Add or remove cron jobs for specific rooms and commands.</p>
</details>
<details><summary>🔧 <strong>Admin Commands</strong></summary>
<p>
<strong>!set [option] [value]</strong> - Set configuration options (admin_user, prefix)<br>
<strong>!get [option]</strong> - Get configuration values<br>
<strong>!saveconf</strong> - Save current configuration<br>
<strong>!loadconf</strong> - Load saved configuration<br>
<strong>!show</strong> - Display current configuration<br>
<strong>!reset</strong> - Reset configuration to defaults<br>
<strong>!load [plugin]</strong> - Load a plugin<br>
<strong>!unload [plugin]</strong> - Unload a plugin<br>
<strong>!reload</strong> - Reload all plugins<br>
<strong>!disable [plugin] [room_id]</strong> - Disable a plugin for specific room<br>
<strong>!enable [plugin] [room_id]</strong> - Enable a plugin for specific room<br>
<strong>!rehash</strong> - Reload configuration<br>
<em>Note: Admin commands require admin_user privileges</em>
</p>
</details>
</p>
</details>
</details>
<!--<details><summary><strong>🤖 Funguy Bot AI Commands</strong></summary>
<p>
<strong>Creative & Writing</strong>: !write, !script, !author, !poem, !rap, !story, !comic, !motiv, !debate, !crit, !litcrit<br>
<strong>Technical</strong>: !tech, !dev, !py, !php, !regex, !math, !web, !it, !security, !ai, !ml, !data, !game, !gaming<br>
<strong>Professional</strong>: !seo, !recruit, !coach, !devrel, !sales, !ceo, !mgmt, !startup, !invest, !fin, !acad<br>
<strong>Educational</strong>: !tutor, !teach, !edu, !hist, !astro, !chem, !psych, !meditate, !socrat, !philos<br>
<strong>Lifestyle</strong>: !fit, !health, !diet, !cook, !travel, !art, !music, !film, !selfhelp<br>
<strong>Specialized</strong>: !legal, !medical, !realest, !auto, !fashion, !design, !interior, !florist<br>
<strong>Communication</strong>: !pron, !spk, !speak, !eloc, !comm, !msg, !langdet<br>
<strong>Business</strong>: !eth, !browse, !search, !create, !review, !curation, !domain<br>
<strong>Entertainment</strong>: !char, !adv, !advgame, !esc, !title, !stats, !prompt<br>
<strong>Technical Specialties</strong>: !intv, !plag, !trv, !foot, !rel, !etymo, !magic, !counsel, !behavior, !mh, !log, !dental, !acc, !chef, !tea, !telemed, !law, !trans, !chess, !time, !dream, !r, !emergency, !worksheet, !test, !create, !guide, !diag, !therapy, !gen, !drunk, !rec, !techtrans, !proof, !spirit, !friend, !chat, !wiki, !kanji, !note, !enhance, !nav, !hypno, !critic, !comp, !journo, !pscoach, !makeup, !childcare, !writing, !syn, !shop, !dining<br>
<em>Each AI command uses specialized prompts optimized for different domains and interfaces with local AI models. Consult ai.json</em>
</p>
</details>-->
<details><summary>🌟 <strong>Funguy Bot Credits</strong></summary>
<p> <p>
<strong>🧙‍♂️ Creator & Developer</strong>: HB is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)<br> <strong>🧙‍♂️ Creator & Developer</strong>: HB is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)<br>
<strong>🚀 Development Context</strong>: Created during recovery from two-level cervical spinal surgery (CDA Cervical Discectomy and Disc Arthroplasty)<br> <strong>🚀 Development Context</strong>: Created during recovery from two-level cervical spinal surgery (CDA Cervical Discectomy and Disc Arthroplasty)<br>
<br> <br>
<strong>Join our Matrix Room</strong>: <a href="https://matrix.to/#/#selfhosting:mozilla.org">Self-hosting | Security | Sysadmin | Homelab | Programming</a> <strong>Join our Matrix Room</strong>: <a href="https://matrix.to/#/#selfhosting:mozilla.org">Selfhosting | Security | Sysadmin | Homelab | Programming</a>
</p> </p>
</details> </details>
""" """
help_parts.append(credits)
await bot.api.send_markdown_message(room.room_id, commands_message) master = "<details><summary><strong>🍄 Funguy Bot Commands</strong> (click to expand)</summary><br>"
logging.info("Sent help documentation to the room") master += "\n".join(help_parts)
master += "</details>"
await bot.api.send_markdown_message(room.room_id, master)
logging.info("Sent dynamic help from %d plugins", len(plugins))
+21
View File
@@ -388,3 +388,24 @@ async def handle_episode(room, bot, args):
await bot.api.send_image_message(room_id=room.room_id, image_filepath=poster_path) await bot.api.send_image_message(room_id=room.room_id, image_filepath=poster_path)
finally: finally:
os.unlink(poster_path) os.unlink(poster_path)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "IMDb lookup via OMDb API"
__help__ = """
<details>
<summary><strong>!imdb</strong> Movie/series details via OMDb</summary>
<ul>
<li><code>!imdb &lt;title&gt;</code> Full details (poster sent separately)</li>
<li><code>!imdb id &lt;tt1234567&gt;</code> Lookup by IMDb ID</li>
<li><code>!imdb search &lt;query&gt;</code> Search titles</li>
<li><code>!imdb episode &lt;series&gt; -s N -e N</code> Episode info</li>
</ul>
<p>Optional flags: <code>-y year</code>, <code>-t movie|series|episode</code>, <code>--short-plot</code><br>
Requires <strong>OMDB_API_KEY</strong> env var.</p>
</details>
"""
+22
View File
@@ -231,3 +231,25 @@ async def generate_text(room, bot, prompt, model, temperature, max_tokens):
if not command_queue.empty(): if not command_queue.empty():
next_command = await command_queue.get() next_command = await command_queue.get()
await handle_command(*next_command) await handle_command(*next_command)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "AI text generation via Infermatic API"
__help__ = """
<details>
<summary><strong>!text</strong> AI text generation (Infermatic)</summary>
<ul>
<li><code>!text &lt;prompt&gt;</code> Generate text using default model</li>
<li><code>!text --list-models</code> List available models</li>
<li><code>!text --use-model &lt;model&gt; &lt;prompt&gt;</code> Specific model</li>
<li><code>--temperature &lt;0.0-1.0&gt;</code> Set creativity (default 0.9)</li>
<li><code>--max-tokens &lt;number&gt;</code> Max output length (default 2048)</li>
</ul>
<p>Requires <strong>INFERMATIC_API</strong> env var.</p>
</details>
"""
+13
View File
@@ -100,3 +100,16 @@ async def handle_command(room, message, bot, prefix, config):
await bot.api.send_markdown_message(room.room_id, f"😕 **{target}** HTTP/HTTPS services are down") await bot.api.send_markdown_message(room.room_id, f"😕 **{target}** HTTP/HTTPS services are down")
logging.info(f"{target} HTTP/HTTPS services are down") logging.info(f"{target} HTTP/HTTPS services are down")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Check if a site is up"
__help__ = """
<details>
<summary><strong>!isup</strong> Is it up?</summary>
<p><code>!isup &lt;domain or IP&gt;</code> Performs DNS resolution and checks HTTP/HTTPS availability.</p>
</details>
"""
+36 -3
View File
@@ -54,6 +54,14 @@ display_name_cache = {}
cache_timestamp = {} cache_timestamp = {}
# ---------------------------------------------------------------------------
# Helper: pluralize "point" vs "points"
# ---------------------------------------------------------------------------
def pluralize_points(amount):
"""Return 'point' if amount is 1 or -1, else 'points'."""
return "point" if abs(amount) == 1 else "points"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Database Setup with Room Support # Database Setup with Room Support
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -625,7 +633,8 @@ async def handle_karma_command(room, message, bot, config):
response = f"🏆 **Top {len(leaderboard)} Karma Leaders**\n\n" response = f"🏆 **Top {len(leaderboard)} Karma Leaders**\n\n"
for i, entry in enumerate(leaderboard, 1): for i, entry in enumerate(leaderboard, 1):
medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else "📌" medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else "📌"
response += f"{medal} **{i}.** {entry['display_name']}: {entry['points']} points\n" pts = entry['points']
response += f"{medal} **{i}.** {entry['display_name']}: {pts} {pluralize_points(pts)}\n"
await bot.api.send_markdown_message(room.room_id, response) await bot.api.send_markdown_message(room.room_id, response)
else: else:
await bot.api.send_markdown_message(room.room_id, "No karma entries found in this room.") await bot.api.send_markdown_message(room.room_id, "No karma entries found in this room.")
@@ -641,7 +650,8 @@ async def handle_karma_command(room, message, bot, config):
if leaderboard: if leaderboard:
response = f"📉 **Bottom {len(leaderboard)} Karma (Needs Love)**\n\n" response = f"📉 **Bottom {len(leaderboard)} Karma (Needs Love)**\n\n"
for i, entry in enumerate(leaderboard, 1): for i, entry in enumerate(leaderboard, 1):
response += f"⚠️ **{i}.** {entry['display_name']}: {entry['points']} points\n" pts = entry['points']
response += f"⚠️ **{i}.** {entry['display_name']}: {pts} {pluralize_points(pts)}\n"
await bot.api.send_markdown_message(room.room_id, response) await bot.api.send_markdown_message(room.room_id, response)
else: else:
await bot.api.send_markdown_message(room.room_id, "No karma entries found in this room.") await bot.api.send_markdown_message(room.room_id, "No karma entries found in this room.")
@@ -661,7 +671,7 @@ async def handle_karma_command(room, message, bot, config):
points = get_karma(room_id, user_id) points = get_karma(room_id, user_id)
percentile = round((1 - rank/total) * 100, 1) if total > 0 else 0 percentile = round((1 - rank/total) * 100, 1) if total > 0 else 0
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id) display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
response = f"📊 **{display_name_resolved}** is ranked #{rank} out of {total} (top {percentile}%)\n💗 Karma: {points} points" response = f"📊 **{display_name_resolved}** is ranked #{rank} out of {total} (top {percentile}%)\n💗 Karma: {points} {pluralize_points(points)}"
await bot.api.send_markdown_message(room.room_id, response) await bot.api.send_markdown_message(room.room_id, response)
else: else:
display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id) display_name_resolved = get_display_name_from_user_id(bot, room_id, user_id)
@@ -801,3 +811,26 @@ def setup(bot):
logging.info("Supports display names (e.g., 'Nexilva' or '🍄 HB🍄') and Matrix IDs") logging.info("Supports display names (e.g., 'Nexilva' or '🍄 HB🍄') and Matrix IDs")
logging.info("Commands: !karma, !karma++, !karma--, !++, !--, and inline ++/--") logging.info("Commands: !karma, !karma++, !karma--, !++, !--, and inline ++/--")
logging.info("=" * 50) logging.info("=" * 50)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Room karma tracking system"
__help__ = """
<details>
<summary><strong>!karma</strong> Karma system</summary>
<ul>
<li><code>!karma &lt;user&gt;</code> Show points</li>
<li><code>!karma++ &lt;user&gt;</code> / <code>!-- &lt;user&gt;</code> Modify karma</li>
<li><code>!karma top [n]</code> / <code>!karma bottom [n]</code> Leaderboard</li>
<li><code>!karma rank &lt;user&gt;</code> Position</li>
<li><code>!karma stats</code> Room statistics</li>
<li><code>!karma history &lt;user&gt;</code> Recent votes</li>
</ul>
<p>Shortcuts: <code>!++ user</code>, <code>!-- user</code>, and inline <code>user++</code> / <code>user--</code>.</p>
</details>
"""
+28
View File
@@ -2019,3 +2019,31 @@ async def handle_command(room, message, bot, prefix, config):
await bot.api.send_text_message( await bot.api.send_text_message(
room.room_id, f"❌ Error processing !{command}: {str(e)}" room.room_id, f"❌ Error processing !{command}: {str(e)}"
) )
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Last.fm integration"
__help__ = """
<details>
<summary><strong>!lastfm</strong> Last.fm music stats (30+ commands)</summary>
<ul>
<li><code>!register &lt;username&gt;</code> Connect account</li>
<li><code>!np [user]</code> Now playing</li>
<li><code>!recent [user] [limit]</code> Recent tracks</li>
<li><code>!toptracks</code>, <code>!topartists</code>, <code>!topalbums</code></li>
<li><code>!loved</code>, <code>!profile</code>, <code>!playcount</code>, <code>!scrobbles</code></li>
<li><code>!compare &lt;user1&gt; &lt;user2&gt;</code> Taste comparison</li>
<li><code>!recommend</code>, <code>!similar &lt;artist&gt;</code>, <code>!tag &lt;genre&gt;</code></li>
<li><code>!charts</code>, <code>!now</code>, <code>!decades</code>, <code>!genres</code>, <code>!tagcloud</code></li>
<li><code>!era &lt;year&gt;</code>, <code>!weekly</code>, <code>!monthly</code>, <code>!yearly</code></li>
<li><code>!first &lt;artist&gt;</code>, <code>!concerts</code>, <code>!radio &lt;artist&gt;</code></li>
<li><code>!collage [user] [size]</code>, <code>!listening</code>, <code>!awards</code></li>
</ul>
<p>For full details: <code>!lastfm</code><br>Requires <strong>LASTFM_API_KEY</strong> env var.</p>
</details>
"""
+13
View File
@@ -113,3 +113,16 @@ async def handle_command(room, message, bot, prefix, config):
# Send unauthorized message if the sender is not the admin # 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.") await bot.api.send_text_message(room.room_id, "You are not authorized to unload plugins.")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Load/unload plugins at runtime"
__help__ = """
<details>
<summary><strong>Admin: !load / !unload</strong></summary>
<p><code>!load &lt;plugin&gt;</code> / <code>!unload &lt;plugin&gt;</code> Dynamically load or unload a plugin module. Admin only.</p>
</details>
"""
+12 -1
View File
@@ -231,4 +231,15 @@ async def handle_command(room, message, bot, prefix, config):
__version__ = "1.0.0" __version__ = "1.0.0"
__author__ = "Funguy Bot" __author__ = "Funguy Bot"
__description__ = "News aggregator using GNews API" __description__ = "News headlines via GNews API"
__help__ = """
<details>
<summary><strong>!news</strong> Latest news headlines</summary>
<ul>
<li><code>!news [top|world|tech|business|science|health|sports|crypto]</code></li>
<li><code>!news search &lt;query&gt;</code></li>
<li>You can append a number: <code>!news tech 8</code></li>
</ul>
<p>Requires <strong>GNEWS_API_KEY</strong> env var.</p>
</details>
"""
+22 -22
View File
@@ -10,49 +10,49 @@ import logging
import simplematrixbotlib as botlib import simplematrixbotlib as botlib
async def handle_command(room, message, bot, prefix, config): async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !plugins command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix) match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"): if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"):
logging.info("Received !plugins command") logging.info("Received !plugins command")
plugin_descriptions = get_plugin_descriptions() plugin_descriptions = get_plugin_descriptions()
# Prepend custom string before the output
plugin_descriptions.insert(0, "<details><summary><strong>🔌Plugins List🔌<br>⤵︎Click Here to Expand⤵︎</strong></summary>") plugin_descriptions.insert(0, "<details><summary><strong>🔌Plugins List🔌<br>⤵︎Click Here to Expand⤵︎</strong></summary>")
plugins_message = "<br>".join(plugin_descriptions) plugins_message = "<br>".join(plugin_descriptions)
plugins_message += "</details>" plugins_message += "</details>"
await bot.api.send_markdown_message(room.room_id, plugins_message) await bot.api.send_markdown_message(room.room_id, plugins_message)
logging.info("Sent plugin list to the room") logging.info("Sent plugin list to the room")
def get_plugin_descriptions(): def get_plugin_descriptions():
"""
Function to get descriptions of all loaded plugin modules.
Returns:
list: A list of plugin descriptions sorted alphabetically.
"""
plugin_descriptions = [] plugin_descriptions = []
for module_name, module in sys.modules.items(): for module_name, module in sys.modules.items():
if module_name.startswith("plugins.") and hasattr(module, "__file__"): if module_name.startswith("plugins.") and hasattr(module, "__file__"):
plugin_path = module.__file__ plugin_path = module.__file__
plugin_name = os.path.basename(plugin_path).split(".")[0] plugin_name = os.path.basename(plugin_path).split(".")[0]
try:
if hasattr(module, "__description__"):
description = module.__description__
elif module.__doc__:
description = module.__doc__.strip().split("\n")[0] description = module.__doc__.strip().split("\n")[0]
except AttributeError: else:
description = "No description available" description = "No description available"
plugin_descriptions.append(f"<strong>[{plugin_name}.py]:</strong> {description}") plugin_descriptions.append(f"<strong>[{plugin_name}.py]:</strong> {description}")
# Sort the plugin descriptions alphabetically by plugin name
plugin_descriptions.sort() plugin_descriptions.sort()
return plugin_descriptions return plugin_descriptions
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "List all loaded plugins"
__help__ = """
<details>
<summary><strong>!plugins</strong> List loaded plugins</summary>
<p>Displays all currently loaded plugins and their descriptions.</p>
</details>
"""
+13
View File
@@ -211,3 +211,16 @@ async def handle_command(room, message, bot, prefix, config):
await bot.api.send_markdown_message(room.room_id, "❌ Error handling !proxy command") await bot.api.send_markdown_message(room.room_id, "❌ Error handling !proxy command")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Working SOCKS5 proxy finder"
__help__ = """
<details>
<summary><strong>!proxy</strong> Random working SOCKS5 proxy</summary>
<p>Fetches, tests, and returns a random working SOCKS5 proxy with latency. Caches good proxies in SQLite.</p>
</details>
"""
+20
View File
@@ -328,3 +328,23 @@ async def handle_shodan_error(room, bot, status_code):
message = error_messages.get(status_code, f"Shodan API error: {status_code}") message = error_messages.get(status_code, f"Shodan API error: {status_code}")
await bot.api.send_text_message(room.room_id, message) await bot.api.send_text_message(room.room_id, message)
logging.error(f"Shodan API error: {status_code}") logging.error(f"Shodan API error: {status_code}")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Shodan.io reconnaissance"
__help__ = """
<details>
<summary><strong>!shodan</strong> Shodan search</summary>
<ul>
<li><code>!shodan ip &lt;ip&gt;</code> IP info with open ports</li>
<li><code>!shodan search &lt;query&gt;</code> Search internet devices</li>
<li><code>!shodan host &lt;domain&gt;</code> Host & subdomain enumeration</li>
<li><code>!shodan count &lt;query&gt;</code> Result counts</li>
</ul>
<p>Requires <strong>SHODAN_KEY</strong> env var.</p>
</details>
"""
+16
View File
@@ -592,3 +592,19 @@ def format_cert_date(date_str):
except: except:
pass pass
return date_str return date_str
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "SSL/TLS security scanner"
__help__ = """
<details>
<summary><strong>!sslscan</strong> SSL/TLS analysis</summary>
<p><code>!sslscan &lt;domain[:port]&gt;</code> Tests protocols, cipher suites, certificate validity, vulnerabilities.<br>
Provides a security score (0-100) and actionable recommendations.</p>
</details>
"""
+23
View File
@@ -185,3 +185,26 @@ def print_help():
<li>&lt;lora:al3xxl:1&gt; alexpainting, alexhuman, alexentity, alexthirdeye, alexforeheads, alexgalactic, spiraling, alexmirror, alexangel, alexkissing, alexthirdeye2, alexthirdeye3, alexhofmann, wearing, glasses, alexgalactic3, alexthirdeye4, alexhuman3, alexgalactic2, alexhuman2, alexbeing2, alexfractal2, alexfractal3</li> <li>&lt;lora:al3xxl:1&gt; alexpainting, alexhuman, alexentity, alexthirdeye, alexforeheads, alexgalactic, spiraling, alexmirror, alexangel, alexkissing, alexthirdeye2, alexthirdeye3, alexhofmann, wearing, glasses, alexgalactic3, alexthirdeye4, alexhuman3, alexgalactic2, alexhuman2, alexbeing2, alexfractal2, alexfractal3</li>
</ul> </ul>
""" """
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Stable Diffusion image generation"
__help__ = """
<details>
<summary><strong>!sd</strong> Generate images via Stable Diffusion</summary>
<p><code>!sd [options] &lt;prompt&gt;</code></p>
<ul>
<li><code>--steps N</code> Sampling steps (default 4)</li>
<li><code>--cfg scale</code> CFG scale (default 2)</li>
<li><code>--h H --w W</code> Image dimensions (default 512)</li>
<li><code>--neg &lt;negative prompt&gt;</code></li>
<li><code>--sampler SAMPLER</code> Sampler name (default DPM++ SDE)</li>
</ul>
<p>Requires a locally running Stable Diffusion API.</p>
</details>
"""
+14
View File
@@ -125,3 +125,17 @@ async def handle_command(room, message, bot, prefix, config):
f"An error occurred during subdomain enumeration for {domain}. Please try again later." f"An error occurred during subdomain enumeration for {domain}. Please try again later."
) )
logging.error(f"Error in subdomains plugin for {domain}: {e}", exc_info=True) logging.error(f"Error in subdomains plugin for {domain}: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Subdomain enumeration via CertSpotter"
__help__ = """
<details>
<summary><strong>!subdomains</strong> Enumerate subdomains</summary>
<p><code>!subdomains &lt;domain&gt;</code> Finds subdomains using SSL certificate transparency logs (CertSpotter API).</p>
</details>
"""
+13
View File
@@ -440,3 +440,16 @@ async def format_system_info(sysinfo):
return output return output
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "System information and monitoring"
__help__ = """
<details>
<summary><strong>!sysinfo</strong> System information</summary>
<p>Displays CPU, RAM, storage, network, Docker, GPU, sensors, and top processes.</p>
</details>
"""
+18
View File
@@ -190,3 +190,21 @@ async def handle_command(room, message, bot, prefix, config):
return return
await bot.api.send_markdown_message(room.room_id, format_response(data, display)) await bot.api.send_markdown_message(room.room_id, format_response(data, display))
logging.info(f"Time sent for {query}") logging.info(f"Time sent for {query}")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "World clock (no hardcoded cities)"
__help__ = """
<details>
<summary><strong>!time</strong> Current time for any city</summary>
<ul>
<li><code>!time &lt;city&gt;</code> Geocode any city (free Open-Meteo API)</li>
<li><code>!time &lt;IANA zone&gt;</code> e.g., <code>Europe/London</code></li>
</ul>
<p>Also shows current temperature if available.</p>
</details>
"""
+19
View File
@@ -191,3 +191,22 @@ async def handle_command(room, message, bot, prefix, config):
"An error occurred while processing the Urban Dictionary request." "An error occurred while processing the Urban Dictionary request."
) )
logging.error(f"Unexpected error in Urban Dictionary plugin: {e}", exc_info=True) logging.error(f"Unexpected error in Urban Dictionary plugin: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Urban Dictionary definitions"
__help__ = """
<details>
<summary><strong>!ud</strong> Urban Dictionary</summary>
<ul>
<li><code>!ud</code> Random definition</li>
<li><code>!ud &lt;term&gt;</code> Top definition</li>
<li><code>!ud &lt;term&gt; &lt;index&gt;</code> Nth definition</li>
</ul>
</details>
"""
+235 -123
View File
@@ -1,162 +1,274 @@
""" """
This plugin provides a command to get weather information for a location. Weather plugin primary: OpenWeatherMap, fallback: OpenMeteo.
Uses OpenWeatherMap when a valid API key is present and the request succeeds.
Falls back to OpenMeteo (no key required) otherwise.
Commands:
!weather <location> e.g. !weather London or !weather "New York,US"
""" """
import logging import logging
import requests
import os import os
import aiohttp
import simplematrixbotlib as botlib import simplematrixbotlib as botlib
from dotenv import load_dotenv from dotenv import load_dotenv
from urllib.parse import quote
# Load environment variables from .env file in the parent directory # ---------------------------------------------------------------------------
# Get the directory where this plugin file is located # Load .env (for OPENWEATHER_API_KEY)
# ---------------------------------------------------------------------------
plugin_dir = os.path.dirname(os.path.abspath(__file__)) plugin_dir = os.path.dirname(os.path.abspath(__file__))
# Get the parent directory (main bot directory)
parent_dir = os.path.dirname(plugin_dir) parent_dir = os.path.dirname(plugin_dir)
# Load .env from parent directory dotenv_path = os.path.join(parent_dir, ".env")
dotenv_path = os.path.join(parent_dir, '.env')
load_dotenv(dotenv_path) load_dotenv(dotenv_path)
# OpenWeatherMap API configuration OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
# Get your free API key from: https://openweathermap.org/api
WEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
WEATHER_API_URL = "https://api.openweathermap.org/data/2.5/weather"
async def handle_command(room, message, bot, prefix, config): # ---------------------------------------------------------------------------
""" # WMO codes → description + emoji (for OpenMeteo)
Function to handle the !weather command. # ---------------------------------------------------------------------------
WMO_CODES = {
0: ("Clear sky", "☀️"),
1: ("Mainly clear", "🌤️"),
2: ("Partly cloudy", ""),
3: ("Overcast", "☁️"),
45: ("Fog", "🌫️"),
48: ("Depositing rime fog", "🌫️"),
51: ("Light drizzle", "🌦️"),
53: ("Moderate drizzle", "🌦️"),
55: ("Dense drizzle", "🌧️"),
56: ("Light freezing drizzle", "🌧️"),
57: ("Dense freezing drizzle", "🌧️"),
61: ("Slight rain", "🌧️"),
63: ("Moderate rain", "🌧️"),
65: ("Heavy rain", "🌧️"),
66: ("Light freezing rain", "🌧️"),
67: ("Heavy freezing rain", "🌧️"),
71: ("Slight snow fall", "❄️"),
73: ("Moderate snow fall", "❄️"),
75: ("Heavy snow fall", "❄️"),
77: ("Snow grains", "❄️"),
80: ("Slight rain showers", "🌦️"),
81: ("Moderate rain showers", "🌧️"),
82: ("Violent rain showers", "🌧️"),
85: ("Slight snow showers", "🌨️"),
86: ("Heavy snow showers", "🌨️"),
95: ("Thunderstorm", "⛈️"),
96: ("Thunderstorm with slight hail", "⛈️"),
99: ("Thunderstorm with heavy hail", "⛈️"),
}
Args: # ---------------------------------------------------------------------------
room (Room): The Matrix room where the command was invoked. # Primary: OpenWeatherMap
message (RoomMessage): The message object containing the command. # ---------------------------------------------------------------------------
bot (Bot): The bot object. async def openweathermap_get(session: aiohttp.ClientSession, location: str) -> dict | None:
prefix (str): The command prefix. """Fetch current weather from OpenWeatherMap. Returns None on failure."""
config (dict): Configuration parameters. if not OPENWEATHER_API_KEY:
logging.info("OpenWeatherMap key missing, skipping primary")
return None
Returns: url = "https://api.openweathermap.org/data/2.5/weather"
None params = {
""" "q": location,
match = botlib.MessageMatch(room, message, bot, prefix) "appid": OPENWEATHER_API_KEY,
if match.is_not_from_this_bot() and match.prefix() and match.command("weather"): "units": "metric", # Celsius
logging.info("Received !weather command") }
try:
async with session.get(url, params=params, timeout=10) as resp:
if resp.status == 200:
return await resp.json()
logging.info(f"OpenWeatherMap HTTP {resp.status}, falling back")
except Exception as e:
logging.warning(f"OpenWeatherMap request error: {e}")
return None
# Check if API key is configured
if not WEATHER_API_KEY: def format_openweathermap(data: dict) -> str:
await bot.api.send_text_message( """Build the one-line weather message from OpenWeatherMap data."""
room.room_id, city = data.get("name", "Unknown")
"Weather API key not configured. Please set OPENWEATHER_API_KEY environment variable." sys_data = data.get("sys", {})
country = sys_data.get("country", "")
main_data = data.get("main", {})
temp_c = main_data.get("temp", 0)
temp_f = round(temp_c * 9 / 5 + 32, 1)
humidity = main_data.get("humidity", 0)
weather_list = data.get("weather", [])
description = weather_list[0]["description"].capitalize() if weather_list else "Unknown"
emoji = "🌡️"
if weather_list:
wmain = weather_list[0].get("main", "")
emoji = {
"Clear": "☀️", "Clouds": "☁️", "Rain": "🌧️", "Drizzle": "🌦️",
"Thunderstorm": "⛈️", "Snow": "❄️", "Mist": "🌫️", "Fog": "🌫️",
"Haze": "🌫️", "Smoke": "🌫️", "Dust": "🌫️", "Sand": "🌫️",
"Ash": "🌫️", "Squall": "💨", "Tornado": "🌪️",
}.get(wmain, "🌡️")
wind = data.get("wind", {}).get("speed", 0)
return (
f"<strong>[{emoji} Weather for {city}, {country}]</strong>: "
f"<strong>Condition:</strong> {description} | "
f"<strong>Temperature:</strong> {temp_c:.1f}°C ({temp_f:.1f}°F) | "
f"<strong>Humidity:</strong> {humidity}% | "
f"<strong>Wind Speed:</strong> {wind} m/s"
) )
# ---------------------------------------------------------------------------
# Fallback: OpenMeteo (no key, free)
# ---------------------------------------------------------------------------
async def meteo_geocode(session: aiohttp.ClientSession, location: str) -> dict | None:
"""Geocode a city name via OpenMeteo. Returns location info dict or None."""
url = "https://geocoding-api.open-meteo.com/v1/search"
params = {"name": location, "count": 1, "language": "en"}
try:
async with session.get(url, params=params, timeout=10) as resp:
if resp.status == 200:
data = await resp.json()
results = data.get("results", [])
if results:
r = results[0]
return {
"name": r["name"],
"latitude": r["latitude"],
"longitude": r["longitude"],
"country": r.get("country", ""),
"state": r.get("admin1", ""),
"timezone": r.get("timezone", "UTC"),
}
except Exception as e:
logging.warning(f"OpenMeteo geocode error: {e}")
return None
async def meteo_weather(session: aiohttp.ClientSession, lat: float, lon: float,
timezone: str = "auto") -> dict | None:
"""Fetch current weather from OpenMeteo. Returns JSON or None."""
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": lat,
"longitude": lon,
"current_weather": "true",
"temperature_unit": "fahrenheit",
"windspeed_unit": "mph",
"timezone": timezone,
}
try:
async with session.get(url, params=params, timeout=10) as resp:
if resp.status == 200:
return await resp.json()
except Exception as e:
logging.warning(f"OpenMeteo weather error: {e}")
return None
def format_meteo(loc_info: dict, weather_data: dict) -> str:
"""Format OpenMeteo result into the same oneline style."""
c = weather_data["current_weather"]
code = c["weathercode"]
desc, emoji = WMO_CODES.get(code, ("Unknown", "🌡️"))
city = loc_info["name"]
country = loc_info.get("country", "")
state = loc_info.get("state", "")
# Build location string
parts = [city]
if state and state != city:
parts.append(state)
if country:
parts.append(country)
loc_str = ", ".join(parts)
temp_f = c["temperature"]
temp_c = round((temp_f - 32) * 5 / 9, 1)
wind = c["windspeed"]
return (
f"<strong>[{emoji} Weather for {loc_str}]</strong>: "
f"<strong>Condition:</strong> {desc} | "
f"<strong>Temperature:</strong> {temp_c}°C ({temp_f}°F) | "
f"<strong>Wind Speed:</strong> {wind} mph"
)
# ---------------------------------------------------------------------------
# Plugin entry point
# ---------------------------------------------------------------------------
async def handle_command(room, message, bot, prefix, config):
match = botlib.MessageMatch(room, message, bot, prefix)
if not (match.is_not_from_this_bot() and match.prefix() and match.command("weather")):
return return
args = match.args() args = match.args()
if not args:
# Check if location was provided
if len(args) < 1:
await bot.api.send_text_message( await bot.api.send_text_message(
room.room_id, room.room_id,
"Usage: !weather <location>\nExample: !weather London or !weather New York,US" "Usage: !weather <location>\nExample: !weather London or !weather New York,US"
) )
logging.info("Sent usage message for !weather")
return return
location = ' '.join(args) location = " ".join(args)
logging.info("Received !weather command for '%s'", location)
try: async with aiohttp.ClientSession() as session:
# Make API request to OpenWeatherMap # 1. Try OpenWeatherMap
params = { owm_data = await openweathermap_get(session, location)
'q': location, if owm_data:
'appid': WEATHER_API_KEY, if owm_data.get("cod") == 200:
'units': 'metric' # Use metric units (Celsius) msg = format_openweathermap(owm_data)
} await bot.api.send_markdown_message(room.room_id, msg)
logging.info("Sent weather via OpenWeatherMap")
return
# OpenWeatherMap returned an error status inside JSON (e.g., 401, 404)
logging.info("OpenWeatherMap returned error code %s, falling back", owm_data.get("cod"))
response = requests.get(WEATHER_API_URL, params=params, timeout=10) # 2. Fallback: OpenMeteo
response.raise_for_status() logging.info("Falling back to OpenMeteo")
loc_info = await meteo_geocode(session, location)
weather_data = response.json() if not loc_info:
# Extract relevant weather information
city_name = weather_data['name']
country = weather_data['sys']['country']
temp = weather_data['main']['temp']
feels_like = weather_data['main']['feels_like']
humidity = weather_data['main']['humidity']
description = weather_data['weather'][0]['description'].capitalize()
wind_speed = weather_data['wind'].get('speed', 0)
# Convert temperature to Fahrenheit for display
temp_f = (temp * 9/5) + 32
feels_like_f = (feels_like * 9/5) + 32
# Get weather emoji based on condition
weather_emoji = get_weather_emoji(weather_data['weather'][0]['main'])
# Format the weather message
weather_message = f"""
<strong>[{weather_emoji} Weather for {city_name}, {country}]</strong>: <strong>Condition:</strong> {description} | <strong>Temperature:</strong> {temp:.1f}°C ({temp_f:.1f}°F) | <strong>Feels like:</strong> {feels_like:.1f}°C ({feels_like_f:.1f}°F) | <strong>Humidity:</strong> {humidity}% | <strong>Wind Speed:</strong> {wind_speed} m/s
""".strip()
await bot.api.send_markdown_message(room.room_id, weather_message)
logging.info(f"Sent weather information for {city_name}")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
await bot.api.send_text_message( await bot.api.send_text_message(
room.room_id, room.room_id,
f"Location '{location}' not found. Please check the spelling and try again." f"Location '{location}' not found."
) )
elif e.response.status_code == 401: return
wdata = await meteo_weather(session, loc_info["latitude"],
loc_info["longitude"],
loc_info.get("timezone", "auto"))
if not wdata:
await bot.api.send_text_message( await bot.api.send_text_message(
room.room_id, room.room_id,
"Weather API authentication failed. Please check the API key configuration." "Could not fetch weather data from any provider."
) )
else: return
await bot.api.send_text_message(
room.room_id,
f"Error fetching weather data: HTTP {e.response.status_code}"
)
logging.error(f"HTTP error fetching weather for '{location}': {e}")
except requests.exceptions.RequestException as e: msg = format_meteo(loc_info, wdata)
await bot.api.send_text_message( await bot.api.send_markdown_message(room.room_id, msg)
room.room_id, logging.info("Sent weather via OpenMeteo (fallback)")
f"Error connecting to weather service: {e}"
)
logging.error(f"Request error fetching weather for '{location}': {e}")
except (KeyError, ValueError) as e:
await bot.api.send_text_message(
room.room_id,
"Error parsing weather data. Please try again later."
)
logging.error(f"Error parsing weather data for '{location}': {e}")
def get_weather_emoji(condition): # ---------------------------------------------------------------------------
""" # Plugin setup
Get an emoji based on weather condition. # ---------------------------------------------------------------------------
def setup(bot):
logging.info("Weather plugin loaded (OpenWeatherMap + OpenMeteo fallback)")
Args:
condition (str): Weather condition from API.
Returns: # ---------------------------------------------------------------------------
str: Weather emoji. # Plugin Metadata
""" # ---------------------------------------------------------------------------
weather_emojis = { __version__ = "1.0.0"
'Clear': '☀️', __author__ = "Funguy Bot"
'Clouds': '☁️', __description__ = "Weather forecast (OWM primary, OpenMeteo fallback)"
'Rain': '🌧️', __help__ = """
'Drizzle': '🌦️', <details>
'Thunderstorm': '⛈️', <summary><strong>!weather</strong> Current weather</summary>
'Snow': '❄️', <p><code>!weather &lt;location&gt;</code> Shows temperature, conditions, humidity, wind.<br>
'Mist': '🌫️', Uses OpenWeatherMap if a valid API key is present; falls back to free OpenMeteo otherwise.</p>
'Fog': '🌫️', </details>
'Haze': '🌫️', """
'Smoke': '🌫️',
'Dust': '🌫️',
'Sand': '🌫️',
'Ash': '🌫️',
'Squall': '💨',
'Tornado': '🌪️'
}
return weather_emojis.get(condition, '🌡️')
+14
View File
@@ -168,3 +168,17 @@ async def handle_command(room, message, bot, prefix, config):
room.room_id, _build_welcome_message(display_name) room.room_id, _build_welcome_message(display_name)
) )
logging.info("Sent manual !welcome to %s", sender) logging.info("Sent manual !welcome to %s", sender)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Room welcome message"
__help__ = """
<details>
<summary><strong>!welcome</strong> Receive welcome message</summary>
<p>Manually triggers the room's welcome message for yourself.</p>
</details>
"""
+14
View File
@@ -203,3 +203,17 @@ async def handle_command(room, message, bot, prefix, config):
f"An unexpected error occurred during WHOIS lookup for {query}. Please try again later." f"An unexpected error occurred during WHOIS lookup for {query}. Please try again later."
) )
logging.error(f"Unexpected error in WHOIS plugin for {query}: {e}", exc_info=True) logging.error(f"Unexpected error in WHOIS plugin for {query}: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "WHOIS lookup"
__help__ = """
<details>
<summary><strong>!whois</strong> WHOIS lookup</summary>
<p><code>!whois &lt;domain or IP&gt;</code> Shows registrar, creation/expiry dates, nameservers, contacts.</p>
</details>
"""
+16
View File
@@ -280,3 +280,19 @@ async def handle_command(room, message, bot, prefix, config):
os.unlink(image_path) os.unlink(image_path)
except OSError: except OSError:
pass pass
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Wikipedia article summary"
__help__ = """
<details>
<summary><strong>!wp</strong> Wikipedia summary</summary>
<p><code>!wp &lt;search term&gt;</code> Returns the lead section and main image from Wikipedia.<br>
Uses MediaWiki APIs, no scraping.</p>
</details>
"""
+15
View File
@@ -43,3 +43,18 @@ async def handle_command(room, message, bot, prefix, config):
os.remove(image_path) os.remove(image_path)
except Exception as e: except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error fetching XKCD comic: {str(e)}") await bot.api.send_text_message(room.room_id, f"Error fetching XKCD comic: {str(e)}")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Random XKCD comic"
__help__ = """
<details>
<summary><strong>!xkcd</strong> Random XKCD comic</summary>
<p>Posts a random XKCD comic image.</p>
</details>
"""
+15
View File
@@ -71,3 +71,18 @@ async def send_collapsible_message(room, bot, content):
""" """
message = f'<details><summary><strong>🍄Funguy ▶YouTube Search🍄<br>⤵︎Click Here To See Results⤵︎</strong></summary>{content}</details>' message = f'<details><summary><strong>🍄Funguy ▶YouTube Search🍄<br>⤵︎Click Here To See Results⤵︎</strong></summary>{content}</details>'
await bot.api.send_markdown_message(room.room_id, message) await bot.api.send_markdown_message(room.room_id, message)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "YouTube video search"
__help__ = """
<details>
<summary><strong>!yt</strong> Search YouTube</summary>
<p><code>!yt &lt;search terms&gt;</code> Returns top 3 results with thumbnails and descriptions.</p>
</details>
"""