various plugin refactors and fixes

This commit is contained in:
2026-05-09 04:51:50 -05:00
parent f822d6a450
commit 5c6234a317
25 changed files with 2044 additions and 3674 deletions
+113 -50
View File
@@ -1,14 +1,17 @@
"""
This plugin provides IP geolocation functionality using free APIs.
IP geolocation plugin uses ip-api.com (primary) and ipapi.co (fallback).
Outputs a formatted code block with emojis and perfectly aligned columns.
"""
import logging
import aiohttp
import simplematrixbotlib as botlib
import socket
import re
from plugins.common import is_public_destination, html_escape, collapsible_summary
from plugins.common import is_public_destination, html_escape, collapsible_summary, code_block
async def is_valid_ip(ip):
"""Check if the provided string is a valid IP address."""
try:
socket.inet_pton(socket.AF_INET, ip)
return True
@@ -20,18 +23,21 @@ async def is_valid_ip(ip):
return False
def is_domain(domain):
"""Check if the provided string is a domain name."""
domain_pattern = re.compile(
r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
)
return bool(domain_pattern.match(domain))
async def resolve_domain(domain):
"""Resolve a domain name to an IP address."""
try:
return socket.gethostbyname(domain)
except socket.gaierror:
return None
async def query_ip_api_com(ip):
"""Query ip-api.com for geolocation information."""
url = f"http://ip-api.com/json/{ip}"
try:
async with aiohttp.ClientSession() as session:
@@ -43,6 +49,7 @@ async def query_ip_api_com(ip):
return None
async def query_ipapi_co(ip):
"""Query ipapi.co for geolocation information (fallback)."""
url = f"https://ipapi.co/{ip}/json/"
try:
async with aiohttp.ClientSession() as session:
@@ -54,69 +61,125 @@ async def query_ipapi_co(ip):
return None
async def query_geolocation(ip):
"""Query geolocation using primary and fallback APIs."""
data = await query_ip_api_com(ip)
if not data or data.get('status') == 'fail':
data = await query_ipapi_co(ip)
return data
async def format_geolocation_results(ip, data):
if not data or ('status' in data and data.get('status') == 'fail'):
return f"🔍 No geolocation data found for {ip}."
country = data.get('country', 'N/A')
country_code = data.get('countryCode', 'N/A')
region = data.get('regionName', data.get('region', 'N/A'))
city = data.get('city', 'N/A')
postal = data.get('zip', 'N/A')
latitude = data.get('lat', 'N/A')
longitude = data.get('lon', 'N/A')
timezone = data.get('timezone', 'N/A')
isp = data.get('isp', 'N/A')
org = data.get('org', 'N/A')
asn = data.get('as', 'N/A')
content = (f"<strong>Country:</strong> {country} ({country_code})<br>"
f"<strong>Region:</strong> {region}<br>"
f"<strong>City:</strong> {city}<br>"
f"<strong>Postal Code:</strong> {postal}<br>"
f"<strong>Coordinates:</strong> {latitude}, {longitude}<br>"
f"<strong>Timezone:</strong> {timezone}<br>"
f"<strong>ISP/Organization:</strong> {isp}<br>"
f"<strong>ASN:</strong> {asn}<br>")
return collapsible_summary(f"🔍 Geolocation: {ip}", content)
async def handle_command(room, message, bot, prefix, config):
"""Handle the !geo command."""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("geo"):
args = match.args()
if len(args) < 1:
await bot.api.send_text_message(room.room_id, "Usage: !geo <ip/domain>")
await bot.api.send_text_message(
room.room_id,
"Usage: !geo <ip_address/domain>\nExample: !geo 8.8.8.8\nExample: !geo example.com"
)
return
query = args[0].strip()
ip = query
if is_domain(query):
await bot.api.send_text_message(room.room_id, f"🔍 Resolving domain {html_escape(query)}...")
ip = await resolve_domain(query)
if not ip:
await bot.api.send_text_message(room.room_id, f"Failed to resolve {html_escape(query)}.")
logging.info(f"Received !geo command for: {query}")
try:
ip = query
if is_domain(query):
await bot.api.send_text_message(
room.room_id,
f"🔍 Resolving domain {html_escape(query)} to IP address..."
)
ip = await resolve_domain(query)
if not ip:
await bot.api.send_text_message(room.room_id,
f"Failed to resolve domain {html_escape(query)} to IP address.")
return
if not is_public_destination(ip):
await bot.api.send_text_message(room.room_id,
"❌ That domain resolves to a private/internal IP, geo not allowed.")
return
await bot.api.send_text_message(room.room_id,
f"Domain {html_escape(query)} resolved to IP {ip}")
elif not await is_valid_ip(query):
await bot.api.send_text_message(room.room_id,
f"Invalid IP address or domain format: {html_escape(query)}")
return
if not is_public_destination(ip):
await bot.api.send_text_message(room.room_id, "❌ Domain resolves to private IP.")
return
await bot.api.send_text_message(room.room_id, f"Resolved to {ip}")
elif not await is_valid_ip(query):
await bot.api.send_text_message(room.room_id, f"Invalid IP/domain: {html_escape(query)}")
return
else:
if not is_public_destination(ip):
await bot.api.send_text_message(room.room_id, "❌ Private IP not allowed.")
else:
if not is_public_destination(ip):
await bot.api.send_text_message(room.room_id,
"❌ Geolocation of private IP addresses is not allowed.")
return
await bot.api.send_text_message(room.room_id,
f"🔍 Looking up geolocation for {ip}...")
geo_data = await query_geolocation(ip)
if not geo_data or ('status' in geo_data and geo_data.get('status') == 'fail'):
await bot.api.send_text_message(room.room_id, f"No geolocation data found for {ip}.")
return
geo_data = await query_geolocation(ip)
result = await format_geolocation_results(ip, geo_data)
await bot.api.send_markdown_message(room.room_id, result)
# Build rows
rows = []
if 'country' in geo_data: # ip-api.com format
country = geo_data.get('country', 'N/A')
country_code = geo_data.get('countryCode', 'N/A')
region = geo_data.get('regionName', geo_data.get('region', 'N/A'))
city = geo_data.get('city', 'N/A')
postal = geo_data.get('zip', 'N/A')
latitude = geo_data.get('lat', 'N/A')
longitude = geo_data.get('lon', 'N/A')
timezone = geo_data.get('timezone', 'N/A')
isp = geo_data.get('isp', 'N/A')
org = geo_data.get('org', 'N/A')
asn = geo_data.get('as', 'N/A')
else: # ipapi.co format
country = geo_data.get('country_name', geo_data.get('country', 'N/A'))
country_code = geo_data.get('country_code', geo_data.get('countryCode', 'N/A'))
region = geo_data.get('region', 'N/A')
city = geo_data.get('city', 'N/A')
postal = geo_data.get('postal', 'N/A')
latitude = geo_data.get('latitude', 'N/A')
longitude = geo_data.get('longitude', 'N/A')
timezone = geo_data.get('timezone', 'N/A')
isp = geo_data.get('org', 'N/A')
org = geo_data.get('org', 'N/A')
asn = geo_data.get('asn', 'N/A')
__version__ = "1.0.2"
rows.append(("🌍", "Country", f"{country} ({country_code})"))
rows.append(("🏙️", "City", city))
if region and region != city:
rows.append(("🏷️", "Region", region))
if postal and postal != 'N/A':
rows.append(("📮", "Postal Code", postal))
rows.append(("📍", "Coordinates", f"{latitude}, {longitude}"))
rows.append(("🕒", "Timezone", timezone))
rows.append(("📡", "ISP", isp))
if org and org != isp:
rows.append(("🏢", "Organization", org))
if asn and asn != 'N/A':
rows.append(("🔢", "ASN", asn))
sections = [{"title": "", "rows": rows}]
block = code_block(f"🔍 IP Geolocation for {ip}", sections)
output = collapsible_summary(f"🔍 Geolocation: {ip}", block)
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Successfully sent geolocation results for {ip}")
except Exception as e:
await bot.api.send_text_message(room.room_id,
f"An error occurred during geolocation lookup for {html_escape(query)}.")
logging.error(f"Error in geo plugin for {query}: {e}", exc_info=True)
# ---------------------------------------------------------------------------
# Plugin Metadata
# ---------------------------------------------------------------------------
__version__ = "1.1.1"
__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> or <code>!geo &lt;domain&gt;</code></li></ul></details>"""
__help__ = """
<details>
<summary><strong>!geo</strong> IP / domain geolocation</summary>
<p><code>!geo &lt;ip or domain&gt;</code> Locate an IP address or domain. Shows country, city, coordinates, ISP, ASN, etc.</p>
</details>
"""