#!/usr/bin/env python3 """ Time Zone Plugin – uses pytz for IANA zones and Open‑Meteo for city geocoding. Outputs a clean code block with emojis and aligned columns via shared code_block. """ import logging import aiohttp import simplematrixbotlib as botlib from datetime import datetime import pytz from plugins.common import collapsible_summary, html_escape, code_block # ------------------------------------------------------------------- # Offline helper for IANA timezone names # ------------------------------------------------------------------- def _get_time_for_iana_zone(zone: str) -> dict | None: """Return a dict with datetime, timezone, and optional temperature using pytz.""" try: tz = pytz.timezone(zone) now = datetime.now(tz) return { "datetime": now.isoformat(), "timezone": zone, "temperature": None # no weather for zone lookups } except pytz.UnknownTimeZoneError: return None # ------------------------------------------------------------------- # Online helpers (Open‑Meteo) # ------------------------------------------------------------------- async def _geocode_city(session: aiohttp.ClientSession, city: str) -> tuple[float, float, str] | None: """Geocode a city name via Open‑Meteo. Returns (lat, lon, display_name) or None.""" from urllib.parse import quote 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() results = data.get("results", []) if results: r = results[0] lat = float(r["latitude"]) lon = float(r["longitude"]) name = r.get("name", city) country = r.get("country", "") admin1 = r.get("admin1", "") display = ", ".join(filter(None, [name, admin1, country])) return lat, lon, display except Exception as e: logging.warning(f"Geocoding error: {e}") return None async def _fetch_weather(session: aiohttp.ClientSession, lat: float, lon: float) -> dict | None: """ Fetch current time and temperature from Open‑Meteo (free, no key). The API returns an ISO 8601 string for the current time. """ url = f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true&timezone=auto" try: async with session.get(url, timeout=10) as resp: if resp.status == 200: data = await resp.json() current = data.get("current_weather", {}) time_str = current.get("time") # ISO 8601, local time temp_c = current.get("temperature") tz = data.get("timezone", "Unknown") if time_str: return { "datetime": time_str, # raw ISO string (e.g. "2024-05-09T14:30") "timezone": tz, "temperature": temp_c } except Exception as e: logging.warning(f"Weather fetch error: {e}") return None # ------------------------------------------------------------------- # Main resolver # ------------------------------------------------------------------- async def resolve_time(session: aiohttp.ClientSession, query: str) -> tuple[dict | None, str]: """Return (data_dict, display_name) or (None, error_message).""" query = query.strip() # 1. Try as IANA zone (offline, always works) if '/' in query or query.lower() in ("utc", "gmt"): data = _get_time_for_iana_zone(query) if data: return data, query.upper() else: return None, f"Timezone '{html_escape(query)}' not recognised." # 2. Otherwise geocode as a city name geocode_result = await _geocode_city(session, query) if not geocode_result: return None, f"Could not find city '{html_escape(query)}'. Try a more specific name or use an IANA zone." lat, lon, display_name = geocode_result weather_data = await _fetch_weather(session, lat, lon) if weather_data: return weather_data, display_name return None, f"Could not fetch time/weather for '{html_escape(display_name)}'." # ------------------------------------------------------------------- # Formatting – uses shared code_block from common.py # ------------------------------------------------------------------- def _format_time_output(data: dict, display_name: str) -> str: """Convert time data into a code block via the shared formatter.""" raw_time = data.get("datetime", "") # Convert ISO string to AM/PM format try: if '+' in raw_time: raw_time = raw_time.split('+')[0] dt = datetime.fromisoformat(raw_time) local_time = dt.strftime("%I:%M:%S %p").lstrip("0") except Exception: local_time = raw_time tz_display = data.get("timezone", "Unknown") temp = data.get("temperature") if temp is not None: temp_f = round(temp * 9/5 + 32, 1) temp_str = f"{temp:.1f}°C / {temp_f:.1f}°F" else: temp_str = "N/A" rows = [ ("🌐", "Location", display_name), ("🕒", "Local Time", local_time), ("📅", "Timezone", tz_display), ("🌡️", "Temperature", temp_str), ] # Wrap rows in a single section with no title (title is part of code_block's main title) sections = [{"title": "", "rows": rows}] return code_block("🕒 Time Info", sections) # ------------------------------------------------------------------- # Help # ------------------------------------------------------------------- _HELP_MD = """
🕒 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 Europe/London
No city names are hardcoded. IANA zones work completely offline.

""" # ------------------------------------------------------------------- # Plugin lifecycle # ------------------------------------------------------------------- def setup(bot): logging.info("Time plugin (offline IANA zones + Open‑Meteo cities) loaded.") async def handle_command(room, message, bot, prefix, config): import simplematrixbotlib as botlib 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_MD) return query = " ".join(args).strip() await bot.api.send_text_message(room.room_id, f"🕒 Looking up time for: {html_escape(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 block = _format_time_output(data, display) output = collapsible_summary(f"🕒 Time in {html_escape(display)}", block) await bot.api.send_markdown_message(room.room_id, output) logging.info(f"Time sent for {query}") __version__ = "1.1.2" __author__ = "Funguy Bot" __description__ = "World clock (offline IANA zones + free geocoding)" __help__ = _HELP_MD