aboutsummaryrefslogtreecommitdiff
path: root/discord
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 /discord
parentBetter TextChannel.is_nsfw() check. (diff)
downloaddiscord.py-c54a6a927d3259b95cace40fc165e25141df9322.tar.xz
discord.py-c54a6a927d3259b95cace40fc165e25141df9322.zip
Implement audit logs.
Diffstat (limited to 'discord')
-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
8 files changed, 609 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