diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb9d6685f..4de38d2a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: types: [file, python] args: [--line-length=100] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.4 hooks: - id: flake8 name: flake8 Formatting diff --git a/interactions/api/gateway/client.py b/interactions/api/gateway/client.py index 42e333677..0c557284d 100644 --- a/interactions/api/gateway/client.py +++ b/interactions/api/gateway/client.py @@ -372,13 +372,9 @@ def _dispatch_event(self, event: str, data: dict) -> None: elif data["type"] == InteractionType.MODAL_SUBMIT: _name = f"modal_{_context.data.custom_id}" - if _context.data._json.get("components"): + if _context.data.components: for component in _context.data.components: - if component.get("components"): - __args.append( - [_value["value"] for _value in component["components"]][0] - ) - else: + if component.components: __args.append([_value.value for _value in component.components][0]) self._dispatch.dispatch("on_modal", _context) @@ -417,6 +413,7 @@ def _dispatch_event(self, event: str, data: dict) -> None: "ChannelPins", "MessageReaction", "MessageReactionRemove", + "MessageDelete", # Extend this for everything that should not be cached ]: id = None @@ -466,7 +463,10 @@ def __modify_guild_cache(): self._dispatch.dispatch(f"on_{name}", obj) __modify_guild_cache() - elif "_update" in name and hasattr(obj, "id"): + elif "_update" in name: + self._dispatch.dispatch(f"on_raw_{name}", obj) + if not id: + return old_obj = self._http.cache[model].get(id) if old_obj: before = model(**old_obj._json) @@ -481,7 +481,6 @@ def __modify_guild_cache(): self._dispatch.dispatch( f"on_{name}", before, old_obj ) # give previously stored and new one - return elif "_remove" in name or "_delete" in name: self._dispatch.dispatch(f"on_raw_{name}", obj) @@ -489,6 +488,8 @@ def __modify_guild_cache(): if id: old_obj = _cache.pop(id) self._dispatch.dispatch(f"on_{name}", old_obj) + elif "_delete_bulk" in name: + self._dispatch.dispatch(f"on_{name}", obj) else: self._dispatch.dispatch(f"on_{name}", obj) diff --git a/interactions/api/http/emoji.py b/interactions/api/http/emoji.py index 9621faecd..89cb41274 100644 --- a/interactions/api/http/emoji.py +++ b/interactions/api/http/emoji.py @@ -1,8 +1,9 @@ from typing import List, Optional from ...api.cache import Cache -from ...api.models.guild import Emoji, Guild -from ...api.models.misc import Snowflake +from ..models.emoji import Emoji +from ..models.guild import Guild +from ..models.misc import Snowflake from .request import _Request from .route import Route diff --git a/interactions/api/http/invite.py b/interactions/api/http/invite.py index 2e55f0537..e7706e479 100644 --- a/interactions/api/http/invite.py +++ b/interactions/api/http/invite.py @@ -4,7 +4,7 @@ from .request import _Request from .route import Route -__all__ = ["InviteRequest"] +__all__ = ("InviteRequest",) class InviteRequest: diff --git a/interactions/api/http/member.py b/interactions/api/http/member.py index dca569f1a..493c82613 100644 --- a/interactions/api/http/member.py +++ b/interactions/api/http/member.py @@ -1,9 +1,9 @@ from typing import List, Optional from ...api.cache import Cache -from ...api.models.guild import Guild -from ...api.models.member import Member -from ...api.models.misc import Snowflake +from ..models.guild import Guild +from ..models.member import Member +from ..models.misc import Snowflake from .request import _Request from .route import Route diff --git a/interactions/api/http/request.py b/interactions/api/http/request.py index e27694cc1..8552ea4ee 100644 --- a/interactions/api/http/request.py +++ b/interactions/api/http/request.py @@ -163,7 +163,8 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: self.buckets[route.endpoint] = _bucket # real-time replacement/update/add if needed. if isinstance(data, dict) and ( - data.get("errors") or (data.get("code") and data.get("code") != 429) + data.get("errors") + or ((code := data.get("code")) and code != 429 and data.get("message")) ): log.debug( f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}" diff --git a/interactions/api/models/__init__.py b/interactions/api/models/__init__.py index 3215ed977..72dcb63c7 100644 --- a/interactions/api/models/__init__.py +++ b/interactions/api/models/__init__.py @@ -8,6 +8,7 @@ from .attrs_utils import * # noqa: F401 F403 from .audit_log import * # noqa: F401 F403 from .channel import * # noqa: F401 F403 +from .emoji import * # noqa: F401 F403 from .flags import * # noqa: F401 F403 from .guild import * # noqa: F401 F403 from .gw import * # noqa: F401 F403 diff --git a/interactions/api/models/attrs_utils.py b/interactions/api/models/attrs_utils.py index de6b72896..5f241b647 100644 --- a/interactions/api/models/attrs_utils.py +++ b/interactions/api/models/attrs_utils.py @@ -47,12 +47,30 @@ def __init__(self, kwargs_dict: dict = None, /, **other_kwargs): discord_name = attrib_name if (value := kwargs.pop(discord_name, MISSING)) is not MISSING: - if value is not None and attrib.metadata.get("add_client"): + if ( + value is not None + and attrib.metadata.get("add_client") + and client is not None + ): if isinstance(value, list): for item in value: - item["_client"] = client + if isinstance(item, dict): + item["_client"] = client + elif isinstance(item, DictSerializerMixin): + item._client = client else: - value["_client"] = client + if isinstance(value, dict): + value["_client"] = client + elif isinstance(value, DictSerializerMixin): + value._client = client + + # make sure json is recursively handled + if isinstance(value, list): + self._json[attrib_name] = [ + i._json if isinstance(i, DictSerializerMixin) else i for i in value + ] + elif isinstance(value, DictSerializerMixin): + self._json[attrib_name] = value._json # type: ignore passed_kwargs[attrib_name] = value diff --git a/interactions/api/models/channel.py b/interactions/api/models/channel.py index e13210b62..e943a7c92 100644 --- a/interactions/api/models/channel.py +++ b/interactions/api/models/channel.py @@ -11,6 +11,7 @@ define, field, ) +from .flags import Permissions from .misc import File, IDMixin, Overwrite, Snowflake from .user import User from .webhook import Webhook @@ -363,11 +364,11 @@ async def modify( ) _nsfw = self.nsfw if nsfw is MISSING else nsfw _permission_overwrites = ( - [overwrite._json for overwrite in self.permission_overwrites] + [overwrite._json for overwrite in permission_overwrites] + if permission_overwrites is not MISSING + else [overwrite._json for overwrite in self.permission_overwrites] if self.permission_overwrites else None - if permission_overwrites is MISSING - else [overwrite._json for overwrite in permission_overwrites] ) _type = self.type @@ -1207,6 +1208,67 @@ async def join(self) -> None: await self._client.join_thread(int(self.id)) + async def get_permissions_for(self, member: "Member") -> Permissions: + """ + Returns the permissions of the member in this specific channel. + + .. note:: + The permissions returned by this function take into account role and + user overwrites that can be assigned to channels or categories. If you + don't need these overwrites, look into :meth:`.Member.get_guild_permissions`. + + :param member: The member to get the permissions from + :type member: Member + :return: Permissions of the member in this channel + :rtype: Permissions + """ + if not self.guild_id: + return Permissions.DEFAULT + + from .guild import Guild + + guild = Guild(**await self._client.get_guild(int(self.guild_id)), _client=self._client) + + permissions = await member.get_guild_permissions(guild) + + if permissions & Permissions.ADMINISTRATOR == Permissions.ADMINISTRATOR: + return Permissions.ALL + + # @everyone role overwrites + from ...client.models.utils import search_iterable + + overwrite_everyone = search_iterable( + self.permission_overwrites, lambda overwrite: int(overwrite.id) == int(self.guild_id) + ) + if overwrite_everyone: + permissions &= ~int(overwrite_everyone[0].deny) + permissions |= int(overwrite_everyone[0].allow) + + # Apply role specific overwrites + allow, deny = 0, 0 + for role_id in member.roles: + overwrite_role = search_iterable( + self.permission_overwrites, lambda overwrite: int(overwrite.id) == int(role_id) + ) + if overwrite_role: + allow |= int(overwrite_role[0].allow) + deny |= int(overwrite_role[0].deny) + + if deny: + permissions &= ~deny + if allow: + permissions |= allow + + # Apply member specific overwrites + overwrite_member = search_iterable( + self.permission_overwrites, lambda overwrite: int(overwrite.id) == int(member.id) + ) + if overwrite_member: + permissions &= ~int(overwrite_member[0].deny) + permissions |= int(overwrite_member[0].allow) + + return Permissions(permissions) + @define() class Thread(Channel): diff --git a/interactions/api/models/emoji.py b/interactions/api/models/emoji.py new file mode 100644 index 000000000..625d9d9c3 --- /dev/null +++ b/interactions/api/models/emoji.py @@ -0,0 +1,125 @@ +from typing import TYPE_CHECKING, List, Optional, Union + +from ..error import LibraryException +from .attrs_utils import ClientSerializerMixin, convert_list, define, field +from .misc import Snowflake +from .user import User + +if TYPE_CHECKING: + from ..http import HTTPClient + from .guild import Guild + +__all__ = ("Emoji",) + + +@define() +class Emoji(ClientSerializerMixin): + """ + A class objecting representing an emoji. + + :ivar Optional[Snowflake] id?: Emoji id + :ivar Optional[str] name?: Emoji name. + :ivar Optional[List[int]] roles?: Roles allowed to use this emoji + :ivar Optional[User] user?: User that created this emoji + :ivar Optional[bool] require_colons?: Status denoting of this emoji must be wrapped in colons + :ivar Optional[bool] managed?: Status denoting if this emoji is managed (by an integration) + :ivar Optional[bool] animated?: Status denoting if this emoji is animated + :ivar Optional[bool] available?: Status denoting if this emoji can be used. (Can be false via server boosting) + """ + + id: Optional[Snowflake] = field(converter=Snowflake, default=None) + name: Optional[str] = field(default=None) + roles: Optional[List[int]] = field(converter=convert_list(int), default=None) + user: Optional[User] = field(converter=User, default=None) + require_colons: Optional[bool] = field(default=None) + managed: Optional[bool] = field(default=None) + animated: Optional[bool] = field(default=None) + available: Optional[bool] = field(default=None) + + def __str__(self): + return ( + f"<{'a' if self.animated else ''}:{self.name}:{self.id}>" + if self.id is not None + else self.name + ) + + @classmethod + async def get( + cls, + guild_id: Union[int, Snowflake, "Guild"], + emoji_id: Union[int, Snowflake], + client: "HTTPClient", + ) -> "Emoji": + """ + Gets an emoji. + + :param guild_id: The id of the guild of the emoji + :type guild_id: Union[int, Snowflake, "Guild"] + :param emoji_id: The id of the emoji + :type emoji_id: Union[int, Snowflake] + :param client: The HTTPClient of your bot. Equals to ``bot._http`` + :type client: HTTPClient + :return: The Emoji as object + :rtype: Emoji + """ + + _guild_id = int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) + + res = await client.get_guild_emoji(guild_id=_guild_id, emoji_id=int(emoji_id)) + return cls(**res, _client=client) + + @classmethod + async def get_all_of_guild( + cls, + guild_id: Union[int, Snowflake, "Guild"], + client: "HTTPClient", + ) -> List["Emoji"]: + """ + Gets all emoji of a guild. + + :param guild_id: The id of the guild to get the emojis of + :type guild_id: Union[int, Snowflake, "Guild"] + :param client: The HTTPClient of your bot. Equals to ``bot._http`` + :type client: HTTPClient + :return: The Emoji as list + :rtype: List[Emoji] + """ + + _guild_id = int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) + + res = await client.get_all_emoji(guild_id=_guild_id) + return [cls(**emoji, _client=client) for emoji in res] + + async def delete( + self, + guild_id: Union[int, Snowflake, "Guild"], + reason: Optional[str] = None, + ) -> None: + """ + Deletes the emoji. + + :param guild_id: The guild id to delete the emoji from + :type guild_id: Union[int, Snowflake, "Guild"] + :param reason?: The reason of the deletion + :type reason?: Optional[str] + """ + if not self._client: + raise LibraryException(code=13) + + _guild_id = int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) + + return await self._client.delete_guild_emoji( + guild_id=_guild_id, emoji_id=int(self.id), reason=reason + ) + + @property + def url(self) -> str: + """ + Returns the emoji's URL. + + :return: URL of the emoji + :rtype: str + """ + url = f"https://cdn.discordapp.com/emojis/{self.id}" + url += ".gif" if self.animated else ".png" + return url diff --git a/interactions/api/models/flags.py b/interactions/api/models/flags.py index 458723eea..577e7a651 100644 --- a/interactions/api/models/flags.py +++ b/interactions/api/models/flags.py @@ -93,6 +93,53 @@ class Permissions(IntFlag): START_EMBEDDED_ACTIVITIES = 1 << 39 MODERATE_MEMBERS = 1 << 40 + DEFAULT = ( + ADD_REACTIONS + | VIEW_CHANNEL + | SEND_MESSAGES + | EMBED_LINKS + | ATTACH_FILES + | READ_MESSAGE_HISTORY + | MENTION_EVERYONE + | USE_EXTERNAL_EMOJIS + ) + ALL = ( + DEFAULT + | CREATE_INSTANT_INVITE + | KICK_MEMBERS + | BAN_MEMBERS + | ADMINISTRATOR + | MANAGE_CHANNELS + | MANAGE_GUILD + | VIEW_AUDIT_LOG + | PRIORITY_SPEAKER + | STREAM + | SEND_TTS_MESSAGES + | MANAGE_MESSAGES + | VIEW_GUILD_INSIGHTS + | CONNECT + | SPEAK + | MUTE_MEMBERS + | DEAFEN_MEMBERS + | MOVE_MEMBERS + | USE_VAD + | CHANGE_NICKNAME + | MANAGE_NICKNAMES + | MANAGE_ROLES + | MANAGE_WEBHOOKS + | MANAGE_EMOJIS_AND_STICKERS + | USE_APPLICATION_COMMANDS + | REQUEST_TO_SPEAK + | MANAGE_EVENTS + | MANAGE_THREADS + | CREATE_PUBLIC_THREADS + | CREATE_PRIVATE_THREADS + | USE_EXTERNAL_STICKERS + | SEND_MESSAGES_IN_THREADS + | START_EMBEDDED_ACTIVITIES + | MODERATE_MEMBERS + ) + class UserFlags(IntFlag): """An integer flag bitshift object representing the different user flags given by Discord.""" diff --git a/interactions/api/models/guild.py b/interactions/api/models/guild.py index d6f167465..856b77bed 100644 --- a/interactions/api/models/guild.py +++ b/interactions/api/models/guild.py @@ -13,8 +13,9 @@ ) from .audit_log import AuditLogEvents, AuditLogs from .channel import Channel, ChannelType, Thread, ThreadMember +from .emoji import Emoji from .member import Member -from .message import Emoji, Sticker +from .message import Sticker from .misc import ( AutoModAction, AutoModTriggerMetadata, diff --git a/interactions/api/models/gw.py b/interactions/api/models/gw.py index 212630d65..61c09f1e3 100644 --- a/interactions/api/models/gw.py +++ b/interactions/api/models/gw.py @@ -12,9 +12,10 @@ field, ) from .channel import Channel, ThreadMember +from .emoji import Emoji from .guild import EventMetadata from .member import Member -from .message import Embed, Emoji, Message, MessageInteraction, Sticker +from .message import Embed, Message, MessageInteraction, Sticker from .misc import ( AutoModAction, AutoModTriggerMetadata, @@ -38,6 +39,7 @@ "ChannelPins", "ThreadMembers", "ThreadList", + "MessageDelete", "MessageReactionRemove", "MessageReaction", "GuildIntegrations", @@ -341,7 +343,7 @@ async def kick( async def add_role( self, role: Union[Role, int], - reason: Optional[str], + reason: Optional[str] = None, ) -> None: """ This method adds a role to a member. @@ -371,7 +373,7 @@ async def add_role( async def remove_role( self, role: Union[Role, int], - reason: Optional[str], + reason: Optional[str] = None, ) -> None: """ This method removes a role from a member. @@ -760,7 +762,22 @@ class Presence(ClientSerializerMixin): @define() -class MessageReaction(DictSerializerMixin): +class MessageDelete(DictSerializerMixin): + """ + A class object representing the gateway event ``MESSAGE_DELETE_BULK``. + + :ivar List[Snowflake] ids: The message IDs of the event. + :ivar Snowflake channel_id: The channel ID of the event. + :ivar Optional[Snowflake] guild_id?: The guild ID of the event. + """ + + ids: List[Snowflake] = field(converter=convert_list(Snowflake)) + channel_id: Snowflake = field(converter=Snowflake) + guild_id: Optional[Snowflake] = field(converter=Snowflake, default=None) + + +@define() +class MessageReaction(ClientSerializerMixin): """ A class object representing the gateway event ``MESSAGE_REACTION_ADD`` and ``MESSAGE_REACTION_REMOVE``. @@ -776,7 +793,7 @@ class MessageReaction(DictSerializerMixin): channel_id: Snowflake = field(converter=Snowflake) message_id: Snowflake = field(converter=Snowflake) guild_id: Optional[Snowflake] = field(converter=Snowflake, default=None) - member: Optional[Member] = field(converter=Member, default=None) + member: Optional[Member] = field(converter=Member, default=None, add_client=True) emoji: Optional[Emoji] = field(converter=Emoji, default=None) diff --git a/interactions/api/models/member.py b/interactions/api/models/member.py index fc23af51c..99bf4b22c 100644 --- a/interactions/api/models/member.py +++ b/interactions/api/models/member.py @@ -380,6 +380,7 @@ async def add_to_thread( def get_avatar_url(self, guild_id: Union[int, Snowflake, "Guild"]) -> Optional[str]: """ Returns the URL of the member's avatar for the specified guild. + :param guild_id: The id of the guild to get the member's avatar from :type guild_id: Union[int, Snowflake, "Guild"] :return: URL of the members's avatar (None will be returned if no avatar is set) @@ -393,3 +394,32 @@ def get_avatar_url(self, guild_id: Union[int, Snowflake, "Guild"]) -> Optional[s url = f"https://cdn.discordapp.com/guilds/{_guild_id}/users/{int(self.user.id)}/avatars/{self.avatar}" url += ".gif" if self.avatar.startswith("a_") else ".png" return url + + async def get_guild_permissions(self, guild: "Guild") -> Permissions: + """ + Returns the permissions of the member for the specified guild. + + .. note:: + The permissions returned by this function will not take into account role and + user overwrites that can be assigned to channels or categories. If you need + these overwrites, look into :meth:`.Channel.get_permissions_for`. + + :param guild: The guild of the member + :type guild: Guild + :return: Base permissions of the member in the specified guild + :rtype: Permissions + """ + if int(guild.owner_id) == int(self.id): + return Permissions.ALL + + role_everyone = await guild.get_role(int(guild.id)) + permissions = int(role_everyone.permissions) + + for role_id in self.roles: + role = await guild.get_role(role_id) + permissions |= int(role.permissions) + + if permissions & Permissions.ADMINISTRATOR == Permissions.ADMINISTRATOR: + return Permissions.ALL + + return Permissions(permissions) diff --git a/interactions/api/models/message.py b/interactions/api/models/message.py index 0b3849a5e..ad10e27d1 100644 --- a/interactions/api/models/message.py +++ b/interactions/api/models/message.py @@ -3,6 +3,7 @@ from enum import IntEnum from typing import TYPE_CHECKING, List, Optional, Union +from ...client.models.component import ActionRow, Button, SelectMenu from ..error import LibraryException from .attrs_utils import ( MISSING, @@ -15,16 +16,14 @@ field, ) from .channel import Channel +from .emoji import Emoji from .member import Member from .misc import File, IDMixin, Snowflake -from .role import Role from .team import Application from .user import User if TYPE_CHECKING: - from ...client.models.component import ActionRow, Button, Component, SelectMenu from ..http import HTTPClient - from .guild import Guild __all__ = ( "MessageType", @@ -39,7 +38,6 @@ "EmbedImageStruct", "EmbedField", "Attachment", - "Emoji", "EmbedFooter", "ReactionObject", "PartialSticker", @@ -188,112 +186,6 @@ class ChannelMention(DictSerializerMixin, IDMixin): name: str = field() -@define() -class Emoji(ClientSerializerMixin): - """ - A class objecting representing an emoji. - - :ivar Optional[Snowflake] id?: Emoji id - :ivar Optional[str] name?: Emoji name. - :ivar Optional[List[Role]] roles?: Roles allowed to use this emoji - :ivar Optional[User] user?: User that created this emoji - :ivar Optional[bool] require_colons?: Status denoting of this emoji must be wrapped in colons - :ivar Optional[bool] managed?: Status denoting if this emoji is managed (by an integration) - :ivar Optional[bool] animated?: Status denoting if this emoji is animated - :ivar Optional[bool] available?: Status denoting if this emoji can be used. (Can be false via server boosting) - """ - - id: Optional[Snowflake] = field(converter=Snowflake, default=None) - name: Optional[str] = field(default=None) - roles: Optional[List[Role]] = field(converter=convert_list(Role), default=None) - user: Optional[User] = field(converter=User, default=None) - require_colons: Optional[bool] = field(default=None) - managed: Optional[bool] = field(default=None) - animated: Optional[bool] = field(default=None) - available: Optional[bool] = field(default=None) - - @classmethod - async def get( - cls, - guild_id: Union[int, Snowflake, "Guild"], - emoji_id: Union[int, Snowflake], - client: "HTTPClient", - ) -> "Emoji": - """ - Gets an emoji. - - :param guild_id: The id of the guild of the emoji - :type guild_id: Union[int, Snowflake, "Guild"] - :param emoji_id: The id of the emoji - :type emoji_id: Union[int, Snowflake] - :param client: The HTTPClient of your bot. Equals to ``bot._http`` - :type client: HTTPClient - :return: The Emoji as object - :rtype: Emoji - """ - - _guild_id = int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) - - res = await client.get_guild_emoji(guild_id=_guild_id, emoji_id=int(emoji_id)) - return cls(**res, _client=client) - - @classmethod - async def get_all_of_guild( - cls, - guild_id: Union[int, Snowflake, "Guild"], - client: "HTTPClient", - ) -> List["Emoji"]: - """ - Gets all emoji of a guild. - - :param guild_id: The id of the guild to get the emojis of - :type guild_id: Union[int, Snowflake, "Guild"] - :param client: The HTTPClient of your bot. Equals to ``bot._http`` - :type client: HTTPClient - :return: The Emoji as list - :rtype: List[Emoji] - """ - - _guild_id = int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) - - res = await client.get_all_emoji(guild_id=_guild_id) - return [cls(**emoji, _client=client) for emoji in res] - - async def delete( - self, - guild_id: Union[int, Snowflake, "Guild"], - reason: Optional[str] = None, - ) -> None: - """ - Deletes the emoji. - - :param guild_id: The guild id to delete the emoji from - :type guild_id: Union[int, Snowflake, "Guild"] - :param reason?: The reason of the deletion - :type reason?: Optional[str] - """ - if not self._client: - raise LibraryException(code=13) - - _guild_id = int(guild_id) if isinstance(guild_id, (int, Snowflake)) else int(guild_id.id) - - return await self._client.delete_guild_emoji( - guild_id=_guild_id, emoji_id=int(self.id), reason=reason - ) - - @property - def url(self) -> str: - """ - Returns the emoji's URL. - - :return: URL of the emoji - :rtype: str - """ - url = f"https://cdn.discordapp.com/emojis/{self.id}" - url += ".gif" if self.animated else ".png" - return url - - @define() class EmbedImageStruct(DictSerializerMixin): """ @@ -839,7 +731,7 @@ class Message(ClientSerializerMixin, IDMixin): :ivar int flags: Message flags :ivar Optional[MessageInteraction] interaction?: Message interaction object, if the message is sent by an interaction. :ivar Optional[Channel] thread?: The thread that started from this message, if any, with a thread member object embedded. - :ivar Optional[Union[Component, List[Component]]] components?: Components associated with this message, if any. + :ivar Optional[List[ActionRow]] components?: Array of Action Rows associated with this message, if any. :ivar Optional[List[PartialSticker]] sticker_items?: An array of message sticker item objects, if sent with them. :ivar Optional[List[Sticker]] stickers?: Array of sticker objects sent with the message if any. Deprecated. :ivar Optional[int] position?: The approximate position of the message in a thread. @@ -883,7 +775,7 @@ class Message(ClientSerializerMixin, IDMixin): ) thread: Optional[Channel] = field(converter=Channel, default=None, add_client=True) - components: Optional[Union["Component", List["Component"]]] = field(default=None) + components: Optional[List["ActionRow"]] = field(converter=convert_list(ActionRow), default=None) sticker_items: Optional[List[PartialSticker]] = field( converter=convert_list(PartialSticker), default=None ) @@ -996,7 +888,7 @@ async def edit( _attachments = [a._json for a in attachments] if not files or files is MISSING: - _files = self.attachments + _files = [] elif isinstance(files, list): _files = [file._json_payload(id) for id, file in enumerate(files)] else: @@ -1018,7 +910,7 @@ async def edit( if not components: _components = [] elif components is MISSING: - _components = self.components + _components = _build_components(components=self.components) else: _components = _build_components(components=components) @@ -1217,7 +1109,7 @@ async def create_reaction( raise LibraryException(code=13) _emoji = ( - f":{emoji.name.replace(':', '')}:{emoji.id or ''}" + (f":{emoji.name.replace(':', '')}:{emoji.id or ''}" if emoji.id else emoji.name) if isinstance(emoji, Emoji) else emoji ) @@ -1251,7 +1143,7 @@ async def remove_all_reactions_of( raise LibraryException(code=13) _emoji = ( - f":{emoji.name.replace(':', '')}:{emoji.id or ''}" + (f":{emoji.name.replace(':', '')}:{emoji.id or ''}" if emoji.id else emoji.name) if isinstance(emoji, Emoji) else emoji ) @@ -1274,7 +1166,7 @@ async def remove_own_reaction_of( raise LibraryException(code=13) _emoji = ( - f":{emoji.name.replace(':', '')}:{emoji.id or ''}" + (f":{emoji.name.replace(':', '')}:{emoji.id or ''}" if emoji.id else emoji.name) if isinstance(emoji, Emoji) else emoji ) @@ -1295,16 +1187,19 @@ async def remove_reaction_from( :type user: Union[Member, user, int] """ _emoji = ( - f":{emoji.name.replace(':', '')}:{emoji.id or ''}" + (f":{emoji.name.replace(':', '')}:{emoji.id or ''}" if emoji.id else emoji.name) if isinstance(emoji, Emoji) else emoji ) if not self._client: raise LibraryException(code=13) - _user_id = user if isinstance(user, int) else user.id + _user_id = user if isinstance(user, (int, Snowflake)) else user.id return await self._client.remove_user_reaction( - channel_id=int(self.channel_id), message_id=int(self.id), user_id=_user_id, emoji=_emoji + channel_id=int(self.channel_id), + message_id=int(self.id), + user_id=int(_user_id), + emoji=_emoji, ) async def get_users_from_reaction( @@ -1325,7 +1220,7 @@ async def get_users_from_reaction( _all_users: List[User] = [] _emoji = ( - f":{emoji.name.replace(':', '')}:{emoji.id or ''}" + (f":{emoji.name.replace(':', '')}:{emoji.id or ''}" if emoji.id else emoji.name) if isinstance(emoji, Emoji) else emoji ) diff --git a/interactions/api/models/presence.py b/interactions/api/models/presence.py index ad01dde18..9ca3d1fc5 100644 --- a/interactions/api/models/presence.py +++ b/interactions/api/models/presence.py @@ -2,9 +2,9 @@ from enum import IntEnum from typing import Any, List, Optional -from ..models import StatusType -from ..models.message import Emoji from .attrs_utils import DictSerializerMixin, convert_list, define, field +from .emoji import Emoji +from .flags import StatusType from .misc import Snowflake __all__ = ( diff --git a/interactions/base.py b/interactions/base.py index 0acab378d..afc7b9c13 100644 --- a/interactions/base.py +++ b/interactions/base.py @@ -6,7 +6,7 @@ "__authors__", ) -__version__ = "4.3.0-rc.1" +__version__ = "4.3.1" __authors__ = { "current": [ diff --git a/interactions/client/context.py b/interactions/client/context.py index 66e79db75..5244cb917 100644 --- a/interactions/client/context.py +++ b/interactions/client/context.py @@ -255,7 +255,7 @@ async def edit( if self.message.components is not None or components is not MISSING: if components is MISSING: - _components = self.message.components + _components = _build_components(components=self.message.components) elif not components: _components = [] else: diff --git a/interactions/client/get.py b/interactions/client/get.py index 8d756249a..ff64a6fa2 100644 --- a/interactions/client/get.py +++ b/interactions/client/get.py @@ -14,9 +14,10 @@ from ..api.error import LibraryException from ..api.http.client import HTTPClient +from ..api.models.emoji import Emoji from ..api.models.guild import Guild from ..api.models.member import Member -from ..api.models.message import Emoji, Message +from ..api.models.message import Message from ..api.models.misc import Snowflake from ..api.models.role import Role from .bot import Client diff --git a/interactions/client/get.pyi b/interactions/client/get.pyi index e4c34f89c..6ecf191fa 100644 --- a/interactions/client/get.pyi +++ b/interactions/client/get.pyi @@ -7,7 +7,8 @@ from ..api.http.client import HTTPClient from ..api.models.channel import Channel from ..api.models.guild import Guild from ..api.models.member import Member -from ..api.models.message import Emoji, Message, Sticker +from ..api.models.message import Message, Sticker +from ..api.models.emoji import Emoji from ..api.models.role import Role from ..api.models.user import User from ..api.models.webhook import Webhook diff --git a/interactions/client/models/command.py b/interactions/client/models/command.py index 29cff3be2..8f29d7bbb 100644 --- a/interactions/client/models/command.py +++ b/interactions/client/models/command.py @@ -257,7 +257,7 @@ def decorator(coro: Callable[..., Awaitable]) -> Callable[..., Awaitable]: param = parameters[-1 - len(coro._options)] - option_type = kwargs.get("type", param.annotation) + option_type = kwargs.pop("type", param.annotation) name = kwargs.pop("name", param.name) if name != param.name: kwargs["converter"] = param.name diff --git a/interactions/client/models/component.py b/interactions/client/models/component.py index 562313902..2d59978ab 100644 --- a/interactions/client/models/component.py +++ b/interactions/client/models/component.py @@ -3,7 +3,7 @@ from ...api.error import LibraryException from ...api.models.attrs_utils import MISSING, DictSerializerMixin, convert_list, define, field -from ...api.models.message import Emoji +from ...api.models.emoji import Emoji from ..enums import ButtonStyle, ComponentType, TextStyleType __all__ = ( @@ -229,7 +229,6 @@ def __attrs_post_init__(self): class Modal(ComponentMixin): """ A class object representing a modal. - The structure for a modal: :: interactions.Modal( title="Application Form", @@ -266,7 +265,8 @@ class ActionRow(ComponentMixin): An ActionRow may also support only 1 text input component only. - The structure for an action row: :: + The structure for an action row: + ..code-block:: python # "..." represents a component object. # Method 1: interactions.ActionRow(...) @@ -412,6 +412,9 @@ def __check_components(): 11, message="The specified components are invalid and could not be created!" ) + if not components: + return components + _components = __check_action_row() if _components: diff --git a/interactions/client/models/misc.py b/interactions/client/models/misc.py index 40c223c0e..7106e7ec1 100644 --- a/interactions/client/models/misc.py +++ b/interactions/client/models/misc.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional +from typing import Dict, List, Optional from ...api.models.attrs_utils import DictSerializerMixin, convert_dict, convert_list, define, field from ...api.models.channel import Channel @@ -9,6 +9,7 @@ from ...api.models.user import User from ..enums import ApplicationCommandType, ComponentType, InteractionType, PermissionType from ..models.command import Option +from .component import ActionRow __all__ = ("InteractionResolvedData", "InteractionData", "Interaction") @@ -55,6 +56,7 @@ class InteractionData(DictSerializerMixin): :ivar Optional[ComponentType] component_type?: The type of component from the interaction. :ivar Optional[List[str]] values?: The values of the selected options in the interaction. :ivar Optional[str] target_id?: The targeted ID of the interaction. + :ivar Optional[List[ActionRow]] components?: Array of Action Rows in modal. """ id: Snowflake = field(converter=Snowflake, default=None) @@ -68,7 +70,7 @@ class InteractionData(DictSerializerMixin): component_type: Optional[ComponentType] = field(converter=ComponentType, default=None) values: Optional[List[str]] = field(default=None) target_id: Optional[Snowflake] = field(converter=Snowflake, default=None) - components: Any = field(default=None) # todo check this type + components: Optional[List[ActionRow]] = field(converter=convert_list(ActionRow), default=None) @define() diff --git a/interactions/client/models/utils.py b/interactions/client/models/utils.py index ea6f31a9a..81b1d6be1 100644 --- a/interactions/client/models/utils.py +++ b/interactions/client/models/utils.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from ..context import CommandContext -__all__ = ("autodefer", "spread_to_rows") +__all__ = ("autodefer", "spread_to_rows", "search_iterable") _T = TypeVar("_T") @@ -74,7 +74,7 @@ def spread_to_rows( *components: Union[ActionRow, Button, SelectMenu], max_in_row: int = 5 ) -> List[ActionRow]: r""" - A helper function that spreads components into :class:`ActionRow`s. + A helper function that spreads components into :class:`ActionRow` s. Example: