diff --git a/package-lock.json b/package-lock.json index 63bce621..4df06edc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2181,9 +2181,9 @@ } }, "knub": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/knub/-/knub-9.6.0.tgz", - "integrity": "sha512-+a/woh8WnSxBkflNjCjvfGASadz80o/0Mot81K9sr8BvcITzeDtoOBaxzeiwCb5NWNtYz/Qp9M7ZZ6Jr5U45bg==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/knub/-/knub-9.6.1.tgz", + "integrity": "sha512-Usydud/TYz8Il/8DLpMSTnAup3nespr3DLlPRnFzmO9mL0hQnCKtqAdp3WoDCmPb6uhqJ3zCe6AH66zRS7VC/w==", "requires": { "escape-string-regexp": "^1.0.5", "js-yaml": "^3.9.1", diff --git a/package.json b/package.json index e26ff678..60c0df6c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "escape-string-regexp": "^1.0.5", "humanize-duration": "^3.15.0", "knex": "^0.14.6", - "knub": "^9.6.0", + "knub": "^9.6.1", "lodash.at": "^4.6.0", "lodash.difference": "^4.5.0", "lodash.intersection": "^4.4.0", diff --git a/src/data/DefaultLogMessages.json b/src/data/DefaultLogMessages.json index f7a00652..301551c9 100644 --- a/src/data/DefaultLogMessages.json +++ b/src/data/DefaultLogMessages.json @@ -6,8 +6,8 @@ "MEMBER_MUTE_EXPIRED": "🔉 **{member.user.username}#{member.user.discriminator}**'s mute expired", "MEMBER_KICK": "👢 **{user.username}#{user.discriminator}** (`{user.id}`) was kicked by {mod.username}#{mod.discriminator}", "MEMBER_BAN": "🔨 **{user.username}#{user.discriminator}** (`{user.id}`) was banned by {mod.username}#{mod.discriminator}", - "MEMBER_UNBAN": "🔓 **{user.username}#{user.discriminator}** (`{user.id}`) was unbanned by {mod.username}#{mod.discriminator}", - "MEMBER_FORCEBAN": "🔨 User `{userId}` was forcebanned by {mod.username}#{mod.discriminator}", + "MEMBER_UNBAN": "🔓 User (`{userId}`) was unbanned by {mod.username}#{mod.discriminator}", + "MEMBER_FORCEBAN": "🔨 User (`{userId}`) was forcebanned by {mod.username}#{mod.discriminator}", "MEMBER_JOIN": "📥 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) joined{new} (created {account_age} ago)", "MEMBER_LEAVE": "📤 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) left the server", "MEMBER_ROLE_ADD": "🔑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) role added **{role.name}** by {mod.username}#{mod.discriminator}", diff --git a/src/plugins/Logs.ts b/src/plugins/Logs.ts index 719a495d..74c3862d 100644 --- a/src/plugins/Logs.ts +++ b/src/plugins/Logs.ts @@ -113,10 +113,14 @@ export class LogsPlugin extends Plugin { ); const mod = relevantAuditLogEntry ? relevantAuditLogEntry.user : unknownUser; - this.serverLogs.log(LogType.MEMBER_BAN, { - user: stripObjectToScalars(user), - mod: stripObjectToScalars(mod) - }); + this.serverLogs.log( + LogType.MEMBER_BAN, + { + user: stripObjectToScalars(user), + mod: stripObjectToScalars(mod) + }, + user.id + ); } @d.event("guildBanRemove") @@ -128,10 +132,14 @@ export class LogsPlugin extends Plugin { ); const mod = relevantAuditLogEntry ? relevantAuditLogEntry.user : unknownUser; - this.serverLogs.log(LogType.MEMBER_UNBAN, { - user: stripObjectToScalars(user), - mod: stripObjectToScalars(mod) - }); + this.serverLogs.log( + LogType.MEMBER_UNBAN, + { + mod: stripObjectToScalars(mod), + userId: user.id + }, + user.id + ); } @d.event("guildMemberUpdate") diff --git a/src/plugins/ModActions.ts b/src/plugins/ModActions.ts index dc0a30ed..1e828653 100644 --- a/src/plugins/ModActions.ts +++ b/src/plugins/ModActions.ts @@ -19,6 +19,17 @@ import { GuildLogs } from "../data/GuildLogs"; import { LogType } from "../data/LogType"; import Timer = NodeJS.Timer; +enum IgnoredEventType { + Ban = 1, + Unban, + Kick +} + +interface IIgnoredEvent { + type: IgnoredEventType; + userId: string; +} + export class ModActionsPlugin extends Plugin { protected cases: GuildCases; protected mutes: GuildMutes; @@ -26,11 +37,15 @@ export class ModActionsPlugin extends Plugin { protected muteClearIntervalId: Timer; + protected ignoredEvents: IIgnoredEvent[]; + async onLoad() { this.cases = new GuildCases(this.guildId); this.mutes = new GuildMutes(this.guildId); this.serverLogs = new GuildLogs(this.guildId); + this.ignoredEvents = []; + // Check for expired mutes every 5s this.clearExpiredMutes(); this.muteClearIntervalId = setInterval(() => this.clearExpiredMutes(), 5000); @@ -89,13 +104,37 @@ export class ModActionsPlugin extends Plugin { }; } + ignoreEvent(type: IgnoredEventType, userId: any) { + this.ignoredEvents.push({ type, userId }); + + // Clear after expiry (15sec by default) + setTimeout(() => { + this.clearIgnoredEvent(type, userId); + }, 1000 * 15); + } + + isEventIgnored(type: IgnoredEventType, userId: any) { + return this.ignoredEvents.some(info => type === info.type && userId === info.userId); + } + + clearIgnoredEvent(type: IgnoredEventType, userId: any) { + this.ignoredEvents.splice( + this.ignoredEvents.findIndex(info => type === info.type && userId === info.userId), + 1 + ); + } + /** * Add a BAN action automatically when a user is banned. * Attempts to find the ban's details in the audit log. */ @d.event("guildBanAdd") async onGuildBanAdd(guild: Guild, user: User) { - await sleep(1000); // Wait a moment for the audit log to update + if (this.isEventIgnored(IgnoredEventType.Ban, user.id)) { + this.clearIgnoredEvent(IgnoredEventType.Ban, user.id); + return; + } + const relevantAuditLogEntry = await findRelevantAuditLogEntry( this.guild, ErisConstants.AuditLogActions.MEMBER_BAN_ADD, @@ -125,6 +164,11 @@ export class ModActionsPlugin extends Plugin { */ @d.event("guildBanRemove") async onGuildBanRemove(guild: Guild, user: User) { + if (this.isEventIgnored(IgnoredEventType.Unban, user.id)) { + this.clearIgnoredEvent(IgnoredEventType.Unban, user.id); + return; + } + const relevantAuditLogEntry = await findRelevantAuditLogEntry( this.guild, ErisConstants.AuditLogActions.MEMBER_BAN_REMOVE, @@ -165,6 +209,11 @@ export class ModActionsPlugin extends Plugin { @d.event("guildMemberRemove") async onGuildMemberRemove(_, member: Member) { + if (this.isEventIgnored(IgnoredEventType.Kick, member.id)) { + this.clearIgnoredEvent(IgnoredEventType.Kick, member.id); + return; + } + const kickAuditLogEntry = await findRelevantAuditLogEntry( this.guild, ErisConstants.AuditLogActions.MEMBER_KICK, @@ -398,6 +447,7 @@ export class ModActionsPlugin extends Plugin { // Kick the user this.serverLogs.ignoreLog(LogType.MEMBER_KICK, args.member.id); + this.ignoreEvent(IgnoredEventType.Kick, args.member.id); args.member.kick(args.reason); // Create a case for this action @@ -442,6 +492,7 @@ export class ModActionsPlugin extends Plugin { // Ban the user this.serverLogs.ignoreLog(LogType.MEMBER_BAN, args.member.id); + this.ignoreEvent(IgnoredEventType.Ban, args.member.id); args.member.ban(1, args.reason); // Create a case for this action @@ -462,9 +513,10 @@ export class ModActionsPlugin extends Plugin { @d.command("unban", " [reason:string$]") @d.permission("ban") async unbanCmd(msg: Message, args: any) { - this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, args.member.id); + this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, args.userId); try { + this.ignoreEvent(IgnoredEventType.Unban, args.userId); await this.guild.unbanMember(args.userId, args.reason); } catch (e) { msg.channel.createMessage(errorMessage("Failed to unban member")); @@ -496,7 +548,8 @@ export class ModActionsPlugin extends Plugin { return; } - this.serverLogs.ignoreLog(LogType.MEMBER_FORCEBAN, args.member.id); + this.ignoreEvent(IgnoredEventType.Ban, args.userId); + this.serverLogs.ignoreLog(LogType.MEMBER_BAN, args.userId); try { await this.guild.banMember(args.userId, 1, args.reason); diff --git a/src/plugins/Utility.ts b/src/plugins/Utility.ts index 3159e450..6c436a3c 100644 --- a/src/plugins/Utility.ts +++ b/src/plugins/Utility.ts @@ -1,14 +1,26 @@ import { Plugin, decorators as d, reply } from "knub"; -import { Channel, Message, TextChannel, User } from "eris"; -import { errorMessage, getMessages, stripObjectToScalars, successMessage } from "../utils"; +import { Channel, Embed, EmbedOptions, Message, TextChannel, User, VoiceChannel } from "eris"; +import { + embedPadding, + errorMessage, + getMessages, + stripObjectToScalars, + successMessage, + trimLines +} from "../utils"; import { GuildLogs } from "../data/GuildLogs"; import { LogType } from "../data/LogType"; +import moment from "moment-timezone"; +import humanizeDuration from "humanize-duration"; +import { GuildCases } from "../data/GuildCases"; +import { CaseType } from "../data/CaseType"; const MAX_SEARCH_RESULTS = 15; const MAX_CLEAN_COUNT = 50; export class UtilityPlugin extends Plugin { protected logs: GuildLogs; + protected cases: GuildCases; getDefaultOptions() { return { @@ -17,7 +29,8 @@ export class UtilityPlugin extends Plugin { level: false, search: false, clean: false, - info: false + info: false, + server: true }, overrides: [ { @@ -27,7 +40,8 @@ export class UtilityPlugin extends Plugin { level: true, search: true, clean: true, - info: true + info: true, + server: true } } ] @@ -36,6 +50,7 @@ export class UtilityPlugin extends Plugin { onLoad() { this.logs = new GuildLogs(this.guildId); + this.cases = new GuildCases(this.guildId); } @d.command("roles") @@ -122,8 +137,9 @@ export class UtilityPlugin extends Plugin { m => m.id !== msg.id, args.count ); - if (messagesToClean.length > 0) + if (messagesToClean.length > 0) { await this.cleanMessages(msg.channel, messagesToClean.map(m => m.id), msg.author); + } msg.channel.createMessage( successMessage( @@ -147,8 +163,9 @@ export class UtilityPlugin extends Plugin { m => m.id !== msg.id && m.author.id === args.userId, args.count ); - if (messagesToClean.length > 0) + if (messagesToClean.length > 0) { await this.cleanMessages(msg.channel, messagesToClean.map(m => m.id), msg.author); + } msg.channel.createMessage( successMessage( @@ -172,8 +189,9 @@ export class UtilityPlugin extends Plugin { m => m.id !== msg.id && m.author.bot, args.count ); - if (messagesToClean.length > 0) + if (messagesToClean.length > 0) { await this.cleanMessages(msg.channel, messagesToClean.map(m => m.id), msg.author); + } msg.channel.createMessage( successMessage( @@ -181,4 +199,134 @@ export class UtilityPlugin extends Plugin { ) ); } + + @d.command("info", "") + @d.permission("info") + async infoCmd(msg: Message, args: { userId: string }) { + const embed: EmbedOptions = { + fields: [] + }; + + const user = this.bot.users.get(args.userId); + if (user) { + const createdAt = moment(user.createdAt); + const accountAge = humanizeDuration(moment().valueOf() - user.createdAt, { + largest: 2, + round: true + }); + + embed.title = `${user.username}#${user.discriminator}`; + embed.thumbnail = { url: user.avatarURL }; + + embed.fields.push({ + name: "User information", + value: + trimLines(` + ID: ${user.id} + Profile: <@!${user.id}> + Created: ${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")}) + `) + embedPadding + }); + } else { + embed.title = `Unknown user`; + } + + const member = this.guild.members.get(args.userId); + if (member) { + const joinedAt = moment(member.joinedAt); + const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { + largest: 2, + round: true + }); + const roles = member.roles.map(id => this.guild.roles.get(id)); + + embed.fields.push({ + name: "Member information", + value: + trimLines(` + Joined: ${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")}) + ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} + `) + embedPadding + }); + } + + const cases = await this.cases.getByUserId(args.userId); + if (cases.length > 0) { + cases.sort((a, b) => { + return a.created_at < b.created_at ? -1 : 1; + }); + + const caseSummaries = cases.map(c => { + return `${CaseType[c.type]} (#${c.case_number})`; + }); + + embed.fields.push({ + name: "Cases", + value: trimLines(` + Total cases: ${cases.length} + Summary: ${caseSummaries.join(", ")} + `) + }); + } + + msg.channel.createMessage({ embed }); + } + + @d.command("server") + @d.permission("server") + async serverCmd(msg: Message) { + await this.guild.fetchAllMembers(); + + const embed: EmbedOptions = { + fields: [] + }; + + embed.thumbnail = { url: this.guild.iconURL }; + + const createdAt = moment(this.guild.createdAt); + const serverAge = humanizeDuration(moment().valueOf() - this.guild.createdAt, { + largest: 2, + round: true + }); + + embed.fields.push({ + name: "Server information", + value: + trimLines(` + Created: ${serverAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")}) + Members: ${this.guild.memberCount} + ${this.guild.features.length > 0 ? "Features: " + this.guild.features.join(", ") : ""} + `) + embedPadding + }); + + const textChannels = this.guild.channels.filter(channel => channel instanceof TextChannel); + const voiceChannels = this.guild.channels.filter(channel => channel instanceof VoiceChannel); + + embed.fields.push({ + name: "Counts", + value: + trimLines(` + Roles: ${this.guild.roles.size} + Text channels: ${textChannels.length} + Voice channels: ${voiceChannels.length} + `) + embedPadding + }); + + const onlineMembers = this.guild.members.filter(m => m.status === "online"); + const dndMembers = this.guild.members.filter(m => m.status === "dnd"); + const idleMembers = this.guild.members.filter(m => m.status === "idle"); + const offlineMembers = this.guild.members.filter(m => m.status === "offline"); + + embed.fields.push({ + name: "Members", + value: trimLines(` + Online: **${onlineMembers.length}** + Idle: **${idleMembers.length}** + DND: **${dndMembers.length}** + Offline: **${offlineMembers.length}** + `) + }); + + msg.channel.createMessage({ embed }); + } } diff --git a/src/utils.ts b/src/utils.ts index 5a2f36a4..b07fd63a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -226,3 +226,15 @@ export async function cleanMessagesInChannel( await bot.deleteMessages(channel.id, ids, reason); } } + +export function trimLines(str: string) { + return str + .trim() + .split("\n") + .map(l => l.trim()) + .join("\n") + .trim(); +} + +export const emptyEmbedValue = "\u200b"; +export const embedPadding = "\n" + emptyEmbedValue;