diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/arguments/contentWithDuration.ts | 9 | ||||
-rw-r--r-- | src/arguments/duration.ts | 30 | ||||
-rw-r--r-- | src/commands/moderation/ban.ts | 224 | ||||
-rw-r--r-- | src/commands/moderation/kick.ts | 125 | ||||
-rw-r--r-- | src/commands/moderation/modlog.ts | 137 | ||||
-rw-r--r-- | src/commands/moderation/mute.ts | 246 | ||||
-rw-r--r-- | src/commands/moderation/warn.ts | 5 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClient.ts | 17 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 83 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuild.ts | 2 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuildMember.ts | 60 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuildMemberManager.ts | 11 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushMessageManager.ts | 3 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushThreadMemberManager.ts | 9 | ||||
-rw-r--r-- | src/lib/extensions/global.d.ts | 9 | ||||
-rw-r--r-- | src/lib/models/ModLog.ts | 19 |
16 files changed, 544 insertions, 445 deletions
diff --git a/src/arguments/contentWithDuration.ts b/src/arguments/contentWithDuration.ts index e69de29..8dd7621 100644 --- a/src/arguments/contentWithDuration.ts +++ b/src/arguments/contentWithDuration.ts @@ -0,0 +1,9 @@ +import { BushArgumentTypeCaster } from '../lib/extensions/discord-akairo/BushArgumentTypeCaster'; +import { BushMessage } from '../lib/extensions/discord.js/BushMessage'; + +export const contentWithDurationTypeCaster: BushArgumentTypeCaster = async ( + _message: BushMessage, + phrase +): Promise<{ duration: number; contentWithoutTime: string }> => { + return client.util.parseDuration(phrase); +}; diff --git a/src/arguments/duration.ts b/src/arguments/duration.ts index 6007b4e..f8b6ab1 100644 --- a/src/arguments/duration.ts +++ b/src/arguments/duration.ts @@ -1,21 +1,19 @@ import { BushArgumentTypeCaster } from '../lib/extensions/discord-akairo/BushArgumentTypeCaster'; import { BushMessage } from '../lib/extensions/discord.js/BushMessage'; -import { BushConstants } from '../lib/utils/BushConstants'; -export const durationTypeCaster: BushArgumentTypeCaster = async (_message: BushMessage, phrase): Promise<number> => { - if (!phrase) return null; +export const durationTypeCaster: BushArgumentTypeCaster = (_message: BushMessage, phrase): number => { + // if (!phrase) return null; + // const regexString = Object.entries(BushConstants.TimeUnits) + // .map(([name, { label }]) => String.raw`(?:(?<${name}>-?(?:\d+)?\.?\d+) *${label})?`) + // .join('\\s*'); + // const match = new RegExp(`^${regexString}$`, 'im').exec(phrase); + // if (!match) return null; + // let milliseconds = 0; + // for (const key in match.groups) { + // const value = Number(match.groups[key] || 0); + // milliseconds += value * BushConstants.TimeUnits[key].value; + // } + // return milliseconds; - const regexString = Object.entries(BushConstants.TimeUnits) - .map(([name, { label }]) => String.raw`(?:(?<${name}>-?(?:\d+)?\.?\d+) *${label})?`) - .join('\\s*'); - const match = new RegExp(`^${regexString}$`, 'i').exec(phrase); - if (!match) return null; - - let milliseconds = 0; - for (const key in match.groups) { - const value = Number(match.groups[key] || 0); - milliseconds += value * BushConstants.TimeUnits[key].value; - } - - return milliseconds; + return client.util.parseDuration(phrase).duration; }; diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 29dc8a6..f3cc1e2 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,17 +1,5 @@ -import { Argument } from 'discord-akairo'; -import { CommandInteraction, Message, User } from 'discord.js'; -import moment from 'moment'; +import { Message, User } from 'discord.js'; import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand'; -import { Ban, Guild, ModLog, ModLogType } from '../../lib/models'; - -/* const durationAliases: Record<string, string[]> = { - weeks: ['w', 'weeks', 'week', 'wk', 'wks'], - days: ['d', 'days', 'day'], - hours: ['h', 'hours', 'hour', 'hr', 'hrs'], - minutes: ['m', 'min', 'mins', 'minutes', 'minute'], - months: ['mo', 'month', 'months'] -}; -const durationRegex = /(?:(\d+)(d(?:ays?)?|h(?:ours?|rs?)?|m(?:inutes?|ins?)?|mo(?:nths?)?|w(?:eeks?|ks?)?)(?: |$))/g; */ export default class BanCommand extends BushCommand { public constructor() { @@ -29,18 +17,13 @@ export default class BanCommand extends BushCommand { }, { id: 'reason', + type: 'contentWithDuration', match: 'restContent', prompt: { start: 'Why would you like to ban this user?', retry: '{error} Choose a ban reason.', optional: true } - }, - { - id: 'time', - type: 'duration', - match: 'option', - flag: '--time' } ], clientPermissions: ['BAN_MEMBERS'], @@ -62,113 +45,112 @@ export default class BanCommand extends BushCommand { name: 'reason', description: 'Why are they getting banned?', required: false - }, - { - type: 'STRING', - name: 'time', - description: 'How long should they be banned for?', - required: false } ], slash: true }); } - async *genResponses( - message: Message | CommandInteraction, - user: User, - reason?: string, - time?: number - ): AsyncIterable<string> { - const duration = moment.duration(); - let modLogEntry: ModLog; - let banEntry: Ban; - // const translatedTime: string[] = []; - // Create guild entry so postgres doesn't get mad when I try and add a modlog entry - await Guild.findOrCreate({ - where: { - id: message.guild.id - }, - defaults: { - id: message.guild.id - } - }); - try { - if (time) { - duration.add(time); - /* const parsed = [...time.matchAll(durationRegex)]; - if (parsed.length < 1) { - yield `${this.client.util.emojis.error} Invalid time.`; - return; - } - for (const part of parsed) { - const translated = Object.keys(durationAliases).find((k) => durationAliases[k].includes(part[2])); - translatedTime.push(part[1] + ' ' + translated); - duration.add(Number(part[1]), translated as 'weeks' | 'days' | 'hours' | 'months' | 'minutes'); - } */ - modLogEntry = ModLog.build({ - user: user.id, - guild: message.guild.id, - reason, - type: ModLogType.TEMP_BAN, - duration: duration.asMilliseconds(), - moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - }); - banEntry = Ban.build({ - user: user.id, - guild: message.guild.id, - reason, - expires: new Date(new Date().getTime() + duration.asMilliseconds()), - modlog: modLogEntry.id - }); - } else { - modLogEntry = ModLog.build({ - user: user.id, - guild: message.guild.id, - reason, - type: ModLogType.BAN, - moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - }); - banEntry = Ban.build({ - user: user.id, - guild: message.guild.id, - reason, - modlog: modLogEntry.id - }); - } - await modLogEntry.save(); - await banEntry.save(); + // async *genResponses( + // message: Message | CommandInteraction, + // user: User, + // reason?: string, + // time?: number + // ): AsyncIterable<string> { + // const duration = moment.duration(); + // let modLogEntry: ModLog; + // let banEntry: Ban; + // // const translatedTime: string[] = []; + // // Create guild entry so postgres doesn't get mad when I try and add a modlog entry + // await Guild.findOrCreate({ + // where: { + // id: message.guild.id + // }, + // defaults: { + // id: message.guild.id + // } + // }); + // try { + // if (time) { + // duration.add(time); + // /* const parsed = [...time.matchAll(durationRegex)]; + // if (parsed.length < 1) { + // yield `${this.client.util.emojis.error} Invalid time.`; + // return; + // } + // for (const part of parsed) { + // const translated = Object.keys(durationAliases).find((k) => durationAliases[k].includes(part[2])); + // translatedTime.push(part[1] + ' ' + translated); + // duration.add(Number(part[1]), translated as 'weeks' | 'days' | 'hours' | 'months' | 'minutes'); + // } */ + // modLogEntry = ModLog.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // type: ModLogType.TEMP_BAN, + // duration: duration.asMilliseconds(), + // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + // }); + // banEntry = Ban.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // expires: new Date(new Date().getTime() + duration.asMilliseconds()), + // modlog: modLogEntry.id + // }); + // } else { + // modLogEntry = ModLog.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // type: ModLogType.BAN, + // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + // }); + // banEntry = Ban.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // modlog: modLogEntry.id + // }); + // } + // await modLogEntry.save(); + // await banEntry.save(); - try { - await user.send( - `You were banned in ${message.guild.name} ${duration ? duration.humanize() : 'permanently'} with reason \`${ - reason || 'No reason given' - }\`` - ); - } catch { - yield `${this.client.util.emojis.warn} Unable to dm user`; - } - await message.guild.members.ban(user, { - reason: `Banned by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${ - reason ? `reason ${reason}` : 'no reason' - }` - }); - yield `${this.client.util.emojis.success} Banned <@!${user.id}> ${ - duration ? duration.humanize() : 'permanently' - } with reason \`${reason || 'No reason given'}\``; - } catch { - yield `${this.client.util.emojis.error} Error banning :/`; - await banEntry.destroy(); - await modLogEntry.destroy(); - return; - } - } - async exec(message: Message, { user, reason, time }: { user: User; reason?: string; time?: number | string }): Promise<void> { - if (typeof time === 'string') { - time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number; - //// time = this.client.commandHandler.resolver.type('duration') - } - for await (const response of this.genResponses(message, user, reason, time)) { - await message.util.send(response); - } + // try { + // await user.send( + // `You were banned in ${message.guild.name} ${duration ? duration.humanize() : 'permanently'} with reason \`${ + // reason || 'No reason given' + // }\`` + // ); + // } catch { + // yield `${this.client.util.emojis.warn} Unable to dm user`; + // } + // await message.guild.members.ban(user, { + // reason: `Banned by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${ + // reason ? `reason ${reason}` : 'no reason' + // }` + // }); + // yield `${this.client.util.emojis.success} Banned <@!${user.id}> ${ + // duration ? duration.humanize() : 'permanently' + // } with reason \`${reason || 'No reason given'}\``; + // } catch { + // yield `${this.client.util.emojis.error} Error banning :/`; + // await banEntry.destroy(); + // await modLogEntry.destroy(); + // return; + // } + // } + async exec( + message: Message, + { user, reason, time }: { user: User; reason?: string; time?: number | string } + ): Promise<unknown> { + return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); + + // if (typeof time === 'string') { + // time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number; + // //// time = this.client.commandHandler.resolver.type('duration') + // } + // for await (const response of this.genResponses(message, user, reason, time)) { + // await message.util.send(response); + // } } } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 09d6abf..df538bc 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,7 +1,5 @@ -import { CommandInteraction, GuildMember, Message } from 'discord.js'; +import { GuildMember, Message } from 'discord.js'; import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand'; -import { BushSlashMessage } from '../../lib/extensions/discord-akairo/BushSlashMessage'; -import { Guild, ModLog, ModLogType } from '../../lib/models'; export default class KickCommand extends BushCommand { public constructor() { @@ -20,7 +18,12 @@ export default class KickCommand extends BushCommand { { id: 'reason', type: 'string', - match: 'restContent' + match: 'restContent', + prompt: { + start: 'Why would you like to kick this user?', + retry: '{error} Choose a valid user to kick.', + optional: true + } } ], clientPermissions: ['KICK_MEMBERS'], @@ -28,19 +31,19 @@ export default class KickCommand extends BushCommand { description: { content: 'Kick a member from the server.', usage: 'kick <member> <reason>', - examples: ['kick @Tyman being cool'] + examples: ['kick @user bad'] }, slashOptions: [ { type: 'USER', name: 'user', - description: 'The user to kick', + description: 'What user would you like to kick?', required: true }, { type: 'STRING', name: 'reason', - description: 'The reason to show in modlogs and audit log', + description: 'Why would you like to kick this user?', required: false } ], @@ -48,63 +51,59 @@ export default class KickCommand extends BushCommand { }); } - private async *genResponses( - message: Message | CommandInteraction, - user: GuildMember, - reason?: string - ): AsyncIterable<string> { - let modlogEnry: ModLog; - // Create guild entry so postgres doesn't get mad when I try and add a modlog entry - await Guild.findOrCreate({ - where: { - id: message.guild.id - }, - defaults: { - id: message.guild.id - } - }); - try { - modlogEnry = ModLog.build({ - user: user.id, - guild: message.guild.id, - moderator: message instanceof Message ? message.author.id : message.user.id, - type: ModLogType.KICK, - reason - }); - await modlogEnry.save(); - } catch (e) { - this.client.console.error(`KickCommand`, `Error saving to database. ${e?.stack}`); - yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; - return; - } - try { - await user.send(`You were kicked in ${message.guild.name} with reason \`${reason || 'No reason given'}\``); - } catch (e) { - yield `${this.client.util.emojis.warn} Unable to dm user`; - } - try { - await user.kick( - `Kicked by ${message instanceof Message ? message.author.tag : message.user.tag} with ${ - reason ? `reason ${reason}` : 'no reason' - }` - ); - } catch { - yield `${this.client.util.emojis.error} Error kicking :/`; - await modlogEnry.destroy(); - return; - } - yield `${this.client.util.emojis.success} Kicked <@!${user.id}> with reason \`${reason || 'No reason given'}\``; - } + // private async *genResponses( + // message: Message | CommandInteraction, + // user: GuildMember, + // reason?: string + // ): AsyncIterable<string> { + // let modlogEnry: ModLog; + // // Create guild entry so postgres doesn't get mad when I try and add a modlog entry + // await Guild.findOrCreate({ + // where: { + // id: message.guild.id + // }, + // defaults: { + // id: message.guild.id + // } + // }); + // try { + // modlogEnry = ModLog.build({ + // user: user.id, + // guild: message.guild.id, + // moderator: message instanceof Message ? message.author.id : message.user.id, + // type: ModLogType.KICK, + // reason + // }); + // await modlogEnry.save(); + // } catch (e) { + // this.client.console.error(`KickCommand`, `Error saving to database. ${e?.stack}`); + // yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; + // return; + // } + // try { + // await user.send(`You were kicked in ${message.guild.name} with reason \`${reason || 'No reason given'}\``); + // } catch (e) { + // yield `${this.client.util.emojis.warn} Unable to dm user`; + // } + // try { + // await user.kick( + // `Kicked by ${message instanceof Message ? message.author.tag : message.user.tag} with ${ + // reason ? `reason ${reason}` : 'no reason' + // }` + // ); + // } catch { + // yield `${this.client.util.emojis.error} Error kicking :/`; + // await modlogEnry.destroy(); + // return; + // } + // yield `${this.client.util.emojis.success} Kicked <@!${user.id}> with reason \`${reason || 'No reason given'}\``; + // } - async exec(message: Message, { user, reason }: { user: GuildMember; reason?: string }): Promise<void> { - for await (const response of this.genResponses(message, user, reason)) { - await message.util.send(response); - } - } + async exec(message: Message, { user, reason }: { user: GuildMember; reason?: string }): Promise<unknown> { + return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); - async execSlash(message: BushSlashMessage, { user, reason }: { user: GuildMember; reason?: string }): Promise<void> { - for await (const response of this.genResponses(message.interaction, user, reason)) { - await message.interaction.reply(response); - } + // for await (const response of this.genResponses(message, user, reason)) { + // await message.util.send(response); + // } } } diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts index d094885..2f7601b 100644 --- a/src/commands/moderation/modlog.ts +++ b/src/commands/moderation/modlog.ts @@ -1,8 +1,8 @@ -import { stripIndent } from 'common-tags'; import { Argument } from 'discord-akairo'; -import { Message, MessageEmbed } from 'discord.js'; +import { MessageEmbed } from 'discord.js'; import moment from 'moment'; import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand'; +import { BushMessage } from '../../lib/extensions/discord.js/BushMessage'; import { ModLog } from '../../lib/models'; export default class ModlogCommand extends BushCommand { @@ -10,50 +10,47 @@ export default class ModlogCommand extends BushCommand { super('modlog', { aliases: ['modlog', 'modlogs'], category: 'moderation', + description: { + content: "View a user's modlogs, or view a specific case.", + usage: 'modlogs <search>', + examples: ['modlogs @Tyman'] + }, args: [ { id: 'search', + type: Argument.union('user', 'string'), prompt: { - start: 'What modlog id or user would you like to see?' + start: 'What case id or user would you like to see?', + retry: '{error} Choose a valid case id or user.' } - }, - { - id: 'page', - type: 'number' } ], userPermissions: ['MANAGE_MESSAGES'], - description: { - content: "View a user's modlogs, or view a specific modlog entry", - usage: 'warn <search> [page]', - examples: ['modlogs @Tyman', 'modlogs @Tyman 3'] - } + slash: true, + slashOptions: [ + { + name: 'search', + description: 'What case id or user would you like to see?', + type: 'STRING', + required: true + } + ] }); } - *args(): unknown { - const search = yield { - id: 'search', - type: Argument.union('user', 'string'), - prompt: { - start: 'What modlog id or user would you like to see?', - retry: '{error} Choose a valid modlog id or user.' - } - }; - if (typeof search === 'string') return { search, page: null }; - else { - const page = yield { - id: 'page', - type: 'number', - prompt: { - start: 'What page?', - retry: '{error} Choose a valid page to view.', - optional: true - } - }; - return { search, page }; - } + + private generateModlogInfo(log: ModLog) { + const modLog = [ + `**Case ID**: ${log.id}`, + `**Type**: ${log.type.toLowerCase()}`, + `**User**: <@!${log.user}> (${log.user})`, + `**Moderator**: <@!${log.moderator}> (${log.moderator})` + ]; + if (log.duration) modLog.push(`**Duration**: ${moment.duration(log.duration, 'milliseconds').humanize()}`); + modLog.push(`**Reason**: ${log.reason || 'No Reason Specified.'}`); + return modLog.join(`\n`); } - async exec(message: Message, { search, page }: { search: string; page: number }): Promise<void> { + + async exec(message: BushMessage, { search }: { search: string }): Promise<unknown> { const foundUser = await this.client.util.resolveUserAsync(search); if (foundUser) { const logs = await ModLog.findAll({ @@ -65,75 +62,27 @@ export default class ModlogCommand extends BushCommand { }); const niceLogs: string[] = []; for (const log of logs) { - niceLogs.push(stripIndent` - **Case ID**: ${log.id} - **Type**: ${log.type.toLowerCase()} - **User**: <@!${log.user}> (${log.user}) - **Moderator**: <@!${log.moderator}> (${log.moderator}) - **Duration**: ${log.duration ? moment.duration(log.duration, 'milliseconds').humanize() : 'N/A'} - **Reason**: ${log.reason || 'None given'} - **${this.client.util.ordinal(logs.indexOf(log) + 1)}** action - `); + niceLogs.push(this.generateModlogInfo(log)); } const chunked: string[][] = this.client.util.chunk(niceLogs, 3); const embedPages = chunked.map( - (e, i) => + (chunk) => new MessageEmbed({ - title: foundUser.tag, - description: e.join('\n**---------------------------**\n'), - footer: { - text: `Page ${i + 1}/${chunked.length}` - }, + title: `${foundUser.tag}'s Mod Logs`, + description: chunk.join('\n**―――――――――――――――――――――――――――**\n'), color: this.client.util.colors.default }) ); - if (page) { - await message.util.send({ embeds: [embedPages[page - 1]] }); - return; - } else { - await message.util.send({ embeds: [embedPages[0]] }); - return; - } + this.client.util.buttonPaginate(message, embedPages, '', true); } else if (search) { const entry = await ModLog.findByPk(search); - if (!entry) { - await message.util.send(`${this.client.util.emojis.error} That modlog does not exist.`); - return; - } - await message.util.send({ - embeds: [ - new MessageEmbed({ - title: `${entry.id}`, - fields: [ - { - name: 'Type', - value: entry.type.toLowerCase(), - inline: true - }, - { - name: 'Duration', - value: `${entry.duration ? moment.duration(entry.duration, 'milliseconds').humanize() : 'N/A'}`, - inline: true - }, - { - name: 'Reason', - value: `${entry.reason || 'None given'}`, - inline: true - }, - { - name: 'Moderator', - value: `<@!${entry.moderator}> (${entry.moderator})`, - inline: true - }, - { - name: 'User', - value: `<@!${entry.user}> (${entry.user})`, - inline: true - } - ] - }) - ] + if (!entry) return message.util.send(`${this.client.util.emojis.error} That modlog does not exist.`); + const embed = new MessageEmbed({ + title: `Case ${entry.id}`, + description: this.generateModlogInfo(entry), + color: this.client.util.colors.default }); + return await this.client.util.buttonPaginate(message, [embed]); } } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 3496489..ffad432 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,8 +1,8 @@ import { Argument } from 'discord-akairo'; -import { CommandInteraction, Message, User } from 'discord.js'; -import moment from 'moment'; import { BushCommand } from '../../lib/extensions/discord-akairo/BushCommand'; -import { Guild, ModLog, ModLogType, Mute } from '../../lib/models'; +import { BushGuildMember } from '../../lib/extensions/discord.js/BushGuildMember'; +import { BushMessage } from '../../lib/extensions/discord.js/BushMessage'; +import { BushUser } from '../../lib/extensions/discord.js/BushUser'; export default class MuteCommand extends BushCommand { public constructor() { @@ -20,18 +20,13 @@ export default class MuteCommand extends BushCommand { }, { id: 'reason', - match: 'separate', + type: 'contentWithDuration', + match: 'rest', prompt: { start: 'Why would you like to mute this user?', - retry: '{error} Choose a mute reason.', + retry: '{error} Choose a mute reason and duration.', optional: true } - }, - { - id: 'time', - type: 'duration', - match: 'option', - flag: '--time' } ], clientPermissions: ['MANAGE_ROLES'], @@ -51,116 +46,143 @@ export default class MuteCommand extends BushCommand { { type: 'STRING', name: 'reason', - description: 'Why the user is getting muted.', - required: false - }, - { - type: 'STRING', - name: 'time', - description: 'How long the user should be muted for.', + description: 'Why is the user is getting muted, and how long should they be muted for?', required: false } ], slash: true }); } - async *genResponses( - message: Message | CommandInteraction, - user: User, - reason?: string, - time?: number - ): AsyncIterable<string> { - const duration = moment.duration(time); - let modlogEnry: ModLog; - let muteEntry: Mute; - // Create guild entry so postgres doesn't get mad when I try and add a modlog entry - await Guild.findOrCreate({ - where: { - id: message.guild.id - }, - defaults: { - id: message.guild.id - } - }); - try { - const muteRole = (await Guild.findByPk(message.guild.id)).get('muteRole'); - try { - if (time) { - modlogEnry = ModLog.build({ - user: user.id, - guild: message.guild.id, - reason, - type: ModLogType.TEMP_MUTE, - duration: duration.asMilliseconds(), - moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - }); - muteEntry = Mute.build({ - user: user.id, - guild: message.guild.id, - reason, - expires: new Date(new Date().getTime() + duration.asMilliseconds()), - modlog: modlogEnry.id - }); - } else { - modlogEnry = ModLog.build({ - user: user.id, - guild: message.guild.id, - reason, - type: ModLogType.MUTE, - moderator: message instanceof CommandInteraction ? message.user.id : message.author.id - }); - muteEntry = Mute.build({ - user: user.id, - guild: message.guild.id, - reason, - modlog: modlogEnry.id - }); - } - await modlogEnry.save(); - await muteEntry.save(); - } catch (e) { - this.client.console.error(`MuteCommand`, `Error saving to database. ${e?.stack}`); - yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; - return; - } - try { - await user.send( - `You were muted in ${message.guild.name} ${time ? `for ${duration.humanize()}` : 'permanently'} with reason \`${ - reason || 'No reason given' - }\`` - ); - } catch (e) { - yield `${this.client.util.emojis.warn} Unable to dm user`; - } - await ( - await message.guild.members.fetch(user) - ).roles.add( - muteRole, - `Muted by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${ - reason ? `reason ${reason}` : 'no reason' - }` - ); - yield `${this.client.util.emojis.success} muted <@!${user.id}> ${ - time ? `for ${duration.humanize()}` : 'permanently' - } with reason \`${reason || 'No reason given'}\``; - } catch { - yield `${this.client.util.emojis.error} Error muting :/`; - await muteEntry.destroy(); - await modlogEnry.destroy(); - return; - } - } + // async *genResponses( + // message: Message | CommandInteraction, + // user: User, + // reason?: string, + // time?: number + // ): AsyncIterable<string> { + // const duration = moment.duration(time); + // let modlogEnry: ModLog; + // let muteEntry: Mute; + // // Create guild entry so postgres doesn't get mad when I try and add a modlog entry + // await Guild.findOrCreate({ + // where: { + // id: message.guild.id + // }, + // defaults: { + // id: message.guild.id + // } + // }); + // try { + // const muteRole = (await Guild.findByPk(message.guild.id)).get('muteRole'); + // try { + // if (time) { + // modlogEnry = ModLog.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // type: ModLogType.TEMP_MUTE, + // duration: duration.asMilliseconds(), + // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + // }); + // muteEntry = Mute.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // expires: new Date(new Date().getTime() + duration.asMilliseconds()), + // modlog: modlogEnry.id + // }); + // } else { + // modlogEnry = ModLog.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // type: ModLogType.MUTE, + // moderator: message instanceof CommandInteraction ? message.user.id : message.author.id + // }); + // muteEntry = Mute.build({ + // user: user.id, + // guild: message.guild.id, + // reason, + // modlog: modlogEnry.id + // }); + // } + // await modlogEnry.save(); + // await muteEntry.save(); + // } catch (e) { + // this.client.console.error(`MuteCommand`, `Error saving to database. ${e?.stack}`); + // yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`; + // return; + // } + // try { + // await user.send( + // `You were muted in ${message.guild.name} ${time ? `for ${duration.humanize()}` : 'permanently'} with reason \`${ + // reason || 'No reason given' + // }\`` + // ); + // } catch (e) { + // yield `${this.client.util.emojis.warn} Unable to dm user`; + // } + // await ( + // await message.guild.members.fetch(user) + // ).roles.add( + // muteRole, + // `Muted by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${ + // reason ? `reason ${reason}` : 'no reason' + // }` + // ); + // yield `${this.client.util.emojis.success} muted <@!${user.id}> ${ + // time ? `for ${duration.humanize()}` : 'permanently' + // } with reason \`${reason || 'No reason given'}\``; + // } catch { + // yield `${this.client.util.emojis.error} Error muting :/`; + // await muteEntry.destroy(); + // await modlogEnry.destroy(); + // return; + // } + // } async exec( - message: Message, - { user, reason, time }: { user: User; reason?: string[]; time?: string | number } - ): Promise<void> { - this.client.console.debug(reason); + message: BushMessage, + { user, reason }: { user: BushUser; reason?: { duration: number; contentWithoutTime: string } } + ): Promise<unknown> { + return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); + // this.client.console.debug(reason); - if (typeof time === 'string') { - time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number; + // if (typeof time === 'string') { + // time = (await Argument.cast('duration', this.client.commandHandler.resolver, message, time)) as number; + // } + // for await (const response of this.genResponses(message, user, reason.join(' '), time)) { + // await message.util.sendNew(response); + // } + + const member = message.guild.members.cache.get(user.id) as BushGuildMember; + if (!this.client.util.moderatorCanModerateUser(message.member, member)) { + return message.util.reply({ + content: `${this.client.util.emojis.error} You cannot mute **${member.user.tag}**.` + }); } - for await (const response of this.genResponses(message, user, reason.join(' '), time)) { - await message.util.sendNew(response); + + const time = + typeof reason === 'string' + ? //@ts-ignore: you are unreachable bitch + await Argument.cast('duration', this.client.commandHandler.resolver, message, reason) + : reason.duration; + const parsedReason = reason.contentWithoutTime; + + const response = await member.mute({ + reason: parsedReason, + moderator: message.author, + duration: time, + createModLogEntry: true + }); + + switch (response) { + case 'success': + return message.util.reply(`${this.client.util.emojis.success} Successfully muted **${member.user.tag}**.`); + case 'no mute role': + return message.util.reply( + `${this.client.util.emojis.error} Could not mute **${ + member.user.tag + }**, you must set a mute role with ${message.guild.getSetting('prefix')}.` + ); } } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index c146db0..d70c9f0 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -15,6 +15,7 @@ export default class WarnCommand extends BushCommand { }, { id: 'reason', + type: 'contentWithDuration', match: 'rest' } ], @@ -25,7 +26,9 @@ export default class WarnCommand extends BushCommand { } }); } - public async exec(message: Message, { member, reason }: { member: GuildMember; reason: string }): Promise<void> { + public async exec(message: Message, { member, reason }: { member: GuildMember; reason: string }): Promise<unknown> { + return message.util.reply(`${this.client.util.emojis.error} This command is not finished.`); + // Create guild entry so postgres doesn't get mad when I try and add a modlog entry await Guild.findOrCreate({ where: { diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index 724f01a..6911573 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -9,13 +9,13 @@ import { MessagePayload, ReplyMessageOptions, Snowflake, - Structures, - UserResolvable + Structures } from 'discord.js'; import * as path from 'path'; import { exit } from 'process'; import readline from 'readline'; import { Sequelize } from 'sequelize'; +import { contentWithDurationTypeCaster } from '../../../arguments/contentWithDuration'; import { durationTypeCaster } from '../../../arguments/duration'; import * as config from '../../../config/options'; import UpdateCacheTask from '../../../tasks/updateCache'; @@ -54,6 +54,9 @@ export type BotConfig = typeof config; export type BushReplyMessageType = string | MessagePayload | ReplyMessageOptions; export type BushEditMessageType = string | MessageEditOptions | MessagePayload; export type BushSendMessageType = string | MessagePayload | MessageOptions; +export type BushThreadMemberResolvable = BushThreadMember | BushUserResolvable; +export type BushUserResolvable = BushUser | Snowflake | BushMessage | BushGuildMember | BushThreadMember; +export type BushGuildMemberResolvable = BushGuildMember | BushUserResolvable; const rl = readline.createInterface({ input: process.stdin, @@ -168,7 +171,7 @@ export class BushClient extends AkairoClient { dialect: 'postgres', host: this.config.db.host, port: this.config.db.port, - logging: this.config.logging.db ? (a) => this.logger.debug(a) : false + logging: this.config.logging.db ? (sql) => this.logger.debug(sql) : false }); this.logger = new BushLogger(this); } @@ -198,7 +201,8 @@ export class BushClient extends AkairoClient { gateway: this.ws }); this.commandHandler.resolver.addTypes({ - duration: durationTypeCaster + duration: durationTypeCaster, + contentWithDuration: contentWithDurationTypeCaster }); // loads all the handlers const loaders = { @@ -240,7 +244,6 @@ export class BushClient extends AkairoClient { /** Starts the bot */ public async start(): Promise<void> { - //@ts-ignore: stfu bitch global.client = this; try { @@ -260,10 +263,10 @@ export class BushClient extends AkairoClient { } } - public isOwner(user: UserResolvable): boolean { + public isOwner(user: BushUserResolvable): boolean { return this.config.owners.includes(this.users.resolveID(user)); } - public isSuperUser(user: UserResolvable): boolean { + public isSuperUser(user: BushUserResolvable): boolean { const userID = this.users.resolveID(user); return !!BushCache?.global?.superUsers?.includes(userID) || this.config.owners.includes(userID); } diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 1f8c0f9..9289598 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -25,10 +25,13 @@ import { } from 'discord.js'; import got from 'got'; import { promisify } from 'util'; -import { Global } from '../../models'; +import { Global, Guild, ModLog, ModLogType } from '../../models'; import { BushCache } from '../../utils/BushCache'; +import { BushConstants } from '../../utils/BushConstants'; +import { BushGuildResolvable } from '../discord.js/BushCommandInteraction'; +import { BushGuildMember } from '../discord.js/BushGuildMember'; import { BushMessage } from '../discord.js/BushMessage'; -import { BushClient } from './BushClient'; +import { BushClient, BushGuildMemberResolvable } from './BushClient'; interface hastebinRes { key: string; @@ -281,6 +284,10 @@ export class BushClientUtil extends ClientUtil { ): Promise<void> { if (deleteOnExit === undefined) deleteOnExit = true; + if (embeds.length === 1) { + return this.sendWithDeleteButton(message, { embeds: embeds }); + } + embeds.forEach((_e, i) => { embeds[i] = embeds[i].setFooter(`Page ${i + 1}/${embeds.length}`); }); @@ -523,14 +530,68 @@ export class BushClientUtil extends ClientUtil { return newArray; } - // public createModLogEntry( - // user: User | Snowflake, - // guild: Guild | Snowflake, - // reason?: string, - // type?: ModLogType, - // duration?: number, - // moderator: User | Snowflake - // ): ModLog { + public parseDuration(content: string): { duration: number; contentWithoutTime: string } { + if (!content) return { duration: 0, contentWithoutTime: null }; + + let duration = 0, + contentWithoutTime = content; - // } + const regexString = Object.entries(BushConstants.TimeUnits) + .map(([name, { label }]) => String.raw`(?:(?<${name}>-?(?:\d+)?\.?\d+) *${label})?`) + .join('\\s*'); + const match = new RegExp(`^${regexString}$`, 'im').exec(content); + if (!match) return null; + + for (const key in match.groups) { + contentWithoutTime = contentWithoutTime.replace(match.groups[key], ''); + const value = Number(match.groups[key] || 0); + duration += value * BushConstants.TimeUnits[key].value; + } + + return { duration, contentWithoutTime }; + } + + /** + * Checks if a moderator can perform a moderation action on another user. + * @param moderator - The person trying to perform the action. + * @param victim - The person getting punished. + */ + public moderatorCanModerateUser(moderator: BushGuildMember, victim: BushGuildMember): boolean { + throw 'not implemented'; + if (moderator.guild.id !== victim.guild.id) throw 'wtf'; + if (moderator.guild.ownerID === moderator.id) return true; + } + + public async createModLogEntry(options: { + type: ModLogType; + user: BushGuildMemberResolvable; + moderator: BushGuildMemberResolvable; + reason: string; + duration: number; + guild: BushGuildResolvable; + }): Promise<void> { + const user = this.client.users.resolveID(options.user); + const moderator = this.client.users.resolveID(options.moderator); + const guild = this.client.guilds.resolveID(options.guild); + + // If guild does not exist create it so the modlog can reference a guild. + await Guild.findOrCreate({ + where: { + id: guild + }, + defaults: { + id: guild + } + }); + + const modLogEntry = ModLog.build({ + type: options.type, + user, + moderator, + reason: options.reason, + duration: options.duration, + guild + }); + await modLogEntry.save(); + } } diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 95e07f9..ea34aec 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -4,6 +4,8 @@ import { BushClient } from '../discord-akairo/BushClient'; export class BushGuild extends Guild { public declare readonly client: BushClient; + // I cba to do this + //// public declare members: GuildMemberManager; public constructor(client: BushClient, data: unknown) { super(client, data); } diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 6bcb9b8..59dc777 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -1,13 +1,69 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { GuildMember } from 'discord.js'; -import { BushClient } from '../discord-akairo/BushClient'; +import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; import { BushGuild } from './BushGuild'; import { BushUser } from './BushUser'; +interface BushPunishmentOptions { + reason?: string; + moderator: BushUserResolvable; + createModLogEntry?: boolean; +} + +interface BushTimedPunishmentOptions extends BushPunishmentOptions { + duration?: number; +} + +type PunishmentResponse = 'success'; + +type WarnResponse = PunishmentResponse; + +type MuteResponse = PunishmentResponse | 'no mute role'; + +type UnmuteResponse = PunishmentResponse; + +type KickResponse = PunishmentResponse; + +interface BushBanOptions extends BushTimedPunishmentOptions { + deleteDays?: number; + duration?: number; +} + +type BanResponse = PunishmentResponse; + export class BushGuildMember extends GuildMember { public declare readonly client: BushClient; public declare guild: BushGuild; - public declare BushUser: BushUser; + public declare user: BushUser; public constructor(client: BushClient, data: unknown, guild: BushGuild) { super(client, data, guild); } + + public async warn(options: BushPunishmentOptions): Promise<WarnResponse> { + throw 'not implemented'; + } + + public async mute(options: BushTimedPunishmentOptions): Promise<MuteResponse> { + throw 'not implemented'; + } + + public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> { + throw 'not implemented'; + } + + public async bushKick(options: BushPunishmentOptions): Promise<KickResponse> { + throw 'not implemented'; + } + + public async bushBan(options?: BushBanOptions): Promise<BanResponse> { + throw 'not implemented'; + } + + public isOwner(): boolean { + return this.client.isOwner(this); + } + + public isSuperUser(): boolean { + return this.client.isSuperUser(this); + } } diff --git a/src/lib/extensions/discord.js/BushGuildMemberManager.ts b/src/lib/extensions/discord.js/BushGuildMemberManager.ts new file mode 100644 index 0000000..dbc2da5 --- /dev/null +++ b/src/lib/extensions/discord.js/BushGuildMemberManager.ts @@ -0,0 +1,11 @@ +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// import { GuildMemberManager } from 'discord.js'; +// import { BushGuild } from './BushGuild'; + +// export class BushGuildMemberManager extends GuildMemberManager { +// public guild: BushGuild; + +// public constructor(guild: BushGuild, iterable?: Iterable<any>) { +// super(guild, iterable); +// } +// } diff --git a/src/lib/extensions/discord.js/BushMessageManager.ts b/src/lib/extensions/discord.js/BushMessageManager.ts index efc6369..181808a 100644 --- a/src/lib/extensions/discord.js/BushMessageManager.ts +++ b/src/lib/extensions/discord.js/BushMessageManager.ts @@ -4,11 +4,12 @@ import { BushClient } from '../discord-akairo/BushClient'; import { BushDMChannel } from './BushDMChannel'; import { BushMessage } from './BushMessage'; import { BushTextChannel } from './BushTextChannel'; +import { BushThreadChannel } from './BushThreadChannel'; export class BushMessageManager extends MessageManager { public declare readonly client: BushClient; public declare cache: Collection<Snowflake, BushMessage>; - public constructor(channel: BushTextChannel | BushDMChannel, iterable?: Iterable<any>) { + public constructor(channel: BushTextChannel | BushDMChannel | BushThreadChannel, iterable?: Iterable<any>) { super(channel, iterable); } } diff --git a/src/lib/extensions/discord.js/BushThreadMemberManager.ts b/src/lib/extensions/discord.js/BushThreadMemberManager.ts index e375322..0c44f71 100644 --- a/src/lib/extensions/discord.js/BushThreadMemberManager.ts +++ b/src/lib/extensions/discord.js/BushThreadMemberManager.ts @@ -1,15 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-empty-interface */ -import { Snowflake, ThreadMemberManager, UserResolvable } from 'discord.js'; +import { ThreadMemberManager } from 'discord.js'; import { BushClient } from '../discord-akairo/BushClient'; -import { BushGuildMember } from './BushGuildMember'; -import { BushMessage } from './BushMessage'; import { BushThreadChannel } from './BushThreadChannel'; -import { BushThreadMember } from './BushThreadMember'; -import { BushUser } from './BushUser'; - -export type BushThreadMemberResolvable = BushThreadMember | UserResolvable; -export type BushUserResolvable = BushUser | Snowflake | BushMessage | BushGuildMember | BushThreadMember; export interface BushThreadMemberManager extends ThreadMemberManager {} diff --git a/src/lib/extensions/global.d.ts b/src/lib/extensions/global.d.ts new file mode 100644 index 0000000..6b5d129 --- /dev/null +++ b/src/lib/extensions/global.d.ts @@ -0,0 +1,9 @@ +import { BushClient } from './discord-akairo/BushClient'; +declare global { + declare namespace NodeJS { + export interface Global { + client: BushClient; + } + } + const client: BushClient; +} diff --git a/src/lib/models/ModLog.ts b/src/lib/models/ModLog.ts index 94c464d..1d850d9 100644 --- a/src/lib/models/ModLog.ts +++ b/src/lib/models/ModLog.ts @@ -1,3 +1,4 @@ +import { Snowflake } from 'discord.js'; import { DataTypes, Sequelize } from 'sequelize'; import { v4 as uuidv4 } from 'uuid'; import { BaseModel } from './BaseModel'; @@ -16,31 +17,31 @@ export enum ModLogType { export interface ModLogModel { id: string; type: ModLogType; - user: string; - moderator: string; + user: Snowflake; + moderator: Snowflake; reason: string; duration: number; - guild: string; + guild: Snowflake; } export interface ModLogModelCreationAttributes { id?: string; type: ModLogType; - user: string; - moderator: string; + user: Snowflake; + moderator: Snowflake; reason?: string; duration?: number; - guild: string; + guild: Snowflake; } export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes> implements ModLogModel { id: string; type: ModLogType; - user: string; - moderator: string; - guild: string; + user: Snowflake; + moderator: Snowflake; reason: string | null; duration: number | null; + guild: Snowflake; static initModel(sequelize: Sequelize): void { ModLog.init( |