diff --git a/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/constant.py b/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/constant.py index 8f7d67d..3b929b2 100644 --- a/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/constant.py +++ b/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/constant.py @@ -1,4 +1,10 @@ from typing import Literal GAME_TYPE: Literal['TOS'] = 'TOS' -BASE_URL = 'https://teatube.cn:8888/' +BASE_URL = { + 'https://teatube.cn:8888/', + 'http://cafuuchino1.studio26f.org:19970', + 'http://cafuuchino2.studio26f.org:19970', + 'http://cafuuchino3.studio26f.org:19970', + 'http://cafuuchino4.studio26f.org:19970', +} diff --git a/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/processor.py b/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/processor.py index cc16857..a2131d4 100644 --- a/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/processor.py +++ b/nonebot_plugin_tetris_stats/game_data_processor/tos_data_processor/processor.py @@ -3,6 +3,7 @@ from re import match from typing import Literal from urllib.parse import urlencode +from httpx import TimeoutException from nonebot.compat import type_validate_json from nonebot_plugin_orm import get_session @@ -104,22 +105,30 @@ class Processor(ProcessorMeta): """获取用户信息""" if self.processed_data.user_info is None: if self.user.teaid is not None: - url = splice_url( - [ - BASE_URL, - 'getTeaIdInfo', - f'?{urlencode({"teaId":self.user.teaid})}', - ] - ) + url = [ + splice_url( + [ + i, + 'getTeaIdInfo', + f'?{urlencode({"teaId":self.user.teaid})}', + ] + ) + for i in BASE_URL + ] else: - url = splice_url( - [ - BASE_URL, - 'getUsernameInfo', - f'?{urlencode({"username":self.user.name})}', - ] - ) - self.raw_response.user_info = await Request.request(url) + url = [ + splice_url( + [ + i, + 'getUsernameInfo', + f'?{urlencode({"username":self.user.name})}', + ] + ) + for i in BASE_URL + ] + self.raw_response.user_info = await Request.failover_request( + url, failover_code=[502], failover_exc=(TimeoutException,) + ) user_info: UserInfo = type_validate_json(UserInfo, self.raw_response.user_info) # type: ignore[arg-type] if not isinstance(user_info, InfoSuccess): raise RequestError(f'用户信息请求错误:\n{user_info.error}') @@ -132,14 +141,19 @@ class Processor(ProcessorMeta): other_parameter = {} params = urlencode(dict(sorted(other_parameter.items()))) if self.processed_data.user_profile.get(params) is None: - self.raw_response.user_profile[params] = await Request.request( - splice_url( - [ - BASE_URL, - 'getProfile', - f'?{urlencode({"id":self.user.teaid or self.user.name,**other_parameter})}', - ] - ) + self.raw_response.user_profile[params] = await Request.failover_request( + [ + splice_url( + [ + i, + 'getProfile', + f'?{urlencode({"id":self.user.teaid or self.user.name,**other_parameter})}', + ] + ) + for i in BASE_URL + ], + failover_code=[502], + failover_exc=(TimeoutException,), ) self.processed_data.user_profile[params] = UserProfile.model_validate_json( self.raw_response.user_profile[params] diff --git a/nonebot_plugin_tetris_stats/utils/exception.py b/nonebot_plugin_tetris_stats/utils/exception.py index 26722b9..a21e1b4 100644 --- a/nonebot_plugin_tetris_stats/utils/exception.py +++ b/nonebot_plugin_tetris_stats/utils/exception.py @@ -18,6 +18,10 @@ class NeedCatchError(TetrisStatsError): class RequestError(NeedCatchError): """请求错误""" + def __init__(self, message: str = '', *, status_code: int | None = None): + super().__init__(message) + self.status_code = status_code + class MessageFormatError(NeedCatchError): """用户发送的消息格式不正确""" diff --git a/nonebot_plugin_tetris_stats/utils/request.py b/nonebot_plugin_tetris_stats/utils/request.py index 12d3662..636563e 100644 --- a/nonebot_plugin_tetris_stats/utils/request.py +++ b/nonebot_plugin_tetris_stats/utils/request.py @@ -1,3 +1,4 @@ +from collections.abc import Sequence from http import HTTPStatus from urllib.parse import urljoin, urlparse @@ -116,7 +117,8 @@ class Request: response = await session.get(url, headers=cls._headers) if response.status_code != HTTPStatus.OK: raise RequestError( - f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}' + f'请求错误 code: {response.status_code} {HTTPStatus(response.status_code).phrase}\n{response.text}', + status_code=response.status_code, ) if is_json: loads(response.content) @@ -127,3 +129,33 @@ class Request: if urlparse(url).netloc.lower().endswith('tetr.io'): return await cls._anti_cloudflare(url) raise + + @classmethod + async def failover_request( + cls, + urls: Sequence[str], + *, + failover_code: Sequence[int], + failover_exc: tuple[type[BaseException], ...], + is_json: bool = True, + ) -> bytes: + error_list: list[RequestError] = [] + for i in urls: + logger.debug(f'尝试请求 {i}') + try: + return await cls.request(i, is_json=is_json) + except RequestError as e: + if e.status_code in failover_code: # 如果状态码在 failover_code 中, 则继续尝试下一个URL + error_list.append(e) + continue + # 如果状态码不在故障转移列表中, 则查找异常栈, 如果异常栈内有 failover_exc 内的异常类型, 则继续尝试下一个URL + tb = e.__traceback__ + while tb is not None: + if isinstance(tb.tb_frame.f_locals.get('exc_value'), failover_exc): + error_list.append(e) + break + tb = tb.tb_next + else: + raise + continue + raise RequestError(f'所有地址皆不可用\n{error_list!r}')