aboutsummaryrefslogtreecommitdiff
path: root/lib/common
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common')
-rw-r--r--lib/common/Appeals.ts140
-rw-r--r--lib/common/Moderation.ts53
2 files changed, 160 insertions, 33 deletions
diff --git a/lib/common/Appeals.ts b/lib/common/Appeals.ts
index 43c56fd..4eb0ea3 100644
--- a/lib/common/Appeals.ts
+++ b/lib/common/Appeals.ts
@@ -1,6 +1,12 @@
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 {
+ formatUnbanResponse,
+ formatUnblockResponse,
+ formatUnmuteResponse,
+ formatUntimeoutResponse
+} from '#lib/utils/FormatResponse.js';
import { capitalize, ModalInput } from '#lib/utils/Utils.js';
import {
ActionRowBuilder,
@@ -17,12 +23,28 @@ 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];
+type RawAppealInfo = [
+ baseId: AppealBase,
+ punishment: `${Action}`,
+ guildId: Snowflake,
+ userId: Snowflake,
+ modlogId: string,
+ extraId?: Snowflake
+];
+
+type AppealInfo = [
+ baseId: AppealBase,
+ punishment: Action,
+ guildId: Snowflake,
+ userId: Snowflake,
+ modlogId: string,
+ extraId?: Snowflake
+];
export type AppealIdString =
- `${RawAppealInfo[0]};${RawAppealInfo[1]};${RawAppealInfo[2]};${RawAppealInfo[3]};${RawAppealInfo[4]}`;
+ `${RawAppealInfo[0]};${RawAppealInfo[1]};${RawAppealInfo[2]};${RawAppealInfo[3]};${RawAppealInfo[4]}${RawAppealInfo[5] extends undefined
+ ? ''
+ : `;${RawAppealInfo[5]}`}`;
function parseAppeal(customId: AppealIdString | string): AppealInfo {
const [baseId, _punishment, guildId, userId, modlogId] = customId.split(';') as RawAppealInfo;
@@ -37,7 +59,7 @@ function parseAppeal(customId: AppealIdString | string): AppealInfo {
* @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 [baseId, punishment, guildId, userId, modlogId, extraId] = parseAppeal(interaction.customId);
const { base, past, appealCustom } = punishments[punishment];
const appealName = appealCustom ?? capitalize(base);
@@ -79,7 +101,7 @@ export async function handleAppealAttempt(interaction: ButtonInteraction) {
};
return await interaction.showModal({
- customId: `appeal_submit;${punishment};${guildId};${userId};${modlogId}`,
+ customId: `appeal_submit;${punishment};${guildId};${userId};${modlogId}${extraId ? `;${extraId}` : ''}`,
title: `${appealName} Appeal`,
components: [
ModalInput({
@@ -109,7 +131,7 @@ export async function handleAppealAttempt(interaction: ButtonInteraction) {
* @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 [baseId, punishment, guildId, userId, modlogId, extraId] = parseAppeal(interaction.customId);
const { base, past, appealCustom } = punishments[punishment];
const appealName = appealCustom ?? capitalize(base);
@@ -159,12 +181,12 @@ export async function handleAppealSubmit(interaction: ModalSubmitInteraction) {
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder({
- customId: `appeal_accept;${punishment};${guildId};${userId};${modlogId}`,
+ customId: `appeal_accept;${punishment};${guildId};${userId};${modlogId}${extraId ? `;${extraId}` : ''}`,
label: 'Accept Appeal',
style: ButtonStyle.Success
}),
new ButtonBuilder({
- customId: `appeal_deny;${punishment};${guildId};${userId};${modlogId}`,
+ customId: `appeal_deny;${punishment};${guildId};${userId};${modlogId}${extraId ? `;${extraId}` : ''}`,
label: 'Deny Appeal',
style: ButtonStyle.Danger
})
@@ -178,7 +200,12 @@ export async function handleAppealSubmit(interaction: ModalSubmitInteraction) {
* @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);
+ if (!interaction.inCachedGuild()) {
+ void interaction.client.console.warn('Appeals', `Appeal decision made in uncached guild: ${interaction.guildId}`);
+ return;
+ }
+
+ const [baseId, punishment, guildId, userId, modlogId, extraId] = parseAppeal(interaction.customId);
const { base, past, appealCustom } = punishments[punishment];
const appealName = (appealCustom ?? base).toLowerCase();
@@ -186,12 +213,12 @@ export async function handleAppealDecision(interaction: ButtonInteraction) {
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.`);
+ return await interaction.reply(`:boom: 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.`
+ `:boom: Case ${input(modlogId)} has an invalid state of ${input(modlog.appeal)}. Please report this to my developers.`
);
}
@@ -218,12 +245,8 @@ export async function handleAppealDecision(interaction: ButtonInteraction) {
]
});
} 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(() => {});
+ const guild = interaction.client.guilds.resolve(guildId);
+ if (!guild) return await interaction.reply(`:boom: I can't find this server.`);
switch (punishment) {
case Action.Warn:
@@ -236,25 +259,98 @@ export async function handleAppealDecision(interaction: ButtonInteraction) {
assert.fail(`Cannot appeal ${appealName} (Action.${Action[punishment]})`);
return;
case Action.Mute: {
- throw new Error('Not implemented');
+ const member = await guild.members.fetch(userId);
+
+ const res = await member.customUnmute({
+ reason: `Appeal accepted.`,
+ moderator: interaction.member,
+ noDM: true
+ });
+
+ if (res !== 'success') {
+ return await interaction.reply({
+ content: formatUnmuteResponse('/', member, res),
+ ephemeral: false
+ });
+ }
+
+ break;
}
case Action.Ban: {
- throw new Error('Not implemented');
+ const user = await interaction.client.users.fetch(userId);
+
+ const res = await guild.customUnban({
+ user: userId,
+ reason: `Appeal accepted.`,
+ moderator: interaction.member,
+ noDM: true
+ });
+
+ if (res !== 'success') {
+ return await interaction.reply({
+ content: formatUnbanResponse(user, res),
+ ephemeral: false
+ });
+ }
+
+ break;
}
case Action.Timeout: {
- throw new Error('Not implemented');
+ const member = await guild.members.fetch(userId);
+
+ const res = await member.customRemoveTimeout({
+ reason: `Appeal accepted.`,
+ moderator: interaction.member,
+ noDM: true
+ });
+
+ if (res !== 'success') {
+ return await interaction.reply({
+ content: formatUntimeoutResponse(member, res),
+ ephemeral: false
+ });
+ }
+
+ break;
}
case Action.Block: {
- throw new Error('Not implemented');
+ assert(extraId, 'Block appeal must have extraId');
+ const member = await guild.members.fetch(userId);
+
+ const res = await member.customUnblock({
+ reason: `Appeal accepted.`,
+ channel: extraId,
+ moderator: interaction.member,
+ noDM: true
+ });
+
+ if (res !== 'success') {
+ return await interaction.reply({
+ content: formatUnblockResponse(member, res),
+ ephemeral: false
+ });
+ }
+
+ break;
}
case Action.AddPunishRole: {
throw new Error('Not implemented');
+
+ break;
}
default: {
const _exhaustiveCheck: never = punishment;
}
}
+ modlog.appeal = AppealStatus.Accepted;
+ await modlog.save();
+
+ // dm
+ await interaction.client.users
+ .send(userId, `Your ${appealName} appeal (${input(modlogId)}) has been accepted in ${guild}.`)
+ .catch(() => {});
+
return await interaction.update({
content: `${emojis.check} Appeal accepted.`,
embeds: interaction.message.embeds,
diff --git a/lib/common/Moderation.ts b/lib/common/Moderation.ts
index 7697b2f..a79d1df 100644
--- a/lib/common/Moderation.ts
+++ b/lib/common/Moderation.ts
@@ -264,17 +264,17 @@ 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;
+ return permissionsResponse.MissingPermissions;
}
const muteRoleID = await guild.getSetting('muteRole');
- if (!muteRoleID) return baseMuteResponse.NO_MUTE_ROLE;
+ if (!muteRoleID) return baseMuteResponse.NoMuteRole;
const muteRole = guild.roles.cache.get(muteRoleID);
- if (!muteRole) return baseMuteResponse.MUTE_ROLE_INVALID;
+ if (!muteRole) return baseMuteResponse.MuteRoleInvalid;
if (muteRole.position >= guild.members.me!.roles.highest.position || muteRole.managed) {
- return baseMuteResponse.MUTE_ROLE_NOT_MANAGEABLE;
+ return baseMuteResponse.MuteRoleNotManageable;
}
return true;
@@ -570,6 +570,14 @@ export interface PunishDMOptions extends BaseOptions {
* The channel that the user was (un)blocked from.
*/
channel?: Snowflake;
+
+ /**
+ * The role that the user was given/removed.
+ */
+ role?: {
+ id: Snowflake;
+ name: string;
+ };
}
/**
@@ -587,17 +595,38 @@ export async function punishDM(options: PunishDMOptions): Promise<boolean> {
const appealsEnabled =
(await options.guild.hasFeature('punishmentAppeals')) && Boolean(await options.guild.getLogChannel('appeals'));
- let content = `You have been ${options.punishment} `;
+ let content = '';
+
+ switch (options.punishment) {
+ case Action.AddPunishRole:
+ assert(options.role, 'Role is required for adding a punishment role.');
+ content += `You have received the "${options.role.name}" punishment role`;
+ break;
+ case Action.RemovePunishRole:
+ assert(options.role, 'Role is required for removing a punishment role.');
+ content += `The "${options.role.name}" punishment role has been removed from you`;
+ break;
+ default:
+ content += `You have been ${options.punishment}`;
+ break;
+ }
+
if ([Action.Block, Action.Unblock].includes(options.punishment)) {
assert(options.channel);
- content += `from <#${options.channel}> `;
+ content += ` from <#${options.channel}>`;
}
- content += `in ${format.input(options.guild.name)} `;
+
+ content += ` in ${format.input(options.guild.name)}`;
if (options.duration !== null && options.duration !== undefined) {
- content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently ';
+ content += options.duration ? ` for ${humanizeDuration(options.duration)}` : ' permanently';
+ }
+
+ if (![Action.AddPunishRole, Action.RemovePunishRole].includes(options.punishment)) {
+ const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided';
+ content += ` for ${format.input(reason)}.`;
+ } else {
+ content += '.';
}
- const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided';
- content += `for ${format.input(reason)}.`;
let components;
if (appealsEnabled && options.modlog) {
@@ -606,11 +635,13 @@ export async function punishDM(options: PunishDMOptions): Promise<boolean> {
const userId = options.client.users.resolveId(options.user);
const modlogCase = options.modlog;
+ const extraId = options.channel ?? options.role?.id;
+
components = [
new ActionRowBuilder<ButtonBuilder>({
components: [
new ButtonBuilder({
- customId: `appeal_attempt;${Action[punishment]};${guildId};${userId};${modlogCase}`,
+ customId: `appeal_attempt;${Action[punishment]};${guildId};${userId};${modlogCase}${extraId ? `;${extraId}` : ''}`,
style: ButtonStyle.Primary,
label: 'Appeal Punishment'
})