diff --git a/structure/client/components/observers/AuditLog.js b/structure/client/components/observers/AuditLog.js new file mode 100644 index 0000000..7f012cb --- /dev/null +++ b/structure/client/components/observers/AuditLog.js @@ -0,0 +1,124 @@ +const { InfractionResolves } = require("../../../../util/Constants"); +const { Observer } = require("../../../interfaces"); + +const { Infraction } = require('../../../moderation/interfaces/'); + +class AuditLogObserver extends Observer { + + constructor(client) { + + super(client, { + name: 'auditLog', + priority: 0, + disabled: false + }); + + this.client = client; + + this.hooks = [ + ['guildBanAdd', this.guildBanAdd.bind(this)], + ['guildBanRemove', this.guildBanRemove.bind(this)], + ['guildMemberRemove', this.guildMemberRemove.bind(this)], + ['guildMemberUpdate', this.guildMemberUpdate.bind(this)] + ]; + + } + + async guildBanAdd(guild, user) { + const settings = guild.settings(); + if(!settings.moderationLog.channel || !settings.moderationLog.infractions.includes('BAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. + const audit = await this._fetchFirstEntry(guild, user, 'MEMBER_BAN_ADD'); + if(!audit || audit.executor.id === this.client.user.id) return undefined; + new Infraction(this.client, { + type: 'BAN', + targetType: 'USER', + guild, + target: user, + executor: audit.executor, + reason: `${audit.reason || 'N/A'}\n*This action was performed without the bot.*` + }).handle(); + } + + async guildBanRemove(guild, user) { + const settings = guild.settings(); + if(!settings.moderationLog.channel || !settings.moderationLog.infractions.includes('UNBAN')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. + const audit = await this._fetchFirstEntry(guild, user, 'MEMBER_BAN_REMOVE'); + if(!audit || audit.executor.id === this.client.user.id) return undefined; + new Infraction(this.client, { + type: 'UNBAN', + targetType: 'USER', + guild, + target: user, + executor: audit.executor, + reason: `*This action was performed without the bot.*` + }).handle(); + } + + async guildMemberRemove(member) { + const settings = member.guild.settings(); + if(!settings.moderationLog.channel || !settings.moderationLog.infractions.includes('KICK')) return undefined; //This is checked by the infraction handling, but it may save resources if checked earlier. + const audit = await this._fetchFirstEntry(member.guild, member.user, 'MEMBER_KICK'); + if(!audit || audit.executor.id === this.client.user.id) return undefined; + new Infraction(this.client, { + type: 'KICK', + targetType: 'USER', + guild: member.guild, + target: member.user, + executor: audit.executor, + reason: `${audit.reason || 'N/A'}\n*This action was performed without the bot.*` + }).handle(); + } + + async guildMemberUpdate(oldMember, newMember) { + if(oldMember.roles.cache.size === newMember.roles.cache.size) return undefined; + const { guild } = newMember; + const settings = await guild.settings(); + if(!settings.moderationLog.channel) return undefined; + + const mutedRole = settings.mute.role; + if(!mutedRole) return undefined; + + if(!oldMember.roles.cache.has(mutedRole) && newMember.roles.cache.has(mutedRole)) { //Member was assigned muted role. + if(!settings.moderationLog.infractions.includes('MUTE')) return undefined; + const audit = await this._fetchFirstEntry(newMember.guild, newMember.user, 'MEMBER_ROLE_UPDATE'); + if(!audit || audit.executor.id === this.client.user.id) return undefined; + new Infraction(this.client, { + type: 'MUTE', + targetType: 'USER', + guild: newMember.guild, + target: newMember.user, + executor: audit.executor, + reason: "*This action was performed without the bot.*" + }).handle(); + + } else if(oldMember.roles.cache.has(mutedRole) && !newMember.roles.cache.has(mutedRole)) { //Member was unassigned muted role. + if(!settings.moderationLog.infractions.includes('UNMUTE')) return undefined; + const audit = await this._fetchFirstEntry(newMember.guild, newMember.user, 'MEMBER_ROLE_UPDATE'); + if(!audit || audit.executor.id === this.client.user.id) return undefined; + new Infraction(this.client, { + type: 'UNMUTE', + targetType: 'USER', + guild: newMember.guild, + target: newMember.user, + executor: audit.executor, + reason: "*This action was performed without the bot.*" + }).handle(); + } + + } + + async _fetchFirstEntry(guild, user, type) { + const audit = await guild.fetchAuditLogs({ limit: 2 }); //Just incase >_> + if(audit.entries.size === 0) return null; + + const entry = audit.entries.filter((e) => e.target.id === user.id).first(); + if(!entry || entry.executor.bot) return null; + if(entry.target.id !== user.id) return null; + if(entry.action !== type) return null; + + return entry; + } + +} + +module.exports = AuditLogObserver; \ No newline at end of file diff --git a/structure/moderation/interfaces/Infraction.js b/structure/moderation/interfaces/Infraction.js index 252c672..2372fb5 100644 --- a/structure/moderation/interfaces/Infraction.js +++ b/structure/moderation/interfaces/Infraction.js @@ -45,7 +45,7 @@ class Infraction { this.duration = isNaN(data.duration) ? null : data.duration; //How long the action will last. Must be in milliseconds. this.callback = isNaN(data.duration) ? null : Date.now() + data.duration * 1000; //At what epoch(?) time it will callback. - this.reason = data.reason.length ? data.reason : 'N/A'; + this.reason = data.reason || 'N/A'; this.points = data.points || 0; this.totalPoints = 0; @@ -66,7 +66,6 @@ class Infraction { this._callbacked = Boolean(data._callbacked); this._fetched = Boolean(data); - } async handle() { @@ -191,7 +190,7 @@ class Infraction { id: `${this.guildId}:${this.case}`, guild: this.guildId, channel: this.channelId, - channelName: this.channel.display, + channelName: this.channel?.display, message: this.messageId, executor: this.executorId, executorTag: this.executor.display,