Skip to content

Commit

Permalink
Experimental support for Audit Logs (discordjs#1403)
Browse files Browse the repository at this point in the history
* 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

* <.<
  • Loading branch information
devsnek authored and Ratismal committed Jul 5, 2017
1 parent 78d05b1 commit d72af1e
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 27 deletions.
50 changes: 34 additions & 16 deletions src/client/rest/RESTMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 31 additions & 5 deletions src/structures/Guild.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,13 @@ class Guild {
* @returns {Promise<Collection<Snowflake, User>>}
*/
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;
});
}

/**
Expand All @@ -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<GuildAuditLogs>}
*/
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
Expand Down Expand Up @@ -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<GuildMember|User|string>} 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.
Expand All @@ -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);
}

/**
Expand Down
196 changes: 196 additions & 0 deletions src/structures/GuildAuditLogs.js
Original file line number Diff line number Diff line change
@@ -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<Invite>|Promise<Webhook>}
*/
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;
14 changes: 8 additions & 6 deletions src/structures/GuildMember.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,23 +472,25 @@ class GuildMember {

/**
* Kick this member from the guild
* @param {string} [reason] Reason for kicking user
* @returns {Promise<GuildMember>}
*/
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<GuildMember>}
* @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);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/util/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/util/Permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit d72af1e

Please sign in to comment.