From c54a6a927d3259b95cace40fc165e25141df9322 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 30 Apr 2017 02:58:27 -0400 Subject: [PATCH] Implement audit logs. --- discord/__init__.py | 1 + discord/audit_logs.py | 319 +++++++++++++++++++ discord/enums.py | 87 +++++- discord/guild.py | 70 ++++- discord/http.py | 27 +- discord/iterators.py | 109 +++++++ discord/member.py | 4 +- discord/permissions.py | 6 +- docs/api.rst | 685 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1294 insertions(+), 14 deletions(-) create mode 100644 discord/audit_logs.py diff --git a/discord/__init__.py b/discord/__init__.py index fe3e97900..231d83ea6 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -42,6 +42,7 @@ from .embeds import Embed from .shard import AutoShardedClient from .player import * from .voice_client import VoiceClient +from .audit_logs import AuditLogChanges, AuditLogEntry, AuditLogDiff import logging diff --git a/discord/audit_logs.py b/discord/audit_logs.py new file mode 100644 index 000000000..bf48b1725 --- /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 ''.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 ''.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) diff --git a/discord/enums.py b/discord/enums.py index 0b8c58b09..d089b7990 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -27,7 +27,8 @@ DEALINGS IN THE SOFTWARE. from enum import Enum __all__ = ['ChannelType', 'MessageType', 'GuildRegion', 'VerificationLevel', - 'ContentFilter', 'Status', 'DefaultAvatar', 'RelationshipType' ] + 'ContentFilter', 'Status', 'DefaultAvatar', 'RelationshipType', + 'AuditLogAction', 'AuditLogActionCategory', ] class ChannelType(Enum): text = 0 @@ -114,6 +115,90 @@ class RelationshipType(Enum): incoming_request = 3 outgoing_request = 4 +class AuditLogActionCategory(Enum): + create = 1 + delete = 2 + update = 3 + +class AuditLogAction(Enum): + guild_update = 1 + channel_create = 10 + channel_update = 11 + channel_delete = 12 + overwrite_create = 13 + overwrite_update = 14 + overwrite_delete = 15 + kick = 20 + member_prune = 21 + ban = 22 + unban = 23 + member_update = 24 + member_role_update = 25 + role_create = 30 + role_update = 31 + role_delete = 32 + invite_create = 40 + invite_update = 41 + invite_delete = 42 + webhook_create = 50 + webhook_update = 51 + webhook_delete = 52 + emoji_create = 60 + emoji_update = 61 + emoji_delete = 62 + + @property + def category(self): + lookup = { + AuditLogAction.guild_update: AuditLogActionCategory.update, + AuditLogAction.channel_create: AuditLogActionCategory.create, + AuditLogAction.channel_update: AuditLogActionCategory.update, + AuditLogAction.channel_delete: AuditLogActionCategory.delete, + AuditLogAction.overwrite_create: AuditLogActionCategory.create, + AuditLogAction.overwrite_update: AuditLogActionCategory.update, + AuditLogAction.overwrite_delete: AuditLogActionCategory.delete, + AuditLogAction.kick: None, + AuditLogAction.member_prune: None, + AuditLogAction.ban: None, + AuditLogAction.unban: None, + AuditLogAction.member_update: AuditLogActionCategory.update, + AuditLogAction.member_role_update: AuditLogActionCategory.update, + AuditLogAction.role_create: AuditLogActionCategory.create, + AuditLogAction.role_update: AuditLogActionCategory.update, + AuditLogAction.role_delete: AuditLogActionCategory.delete, + AuditLogAction.invite_create: AuditLogActionCategory.create, + AuditLogAction.invite_update: AuditLogActionCategory.update, + AuditLogAction.invite_delete: AuditLogActionCategory.delete, + AuditLogAction.webhook_create: AuditLogActionCategory.create, + AuditLogAction.webhook_update: AuditLogActionCategory.update, + AuditLogAction.webhook_delete: AuditLogActionCategory.delete, + AuditLogAction.emoji_create: AuditLogActionCategory.create, + AuditLogAction.emoji_update: AuditLogActionCategory.update, + AuditLogAction.emoji_delete: AuditLogActionCategory.delete, + } + return lookup[self] + + @property + def target_type(self): + v = self.value + if v == -1: + return 'all' + elif v < 10: + return 'guild' + elif v < 20: + return 'channel' + elif v < 30: + return 'user' + elif v < 40: + return 'role' + elif v < 50: + return 'invite' + elif v < 60: + return 'webhook' + elif v < 70: + return 'emoji' + + def try_enum(cls, val): """A function that tries to turn the value into enum ``cls``. diff --git a/discord/guild.py b/discord/guild.py index 209d057bd..50426fd35 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -41,6 +41,7 @@ from .enums import GuildRegion, Status, ChannelType, try_enum, VerificationLevel from .mixins import Hashable from .user import User from .invite import Invite +from .iterators import AuditLogIterator BanEntry = namedtuple('BanEntry', 'reason user') @@ -921,7 +922,7 @@ class Guild(Hashable): return role @asyncio.coroutine - def kick(self, user): + def kick(self, user, *, reason=None): """|coro| Kicks a user from the guild. @@ -935,6 +936,8 @@ class Guild(Hashable): ----------- user: :class:`abc.Snowflake` The user to kick from their guild. + reason: Optional[str] + The reason the user got kicked. Raises ------- @@ -943,10 +946,10 @@ class Guild(Hashable): HTTPException Kicking failed. """ - yield from self._state.http.kick(user.id, self.id) + yield from self._state.http.kick(user.id, self.id, reason=reason) @asyncio.coroutine - def ban(self, user, *, delete_message_days=1): + def ban(self, user, *, reason=None, delete_message_days=1): """|coro| Bans a user from the guild. @@ -963,6 +966,8 @@ class Guild(Hashable): delete_message_days: int The number of days worth of messages to delete from the user in the guild. The minimum is 0 and the maximum is 7. + reason: Optional[str] + The reason the user got banned. Raises ------- @@ -971,7 +976,7 @@ class Guild(Hashable): HTTPException Banning failed. """ - yield from self._state.http.ban(user.id, self.id, delete_message_days) + yield from self._state.http.ban(user.id, self.id, delete_message_days, reason=reason) @asyncio.coroutine def unban(self, user): @@ -1017,3 +1022,60 @@ class Guild(Hashable): if state.is_bot: raise ClientException('Must not be a bot account to ack messages.') return state.http.ack_guild(self.id) + + def audit_logs(self, *, limit=100, before=None, after=None, reverse=None, user=None, action=None): + """Return an :class:`AsyncIterator` that enables receiving the guild's audit logs. + + You must have :attr:`Permissions.view_audit_log` permission to use this. + + Parameters + ----------- + limit: Optional[int] + The number of entries to retrieve. If ``None`` retrieve all entries. + before: Union[:class:`abc.Snowflake`, datetime] + Retrieve entries before this date or entry. + If a date is provided it must be a timezone-naive datetime representing UTC time. + after: Union[:class:`abc.Snowflake`, datetime] + Retrieve entries after this date or entry. + If a date is provided it must be a timezone-naive datetime representing UTC time. + reverse: bool + If set to true, return entries in oldest->newest order. If unspecified, + this defaults to ``False`` for most cases. However if passing in a + ``after`` parameter then this is set to ``True``. This avoids getting entries + out of order in the ``after`` case. + user: :class:`abc.Snowflake` + The moderator to filter entries from. + action: :class:`AuditLogAction` + The action to filter with. + + Yields + -------- + :class:`AuditLogEntry` + The audit log entry. + + Examples + ---------- + + Getting the first 100 entries: :: + + async for entry in guild.audit_logs(limit=100): + print('{0.user} did {0.action} to {0.target}'.format(entry)) + + Getting entries for a specific action: :: + + async for entry in guild.audit_logs(action=discord.AuditLogAction.ban): + print('{0.user} banned {0.target}'.format(entry)) + + Getting entries made by a specific user: :: + + entries = await guild.audit_logs(limit=None, user=guild.me).flatten() + await guild.default_channel.send('I made {} moderation actions.'.format(len(entries))) + """ + if user: + user = user.id + + if action: + action = action.value + + return AuditLogIterator(self, before=before, after=after, limit=limit, + reverse=reverse, user_id=user, action_type=action) diff --git a/discord/http.py b/discord/http.py index dde4ba35e..eab37c629 100644 --- a/discord/http.py +++ b/discord/http.py @@ -410,15 +410,20 @@ class HTTPClient: # Member management - def kick(self, user_id, guild_id): + def kick(self, user_id, guild_id, reason=None): r = Route('DELETE', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) - return self.request(r) + if reason: + return self.request(r, params={'reason': reason }) + return self.request(r, params=params) - def ban(self, user_id, guild_id, delete_message_days=1): + def ban(self, user_id, guild_id, delete_message_days=1, reason=None): r = Route('PUT', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) params = { - 'delete-message-days': delete_message_days + 'delete-message-days': delete_message_days, } + if reason: + params['reason'] = reason + return self.request(r, params=params) def unban(self, user_id, guild_id): @@ -557,6 +562,20 @@ class HTTPClient: r = Route('PATCH', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) return self.request(r, json=payload) + def get_audit_logs(self, guild_id, limit=100, before=None, after=None, user_id=None, action_type=None): + params = { 'limit': limit } + if before: + params['before'] = before + if after: + params['after'] = after + if user_id: + params['user_id'] = user_id + if action_type: + params['action_type'] = action_type + + r = Route('GET', '/guilds/{guild_id}/audit-logs', guild_id=guild_id) + return self.request(r, params=params) + # Invite management def create_invite(self, channel_id, **options): diff --git a/discord/iterators.py b/discord/iterators.py index c3ac56432..86e219317 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -31,6 +31,7 @@ import datetime from .errors import NoMoreItems from .utils import time_snowflake, maybe_coroutine from .object import Object +from .audit_logs import AuditLogEntry PY35 = sys.version_info >= (3, 5) @@ -369,3 +370,111 @@ class HistoryIterator(_AsyncIterator): self.around = None return data return [] + +class AuditLogIterator(_AsyncIterator): + def __init__(self, guild, limit=None, before=None, after=None, reverse=None, user_id=None, action_type=None): + if isinstance(before, datetime.datetime): + before = Object(id=time_snowflake(before, high=False)) + if isinstance(after, datetime.datetime): + after = Object(id=time_snowflake(after, high=True)) + + + self.guild = guild + self.loop = guild._state.loop + self.request = guild._state.http.get_audit_logs + self.limit = limit + self.before = before + self.user_id = user_id + self.action_type = action_type + self.after = after + self._users = {} + self._state = guild._state + + if reverse is None: + self.reverse = after is not None + else: + self.reverse = reverse + + self._filter = None # entry dict -> bool + + self.entries = asyncio.Queue(loop=self.loop) + + if self.before and self.after: + if self.reverse: + self._strategy = self._after_strategy + self._filter = lambda m: int(m['id']) < self.before.id + else: + self._strategy = self._before_strategy + self._filter = lambda m: int(m['id']) > self.after.id + elif self.after: + self._strategy = self._after_strategy + else: + self._strategy = self._before_strategy + + @asyncio.coroutine + def _before_strategy(self, retrieve): + before = self.before.id if self.before else None + data = yield from self.request(self.guild.id, limit=retrieve, user_id=self.user_id, + action_type=self.action_type, before=before) + if len(data): + if self.limit is not None: + self.limit -= retrieve + self.before = Object(id=int(data['audit_log_entries'][-1]['id'])) + return data + + @asyncio.coroutine + def _after_strategy(self, retrieve): + after = self.after.id if self.after else None + data = yield from self.request(self.guild.id, limit=retrieve, user_id=self.user_id, + action_type=self.action_type, after=after) + if len(data): + if self.limit is not None: + self.limit -= retrieve + self.after = Object(id=int(data['audit_log_entries'][0]['id'])) + return data + + @asyncio.coroutine + def get(self): + if self.entries.empty(): + yield from self._fill() + + try: + return self.entries.get_nowait() + except asyncio.QueueEmpty: + raise NoMoreItems() + + def _get_retrieve(self): + l = self.limit + if l is None: + r = 100 + elif l <= 100: + r = l + else: + r = 100 + + self.retrieve = r + return r > 0 + + @asyncio.coroutine + def _fill(self): + from .user import User + + if self._get_retrieve(): + data = yield from self._strategy(self.retrieve) + users = data.get('users', []) + data = data.get('audit_log_entries', []) + + if self.limit is None and len(data) < 100: + self.limit = 0 # terminate the infinite loop + + if self.reverse: + data = reversed(data) + if self._filter: + data = filter(self._filter, data) + + for user in users: + u = User(data=user, state=self._state) + self._users[u.id] = u + + for element in data: + yield from self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild)) diff --git a/discord/member.py b/discord/member.py index 8710269bd..35ae584a1 100644 --- a/discord/member.py +++ b/discord/member.py @@ -357,12 +357,12 @@ class Member(discord.abc.Messageable): yield from self.guild.unban(self) @asyncio.coroutine - def kick(self): + def kick(self, *, reason=None): """|coro| Kicks this member. Equivalent to :meth:`Guild.kick` """ - yield from self.guild.kick(self) + yield from self.guild.kick(self, reason=reason) @asyncio.coroutine def edit(self, **fields): diff --git a/discord/permissions.py b/discord/permissions.py index 42f4718fa..781ebe4c4 100644 --- a/discord/permissions.py +++ b/discord/permissions.py @@ -284,12 +284,12 @@ class Permissions: self._set(6, value) @property - def view_audit_log(self): + def view_audit_logs(self): """Returns True if a user can view the guild's audit log.""" return self._bit(7) - @view_audit_log.setter - def view_audit_log(self, value): + @view_audit_logs.setter + def view_audit_logs(self, value): self._set(7, value) # 2 unused diff --git a/docs/api.rst b/docs/api.rst index eb2871212..303cc1748 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -709,6 +709,400 @@ All enumerations are subclasses of `enum`_. You have sent a friend request to this user. + +.. class:: AuditLogAction + + Represents the type of action being done for a :class:`AuditLogEntry`\, + which is retrievable via :meth:`Guild.audit_log`. + + .. attribute:: guild_update + + The guild has updated. Things that trigger this include: + + - Changing the guild vanity URL + - Changing the guild invite splash + - Changing the guild AFK channel or timeout + - Changing the guild voice server region + - Changing the guild icon + - Changing the guild moderation settings + - Changing things related to the guild widget + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Guild`. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.afk_channel` + - :attr:`~AuditLogDiff.afk_timeout` + - :attr:`~AuditLogDiff.default_message_notifications` + - :attr:`~AuditLogDiff.explicit_content_filter` + - :attr:`~AuditLogDiff.mfa_level` + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.owner` + - :attr:`~AuditLogDiff.splash` + - :attr:`~AuditLogDiff.vanity_url_code` + + .. attribute:: channel_create + + A new channel was created. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + either a :class:`abc.GuildChannel` or :class:`Object` with an ID. + + A more filled out object in the :class:`Object` case can be found + by using :attr:`~AuditLogEntry.after`. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.type` + - :attr:`~AuditLogDiff.overwrites` + + .. attribute:: channel_update + + A channel was updated. Things that trigger this include: + + - The channel name or topic was changed + - The channel bitrate was changed + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`abc.GuildChannel` or :class:`Object` with an ID. + + A more filled out object in the :class:`Object` case can be found + by using :attr:`~AuditLogEntry.after` or :attr:`~AuditLogEntry.before`. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.type` + - :attr:`~AuditLogDiff.position` + - :attr:`~AuditLogDiff.overwrites` + - :attr:`~AuditLogDiff.topic` + - :attr:`~AuditLogDiff.bitrate` + + .. attribute:: channel_delete + + A channel was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + an :class:`Object` with an ID. + + A more filled out object can be found by using the + :attr:`~AuditLogEntry.before` object. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.type` + - :attr:`~AuditLogDiff.overwrites` + + .. attribute:: overwrite_create + + A channel permission overwrite was created. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`abc.GuildChannel` or :class:`Object` with an ID. + + When this is the action, the type of :attr:`~AuditLogEntry.extra` is + either a :class:`Role` or :class:`Member`. If the object is not found + then it is a :class:`Object` with an ID being filled, a name, and a + ``type`` attribute set to either ``'role'`` or ``'member'`` to help + dictate what type of ID it is. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.deny` + - :attr:`~AuditLogDiff.allow` + - :attr:`~AuditLogDiff.id` + - :attr:`~AuditLogDiff.type` + + .. attribute:: overwrite_update + + A channel permission overwrite was changed, this is typically + when the permission values change. + + See :attr:`overwrite_create` for more information on how the + :attr:`~AuditLogEntry.target` and :attr:`~AuditLogEntry.extra` fields + are set. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.deny` + - :attr:`~AuditLogDiff.allow` + - :attr:`~AuditLogDiff.id` + - :attr:`~AuditLogDiff.type` + + .. attribute:: overwrite_delete + + A channel permission overwrite was deleted. + + See :attr:`overwrite_create` for more information on how the + :attr:`~AuditLogEntry.target` and :attr:`~AuditLogEntry.extra` fields + are set. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.deny` + - :attr:`~AuditLogDiff.allow` + - :attr:`~AuditLogDiff.id` + - :attr:`~AuditLogDiff.type` + + .. attribute:: kick + + A member was kicked. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`User` who got kicked. + + When this is the action, :attr:`~AuditLogEntry.changes` is empty. + + .. attribute:: member_prune + + A member prune was triggered. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + set to `None`. + + When this is the action, the type of :attr:`~AuditLogEntry.extra` is + set to an unspecified proxy object with two attributes: + + - ``delete_members_days``: An integer specifying how far the prune was. + - ``members_removed``: An integer specifying how many members were removed. + + When this is the action, :attr:`~AuditLogEntry.changes` is empty. + + .. attribute:: ban + + A member was banned. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`User` who got banned. + + When this is the action, :attr:`~AuditLogEntry.changes` is empty. + + .. attribute:: unban + + A member was unbanned. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`User` who got unbanned. + + When this is the action, :attr:`~AuditLogEntry.changes` is empty. + + .. attribute:: member_update + + A member has updated. This triggers in the following situations: + + - A nickname was changed + - They were server muted or deafened (or it was undo'd) + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Member` or :class:`User` who got updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.nick` + - :attr:`~AuditLogDiff.mute` + - :attr:`~AuditLogDiff.deaf` + + .. attribute:: member_role_update + + A member's role has been updated. This triggers when a member + either gains a role or losses a role. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Member` or :class:`User` who got the role. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.role` + + .. attribute:: role_create + + A new role was created. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Role` or a :class:`Object` with the ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.colour` + - :attr:`~AuditLogDiff.mentionable` + - :attr:`~AuditLogDiff.hoist` + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.permissions` + + .. attribute:: role_update + + A role was updated. This triggers in the following situations: + + - The name has changed + - The permissions have changed + - The colour has changed + - Its hoist/mentionable state has changed + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Role` or a :class:`Object` with the ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.colour` + - :attr:`~AuditLogDiff.mentionable` + - :attr:`~AuditLogDiff.hoist` + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.permissions` + + .. attribute:: role_delete + + A role was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Role` or a :class:`Object` with the ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.colour` + - :attr:`~AuditLogDiff.mentionable` + - :attr:`~AuditLogDiff.hoist` + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.permissions` + + .. attribute:: invite_create + + An invite was created. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Invite` that was created. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.max_age` + - :attr:`~AuditLogDiff.code` + - :attr:`~AuditLogDiff.temporary` + - :attr:`~AuditLogDiff.inviter` + - :attr:`~AuditLogDiff.channel` + - :attr:`~AuditLogDiff.uses` + - :attr:`~AuditLogDiff.max_uses` + + .. attribute:: invite_update + + An invite was updated. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Invite` that was updated. + + .. attribute:: invite_delete + + An invite was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Invite` that was deleted. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.max_age` + - :attr:`~AuditLogDiff.code` + - :attr:`~AuditLogDiff.temporary` + - :attr:`~AuditLogDiff.inviter` + - :attr:`~AuditLogDiff.channel` + - :attr:`~AuditLogDiff.uses` + - :attr:`~AuditLogDiff.max_uses` + + .. attribute:: webhook_create + + A webhook was created. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Object` with the webhook ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.channel` + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.type` (always set to ``1`` if so) + + .. attribute:: webhook_update + + A webhook was updated. This trigger in the following situations: + + - The webhook name changed + - The webhook channel changed + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Object` with the webhook ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.channel` + - :attr:`~AuditLogDiff.name` + + .. attribute:: webhook_delete + + A webhook was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Object` with the webhook ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.channel` + - :attr:`~AuditLogDiff.name` + - :attr:`~AuditLogDiff.type` (always set to ``1`` if so) + + .. attribute:: emoji_create + + An emoji was created. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Emoji` or :class:`Object` with the emoji ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + + .. attribute:: emoji_update + + An emoji was updated. This triggers when the name has changed. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Emoji` or :class:`Object` with the emoji ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + + .. attribute:: emoji_delete + + An emoji was deleted. + + When this is the action, the type of :attr:`~AuditLogEntry.target` is + the :class:`Object` with the emoji ID. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.name` + + +.. class:: AuditLogActionCategory + + Represents the category that the :class:`AuditLogAction` belongs to. + + This can be retrieved via :attr:`AuditLogEntry.category`. + + .. attribute:: create + + The action is the creation of something. + + .. attribute:: delete + + The action is the deletion of something. + + .. attribute:: update + + The action is the update of something. + + + Async Iterator ---------------- @@ -785,6 +1179,297 @@ Certain utilities make working with async iterators easier, detailed below. :param predicate: The predicate to call on every element. Could be a coroutine. :return: An async iterator. + +Audit Log Data +---------------- + +Working with :meth:`Guild.audit_logs` is a complicated process with a lot of machinery +involved. The library attempts to make it easy to use and friendly. In order to accomplish +this goal, it must make use of a couple of data classes that aid in this goal. + +.. autoclass:: AuditLogEntry + :members: + +.. class:: AuditLogChanges + + An audit log change set. + + .. attribute:: before + + The old value. The attribute has the type of :class:`AuditLogDiff`. + + Depending on the :class:`AuditLogActionCategory` retrieved by + :attr:`~AuditLogEntry.category`\, the data retrieved by this + attribute differs: + + +----------------------------------------+---------------------------------------------------+ + | Category | Description | + +----------------------------------------+---------------------------------------------------+ + | :attr:`~AuditLogActionCategory.create` | All attributes are set to ``None``. | + +----------------------------------------+---------------------------------------------------+ + | :attr:`~AuditLogActionCategory.delete` | All attributes are set the value before deletion. | + +----------------------------------------+---------------------------------------------------+ + | :attr:`~AuditLogActionCategory.update` | All attributes are set the value before updating. | + +----------------------------------------+---------------------------------------------------+ + | ``None`` | No attributes are set. | + +----------------------------------------+---------------------------------------------------+ + + .. attribute:: after + + The new value. The attribute has the type of :class:`AuditLogDiff`. + + Depending on the :class:`AuditLogActionCategory` retrieved by + :attr:`~AuditLogEntry.category`\, the data retrieved by this + attribute differs: + + +----------------------------------------+--------------------------------------------------+ + | Category | Description | + +----------------------------------------+--------------------------------------------------+ + | :attr:`~AuditLogActionCategory.create` | All attributes are set to the created value | + +----------------------------------------+--------------------------------------------------+ + | :attr:`~AuditLogActionCategory.delete` | All attributes are set to ``None`` | + +----------------------------------------+--------------------------------------------------+ + | :attr:`~AuditLogActionCategory.update` | All attributes are set the value after updating. | + +----------------------------------------+--------------------------------------------------+ + | ``None`` | No attributes are set. | + +----------------------------------------+--------------------------------------------------+ + +.. class:: AuditLogDiff + + Represents an audit log "change" object. A change object has dynamic + attributes that depend on the type of action being done. Certain actions + map to certain attributes being set. + + Note that accessing an attribute that does not match the specified action + will lead to an attribute error. + + To get a list of attributes that have been set, you can iterate over + them. To see a list of all possible attributes that could be set based + on the action being done, check the documentation for :class:`AuditLogAction`, + otherwise check the documentation below for all attributes that are possible. + + .. describe:: iter(diff) + + Return an iterator over (attribute, value) tuple of this diff. + + .. attribute:: name + + *str* – A name of something. + + .. attribute:: icon + + *str* – A guild's icon hash. See also :attr:`Guild.icon`. + + .. attribute:: splash + + *str* – The guild's invite splash hash. See also :attr:`Guild.splash`. + + .. attribute:: owner + + *Union[:class:`Member`, :class:`User`]`* – The guild's owner. See also :attr:`Guild.owner` + + .. attribute:: region + + *:class:`GuildRegion`* – The guild's voice region. See also :attr:`Guild.region`. + + .. attribute:: afk_channel + + *Union[:class:`VoiceChannel`, :class:`Object`]* – The guild's AFK channel. + + If this could not be found, then it falls back to a :class:`Object` + with the ID being set. + + See :attr:`Guild.afk_channel`. + + .. attribute:: afk_timeout + + *int* – The guild's AFK timeout. See :attr:`Guild.afk_timeout`. + + .. attribute:: mfa_level + + *int* - The guild's MFA level. See :attr:`Guild.mfa_level`. + + .. attribute:: widget_enabled + + *bool* – The guild's widget has been enabled or disabled. + + .. attribute:: widget_channel + + *Union[:class:`TextChannel`, :class:`Object`]* – The widget's channel. + + If this could not be found then it falls back to a :class:`Object` + with the ID being set. + + .. attribute:: verification_level + + *:class:`VerificationLevel`* – The guild's verification level. + + See also :attr:`Guild.verification_level`. + + .. attribute:: explicit_content_filter + + *:class:`ContentFilter`* – The guild's content filter. + + See also :attr:`Guild.explicit_content_filter`. + + .. attribute:: default_message_notifications + + *int* – The guild's default message notification setting. + + .. attribute:: vanity_url_code + + *str* – The guild's vanity URL. + + .. attribute:: position + + *int* – The position of a :class:`Role` or :class:`abc.GuildChannel`. + + .. attribute:: type + + *Union[int, str]* – The type of channel or channel permission overwrite. + + If the type is an ``int``, then it is a type of channel which can be either + ``0`` to indicate a text channel or ``1`` to indicate a voice channel. + + If the type is a ``str``, then it is a type of permission overwrite which + can be either ``'role'`` or ``'member'``. + + .. attribute:: topic + + *str* – The topic of a :class:`TextChannel`. + + See also :attr:`TextChannel.topic`. + + .. attribute:: bitrate + + *int* – The bitrate of a :class:`VoiceChannel`. + + See also :attr:`VoiceChannel.bitrate`. + + .. attribute:: overwrites + + *List[Tuple[target, :class:`PermissionOverwrite`]]* – A list of + permission overwrite tuples that represents a target and a + :class:`PermissionOverwrite` for said target. + + The first element is the object being targeted, which can either + be a :class:`Member` or :class:`User` or :class:`Role`. If this object + is not found then it is a :class:`Object` with an ID being filled and + a ``type`` attribute set to either ``'role'`` or ``'member'`` to help + decide what type of ID it is. + + .. attribute:: role + + *Union[:class:`Role`, :class:`Object`]* – A role being added or removed + from a member. + + If the role is not found then it is a :class:`Object` with the ID being + filled in. + + .. attribute:: nick + + *Optional[str]* – The nickname of a member. + + See also :attr:`Member.nick` + + .. attribute:: deaf + + *bool* – Whether the member is being server deafened. + + See also :attr:`VoiceState.deaf`. + + .. attribute:: mute + + *bool* – Whether the member is being server muted. + + See also :attr:`VoiceState.mute`. + + .. attribute:: permissions + + *:class:`Permissions`* – The permissions of a role. + + See also :attr:`Role.permissions`. + + .. attribute:: colour + color + + *:class:`Colour`* – The colour of a role. + + See also :attr:`Role.colour` + + .. attribute:: hoist + + *bool* – Whether the role is being hoisted or not. + + See also :attr:`Role.hoist` + + .. attribute:: mentionable + + *bool* – Whether the role is mentionable or not. + + See also :attr:`Role.mentionable` + + .. attribute:: code + + *str* – The invite's code. + + See also :attr:`Invite.code` + + .. attribute:: channel + + *Union[:class:`abc.GuildChannel`, :class:`Object`]* – A guild channel. + + If the channel is not found then it is a :class:`Object` with the ID + being set. In some cases the channel name is also set. + + .. attribute:: inviter + + *:class:`User`* – The user who created the invite. + + See also :attr:`Invite.inviter`. + + .. attribute:: max_uses + + *int* – The invite's max uses. + + See also :attr:`Invite.max_uses`. + + .. attribute:: uses + + *int* – The invite's current uses. + + See also :attr:`Invite.uses`. + + .. attribute:: max_age + + *int* – The invite's max age in seconds. + + See also :attr:`Invite.max_age`. + + .. attribute:: temporary + + *bool* – If the invite is a temporary invite. + + See also :attr:`Invite.temporary`. + + .. attribute:: allow + deny + + *:class:`Permissions`* – The permissions being allowed or denied. + + .. attribute:: id + + *int* – The ID of the object being changed. + + .. attribute:: avatar + + *str* – The avatar hash of a member. + + See also :attr:`User.avatar`. + +.. this is currently missing the following keys: reason and application_id + I'm not sure how to about porting these + .. _discord_api_data: Data Classes