Skip to content

Commit

Permalink
Experimental support for Audit Logs (#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 amishshah committed Apr 29, 2017
1 parent b0a3528 commit 4127cf6
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 @@ -347,7 +347,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 @@ -374,6 +380,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 @@ -607,8 +627,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 @@ -618,8 +639,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

1 comment on commit 4127cf6

@devsnek
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wtf is this shitcode who decided to merge this you should be ashamed

Please sign in to comment.