Beispiel #1
0
    async def test_song(self, song_id: int = 0) -> ExtDict:
        codes = {-1: MissingAccess(f"Failed to fetch artist info for ID: {song_id}")}
        payload = Params().create_new("web").put_definer("song", song_id).close()
        resp = await self.http.request(
            Route.TEST_SONG, params=payload, method="get", error_codes=codes
        )

        data = ExtDict(id=song_id)

        try:
            data.update(extract_info_from_endpoint(resp))
        except ValueError:
            raise MissingAccess(f"Failed to load data. Response: {resp!r}.") from None

        return data
Beispiel #2
0
    async def get_user_list(
        self, type: int = 0, *, exclude: Tuple[Type[BaseException]] = (), client: Client
    ) -> List[ExtDict]:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_type(type)
            .finish()
        )
        codes = {
            -1: MissingAccess("Failed to fetch a user list."),
            -2: NothingFound("gd.AbstractUser"),
        }

        resp = await self.http.request(
            Route.GET_USER_LIST, payload, error_codes=codes, exclude=exclude
        )

        if resp is None:
            return []

        resp, parser = resp.split("|"), Parser().with_split(":").should_map()

        return list(map(parser.parse, resp))
Beispiel #3
0
    async def like(
        self, item_id: int, typeid: int, special: int, dislike: bool = False, *, client: Client
    ) -> None:
        like = dislike ^ 1

        rs, udid, uuid = Coder.gen_rs(), Params.gen_udid(), Params.gen_uuid()
        values = [special, item_id, like, typeid, rs, client.account_id, udid, uuid]
        chk = Coder.gen_chk(type="like_rate", values=values)

        payload = (
            Params()
            .create_new(game_version=20)
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_udid(udid)
            .put_uuid(uuid)
            .put_definer("itemid", item_id)
            .put_like(like)
            .put_type(typeid)
            .put_special(special)
            .put_rs(rs)
            .put_chk(chk)
            .finish()
        )

        resp = await self.http.request(Route.LIKE_ITEM, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to like an item by ID: {item_id}.")
Beispiel #4
0
    async def search_user(self, query: Union[int, str], return_abstract: bool = False) -> ExtDict:

        payload = (
            Params().create_new().put_definer("search", query).put_total(0).put_page(0).finish()
        )
        codes = {-1: MissingAccess(f"Searching for {query!r} failed.")}

        resp = await self.http.request(Route.USER_SEARCH, payload, error_codes=codes)
        mapped = Parser().split("#").take(0).check_empty().split(":").should_map().parse(resp)

        if mapped is None:
            raise codes.get(-1)

        account_id = mapped.getcast(Index.USER_ACCOUNT_ID, 0, int)

        if return_abstract or not account_id:
            return mapped

        # ok; if we should not return abstract, let's find all other parameters
        payload = Params().create_new().put_definer("user", account_id).finish()

        resp = await self.http.request(Route.GET_USER_INFO, payload, error_codes=codes)
        mapped.update(Parser().with_split(":").should_map().parse(resp))

        return mapped
Beispiel #5
0
    async def update_settings(
        self,
        message_policy: MessagePolicyType,
        friend_request_policy: FriendRequestPolicyType,
        comment_policy: CommentPolicyType,
        youtube: str,
        twitter: str,
        twitch: str,
        *,
        client: Client,
    ) -> None:
        payload = (
            Params()
            .create_new("web")
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_profile_upd(
                message_policy.value,
                friend_request_policy.value,
                comment_policy.value,
                youtube,
                twitter,
                twitch,
            )
            .finish_login()
        )
        resp = await self.http.request(Route.UPDATE_ACC_SETTINGS, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to update profile settings of a client: {client!r}.")
Beispiel #6
0
    async def get_user(self, account_id: int = 0, return_only_stats: bool = False) -> ExtDict:
        payload = Params().create_new().put_definer("user", account_id).finish()
        codes = {-1: MissingAccess(f"No users were found with ID: {account_id}.")}

        resp = await self.http.request(Route.GET_USER_INFO, payload, error_codes=codes)
        mapped = Parser().with_split(":").should_map().parse(resp)

        if return_only_stats:
            return mapped

        another = (
            Params()
            .create_new()
            .put_definer("search", mapped.getcast(Index.USER_PLAYER_ID, 0, int))
            .put_total(0)
            .put_page(0)
            .finish()
        )
        some_resp = await self.http.request(Route.USER_SEARCH, another)

        new_resp = (
            Parser().split("#").take(0).check_empty().split(":").should_map().parse(some_resp)
        )

        if new_resp is None:
            raise codes.get(-1)

        mapped.update(
            {k: new_resp.get(k) for k in (Index.USER_NAME, Index.USER_ICON, Index.USER_ICON_TYPE)}
        )

        return mapped
Beispiel #7
0
    async def get_page_messages(
        self,
        sent_or_inbox: str,
        page: int,
        *,
        exclude: Tuple[Type[BaseException]] = (),
        client: Client,
    ) -> List[ExtDict]:
        assert sent_or_inbox in ("inbox", "sent")
        inbox = 0 if sent_or_inbox != "sent" else 1

        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_page(page)
            .put_total(0)
            .get_sent(inbox)
            .finish()
        )
        codes = {-1: MissingAccess("Failed to get messages."), -2: NothingFound("gd.Message")}

        resp = await self.http.request(
            Route.GET_PRIVATE_MESSAGES, payload, error_codes=codes, exclude=exclude
        )
        resp = Parser().split("#").take(0).check_empty().split("|").parse(resp)

        if resp is None:
            return []

        parser = Parser().with_split(":").should_map()
        return list(map(parser.parse, resp))
Beispiel #8
0
    async def rate_level(self, level_id: int, rating: int, *, client: Client) -> None:
        assert 0 < rating <= 10, "Invalid star value given."

        rs, udid, uuid = Coder.gen_rs(), Params.gen_udid(), Params.gen_uuid()
        values = [level_id, rating, rs, client.account_id, udid, uuid]
        chk = Coder.gen_chk(type="like_rate", values=values)

        payload = (
            Params()
            .create_new()
            .put_definer("levelid", level_id)
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_udid(udid)
            .put_uuid(uuid)
            .put_definer("stars", rating)
            .put_rs(rs)
            .put_chk(chk)
            .finish()
        )

        resp = await self.http.request(Route.RATE_LEVEL_STARS, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to rate level by ID: {level_id}.")
Beispiel #9
0
    async def load_save(self, client: Client) -> Tuple[Optional[api.Database], Optional[Save]]:
        link = Route.GD_URL

        payload = (
            Params()
            .create_new()
            .put_username(client.name)
            .put_definer("password", client.password)
            .finish_login()
        )
        codes = {-11: MissingAccess(f"Failed to load data for client: {client!r}.")}

        resp = await self.http.request(
            Route.LOAD_DATA, payload, error_codes=codes, custom_base=link
        )

        try:
            main, levels, *_ = resp.split(";")
            db = await api.save.from_string_async(main, levels, xor=False, follow_os=False)
            save = await SaveParser.aio_parse(db.main.dump())

            return db, save

        except Exception:
            return None, None
Beispiel #10
0
def check_logged_obj(obj: Any, func_name: str) -> None:
    try:
        client = obj if hasattr(obj, "is_logged") else obj.client

    except AttributeError:
        raise MissingAccess(
            message=f"Failed to find client on object: {obj!r}.") from None

    else:
        if client is None:
            raise MissingAccess(message=(
                f"Attempt to check if client is logged for {obj!r} returned None. "
                "Have you made this object by hand?"))

        if not client.is_logged():
            raise NotLoggedError(func_name)
Beispiel #11
0
 async def get_song(self, song_id: int = 0) -> ExtDict:
     payload = Params().create_new().put_definer("song", song_id).finish()
     codes = {
         -1: MissingAccess(f"No songs were found with ID: {song_id}."),
         -2: SongRestrictedForUsage(song_id),
     }
     resp = await self.http.request(Route.GET_SONG_INFO, payload, error_codes=codes)
     return Parser().with_split("~|~").should_map().parse(resp)
Beispiel #12
0
    async def get_timely_info(self, type_id: int = -1) -> Tuple[int, int, int]:
        # Daily: -1, Weekly: -2
        weekly = ~type_id
        payload = Params().create_new().put_weekly(weekly).finish()
        codes = {-1: MissingAccess(f"Failed to fetch a {type!r} level.")}
        resp = await self.http.request(Route.GET_TIMELY, payload, error_codes=codes)

        try:
            number, cooldown, *_ = map(int, resp.split("|"))
        except ValueError:  # unpacking failed or something else
            raise MissingAccess(
                "Failed to fetch a timely level. Most likely it is being refreshed."
            ) from None

        number %= 100000
        weekly += 1

        return (weekly, number, cooldown)
Beispiel #13
0
    async def upload(self, **kwargs) -> None:
        r"""|coro|

        Upload ``self``.

        Parameters
        ----------
        \*\*kwargs
            Arguments that :meth:`.Client.upload_level` accepts.
            Defaults are properties of the level.
        """
        track, song_id = (self.song.id, 0)

        if self.song.is_custom():
            track, song_id = song_id, track

        try:
            client = self.client
        except Exception:
            client = kwargs.pop("from_client", None)

            if client is None:
                raise MissingAccess(
                    "Could not find the client to upload level from. "
                    "Either attach a client to this level or provide <from_client> parameter."
                ) from None

        password = kwargs.pop("password", self.password)

        args = dict(
            name=self.name,
            id=self.id,
            version=self.version,
            length=abs(self.length.value),
            track=track,
            song_id=song_id,
            two_player=False,
            is_auto=self.is_auto(),
            original=self.original_id,
            objects=self.objects,
            coins=self.coins,
            star_amount=self.stars,
            unlisted=False,
            friends_only=False,
            ldm=False,
            password=password,
            copyable=self.is_copyable(),
            description=self.description,
            data=self.data,
        )

        args.update(kwargs)

        uploaded = await client.upload_level(**args)

        self.options = uploaded.options
Beispiel #14
0
    async def accept_friend_request(
        self, typeof: MessageOrRequestType, request_id: int, user_id: int, client: Client
    ) -> None:
        if typeof.value:  # is gd.MessageOrRequestType.SENT
            raise MissingAccess(
                "Failed to accept a friend request. Reason: request is sent, not received one."
            )
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_definer("user", user_id)
            .put_definer("requestid", request_id)
            .finish()
        )
        resp = await self.http.request(Route.ACCEPT_REQUEST, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to accept a friend request by ID: {request_id!r}.")
Beispiel #15
0
    async def send_level(
        self, level_id: int, rating: int, featured: bool, *, client: Client
    ) -> None:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_definer("levelid", level_id)
            .put_definer("stars", rating)
            .put_feature(int(featured))
            .finish_mod()
        )
        codes = {
            -2: MissingAccess(f"Missing moderator permissions to send a level by ID: {level_id!r}.")
        }

        resp = await self.http.request(Route.SUGGEST_LEVEL_STARS, payload, error_codes=codes)

        if resp != 1:
            raise MissingAccess(f"Failed to send a level by ID: {level_id!r}.")
Beispiel #16
0
    async def get_level_info(self, level_id: int = 0) -> Tuple[ExtDict, ExtDict, ExtDict]:
        # level data, creator, song
        assert level_id >= -2, "Invalid Level ID provided."

        if level_id < 0:
            type, number, cooldown = await self.get_timely_info(level_id)
        else:
            type, number, cooldown = 0, -1, -1

        ext = {"101": type, "102": number, "103": cooldown}

        codes = {-1: MissingAccess(f"Failed to get a level. Given ID: {level_id}")}

        payload = Params().create_new().put_definer("levelid", level_id).finish()
        resp = await self.http.request(Route.DOWNLOAD_LEVEL, payload, error_codes=codes)

        level_data = Parser().split("#").take(0).split(":").add_ext(ext).should_map().parse(resp)

        real_id = level_data.getcast(Index.LEVEL_ID, 0, int)

        payload = (
            Params()
            .create_new()
            .put_definer("search", real_id)
            .put_filters(Filters.setup_empty())
            .finish()
        )
        resp = await self.http.request(Route.LEVEL_SEARCH, payload, error_codes=codes)

        if not resp or resp.count("#") < 2:
            raise codes.get(-1)

        data = resp.split("#")

        # getting song
        song_data = data[2]

        if not song_data:
            song = Converter.to_normal_song(level_data.getcast(Index.LEVEL_AUDIO_TRACK, 0, int))
        else:
            song = Parser().with_split("~|~").should_map().parse(song_data)

        # getting creator
        creator_data = data[1]

        if not creator_data:
            id, name, account_id = (0, "unknown", 0)
        else:
            id, name, account_id = creator_data.split(":")

        creator = ExtDict(id=id, name=name, account_id=account_id)

        return level_data, creator, song
Beispiel #17
0
    async def unfriend_user(self, account_id: int, *, client: Client) -> None:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_definer("user", account_id)
            .finish()
        )
        resp = await self.http.request(Route.REMOVE_FRIEND, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to unfriend a user by Account ID: {account_id!r}.")
Beispiel #18
0
    async def read_friend_request(self, request_id: int, client: Client) -> None:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_definer("requestid", request_id)
            .finish()
        )
        resp = await self.http.request(Route.READ_REQUEST, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to read a friend request by ID: {request_id!r}.")
Beispiel #19
0
    async def get_leaderboard(
        self, level_id: int, strategy: LevelLeaderboardStrategy, *, client: Client
    ) -> List[ExtDict]:
        # timely_type: TimelyType, played: bool = False, timely_index: int = 0, percentage: int = 0,
        # jumps: int = 0, attempts: int = 0, seconds: int = 0, coins: int = 0
        # rs = Coder.gen_rs()
        # seed = Coder.gen_level_lb_seed(jumps, percentage, seconds, played)

        # if str(timely_type) == 'weekly':
        #     timely_index += 100000

        # values = [
        #     client.account_id, level_id, percentage, seconds, jumps, attempts,
        #     percentage, 100 - percentage, 1, coins, timely_index, rs
        # ]

        # chk = Coder.gen_chk(type='levelscore', values=values)

        params = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_definer("levelid", level_id)
            .put_password(client.encodedpass)
            .put_type(strategy.value)
        )

        # params.put_percent(percentage).put_chk(chk)

        # for index, value in enumerate((
        #     attempts + 8354, jumps + 3991, seconds + 4085, seed, random.randint(100, 10000),
        #     "", rs, attempts, coins + 5819, timely_index
        # ), 1):
        #     params.put_seed(value, prefix='s', suffix=index)

        payload = params.finish()

        codes = {-1: MissingAccess(f"Failed to get leaderboard of the level by ID: {level_id!r}.")}

        resp = await self.http.request(Route.GET_LEVEL_SCORES, payload, error_codes=codes)

        if not resp:
            return []

        resp, parser = (
            resp.split("|"),
            Parser().with_split(":").add_ext({"101": level_id}).should_map(),
        )

        return list(map(parser.parse, filter(is_not_empty, resp)))
Beispiel #20
0
    async def delete_level(self, level_id: int, *, client: Client) -> None:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_definer("levelid", level_id)
            .put_password(client.encodedpass)
            .finish_level()
        )

        resp = await self.http.request(Route.DELETE_LEVEL, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to delete a level by ID: {level_id}.")
Beispiel #21
0
    async def retrieve_page_comments(
        self,
        account_id: int,
        id: int,
        type: str = "profile",
        page: int = 0,
        *,
        strategy: CommentStrategy,
        exclude: Tuple[Type[BaseException]] = (),
    ) -> List[ExtDict]:
        assert isinstance(page, int) and page >= 0
        assert type in ("profile", "level")

        is_level = type == "level"

        typeid = is_level ^ 1
        definer = "userid" if is_level else "accountid"
        selfid = id if is_level else account_id
        route = Route.GET_COMMENT_HISTORY if is_level else Route.GET_ACC_COMMENTS

        parser = Parser().add_ext({"101": typeid}).should_map()

        if is_level:
            parser.split(":").take(0).split("~")
        else:
            parser.with_split("~")

        param_obj = Params().create_new().put_definer(definer, selfid).put_page(page).put_total(0)
        if is_level:
            param_obj.put_mode(strategy.value)
        payload = param_obj.finish()

        codes = {
            -1: MissingAccess(f"Failed to retrieve comment for user by Account ID: {account_id!r}.")
        }

        resp = await self.http.request(route, payload, error_codes=codes, exclude=exclude)

        if not resp:
            return []

        splitted = resp.split("#").pop(0)

        if not splitted:
            if issubclass(NothingFound, exclude):
                return []
            raise NothingFound("gd.Comment")

        return list(map(parser.parse, filter(is_not_empty, splitted.split("|"))))
Beispiel #22
0
    async def login(self, user: str, password: str) -> Tuple[int, int]:
        # account_id, id
        payload = (
            Params().create_new().put_login_definer(username=user, password=password).finish_login()
        )
        codes = {
            -1: LoginFailure(login=user, password=password),
            -12: MissingAccess(f"Account {user!r} (password {password!r}) is disabled."),
        }

        resp = await self.http.request(Route.LOGIN, payload, error_codes=codes)

        account_id, id, *junk = resp.split(",")

        return int(account_id), int(id)
Beispiel #23
0
    async def do_save(self, data: str, client: Client) -> None:
        link = Route.GD_URL

        codes = {
            -4: MissingAccess("Data is too large."),
            -5: MissingAccess("Invalid login credentials."),
            -6: MissingAccess("Something wrong happened."),
        }

        payload = (
            Params()
            .create_new()
            .put_username(client.name)
            .put_definer("password", client.password)
            .put_save_data(data)
            .finish_login()
        )

        resp = await self.http.request(
            Route.SAVE_DATA, payload, custom_base=link, error_codes=codes
        )

        if resp != 1:
            raise MissingAccess(f"Failed to do backup for client: {client!r}")
Beispiel #24
0
    async def update_level_desc(self, level_id: int, content: str, *, client: Client) -> None:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_definer("levelid", level_id)
            .put_level_desc(content)
            .finish()
        )

        resp = await self.http.request(Route.UPDATE_LEVEL_DESC, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to update description of the level by ID: {level_id}.")
Beispiel #25
0
    async def get_level_comments(
        self,
        level_id: int,
        strategy: CommentStrategy,
        amount: int,
        exclude: Tuple[Type[BaseException]] = (),
    ) -> List[Tuple[ExtDict, ExtDict]]:
        # comment, user
        payload = (
            Params()
            .create_new()
            .put_definer("levelid", level_id)
            .put_page(0)
            .put_total(0)
            .put_mode(strategy.value)
            .put_count(amount)
            .finish()
        )
        codes = {
            -1: MissingAccess(f"Failed to get comments of a level by ID: {level_id!r}."),
            -2: NothingFound("gd.Comment"),
        }

        resp = await self.http.request(
            Route.GET_COMMENTS, payload, error_codes=codes, exclude=exclude
        )

        if resp is None:
            return []

        splitted = Parser().split("#").take(0).split("|").parse(resp)
        parser = Parser().with_split("~").should_map()

        res = []

        for elem in filter(is_not_empty, splitted):
            com_data, user_data, *_ = map(parser.parse, elem.split(":"))
            com_data.update({"1": level_id, "101": 0, "102": 0})

            user_data = ExtDict(
                account_id=user_data.getcast(Index.USER_ACCOUNT_ID, 0, int),
                id=com_data.getcast(Index.COMMENT_AUTHOR_ID, 0, int),
                name=user_data.get(Index.USER_NAME, "unknown"),
            )

            res.append((com_data, user_data))

        return res
Beispiel #26
0
    async def block_user(self, account_id: int, unblock: bool = False, *, client: Client) -> None:
        route = Route.UNBLOCK_USER if unblock else Route.BLOCK_USER
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_definer("user", account_id)
            .finish()
        )
        resp = await self.http.request(route, payload)

        if resp != 1:
            raise MissingAccess(
                f"Failed to {'un' if unblock else ''}block a user by Account ID: {account_id!r}."
            )
Beispiel #27
0
    async def delete_message(
        self, typeof: MessageOrRequestType, message_id: int, client: Client
    ) -> None:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_definer("messageid", message_id)
            .put_password(client.encodedpass)
            .put_is_sender(typeof.name.lower())
            .finish()
        )
        resp = await self.http.request(Route.DELETE_PRIVATE_MESSAGE, payload)

        if resp != 1:
            raise MissingAccess(f"Failed to delete a message by ID: {message_id!r}.")
Beispiel #28
0
    async def post_comment(self, content: str, *, client: Client) -> None:
        to_gen = [client.name, 0, 0, 1]

        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_username(client.name)
            .put_password(client.encodedpass)
            .put_comment(content, to_gen)
            .comment_for("profile")
            .finish()
        )
        codes = {-1: MissingAccess("Failed to post a comment.")}

        await self.http.request(Route.UPLOAD_ACC_COMMENT, payload, error_codes=codes)
Beispiel #29
0
    async def update_profile(self, settings: Dict[str, int], *, client: Client) -> None:
        settings_cased = {Converter.snake_to_camel(name): value for name, value in settings.items()}

        rs = Coder.gen_rs()

        req_chk_params = [client.account_id]
        req_chk_params.extend(
            settings.get(param, 0)
            for param in (
                "user_coins",
                "demons",
                "stars",
                "coins",
                "icon_type",
                "icon",
                "diamonds",
                "acc_icon",
                "acc_ship",
                "acc_ball",
                "acc_bird",
                "acc_dart",
                "acc_robot",
                "acc_glow",
                "acc_spider",
                "acc_explosion",
            )
        )

        chk = Coder.gen_chk(type="userscore", values=req_chk_params)

        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_password(client.encodedpass)
            .put_username(client.name)
            .put_seed(rs)
            .put_seed(chk, suffix=str(2))
            .finish()
        )

        payload.update(settings_cased)

        resp = await self.http.request(Route.UPDATE_USER_SCORE, payload)

        if not resp > 0:
            raise MissingAccess(f"Failed to update profile of a client: {client!r}")
Beispiel #30
0
    async def read_message(
        self, typeof: MessageOrRequestType, message_id: int, client: Client
    ) -> str:
        payload = (
            Params()
            .create_new()
            .put_definer("accountid", client.account_id)
            .put_definer("messageid", message_id)
            .put_is_sender(typeof.name.lower())
            .put_password(client.encodedpass)
            .finish()
        )
        codes = {-1: MissingAccess(f"Failed to read a message by ID: {message_id!r}.")}
        resp = await self.http.request(Route.READ_PRIVATE_MESSAGE, payload, error_codes=codes,)
        mapped = Parser().with_split(":").should_map().parse(resp)

        return Coder.decode(type="message", string=mapped.get(Index.MESSAGE_BODY, ""))