🍄 Funguy Bot Commands 🍄
-
-📖 !help
-Displays comprehensive help documentation for all available commands with usage examples.
-
+ if not plugins:
+ await bot.api.send_text_message(room.room_id, "No plugins are currently loaded.")
+ return
-🔌 !plugins
-Lists all loaded plugins along with their descriptions in alphabetical order.
-
+ # 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
-🃏 !fortune
-Returns a random fortune message. Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room.
-
+ # 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"!{pname}
{first_line}
"
+ )
-⏰ !date
-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.
-
-
-💻 !proxy
-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.
-
-
-📶 !isup [domain/ip]
-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.
-
-
-☯ !karma [user]
-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.
-
-
-⇧ !karma [user] up
-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.
-
-
-⇩ !karma [user] down
-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.
-
-
-🌧️ !weather [location]
-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.
-
-
-📖 !ud [term] [index]
-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.
-
-
-🔍 !dns [domain]
-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.
-
-
-💰 !btc
-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.
-
-
-🌐 !whois <domain/ip>
-Perform comprehensive WHOIS lookups for domains and IP addresses. Retrieves registrar information, registration dates, name servers, and contact details from WHOIS databases.
-Usage:
-
-!whois <domain> - Query domain registration information
-!whois <ip> - Query IP address allocation details
-
-Examples:
-
-!whois example.com
-!whois google.com
-!whois 8.8.8.8
-!whois 1.1.1.1
-
-Output includes: Domain/IP information, registrar, WHOIS server, creation/expiration dates, name servers, and contact details.
-
-
-🔍 !subdomains
-Enumerate subdomains using SSL certificate transparency logs. Discovers associated subdomains by querying the CertSpotter API for SSL certificates issued for a domain.
-Usage:
-
-!subdomains - Enumerate subdomains for any domain
-!subdomains example.com - Example subdomain enumeration
-
-Features:
-
-- Discovers subdomains through SSL certificate transparency logs
-- Uses the free CertSpotter API for enumeration
-- No rate limiting or API key required
-- Identifies subdomains through certificate SAN (Subject Alternative Name) enumeration
-
-Examples:
-
-!subdomains google.com
-!subdomains github.com
-!subdomains example.com
-
-Essential for reconnaissance and subdomain enumeration in penetration testing
-
-
-📍 !geo [ip/domain]
-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.
-Usage:
-
-!geo - Geolocate an IP address
-!geo - Geolocate a domain (automatically resolves to IP)
-
-Features:
-
-- Uses ip-api.com as primary geolocation service with ipapi.co fallback
-- Automatic domain to IP resolution
-- Comprehensive geographic information
-- No API key required for basic usage
-- Supports both IPv4 and IPv6 addresses
-
-Examples:
-
-!geo 8.8.8.8
-!geo example.com
-!geo google.com
-
-Information provided:
-
-- Country and country code
-- Region/State
-- City
-- Postal code
-- Latitude/Longitude coordinates
-- Timezone
-- ISP/Organization
-- Autonomous System Number (ASN)
-
-Essential for network reconnaissance and IP investigation
-
-
-🔍 !shodan [command] [query]
-Shodan.io integration for security reconnaissance and threat intelligence.
-Commands:
-
-!shodan ip <ip_address> - Detailed IP information (services, ports, banners)
-!shodan search <query> - Search Shodan database with filters
-!shodan host <domain> - Host information and subdomain enumeration
-!shodan count <query> - Count results with geographic/organization breakdown
-!shodan test - Test API connection and debug queries
-
-Search Examples:
-
-!shodan search apache
-!shodan search "port:22 country:US"
-!shodan search "product:nginx"
-!shodan search "net:192.168.1.0/24"
-!shodan search "http.title:'admin'"
-
-Common Filters: country, city, port, product, os, org, net, has_ssl, http.title
-Requires SHODAN_KEY environment variable
-
-
-🌐 !dnsdumpster [domain]
-Comprehensive DNS reconnaissance and attack surface mapping using DNSDumpster.com API.
-Commands:
-
-!dnsdumpster <domain> - Complete DNS reconnaissance for any domain
-!dnsdumpster test - Test API connection and key validity
-
-Features:
-
-- A Records - All IPv4 addresses with geographic and ASN information
-- NS Records - Complete name server information with IP locations
-- MX Records - All mail servers with geographic data
-- CNAME Records - Full alias chain mappings
-- TXT Records - All text records including SPF, DKIM, verification
-- Additional Records - AAAA, SRV, SOA, PTR records when available
-- Web Services - HTTP/HTTPS service detection with banner information
-
-Examples:
-
-!dnsdumpster google.com
-!dnsdumpster github.com
-!dnsdumpster example.com
-
-Requires DNSDUMPSTER_KEY environment variable
-Rate Limit: 1 request per 2 seconds
-
-
-
-💣 !exploitdb - Search Exploit Database
-
-Description:
-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.
-
-Usage:
-!exploitdb <search_term> [max_results]
-
-Parameters:
-• search_term (required) - Software name, CVE number, or vulnerability type
-• max_results (optional) - Number of results to return (1-10, default: 5)
-
-Examples:
-!exploitdb wordpress - Search for WordPress exploits
-!exploitdb apache 3 - Get 3 Apache exploits
-!exploitdb windows privilege escalation - Search for Windows privesc
-!exploitdb CVE-2021-44228 - Search by CVE number
-!exploitdb linux kernel 10 - Get 10 Linux kernel exploits
-!exploitdb sql injection - Search for SQL injection exploits
-
-Output Includes:
-• Exploit title and description
-• EDB-ID (Exploit Database ID)
-• Exploit type (webapps, local, remote, etc.)
-• Platform/OS (PHP, Linux, Windows, etc.)
-• Author name
-• Publication date
-• Direct link to full exploit code
-
-Notes:
-• Searches the official Exploit-DB CSV database
-• May take a few seconds on first use (downloads database)
-• Falls back to search links if database unavailable
-
-⚠️ Use responsibly and only on systems you have permission to test.
-
-
-🛡️ !headers <url>
-Comprehensive HTTP security header analysis with security scoring and recommendations.
-Features:
-
-- Security scoring (0-100) with color-coded ratings
-- Critical security header validation and configuration checking
-- HTTP to HTTPS redirect chain analysis
-- SSL certificate information for HTTPS sites
-- Information disclosure header detection
-- Actionable security recommendations
-
-Security Headers Analyzed:
-
-Strict-Transport-Security - HSTS enforcement
-Content-Security-Policy - XSS protection
-X-Frame-Options - Clickjacking protection
-X-Content-Type-Options - MIME sniffing prevention
-Referrer-Policy - Referrer control
-Feature-Policy - Browser feature restrictions
-- Server information headers
-
-Security Ratings:
-
-- 🟢 Excellent (80-100) - Strong configuration
-- 🟡 Good (60-79) - Moderate, needs improvement
-- 🟠 Fair (40-59) - Basic, significant improvements needed
-- 🔴 Poor (0-39) - Weak configuration
-
-Examples:
-
-!headers example.com
-!headers https://github.com
-!headers localhost:3000
-!headers subdomain.target.com
-
-Provides enterprise-grade security analysis for penetration testers and developers
-
-
-🔄 !hashid <hash>
-Advanced hash type identification with confidence scoring and tool recommendations.
-Features:
-
-- 100+ hash types including modern, legacy, and exotic algorithms
-- Color-coded confidence scoring (🟢 Very High to 🔴 Low)
-- Hashcat mode numbers and John the Ripper format names
-- Context-aware parsing for various hash formats
-
-Supported Categories:
-
-- Modern: yescrypt, scrypt, Argon2, bcrypt
-- Unix/Linux: SHA-512/256 Crypt, MD5 Crypt, apr1
-- Raw Hashes: MD5, SHA family, SHA-3, NTLM, LM
-- Databases: MySQL, PostgreSQL, Oracle, MSSQL
-- Web/CMS: WordPress, Drupal, phpBB3, Django
-- LDAP: SSHA, SMD5, LDAP crypt formats
-- Network: NetNTLMv1/v2, Kerberos
-- Exotic: Whirlpool, RIPEMD, GOST, BLAKE2
-
-Tool Integration:
-
-- Hashcat: Mode numbers for
-m parameter
-- John: Format names for
--format= parameter
-- Multi-tool compatibility
-
-Examples:
-
-!hashid 5d41402abc4b2a76b9719d911017c592 (MD5)
-!hashid aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d (SHA-1)
-!hashid $6$rounds=5000$salt$hash... (SHA-512 Crypt)
-!hashid $y$j9T$... (yescrypt - modern Linux)
-!hashid 8846f7eaee8fb117ad06bdd830b7586c (NTLM)
-!hashid *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 (MySQL)
-
-Confidence Legend:
-
-- 🟢 Very High (90-100%) - Single definitive match
-- 🟡 High (80-89%) - Strong match with minor alternatives
-- 🟠 Medium (60-79%) - Multiple plausible matches
-- 🔴 Low (0-59%) - Uncertain, needs context
-
-Essential for penetration testers, forensic analysts, and password cracking
-
-
-🔐 !sslscan <domain[:port]>
-Comprehensive SSL/TLS security scanning and analysis with vulnerability detection.
-Features:
-
-- TLS 1.0-1.3 protocol support testing with security scoring
-- Certificate chain validation, expiration, and signature analysis
-- 25+ cipher suite testing with strength classification
-- Vulnerability detection (POODLE, weak ciphers, protocol issues)
-- 0-100 security rating with color-coded assessment
-- PCI DSS and modern security standards compliance checking
-
-Security Checks:
-
-- Protocol Security: TLS 1.2/1.3 enforcement, insecure protocol detection
-- Certificate Health: Expiration monitoring, signature validation
-- Cipher Security: RC4, DES, 3DES, NULL cipher detection
-- Modern Standards: Forward Secrecy, strong encryption
-
-Security Ratings:
-
-- 🟢 Excellent (90-100) - Modern TLS with strong security
-- 🟡 Good (80-89) - Good security, minor improvements needed
-- 🟠 Fair (60-79) - Moderate security, significant improvements
-- 🔴 Poor (0-59) - Critical issues requiring immediate attention
-
-Examples:
-
-!sslscan example.com
-!sslscan github.com:443
-!sslscan localhost:8443
-!sslscan 192.168.1.1:443
-
-Essential for security teams, system administrators, and developers ensuring TLS compliance
-Note: SSLv2/SSLv3 testing limited by Python security features
-
-
-🎵 Last.fm Integration
-Comprehensive Last.fm integration with 30+ commands for music analytics and social features.
-Commands:
-
-!register - Register your Last.fm username
-!np [user] - Show currently playing track
-!recent [user] [limit] - Show recent tracks (default 10, max 50)
-!toptracks [user] [period] - Show top tracks (overall/7day/1month/3month/6month/12month)
-!topartists [user] [period] - Show top artists
-!topalbums [user] [period] - Show top albums
-!loved [user] - Show recently loved tracks
-!profile [user] - Detailed user profile
-!playcount [user] - Total scrobbles
-!scrobbles [user] - Detailed scrobbling statistics
-!compare - Compare musical tastes
-!taste [user] - Top artists with taste-o-meter
-!friends [user] - Show Last.fm friends
-!recommend [user] - Artist recommendations
-!similar - Find similar artists
-!tag - Top artists for a tag/genre
-!charts - Global top tracks chart
-!tagcloud [user] - Top genre tags
-!now - What are registered users playing?
-!decades [user] - Favorite decades analysis
-!genres [user] - Top genres/tags
-!era - Popular tracks from a year
-!weekly [user] - Weekly listening report
-!monthly [user] - Monthly listening report
-!yearly [user] [year] - Yearly listening report
-!first [user] - Find first scrobble of an artist
-!concerts [user] - Upcoming concerts for top artists
-!radio - Generate playlist based on artist
-!mashup - Musical connections between artists
-!collage [user] [size] - Top album art URLs
-!listening [user] - Currently listening with album art
-!awards [user] - Milestone achievements
-
-Features:
-
-- Register your Matrix ID with your Last.fm username
-- Display currently playing tracks with artist and album information
-- Compare musical tastes between users
-- Discover similar artists and genres
-- Get personalized artist recommendations
-- View detailed listening statistics and reports
-- Find upcoming concerts for your favorite artists
-- Generate playlists based on your musical preferences
-- View milestone achievements and listening habits
-- Uses SQLite database to store user associations
-- Requires LASTFM_API_KEY environment variable
-
-Examples:
-
-!register your_lastfm_username - Register your Last.fm username
-!np - Show your currently playing track
-!recent 20 - Show your 20 most recent tracks
-!topartists 7day - Show your top artists from the last 7 days
-!compare user1 user2 - Compare musical tastes between two users
-!similar radiohead - Find artists similar to Radiohead
-!tag electronic - Show top electronic artists
-!era 1994 - Show popular tracks from 1994
-!radio metallica - Generate a playlist based on Metallica
-!mashup metallica megadeth - Find musical connections between Metallica and Megadeth
-
-Requirements:
-Last.fm account at last.fm
-LASTFM_API_KEY in .env file
-YOUTUBE_API_KEY in .env file (for YouTube integration)
-
-
-
-
-
-📸 !sd [prompt]
-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'.
-
-
-📄 !text [prompt]
-Generates text using the Infermatic AI API. Supports multiple models, configurable parameters, and model listing. Uses queuing system for sequential processing.
-Usage:
-
-!text <prompt> - Generate text using the default model
-!text --list-models - List all available models from Infermatic AI
-!text --use-model <model_name> <prompt> - Use a specific model instead of the default
-!text --temperature <value> <prompt> - Set temperature (0.0-1.0, default: 0.9)
-!text --max-tokens <value> <prompt> - Set maximum tokens to generate (default: 2048)
-
-Configuration:
-
-- Requires
INFERMATIC_API environment variable set to your API key
-- Requires
INFERMATIC_MODEL environment variable for default model (default: Sao10K-L3.1-70B-Hanami-x1)
-
-Model Management:
-
-- Use
!text --list-models to see all available models
-- Models support different capabilities and context lengths
-- Costs and token limits vary by model
-
-Examples:
-
-!text write a python function to calculate fibonacci
-!text --list-models
-!text --use-model llama-v3-8b-instruct explain quantum computing
-!text --temperature 0.7 --max-tokens 500 write a haiku about AI
-
-
-
-📰 !xkcd
-Fetches and displays a random XKCD comic. Downloads comic image and sends it directly to the chat room.
-
-
-🎬 YouTube Features
-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.
-
-
-📘 !wp
-Fetches Wikipedia summaries and main images for search terms using MediaWiki APIs. No HTML scraping or BeautifulSoup required.
-Usage:
-!wp - Fetch Wikipedia summary for any search term
-!wp help - Show usage instructions
-
-Examples:
-!wp artificial intelligence
-!wp machine learning
-!wp python programming
-
-
-
-📘 !time
-Fetches current time information for locations using the TimeAPI.io service.
-Usage:
-!time - Get time for a location
-!time help - Show usage instructions
-
-Examples:
-!time London - Get time in London
-!time Tokyo - Get time in Tokyo
-!time New York - Get time in New York
-
-Supported locations: Major cities worldwide including New York, London, Tokyo, Sydney, Paris, Berlin, etc.
-
-
-
-📚 !arxiv [query]
-Search academic papers on arXiv.org. Categories include AI, ML, Security, Physics, Math, and more. No API key required.
-Commands:
-
-!arxiv - Search for papers (shows abstracts)
-!arxiv list - List papers without abstracts
-!arxiv category - Browse recent papers by category
-!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"
- }
-}
-
-
-
-
-
📚 !arxiv [query]
-
Search academic papers on arXiv.org. Categories include AI, ML, Security, Physics, Math, and more. No API key required.
-
Commands:
-
-!arxiv - Search for papers (shows abstracts)
-!arxiv list - List papers without abstracts
-!arxiv category - Browse recent papers by category
-!arxiv recent - Most recent papers in category
-!arxiv random - Get a random paper
-!arxiv - Get paper by arXiv ID (e.g., 2101.00101)
-
-
Categories: ai, ml, security, crypto, cv, nlp, math, physics, quantum, bio, software
-
-
-
📰 !news [category/query]
-Fetch latest headlines from various news categories using GNews API. Requires GNEWS_API_KEY environment variable.
-Commands:
-
-!news - Get top headlines (default)
-!news top - Top headlines
-!news world - World news
-!news tech - Technology news
-!news business - Business news
-!news science - Science news
-!news health - Health news
-!news crypto - Cryptocurrency news
-!news search - Search for specific news
-
-
-
-
🔥 !hn [command]
-Fetch top stories from Hacker News using Firebase API. No API key required.
-Commands:
-
-!hn - Show top 5 stories (default)
-!hn top - Top stories
-!hn new - Newest stories
-!hn best - Best stories
-!hn ask - Ask HN threads
-!hn show - Show HN posts
-!hn job - Job postings
-!hn story - Get details of a specific story
-!hn comments - Show comments for a story
-!hn search - Search stories (via Algolia)
-
-
-
-
☯ !karma [user]
-Track karma points for users with leaderboards and statistics. Supports display names and Matrix IDs.
-Commands:
-
-!karma - Show karma for a user
-!karma++ - Give +1 karma to a user
-!karma-- - Give -1 karma to a user
-!karma top [n] - Show top karma entries
-!karma bottom [n] - Show bottom karma entries
-!karma rank - Show rank of user
-!karma stats - Show overall statistics
-!karma history - Show recent karma history
-!++ - Shortcut for !karma++
-!-- - Shortcut for !karma--
-++ - Inline karma (message contains ++)
--- - Inline karma (message contains --)
-
-Features:
-
-- Supports display names and Matrix IDs
-- Room-specific karma tracking
-- Rate limiting to prevent spam
-- Karma history tracking
-- Leaderboards and statistics
-
-
-
-
🔥 !hn [command]
-Fetch top stories from Hacker News using Firebase API. No API key required.
-Commands:
-
-!hn - Show top 5 stories (default)
-!hn top - Top stories
-!hn new - Newest stories
-!hn best - Best stories
-!hn ask - Ask HN threads
-!hn show - Show HN posts
-!hn job - Job postings
-!hn story - Get details of a specific story
-!hn comments - Show comments for a story
-!hn search - Search stories (via Algolia)
-
-
-
-
⏱️ !cron [add|remove] [room_id] [cron_entry] [command]
-Schedule automated commands using cron syntax. Add or remove cron jobs for specific rooms and commands.
-
-
-
🔧 Admin Commands
-
-!set [option] [value] - Set configuration options (admin_user, prefix)
-!get [option] - Get configuration values
-!saveconf - Save current configuration
-!loadconf - Load saved configuration
-!show - Display current configuration
-!reset - Reset configuration to defaults
-!load [plugin] - Load a plugin
-!unload [plugin] - Unload a plugin
-!reload - Reload all plugins
-!disable [plugin] [room_id] - Disable a plugin for specific room
-!enable [plugin] [room_id] - Enable a plugin for specific room
-!rehash - Reload configuration
-Note: Admin commands require admin_user privileges
-
-
-
-
-
-
-
-
-
-
🌟 Funguy Bot Credits
+ # Append bot credits
+ credits = """
+🌟 Funguy Bot Credits
🧙♂️ Creator & Developer: HB is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)
🚀 Development Context: Created during recovery from two-level cervical spinal surgery (CDA Cervical Discectomy and Disc Arthroplasty)
-Join our Matrix Room: Self-hosting | Security | Sysadmin | Homelab | Programming
+Join our Matrix Room: Self‑hosting | Security | Sysadmin | Homelab | Programming
"""
+ help_parts.append(credits)
- await bot.api.send_markdown_message(room.room_id, commands_message)
- logging.info("Sent help documentation to the room")
+ master = "🍄 Funguy Bot Commands (click to expand)
"
+ master += "\n".join(help_parts)
+ master += " "
+
+ await bot.api.send_markdown_message(room.room_id, master)
+ logging.info("Sent dynamic help from %d plugins", len(plugins))
diff --git a/plugins/imdb.py b/plugins/imdb.py
index c8dd2c8..cd9649e 100644
--- a/plugins/imdb.py
+++ b/plugins/imdb.py
@@ -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__ = """
+
+!imdb – Movie/series details via OMDb
+
+!imdb <title> – Full details (poster sent separately)
+!imdb id <tt1234567> – Lookup by IMDb ID
+!imdb search <query> – Search titles
+!imdb episode <series> -s N -e N – Episode info
+
+Optional flags: -y year, -t movie|series|episode, --short-plot
+Requires OMDB_API_KEY env var.
+
+"""
diff --git a/plugins/infermatic-text.py b/plugins/infermatic-text.py
index 2dc2218..348f3ab 100644
--- a/plugins/infermatic-text.py
+++ b/plugins/infermatic-text.py
@@ -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__ = """
+
+!text – AI text generation (Infermatic)
+
+!text <prompt> – Generate text using default model
+!text --list-models – List available models
+!text --use-model <model> <prompt> – Specific model
+--temperature <0.0-1.0> – Set creativity (default 0.9)
+--max-tokens <number> – Max output length (default 2048)
+
+Requires INFERMATIC_API env var.
+
+"""
diff --git a/plugins/isup.py b/plugins/isup.py
index 236ac04..18df6a3 100644
--- a/plugins/isup.py
+++ b/plugins/isup.py
@@ -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__ = """
+
+!isup – Is it up?
+!isup <domain or IP> – Performs DNS resolution and checks HTTP/HTTPS availability.
+
+"""
diff --git a/plugins/karma.py b/plugins/karma.py
index 833a823..019a904 100644
--- a/plugins/karma.py
+++ b/plugins/karma.py
@@ -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__ = """
+
+!karma – Karma system
+
+!karma <user> – Show points
+!karma++ <user> / !-- <user> – Modify karma
+!karma top [n] / !karma bottom [n] – Leaderboard
+!karma rank <user> – Position
+!karma stats – Room statistics
+!karma history <user> – Recent votes
+
+Shortcuts: !++ user, !-- user, and inline user++ / user--.
+
+"""
diff --git a/plugins/lastfm.py b/plugins/lastfm.py
index 20f2262..1e61ca6 100644
--- a/plugins/lastfm.py
+++ b/plugins/lastfm.py
@@ -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__ = """
+
+!lastfm – Last.fm music stats (30+ commands)
+
+!register <username> – Connect account
+!np [user] – Now playing
+!recent [user] [limit] – Recent tracks
+!toptracks, !topartists, !topalbums
+!loved, !profile, !playcount, !scrobbles
+!compare <user1> <user2> – Taste comparison
+!recommend, !similar <artist>, !tag <genre>
+!charts, !now, !decades, !genres, !tagcloud
+!era <year>, !weekly, !monthly, !yearly
+!first <artist>, !concerts, !radio <artist>
+!collage [user] [size], !listening, !awards
+
+For full details: !lastfm
Requires LASTFM_API_KEY env var.
+
+"""
diff --git a/plugins/loadplugin.py b/plugins/loadplugin.py
index fdaf95f..acaf1ed 100644
--- a/plugins/loadplugin.py
+++ b/plugins/loadplugin.py
@@ -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__ = """
+
+Admin: !load / !unload
+!load <plugin> / !unload <plugin> – Dynamically load or unload a plugin module. Admin only.
+
+"""
diff --git a/plugins/news.py b/plugins/news.py
index a5cc614..36c7784 100644
--- a/plugins/news.py
+++ b/plugins/news.py
@@ -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__ = """
+
+!news – Latest news headlines
+
+!news [top|world|tech|business|science|health|sports|crypto]
+!news search <query>
+- You can append a number:
!news tech 8
+
+Requires GNEWS_API_KEY env var.
+
+"""
diff --git a/plugins/plugins.py b/plugins/plugins.py
index a407b8f..1b2bd17 100644
--- a/plugins/plugins.py
+++ b/plugins/plugins.py
@@ -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, "🔌Plugins List🔌
⤵︎Click Here to Expand⤵︎
")
-
plugins_message = "
".join(plugin_descriptions)
plugins_message += " "
+
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"[{plugin_name}.py]: {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__ = """
+
+!plugins – List loaded plugins
+Displays all currently loaded plugins and their descriptions.
+
+"""
diff --git a/plugins/proxy.py b/plugins/proxy.py
index e29b7ff..d611813 100644
--- a/plugins/proxy.py
+++ b/plugins/proxy.py
@@ -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__ = """
+
+!proxy – Random working SOCKS5 proxy
+Fetches, tests, and returns a random working SOCKS5 proxy with latency. Caches good proxies in SQLite.
+
+"""
diff --git a/plugins/shodan.py b/plugins/shodan.py
index 569eb45..2a64d82 100644
--- a/plugins/shodan.py
+++ b/plugins/shodan.py
@@ -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__ = """
+
+!shodan – Shodan search
+
+!shodan ip <ip> – IP info with open ports
+!shodan search <query> – Search internet devices
+!shodan host <domain> – Host & subdomain enumeration
+!shodan count <query> – Result counts
+
+Requires SHODAN_KEY env var.
+
+"""
diff --git a/plugins/sslscan.py b/plugins/sslscan.py
index 6bf1076..c0b1bc1 100644
--- a/plugins/sslscan.py
+++ b/plugins/sslscan.py
@@ -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__ = """
+
+!sslscan – SSL/TLS analysis
+!sslscan <domain[:port]> – Tests protocols, cipher suites, certificate validity, vulnerabilities.
+Provides a security score (0-100) and actionable recommendations.
+
+"""
diff --git a/plugins/stable-diffusion.py b/plugins/stable-diffusion.py
index bbbde04..a334f48 100644
--- a/plugins/stable-diffusion.py
+++ b/plugins/stable-diffusion.py
@@ -185,3 +185,26 @@ def print_help():
- <lora:al3xxl:1> alexpainting, alexhuman, alexentity, alexthirdeye, alexforeheads, alexgalactic, spiraling, alexmirror, alexangel, alexkissing, alexthirdeye2, alexthirdeye3, alexhofmann, wearing, glasses, alexgalactic3, alexthirdeye4, alexhuman3, alexgalactic2, alexhuman2, alexbeing2, alexfractal2, alexfractal3
"""
+
+
+# ---------------------------------------------------------------------------
+# Plugin Metadata
+# ---------------------------------------------------------------------------
+
+__version__ = "1.0.0"
+__author__ = "Funguy Bot"
+__description__ = "Stable Diffusion image generation"
+__help__ = """
+
+!sd – Generate images via Stable Diffusion
+!sd [options] <prompt>
+
+--steps N – Sampling steps (default 4)
+--cfg scale – CFG scale (default 2)
+--h H --w W – Image dimensions (default 512)
+--neg <negative prompt>
+--sampler SAMPLER – Sampler name (default DPM++ SDE)
+
+Requires a locally running Stable Diffusion API.
+
+"""
diff --git a/plugins/subdomains.py b/plugins/subdomains.py
index 36c625e..51861b1 100644
--- a/plugins/subdomains.py
+++ b/plugins/subdomains.py
@@ -124,4 +124,18 @@ async def handle_command(room, message, bot, prefix, config):
room.room_id,
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)
\ No newline at end of file
+ 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__ = """
+
+!subdomains – Enumerate subdomains
+!subdomains <domain> – Finds subdomains using SSL certificate transparency logs (CertSpotter API).
+
+"""
diff --git a/plugins/sysinfo.py b/plugins/sysinfo.py
index 7cb331b..7e285f2 100644
--- a/plugins/sysinfo.py
+++ b/plugins/sysinfo.py
@@ -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__ = """
+
+!sysinfo – System information
+Displays CPU, RAM, storage, network, Docker, GPU, sensors, and top processes.
+
+"""
diff --git a/plugins/timezone.py b/plugins/timezone.py
index 2435a56..41ca687 100644
--- a/plugins/timezone.py
+++ b/plugins/timezone.py
@@ -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__ = """
+
+!time – Current time for any city
+
+!time <city> – Geocode any city (free Open-Meteo API)
+!time <IANA zone> – e.g., Europe/London
+
+Also shows current temperature if available.
+
+"""
diff --git a/plugins/urbandictionary.py b/plugins/urbandictionary.py
index 08b364d..526b6dd 100644
--- a/plugins/urbandictionary.py
+++ b/plugins/urbandictionary.py
@@ -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__ = """
+
+!ud – Urban Dictionary
+
+!ud – Random definition
+!ud <term> – Top definition
+!ud <term> <index> – Nth definition
+
+
+"""
diff --git a/plugins/weather.py b/plugins/weather.py
index 1ab3900..950bbd6 100644
--- a/plugins/weather.py
+++ b/plugins/weather.py
@@ -1,162 +1,274 @@
"""
-This plugin provides a command to get weather information for a location.
+Weather plugin – primary: OpenWeatherMap, fallback: Open‑Meteo.
+
+Uses OpenWeatherMap when a valid API key is present and the request succeeds.
+Falls back to Open‑Meteo (no key required) otherwise.
+
+Commands:
+ !weather 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 Open‑Meteo)
+# ---------------------------------------------------------------------------
+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 \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"""
-[{weather_emoji} Weather for {city_name}, {country}]: Condition: {description} | Temperature: {temp:.1f}°C ({temp_f:.1f}°F) | Feels like: {feels_like:.1f}°C ({feels_like_f:.1f}°F) | Humidity: {humidity}% | Wind Speed: {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"[{emoji} Weather for {city}, {country}]: "
+ f"Condition: {description} | "
+ f"Temperature: {temp_c:.1f}°C ({temp_f:.1f}°F) | "
+ f"Humidity: {humidity}% | "
+ f"Wind Speed: {wind} m/s"
+ )
+
+
+# ---------------------------------------------------------------------------
+# Fallback: Open‑Meteo (no key, free)
+# ---------------------------------------------------------------------------
+async def meteo_geocode(session: aiohttp.ClientSession, location: str) -> dict | None:
+ """Geocode a city name via Open‑Meteo. 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"Open‑Meteo 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 Open‑Meteo. 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"Open‑Meteo weather error: {e}")
+ return None
+
+
+def format_meteo(loc_info: dict, weather_data: dict) -> str:
+ """Format Open‑Meteo result into the same one‑line 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"[{emoji} Weather for {loc_str}]: "
+ f"Condition: {desc} | "
+ f"Temperature: {temp_c}°C ({temp_f}°F) | "
+ f"Wind Speed: {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 \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: Open‑Meteo
+ logging.info("Falling back to Open‑Meteo")
+ 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 Open‑Meteo (fallback)")
+
+
+# ---------------------------------------------------------------------------
+# Plugin setup
+# ---------------------------------------------------------------------------
+def setup(bot):
+ logging.info("Weather plugin loaded (OpenWeatherMap + Open‑Meteo fallback)")
+
+
+# ---------------------------------------------------------------------------
+# Plugin Metadata
+# ---------------------------------------------------------------------------
+__version__ = "1.0.0"
+__author__ = "Funguy Bot"
+__description__ = "Weather forecast (OWM primary, Open‑Meteo fallback)"
+__help__ = """
+
+!weather – Current weather
+!weather <location> – Shows temperature, conditions, humidity, wind.
+Uses OpenWeatherMap if a valid API key is present; falls back to free Open‑Meteo otherwise.
+
+"""
diff --git a/plugins/welcome.py b/plugins/welcome.py
index 1973ee3..a4b2be7 100644
--- a/plugins/welcome.py
+++ b/plugins/welcome.py
@@ -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__ = """
+
+!welcome – Receive welcome message
+Manually triggers the room's welcome message for yourself.
+
+"""
diff --git a/plugins/whois.py b/plugins/whois.py
index a5ff539..059794f 100644
--- a/plugins/whois.py
+++ b/plugins/whois.py
@@ -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__ = """
+
+!whois – WHOIS lookup
+!whois <domain or IP> – Shows registrar, creation/expiry dates, nameservers, contacts.
+
+"""
diff --git a/plugins/wikipedia.py b/plugins/wikipedia.py
index d3fab9f..453de7f 100644
--- a/plugins/wikipedia.py
+++ b/plugins/wikipedia.py
@@ -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__ = """
+
+!wp – Wikipedia summary
+!wp <search term> – Returns the lead section and main image from Wikipedia.
+Uses MediaWiki APIs, no scraping.
+
+"""
diff --git a/plugins/xkcd.py b/plugins/xkcd.py
index 3c9d1fb..b1a3f3f 100644
--- a/plugins/xkcd.py
+++ b/plugins/xkcd.py
@@ -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__ = """
+
+!xkcd – Random XKCD comic
+Posts a random XKCD comic image.
+
+"""
diff --git a/plugins/youtube-search.py b/plugins/youtube-search.py
index 21b3624..6152f8f 100644
--- a/plugins/youtube-search.py
+++ b/plugins/youtube-search.py
@@ -71,3 +71,18 @@ async def send_collapsible_message(room, bot, content):
"""
message = f'🍄Funguy ▶YouTube Search🍄
⤵︎Click Here To See Results⤵︎
{content} '
await bot.api.send_markdown_message(room.room_id, message)
+
+
+# ---------------------------------------------------------------------------
+# Plugin Metadata
+# ---------------------------------------------------------------------------
+
+__version__ = "1.0.0"
+__author__ = "Funguy Bot"
+__description__ = "YouTube video search"
+__help__ = """
+
+!yt – Search YouTube
+!yt <search terms> – Returns top 3 results with thumbnails and descriptions.
+
+"""