New plugins

This commit is contained in:
2025-10-16 11:55:31 -05:00
parent 8eb21d49da
commit 5ace1083f1
7 changed files with 672 additions and 4 deletions

View File

@@ -131,6 +131,12 @@ Fetches current weather information for any location using OpenWeatherMap API.
**📖 !ud [term] [index]** **📖 !ud [term] [index]**
Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index. Fetches definitions from Urban Dictionary. Use without arguments for random definition, or specify term and optional index.
**🔍 !dns [domain]**
Performs comprehensive DNS reconnaissance on a domain. Shows A, AAAA, MX, NS, TXT, CNAME, SOA, and other DNS records.
**💰 !btc**
Fetches the current Bitcoin price in USD from bitcointicker.co API.
### AI & Generation Commands ### AI & Generation Commands
**🤖 AI Commands (!tech, !music, !eth, etc.)** **🤖 AI Commands (!tech, !music, !eth, etc.)**

View File

@@ -18,9 +18,12 @@ import toml # Library for parsing TOML configuration files
from plugins.config import FunguyConfig from plugins.config import FunguyConfig
# Whitelist of allowed plugins to prevent arbitrary code execution # Whitelist of allowed plugins to prevent arbitrary code execution
ALLOWED_PLUGINS = {'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma', ALLOWED_PLUGINS = {
'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion', 'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma',
'xkcd', 'youtube-preview', 'youtube-search', 'weather', 'urbandictionary'} 'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion',
'xkcd', 'youtube-preview', 'youtube-search', 'weather', 'urbandictionary',
'bitcoin', 'dns', 'shodan'
}
class FunguyBot: class FunguyBot:
""" """

109
plugins/bitcoin.py Normal file
View File

@@ -0,0 +1,109 @@
"""
This plugin provides a command to fetch the current Bitcoin price.
"""
import logging
import requests
import simplematrixbotlib as botlib
BITCOIN_API_URL = "https://api.bitcointicker.co/trades/bitstamp/btcusd/60/"
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !btc command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("btc"):
logging.info("Received !btc command")
try:
# Fetch Bitcoin price data
headers = {
'Accept-Encoding': 'gzip, deflate',
'User-Agent': 'FunguyBot/1.0'
}
logging.info(f"Fetching Bitcoin price from {BITCOIN_API_URL}")
response = requests.get(BITCOIN_API_URL, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if not data or len(data) == 0:
await bot.api.send_text_message(
room.room_id,
"No Bitcoin price data available."
)
logging.warning("No Bitcoin price data returned from API")
return
# Get the most recent trade (last item in the array)
latest_trade = data[-1]
price = latest_trade.get('price')
if price is None:
await bot.api.send_text_message(
room.room_id,
"Could not extract Bitcoin price from API response."
)
logging.error("Price field not found in API response")
return
# Convert to float and format with commas
try:
price_float = float(price)
price_formatted = f"${price_float:,.2f}"
except (ValueError, TypeError):
price_formatted = f"${price}"
# Optional: Get additional info if available
timestamp = latest_trade.get('timestamp', '')
volume = latest_trade.get('volume', '')
# Build the message
message_text = f"<strong>₿ BTC/USD</strong>"
message_text += f"<strong> Current Price:</strong> {price_formatted}"
message_text += ", <em>bitcointicker.co</em>"
await bot.api.send_markdown_message(room.room_id, message_text)
logging.info(f"Sent Bitcoin price: {price_formatted}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(
room.room_id,
"Request timed out. Bitcoin price API may be slow or unavailable."
)
logging.error("Bitcoin API timeout")
except requests.exceptions.RequestException as e:
await bot.api.send_text_message(
room.room_id,
f"Error fetching Bitcoin price: {e}"
)
logging.error(f"Error fetching Bitcoin price: {e}")
except (KeyError, IndexError, ValueError) as e:
await bot.api.send_text_message(
room.room_id,
"Error parsing Bitcoin price data."
)
logging.error(f"Error parsing Bitcoin API response: {e}", exc_info=True)
except Exception as e:
await bot.api.send_text_message(
room.room_id,
"An unexpected error occurred while fetching Bitcoin price."
)
logging.error(f"Unexpected error in Bitcoin plugin: {e}", exc_info=True)

209
plugins/dns.py Normal file
View File

@@ -0,0 +1,209 @@
"""
This plugin provides a command to perform DNS reconnaissance on a domain.
"""
import logging
import dns.resolver
import dns.reversename
import simplematrixbotlib as botlib
import re
# Common DNS record types to query
RECORD_TYPES = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'PTR', 'SRV']
def is_valid_domain(domain):
"""
Validate if the provided string is a valid domain name.
Args:
domain (str): The domain to validate.
Returns:
bool: True if valid, False otherwise.
"""
# Basic domain validation regex
pattern = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
return re.match(pattern, domain) is not None
def format_dns_record(record_type, records):
"""
Format DNS records for display.
Args:
record_type (str): The type of DNS record.
records (list): List of DNS record values.
Returns:
str: Formatted HTML string.
"""
if not records:
return ""
output = f"<strong>{record_type} Records:</strong><br>"
for record in records:
output += f"{record}<br>"
return output
async def query_dns_records(domain):
"""
Query all common DNS record types for a domain.
Args:
domain (str): The domain to query.
Returns:
dict: Dictionary with record types as keys and lists of records as values.
"""
results = {}
resolver = dns.resolver.Resolver()
resolver.timeout = 5
resolver.lifetime = 5
for record_type in RECORD_TYPES:
try:
logging.info(f"Querying {record_type} records for {domain}")
answers = resolver.resolve(domain, record_type)
records = []
for rdata in answers:
if record_type == 'MX':
# MX records have preference and exchange
records.append(f"{rdata.preference} {rdata.exchange}")
elif record_type == 'SOA':
# SOA records have multiple fields
records.append(f"{rdata.mname} {rdata.rname}")
elif record_type == 'SRV':
# SRV records have priority, weight, port, and target
records.append(f"{rdata.priority} {rdata.weight} {rdata.port} {rdata.target}")
elif record_type == 'TXT':
# TXT records can have multiple strings
txt_data = ' '.join([s.decode() if isinstance(s, bytes) else str(s) for s in rdata.strings])
records.append(txt_data)
else:
records.append(str(rdata))
if records:
results[record_type] = records
logging.info(f"Found {len(records)} {record_type} record(s)")
except dns.resolver.NoAnswer:
logging.debug(f"No {record_type} records found for {domain}")
continue
except dns.resolver.NXDOMAIN:
logging.warning(f"Domain {domain} does not exist")
return None
except dns.resolver.Timeout:
logging.warning(f"Timeout querying {record_type} for {domain}")
continue
except Exception as e:
logging.error(f"Error querying {record_type} for {domain}: {e}")
continue
return results
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle the !dns command.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("dns"):
logging.info("Received !dns command")
args = match.args()
if len(args) != 1:
await bot.api.send_text_message(
room.room_id,
"Usage: !dns <domain>\nExample: !dns example.com"
)
logging.info("Sent usage message for !dns")
return
domain = args[0].lower().strip()
# Remove protocol if present
domain = domain.replace('http://', '').replace('https://', '')
# Remove trailing slash if present
domain = domain.rstrip('/')
# Remove www. prefix if present (optional - you can keep it if you want)
# domain = domain.replace('www.', '')
# Validate domain
if not is_valid_domain(domain):
await bot.api.send_text_message(
room.room_id,
f"Invalid domain name: {domain}"
)
logging.warning(f"Invalid domain provided: {domain}")
return
try:
logging.info(f"Starting DNS reconnaissance for {domain}")
# Send "working on it" message for longer queries
await bot.api.send_text_message(
room.room_id,
f"🔍 Performing DNS reconnaissance on {domain}..."
)
# Query DNS records
results = await query_dns_records(domain)
if results is None:
await bot.api.send_text_message(
room.room_id,
f"Domain {domain} does not exist (NXDOMAIN)"
)
return
if not results:
await bot.api.send_text_message(
room.room_id,
f"No DNS records found for {domain}"
)
return
# Format the output
output = f"<strong>🔍 DNS Records for {domain}</strong><br><br>"
# Order the records in a logical way
preferred_order = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'SRV', 'PTR']
for record_type in preferred_order:
if record_type in results:
output += format_dns_record(record_type, results[record_type])
output += "<br>"
# Add any remaining record types not in preferred order
for record_type in results:
if record_type not in preferred_order:
output += format_dns_record(record_type, results[record_type])
output += "<br>"
# Wrap in collapsible details if output is large
if output.count('<br>') > 15:
output = f"<details><summary><strong>🔍 DNS Records for {domain}</strong></summary>{output}</details>"
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent DNS records for {domain}")
except Exception as e:
await bot.api.send_text_message(
room.room_id,
f"An error occurred while performing DNS lookup: {str(e)}"
)
logging.error(f"Error in DNS plugin for {domain}: {e}", exc_info=True)

View File

@@ -69,6 +69,14 @@ async def handle_command(room, message, bot, prefix, config):
<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> <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>
<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>!sd [prompt]</strong></summary> <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> <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>

View File

@@ -57,7 +57,10 @@ async def load_plugin(plugin_name):
'youtube-preview': 'plugins.youtube-preview', 'youtube-preview': 'plugins.youtube-preview',
'youtube-search': 'plugins.youtube-search', 'youtube-search': 'plugins.youtube-search',
'weather': 'plugins.weather', 'weather': 'plugins.weather',
'urbandictionary': 'plugins.urbandictionary' 'urbandictionary': 'plugins.urbandictionary',
'bitcoin':'plugins.bitcoin',
'dns':'plugins.dns',
'shodan':'plugins.shodan'
} }
# Get the module path from the mapping # Get the module path from the mapping

330
plugins/shodan.py Normal file
View File

@@ -0,0 +1,330 @@
"""
This plugin provides Shodan.io integration for security research and reconnaissance.
"""
import logging
import os
import requests
import simplematrixbotlib as botlib
from dotenv import load_dotenv
# Load environment variables from .env file
plugin_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(plugin_dir)
dotenv_path = os.path.join(parent_dir, '.env')
load_dotenv(dotenv_path)
SHODAN_API_KEY = os.getenv("SHODAN_KEY", "")
SHODAN_API_BASE = "https://api.shodan.io"
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle Shodan commands.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (Bot): The bot object.
prefix (str): The command prefix.
config (dict): Configuration parameters.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("shodan"):
logging.info("Received !shodan command")
# Check if API key is configured
if not SHODAN_API_KEY:
await bot.api.send_text_message(
room.room_id,
"Shodan API key not configured. Please set SHODAN_KEY environment variable."
)
logging.error("Shodan API key not configured")
return
args = match.args()
if len(args) < 1:
await show_usage(room, bot)
return
subcommand = args[0].lower()
if subcommand == "ip":
if len(args) < 2:
await bot.api.send_text_message(room.room_id, "Usage: !shodan ip <ip_address>")
return
ip = args[1]
await shodan_ip_lookup(room, bot, ip)
elif subcommand == "search":
if len(args) < 2:
await bot.api.send_text_message(room.room_id, "Usage: !shodan search <query>")
return
query = ' '.join(args[1:])
await shodan_search(room, bot, query)
elif subcommand == "host":
if len(args) < 2:
await bot.api.send_text_message(room.room_id, "Usage: !shodan host <domain/ip>")
return
host = args[1]
await shodan_host(room, bot, host)
elif subcommand == "count":
if len(args) < 2:
await bot.api.send_text_message(room.room_id, "Usage: !shodan count <query>")
return
query = ' '.join(args[1:])
await shodan_count(room, bot, query)
else:
await show_usage(room, bot)
async def show_usage(room, bot):
"""Display Shodan command usage."""
usage = """
<strong>🔍 Shodan Commands:</strong>
<strong>!shodan ip &lt;ip_address&gt;</strong> - Get detailed information about an IP
<strong>!shodan search &lt;query&gt;</strong> - Search Shodan database
<strong>!shodan host &lt;domain/ip&gt;</strong> - Get host information
<strong>!shodan count &lt;query&gt;</strong> - Count results for a search query
<strong>Search Examples:</strong>
• <code>!shodan search apache</code>
• <code>!shodan search "port:22"</code>
• <code>!shodan search "country:US product:nginx"</code>
• <code>!shodan search "net:192.168.1.0/24"</code>
"""
await bot.api.send_markdown_message(room.room_id, usage)
async def shodan_ip_lookup(room, bot, ip):
"""Look up information about a specific IP address."""
try:
url = f"{SHODAN_API_BASE}/shodan/host/{ip}"
params = {"key": SHODAN_API_KEY}
logging.info(f"Fetching Shodan IP info for: {ip}")
response = requests.get(url, params=params, timeout=15)
if response.status_code == 404:
await bot.api.send_text_message(room.room_id, f"No information found for IP: {ip}")
return
elif response.status_code == 401:
await bot.api.send_text_message(room.room_id, "Invalid Shodan API key")
return
elif response.status_code != 200:
await bot.api.send_text_message(room.room_id, f"Shodan API error: {response.status_code}")
return
data = response.json()
# Format the response
output = f"<strong>🔍 Shodan IP Lookup: {ip}</strong><br><br>"
if data.get('country_name'):
output += f"<strong>📍 Location:</strong> {data.get('city', 'N/A')}, {data.get('country_name', 'N/A')}<br>"
if data.get('org'):
output += f"<strong>🏢 Organization:</strong> {data['org']}<br>"
if data.get('os'):
output += f"<strong>💻 Operating System:</strong> {data['os']}<br>"
if data.get('ports'):
output += f"<strong>🔌 Open Ports:</strong> {', '.join(map(str, data['ports']))}<br>"
output += f"<strong>🕒 Last Update:</strong> {data.get('last_update', 'N/A')}<br><br>"
# Show services
if data.get('data'):
output += "<strong>📡 Services:</strong><br>"
for service in data['data'][:5]: # Limit to first 5 services
port = service.get('port', 'N/A')
product = service.get('product', 'Unknown')
version = service.get('version', '')
banner = service.get('data', '')[:100] + "..." if len(service.get('data', '')) > 100 else service.get('data', '')
output += f" • <strong>Port {port}:</strong> {product} {version}<br>"
if banner:
output += f" <em>{banner}</em><br>"
if len(data['data']) > 5:
output += f" • ... and {len(data['data']) - 5} more services<br>"
# Wrap in collapsible if output is large
if len(output) > 500:
output = f"<details><summary><strong>🔍 Shodan IP Lookup: {ip}</strong></summary>{output}</details>"
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan IP info for {ip}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error fetching Shodan data: {str(e)}")
logging.error(f"Error in shodan_ip_lookup: {e}")
async def shodan_search(room, bot, query):
"""Search the Shodan database."""
try:
url = f"{SHODAN_API_BASE}/shodan/host/search"
params = {
"key": SHODAN_API_KEY,
"query": query,
"minify": True,
"limit": 5 # Limit results to avoid huge responses
}
logging.info(f"Searching Shodan for: {query}")
response = requests.get(url, params=params, timeout=15)
if response.status_code != 200:
await handle_shodan_error(room, bot, response.status_code)
return
data = response.json()
if not data.get('matches'):
await bot.api.send_text_message(room.room_id, f"No results found for: {query}")
return
output = f"<strong>🔍 Shodan Search: '{query}'</strong><br>"
output += f"<strong>Total Results:</strong> {data.get('total', 0):,}<br><br>"
for match in data['matches'][:5]: # Show first 5 results
ip = match.get('ip_str', 'N/A')
port = match.get('port', 'N/A')
org = match.get('org', 'Unknown')
product = match.get('product', 'Unknown')
output += f"<strong>🌐 {ip}:{port}</strong><br>"
output += f" • <strong>Organization:</strong> {org}<br>"
output += f" • <strong>Service:</strong> {product}<br>"
if match.get('location'):
loc = match['location']
if loc.get('city') and loc.get('country_name'):
output += f" • <strong>Location:</strong> {loc['city']}, {loc['country_name']}<br>"
output += "<br>"
if data.get('total', 0) > 5:
output += f"<em>Showing 5 of {data['total']:,} results. Refine your search for more specific results.</em>"
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan search results for: {query}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error searching Shodan: {str(e)}")
logging.error(f"Error in shodan_search: {e}")
async def shodan_host(room, bot, host):
"""Get host information (domain or IP)."""
try:
url = f"{SHODAN_API_BASE}/dns/domain/{host}"
params = {"key": SHODAN_API_KEY}
logging.info(f"Fetching Shodan host info for: {host}")
response = requests.get(url, params=params, timeout=15)
if response.status_code == 404:
# Try IP lookup instead
await shodan_ip_lookup(room, bot, host)
return
elif response.status_code != 200:
await handle_shodan_error(room, bot, response.status_code)
return
data = response.json()
output = f"<strong>🔍 Shodan Host: {host}</strong><br><br>"
if data.get('subdomains'):
output += f"<strong>🌐 Subdomains ({len(data['subdomains'])}):</strong><br>"
for subdomain in sorted(data['subdomains'])[:10]: # Show first 10
output += f"{subdomain}.{host}<br>"
if len(data['subdomains']) > 10:
output += f" • ... and {len(data['subdomains']) - 10} more<br>"
if data.get('tags'):
output += f"<br><strong>🏷️ Tags:</strong> {', '.join(data['tags'])}<br>"
if data.get('data'):
output += f"<br><strong>📊 Records Found:</strong> {len(data['data'])}<br>"
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan host info for: {host}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error fetching host info: {str(e)}")
logging.error(f"Error in shodan_host: {e}")
async def shodan_count(room, bot, query):
"""Count results for a search query."""
try:
url = f"{SHODAN_API_BASE}/shodan/host/count"
params = {
"key": SHODAN_API_KEY,
"query": query
}
logging.info(f"Counting Shodan results for: {query}")
response = requests.get(url, params=params, timeout=15)
if response.status_code != 200:
await handle_shodan_error(room, bot, response.status_code)
return
data = response.json()
output = f"<strong>🔍 Shodan Count: '{query}'</strong><br><br>"
output += f"<strong>Total Results:</strong> {data.get('total', 0):,}<br>"
# Show top countries if available
if data.get('facets') and 'country' in data['facets']:
output += "<br><strong>🌍 Top Countries:</strong><br>"
for country in data['facets']['country'][:5]:
output += f"{country['value']}: {country['count']:,}<br>"
# Show top organizations if available
if data.get('facets') and 'org' in data['facets']:
output += "<br><strong>🏢 Top Organizations:</strong><br>"
for org in data['facets']['org'][:5]:
output += f"{org['value']}: {org['count']:,}<br>"
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Sent Shodan count for: {query}")
except requests.exceptions.Timeout:
await bot.api.send_text_message(room.room_id, "Shodan API request timed out")
logging.error("Shodan API timeout")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error counting Shodan results: {str(e)}")
logging.error(f"Error in shodan_count: {e}")
async def handle_shodan_error(room, bot, status_code):
"""Handle Shodan API errors."""
error_messages = {
401: "Invalid Shodan API key",
403: "Access denied - check API key permissions",
404: "No results found",
429: "Rate limit exceeded - try again later",
500: "Shodan API server error",
503: "Shodan API temporarily unavailable"
}
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}")