""" 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 \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__ = """
!geo – IP / domain geolocation

!geo <ip or domain> – Locate an IP address or domain. Shows country, city, coordinates, ISP, ASN, etc.

"""