186 lines
7.6 KiB
Python
186 lines
7.6 KiB
Python
"""
|
||
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, 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
|
||
except socket.error:
|
||
try:
|
||
socket.inet_pton(socket.AF_INET6, ip)
|
||
return True
|
||
except socket.error:
|
||
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:
|
||
async with session.get(url) as response:
|
||
if response.status == 200:
|
||
return await response.json()
|
||
except Exception as e:
|
||
logging.error(f"ip-api.com error: {e}")
|
||
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:
|
||
async with session.get(url) as response:
|
||
if response.status == 200:
|
||
return await response.json()
|
||
except Exception as e:
|
||
logging.error(f"ipapi.co error: {e}")
|
||
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 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_address/domain>\nExample: !geo 8.8.8.8\nExample: !geo example.com"
|
||
)
|
||
return
|
||
query = args[0].strip()
|
||
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
|
||
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
|
||
|
||
# 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')
|
||
|
||
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>
|
||
<p><code>!geo <ip or domain></code> – Locate an IP address or domain. Shows country, city, coordinates, ISP, ASN, etc.</p>
|
||
</details>
|
||
"""
|