'use strict'; const { Collection } = require('@discordjs/collection'); const { GuildScheduledEvent } = require('./GuildScheduledEvent'); const Integration = require('./Integration'); const Invite = require('./Invite'); const { StageInstance } = require('./StageInstance'); const { Sticker } = require('./Sticker'); const Webhook = require('./Webhook'); const { OverwriteTypes, PartialTypes } = require('../util/Constants'); const SnowflakeUtil = require('../util/SnowflakeUtil'); const Util = require('../util/Util'); /** * The target type of an entry. Here are the available types: * * GUILD * * CHANNEL * * USER * * ROLE * * INVITE * * WEBHOOK * * EMOJI * * MESSAGE * * INTEGRATION * * STAGE_INSTANCE * * STICKER * * THREAD * * GUILD_SCHEDULED_EVENT * @typedef {string} AuditLogTargetType */ /** * Key mirror of all available audit log targets. * @name GuildAuditLogs.Targets * @type {Object} */ const Targets = { ALL: 'ALL', GUILD: 'GUILD', GUILD_SCHEDULED_EVENT: 'GUILD_SCHEDULED_EVENT', CHANNEL: 'CHANNEL', USER: 'USER', ROLE: 'ROLE', INVITE: 'INVITE', WEBHOOK: 'WEBHOOK', EMOJI: 'EMOJI', MESSAGE: 'MESSAGE', INTEGRATION: 'INTEGRATION', STAGE_INSTANCE: 'STAGE_INSTANCE', STICKER: 'STICKER', THREAD: 'THREAD', UNKNOWN: 'UNKNOWN', }; /** * The action of an entry. Here are the available actions: * * ALL: null * * 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 * * MEMBER_MOVE: 26 * * MEMBER_DISCONNECT: 27 * * BOT_ADD: 28, * * 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 * * MESSAGE_DELETE: 72 * * MESSAGE_BULK_DELETE: 73 * * MESSAGE_PIN: 74 * * MESSAGE_UNPIN: 75 * * INTEGRATION_CREATE: 80 * * INTEGRATION_UPDATE: 81 * * INTEGRATION_DELETE: 82 * * STAGE_INSTANCE_CREATE: 83 * * STAGE_INSTANCE_UPDATE: 84 * * STAGE_INSTANCE_DELETE: 85 * * STICKER_CREATE: 90 * * STICKER_UPDATE: 91 * * STICKER_DELETE: 92 * * GUILD_SCHEDULED_EVENT_CREATE: 100 * * GUILD_SCHEDULED_EVENT_UPDATE: 101 * * GUILD_SCHEDULED_EVENT_DELETE: 102 * * THREAD_CREATE: 110 * * THREAD_UPDATE: 111 * * THREAD_DELETE: 112 * @typedef {?(number|string)} AuditLogAction * @see {@link https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events} */ /** * All available actions keyed under their names to their numeric values. * @name GuildAuditLogs.Actions * @type {Object} */ const Actions = { ALL: null, 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, MEMBER_MOVE: 26, MEMBER_DISCONNECT: 27, BOT_ADD: 28, 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, MESSAGE_DELETE: 72, MESSAGE_BULK_DELETE: 73, MESSAGE_PIN: 74, MESSAGE_UNPIN: 75, INTEGRATION_CREATE: 80, INTEGRATION_UPDATE: 81, INTEGRATION_DELETE: 82, STAGE_INSTANCE_CREATE: 83, STAGE_INSTANCE_UPDATE: 84, STAGE_INSTANCE_DELETE: 85, STICKER_CREATE: 90, STICKER_UPDATE: 91, STICKER_DELETE: 92, GUILD_SCHEDULED_EVENT_CREATE: 100, GUILD_SCHEDULED_EVENT_UPDATE: 101, GUILD_SCHEDULED_EVENT_DELETE: 102, THREAD_CREATE: 110, THREAD_UPDATE: 111, THREAD_DELETE: 112, }; /** * Audit logs entries are held in this class. */ class GuildAuditLogs { constructor(guild, data) { if (data.users) for (const user of data.users) guild.client.users._add(user); if (data.threads) for (const thread of data.threads) guild.client.channels._add(thread, guild); /** * Cached webhooks * @type {Collection} * @private */ this.webhooks = new Collection(); if (data.webhooks) { for (const hook of data.webhooks) { this.webhooks.set(hook.id, new Webhook(guild.client, hook)); } } /** * Cached integrations * @type {Collection} * @private */ this.integrations = new Collection(); if (data.integrations) { for (const integration of data.integrations) { this.integrations.set(integration.id, new Integration(guild.client, integration, guild)); } } /** * The entries for this guild's audit logs * @type {Collection} */ this.entries = new Collection(); for (const item of data.audit_log_entries) { const entry = new GuildAuditLogsEntry(this, guild, item); this.entries.set(entry.id, entry); } } /** * Handles possible promises for entry targets. * @returns {Promise} */ static async build(...args) { const logs = new GuildAuditLogs(...args); await Promise.all(logs.entries.map(e => e.target)); return logs; } /** * The target of an entry. It can be one of: * * A guild * * A channel * * A user * * A role * * An invite * * A webhook * * An emoji * * A message * * An integration * * A stage instance * * A sticker * * A guild scheduled event * * A thread * * An object with an id key if target was deleted * * An object where the keys represent either the new value or the old value * @typedef {?(Object|Guild|Channel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance|Sticker| * GuildScheduledEvent)} AuditLogEntryTarget */ /** * Finds the target type from the entry action. * @param {AuditLogAction} target The action target * @returns {AuditLogTargetType} */ 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; if (target < 80) return Targets.MESSAGE; if (target < 83) return Targets.INTEGRATION; if (target < 86) return Targets.STAGE_INSTANCE; if (target < 100) return Targets.STICKER; if (target < 110) return Targets.GUILD_SCHEDULED_EVENT; if (target < 120) return Targets.THREAD; return Targets.UNKNOWN; } /** * The action type of an entry, e.g. `CREATE`. Here are the available types: * * CREATE * * DELETE * * UPDATE * * ALL * @typedef {string} AuditLogActionType */ /** * Finds the action type from the entry action. * @param {AuditLogAction} action The action target * @returns {AuditLogActionType} */ static actionType(action) { if ( [ Actions.CHANNEL_CREATE, Actions.CHANNEL_OVERWRITE_CREATE, Actions.MEMBER_BAN_REMOVE, Actions.BOT_ADD, Actions.ROLE_CREATE, Actions.INVITE_CREATE, Actions.WEBHOOK_CREATE, Actions.EMOJI_CREATE, Actions.MESSAGE_PIN, Actions.INTEGRATION_CREATE, Actions.STAGE_INSTANCE_CREATE, Actions.STICKER_CREATE, Actions.GUILD_SCHEDULED_EVENT_CREATE, Actions.THREAD_CREATE, ].includes(action) ) { return 'CREATE'; } if ( [ Actions.CHANNEL_DELETE, Actions.CHANNEL_OVERWRITE_DELETE, Actions.MEMBER_KICK, Actions.MEMBER_PRUNE, Actions.MEMBER_BAN_ADD, Actions.MEMBER_DISCONNECT, Actions.ROLE_DELETE, Actions.INVITE_DELETE, Actions.WEBHOOK_DELETE, Actions.EMOJI_DELETE, Actions.MESSAGE_DELETE, Actions.MESSAGE_BULK_DELETE, Actions.MESSAGE_UNPIN, Actions.INTEGRATION_DELETE, Actions.STAGE_INSTANCE_DELETE, Actions.STICKER_DELETE, Actions.GUILD_SCHEDULED_EVENT_DELETE, Actions.THREAD_DELETE, ].includes(action) ) { return 'DELETE'; } if ( [ Actions.GUILD_UPDATE, Actions.CHANNEL_UPDATE, Actions.CHANNEL_OVERWRITE_UPDATE, Actions.MEMBER_UPDATE, Actions.MEMBER_ROLE_UPDATE, Actions.MEMBER_MOVE, Actions.ROLE_UPDATE, Actions.INVITE_UPDATE, Actions.WEBHOOK_UPDATE, Actions.EMOJI_UPDATE, Actions.INTEGRATION_UPDATE, Actions.STAGE_INSTANCE_UPDATE, Actions.STICKER_UPDATE, Actions.GUILD_SCHEDULED_EVENT_UPDATE, Actions.THREAD_UPDATE, ].includes(action) ) { return 'UPDATE'; } return 'ALL'; } toJSON() { return Util.flatten(this); } } /** * Audit logs entry. */ class GuildAuditLogsEntry { constructor(logs, guild, data) { const targetType = GuildAuditLogs.targetType(data.action_type); /** * The target type of this entry * @type {AuditLogTargetType} */ this.targetType = targetType; /** * The action type of this entry * @type {AuditLogActionType} */ this.actionType = GuildAuditLogs.actionType(data.action_type); /** * Specific action type of this entry in its string presentation * @type {AuditLogAction} */ this.action = Object.keys(Actions).find(k => Actions[k] === data.action_type); /** * The reason of this entry * @type {?string} */ this.reason = data.reason ?? null; /** * The user that executed this entry * @type {?User} */ this.executor = data.user_id ? guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users._add({ id: data.user_id }) : guild.client.users.cache.get(data.user_id) : null; /** * An entry in the audit log representing a specific change. * @typedef {Object} AuditLogChange * @property {string} key The property that was changed, e.g. `nick` for nickname changes * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname * @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname */ /** * Specific property changes * @type {?AuditLogChange[]} */ this.changes = data.changes?.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) ?? null; /** * The entry's id * @type {Snowflake} */ this.id = data.id; /** * Any extra data from the entry * @type {?(Object|Role|GuildMember)} */ this.extra = null; switch (data.action_type) { case Actions.MEMBER_PRUNE: this.extra = { removed: Number(data.options.members_removed), days: Number(data.options.delete_member_days), }; break; case Actions.MEMBER_MOVE: case Actions.MESSAGE_DELETE: case Actions.MESSAGE_BULK_DELETE: this.extra = { channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, count: Number(data.options.count), }; break; case Actions.MESSAGE_PIN: case Actions.MESSAGE_UNPIN: this.extra = { channel: guild.client.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, messageId: data.options.message_id, }; break; case Actions.MEMBER_DISCONNECT: this.extra = { count: Number(data.options.count), }; break; case Actions.CHANNEL_OVERWRITE_CREATE: case Actions.CHANNEL_OVERWRITE_UPDATE: case Actions.CHANNEL_OVERWRITE_DELETE: switch (Number(data.options.type)) { case OverwriteTypes.role: this.extra = guild.roles.cache.get(data.options.id) ?? { id: data.options.id, name: data.options.role_name, type: OverwriteTypes[OverwriteTypes.role], }; break; case OverwriteTypes.member: this.extra = guild.members.cache.get(data.options.id) ?? { id: data.options.id, type: OverwriteTypes[OverwriteTypes.member], }; break; default: break; } break; case Actions.STAGE_INSTANCE_CREATE: case Actions.STAGE_INSTANCE_DELETE: case Actions.STAGE_INSTANCE_UPDATE: this.extra = { channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id }, }; break; default: break; } /** * The target of this entry * @type {?AuditLogEntryTarget} */ this.target = null; if (targetType === Targets.UNKNOWN) { this.target = this.changes.reduce((o, c) => { o[c.key] = c.new ?? c.old; return o; }, {}); this.target.id = data.target_id; // MEMBER_DISCONNECT and similar types do not provide a target_id. } else if (targetType === Targets.USER && data.target_id) { this.target = guild.client.options.partials.includes(PartialTypes.USER) ? guild.client.users._add({ id: data.target_id }) : guild.client.users.cache.get(data.target_id); } else if (targetType === Targets.GUILD) { this.target = guild.client.guilds.cache.get(data.target_id); } else if (targetType === Targets.WEBHOOK) { this.target = logs.webhooks.get(data.target_id) ?? new Webhook( guild.client, this.changes.reduce( (o, c) => { o[c.key] = c.new ?? c.old; return o; }, { id: data.target_id, guild_id: guild.id, }, ), ); } else if (targetType === Targets.INVITE) { let change = this.changes.find(c => c.key === 'code'); change = change.new ?? change.old; this.target = guild.invites.cache.get(change) ?? new Invite( guild.client, this.changes.reduce( (o, c) => { o[c.key] = c.new ?? c.old; return o; }, { guild }, ), ); } else if (targetType === Targets.MESSAGE) { // Discord sends a channel id for the MESSAGE_BULK_DELETE action type. this.target = data.action_type === Actions.MESSAGE_BULK_DELETE ? guild.channels.cache.get(data.target_id) ?? { id: data.target_id } : guild.client.users.cache.get(data.target_id); } else if (targetType === Targets.INTEGRATION) { this.target = logs.integrations.get(data.target_id) ?? new Integration( guild.client, this.changes.reduce( (o, c) => { o[c.key] = c.new ?? c.old; return o; }, { id: data.target_id }, ), guild, ); } else if (targetType === Targets.CHANNEL || targetType === Targets.THREAD) { this.target = guild.channels.cache.get(data.target_id) ?? this.changes.reduce( (o, c) => { o[c.key] = c.new ?? c.old; return o; }, { id: data.target_id }, ); } else if (targetType === Targets.STAGE_INSTANCE) { this.target = guild.stageInstances.cache.get(data.target_id) ?? new StageInstance( guild.client, this.changes.reduce( (o, c) => { o[c.key] = c.new ?? c.old; return o; }, { id: data.target_id, channel_id: data.options?.channel_id, guild_id: guild.id, }, ), ); } else if (targetType === Targets.STICKER) { this.target = guild.stickers.cache.get(data.target_id) ?? new Sticker( guild.client, this.changes.reduce( (o, c) => { o[c.key] = c.new ?? c.old; return o; }, { id: data.target_id }, ), ); } else if (targetType === Targets.GUILD_SCHEDULED_EVENT) { this.target = guild.scheduledEvents.cache.get(data.target_id) ?? new GuildScheduledEvent( guild.client, this.changes.reduce( (o, c) => { o[c.key] = c.new ?? c.old; return o; }, { id: data.target_id, guild_id: guild.id }, ), ); } else if (data.target_id) { this.target = guild[`${targetType.toLowerCase()}s`]?.cache.get(data.target_id) ?? { id: data.target_id }; } } /** * The timestamp this entry was created at * @type {number} * @readonly */ get createdTimestamp() { return SnowflakeUtil.timestampFrom(this.id); } /** * The time this entry was created at * @type {Date} * @readonly */ get createdAt() { return new Date(this.createdTimestamp); } toJSON() { return Util.flatten(this, { createdTimestamp: true }); } } GuildAuditLogs.Actions = Actions; GuildAuditLogs.Targets = Targets; GuildAuditLogs.Entry = GuildAuditLogsEntry; module.exports = GuildAuditLogs;