From dff6bcc7457febf7bb1d797bd777e728f623e938 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sun, 7 May 2017 03:08:06 -0400 Subject: [PATCH] Add support for audit log reasons. Most routes now have a 'reason' keyword argument. --- discord/abc.py | 35 +++++++++---- discord/channel.py | 40 ++++++++------ discord/emoji.py | 15 ++++-- discord/guild.py | 62 +++++++++++++++------- discord/http.py | 127 ++++++++++++++++++++++++--------------------- discord/invite.py | 9 +++- discord/member.py | 30 +++++++---- discord/message.py | 9 +++- discord/role.py | 21 +++++--- 9 files changed, 217 insertions(+), 131 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index c54404799..6eb4fe92d 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -121,7 +121,7 @@ class GuildChannel: return self.name @asyncio.coroutine - def _move(self, position): + def _move(self, position, *, reason): if position < 0: raise InvalidArgument('Channel position cannot be less than 0.') @@ -145,7 +145,7 @@ class GuildChannel: channels.insert(position, self) payload = [{'id': c.id, 'position': index } for index, c in enumerate(channels)] - yield from http.move_channel_position(self.guild.id, payload) + yield from http.move_channel_position(self.guild.id, payload, reason=reason) def _fill_overwrites(self, data): self._overwrites = [] @@ -351,13 +351,19 @@ class GuildChannel: return base @asyncio.coroutine - def delete(self): + def delete(self, *, reason=None): """|coro| Deletes the channel. You must have Manage Channel permission to use this. + Parameters + ----------- + reason: Optional[str] + The reason for deleting this channel. + Shows up on the audit log. + Raises ------- Forbidden @@ -367,10 +373,10 @@ class GuildChannel: HTTPException Deleting the channel failed. """ - yield from self._state.http.delete_channel(self.id) + yield from self._state.http.delete_channel(self.id, reason=reason) @asyncio.coroutine - def set_permissions(self, target, *, overwrite=_undefined, **permissions): + def set_permissions(self, target, *, overwrite=_undefined, reason=None, **permissions): """|coro| Sets the channel specific permission overwrites for a target in the @@ -418,6 +424,8 @@ class GuildChannel: \*\*permissions A keyword argument list of permissions to set for ease of use. Cannot be mixed with ``overwrite``. + reason: Optional[str] + The reason for doing this action. Shows up on the audit log. Raises ------- @@ -453,15 +461,15 @@ class GuildChannel: # TODO: wait for event if overwrite is None: - yield from http.delete_channel_permissions(self.id, target.id) + yield from http.delete_channel_permissions(self.id, target.id, reason=reason) elif isinstance(overwrite, PermissionOverwrite): (allow, deny) = overwrite.pair() - yield from http.edit_channel_permissions(self.id, target.id, allow.value, deny.value, perm_type) + yield from http.edit_channel_permissions(self.id, target.id, allow.value, deny.value, perm_type, reason=reason) else: raise InvalidArgument('Invalid overwrite type provided.') @asyncio.coroutine - def create_invite(self, **fields): + def create_invite(self, *, reason=None, **fields): """|coro| Creates an instant invite. @@ -481,6 +489,8 @@ class GuildChannel: Indicates if a unique invite URL should be created. Defaults to True. If this is set to False then it will return a previously created invite. + reason: Optional[str] + The reason for creating this invite. Shows up on the audit log. Raises ------- @@ -493,7 +503,7 @@ class GuildChannel: The invite that was created. """ - data = yield from self._state.http.create_invite(self.id, **fields) + data = yield from self._state.http.create_invite(self.id, reason=reason, **fields) return Invite.from_incomplete(data=data, state=self._state) @asyncio.coroutine @@ -537,7 +547,7 @@ class Messageable(metaclass=abc.ABCMeta): raise NotImplementedError @asyncio.coroutine - def send(self, content=None, *, tts=False, embed=None, file=None, files=None, delete_after=None): + def send(self, content=None, *, tts=False, embed=None, file=None, files=None, reason=None, delete_after=None): """|coro| Sends a message to the destination with the content given. @@ -571,6 +581,9 @@ class Messageable(metaclass=abc.ABCMeta): If provided, the number of seconds to wait in the background before deleting the message we just sent. If the deletion fails, then it is silently ignored. + reason: Optional[str] + The reason for deleting the message, if necessary. + Shows up on the audit log. Raises -------- @@ -626,7 +639,7 @@ class Messageable(metaclass=abc.ABCMeta): def delete(): yield from asyncio.sleep(delete_after, loop=state.loop) try: - yield from ret.delete() + yield from ret.delete(reason=reason) except: pass compat.create_task(delete(), loop=state.loop) diff --git a/discord/channel.py b/discord/channel.py index fee9fd01e..80948c8ee 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -37,9 +37,9 @@ import asyncio __all__ = ('TextChannel', 'VoiceChannel', 'DMChannel', 'GroupChannel', '_channel_factory') @asyncio.coroutine -def _single_delete_strategy(messages): +def _single_delete_strategy(messages, *, reason): for m in messages: - yield from m.delete() + yield from m.delete(reason=reason) class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): """Represents a Discord guild text channel. @@ -116,7 +116,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): return n == 'nsfw' or n[:5] == 'nsfw-' @asyncio.coroutine - def edit(self, **options): + def edit(self, *, reason=None, **options): """|coro| Edits the channel. @@ -132,6 +132,8 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): The new channel's topic. position: int The new channel's position. + reason: Optional[str] + The reason for editing this channel. Shows up on the audit log. Raises ------ @@ -147,15 +149,15 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): except KeyError: pass else: - yield from self._move(position) + yield from self._move(position, reason=reason) self.position = position if options: - data = yield from self._state.http.edit_channel(self.id, **options) + data = yield from self._state.http.edit_channel(self.id, reason=reason, **options) self._update(self.guild, data) @asyncio.coroutine - def delete_messages(self, messages): + def delete_messages(self, messages, *, reason=None): """|coro| Deletes a list of messages. This is similar to :meth:`Message.delete` @@ -165,8 +167,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): Parameters ----------- - messages : iterable of :class:`Message` + messages: iterable of :class:`Message` An iterable of messages denoting which ones to bulk delete. + reason: Optional[str] + The reason for bulk deleting these messages. Shows up on the audit log. Raises ------ @@ -186,10 +190,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): message_ids = [m.id for m in messages] channel = yield from self._get_channel() - yield from self._state.http.delete_messages(channel.id, message_ids) + yield from self._state.http.delete_messages(channel.id, message_ids, reason=reason) @asyncio.coroutine - def purge(self, *, limit=100, check=None, before=None, after=None, around=None): + def purge(self, *, limit=100, check=None, before=None, after=None, around=None, reason=None): """|coro| Purges a list of messages that meet the criteria given by the predicate @@ -219,6 +223,8 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): Same as ``after`` in :meth:`history`. around Same as ``around`` in :meth:`history`. + reason: Optional[str] + The reason for doing this action. Shows up on the audit log. Raises ------- @@ -262,17 +268,17 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): if count >= 2: # more than 2 messages -> bulk delete to_delete = ret[-count:] - yield from strategy(to_delete) + yield from strategy(to_delete, reason=reason) elif count == 1: # delete a single message - yield from ret[-1].delete() + yield from ret[-1].delete(reason=reason) return ret else: if count == 100: # we've reached a full 'queue' to_delete = ret[-100:] - yield from strategy(to_delete) + yield from strategy(to_delete, reason=reason) count = 0 yield from asyncio.sleep(1) @@ -283,7 +289,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): yield from ret[-1].delete() elif count >= 2: to_delete = ret[-count:] - yield from strategy(to_delete) + yield from strategy(to_delete, reason=reason) count = 0 strategy = _single_delete_strategy @@ -362,7 +368,7 @@ class VoiceChannel(discord.abc.Callable, discord.abc.GuildChannel, Hashable): return ret @asyncio.coroutine - def edit(self, **options): + def edit(self, *, reason=None, **options): """|coro| Edits the channel. @@ -378,6 +384,8 @@ class VoiceChannel(discord.abc.Callable, discord.abc.GuildChannel, Hashable): The new channel's user limit. position: int The new channel's position. + reason: Optional[str] + The reason for editing this channel. Shows up on the audit log. Raises ------ @@ -392,11 +400,11 @@ class VoiceChannel(discord.abc.Callable, discord.abc.GuildChannel, Hashable): except KeyError: pass else: - yield from self._move(position) + yield from self._move(position, reason=reason) self.position = position if options: - data = yield from self._state.http.edit_channel(self.id, **options) + data = yield from self._state.http.edit_channel(self.id, reason=reason, **options) self._update(self.guild, data) class DMChannel(discord.abc.Messageable, Hashable): diff --git a/discord/emoji.py b/discord/emoji.py index ff839c4d1..fdb13430a 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -118,7 +118,7 @@ class Emoji(Hashable): @asyncio.coroutine - def delete(self): + def delete(self, *, reason=None): """|coro| Deletes the custom emoji. @@ -128,6 +128,11 @@ class Emoji(Hashable): Guild local emotes can only be deleted by user bots. + Parameters + ----------- + reason: Optional[str] + The reason for deleting this emoji. Shows up on the audit log. + Raises ------- Forbidden @@ -136,10 +141,10 @@ class Emoji(Hashable): An error occurred deleting the emoji. """ - yield from self._state.http.delete_custom_emoji(self.guild.id, self.id) + yield from self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason) @asyncio.coroutine - def edit(self, *, name): + def edit(self, *, name, reason=None): """|coro| Edits the custom emoji. @@ -153,6 +158,8 @@ class Emoji(Hashable): ----------- name: str The new emoji name. + reason: Optional[str] + The reason for editing this emoji. Shows up on the audit log. Raises ------- @@ -162,4 +169,4 @@ class Emoji(Hashable): An error occurred editing the emoji. """ - yield from self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name) + yield from self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, reason=reason) diff --git a/discord/guild.py b/discord/guild.py index 9e79ac643..55e8fd533 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -427,7 +427,7 @@ class Guild(Hashable): return utils.find(pred, members) - def _create_channel(self, name, overwrites, type): + def _create_channel(self, name, overwrites, type, reason): if overwrites is None: overwrites = {} elif not isinstance(overwrites, dict): @@ -452,10 +452,10 @@ class Guild(Hashable): perms.append(payload) - return self._state.http.create_channel(self.id, name, str(type), permission_overwrites=perms) + return self._state.http.create_channel(self.id, name, str(type), permission_overwrites=perms, reason=reason) @asyncio.coroutine - def create_text_channel(self, name, *, overwrites=None): + def create_text_channel(self, name, *, overwrites=None, reason=None): """|coro| Creates a :class:`TextChannel` for the guild. @@ -495,6 +495,8 @@ class Guild(Hashable): A `dict` of target (either a role or a member) to :class:`PermissionOverwrite` to apply upon creation of a channel. Useful for creating secret channels. + reason: Optional[str] + The reason for creating this channel. Shows up on the audit log. Raises ------- @@ -510,17 +512,17 @@ class Guild(Hashable): :class:`TextChannel` The channel that was just created. """ - data = yield from self._create_channel(name, overwrites, ChannelType.text) + data = yield from self._create_channel(name, overwrites, ChannelType.text, reason=reason) return TextChannel(state=self._state, guild=self, data=data) @asyncio.coroutine - def create_voice_channel(self, name, *, overwrites=None): + def create_voice_channel(self, name, *, overwrites=None, reason=None): """|coro| Same as :meth:`create_text_channel` except makes a :class:`VoiceChannel` instead. """ - data = yield from self._create_channel(name, overwrites, ChannelType.voice) + data = yield from self._create_channel(name, overwrites, ChannelType.voice, reason=reason) return VoiceChannel(state=self._state, guild=self, data=data) @asyncio.coroutine @@ -559,7 +561,7 @@ class Guild(Hashable): yield from self._state.http.delete_guild(self.id) @asyncio.coroutine - def edit(self, **fields): + def edit(self, *, reason=None, **fields): """|coro| Edits the guild. @@ -590,6 +592,8 @@ class Guild(Hashable): be owner of the guild to do this. verification_level: :class:`VerificationLevel` The new verification level for the guild. + reason: Optional[str] + The reason for editing this guild. Shows up on the audit log. Raises ------- @@ -642,7 +646,8 @@ class Guild(Hashable): raise InvalidArgument('verification_level field must of type VerificationLevel') fields['verification_level'] = level.value - yield from self._state.http.edit_guild(self.id, **fields) + + yield from self._state.http.edit_guild(self.id, reason=reason, **fields) @asyncio.coroutine @@ -678,7 +683,7 @@ class Guild(Hashable): for e in data] @asyncio.coroutine - def prune_members(self, *, days): + def prune_members(self, *, days, reason=None): """|coro| Prunes the guild from its inactive members. @@ -696,6 +701,8 @@ class Guild(Hashable): ----------- days: int The number of days before counting as inactive. + reason: Optional[str] + The reason for doing this action. Shows up on the audit log. Raises ------- @@ -715,7 +722,7 @@ class Guild(Hashable): if not isinstance(days, int): raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days)) - data = yield from self._state.http.prune_members(self.id, days) + data = yield from self._state.http.prune_members(self.id, days, reason=reason) return data['pruned'] @asyncio.coroutine @@ -784,7 +791,7 @@ class Guild(Hashable): return result @asyncio.coroutine - def create_invite(self, **fields): + def create_invite(self, *, reason=None, **fields): """|coro| Creates an instant invite. @@ -804,6 +811,8 @@ class Guild(Hashable): Indicates if a unique invite URL should be created. Defaults to True. If this is set to False then it will return a previously created invite. + reason: Optional[str] + The reason for creating this invite. Shows up on the audit log. Raises ------- @@ -816,11 +825,11 @@ class Guild(Hashable): The invite that was created. """ - data = yield from self._state.http.create_invite(self.id, **fields) + data = yield from self._state.http.create_invite(self.id, reason=reason, **fields) return Invite.from_incomplete(data=data, state=self._state) @asyncio.coroutine - def create_custom_emoji(self, *, name, image): + def create_custom_emoji(self, *, name, image, reason=None): """|coro| Creates a custom :class:`Emoji` for the guild. @@ -839,6 +848,8 @@ class Guild(Hashable): image: bytes The *bytes-like* object representing the image data to use. Only JPG and PNG images are supported. + reason: Optional[str] + The reason for creating this emoji. Shows up on the audit log. Returns -------- @@ -854,11 +865,11 @@ class Guild(Hashable): """ img = utils._bytes_to_base64_data(image) - data = yield from self._state.http.create_custom_emoji(self.id, name, img) + data = yield from self._state.http.create_custom_emoji(self.id, name, img, reason=reason) return self._state.store_emoji(self, data) @asyncio.coroutine - def create_role(self, **fields): + def create_role(self, *, reason=None, **fields): """|coro| Creates a :class:`Role` for the guild. @@ -880,6 +891,8 @@ class Guild(Hashable): mentionable: bool Indicates if the role should be mentionable by others. Defaults to False. + reason: Optional[str] + The reason for creating this role. Shows up on the audit log. Returns -------- @@ -915,7 +928,7 @@ class Guild(Hashable): if key not in valid_keys: raise InvalidArgument('%r is not a valid field.' % key) - data = yield from self._state.http.create_role(self.id, **fields) + data = yield from self._state.http.create_role(self.id, reason=reason, **fields) role = Role(guild=self, data=data, state=self._state) # TODO: add to cache @@ -979,7 +992,7 @@ class Guild(Hashable): yield from self._state.http.ban(user.id, self.id, delete_message_days, reason=reason) @asyncio.coroutine - def unban(self, user): + def unban(self, user, *, reason=None): """|coro| Unbans a user from the guild. @@ -993,6 +1006,8 @@ class Guild(Hashable): ----------- user: :class:`abc.Snowflake` The user to unban. + reason: Optional[str] + The reason for doing this action. Shows up on the audit log. Raises ------- @@ -1001,7 +1016,7 @@ class Guild(Hashable): HTTPException Unbanning failed. """ - yield from self._state.http.unban(user.id, self.id) + yield from self._state.http.unban(user.id, self.id, reason=reason) @asyncio.coroutine def vanity_invite(self): @@ -1038,7 +1053,7 @@ class Guild(Hashable): return Invite(state=self._state, data=payload) @asyncio.coroutine - def change_vanity_invite(self, new_code): + def change_vanity_invite(self, new_code, *, reason=None): """|coro| Changes the guild's special vanity invite. @@ -1048,6 +1063,13 @@ class Guild(Hashable): You must have :attr:`Permissions.manage_guild` to use this as well. + Parameters + ----------- + new_code: str + The new vanity URL code. + reason: Optional[str] + The reason for changing the vanity invite. Shows up on the audit log. + Raises ------- Forbidden @@ -1056,7 +1078,7 @@ class Guild(Hashable): Setting the vanity invite failed. """ - yield from self._state.http.change_vanity_code(self.id, new_code) + yield from self._state.http.change_vanity_code(self.id, new_code, reason=reason) def ack(self): """|coro| diff --git a/discord/http.py b/discord/http.py index c22977c9d..ba850d9d1 100644 --- a/discord/http.py +++ b/discord/http.py @@ -125,6 +125,14 @@ class HTTPClient: headers['Content-Type'] = 'application/json' kwargs['data'] = utils.to_json(kwargs.pop('json')) + try: + reason = kwargs.pop('reason') + except KeyError: + pass + else: + if reason: + headers['X-Audit-Log-Reason'] = reason + kwargs['headers'] = headers if not self._global_over.is_set(): @@ -336,18 +344,18 @@ class HTTPClient: def ack_guild(self, guild_id): return self.request(Route('POST', '/guilds/{guild_id}/ack', guild_id=guild_id)) - def delete_message(self, channel_id, message_id): + def delete_message(self, channel_id, message_id, *, reason=None): r = Route('DELETE', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, message_id=message_id) - return self.request(r) + return self.request(r, reason=reason) - def delete_messages(self, channel_id, message_ids): + def delete_messages(self, channel_id, message_ids, *, reason=None): r = Route('POST', '/channels/{channel_id}/messages/bulk_delete', channel_id=channel_id) payload = { 'messages': message_ids } - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) def edit_message(self, message_id, channel_id, **fields): r = Route('PATCH', '/channels/{channel_id}/messages/{message_id}', channel_id=channel_id, @@ -426,11 +434,11 @@ class HTTPClient: return self.request(r, params=params) - def unban(self, user_id, guild_id): + def unban(self, user_id, guild_id, *, reason=None): r = Route('DELETE', '/guilds/{guild_id}/bans/{user_id}', guild_id=guild_id, user_id=user_id) - return self.request(r) + return self.request(r, reason=reason) - def guild_voice_state(self, user_id, guild_id, *, mute=None, deafen=None): + def guild_voice_state(self, user_id, guild_id, *, mute=None, deafen=None, reason=None): r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) payload = {} if mute is not None: @@ -439,7 +447,7 @@ class HTTPClient: if deafen is not None: payload['deaf'] = deafen - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) def edit_profile(self, password, username, avatar, **fields): payload = { @@ -456,38 +464,40 @@ class HTTPClient: return self.request(Route('PATCH', '/users/@me'), json=payload) - def change_my_nickname(self, guild_id, nickname): + def change_my_nickname(self, guild_id, nickname, *, reason=None): + r = Route('PATCH', '/guilds/{guild_id}/members/@me/nick', guild_id=guild_id) payload = { 'nick': nickname } - return self.request(Route('PATCH', '/guilds/{guild_id}/members/@me/nick', guild_id=guild_id), json=payload) + return self.request(r, json=payload, reason=reason) - def change_nickname(self, guild_id, user_id, nickname): + def change_nickname(self, guild_id, user_id, nickname, *, reason=None): r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) payload = { 'nick': nickname } - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) - def edit_member(self, guild_id, user_id, **fields): + def edit_member(self, guild_id, user_id, *, reason=None, **fields): r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id) - return self.request(r, json=fields) + return self.request(r, json=fields, reason=reason) # Channel management - def edit_channel(self, channel_id, **options): + def edit_channel(self, channel_id, *, reason=None, **options): + r = Route('PATCH', '/channels/{channel_id}', channel_id=channel_id) valid_keys = ('name', 'topic', 'bitrate', 'user_limit', 'position') payload = { k: v for k, v in options.items() if k in valid_keys } - return self.request(Route('PATCH', '/channels/{channel_id}', channel_id=channel_id), json=payload) + return self.request(r, reason=reason, json=payload) - def move_channel_position(self, guild_id, positions): + def move_channel_position(self, guild_id, positions, *, reason=None): r = Route('PATCH', '/guilds/{guild_id}/channels', guild_id=guild_id) - return self.request(r, json=positions) + return self.request(r, json=positions, reason=reason) - def create_channel(self, guild_id, name, channe_type, permission_overwrites=None): + def create_channel(self, guild_id, name, channe_type, permission_overwrites=None, *, reason=None): payload = { 'name': name, 'type': channe_type @@ -496,10 +506,10 @@ class HTTPClient: if permission_overwrites is not None: payload['permission_overwrites'] = permission_overwrites - return self.request(Route('POST', '/guilds/{guild_id}/channels', guild_id=guild_id), json=payload) + return self.request(Route('POST', '/guilds/{guild_id}/channels', guild_id=guild_id), json=payload, reason=reason) - def delete_channel(self, channel_id): - return self.request(Route('DELETE', '/channels/{channel_id}', channel_id=channel_id)) + def delete_channel(self, channel_id, *, reason=None): + return self.request(Route('DELETE', '/channels/{channel_id}', channel_id=channel_id), reason=reason) # Guild management @@ -518,7 +528,7 @@ class HTTPClient: return self.request(Route('POST', '/guilds'), json=payload) - def edit_guild(self, guild_id, **fields): + def edit_guild(self, guild_id, *, reason=None, **fields): valid_keys = ('name', 'region', 'icon', 'afk_timeout', 'owner_id', 'afk_channel_id', 'splash', 'verification_level') @@ -526,7 +536,7 @@ class HTTPClient: k: v for k, v in fields.items() if k in valid_keys } - return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload) + return self.request(Route('PATCH', '/guilds/{guild_id}', guild_id=guild_id), json=payload, reason=reason) def get_bans(self, guild_id): return self.request(Route('GET', '/guilds/{guild_id}/bans', guild_id=guild_id)) @@ -534,15 +544,15 @@ class HTTPClient: def get_vanity_code(self, guild_id): return self.request(Route('GET', '/guilds/{guild_id}/vanity-url', guild_id=guild_id)) - def change_vanity_code(self, guild_id, code): + def change_vanity_code(self, guild_id, code, *, reason=None): payload = { 'code': code } - return self.request(Route('PATCH', '/guilds/{guild_id}/vanity-url', guild_id=guild_id), json=payload) + return self.request(Route('PATCH', '/guilds/{guild_id}/vanity-url', guild_id=guild_id), json=payload, reason=reason) - def prune_members(self, guild_id, days): + def prune_members(self, guild_id, days, *, reason=None): params = { 'days': days } - return self.request(Route('POST', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params) + return self.request(Route('POST', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params, reason=reason) def estimate_pruned_members(self, guild_id, days): params = { @@ -550,24 +560,25 @@ class HTTPClient: } return self.request(Route('GET', '/guilds/{guild_id}/prune', guild_id=guild_id), params=params) - def create_custom_emoji(self, guild_id, name, image): + def create_custom_emoji(self, guild_id, name, image, *, reason=None): payload = { 'name': name, 'image': image } r = Route('POST', '/guilds/{guild_id}/emojis', guild_id=guild_id) - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) - def delete_custom_emoji(self, guild_id, emoji_id): - return self.request(Route('DELETE', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id)) + def delete_custom_emoji(self, guild_id, emoji_id, *, reason=None): + r = Route('DELETE', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) + return self.request(r, reason=reason) - def edit_custom_emoji(self, guild_id, emoji_id, *, name): + def edit_custom_emoji(self, guild_id, emoji_id, *, name, reason=None): payload = { 'name': name } r = Route('PATCH', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) def get_audit_logs(self, guild_id, limit=100, before=None, after=None, user_id=None, action_type=None): params = { 'limit': limit } @@ -585,7 +596,7 @@ class HTTPClient: # Invite management - def create_invite(self, channel_id, **options): + def create_invite(self, channel_id, *, reason=None, **options): r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) payload = { 'max_age': options.get('max_age', 0), @@ -594,7 +605,7 @@ class HTTPClient: 'unique': options.get('unique', True) } - return self.request(r, json=payload) + return self.request(r, reason=reason, json=payload) def get_invite(self, invite_id): return self.request(Route('GET', '/invite/{invite_id}', invite_id=invite_id)) @@ -605,45 +616,45 @@ class HTTPClient: def invites_from_channel(self, channel_id): return self.request(Route('GET', '/channels/{channel_id}/invites', channel_id=channel_id)) - def delete_invite(self, invite_id): - return self.request(Route('DELETE', '/invite/{invite_id}', invite_id=invite_id)) + def delete_invite(self, invite_id, *, reason=None): + return self.request(Route('DELETE', '/invite/{invite_id}', invite_id=invite_id), reason=reason) # Role management - def edit_role(self, guild_id, role_id, **fields): + def edit_role(self, guild_id, role_id, *, reason=None, **fields): r = Route('PATCH', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) valid_keys = ('name', 'permissions', 'color', 'hoist', 'mentionable') payload = { k: v for k, v in fields.items() if k in valid_keys } - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) - def delete_role(self, guild_id, role_id): + def delete_role(self, guild_id, role_id, *, reason=None): r = Route('DELETE', '/guilds/{guild_id}/roles/{role_id}', guild_id=guild_id, role_id=role_id) - return self.request(r) + return self.request(r, reason=reason) - def replace_roles(self, user_id, guild_id, role_ids): - return self.edit_member(guild_id=guild_id, user_id=user_id, roles=role_ids) + def replace_roles(self, user_id, guild_id, role_ids, *, reason=None): + return self.edit_member(guild_id=guild_id, user_id=user_id, roles=role_ids, reason=reason) - def create_role(self, guild_id, **fields): + def create_role(self, guild_id, *, reason=None, **fields): r = Route('POST', '/guilds/{guild_id}/roles', guild_id=guild_id) - return self.request(r, json=fields) + return self.request(r, json=fields, reason=reason) - def move_role_position(self, guild_id, positions): + def move_role_position(self, guild_id, positions, *, reason=None): r = Route('PATCH', '/guilds/{guild_id}/roles', guild_id=guild_id) - return self.request(r, json=positions) + return self.request(r, json=positions, reason=reason) - def add_role(self, guild_id, user_id, role_id): + def add_role(self, guild_id, user_id, role_id, *, reason=None): r = Route('PUT', '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', guild_id=guild_id, user_id=user_id, role_id=role_id) - return self.request(r) + return self.request(r, reason=reason) - def remove_role(self, guild_id, user_id, role_id): + def remove_role(self, guild_id, user_id, role_id, *, reason=None): r = Route('DELETE', '/guilds/{guild_id}/members/{user_id}/roles/{role_id}', guild_id=guild_id, user_id=user_id, role_id=role_id) - return self.request(r) + return self.request(r, reason=reason) - def edit_channel_permissions(self, channel_id, target, allow, deny, type): + def edit_channel_permissions(self, channel_id, target, allow, deny, type, *, reason=None): payload = { 'id': target, 'allow': allow, @@ -651,16 +662,16 @@ class HTTPClient: 'type': type } r = Route('PUT', '/channels/{channel_id}/permissions/{target}', channel_id=channel_id, target=target) - return self.request(r, json=payload) + return self.request(r, json=payload, reason=reason) - def delete_channel_permissions(self, channel_id, target): + def delete_channel_permissions(self, channel_id, target, *, reason=None): r = Route('DELETE', '/channels/{channel_id}/permissions/{target}', channel_id=channel_id, target=target) - return self.request(r) + return self.request(r, reason=reason) # Voice management - def move_member(self, user_id, guild_id, channel_id): - return self.edit_member(guild_id=guild_id, user_id=user_id, channel_id=channel_id) + def move_member(self, user_id, guild_id, channel_id, *, reason=None): + return self.edit_member(guild_id=guild_id, user_id=user_id, channel_id=channel_id, reason=reason) # Relationship related diff --git a/discord/invite.py b/discord/invite.py index 17e3aecd5..44aa90f6b 100644 --- a/discord/invite.py +++ b/discord/invite.py @@ -147,11 +147,16 @@ class Invite(Hashable): yield from self._state.http.accept_invite(self.code) @asyncio.coroutine - def delete(self): + def delete(self, *, reason=None): """|coro| Revokes the instant invite. + Parameters + ----------- + reason: Optional[str] + The reason for deleting this invite. Shows up on the audit log. + Raises ------- Forbidden @@ -162,4 +167,4 @@ class Invite(Hashable): Revoking the invite failed. """ - yield from self._state.http.delete_invite(self.code) + yield from self._state.http.delete_invite(self.code, reason=reason) diff --git a/discord/member.py b/discord/member.py index 5cc722e7b..3df44c482 100644 --- a/discord/member.py +++ b/discord/member.py @@ -349,12 +349,12 @@ class Member(discord.abc.Messageable): yield from self.guild.ban(self, **kwargs) @asyncio.coroutine - def unban(self): + def unban(self, *, reason=None): """|coro| Unbans this member. Equivalent to :meth:`Guild.unban` """ - yield from self.guild.unban(self) + yield from self.guild.unban(self, reason=reason) @asyncio.coroutine def kick(self, *, reason=None): @@ -365,7 +365,7 @@ class Member(discord.abc.Messageable): yield from self.guild.kick(self, reason=reason) @asyncio.coroutine - def edit(self, **fields): + def edit(self, *, reason=None, **fields): """|coro| Edits the member's data. @@ -400,6 +400,8 @@ class Member(discord.abc.Messageable): The member's new list of roles. This *replaces* the roles. voice_channel: :class:`VoiceChannel` The voice channel to move the member to. + reason: Optional[str] + The reason for editing this member. Shows up on the audit log. Raises ------- @@ -420,7 +422,7 @@ class Member(discord.abc.Messageable): else: nick = nick if nick else '' if self._state.self_id == self.id: - yield from http.change_my_nickname(guild_id, nick) + yield from http.change_my_nickname(guild_id, nick, reason=reason) else: payload['nick'] = nick @@ -446,12 +448,12 @@ class Member(discord.abc.Messageable): else: payload['roles'] = tuple(r.id for r in roles) - yield from http.edit_member(guild_id, self.id, **payload) + yield from http.edit_member(guild_id, self.id, reason=reason, **payload) # TODO: wait for WS event for modify-in-place behaviour @asyncio.coroutine - def move_to(self, channel): + def move_to(self, channel, *, reason=None): """|coro| Moves a member to a new voice channel (they must be connected first). @@ -465,11 +467,13 @@ class Member(discord.abc.Messageable): ----------- channel: :class:`VoiceChannel` The new voice channel to move the member to. + reason: Optional[str] + The reason for doing this action. Shows up on the audit log. """ - yield from self.edit(voice_channel=channel) + yield from self.edit(voice_channel=channel, reason=reason) @asyncio.coroutine - def add_roles(self, *roles): + def add_roles(self, *roles, reason=None): """|coro| Gives the member a number of :class:`Role`\s. @@ -481,6 +485,8 @@ class Member(discord.abc.Messageable): ----------- \*roles An argument list of :class:`Role`\s to give the member. + reason: Optional[str] + The reason for adding these roles. Shows up on the audit log. Raises ------- @@ -491,10 +497,10 @@ class Member(discord.abc.Messageable): """ new_roles = utils._unique(r for s in (self.roles[1:], roles) for r in s) - yield from self.edit(roles=new_roles) + yield from self.edit(roles=new_roles, reason=reason) @asyncio.coroutine - def remove_roles(self, *roles): + def remove_roles(self, *roles, reason=None): """|coro| Removes :class:`Role`\s from this member. @@ -506,6 +512,8 @@ class Member(discord.abc.Messageable): ----------- \*roles An argument list of :class:`Role`\s to remove from the member. + reason: Optional[str] + The reason for removing these roles. Shows up on the audit log. Raises ------- @@ -522,4 +530,4 @@ class Member(discord.abc.Messageable): except ValueError: pass - yield from self.edit(roles=new_roles) + yield from self.edit(roles=new_roles, reason=reason) diff --git a/discord/message.py b/discord/message.py index 08122fefb..39d573edd 100644 --- a/discord/message.py +++ b/discord/message.py @@ -407,7 +407,7 @@ class Message: return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self) @asyncio.coroutine - def delete(self): + def delete(self, *, reason=None): """|coro| Deletes the message. @@ -416,6 +416,11 @@ class Message: delete other people's messages, you need the :attr:`Permissions.manage_messages` permission. + Parameters + ------------ + reason: Optional[str] + The reason for deleting this message. Shows up on the audit log. + Raises ------ Forbidden @@ -423,7 +428,7 @@ class Message: HTTPException Deleting the message failed. """ - yield from self._state.http.delete_message(self.channel.id, self.id) + yield from self._state.http.delete_message(self.channel.id, self.id, reason=reason) @asyncio.coroutine def edit(self, **fields): diff --git a/discord/role.py b/discord/role.py index 3a1d6c8ad..088a076b1 100644 --- a/discord/role.py +++ b/discord/role.py @@ -160,7 +160,7 @@ class Role(Hashable): return [member for member in all_members if self in member.roles] @asyncio.coroutine - def _move(self, position): + def _move(self, position, reason): if position <= 0: raise InvalidArgument("Cannot move role to position 0 or below") @@ -184,10 +184,10 @@ class Role(Hashable): roles.append(self.id) payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)] - yield from http.move_role_position(self.guild.id, payload) + yield from http.move_role_position(self.guild.id, payload, reason=reason) @asyncio.coroutine - def edit(self, **fields): + def edit(self, *, reason=None, **fields): """|coro| Edits the role. @@ -212,6 +212,8 @@ class Role(Hashable): position: int The new role's position. This must be below your top role's position or it will fail. + reason: Optional[str] + The reason for editing this role. Shows up on the audit log. Raises ------- @@ -226,7 +228,7 @@ class Role(Hashable): position = fields.get('position') if position is not None: - yield from self._move(position) + yield from self._move(position, reason=reason) self.position = position try: @@ -242,11 +244,11 @@ class Role(Hashable): 'mentionable': fields.get('mentionable', self.mentionable) } - data = yield from self._state.http.edit_role(self.guild.id, self.id, **payload) + data = yield from self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) self._update(data) @asyncio.coroutine - def delete(self): + def delete(self, *, reason=None): """|coro| Deletes the role. @@ -254,6 +256,11 @@ class Role(Hashable): You must have the :attr:`Permissions.manage_roles` permission to use this. + Parameters + ----------- + reason: Optional[str] + The reason for deleting this role. Shows up on the audit log. + Raises -------- Forbidden @@ -262,4 +269,4 @@ class Role(Hashable): Deleting the role failed. """ - yield from self._state.http.delete_role(self.guild.id, self.id) + yield from self._state.http.delete_role(self.guild.id, self.id, reason=reason)