Module genshin.client.components.hoyolab

Hoyolab component.

Classes

class HoyolabClient (cookies: ForwardRef('http.cookies.BaseCookie[Any]') | Mapping[Any, Any] | str | Sequence[ForwardRef('http.cookies.BaseCookie[Any]') | Mapping[Any, Any] | str] | None = None,
*,
authkey: str | None = None,
lang: str = 'en-us',
region: Region = Region.OVERSEAS,
proxy: str | None = None,
game: Game | None = None,
uid: int | None = None,
hoyolab_id: int | None = None,
device_id: str | None = None,
device_fp: str | None = None,
headers: Mapping[str, str] | Mapping[multidict._multidict.istr, str] | multidict._multidict.CIMultiDict | multidict._multidict.CIMultiDictProxy | Iterable[Tuple[str | multidict._multidict.istr, str]] | None = None,
cache: BaseCache | None = None,
debug: bool = False)
Expand source code
class HoyolabClient(base.BaseClient):
    """Hoyolab component."""

    async def _get_server_region(self, uid: int, game: types.Game) -> str:
        """Fetch the server region of an account from the API."""
        data = await self.request(
            routes.GET_USER_REGION_URL.get_url(),
            params=dict(game_biz=utility.get_prod_game_biz(self.region, game)),
            cache=client_cache.cache_key("server_region", game=game, uid=uid, region=self.region),
        )
        for account in data["list"]:
            if account["game_uid"] == str(uid):
                return account["region"]

        raise ValueError(f"Failed to recognize server for game {game!r} and uid {uid!r}")

    async def _request_announcements(
        self,
        game: types.Game,
        uid: int,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of game announcements."""
        if game is types.Game.GENSHIN:
            params = dict(
                game="hk4e",
                game_biz="hk4e_global",
                bundle_id="hk4e_global",
                platform="pc",
                region=utility.recognize_genshin_server(uid),
                uid=uid,
                level=8,
                lang=lang or self.lang,
            )
            url = routes.HK4E_URL.get_url()
        elif game is types.Game.ZZZ:
            params = dict(
                game="nap",
                game_biz="nap_global",
                bundle_id="nap_global",
                platform="pc",
                region=utility.recognize_zzz_server(uid),
                level=60,
                lang=lang or self.lang,
                uid=uid,
            )
            url = routes.NAP_URL.get_url()
        elif game is types.Game.STARRAIL:
            params = dict(
                game="hkrpg",
                game_biz="hkrpg_global",
                bundle_id="hkrpg_global",
                platform="pc",
                region=utility.recognize_starrail_server(uid),
                uid=uid,
                level=70,
                lang=lang or self.lang,
                channel_id=1,
            )
            url = routes.HKRPG_URL.get_url()
        else:
            msg = f"{game!r} is not supported yet."
            raise ValueError(msg)

        info, details = await asyncio.gather(
            self.request_hoyolab(
                url / "announcement/api/getAnnList",
                lang=lang,
                params=params,
            ),
            self.request_hoyolab(
                url / "announcement/api/getAnnContent",
                lang=lang,
                params=params,
            ),
        )

        announcements: list[typing.Mapping[str, typing.Any]] = []
        extra_list: list[typing.Mapping[str, typing.Any]] = (
            info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else []
        )
        for sublist in info["list"] + extra_list:
            for info in sublist["list"]:
                detail = next((i for i in details["list"] if i["ann_id"] == info["ann_id"]), None)
                announcements.append({**info, **(detail or {})})

        return [models.Announcement(**i) for i in announcements]

    async def _request_mimo(
        self,
        endpoint: str,
        *,
        method: typing.Optional[str] = None,
        params: typing.Optional[typing.Mapping[str, typing.Any]] = None,
        data: typing.Any = None,
    ) -> typing.Any:
        game_id = params.get("game_id") if params else data.get("game_id")
        if game_id is None and self.game is None:
            raise ValueError("Cannot determine game for this traveling mimo request.")

        if game_id == 2 or self.game is types.Game.GENSHIN:
            url = routes.MIMO_URL.get_url() / "nata" / endpoint.replace("-", "_")
        else:
            url = routes.MIMO_URL.get_url() / endpoint
        return await self.request(url, method=method, params=params, data=data)

    async def search_users(
        self,
        keyword: str,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.PartialHoyolabUser]:
        """Search hoyolab users."""
        data = await self.request_bbs(
            "community/search/wapi/search/user",
            lang=lang,
            params=dict(keyword=keyword, page_size=20),
            cache=client_cache.cache_key("search", keyword=keyword, lang=self.lang),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_hoyolab_user(
        self,
        hoyolab_id: typing.Optional[int] = None,
        *,
        lang: typing.Optional[str] = None,
    ) -> models.FullHoyolabUser:
        """Get a hoyolab user."""
        if self.region == types.Region.OVERSEAS:
            url = "/community/painter/wapi/user/full"
        elif self.region == types.Region.CHINESE:
            url = "/user/wapi/getUserFullInfo"
        else:
            raise TypeError(f"{self.region!r} is not a valid region.")

        data = await self.request_bbs(
            url=url,
            lang=lang,
            params=dict(uid=hoyolab_id) if hoyolab_id else None,
            cache=client_cache.cache_key("hoyolab", uid=hoyolab_id, lang=lang or self.lang),
        )
        return models.FullHoyolabUser(**data["user_info"])

    async def get_recommended_users(self, *, limit: int = 200) -> typing.Sequence[models.PartialHoyolabUser]:
        """Get a list of recommended active users."""
        data = await self.request_bbs(
            "community/user/wapi/recommendActive",
            params=dict(page_size=limit),
            cache=client_cache.cache_key("recommended"),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_genshin_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Genshin Impact announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.GENSHIN)
        else:
            uid = uid or 900000005
        return await self._request_announcements(types.Game.GENSHIN, uid, lang=lang)

    async def get_zzz_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Zenless Zone Zero announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.ZZZ)
        else:
            uid = uid or 1300000000
        return await self._request_announcements(types.Game.ZZZ, uid, lang=lang)

    async def get_starrail_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Star Rail announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.STARRAIL)
        else:
            uid = uid or 809162009
        return await self._request_announcements(types.Game.STARRAIL, uid, lang=lang)

    @managers.requires_cookie_token
    async def redeem_code(
        self,
        code: str,
        uid: typing.Optional[int] = None,
        *,
        game: typing.Optional[types.Game] = None,
        lang: typing.Optional[str] = None,
        region: typing.Optional[str] = None,
    ) -> None:
        """Redeems a gift code for the current user."""
        if game is None:
            if self.default_game is None:
                raise RuntimeError("No default game set.")

            game = self.default_game

        if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, types.Game.TOT}:
            raise ValueError(f"{game} does not support code redemption.")

        uid = uid or await self._get_uid(game)

        try:
            region = region or utility.recognize_server(uid, game)
        except Exception:
            warnings.warn(f"Failed to recognize server for game {game!r} and uid {uid!r}, fetching from API now.")
            region = await self._get_server_region(uid, game)

        await self.request(
            routes.CODE_URL.get_url(self.region, game),
            params=dict(
                uid=uid,
                region=region,
                cdkey=code,
                game_biz=utility.get_prod_game_biz(self.region, game),
                lang=utility.create_short_lang_code(lang or self.lang),
            ),
            method="POST" if game is types.Game.STARRAIL else "GET",
        )

    @managers.no_multi
    async def check_in_community(self) -> None:
        """Check in to the hoyolab community and claim your daily 5 community exp."""
        raise RuntimeError("This API is deprecated.")

    @base.region_specific(types.Region.OVERSEAS)
    async def fetch_mi18n(
        self, url: typing.Union[str, yarl.URL], filename: str, *, lang: typing.Optional[str] = None
    ) -> typing.Mapping[str, str]:
        """Fetch a mi18n file."""
        return await self.request(
            yarl.URL(url) / f"{filename}/{filename}-{lang or self.lang}.json",
            cache=client_cache.cache_key("mi18n", filename=filename, url=url, lang=lang or self.lang),
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_games(self, *, lang: typing.Optional[str] = None) -> typing.Sequence[models.MimoGame]:
        """Get a list of Traveling Mimo games."""
        data = await self._request_mimo("index", params=dict(lang=lang or self.lang))
        if self.game is None:
            raise RuntimeError("No default game set.")

        if self.game is types.Game.GENSHIN:
            return [models.MimoGame(**i["act_info"]) for i in data["act_list"]]
        return [models.MimoGame(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def _get_mimo_game_data(
        self, game: typing.Union[typing.Literal["hoyolab"], types.Game]
    ) -> typing.Tuple[int, int]:
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.id, mimo_game.version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def _parse_mimo_args(
        self,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> typing.Tuple[int, int]:
        if game_id is None or version_id is None:
            if game is None:
                if self.default_game is None:
                    raise RuntimeError("No default game set.")
                game = self.default_game

            if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, "hoyolab"}:
                raise ValueError(f"{game!r} does not support Traveling Mimo.")
            game_id, version_id = await self._get_mimo_game_data(game)

        return game_id, version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_tasks(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoTask]:
        """Get a list of Traveling Mimo missions (tasks)."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "task-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoTask(**i) for i in data["task_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def claim_mimo_task_reward(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Claim a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "receive-point",
            params=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST" if game_id == 2 else "GET",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def finish_mimo_task(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Finish a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "finish-task",
            data=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_shop_items(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoShopItem]:
        """Get a list of Traveling Mimo shop items."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoShopItem(**i) for i in data["exchange_award_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def buy_mimo_shop_item(
        self,
        item_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> str:
        """Buy an item from the Traveling Mimo shop and return a gift code to redeem it."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange",
            data=dict(award_id=item_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return data["exchange_code"]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_point_count(
        self,
        *,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> int:
        """Get the current Traveling Mimo point count."""
        game = game or self.default_game
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.point

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_lottery_info(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryInfo:
        """Get Traveling Mimo lottery info."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery-info",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return models.MimoLotteryInfo(**data)

    @base.region_specific(types.Region.OVERSEAS)
    async def draw_mimo_lottery(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryResult:
        """Draw a Traveling Mimo lottery."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery",
            data=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return models.MimoLotteryResult(**data)

    async def reply_to_post(self, content: str, *, post_id: int) -> int:
        """Reply to a community post."""
        data = await self.request_bbs(
            "community/post/wapi/releaseReply",
            data=dict(
                post_id=str(post_id),
                content=f"<p>{content}</p>",
                image_list=[],
                reply_bubble_id="",
                structured_content=json.dumps([{"insert": f"{content}\n"}]),
            ),
            method="POST",
            headers={"x-rpc-device_id": str(uuid.uuid4())},
        )
        return int(data["reply_id"])

    async def delete_reply(self, *, reply_id: int, post_id: int) -> None:
        """Delete a reply."""
        await self.request_bbs(
            "community/post/wapi/deleteReply",
            data=dict(reply_id=reply_id, post_id=post_id),
            method="POST",
        )

    async def get_replies(self, *, size: int = 15) -> typing.Sequence[models.Reply]:
        """Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content."""
        data = await self.request_bbs(
            "community/post/wapi/userReply",
            params=dict(size=size),
        )
        return [models.Reply(**i["reply"]) for i in data["list"]]

    async def _request_join(self, topic_id: int, *, is_cancel: bool) -> None:
        await self.request_bbs(
            "community/topic/wapi/join",
            data=dict(topic_id=topic_id, is_cancel=is_cancel),
            method="POST",
        )

    async def join_topic(self, topic_id: int) -> None:
        """Join a topic."""
        await self._request_join(topic_id, is_cancel=False)

    async def leave_topic(self, topic_id: int) -> None:
        """Leave a topic."""
        await self._request_join(topic_id, is_cancel=True)

    @base.region_specific(types.Region.OVERSEAS)
    async def get_web_events(
        self,
        game: typing.Optional[types.Game] = None,
        *,
        size: int = 15,
        offset: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> list[models.WebEvent]:
        """Get a list of web events."""
        game = game or self.default_game
        if game is None:
            raise ValueError("No default game set.")

        data = await self.request_bbs(
            "community/community_contribution/wapi/event/list",
            params=dict(gids=WEB_EVENT_GAME_IDS[game], size=size, offset=offset or ""),
            lang=lang,
        )
        return [models.WebEvent(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_accompany_characters(
        self, *, lang: typing.Optional[str] = None
    ) -> typing.Sequence[models.AccompanyCharacterGame]:
        """Get a list of accompany characters, this endpoint doesn't require cookies."""
        data = await self.request_bbs(
            "community/painter/api/getChannelRoleList",
            cache=client_cache.cache_key("accp_chars"),
            method="POST",
            lang=lang,
        )
        return [models.AccompanyCharacterGame(**i) for i in data["game_roles_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def accompany_character(self, *, role_id: int, topic_id: int) -> models.AccompanyResult:
        """Accompany a character, role_id and topic_id can be found by calling get_accompany_characters."""
        data = await self.request_bbs(
            "community/apihub/api/user/accompany/role", params=dict(role_id=role_id, topic_id=topic_id)
        )
        return models.AccompanyResult(**data)

Hoyolab component.

Ancestors

Subclasses

Class variables

var logger : logging.Logger

Instance variables

var authkeys : dict[Game, str]
Expand source code
class HoyolabClient(base.BaseClient):
    """Hoyolab component."""

    async def _get_server_region(self, uid: int, game: types.Game) -> str:
        """Fetch the server region of an account from the API."""
        data = await self.request(
            routes.GET_USER_REGION_URL.get_url(),
            params=dict(game_biz=utility.get_prod_game_biz(self.region, game)),
            cache=client_cache.cache_key("server_region", game=game, uid=uid, region=self.region),
        )
        for account in data["list"]:
            if account["game_uid"] == str(uid):
                return account["region"]

        raise ValueError(f"Failed to recognize server for game {game!r} and uid {uid!r}")

    async def _request_announcements(
        self,
        game: types.Game,
        uid: int,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of game announcements."""
        if game is types.Game.GENSHIN:
            params = dict(
                game="hk4e",
                game_biz="hk4e_global",
                bundle_id="hk4e_global",
                platform="pc",
                region=utility.recognize_genshin_server(uid),
                uid=uid,
                level=8,
                lang=lang or self.lang,
            )
            url = routes.HK4E_URL.get_url()
        elif game is types.Game.ZZZ:
            params = dict(
                game="nap",
                game_biz="nap_global",
                bundle_id="nap_global",
                platform="pc",
                region=utility.recognize_zzz_server(uid),
                level=60,
                lang=lang or self.lang,
                uid=uid,
            )
            url = routes.NAP_URL.get_url()
        elif game is types.Game.STARRAIL:
            params = dict(
                game="hkrpg",
                game_biz="hkrpg_global",
                bundle_id="hkrpg_global",
                platform="pc",
                region=utility.recognize_starrail_server(uid),
                uid=uid,
                level=70,
                lang=lang or self.lang,
                channel_id=1,
            )
            url = routes.HKRPG_URL.get_url()
        else:
            msg = f"{game!r} is not supported yet."
            raise ValueError(msg)

        info, details = await asyncio.gather(
            self.request_hoyolab(
                url / "announcement/api/getAnnList",
                lang=lang,
                params=params,
            ),
            self.request_hoyolab(
                url / "announcement/api/getAnnContent",
                lang=lang,
                params=params,
            ),
        )

        announcements: list[typing.Mapping[str, typing.Any]] = []
        extra_list: list[typing.Mapping[str, typing.Any]] = (
            info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else []
        )
        for sublist in info["list"] + extra_list:
            for info in sublist["list"]:
                detail = next((i for i in details["list"] if i["ann_id"] == info["ann_id"]), None)
                announcements.append({**info, **(detail or {})})

        return [models.Announcement(**i) for i in announcements]

    async def _request_mimo(
        self,
        endpoint: str,
        *,
        method: typing.Optional[str] = None,
        params: typing.Optional[typing.Mapping[str, typing.Any]] = None,
        data: typing.Any = None,
    ) -> typing.Any:
        game_id = params.get("game_id") if params else data.get("game_id")
        if game_id is None and self.game is None:
            raise ValueError("Cannot determine game for this traveling mimo request.")

        if game_id == 2 or self.game is types.Game.GENSHIN:
            url = routes.MIMO_URL.get_url() / "nata" / endpoint.replace("-", "_")
        else:
            url = routes.MIMO_URL.get_url() / endpoint
        return await self.request(url, method=method, params=params, data=data)

    async def search_users(
        self,
        keyword: str,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.PartialHoyolabUser]:
        """Search hoyolab users."""
        data = await self.request_bbs(
            "community/search/wapi/search/user",
            lang=lang,
            params=dict(keyword=keyword, page_size=20),
            cache=client_cache.cache_key("search", keyword=keyword, lang=self.lang),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_hoyolab_user(
        self,
        hoyolab_id: typing.Optional[int] = None,
        *,
        lang: typing.Optional[str] = None,
    ) -> models.FullHoyolabUser:
        """Get a hoyolab user."""
        if self.region == types.Region.OVERSEAS:
            url = "/community/painter/wapi/user/full"
        elif self.region == types.Region.CHINESE:
            url = "/user/wapi/getUserFullInfo"
        else:
            raise TypeError(f"{self.region!r} is not a valid region.")

        data = await self.request_bbs(
            url=url,
            lang=lang,
            params=dict(uid=hoyolab_id) if hoyolab_id else None,
            cache=client_cache.cache_key("hoyolab", uid=hoyolab_id, lang=lang or self.lang),
        )
        return models.FullHoyolabUser(**data["user_info"])

    async def get_recommended_users(self, *, limit: int = 200) -> typing.Sequence[models.PartialHoyolabUser]:
        """Get a list of recommended active users."""
        data = await self.request_bbs(
            "community/user/wapi/recommendActive",
            params=dict(page_size=limit),
            cache=client_cache.cache_key("recommended"),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_genshin_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Genshin Impact announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.GENSHIN)
        else:
            uid = uid or 900000005
        return await self._request_announcements(types.Game.GENSHIN, uid, lang=lang)

    async def get_zzz_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Zenless Zone Zero announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.ZZZ)
        else:
            uid = uid or 1300000000
        return await self._request_announcements(types.Game.ZZZ, uid, lang=lang)

    async def get_starrail_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Star Rail announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.STARRAIL)
        else:
            uid = uid or 809162009
        return await self._request_announcements(types.Game.STARRAIL, uid, lang=lang)

    @managers.requires_cookie_token
    async def redeem_code(
        self,
        code: str,
        uid: typing.Optional[int] = None,
        *,
        game: typing.Optional[types.Game] = None,
        lang: typing.Optional[str] = None,
        region: typing.Optional[str] = None,
    ) -> None:
        """Redeems a gift code for the current user."""
        if game is None:
            if self.default_game is None:
                raise RuntimeError("No default game set.")

            game = self.default_game

        if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, types.Game.TOT}:
            raise ValueError(f"{game} does not support code redemption.")

        uid = uid or await self._get_uid(game)

        try:
            region = region or utility.recognize_server(uid, game)
        except Exception:
            warnings.warn(f"Failed to recognize server for game {game!r} and uid {uid!r}, fetching from API now.")
            region = await self._get_server_region(uid, game)

        await self.request(
            routes.CODE_URL.get_url(self.region, game),
            params=dict(
                uid=uid,
                region=region,
                cdkey=code,
                game_biz=utility.get_prod_game_biz(self.region, game),
                lang=utility.create_short_lang_code(lang or self.lang),
            ),
            method="POST" if game is types.Game.STARRAIL else "GET",
        )

    @managers.no_multi
    async def check_in_community(self) -> None:
        """Check in to the hoyolab community and claim your daily 5 community exp."""
        raise RuntimeError("This API is deprecated.")

    @base.region_specific(types.Region.OVERSEAS)
    async def fetch_mi18n(
        self, url: typing.Union[str, yarl.URL], filename: str, *, lang: typing.Optional[str] = None
    ) -> typing.Mapping[str, str]:
        """Fetch a mi18n file."""
        return await self.request(
            yarl.URL(url) / f"{filename}/{filename}-{lang or self.lang}.json",
            cache=client_cache.cache_key("mi18n", filename=filename, url=url, lang=lang or self.lang),
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_games(self, *, lang: typing.Optional[str] = None) -> typing.Sequence[models.MimoGame]:
        """Get a list of Traveling Mimo games."""
        data = await self._request_mimo("index", params=dict(lang=lang or self.lang))
        if self.game is None:
            raise RuntimeError("No default game set.")

        if self.game is types.Game.GENSHIN:
            return [models.MimoGame(**i["act_info"]) for i in data["act_list"]]
        return [models.MimoGame(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def _get_mimo_game_data(
        self, game: typing.Union[typing.Literal["hoyolab"], types.Game]
    ) -> typing.Tuple[int, int]:
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.id, mimo_game.version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def _parse_mimo_args(
        self,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> typing.Tuple[int, int]:
        if game_id is None or version_id is None:
            if game is None:
                if self.default_game is None:
                    raise RuntimeError("No default game set.")
                game = self.default_game

            if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, "hoyolab"}:
                raise ValueError(f"{game!r} does not support Traveling Mimo.")
            game_id, version_id = await self._get_mimo_game_data(game)

        return game_id, version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_tasks(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoTask]:
        """Get a list of Traveling Mimo missions (tasks)."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "task-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoTask(**i) for i in data["task_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def claim_mimo_task_reward(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Claim a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "receive-point",
            params=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST" if game_id == 2 else "GET",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def finish_mimo_task(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Finish a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "finish-task",
            data=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_shop_items(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoShopItem]:
        """Get a list of Traveling Mimo shop items."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoShopItem(**i) for i in data["exchange_award_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def buy_mimo_shop_item(
        self,
        item_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> str:
        """Buy an item from the Traveling Mimo shop and return a gift code to redeem it."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange",
            data=dict(award_id=item_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return data["exchange_code"]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_point_count(
        self,
        *,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> int:
        """Get the current Traveling Mimo point count."""
        game = game or self.default_game
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.point

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_lottery_info(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryInfo:
        """Get Traveling Mimo lottery info."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery-info",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return models.MimoLotteryInfo(**data)

    @base.region_specific(types.Region.OVERSEAS)
    async def draw_mimo_lottery(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryResult:
        """Draw a Traveling Mimo lottery."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery",
            data=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return models.MimoLotteryResult(**data)

    async def reply_to_post(self, content: str, *, post_id: int) -> int:
        """Reply to a community post."""
        data = await self.request_bbs(
            "community/post/wapi/releaseReply",
            data=dict(
                post_id=str(post_id),
                content=f"<p>{content}</p>",
                image_list=[],
                reply_bubble_id="",
                structured_content=json.dumps([{"insert": f"{content}\n"}]),
            ),
            method="POST",
            headers={"x-rpc-device_id": str(uuid.uuid4())},
        )
        return int(data["reply_id"])

    async def delete_reply(self, *, reply_id: int, post_id: int) -> None:
        """Delete a reply."""
        await self.request_bbs(
            "community/post/wapi/deleteReply",
            data=dict(reply_id=reply_id, post_id=post_id),
            method="POST",
        )

    async def get_replies(self, *, size: int = 15) -> typing.Sequence[models.Reply]:
        """Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content."""
        data = await self.request_bbs(
            "community/post/wapi/userReply",
            params=dict(size=size),
        )
        return [models.Reply(**i["reply"]) for i in data["list"]]

    async def _request_join(self, topic_id: int, *, is_cancel: bool) -> None:
        await self.request_bbs(
            "community/topic/wapi/join",
            data=dict(topic_id=topic_id, is_cancel=is_cancel),
            method="POST",
        )

    async def join_topic(self, topic_id: int) -> None:
        """Join a topic."""
        await self._request_join(topic_id, is_cancel=False)

    async def leave_topic(self, topic_id: int) -> None:
        """Leave a topic."""
        await self._request_join(topic_id, is_cancel=True)

    @base.region_specific(types.Region.OVERSEAS)
    async def get_web_events(
        self,
        game: typing.Optional[types.Game] = None,
        *,
        size: int = 15,
        offset: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> list[models.WebEvent]:
        """Get a list of web events."""
        game = game or self.default_game
        if game is None:
            raise ValueError("No default game set.")

        data = await self.request_bbs(
            "community/community_contribution/wapi/event/list",
            params=dict(gids=WEB_EVENT_GAME_IDS[game], size=size, offset=offset or ""),
            lang=lang,
        )
        return [models.WebEvent(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_accompany_characters(
        self, *, lang: typing.Optional[str] = None
    ) -> typing.Sequence[models.AccompanyCharacterGame]:
        """Get a list of accompany characters, this endpoint doesn't require cookies."""
        data = await self.request_bbs(
            "community/painter/api/getChannelRoleList",
            cache=client_cache.cache_key("accp_chars"),
            method="POST",
            lang=lang,
        )
        return [models.AccompanyCharacterGame(**i) for i in data["game_roles_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def accompany_character(self, *, role_id: int, topic_id: int) -> models.AccompanyResult:
        """Accompany a character, role_id and topic_id can be found by calling get_accompany_characters."""
        data = await self.request_bbs(
            "community/apihub/api/user/accompany/role", params=dict(role_id=role_id, topic_id=topic_id)
        )
        return models.AccompanyResult(**data)
var cacheBaseCache
Expand source code
class HoyolabClient(base.BaseClient):
    """Hoyolab component."""

    async def _get_server_region(self, uid: int, game: types.Game) -> str:
        """Fetch the server region of an account from the API."""
        data = await self.request(
            routes.GET_USER_REGION_URL.get_url(),
            params=dict(game_biz=utility.get_prod_game_biz(self.region, game)),
            cache=client_cache.cache_key("server_region", game=game, uid=uid, region=self.region),
        )
        for account in data["list"]:
            if account["game_uid"] == str(uid):
                return account["region"]

        raise ValueError(f"Failed to recognize server for game {game!r} and uid {uid!r}")

    async def _request_announcements(
        self,
        game: types.Game,
        uid: int,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of game announcements."""
        if game is types.Game.GENSHIN:
            params = dict(
                game="hk4e",
                game_biz="hk4e_global",
                bundle_id="hk4e_global",
                platform="pc",
                region=utility.recognize_genshin_server(uid),
                uid=uid,
                level=8,
                lang=lang or self.lang,
            )
            url = routes.HK4E_URL.get_url()
        elif game is types.Game.ZZZ:
            params = dict(
                game="nap",
                game_biz="nap_global",
                bundle_id="nap_global",
                platform="pc",
                region=utility.recognize_zzz_server(uid),
                level=60,
                lang=lang or self.lang,
                uid=uid,
            )
            url = routes.NAP_URL.get_url()
        elif game is types.Game.STARRAIL:
            params = dict(
                game="hkrpg",
                game_biz="hkrpg_global",
                bundle_id="hkrpg_global",
                platform="pc",
                region=utility.recognize_starrail_server(uid),
                uid=uid,
                level=70,
                lang=lang or self.lang,
                channel_id=1,
            )
            url = routes.HKRPG_URL.get_url()
        else:
            msg = f"{game!r} is not supported yet."
            raise ValueError(msg)

        info, details = await asyncio.gather(
            self.request_hoyolab(
                url / "announcement/api/getAnnList",
                lang=lang,
                params=params,
            ),
            self.request_hoyolab(
                url / "announcement/api/getAnnContent",
                lang=lang,
                params=params,
            ),
        )

        announcements: list[typing.Mapping[str, typing.Any]] = []
        extra_list: list[typing.Mapping[str, typing.Any]] = (
            info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else []
        )
        for sublist in info["list"] + extra_list:
            for info in sublist["list"]:
                detail = next((i for i in details["list"] if i["ann_id"] == info["ann_id"]), None)
                announcements.append({**info, **(detail or {})})

        return [models.Announcement(**i) for i in announcements]

    async def _request_mimo(
        self,
        endpoint: str,
        *,
        method: typing.Optional[str] = None,
        params: typing.Optional[typing.Mapping[str, typing.Any]] = None,
        data: typing.Any = None,
    ) -> typing.Any:
        game_id = params.get("game_id") if params else data.get("game_id")
        if game_id is None and self.game is None:
            raise ValueError("Cannot determine game for this traveling mimo request.")

        if game_id == 2 or self.game is types.Game.GENSHIN:
            url = routes.MIMO_URL.get_url() / "nata" / endpoint.replace("-", "_")
        else:
            url = routes.MIMO_URL.get_url() / endpoint
        return await self.request(url, method=method, params=params, data=data)

    async def search_users(
        self,
        keyword: str,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.PartialHoyolabUser]:
        """Search hoyolab users."""
        data = await self.request_bbs(
            "community/search/wapi/search/user",
            lang=lang,
            params=dict(keyword=keyword, page_size=20),
            cache=client_cache.cache_key("search", keyword=keyword, lang=self.lang),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_hoyolab_user(
        self,
        hoyolab_id: typing.Optional[int] = None,
        *,
        lang: typing.Optional[str] = None,
    ) -> models.FullHoyolabUser:
        """Get a hoyolab user."""
        if self.region == types.Region.OVERSEAS:
            url = "/community/painter/wapi/user/full"
        elif self.region == types.Region.CHINESE:
            url = "/user/wapi/getUserFullInfo"
        else:
            raise TypeError(f"{self.region!r} is not a valid region.")

        data = await self.request_bbs(
            url=url,
            lang=lang,
            params=dict(uid=hoyolab_id) if hoyolab_id else None,
            cache=client_cache.cache_key("hoyolab", uid=hoyolab_id, lang=lang or self.lang),
        )
        return models.FullHoyolabUser(**data["user_info"])

    async def get_recommended_users(self, *, limit: int = 200) -> typing.Sequence[models.PartialHoyolabUser]:
        """Get a list of recommended active users."""
        data = await self.request_bbs(
            "community/user/wapi/recommendActive",
            params=dict(page_size=limit),
            cache=client_cache.cache_key("recommended"),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_genshin_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Genshin Impact announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.GENSHIN)
        else:
            uid = uid or 900000005
        return await self._request_announcements(types.Game.GENSHIN, uid, lang=lang)

    async def get_zzz_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Zenless Zone Zero announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.ZZZ)
        else:
            uid = uid or 1300000000
        return await self._request_announcements(types.Game.ZZZ, uid, lang=lang)

    async def get_starrail_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Star Rail announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.STARRAIL)
        else:
            uid = uid or 809162009
        return await self._request_announcements(types.Game.STARRAIL, uid, lang=lang)

    @managers.requires_cookie_token
    async def redeem_code(
        self,
        code: str,
        uid: typing.Optional[int] = None,
        *,
        game: typing.Optional[types.Game] = None,
        lang: typing.Optional[str] = None,
        region: typing.Optional[str] = None,
    ) -> None:
        """Redeems a gift code for the current user."""
        if game is None:
            if self.default_game is None:
                raise RuntimeError("No default game set.")

            game = self.default_game

        if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, types.Game.TOT}:
            raise ValueError(f"{game} does not support code redemption.")

        uid = uid or await self._get_uid(game)

        try:
            region = region or utility.recognize_server(uid, game)
        except Exception:
            warnings.warn(f"Failed to recognize server for game {game!r} and uid {uid!r}, fetching from API now.")
            region = await self._get_server_region(uid, game)

        await self.request(
            routes.CODE_URL.get_url(self.region, game),
            params=dict(
                uid=uid,
                region=region,
                cdkey=code,
                game_biz=utility.get_prod_game_biz(self.region, game),
                lang=utility.create_short_lang_code(lang or self.lang),
            ),
            method="POST" if game is types.Game.STARRAIL else "GET",
        )

    @managers.no_multi
    async def check_in_community(self) -> None:
        """Check in to the hoyolab community and claim your daily 5 community exp."""
        raise RuntimeError("This API is deprecated.")

    @base.region_specific(types.Region.OVERSEAS)
    async def fetch_mi18n(
        self, url: typing.Union[str, yarl.URL], filename: str, *, lang: typing.Optional[str] = None
    ) -> typing.Mapping[str, str]:
        """Fetch a mi18n file."""
        return await self.request(
            yarl.URL(url) / f"{filename}/{filename}-{lang or self.lang}.json",
            cache=client_cache.cache_key("mi18n", filename=filename, url=url, lang=lang or self.lang),
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_games(self, *, lang: typing.Optional[str] = None) -> typing.Sequence[models.MimoGame]:
        """Get a list of Traveling Mimo games."""
        data = await self._request_mimo("index", params=dict(lang=lang or self.lang))
        if self.game is None:
            raise RuntimeError("No default game set.")

        if self.game is types.Game.GENSHIN:
            return [models.MimoGame(**i["act_info"]) for i in data["act_list"]]
        return [models.MimoGame(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def _get_mimo_game_data(
        self, game: typing.Union[typing.Literal["hoyolab"], types.Game]
    ) -> typing.Tuple[int, int]:
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.id, mimo_game.version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def _parse_mimo_args(
        self,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> typing.Tuple[int, int]:
        if game_id is None or version_id is None:
            if game is None:
                if self.default_game is None:
                    raise RuntimeError("No default game set.")
                game = self.default_game

            if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, "hoyolab"}:
                raise ValueError(f"{game!r} does not support Traveling Mimo.")
            game_id, version_id = await self._get_mimo_game_data(game)

        return game_id, version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_tasks(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoTask]:
        """Get a list of Traveling Mimo missions (tasks)."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "task-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoTask(**i) for i in data["task_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def claim_mimo_task_reward(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Claim a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "receive-point",
            params=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST" if game_id == 2 else "GET",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def finish_mimo_task(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Finish a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "finish-task",
            data=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_shop_items(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoShopItem]:
        """Get a list of Traveling Mimo shop items."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoShopItem(**i) for i in data["exchange_award_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def buy_mimo_shop_item(
        self,
        item_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> str:
        """Buy an item from the Traveling Mimo shop and return a gift code to redeem it."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange",
            data=dict(award_id=item_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return data["exchange_code"]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_point_count(
        self,
        *,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> int:
        """Get the current Traveling Mimo point count."""
        game = game or self.default_game
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.point

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_lottery_info(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryInfo:
        """Get Traveling Mimo lottery info."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery-info",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return models.MimoLotteryInfo(**data)

    @base.region_specific(types.Region.OVERSEAS)
    async def draw_mimo_lottery(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryResult:
        """Draw a Traveling Mimo lottery."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery",
            data=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return models.MimoLotteryResult(**data)

    async def reply_to_post(self, content: str, *, post_id: int) -> int:
        """Reply to a community post."""
        data = await self.request_bbs(
            "community/post/wapi/releaseReply",
            data=dict(
                post_id=str(post_id),
                content=f"<p>{content}</p>",
                image_list=[],
                reply_bubble_id="",
                structured_content=json.dumps([{"insert": f"{content}\n"}]),
            ),
            method="POST",
            headers={"x-rpc-device_id": str(uuid.uuid4())},
        )
        return int(data["reply_id"])

    async def delete_reply(self, *, reply_id: int, post_id: int) -> None:
        """Delete a reply."""
        await self.request_bbs(
            "community/post/wapi/deleteReply",
            data=dict(reply_id=reply_id, post_id=post_id),
            method="POST",
        )

    async def get_replies(self, *, size: int = 15) -> typing.Sequence[models.Reply]:
        """Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content."""
        data = await self.request_bbs(
            "community/post/wapi/userReply",
            params=dict(size=size),
        )
        return [models.Reply(**i["reply"]) for i in data["list"]]

    async def _request_join(self, topic_id: int, *, is_cancel: bool) -> None:
        await self.request_bbs(
            "community/topic/wapi/join",
            data=dict(topic_id=topic_id, is_cancel=is_cancel),
            method="POST",
        )

    async def join_topic(self, topic_id: int) -> None:
        """Join a topic."""
        await self._request_join(topic_id, is_cancel=False)

    async def leave_topic(self, topic_id: int) -> None:
        """Leave a topic."""
        await self._request_join(topic_id, is_cancel=True)

    @base.region_specific(types.Region.OVERSEAS)
    async def get_web_events(
        self,
        game: typing.Optional[types.Game] = None,
        *,
        size: int = 15,
        offset: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> list[models.WebEvent]:
        """Get a list of web events."""
        game = game or self.default_game
        if game is None:
            raise ValueError("No default game set.")

        data = await self.request_bbs(
            "community/community_contribution/wapi/event/list",
            params=dict(gids=WEB_EVENT_GAME_IDS[game], size=size, offset=offset or ""),
            lang=lang,
        )
        return [models.WebEvent(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_accompany_characters(
        self, *, lang: typing.Optional[str] = None
    ) -> typing.Sequence[models.AccompanyCharacterGame]:
        """Get a list of accompany characters, this endpoint doesn't require cookies."""
        data = await self.request_bbs(
            "community/painter/api/getChannelRoleList",
            cache=client_cache.cache_key("accp_chars"),
            method="POST",
            lang=lang,
        )
        return [models.AccompanyCharacterGame(**i) for i in data["game_roles_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def accompany_character(self, *, role_id: int, topic_id: int) -> models.AccompanyResult:
        """Accompany a character, role_id and topic_id can be found by calling get_accompany_characters."""
        data = await self.request_bbs(
            "community/apihub/api/user/accompany/role", params=dict(role_id=role_id, topic_id=topic_id)
        )
        return models.AccompanyResult(**data)
var cookie_managerBaseCookieManager
Expand source code
class HoyolabClient(base.BaseClient):
    """Hoyolab component."""

    async def _get_server_region(self, uid: int, game: types.Game) -> str:
        """Fetch the server region of an account from the API."""
        data = await self.request(
            routes.GET_USER_REGION_URL.get_url(),
            params=dict(game_biz=utility.get_prod_game_biz(self.region, game)),
            cache=client_cache.cache_key("server_region", game=game, uid=uid, region=self.region),
        )
        for account in data["list"]:
            if account["game_uid"] == str(uid):
                return account["region"]

        raise ValueError(f"Failed to recognize server for game {game!r} and uid {uid!r}")

    async def _request_announcements(
        self,
        game: types.Game,
        uid: int,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of game announcements."""
        if game is types.Game.GENSHIN:
            params = dict(
                game="hk4e",
                game_biz="hk4e_global",
                bundle_id="hk4e_global",
                platform="pc",
                region=utility.recognize_genshin_server(uid),
                uid=uid,
                level=8,
                lang=lang or self.lang,
            )
            url = routes.HK4E_URL.get_url()
        elif game is types.Game.ZZZ:
            params = dict(
                game="nap",
                game_biz="nap_global",
                bundle_id="nap_global",
                platform="pc",
                region=utility.recognize_zzz_server(uid),
                level=60,
                lang=lang or self.lang,
                uid=uid,
            )
            url = routes.NAP_URL.get_url()
        elif game is types.Game.STARRAIL:
            params = dict(
                game="hkrpg",
                game_biz="hkrpg_global",
                bundle_id="hkrpg_global",
                platform="pc",
                region=utility.recognize_starrail_server(uid),
                uid=uid,
                level=70,
                lang=lang or self.lang,
                channel_id=1,
            )
            url = routes.HKRPG_URL.get_url()
        else:
            msg = f"{game!r} is not supported yet."
            raise ValueError(msg)

        info, details = await asyncio.gather(
            self.request_hoyolab(
                url / "announcement/api/getAnnList",
                lang=lang,
                params=params,
            ),
            self.request_hoyolab(
                url / "announcement/api/getAnnContent",
                lang=lang,
                params=params,
            ),
        )

        announcements: list[typing.Mapping[str, typing.Any]] = []
        extra_list: list[typing.Mapping[str, typing.Any]] = (
            info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else []
        )
        for sublist in info["list"] + extra_list:
            for info in sublist["list"]:
                detail = next((i for i in details["list"] if i["ann_id"] == info["ann_id"]), None)
                announcements.append({**info, **(detail or {})})

        return [models.Announcement(**i) for i in announcements]

    async def _request_mimo(
        self,
        endpoint: str,
        *,
        method: typing.Optional[str] = None,
        params: typing.Optional[typing.Mapping[str, typing.Any]] = None,
        data: typing.Any = None,
    ) -> typing.Any:
        game_id = params.get("game_id") if params else data.get("game_id")
        if game_id is None and self.game is None:
            raise ValueError("Cannot determine game for this traveling mimo request.")

        if game_id == 2 or self.game is types.Game.GENSHIN:
            url = routes.MIMO_URL.get_url() / "nata" / endpoint.replace("-", "_")
        else:
            url = routes.MIMO_URL.get_url() / endpoint
        return await self.request(url, method=method, params=params, data=data)

    async def search_users(
        self,
        keyword: str,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.PartialHoyolabUser]:
        """Search hoyolab users."""
        data = await self.request_bbs(
            "community/search/wapi/search/user",
            lang=lang,
            params=dict(keyword=keyword, page_size=20),
            cache=client_cache.cache_key("search", keyword=keyword, lang=self.lang),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_hoyolab_user(
        self,
        hoyolab_id: typing.Optional[int] = None,
        *,
        lang: typing.Optional[str] = None,
    ) -> models.FullHoyolabUser:
        """Get a hoyolab user."""
        if self.region == types.Region.OVERSEAS:
            url = "/community/painter/wapi/user/full"
        elif self.region == types.Region.CHINESE:
            url = "/user/wapi/getUserFullInfo"
        else:
            raise TypeError(f"{self.region!r} is not a valid region.")

        data = await self.request_bbs(
            url=url,
            lang=lang,
            params=dict(uid=hoyolab_id) if hoyolab_id else None,
            cache=client_cache.cache_key("hoyolab", uid=hoyolab_id, lang=lang or self.lang),
        )
        return models.FullHoyolabUser(**data["user_info"])

    async def get_recommended_users(self, *, limit: int = 200) -> typing.Sequence[models.PartialHoyolabUser]:
        """Get a list of recommended active users."""
        data = await self.request_bbs(
            "community/user/wapi/recommendActive",
            params=dict(page_size=limit),
            cache=client_cache.cache_key("recommended"),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_genshin_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Genshin Impact announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.GENSHIN)
        else:
            uid = uid or 900000005
        return await self._request_announcements(types.Game.GENSHIN, uid, lang=lang)

    async def get_zzz_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Zenless Zone Zero announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.ZZZ)
        else:
            uid = uid or 1300000000
        return await self._request_announcements(types.Game.ZZZ, uid, lang=lang)

    async def get_starrail_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Star Rail announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.STARRAIL)
        else:
            uid = uid or 809162009
        return await self._request_announcements(types.Game.STARRAIL, uid, lang=lang)

    @managers.requires_cookie_token
    async def redeem_code(
        self,
        code: str,
        uid: typing.Optional[int] = None,
        *,
        game: typing.Optional[types.Game] = None,
        lang: typing.Optional[str] = None,
        region: typing.Optional[str] = None,
    ) -> None:
        """Redeems a gift code for the current user."""
        if game is None:
            if self.default_game is None:
                raise RuntimeError("No default game set.")

            game = self.default_game

        if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, types.Game.TOT}:
            raise ValueError(f"{game} does not support code redemption.")

        uid = uid or await self._get_uid(game)

        try:
            region = region or utility.recognize_server(uid, game)
        except Exception:
            warnings.warn(f"Failed to recognize server for game {game!r} and uid {uid!r}, fetching from API now.")
            region = await self._get_server_region(uid, game)

        await self.request(
            routes.CODE_URL.get_url(self.region, game),
            params=dict(
                uid=uid,
                region=region,
                cdkey=code,
                game_biz=utility.get_prod_game_biz(self.region, game),
                lang=utility.create_short_lang_code(lang or self.lang),
            ),
            method="POST" if game is types.Game.STARRAIL else "GET",
        )

    @managers.no_multi
    async def check_in_community(self) -> None:
        """Check in to the hoyolab community and claim your daily 5 community exp."""
        raise RuntimeError("This API is deprecated.")

    @base.region_specific(types.Region.OVERSEAS)
    async def fetch_mi18n(
        self, url: typing.Union[str, yarl.URL], filename: str, *, lang: typing.Optional[str] = None
    ) -> typing.Mapping[str, str]:
        """Fetch a mi18n file."""
        return await self.request(
            yarl.URL(url) / f"{filename}/{filename}-{lang or self.lang}.json",
            cache=client_cache.cache_key("mi18n", filename=filename, url=url, lang=lang or self.lang),
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_games(self, *, lang: typing.Optional[str] = None) -> typing.Sequence[models.MimoGame]:
        """Get a list of Traveling Mimo games."""
        data = await self._request_mimo("index", params=dict(lang=lang or self.lang))
        if self.game is None:
            raise RuntimeError("No default game set.")

        if self.game is types.Game.GENSHIN:
            return [models.MimoGame(**i["act_info"]) for i in data["act_list"]]
        return [models.MimoGame(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def _get_mimo_game_data(
        self, game: typing.Union[typing.Literal["hoyolab"], types.Game]
    ) -> typing.Tuple[int, int]:
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.id, mimo_game.version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def _parse_mimo_args(
        self,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> typing.Tuple[int, int]:
        if game_id is None or version_id is None:
            if game is None:
                if self.default_game is None:
                    raise RuntimeError("No default game set.")
                game = self.default_game

            if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, "hoyolab"}:
                raise ValueError(f"{game!r} does not support Traveling Mimo.")
            game_id, version_id = await self._get_mimo_game_data(game)

        return game_id, version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_tasks(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoTask]:
        """Get a list of Traveling Mimo missions (tasks)."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "task-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoTask(**i) for i in data["task_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def claim_mimo_task_reward(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Claim a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "receive-point",
            params=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST" if game_id == 2 else "GET",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def finish_mimo_task(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Finish a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "finish-task",
            data=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_shop_items(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoShopItem]:
        """Get a list of Traveling Mimo shop items."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoShopItem(**i) for i in data["exchange_award_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def buy_mimo_shop_item(
        self,
        item_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> str:
        """Buy an item from the Traveling Mimo shop and return a gift code to redeem it."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange",
            data=dict(award_id=item_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return data["exchange_code"]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_point_count(
        self,
        *,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> int:
        """Get the current Traveling Mimo point count."""
        game = game or self.default_game
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.point

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_lottery_info(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryInfo:
        """Get Traveling Mimo lottery info."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery-info",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return models.MimoLotteryInfo(**data)

    @base.region_specific(types.Region.OVERSEAS)
    async def draw_mimo_lottery(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryResult:
        """Draw a Traveling Mimo lottery."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery",
            data=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return models.MimoLotteryResult(**data)

    async def reply_to_post(self, content: str, *, post_id: int) -> int:
        """Reply to a community post."""
        data = await self.request_bbs(
            "community/post/wapi/releaseReply",
            data=dict(
                post_id=str(post_id),
                content=f"<p>{content}</p>",
                image_list=[],
                reply_bubble_id="",
                structured_content=json.dumps([{"insert": f"{content}\n"}]),
            ),
            method="POST",
            headers={"x-rpc-device_id": str(uuid.uuid4())},
        )
        return int(data["reply_id"])

    async def delete_reply(self, *, reply_id: int, post_id: int) -> None:
        """Delete a reply."""
        await self.request_bbs(
            "community/post/wapi/deleteReply",
            data=dict(reply_id=reply_id, post_id=post_id),
            method="POST",
        )

    async def get_replies(self, *, size: int = 15) -> typing.Sequence[models.Reply]:
        """Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content."""
        data = await self.request_bbs(
            "community/post/wapi/userReply",
            params=dict(size=size),
        )
        return [models.Reply(**i["reply"]) for i in data["list"]]

    async def _request_join(self, topic_id: int, *, is_cancel: bool) -> None:
        await self.request_bbs(
            "community/topic/wapi/join",
            data=dict(topic_id=topic_id, is_cancel=is_cancel),
            method="POST",
        )

    async def join_topic(self, topic_id: int) -> None:
        """Join a topic."""
        await self._request_join(topic_id, is_cancel=False)

    async def leave_topic(self, topic_id: int) -> None:
        """Leave a topic."""
        await self._request_join(topic_id, is_cancel=True)

    @base.region_specific(types.Region.OVERSEAS)
    async def get_web_events(
        self,
        game: typing.Optional[types.Game] = None,
        *,
        size: int = 15,
        offset: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> list[models.WebEvent]:
        """Get a list of web events."""
        game = game or self.default_game
        if game is None:
            raise ValueError("No default game set.")

        data = await self.request_bbs(
            "community/community_contribution/wapi/event/list",
            params=dict(gids=WEB_EVENT_GAME_IDS[game], size=size, offset=offset or ""),
            lang=lang,
        )
        return [models.WebEvent(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_accompany_characters(
        self, *, lang: typing.Optional[str] = None
    ) -> typing.Sequence[models.AccompanyCharacterGame]:
        """Get a list of accompany characters, this endpoint doesn't require cookies."""
        data = await self.request_bbs(
            "community/painter/api/getChannelRoleList",
            cache=client_cache.cache_key("accp_chars"),
            method="POST",
            lang=lang,
        )
        return [models.AccompanyCharacterGame(**i) for i in data["game_roles_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def accompany_character(self, *, role_id: int, topic_id: int) -> models.AccompanyResult:
        """Accompany a character, role_id and topic_id can be found by calling get_accompany_characters."""
        data = await self.request_bbs(
            "community/apihub/api/user/accompany/role", params=dict(role_id=role_id, topic_id=topic_id)
        )
        return models.AccompanyResult(**data)
var custom_headers : multidict._multidict.CIMultiDict[str]
Expand source code
class HoyolabClient(base.BaseClient):
    """Hoyolab component."""

    async def _get_server_region(self, uid: int, game: types.Game) -> str:
        """Fetch the server region of an account from the API."""
        data = await self.request(
            routes.GET_USER_REGION_URL.get_url(),
            params=dict(game_biz=utility.get_prod_game_biz(self.region, game)),
            cache=client_cache.cache_key("server_region", game=game, uid=uid, region=self.region),
        )
        for account in data["list"]:
            if account["game_uid"] == str(uid):
                return account["region"]

        raise ValueError(f"Failed to recognize server for game {game!r} and uid {uid!r}")

    async def _request_announcements(
        self,
        game: types.Game,
        uid: int,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of game announcements."""
        if game is types.Game.GENSHIN:
            params = dict(
                game="hk4e",
                game_biz="hk4e_global",
                bundle_id="hk4e_global",
                platform="pc",
                region=utility.recognize_genshin_server(uid),
                uid=uid,
                level=8,
                lang=lang or self.lang,
            )
            url = routes.HK4E_URL.get_url()
        elif game is types.Game.ZZZ:
            params = dict(
                game="nap",
                game_biz="nap_global",
                bundle_id="nap_global",
                platform="pc",
                region=utility.recognize_zzz_server(uid),
                level=60,
                lang=lang or self.lang,
                uid=uid,
            )
            url = routes.NAP_URL.get_url()
        elif game is types.Game.STARRAIL:
            params = dict(
                game="hkrpg",
                game_biz="hkrpg_global",
                bundle_id="hkrpg_global",
                platform="pc",
                region=utility.recognize_starrail_server(uid),
                uid=uid,
                level=70,
                lang=lang or self.lang,
                channel_id=1,
            )
            url = routes.HKRPG_URL.get_url()
        else:
            msg = f"{game!r} is not supported yet."
            raise ValueError(msg)

        info, details = await asyncio.gather(
            self.request_hoyolab(
                url / "announcement/api/getAnnList",
                lang=lang,
                params=params,
            ),
            self.request_hoyolab(
                url / "announcement/api/getAnnContent",
                lang=lang,
                params=params,
            ),
        )

        announcements: list[typing.Mapping[str, typing.Any]] = []
        extra_list: list[typing.Mapping[str, typing.Any]] = (
            info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else []
        )
        for sublist in info["list"] + extra_list:
            for info in sublist["list"]:
                detail = next((i for i in details["list"] if i["ann_id"] == info["ann_id"]), None)
                announcements.append({**info, **(detail or {})})

        return [models.Announcement(**i) for i in announcements]

    async def _request_mimo(
        self,
        endpoint: str,
        *,
        method: typing.Optional[str] = None,
        params: typing.Optional[typing.Mapping[str, typing.Any]] = None,
        data: typing.Any = None,
    ) -> typing.Any:
        game_id = params.get("game_id") if params else data.get("game_id")
        if game_id is None and self.game is None:
            raise ValueError("Cannot determine game for this traveling mimo request.")

        if game_id == 2 or self.game is types.Game.GENSHIN:
            url = routes.MIMO_URL.get_url() / "nata" / endpoint.replace("-", "_")
        else:
            url = routes.MIMO_URL.get_url() / endpoint
        return await self.request(url, method=method, params=params, data=data)

    async def search_users(
        self,
        keyword: str,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.PartialHoyolabUser]:
        """Search hoyolab users."""
        data = await self.request_bbs(
            "community/search/wapi/search/user",
            lang=lang,
            params=dict(keyword=keyword, page_size=20),
            cache=client_cache.cache_key("search", keyword=keyword, lang=self.lang),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_hoyolab_user(
        self,
        hoyolab_id: typing.Optional[int] = None,
        *,
        lang: typing.Optional[str] = None,
    ) -> models.FullHoyolabUser:
        """Get a hoyolab user."""
        if self.region == types.Region.OVERSEAS:
            url = "/community/painter/wapi/user/full"
        elif self.region == types.Region.CHINESE:
            url = "/user/wapi/getUserFullInfo"
        else:
            raise TypeError(f"{self.region!r} is not a valid region.")

        data = await self.request_bbs(
            url=url,
            lang=lang,
            params=dict(uid=hoyolab_id) if hoyolab_id else None,
            cache=client_cache.cache_key("hoyolab", uid=hoyolab_id, lang=lang or self.lang),
        )
        return models.FullHoyolabUser(**data["user_info"])

    async def get_recommended_users(self, *, limit: int = 200) -> typing.Sequence[models.PartialHoyolabUser]:
        """Get a list of recommended active users."""
        data = await self.request_bbs(
            "community/user/wapi/recommendActive",
            params=dict(page_size=limit),
            cache=client_cache.cache_key("recommended"),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_genshin_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Genshin Impact announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.GENSHIN)
        else:
            uid = uid or 900000005
        return await self._request_announcements(types.Game.GENSHIN, uid, lang=lang)

    async def get_zzz_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Zenless Zone Zero announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.ZZZ)
        else:
            uid = uid or 1300000000
        return await self._request_announcements(types.Game.ZZZ, uid, lang=lang)

    async def get_starrail_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Star Rail announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.STARRAIL)
        else:
            uid = uid or 809162009
        return await self._request_announcements(types.Game.STARRAIL, uid, lang=lang)

    @managers.requires_cookie_token
    async def redeem_code(
        self,
        code: str,
        uid: typing.Optional[int] = None,
        *,
        game: typing.Optional[types.Game] = None,
        lang: typing.Optional[str] = None,
        region: typing.Optional[str] = None,
    ) -> None:
        """Redeems a gift code for the current user."""
        if game is None:
            if self.default_game is None:
                raise RuntimeError("No default game set.")

            game = self.default_game

        if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, types.Game.TOT}:
            raise ValueError(f"{game} does not support code redemption.")

        uid = uid or await self._get_uid(game)

        try:
            region = region or utility.recognize_server(uid, game)
        except Exception:
            warnings.warn(f"Failed to recognize server for game {game!r} and uid {uid!r}, fetching from API now.")
            region = await self._get_server_region(uid, game)

        await self.request(
            routes.CODE_URL.get_url(self.region, game),
            params=dict(
                uid=uid,
                region=region,
                cdkey=code,
                game_biz=utility.get_prod_game_biz(self.region, game),
                lang=utility.create_short_lang_code(lang or self.lang),
            ),
            method="POST" if game is types.Game.STARRAIL else "GET",
        )

    @managers.no_multi
    async def check_in_community(self) -> None:
        """Check in to the hoyolab community and claim your daily 5 community exp."""
        raise RuntimeError("This API is deprecated.")

    @base.region_specific(types.Region.OVERSEAS)
    async def fetch_mi18n(
        self, url: typing.Union[str, yarl.URL], filename: str, *, lang: typing.Optional[str] = None
    ) -> typing.Mapping[str, str]:
        """Fetch a mi18n file."""
        return await self.request(
            yarl.URL(url) / f"{filename}/{filename}-{lang or self.lang}.json",
            cache=client_cache.cache_key("mi18n", filename=filename, url=url, lang=lang or self.lang),
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_games(self, *, lang: typing.Optional[str] = None) -> typing.Sequence[models.MimoGame]:
        """Get a list of Traveling Mimo games."""
        data = await self._request_mimo("index", params=dict(lang=lang or self.lang))
        if self.game is None:
            raise RuntimeError("No default game set.")

        if self.game is types.Game.GENSHIN:
            return [models.MimoGame(**i["act_info"]) for i in data["act_list"]]
        return [models.MimoGame(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def _get_mimo_game_data(
        self, game: typing.Union[typing.Literal["hoyolab"], types.Game]
    ) -> typing.Tuple[int, int]:
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.id, mimo_game.version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def _parse_mimo_args(
        self,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> typing.Tuple[int, int]:
        if game_id is None or version_id is None:
            if game is None:
                if self.default_game is None:
                    raise RuntimeError("No default game set.")
                game = self.default_game

            if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, "hoyolab"}:
                raise ValueError(f"{game!r} does not support Traveling Mimo.")
            game_id, version_id = await self._get_mimo_game_data(game)

        return game_id, version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_tasks(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoTask]:
        """Get a list of Traveling Mimo missions (tasks)."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "task-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoTask(**i) for i in data["task_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def claim_mimo_task_reward(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Claim a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "receive-point",
            params=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST" if game_id == 2 else "GET",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def finish_mimo_task(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Finish a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "finish-task",
            data=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_shop_items(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoShopItem]:
        """Get a list of Traveling Mimo shop items."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoShopItem(**i) for i in data["exchange_award_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def buy_mimo_shop_item(
        self,
        item_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> str:
        """Buy an item from the Traveling Mimo shop and return a gift code to redeem it."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange",
            data=dict(award_id=item_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return data["exchange_code"]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_point_count(
        self,
        *,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> int:
        """Get the current Traveling Mimo point count."""
        game = game or self.default_game
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.point

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_lottery_info(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryInfo:
        """Get Traveling Mimo lottery info."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery-info",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return models.MimoLotteryInfo(**data)

    @base.region_specific(types.Region.OVERSEAS)
    async def draw_mimo_lottery(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryResult:
        """Draw a Traveling Mimo lottery."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery",
            data=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return models.MimoLotteryResult(**data)

    async def reply_to_post(self, content: str, *, post_id: int) -> int:
        """Reply to a community post."""
        data = await self.request_bbs(
            "community/post/wapi/releaseReply",
            data=dict(
                post_id=str(post_id),
                content=f"<p>{content}</p>",
                image_list=[],
                reply_bubble_id="",
                structured_content=json.dumps([{"insert": f"{content}\n"}]),
            ),
            method="POST",
            headers={"x-rpc-device_id": str(uuid.uuid4())},
        )
        return int(data["reply_id"])

    async def delete_reply(self, *, reply_id: int, post_id: int) -> None:
        """Delete a reply."""
        await self.request_bbs(
            "community/post/wapi/deleteReply",
            data=dict(reply_id=reply_id, post_id=post_id),
            method="POST",
        )

    async def get_replies(self, *, size: int = 15) -> typing.Sequence[models.Reply]:
        """Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content."""
        data = await self.request_bbs(
            "community/post/wapi/userReply",
            params=dict(size=size),
        )
        return [models.Reply(**i["reply"]) for i in data["list"]]

    async def _request_join(self, topic_id: int, *, is_cancel: bool) -> None:
        await self.request_bbs(
            "community/topic/wapi/join",
            data=dict(topic_id=topic_id, is_cancel=is_cancel),
            method="POST",
        )

    async def join_topic(self, topic_id: int) -> None:
        """Join a topic."""
        await self._request_join(topic_id, is_cancel=False)

    async def leave_topic(self, topic_id: int) -> None:
        """Leave a topic."""
        await self._request_join(topic_id, is_cancel=True)

    @base.region_specific(types.Region.OVERSEAS)
    async def get_web_events(
        self,
        game: typing.Optional[types.Game] = None,
        *,
        size: int = 15,
        offset: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> list[models.WebEvent]:
        """Get a list of web events."""
        game = game or self.default_game
        if game is None:
            raise ValueError("No default game set.")

        data = await self.request_bbs(
            "community/community_contribution/wapi/event/list",
            params=dict(gids=WEB_EVENT_GAME_IDS[game], size=size, offset=offset or ""),
            lang=lang,
        )
        return [models.WebEvent(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_accompany_characters(
        self, *, lang: typing.Optional[str] = None
    ) -> typing.Sequence[models.AccompanyCharacterGame]:
        """Get a list of accompany characters, this endpoint doesn't require cookies."""
        data = await self.request_bbs(
            "community/painter/api/getChannelRoleList",
            cache=client_cache.cache_key("accp_chars"),
            method="POST",
            lang=lang,
        )
        return [models.AccompanyCharacterGame(**i) for i in data["game_roles_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def accompany_character(self, *, role_id: int, topic_id: int) -> models.AccompanyResult:
        """Accompany a character, role_id and topic_id can be found by calling get_accompany_characters."""
        data = await self.request_bbs(
            "community/apihub/api/user/accompany/role", params=dict(role_id=role_id, topic_id=topic_id)
        )
        return models.AccompanyResult(**data)
var uids : dict[Game, int]
Expand source code
class HoyolabClient(base.BaseClient):
    """Hoyolab component."""

    async def _get_server_region(self, uid: int, game: types.Game) -> str:
        """Fetch the server region of an account from the API."""
        data = await self.request(
            routes.GET_USER_REGION_URL.get_url(),
            params=dict(game_biz=utility.get_prod_game_biz(self.region, game)),
            cache=client_cache.cache_key("server_region", game=game, uid=uid, region=self.region),
        )
        for account in data["list"]:
            if account["game_uid"] == str(uid):
                return account["region"]

        raise ValueError(f"Failed to recognize server for game {game!r} and uid {uid!r}")

    async def _request_announcements(
        self,
        game: types.Game,
        uid: int,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of game announcements."""
        if game is types.Game.GENSHIN:
            params = dict(
                game="hk4e",
                game_biz="hk4e_global",
                bundle_id="hk4e_global",
                platform="pc",
                region=utility.recognize_genshin_server(uid),
                uid=uid,
                level=8,
                lang=lang or self.lang,
            )
            url = routes.HK4E_URL.get_url()
        elif game is types.Game.ZZZ:
            params = dict(
                game="nap",
                game_biz="nap_global",
                bundle_id="nap_global",
                platform="pc",
                region=utility.recognize_zzz_server(uid),
                level=60,
                lang=lang or self.lang,
                uid=uid,
            )
            url = routes.NAP_URL.get_url()
        elif game is types.Game.STARRAIL:
            params = dict(
                game="hkrpg",
                game_biz="hkrpg_global",
                bundle_id="hkrpg_global",
                platform="pc",
                region=utility.recognize_starrail_server(uid),
                uid=uid,
                level=70,
                lang=lang or self.lang,
                channel_id=1,
            )
            url = routes.HKRPG_URL.get_url()
        else:
            msg = f"{game!r} is not supported yet."
            raise ValueError(msg)

        info, details = await asyncio.gather(
            self.request_hoyolab(
                url / "announcement/api/getAnnList",
                lang=lang,
                params=params,
            ),
            self.request_hoyolab(
                url / "announcement/api/getAnnContent",
                lang=lang,
                params=params,
            ),
        )

        announcements: list[typing.Mapping[str, typing.Any]] = []
        extra_list: list[typing.Mapping[str, typing.Any]] = (
            info["pic_list"][0]["type_list"] if "pic_list" in info and info["pic_list"] else []
        )
        for sublist in info["list"] + extra_list:
            for info in sublist["list"]:
                detail = next((i for i in details["list"] if i["ann_id"] == info["ann_id"]), None)
                announcements.append({**info, **(detail or {})})

        return [models.Announcement(**i) for i in announcements]

    async def _request_mimo(
        self,
        endpoint: str,
        *,
        method: typing.Optional[str] = None,
        params: typing.Optional[typing.Mapping[str, typing.Any]] = None,
        data: typing.Any = None,
    ) -> typing.Any:
        game_id = params.get("game_id") if params else data.get("game_id")
        if game_id is None and self.game is None:
            raise ValueError("Cannot determine game for this traveling mimo request.")

        if game_id == 2 or self.game is types.Game.GENSHIN:
            url = routes.MIMO_URL.get_url() / "nata" / endpoint.replace("-", "_")
        else:
            url = routes.MIMO_URL.get_url() / endpoint
        return await self.request(url, method=method, params=params, data=data)

    async def search_users(
        self,
        keyword: str,
        *,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.PartialHoyolabUser]:
        """Search hoyolab users."""
        data = await self.request_bbs(
            "community/search/wapi/search/user",
            lang=lang,
            params=dict(keyword=keyword, page_size=20),
            cache=client_cache.cache_key("search", keyword=keyword, lang=self.lang),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_hoyolab_user(
        self,
        hoyolab_id: typing.Optional[int] = None,
        *,
        lang: typing.Optional[str] = None,
    ) -> models.FullHoyolabUser:
        """Get a hoyolab user."""
        if self.region == types.Region.OVERSEAS:
            url = "/community/painter/wapi/user/full"
        elif self.region == types.Region.CHINESE:
            url = "/user/wapi/getUserFullInfo"
        else:
            raise TypeError(f"{self.region!r} is not a valid region.")

        data = await self.request_bbs(
            url=url,
            lang=lang,
            params=dict(uid=hoyolab_id) if hoyolab_id else None,
            cache=client_cache.cache_key("hoyolab", uid=hoyolab_id, lang=lang or self.lang),
        )
        return models.FullHoyolabUser(**data["user_info"])

    async def get_recommended_users(self, *, limit: int = 200) -> typing.Sequence[models.PartialHoyolabUser]:
        """Get a list of recommended active users."""
        data = await self.request_bbs(
            "community/user/wapi/recommendActive",
            params=dict(page_size=limit),
            cache=client_cache.cache_key("recommended"),
        )
        return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

    async def get_genshin_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Genshin Impact announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.GENSHIN)
        else:
            uid = uid or 900000005
        return await self._request_announcements(types.Game.GENSHIN, uid, lang=lang)

    async def get_zzz_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Zenless Zone Zero announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.ZZZ)
        else:
            uid = uid or 1300000000
        return await self._request_announcements(types.Game.ZZZ, uid, lang=lang)

    async def get_starrail_announcements(
        self,
        *,
        uid: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.Announcement]:
        """Get a list of Star Rail announcements."""
        if self.cookie_manager.multi:
            uid = uid or await self._get_uid(types.Game.STARRAIL)
        else:
            uid = uid or 809162009
        return await self._request_announcements(types.Game.STARRAIL, uid, lang=lang)

    @managers.requires_cookie_token
    async def redeem_code(
        self,
        code: str,
        uid: typing.Optional[int] = None,
        *,
        game: typing.Optional[types.Game] = None,
        lang: typing.Optional[str] = None,
        region: typing.Optional[str] = None,
    ) -> None:
        """Redeems a gift code for the current user."""
        if game is None:
            if self.default_game is None:
                raise RuntimeError("No default game set.")

            game = self.default_game

        if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, types.Game.TOT}:
            raise ValueError(f"{game} does not support code redemption.")

        uid = uid or await self._get_uid(game)

        try:
            region = region or utility.recognize_server(uid, game)
        except Exception:
            warnings.warn(f"Failed to recognize server for game {game!r} and uid {uid!r}, fetching from API now.")
            region = await self._get_server_region(uid, game)

        await self.request(
            routes.CODE_URL.get_url(self.region, game),
            params=dict(
                uid=uid,
                region=region,
                cdkey=code,
                game_biz=utility.get_prod_game_biz(self.region, game),
                lang=utility.create_short_lang_code(lang or self.lang),
            ),
            method="POST" if game is types.Game.STARRAIL else "GET",
        )

    @managers.no_multi
    async def check_in_community(self) -> None:
        """Check in to the hoyolab community and claim your daily 5 community exp."""
        raise RuntimeError("This API is deprecated.")

    @base.region_specific(types.Region.OVERSEAS)
    async def fetch_mi18n(
        self, url: typing.Union[str, yarl.URL], filename: str, *, lang: typing.Optional[str] = None
    ) -> typing.Mapping[str, str]:
        """Fetch a mi18n file."""
        return await self.request(
            yarl.URL(url) / f"{filename}/{filename}-{lang or self.lang}.json",
            cache=client_cache.cache_key("mi18n", filename=filename, url=url, lang=lang or self.lang),
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_games(self, *, lang: typing.Optional[str] = None) -> typing.Sequence[models.MimoGame]:
        """Get a list of Traveling Mimo games."""
        data = await self._request_mimo("index", params=dict(lang=lang or self.lang))
        if self.game is None:
            raise RuntimeError("No default game set.")

        if self.game is types.Game.GENSHIN:
            return [models.MimoGame(**i["act_info"]) for i in data["act_list"]]
        return [models.MimoGame(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def _get_mimo_game_data(
        self, game: typing.Union[typing.Literal["hoyolab"], types.Game]
    ) -> typing.Tuple[int, int]:
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.id, mimo_game.version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def _parse_mimo_args(
        self,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> typing.Tuple[int, int]:
        if game_id is None or version_id is None:
            if game is None:
                if self.default_game is None:
                    raise RuntimeError("No default game set.")
                game = self.default_game

            if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, "hoyolab"}:
                raise ValueError(f"{game!r} does not support Traveling Mimo.")
            game_id, version_id = await self._get_mimo_game_data(game)

        return game_id, version_id

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_tasks(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoTask]:
        """Get a list of Traveling Mimo missions (tasks)."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "task-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoTask(**i) for i in data["task_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def claim_mimo_task_reward(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Claim a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "receive-point",
            params=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST" if game_id == 2 else "GET",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def finish_mimo_task(
        self,
        task_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> None:
        """Finish a Traveling Mimo mission (task) reward."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        await self._request_mimo(
            "finish-task",
            data=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_shop_items(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> typing.Sequence[models.MimoShopItem]:
        """Get a list of Traveling Mimo shop items."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange-list",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return [models.MimoShopItem(**i) for i in data["exchange_award_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def buy_mimo_shop_item(
        self,
        item_id: int,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> str:
        """Buy an item from the Traveling Mimo shop and return a gift code to redeem it."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "exchange",
            data=dict(award_id=item_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return data["exchange_code"]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_point_count(
        self,
        *,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    ) -> int:
        """Get the current Traveling Mimo point count."""
        game = game or self.default_game
        games = await self.get_mimo_games()
        mimo_game = next((i for i in games if i.game == game), None)
        if mimo_game is None:
            raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
        return mimo_game.point

    @base.region_specific(types.Region.OVERSEAS)
    async def get_mimo_lottery_info(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryInfo:
        """Get Traveling Mimo lottery info."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery-info",
            params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        )
        return models.MimoLotteryInfo(**data)

    @base.region_specific(types.Region.OVERSEAS)
    async def draw_mimo_lottery(
        self,
        *,
        game_id: typing.Optional[int] = None,
        version_id: typing.Optional[int] = None,
        game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
        lang: typing.Optional[str] = None,
    ) -> models.MimoLotteryResult:
        """Draw a Traveling Mimo lottery."""
        game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
        data = await self._request_mimo(
            "lottery",
            data=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
            method="POST",
        )
        return models.MimoLotteryResult(**data)

    async def reply_to_post(self, content: str, *, post_id: int) -> int:
        """Reply to a community post."""
        data = await self.request_bbs(
            "community/post/wapi/releaseReply",
            data=dict(
                post_id=str(post_id),
                content=f"<p>{content}</p>",
                image_list=[],
                reply_bubble_id="",
                structured_content=json.dumps([{"insert": f"{content}\n"}]),
            ),
            method="POST",
            headers={"x-rpc-device_id": str(uuid.uuid4())},
        )
        return int(data["reply_id"])

    async def delete_reply(self, *, reply_id: int, post_id: int) -> None:
        """Delete a reply."""
        await self.request_bbs(
            "community/post/wapi/deleteReply",
            data=dict(reply_id=reply_id, post_id=post_id),
            method="POST",
        )

    async def get_replies(self, *, size: int = 15) -> typing.Sequence[models.Reply]:
        """Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content."""
        data = await self.request_bbs(
            "community/post/wapi/userReply",
            params=dict(size=size),
        )
        return [models.Reply(**i["reply"]) for i in data["list"]]

    async def _request_join(self, topic_id: int, *, is_cancel: bool) -> None:
        await self.request_bbs(
            "community/topic/wapi/join",
            data=dict(topic_id=topic_id, is_cancel=is_cancel),
            method="POST",
        )

    async def join_topic(self, topic_id: int) -> None:
        """Join a topic."""
        await self._request_join(topic_id, is_cancel=False)

    async def leave_topic(self, topic_id: int) -> None:
        """Leave a topic."""
        await self._request_join(topic_id, is_cancel=True)

    @base.region_specific(types.Region.OVERSEAS)
    async def get_web_events(
        self,
        game: typing.Optional[types.Game] = None,
        *,
        size: int = 15,
        offset: typing.Optional[int] = None,
        lang: typing.Optional[str] = None,
    ) -> list[models.WebEvent]:
        """Get a list of web events."""
        game = game or self.default_game
        if game is None:
            raise ValueError("No default game set.")

        data = await self.request_bbs(
            "community/community_contribution/wapi/event/list",
            params=dict(gids=WEB_EVENT_GAME_IDS[game], size=size, offset=offset or ""),
            lang=lang,
        )
        return [models.WebEvent(**i) for i in data["list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def get_accompany_characters(
        self, *, lang: typing.Optional[str] = None
    ) -> typing.Sequence[models.AccompanyCharacterGame]:
        """Get a list of accompany characters, this endpoint doesn't require cookies."""
        data = await self.request_bbs(
            "community/painter/api/getChannelRoleList",
            cache=client_cache.cache_key("accp_chars"),
            method="POST",
            lang=lang,
        )
        return [models.AccompanyCharacterGame(**i) for i in data["game_roles_list"]]

    @base.region_specific(types.Region.OVERSEAS)
    async def accompany_character(self, *, role_id: int, topic_id: int) -> models.AccompanyResult:
        """Accompany a character, role_id and topic_id can be found by calling get_accompany_characters."""
        data = await self.request_bbs(
            "community/apihub/api/user/accompany/role", params=dict(role_id=role_id, topic_id=topic_id)
        )
        return models.AccompanyResult(**data)

Methods

async def accompany_character(self, *, role_id: int, topic_id: int) ‑> AccompanyResult
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def accompany_character(self, *, role_id: int, topic_id: int) -> models.AccompanyResult:
    """Accompany a character, role_id and topic_id can be found by calling get_accompany_characters."""
    data = await self.request_bbs(
        "community/apihub/api/user/accompany/role", params=dict(role_id=role_id, topic_id=topic_id)
    )
    return models.AccompanyResult(**data)

Accompany a character, role_id and topic_id can be found by calling get_accompany_characters.

async def buy_mimo_shop_item(self,
item_id: int,
*,
game_id: int | None = None,
version_id: int | None = None,
game: Literal['hoyolab'] | Game | None = None,
lang: str | None = None) ‑> str
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def buy_mimo_shop_item(
    self,
    item_id: int,
    *,
    game_id: typing.Optional[int] = None,
    version_id: typing.Optional[int] = None,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    lang: typing.Optional[str] = None,
) -> str:
    """Buy an item from the Traveling Mimo shop and return a gift code to redeem it."""
    game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
    data = await self._request_mimo(
        "exchange",
        data=dict(award_id=item_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
        method="POST",
    )
    return data["exchange_code"]

Buy an item from the Traveling Mimo shop and return a gift code to redeem it.

async def check_in_community(self) ‑> None
Expand source code
@managers.no_multi
async def check_in_community(self) -> None:
    """Check in to the hoyolab community and claim your daily 5 community exp."""
    raise RuntimeError("This API is deprecated.")

Check in to the hoyolab community and claim your daily 5 community exp.

async def claim_mimo_task_reward(self,
task_id: int,
*,
game_id: int | None = None,
version_id: int | None = None,
game: Literal['hoyolab'] | Game | None = None,
lang: str | None = None) ‑> None
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def claim_mimo_task_reward(
    self,
    task_id: int,
    *,
    game_id: typing.Optional[int] = None,
    version_id: typing.Optional[int] = None,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    lang: typing.Optional[str] = None,
) -> None:
    """Claim a Traveling Mimo mission (task) reward."""
    game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
    await self._request_mimo(
        "receive-point",
        params=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
        method="POST" if game_id == 2 else "GET",
    )

Claim a Traveling Mimo mission (task) reward.

async def delete_reply(self, *, reply_id: int, post_id: int) ‑> None
Expand source code
async def delete_reply(self, *, reply_id: int, post_id: int) -> None:
    """Delete a reply."""
    await self.request_bbs(
        "community/post/wapi/deleteReply",
        data=dict(reply_id=reply_id, post_id=post_id),
        method="POST",
    )

Delete a reply.

async def draw_mimo_lottery(self,
*,
game_id: int | None = None,
version_id: int | None = None,
game: Literal['hoyolab'] | Game | None = None,
lang: str | None = None) ‑> MimoLotteryResult
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def draw_mimo_lottery(
    self,
    *,
    game_id: typing.Optional[int] = None,
    version_id: typing.Optional[int] = None,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    lang: typing.Optional[str] = None,
) -> models.MimoLotteryResult:
    """Draw a Traveling Mimo lottery."""
    game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
    data = await self._request_mimo(
        "lottery",
        data=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
        method="POST",
    )
    return models.MimoLotteryResult(**data)

Draw a Traveling Mimo lottery.

async def fetch_mi18n(self, url: str | yarl.URL, filename: str, *, lang: str | None = None) ‑> Mapping[str, str]
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def fetch_mi18n(
    self, url: typing.Union[str, yarl.URL], filename: str, *, lang: typing.Optional[str] = None
) -> typing.Mapping[str, str]:
    """Fetch a mi18n file."""
    return await self.request(
        yarl.URL(url) / f"{filename}/{filename}-{lang or self.lang}.json",
        cache=client_cache.cache_key("mi18n", filename=filename, url=url, lang=lang or self.lang),
    )

Fetch a mi18n file.

async def finish_mimo_task(self,
task_id: int,
*,
game_id: int | None = None,
version_id: int | None = None,
game: Literal['hoyolab'] | Game | None = None,
lang: str | None = None) ‑> None
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def finish_mimo_task(
    self,
    task_id: int,
    *,
    game_id: typing.Optional[int] = None,
    version_id: typing.Optional[int] = None,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    lang: typing.Optional[str] = None,
) -> None:
    """Finish a Traveling Mimo mission (task) reward."""
    game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
    await self._request_mimo(
        "finish-task",
        data=dict(task_id=task_id, game_id=game_id, lang=lang or self.lang, version_id=version_id),
        method="POST",
    )

Finish a Traveling Mimo mission (task) reward.

async def get_accompany_characters(self, *, lang: str | None = None) ‑> Sequence[AccompanyCharacterGame]
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def get_accompany_characters(
    self, *, lang: typing.Optional[str] = None
) -> typing.Sequence[models.AccompanyCharacterGame]:
    """Get a list of accompany characters, this endpoint doesn't require cookies."""
    data = await self.request_bbs(
        "community/painter/api/getChannelRoleList",
        cache=client_cache.cache_key("accp_chars"),
        method="POST",
        lang=lang,
    )
    return [models.AccompanyCharacterGame(**i) for i in data["game_roles_list"]]

Get a list of accompany characters, this endpoint doesn't require cookies.

async def get_genshin_announcements(self, *, uid: int | None = None, lang: str | None = None) ‑> Sequence[Announcement]
Expand source code
async def get_genshin_announcements(
    self,
    *,
    uid: typing.Optional[int] = None,
    lang: typing.Optional[str] = None,
) -> typing.Sequence[models.Announcement]:
    """Get a list of Genshin Impact announcements."""
    if self.cookie_manager.multi:
        uid = uid or await self._get_uid(types.Game.GENSHIN)
    else:
        uid = uid or 900000005
    return await self._request_announcements(types.Game.GENSHIN, uid, lang=lang)

Get a list of Genshin Impact announcements.

async def get_hoyolab_user(self, hoyolab_id: int | None = None, *, lang: str | None = None) ‑> FullHoyolabUser
Expand source code
async def get_hoyolab_user(
    self,
    hoyolab_id: typing.Optional[int] = None,
    *,
    lang: typing.Optional[str] = None,
) -> models.FullHoyolabUser:
    """Get a hoyolab user."""
    if self.region == types.Region.OVERSEAS:
        url = "/community/painter/wapi/user/full"
    elif self.region == types.Region.CHINESE:
        url = "/user/wapi/getUserFullInfo"
    else:
        raise TypeError(f"{self.region!r} is not a valid region.")

    data = await self.request_bbs(
        url=url,
        lang=lang,
        params=dict(uid=hoyolab_id) if hoyolab_id else None,
        cache=client_cache.cache_key("hoyolab", uid=hoyolab_id, lang=lang or self.lang),
    )
    return models.FullHoyolabUser(**data["user_info"])

Get a hoyolab user.

async def get_mimo_games(self, *, lang: str | None = None) ‑> Sequence[MimoGame]
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def get_mimo_games(self, *, lang: typing.Optional[str] = None) -> typing.Sequence[models.MimoGame]:
    """Get a list of Traveling Mimo games."""
    data = await self._request_mimo("index", params=dict(lang=lang or self.lang))
    if self.game is None:
        raise RuntimeError("No default game set.")

    if self.game is types.Game.GENSHIN:
        return [models.MimoGame(**i["act_info"]) for i in data["act_list"]]
    return [models.MimoGame(**i) for i in data["list"]]

Get a list of Traveling Mimo games.

async def get_mimo_lottery_info(self,
*,
game_id: int | None = None,
version_id: int | None = None,
game: Literal['hoyolab'] | Game | None = None,
lang: str | None = None) ‑> MimoLotteryInfo
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def get_mimo_lottery_info(
    self,
    *,
    game_id: typing.Optional[int] = None,
    version_id: typing.Optional[int] = None,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    lang: typing.Optional[str] = None,
) -> models.MimoLotteryInfo:
    """Get Traveling Mimo lottery info."""
    game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
    data = await self._request_mimo(
        "lottery-info",
        params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
    )
    return models.MimoLotteryInfo(**data)

Get Traveling Mimo lottery info.

async def get_mimo_point_count(self,
*,
game: Literal['hoyolab'] | Game | None = None) ‑> int
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def get_mimo_point_count(
    self,
    *,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
) -> int:
    """Get the current Traveling Mimo point count."""
    game = game or self.default_game
    games = await self.get_mimo_games()
    mimo_game = next((i for i in games if i.game == game), None)
    if mimo_game is None:
        raise ValueError(f"Game {game!r} not found in the list of Traveling Mimo games.")
    return mimo_game.point

Get the current Traveling Mimo point count.

async def get_mimo_shop_items(self,
*,
game_id: int | None = None,
version_id: int | None = None,
game: Literal['hoyolab'] | Game | None = None,
lang: str | None = None) ‑> Sequence[MimoShopItem]
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def get_mimo_shop_items(
    self,
    *,
    game_id: typing.Optional[int] = None,
    version_id: typing.Optional[int] = None,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    lang: typing.Optional[str] = None,
) -> typing.Sequence[models.MimoShopItem]:
    """Get a list of Traveling Mimo shop items."""
    game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
    data = await self._request_mimo(
        "exchange-list",
        params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
    )
    return [models.MimoShopItem(**i) for i in data["exchange_award_list"]]

Get a list of Traveling Mimo shop items.

async def get_mimo_tasks(self,
*,
game_id: int | None = None,
version_id: int | None = None,
game: Literal['hoyolab'] | Game | None = None,
lang: str | None = None) ‑> Sequence[MimoTask]
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def get_mimo_tasks(
    self,
    *,
    game_id: typing.Optional[int] = None,
    version_id: typing.Optional[int] = None,
    game: typing.Optional[typing.Union[typing.Literal["hoyolab"], types.Game]] = None,
    lang: typing.Optional[str] = None,
) -> typing.Sequence[models.MimoTask]:
    """Get a list of Traveling Mimo missions (tasks)."""
    game_id, version_id = await self._parse_mimo_args(game_id, version_id, game)
    data = await self._request_mimo(
        "task-list",
        params=dict(game_id=game_id, lang=lang or self.lang, version_id=version_id),
    )
    return [models.MimoTask(**i) for i in data["task_list"]]

Get a list of Traveling Mimo missions (tasks).

Expand source code
async def get_recommended_users(self, *, limit: int = 200) -> typing.Sequence[models.PartialHoyolabUser]:
    """Get a list of recommended active users."""
    data = await self.request_bbs(
        "community/user/wapi/recommendActive",
        params=dict(page_size=limit),
        cache=client_cache.cache_key("recommended"),
    )
    return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

Get a list of recommended active users.

async def get_replies(self, *, size: int = 15) ‑> Sequence[Reply]
Expand source code
async def get_replies(self, *, size: int = 15) -> typing.Sequence[models.Reply]:
    """Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content."""
    data = await self.request_bbs(
        "community/post/wapi/userReply",
        params=dict(size=size),
    )
    return [models.Reply(**i["reply"]) for i in data["list"]]

Get the latest replies as a list of tuples, where the first element is the reply ID and the second is the content.

async def get_starrail_announcements(self, *, uid: int | None = None, lang: str | None = None) ‑> Sequence[Announcement]
Expand source code
async def get_starrail_announcements(
    self,
    *,
    uid: typing.Optional[int] = None,
    lang: typing.Optional[str] = None,
) -> typing.Sequence[models.Announcement]:
    """Get a list of Star Rail announcements."""
    if self.cookie_manager.multi:
        uid = uid or await self._get_uid(types.Game.STARRAIL)
    else:
        uid = uid or 809162009
    return await self._request_announcements(types.Game.STARRAIL, uid, lang=lang)

Get a list of Star Rail announcements.

async def get_web_events(self,
game: Game | None = None,
*,
size: int = 15,
offset: int | None = None,
lang: str | None = None) ‑> list[WebEvent]
Expand source code
@base.region_specific(types.Region.OVERSEAS)
async def get_web_events(
    self,
    game: typing.Optional[types.Game] = None,
    *,
    size: int = 15,
    offset: typing.Optional[int] = None,
    lang: typing.Optional[str] = None,
) -> list[models.WebEvent]:
    """Get a list of web events."""
    game = game or self.default_game
    if game is None:
        raise ValueError("No default game set.")

    data = await self.request_bbs(
        "community/community_contribution/wapi/event/list",
        params=dict(gids=WEB_EVENT_GAME_IDS[game], size=size, offset=offset or ""),
        lang=lang,
    )
    return [models.WebEvent(**i) for i in data["list"]]

Get a list of web events.

async def get_zzz_announcements(self, *, uid: int | None = None, lang: str | None = None) ‑> Sequence[Announcement]
Expand source code
async def get_zzz_announcements(
    self,
    *,
    uid: typing.Optional[int] = None,
    lang: typing.Optional[str] = None,
) -> typing.Sequence[models.Announcement]:
    """Get a list of Zenless Zone Zero announcements."""
    if self.cookie_manager.multi:
        uid = uid or await self._get_uid(types.Game.ZZZ)
    else:
        uid = uid or 1300000000
    return await self._request_announcements(types.Game.ZZZ, uid, lang=lang)

Get a list of Zenless Zone Zero announcements.

async def join_topic(self, topic_id: int) ‑> None
Expand source code
async def join_topic(self, topic_id: int) -> None:
    """Join a topic."""
    await self._request_join(topic_id, is_cancel=False)

Join a topic.

async def leave_topic(self, topic_id: int) ‑> None
Expand source code
async def leave_topic(self, topic_id: int) -> None:
    """Leave a topic."""
    await self._request_join(topic_id, is_cancel=True)

Leave a topic.

async def redeem_code(self,
code: str,
uid: int | None = None,
*,
game: Game | None = None,
lang: str | None = None,
region: str | None = None) ‑> None
Expand source code
@managers.requires_cookie_token
async def redeem_code(
    self,
    code: str,
    uid: typing.Optional[int] = None,
    *,
    game: typing.Optional[types.Game] = None,
    lang: typing.Optional[str] = None,
    region: typing.Optional[str] = None,
) -> None:
    """Redeems a gift code for the current user."""
    if game is None:
        if self.default_game is None:
            raise RuntimeError("No default game set.")

        game = self.default_game

    if game not in {types.Game.GENSHIN, types.Game.ZZZ, types.Game.STARRAIL, types.Game.TOT}:
        raise ValueError(f"{game} does not support code redemption.")

    uid = uid or await self._get_uid(game)

    try:
        region = region or utility.recognize_server(uid, game)
    except Exception:
        warnings.warn(f"Failed to recognize server for game {game!r} and uid {uid!r}, fetching from API now.")
        region = await self._get_server_region(uid, game)

    await self.request(
        routes.CODE_URL.get_url(self.region, game),
        params=dict(
            uid=uid,
            region=region,
            cdkey=code,
            game_biz=utility.get_prod_game_biz(self.region, game),
            lang=utility.create_short_lang_code(lang or self.lang),
        ),
        method="POST" if game is types.Game.STARRAIL else "GET",
    )

Redeems a gift code for the current user.

async def reply_to_post(self, content: str, *, post_id: int) ‑> int
Expand source code
async def reply_to_post(self, content: str, *, post_id: int) -> int:
    """Reply to a community post."""
    data = await self.request_bbs(
        "community/post/wapi/releaseReply",
        data=dict(
            post_id=str(post_id),
            content=f"<p>{content}</p>",
            image_list=[],
            reply_bubble_id="",
            structured_content=json.dumps([{"insert": f"{content}\n"}]),
        ),
        method="POST",
        headers={"x-rpc-device_id": str(uuid.uuid4())},
    )
    return int(data["reply_id"])

Reply to a community post.

async def search_users(self, keyword: str, *, lang: str | None = None) ‑> Sequence[PartialHoyolabUser]
Expand source code
async def search_users(
    self,
    keyword: str,
    *,
    lang: typing.Optional[str] = None,
) -> typing.Sequence[models.PartialHoyolabUser]:
    """Search hoyolab users."""
    data = await self.request_bbs(
        "community/search/wapi/search/user",
        lang=lang,
        params=dict(keyword=keyword, page_size=20),
        cache=client_cache.cache_key("search", keyword=keyword, lang=self.lang),
    )
    return [models.PartialHoyolabUser(**i["user"]) for i in data["list"]]

Search hoyolab users.

Inherited members