diff --git a/interactions/api/error.py b/interactions/api/error.py index 1effac58d..2b26d0d7c 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -72,6 +72,7 @@ def log(self, message: str, *args): @staticmethod def lookup(code: int) -> str: + # https://discord.com/developers/docs/topics/opcodes-and-status-codes#json return { # Default error integer enum 0: "Unknown error", @@ -197,6 +198,7 @@ def lookup(code: int) -> str: 30047: "Maximum number of pinned threads in a forum channel has been reached", 30048: "Maximum number of tags in a forum channel has been reached", 30052: "Bitrate is too high for channel of this type", + 31001: "Undocumented error/rate-limit related.", 40001: "Unauthorized. Provide a valid token and try again", 40002: "You need to verify your account in order to perform this action", 40003: "You are opening direct messages too fast", diff --git a/interactions/api/http/request.py b/interactions/api/http/request.py index 9d79c2b8a..fa6531589 100644 --- a/interactions/api/http/request.py +++ b/interactions/api/http/request.py @@ -171,7 +171,7 @@ 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 (code and code != 429 and message) + data.get("errors") or (code and code not in {429, 31001} and message) ): log.debug( f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}" @@ -189,15 +189,15 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: message=f"'{message}'. Make sure that your token is set properly.", severity=50, ) - if code == 429: + if code in {429, 31001}: hours = int(reset_after // 3600) minutes = int((reset_after % 3600) // 60) seconds = int(reset_after % 60) log.warning( - f"(429) {LibraryException.lookup(429)} Locking down future requests for " - + f"{f'{hours} hours ' if hours else ''}" - + f"{f'{minutes} minutes ' if minutes else ''}" - + f"{f'{seconds} seconds ' if seconds else ''}" + "(429/31001) The Bot has encountered a rate-limit. Resuming future requests after " + f"{f'{hours} hours ' if hours else ''}" + f"{f'{minutes} minutes ' if minutes else ''}" + f"{f'{seconds} seconds ' if seconds else ''}" ) if is_global: self._global_lock.reset_after = reset_after diff --git a/interactions/api/models/channel.py b/interactions/api/models/channel.py index e74971510..eb644d460 100644 --- a/interactions/api/models/channel.py +++ b/interactions/api/models/channel.py @@ -30,7 +30,7 @@ from ...utils.utils import search_iterable from ..error import LibraryException from .emoji import Emoji -from .flags import Permissions +from .flags import MessageFlags, Permissions from .misc import AllowedMentions, File, IDMixin, Overwrite, Snowflake from .role import Role from .user import User @@ -1178,11 +1178,17 @@ async def normal_delete(): before=_before, ) ] + if not messages: + return _all amount -= min(amount, 100) messages2 = messages.copy() for message in messages2: - if message.flags == (1 << 7): + if ( + message.flags & MessageFlags.EPHEMERAL + or message.flags & MessageFlags.LOADING + or not message.deletable + ): messages.remove(message) amount += 1 _before = int(message.id) @@ -1194,14 +1200,17 @@ async def normal_delete(): messages.remove(message) amount += 1 _before = int(message.id) + + for message in messages: # show results faster + await self._client.delete_message( + channel_id=int(self.id), + message_id=int(message.id), + reason=reason, + ) + _all += messages - for message in _all: - await self._client.delete_message( - channel_id=int(self.id), - message_id=int(message.id), - reason=reason, - ) + return _all async def bulk_delete(): nonlocal _before, _all, amount, check, reason @@ -1218,14 +1227,19 @@ async def bulk_delete(): before=_before, ) ] + if not messages: + return _all messages2 = messages.copy() for message in messages2: if datetime.fromisoformat(str(message.timestamp)) < _allowed_time: messages.remove(message) _stop = True - messages2 = messages.copy() - for message in messages2: - if message.flags == (1 << 7): + + elif ( + message.flags & MessageFlags.EPHEMERAL + or message.flags & MessageFlags.LOADING + or not message.deletable + ): messages.remove(message) amount += 1 _before = int(message.id) @@ -1268,15 +1282,19 @@ async def bulk_delete(): before=_before, ) ] + if not messages: + return _all + amount -= amount messages2 = messages.copy() for message in messages2: if datetime.fromisoformat(str(message.timestamp)) < _allowed_time: messages.remove(message) _stop = True - amount -= amount - messages2 = messages.copy() - for message in messages2: - if message.flags == (1 << 7): + elif ( + message.flags & MessageFlags.EPHEMERAL + or message.flags & MessageFlags.LOADING + or not message.deletable + ): messages.remove(message) amount += 1 _before = int(message.id) @@ -1316,10 +1334,16 @@ async def bulk_delete(): before=_before, ) ] + if not messages: + return _all amount -= 1 messages2 = messages.copy() for message in messages2: - if message.flags == (1 << 7): + if ( + message.flags & MessageFlags.EPHEMERAL + or message.flags & MessageFlags.LOADING + or not message.deletable + ): messages.remove(message) amount += 1 _before = int(message.id) @@ -1339,6 +1363,7 @@ async def bulk_delete(): message_id=int(messages[0].id), reason=reason, ) + return _all if bulk: await bulk_delete() diff --git a/interactions/api/models/flags.py b/interactions/api/models/flags.py index 6ebeb38d8..438c8d77f 100644 --- a/interactions/api/models/flags.py +++ b/interactions/api/models/flags.py @@ -1,6 +1,6 @@ from enum import Enum, IntFlag -__all__ = ("Intents", "AppFlags", "StatusType", "UserFlags", "Permissions") +__all__ = ("Intents", "AppFlags", "StatusType", "UserFlags", "Permissions", "MessageFlags") class Intents(IntFlag): @@ -186,3 +186,31 @@ class StatusType(str, Enum): IDLE = "idle" INVISIBLE = "invisible" OFFLINE = "offline" + + +class MessageFlags(IntFlag): + """ + .. versionadded:: 4.4.0 + + An integer flag bitshift object representing the different message flags given by Discord. + + :ivar int CROSSPOSTED: this message has been published to subscribed channels (via Channel Following) + :ivar int IS_CROSSPOST: this message originated from a message in another channel (via Channel Following) + :ivar int SUPPRESS_EMBEDS: do not include any embeds when serializing this message + :ivar int SOURCE_MESSAGE_DELETED: the source message for this crosspost has been deleted (via Channel Following) + :ivar int URGENT: this message came from the urgent message system + :ivar int HAS_THREAD: this message has an associated thread, with the same id as the message + :ivar int EPHEMERAL: this message is only visible to the user who invoked the Interaction + :ivar int LOADING: this message is an Interaction Response and the bot is thinking + :ivar int FAILED_TO_MENTION_SOME_ROLES_IN_THREAD: this message failed to mention some roles and add their members to the thread + """ + + CROSSPOSTED = 1 << 0 + IS_CROSSPOST = 1 << 1 + SUPPRESS_EMBEDS = 1 << 2 + SOURCE_MESSAGE_DELETED = 1 << 3 + URGENT = 1 << 4 + HAS_THREAD = 1 << 5 + EPHEMERAL = 1 << 6 + LOADING = 1 << 7 + FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8 diff --git a/interactions/api/models/message.py b/interactions/api/models/message.py index d9e6b5520..827fbd4dc 100644 --- a/interactions/api/models/message.py +++ b/interactions/api/models/message.py @@ -18,6 +18,7 @@ from ..error import LibraryException from .channel import Channel from .emoji import Emoji +from .flags import MessageFlags from .member import Member from .misc import AllowedMentions, File, IDMixin, Snowflake from .team import Application @@ -75,6 +76,15 @@ class MessageType(IntEnum): CONTEXT_MENU_COMMAND = 23 AUTO_MODERATION_ACTION = 24 + @staticmethod + def not_deletable() -> List[int]: + """ + .. versionadded:: 4.4.0 + + returns A list of message types which are not deletable + """ + return [1, 2, 3, 4, 5, 14, 15, 16, 17, 21] + @define() class MessageActivity(DictSerializerMixin): @@ -692,7 +702,7 @@ class Message(ClientSerializerMixin, IDMixin): application: Optional[Application] = field(converter=Application, default=None) application_id: Optional[Snowflake] = field(converter=Snowflake, default=None) message_reference: Optional[MessageReference] = field(converter=MessageReference, default=None) - flags: int = field(default=None) + flags: Optional[Union[int, MessageFlags]] = field(converter=MessageFlags, default=None) referenced_message: Optional[MessageReference] = field(converter=MessageReference, default=None) interaction: Optional[MessageInteraction] = field( converter=MessageInteraction, default=None, add_client=True, repr=False @@ -708,6 +718,15 @@ class Message(ClientSerializerMixin, IDMixin): ) # deprecated position: Optional[int] = field(default=None, repr=False) + @property + def deletable(self) -> bool: + """ + .. versionadded:: 4.4.0 + + Returns if the message can be deleted or not + """ + return self.type not in self.type.not_deletable() + def __attrs_post_init__(self): if self.member and self.guild_id: self.member._extras["guild_id"] = self.guild_id @@ -793,9 +812,9 @@ async def edit( raise LibraryException(message="You cannot edit a hidden message!", code=12) _flags = self.flags if suppress_embeds is not MISSING and suppress_embeds: - _flags |= 1 << 2 + _flags |= MessageFlags.SUPPRESS_EMBEDS elif suppress_embeds is not MISSING: - _flags &= ~1 << 2 + _flags &= ~MessageFlags.SUPPRESS_EMBEDS from ...client.models.component import _build_components diff --git a/interactions/client/context.py b/interactions/client/context.py index e1923b636..97bb6beb7 100644 --- a/interactions/client/context.py +++ b/interactions/client/context.py @@ -3,7 +3,7 @@ from ..api.error import LibraryException from ..api.models.channel import Channel -from ..api.models.flags import Permissions +from ..api.models.flags import MessageFlags, Permissions from ..api.models.guild import Guild from ..api.models.member import Member from ..api.models.message import Attachment, Embed, File, Message, MessageReference @@ -180,9 +180,9 @@ async def send( else: _components = [] - _flags: int = (1 << 6) if ephemeral else 0 + _flags = MessageFlags.EPHEMERAL if ephemeral else MessageFlags(0) if suppress_embeds: - _flags += 1 << 2 + _flags |= MessageFlags.SUPPRESS_EMBEDS _attachments = [] if attachments is MISSING else [a._json for a in attachments] @@ -204,7 +204,7 @@ async def send( allowed_mentions=_allowed_mentions, components=_components, attachments=_files, - flags=_flags, + flags=_flags.value, ), files, ) @@ -476,7 +476,7 @@ async def defer(self, ephemeral: Optional[bool] = False) -> None: """ if not self.responded: self.deferred = True - _ephemeral: int = (1 << 6) if ephemeral else 0 + _ephemeral: int = MessageFlags.EPHEMERAL.value if ephemeral else 0 self.callback = InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE await self._client.create_interaction_response( @@ -720,7 +720,7 @@ async def defer( if not self.responded: self.deferred = True - _ephemeral: int = (1 << 6) if bool(ephemeral) else 0 + _ephemeral: int = MessageFlags.EPHEMERAL.value if bool(ephemeral) else 0 # ephemeral doesn't change callback typings. just data json if edit_origin: