diff --git a/docs/api.rst b/docs/api.rst index 0f7b268..b5f4359 100755 --- a/docs/api.rst +++ b/docs/api.rst @@ -139,6 +139,12 @@ Status .. autonamedtuple:: Status +UserBadges +~~~~~~~~~~~ + +.. autoclass:: UserBadges + :members: + Enums ====== diff --git a/revolt/__init__.py b/revolt/__init__.py index e9fb1a4..68a80d0 100755 --- a/revolt/__init__.py +++ b/revolt/__init__.py @@ -8,6 +8,7 @@ from .enums import (AssetType, ChannelType, PresenceType, RelationshipType, SortType) from .errors import HTTPError, RevoltError, ServerError from .file import File +from .flags import UserBadges from .member import Member from .message import Masquerade, Message, MessageReply from .messageable import Messageable diff --git a/revolt/channel.py b/revolt/channel.py index 90470f2..7dfd6d3 100755 --- a/revolt/channel.py +++ b/revolt/channel.py @@ -69,12 +69,12 @@ class GroupDMChannel(Channel, Messageable): self.owner = state.get_user(data["owner"]) if perms := data.get("permissions"): - self.permissions = ChannelPermissions(perms) + self.permissions = ChannelPermissions._from_value(perms) def _update(self, *, name: Optional[str] = None, recipients: Optional[list[str]] = None): if name: self.name = name - + if recipients: self.recipients = [self.state.get_user(user_id) for user_id in recipients] @@ -93,19 +93,19 @@ class TextChannel(Channel, Messageable): """A text channel""" def __init__(self, data: TextChannelPayload, state: State): super().__init__(data, state) - + self.server_id = data["server"] self.name = data["name"] self.description = data.get("description") - + last_message_id = data.get("last_message") self.last_message_id = last_message_id if perms := data.get("default_permissions"): - self.default_permissions = ChannelPermissions(perms) + self.default_permissions = ChannelPermissions._from_value(perms) if role_perms := data.get("role_permissions"): - self.role_permissions = {role_id: ChannelPermissions(perms) for role_id, perms in role_perms.items()} + self.role_permissions = {role_id: ChannelPermissions._from_value(perms) for role_id, perms in role_perms.items()} def _get_channel_id(self) -> str: return self.id @@ -149,10 +149,10 @@ class VoiceChannel(Channel): self.description = data.get("description") if perms := data.get("default_permissions"): - self.default_permissions = ChannelPermissions(perms) + self.default_permissions = ChannelPermissions._from_value(perms) if role_perms := data.get("role_permissions"): - self.role_permissions = {role_id: ChannelPermissions(perms) for role_id, perms in role_perms.items()} + self.role_permissions = {role_id: ChannelPermissions._from_value(perms) for role_id, perms in role_perms.items()} def _update(self, *, name: Optional[str] = None, description: Optional[str] = None): if name: diff --git a/revolt/flags.py b/revolt/flags.py new file mode 100755 index 0000000..1c79d94 --- /dev/null +++ b/revolt/flags.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from typing import Callable, Iterator, Optional, TypeVar, Union, overload + +__all__ = ("flag_value", "Flags", "UserBadges") + +F_T = TypeVar("F_T", bound="Flags") +F_V = TypeVar("F_V", bound="flag_value") + + +class flag_value: + __slots__ = ("flag", "__doc__") + + def __init__(self, func: Callable[[], int]): + self.flag = func() + self.__doc__ = func.__doc__ + + @overload + def __get__(self: F_V, instance: None, owner: type[F_T]) -> F_V: + ... + + @overload + def __get__(self, instance: F_T, owner: type[F_T]) -> bool: + ... + + def __get__(self: F_V, instance: Optional[F_T], owner: type[F_T]) -> Union[F_V, bool]: + if instance is None: + return self + + return instance._check_flag(self.flag) + + def __set__(self, instance: Flags, value: bool): + instance._set_flag(self.flag, value) + +class Flags: + def __init__(self, **kwargs: bool): + self.value = 0 + + for k, v in kwargs.items(): + setattr(self, k, v) + + @classmethod + def _from_value(cls: type[F_T], value: int) -> F_T: + self = cls.__new__(cls) + self.value = value + return self + + def _check_flag(self, flag: int) -> bool: + return (self.value & flag) == flag + + def _set_flag(self, flag: int, value: bool): + if value: + self.value |= flag + else: + self.value &= ~flag + + def __eq__(self: F_T, other: F_T) -> bool: + return self.value == other.value + + def __ne__(self: F_T, other: F_T) -> bool: + return not self.__eq__(other) + + def __or__(self: F_T, other: F_T) -> F_T: + return self.__class__._from_value(self.value | other.value) + + def __and__(self: F_T, other: F_T) -> F_T: + return self.__class__._from_value(self.value & other.value) + + def __invert__(self: F_T) -> F_T: + return self.__class__._from_value(~self.value) + + def __add__(self: F_T, other: F_T) -> F_T: + return self | other + + def __sub__(self: F_T, other: F_T) -> F_T: + return self & ~other + + def __lt__(self: F_T, other: F_T) -> bool: + return self.value < other.value + + def __gt__(self: F_T, other: F_T) -> bool: + return self.value > other.value + + def __repr__(self): + return f"<{self.__class__.__name__} value={self.value}>" + + def __iter__(self) -> Iterator[tuple[str, bool]]: + for name, value in self.__class__.__dict__.items(): + if isinstance(value, flag_value): + yield name, value.__get__(self, self.__class__) + + def __hash__(self) -> int: + return hash(self.value) + +class UserBadges(Flags): + """Contains all user badges""" + + @flag_value + def developer(): + """:class:`bool` The developer badge.""" + return 1 << 0 + + @flag_value + def translator(): + """:class:`bool` The translator badge.""" + return 1 << 1 + + @flag_value + def supporter(): + """:class:`bool` The supporter badge.""" + return 1 << 2 + + @flag_value + def responsible_disclosure(): + """:class:`bool` The responsible disclosure badge.""" + return 1 << 3 + + @flag_value + def founder(): + """:class:`bool` The founder badge.""" + return 1 << 4 + + @flag_value + def platform_moderation(): + """:class:`bool` The platform moderation badge.""" + return 1 << 5 + + @flag_value + def active_supporter(): + """:class:`bool` The active supporter badge.""" + return 1 << 6 + + @flag_value + def paw(): + """:class:`bool` The paw badge.""" + return 1 << 7 + + @flag_value + def early_adopter(): + """:class:`bool` The early adopter badge.""" + return 1 << 8 + + @flag_value + def reserved_relevant_joke_badge_1(): + """:class:`bool` The reserved relevant joke badge 1 badge.""" + return 1 << 9 diff --git a/revolt/permissions.py b/revolt/permissions.py index 4dc81fa..5c9cb75 100755 --- a/revolt/permissions.py +++ b/revolt/permissions.py @@ -1,15 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from .types import Permission as PermissionTuple +from .flags import Flags, flag_value __all__ = ( "ChannelPermissions", "ServerPermissions" - ) +) # Channel permissions # @@ -37,315 +34,120 @@ __all__ = ( # ChangeAvatar = 0b00000000000000000100000000000000 // 16382 # RemoveAvatars = 0b00000000000000001000000000000000 // 32768 -class _Permission: - - __slots__ = ("_value",) - - def __init__(self, value: int): - self._value = value - - def __or__(self, p: _Permission): - self._value |= p._value - return self - - def __and__(self, p: _Permission): - self._value &= p._value - return self - - def __invert__(self): - self._value = ~self._value - return self - - def __add__(self, p: _Permission): - # For example we have a bit array `0011` representing permissions - # To allow a permission we would convert the bit for that permission from 0 to 1 - # Let's say we want to allow permissions with bit array `0101` - # Our desired result is `0111` which can be achieved by simple OR (A+B) - return self | p - - def __sub__(self, p: _Permission): - # For example we have a bit array `0011` representing permissions - # To deny a permission we would convert the bit for that permission from 1 to 0 - # Let's say we want to deny permissions with bit array `0101` - # Our desired result is `0010` which can be achieved by Selective Clear Algorithm (A.B') - return self & ~p - - def __lt__(self, p: _Permission): - return self._value < p._value - - def __gt__(self, p: _Permission): - return self._value > p._value - - def __eq__(self, p: _Permission): - return self._value == p._value - - def __repr__(self): - return f"{self.__class__.__name__}<{self._value:032b}>" - - def _check(self, bit_pos: int): - # To check if a permission is present we would need to compare the bit for that position in our bit array - # For example if we have permission `0100` and we need to check if it is present in our bit array `0101` - # We need to compare the 3rd bit from the left, so we shift it 2 places (n-1) and perform AND with 1 - # If the result is 1 the bit was 1 and the permission is present - return (self._value >> bit_pos) & 1 == 1 - -class ChannelPermissions(_Permission): +class ChannelPermissions(Flags): """Represents the channel permissions for a role as seen in channel settings.""" - - __slots__ = () - - @property - def value(self) -> int: - return self._value @classmethod def none(cls) -> ChannelPermissions: - return cls(0) + return cls._from_value(0) @classmethod def all(cls) -> ChannelPermissions: - # channel permissions here do not have manage messages permission - return cls(0b00000000000000000000000011111011) + return cls._from_value(0b11111011) @classmethod def view(cls) -> ChannelPermissions: - return cls(0b00000000000000000000000000000001) - - # view permission is always present and therefore is included in every permission below + return cls._from_value(0b1) @classmethod def send_message(cls) -> ChannelPermissions: - return cls(0b00000000000000000000000000000011) + return cls._from_value(0b11) @classmethod def manage_channel(cls) -> ChannelPermissions: - return cls(0b00000000000000000000000000001001) + return cls._from_value(0b1001) @classmethod def voice_call(cls) -> ChannelPermissions: - return cls(0b00000000000000000000000000010001) + return cls._from_value(0b10001) @classmethod def invite_others(cls) -> ChannelPermissions: - return cls(0b00000000000000000000000000100001) + return cls._from_value(0b100001) @classmethod def embed_links(cls) -> ChannelPermissions: - return cls(0b00000000000000000000000001000001) + return cls._from_value(0b1000001) @classmethod def upload_files(cls) -> ChannelPermissions: - return cls(0b00000000000000000000000010000001) + return cls._from_value(0b10000001) - @property - def can_view(self) -> bool: - return self._check(0) + @flag_value + def can_view() -> int: + return 1 << 0 - @property - def can_send_message(self) -> bool: - return self._check(1) + @flag_value + def can_send_message() -> int: + return 1 << 1 - @property - def can_manage_channel(self) -> bool: - return self._check(3) + @flag_value + def can_manage_channel() -> int: + return 1 << 3 - @property - def can_voice_call(self) -> bool: - return self._check(4) + @flag_value + def can_voice_call() -> int: + return 1 << 4 - @property - def can_invite_others(self) -> bool: - return self._check(5) + @flag_value + def can_invite_others() -> int: + return 1 << 5 - @property - def can_embed_links(self) -> bool: - return self._check(6) + @flag_value + def can_embed_links() -> int: + return 1 << 6 - @property - def can_upload_files(self) -> bool: - return self._check(7) + @flag_value + def can_upload_files() -> int: + return 1 << 7 -class ServerPermissions: +class ServerPermissions(Flags): """Represents the server permissions for a role as seen in server settings.""" - - __slots__ = ("_server_perms", "_channel_perms", ) - - def __init__(self, server_perms: int, channel_perms: int) -> None: - self._server_perms = _Permission(server_perms) - self._channel_perms = _Permission(channel_perms) - - def __add__(self, p: ServerPermissions) -> ServerPermissions: - self._server_perms += p._server_perms - self._channel_perms += p._channel_perms - return self - - def __sub__(self, p: ServerPermissions) -> ServerPermissions: - self._server_perms -= p._server_perms - self._channel_perms -= p._channel_perms - return self - - def __lt__(self, p: ServerPermissions) -> bool: - return self._server_perms < p._server_perms and self._channel_perms < p._channel_perms - - def __gt__(self, p: ServerPermissions) -> bool: - return self._server_perms > p._server_perms and self._channel_perms > p._channel_perms - - def __eq__(self, p: ServerPermissions) -> bool: - return self._server_perms == p._server_perms and self._channel_perms == p._channel_perms - - def __repr__(self) -> str: - return f"{self.__class__.__name__}" - - @property - def value(self) -> PermissionTuple: - return self._server_perms._value, self._channel_perms._value @classmethod def none(cls) -> ServerPermissions: - return cls(0, 0) + return cls._from_value(0) @classmethod def all(cls) -> ServerPermissions: - # channel permissions here do not have manage channel permission - return cls(0b00000000000000001111000000111111, 0b00000000000000000000000011110111) + return cls._from_value(0b1111000000111111) - # view server and view channel are always present and therefore are included in every permission below - # server permissions + @flag_value + def view_server() -> int: + return 1 << 0 - @classmethod - def view_server(cls) -> ServerPermissions: - return cls(0b00000000000000000000000000000001, 1) + @flag_value + def manage_roles() -> int: + return 1 << 1 - @classmethod - def manage_roles(cls) -> ServerPermissions: - return cls(0b00000000000000000000000000000011, 1) + @flag_value + def manage_channels() -> int: + return 1 << 2 - @classmethod - def manage_channels(cls) -> ServerPermissions: - return cls(0b00000000000000000000000000000101, 1) + @flag_value + def manage_server() -> int: + return 1 << 3 - @classmethod - def manage_server(cls) -> ServerPermissions: - return cls(0b00000000000000000000000000001001, 1) + @flag_value + def kick_members() -> int: + return 1 << 4 - @classmethod - def kick_members(cls) -> ServerPermissions: - return cls(0b00000000000000000000000000010001, 1) + @flag_value + def ban_members() -> int: + return 1 << 5 - @classmethod - def ban_members(cls) -> ServerPermissions: - return cls(0b00000000000000000000000000100001, 1) + @flag_value + def change_nicknames() -> int: + return 1 << 12 - @classmethod - def change_nicknames(cls) -> ServerPermissions: - return cls(0b00000000000000000001000000000001, 1) + @flag_value + def manage_nicknames() -> int: + return 1 << 13 - @classmethod - def manage_nicknames(cls) -> ServerPermissions: - return cls(0b00000000000000000010000000000001, 1) + @flag_value + def change_avatar() -> int: + return 1 << 14 - @classmethod - def change_avatar(cls) -> ServerPermissions: - return cls(0b00000000000000000100000000000001, 1) - - @classmethod - def remove_avatars(cls) -> ServerPermissions: - return cls(0b00000000000000001000000000000001, 1) - - @property - def can_view_server(self) -> bool: - return self._server_perms._check(0) - - @property - def can_manage_roles(self) -> bool: - return self._server_perms._check(1) - - @property - def can_manage_channels(self) -> bool: - return self._server_perms._check(2) - - @property - def can_manage_server(self) -> bool: - return self._server_perms._check(3) - - @property - def can_kick_members(self) -> bool: - return self._server_perms._check(4) - - @property - def can_ban_members(self) -> bool: - return self._server_perms._check(5) - - @property - def can_change_nicknames(self) -> bool: - return self._server_perms._check(12) - - @property - def can_manage_nicknames(self) -> bool: - return self._server_perms._check(13) - - @property - def can_change_avatar(self) -> bool: - return self._server_perms._check(14) - - @property - def can_remove_avatars(self) -> bool: - return self._server_perms._check(15) - - # channel permissions - - @classmethod - def view_channels(cls) -> ServerPermissions: - return cls(1, 0b00000000000000000000000000000001) - - @classmethod - def send_message(cls) -> ServerPermissions: - return cls(1, 0b00000000000000000000000000000011) - - @classmethod - def manage_messages(cls) -> ServerPermissions: - return cls(1, 0b00000000000000000000000000000101) - - @classmethod - def voice_call(cls) -> ServerPermissions: - return cls(1, 0b00000000000000000000000000010001) - - @classmethod - def invite_others(cls) -> ServerPermissions: - return cls(1, 0b00000000000000000000000000100001) - - @classmethod - def embed_links(cls) -> ServerPermissions: - return cls(1, 0b00000000000000000000000001000001) - - @classmethod - def upload_files(cls) -> ServerPermissions: - return cls(1, 0b00000000000000000000000010000001) - - @property - def can_view_channel(self) -> bool: - return self._channel_perms._check(0) - - @property - def can_send_message(self) -> bool: - return self._channel_perms._check(1) - - @property - def can_manage_messages(self) -> bool: - return self._channel_perms._check(2) - - @property - def can_voice_call(self) -> bool: - return self._channel_perms._check(4) - - @property - def can_invite_others(self) -> bool: - return self._channel_perms._check(5) - - @property - def can_embed_links(self) -> bool: - return self._channel_perms._check(6) - - @property - def can_upload_files(self) -> bool: - return self._channel_perms._check(7) + @flag_value + def remove_avatars() -> int: + return 1 << 15 diff --git a/revolt/role.py b/revolt/role.py index 5d15a26..b865744 100755 --- a/revolt/role.py +++ b/revolt/role.py @@ -2,7 +2,9 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from .permissions import ServerPermissions +from revolt.types import server + +from .permissions import ChannelPermissions, ServerPermissions if TYPE_CHECKING: from .server import Server @@ -14,7 +16,7 @@ __all__ = ("Role",) class Role: """Represents a role - + Attributes ----------- id: :class:`str` @@ -29,11 +31,13 @@ class Role: The position of the role in the role heirarchy server: :class:`Server` The server the role belongs to - permissions: :class:`ServerPermissions` + server_permissions: :class:`ServerPermissions` The server permissions for the role + channel_permissions: :class:`ChannelPermissions` + The channel permissions for the role """ - __slots__ = ("id", "name", "colour", "hoist", "rank", "state", "server", "permissions") - + __slots__ = ("id", "name", "colour", "hoist", "rank", "state", "server", "server_permissions", "channel_permissions") + def __init__(self, data: RolePayload, role_id: str, state: State, server: Server): self.state = state self.id = role_id @@ -42,20 +46,30 @@ class Role: self.hoist = data.get("hoist", False) self.rank = data.get("rank", 0) self.server = server - self.permissions = ServerPermissions(*data.get("permissions")) + self.server_permissions = ServerPermissions._from_value(data["permissions"][0]) + self.channel_permissions = ChannelPermissions._from_value(data["permissions"][1]) @property def color(self): return self.colour - async def set_permissions(self, permissions: ServerPermissions) -> None: + async def set_permissions(self, *, server_permissions: Optional[ServerPermissions] = None, channel_permissions: Optional[ChannelPermissions] = None) -> None: """Sets the permissions for a role in a server. Parameters ----------- - permissions: :class:`ServerPermissions` - The new permissions for the role + server_permissions: Optional[:class:`ServerPermissions`] + The new server permissions for the role + channel_permissions: Optional[:class:`ChannelPermissions`] + The new channel permissions for the role """ - await self.state.http.set_role_permissions(self.server.id, self.id, *permissions.value) + + if not server_permissions and not channel_permissions: + return + + server_value = (server_permissions or self.server_permissions).value + channel_value = (channel_permissions or self.channel_permissions).value + + await self.state.http.set_role_permissions(self.server.id, self.id, server_value, channel_value) def _update(self, *, name: Optional[str] = None, colour: Optional[str] = None, hoist: Optional[bool] = None, rank: Optional[int] = None): if name: diff --git a/revolt/server.py b/revolt/server.py index 89bbea0..bc96850 100755 --- a/revolt/server.py +++ b/revolt/server.py @@ -1,11 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Union, cast +from typing import TYPE_CHECKING, Optional from .asset import Asset from .category import Category from .channel import Channel -from .permissions import ServerPermissions +from .permissions import ChannelPermissions, ServerPermissions from .role import Role if TYPE_CHECKING: @@ -89,14 +89,15 @@ class Server: banner: Optional[:class:`Asset`] The servers banner """ - __slots__ = ("state", "id", "name", "owner_id", "default_permissions", "_members", "_roles", "_channels", "description", "icon", "banner", "nsfw", "system_messages", "categories") + __slots__ = ("state", "id", "name", "owner_id", "default_server_permissions", "default_channel_permissions", "_members", "_roles", "_channels", "description", "icon", "banner", "nsfw", "system_messages", "categories") def __init__(self, data: ServerPayload, state: State): self.state = state self.id = data["_id"] self.name = data["name"] self.owner_id = data["owner"] - self.default_permissions = ServerPermissions(*data["default_permissions"]) + self.default_server_permissions = ServerPermissions._from_value(data["default_permissions"][0]) + self.default_channel_permissions = ChannelPermissions._from_value(data["default_permissions"][1]) self.description = data.get("description") or None self.nsfw = data.get("nsfw", False) self.system_messages = SystemMessages(data.get("system_messages", {}), state) @@ -130,7 +131,8 @@ class Server: if banner: self.banner = Asset(banner, self.state) if default_permissions: - self.default_permissions = ServerPermissions(*default_permissions) + self.default_server_permissions = ServerPermissions._from_value(default_permissions[0]) + self.default_channel_permissions = ChannelPermissions._from_value(default_permissions[1]) if nsfw is not None: self.nsfw = nsfw if system_messages is not None: @@ -203,11 +205,16 @@ class Server: """:class:`Member` The owner of the server""" return self.get_member(self.owner_id) - async def set_default_permissions(self, permissions: ServerPermissions) -> None: + async def set_default_permissions(self, *, server_permissions: Optional[ServerPermissions] = None, channel_permissions: Optional[ChannelPermissions] = None) -> None: """Sets the default server permissions. Parameters ----------- - permissions: :class:`ServerPermissions` + server_permissions: Optional[:class:`ServerPermissions`] The new default server permissions + channel_permissions: Optional[:class:`ChannelPermissions`] + the new default channel permissions """ - await self.state.http.set_default_permissions(self.id, *permissions.value) + server_value = (server_permissions or self.default_server_permissions).value + channel_value = (channel_permissions or self.default_channel_permissions).value + + await self.state.http.set_default_permissions(self.id, server_value, channel_value) diff --git a/revolt/user.py b/revolt/user.py index 0223533..65b4c80 100755 --- a/revolt/user.py +++ b/revolt/user.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, NamedTuple, Optional, Union from .asset import Asset, PartialAsset from .enums import PresenceType, RelationshipType +from .flags import UserBadges if TYPE_CHECKING: from .state import State @@ -40,7 +41,7 @@ class User: Whether or not the user is a bot owner: Optional[:class:`User`] The bot's owner if the user is a bot - badges: :class:`int` + badges: :class:`UserBadges` The users badges online: :class:`bool` Whether or not the user is online @@ -69,7 +70,7 @@ class User: self.bot = False self.owner_id = None - self.badges = data.get("badges", 0) + self.badges = UserBadges._from_value(data.get("badges", 0)) self.online = data.get("online", False) self.flags = data.get("flags", 0)