aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-10-03 22:57:40 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-10-03 22:57:40 -0400
commit612ed820a0600ec11ed642005377cd7f5a8a8b77 (patch)
tree6bca4e7268fd0063ff53cf64fa44df62a23dba50 /lib
parented98ff7e2679f362f2657e77a6cf8dd3ce9b3d43 (diff)
downloadtanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.tar.gz
tanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.tar.bz2
tanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.zip
wip
Diffstat (limited to 'lib')
-rw-r--r--lib/automod/AutomodShared.ts4
-rw-r--r--lib/common/Appeals.ts273
-rw-r--r--lib/common/ButtonPaginator.ts6
-rw-r--r--lib/common/ConfirmationPrompt.ts12
-rw-r--r--lib/common/DeleteButton.ts8
-rw-r--r--lib/common/HighlightManager.ts3
-rw-r--r--lib/common/Moderation.ts668
-rw-r--r--lib/common/tags.ts132
-rw-r--r--lib/extensions/discord-akairo/BotCommand.ts33
-rw-r--r--lib/extensions/discord-akairo/BotCommandHandler.ts11
-rw-r--r--lib/extensions/discord-akairo/BotInhibitor.ts6
-rw-r--r--lib/extensions/discord-akairo/BotInhibitorHandler.ts7
-rw-r--r--lib/extensions/discord-akairo/BotListener.ts5
-rw-r--r--lib/extensions/discord-akairo/BotListenerHandler.ts2
-rw-r--r--lib/extensions/discord-akairo/BotTask.ts2
-rw-r--r--lib/extensions/discord-akairo/BotTaskHandler.ts2
-rw-r--r--lib/extensions/discord-akairo/SlashMessage.ts2
-rw-r--r--lib/extensions/discord-akairo/TanzaniteClient.ts48
-rw-r--r--lib/extensions/discord.js/BotClientEvents.ts2
-rw-r--r--lib/extensions/discord.js/ExtendedGuild.ts40
-rw-r--r--lib/extensions/discord.js/ExtendedGuildMember.ts48
-rw-r--r--lib/extensions/discord.js/ExtendedMessage.ts8
-rw-r--r--lib/extensions/discord.js/ExtendedUser.ts2
-rw-r--r--lib/global.ts4
-rw-r--r--lib/index.ts4
-rw-r--r--lib/models/instance/ActivePunishment.ts8
-rw-r--r--lib/models/instance/Guild.ts10
-rw-r--r--lib/models/instance/Level.ts3
-rw-r--r--lib/models/instance/ModLog.ts123
-rw-r--r--lib/types/misc.ts8
-rw-r--r--lib/utils/Arg.ts2
-rw-r--r--lib/utils/BotClientUtils.ts5
-rw-r--r--lib/utils/Constants.ts3
-rw-r--r--lib/utils/ErrorHandler.ts2
-rw-r--r--lib/utils/Utils.ts16
35 files changed, 1008 insertions, 504 deletions
diff --git a/lib/automod/AutomodShared.ts b/lib/automod/AutomodShared.ts
index 29b0536..48217dd 100644
--- a/lib/automod/AutomodShared.ts
+++ b/lib/automod/AutomodShared.ts
@@ -162,7 +162,7 @@ export async function handleAutomodInteraction(interaction: ButtonInteraction) {
ephemeral: true
});
- const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true;
+ const check = victim ? await Moderation.permissionCheck(moderator, victim, Moderation.Action.Ban, true) : true;
if (check !== true) return interaction.reply({ content: check, ephemeral: true });
const result = await interaction.guild?.customBan({
@@ -203,7 +203,7 @@ export async function handleAutomodInteraction(interaction: ButtonInteraction) {
ephemeral: true
});
- const check = await Moderation.permissionCheck(moderator, victim, 'unmute', true);
+ const check = await Moderation.permissionCheck(moderator, victim, Moderation.Action.Unmute, true);
if (check !== true) return interaction.reply({ content: check, ephemeral: true });
const check2 = await Moderation.checkMutePermissions(interaction.guild);
diff --git a/lib/common/Appeals.ts b/lib/common/Appeals.ts
new file mode 100644
index 0000000..43c56fd
--- /dev/null
+++ b/lib/common/Appeals.ts
@@ -0,0 +1,273 @@
+import { AppealStatus, ModLog } from '#lib/models/instance/ModLog.js';
+import { colors, emojis } from '#lib/utils/Constants.js';
+import { input } from '#lib/utils/Format.js';
+import { capitalize, ModalInput } from '#lib/utils/Utils.js';
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ EmbedBuilder,
+ TextInputStyle,
+ type ButtonInteraction,
+ type ModalSubmitInteraction,
+ type Snowflake
+} from 'discord.js';
+import assert from 'node:assert/strict';
+import { Action, punishments } from './Moderation.js';
+
+type AppealBase = 'appeal_attempt' | 'appeal_submit' | 'appeal_accept' | 'appeal_deny';
+
+type RawAppealInfo = [baseId: AppealBase, punishment: `${Action}`, guildId: Snowflake, userId: Snowflake, modlogId: string];
+
+type AppealInfo = [baseId: AppealBase, punishment: Action, guildId: Snowflake, userId: Snowflake, modlogId: string];
+
+export type AppealIdString =
+ `${RawAppealInfo[0]};${RawAppealInfo[1]};${RawAppealInfo[2]};${RawAppealInfo[3]};${RawAppealInfo[4]}`;
+
+function parseAppeal(customId: AppealIdString | string): AppealInfo {
+ const [baseId, _punishment, guildId, userId, modlogId] = customId.split(';') as RawAppealInfo;
+
+ const punishment = Action[Action[_punishment] as keyof typeof Action];
+
+ return [baseId, punishment, guildId, userId, modlogId];
+}
+
+/**
+ * Handles when a user clicks the "Appeal Punishment" button on a punishment dm.
+ * @param interaction A button interaction with a custom id thar starts with "appeal_attempt;".
+ */
+export async function handleAppealAttempt(interaction: ButtonInteraction) {
+ const [baseId, punishment, guildId, userId, modlogId] = parseAppeal(interaction.customId);
+
+ const { base, past, appealCustom } = punishments[punishment];
+ const appealName = appealCustom ?? capitalize(base);
+
+ const guild = interaction.client.guilds.resolve(guildId);
+ if (!guild) {
+ return await interaction.reply(`${emojis.error} I am no longer in that server.`);
+ }
+
+ const modlog = await ModLog.findByPk(modlogId);
+ if (!modlog) {
+ return await interaction.reply(`:skull: I cannot find the modlog ${input(modlogId)}. Please report this to my developers.`);
+ }
+
+ switch (modlog.appeal) {
+ case AppealStatus.Accepted:
+ return await interaction.reply(
+ `${emojis.error} Your punishment (${input(modlogId)}) has already been appealed and accepted.`
+ );
+ case AppealStatus.Denied:
+ return await interaction.reply(
+ `${emojis.error} Your punishment (${input(modlogId)}) has already been appealed and denied.`
+ );
+ case AppealStatus.Submitted:
+ return await interaction.reply(
+ `${emojis.error} Your punishment (${input(
+ modlogId
+ )}) has already been appealed, please be patient for a moderator to review your appeal.`
+ );
+ default: {
+ const _exhaustiveCheck: AppealStatus.None = modlog.appeal;
+ }
+ }
+
+ const baseInput = {
+ style: TextInputStyle.Paragraph,
+ required: true,
+ maxLength: 1024
+ };
+
+ return await interaction.showModal({
+ customId: `appeal_submit;${punishment};${guildId};${userId};${modlogId}`,
+ title: `${appealName} Appeal`,
+ components: [
+ ModalInput({
+ ...baseInput,
+ label: `Why were you ${past}?`,
+ placeholder: `Why do you think you received a ${base}?`,
+ customId: 'appeal_reason'
+ }),
+ ModalInput({
+ ...baseInput,
+ label: 'Do you believe it was fair?',
+ placeholder: `Do you think that your ${base} is fair?`,
+ customId: 'appeal_fair'
+ }),
+ ModalInput({
+ ...baseInput,
+ label: `Why should your ${base} be removed?`,
+ placeholder: `Why do you think your ${base} be removed?`,
+ customId: 'appeal_why'
+ })
+ ]
+ });
+}
+
+/**
+ * Handles when a user submits the modal for appealing a punishment.
+ * @param interaction A modal interaction with a custom id that starts with "appeal_submit;".
+ */
+export async function handleAppealSubmit(interaction: ModalSubmitInteraction) {
+ const [baseId, punishment, guildId, userId, modlogId] = parseAppeal(interaction.customId);
+
+ const { base, past, appealCustom } = punishments[punishment];
+ const appealName = appealCustom ?? capitalize(base);
+
+ const guild = interaction.client.guilds.resolve(guildId);
+ if (!guild) {
+ return await interaction.reply(`${emojis.error} I am no longer in that server.`);
+ }
+
+ const modlog = await ModLog.findByPk(modlogId);
+ if (!modlog) {
+ return await interaction.reply(`:skull: I cannot find the modlog ${input(modlogId)}. Please report this to my developers.`);
+ }
+
+ if (modlog.appeal !== AppealStatus.None) {
+ return await interaction.reply(`Invalid appeal status: ${modlog.appeal}`);
+ }
+
+ modlog.appeal = AppealStatus.Submitted;
+ await modlog.save();
+
+ const appealChannel = await guild.getLogChannel('appeals');
+ if (!appealChannel) {
+ return await interaction.reply(`${emojis.error} I could not find an appeals channel in this server.`);
+ }
+
+ const user = await interaction.client.users.fetch(userId);
+
+ const reason = interaction.fields.getTextInputValue('appeal_reason');
+ const fair = interaction.fields.getTextInputValue('appeal_fair');
+ const why = interaction.fields.getTextInputValue('appeal_why');
+
+ const embed = new EmbedBuilder()
+ .setTitle(`${appealName} Appeal`)
+ .setColor(colors.newBlurple)
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${modlogId}` })
+ .setAuthor({ name: user.tag, iconURL: user.displayAvatarURL() })
+ .addFields(
+ { name: `Why were you ${past}?`, value: reason },
+ { name: 'Do you believe it was fair?', value: fair },
+ { name: `Why should your ${base} be removed?`, value: why }
+ );
+ return await appealChannel.send({
+ content: `Appeal submitted by ${user.tag} (${user.id})`,
+ embeds: [embed],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder({
+ customId: `appeal_accept;${punishment};${guildId};${userId};${modlogId}`,
+ label: 'Accept Appeal',
+ style: ButtonStyle.Success
+ }),
+ new ButtonBuilder({
+ customId: `appeal_deny;${punishment};${guildId};${userId};${modlogId}`,
+ label: 'Deny Appeal',
+ style: ButtonStyle.Danger
+ })
+ )
+ ]
+ });
+}
+
+/**
+ * Handles interactions when a moderator clicks the "Accept" or "Deny" button on a punishment appeal.
+ * @param interaction A button interaction with a custom id that starts with "appeal_accept;" or "appeal_deny;".
+ */
+export async function handleAppealDecision(interaction: ButtonInteraction) {
+ const [baseId, punishment, guildId, userId, modlogId] = parseAppeal(interaction.customId);
+
+ const { base, past, appealCustom } = punishments[punishment];
+ const appealName = (appealCustom ?? base).toLowerCase();
+
+ const modlog = await ModLog.findByPk(modlogId);
+
+ if (!modlog) {
+ return await interaction.reply(`:skull: I cannot find the modlog ${input(modlogId)}. Please report this to my developers.`);
+ }
+
+ if (modlog.appeal !== AppealStatus.Submitted) {
+ return await interaction.reply(
+ `:skull: Case ${input(modlogId)} has an invalid state of ${input(modlog.appeal)}. Please report this to my developers.`
+ );
+ }
+
+ if (baseId === 'appeal_deny') {
+ modlog.appeal = AppealStatus.Denied;
+ await modlog.save();
+
+ await interaction.client.users
+ .send(userId, `Your ${appealName} appeal has been denied in ${interaction.client.guilds.resolve(guildId)!}.`)
+ .catch(() => {});
+
+ return await interaction.update({
+ content: `${emojis.cross} Appeal denied.`,
+ embeds: interaction.message.embeds,
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder({
+ disabled: true,
+ style: ButtonStyle.Danger,
+ label: 'Appeal Denied',
+ custom_id: 'noop'
+ })
+ )
+ ]
+ });
+ } else if (baseId === 'appeal_accept') {
+ modlog.appeal = AppealStatus.Accepted;
+ await modlog.save();
+
+ await interaction.client.users
+ .send(userId, `Your ${appealName} appeal has been accepted in ${interaction.client.guilds.resolve(guildId)!}.`)
+ .catch(() => {});
+
+ switch (punishment) {
+ case Action.Warn:
+ case Action.Unmute:
+ case Action.Kick:
+ case Action.Unban:
+ case Action.Untimeout:
+ case Action.Unblock:
+ case Action.RemovePunishRole:
+ assert.fail(`Cannot appeal ${appealName} (Action.${Action[punishment]})`);
+ return;
+ case Action.Mute: {
+ throw new Error('Not implemented');
+ }
+ case Action.Ban: {
+ throw new Error('Not implemented');
+ }
+ case Action.Timeout: {
+ throw new Error('Not implemented');
+ }
+ case Action.Block: {
+ throw new Error('Not implemented');
+ }
+ case Action.AddPunishRole: {
+ throw new Error('Not implemented');
+ }
+ default: {
+ const _exhaustiveCheck: never = punishment;
+ }
+ }
+
+ return await interaction.update({
+ content: `${emojis.check} Appeal accepted.`,
+ embeds: interaction.message.embeds,
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder({
+ disabled: true,
+ style: ButtonStyle.Success,
+ label: 'Appeal Accepted',
+ custom_id: 'noop'
+ })
+ )
+ ]
+ });
+ }
+}
diff --git a/lib/common/ButtonPaginator.ts b/lib/common/ButtonPaginator.ts
index 02c78ea..c8c9229 100644
--- a/lib/common/ButtonPaginator.ts
+++ b/lib/common/ButtonPaginator.ts
@@ -1,5 +1,5 @@
import { DeleteButton, type CommandMessage, type SlashMessage } from '#lib';
-import { CommandUtil } from 'discord-akairo';
+import { CommandUtil } from '@notenoughupdates/discord-akairo';
import {
ActionRowBuilder,
ButtonBuilder,
@@ -34,7 +34,7 @@ export class ButtonPaginator {
protected constructor(
protected message: CommandMessage | SlashMessage,
protected embeds: EmbedBuilder[] | APIEmbed[],
- protected text: string | null,
+ protected text: string,
protected deleteOnExit: boolean,
startOn: number
) {
@@ -199,7 +199,7 @@ export class ButtonPaginator {
public static async send(
message: CommandMessage | SlashMessage,
embeds: EmbedBuilder[] | APIEmbed[],
- text: string | null = null,
+ text: string = '',
deleteOnExit = true,
startOn = 1
) {
diff --git a/lib/common/ConfirmationPrompt.ts b/lib/common/ConfirmationPrompt.ts
index b87d9ef..631b8de 100644
--- a/lib/common/ConfirmationPrompt.ts
+++ b/lib/common/ConfirmationPrompt.ts
@@ -1,5 +1,11 @@
import { type CommandMessage, type SlashMessage } from '#lib';
-import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type MessageComponentInteraction, type MessageOptions } from 'discord.js';
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ type MessageComponentInteraction,
+ type MessageCreateOptions
+} from 'discord.js';
/**
* Sends a message with buttons for the user to confirm or cancel the action.
@@ -9,7 +15,7 @@ export class ConfirmationPrompt {
* @param message The message that triggered the command
* @param messageOptions Options for sending the message
*/
- protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {}
+ protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageCreateOptions) {}
/**
* Sends a message with buttons for the user to confirm or cancel the action.
@@ -58,7 +64,7 @@ export class ConfirmationPrompt {
* @param message The message that triggered the command
* @param sendOptions Options for sending the message
*/
- public static async send(message: CommandMessage | SlashMessage, sendOptions: MessageOptions): Promise<boolean> {
+ public static async send(message: CommandMessage | SlashMessage, sendOptions: MessageCreateOptions): Promise<boolean> {
return new ConfirmationPrompt(message, sendOptions).send();
}
}
diff --git a/lib/common/DeleteButton.ts b/lib/common/DeleteButton.ts
index 340d07f..2d053cd 100644
--- a/lib/common/DeleteButton.ts
+++ b/lib/common/DeleteButton.ts
@@ -1,5 +1,5 @@
import { PaginateEmojis, type CommandMessage, type SlashMessage } from '#lib';
-import { CommandUtil } from 'discord-akairo';
+import { CommandUtil } from '@notenoughupdates/discord-akairo';
import {
ActionRowBuilder,
ButtonBuilder,
@@ -7,7 +7,7 @@ import {
MessageComponentInteraction,
MessageEditOptions,
MessagePayload,
- type MessageOptions
+ type MessageCreateOptions
} from 'discord.js';
/**
@@ -18,7 +18,7 @@ export class DeleteButton {
* @param message The message to respond to
* @param messageOptions The send message options
*/
- protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {}
+ protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageCreateOptions) {}
/**
* Sends a message with a button for the user to delete it.
@@ -72,7 +72,7 @@ export class DeleteButton {
* @param message The message to respond to
* @param options The send message options
*/
- public static async send(message: CommandMessage | SlashMessage, options: Omit<MessageOptions, 'components'>) {
+ public static async send(message: CommandMessage | SlashMessage, options: Omit<MessageCreateOptions, 'components'>) {
return new DeleteButton(message, options).send();
}
}
diff --git a/lib/common/HighlightManager.ts b/lib/common/HighlightManager.ts
index ca71a83..3a76a5b 100644
--- a/lib/common/HighlightManager.ts
+++ b/lib/common/HighlightManager.ts
@@ -435,6 +435,9 @@ export class HighlightManager {
}
private generateDmEmbed(message: Message, hl: HighlightWord) {
+ // janky MessageManager typings
+ assert(message.inGuild());
+
const recentMessages = message.channel.messages.cache
.filter((m) => m.createdTimestamp <= message.createdTimestamp && m.id !== message.id)
.filter((m) => m.cleanContent?.trim().length > 0)
diff --git a/lib/common/Moderation.ts b/lib/common/Moderation.ts
index 60e32c0..7697b2f 100644
--- a/lib/common/Moderation.ts
+++ b/lib/common/Moderation.ts
@@ -1,17 +1,7 @@
-import {
- ActivePunishment,
- ActivePunishmentType,
- baseMuteResponse,
- colors,
- emojis,
- format,
- Guild as GuildDB,
- humanizeDuration,
- ModLog,
- permissionsResponse,
- type ModLogType,
- type ValueOf
-} from '#lib';
+import { baseMuteResponse, permissionsResponse } from '#lib/extensions/discord.js/ExtendedGuildMember.js';
+import { ActivePunishment, ActivePunishmentType, Guild as GuildDB, ModLog, type ModLogType } from '#lib/models/index.js';
+import { colors, emojis } from '#lib/utils/Constants.js';
+import { format, humanizeDuration, ValueOf } from '#lib/utils/Utils.js';
import assert from 'assert/strict';
import {
ActionRowBuilder,
@@ -28,29 +18,178 @@ import {
type UserResolvable
} from 'discord.js';
-enum punishMap {
- 'warned' = 'warn',
- 'muted' = 'mute',
- 'unmuted' = 'unmute',
- 'kicked' = 'kick',
- 'banned' = 'ban',
- 'unbanned' = 'unban',
- 'timedout' = 'timeout',
- 'untimedout' = 'untimeout',
- 'blocked' = 'block',
- 'unblocked' = 'unblock'
+export enum Action {
+ Warn,
+ Mute,
+ Unmute,
+ Kick,
+ Ban,
+ Unban,
+ Timeout,
+ Untimeout,
+ Block,
+ Unblock,
+ AddPunishRole,
+ RemovePunishRole
+}
+
+interface ActionInfo {
+ /**
+ * The base verb of the action
+ */
+ base: string;
+
+ /**
+ * The past tense form of the action
+ */
+ past: string;
+
+ /**
+ * Whether or not a user can appeal this action
+ */
+ appealable: boolean;
+
+ /**
+ * Whether a moderator can perform this action on themself.
+ */
+ selfInflictable: boolean;
+
+ /**
+ * Whether the action requires the target to be in the guild.
+ */
+ membershipRequired: boolean;
+
+ /**
+ * Custom appeal title, otherwise {@link ActionInfo.base} is used.
+ */
+ appealCustom?: string;
}
-enum reversedPunishMap {
- 'warn' = 'warned',
- 'mute' = 'muted',
- 'unmute' = 'unmuted',
- 'kick' = 'kicked',
- 'ban' = 'banned',
- 'unban' = 'unbanned',
- 'timeout' = 'timedout',
- 'untimeout' = 'untimedout',
- 'block' = 'blocked',
- 'unblock' = 'unblocked'
+
+export const punishments: Record<Action, ActionInfo> = {
+ [Action.Warn]: {
+ base: 'warn',
+ past: 'warned',
+ appealable: false,
+ selfInflictable: false,
+ membershipRequired: true
+ },
+ [Action.Mute]: {
+ base: 'mute',
+ past: 'muted',
+ appealable: true,
+ selfInflictable: false,
+ membershipRequired: true
+ },
+ [Action.Unmute]: {
+ base: 'unmute',
+ past: 'unmuted',
+ appealable: false,
+ selfInflictable: true,
+ membershipRequired: true
+ },
+ [Action.Kick]: {
+ base: 'kick',
+ past: 'kicked',
+ appealable: false,
+ selfInflictable: false,
+ membershipRequired: true
+ },
+ [Action.Ban]: {
+ base: 'ban',
+ past: 'banned',
+ appealable: true,
+ selfInflictable: false,
+ membershipRequired: false
+ },
+ [Action.Unban]: {
+ base: 'unban',
+ past: 'unbanned',
+ appealable: false,
+ selfInflictable: true,
+ membershipRequired: false
+ },
+ [Action.Timeout]: {
+ base: 'timeout',
+ past: 'timed out',
+ appealable: true,
+ selfInflictable: false,
+ membershipRequired: true
+ },
+ [Action.Untimeout]: {
+ base: 'untimeout',
+ past: 'untimed out',
+ appealable: false,
+ selfInflictable: true,
+ membershipRequired: true
+ },
+ [Action.Block]: {
+ base: 'block',
+ past: 'blocked',
+ appealable: true,
+ selfInflictable: false,
+ membershipRequired: true
+ },
+ [Action.Unblock]: {
+ base: 'unblock',
+ past: 'unblocked',
+ appealable: false,
+ selfInflictable: true,
+ membershipRequired: true
+ },
+ [Action.AddPunishRole]: {
+ base: 'add a punishment role to',
+ past: 'added punishment role',
+ appealable: true,
+ appealCustom: 'Punishment Role',
+ selfInflictable: false,
+ membershipRequired: true
+ },
+ [Action.RemovePunishRole]: {
+ base: 'remove a punishment role from',
+ past: 'removed punishment role',
+ appealable: false,
+ selfInflictable: true,
+ membershipRequired: true
+ }
+};
+
+interface BaseOptions {
+ /**
+ * The client.
+ */
+ client: Client;
+}
+
+interface BaseCreateModLogEntryOptions extends BaseOptions {
+ /**
+ * The type of modlog entry.
+ */
+ type: ModLogType;
+
+ /**
+ * The reason for the punishment.
+ */
+ reason: string | undefined | null;
+
+ /**
+ * The duration of the punishment.
+ */
+ duration?: number;
+
+ /**
+ * Whether the punishment is a pseudo punishment.
+ */
+ pseudo?: boolean;
+
+ /**
+ * The evidence for the punishment.
+ */
+ evidence?: string;
+
+ /**
+ * Makes the modlog entry hidden.
+ */
+ hidden?: boolean;
}
/**
@@ -65,57 +204,52 @@ enum reversedPunishMap {
export async function permissionCheck(
moderator: GuildMember,
victim: GuildMember,
- type:
- | 'mute'
- | 'unmute'
- | 'warn'
- | 'kick'
- | 'ban'
- | 'unban'
- | 'add a punishment role to'
- | 'remove a punishment role from'
- | 'block'
- | 'unblock'
- | 'timeout'
- | 'untimeout',
+ type: Action,
checkModerator = true,
force = false
): Promise<true | string> {
if (force) return true;
+ const action = punishments[type];
+
// If the victim is not in the guild anymore it will be undefined
- if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true;
+ if (!victim?.guild && action.membershipRequired) return true;
- if (moderator.guild.id !== victim.guild.id) {
- throw new Error('moderator and victim not in same guild');
- }
+ assert(moderator.guild.id === victim.guild.id, 'moderator and victim should be from the same guild');
const isOwner = moderator.guild.ownerId === moderator.id;
- if (moderator.id === victim.id && !type.startsWith('un')) {
- return `${emojis.error} You cannot ${type} yourself.`;
+
+ const selfInflicted = moderator.id === victim.id;
+
+ if (selfInflicted && !action.selfInflictable) {
+ return `${emojis.error} You cannot ${action.base} yourself.`;
}
if (
moderator.roles.highest.position <= victim.roles.highest.position &&
!isOwner &&
- !(type.startsWith('un') && moderator.id === victim.id)
+ !(action.selfInflictable && selfInflicted)
) {
- return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`;
+ return `${emojis.error} You cannot ${action.base} ${format.input(
+ victim.user.tag
+ )} because they have higher or equal role hierarchy as you do.`;
}
if (
victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position &&
- !(type.startsWith('un') && moderator.id === victim.id)
+ !(action.selfInflictable && selfInflicted)
) {
- return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`;
+ return `${emojis.error} You cannot ${action.base} ${format.input(
+ victim.user.tag
+ )} because they have higher or equal role hierarchy as I do.`;
}
if (
checkModerator &&
victim.permissions.has(PermissionFlagsBits.ManageMessages) &&
- !(type.startsWith('un') && moderator.id === victim.id)
+ !(action.selfInflictable && selfInflicted)
) {
if (await moderator.guild.hasFeature('modsCanPunishMods')) {
return true;
} else {
- return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`;
+ return `${emojis.error} You cannot ${action.base} ${format.input(victim.user.tag)} because they are a moderator.`;
}
}
return true;
@@ -129,18 +263,49 @@ export async function permissionCheck(
export async function checkMutePermissions(
guild: Guild
): Promise<ValueOf<typeof baseMuteResponse> | ValueOf<typeof permissionsResponse> | true> {
- if (!guild.members.me!.permissions.has('ManageRoles')) return permissionsResponse.MISSING_PERMISSIONS;
+ if (!guild.members.me!.permissions.has('ManageRoles')) {
+ return permissionsResponse.MISSING_PERMISSIONS;
+ }
+
const muteRoleID = await guild.getSetting('muteRole');
if (!muteRoleID) return baseMuteResponse.NO_MUTE_ROLE;
+
const muteRole = guild.roles.cache.get(muteRoleID);
if (!muteRole) return baseMuteResponse.MUTE_ROLE_INVALID;
- if (muteRole.position >= guild.members.me!.roles.highest.position || muteRole.managed)
+
+ if (muteRole.position >= guild.members.me!.roles.highest.position || muteRole.managed) {
return baseMuteResponse.MUTE_ROLE_NOT_MANAGEABLE;
+ }
return true;
}
/**
+ * Options for creating a modlog entry.
+ */
+export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions {
+ /**
+ * The client.
+ */
+ client: Client;
+
+ /**
+ * The user that a modlog entry is created for.
+ */
+ user: GuildMemberResolvable;
+
+ /**
+ * The moderator that created the modlog entry.
+ */
+ moderator: GuildMemberResolvable;
+
+ /**
+ * The guild that the punishment is created for.
+ */
+ guild: GuildResolvable;
+}
+
+/**
* Creates a modlog entry for a punishment.
* @param options Options for creating a modlog entry.
* @param getCaseNumber Whether or not to get the case number of the entry.
@@ -166,6 +331,26 @@ export async function createModLogEntry(
}
/**
+ * Simple options for creating a modlog entry.
+ */
+export interface SimpleCreateModLogEntryOptions extends BaseCreateModLogEntryOptions {
+ /**
+ * The user that a modlog entry is created for.
+ */
+ user: Snowflake;
+
+ /**
+ * The moderator that created the modlog entry.
+ */
+ moderator: Snowflake;
+
+ /**
+ * The guild that the punishment is created for.
+ */
+ guild: Snowflake;
+}
+
+/**
* Creates a modlog entry with already resolved ids.
* @param options Options for creating a modlog entry.
* @param getCaseNumber Whether or not to get the case number of the entry.
@@ -192,6 +377,7 @@ export async function createModLogEntrySimple(
evidence: options.evidence,
hidden: options.hidden ?? false
});
+
const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => {
await options.client.utils.handleError('createModLogEntry', e);
return null;
@@ -206,6 +392,41 @@ export async function createModLogEntrySimple(
}
/**
+ * Options for creating a punishment entry.
+ */
+export interface CreatePunishmentEntryOptions extends BaseOptions {
+ /**
+ * The type of punishment.
+ */
+ type: 'mute' | 'ban' | 'role' | 'block';
+
+ /**
+ * The user that the punishment is created for.
+ */
+ user: GuildMemberResolvable;
+
+ /**
+ * The length of time the punishment lasts for.
+ */
+ duration: number | undefined;
+
+ /**
+ * The guild that the punishment is created for.
+ */
+ guild: GuildResolvable;
+
+ /**
+ * The id of the modlog that is linked to the punishment entry.
+ */
+ modlog: string;
+
+ /**
+ * Extra information for the punishment. The role for role punishments and the channel for blocks.
+ */
+ extraInfo?: Snowflake;
+}
+
+/**
* Creates a punishment entry.
* @param options Options for creating the punishment entry.
* @returns The database entry, or null if no entry is created.
@@ -221,6 +442,7 @@ export async function createPunishmentEntry(options: CreatePunishmentEntryOption
? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo }
: { user, type, guild, expires, modlog: options.modlog }
);
+
return await entry.save().catch(async (e) => {
await options.client.utils.handleError('createPunishmentEntry', e);
return null;
@@ -228,6 +450,31 @@ export async function createPunishmentEntry(options: CreatePunishmentEntryOption
}
/**
+ * Options for removing a punishment entry.
+ */
+export interface RemovePunishmentEntryOptions extends BaseOptions {
+ /**
+ * The type of punishment.
+ */
+ type: 'mute' | 'ban' | 'role' | 'block';
+
+ /**
+ * The user that the punishment is destroyed for.
+ */
+ user: GuildMemberResolvable;
+
+ /**
+ * The guild that the punishment was in.
+ */
+ guild: GuildResolvable;
+
+ /**
+ * Extra information for the punishment. The role for role punishments and the channel for blocks.
+ */
+ extraInfo?: Snowflake;
+}
+
+/**
* Destroys a punishment entry.
* @param options Options for destroying the punishment entry.
* @returns Whether or not the entry was destroyed.
@@ -250,6 +497,7 @@ export async function removePunishmentEntry(options: RemovePunishmentEntryOption
await options.client.utils.handleError('removePunishmentEntry', e);
success = false;
});
+
if (entries) {
const promises = entries.map(async (entry) =>
entry.destroy().catch(async (e) => {
@@ -270,212 +518,14 @@ export async function removePunishmentEntry(options: RemovePunishmentEntryOption
*/
function findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') {
const typeMap = {
- ['mute']: ActivePunishmentType.MUTE,
- ['ban']: ActivePunishmentType.BAN,
- ['role']: ActivePunishmentType.ROLE,
- ['block']: ActivePunishmentType.BLOCK
+ mute: ActivePunishmentType.Mute,
+ ban: ActivePunishmentType.Ban,
+ role: ActivePunishmentType.Role,
+ block: ActivePunishmentType.Block
};
return typeMap[type];
}
-export function punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent {
- return punishMap[punishment];
-}
-
-export function punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM {
- return reversedPunishMap[punishment];
-}
-
-/**
- * Notifies the specified user of their punishment.
- * @param options Options for notifying the user.
- * @returns Whether or not the dm was successfully sent.
- */
-export async function punishDM(options: PunishDMOptions): Promise<boolean> {
- const ending = await options.guild.getSetting('punishmentEnding');
- const dmEmbed =
- ending && ending.length && options.sendFooter
- ? new EmbedBuilder().setDescription(ending).setColor(colors.newBlurple)
- : undefined;
-
- const appealsEnabled = !!(
- (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals'))
- );
-
- let content = `You have been ${options.punishment} `;
- if (options.punishment.includes('blocked')) {
- assert(options.channel);
- content += `from <#${options.channel}> `;
- }
- content += `in ${format.input(options.guild.name)} `;
- if (options.duration !== null && options.duration !== undefined)
- content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently ';
- const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided';
- content += `for ${format.input(reason)}.`;
-
- let components;
- if (appealsEnabled && options.modlog)
- components = [
- new ActionRowBuilder<ButtonBuilder>({
- components: [
- new ButtonBuilder({
- customId: `appeal;${punishmentToPresentTense(options.punishment)};${
- options.guild.id
- };${options.client.users.resolveId(options.user)};${options.modlog}`,
- style: ButtonStyle.Primary,
- label: 'Appeal'
- }).toJSON()
- ]
- })
- ];
-
- const dmSuccess = await options.client.users
- .send(options.user, {
- content,
- embeds: dmEmbed ? [dmEmbed] : undefined,
- components
- })
- .catch(() => false);
- return !!dmSuccess;
-}
-
-interface BaseCreateModLogEntryOptions extends BaseOptions {
- /**
- * The type of modlog entry.
- */
- type: ModLogType;
-
- /**
- * The reason for the punishment.
- */
- reason: string | undefined | null;
-
- /**
- * The duration of the punishment.
- */
- duration?: number;
-
- /**
- * Whether the punishment is a pseudo punishment.
- */
- pseudo?: boolean;
-
- /**
- * The evidence for the punishment.
- */
- evidence?: string;
-
- /**
- * Makes the modlog entry hidden.
- */
- hidden?: boolean;
-}
-
-/**
- * Options for creating a modlog entry.
- */
-export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions {
- /**
- * The client.
- */
- client: Client;
-
- /**
- * The user that a modlog entry is created for.
- */
- user: GuildMemberResolvable;
-
- /**
- * The moderator that created the modlog entry.
- */
- moderator: GuildMemberResolvable;
-
- /**
- * The guild that the punishment is created for.
- */
- guild: GuildResolvable;
-}
-
-/**
- * Simple options for creating a modlog entry.
- */
-export interface SimpleCreateModLogEntryOptions extends BaseCreateModLogEntryOptions {
- /**
- * The user that a modlog entry is created for.
- */
- user: Snowflake;
-
- /**
- * The moderator that created the modlog entry.
- */
- moderator: Snowflake;
-
- /**
- * The guild that the punishment is created for.
- */
- guild: Snowflake;
-}
-
-/**
- * Options for creating a punishment entry.
- */
-export interface CreatePunishmentEntryOptions extends BaseOptions {
- /**
- * The type of punishment.
- */
- type: 'mute' | 'ban' | 'role' | 'block';
-
- /**
- * The user that the punishment is created for.
- */
- user: GuildMemberResolvable;
-
- /**
- * The length of time the punishment lasts for.
- */
- duration: number | undefined;
-
- /**
- * The guild that the punishment is created for.
- */
- guild: GuildResolvable;
-
- /**
- * The id of the modlog that is linked to the punishment entry.
- */
- modlog: string;
-
- /**
- * Extra information for the punishment. The role for role punishments and the channel for blocks.
- */
- extraInfo?: Snowflake;
-}
-
-/**
- * Options for removing a punishment entry.
- */
-export interface RemovePunishmentEntryOptions extends BaseOptions {
- /**
- * The type of punishment.
- */
- type: 'mute' | 'ban' | 'role' | 'block';
-
- /**
- * The user that the punishment is destroyed for.
- */
- user: GuildMemberResolvable;
-
- /**
- * The guild that the punishment was in.
- */
- guild: GuildResolvable;
-
- /**
- * Extra information for the punishment. The role for role punishments and the channel for blocks.
- */
- extraInfo?: Snowflake;
-}
-
/**
* Options for sending a user a punishment dm.
*/
@@ -498,7 +548,7 @@ export interface PunishDMOptions extends BaseOptions {
/**
* The punishment that the user has received.
*/
- punishment: PunishmentTypeDM;
+ punishment: Action;
/**
* The reason the user's punishment.
@@ -522,35 +572,59 @@ export interface PunishDMOptions extends BaseOptions {
channel?: Snowflake;
}
-interface BaseOptions {
- /**
- * The client.
- */
- client: Client;
-}
+/**
+ * Notifies the specified user of their punishment.
+ * @param options Options for notifying the user.
+ * @returns Whether or not the dm was successfully sent.
+ */
+export async function punishDM(options: PunishDMOptions): Promise<boolean> {
+ const ending = await options.guild.getSetting('punishmentEnding');
+ const dmEmbed =
+ ending && ending.length && options.sendFooter
+ ? new EmbedBuilder().setDescription(ending).setColor(colors.newBlurple)
+ : undefined;
+
+ const appealsEnabled =
+ (await options.guild.hasFeature('punishmentAppeals')) && Boolean(await options.guild.getLogChannel('appeals'));
-export type PunishmentTypeDM =
- | 'warned'
- | 'muted'
- | 'unmuted'
- | 'kicked'
- | 'banned'
- | 'unbanned'
- | 'timedout'
- | 'untimedout'
- | 'blocked'
- | 'unblocked';
-
-export type PunishmentTypePresent =
- | 'warn'
- | 'mute'
- | 'unmute'
- | 'kick'
- | 'ban'
- | 'unban'
- | 'timeout'
- | 'untimeout'
- | 'block'
- | 'unblock';
-
-export type AppealButtonId = `appeal;${PunishmentTypePresent};${Snowflake};${Snowflake};${string}`;
+ let content = `You have been ${options.punishment} `;
+ if ([Action.Block, Action.Unblock].includes(options.punishment)) {
+ assert(options.channel);
+ content += `from <#${options.channel}> `;
+ }
+ content += `in ${format.input(options.guild.name)} `;
+ if (options.duration !== null && options.duration !== undefined) {
+ content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently ';
+ }
+ const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided';
+ content += `for ${format.input(reason)}.`;
+
+ let components;
+ if (appealsEnabled && options.modlog) {
+ const punishment = options.punishment;
+ const guildId = options.guild.id;
+ const userId = options.client.users.resolveId(options.user);
+ const modlogCase = options.modlog;
+
+ components = [
+ new ActionRowBuilder<ButtonBuilder>({
+ components: [
+ new ButtonBuilder({
+ customId: `appeal_attempt;${Action[punishment]};${guildId};${userId};${modlogCase}`,
+ style: ButtonStyle.Primary,
+ label: 'Appeal Punishment'
+ })
+ ]
+ })
+ ];
+ }
+
+ const dmSuccess = await options.client.users
+ .send(options.user, {
+ content,
+ embeds: dmEmbed ? [dmEmbed] : undefined,
+ components
+ })
+ .catch(() => false);
+ return !!dmSuccess;
+}
diff --git a/lib/common/tags.ts b/lib/common/tags.ts
index 4af8783..826b820 100644
--- a/lib/common/tags.ts
+++ b/lib/common/tags.ts
@@ -1,5 +1,5 @@
-/* these functions are adapted from the common-tags npm package which is licensed under the MIT license */
-/* the JSDOCs are adapted from the @types/common-tags npm package which is licensed under the MIT license */
+/* The stripIndent, stripIndents, and format functions are adapted from the common-tags npm package which is licensed under the MIT license */
+/* The JSDOCs for said functions are adapted from the @types/common-tags npm package which is licensed under the MIT license */
/**
* Strips the **initial** indentation from the beginning of each line in a multiline string.
@@ -32,3 +32,131 @@ function format(strings: TemplateStringsArray, ...expressions: any[]) {
.replace(/^\n/, '');
return str;
}
+
+export function commas(strings: TemplateStringsArray, ...expressions: any[]) {
+ const str = strings
+ .reduce((result, string, index) => ''.concat(result, localString(expressions[index - 1]), string))
+ .replace(/[^\S\n]+$/gm, '')
+ .replace(/^\n/, '');
+
+ return str;
+}
+
+function localString(val: any) {
+ return typeof val === 'number' ? val.toLocaleString() : val;
+}
+
+export function commasStripIndents(strings: TemplateStringsArray, ...expressions: any[]) {
+ return stripIndents`${commas(strings, ...expressions)}`;
+}
+
+function splitByNewline(strings: TemplateStringsArray, ...expressions: any[]): any[][] {
+ const ret: any[][] = [];
+
+ let current: any[] = [];
+
+ for (let i = 0; i < strings.length; i++) {
+ const string = strings[i];
+ if (string.includes('\n')) {
+ // divide the string by newlines
+ const [first, ...rest] = string.split('\n');
+
+ // no point add an empty string
+ if (first !== '') {
+ // complete the current line
+ current.push(first);
+ }
+
+ // ignore empty first line
+ if (i !== 0 && current.length !== 1 && current[1] !== '') {
+ // add the current line to the list of lines
+ ret.push(current);
+ }
+
+ // handle multiple newlines
+ if (rest.length > 1) {
+ // loop though everything but the final element
+ for (const line of rest.slice(0, -1)) {
+ ret.push([line]);
+ }
+ }
+
+ // since there are no more empty newlines, add to the current line so that expressions can be added
+ const last = rest[rest.length - 1];
+ current = [last];
+ } else {
+ // if there are no newlines, just add to the current line
+ current.push(string);
+ }
+
+ // now add the expression
+ if (i < expressions.length) current.push(expressions[i]);
+ }
+
+ // add the final line
+ ret.push(current);
+
+ return ret;
+}
+
+/**
+ * Creates information fields for embeds. Commas are added to numbers.
+ * Lines are ignored if the expression is `null`, `undefined`, or `false`.
+ * Additionally, leading whitespace is removed. If the first line is empty,
+ * it is ignored.
+ * @example
+ * ```ts
+ * const value = 'value';
+ * const condition = false;
+ *
+ * embedField`
+ * Header ${value}
+ * Another Header ${condition && 50}
+ * A Third Header ${50000}`
+ *
+ * // **Header:** value
+ * // **A Third Header:** 50,000
+ * ```
+ */
+export function embedField(strings: TemplateStringsArray, ...expressions: any[]) {
+ const lines: any[][] = splitByNewline(strings, ...expressions);
+
+ // loop through each line and remove any leading whitespace
+ for (let i = 0; i < lines.length; i++) {
+ lines[i][0] = lines[i][0].replace(/^[^\S\n]+/gm, '');
+ }
+
+ const result: string[] = [];
+
+ out: for (let i = 0; i < lines.length; i++) {
+ // eslint-disable-next-line prefer-const
+ let [header, ...rest] = lines[i];
+
+ header = `**${header.trim()}:**`;
+
+ const lineContent: string[] = [];
+
+ for (let i = 0; i < rest.length; i++) {
+ const value = rest[i];
+ if (typeof value === 'string') {
+ lineContent.push(value);
+ } else if (typeof value === 'number') {
+ // add commas to numbers
+ lineContent.push(value.toLocaleString());
+ } else if (value === null || value === undefined || value === false) {
+ if (i === 0) {
+ // ignore this line
+ continue out;
+ } else {
+ throw new Error('Null or false values can only be used as the first expression in a line.');
+ }
+ } else {
+ lineContent.push(value.toString());
+ }
+ }
+
+ result.push(`${header} ${lineContent.join('')}`);
+ }
+
+ return result.join('\n');
+}
diff --git a/lib/extensions/discord-akairo/BotCommand.ts b/lib/extensions/discord-akairo/BotCommand.ts
index 11a8bad..a975667 100644
--- a/lib/extensions/discord-akairo/BotCommand.ts
+++ b/lib/extensions/discord-akairo/BotCommand.ts
@@ -11,14 +11,6 @@ import type {
import {
Command,
CommandArguments,
- type AkairoApplicationCommandAutocompleteOption,
- type AkairoApplicationCommandChannelOptionData,
- type AkairoApplicationCommandChoicesData,
- type AkairoApplicationCommandNonOptionsData,
- type AkairoApplicationCommandNumericOptionData,
- type AkairoApplicationCommandOptionData,
- type AkairoApplicationCommandSubCommandData,
- type AkairoApplicationCommandSubGroupData,
type ArgumentMatch,
type ArgumentOptions,
type ArgumentType,
@@ -29,8 +21,9 @@ import {
type ContextMenuCommand,
type SlashOption,
type SlashResolveType
-} from 'discord-akairo';
+} from '@notenoughupdates/discord-akairo';
import {
+ ApplicationCommandChannelOption,
PermissionsBitField,
type ApplicationCommandOptionChoiceData,
type ApplicationCommandOptionType,
@@ -39,7 +32,7 @@ import {
type Snowflake,
type User
} from 'discord.js';
-import _ from 'lodash';
+import { camelCase } from 'lodash-es';
import { SlashMessage } from './SlashMessage.js';
export interface OverriddenBaseArgumentType extends BaseArgumentType {
@@ -89,7 +82,7 @@ interface BaseBotArgumentOptions extends Omit<ArgumentOptions, 'type' | 'prompt'
/**
* The type used for slash commands. Set to false to disable this argument for slash commands.
*/
- slashType: AkairoApplicationCommandOptionData['type'] | false;
+ slashType: SlashOption['type'] | false;
/**
* Allows you to get a discord resolved object
@@ -111,7 +104,7 @@ interface BaseBotArgumentOptions extends Omit<ArgumentOptions, 'type' | 'prompt'
/**
* When the option type is channel, the allowed types of channels that can be selected
*/
- channelTypes?: AkairoApplicationCommandChannelOptionData['channelTypes'];
+ channelTypes?: ApplicationCommandChannelOption['channelTypes'];
/**
* The minimum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option
@@ -508,8 +501,11 @@ export abstract class BotCommand extends Command {
(options_.slash || options_.slashOnly) &&
arg.slashType !== false
) {
+ // credit to https://dev.to/lucianbc/union-type-merging-in-typescript-9al
+ type AllKeys<T> = T extends any ? keyof T : never;
+
const newArg: {
- [key in SlashOptionKeys]?: any;
+ [key in AllKeys<SlashOption>]?: any;
} = {
name: arg.id,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -560,7 +556,7 @@ export abstract class BotCommand extends Command {
});
for (const arg of combined) {
- const name = _.camelCase('id' in arg ? arg.id : arg.name),
+ const name = camelCase('id' in arg ? arg.id : arg.name),
description = arg.description || '*No description provided.*',
optional = arg.optional ?? false,
autocomplete = arg.autocomplete ?? false,
@@ -607,15 +603,6 @@ export abstract class BotCommand extends Command {
public abstract override exec(message: CommandMessage | SlashMessage, args: CommandArguments): any;
}
-type SlashOptionKeys =
- | keyof AkairoApplicationCommandSubGroupData
- | keyof AkairoApplicationCommandNonOptionsData
- | keyof AkairoApplicationCommandChannelOptionData
- | keyof AkairoApplicationCommandChoicesData
- | keyof AkairoApplicationCommandAutocompleteOption
- | keyof AkairoApplicationCommandNumericOptionData
- | keyof AkairoApplicationCommandSubCommandData;
-
interface PseudoArguments extends BaseBotArgumentType {
boolean: boolean;
flag: boolean;
diff --git a/lib/extensions/discord-akairo/BotCommandHandler.ts b/lib/extensions/discord-akairo/BotCommandHandler.ts
index e9b509f..c1415e3 100644
--- a/lib/extensions/discord-akairo/BotCommandHandler.ts
+++ b/lib/extensions/discord-akairo/BotCommandHandler.ts
@@ -1,5 +1,10 @@
-import type { BotCommand, CommandMessage, SlashMessage } from '#lib';
-import { CommandHandler, CommandHandlerEvents, type Category, type CommandHandlerOptions } from 'discord-akairo';
+import type { BotCommand, CommandMessage, SlashMessage, TanzaniteClient } from '#lib';
+import {
+ CommandHandler,
+ CommandHandlerEvents,
+ type Category,
+ type CommandHandlerOptions
+} from '@notenoughupdates/discord-akairo';
import { GuildMember, PermissionResolvable, type Collection, type Message, type PermissionsString } from 'discord.js';
import { CommandHandlerEvent } from '../../utils/Constants.js';
@@ -41,6 +46,8 @@ export interface BotCommandHandlerEvents extends CommandHandlerEvents {
}
export class BotCommandHandler extends CommandHandler {
+ public declare readonly client: TanzaniteClient;
+
public declare modules: Collection<string, BotCommand>;
public declare categories: Collection<string, Category<string, BotCommand>>;
diff --git a/lib/extensions/discord-akairo/BotInhibitor.ts b/lib/extensions/discord-akairo/BotInhibitor.ts
index 8892b8b..8a53e0d 100644
--- a/lib/extensions/discord-akairo/BotInhibitor.ts
+++ b/lib/extensions/discord-akairo/BotInhibitor.ts
@@ -1,8 +1,10 @@
-import type { BotCommand, CommandMessage, InhibitorReason, InhibitorType, SlashMessage } from '#lib';
-import { Inhibitor, InhibitorOptions } from 'discord-akairo';
+import type { BotCommand, CommandMessage, InhibitorReason, InhibitorType, SlashMessage, TanzaniteClient } from '#lib';
+import { Inhibitor, InhibitorOptions } from '@notenoughupdates/discord-akairo';
import { Message } from 'discord.js';
export abstract class BotInhibitor extends Inhibitor {
+ public declare readonly client: TanzaniteClient;
+
public constructor(id: InhibitorReason, options?: BotInhibitorOptions) {
super(id, options);
}
diff --git a/lib/extensions/discord-akairo/BotInhibitorHandler.ts b/lib/extensions/discord-akairo/BotInhibitorHandler.ts
index c6f318d..05caca6 100644
--- a/lib/extensions/discord-akairo/BotInhibitorHandler.ts
+++ b/lib/extensions/discord-akairo/BotInhibitorHandler.ts
@@ -1,3 +1,6 @@
-import { InhibitorHandler } from 'discord-akairo';
+import { InhibitorHandler } from '@notenoughupdates/discord-akairo';
+import { TanzaniteClient } from './TanzaniteClient.js';
-export class BotInhibitorHandler extends InhibitorHandler {}
+export class BotInhibitorHandler extends InhibitorHandler {
+ public declare readonly client: TanzaniteClient;
+}
diff --git a/lib/extensions/discord-akairo/BotListener.ts b/lib/extensions/discord-akairo/BotListener.ts
index 4f760e2..85acce3 100644
--- a/lib/extensions/discord-akairo/BotListener.ts
+++ b/lib/extensions/discord-akairo/BotListener.ts
@@ -1,6 +1,9 @@
-import { Listener, type ListenerOptions } from 'discord-akairo';
+import { Listener, type ListenerOptions } from '@notenoughupdates/discord-akairo';
+import { TanzaniteClient } from './TanzaniteClient.js';
export abstract class BotListener extends Listener {
+ public declare readonly client: TanzaniteClient<boolean>;
+
public constructor(id: string, options: BotListenerOptions) {
super(id, options);
}
diff --git a/lib/extensions/discord-akairo/BotListenerHandler.ts b/lib/extensions/discord-akairo/BotListenerHandler.ts
index bc14a53..6a1ad4c 100644
--- a/lib/extensions/discord-akairo/BotListenerHandler.ts
+++ b/lib/extensions/discord-akairo/BotListenerHandler.ts
@@ -1,4 +1,4 @@
-import { ListenerHandler } from 'discord-akairo';
+import { ListenerHandler } from '@notenoughupdates/discord-akairo';
import type readline from 'readline';
import { TanzaniteClient } from './TanzaniteClient.js';
diff --git a/lib/extensions/discord-akairo/BotTask.ts b/lib/extensions/discord-akairo/BotTask.ts
index 09b30ed..fd0dc2e 100644
--- a/lib/extensions/discord-akairo/BotTask.ts
+++ b/lib/extensions/discord-akairo/BotTask.ts
@@ -1,3 +1,3 @@
-import { Task } from 'discord-akairo';
+import { Task } from '@notenoughupdates/discord-akairo';
export abstract class BotTask extends Task {}
diff --git a/lib/extensions/discord-akairo/BotTaskHandler.ts b/lib/extensions/discord-akairo/BotTaskHandler.ts
index b522f2c..1b4b5bd 100644
--- a/lib/extensions/discord-akairo/BotTaskHandler.ts
+++ b/lib/extensions/discord-akairo/BotTaskHandler.ts
@@ -1,3 +1,3 @@
-import { TaskHandler } from 'discord-akairo';
+import { TaskHandler } from '@notenoughupdates/discord-akairo';
export class BotTaskHandler extends TaskHandler {}
diff --git a/lib/extensions/discord-akairo/SlashMessage.ts b/lib/extensions/discord-akairo/SlashMessage.ts
index 0a6669b..b93f25f 100644
--- a/lib/extensions/discord-akairo/SlashMessage.ts
+++ b/lib/extensions/discord-akairo/SlashMessage.ts
@@ -1,3 +1,3 @@
-import { AkairoMessage } from 'discord-akairo';
+import { AkairoMessage } from '@notenoughupdates/discord-akairo';
export class SlashMessage extends AkairoMessage {}
diff --git a/lib/extensions/discord-akairo/TanzaniteClient.ts b/lib/extensions/discord-akairo/TanzaniteClient.ts
index ac09aea..a8346ba 100644
--- a/lib/extensions/discord-akairo/TanzaniteClient.ts
+++ b/lib/extensions/discord-akairo/TanzaniteClient.ts
@@ -11,8 +11,20 @@ import {
snowflake
} from '#args';
import type { Config } from '#config';
-import { patch, type PatchedElements } from '@notenoughupdates/events-intercept';
-import * as Sentry from '@sentry/node';
+import {
+ ActivePunishment,
+ Global,
+ Guild as GuildModel,
+ GuildCount,
+ Highlight,
+ Level,
+ MemberCount,
+ ModLog,
+ Reminder,
+ Shared,
+ Stat,
+ StickyRole
+} from '#lib/models/index.js';
import {
AkairoClient,
ArgumentTypeCaster,
@@ -20,7 +32,9 @@ import {
version as akairoVersion,
type ArgumentPromptData,
type OtherwiseContentSupplier
-} from 'discord-akairo';
+} from '@notenoughupdates/discord-akairo';
+import * as Sentry from '@sentry/node';
+import { patch, type PatchedElements } from '@tanzanite/events-intercept';
import {
ActivityType,
GatewayIntentBits,
@@ -32,33 +46,19 @@ import {
type Awaitable,
type If,
type Message,
- type MessageOptions,
+ type MessageCreateOptions,
type Snowflake,
type UserResolvable
} from 'discord.js';
-import type EventEmitter from 'events';
import { google } from 'googleapis';
-import path from 'path';
-import readline from 'readline';
+import { type EventEmitter } from 'node:events';
+import path from 'node:path';
+import readline from 'node:readline';
+import { fileURLToPath } from 'node:url';
import { Options as SequelizeOptions, Sequelize, Sequelize as SequelizeType } from 'sequelize';
-import { fileURLToPath } from 'url';
import { tinyColor } from '../../arguments/tinyColor.js';
import { BotCache } from '../../common/BotCache.js';
import { HighlightManager } from '../../common/HighlightManager.js';
-import {
- ActivePunishment,
- Global,
- Guild as GuildModel,
- GuildCount,
- Highlight,
- Level,
- MemberCount,
- ModLog,
- Reminder,
- Shared,
- Stat,
- StickyRole
-} from '../../models/index.js';
import { AllowedMentions } from '../../utils/AllowedMentions.js';
import { BotClientUtils } from '../../utils/BotClientUtils.js';
import { emojis } from '../../utils/Constants.js';
@@ -279,7 +279,7 @@ export class TanzaniteClient<Ready extends boolean = boolean> extends AkairoClie
const modify = async (
message: Message,
- text: string | MessagePayload | MessageOptions | OtherwiseContentSupplier,
+ text: string | MessagePayload | MessageCreateOptions | OtherwiseContentSupplier,
data: ArgumentPromptData,
replaceError: boolean
) => {
@@ -387,7 +387,7 @@ export class TanzaniteClient<Ready extends boolean = boolean> extends AkairoClie
*/
public async init() {
if (parseInt(process.versions.node.split('.')[0]) < 18) {
- void (await this.console.error('version', `Please use node <<v18.x.x>>, not <<${process.version}>>.`, false));
+ void (await this.console.error('version', `Please use node <<v18.x.x>> or greater, not <<${process.version}>>.`, false));
process.exit(2);
}
diff --git a/lib/extensions/discord.js/BotClientEvents.ts b/lib/extensions/discord.js/BotClientEvents.ts
index 941a6d8..88f67e9 100644
--- a/lib/extensions/discord.js/BotClientEvents.ts
+++ b/lib/extensions/discord.js/BotClientEvents.ts
@@ -1,4 +1,4 @@
-import type { AkairoClientEvents } from 'discord-akairo';
+import type { AkairoClientEvents } from '@notenoughupdates/discord-akairo';
import type {
ButtonInteraction,
Collection,
diff --git a/lib/extensions/discord.js/ExtendedGuild.ts b/lib/extensions/discord.js/ExtendedGuild.ts
index 6bf81ee..84db7d0 100644
--- a/lib/extensions/discord.js/ExtendedGuild.ts
+++ b/lib/extensions/discord.js/ExtendedGuild.ts
@@ -1,4 +1,5 @@
import {
+ Action,
createModLogEntry,
createModLogEntrySimple,
createPunishmentEntry,
@@ -17,15 +18,16 @@ import {
Guild,
JSONEncodable,
Message,
+ MessageCreateOptions,
MessageType,
PermissionFlagsBits,
SnowflakeUtil,
ThreadChannel,
+ WebhookCreateMessageOptions,
type APIMessage,
type GuildMember,
type GuildMemberResolvable,
type GuildTextBasedChannel,
- type MessageOptions,
type MessagePayload,
type NewsChannel,
type Snowflake,
@@ -33,10 +35,9 @@ import {
type User,
type UserResolvable,
type VoiceChannel,
- type Webhook,
- type WebhookMessageOptions
+ type Webhook
} from 'discord.js';
-import _ from 'lodash';
+import { camelCase } from 'lodash-es';
import { TanzaniteClient } from '../discord-akairo/TanzaniteClient.js';
import { banResponse, BanResponse, dmResponse, permissionsResponse, punishmentEntryRemove } from './ExtendedGuildMember.js';
@@ -91,7 +92,10 @@ interface Extension {
* @param logType The corresponding channel that the message will be sent to
* @param message The parameters for {@link TextChannel.send}
*/
- sendLogChannel(logType: GuildLogType, message: string | MessagePayload | MessageOptions): Promise<Message | null | undefined>;
+ sendLogChannel(
+ logType: GuildLogType,
+ message: string | MessagePayload | MessageCreateOptions
+ ): Promise<Message | null | undefined>;
/**
* Sends a formatted error message in a guild's error log channel
* @param title The title of the error embed
@@ -135,7 +139,7 @@ interface Extension {
declare module 'discord.js' {
export interface BaseGuild {
- client: TanzaniteClient;
+ client: TanzaniteClient<true>;
}
export interface Guild extends AnonymousGuild, Extension {}
@@ -202,7 +206,7 @@ export class ExtendedGuild extends Guild implements Extension {
public override async sendLogChannel(
logType: GuildLogType,
- message: string | MessagePayload | MessageOptions
+ message: string | MessagePayload | MessageCreateOptions
): Promise<Message | null | undefined> {
const logChannel = await this.getLogChannel(logType);
if (!logChannel || !logChannel.isTextBased()) {
@@ -220,7 +224,7 @@ export class ExtendedGuild extends Guild implements Extension {
}
public override async error(title: string, message: string): Promise<void> {
- void this.client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>'));
+ void this.client.console.info(camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>'));
void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: colors.error }] });
}
@@ -240,7 +244,7 @@ export class ExtendedGuild extends Guild implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN,
+ type: options.duration ? ModLogType.TempBan : ModLogType.PermBan,
user: user,
moderator: moderator.id,
reason: options.reason,
@@ -257,7 +261,7 @@ export class ExtendedGuild extends Guild implements Extension {
modlog: modlog.id,
guild: this,
user: user,
- punishment: 'banned',
+ punishment: Action.Ban,
duration: options.duration ?? 0,
reason: options.reason ?? undefined,
sendFooter: true
@@ -309,7 +313,7 @@ export class ExtendedGuild extends Guild implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntrySimple({
client: this.client,
- type: ModLogType.PERM_BAN,
+ type: ModLogType.PermBan,
user: options.user,
moderator: options.moderator,
reason: options.reason,
@@ -326,7 +330,7 @@ export class ExtendedGuild extends Guild implements Extension {
modlog: modlog.id,
guild: this,
user: options.user,
- punishment: 'banned',
+ punishment: Action.Ban,
duration: 0,
reason: options.reason ?? undefined,
sendFooter: true
@@ -390,7 +394,7 @@ export class ExtendedGuild extends Guild implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: ModLogType.UNBAN,
+ type: ModLogType.Unban,
user: user.id,
moderator: moderator.id,
reason: options.reason,
@@ -414,7 +418,7 @@ export class ExtendedGuild extends Guild implements Extension {
client: this.client,
guild: this,
user: user,
- punishment: 'unbanned',
+ punishment: Action.Unban,
reason: options.reason ?? undefined,
sendFooter: false
});
@@ -546,7 +550,7 @@ export class ExtendedGuild extends Guild implements Extension {
if (!webhook) return null;
- const sendOptions: Omit<WebhookMessageOptions, 'flags'> = {};
+ const sendOptions: Omit<WebhookCreateMessageOptions, 'flags'> = {};
const displayName = quote.member?.displayName ?? quote.author.username;
@@ -559,7 +563,7 @@ export class ExtendedGuild extends Guild implements Extension {
sendOptions.content = quote.content || undefined;
sendOptions.threadId = channel instanceof ThreadChannel ? channel.id : undefined;
sendOptions.embeds = quote.embeds.length ? quote.embeds : undefined;
- //@ts-expect-error: jank
+ // @ts-expect-error: jank
sendOptions.attachments = quote.attachments.size
? [...quote.attachments.values()].map((a) => AttachmentBuilder.from(a as JSONEncodable<AttachmentPayload>))
: undefined;
@@ -720,7 +724,9 @@ export class ExtendedGuild extends Guild implements Extension {
}
sendOptions.allowedMentions = AllowedMentions.none();
- sendOptions.username ??= quote.member?.displayName ?? quote.author.username;
+ sendOptions.username ??= (quote.member?.displayName ?? quote.author.username)
+ .replaceAll(/discord/gi, '[REDACTED]')
+ .replaceAll(/clyde/gi, '[REDACTED]');
sendOptions.avatarURL = quote.member?.displayAvatarURL({ size: 2048 }) ?? quote.author.displayAvatarURL({ size: 2048 });
return await webhook.send(sendOptions); /* .catch((e: any) => e); */
diff --git a/lib/extensions/discord.js/ExtendedGuildMember.ts b/lib/extensions/discord.js/ExtendedGuildMember.ts
index 9ef45f1..b11e9e3 100644
--- a/lib/extensions/discord.js/ExtendedGuildMember.ts
+++ b/lib/extensions/discord.js/ExtendedGuildMember.ts
@@ -8,11 +8,11 @@ import {
type Role
} from 'discord.js';
import {
+ Action,
checkMutePermissions,
createModLogEntry,
createPunishmentEntry,
punishDM,
- PunishmentTypeDM,
removePunishmentEntry
} from '../../common/Moderation.js';
import { ModLogType } from '../../models/index.js';
@@ -32,7 +32,7 @@ interface Extension {
* @returns Whether or not the dm was sent successfully.
*/
customPunishDM(
- punishment: PunishmentTypeDM,
+ punishment: Action,
reason?: string | null,
duration?: number,
modlog?: string,
@@ -119,13 +119,13 @@ interface Extension {
declare module 'discord.js' {
export interface GuildMember extends Extension {
- readonly client: TanzaniteClient;
+ readonly client: TanzaniteClient<true>;
}
}
export class ExtendedGuildMember extends GuildMember implements Extension {
public override async customPunishDM(
- punishment: PunishmentTypeDM,
+ punishment: Action,
reason?: string | null,
duration?: number,
modlog?: string,
@@ -154,7 +154,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
const result = await createModLogEntry(
{
client: this.client,
- type: ModLogType.WARN,
+ type: ModLogType.Warn,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -169,7 +169,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
if (!options.silent) {
// dm user
- const dmSuccess = await this.customPunishDM('warned', options.reason);
+ const dmSuccess = await this.customPunishDM(Action.Warn, options.reason);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return { result: warnResponse.DM_ERROR, caseNum: result.caseNum };
}
@@ -195,7 +195,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
if (options.addToModlog || options.duration) {
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE,
+ type: options.duration ? ModLogType.TempPunishmentRole : ModLogType.PermPunishmentRole,
guild: this.guild,
moderator: moderator.id,
user: this,
@@ -262,7 +262,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
if (options.addToModlog) {
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: ModLogType.REMOVE_PUNISHMENT_ROLE,
+ type: ModLogType.RemovePunishmentRole,
guild: this.guild,
moderator: moderator.id,
user: this,
@@ -362,7 +362,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE,
+ type: options.duration ? ModLogType.TempMute : ModLogType.PermMute,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -389,7 +389,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
if (!options.silent) {
// dm user
- const dmSuccess = await this.customPunishDM('muted', options.reason, options.duration ?? 0, modlog.id);
+ const dmSuccess = await this.customPunishDM(Action.Mute, options.reason, options.duration ?? 0, modlog.id);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return muteResponse.DM_ERROR;
}
@@ -441,7 +441,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: ModLogType.UNMUTE,
+ type: ModLogType.Unmute,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -465,7 +465,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
if (!options.silent) {
// dm user
- const dmSuccess = await this.customPunishDM('unmuted', options.reason, undefined, '', false);
+ const dmSuccess = await this.customPunishDM(Action.Unmute, options.reason, undefined, '', false);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return unmuteResponse.DM_ERROR;
}
@@ -505,7 +505,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: ModLogType.KICK,
+ type: ModLogType.Kick,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -517,7 +517,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
caseID = modlog.id;
// dm user
- const dmSuccess = options.silent ? null : await this.customPunishDM('kicked', options.reason, undefined, modlog.id);
+ const dmSuccess = options.silent ? null : await this.customPunishDM(Action.Kick, options.reason, undefined, modlog.id);
dmSuccessEvent = dmSuccess ?? undefined;
// kick
@@ -564,7 +564,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN,
+ type: options.duration ? ModLogType.TempBan : ModLogType.PermBan,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -579,7 +579,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// dm user
const dmSuccess = options.silent
? null
- : await this.customPunishDM('banned', options.reason, options.duration ?? 0, modlog.id);
+ : await this.customPunishDM(Action.Ban, options.reason, options.duration ?? 0, modlog.id);
dmSuccessEvent = dmSuccess ?? undefined;
// ban
@@ -646,7 +646,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: options.duration ? ModLogType.TEMP_CHANNEL_BLOCK : ModLogType.PERM_CHANNEL_BLOCK,
+ type: options.duration ? ModLogType.TempChannelBlock : ModLogType.PermChannelBlock,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -674,7 +674,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
? null
: await punishDM({
client: this.client,
- punishment: 'blocked',
+ punishment: Action.Block,
reason: options.reason ?? undefined,
duration: options.duration ?? 0,
modlog: modlog.id,
@@ -736,7 +736,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: ModLogType.CHANNEL_UNBLOCK,
+ type: ModLogType.ChannelUnblock,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -762,7 +762,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
? null
: await punishDM({
client: this.client,
- punishment: 'unblocked',
+ punishment: Action.Unblock,
reason: options.reason ?? undefined,
guild: this.guild,
user: this,
@@ -819,7 +819,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: ModLogType.TIMEOUT,
+ type: ModLogType.Timeout,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -834,7 +834,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
if (!options.silent) {
// dm user
- const dmSuccess = await this.customPunishDM('timedout', options.reason, options.duration, modlog.id);
+ const dmSuccess = await this.customPunishDM(Action.Timeout, options.reason, options.duration, modlog.id);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return timeoutResponse.DM_ERROR;
}
@@ -877,7 +877,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
// add modlog entry
const { log: modlog } = await createModLogEntry({
client: this.client,
- type: ModLogType.REMOVE_TIMEOUT,
+ type: ModLogType.RemoveTimeout,
user: this,
moderator: moderator.id,
reason: options.reason,
@@ -891,7 +891,7 @@ export class ExtendedGuildMember extends GuildMember implements Extension {
if (!options.silent) {
// dm user
- const dmSuccess = await this.customPunishDM('untimedout', options.reason, undefined, '', false);
+ const dmSuccess = await this.customPunishDM(Action.Untimeout, options.reason, undefined, '', false);
dmSuccessEvent = dmSuccess;
if (!dmSuccess) return removeTimeoutResponse.DM_ERROR;
}
diff --git a/lib/extensions/discord.js/ExtendedMessage.ts b/lib/extensions/discord.js/ExtendedMessage.ts
index 1bb0904..07cba3d 100644
--- a/lib/extensions/discord.js/ExtendedMessage.ts
+++ b/lib/extensions/discord.js/ExtendedMessage.ts
@@ -1,11 +1,11 @@
-import { CommandUtil } from 'discord-akairo';
+import { CommandUtil } from '@notenoughupdates/discord-akairo';
import { Message, type Client } from 'discord.js';
import type { RawMessageData } from 'discord.js/typings/rawDataTypes.js';
-export class ExtendedMessage<Cached extends boolean = boolean> extends Message<Cached> {
- public declare util: CommandUtil<Message>;
+export class ExtendedMessage<InGuild extends boolean = boolean> extends Message<InGuild> {
+ public declare util: CommandUtil<Message<InGuild>>;
- public constructor(client: Client, data: RawMessageData) {
+ public constructor(client: Client<true>, data: RawMessageData) {
super(client, data);
this.util = new CommandUtil(client.commandHandler, this);
}
diff --git a/lib/extensions/discord.js/ExtendedUser.ts b/lib/extensions/discord.js/ExtendedUser.ts
index 7846a70..8f9d27b 100644
--- a/lib/extensions/discord.js/ExtendedUser.ts
+++ b/lib/extensions/discord.js/ExtendedUser.ts
@@ -14,7 +14,7 @@ interface Extension {
declare module 'discord.js' {
export interface User extends Extension {
- readonly client: TanzaniteClient;
+ readonly client: TanzaniteClient<true>;
}
}
diff --git a/lib/global.ts b/lib/global.ts
index 0a0bcca..d3419c7 100644
--- a/lib/global.ts
+++ b/lib/global.ts
@@ -1,5 +1,7 @@
/* eslint-disable */
+declare const nodeFetch: typeof import('node-fetch').default;
+
declare global {
interface ReadonlyArray<T> {
includes<S, R extends `${Extract<S, string>}`>(
@@ -8,6 +10,8 @@ declare global {
fromIndex?: number
): searchElement is R & S;
}
+
+ var fetch: typeof nodeFetch;
}
export {};
diff --git a/lib/index.ts b/lib/index.ts
index fc7bb4c..d486d68 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -3,6 +3,7 @@ export * from './automod/AutomodShared.js';
export * from './automod/MemberAutomod.js';
export * from './automod/MessageAutomod.js';
export * from './automod/PresenceAutomod.js';
+export * from './common/Appeals.js';
export * from './common/BotCache.js';
export * from './common/ButtonPaginator.js';
export * from './common/CanvasProgressBar.js';
@@ -10,12 +11,9 @@ export * from './common/ConfirmationPrompt.js';
export * from './common/DeleteButton.js';
export * as Moderation from './common/Moderation.js';
export type {
- AppealButtonId,
CreateModLogEntryOptions,
CreatePunishmentEntryOptions,
PunishDMOptions,
- PunishmentTypeDM,
- PunishmentTypePresent,
RemovePunishmentEntryOptions,
SimpleCreateModLogEntryOptions
} from './common/Moderation.js';
diff --git a/lib/models/instance/ActivePunishment.ts b/lib/models/instance/ActivePunishment.ts
index 9bd9d01..1b57f47 100644
--- a/lib/models/instance/ActivePunishment.ts
+++ b/lib/models/instance/ActivePunishment.ts
@@ -4,10 +4,10 @@ import { DataTypes, type Sequelize } from 'sequelize';
import { BaseModel } from '../BaseModel.js';
export enum ActivePunishmentType {
- BAN = 'BAN',
- MUTE = 'MUTE',
- ROLE = 'ROLE',
- BLOCK = 'BLOCK'
+ Ban = 'BAN',
+ Mute = 'MUTE',
+ Role = 'ROLE',
+ Block = 'BLOCK'
}
export interface ActivePunishmentModel {
diff --git a/lib/models/instance/Guild.ts b/lib/models/instance/Guild.ts
index 72091ca..462beee 100644
--- a/lib/models/instance/Guild.ts
+++ b/lib/models/instance/Guild.ts
@@ -210,7 +210,7 @@ export const guildSettingsObj = asGuildSetting({
name: 'Auto Publish Channels',
description: 'Channels were every message is automatically published.',
type: 'channel-array',
- subType: [ChannelType.GuildNews]
+ subType: [ChannelType.GuildAnnouncement]
},
welcomeChannel: {
name: 'Welcome Channel',
@@ -218,10 +218,10 @@ export const guildSettingsObj = asGuildSetting({
type: 'channel',
subType: [
ChannelType.GuildText,
- ChannelType.GuildNews,
- ChannelType.GuildNewsThread,
- ChannelType.GuildPublicThread,
- ChannelType.GuildPrivateThread
+ ChannelType.GuildAnnouncement,
+ ChannelType.AnnouncementThread,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread
]
},
muteRole: {
diff --git a/lib/models/instance/Level.ts b/lib/models/instance/Level.ts
index e22d63b..27def46 100644
--- a/lib/models/instance/Level.ts
+++ b/lib/models/instance/Level.ts
@@ -18,6 +18,9 @@ export interface LevelModelCreationAttributes {
* Leveling information for a user in a guild.
*/
export class Level extends BaseModel<LevelModel, LevelModelCreationAttributes> implements LevelModel {
+ public static MAX_XP = 2147483647;
+ public static MAX_LEVEL = Level.convertXpToLevel(Level.MAX_XP);
+
/**
* The user's id.
*/
diff --git a/lib/models/instance/ModLog.ts b/lib/models/instance/ModLog.ts
index 324ad83..7a1a60a 100644
--- a/lib/models/instance/ModLog.ts
+++ b/lib/models/instance/ModLog.ts
@@ -4,104 +4,98 @@ import { DataTypes, type Sequelize } from 'sequelize';
import { BaseModel } from '../BaseModel.js';
export enum ModLogType {
- PERM_BAN = 'PERM_BAN',
- TEMP_BAN = 'TEMP_BAN',
- UNBAN = 'UNBAN',
- KICK = 'KICK',
- PERM_MUTE = 'PERM_MUTE',
- TEMP_MUTE = 'TEMP_MUTE',
- UNMUTE = 'UNMUTE',
- WARN = 'WARN',
- PERM_PUNISHMENT_ROLE = 'PERM_PUNISHMENT_ROLE',
- TEMP_PUNISHMENT_ROLE = 'TEMP_PUNISHMENT_ROLE',
- REMOVE_PUNISHMENT_ROLE = 'REMOVE_PUNISHMENT_ROLE',
- PERM_CHANNEL_BLOCK = 'PERM_CHANNEL_BLOCK',
- TEMP_CHANNEL_BLOCK = 'TEMP_CHANNEL_BLOCK',
- CHANNEL_UNBLOCK = 'CHANNEL_UNBLOCK',
- TIMEOUT = 'TIMEOUT',
- REMOVE_TIMEOUT = 'REMOVE_TIMEOUT'
+ PermBan = 'PERM_BAN',
+ TempBan = 'TEMP_BAN',
+ Unban = 'UNBAN',
+ Kick = 'KICK',
+ PermMute = 'PERM_MUTE',
+ TempMute = 'TEMP_MUTE',
+ Unmute = 'UNMUTE',
+ Warn = 'WARN',
+ PermPunishmentRole = 'PERM_PUNISHMENT_ROLE',
+ TempPunishmentRole = 'TEMP_PUNISHMENT_ROLE',
+ RemovePunishmentRole = 'REMOVE_PUNISHMENT_ROLE',
+ PermChannelBlock = 'PERM_CHANNEL_BLOCK',
+ TempChannelBlock = 'TEMP_CHANNEL_BLOCK',
+ ChannelUnblock = 'CHANNEL_UNBLOCK',
+ Timeout = 'TIMEOUT',
+ RemoveTimeout = 'REMOVE_TIMEOUT'
}
-export interface ModLogModel {
- id: string;
- type: ModLogType;
- user: Snowflake;
- moderator: Snowflake;
- reason: string | null;
- duration: number | null;
- guild: Snowflake;
- evidence: string;
- pseudo: boolean;
- hidden: boolean;
-}
-
-export interface ModLogModelCreationAttributes {
- id?: string;
- type: ModLogType;
- user: Snowflake;
- moderator: Snowflake;
- reason?: string | null;
- duration?: number;
- guild: Snowflake;
- evidence?: string;
- pseudo?: boolean;
- hidden?: boolean;
+export enum AppealStatus {
+ None = 'NONE',
+ Submitted = 'SUBMITTED',
+ Accepted = 'ACCEPTED',
+ Denied = 'DENIED'
}
-/**
- * A mod log case.
- */
-export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes> implements ModLogModel {
+export interface ModLogModel {
/**
* The primary key of the modlog entry.
*/
- public declare id: string;
-
+ id: string;
/**
* The type of punishment.
*/
- public declare type: ModLogType;
-
+ type: ModLogType;
/**
* The user being punished.
*/
- public declare user: Snowflake;
-
+ user: Snowflake;
/**
* The user carrying out the punishment.
*/
- public declare moderator: Snowflake;
-
+ moderator: Snowflake;
/**
* The reason the user is getting punished.
*/
- public declare reason: string | null;
-
+ reason: string | null;
/**
* The amount of time the user is getting punished for.
*/
- public declare duration: number | null;
-
+ duration: number | null;
/**
* The guild the user is getting punished in.
*/
- public declare guild: Snowflake;
-
+ guild: Snowflake;
/**
* Evidence of what the user is getting punished for.
*/
- public declare evidence: string;
-
+ evidence: string;
/**
* Not an actual modlog just used so a punishment entry can be made.
*/
- public declare pseudo: boolean;
-
+ pseudo: boolean;
/**
* Hides from the modlog command unless show hidden is specified.
*/
- public declare hidden: boolean;
+ hidden: boolean;
+ /**
+ * The status of an appeal for this punishment
+ */
+ appeal: AppealStatus;
+}
+export interface ModLogModelCreationAttributes {
+ id?: string;
+ type: ModLogType;
+ user: Snowflake;
+ moderator: Snowflake;
+ reason?: string | null;
+ duration?: number;
+ guild: Snowflake;
+ evidence?: string;
+ pseudo?: boolean;
+ hidden?: boolean;
+ appeal?: AppealStatus;
+}
+
+export interface ModLog extends ModLogModel {}
+
+/**
+ * A mod log case.
+ */
+export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes> {
/**
* Initializes the model.
* @param sequelize The sequelize instance.
@@ -118,7 +112,8 @@ export class ModLog extends BaseModel<ModLogModel, ModLogModelCreationAttributes
guild: { type: DataTypes.STRING, references: { model: 'Guilds', key: 'id' } },
evidence: { type: DataTypes.TEXT, allowNull: true },
pseudo: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
- hidden: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false }
+ hidden: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
+ appeal: { type: DataTypes.STRING, allowNull: false, defaultValue: AppealStatus.None }
},
{ sequelize }
);
diff --git a/lib/types/misc.ts b/lib/types/misc.ts
index 5bf760c..5f28dae 100644
--- a/lib/types/misc.ts
+++ b/lib/types/misc.ts
@@ -1,14 +1,14 @@
import type {
InteractionReplyOptions,
+ MessageCreateOptions,
MessageEditOptions,
- MessageOptions,
MessagePayload,
- ReplyMessageOptions,
+ MessageReplyOptions,
WebhookEditMessageOptions
} from 'discord.js';
-export type ReplyMessageType = string | MessagePayload | ReplyMessageOptions;
+export type ReplyMessageType = string | MessagePayload | MessageReplyOptions;
export type EditMessageType = string | MessageEditOptions | MessagePayload;
export type SlashSendMessageType = string | MessagePayload | InteractionReplyOptions;
export type SlashEditMessageType = string | MessagePayload | WebhookEditMessageOptions;
-export type SendMessageType = string | MessagePayload | MessageOptions;
+export type SendMessageType = string | MessagePayload | MessageCreateOptions;
diff --git a/lib/utils/Arg.ts b/lib/utils/Arg.ts
index 80ca878..803230b 100644
--- a/lib/utils/Arg.ts
+++ b/lib/utils/Arg.ts
@@ -1,5 +1,5 @@
import type { BaseBotArgumentType, BotArgumentType, BotArgumentTypeCaster, CommandMessage, SlashMessage } from '#lib';
-import { Argument, type Command, type Flag, type ParsedValuePredicate } from 'discord-akairo';
+import { Argument, type Command, type Flag, type ParsedValuePredicate } from '@notenoughupdates/discord-akairo';
import { type Message } from 'discord.js';
/**
diff --git a/lib/utils/BotClientUtils.ts b/lib/utils/BotClientUtils.ts
index 4b2c99b..6837237 100644
--- a/lib/utils/BotClientUtils.ts
+++ b/lib/utils/BotClientUtils.ts
@@ -21,7 +21,7 @@ import {
type Snowflake,
type UserResolvable
} from 'discord.js';
-import _ from 'lodash';
+import { camelCase } from 'lodash-es';
import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './Constants.js';
import { generateErrorEmbed } from './ErrorHandler.js';
import { addOrRemoveFromArray, formatError, inspect } from './Utils.js';
@@ -329,7 +329,7 @@ export class BotClientUtils {
* @param error
*/
public async handleError(context: string, error: Error) {
- await this.client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false);
+ await this.client.console.error(camelCase(context), `An error occurred:\n${formatError(error, false)}`, false);
await this.client.console.channelError({
embeds: await generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context })
});
@@ -382,6 +382,7 @@ export class BotClientUtils {
public async uploadImageToImgur(image: string) {
const clientId = this.client.config.credentials.imgurClientId;
+ // @ts-expect-error: missing global types
const formData = new FormData();
formData.append('type', 'base64');
formData.append('image', image);
diff --git a/lib/utils/Constants.ts b/lib/utils/Constants.ts
index dd65e28..5ecbce2 100644
--- a/lib/utils/Constants.ts
+++ b/lib/utils/Constants.ts
@@ -1,4 +1,4 @@
-import { default as deepLock } from 'deep-lock';
+import deepLock from '@tanzanite/deep-lock';
import { Colors, GuildFeature, Snowflake } from 'discord.js';
const rawCapeUrl = 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/';
@@ -294,6 +294,7 @@ export const mappings = deepLock({
AUTO_MODERATION: { name: 'Auto Moderation', important: false, emoji: '<:autoModeration:1010579417942200321>', weight: 33 },
MEMBER_PROFILES: { name: 'Member Profiles', important: false, emoji: '<:memberProfiles:1010580480409747547>', weight: 34 },
NEW_THREAD_PERMISSIONS: { name: 'New Thread Permissions', important: false, emoji: '<:newThreadPermissions:1010580968442171492>', weight: 35 },
+ [GuildFeature.InvitesDisabled]: { name: 'Invites Disabled', important: false, emoji: null, weight: 36 },
},
regions: {
diff --git a/lib/utils/ErrorHandler.ts b/lib/utils/ErrorHandler.ts
index 3f8be89..ea4a026 100644
--- a/lib/utils/ErrorHandler.ts
+++ b/lib/utils/ErrorHandler.ts
@@ -1,4 +1,4 @@
-import { AkairoMessage, Command } from 'discord-akairo';
+import { AkairoMessage, Command } from '@notenoughupdates/discord-akairo';
import { ChannelType, Client, EmbedBuilder, escapeInlineCode, GuildTextBasedChannel, Message } from 'discord.js';
import { BotCommandHandlerEvents } from '../extensions/discord-akairo/BotCommandHandler.js';
import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js';
diff --git a/lib/utils/Utils.ts b/lib/utils/Utils.ts
index 13806ec..ea70abf 100644
--- a/lib/utils/Utils.ts
+++ b/lib/utils/Utils.ts
@@ -1,9 +1,11 @@
+import { Util as AkairoUtil } from '@notenoughupdates/discord-akairo';
import { humanizeDuration as humanizeDurationMod } from '@notenoughupdates/humanize-duration';
+import deepLock from '@tanzanite/deep-lock';
import assert from 'assert/strict';
import cp from 'child_process';
-import deepLock from 'deep-lock';
-import { Util as AkairoUtil } from 'discord-akairo';
import {
+ ActionRowBuilder,
+ APITextInputComponent,
Constants as DiscordConstants,
EmbedBuilder,
Message,
@@ -11,6 +13,8 @@ import {
PermissionFlagsBits,
PermissionsBitField,
PermissionsString,
+ TextInputBuilder,
+ TextInputComponentData,
type APIEmbed,
type APIMessage,
type CommandInteraction,
@@ -159,7 +163,7 @@ export async function slashRespond(
delete (newResponseOptions as InteractionReplyOptions).ephemeral; // Cannot change a preexisting message to be ephemeral
return (await interaction.editReply(newResponseOptions)) as Message | APIMessage;
} else {
- await interaction.reply(newResponseOptions);
+ await interaction.reply(newResponseOptions as SlashSendMessageType);
return await interaction.fetchReply().catch(() => undefined);
}
}
@@ -547,3 +551,9 @@ export function deepWriteable<T>(obj: T): DeepWritable<T> {
export function formatPerms(permissions: PermissionsString[]) {
return permissions.map((p) => `\`${mappings.permissions[p]?.name ?? p}\``).join(', ');
}
+
+export function ModalInput(options: Partial<TextInputComponentData | APITextInputComponent>): ActionRowBuilder<TextInputBuilder> {
+ return new ActionRowBuilder<TextInputBuilder>({
+ components: [new TextInputBuilder(options)]
+ });
+}