aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRapptz <[email protected]>2017-04-30 02:58:27 -0400
committerRapptz <[email protected]>2017-04-30 02:58:27 -0400
commitc54a6a927d3259b95cace40fc165e25141df9322 (patch)
treec9206cb32322a066f2bddf22b48f46e4a543231f
parentBetter TextChannel.is_nsfw() check. (diff)
downloaddiscord.py-c54a6a927d3259b95cace40fc165e25141df9322.tar.xz
discord.py-c54a6a927d3259b95cace40fc165e25141df9322.zip
Implement audit logs.
-rw-r--r--discord/__init__.py1
-rw-r--r--discord/audit_logs.py319
-rw-r--r--discord/enums.py87
-rw-r--r--discord/guild.py70
-rw-r--r--discord/http.py27
-rw-r--r--discord/iterators.py109
-rw-r--r--discord/member.py4
-rw-r--r--discord/permissions.py6
-rw-r--r--docs/api.rst685
9 files changed, 1294 insertions, 14 deletions
diff --git a/discord/__init__.py b/discord/__init__.py
index fe3e9790..231d83ea 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 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)
diff --git a/discord/enums.py b/discord/enums.py
index 0b8c58b0..d089b799 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 209d057b..50426fd3 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 dde4ba35..eab37c62 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 c3ac5643..86e21931 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 8710269b..35ae584a 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 42f4718f..781ebe4c 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 eb287121..303cc174 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