diff options
Diffstat (limited to 'discord/audit_logs.py')
| -rw-r--r-- | discord/audit_logs.py | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/discord/audit_logs.py b/discord/audit_logs.py new file mode 100644 index 00000000..bf48b172 --- /dev/null +++ b/discord/audit_logs.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- + +""" +The MIT License (MIT) + +Copyright (c) 2015-2017 Rapptz + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" + +from . import utils, enums +from .object import Object +from .permissions import PermissionOverwrite, Permissions +from .colour import Colour +from .invite import Invite + +def _transform_verification_level(entry, data): + return enums.try_enum(enums.VerificationLevel, data) + +def _transform_explicit_content_filter(entry, data): + return enums.try_enum(enums.ContentFilter, data) + +def _transform_permissions(entry, data): + return Permissions(data) + +def _transform_color(entry, data): + return Colour(data) + +def _transform_snowflake(entry, data): + return int(data) + +def _transform_channel(entry, data): + if data is None: + return None + channel = entry.guild.get_channel(int(data)) or Object(id=data) + return channel + +def _transform_owner_id(entry, data): + if data is None: + return None + return entry._get_member(int(data)) + +def _transform_inviter_id(entry, data): + if data is None: + return None + return entry._get_member(int(data)) + +def _transform_overwrites(entry, data): + overwrites = [] + for elem in data: + allow = Permissions(elem['allow']) + deny = Permissions(elem['deny']) + ow = PermissionOverwrite.from_pair(allow, deny) + + ow_type = elem['type'] + ow_id = int(elem['id']) + if ow_type == 'role': + target = utils.find(lambda r: r.id == ow_id, entry.guild.roles) + else: + target = entry._get_member(ow_id) + + if target is None: + target = Object(id=ow_id) + + overwrites.append((target, ow)) + + return overwrites + +class AuditLogDiff: + def __len__(self): + return len(self.__dict__) + + def __iter__(self): + return self.__dict__.items() + + def __repr__(self): + return '<AuditLogDiff attrs={0!r}>'.format(tuple(self.__dict__)) + +class AuditLogChanges: + TRANSFORMERS = { + 'verification_level': (None, _transform_verification_level), + 'explicit_content_filter': (None, _transform_explicit_content_filter), + 'allow': (None, _transform_permissions), + 'deny': (None, _transform_permissions), + 'permissions': (None, _transform_permissions), + 'id': (None, _transform_snowflake), + 'color': ('colour', _transform_color), + 'owner_id': ('owner', _transform_owner_id), + 'inviter_id': ('inviter', _transform_inviter_id), + 'channel_id': ('channel', _transform_channel), + 'afk_channel_id': ('afk_channel', _transform_channel), + 'widget_channel_id': ('widget_channel', _transform_channel), + 'permission_overwrites': ('overwrites', _transform_overwrites), + 'splash_hash': ('splash', None), + 'icon_hash': ('icon', None), + 'avatar_hash': ('avatar', None), + } + + def __init__(self, entry, data): + self.before = AuditLogDiff() + self.after = AuditLogDiff() + + for elem in data: + attr = elem['key'] + + # special cases for role add/remove + if attr == '$add': + self._handle_role(self.before, self.after, entry, elem) + continue + elif attr == '$remove': + self._handle_role(self.after, self.before, entry, elem) + continue + + transformer = self.TRANSFORMERS.get(attr) + if transformer: + attr, transformer = transformer + + try: + before = elem['old_value'] + except KeyError: + before = None + else: + if transformer: + before = transformer(entry, before) + + setattr(self.before, attr, before) + + try: + after = elem['new_value'] + except KeyError: + after = None + else: + if transformer: + after = transformer(entry, after) + + setattr(self.after, attr, after) + + # add an alias + if hasattr(self.after, 'colour'): + self.after.color = self.after.colour + self.before.color = self.before.colour + + def _handle_role(self, first, second, entry, elem): + setattr(first, 'role', None) + + # TODO: partial data? + role_id = int(elem['id']) + role = utils.find(lambda r: r.id == role_id, entry.guild.roles) + + if role is None: + role = discord.Object(id=role_id) + role.name = elem['name'] + + setattr(second, 'role', role) + +class AuditLogEntry: + """Represents an Audit Log entry. + + You retrieve these via :meth:`Guild.audit_log`. + + Attributes + ----------- + action: :class:`AuditLogAction` + The action that was done. + user: :class:`abc.User` + The user who initiated this action. Usually a :class:`Member`\, unless gone + then it's a :class:`User`. + id: int + The entry ID. + target: Any + The target that got changed. The exact type of this depends on + the action being done. + reason: Optional[str] + The reason this action was done. + extra: Any + Extra information that this entry has that might be useful. + For most actions, this is ``None``. However in some cases it + contains extra information. See :class:`AuditLogAction` for + which actions have this field filled out. + """ + + def __init__(self, *, users, data, guild): + self._state = guild._state + self.guild = guild + self._users = users + self._from_data(data) + + def _from_data(self, data): + self.action = enums.AuditLogAction(data['action_type']) + self.id = int(data['id']) + + # this key is technically not usually present + self.reason = data.get('reason') + self.extra = data.get('options') + + if self.extra: + if self.action is enums.AuditLogAction.member_prune: + # member prune has two keys with useful information + self.extra = type('_AuditLogProxy', (), {k: int(v) for k, v in self.extra.items()})() + elif self.action.name.startswith('overwrite_'): + # the overwrite_ actions have a dict with some information + instance_id = int(self.extra['id']) + the_type = self.extra.get('type') + if the_type == 'member': + self.extra = self._get_member(instance_id) + else: + role = utils.find(lambda r: r.id == instance_id, self.guild.roles) + if role is None: + role = Object(id=instance_id) + role.name = self.extra.get('role_name') + self.extra = role + + # this key is not present when the above is present, typically. + # It's a list of { new_value: a, old_value: b, key: c } + # where new_value and old_value are not guaranteed to be there depending + # on the action type, so let's just fetch it for now and only turn it + # into meaningful data when requested + self._changes = data.get('changes', []) + + self.user = self._get_member(int(data['user_id'])) + self._target_id = utils._get_as_snowflake(data, 'target_id') + + def _get_member(self, user_id): + return self.guild.get_member(user_id) or self._users.get(user_id) + + def __repr__(self): + return '<AuditLogEntry id={0.id} action={0.action} user={0.user!r}>'.format(self) + + @utils.cached_property + def created_at(self): + """Returns the entry's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @utils.cached_property + def target(self): + try: + converter = getattr(self, '_convert_target_' + self.action.target_type) + except AttributeError: + return Object(id=self._target_id) + else: + return converter(self._target_id) + + @utils.cached_property + def category(self): + """Optional[:class:`AuditLogActionCategory`]: The category of the action, if applicable.""" + return self.action.category + + @utils.cached_property + def changes(self): + """:class:`AuditLogChanges`: The list of changes this entry has.""" + obj = AuditLogChanges(self, self._changes) + del self._changes + return obj + + @utils.cached_property + def before(self): + """:class:`AuditLogDiff`: The target's prior state.""" + return self.changes.before + + @utils.cached_property + def after(self): + """:class:`AuditLogDiff`: The target's subsequent state.""" + return self.changes.after + + def _convert_target_guild(self, target_id): + return self.guild + + def _convert_target_channel(self, target_id): + ch = self.guild.get_channel(target_id) + if ch is None: + return Object(id=target_id) + return ch + + def _convert_target_user(self, target_id): + return self._get_member(target_id) + + def _convert_target_role(self, target_id): + role = utils.find(lambda r: r.id == target_id, self.guild.roles) + if role is None: + return Object(id=target_id) + return role + + def _convert_target_invite(self, target_id): + # invites have target_id set to null + # so figure out which change has the full invite data + changeset = self.before if self.action is enums.AuditLogAction.invite_delete else self.after + + fake_payload = { + 'max_age': changeset.max_age, + 'max_uses': changeset.max_uses, + 'code': changeset.code, + 'temporary': changeset.temporary, + 'channel': changeset.channel, + 'uses': changeset.uses, + 'guild': self.guild, + } + + obj = Invite(state=self._state, data=fake_payload) + obj.inviter = changeset.inviter + return obj + + def _convert_target_emoji(self, target_id): + return self._state.get_emoji(target_id) or Object(id=target_id) |