From 77bdff69b628240f3a7ec1aa18f12dcb3a262f95 Mon Sep 17 00:00:00 2001 From: whitefang <83544729+vantough@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:01:29 +0530 Subject: [PATCH 1/4] Add Redis keepalive background updater on startup --- pyUltroid/__main__.py | 2 ++ pyUltroid/startup/_database.py | 4 ++-- pyUltroid/startup/funcs.py | 30 ++++++++++++++++++++++++++++++ pyUltroid/version.py | 4 ++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/pyUltroid/__main__.py b/pyUltroid/__main__.py index e87d368e47..061b79ea7e 100644 --- a/pyUltroid/__main__.py +++ b/pyUltroid/__main__.py @@ -18,6 +18,7 @@ def main(): WasItRestart, autopilot, customize, + keep_redis_alive, plug, ready, startup_stuff, @@ -49,6 +50,7 @@ def main(): LOGS.info("Initialising...") ultroid_bot.run_in_loop(autopilot()) + ultroid_bot.loop.create_task(keep_redis_alive()) pmbot = udB.get_key("PMBOT") manager = udB.get_key("MANAGER") diff --git a/pyUltroid/startup/_database.py b/pyUltroid/startup/_database.py index 2611a5c32d..d6845c9199 100644 --- a/pyUltroid/startup/_database.py +++ b/pyUltroid/startup/_database.py @@ -274,11 +274,11 @@ def __init__( if platform.lower() == "qovery" and not host: var, hash_, host, password = "", "", "", "" for vars_ in os.environ: - if vars_.startswith("QOVERY_REDIS_") and vars.endswith("_HOST"): + if vars_.startswith("QOVERY_REDIS_") and vars_.endswith("_HOST"): var = vars_ if var: hash_ = var.split("_", maxsplit=2)[1].split("_")[0] - if hash: + if hash_: kwargs["host"] = os.environ.get(f"QOVERY_REDIS_{hash_}_HOST") kwargs["port"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PORT") kwargs["password"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PASSWORD") diff --git a/pyUltroid/startup/funcs.py b/pyUltroid/startup/funcs.py index 39ca973d45..eda2fa39c2 100644 --- a/pyUltroid/startup/funcs.py +++ b/pyUltroid/startup/funcs.py @@ -10,6 +10,7 @@ import random import shutil import time +from datetime import datetime, timezone as dt_timezone from random import randint from ..configs import Var @@ -47,6 +48,8 @@ from ..fns.helper import download_file, inline_mention, updater db_url = 0 +REDIS_KEEPALIVE_KEY = "KEEP_ACTIVE" +REDIS_KEEPALIVE_INTERVAL_SECONDS = 7 * 24 * 60 * 60 async def autoupdate_local_database(): @@ -152,6 +155,33 @@ async def startup_stuff(): time.tzset() +async def keep_redis_alive(): + from .. import udB + + if udB.name != "Redis": + return + + interval = udB.get_key("REDIS_KEEPALIVE_INTERVAL") + try: + interval = int(interval) if interval else REDIS_KEEPALIVE_INTERVAL_SECONDS + except (TypeError, ValueError): + interval = REDIS_KEEPALIVE_INTERVAL_SECONDS + interval = max(interval, 60) + + while True: + try: + now = datetime.now(dt_timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") + udB.set_key(REDIS_KEEPALIVE_KEY, f"Updated value at {now}") + LOGS.debug( + "Redis keepalive updated key '%s' (next run in %s seconds).", + REDIS_KEEPALIVE_KEY, + interval, + ) + except Exception as exc: + LOGS.warning("Redis keepalive update failed: %s", exc) + await asyncio.sleep(interval) + + async def autobot(): from .. import udB, ultroid_bot diff --git a/pyUltroid/version.py b/pyUltroid/version.py index 592d3fd0b6..9752b2c2ec 100644 --- a/pyUltroid/version.py +++ b/pyUltroid/version.py @@ -1,2 +1,2 @@ -__version__ = "2025.05.31" -ultroid_version = "2.1.1" +__version__ = "2026.04.03" +ultroid_version = "2.1.2" From d6a98b5d634856d6a74822b14e116921eb3f7246 Mon Sep 17 00:00:00 2001 From: whitefang <83544729+vantough@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:00:35 +0530 Subject: [PATCH 2/4] Improve weather/air error handling and messaging --- plugins/weather.py | 109 +++++++++++++++++++++++++++---------------- pyUltroid/version.py | 4 +- 2 files changed, 72 insertions(+), 41 deletions(-) diff --git a/plugins/weather.py b/plugins/weather.py index 26420330ec..92fa8b1166 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -19,11 +19,12 @@ import datetime import time from datetime import timedelta +from json import JSONDecodeError import aiohttp import pytz -from . import async_searcher, get_string, udB, ultroid_cmd +from . import LOGS, async_searcher, get_string, udB, ultroid_cmd async def get_timezone(offset_seconds, use_utc=False): @@ -58,15 +59,56 @@ async def getWindinfo(speed: str, degree: str) -> str: return f"[{dirs[ix % len(dirs)]}] {kmph}" async def get_air_pollution_data(latitude, longitude, api_key): - url = f"http://api.openweathermap.org/data/2.5/air_pollution?lat={latitude}&lon={longitude}&appid={api_key}" - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - data = await response.json() - if "list" in data: - air_pollution = data["list"][0] - return air_pollution - else: - return None + url = f"https://api.openweathermap.org/data/2.5/air_pollution?lat={latitude}&lon={longitude}&appid={api_key}" + timeout = aiohttp.ClientTimeout(total=12) + try: + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url) as response: + data = await response.json() + except TimeoutError: + LOGS.exception("OpenWeather air pollution request timed out.") + return None, "Air quality lookup timed out. Please try again in a moment." + except aiohttp.ClientError: + LOGS.exception("OpenWeather air pollution request failed.") + return None, "Unable to reach the air quality service right now. Try again soon." + except (aiohttp.ContentTypeError, JSONDecodeError, ValueError): + LOGS.exception("Invalid air pollution response payload.") + return None, "Air quality service returned invalid data. Please try again." + + try: + air_pollution = data["list"][0] + except (KeyError, IndexError, TypeError): + LOGS.exception("Air pollution response missing expected fields: %s", data) + return None, "No air quality data found for that location. Try city,country format." + return air_pollution, None + + +async def get_geocode_data(input_str: str): + if input_str.lower() == "perth": + geo_url = "https://geocode.xyz/perth%20au?json=1" + else: + geo_url = f"https://geocode.xyz/{input_str}?json=1" + try: + geo_data = await async_searcher(geo_url, re_json=True, timeout=12) + except TimeoutError: + LOGS.exception("Geocoding request timed out for location: %s", input_str) + return None, "Geocoding timed out. Try city,country format." + except aiohttp.ClientError: + LOGS.exception("Geocoding request failed for location: %s", input_str) + return None, "Unable to reach geocoding service. Try again soon." + except (aiohttp.ContentTypeError, JSONDecodeError, ValueError, TypeError): + LOGS.exception("Geocoding response is not valid JSON for location: %s", input_str) + return None, "Could not read geocoding response. Try city,country format." + + try: + latitude = geo_data["latt"] + longitude = geo_data["longt"] + city = geo_data["standard"]["city"] + prov = geo_data["standard"]["prov"] + except (KeyError, TypeError): + LOGS.exception("Geocoding response missing expected fields: %s", geo_data) + return None, "Location not found. Try city,country format (example: London,GB)." + return {"latitude": latitude, "longitude": longitude, "city": city, "prov": prov}, None @ultroid_cmd(pattern="weather ?(.*)") @@ -83,10 +125,10 @@ async def weather(event): return input_str = event.pattern_match.group(1) if not input_str: - await event.eor("No Location was Given...", time=5) + await event.eor("No location was given. Try city,country format.", time=5) return elif input_str == "butler": - await event.eor("search butler,au for australila", time=5) + await event.eor("Search butler,au for Australia.", time=5) sample_url = f"https://api.openweathermap.org/data/2.5/weather?q={input_str}&APPID={x}&units=metric" try: response_api = await async_searcher(sample_url, re_json=True) @@ -116,7 +158,11 @@ async def weather(event): else: await msg.edit(response_api["message"]) except Exception as e: - await event.eor(f"An unexpected error occurred: {str(e)}", time=5) + LOGS.exception("Weather lookup failed for input: %s", input_str) + await event.eor( + f"Unable to fetch weather right now ({str(e)}). Try city,country format.", + time=5, + ) @ultroid_cmd(pattern="air ?(.*)") @@ -133,39 +179,24 @@ async def air_pollution(event): return input_str = event.pattern_match.group(1) if not input_str: - await event.eor("`No Location was Given...`", time=5) + await event.eor("No location was given. Try city,country format.", time=5) return - if input_str.lower() == "perth": - geo_url = f"https://geocode.xyz/perth%20au?json=1" - else: - geo_url = f"https://geocode.xyz/{input_str}?json=1" - geo_data = await async_searcher(geo_url, re_json=True) - try: - longitude = geo_data["longt"] - latitude = geo_data["latt"] - except KeyError as e: - LOGS.info(e) - await event.eor("`Unable to find coordinates for the given location.`", time=5) + geocode_data, geocode_error = await get_geocode_data(input_str) + if geocode_error: + await event.eor(geocode_error, time=5) return - try: - city = geo_data["standard"]["city"] - prov = geo_data["standard"]["prov"] - except KeyError as e: - LOGS.info(e) - await event.eor("`Unable to find city for the given coordinates.`", time=5) - return - air_pollution_data = await get_air_pollution_data(latitude, longitude, x) - if air_pollution_data is None: - await event.eor( - "`Unable to fetch air pollution data for the given location.`", time=5 - ) + air_pollution_data, air_error = await get_air_pollution_data( + geocode_data["latitude"], geocode_data["longitude"], x + ) + if air_error: + await event.eor(air_error, time=5) return await msg.edit( - f"{city}, {prov}\n\n" + f"{geocode_data['city']}, {geocode_data['prov']}\n\n" f"╭────────────────•\n" f"╰➢ **𝖠𝖰𝖨:** {air_pollution_data['main']['aqi']}\n" f"╰➢ **𝖢𝖺𝗋𝖻𝗈𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['co']}µg/m³\n" - f"╰➢ **𝖭𝗈𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no']}µg/m³\n" + f"╰➢ **𝖭𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no']}µg/m³\n" f"╰➢ **𝖭𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no2']}µg/m³\n" f"╰➢ **𝖮𝗓𝗈𝗇𝖾:** {air_pollution_data['components']['o3']}µg/m³\n" f"╰➢ **𝖲𝗎𝗅𝗉𝗁𝗎𝗋 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['so2']}µg/m³\n" diff --git a/pyUltroid/version.py b/pyUltroid/version.py index 9752b2c2ec..82f667b048 100644 --- a/pyUltroid/version.py +++ b/pyUltroid/version.py @@ -1,2 +1,2 @@ -__version__ = "2026.04.03" -ultroid_version = "2.1.2" +__version__ = "2026.04.05" +ultroid_version = "2.1.3" From b415bad3e11307db3a1d772d588501288adaf10a Mon Sep 17 00:00:00 2001 From: whitefang <83544729+vantough@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:06:12 +0530 Subject: [PATCH 3/4] Refine weather error flows and expand verification fixes --- plugins/weather.py | 46 +++++++++++++++++++++++++------------------- pyUltroid/version.py | 2 +- tasks/lessons.md | 5 +++++ 3 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 tasks/lessons.md diff --git a/plugins/weather.py b/plugins/weather.py index 92fa8b1166..4a93f3e27d 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -18,6 +18,7 @@ import datetime import time +import asyncio from datetime import timedelta from json import JSONDecodeError @@ -65,7 +66,7 @@ async def get_air_pollution_data(latitude, longitude, api_key): async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get(url) as response: data = await response.json() - except TimeoutError: + except (asyncio.TimeoutError, TimeoutError): LOGS.exception("OpenWeather air pollution request timed out.") return None, "Air quality lookup timed out. Please try again in a moment." except aiohttp.ClientError: @@ -90,7 +91,7 @@ async def get_geocode_data(input_str: str): geo_url = f"https://geocode.xyz/{input_str}?json=1" try: geo_data = await async_searcher(geo_url, re_json=True, timeout=12) - except TimeoutError: + except (asyncio.TimeoutError, TimeoutError): LOGS.exception("Geocoding request timed out for location: %s", input_str) return None, "Geocoding timed out. Try city,country format." except aiohttp.ClientError: @@ -129,10 +130,11 @@ async def weather(event): return elif input_str == "butler": await event.eor("Search butler,au for Australia.", time=5) + return sample_url = f"https://api.openweathermap.org/data/2.5/weather?q={input_str}&APPID={x}&units=metric" try: response_api = await async_searcher(sample_url, re_json=True) - if response_api["cod"] == 200: + if str(response_api.get("cod")) == "200": country_time_zone = int(response_api["timezone"]) tz = f"{await get_timezone(country_time_zone)}" sun_rise_time = int(response_api["sys"]["sunrise"]) + country_time_zone @@ -156,11 +158,11 @@ async def weather(event): f"╰────────────────•\n\n" ) else: - await msg.edit(response_api["message"]) - except Exception as e: + await msg.edit(response_api.get("message", "Location not found. Try city,country format.")) + except Exception: LOGS.exception("Weather lookup failed for input: %s", input_str) await event.eor( - f"Unable to fetch weather right now ({str(e)}). Try city,country format.", + "Unable to fetch weather right now. Try city,country format.", time=5, ) @@ -191,17 +193,21 @@ async def air_pollution(event): if air_error: await event.eor(air_error, time=5) return - await msg.edit( - f"{geocode_data['city']}, {geocode_data['prov']}\n\n" - f"╭────────────────•\n" - f"╰➢ **𝖠𝖰𝖨:** {air_pollution_data['main']['aqi']}\n" - f"╰➢ **𝖢𝖺𝗋𝖻𝗈𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['co']}µg/m³\n" - f"╰➢ **𝖭𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no']}µg/m³\n" - f"╰➢ **𝖭𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no2']}µg/m³\n" - f"╰➢ **𝖮𝗓𝗈𝗇𝖾:** {air_pollution_data['components']['o3']}µg/m³\n" - f"╰➢ **𝖲𝗎𝗅𝗉𝗁𝗎𝗋 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['so2']}µg/m³\n" - f"╰➢ **𝖠𝗆𝗆𝗈𝗇𝗂𝖺:** {air_pollution_data['components']['nh3']}µg/m³\n" - f"╰➢ **𝖥𝗂𝗇𝖾 𝖯𝖺𝗋𝗍𝗂𝖼𝗅𝖾𝗌 (PM₂.₅):** {air_pollution_data['components']['pm2_5']}\n" - f"╰➢ **𝖢𝗈𝖺𝗋𝗌𝖾 𝖯𝖺𝗋𝗍𝗂𝖼𝗅𝖾𝗌 (PM₁₀):** {air_pollution_data['components']['pm10']}\n" - f"╰────────────────•\n\n" - ) + try: + await msg.edit( + f"{geocode_data['city']}, {geocode_data['prov']}\n\n" + f"╭────────────────•\n" + f"╰➢ **𝖠𝖰𝖨:** {air_pollution_data['main']['aqi']}\n" + f"╰➢ **𝖢𝖺𝗋𝖻𝗈𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['co']}µg/m³\n" + f"╰➢ **𝖭𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no']}µg/m³\n" + f"╰➢ **𝖭𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no2']}µg/m³\n" + f"╰➢ **𝖮𝗓𝗈𝗇𝖾:** {air_pollution_data['components']['o3']}µg/m³\n" + f"╰➢ **𝖲𝗎𝗅𝗉𝗁𝗎𝗋 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['so2']}µg/m³\n" + f"╰➢ **𝖠𝗆𝗆𝗈𝗇𝗂𝖺:** {air_pollution_data['components']['nh3']}µg/m³\n" + f"╰➢ **𝖥𝗂𝗇𝖾 𝖯𝖺𝗋𝗍𝗂𝖼𝗅𝖾𝗌 (PM₂.₅):** {air_pollution_data['components']['pm2_5']}\n" + f"╰➢ **𝖢𝗈𝖺𝗋𝗌𝖾 𝖯𝖺𝗋𝗍𝗂𝖼𝗅𝖾𝗌 (PM₁₀):** {air_pollution_data['components']['pm10']}\n" + f"╰────────────────•\n\n" + ) + except (KeyError, TypeError): + LOGS.exception("Air pollution data missing display fields: %s", air_pollution_data) + await event.eor("Air quality data is incomplete for this location. Try city,country format.", time=5) diff --git a/pyUltroid/version.py b/pyUltroid/version.py index 82f667b048..145ae9c617 100644 --- a/pyUltroid/version.py +++ b/pyUltroid/version.py @@ -1,2 +1,2 @@ __version__ = "2026.04.05" -ultroid_version = "2.1.3" +ultroid_version = "2.1.4" diff --git a/tasks/lessons.md b/tasks/lessons.md new file mode 100644 index 0000000000..3a414da5a5 --- /dev/null +++ b/tasks/lessons.md @@ -0,0 +1,5 @@ +# Lessons Learned + +- When users ask for follow-up fixes after a review, expand verification beyond syntax checks (e.g., run unit test suites and targeted plugin tests) before finalizing. +- Avoid exposing raw exception text in user-facing bot responses; keep details in logs and return concise actionable guidance. +- For location-hint branches (like `butler`), return immediately after sending the hint to avoid accidental follow-up API calls. From 68b663cefa27e108ebc338561d44975d2156a69c Mon Sep 17 00:00:00 2001 From: whitefang <83544729+vantough@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:06:17 +0530 Subject: [PATCH 4/4] Remove lessons file and fix timezone offset sign handling --- plugins/weather.py | 5 +++-- pyUltroid/version.py | 2 +- tasks/lessons.md | 5 ----- 3 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 tasks/lessons.md diff --git a/plugins/weather.py b/plugins/weather.py index 4a93f3e27d..8a255d7258 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -30,8 +30,9 @@ async def get_timezone(offset_seconds, use_utc=False): offset = timedelta(seconds=offset_seconds) - hours, remainder = divmod(offset.seconds, 3600) - sign = "+" if offset.total_seconds() >= 0 else "-" + total_seconds = int(offset.total_seconds()) + hours = abs(total_seconds) // 3600 + sign = "+" if total_seconds >= 0 else "-" timezone = "UTC" if use_utc else "GMT" if use_utc: for m in pytz.all_timezones: diff --git a/pyUltroid/version.py b/pyUltroid/version.py index 145ae9c617..4035c1ff71 100644 --- a/pyUltroid/version.py +++ b/pyUltroid/version.py @@ -1,2 +1,2 @@ __version__ = "2026.04.05" -ultroid_version = "2.1.4" +ultroid_version = "2.1.5" diff --git a/tasks/lessons.md b/tasks/lessons.md deleted file mode 100644 index 3a414da5a5..0000000000 --- a/tasks/lessons.md +++ /dev/null @@ -1,5 +0,0 @@ -# Lessons Learned - -- When users ask for follow-up fixes after a review, expand verification beyond syntax checks (e.g., run unit test suites and targeted plugin tests) before finalizing. -- Avoid exposing raw exception text in user-facing bot responses; keep details in logs and return concise actionable guidance. -- For location-hint branches (like `butler`), return immediately after sending the hint to avoid accidental follow-up API calls.