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()
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
@self.bot.listener.on_message_event
async def wrapper_handle_commands(room, message):
+14
View File
@@ -320,3 +320,17 @@ def setup(bot):
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__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."
)
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.prefix = "!"
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:
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"
else:
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:
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)}"
)
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>"
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)}"
)
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')
await bot.api.send_markdown_message(room.room_id, fortune_output)
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."
)
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"
__author__ = "Funguy Bot"
__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)}"
)
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>"
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>
"""
+56 -654
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 simplematrixbotlib as botlib
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !help command.
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("help")):
return
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.
args = match.args()
plugins = getattr(bot, "plugins", {})
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("help"):
logging.info("Fetching command help documentation")
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>
if not plugins:
await bot.api.send_text_message(room.room_id, "No plugins are currently loaded.")
return
<details><summary>🔌 <strong>!plugins</strong></summary>
<p>Lists all loaded plugins along with their descriptions in alphabetical order.</p>
</details>
# If a specific plugin is requested
if args:
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>!fortune</strong></summary>
<p>Returns a random fortune message. Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room.</p>
</details>
# Aggregate help from all plugins
help_parts = []
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>!date</strong></summary>
<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>
</details>
<details><summary>💻 <strong>!proxy</strong></summary>
<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>
</details>
<details><summary>📶 <strong>!isup [domain/ip]</strong></summary>
<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>
</details>
<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>
# Append bot credits
credits = """
<details><summary><strong>🌟 Funguy Bot Credits</strong></summary>
<p>
<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>
<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>
</details>
"""
help_parts.append(credits)
await bot.api.send_markdown_message(room.room_id, commands_message)
logging.info("Sent help documentation to the room")
master = "<details><summary><strong>🍄 Funguy Bot Commands</strong> (click to expand)</summary><br>"
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)
finally:
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():
next_command = await command_queue.get()
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")
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 = {}
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------
@@ -625,7 +633,8 @@ async def handle_karma_command(room, message, bot, config):
response = f"🏆 **Top {len(leaderboard)} Karma Leaders**\n\n"
for i, entry in enumerate(leaderboard, 1):
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)
else:
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:
response = f"📉 **Bottom {len(leaderboard)} Karma (Needs Love)**\n\n"
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)
else:
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)
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)
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)
else:
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("Commands: !karma, !karma++, !karma--, !++, !--, and inline ++/--")
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(
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
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"
__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
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)
if match.is_not_from_this_bot() and match.prefix() and match.command("plugins"):
logging.info("Received !plugins command")
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>")
plugins_message = "<br>".join(plugin_descriptions)
plugins_message += "</details>"
await bot.api.send_markdown_message(room.room_id, plugins_message)
logging.info("Sent plugin list to the room")
def get_plugin_descriptions():
"""
Function to get descriptions of all loaded plugin modules.
Returns:
list: A list of plugin descriptions sorted alphabetically.
"""
plugin_descriptions = []
for module_name, module in sys.modules.items():
if module_name.startswith("plugins.") and hasattr(module, "__file__"):
plugin_path = module.__file__
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]
except AttributeError:
else:
description = "No description available"
plugin_descriptions.append(f"<strong>[{plugin_name}.py]:</strong> {description}")
# Sort the plugin descriptions alphabetically by plugin name
plugin_descriptions.sort()
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")
# ---------------------------------------------------------------------------
# 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}")
await bot.api.send_text_message(room.room_id, message)
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:
pass
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>
</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."
)
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
# ---------------------------------------------------------------------------
# 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
await bot.api.send_markdown_message(room.room_id, format_response(data, display))
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."
)
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>
"""
+257 -145
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 requests
import os
import aiohttp
import simplematrixbotlib as botlib
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__))
# Get the parent directory (main bot directory)
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)
# OpenWeatherMap API configuration
# 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"
OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY", "")
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !weather command.
# ---------------------------------------------------------------------------
# WMO codes → description + emoji (for OpenMeteo)
# ---------------------------------------------------------------------------
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.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
# ---------------------------------------------------------------------------
# Primary: OpenWeatherMap
# ---------------------------------------------------------------------------
async def openweathermap_get(session: aiohttp.ClientSession, location: str) -> dict | None:
"""Fetch current weather from OpenWeatherMap. Returns None on failure."""
if not OPENWEATHER_API_KEY:
logging.info("OpenWeatherMap key missing, skipping primary")
return None
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("weather"):
logging.info("Received !weather command")
# Check if API key is configured
if not WEATHER_API_KEY:
await bot.api.send_text_message(
room.room_id,
"Weather API key not configured. Please set OPENWEATHER_API_KEY environment variable."
)
return
args = match.args()
# Check if location was provided
if len(args) < 1:
await bot.api.send_text_message(
room.room_id,
"Usage: !weather <location>\nExample: !weather London or !weather New York,US"
)
logging.info("Sent usage message for !weather")
return
location = ' '.join(args)
try:
# Make API request to OpenWeatherMap
params = {
'q': location,
'appid': WEATHER_API_KEY,
'units': 'metric' # Use metric units (Celsius)
}
response = requests.get(WEATHER_API_URL, params=params, timeout=10)
response.raise_for_status()
weather_data = response.json()
# 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(
room.room_id,
f"Location '{location}' not found. Please check the spelling and try again."
)
elif e.response.status_code == 401:
await bot.api.send_text_message(
room.room_id,
"Weather API authentication failed. Please check the API key configuration."
)
else:
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:
await bot.api.send_text_message(
room.room_id,
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):
"""
Get an emoji based on weather condition.
Args:
condition (str): Weather condition from API.
Returns:
str: Weather emoji.
"""
weather_emojis = {
'Clear': '☀️',
'Clouds': '☁️',
'Rain': '🌧️',
'Drizzle': '🌦️',
'Thunderstorm': '⛈️',
'Snow': '❄️',
'Mist': '🌫️',
'Fog': '🌫️',
'Haze': '🌫️',
'Smoke': '🌫️',
'Dust': '🌫️',
'Sand': '🌫️',
'Ash': '🌫️',
'Squall': '💨',
'Tornado': '🌪️'
url = "https://api.openweathermap.org/data/2.5/weather"
params = {
"q": location,
"appid": OPENWEATHER_API_KEY,
"units": "metric", # Celsius
}
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
return weather_emojis.get(condition, '🌡️')
def format_openweathermap(data: dict) -> str:
"""Build the one-line weather message from OpenWeatherMap data."""
city = data.get("name", "Unknown")
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
args = match.args()
if not args:
await bot.api.send_text_message(
room.room_id,
"Usage: !weather <location>\nExample: !weather London or !weather New York,US"
)
return
location = " ".join(args)
logging.info("Received !weather command for '%s'", location)
async with aiohttp.ClientSession() as session:
# 1. Try OpenWeatherMap
owm_data = await openweathermap_get(session, location)
if owm_data:
if owm_data.get("cod") == 200:
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"))
# 2. Fallback: OpenMeteo
logging.info("Falling back to OpenMeteo")
loc_info = await meteo_geocode(session, location)
if not loc_info:
await bot.api.send_text_message(
room.room_id,
f"Location '{location}' not found."
)
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(
room.room_id,
"Could not fetch weather data from any provider."
)
return
msg = format_meteo(loc_info, wdata)
await bot.api.send_markdown_message(room.room_id, msg)
logging.info("Sent weather via OpenMeteo (fallback)")
# ---------------------------------------------------------------------------
# Plugin setup
# ---------------------------------------------------------------------------
def setup(bot):
logging.info("Weather plugin loaded (OpenWeatherMap + OpenMeteo fallback)")
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.0.0"
__author__ = "Funguy Bot"
__description__ = "Weather forecast (OWM primary, OpenMeteo fallback)"
__help__ = """
<details>
<summary><strong>!weather</strong> Current weather</summary>
<p><code>!weather &lt;location&gt;</code> Shows temperature, conditions, humidity, wind.<br>
Uses OpenWeatherMap if a valid API key is present; falls back to free OpenMeteo otherwise.</p>
</details>
"""
+14
View File
@@ -168,3 +168,17 @@ async def handle_command(room, message, bot, prefix, config):
room.room_id, _build_welcome_message(display_name)
)
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."
)
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)
except OSError:
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)
except Exception as 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>'
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>
"""