diff --git a/VERSION b/VERSION index 6476b3a..c8f955a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.18 +1.8.19 diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 9c3e440..4ad0158 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -12,7 +12,6 @@ from re import Match, Pattern from typing import Any, cast -import aiohttp import defusedxml.ElementTree as etree import geoip2.database import gtts @@ -34,6 +33,7 @@ get_url_youtube_info, print_url_metadata, ) +from torchlight.Utils import Utils class BaseCommand: @@ -295,20 +295,13 @@ def Clean(self, text: str) -> str: ).strip() async def Calculate(self, parameters_json: dict[str, str], player: Player) -> int: - async with aiohttp.ClientSession() as session: - resp = await asyncio.wait_for( - session.get( - "http://api.wolframalpha.com/v2/query", - params=parameters_json, - ), - 10, - ) - if not resp: - return 1 - - data = await asyncio.wait_for(resp.text(), 5) - if not data: - return 2 + data = await Utils.FetchText( + "http://api.wolframalpha.com/v2/query", + params=parameters_json, + timeout=15, + ) + if not data: + return 2 root = etree.fromstring(data) @@ -389,29 +382,21 @@ async def _func(self, message: list[str], player: Player) -> int: if self.check_disabled(player): return -1 - async with aiohttp.ClientSession() as session: - resp = await asyncio.wait_for( - session.get(f"https://api.urbandictionary.com/v0/define?term={message[1]}"), - 5, - ) - if not resp: - return 1 - - data = await asyncio.wait_for(resp.json(), 5) - if not data: - return 3 + data = await Utils.FetchJson(f"https://api.urbandictionary.com/v0/define?term={message[1]}") + if not data: + return 3 - if "list" not in data or not data["list"]: - self.torchlight.SayChat(f"[UB] No definition found for: {message[1]}", player) - return 4 + if "list" not in data or not data["list"]: + self.torchlight.SayChat(f"[UB] No definition found for: {message[1]}", player) + return 4 - def print_item(item: dict[str, Any]) -> None: - self.torchlight.SayChat( - "[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), - player, - ) + def print_item(item: dict[str, Any]) -> None: + self.torchlight.SayChat( + "[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), + player, + ) - print_item(data["list"][0]) + print_item(data["list"][0]) return 0 @@ -456,21 +441,13 @@ async def _func(self, message: list[str], player: Player) -> int: else: search = f"q={message[1]}" - async with aiohttp.ClientSession() as session: - resp = await asyncio.wait_for( - session.get( - "https://api.openweathermap.org/data/2.5/weather?APPID={}&units=metric&{}".format( - self.torchlight.config["OpenWeatherAPIKey"], search - ) - ), - 5, + data = await Utils.FetchJson( + "https://api.openweathermap.org/data/2.5/weather?APPID={}&units=metric&{}".format( + self.torchlight.config["OpenWeatherAPIKey"], search ) - if not resp: - return 2 - - data = await asyncio.wait_for(resp.json(), 5) - if not data: - return 3 + ) + if not data: + return 3 if data["cod"] != 200: self.torchlight.SayPrivate(player, "[OW] {}".format(data["message"])) @@ -520,49 +497,8 @@ async def _func(self, message: list[str], player: Player) -> int: search = "autoip" additional = "?geo_ip={}".format(player.address.split(":")[0]) else: - async with aiohttp.ClientSession() as session: - resp = await asyncio.wait_for( - session.get(f"http://autocomplete.wunderground.com/aq?format=JSON&query={message[1]}"), - 5, - ) - if not resp: - return 2 - - try: - data = await asyncio.wait_for(resp.json(), 5) - if not data: - return 3 - except Exception as e: - self.logger.error(e) - self.torchlight.SayPrivate( - message="Failed to retrieve data from the wunderground api", - player=player, - ) - return 1 - - if not data["RESULTS"]: - self.torchlight.SayPrivate(player, "[WU] No cities match your search query.") - return 4 - - search = data["RESULTS"][0]["name"] - additional = "" - - async with aiohttp.ClientSession() as session: - resp = await asyncio.wait_for( - session.get( - "http://api.wunderground.com/api/{}/conditions/q/{}.json{}".format( - self.torchlight.config["WundergroundAPIKey"], - search, - additional, - ) - ), - 5, - ) - if not resp: - return 2 - try: - data = await asyncio.wait_for(resp.json(), 5) + data = await Utils.FetchJson(f"http://autocomplete.wunderground.com/aq?format=JSON&query={message[1]}") if not data: return 3 except Exception as e: @@ -573,6 +509,31 @@ async def _func(self, message: list[str], player: Player) -> int: ) return 1 + if not data["RESULTS"]: + self.torchlight.SayPrivate(player, "[WU] No cities match your search query.") + return 4 + + search = data["RESULTS"][0]["name"] + additional = "" + + try: + data = await Utils.FetchJson( + "http://api.wunderground.com/api/{}/conditions/q/{}.json{}".format( + self.torchlight.config["WundergroundAPIKey"], + search, + additional, + ) + ) + if not data: + return 3 + except Exception as e: + self.logger.error(e) + self.torchlight.SayPrivate( + message="Failed to retrieve data from the wunderground api", + player=player, + ) + return 1 + if "error" in data["response"]: self.torchlight.SayPrivate( player, diff --git a/src/torchlight/URLInfo.py b/src/torchlight/URLInfo.py index e693393..361bc0c 100644 --- a/src/torchlight/URLInfo.py +++ b/src/torchlight/URLInfo.py @@ -1,4 +1,3 @@ -import asyncio import io import json import logging @@ -18,18 +17,16 @@ # @profile async def get_url_data(url: str) -> tuple[bytes, str, int]: - async with aiohttp.ClientSession() as session: - resp = await asyncio.wait_for(session.get(url), 5) - if resp: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: + async with session.get(url) as resp: content_type: str = resp.headers.get("Content-Type", "") content_length_raw: str = resp.headers.get("Content-Length", "") - content = await asyncio.wait_for(resp.content.read(65536), 5) + content = await resp.content.read(65536) content_length = -1 if content_length_raw: content_length = int(content_length_raw) - resp.close() return content, content_type, content_length diff --git a/src/torchlight/Utils.py b/src/torchlight/Utils.py index e9b14bf..b265705 100644 --- a/src/torchlight/Utils.py +++ b/src/torchlight/Utils.py @@ -3,8 +3,23 @@ from collections.abc import Coroutine from typing import Any +import aiohttp + class Utils: + @staticmethod + async def FetchText(url: str, *, timeout: float = 10, params: dict[str, str] | None = None) -> str: + # timeout is the total budget for connecting and reading the body. + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session: + async with session.get(url, params=params) as resp: + return await resp.text() + + @staticmethod + async def FetchJson(url: str, *, timeout: float = 10, params: dict[str, str] | None = None) -> Any: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session: + async with session.get(url, params=params) as resp: + return await resp.json() + @staticmethod def FireAndForget(coro: Coroutine[Any, Any, Any], logger: logging.Logger) -> asyncio.Task: # Logs exceptions instead of letting asyncio silently discard them on an untracked task.