diff --git a/docs/api.models.audit_log.rst b/docs/api.models.audit_log.rst new file mode 100644 index 000000000..658c4ab7b --- /dev/null +++ b/docs/api.models.audit_log.rst @@ -0,0 +1,7 @@ +.. currentmodule:: interactions + +Audit-Log Models +================ + +.. automodule:: interactions.api.models.audit_log + :members: diff --git a/docs/api.models.rst b/docs/api.models.rst index dc1bcc2cb..b9fd49298 100644 --- a/docs/api.models.rst +++ b/docs/api.models.rst @@ -6,6 +6,7 @@ Model Objects .. toctree:: :maxdepth: 2 + api.models.audit_log.rst api.models.channel.rst api.models.guild.rst api.models.gw.rst diff --git a/interactions/api/models/__init__.py b/interactions/api/models/__init__.py index a5099cb8a..3215ed977 100644 --- a/interactions/api/models/__init__.py +++ b/interactions/api/models/__init__.py @@ -6,6 +6,7 @@ models for dispatched Gateway events. """ from .attrs_utils import * # noqa: F401 F403 +from .audit_log import * # noqa: F401 F403 from .channel import * # noqa: F401 F403 from .flags import * # noqa: F401 F403 from .guild import * # noqa: F401 F403 diff --git a/interactions/api/models/audit_log.py b/interactions/api/models/audit_log.py new file mode 100644 index 000000000..20f01dd78 --- /dev/null +++ b/interactions/api/models/audit_log.py @@ -0,0 +1,277 @@ +from enum import IntEnum +from typing import TYPE_CHECKING, List, Optional, TypeVar + +from .attrs_utils import DictSerializerMixin, convert_list, define, field +from .channel import Channel +from .misc import Snowflake +from .user import User +from .webhook import Webhook + +__all__ = ( + "AuditLogEntry", + "AuditLogEvents", + "AuditLogs", + "AuditLogChange", + "OptionalAuditEntryInfo", +) + +_T = TypeVar("_T") + + +if TYPE_CHECKING: + from .guild import Integration, ScheduledEvents + from .gw import AutoModerationRule + + +class AuditLogEvents(IntEnum): + """ + A class object representing the different types of AuditLogEvents. + + :ivar int GUILD_UPDATE: 1 - Server settings were updated + :ivar int CHANNEL_CREATE: 10 - Channel was created + :ivar int CHANNEL_UPDATE: 11 - Channel settings were updated + :ivar int CHANNEL_DELETE: 12 - Channel was deleted + :ivar int CHANNEL_OVERWRITE_CREATE: 13 - Permission overwrite was added to a channel + :ivar int CHANNEL_OVERWRITE_UPDATE: 14 - Permission overwrite was updated for a channel + :ivar int CHANNEL_OVERWRITE_DELETE: 15 - Permission overwrite was deleted from a channel + :ivar int MEMBER_KICK: 20 - Member was removed from server + :ivar int MEMBER_PRUNE: 21 - Members were pruned from server + :ivar int MEMBER_BAN_ADD: 22 - Member was banned from server + :ivar int MEMBER_BAN_REMOVE: 23 - Server ban was lifted for a member + :ivar int MEMBER_UPDATE: 24 - Member was updated in server + :ivar int MEMBER_ROLE_UPDATE: 25 - Member was added or removed from a role + :ivar int MEMBER_MOVE: 26 - Member was moved to a different voice channel + :ivar int MEMBER_DISCONNECT: 27 - Member was disconnected from a voice channel + :ivar int BOT_ADD: 28 - Bot user was added to server + :ivar int ROLE_CREATE: 30 - Role was created + :ivar int ROLE_UPDATE: 31 - Role was updated + :ivar int ROLE_DELETE: 32 - Role was deleted + :ivar int INVITE_CREATE: 40 - Server invite was created + :ivar int INVITE_UPDATE: 41 - Server invite was updated + :ivar int INVITE_DELETE: 42 - Server invite was deleted + :ivar int WEBHOOK_CREATE: 50 - Webhook was created + :ivar int WEBHOOK_UPDATE: 51 - Webhook properties or channel were updated + :ivar int WEBHOOK_DELETE: 52 - Webhook was deleted + :ivar int EMOJI_CREATE: 60 - Emoji was created + :ivar int EMOJI_UPDATE: 61 - Emoji name was updated + :ivar int EMOJI_DELETE: 62 - Emoji was deleted + :ivar int MESSAGE_DELETE: 72 - Single message was deleted + :ivar int MESSAGE_BULK_DELETE: 73 - Multiple messages were deleted + :ivar int MESSAGE_PIN: 74 - Message was pinned to a channel + :ivar int MESSAGE_UNPIN: 75 - Message was unpinned from a channel + :ivar int INTEGRATION_CREATE: 80 - App was added to server + :ivar int INTEGRATION_UPDATE: 81 - App was updated (as an example, its scopes were updated) + :ivar int INTEGRATION_DELETE: 82 - App was removed from server + :ivar int STAGE_INSTANCE_CREATE: 83 - Stage instance was created (stage channel becomes live) + :ivar int STAGE_INSTANCE_UPDATE: 84 - Stage instance details were updated + :ivar int STAGE_INSTANCE_DELETE: 85 - Stage instance was deleted (stage channel no longer live) + :ivar int STICKER_CREATE: 90 - Sticker was created + :ivar int STICKER_UPDATE: 91 - Sticker details were updated + :ivar int STICKER_DELETE: 92 - Sticker was deleted + :ivar int GUILD_SCHEDULED_EVENT_CREATE: 100 - Event was created + :ivar int GUILD_SCHEDULED_EVENT_UPDATE: 101 - Event was updated + :ivar int GUILD_SCHEDULED_EVENT_DELETE: 102 - Event was cancelled + :ivar int THREAD_CREATE: 110 - Thread was created in a channel + :ivar int THREAD_UPDATE: 111 - Thread was updated + :ivar int THREAD_DELETE: 112 - Thread was deleted + :ivar int APPLICATION_COMMAND_PERMISSION_UPDATE: 121 - Permissions were updated for a command + :ivar int AUTO_MODERATION_RULE_CREATE: 140 - Auto Moderation rule was created + :ivar int AUTO_MODERATION_RULE_UPDATE: 141 - Auto Moderation rule was updated + :ivar int AUTO_MODERATION_RULE_DELETE: 142 - Auto Moderation rule was deleted + :ivar int AUTO_MODERATION_BLOCK_MESSAGE: 143 - Message was blocked by AutoMod (according to a rule) + """ + + # guild related + GUILD_UPDATE = 1 + + # channel related + CHANNEL_CREATE = 10 + CHANNEL_UPDATE = 11 + CHANNEL_DELETE = 12 + CHANNEL_OVERWRITE_CREATE = 13 + CHANNEL_OVERWRITE_UPDATE = 14 + CHANNEL_OVERWRITE_DELETE = 15 + + # member related + MEMBER_KICK = 20 + MEMBER_PRUNE = 21 + MEMBER_BAN_ADD = 22 + MEMBER_BAN_REMOVE = 23 + MEMBER_UPDATE = 24 + MEMBER_ROLE_UPDATE = 25 + MEMBER_MOVE = 26 + MEMBER_DISCONNECT = 27 + BOT_ADD = 28 + + # role related + ROLE_CREATE = 30 + ROLE_UPDATE = 31 + ROLE_DELETE = 32 + + # invite related + INVITE_CREATE = 40 + INVITE_UPDATE = 41 + INVITE_DELETE = 42 + + # webhook related + WEBHOOK_CREATE = 50 + WEBHOOK_UPDATE = 51 + WEBHOOK_DELETE = 52 + + # emoji related + EMOJI_CREATE = 60 + EMOJI_UPDATE = 61 + EMOJI_DELETE = 62 + + # message related + MESSAGE_DELETE = 72 + MESSAGE_BULK_DELETE = 73 + MESSAGE_PIN = 74 + MESSAGE_UNPIN = 75 + + # integration related + INTEGRATION_CREATE = 80 + INTEGRATION_UPDATE = 81 + INTEGRATION_DELETE = 82 + + # stage instance related + STAGE_INSTANCE_CREATE = 83 + STAGE_INSTANCE_UPDATE = 84 + STAGE_INSTANCE_DELETE = 85 + + # sticker related + STICKER_CREATE = 90 + STICKER_UPDATE = 91 + STICKER_DELETE = 92 + + # guild scheduled event related + GUILD_SCHEDULED_EVENT_CREATE = 100 + GUILD_SCHEDULED_EVENT_UPDATE = 101 + GUILD_SCHEDULED_EVENT_DELETE = 102 + + # thread related + THREAD_CREATE = 110 + THREAD_UPDATE = 111 + THREAD_DELETE = 112 + + # app-command permissions related + APPLICATION_COMMAND_PERMISSION_UPDATE = 121 + + # auto mod related + AUTO_MODERATION_RULE_CREATE = 140 + AUTO_MODERATION_RULE_UPDATE = 141 + AUTO_MODERATION_RULE_DELETE = 142 + AUTO_MODERATION_BLOCK_MESSAGE = 143 + + +@define() +class AuditLogChange(DictSerializerMixin): + """ + A class object representing an AuditLogChange. + + :ivar Optional[_T] new_value?: New value of the key + :ivar Optional[_T] old_value?: Old value of the key + :ivar str key: Name of the changed entity, with a few [exceptions](https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-exceptions) + """ + + new_value: Optional[_T] = field(default=None) + old_value: Optional[_T] = field(default=None) + key: str = field() + + +@define() +class OptionalAuditEntryInfo(DictSerializerMixin): + """ + A class object representing OptionalAuditEntryInfo. + + :ivar Snowflake application_id: ID of the app whose permissions were targeted. ``AuditLogEvents``-type: 121 + :ivar Snowflake channel_id: Channel in which the entities were targeted. ``AuditLogEvents``-types: 26, 74, 75, 72, 83, 84, 85 + :ivar str count: Number of entities that were targeted. ``AuditLogEvents``-types: 72, 73, 27, 26 + :ivar str delete_member_days: Number of days after which inactive members were kicked. ``AuditLogEvents``-types: 21 + :ivar Snowflake id: ID of the overwritten entity. ``AuditLogEvents``-types: 13, 14, 15 + :ivar str members_removed: Number of members removed by the prune. ``AuditLogEvents``-types: 21 + :ivar Snowflake message_id: ID of the message that was targeted. ``AuditLogEvents``-types: 74, 75 + :ivar Optional[str] role_name: Name of the role if type is "0" (not present if type is "1"). ``AuditLogEvents``-types: 13, 14, 15 + :ivar str type: Type of overwritten entity - role ("0") or member ("1"). ``AuditLogEvents``-types: 13, 14, 15 + """ + + application_id: Snowflake = field(converter=Snowflake) + channel_id: Snowflake = field(converter=Snowflake) + count: str = field() + delete_member_days: str = field() + id: Snowflake = field(converter=Snowflake) + members_removed: str = field() + message_id: Snowflake = field(converter=Snowflake) + role_name: Optional[str] = field(default=None) + type: str = field() + + +@define() +class AuditLogEntry(DictSerializerMixin): + """ + A class object representing an AuditLogEntry. + + :ivar Optional[str] target_id?: ID of the affected entity (webhook, user, role, etc.) + :ivar Optional[List[AuditLogChange]] changes?: Changes made to the target_id + :ivar Optional[Snowflake] user_id?: User or app that made the changes + :ivar Snowflake id: ID of the entry + :ivar AuditLogEvents action_type: Type of action that occurred + :ivar OptionalAuditEntryInfo options?: Additional info for certain event types + :ivar str reason?: Reason for the change (1-512 characters) + """ + + target_id: Optional[str] = field(default=None) + changes: Optional[List[AuditLogChange]] = field( + converter=convert_list(AuditLogChange), default=None + ) + user_id: Optional[Snowflake] = field(converter=Snowflake, default=None) + id: Snowflake = field(converter=Snowflake) + action_type: AuditLogEvents = field(converter=AuditLogEvents) + options: Optional[OptionalAuditEntryInfo] = field( + converter=OptionalAuditEntryInfo, default=None + ) + reason: Optional[str] = field(default=None) + + +@define() +class AuditLogs(DictSerializerMixin): + """ + A class object representing the audit logs of a guild. + + :ivar List[AuditLogEntry] audit_log_entries: List of audit log entries, sorted from most to least recent. + :ivar List[AutoModerationRule] auto_moderation_rules: List of auto moderation rules referenced in the audit log. + :ivar List[ScheduledEvents] guild_scheduled_events: List of guild scheduled events referenced in the audit log + :ivar List[Integration] integrations: List of partial integration objects + :ivar List[Channel] threads: List of threads referenced in the audit log + :ivar List[User] users: List of users referenced in the audit log + :ivar List[Webhook] webhooks: List of webhooks referenced in the audit log + """ + + audit_log_entries: List[AuditLogEntry] = field( + converter=convert_list(AuditLogEntry), default=None + ) + auto_moderation_rules: List["AutoModerationRule"] = field(default=None) + guild_scheduled_events: List["ScheduledEvents"] = field(default=None) + integrations: List["Integration"] = field(default=None) + threads: List[Channel] = field(converter=convert_list(Channel), default=None) + users: List[User] = field(converter=convert_list(User), default=None) + webhooks: List[Webhook] = field(converter=convert_list(Webhook), default=None) + + def __attrs_post__init(self): + if self.guild_scheduled_events: + from .guild import ScheduledEvents + + self.guild_scheduled_events = [ + ScheduledEvents(**event) for event in self.guild_scheduled_events + ] + if self.integrations: + from .guild import Integration + + self.integrations = [Integration(**integration) for integration in self.integrations] + + if self.auto_moderation_rules: + from .gw import AutoModerationRule + + self.auto_moderation_rules = [ + AutoModerationRule(**rule) for rule in self.auto_moderation_rules + ] diff --git a/interactions/api/models/guild.py b/interactions/api/models/guild.py index 1fb23b788..47fcabbeb 100644 --- a/interactions/api/models/guild.py +++ b/interactions/api/models/guild.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum, IntEnum -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from ..error import LibraryException from .attrs_utils import ( @@ -11,6 +11,7 @@ define, field, ) +from .audit_log import AuditLogEvents, AuditLogs from .channel import Channel, ChannelType, Thread, ThreadMember from .member import Member from .message import Emoji, Sticker @@ -2271,6 +2272,161 @@ async def modify_auto_moderation_rule( ) return AutoModerationRule(**res) + async def get_audit_logs( + self, + limit: Optional[int] = 100, + user_id: Optional[Union[User, int, Snowflake]] = MISSING, + action_type: Optional[Union[int, AuditLogEvents]] = MISSING, + before: Optional[Union[int, Snowflake]] = MISSING, + ) -> AuditLogs: + """ + Gets the audit logs of the guild. + + :param limit?: How many entries to get, default 100 + :type limit?: Optional[int] + :param user_id?: User ID snowflake. filter the log for actions made by a user. + :type user_id?: Optional[Union[User, int, Snowflake]] + :param action_type?: The Type of the audit log action. + :type action_type?: Optional[Union[int, AuditLogEvents]] + :param before?: filter the log before a certain entry id. + :type before?: Union[int, Snowflake] + :return: The guild audit logs + :rtype: AuditLogs + """ + + _user_id = ( + (user_id.id if isinstance(user_id, User) else user_id) + if user_id is not MISSING + else None + ) + _before = before if before is not MISSING else None + _action_type = action_type if action_type is not MISSING else None + + res = await self._client.get_guild_auditlog( + guild_id=int(self.id), + limit=limit, + before=_before, + user_id=_user_id, + action_type=_action_type, + ) + return AuditLogs(**res) + + async def get_latest_audit_log_action( + self, + of: Union[ + User, + Snowflake, + AuditLogEvents, + int, + Member, + Tuple[Union[User, Member, Snowflake, int], Union[AuditLogs, int]], + ], + ) -> AuditLogs: + """ + Gets the latest audit log action of either a user or an action type + + :param of: The user, user id or action type to look for + :type of: Union[User, Snowflake, AuditLogEvents, int, Tuple[Union[User, Snowflake, int], Union[AuditLogs, int]]] + :return: The latest AuditLog action that applies to the ``of`` parameter + :rtype: AuditLogs + """ + + if isinstance(of, tuple): + if len(of) != 2 or len(str(of[1])) > 3: + raise LibraryException( + 12, + "You specified invalid arguments in the tuple. Make sure the first argument" + "is the user ID and the second is the action type!", + ) + + _user = of[0].id if isinstance(of[0], (Member, User)) else of[0] + res = await self._client.get_guild_auditlog( + guild_id=int(self.id), user_id=_user, action_type=of[1] + ) + + elif isinstance(of, AuditLogEvents) or isinstance(of, int) and len(str(of)) <= 3: + res = await self._client.get_guild_auditlog( + guild_id=int(self.id), limit=1, action_type=of + ) + + else: + if isinstance(of, (Member, User}): + of = of.id + res = await self._client.get_guild_auditlog(guild_id=int(self.id), user_id=of, limit=1) + + return AuditLogs(**res) + + async def get_full_audit_logs( + self, + user_id: Optional[Union[User, int, Snowflake]] = MISSING, + action_type: Optional[Union[int, AuditLogEvents]] = MISSING, + ) -> AuditLogs: + """ + Gets the full audit log of the guild. + + :param user_id?: User ID snowflake. filter the log for actions made by a user. + :type user_id?: Optional[Union[User, int, Snowflake]] + :param action_type?: The type of the audit log action. + :type action_type?: Optional[Union[int, AuditLogEvents]] + :return: The full AuditLog of the guild + :rtype: AuditLogs + """ + + _action_type = action_type if action_type is not MISSING else None + _user_id = ( + (user_id.id if isinstance(user_id, User) else user_id) + if user_id is not MISSING + else None + ) + _audit_log_dict: dict = { + "audit_log_entries": [], + "users": [], + "integrations": [], + "webhooks": [], + "guild_scheduled_events": [], + "threads": [], + "application_commands": [], + "auto_moderation_rules": [], + } + + res = await self._client.get_guild_auditlog( + guild_id=int(self.id), user_id=_user_id, action_type=_action_type, limit=100 + ) + + if len(res["audit_log_entries"]) < 100: + return AuditLogs(**res) + + while len(res["audit_log_entries"]) == 100: + _before = res["audit_log_entries"][-1]["id"] + + double = False + for key, values in res.items(): + for value in values: + if value not in _audit_log_dict[key]: + _audit_log_dict[key] = value + else: + double = True + # It is possible that an item is already present, however we should not break directly out + # in case other attributes are not present yet. + if double: + break + + res = await self._client.get_guild_auditlog( + guild_id=int(self.id), + user_id=_user_id, + before=_before, + action_type=_action_type, + limit=100, + ) + + if not double: + for key, values in res.items(): + for value in values: + if value not in _audit_log_dict[key]: + _audit_log_dict[key] = value + + return AuditLogs(**_audit_log_dict) + @property def icon_url(self) -> Optional[str]: """