From d72af1e171d2851e13c19a8f18a70345b889fc2e Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sat, 29 Apr 2017 14:34:57 -0500 Subject: [PATCH] Experimental support for Audit Logs (#1403) * start audit logs * make better var types so gawdl3y doesn't shit on this * add constructor stuff * make more changes * add entry creation * add methods * make it all work hopefully * aaa * aaaa * i wish i could test this locally * fix users, guild when i feel like it * make guild prop non-enumerable * make better types * change nouns * e * Update GuildAuditLogs.js * Update GuildAuditLogs.js * Update GuildAuditLogs.js * eek * Update GuildAuditLogs.js * Update GuildAuditLogs.js * friggin trailing spaces * Update GuildAuditLogs.js * docs! * Update GuildAuditLogs.js * reason stuff * Update GuildAuditLogs.js * Update GuildAuditLogs.js * support before/after for pagination * Update Guild.js * Update GuildAuditLogs.js * mfw using github web editor * fix build * Update Guild.js * amazing cache fuckery shit evil * cool stuff * make building audit logs nicer * ban endpoint stuff * dox * <.< --- src/client/rest/RESTMethods.js | 50 +++++--- src/structures/Guild.js | 36 +++++- src/structures/GuildAuditLogs.js | 196 +++++++++++++++++++++++++++++++ src/structures/GuildMember.js | 14 ++- src/util/Constants.js | 1 + src/util/Permissions.js | 1 + 6 files changed, 271 insertions(+), 27 deletions(-) create mode 100644 src/structures/GuildAuditLogs.js diff --git a/src/client/rest/RESTMethods.js b/src/client/rest/RESTMethods.js index d40e9aa3ac90..69d29ecbad12 100644 --- a/src/client/rest/RESTMethods.js +++ b/src/client/rest/RESTMethods.js @@ -19,6 +19,7 @@ const Channel = require('../../structures/Channel'); const GroupDMChannel = require('../../structures/GroupDMChannel'); const Guild = require('../../structures/Guild'); const VoiceRegion = require('../../structures/VoiceRegion'); +const GuildAuditLogs = require('../../structures/GuildAuditLogs'); class RESTMethods { constructor(restManager) { @@ -386,8 +387,9 @@ class RESTMethods { ); } - kickGuildMember(guild, member) { - return this.rest.makeRequest('delete', Endpoints.Guild(guild).Member(member), true).then(() => + kickGuildMember(guild, member, reason) { + const url = `${Endpoints.Guild(guild).Member(member)}?reason=${reason}`; + return this.rest.makeRequest('delete', url, true).then(() => this.client.actions.GuildMemberRemove.handle({ guild_id: guild.id, user: member.user, @@ -530,14 +532,12 @@ class RESTMethods { return this.rest.makeRequest('post', Endpoints.Channel(channelID).typing, true); } - banGuildMember(guild, member, deleteDays = 0) { + banGuildMember(guild, member, options) { const id = this.client.resolver.resolveUserID(member); if (!id) return Promise.reject(new Error('Couldn\'t resolve the user ID to ban.')); - return this.rest.makeRequest( - 'put', `${Endpoints.Guild(guild).bans}/${id}?delete-message-days=${deleteDays}`, true, { - 'delete-message-days': deleteDays, - } - ).then(() => { + + const url = `${Endpoints.Guild(guild).bans}/${id}?${querystring.stringify(options)}`; + return this.rest.makeRequest('put', url, true).then(() => { if (member instanceof GuildMember) return member; const user = this.client.resolver.resolveUser(id); if (user) { @@ -576,14 +576,15 @@ class RESTMethods { } getGuildBans(guild) { - return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(banItems => { - const bannedUsers = new Collection(); - for (const banItem of banItems) { - const user = this.client.dataManager.newUser(banItem.user); - bannedUsers.set(user.id, user); - } - return bannedUsers; - }); + return this.rest.makeRequest('get', Endpoints.Guild(guild).bans, true).then(bans => + bans.reduce((collection, ban) => { + collection.set(ban.user.id, { + reason: ban.reason, + user: this.client.dataManager.newUser(ban.user), + }); + return collection; + }, new Collection()) + ); } updateGuildRole(role, _data) { @@ -674,6 +675,23 @@ class RESTMethods { .then(() => this.client.actions.GuildEmojiDelete.handle(emoji).data); } + getGuildAuditLogs(guild, options = {}) { + if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id; + if (options.after && options.after instanceof GuildAuditLogs.Entry) options.after = options.after.id; + if (typeof options.type === 'string') options.type = GuildAuditLogs.Actions[options.type]; + + const queryString = (querystring.stringify({ + before: options.before, + after: options.after, + limit: options.limit, + user_id: this.client.resolver.resolveUserID(options.user), + action_type: options.type, + }).match(/[^=&?]+=[^=&?]+/g) || []).join('&'); + + return this.rest.makeRequest('get', `${Endpoints.Guild(guild).auditLogs}?${queryString}`, true) + .then(data => GuildAuditLogs.build(guild, data)); + } + getWebhook(id, token) { return this.rest.makeRequest('get', Endpoints.Webhook(id, token), !token).then(data => new Webhook(this.client, data) diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 1a33ed16b25c..66d7ec4602b2 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -402,7 +402,13 @@ class Guild { * @returns {Promise>} */ fetchBans() { - return this.client.rest.methods.getGuildBans(this); + return this.client.rest.methods.getGuildBans(this) + // This entire re-mapping can be removed in the next major release + .then(bans => { + const users = new Collection(); + for (const ban of bans.values()) users.set(ban.user.id, ban.user); + return users; + }); } /** @@ -429,6 +435,20 @@ class Guild { return this.client.rest.methods.fetchVoiceRegions(this.id); } + /** + * Fetch audit logs for this guild + * @param {Object} [options={}] Options for fetching audit logs + * @param {Snowflake|GuildAuditLogsEntry} [options.before] Limit to entries from before specified entry + * @param {Snowflake|GuildAuditLogsEntry} [options.after] Limit to entries from after specified entry + * @param {number} [options.limit] Limit number of entries + * @param {UserResolvable} [options.user] Only show entries involving this user + * @param {string|number} [options.type] Only show entries involving this action type + * @returns {Promise} + */ + fetchAuditLogs(options) { + return this.client.rest.methods.getGuildAuditLogs(this, options); + } + /** * Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission. * @param {UserResolvable} user User to add to the guild @@ -662,8 +682,9 @@ class Guild { /** * Bans a user from the guild. * @param {UserResolvable} user The user to ban - * @param {number} [deleteDays=0] The amount of days worth of messages from this user that should - * also be deleted. Between `0` and `7`. + * @param {Object} [options] Ban options. + * @param {number} [options.days=0] Number of days of messages to delete + * @param {string} [options.reason] Reason for banning * @returns {Promise} Result object will be resolved as specifically as possible. * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot * be resolved, the user ID will be the result. @@ -673,8 +694,13 @@ class Guild { * .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`)) * .catch(console.error); */ - ban(user, deleteDays = 0) { - return this.client.rest.methods.banGuildMember(this, user, deleteDays); + ban(user, options = {}) { + if (typeof options === 'number') { + options = { reason: null, days: options }; + } else if (typeof options === 'string') { + options = { reason: options, days: 0 }; + } + return this.client.rest.methods.banGuildMember(this, user, options); } /** diff --git a/src/structures/GuildAuditLogs.js b/src/structures/GuildAuditLogs.js new file mode 100644 index 000000000000..d83b44fb6618 --- /dev/null +++ b/src/structures/GuildAuditLogs.js @@ -0,0 +1,196 @@ +const Targets = { + GUILD: 'GUILD', + CHANNEL: 'CHANNEL', + USER: 'USER', + ROLE: 'ROLE', + INVITE: 'INVITE', + WEBHOOK: 'WEBHOOK', + EMOJI: 'EMOJI', +}; + +const Actions = { + GUILD_UPDATE: 1, + CHANNEL_CREATE: 10, + CHANNEL_UPDATE: 11, + CHANNEL_DELETE: 12, + CHANNEL_OVERWRITE_CREATE: 13, + CHANNEL_OVERWRITE_UPDATE: 14, + CHANNEL_OVERWRITE_DELETE: 15, + MEMBER_KICK: 20, + MEMBER_PRUNE: 21, + MEMBER_BAN_ADD: 22, + MEMBER_BAN_REMOVE: 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, +}; + +class GuildAuditLogs { + constructor(guild, data) { + if (data.users) for (const user of data.users) guild.client.dataManager.newUser(user); + + /** + * Entries for this Guild's audit logs + * @type {GuildAuditLogsEntry[]} + */ + this.entries = []; + for (const entry of data.audit_log_entries) this.entries.push(new GuildAuditLogsEntry(guild, entry)); + } + + /** + * Handles possible promises for entry targets + * @returns {GuildAuditLogs} + */ + static build(...args) { + return new Promise(resolve => { + const logs = new GuildAuditLogs(...args); + Promise.all(logs.entries.map(e => e.target)).then(() => resolve(logs)); + }); + } + + /** + * Find target type from entry action + * @param {number} target Action target + * @returns {?string} + */ + static targetType(target) { + if (target < 10) return Targets.GUILD; + if (target < 20) return Targets.CHANNEL; + if (target < 30) return Targets.USER; + if (target < 40) return Targets.ROLE; + if (target < 50) return Targets.INVITE; + if (target < 60) return Targets.WEBHOOK; + if (target < 70) return Targets.EMOJI; + return null; + } + + + /** + * Find action type from entry action + * @param {string} action Action target + * @returns {string} + */ + static actionType(action) { + if ([ + Actions.CHANNEL_CREATE, + Actions.CHANNEL_OVERWRITE_CREATE, + Actions.MEMBER_BAN_REMOVE, + Actions.ROLE_CREATE, + Actions.INVITE_CREATE, + Actions.WEBHOOK_CREATE, + Actions.EMOJI_CREATE, + ].includes(action)) return 'CREATE'; + + if ([ + Actions.CHANNEL_DELETE, + Actions.CHANNEL_OVERWRITE_DELETE, + Actions.MEMBER_KICK, + Actions.MEMBER_PRUNE, + Actions.MEMBER_BAN_ADD, + Actions.ROLE_DELETE, + Actions.INVITE_DELETE, + Actions.WEBHOOK_DELETE, + Actions.EMOJI_DELETE, + ].includes(action)) return 'DELETE'; + + if ([ + Actions.GUILD_UPDATE, + Actions.CHANNEL_UPDATE, + Actions.CHANNEL_OVERWRITE_UPDATE, + Actions.MEMBER_UPDATE, + Actions.ROLE_UPDATE, + Actions.INVITE_UPDATE, + Actions.WEBHOOK_UPDATE, + Actions.EMOJI_UPDATE, + ].includes(action)) return 'UPDATE'; + + return 'ALL'; + } +} + +class GuildAuditLogsEntry { + constructor(guild, data) { + const targetType = GuildAuditLogs.targetType(data.action_type); + /** + * Target type of this entry + * @type {string} + */ + this.targetType = targetType; + + /** + * Action type of this entry + * @type {string} + */ + this.actionType = GuildAuditLogs.actionType(data.action_type); + + /** + * Specific action type of this entry + * @type {string} + */ + this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type); + + /** + * Reason of this entry + * @type {?string} + */ + this.reason = data.reason || null; + + /** + * User that executed this entry + * @type {User} + */ + this.executor = guild.client.users.get(data.user_id); + + /** + * Specific property changes + * @type {Object[]} + */ + this.changes = data.changes ? data.changes.map(c => ({ name: c.key, old: c.old_value, new: c.new_value })) : null; + + /** + * ID of this entry + * @type {Snowflake} + */ + this.id = data.id; + + if (['USER', 'GUILD'].includes(targetType)) { + /** + * Target of this entry + * @type {?Guild|User|Role|Emoji|Promise|Promise} + */ + this.target = guild.client[`${targetType.toLowerCase()}s`].get(data.target_id); + } else if (targetType === 'WEBHOOK') { + this.target = guild.fetchWebhooks() + .then(hooks => { + this.target = hooks.find(h => h.id === data.target_id); + return this.target; + }); + } else if (targetType === 'INVITE') { + const change = this.changes.find(c => c.name === 'code'); + this.target = guild.fetchInvites() + .then(invites => { + this.target = invites.find(i => i.code === (change.new || change.old)); + return this.target; + }); + } else { + this.target = guild[`${targetType.toLowerCase()}s`].get(data.target_id); + } + } +} + +GuildAuditLogs.Actions = Actions; +GuildAuditLogs.Targets = Targets; +GuildAuditLogs.Entry = GuildAuditLogsEntry; + +module.exports = GuildAuditLogs; diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 3785290ebf27..e9c415057880 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -472,23 +472,25 @@ class GuildMember { /** * Kick this member from the guild + * @param {string} [reason] Reason for kicking user * @returns {Promise} */ - kick() { - return this.client.rest.methods.kickGuildMember(this.guild, this); + kick(reason) { + return this.client.rest.methods.kickGuildMember(this.guild, this, reason); } /** * Ban this guild member - * @param {number} [deleteDays=0] The amount of days worth of messages from this member that should - * also be deleted. Between `0` and `7`. + * @param {Object} [options] Ban options. + * @param {number} [options.days=0] Number of days of messages to delete + * @param {string} [options.reason] Reason for banning * @returns {Promise} * @example * // ban a guild member * guildMember.ban(7); */ - ban(deleteDays = 0) { - return this.client.rest.methods.banGuildMember(this.guild, this, deleteDays); + ban(options) { + return this.guild.ban(this, options); } /** diff --git a/src/util/Constants.js b/src/util/Constants.js index 18cd0ec18360..09e0a6fc5fa3 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -132,6 +132,7 @@ const Endpoints = exports.Endpoints = { webhooks: `${base}/webhooks`, ack: `${base}/ack`, settings: `${base}/settings`, + auditLogs: `${base}/audit-logs`, Emoji: emojiID => Endpoints.CDN(root).Emoji(emojiID), Icon: (root, hash) => Endpoints.CDN(root).Icon(guildID, hash), Splash: (root, hash) => Endpoints.CDN(root).Splash(guildID, hash), diff --git a/src/util/Permissions.js b/src/util/Permissions.js index bbc4e4d77b8b..d58144c258bb 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -203,6 +203,7 @@ Permissions.FLAGS = { MANAGE_CHANNELS: 1 << 4, MANAGE_GUILD: 1 << 5, ADD_REACTIONS: 1 << 6, + VIEW_AUDIT_LOG: 1 << 7, READ_MESSAGES: 1 << 10, SEND_MESSAGES: 1 << 11,