diff --git a/plugins/ai.json b/plugins/disabled/ai.json similarity index 100% rename from plugins/ai.json rename to plugins/disabled/ai.json diff --git a/plugins/ai.py b/plugins/disabled/ai.py similarity index 100% rename from plugins/ai.py rename to plugins/disabled/ai.py diff --git a/plugins/help.py b/plugins/help.py index ec8efee..633a27d 100644 --- a/plugins/help.py +++ b/plugins/help.py @@ -527,7 +527,7 @@ Search Exploit-DB for security vulnerabilities and exploits. Returns detailed in -
๐Ÿค– Funguy Bot AI Commands +
๐ŸŒŸ Funguy Bot Credits

diff --git a/plugins/timezone.py b/plugins/timezone.py new file mode 100644 index 0000000..2435a56 --- /dev/null +++ b/plugins/timezone.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +""" +Time Zone Plugin โ€“ completely hardcoded-free using Open-Meteo APIs. +""" + +import logging +import aiohttp +import simplematrixbotlib as botlib +from urllib.parse import quote +from datetime import datetime + +def format_ampm(dt_str: str) -> str: + """Convert ISO datetime to AM/PM format.""" + try: + if '+' in dt_str: + dt_str = dt_str.split('+')[0] + if '.' in dt_str: + dt_str = dt_str.split('.')[0] + dt_str = dt_str.replace('T', ' ') + dt = datetime.fromisoformat(dt_str) + return dt.strftime("%I:%M:%S %p").lstrip("0") + except: + return dt_str + +async def geocode_city(session: aiohttp.ClientSession, city: str) -> tuple[float, float, str] | None: + """ + Open-Meteo Geocoding API (free, no key, no hardcoding). + Returns (latitude, longitude, display_name) or None. + """ + url = f"https://geocoding-api.open-meteo.com/v1/search?name={quote(city)}&count=1&language=en&format=json" + + try: + async with session.get(url, timeout=10) as resp: + if resp.status == 200: + data = await resp.json() + if data.get("results") and len(data["results"]) > 0: + result = data["results"][0] + lat = result["latitude"] + lon = result["longitude"] + name = result.get("name", city) + country = result.get("country", "") + admin1 = result.get("admin1", "") + + # Build display name: "Lahore, Punjab, Pakistan" + display_parts = [name] + if admin1 and admin1 != name: + display_parts.append(admin1) + if country: + display_parts.append(country) + display_name = ", ".join(display_parts) + + logging.info(f"Geocoded: {city} โ†’ {display_name} ({lat}, {lon})") + return lat, lon, display_name + else: + logging.warning(f"Geocoding API HTTP {resp.status} for {city}") + except Exception as e: + logging.warning(f"Geocoding error: {e}") + return None + +async def get_timezone(lat: float, lon: float) -> str | None: + """ + Get timezone name from coordinates using timezonedb (free tier, no key). + Alternative: use Open-Meteo's time API directly. + """ + # Open-Meteo's time API accepts coordinates directly + # We'll use this instead of timezonedb + return None # Will be handled in fetch_time_by_coords + +async def fetch_time_by_coords(session: aiohttp.ClientSession, lat: float, lon: float) -> dict | None: + """ + Get current time using Open-Meteo (no key required). + """ + url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true&timezone=auto&timeformat=unixtime" + + try: + async with session.get(url, timeout=10) as resp: + if resp.status == 200: + data = await resp.json() + current = data.get("current_weather", {}) + timezone = data.get("timezone", "Unknown") + unixtime = current.get("time") + temperature = current.get("temperature") + + if unixtime: + # Convert UNIX timestamp to datetime + dt = datetime.fromtimestamp(unixtime) + return { + "datetime": dt.isoformat(), + "timezone": timezone, + "temperature": temperature + } + except Exception as e: + logging.warning(f"Time fetch error: {e}") + return None + +async def fetch_time_by_zone(session: aiohttp.ClientSession, zone: str) -> dict | None: + """Get current time for a named timezone using Open-Meteo.""" + # Open-Meteo doesn't have named timezone endpoint, need to geocode a representative city + # Fallback to worldtimeapi.org for IANA zones + url = f"http://worldtimeapi.org/api/timezone/{zone}" + try: + async with session.get(url, timeout=10) as resp: + if resp.status == 200: + return await resp.json() + except Exception as e: + logging.warning(f"Timezone API error: {e}") + return None + +async def resolve_time(session: aiohttp.ClientSession, query: str) -> tuple[dict | None, str]: + """Main resolution: geocode any city, then get time.""" + query = query.strip().lower() + + # Check if it's an IANA zone (contains '/') + if '/' in query or query in ("utc", "gmt"): + data = await fetch_time_by_zone(session, query) + if data: + return data, query.upper() + return None, f"Timezone '{query}' not found" + + # Geocode the city (no hardcoding!) + geocode_result = await geocode_city(session, query) + if not geocode_result: + return None, f"Could not find city '{query}'. Try being more specific." + + lat, lon, display_name = geocode_result + + # Get time from coordinates + data = await fetch_time_by_coords(session, lat, lon) + if not data: + return None, f"Could not get time for '{display_name}'" + + return data, display_name + +def format_response(data: dict, display_name: str) -> str: + """Format time data into HTML.""" + raw_time = data.get("datetime", "") + local_time = format_ampm(raw_time) if raw_time else "Unknown" + tz = data.get("timezone", "Unknown") + temp = data.get("temperature") + temp_str = f"
๐ŸŒก๏ธ Temperature: {temp}ยฐC" if temp is not None else "" + + return f""" +

+๐Ÿ•’ Time in {display_name} +

+๐Ÿ“ Timezone: {tz}
+๐Ÿ“… Local time: {local_time}{temp_str} +

+
+""" + +def help_text() -> str: + return """ +
+๐Ÿ•’ Time Plugin Help +

+!time <any city> โ€“ Get current time for ANY city worldwide
+!time <IANA zone> โ€“ e.g., Europe/London, Asia/Karachi
+!time help โ€“ Show this help

+Examples:
+!time Lahore
+!time New York
+!time Paris
+!time Asia/Karachi

+No city names are hardcoded. The bot uses Open-Meteo's geocoding API. +

+
+""" + +def setup(bot): + logging.info("Time plugin (zero hardcoded cities) loaded.") + +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("time")): + return + + args = match.args() + if not args or args[0].lower() == "help": + await bot.api.send_markdown_message(room.room_id, help_text()) + return + + query = " ".join(args).strip() + await bot.api.send_text_message(room.room_id, f"๐Ÿ•’ Looking up time for: {query}...") + + async with aiohttp.ClientSession() as session: + data, display = await resolve_time(session, query) + if data is None: + await bot.api.send_text_message(room.room_id, f"โŒ {display}") + return + await bot.api.send_markdown_message(room.room_id, format_response(data, display)) + logging.info(f"Time sent for {query}") diff --git a/plugins/welcome.py b/plugins/welcome.py index b974e3b..1973ee3 100644 --- a/plugins/welcome.py +++ b/plugins/welcome.py @@ -2,7 +2,7 @@ Plugin for welcoming new users to the room. Features: - * Automatically greets users when they join the target room. + * Automatically greets users when they first join the target room. * !welcome command โ€“ manually trigger the welcome message for yourself. * Per-user cooldown to prevent welcome spam on repeated part/join cycles. @@ -52,17 +52,7 @@ def _record_welcome(user_id: str) -> None: def _build_welcome_message(display_name: str) -> str: """Return the Markdown welcome message for a given display name.""" - return ( - f"Welcome to the room, **{display_name}**! ๐ŸŽ‰\n\n" - "We're glad to have you here in " - "**Selfโ€‘hosting | Security | Sysadmin | Homelab | Programming**!\n\n" - "To help us get to know you better:\n" - "โ€ข ๐Ÿ”ง **What do you selfโ€‘host?** Got any cool services running?\n" - "โ€ข ๐Ÿ” **Are you into cybersecurity?** What areas interest you most?\n" - "โ€ข ๐Ÿ’ป **What programming languages do you use?**\n" - "โ€ข ๐Ÿ  **Tell us about your homelab setup!**\n\n" - "Feel free to introduce yourself and jump into the conversation! ๐Ÿ„" - ) + return f"Hey **{display_name}**, welcome in โ€” so glad to have you here! ๐ŸŽ‰ Feel free to tell us a bit about what you're into (selfโ€‘hosting, security, homelabs, programmingโ€ฆ or just lurk and say hi whenever ๐Ÿ„)" # --------------------------------------------------------------------------- @@ -97,6 +87,28 @@ def setup(bot): logging.debug("Ignoring bot's own join event") return + # Check if this is a genuine first join by looking for previous membership + is_first_join = True + + # Check prev_content directly on the event + if hasattr(event, 'prev_content') and event.prev_content: + prev_membership = event.prev_content.get('membership') + if prev_membership is not None: + is_first_join = False + logging.debug(f"User {event.sender} had previous membership '{prev_membership}' in prev_content") + + # Check unsigned field (some events store prev_content there) + if is_first_join and hasattr(event, 'unsigned') and event.unsigned: + prev_content = event.unsigned.get('prev_content', {}) + prev_membership = prev_content.get('membership') + if prev_membership is not None: + is_first_join = False + logging.debug(f"User {event.sender} had previous membership '{prev_membership}' in unsigned") + + if not is_first_join: + logging.info(f"Skipping welcome for {event.sender} - not a first join (name change or rejoin)") + return + # --- Cooldown check --- if _is_on_cooldown(event.sender): remaining = WELCOME_COOLDOWN_SECONDS - (