from gd.typing import (
AbstractUser,
Any,
Client,
Comment,
FriendRequest,
Iterable,
Level,
LevelRecord,
Message,
Optional,
List,
Tuple,
Type,
Union,
User,
)
from gd.abstractentity import AbstractEntity
from gd.errors import NothingFound
from gd.utils.enums import CommentStrategy, LevelLeaderboardStrategy
from gd.utils.filters import Filters
from gd.utils.indexer import Index
from gd.utils.parser import ExtDict
from gd.utils.search_utils import get
from gd.utils.text_tools import make_repr
def try_int(some: Any) -> int:
try:
return int(some)
except Exception: # noqa
return 0
def excluding(*args: Tuple[Type[BaseException]]) -> Tuple[Type[BaseException]]:
return args
DEFAULT_EXCLUDE: Tuple[Type[BaseException]] = excluding(NothingFound)
[docs]class AbstractUser(AbstractEntity):
"""Class that represents an Abstract Geometry Dash User.
This class is derived from :class:`.AbstractEntity`.
"""
def __init__(self, *, client: Client, **options) -> None:
options.update(
{key: try_int(value) for key, value in options.items() if key in {"account_id", "id"}}
)
super().__init__(client=client, **options)
def __repr__(self) -> str:
info = {"name": repr(self.name), "id": self.id, "account_id": self.account_id}
return make_repr(self, info)
def __str__(self) -> str:
return str(self.name)
@classmethod
def from_data(cls, data: ExtDict, type: str = "normal", *, client: Client) -> AbstractUser:
return cls(
account_id=data.getcast(Index.USER_ACCOUNT_ID, 0, int),
id=data.getcast(Index.USER_PLAYER_ID, 0, int),
name=data.get(Index.USER_NAME, "unknown"),
client=client,
)
@property
def name(self) -> str:
""":class:`str`: String representing name of the user."""
return self.options.get("name", "")
@property
def account_id(self) -> int:
""":class:`int`: Account ID of the user."""
return self.options.get("account_id", 0)
@property
def parse_dict(self) -> ExtDict:
return ExtDict({k: getattr(self, k) for k in ("name", "id", "account_id")})
[docs] def is_registered(self) -> bool:
""":class:`bool`: Indicates whether user is registered or not."""
return self.id > 0 and self.account_id > 0
[docs] def as_user(self) -> AbstractUser:
"""Returns :class:`.AbstractUser` object.
This is used mainly in subclasses.
Returns
-------
:class:`.AbstractUser`
Abstract User from given object.
"""
return AbstractUser(**self.parse_dict, client=self.client)
[docs] async def to_user(self) -> User:
"""|coro|
Convert ``self`` to :class:`.User` object.
Returns
-------
:class:`.User`
A user object corresponding to the abstract one.
"""
return await self.client.get_user(self.account_id)
[docs] async def update(self) -> None:
"""|coro|
Update ``self``.
"""
new = await self.client.fetch_user(self.account_id)
self.options = new.options
[docs] async def send(self, subject: str, body: str) -> Optional[Message]:
"""|coro|
Send the message to ``self``. Requires logged client.
Parameters
----------
subject: :class:`str`
The subject of the message.
body: :class:`str`
The body of the message.
from_client: :class:`.Client`
A logged in client to send a message from. If ``None``,
defaults to a client attached to this user.
Raises
------
:exc:`.MissingAccess`
Failed to send a message.
Returns
-------
Optional[:class:`.Message`]
Sent message.
"""
return await self.client.send_message(self, subject, body)
[docs] async def block(self) -> None:
"""|coro|
Block a user. Requires logged in client.
Raises
------
:exc:`.MissingAccess`
Failed to block a user.
"""
await self.client.block(self)
[docs] async def unblock(self) -> None:
"""|coro|
Unblock a user. Requires logged in client.
Raises
------
:exc:`.MissingAccess`
Failed to unblock a user.
"""
await self.client.unblock(self)
[docs] async def unfriend(self) -> None:
"""|coro|
Try to unfriend a user. Requires logged in client.
Raises
------
:exc:`.MissingAccess`
Failed to unfriend a user.
"""
await self.client.unfriend(self)
[docs] async def send_friend_request(self, message: str = "") -> Optional[FriendRequest]:
"""|coro|
Send a friend request to a user.
.. note::
This function does not raise any error if request was already sent.
Parameters
----------
message: :class:`str`
A message to attach to a request.
Raises
------
:exc:`.MissingAccess`
Failed to send a friend request to user.
Returns
-------
Optional[:class:`.FriendRequest`]
Sent friend request.
"""
return await self.client.send_friend_request(self, message)
[docs] async def get_levels_on_page(
self, page: int = 0, *, exclude: Tuple[Type[BaseException]] = DEFAULT_EXCLUDE
) -> List[Level]:
"""|coro|
Fetches user's levels on a given page.
This function is equivalent to calling:
.. code-block:: python3
await self.client.search_levels_on_page(
page=page, filters=gd.Filters.setup_by_user(),
user=self, exclude=exclude
)
# 'self' is an AbstractUser instance here.
Parameters
----------
page: :class:`int`
Page to look for levels at.
exclude: Tuple[Type[:exc:`BaseException`]]
Exceptions to ignore. By default includes only :exc:`.NothingFound`.
Returns
-------
List[:class:`.Level`]
All levels found. Can be an empty list.
"""
filters = Filters.setup_by_user()
return await self.client.search_levels_on_page(
page=page, filters=filters, user=self, exclude=exclude
)
[docs] async def get_levels(self, pages: Iterable[int] = range(10)) -> List[Level]:
"""|coro|
Gets levels on specified pages.
This is equivalent to calling:
.. code-block:: python3
return await self.client.search_levels(
pages=pages, filters=gd.Filters.setup_by_user(), user=self
)
# where 'self' is an AbstractUser instance.
Parameters
----------
pages: Sequence[:class:`int`]
Pages to look at, represented as a finite sequence, so iterations can be performed.
Returns
-------
List[:class:`.Level`]
List of levels found. Can be an empty list.
"""
filters = Filters.setup_by_user()
return await self.client.search_levels(pages=pages, filters=filters, user=self)
[docs]class LevelRecord(AbstractUser):
"""Class that represents Geometry Dash User's Level Record.
This class is derived from :class:`.AbstractUser`.
"""
def __repr__(self) -> str:
info = self.options.copy()
info.pop("client", None)
return make_repr(self, info)
@classmethod
def from_data(
cls, data: ExtDict, strategy: LevelLeaderboardStrategy, client: Client
) -> LevelRecord:
return cls(
account_id=data.getcast(Index.USER_ACCOUNT_ID, 0, int),
name=data.get(Index.USER_NAME, "unknown"),
id=data.getcast(Index.USER_PLAYER_ID, 0, int),
level_id=data.getcast(Index.USER_LEVEL_ID, 0, int),
place=data.getcast(Index.USER_TOP_PLACE, 0, int),
percentage=data.getcast(Index.USER_PERCENT, 0, int),
coins=data.getcast(Index.USER_SECRET_COINS, 0, int),
timestamp=data.get(Index.USER_RECORD_TIMESTAMP, "unknown"),
type=strategy,
client=client,
)
[docs] async def update(self) -> None:
"""|coro|
Update ``self``.
"""
from gd.level import Level # this is a hack because *circular imports*
records = await Level(id=self.level_id, client=self.client).get_leaderboard(self.type.value)
record = get(records, account_id=self.account_id)
if record is not None:
self.options = record.options
@property
def level_id(self) -> int:
""":class:`int`: An integer representing ID of the level the record was retrieved from."""
return self.options.get("level_id", 0)
@property
def percentage(self) -> int:
""":class:`int`: Percentage of the record."""
return self.options.get("percentage", 0)
@property
def coins(self) -> int:
""":class:`int`: Amount of coins collected."""
return self.options.get("coins", 0)
@property
def timestamp(self) -> str:
""":class:`str`: Human-readable string representation of a timestamp."""
return self.options.get("timestamp", "unknown")
@property
def place(self) -> int:
""":class:`int`: User's place in leaderboard. ``0`` if not set."""
return self.options.get("place", 0)
@property
def type(self) -> LevelLeaderboardStrategy:
return LevelLeaderboardStrategy.from_value(self.options.get("type", 0))