From 7f75b6f828430e9bc564414c85e4b8e43c0857f1 Mon Sep 17 00:00:00 2001 From: RealEthanPlayzDev Date: Sat, 6 Jan 2024 10:53:17 +0700 Subject: [PATCH] TempBan --- .vscode/settings.json | 13 ++ prisma/schema.prisma | 11 +- src/commands/Moderation/Ban.ts | 1 + src/commands/Moderation/Kick.ts | 1 + src/commands/Moderation/Mute.ts | 3 +- src/commands/Moderation/Punishments.ts | 4 + src/commands/Moderation/RemoveCase.ts | 15 ++- src/commands/Moderation/TempBan.ts | 171 +++++++++++++++++++++++++ src/commands/Moderation/Unban.ts | 1 + src/commands/Moderation/Warn.ts | 1 + src/commands/index.ts | 2 + src/util/MeteoriumClient.ts | 4 + 12 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/commands/Moderation/TempBan.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..086727c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "editor.formatOnSave": true, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace", + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0884cce..37ad494 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,6 +23,7 @@ model Guild { enum ModerationAction { Ban + TempBan Kick Mute Warn @@ -38,7 +39,9 @@ model ModerationCase { GuildId String Reason String AttachmentProof String - MuteDuration String @default("0") + Duration String @default("0") + CreatedAt DateTime @default(dbgenerated("0")) + ActiveTempBans ActiveTempBans[] } model Tag { @@ -48,3 +51,9 @@ model Tag { Content String Image String } + +model ActiveTempBans { + ActiveTempBanId Int @id @default(autoincrement()) + GlobalCaseId Int + Case ModerationCase @relation(fields: [GlobalCaseId], references: [GlobalCaseId]) +} diff --git a/src/commands/Moderation/Ban.ts b/src/commands/Moderation/Ban.ts index fcfa9f9..5c1ecf5 100644 --- a/src/commands/Moderation/Ban.ts +++ b/src/commands/Moderation/Ban.ts @@ -56,6 +56,7 @@ export const Command: MeteoriumCommand = { GuildId: interaction.guildId, Reason: Reason, AttachmentProof: AttachmentProof ? AttachmentProof.url : "", + CreatedAt: new Date(), }, }); await interaction.guild.members.ban(User, { diff --git a/src/commands/Moderation/Kick.ts b/src/commands/Moderation/Kick.ts index 72e42bc..7e29dfd 100644 --- a/src/commands/Moderation/Kick.ts +++ b/src/commands/Moderation/Kick.ts @@ -56,6 +56,7 @@ export const Command: MeteoriumCommand = { GuildId: interaction.guildId, Reason: Reason, AttachmentProof: AttachmentProof ? AttachmentProof.url : "", + CreatedAt: new Date(), }, }); await interaction.guild.members.kick( diff --git a/src/commands/Moderation/Mute.ts b/src/commands/Moderation/Mute.ts index 12339ee..c75a3bc 100644 --- a/src/commands/Moderation/Mute.ts +++ b/src/commands/Moderation/Mute.ts @@ -68,7 +68,8 @@ export const Command: MeteoriumCommand = { GuildId: interaction.guildId, Reason: Reason, AttachmentProof: AttachmentProof ? AttachmentProof.url : "", - MuteDuration: Duration, + Duration: Duration, + CreatedAt: new Date(), }, }); await GuildUser.timeout( diff --git a/src/commands/Moderation/Punishments.ts b/src/commands/Moderation/Punishments.ts index 9e05ee6..65115fe 100644 --- a/src/commands/Moderation/Punishments.ts +++ b/src/commands/Moderation/Punishments.ts @@ -56,6 +56,10 @@ export const Command: MeteoriumCommand = { TotalBan++; break; } + case ModerationAction.TempBan: { + TotalBan++; + break; + } case ModerationAction.Kick: { TotalKick++; break; diff --git a/src/commands/Moderation/RemoveCase.ts b/src/commands/Moderation/RemoveCase.ts index 25ebb21..fb75c4d 100644 --- a/src/commands/Moderation/RemoveCase.ts +++ b/src/commands/Moderation/RemoveCase.ts @@ -51,7 +51,7 @@ export const Command: MeteoriumCommand = { .setColor("Red"); if (Case.Action == ModerationAction.Mute) - ConfirmationEmbed.addFields([{ name: "Duration", value: Case.MuteDuration }]); + ConfirmationEmbed.addFields([{ name: "Duration", value: Case.Duration }]); const ConfirmationInteractionResult = await interaction.reply({ content: "Are you sure you want to remove this punishment?", @@ -86,6 +86,19 @@ export const Command: MeteoriumCommand = { Case.TargetUserId, `Case ${CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, ); + else if (Case.Action == ModerationAction.TempBan) { + await interaction.guild.members.unban( + Case.TargetUserId, + `Case ${CaseId} removed by ${interaction.user.username} (${interaction.user.id})`, + ); + const ATB = await client.Database.activeTempBans.findFirst({ + where: { GlobalCaseId: Case.GlobalCaseId }, + }); + if (ATB) + await client.Database.activeTempBans.delete({ + where: { ActiveTempBanId: ATB.ActiveTempBanId }, + }); + } await interaction.editReply({ content: "", embeds: [SuccessDeleteEmbed], components: [] }); diff --git a/src/commands/Moderation/TempBan.ts b/src/commands/Moderation/TempBan.ts new file mode 100644 index 0000000..313a596 --- /dev/null +++ b/src/commands/Moderation/TempBan.ts @@ -0,0 +1,171 @@ +import { ModerationAction } from "@prisma/client"; +import { SlashCommandBuilder, userMention } from "discord.js"; +import type { MeteoriumCommand } from ".."; +import { MeteoriumEmbedBuilder } from "../../util/MeteoriumEmbedBuilder"; +import ms from "ms"; + +export const Command: MeteoriumCommand = { + InteractionData: new SlashCommandBuilder() + .setName("tempban") + .setDescription("Temporarily bans someone inside this server and create a new case regarding it") + .addUserOption((option) => + option.setName("user").setDescription("The user to be temporarily banned").setRequired(true), + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason on why the user was temporarily banned") + .setRequired(true), + ) + .addStringOption((option) => + option.setName("duration").setDescription("The duration of the temporary ban").setRequired(true), + ) + .addAttachmentOption((option) => + option + .setName("proof") + .setDescription("An media containing proof to prove the reason valid") + .setRequired(false), + ), + async Callback(interaction, client) { + if (!interaction.member.permissions.has("BanMembers")) + return await interaction.editReply({ + content: "You do not have permission to temporarily ban users from this server.", + }); + + const User = interaction.options.getUser("user", true); + const Reason = interaction.options.getString("reason", true); + const Duration = await interaction.options.getString("duration", true); + const AttachmentProof = interaction.options.getAttachment("proof", false); + const GuildUser = await interaction.guild.members.fetch(User).catch(() => null); + const GuildSchema = (await client.Database.guild.findUnique({ where: { GuildId: interaction.guildId } }))!; + + if (User.id == interaction.user.id) + return await interaction.reply({ content: "You can't ban yourself!", ephemeral: true }); + if (User.bot) + return await interaction.reply({ content: "You can't ban bots! (do it manually)", ephemeral: true }); + if ( + GuildUser && + GuildUser.moderatable && + GuildUser.roles.highest.position >= interaction.member.roles.highest.position + ) + return interaction.reply({ + content: "You (or the bot) can't moderate this user due to lack of permission/hierachy.", + ephemeral: true, + }); + + await client.Database.guild.update({ + where: { GuildId: interaction.guildId }, + data: { CurrentCaseId: GuildSchema.CurrentCaseId + 1 }, + }); + const CaseResult = await client.Database.moderationCase.create({ + data: { + CaseId: GuildSchema.CurrentCaseId + 1, + Action: ModerationAction.TempBan, + TargetUserId: User.id, + ModeratorUserId: interaction.user.id, + GuildId: interaction.guildId, + Reason: Reason, + AttachmentProof: AttachmentProof ? AttachmentProof.url : "", + Duration: Duration, + CreatedAt: new Date(), + }, + }); + await client.Database.activeTempBans.create({ + data: { + GlobalCaseId: CaseResult.GlobalCaseId, + }, + }); + await interaction.guild.members.ban(User, { + reason: `Case ${CaseResult.CaseId} by ${interaction.user.username} (${interaction.user.id}): ${Reason}`, + }); + + const LogEmbed = new MeteoriumEmbedBuilder(undefined, interaction.user) + .setAuthor({ + name: `Case: #${CaseResult.CaseId} | tempban | ${User.username}`, + iconURL: User.displayAvatarURL({ extension: "png" }), + }) + .addFields( + { name: "User", value: userMention(User.id) }, + { + name: "Moderator", + value: userMention(interaction.user.id), + }, + { name: "Reason", value: Reason }, + { name: "Duration", value: Duration }, + ) + .setImage(AttachmentProof ? AttachmentProof.url : null) + .setFooter({ text: `Id: ${User.id}` }) + .setTimestamp() + .setColor("Red"); + + const PublicModLogChannel = await interaction.guild.channels + .fetch(GuildSchema.PublicModLogChannelId) + .catch(() => null); + if (PublicModLogChannel && PublicModLogChannel.isTextBased()) + await PublicModLogChannel.send({ embeds: [LogEmbed] }); + + const GuildSetting = await client.Database.guild.findUnique({ where: { GuildId: interaction.guild.id } }); + if (GuildSetting && GuildSetting.LoggingChannelId != "") + client.channels + .fetch(GuildSetting.LoggingChannelId) + .then(async (channel) => { + if (channel && channel.isTextBased()) + await channel.send({ + embeds: [ + new MeteoriumEmbedBuilder(undefined, interaction.user) + .setTitle("Moderation action") + .setFields([ + { name: "Case id", value: String(CaseResult.CaseId) }, + { + name: "Moderator", + value: `${interaction.user.username} (${ + interaction.user.id + }) (${userMention(interaction.user.id)})`, + }, + { + name: "Offending user", + value: `${User.username} (${User.id}) (${userMention(User.id)})`, + }, + { name: "Action", value: "Temp Ban" }, + { name: "Reason", value: Reason }, + { name: "Duration", value: Duration }, + { name: "Proof", value: AttachmentProof ? AttachmentProof.url : "N/A" }, + ]) + .setImage(AttachmentProof ? AttachmentProof.url : null), + ], + }); + }) + .catch(() => null); + + return await interaction.reply({ + content: + PublicModLogChannel != null && PublicModLogChannel.isTextBased() + ? undefined + : "(Warning: could not send log message to the public mod log channel)", + embeds: [LogEmbed], + ephemeral: GuildSchema?.PublicModLogChannelId != "", + }); + }, + Init(client) { + setInterval(async () => { + await client.Database.$transaction(async (tx) => { + const ActiveTBs = await tx.activeTempBans.findMany({ include: { Case: true } }); + const Promises = [ + ActiveTBs.map(async (active) => { + const CreatedAt = active.Case.CreatedAt; + const ExpiresAt = new Date(Number(active.Case.CreatedAt) + ms(active.Case.Duration)); + if (ExpiresAt <= CreatedAt) { + const Guild = await client.guilds.fetch(active.Case.GuildId); + const User = await client.users.fetch(active.Case.TargetUserId); + await Guild.members.unban(User); + return tx.activeTempBans.delete({ where: { ActiveTempBanId: active.ActiveTempBanId } }); + } + return; + }), + ]; + await Promise.all(Promises); + return; + }); + }, 10000); + }, +}; diff --git a/src/commands/Moderation/Unban.ts b/src/commands/Moderation/Unban.ts index 4e0e3c7..acde3f7 100644 --- a/src/commands/Moderation/Unban.ts +++ b/src/commands/Moderation/Unban.ts @@ -48,6 +48,7 @@ export const Command: MeteoriumCommand = { GuildId: interaction.guildId, Reason: Reason, AttachmentProof: AttachmentProof ? AttachmentProof.url : "", + CreatedAt: new Date(), }, }); await interaction.guild.members.unban( diff --git a/src/commands/Moderation/Warn.ts b/src/commands/Moderation/Warn.ts index b10a5c7..e877a5a 100644 --- a/src/commands/Moderation/Warn.ts +++ b/src/commands/Moderation/Warn.ts @@ -56,6 +56,7 @@ export const Command: MeteoriumCommand = { GuildId: interaction.guildId, Reason: Reason, AttachmentProof: AttachmentProof ? AttachmentProof.url : "", + CreatedAt: new Date(), }, }); diff --git a/src/commands/index.ts b/src/commands/index.ts index 774f2cf..b641d47 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -32,9 +32,11 @@ export * as case from "./Moderation/Case"; export * as removecase from "./Moderation/RemoveCase"; export * as unban from "./Moderation/Unban"; export * as createcase from "./Moderation/CreateCase"; +export * as tempban from "./Moderation/TempBan"; export type MeteoriumCommand = { InteractionData: Pick; Callback(interaction: ChatInputCommandInteraction<"cached">, client: MeteoriumClient): Awaitable; Autocomplete?(interaction: AutocompleteInteraction<"cached">, client: MeteoriumClient): Awaitable; + Init?(client: MeteoriumClient): Awaitable; }; diff --git a/src/util/MeteoriumClient.ts b/src/util/MeteoriumClient.ts index 71d050b..ee66f7f 100644 --- a/src/util/MeteoriumClient.ts +++ b/src/util/MeteoriumClient.ts @@ -46,6 +46,10 @@ export class MeteoriumClient extends Client { this.Commands.clear(); for (const [Name, { Command }] of Object.entries(Commands)) { loginNS.debug(`Registering command -> ${Name} ${Command}`); + if (Command.Init) { + loginNS.debug(`Running command init -> ${Name} ${Command}`); + await Command.Init(this); + } this.Commands.set(Name, Command); }