From 3b30133a88127757730690cb0a4696670ec2b27a Mon Sep 17 00:00:00 2001 From: JokelBaf <60827680+JokelBaf@users.noreply.github.com> Date: Sat, 13 Jan 2024 21:28:52 +0200 Subject: [PATCH 1/3] Rewrite Geetest logic entirely and add stoken cookie refresh function (#152) --- genshin/__main__.py | 2 +- genshin/client/components/geetest/client.py | 240 ++++++++++++++++++-- genshin/client/components/geetest/server.py | 188 ++++++++------- genshin/client/manager/cookie.py | 50 +++- genshin/client/routes.py | 14 ++ genshin/constants.py | 1 + genshin/utility/geetest.py | 51 ++--- 7 files changed, 412 insertions(+), 134 deletions(-) diff --git a/genshin/__main__.py b/genshin/__main__.py index 05134196..629ed987 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -228,7 +228,7 @@ async def lineups(client: genshin.Client, scenario: typing.Optional[str]) -> Non click.echo(f"{click.style('Characters:', bold=True)}") for group, characters in enumerate(lineup.characters, 1): if len(lineup.characters) > 1: - click.echo(f"- Group {group }:") + click.echo(f"- Group {group}:") for character in characters: click.echo(f" - {character.name} ({character.role})") diff --git a/genshin/client/components/geetest/client.py b/genshin/client/components/geetest/client.py index 243bf3f4..898a4d23 100644 --- a/genshin/client/components/geetest/client.py +++ b/genshin/client/components/geetest/client.py @@ -1,14 +1,14 @@ """Geetest client component.""" -import base64 import json import typing import aiohttp import aiohttp.web -import yarl -from genshin import errors +from genshin import constants, errors +from genshin.client import routes from genshin.client.components import base +from genshin.utility import ds as ds_utility from genshin.utility import geetest as geetest_utility from . import server @@ -16,39 +16,48 @@ __all__ = ["GeetestClient"] -WEB_LOGIN_URL = yarl.URL("https://sg-public-api.hoyolab.com/account/ma-passport/api/webLoginByPassword") - - class GeetestClient(base.BaseClient): """Geetest client component.""" - async def login_with_geetest( - self, account: str, password: str, session_id: str, geetest: typing.Dict[str, str] - ) -> typing.Mapping[str, str]: - """Login with a password and a solved geetest. + async def web_login( + self, + account: str, + password: str, + *, + tokenType: typing.Optional[int] = 6, + geetest: typing.Optional[typing.Dict[str, typing.Any]] = None, + ) -> typing.Dict[str, typing.Any]: + """Login with a password using web endpoint. - Token type is a bitfield of cookie_token, ltoken, stoken. + Returns either data from aigis header or cookies. """ + headers = {**geetest_utility.WEB_LOGIN_HEADERS} + if geetest: + mmt_data = geetest["data"] + session_id = geetest["session_id"] + headers["x-rpc-aigis"] = geetest_utility.get_aigis_header(session_id, mmt_data) + payload = { - "account": geetest_utility.encrypt_geetest_password(account), - "password": geetest_utility.encrypt_geetest_password(password), - "token_type": 6, + "account": geetest_utility.encrypt_geetest_credentials(account), + "password": geetest_utility.encrypt_geetest_credentials(password), + "token_type": tokenType, } - # we do not want to use the previous cookie manager sessions - async with aiohttp.ClientSession() as session: async with session.post( - WEB_LOGIN_URL, + routes.WEB_LOGIN_URL.get_url(), json=payload, - headers={ - **geetest_utility.HEADERS, - "x-rpc-aigis": f"{session_id};{base64.b64encode(json.dumps(geetest).encode()).decode()}", - }, + headers=headers, ) as r: data = await r.json() cookies = {cookie.key: cookie.value for cookie in r.cookies.values()} + if data["retcode"] == -3101: + # Captcha triggered + aigis = json.loads(r.headers["x-rpc-aigis"]) + aigis["data"] = json.loads(aigis["data"]) + return aigis + if not data["data"]: errors.raise_for_retcode(data) @@ -59,9 +68,190 @@ async def login_with_geetest( return cookies - async def login_with_password(self, account: str, password: str, *, port: int = 5000) -> typing.Mapping[str, str]: - """Login with a password. + async def app_login( + self, + account: str, + password: str, + *, + geetest: typing.Optional[typing.Dict[str, typing.Any]] = None, + ) -> typing.Dict[str, typing.Any]: + """Login with a password using HoYoLab app endpoint. + + Returns data from aigis header or action_ticket or cookies. + """ + headers = { + **geetest_utility.APP_LOGIN_HEADERS, + "ds": ds_utility.generate_dynamic_secret(constants.DS_SALT["app_login"]), + } + if geetest: + mmt_data = geetest["data"] + session_id = geetest["session_id"] + headers["x-rpc-aigis"] = geetest_utility.get_aigis_header(session_id, mmt_data) + + payload = { + "account": geetest_utility.encrypt_geetest_credentials(account), + "password": geetest_utility.encrypt_geetest_credentials(password), + } + + async with aiohttp.ClientSession() as session: + async with session.post( + routes.APP_LOGIN_URL.get_url(), + json=payload, + headers=headers, + ) as r: + data = await r.json() + + if data["retcode"] == -3101: + # Captcha triggered + aigis = json.loads(r.headers["x-rpc-aigis"]) + aigis["data"] = json.loads(aigis["data"]) + return aigis + + if data["retcode"] == -3239: + # Email verification required + return json.loads(r.headers["x-rpc-verify"]) + + if not data["data"]: + errors.raise_for_retcode(data) + + cookies = { + "stoken": data["data"]["token"]["token"], + "ltuid_v2": data["data"]["user_info"]["aid"], + "ltmid_v2": data["data"]["user_info"]["mid"], + "account_id_v2": data["data"]["user_info"]["aid"], + "account_mid_v2": data["data"]["user_info"]["mid"], + } + self.set_cookies(cookies) + + return cookies + + async def send_verification_email( + self, + ticket: typing.Dict[str, typing.Any], + *, + geetest: typing.Optional[typing.Dict[str, typing.Any]] = None, + ) -> typing.Union[None, typing.Dict[str, typing.Any]]: + """Send verification email. - This will start a webserver. + Returns None if success, aigis headers (mmt/aigis) otherwise. """ - return await server.login_with_app(self, account, password, port=port) + headers = {**geetest_utility.EMAIL_SEND_HEADERS} + if geetest: + mmt_data = geetest["data"] + session_id = geetest["session_id"] + headers["x-rpc-aigis"] = geetest_utility.get_aigis_header(session_id, mmt_data) + + async with aiohttp.ClientSession() as session: + async with session.post( + routes.SEND_VERIFICATION_CODE_URL.get_url(), + json={ + "action_type": "verify_for_component", + "action_ticket": ticket["verify_str"]["ticket"], + }, + headers=headers, + ) as r: + data = await r.json() + + if data["retcode"] == -3101: + # Captcha triggered + aigis = json.loads(r.headers["x-rpc-aigis"]) + aigis["data"] = json.loads(aigis["data"]) + return aigis + + if data["retcode"] != 0: + errors.raise_for_retcode(data) + + return None + + async def verify_email(self, code: str, ticket: typing.Dict[str, typing.Any]) -> None: + """Verify email.""" + async with aiohttp.ClientSession() as session: + async with session.post( + routes.VERIFY_EMAIL_URL.get_url(), + json={ + "action_type": "verify_for_component", + "action_ticket": ticket["verify_str"]["ticket"], + "email_captcha": code, + "verify_method": 2, + }, + headers=geetest_utility.EMAIL_VERIFY_HEADERS, + ) as r: + data = await r.json() + + if data["retcode"] != 0: + errors.raise_for_retcode(data) + + return None + + async def login_with_password( + self, + account: str, + password: str, + *, + port: int = 5000, + tokenType: typing.Optional[int] = 6, + geetest_solver: typing.Optional[ + typing.Callable[[typing.Dict[str, typing.Any]], typing.Awaitable[typing.Dict[str, typing.Any]]] + ] = None, + ) -> typing.Dict[str, str]: + """Login with a password via web endpoint. + + Note that this will start a webserver if captcha is + triggered and `geetest_solver` is not passed. + """ + result = await self.web_login(account, password, tokenType=tokenType) + + if "session_id" not in result: + # Captcha not triggered + return result + + if geetest_solver: + geetest = await geetest_solver(result) + else: + geetest = await server.solve_geetest(result, port=port) + + return await self.web_login(account, password, tokenType=tokenType, geetest=geetest) + + async def login_with_app_password( + self, + account: str, + password: str, + *, + port: int = 5000, + geetest_solver: typing.Optional[ + typing.Callable[[typing.Dict[str, typing.Any]], typing.Awaitable[typing.Dict[str, typing.Any]]] + ] = None, + ) -> typing.Dict[str, str]: + """Login with a password via HoYoLab app endpoint. + + Note that this will start a webserver if either of the + following happens: + + 1. Captcha is triggered and `geetest_solver` is not passed. + 2. Email verification is triggered (can happen if you + first login with a new device). + """ + result = await self.app_login(account, password) + + if "session_id" in result: + # Captcha triggered + if geetest_solver: + geetest = await geetest_solver(result) + else: + geetest = await server.solve_geetest(result, port=port) + + result = await self.app_login(account, password, geetest=geetest) + + if "risk_ticket" in result: + # Email verification required + mmt = await self.send_verification_email(result) + if mmt: + if geetest_solver: + geetest = await geetest_solver(mmt) + else: + geetest = await server.solve_geetest(mmt, port=port) + + await server.verify_email(self, result, port=port) + result = await self.app_login(account, password) + + return result diff --git a/genshin/client/components/geetest/server.py b/genshin/client/components/geetest/server.py index dc781349..639c3fc9 100644 --- a/genshin/client/components/geetest/server.py +++ b/genshin/client/components/geetest/server.py @@ -1,4 +1,4 @@ -"""Aiohttp webserver used for login.""" +"""Aiohttp webserver used for captcha solving and email verification.""" from __future__ import annotations import asyncio @@ -8,70 +8,97 @@ import aiohttp from aiohttp import web -from genshin.errors import raise_for_retcode -from genshin.utility import geetest - from . import client -__all__ = ["login_with_app"] - -INDEX = """ - - - - - - - + - -""" + } + ) + ); + + + """ + if page == "captcha" + else """ + + + + + + + + + """ + ) + GT_URL = "https://raw.githubusercontent.com/GeeTeam/gt3-node-sdk/master/demo/static/libs/gt.js" -async def login_with_app(client: client.GeetestClient, account: str, password: str, *, port: int = 5000) -> typing.Any: - """Create and run an application for handling login.""" +async def launch_webapp( + page: typing.Literal["captcha", "verify-email"], + *, + port: int = 5000, + mmt: typing.Optional[typing.Dict[str, typing.Any]] = None, +) -> typing.Any: + """Create and run a webapp to solve captcha or send verification code.""" routes = web.RouteTableDef() future: asyncio.Future[typing.Any] = asyncio.Future() - mmt_key: str = "" + @routes.get("/captcha") + async def captcha(request: web.Request) -> web.StreamResponse: + return web.Response(body=get_page("captcha"), content_type="text/html") - @routes.get("/") - async def index(request: web.Request) -> web.StreamResponse: - return web.Response(body=INDEX, content_type="text/html") + @routes.get("/verify-email") + async def verify_email(request: web.Request) -> web.StreamResponse: + return web.Response(body=get_page("verify-email"), content_type="text/html") @routes.get("/gt.js") async def gt(request: web.Request) -> web.StreamResponse: @@ -83,33 +110,14 @@ async def gt(request: web.Request) -> web.StreamResponse: @routes.get("/mmt") async def mmt_endpoint(request: web.Request) -> web.Response: - nonlocal mmt_key - - mmt = await geetest.create_mmt(account, password) - if mmt["data"] is None: - raise_for_retcode(mmt) # type: ignore - - mmt_key = mmt["data"] return web.json_response(mmt) - @routes.post("/login") - async def login_endpoint(request: web.Request) -> web.Response: + @routes.post("/send-data") + async def send_data_endpoint(request: web.Request) -> web.Response: body = await request.json() + future.set_result(body) - try: - data = await client.login_with_geetest( - account=account, - password=password, - session_id=body["sid"], - geetest=body["gt"], - ) - except Exception as e: - future.set_exception(e) - return web.json_response({}, status=500) - - future.set_result(data) - - return web.json_response(data) + return web.Response(status=204) app = web.Application() app.add_routes(routes) @@ -118,8 +126,8 @@ async def login_endpoint(request: web.Request) -> web.Response: await runner.setup() site = web.TCPSite(runner, host="localhost", port=port) - print(f"Opened browser in http://localhost:{port}") # noqa - webbrowser.open_new_tab(f"http://localhost:{port}") + print(f"Opening http://localhost:{port}/{page} in browser...") # noqa + webbrowser.open_new_tab(f"http://localhost:{port}/{page}") await site.start() @@ -130,3 +138,25 @@ async def login_endpoint(request: web.Request) -> web.Response: await runner.shutdown() return data + + +async def solve_geetest( + mmt: typing.Dict[str, typing.Any], + *, + port: int = 5000, +) -> typing.Dict[str, typing.Any]: + """Solve a geetest captcha manually.""" + return await launch_webapp("captcha", port=port, mmt=mmt) + + +async def verify_email( + client: client.GeetestClient, + ticket: typing.Dict[str, typing.Any], + *, + port: int = 5000, +) -> None: + """Verify email to login via HoYoLab app endpoint.""" + data = await launch_webapp("verify-email", port=port) + code = data["code"] + + return await client.verify_email(code, ticket) diff --git a/genshin/client/manager/cookie.py b/genshin/client/manager/cookie.py index a76bb226..1780de6e 100644 --- a/genshin/client/manager/cookie.py +++ b/genshin/client/manager/cookie.py @@ -11,6 +11,9 @@ - fetch_cookie_token_info - cookie_token -> cookie_token - login_ticket -> cookie_token +- fetch_cookie_with_stoken_v2 + - stoken (v2) + mid -> ltoken_v2 (token_type=2) + - stoken (v2) + mid -> cookie_token_v2 (token_type=4) """ from __future__ import annotations @@ -19,11 +22,18 @@ import aiohttp import aiohttp.typedefs -from genshin import errors, types +from genshin import constants, errors, types from genshin.client import routes from genshin.client.manager import managers +from genshin.utility import ds as ds_utility -__all__ = ["complete_cookies", "fetch_cookie_token_info", "fetch_cookie_with_cookie", "refresh_cookie_token"] +__all__ = [ + "complete_cookies", + "fetch_cookie_token_info", + "fetch_cookie_with_cookie", + "fetch_cookie_with_stoken_v2", + "refresh_cookie_token", +] async def fetch_cookie_with_cookie( @@ -59,6 +69,42 @@ async def fetch_cookie_with_cookie( return data +async def fetch_cookie_with_stoken_v2( + cookies: managers.CookieOrHeader, + *, + token_types: typing.List[typing.Literal[2, 4]], +) -> typing.Mapping[str, str]: + """Fetch cookie (v2) with an stoken (v2) and mid.""" + cookies = managers.parse_cookie(cookies) + if "ltmid_v2" in cookies: + # The endpoint requires ltmid_v2 to be named mid + cookies["mid"] = cookies["ltmid_v2"] + + url = routes.COOKIE_V2_REFRESH_URL.get_url() + + headers = { + "ds": ds_utility.generate_dynamic_secret(constants.DS_SALT["app_login"]), + "x-rpc-app_id": "c9oqaq3s3gu8", + } + body = {"dst_token_types": token_types} + + async with aiohttp.ClientSession() as session: + r = await session.request("POST", url, json=body, headers=headers, cookies=cookies) + data = await r.json(content_type=None) + + if data["retcode"] != 0: + errors.raise_for_retcode(data) + + cookies = dict() + for token in data["data"]["tokens"]: + if token["token_type"] == 2: + cookies["ltoken_v2"] = token["token"] + elif token["token_type"] == 4: + cookies["cookie_token_v2"] = token["token"] + + return cookies + + async def fetch_cookie_token_info( cookies: managers.CookieOrHeader, *, diff --git a/genshin/client/routes.py b/genshin/client/routes.py index 6befaa2e..e4804618 100644 --- a/genshin/client/routes.py +++ b/genshin/client/routes.py @@ -8,10 +8,12 @@ __all__ = [ "ACCOUNT_URL", + "APP_LOGIN_URL", "BBS_REFERER_URL", "BBS_URL", "CALCULATOR_URL", "COMMUNITY_URL", + "COOKIE_V2_REFRESH_URL", "DETAIL_LEDGER_URL", "GACHA_URL", "HK4E_URL", @@ -23,8 +25,10 @@ "Route", "TAKUMI_URL", "TEAPOT_URL", + "VERIFY_EMAIL_URL", "WEBAPI_URL", "WEBSTATIC_URL", + "WEB_LOGIN_URL", "YSULOG_URL", ] @@ -207,3 +211,13 @@ def get_url(self, region: types.Region, game: types.Game) -> yarl.URL: bbs="https://webstatic-sea.mihoyo.com/admin/mi18n/bbs_cn/m11241040191111/m11241040191111-{lang}.json", inquiry="https://mi18n-os.hoyoverse.com/webstatic/admin/mi18n/hk4e_global/m02251421001311/m02251421001311-{lang}.json", ) + +COOKIE_V2_REFRESH_URL = Route("https://sg-public-api.hoyoverse.com/account/ma-passport/token/getBySToken") + +WEB_LOGIN_URL = Route("https://sg-public-api.hoyolab.com/account/ma-passport/api/webLoginByPassword") +APP_LOGIN_URL = Route("https://sg-public-api.hoyoverse.com/account/ma-passport/api/appLoginByPassword") + +SEND_VERIFICATION_CODE_URL = Route( + "https://sg-public-api.hoyoverse.com/account/ma-verifier/api/createEmailCaptchaByActionTicket" +) +VERIFY_EMAIL_URL = Route("https://sg-public-api.hoyoverse.com/account/ma-verifier/api/verifyActionTicketPartly") diff --git a/genshin/constants.py b/genshin/constants.py index 39a40dbf..800d6012 100644 --- a/genshin/constants.py +++ b/genshin/constants.py @@ -26,6 +26,7 @@ DS_SALT = { types.Region.OVERSEAS: "6s25p5ox5y14umn1p61aqyyvbvvl3lrt", types.Region.CHINESE: "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs", + "app_login": "IZPgfb0dRPtBeLuFkdDznSZ6f4wWt6y2", "cn_signin": "9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7", } """Dynamic Secret Salts.""" diff --git a/genshin/utility/geetest.py b/genshin/utility/geetest.py index 0827a8f5..4d535d7d 100644 --- a/genshin/utility/geetest.py +++ b/genshin/utility/geetest.py @@ -3,11 +3,10 @@ import json import typing -import aiohttp - -__all__ = ["create_mmt", "encrypt_geetest_password"] +__all__ = ["encrypt_geetest_credentials"] +# RSA key is the same for app and web login LOGIN_KEY_CERT = b""" -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4PMS2JVMwBsOIrYWRluY @@ -20,7 +19,7 @@ -----END PUBLIC KEY----- """ -HEADERS = { +WEB_LOGIN_HEADERS = { "x-rpc-app_id": "c9oqaq3s3gu8", "x-rpc-client_type": "4", "x-rpc-sdk_version": "2.14.1", @@ -32,36 +31,34 @@ "Referer": "https://account.hoyolab.com/", } +APP_LOGIN_HEADERS = { + "x-rpc-app_id": "c9oqaq3s3gu8", + "x-rpc-app_version": "2.47.0", + "x-rpc-client_type": "2", + "x-rpc-sdk_version": "2.22.0", + "x-rpc-game_biz": "bbs_oversea", + "Origin": "https://account.hoyoverse.com", + "Referer": "https://account.hoyoverse.com/", +} -async def create_mmt(account: str, password: str) -> typing.Mapping[str, typing.Any]: - """Create a new hoyolab mmt.""" - async with aiohttp.ClientSession() as session: - _payload = { - "account": encrypt_geetest_password(account), - "password": encrypt_geetest_password(password), - "token_type": 6, - } - - r = await session.post( - "https://sg-public-api.hoyolab.com/account/ma-passport/api/webLoginByPassword", - json=_payload, - headers=HEADERS, - ) - - data = await r.json() - - if data["retcode"] == -3101: - aigis = json.loads(r.headers["x-rpc-aigis"]) - aigis["data"] = json.loads(aigis["data"]) - return aigis +EMAIL_SEND_HEADERS = { + "x-rpc-app_id": "c9oqaq3s3gu8", +} - return {} +EMAIL_VERIFY_HEADERS = { + "x-rpc-app_id": "c9oqaq3s3gu8", +} -def encrypt_geetest_password(text: str) -> str: +def encrypt_geetest_credentials(text: str) -> str: """Encrypt text for geetest.""" import rsa public_key = rsa.PublicKey.load_pkcs1_openssl_pem(LOGIN_KEY_CERT) crypto = rsa.encrypt(text.encode("utf-8"), public_key) return base64.b64encode(crypto).decode("utf-8") + + +def get_aigis_header(session_id: str, mmt_data: typing.Dict[str, typing.Any]) -> str: + """Get aigis header.""" + return f"{session_id};{base64.b64encode(json.dumps(mmt_data).encode()).decode()}" From 5570643029fcd376cd0677ed6ce5c577410d559a Mon Sep 17 00:00:00 2001 From: ashlen Date: Sat, 13 Jan 2024 19:30:59 +0000 Subject: [PATCH 2/3] Fix type errors and bump pypi version --- genshin/client/components/base.py | 2 +- genshin/client/manager/managers.py | 2 +- genshin/models/genshin/chronicle/notes.py | 10 +++++----- genshin/models/genshin/chronicle/stats.py | 4 +--- genshin/models/hoyolab/record.py | 2 +- genshin/utility/logfile.py | 16 ++++------------ pyproject.toml | 4 ++-- setup.py | 2 +- 8 files changed, 16 insertions(+), 26 deletions(-) diff --git a/genshin/client/components/base.py b/genshin/client/components/base.py index cd3040a8..a8d24137 100644 --- a/genshin/client/components/base.py +++ b/genshin/client/components/base.py @@ -26,7 +26,7 @@ class BaseClient(abc.ABC): """Base ABC Client.""" - __slots__ = ("cookie_manager", "cache", "_lang", "_region", "_default_game", "uids", "authkeys") + __slots__ = ("cookie_manager", "cache", "_lang", "_region", "_default_game", "uids", "authkeys", "_hoyolab_id") USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" # noqa: E501 diff --git a/genshin/client/manager/managers.py b/genshin/client/manager/managers.py index d88b0d1f..450b21a4 100644 --- a/genshin/client/manager/managers.py +++ b/genshin/client/manager/managers.py @@ -209,7 +209,7 @@ def multi(self) -> bool: return False @property - def jar(self) -> http.cookies.SimpleCookie[str]: + def jar(self) -> http.cookies.SimpleCookie: """SimpleCookie containing the cookies.""" return http.cookies.SimpleCookie(self.cookies) diff --git a/genshin/models/genshin/chronicle/notes.py b/genshin/models/genshin/chronicle/notes.py index ca59e84a..ea73337e 100644 --- a/genshin/models/genshin/chronicle/notes.py +++ b/genshin/models/genshin/chronicle/notes.py @@ -14,13 +14,13 @@ from genshin.models.model import Aliased, APIModel __all__ = [ - "Expedition", - "TaskRewardStatus", - "TaskReward", - "AttendanceRewardStatus", "AttendanceReward", + "AttendanceRewardStatus", "DailyTasks", - "Notes" + "Expedition", + "Notes", + "TaskReward", + "TaskRewardStatus", ] diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index 32c5c5d9..fb4d9e75 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -100,9 +100,7 @@ def __add_base_offering( offerings: typing.Sequence[typing.Any], values: typing.Dict[str, typing.Any], ) -> typing.Sequence[typing.Any]: - if values["type"] == "Reputation" and not any( - values["type"] == o["name"] for o in offerings - ): + if values["type"] == "Reputation" and not any(values["type"] == o["name"] for o in offerings): offerings = [*offerings, dict(name=values["type"], level=values["level"])] return offerings diff --git a/genshin/models/hoyolab/record.py b/genshin/models/hoyolab/record.py index 2a8e693d..c6a22648 100644 --- a/genshin/models/hoyolab/record.py +++ b/genshin/models/hoyolab/record.py @@ -159,7 +159,7 @@ def __new__(cls, **kwargs: typing.Any) -> RecordCard: elif game_id == 6: cls = StarRailRecodeCard - return super().__new__(cls) + return super().__new__(cls) # type: ignore game_id: int game_biz: str = "" diff --git a/genshin/utility/logfile.py b/genshin/utility/logfile.py index faafc079..f516b5e9 100644 --- a/genshin/utility/logfile.py +++ b/genshin/utility/logfile.py @@ -28,20 +28,12 @@ def _search_output_log(content: str) -> pathlib.Path: """Search output log for data_2.""" - match1 = re.search(r'([A-Z]:/.*?/GenshinImpact_Data)', content, re.MULTILINE) - match2 = re.search(r'([A-Z]:/.*?/YuanShen_Data)', content, re.MULTILINE) - match3 = re.search(r'([A-Z]:/.*?/StarRail_Data)', content, re.MULTILINE) - if match1 is None and match2 is None and match3 is None: + match = re.search(r"([A-Z]:/.*?/GenshinImpact_Data)", content, re.MULTILINE) + match = match or re.search(r"([A-Z]:/.*?/YuanShen_Data)", content, re.MULTILINE) + match = match or re.search(r"([A-Z]:/.*?/StarRail_Data)", content, re.MULTILINE) + if match is None: raise FileNotFoundError("No Genshin/Star Rail installation location in logfile") - match = None - if match1 is not None: - match = match1 - elif match2 is not None: - match = match2 - elif match3 is not None: - match = match3 - base_dir = pathlib.Path(match[1]) / "webCaches" webCaches = [entry for entry in base_dir.iterdir() if entry.is_dir() and entry.name.startswith("2.")] diff --git a/pyproject.toml b/pyproject.toml index fe57d030..804aafe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,14 +24,14 @@ reportIncompatibleMethodOverride = "none" # This relies on ordering for keyword reportUnusedFunction = "none" # Makes usage of validators impossible reportPrivateUsage = "none" reportUnknownMemberType = "none" +reportUntypedFunctionDecorator = "none" +reportIncompatibleVariableOverride = "none" [tool.mypy] warn_unreachable = false disallow_untyped_defs = true ignore_missing_imports = true -install_types = true -non_interactive = true # pyright warn_unused_ignores = false diff --git a/setup.py b/setup.py index 71b2bd0e..96e87d66 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="genshin", - version="1.6.1", + version="1.6.2", author="thesadru", author_email="thesadru@gmail.com", description="An API wrapper for Genshin Impact.", From 28688ce5ab188c15955dcd3dabd5785fe923f055 Mon Sep 17 00:00:00 2001 From: JokelBaf <60827680+JokelBaf@users.noreply.github.com> Date: Sat, 20 Jan 2024 05:47:09 +0200 Subject: [PATCH 3/3] Add clearer error for an empty string in cookie value when accessing `user_id` --- genshin/client/manager/managers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/genshin/client/manager/managers.py b/genshin/client/manager/managers.py index 450b21a4..fceb4ca5 100644 --- a/genshin/client/manager/managers.py +++ b/genshin/client/manager/managers.py @@ -249,6 +249,9 @@ def user_id(self) -> typing.Optional[int]: """ for name, value in self.cookies.items(): if name in ("ltuid", "account_id", "ltuid_v2", "account_id_v2"): + if not value: + raise ValueError(f"{name} can not be an empty string.") + return int(value) return None