From 202dc9ea2580623aa493eb6a051c7b2ec560a95b Mon Sep 17 00:00:00 2001 From: KT Date: Sat, 3 Feb 2024 07:38:32 +0800 Subject: [PATCH 01/20] Add HSR pure fiction (#157) --- .flake8 | 4 +- genshin/__init__.py | 1 + genshin/__main__.py | 1 + genshin/client/__init__.py | 1 + genshin/client/cache.py | 1 + genshin/client/clients.py | 1 + genshin/client/compatibility.py | 1 + genshin/client/components/base.py | 1 + .../client/components/calculator/__init__.py | 1 + .../components/calculator/calculator.py | 1 + .../client/components/calculator/client.py | 1 + .../client/components/chronicle/__init__.py | 1 + genshin/client/components/chronicle/client.py | 1 + .../client/components/chronicle/starrail.py | 13 +++ genshin/client/components/daily.py | 7 +- genshin/client/components/diary.py | 1 + genshin/client/components/gacha.py | 1 + genshin/client/components/geetest/__init__.py | 1 + genshin/client/components/geetest/client.py | 1 + genshin/client/components/geetest/server.py | 1 + genshin/client/components/hoyolab.py | 1 + genshin/client/components/lineup.py | 1 + genshin/client/components/teapot.py | 1 + genshin/client/components/transaction.py | 1 + genshin/client/components/wiki.py | 16 ++-- genshin/client/manager/__init__.py | 1 + genshin/client/manager/cookie.py | 1 + genshin/client/manager/managers.py | 1 + genshin/client/ratelimit.py | 1 + genshin/client/routes.py | 1 + genshin/constants.py | 1 + genshin/errors.py | 1 + genshin/models/__init__.py | 1 + genshin/models/genshin/__init__.py | 1 + genshin/models/genshin/calculator.py | 1 + genshin/models/genshin/chronicle/__init__.py | 1 + .../models/genshin/chronicle/activities.py | 1 + genshin/models/genshin/chronicle/notes.py | 1 + genshin/models/genshin/chronicle/tcg.py | 1 + genshin/models/genshin/constants.py | 1 + genshin/models/genshin/daily.py | 1 + genshin/models/genshin/diary.py | 1 + genshin/models/genshin/gacha.py | 1 + genshin/models/genshin/lineup.py | 1 + genshin/models/genshin/teapot.py | 1 + genshin/models/genshin/wiki.py | 1 + genshin/models/honkai/__init__.py | 1 + genshin/models/honkai/battlesuit.py | 1 + genshin/models/honkai/chronicle/__init__.py | 1 + .../models/honkai/chronicle/battlesuits.py | 1 + genshin/models/honkai/chronicle/modes.py | 1 + genshin/models/honkai/chronicle/stats.py | 1 + genshin/models/honkai/constants.py | 1 + genshin/models/hoyolab/__init__.py | 1 + genshin/models/hoyolab/record.py | 1 + genshin/models/model.py | 1 + genshin/models/starrail/__init__.py | 1 + genshin/models/starrail/character.py | 1 + genshin/models/starrail/chronicle/base.py | 1 + .../models/starrail/chronicle/challenge.py | 83 ++++++++++++++++++- .../models/starrail/chronicle/characters.py | 1 + genshin/models/starrail/chronicle/notes.py | 1 + genshin/models/starrail/chronicle/rogue.py | 1 + genshin/models/starrail/chronicle/stats.py | 1 + genshin/paginators/__init__.py | 1 + genshin/paginators/api.py | 1 + genshin/paginators/base.py | 3 +- genshin/types.py | 1 + genshin/utility/__init__.py | 1 + genshin/utility/concurrency.py | 1 + genshin/utility/deprecation.py | 1 + genshin/utility/ds.py | 1 + genshin/utility/fs.py | 1 + genshin/utility/geetest.py | 1 + genshin/utility/logfile.py | 1 + genshin/utility/uid.py | 1 + noxfile.py | 1 + tests/models/test_model.py | 3 +- 78 files changed, 179 insertions(+), 21 deletions(-) diff --git a/.flake8 b/.flake8 index c39efc79..eb1a7f74 100644 --- a/.flake8 +++ b/.flake8 @@ -6,6 +6,7 @@ exclude = tests, test.py # D105: Missing docstring in magic method # D106: Missing docstring Model.Config # D419: Docstring is empty +# E704: Multiple statements on one line (def) # S101: Use of assert for type checking # S303: Use of md5 # S311: Use of pseudo-random generators @@ -14,7 +15,8 @@ exclude = tests, test.py ignore = A001, A002, A003, C408, - D105, D106, D419 + D105, D106, D419, + E704, S101, S303, S311, S324, W503, diff --git a/genshin/__init__.py b/genshin/__init__.py index 66b92d77..a5a9ffb9 100644 --- a/genshin/__init__.py +++ b/genshin/__init__.py @@ -6,6 +6,7 @@ Source Code: https://github.com/thesadru/genshin.py """ + from . import models, utility from .client import * from .constants import * diff --git a/genshin/__main__.py b/genshin/__main__.py index 629ed987..2f7e8f3a 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -1,4 +1,5 @@ """CLI tools.""" + import asyncio import datetime import functools diff --git a/genshin/client/__init__.py b/genshin/client/__init__.py index b955bcc9..9faaf5c5 100644 --- a/genshin/client/__init__.py +++ b/genshin/client/__init__.py @@ -1,4 +1,5 @@ """Default client implementation.""" + from . import components from .cache import * from .clients import * diff --git a/genshin/client/cache.py b/genshin/client/cache.py index fbd42b29..1138c1d8 100644 --- a/genshin/client/cache.py +++ b/genshin/client/cache.py @@ -1,4 +1,5 @@ """Cache for client.""" + from __future__ import annotations import abc diff --git a/genshin/client/clients.py b/genshin/client/clients.py index a4b426e6..87e8aa9d 100644 --- a/genshin/client/clients.py +++ b/genshin/client/clients.py @@ -1,4 +1,5 @@ """A simple HTTP client for API endpoints.""" + from .components import ( calculator, chronicle, diff --git a/genshin/client/compatibility.py b/genshin/client/compatibility.py index 4ed9e1be..ce4d8fc2 100644 --- a/genshin/client/compatibility.py +++ b/genshin/client/compatibility.py @@ -1,4 +1,5 @@ """Reverse-compatibility layer for previous versions.""" + from __future__ import annotations import typing diff --git a/genshin/client/components/base.py b/genshin/client/components/base.py index a8d24137..49673cf9 100644 --- a/genshin/client/components/base.py +++ b/genshin/client/components/base.py @@ -1,4 +1,5 @@ """Base ABC Client.""" + import abc import asyncio import base64 diff --git a/genshin/client/components/calculator/__init__.py b/genshin/client/components/calculator/__init__.py index 8c47acf1..b8770c14 100644 --- a/genshin/client/components/calculator/__init__.py +++ b/genshin/client/components/calculator/__init__.py @@ -1,2 +1,3 @@ """Calculator client.""" + from .client import * diff --git a/genshin/client/components/calculator/calculator.py b/genshin/client/components/calculator/calculator.py index 6ac2ba7f..435bd755 100644 --- a/genshin/client/components/calculator/calculator.py +++ b/genshin/client/components/calculator/calculator.py @@ -2,6 +2,7 @@ Over-engineered for the sake of extendability and maintainability. """ + from __future__ import annotations import abc diff --git a/genshin/client/components/calculator/client.py b/genshin/client/components/calculator/client.py index 5140f9c0..a629ab84 100644 --- a/genshin/client/components/calculator/client.py +++ b/genshin/client/components/calculator/client.py @@ -1,4 +1,5 @@ """Calculator client.""" + from __future__ import annotations import asyncio diff --git a/genshin/client/components/chronicle/__init__.py b/genshin/client/components/chronicle/__init__.py index 30fb3fc8..076cfcbe 100644 --- a/genshin/client/components/chronicle/__init__.py +++ b/genshin/client/components/chronicle/__init__.py @@ -1,2 +1,3 @@ """Battle chronicle client components.""" + from .client import * diff --git a/genshin/client/components/chronicle/client.py b/genshin/client/components/chronicle/client.py index a8a2eca3..94f31407 100644 --- a/genshin/client/components/chronicle/client.py +++ b/genshin/client/components/chronicle/client.py @@ -1,4 +1,5 @@ """Battle chronicle component.""" + from . import genshin, honkai, starrail __all__ = ["BattleChronicleClient"] diff --git a/genshin/client/components/chronicle/starrail.py b/genshin/client/components/chronicle/starrail.py index 57cbedb6..b1935341 100644 --- a/genshin/client/components/chronicle/starrail.py +++ b/genshin/client/components/chronicle/starrail.py @@ -1,4 +1,5 @@ """StarRail battle chronicle component.""" + import asyncio import typing @@ -126,3 +127,15 @@ async def get_starrail_rogue( payload = dict(schedule_type=schedule_type, need_detail="true") data = await self._request_starrail_record("rogue", uid, lang=lang, payload=payload) return models.StarRailRogue(**data) + + async def get_starrail_pure_fiction( + self, + uid: typing.Optional[int] = None, + *, + previous: bool = False, + lang: typing.Optional[str] = None, + ) -> models.StarRailPureFiction: + """Get starrail pure fiction runs.""" + payload = dict(schedule_type=2 if previous else 1, need_all="true") + data = await self._request_starrail_record("challenge_story", uid, lang=lang, payload=payload) + return models.StarRailPureFiction(**data) diff --git a/genshin/client/components/daily.py b/genshin/client/components/daily.py index b9ca2964..53981d29 100644 --- a/genshin/client/components/daily.py +++ b/genshin/client/components/daily.py @@ -1,4 +1,5 @@ """Daily reward component.""" + import asyncio import datetime import functools @@ -150,8 +151,7 @@ async def claim_daily_reward( # noqa: D102 missing docstring in overload? lang: typing.Optional[str] = None, reward: typing.Literal[True] = ..., challenge: typing.Optional[typing.Mapping[str, str]] = None, - ) -> models.DailyReward: - ... + ) -> models.DailyReward: ... @typing.overload async def claim_daily_reward( # noqa: D102 missing docstring in overload? @@ -161,8 +161,7 @@ async def claim_daily_reward( # noqa: D102 missing docstring in overload? lang: typing.Optional[str] = None, reward: typing.Literal[False], challenge: typing.Optional[typing.Mapping[str, str]] = None, - ) -> None: - ... + ) -> None: ... async def claim_daily_reward( self, diff --git a/genshin/client/components/diary.py b/genshin/client/components/diary.py index f175d1bd..02b6d042 100644 --- a/genshin/client/components/diary.py +++ b/genshin/client/components/diary.py @@ -1,4 +1,5 @@ """Diary component.""" + import datetime import functools import typing diff --git a/genshin/client/components/gacha.py b/genshin/client/components/gacha.py index 71e27dfc..fe6cad62 100644 --- a/genshin/client/components/gacha.py +++ b/genshin/client/components/gacha.py @@ -1,4 +1,5 @@ """Wish component.""" + import asyncio import functools import typing diff --git a/genshin/client/components/geetest/__init__.py b/genshin/client/components/geetest/__init__.py index 97d622bd..3871455b 100644 --- a/genshin/client/components/geetest/__init__.py +++ b/genshin/client/components/geetest/__init__.py @@ -2,4 +2,5 @@ Credits to M-307 - https://github.com/mrwan200 """ + from .client import * diff --git a/genshin/client/components/geetest/client.py b/genshin/client/components/geetest/client.py index 898a4d23..036fa4b4 100644 --- a/genshin/client/components/geetest/client.py +++ b/genshin/client/components/geetest/client.py @@ -1,4 +1,5 @@ """Geetest client component.""" + import json import typing diff --git a/genshin/client/components/geetest/server.py b/genshin/client/components/geetest/server.py index 639c3fc9..a8bb641b 100644 --- a/genshin/client/components/geetest/server.py +++ b/genshin/client/components/geetest/server.py @@ -1,4 +1,5 @@ """Aiohttp webserver used for captcha solving and email verification.""" + from __future__ import annotations import asyncio diff --git a/genshin/client/components/hoyolab.py b/genshin/client/components/hoyolab.py index eef52a91..7e1804dc 100644 --- a/genshin/client/components/hoyolab.py +++ b/genshin/client/components/hoyolab.py @@ -1,4 +1,5 @@ """Hoyolab component.""" + import asyncio import typing diff --git a/genshin/client/components/lineup.py b/genshin/client/components/lineup.py index bc71ae7f..69326e3d 100644 --- a/genshin/client/components/lineup.py +++ b/genshin/client/components/lineup.py @@ -1,4 +1,5 @@ """Lineup component.""" + import functools import typing diff --git a/genshin/client/components/teapot.py b/genshin/client/components/teapot.py index e58e9c25..b3a75252 100644 --- a/genshin/client/components/teapot.py +++ b/genshin/client/components/teapot.py @@ -1,4 +1,5 @@ """Teapot component.""" + import functools import typing diff --git a/genshin/client/components/transaction.py b/genshin/client/components/transaction.py index 010e9bee..73dcd9cb 100644 --- a/genshin/client/components/transaction.py +++ b/genshin/client/components/transaction.py @@ -1,4 +1,5 @@ """Transaction client.""" + import functools import typing import urllib.parse diff --git a/genshin/client/components/wiki.py b/genshin/client/components/wiki.py index fbe1bb9c..5320fa46 100644 --- a/genshin/client/components/wiki.py +++ b/genshin/client/components/wiki.py @@ -1,4 +1,5 @@ """Wiki component.""" + import typing from genshin import types @@ -34,8 +35,7 @@ async def get_wiki_previews( # noqa: D102 missing docstring in overload? menu: typing.Literal[models.WikiPageType.CHARACTER], *, lang: typing.Optional[str] = None, - ) -> typing.Sequence[models.CharacterPreview]: - ... + ) -> typing.Sequence[models.CharacterPreview]: ... @typing.overload async def get_wiki_previews( # noqa: D102 missing docstring in overload? @@ -43,8 +43,7 @@ async def get_wiki_previews( # noqa: D102 missing docstring in overload? menu: typing.Literal[models.WikiPageType.WEAPON], *, lang: typing.Optional[str] = None, - ) -> typing.Sequence[models.WeaponPreview]: - ... + ) -> typing.Sequence[models.WeaponPreview]: ... @typing.overload async def get_wiki_previews( # noqa: D102 missing docstring in overload? @@ -52,8 +51,7 @@ async def get_wiki_previews( # noqa: D102 missing docstring in overload? menu: typing.Literal[models.WikiPageType.ARTIFACT], *, lang: typing.Optional[str] = None, - ) -> typing.Sequence[models.ArtifactPreview]: - ... + ) -> typing.Sequence[models.ArtifactPreview]: ... @typing.overload async def get_wiki_previews( # noqa: D102 missing docstring in overload? @@ -61,8 +59,7 @@ async def get_wiki_previews( # noqa: D102 missing docstring in overload? menu: typing.Literal[models.WikiPageType.ENEMY], *, lang: typing.Optional[str] = None, - ) -> typing.Sequence[models.EnemyPreview]: - ... + ) -> typing.Sequence[models.EnemyPreview]: ... @typing.overload async def get_wiki_previews( # noqa: D102 missing docstring in overload? @@ -70,8 +67,7 @@ async def get_wiki_previews( # noqa: D102 missing docstring in overload? menu: int, *, lang: typing.Optional[str] = None, - ) -> typing.Sequence[models.BaseWikiPreview]: - ... + ) -> typing.Sequence[models.BaseWikiPreview]: ... async def get_wiki_previews( self, diff --git a/genshin/client/manager/__init__.py b/genshin/client/manager/__init__.py index 5520a866..070264a0 100644 --- a/genshin/client/manager/__init__.py +++ b/genshin/client/manager/__init__.py @@ -1,3 +1,4 @@ """Cookie managers.""" + from .cookie import * from .managers import * diff --git a/genshin/client/manager/cookie.py b/genshin/client/manager/cookie.py index 1780de6e..a2b59d0c 100644 --- a/genshin/client/manager/cookie.py +++ b/genshin/client/manager/cookie.py @@ -15,6 +15,7 @@ - stoken (v2) + mid -> ltoken_v2 (token_type=2) - stoken (v2) + mid -> cookie_token_v2 (token_type=4) """ + from __future__ import annotations import typing diff --git a/genshin/client/manager/managers.py b/genshin/client/manager/managers.py index fceb4ca5..0e33324b 100644 --- a/genshin/client/manager/managers.py +++ b/genshin/client/manager/managers.py @@ -1,4 +1,5 @@ """Cookie managers for making authenticated requests.""" + from __future__ import annotations import abc diff --git a/genshin/client/ratelimit.py b/genshin/client/ratelimit.py index c5385400..61ef328d 100644 --- a/genshin/client/ratelimit.py +++ b/genshin/client/ratelimit.py @@ -1,4 +1,5 @@ """Ratelimit handlers.""" + import asyncio import functools import typing diff --git a/genshin/client/routes.py b/genshin/client/routes.py index e4804618..becf0e37 100644 --- a/genshin/client/routes.py +++ b/genshin/client/routes.py @@ -1,4 +1,5 @@ """API routes.""" + import abc import typing diff --git a/genshin/constants.py b/genshin/constants.py index 800d6012..2d86a6ff 100644 --- a/genshin/constants.py +++ b/genshin/constants.py @@ -1,4 +1,5 @@ """Constants hardcoded for optimizations.""" + from . import types __all__ = ["LANGS"] diff --git a/genshin/errors.py b/genshin/errors.py index de22d3b4..1ea5e1e2 100644 --- a/genshin/errors.py +++ b/genshin/errors.py @@ -1,4 +1,5 @@ """Errors received from the API.""" + import typing __all__ = [ diff --git a/genshin/models/__init__.py b/genshin/models/__init__.py index 5fb96b34..35edc5e7 100644 --- a/genshin/models/__init__.py +++ b/genshin/models/__init__.py @@ -1,4 +1,5 @@ """API models.""" + from .genshin import * from .honkai import * from .hoyolab import * diff --git a/genshin/models/genshin/__init__.py b/genshin/models/genshin/__init__.py index e841f5a4..248c29cc 100644 --- a/genshin/models/genshin/__init__.py +++ b/genshin/models/genshin/__init__.py @@ -1,4 +1,5 @@ """Genshin models.""" + from .calculator import * from .character import * from .chronicle import * diff --git a/genshin/models/genshin/calculator.py b/genshin/models/genshin/calculator.py index d7c258fb..4e871f13 100644 --- a/genshin/models/genshin/calculator.py +++ b/genshin/models/genshin/calculator.py @@ -1,4 +1,5 @@ """Genshin calculator models.""" + from __future__ import annotations import collections diff --git a/genshin/models/genshin/chronicle/__init__.py b/genshin/models/genshin/chronicle/__init__.py index 5fc8848c..094b8f71 100644 --- a/genshin/models/genshin/chronicle/__init__.py +++ b/genshin/models/genshin/chronicle/__init__.py @@ -1,4 +1,5 @@ """Battle chronicle models.""" + from .abyss import * from .activities import * from .characters import * diff --git a/genshin/models/genshin/chronicle/activities.py b/genshin/models/genshin/chronicle/activities.py index 2f9535d5..bb2b3931 100644 --- a/genshin/models/genshin/chronicle/activities.py +++ b/genshin/models/genshin/chronicle/activities.py @@ -1,4 +1,5 @@ """Chronicle activities models.""" + import datetime import re import typing diff --git a/genshin/models/genshin/chronicle/notes.py b/genshin/models/genshin/chronicle/notes.py index ea73337e..c9c52ac8 100644 --- a/genshin/models/genshin/chronicle/notes.py +++ b/genshin/models/genshin/chronicle/notes.py @@ -1,4 +1,5 @@ """Genshin chronicle notes.""" + import datetime import enum import typing diff --git a/genshin/models/genshin/chronicle/tcg.py b/genshin/models/genshin/chronicle/tcg.py index d2d5b934..ea1ec1dc 100644 --- a/genshin/models/genshin/chronicle/tcg.py +++ b/genshin/models/genshin/chronicle/tcg.py @@ -1,4 +1,5 @@ """Genshin serenitea pot replica display models.""" + from __future__ import annotations import enum diff --git a/genshin/models/genshin/constants.py b/genshin/models/genshin/constants.py index 8b8dd8ef..3b5cc134 100644 --- a/genshin/models/genshin/constants.py +++ b/genshin/models/genshin/constants.py @@ -1,4 +1,5 @@ """Genshin model constants.""" + import typing __all__ = ["CHARACTER_NAMES", "DBChar"] diff --git a/genshin/models/genshin/daily.py b/genshin/models/genshin/daily.py index c21854b0..1b2e2443 100644 --- a/genshin/models/genshin/daily.py +++ b/genshin/models/genshin/daily.py @@ -1,4 +1,5 @@ """Daily reward models.""" + import calendar import datetime import typing diff --git a/genshin/models/genshin/diary.py b/genshin/models/genshin/diary.py index d34617f5..afe27314 100644 --- a/genshin/models/genshin/diary.py +++ b/genshin/models/genshin/diary.py @@ -1,4 +1,5 @@ """Genshin diary models.""" + import datetime import enum import typing diff --git a/genshin/models/genshin/gacha.py b/genshin/models/genshin/gacha.py index 01bd0fdd..777b32a3 100644 --- a/genshin/models/genshin/gacha.py +++ b/genshin/models/genshin/gacha.py @@ -1,4 +1,5 @@ """Genshin wish models.""" + import datetime import enum import re diff --git a/genshin/models/genshin/lineup.py b/genshin/models/genshin/lineup.py index 5090601b..6ae71946 100644 --- a/genshin/models/genshin/lineup.py +++ b/genshin/models/genshin/lineup.py @@ -1,4 +1,5 @@ """Genshin lineup models.""" + from __future__ import annotations import datetime diff --git a/genshin/models/genshin/teapot.py b/genshin/models/genshin/teapot.py index 543c0216..c12e3c90 100644 --- a/genshin/models/genshin/teapot.py +++ b/genshin/models/genshin/teapot.py @@ -1,4 +1,5 @@ """Genshin serenitea pot replica display models.""" + from __future__ import annotations import datetime diff --git a/genshin/models/genshin/wiki.py b/genshin/models/genshin/wiki.py index a37d92fb..d1898455 100644 --- a/genshin/models/genshin/wiki.py +++ b/genshin/models/genshin/wiki.py @@ -1,4 +1,5 @@ """Genshin wish models.""" + import enum import json import typing diff --git a/genshin/models/honkai/__init__.py b/genshin/models/honkai/__init__.py index 36ec2f19..3d8f5c8f 100644 --- a/genshin/models/honkai/__init__.py +++ b/genshin/models/honkai/__init__.py @@ -1,4 +1,5 @@ """Honkai models.""" + from .battlesuit import * from .chronicle import * from .constants import * diff --git a/genshin/models/honkai/battlesuit.py b/genshin/models/honkai/battlesuit.py index cbc8ef6b..e77fbe34 100644 --- a/genshin/models/honkai/battlesuit.py +++ b/genshin/models/honkai/battlesuit.py @@ -1,4 +1,5 @@ """Honkai battlesuit model.""" + import logging import re import typing diff --git a/genshin/models/honkai/chronicle/__init__.py b/genshin/models/honkai/chronicle/__init__.py index fcf46466..d5698270 100644 --- a/genshin/models/honkai/chronicle/__init__.py +++ b/genshin/models/honkai/chronicle/__init__.py @@ -1,4 +1,5 @@ """Battle chronicle models.""" + from .battlesuits import * from .modes import * from .stats import * diff --git a/genshin/models/honkai/chronicle/battlesuits.py b/genshin/models/honkai/chronicle/battlesuits.py index eb08661e..6fec9c9d 100644 --- a/genshin/models/honkai/chronicle/battlesuits.py +++ b/genshin/models/honkai/chronicle/battlesuits.py @@ -1,4 +1,5 @@ """Honkai chronicle battlesuits.""" + import re import typing diff --git a/genshin/models/honkai/chronicle/modes.py b/genshin/models/honkai/chronicle/modes.py index 3ba35625..92067ebd 100644 --- a/genshin/models/honkai/chronicle/modes.py +++ b/genshin/models/honkai/chronicle/modes.py @@ -1,4 +1,5 @@ """Honkai battle chronicle models.""" + from __future__ import annotations import datetime diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index 43d9d513..b7f57490 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -1,4 +1,5 @@ """Honkai stats models.""" + import typing if typing.TYPE_CHECKING: diff --git a/genshin/models/honkai/constants.py b/genshin/models/honkai/constants.py index f793e5a8..3f644fa4 100644 --- a/genshin/models/honkai/constants.py +++ b/genshin/models/honkai/constants.py @@ -1,4 +1,5 @@ """Honkai model constants.""" + import typing __all__ = ["BATTLESUIT_IDENTIFIERS"] diff --git a/genshin/models/hoyolab/__init__.py b/genshin/models/hoyolab/__init__.py index b11f0436..9a911330 100644 --- a/genshin/models/hoyolab/__init__.py +++ b/genshin/models/hoyolab/__init__.py @@ -1,4 +1,5 @@ """Hoyolab models.""" + from .announcements import * from .private import * from .record import * diff --git a/genshin/models/hoyolab/record.py b/genshin/models/hoyolab/record.py index c6a22648..e47225d4 100644 --- a/genshin/models/hoyolab/record.py +++ b/genshin/models/hoyolab/record.py @@ -1,4 +1,5 @@ """Base hoyolab APIModels.""" + from __future__ import annotations import enum diff --git a/genshin/models/model.py b/genshin/models/model.py index c930eba5..9f80e7f3 100644 --- a/genshin/models/model.py +++ b/genshin/models/model.py @@ -1,4 +1,5 @@ """Modified pydantic model.""" + from __future__ import annotations import abc diff --git a/genshin/models/starrail/__init__.py b/genshin/models/starrail/__init__.py index 5f44a33b..2ecf1bf2 100644 --- a/genshin/models/starrail/__init__.py +++ b/genshin/models/starrail/__init__.py @@ -1,3 +1,4 @@ """Starrail models.""" + from .character import * from .chronicle import * diff --git a/genshin/models/starrail/character.py b/genshin/models/starrail/character.py index dc943f6e..1f311bd8 100644 --- a/genshin/models/starrail/character.py +++ b/genshin/models/starrail/character.py @@ -1,4 +1,5 @@ """Starrail base character model.""" + from genshin.models.model import APIModel, Unique diff --git a/genshin/models/starrail/chronicle/base.py b/genshin/models/starrail/chronicle/base.py index 97645018..0bd44af5 100644 --- a/genshin/models/starrail/chronicle/base.py +++ b/genshin/models/starrail/chronicle/base.py @@ -1,4 +1,5 @@ """Starrail Chronicle Base Model.""" + import datetime from genshin.models.model import APIModel diff --git a/genshin/models/starrail/chronicle/challenge.py b/genshin/models/starrail/chronicle/challenge.py index 18510312..b985ecf8 100644 --- a/genshin/models/starrail/chronicle/challenge.py +++ b/genshin/models/starrail/chronicle/challenge.py @@ -1,12 +1,29 @@ """Starrail chronicle challenge.""" -from typing import List + +from typing import TYPE_CHECKING, Any, Dict, List + +if TYPE_CHECKING: + import pydantic.v1 as pydantic +else: + try: + import pydantic.v1 as pydantic + except ImportError: + import pydantic from genshin.models.model import Aliased, APIModel from genshin.models.starrail.character import FloorCharacter from .base import PartialTime -__all__ = ["FloorNode", "StarRailChallenge", "StarRailFloor"] +__all__ = [ + "FictionBuff", + "FictionFloor", + "FictionFloorNode", + "FloorNode", + "StarRailChallenge", + "StarRailFloor", + "StarRailPureFiction", +] class FloorNode(APIModel): @@ -40,3 +57,65 @@ class StarRailChallenge(APIModel): has_data: bool floors: List[StarRailFloor] = Aliased("all_floor_detail") + + +class FictionBuff(APIModel): + """Buff for a Pure Fiction floor.""" + + id: int + name: str = Aliased("name_mi18n") + description: str = Aliased("desc_mi18n") + icon: str + + +class FictionFloorNode(FloorNode): + """Node for a Pure Fiction floor.""" + + buff: FictionBuff + score: int + + +class FictionFloor(APIModel): + """Floor in a Pure Fiction challenge.""" + + id: int = Aliased("maze_id") + name: str + round_num: int + star_num: int + node_1: FictionFloorNode + node_2: FictionFloorNode + is_fast: bool + + @property + def score(self) -> int: + """Total score of the floor.""" + return self.node_1.score + self.node_2.score + + +class StarRailPureFiction(APIModel): + """Pure Fiction challenge in a season.""" + + name: str + season_id: int + begin_time: PartialTime + end_time: PartialTime + + total_stars: int = Aliased("star_num") + max_floor: str + total_battles: int = Aliased("battle_num") + has_data: bool + + floors: List[FictionFloor] = Aliased("all_floor_detail") + max_floor_id: int + + @pydantic.root_validator(pre=True) + def __unnest_groups(cls, values: Dict[str, Any]) -> Dict[str, Any]: + if "groups" in values and isinstance(values["groups"], List): + groups: List[Dict[str, Any]] = values["groups"] + if len(groups) > 0: + values["name"] = groups[0]["name_mi18n"] + values["season_id"] = groups[0]["schedule_id"] + values["begin_time"] = groups[0]["begin_time"] + values["end_time"] = groups[0]["end_time"] + + return values diff --git a/genshin/models/starrail/chronicle/characters.py b/genshin/models/starrail/chronicle/characters.py index bf8d4c55..f6d3bde3 100644 --- a/genshin/models/starrail/chronicle/characters.py +++ b/genshin/models/starrail/chronicle/characters.py @@ -1,4 +1,5 @@ """Starrail chronicle character.""" + from typing import List, Optional from genshin.models.model import APIModel diff --git a/genshin/models/starrail/chronicle/notes.py b/genshin/models/starrail/chronicle/notes.py index 59e03c21..d3fa8f8e 100644 --- a/genshin/models/starrail/chronicle/notes.py +++ b/genshin/models/starrail/chronicle/notes.py @@ -1,4 +1,5 @@ """Starrail chronicle notes.""" + import datetime import typing diff --git a/genshin/models/starrail/chronicle/rogue.py b/genshin/models/starrail/chronicle/rogue.py index 9a034e95..ea2b5329 100644 --- a/genshin/models/starrail/chronicle/rogue.py +++ b/genshin/models/starrail/chronicle/rogue.py @@ -1,4 +1,5 @@ """Starrail Rogue models.""" + from typing import List from genshin.models.model import APIModel diff --git a/genshin/models/starrail/chronicle/stats.py b/genshin/models/starrail/chronicle/stats.py index 89aa17c8..c25a17ba 100644 --- a/genshin/models/starrail/chronicle/stats.py +++ b/genshin/models/starrail/chronicle/stats.py @@ -1,4 +1,5 @@ """Starrail chronicle stats.""" + import typing from genshin.models.model import Aliased, APIModel diff --git a/genshin/paginators/__init__.py b/genshin/paginators/__init__.py index 8afc4e8a..468a0250 100644 --- a/genshin/paginators/__init__.py +++ b/genshin/paginators/__init__.py @@ -1,3 +1,4 @@ """Fancy paginators with a large amount of useful methods.""" + from .api import * from .base import * diff --git a/genshin/paginators/api.py b/genshin/paginators/api.py index 217665a9..8201a84f 100644 --- a/genshin/paginators/api.py +++ b/genshin/paginators/api.py @@ -1,4 +1,5 @@ """Base paginators made specifically for interaction with the api.""" + from __future__ import annotations import abc diff --git a/genshin/paginators/base.py b/genshin/paginators/base.py index 4eff9bae..cf5be77e 100644 --- a/genshin/paginators/base.py +++ b/genshin/paginators/base.py @@ -91,8 +91,7 @@ def __await__(self) -> typing.Generator[None, None, typing.Sequence[T]]: return self.flatten().__await__() @abc.abstractmethod - async def __anext__(self) -> T: - ... + async def __anext__(self) -> T: ... class BasicPaginator(typing.Generic[T], Paginator[T], abc.ABC): diff --git a/genshin/types.py b/genshin/types.py index c1ced452..9b87fd9c 100644 --- a/genshin/types.py +++ b/genshin/types.py @@ -1,4 +1,5 @@ """Types used in the library.""" + import enum import typing diff --git a/genshin/utility/__init__.py b/genshin/utility/__init__.py index b2254923..cd1e07c3 100644 --- a/genshin/utility/__init__.py +++ b/genshin/utility/__init__.py @@ -1,4 +1,5 @@ """Utilities for genshin.py.""" + from . import geetest from .concurrency import * from .ds import * diff --git a/genshin/utility/concurrency.py b/genshin/utility/concurrency.py index 9e6d0967..681a5cf5 100644 --- a/genshin/utility/concurrency.py +++ b/genshin/utility/concurrency.py @@ -1,4 +1,5 @@ """Utilities for concurrency optimizations.""" + from __future__ import annotations import asyncio diff --git a/genshin/utility/deprecation.py b/genshin/utility/deprecation.py index 0d06fd1d..c99d9bb1 100644 --- a/genshin/utility/deprecation.py +++ b/genshin/utility/deprecation.py @@ -1,4 +1,5 @@ """Deprecation decorator.""" + import functools import inspect import typing diff --git a/genshin/utility/ds.py b/genshin/utility/ds.py index 8f9466d5..1cd169f5 100644 --- a/genshin/utility/ds.py +++ b/genshin/utility/ds.py @@ -1,4 +1,5 @@ """Dynamic secret generation.""" + import hashlib import json import random diff --git a/genshin/utility/fs.py b/genshin/utility/fs.py index feac290a..51f33186 100644 --- a/genshin/utility/fs.py +++ b/genshin/utility/fs.py @@ -1,4 +1,5 @@ """File system related utilities.""" + import functools import pathlib import tempfile diff --git a/genshin/utility/geetest.py b/genshin/utility/geetest.py index 4d535d7d..300ba2ae 100644 --- a/genshin/utility/geetest.py +++ b/genshin/utility/geetest.py @@ -1,4 +1,5 @@ """Geetest utilities.""" + import base64 import json import typing diff --git a/genshin/utility/logfile.py b/genshin/utility/logfile.py index f516b5e9..f0b6719b 100644 --- a/genshin/utility/logfile.py +++ b/genshin/utility/logfile.py @@ -1,4 +1,5 @@ """Search logfile for authkeys.""" + import pathlib import re import typing diff --git a/genshin/utility/uid.py b/genshin/utility/uid.py index 132aece9..e0fabcae 100644 --- a/genshin/utility/uid.py +++ b/genshin/utility/uid.py @@ -1,4 +1,5 @@ """Utility functions related to genshin.""" + import typing from genshin import types diff --git a/noxfile.py b/noxfile.py index 9f5c0ea1..550a0401 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,5 @@ """Nox file.""" + from __future__ import annotations import logging diff --git a/tests/models/test_model.py b/tests/models/test_model.py index 50897a6e..c9978389 100644 --- a/tests/models/test_model.py +++ b/tests/models/test_model.py @@ -6,8 +6,7 @@ import genshin -class LiteralCharacter(genshin.models.BaseCharacter): - ... +class LiteralCharacter(genshin.models.BaseCharacter): ... LiteralCharacter.__pre_root_validators__ = LiteralCharacter.__pre_root_validators__[:-1] From a424187d1a085980cf9a4f0142c3a0d099be4c94 Mon Sep 17 00:00:00 2001 From: Zhi Heng Date: Tue, 6 Feb 2024 22:25:39 +0800 Subject: [PATCH 02/20] Make HSR Pure Fiction floor buff optional (#158) --- genshin/models/starrail/chronicle/challenge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/genshin/models/starrail/chronicle/challenge.py b/genshin/models/starrail/chronicle/challenge.py index b985ecf8..d71f2ee2 100644 --- a/genshin/models/starrail/chronicle/challenge.py +++ b/genshin/models/starrail/chronicle/challenge.py @@ -1,6 +1,6 @@ """Starrail chronicle challenge.""" -from typing import TYPE_CHECKING, Any, Dict, List +from typing import TYPE_CHECKING, Any, Dict, List, Optional if TYPE_CHECKING: import pydantic.v1 as pydantic @@ -71,7 +71,7 @@ class FictionBuff(APIModel): class FictionFloorNode(FloorNode): """Node for a Pure Fiction floor.""" - buff: FictionBuff + buff: Optional[FictionBuff] score: int From 9b3182771f1bf1cb03d5f35fc5fcb850ff638c19 Mon Sep 17 00:00:00 2001 From: JokelBaf <60827680+JokelBaf@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:08:28 +0200 Subject: [PATCH 03/20] Fixes and improvements in geetest module --- genshin/client/components/geetest/client.py | 31 +++++++++++++-------- genshin/client/components/geetest/server.py | 5 ++-- genshin/utility/geetest.py | 16 +++++------ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/genshin/client/components/geetest/client.py b/genshin/client/components/geetest/client.py index 036fa4b4..d5d794bd 100644 --- a/genshin/client/components/geetest/client.py +++ b/genshin/client/components/geetest/client.py @@ -20,7 +20,7 @@ class GeetestClient(base.BaseClient): """Geetest client component.""" - async def web_login( + async def _web_login( self, account: str, password: str, @@ -69,12 +69,13 @@ async def web_login( return cookies - async def app_login( + async def _app_login( self, account: str, password: str, *, geetest: typing.Optional[typing.Dict[str, typing.Any]] = None, + ticket: typing.Optional[typing.Dict[str, typing.Any]] = None, ) -> typing.Dict[str, typing.Any]: """Login with a password using HoYoLab app endpoint. @@ -89,6 +90,10 @@ async def app_login( session_id = geetest["session_id"] headers["x-rpc-aigis"] = geetest_utility.get_aigis_header(session_id, mmt_data) + if ticket: + ticket["verify_str"] = json.dumps(ticket["verify_str"]) + headers["x-rpc-verify"] = json.dumps(ticket) + payload = { "account": geetest_utility.encrypt_geetest_credentials(account), "password": geetest_utility.encrypt_geetest_credentials(password), @@ -110,7 +115,9 @@ async def app_login( if data["retcode"] == -3239: # Email verification required - return json.loads(r.headers["x-rpc-verify"]) + verify = json.loads(r.headers["x-rpc-verify"]) + verify["verify_str"] = json.loads(verify["verify_str"]) + return verify if not data["data"]: errors.raise_for_retcode(data) @@ -126,7 +133,7 @@ async def app_login( return cookies - async def send_verification_email( + async def _send_verification_email( self, ticket: typing.Dict[str, typing.Any], *, @@ -164,7 +171,7 @@ async def send_verification_email( return None - async def verify_email(self, code: str, ticket: typing.Dict[str, typing.Any]) -> 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( @@ -200,7 +207,7 @@ async def login_with_password( 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) + result = await self._web_login(account, password, tokenType=tokenType) if "session_id" not in result: # Captcha not triggered @@ -211,7 +218,7 @@ async def login_with_password( else: geetest = await server.solve_geetest(result, port=port) - return await self.web_login(account, password, tokenType=tokenType, geetest=geetest) + return await self._web_login(account, password, tokenType=tokenType, geetest=geetest) async def login_with_app_password( self, @@ -232,7 +239,7 @@ async def login_with_app_password( 2. Email verification is triggered (can happen if you first login with a new device). """ - result = await self.app_login(account, password) + result = await self._app_login(account, password) if "session_id" in result: # Captcha triggered @@ -241,18 +248,20 @@ async def login_with_app_password( else: geetest = await server.solve_geetest(result, port=port) - result = await self.app_login(account, password, geetest=geetest) + result = await self._app_login(account, password, geetest=geetest) if "risk_ticket" in result: # Email verification required - mmt = await self.send_verification_email(result) + 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 self._send_verification_email(result, geetest=geetest) + await server.verify_email(self, result, port=port) - result = await self.app_login(account, password) + result = await self._app_login(account, password, ticket=result) return result diff --git a/genshin/client/components/geetest/server.py b/genshin/client/components/geetest/server.py index a8bb641b..6f69ae8e 100644 --- a/genshin/client/components/geetest/server.py +++ b/genshin/client/components/geetest/server.py @@ -65,7 +65,7 @@ def get_page(page: typing.Literal["captcha", "verify-email"]) -> str: - """ - if page == "captcha" - else """ + """, + "verify-email": """ @@ -76,15 +72,35 @@ def get_page(page: typing.Literal["captcha", "verify-email"]) -> str: }; - """ - ) + """, + "enter-otp": """ + + + + + + + + + """, +} GT_URL = "https://raw.githubusercontent.com/GeeTeam/gt3-node-sdk/master/demo/static/libs/gt.js" async def launch_webapp( - page: typing.Literal["captcha", "verify-email"], + page: typing.Literal["captcha", "verify-email", "enter-otp"], *, port: int = 5000, mmt: typing.Optional[typing.Dict[str, typing.Any]] = None, @@ -95,11 +111,15 @@ async def launch_webapp( @routes.get("/captcha") async def captcha(request: web.Request) -> web.StreamResponse: - return web.Response(body=get_page("captcha"), content_type="text/html") + return web.Response(body=PAGES["captcha"], 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") + return web.Response(body=PAGES["verify-email"], content_type="text/html") + + @routes.get("/enter-otp") + async def enter_otp(request: web.Request) -> web.StreamResponse: + return web.Response(body=PAGES["enter-otp"], content_type="text/html") @routes.get("/gt.js") async def gt(request: web.Request) -> web.StreamResponse: @@ -162,3 +182,11 @@ async def verify_email( code = data["code"] return await client._verify_email(code, ticket) + + +async def enter_otp(port: int = 5000) -> str: + """Lets user enter the OTP.""" + # The enter-otp page is the same as verify-email page. + data = await launch_webapp("enter-otp", port=port) + code = data["code"] + return code diff --git a/genshin/client/routes.py b/genshin/client/routes.py index f61dea89..aa153ee3 100644 --- a/genshin/client/routes.py +++ b/genshin/client/routes.py @@ -13,6 +13,7 @@ "BBS_REFERER_URL", "BBS_URL", "CALCULATOR_URL", + "CN_WEB_LOGIN_URL", "COMMUNITY_URL", "COOKIE_V2_REFRESH_URL", "DETAIL_LEDGER_URL", @@ -217,8 +218,13 @@ def get_url(self, region: types.Region, game: types.Game) -> yarl.URL: 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") +CN_WEB_LOGIN_URL = Route("https://passport-api.miyoushe.com/account/ma-cn-passport/web/loginByPassword") 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") + +CHECK_MOBILE_VALIDITY_URL = Route("https://webapi.account.mihoyo.com/Api/is_mobile_registrable") +MOBILE_OTP_URL = Route("https://passport-api.miyoushe.com/account/ma-cn-verifier/verifier/createLoginCaptcha") +MOBILE_LOGIN_URL = Route("https://passport-api.miyoushe.com/account/ma-cn-passport/web/loginByMobileCaptcha") diff --git a/genshin/errors.py b/genshin/errors.py index 1ea5e1e2..f62ade2f 100644 --- a/genshin/errors.py +++ b/genshin/errors.py @@ -31,7 +31,11 @@ class GenshinException(Exception): original: str = "" msg: str = "" - def __init__(self, response: typing.Mapping[str, typing.Any] = {}, msg: typing.Optional[str] = None) -> None: + def __init__( + self, + response: typing.Mapping[str, typing.Any] = {}, + msg: typing.Optional[str] = None, + ) -> None: self.retcode = response.get("retcode", self.retcode) self.original = response.get("message", "") self.msg = msg or self.msg or self.original @@ -176,6 +180,12 @@ class AccountHasLocked(GenshinException): msg = "Account has been locked because exceeded password limit. Please wait 20 minute and try again" +class WrongOTP(GenshinException): + """Wrong OTP code.""" + + msg = "The provided OTP code is wrong." + + _TGE = typing.Type[GenshinException] _errors: typing.Dict[int, typing.Union[_TGE, str, typing.Tuple[_TGE, typing.Optional[str]]]] = { # misc hoyolab @@ -189,7 +199,10 @@ class AccountHasLocked(GenshinException): # database game record 10101: TooManyRequests, 10102: DataNotPublic, - 10103: (InvalidCookies, "Cookies are valid but do not have a hoyolab account bound to them."), + 10103: ( + InvalidCookies, + "Cookies are valid but do not have a hoyolab account bound to them.", + ), 10104: "Cannot view real-time notes of other users.", # calculator -500001: "Invalid fields in calculation.", @@ -210,7 +223,10 @@ class AccountHasLocked(GenshinException): -2016: RedemptionCooldown, -2017: RedemptionClaimed, -2018: RedemptionClaimed, - -2021: (RedemptionException, "Cannot claim codes for accounts with adventure rank lower than 10."), + -2021: ( + RedemptionException, + "Cannot claim codes for accounts with adventure rank lower than 10.", + ), # rewards -5003: AlreadyClaimed, # chinese @@ -219,6 +235,7 @@ class AccountHasLocked(GenshinException): # account -3208: AccountLoginFail, -3202: AccountHasLocked, + -3205: WrongOTP, } ERRORS: typing.Dict[int, typing.Tuple[_TGE, typing.Optional[str]]] = { diff --git a/genshin/utility/geetest.py b/genshin/utility/geetest.py index ad8ed8bd..fe388afb 100644 --- a/genshin/utility/geetest.py +++ b/genshin/utility/geetest.py @@ -4,11 +4,13 @@ import json import typing +from ..types import Region + __all__ = ["encrypt_geetest_credentials"] # RSA key is the same for app and web login -LOGIN_KEY_CERT = b""" +OS_LOGIN_KEY_CERT = b""" -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4PMS2JVMwBsOIrYWRluY wEiFZL7Aphtm9z5Eu/anzJ09nB00uhW+ScrDWFECPwpQto/GlOJYCUwVM/raQpAj @@ -20,6 +22,15 @@ -----END PUBLIC KEY----- """ +CN_LOGIN_KEY_CERT = b""" +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDvekdPMHN3AYhm/vktJT+YJr7 +cI5DcsNKqdsx5DZX0gDuWFuIjzdwButrIYPNmRJ1G8ybDIF7oDW2eEpm5sMbL9zs +9ExXCdvqrn51qELbqj0XxtMTIpaCHFSI50PfPpTFV9Xt/hmyVwokoOXFlAEgCn+Q +CgGs52bFoYMtyi+xEQIDAQAB +-----END PUBLIC KEY----- +""" + WEB_LOGIN_HEADERS = { "x-rpc-app_id": "c9oqaq3s3gu8", "x-rpc-client_type": "4", @@ -38,6 +49,16 @@ # "x-rpc-device_id": "1c33337bd45c1bfs", } +CN_LOGIN_HEADERS = { + "x-rpc-app_id": "bll8iq97cem8", + "x-rpc-client_type": "4", + "Origin": "https://user.miyoushe.com", + "Referer": "https://user.miyoushe.com/", + "x-rpc-source": "v2.webLogin", + "x-rpc-mi_referrer": "https://user.miyoushe.com/login-platform/index.html?app_id=bll8iq97cem8&theme=&token_type=4&game_biz=bbs_cn&message_origin=https%253A%252F%252Fwww.miyoushe.com&succ_back_type=message%253Alogin-platform%253Alogin-success&fail_back_type=message%253Alogin-platform%253Alogin-fail&ux_mode=popup&iframe_level=1#/login/password", # noqa: E501 + "x-rpc-device_id": "586f2440-856a-4243-8076-2b0a12314197", +} + EMAIL_SEND_HEADERS = { "x-rpc-app_id": "c9oqaq3s3gu8", "x-rpc-client_type": "2", @@ -49,11 +70,13 @@ } -def encrypt_geetest_credentials(text: str) -> str: +def encrypt_geetest_credentials(text: str, region: Region = Region.OVERSEAS) -> str: """Encrypt text for geetest.""" import rsa - public_key = rsa.PublicKey.load_pkcs1_openssl_pem(LOGIN_KEY_CERT) + public_key = rsa.PublicKey.load_pkcs1_openssl_pem( + OS_LOGIN_KEY_CERT if region is Region.OVERSEAS else CN_LOGIN_KEY_CERT + ) crypto = rsa.encrypt(text.encode("utf-8"), public_key) return base64.b64encode(crypto).decode("utf-8") From f6f4c11bd22abe5b31c741db5a29598480c8bf27 Mon Sep 17 00:00:00 2001 From: ashlen Date: Wed, 27 Mar 2024 12:02:03 +0100 Subject: [PATCH 14/20] Add star-rail notes to cli (#170) resolves #169 --- genshin/__main__.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/genshin/__main__.py b/genshin/__main__.py index 2f7e8f3a..fd047c9a 100644 --- a/genshin/__main__.py +++ b/genshin/__main__.py @@ -68,8 +68,10 @@ async def accounts(client: genshin.Client) -> None: genshin_group: click.Group = click.Group("genshin", help="Genshin-related commands.") honkai_group: click.Group = click.Group("honkai", help="Honkai-related commands.") +starrail_group: click.Group = click.Group("starrail", help="StarRail-related commands.") cli.add_command(genshin_group) cli.add_command(honkai_group) +cli.add_command(starrail_group) @honkai_group.command("stats") @@ -199,6 +201,33 @@ async def genshin_notes(client: genshin.Client, uid: typing.Optional[int]) -> No click.echo(f" - {expedition.status}") +@starrail_group.command("notes") +@click.argument("uid", type=int, default=None, required=False) +@client_command +async def starrail_notes(client: genshin.Client, uid: typing.Optional[int]) -> None: + """Show real-Time starrail notes.""" + click.echo("Real-Time notes.") + + data = await client.get_starrail_notes(uid) + + click.echo(f"{click.style('TB power:', bold=True)} {data.current_stamina}/{data.max_stamina}", nl=False) + click.echo(f" (Full in {data.stamina_recover_time})" if data.stamina_recover_time > datetime.timedelta(0) else "") + click.echo(f"{click.style('Reserved TB power:', bold=True)} {data.current_reserve_stamina}/2400") + click.echo(f"{click.style('Daily training:', bold=True)} {data.current_train_score}/{data.max_train_score}") + click.echo(f"{click.style('Simulated Universe:', bold=True)} {data.current_rogue_score}/{data.max_rogue_score}") + click.echo( + f"{click.style('Echo of War:', bold=True)} {data.remaining_weekly_discounts}/{data.max_weekly_discounts}" + ) + + click.echo(f"\n{click.style('Assignments:', bold=True)} {data.accepted_epedition_num}/{data.total_expedition_num}") + for expedition in data.expeditions: + if expedition.remaining_time > datetime.timedelta(0): + remaining = f"{expedition.remaining_time} remaining" + click.echo(f" - {expedition.name} | {remaining}") + else: + click.echo(f" - {expedition.name} | Finished") + + @cli.command() @click.option("--scenario", help="Scenario ID or name to use (eg '12-3').", type=str, default=None) @client_command From 803b94d0444fb973e820b6cdc8bbac704c863e60 Mon Sep 17 00:00:00 2001 From: seria Date: Thu, 28 Mar 2024 23:34:44 +0900 Subject: [PATCH 15/20] Migrate to ruff (#168) --- .flake8 | 43 -------------- genshin-dev/lint-requirements.txt | 14 +---- genshin-dev/reformat-requirements.txt | 4 +- genshin/models/genshin/chronicle/abyss.py | 8 +-- genshin/models/genshin/chronicle/stats.py | 2 +- genshin/models/genshin/lineup.py | 2 +- genshin/models/honkai/chronicle/stats.py | 5 +- genshin/utility/extdb.py | 5 +- noxfile.py | 9 ++- pyproject.toml | 68 ++++++++++++++++++++++- 10 files changed, 85 insertions(+), 75 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index eb1a7f74..00000000 --- a/.flake8 +++ /dev/null @@ -1,43 +0,0 @@ -[flake8] -exclude = tests, test.py - -# A001, A002, A003: `id` variable/parameter/attribute -# C408: dict() with keyword arguments -# D105: Missing docstring in magic method -# D106: Missing docstring Model.Config -# D419: Docstring is empty -# E704: Multiple statements on one line (def) -# S101: Use of assert for type checking -# S303: Use of md5 -# S311: Use of pseudo-random generators -# S324: Use of md5 without usedforsecurity=False (3.9+) -# W503: line break before binary operator -ignore = - A001, A002, A003, - C408, - D105, D106, D419, - E704, - S101, S303, S311, S324, - W503, - -# F401: unused import. -# F403: cannot detect unused vars if we use starred import -# D10*: docstrings -# S10*: hardcoded passwords -# F841: unused variable -# I900: nox is a dev dependency -per-file-ignores = - **/__init__.py: F401, F403 - tests/**: D10, S10, F841 - noxfile.py: I900 - -max-complexity = 16 -max-function-length = 100 -max-line-length = 130 - -max_annotations_complexity = 5 - -accept-encodings = utf-8 -docstring-convention = numpy -ignore-decorators = property -requirements_file = requirements.txt \ No newline at end of file diff --git a/genshin-dev/lint-requirements.txt b/genshin-dev/lint-requirements.txt index 4073601f..ede3eb4e 100644 --- a/genshin-dev/lint-requirements.txt +++ b/genshin-dev/lint-requirements.txt @@ -1,13 +1 @@ -flake8 - -flake8-annotations-complexity # complex annotation -flake8-black # runs black -flake8-builtins # builtin shadowing -flake8-docstrings # proper formatting and grammar in docstrings -flake8-isort # runs isort -flake8-mutable # mutable default argument detection -flake8-pep3101 # new-style format strings only -flake8-print # complain about print statements in code -flake8-pytest-style # pytest checks -flake8-raise # exception raising -flake8-requirements # requirements.txt check +ruff \ No newline at end of file diff --git a/genshin-dev/reformat-requirements.txt b/genshin-dev/reformat-requirements.txt index bee49858..1cf5c13f 100644 --- a/genshin-dev/reformat-requirements.txt +++ b/genshin-dev/reformat-requirements.txt @@ -1,3 +1,3 @@ black -isort -sort-all +ruff +sort-all \ No newline at end of file diff --git a/genshin/models/genshin/chronicle/abyss.py b/genshin/models/genshin/chronicle/abyss.py index 829fcd7d..159c2f8f 100644 --- a/genshin/models/genshin/chronicle/abyss.py +++ b/genshin/models/genshin/chronicle/abyss.py @@ -47,13 +47,13 @@ class CharacterRanks(APIModel): most_played: typing.Sequence[AbyssRankCharacter] = Aliased("reveal_rank", default=[], mi18n="bbs/go_fight_count") most_kills: typing.Sequence[AbyssRankCharacter] = Aliased("defeat_rank", default=[], mi18n="bbs/max_rout_count") strongest_strike: typing.Sequence[AbyssRankCharacter] = Aliased("damage_rank", default=[], mi18n="bbs/powerful_attack") - most_damage_taken: typing.Sequence[AbyssRankCharacter] = Aliased("take_damage_rank", default=[], mi18n="bbs/receive_max_damage") - most_bursts_used: typing.Sequence[AbyssRankCharacter] = Aliased("energy_skill_rank", default=[], mi18n="bbs/element_break_count") - most_skills_used: typing.Sequence[AbyssRankCharacter] = Aliased("normal_skill_rank", default=[], mi18n="bbs/element_skill_use_count") + most_damage_taken: typing.Sequence[AbyssRankCharacter] = Aliased("take_damage_rank", default=[], mi18n="bbs/receive_max_damage") # noqa: E501 + most_bursts_used: typing.Sequence[AbyssRankCharacter] = Aliased("energy_skill_rank", default=[], mi18n="bbs/element_break_count") # noqa: E501 + most_skills_used: typing.Sequence[AbyssRankCharacter] = Aliased("normal_skill_rank", default=[], mi18n="bbs/element_skill_use_count") # noqa: E501 # fmt: on def as_dict(self, lang: typing.Optional[str] = None) -> typing.Mapping[str, typing.Any]: - """Helper function which turns fields into properly named ones""" + """Turn fields into properly named ones.""" return { self._get_mi18n(field, lang or self.lang): getattr(self, field.name) for field in self.__fields__.values() diff --git a/genshin/models/genshin/chronicle/stats.py b/genshin/models/genshin/chronicle/stats.py index fb4d9e75..0734a886 100644 --- a/genshin/models/genshin/chronicle/stats.py +++ b/genshin/models/genshin/chronicle/stats.py @@ -53,7 +53,7 @@ class Stats(APIModel): # fmt: on def as_dict(self, lang: typing.Optional[str] = None) -> typing.Mapping[str, typing.Any]: - """Helper function which turns fields into properly named ones""" + """Turn fields into properly named ones.""" return { self._get_mi18n(field, lang or self.lang): getattr(self, field.name) for field in self.__fields__.values() diff --git a/genshin/models/genshin/lineup.py b/genshin/models/genshin/lineup.py index 6ae71946..76f3865e 100644 --- a/genshin/models/genshin/lineup.py +++ b/genshin/models/genshin/lineup.py @@ -296,7 +296,7 @@ def __parse_characters(cls, value: typing.Any) -> typing.Any: if isinstance(value[0], typing.Sequence): return value - return [[character for character in group["group"]] for group in value] + return [list(group["group"]) for group in value] class Lineup(LineupPreview): diff --git a/genshin/models/honkai/chronicle/stats.py b/genshin/models/honkai/chronicle/stats.py index b7f57490..1bb6745d 100644 --- a/genshin/models/honkai/chronicle/stats.py +++ b/genshin/models/honkai/chronicle/stats.py @@ -24,7 +24,7 @@ def _model_to_dict(model: APIModel, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: - """Helper function which turns fields into properly named ones""" + """Turn fields into properly named ones.""" ret: typing.Dict[str, typing.Any] = {} for field in model.__fields__.values(): if not field.field_info.extra.get("mi18n"): @@ -123,7 +123,7 @@ class OldAbyssStats(APIModel): raw_tier: int = Aliased("latest_area", mi18n="bbs/settled_level") raw_latest_rank: typing.Optional[int] = Aliased("latest_level", mi18n="bbs/rank") # TODO: Add proper key - latest_type: str = Aliased( mi18n="bbs/latest_type") + latest_type: str = Aliased( mi18n="bbs/latest_type") # fmt: on @pydantic.validator("raw_q_singularis_rank", "raw_dirac_sea_rank", "raw_latest_rank", pre=True) @@ -240,6 +240,7 @@ def __pack_gamemode_stats(cls, values: typing.Dict[str, typing.Any]) -> typing.D return values def as_dict(self, lang: str = "en-us") -> typing.Mapping[str, typing.Any]: + """Turn fields into properly named ones.""" return _model_to_dict(self, lang) diff --git a/genshin/utility/extdb.py b/genshin/utility/extdb.py index 52fb5ac6..8225d504 100644 --- a/genshin/utility/extdb.py +++ b/genshin/utility/extdb.py @@ -2,6 +2,7 @@ import asyncio import json +import logging import time import typing import warnings @@ -20,6 +21,8 @@ "update_characters_genshindata", ) +LOGGER_ = logging.getLogger(__name__) + CACHE_FILE = fs.get_tempdir() / "characters.json" if CACHE_FILE.exists() and time.time() - CACHE_FILE.stat().st_mtime < 7 * 24 * 60 * 60: @@ -235,7 +238,7 @@ async def update_characters_any( try: await updator(langs) except Exception: - continue + LOGGER_.exception("Failed to update characters with %s", updator.__name__) else: return diff --git a/noxfile.py b/noxfile.py index 550a0401..3da55b37 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,7 +31,7 @@ def install_requirements(session: nox.Session, *requirements: str, literal: bool """Install requirements.""" if not literal and all(requirement.isalpha() for requirement in requirements): files = ["requirements.txt"] + [f"./genshin-dev/{requirement}-requirements.txt" for requirement in requirements] - requirements = ("pip",) + tuple(arg for file in files for arg in ("-r", file)) + requirements = ("pip", *tuple(arg for file in files for arg in ("-r", file))) session.install("--upgrade", *requirements, silent=not isverbose()) @@ -52,16 +52,15 @@ def docs(session: nox.Session) -> None: def lint(session: nox.Session) -> None: """Run this project's modules against the pre-defined flake8 linters.""" install_requirements(session, "lint") - session.run("flake8", "--version") - session.run("flake8", *GENERAL_TARGETS, *verbose_args()) + session.run("ruff", "check", *GENERAL_TARGETS, *verbose_args()) @nox.session() def reformat(session: nox.Session) -> None: """Reformat this project's modules to fit the standard style.""" install_requirements(session, "reformat") - session.run("black", *GENERAL_TARGETS, *verbose_args()) - session.run("isort", *GENERAL_TARGETS, *verbose_args()) + session.run("python", "-m", "black", *GENERAL_TARGETS, *verbose_args()) + session.run("python", "-m", "ruff", "check", "--fix-only", "--fixable", "ALL", *GENERAL_TARGETS, *verbose_args()) session.log("sort-all") LOGGER.disabled = True diff --git a/pyproject.toml b/pyproject.toml index 804aafe2..96634f51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,72 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 120 -target-version = ["py39"] -[tool.isort] -profile = "black" +[tool.ruff] +line-length = 120 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "A", + "C4", + "C9", + "D", + "E", + "F", + "S", + "W", + "T20", + "PT", + "RSE" +] +exclude = ["tests", "test.py"] + +# A001, A002, A003: `id` variable/parameter/attribute +# C408: dict() with keyword arguments +# D101: Missing docstring in public module +# D105: Missing docstring in magic method +# D106: Missing docstring Model.Config +# D400: First line should end with a period +# D419: Docstring is empty +# PT007: Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple` +# PT018: Assertion should be broken down into multiple parts +# S101: Use of assert for type checking +# S303: Use of md5 +# S311: Use of pseudo-random generators +# S324: Use of md5 without usedforsecurity=False (3.9+) +ignore = [ + "A001", "A002", "A003", + "C408", + "D100", "D105", "D106", "D400", "D419", + "PT007", "PT018", + "S101", "S303", "S311", "S324", +] + +# auto-fixing too intrusive +# F401: Unused import +# F841: Unused variable +# B007: Unused loop variable +unfixable = ["F401", "F841", "B007"] + +[tool.ruff.lint.per-file-ignores] +# F401: unused import. +# F403: cannot detect unused vars if we use starred import +# D10*: docstrings +# S10*: hardcoded passwords +# F841: unused variable +"**/__init__.py" = ["F401", "F403"] +"tests/**" = ["D10", "S10", "F841"] + +[tool.ruff.lint.mccabe] +max-complexity = 16 + +[tool.ruff.lint.pycodestyle] +max-line-length = 130 + +[tool.ruff.lint.pydocstyle] +convention = "numpy" +ignore-decorators = ["property"] [tool.pytest.ini_options] asyncio_mode = "auto" From 16d7a7c258a1aefefbaa07bbbe0922f0f7239f78 Mon Sep 17 00:00:00 2001 From: seria Date: Mon, 1 Apr 2024 07:27:20 +0900 Subject: [PATCH 16/20] Add QR code login method for Miyoushe (#173) * chore(deps): Add dependencies * feat: Add cookie and qrcode models * feat: Add new routes * feat: Add new ds salt * feat: Add new qrcode-related methods to GeetestClient * feat: Add new game token related functions * feat: Add new ds utilities * Export miyoushe modules * refactor: Refactor passport ds utility func * chore: Run nox * chore(deps): Merge qrcode and pillow * fix: Change account_id type to int * feat: Add public method to client * feat: Set cookies to client after obtaining cookies * chore(deps): Add qrcode[pil] to geetest extras * docs: Add docstring for private methods --- genshin/client/components/geetest/client.py | 98 +++++++++++++++++++++ genshin/client/manager/cookie.py | 62 +++++++++++++ genshin/client/routes.py | 9 ++ genshin/constants.py | 1 + genshin/models/__init__.py | 1 + genshin/models/miyoushe/__init__.py | 4 + genshin/models/miyoushe/cookie.py | 23 +++++ genshin/models/miyoushe/qrcode.py | 54 ++++++++++++ genshin/utility/ds.py | 13 ++- requirements.txt | 1 + setup.py | 5 +- 11 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 genshin/models/miyoushe/__init__.py create mode 100644 genshin/models/miyoushe/cookie.py create mode 100644 genshin/models/miyoushe/qrcode.py diff --git a/genshin/client/components/geetest/client.py b/genshin/client/components/geetest/client.py index 5398ab29..dbc9245c 100644 --- a/genshin/client/components/geetest/client.py +++ b/genshin/client/components/geetest/client.py @@ -1,14 +1,23 @@ """Geetest client component.""" +import asyncio import json +import logging +import random import typing +from string import ascii_letters, digits import aiohttp import aiohttp.web +import qrcode +import qrcode.image.pil +from qrcode.constants import ERROR_CORRECT_L from genshin import constants, errors from genshin.client import routes from genshin.client.components import base +from genshin.client.manager.cookie import fetch_cookie_token_by_game_token, fetch_stoken_by_game_token +from genshin.models.miyoushe.qrcode import QRCodeCheckResult, QRCodeCreationResult, QRCodeStatus from genshin.utility import ds as ds_utility from genshin.utility import geetest as geetest_utility @@ -16,6 +25,8 @@ __all__ = ["GeetestClient"] +LOGGER_ = logging.getLogger(__name__) + class GeetestClient(base.BaseClient): """Geetest client component.""" @@ -314,6 +325,53 @@ async def _login_with_mobile_otp(self, mobile: str, otp: str) -> typing.Dict[str return cookies + async def _create_qrcode(self) -> QRCodeCreationResult: + """Create a QR code for login.""" + device_id = "".join(random.choices(ascii_letters + digits, k=64)) + app_id = "8" + payload = { + "app_id": app_id, + "device": device_id, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + routes.CREATE_QRCODE_URL.get_url(), + json=payload, + ) as r: + data = await r.json() + + if not data["data"]: + errors.raise_for_retcode(data) + + url: str = data["data"]["url"] + return QRCodeCreationResult( + app_id=app_id, + ticket=url.split("ticket=")[1], + device_id=device_id, + url=url, + ) + + async def _check_qrcode(self, app_id: str, device_id: str, ticket: str) -> QRCodeCheckResult: + """Check the status of a QR code login.""" + payload = { + "app_id": app_id, + "device": device_id, + "ticket": ticket, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + routes.CHECK_QRCODE_URL.get_url(), + json=payload, + ) as r: + data = await r.json() + + if not data["data"]: + errors.raise_for_retcode(data) + + return QRCodeCheckResult(**data["data"]) + async def login_with_password( self, account: str, @@ -463,3 +521,43 @@ async def login_with_app_password( result = await self._app_login(account, password, ticket=result) return result + + async def login_with_qrcode(self) -> typing.Dict[str, str]: + """Login with QR code, only available for Miyoushe users. + + Returns cookies. + """ + creation_result = await self._create_qrcode() + qrcode_: qrcode.image.pil.PilImage = qrcode.make(creation_result.url, error_correction=ERROR_CORRECT_L) # type: ignore + qrcode_.show() + + scanned = False + while True: + check_result = await self._check_qrcode( + creation_result.app_id, creation_result.device_id, creation_result.ticket + ) + if check_result.status == QRCodeStatus.SCANNED and not scanned: + LOGGER_.info("QR code scanned") + scanned = True + elif check_result.status == QRCodeStatus.CONFIRMED: + LOGGER_.info("QR code login confirmed") + break + + await asyncio.sleep(2) + + raw_data = check_result.payload.raw + assert raw_data is not None + + cookie_token = await fetch_cookie_token_by_game_token( + game_token=raw_data.game_token, account_id=raw_data.account_id + ) + stoken = await fetch_stoken_by_game_token(game_token=raw_data.game_token, account_id=int(raw_data.account_id)) + + cookies = { + "stoken_v2": stoken.token, + "stuid": stoken.aid, + "mid": stoken.mid, + "cookie_token": cookie_token, + } + self.set_cookies(cookies) + return cookies diff --git a/genshin/client/manager/cookie.py b/genshin/client/manager/cookie.py index a2b59d0c..76779f4d 100644 --- a/genshin/client/manager/cookie.py +++ b/genshin/client/manager/cookie.py @@ -18,7 +18,10 @@ from __future__ import annotations +import random import typing +import uuid +from string import ascii_letters, digits import aiohttp import aiohttp.typedefs @@ -26,16 +29,33 @@ from genshin import constants, errors, types from genshin.client import routes from genshin.client.manager import managers +from genshin.models.miyoushe.cookie import StokenResult from genshin.utility import ds as ds_utility __all__ = [ "complete_cookies", + "fetch_cookie_token_by_game_token", "fetch_cookie_token_info", "fetch_cookie_with_cookie", "fetch_cookie_with_stoken_v2", + "fetch_stoken_by_game_token", "refresh_cookie_token", ] +STOKEN_BY_GAME_TOKEN_HEADERS = { + "x-rpc-app_version": "2.41.0", + "x-rpc-aigis": "", + "Content-Type": "application/json", + "Accept": "application/json", + "x-rpc-game_biz": "bbs_cn", + "x-rpc-sys_version": "11", + "x-rpc-device_name": "GenshinUid_login_device_lulu", + "x-rpc-device_model": "GenshinUid_login_device_lulu", + "x-rpc-app_id": "bll8iq97cem8", + "x-rpc-client_type": "2", + "User-Agent": "okhttp/4.8.0", +} + async def fetch_cookie_with_cookie( cookies: managers.CookieOrHeader, @@ -166,3 +186,45 @@ async def complete_cookies( cookies = await refresh_cookie_token(cookies, region=region) # type: ignore[assignment] return cookies + + +async def fetch_cookie_token_by_game_token(*, game_token: str, account_id: str) -> str: + """Fetch cookie token by game token, which is obtained by scanning a QR code.""" + url = routes.GET_COOKIE_TOKEN_BY_GAME_TOKEN_URL.get_url() + params = { + "game_token": game_token, + "account_id": account_id, + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, params=params) as r: + data = await r.json() + + if not data["data"]: + errors.raise_for_retcode(data) + + return data["data"]["cookie_token"] + + +async def fetch_stoken_by_game_token(*, game_token: str, account_id: int) -> StokenResult: + """Fetch cookie token by game token, which is obtained by scanning a QR code.""" + url = routes.GET_STOKEN_BY_GAME_TOKEN_URL.get_url() + payload = { + "account_id": account_id, + "game_token": game_token, + } + headers = { + "DS": ds_utility.generate_passport_ds(body=payload), + "x-rpc-device_id": uuid.uuid4().hex, + "x-rpc-device_fp": "".join(random.choices(ascii_letters + digits, k=13)), + **STOKEN_BY_GAME_TOKEN_HEADERS, + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, json=payload, headers=headers) as r: + data = await r.json() + + if not data["data"]: + errors.raise_for_retcode(data) + + return StokenResult(**data["data"]) diff --git a/genshin/client/routes.py b/genshin/client/routes.py index aa153ee3..a398deb8 100644 --- a/genshin/client/routes.py +++ b/genshin/client/routes.py @@ -13,11 +13,15 @@ "BBS_REFERER_URL", "BBS_URL", "CALCULATOR_URL", + "CHECK_QRCODE_URL", "CN_WEB_LOGIN_URL", "COMMUNITY_URL", "COOKIE_V2_REFRESH_URL", + "CREATE_QRCODE_URL", "DETAIL_LEDGER_URL", "GACHA_URL", + "GET_COOKIE_TOKEN_BY_GAME_TOKEN_URL", + "GET_STOKEN_BY_GAME_TOKEN_URL", "HK4E_URL", "INFO_LEDGER_URL", "LINEUP_URL", @@ -215,6 +219,8 @@ def get_url(self, region: types.Region, game: types.Game) -> yarl.URL: ) COOKIE_V2_REFRESH_URL = Route("https://sg-public-api.hoyoverse.com/account/ma-passport/token/getBySToken") +GET_COOKIE_TOKEN_BY_GAME_TOKEN_URL = Route("https://api-takumi.mihoyo.com/auth/api/getCookieAccountInfoByGameToken") +GET_STOKEN_BY_GAME_TOKEN_URL = Route("https://passport-api.mihoyo.com/account/ma-cn-session/app/getTokenByGameToken") 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") @@ -228,3 +234,6 @@ def get_url(self, region: types.Region, game: types.Game) -> yarl.URL: CHECK_MOBILE_VALIDITY_URL = Route("https://webapi.account.mihoyo.com/Api/is_mobile_registrable") MOBILE_OTP_URL = Route("https://passport-api.miyoushe.com/account/ma-cn-verifier/verifier/createLoginCaptcha") MOBILE_LOGIN_URL = Route("https://passport-api.miyoushe.com/account/ma-cn-passport/web/loginByMobileCaptcha") + +CREATE_QRCODE_URL = Route("https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/fetch") +CHECK_QRCODE_URL = Route("https://hk4e-sdk.mihoyo.com/hk4e_cn/combo/panda/qrcode/query") diff --git a/genshin/constants.py b/genshin/constants.py index 2d86a6ff..64e42e05 100644 --- a/genshin/constants.py +++ b/genshin/constants.py @@ -29,5 +29,6 @@ types.Region.CHINESE: "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs", "app_login": "IZPgfb0dRPtBeLuFkdDznSZ6f4wWt6y2", "cn_signin": "9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7", + "cn_passport": "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS", } """Dynamic Secret Salts.""" diff --git a/genshin/models/__init__.py b/genshin/models/__init__.py index 35edc5e7..38da6131 100644 --- a/genshin/models/__init__.py +++ b/genshin/models/__init__.py @@ -3,5 +3,6 @@ from .genshin import * from .honkai import * from .hoyolab import * +from .miyoushe import * from .model import * from .starrail import * diff --git a/genshin/models/miyoushe/__init__.py b/genshin/models/miyoushe/__init__.py new file mode 100644 index 00000000..f34841ca --- /dev/null +++ b/genshin/models/miyoushe/__init__.py @@ -0,0 +1,4 @@ +"""Miyoushe models.""" + +from .cookie import * +from .qrcode import * diff --git a/genshin/models/miyoushe/cookie.py b/genshin/models/miyoushe/cookie.py new file mode 100644 index 00000000..4cca01cf --- /dev/null +++ b/genshin/models/miyoushe/cookie.py @@ -0,0 +1,23 @@ +"""Miyoushe Cookie Models""" + +import typing + +from pydantic import BaseModel, model_validator + +__all__ = ("StokenResult",) + + +class StokenResult(BaseModel): + """Stoken result.""" + + aid: str + mid: str + token: str + + @model_validator(mode="before") + def _transform_result(cls, values: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + return { + "aid": values["user_info"]["aid"], + "mid": values["user_info"]["mid"], + "token": values["token"]["token"], + } diff --git a/genshin/models/miyoushe/qrcode.py b/genshin/models/miyoushe/qrcode.py new file mode 100644 index 00000000..5ce6d0a6 --- /dev/null +++ b/genshin/models/miyoushe/qrcode.py @@ -0,0 +1,54 @@ +"""Miyoushe QR Code Models""" + +import json +from enum import Enum + +from pydantic import BaseModel, Field, field_validator + +__all__ = ("QRCodeCheckResult", "QRCodeCreationResult", "QRCodePayload", "QRCodeRawData", "QRCodeStatus") + + +class QRCodeStatus(Enum): + """QR code check status.""" + + INIT = "Init" + SCANNED = "Scanned" + CONFIRMED = "Confirmed" + + +class QRCodeRawData(BaseModel): + """QR code raw data.""" + + account_id: str = Field(alias="uid") + """Miyoushe account id.""" + game_token: str = Field(alias="token") + + +class QRCodePayload(BaseModel): + """QR code check result payload.""" + + proto: str + raw: QRCodeRawData | None + ext: str + + @field_validator("raw", mode="before") + def _convert_raw_data(cls, value: str | None) -> QRCodeRawData | None: + if value: + return QRCodeRawData(**json.loads(value)) + return None + + +class QRCodeCheckResult(BaseModel): + """QR code check result.""" + + status: QRCodeStatus = Field(alias="stat") + payload: QRCodePayload + + +class QRCodeCreationResult(BaseModel): + """QR code creation result.""" + + app_id: str + ticket: str + device_id: str + url: str diff --git a/genshin/utility/ds.py b/genshin/utility/ds.py index 1cd169f5..adae34c3 100644 --- a/genshin/utility/ds.py +++ b/genshin/utility/ds.py @@ -9,7 +9,7 @@ from genshin import constants, types -__all__ = ["generate_cn_dynamic_secret", "generate_dynamic_secret", "get_ds_headers"] +__all__ = ["generate_cn_dynamic_secret", "generate_dynamic_secret", "generate_passport_ds", "get_ds_headers"] def generate_dynamic_secret(salt: str = constants.DS_SALT[types.Region.OVERSEAS]) -> str: @@ -59,3 +59,14 @@ def get_ds_headers( else: raise TypeError(f"{region!r} is not a valid region.") return ds_headers + + +def generate_passport_ds(body: typing.Mapping[str, typing.Any]) -> str: + """Create a dynamic secret for Miyoushe passport API.""" + salt = constants.DS_SALT["cn_passport"] + t = int(time.time()) + r = "".join(random.sample(string.ascii_letters, 6)) + b = json.dumps(body) + h = hashlib.md5(f"salt={salt}&t={t}&r={r}&b={b}&q=".encode()).hexdigest() + result = f"{t},{r},{h}" + return result diff --git a/requirements.txt b/requirements.txt index 95cbe366..fa1c601e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ browser-cookie3 rsa aioredis click +qrcode[pil] \ No newline at end of file diff --git a/setup.py b/setup.py index 96e87d66..6ac94305 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ """Run setuptools.""" + from setuptools import find_packages, setup setup( @@ -17,9 +18,9 @@ python_requires=">=3.8", install_requires=["aiohttp", "pydantic"], extras_require={ - "all": ["browser-cookie3", "rsa", "click"], + "all": ["browser-cookie3", "rsa", "click", "qrcode[pil]"], "cookies": ["browser-cookie3"], - "geetest": ["rsa"], + "geetest": ["rsa", "qrcode[pil]"], "cli": ["click"], }, include_package_data=True, From 45532009be345af2792d6c9c1876d4239ee88ec3 Mon Sep 17 00:00:00 2001 From: seria Date: Mon, 1 Apr 2024 07:49:32 +0900 Subject: [PATCH 17/20] Fix UID utilities to work with new Genshin UID standard (#171) * fix: Fix uid utilities * fix: Fix logic * refactor: Use string slicing approach * refactor: Use in-member check --- genshin/utility/uid.py | 72 ++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/genshin/utility/uid.py b/genshin/utility/uid.py index e0fabcae..64e4b668 100644 --- a/genshin/utility/uid.py +++ b/genshin/utility/uid.py @@ -15,22 +15,42 @@ "recognize_starrail_server", ] -UID_RANGE: typing.Mapping[types.Game, typing.Mapping[types.Region, typing.Sequence[int]]] = { +UID_RANGE: typing.Mapping[types.Game, typing.Mapping[types.Region, typing.Sequence[str]]] = { types.Game.GENSHIN: { - types.Region.OVERSEAS: (6, 7, 8, 9), - types.Region.CHINESE: (1, 2, 5), + types.Region.OVERSEAS: ("6", "7", "8", "18", "9"), + types.Region.CHINESE: ("1", "2", "3", "5"), }, types.Game.STARRAIL: { - types.Region.OVERSEAS: (6, 7, 8, 9), - types.Region.CHINESE: (1, 2, 5), + types.Region.OVERSEAS: ("6", "7", "8", "9"), + types.Region.CHINESE: ("1", "2", "5"), }, types.Game.HONKAI: { - types.Region.OVERSEAS: (1, 2), - types.Region.CHINESE: (3, 4), + types.Region.OVERSEAS: ("1", "2"), + types.Region.CHINESE: ("3", "4"), }, } """Mapping of games and regions to their respective UID ranges.""" +GENSHIN_SERVER_RANGE: typing.Mapping[str, typing.Sequence[str]] = { + "cn_gf01": ("1", "2", "3"), + "cn_qd01": ("5",), + "os_usa": ("6",), + "os_euro": ("7",), + "os_asia": ("8", "18"), + "os_cht": ("9",), +} +"""Mapping of Genshin servers to their respective UID ranges.""" + +STARRAIL_SERVER_RANGE: typing.Mapping[str, typing.Sequence[str]] = { + "prod_gf_cn": ("1", "2"), + "prod_qd_cn": ("5",), + "prod_official_usa": ("6",), + "prod_official_eur": ("7",), + "prod_official_asia": ("8",), + "prod_official_cht": ("9",), +} +"""Mapping of Star Rail servers to their respective UID ranges.""" + def create_short_lang_code(lang: str) -> str: """Create an alternative short lang code.""" @@ -39,18 +59,9 @@ def create_short_lang_code(lang: str) -> str: def recognize_genshin_server(uid: int) -> str: """Recognize which server a Genshin UID is from.""" - server = { - "1": "cn_gf01", - "2": "cn_gf01", - "5": "cn_qd01", - "6": "os_usa", - "7": "os_euro", - "8": "os_asia", - "9": "os_cht", - }.get(str(uid)[0]) - - if server: - return server + for server_name, digits in GENSHIN_SERVER_RANGE.items(): + if str(uid)[:-8] in digits: + return server_name raise ValueError(f"UID {uid} isn't associated with any server") @@ -91,18 +102,9 @@ def recognize_honkai_server(uid: int) -> str: def recognize_starrail_server(uid: int) -> str: """Recognize which server a Star Rail UID is from.""" - server = { - "1": "prod_gf_cn", - "2": "prod_gf_cn", - "5": "prod_qd_cn", - "6": "prod_official_usa", - "7": "prod_official_eur", - "8": "prod_official_asia", - "9": "prod_official_cht", - }.get(str(uid)[0]) - - if server: - return server + for server, digits in STARRAIL_SERVER_RANGE.items(): + if str(uid)[:-8] in digits: + return server raise ValueError(f"UID {uid} isn't associated with any server") @@ -124,10 +126,8 @@ def recognize_game(uid: int, region: types.Region) -> typing.Optional[types.Game if len(str(uid)) == 8: return types.Game.HONKAI - first = int(str(uid)[0]) - for game, digits in UID_RANGE.items(): - if first in digits[region]: + if str(uid)[:-8] in digits[region]: return game return None @@ -135,10 +135,8 @@ def recognize_game(uid: int, region: types.Region) -> typing.Optional[types.Game def recognize_region(uid: int, game: types.Game) -> typing.Optional[types.Region]: """Recognize the region of a uid.""" - first = int(str(uid)[0]) - for region, digits in UID_RANGE[game].items(): - if first in digits: + if str(uid)[:-8] in digits: return region return None From 6b30d30e211b655bee1fcf5aaa342ed4ba0f9564 Mon Sep 17 00:00:00 2001 From: seria Date: Mon, 1 Apr 2024 08:36:08 +0900 Subject: [PATCH 18/20] Fix get_banner_details (#174) * fix: Fix routes * fix: Fix routes and add temp fix --- genshin/client/components/gacha.py | 13 ++++++++++--- genshin/client/routes.py | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/genshin/client/components/gacha.py b/genshin/client/components/gacha.py index fe6cad62..19fc90b0 100644 --- a/genshin/client/components/gacha.py +++ b/genshin/client/components/gacha.py @@ -222,7 +222,7 @@ async def _get_banner_details( server = "prod_official_asia" if game == types.Game.STARRAIL else "os_asia" data = await self.request_webstatic( - f"/{region}/gacha_info/{server}/{banner_id}/{lang}.json", + f"/gacha_info/{region}/{server}/{banner_id}/{lang}.json", cache=client_cache.cache_key("banner", endpoint="details", banner=banner_id, lang=lang), ) return models.BannerDetails(**data, banner_id=banner_id) @@ -240,12 +240,19 @@ async def get_genshin_banner_ids(self) -> typing.Sequence[str]: Uses the current cn banners. """ + + def process_gacha(data: typing.Mapping[str, typing.Any]) -> str: + # Temporary fix for 4.5 chronicled wish + if data["gacha_type"] == 500: + return "8b10b48c52dd6870f92d72e9963b44bb8968ed2f" + return data["gacha_id"] + data = await self.request_webstatic( - "hk4e/gacha_info/cn_gf01/gacha/list.json", + "gacha_info/hk4e/cn_gf01/gacha/list.json", region=types.Region.CHINESE, cache=client_cache.cache_key("banner", endpoint="ids"), ) - return [i["gacha_id"] for i in data["data"]["list"]] + return list(map(process_gacha, data["data"]["list"])) async def get_banner_details( self, diff --git a/genshin/client/routes.py b/genshin/client/routes.py index a398deb8..a24d048c 100644 --- a/genshin/client/routes.py +++ b/genshin/client/routes.py @@ -102,8 +102,8 @@ def get_url(self, region: types.Region, game: types.Game) -> yarl.URL: WEBSTATIC_URL = InternationalRoute( - "https://webstatic-sea.hoyoverse.com/", - "https://webstatic.mihoyo.com/", + "https://operation-webstatic.hoyoverse.com/", + "https://operation-webstatic.mihoyo.com/", ) WEBAPI_URL = InternationalRoute( From b83eeb6f62392429d6a9aa10215d8ad5211c5d83 Mon Sep 17 00:00:00 2001 From: seria Date: Wed, 3 Apr 2024 09:45:25 +0900 Subject: [PATCH 19/20] Fix update_charactes_enka function (#176) --- genshin/utility/extdb.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/genshin/utility/extdb.py b/genshin/utility/extdb.py index 8225d504..b3058cd3 100644 --- a/genshin/utility/extdb.py +++ b/genshin/utility/extdb.py @@ -173,8 +173,10 @@ async def update_characters_enka(langs: typing.Sequence[str] = ()) -> None: continue # traveler element for short_lang, loc in locs.items(): + if (lang := ENKA_LANG_MAP.get(short_lang)) is None: + continue update_character_name( - lang=ENKA_LANG_MAP[short_lang], + lang=lang, id=int(strid), icon_name=char["SideIconName"][len("UI_AvatarIcon_Side_") :], # noqa: E203 name=loc[str(char["NameTextMapHash"])], From 524b51a0ce372820831f39bb4f78b053149aadeb Mon Sep 17 00:00:00 2001 From: ashlen Date: Wed, 10 Apr 2024 17:49:12 +0000 Subject: [PATCH 20/20] Bump pypi version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6ac94305..c8cb0974 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="genshin", - version="1.6.2", + version="1.6.3", author="thesadru", author_email="thesadru@gmail.com", description="An API wrapper for Genshin Impact.",