diff --git a/.vscode/settings.json b/.vscode/settings.json index 29a71f737..0c32ddfd8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,7 +7,7 @@ }, "eslint.workingDirectories": [{ "pattern": "./packages/*" }], "unocss.root": "./packages/website", - "i18n-ally.localesPaths": "./packages/yuudachi/locales", + "i18n-ally.localesPaths": ["./packages/yuudachi/locales", "apps/yuudachi/locales"], "i18n-ally.enabledFrameworks": ["i18next"], "i18n-ally.sourceLanguage": "en-US", "i18n-ally.displayLanguage": "en-US", diff --git a/apps/yuudachi/locales/en-US/translation.json b/apps/yuudachi/locales/en-US/translation.json index 61854c5a6..3bdf9fbf9 100644 --- a/apps/yuudachi/locales/en-US/translation.json +++ b/apps/yuudachi/locales/en-US/translation.json @@ -79,6 +79,16 @@ "no_reason": "No reason", "show_history_target": "Show history for target {{- user}}", "show_history_author": "Show history for author {{- user}}" + }, + "update_restriction_level": { + "errors": { + "no_change": "The restriction level has not been changed.", + "no_reason": "No reason provided for updating the restriction level. Reason is required when changing to `Restricted` or `Blocked`." + }, + "base": "Successfully changed the report restriction level to `{{- level}}`.", + "enabled": "- All the restrictions to create reports have been lifted.", + "restricted": "- The creation of reports have been restricted. Members will be required to confirm their report before proceeding.\n\n**Reason:** ```{{- reason}}```", + "blocked": "- The creation of reports have been blocked. Members will not be able to create reports.\n\n**Reason:** ```{{- reason}}```" } }, "warn": { @@ -358,14 +368,17 @@ "invalid_attachment": "Invalid attachment, only images are allowed.", "timed_out": "The report has timed out, please try again.", "bot": "You cannot report bots.", - "no_attachment_forward": "This user has already been recently reported, you must specify an attachment to forward if it helps the context of the report." + "no_attachment_forward": "This user has already been recently reported, you must specify an attachment to forward if it helps the context of the report.", + "disabled": "The creation of reports has been disabled by the server moderators for the following reason:\n```{{- reason}}```" }, "warnings": "**Attention:** We are not Discord and we **cannot** moderate {{- trust_and_safety}} issues.\n**Creating false reports may lead to moderation actions.**", "trust_and_safety_sub": "Trust & Safety", + "restriction": "The reports have been restricted by the moderators for the following reason:\n```{{- reason}}```\nIf your report does not fall under this category, please proceed with the report creation.", "buttons": { "forward": "Forward Message", "forward_attachment": "Forward Attachment", "execute": "Create Report", + "proceed": "Proceed with Report", "cancel": "Cancel", "discord_report": "Report to Discord" }, @@ -763,8 +776,16 @@ } }, "table": { - "success_titles": ["Case id", "Member id", "Username"], - "fail_titles": ["Member id", "Username", "Error"] + "success_titles": [ + "Case id", + "Member id", + "Username" + ], + "fail_titles": [ + "Member id", + "Username", + "Error" + ] } }, "report_log": { @@ -788,4 +809,4 @@ } } } -} +} \ No newline at end of file diff --git a/apps/yuudachi/migrations/1673411947-reports-restriction-level.js b/apps/yuudachi/migrations/1673411947-reports-restriction-level.js new file mode 100644 index 000000000..b8bf08356 --- /dev/null +++ b/apps/yuudachi/migrations/1673411947-reports-restriction-level.js @@ -0,0 +1,17 @@ +/** + * @param {import('postgres').Sql} sql + */ +export async function up(sql) { + await sql.unsafe(` + create type guild_reports_restriction_level as enum ('enabled', 'restricted', 'blocked'); + + alter table guild_settings + add column reports_restriction_level guild_reports_restriction_level not null default 'enabled'; + + alter table guild_settings + add column reports_restriction_reason text default null; + + comment on column guild_settings.reports_restriction_reason is 'The reason why reports are blocked/restricted in this guild'; + comment on column guild_settings.reports_restriction_level is 'The restriction_level of reports in this guild, whether they are enabled, restricted (requires confirmation), or blocked'; + `); +} diff --git a/apps/yuudachi/src/Constants.ts b/apps/yuudachi/src/Constants.ts index a44d931b0..daf6ee144 100644 --- a/apps/yuudachi/src/Constants.ts +++ b/apps/yuudachi/src/Constants.ts @@ -45,6 +45,15 @@ export const REPORT_DUPLICATE_PRE_EXPIRE_SECONDS = 3; export const REPORT_DUPLICATE_EXPIRE_SECONDS = 12 * 60 * 60; export const REPORT_MESSAGE_CONTEXT_LIMIT = 20; +export enum ReportsRestrictionLevel { + Blocked = "blocked", + Enabled = "enabled", + Restricted = "restricted", +} + +export const REPORTS_RESTRICTION_REASON_MIN_LENGTH = 3; +export const REPORTS_RESTRICTION_REASON_MAX_LENGTH = 1_000; + export const APPEAL_REASON_MAX_LENGTH = 1_500; export const APPEAL_REASON_MIN_LENGTH = 160; diff --git a/apps/yuudachi/src/commands/moderation/report.ts b/apps/yuudachi/src/commands/moderation/report.ts index ebc67a8d2..b2435bbd2 100644 --- a/apps/yuudachi/src/commands/moderation/report.ts +++ b/apps/yuudachi/src/commands/moderation/report.ts @@ -1,12 +1,33 @@ -import { Command, logger, kRedis, createModal, createModalActionRow, createTextComponent } from "@yuudachi/framework"; +import { + Command, + logger, + kRedis, + createModal, + createModalActionRow, + createTextComponent, + createButton, + createMessageActionRow, +} from "@yuudachi/framework"; import type { ArgsParam, InteractionParam, LocaleParam, CommandMethod } from "@yuudachi/framework/types"; -import { type GuildMember, type User, type Message, TextInputStyle, ComponentType } from "discord.js"; +import { + TextInputStyle, + ComponentType, + ButtonStyle, + type MessageContextMenuCommandInteraction, + type Guild, + type ModalSubmitInteraction, + type ChatInputCommandInteraction, + type GuildMember, + type User, + type Message, + type UserContextMenuCommandInteraction, +} from "discord.js"; import i18next from "i18next"; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { Redis } from "ioredis"; import { nanoid } from "nanoid"; import { inject, injectable } from "tsyringe"; -import { REPORT_REASON_MAX_LENGTH, REPORT_REASON_MIN_LENGTH } from "../../Constants.js"; +import { REPORT_REASON_MAX_LENGTH, REPORT_REASON_MIN_LENGTH, ReportsRestrictionLevel } from "../../Constants.js"; import type { Report } from "../../functions/reports/createReport.js"; import { getPendingReportByTarget } from "../../functions/reports/getReport.js"; import { checkLogChannel } from "../../functions/settings/checkLogChannel.js"; @@ -31,11 +52,7 @@ export default class extends Command< ): Promise { await interaction.deferReply({ ephemeral: true }); - const reportChannelId = await getGuildSetting(interaction.guildId, SettingsKeys.ReportChannelId); - const reportChannel = checkLogChannel(interaction.guild, reportChannelId); - if (!reportChannel) { - throw new Error(i18next.t("common.errors.no_report_channel", { lng: locale })); - } + await this.validateReportChannel(interaction.guild, locale); if (Object.keys(args)[0] === "message") { const parsedLink = parseMessageLink(args.message.message_link); @@ -53,7 +70,23 @@ export default class extends Command< const { guildId, channelId, messageId } = parsedLink; const messageArg = await resolveMessage(interaction.channelId, guildId!, channelId!, messageId!, locale); - const pendingReport = await this.validateReport(interaction.member, messageArg.author, locale, messageArg); + const { pendingReport, restrictionLevel } = await this.validateReport( + interaction.member, + messageArg.author, + locale, + messageArg, + ); + + const canProceedWithReport = await this.showRestrictedConfirmationIfNecessary( + interaction, + restrictionLevel, + "message", + locale, + ); + + if (!canProceedWithReport) { + return; + } await message( interaction, @@ -64,19 +97,36 @@ export default class extends Command< locale, pendingReport, ); - } else { - if (!args.user.user.member) { - throw new Error(i18next.t("command.common.errors.target_not_found", { lng: locale })); - } - const pendingReport = await this.validateReport(interaction.member, args.user.user.user, locale); + return; + } + + if (!args.user.user.member) { + throw new Error(i18next.t("command.common.errors.target_not_found", { lng: locale })); + } - if (pendingReport && !args.user.attachment) { - throw new Error(i18next.t("command.mod.report.common.errors.no_attachment_forward", { lng: locale })); - } + const { pendingReport, restrictionLevel } = await this.validateReport( + interaction.member, + args.user.user.user, + locale, + ); - await user(interaction, args.user, locale, pendingReport); + if (pendingReport && !args.user.attachment) { + throw new Error(i18next.t("command.mod.report.common.errors.no_attachment_forward", { lng: locale })); + } + + const canProceedWithReport = await this.showRestrictedConfirmationIfNecessary( + interaction, + restrictionLevel, + "user", + locale, + ); + + if (!canProceedWithReport) { + return; } + + await user(interaction, args.user, locale, pendingReport); } public override async userContext( @@ -84,50 +134,147 @@ export default class extends Command< args: ArgsParam, locale: LocaleParam, ): Promise { - const reportChannelId = await getGuildSetting(interaction.guildId, SettingsKeys.ReportChannelId); - const reportChannel = checkLogChannel(interaction.guild, reportChannelId); - if (!reportChannel) { - throw new Error(i18next.t("common.errors.no_report_channel", { lng: locale })); - } + await this.validateReportChannel(interaction.guild, locale); - const modalKey = nanoid(); + const { pendingReport, restrictionLevel } = await this.validateReport(interaction.member, args.user.user, locale); if (!args.user.member) { throw new Error(i18next.t("command.common.errors.target_not_found", { lng: locale })); } - await this.validateReport(interaction.member, args.user.user, locale); + if (pendingReport) { + throw new Error(i18next.t("command.mod.report.common.errors.no_attachment_forward", { lng: locale })); + } - const modal = createModal({ - customId: modalKey, - title: i18next.t("command.mod.report.user.modal.title", { lng: locale }), - components: [ - createModalActionRow([ - createTextComponent({ - customId: "reason", - label: i18next.t("command.mod.report.common.modal.label", { lng: locale }), - minLength: REPORT_REASON_MIN_LENGTH, - maxLength: REPORT_REASON_MAX_LENGTH, - placeholder: i18next.t("command.mod.report.common.modal.placeholder", { lng: locale }), - required: true, - style: TextInputStyle.Paragraph, - }), - ]), - ], + const handlerResponse = await this.handleContextInteraction(interaction, restrictionLevel, locale); + + if (!handlerResponse) { + return; + } + + const { modalInteraction, reason } = handlerResponse; + + await user( + modalInteraction, + { + user: args.user, + reason, + }, + locale, + pendingReport, + ); + } + + public override async messageContext( + interaction: InteractionParam, + args: ArgsParam, + locale: LocaleParam, + ): Promise { + await this.validateReportChannel(interaction.guild, locale); + + const { pendingReport, restrictionLevel } = await this.validateReport( + interaction.member, + args.message.author, + locale, + args.message, + ); + + const handlerResponse = await this.handleContextInteraction(interaction, restrictionLevel, locale); + + if (!handlerResponse) { + return; + } + + const { modalInteraction, reason } = handlerResponse; + + await message( + modalInteraction, + { + message: args.message, + reason, + }, + locale, + pendingReport, + ); + } + + private async handleContextInteraction( + interaction: MessageContextMenuCommandInteraction<"cached"> | UserContextMenuCommandInteraction<"cached">, + restrictionLevel: ReportsRestrictionLevel, + locale: string, + ): Promise<{ + modalInteraction: ModalSubmitInteraction<"cached">; + reason: string; + } | null> { + const type = interaction.isMessageContextMenuCommand() ? "message" : "user"; + + const modalResponse = await this.promptReasonModal(type, interaction, locale); + + if (!modalResponse) { + return null; + } + + const { modalInteraction, reason } = modalResponse; + + const canProceedWithReport = await this.showRestrictedConfirmationIfNecessary( + modalInteraction, + restrictionLevel, + type, + locale, + ); + + if (!canProceedWithReport) { + return null; + } + + return { + modalInteraction, + reason, + }; + } + + private async showRestrictedConfirmationIfNecessary( + interaction: ChatInputCommandInteraction<"cached"> | ModalSubmitInteraction<"cached">, + restrictionLevel: ReportsRestrictionLevel, + type: "message" | "user", + locale: string, + ): Promise { + if (restrictionLevel !== ReportsRestrictionLevel.Restricted) { + return true; + } + + const confirmationKey = nanoid(); + const cancelKey = nanoid(); + + const confirmationButton = createButton({ + label: i18next.t("command.mod.report.common.buttons.proceed", { lng: locale }), + customId: confirmationKey, + style: ButtonStyle.Danger, }); - await interaction.showModal(modal); + const cancelButton = createButton({ + label: i18next.t("command.common.buttons.cancel", { lng: locale }), + customId: cancelKey, + style: ButtonStyle.Secondary, + }); - const modalInteraction = await interaction - .awaitModalSubmit({ - time: 120_000, - filter: (component) => component.customId === modalKey, + const reason = await getGuildSetting(interaction.guildId, SettingsKeys.ReportsRestrictionReason); + + const reply = await interaction.editReply({ + content: i18next.t("command.mod.report.common.restriction", { lng: locale, reason }), + components: [createMessageActionRow([cancelButton, confirmationButton])], + }); + + const collectedInteraction = await reply + .awaitMessageComponent({ + filter: (collected) => collected.user.id === interaction.user.id, + componentType: ComponentType.Button, + time: 60_000, }) .catch(async () => { try { - await interaction.followUp({ - content: i18next.t("command.mod.report.common.errors.timed_out", { lng: locale }), - ephemeral: true, + await interaction.editReply({ + content: i18next.t("command.common.errors.timed_out", { lng: locale }), components: [], }); } catch (error_) { @@ -138,44 +285,90 @@ export default class extends Command< return undefined; }); - if (!modalInteraction) { - return; + if (!collectedInteraction) { + return false; } - await modalInteraction.deferReply({ ephemeral: true }); + if (collectedInteraction?.customId === cancelKey) { + await collectedInteraction.update({ + content: i18next.t(`command.mod.report.${type}.cancel`, { lng: locale }), + components: [], + }); - const reason = modalInteraction.components - .flatMap((row) => row.components) - .map((component) => (component.type === ComponentType.TextInput ? component.value || "" : "")); + return false; + } - await user( - modalInteraction, - { - user: args.user, - reason: reason.join(" "), - }, - locale, + await collectedInteraction?.deferUpdate(); + + return true; + } + + private async validateReport( + author: GuildMember, + target: User, + locale: string, + message?: Message, + ): Promise<{ + pendingReport: Report | null; + restrictionLevel: ReportsRestrictionLevel; + }> { + if (target.bot) { + throw new Error(i18next.t("command.mod.report.common.errors.bot", { lng: locale })); + } + + if (target.id === author.id) { + throw new Error(i18next.t("command.mod.report.common.errors.no_self", { lng: locale })); + } + + const restrictionLevel = await getGuildSetting( + author.guild.id, + SettingsKeys.ReportsRestrictionLevel, ); + + if (restrictionLevel === ReportsRestrictionLevel.Blocked) { + const reason = await getGuildSetting(author.guild.id, SettingsKeys.ReportsRestrictionReason); + throw new Error(i18next.t("command.mod.report.common.errors.disabled", { lng: locale, reason })); + } + + const userKey = `guild:${author.guild.id}:report:user:${target.id}`; + const latestReport = await getPendingReportByTarget(author.guild.id, target.id); + if (latestReport || (await this.redis.exists(userKey))) { + if (!latestReport || (latestReport.attachmentUrl && !message)) { + throw new Error(i18next.t("command.mod.report.common.errors.recently_reported.user", { lng: locale })); + } + + if (message && latestReport?.contextMessagesIds.includes(message.id)) { + throw new Error(i18next.t("command.mod.report.common.errors.recently_reported.message", { lng: locale })); + } + } + + return { + pendingReport: latestReport, + restrictionLevel, + }; } - public override async messageContext( - interaction: InteractionParam, - args: ArgsParam, - locale: LocaleParam, - ): Promise { - const reportChannelId = await getGuildSetting(interaction.guildId, SettingsKeys.ReportChannelId); - const reportChannel = checkLogChannel(interaction.guild, reportChannelId); + private async validateReportChannel(guild: Guild, locale: string) { + const reportChannelId = await getGuildSetting(guild.id, SettingsKeys.ReportChannelId); + const reportChannel = checkLogChannel(guild, reportChannelId); if (!reportChannel) { throw new Error(i18next.t("common.errors.no_report_channel", { lng: locale })); } + } + private async promptReasonModal( + type: "message" | "user", + interaction: MessageContextMenuCommandInteraction<"cached"> | UserContextMenuCommandInteraction<"cached">, + locale: string, + ): Promise<{ + modalInteraction: ModalSubmitInteraction<"cached">; + reason: string; + } | null> { const modalKey = nanoid(); - const pendingReport = await this.validateReport(interaction.member, args.message.author, locale, args.message); - const modal = createModal({ customId: modalKey, - title: i18next.t("command.mod.report.message.modal.title", { lng: locale }), + title: i18next.t(`command.mod.report.${type}.modal.title`, { lng: locale }), components: [ createModalActionRow([ createTextComponent({ @@ -214,52 +407,17 @@ export default class extends Command< }); if (!modalInteraction) { - return; + return null; } await modalInteraction.deferReply({ ephemeral: true }); - const reason = modalInteraction.components - .flatMap((row) => row.components) - .map((component) => (component.type === ComponentType.TextInput ? component.value || "" : "")); - - await message( + return { modalInteraction, - { - message: args.message, - reason: reason.join(" "), - }, - locale, - pendingReport, - ); - } - - private async validateReport( - author: GuildMember, - target: User, - locale: string, - message?: Message, - ): Promise { - if (target.bot) { - throw new Error(i18next.t("command.mod.report.common.errors.bot", { lng: locale })); - } - - if (target.id === author.id) { - throw new Error(i18next.t("command.mod.report.common.errors.no_self", { lng: locale })); - } - - const userKey = `guild:${author.guild.id}:report:user:${target.id}`; - const latestReport = await getPendingReportByTarget(author.guild.id, target.id); - if (latestReport || (await this.redis.exists(userKey))) { - if (!latestReport || (latestReport.attachmentUrl && !message)) { - throw new Error(i18next.t("command.mod.report.common.errors.recently_reported.user", { lng: locale })); - } - - if (message && latestReport?.contextMessagesIds.includes(message.id)) { - throw new Error(i18next.t("command.mod.report.common.errors.recently_reported.message", { lng: locale })); - } - } - - return latestReport; + reason: modalInteraction.components + .flatMap((row) => row.components) + .map((component) => (component.type === ComponentType.TextInput ? component.value || "" : "")) + .join(" "), + }; } } diff --git a/apps/yuudachi/src/commands/moderation/reports.ts b/apps/yuudachi/src/commands/moderation/reports.ts index c3ac361a2..2df39e36b 100644 --- a/apps/yuudachi/src/commands/moderation/reports.ts +++ b/apps/yuudachi/src/commands/moderation/reports.ts @@ -4,6 +4,7 @@ import { handleReportAutocomplete } from "../../functions/autocomplete/reports.j import type { ReportUtilsCommand } from "../../interactions/index.js"; import { lookup } from "./sub/reports/lookup.js"; import { status } from "./sub/reports/status.js"; +import { updateRestrictionLevel } from "./sub/reports/update-restricition-level.js"; export default class extends Command { public override async autocomplete( @@ -19,7 +20,6 @@ export default class extends Command { args: ArgsParam, locale: LocaleParam, ): Promise { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition await interaction.deferReply({ ephemeral: args.lookup?.hide ?? true }); switch (Object.keys(args)[0]) { @@ -29,6 +29,9 @@ export default class extends Command { case "status": await status(interaction, args.status, locale); break; + case "update-restriction-level": + await updateRestrictionLevel(interaction, args["update-restriction-level"], locale); + break; default: } } diff --git a/apps/yuudachi/src/commands/moderation/sub/reports/update-restricition-level.ts b/apps/yuudachi/src/commands/moderation/sub/reports/update-restricition-level.ts new file mode 100644 index 000000000..be00f9b2f --- /dev/null +++ b/apps/yuudachi/src/commands/moderation/sub/reports/update-restricition-level.ts @@ -0,0 +1,47 @@ +import type { ArgsParam, InteractionParam, LocaleParam } from "@yuudachi/framework/types"; +import i18next from "i18next"; +import { ReportsRestrictionLevel } from "../../../../Constants.js"; +import { checkLogChannel } from "../../../../functions/settings/checkLogChannel.js"; +import { getGuildSetting, SettingsKeys } from "../../../../functions/settings/getGuildSetting.js"; +import { updateGuildSetting } from "../../../../functions/settings/updateGuildSetting.js"; +import type { ReportUtilsCommand } from "../../../../interactions/index.js"; + +export async function updateRestrictionLevel( + interaction: InteractionParam, + args: ArgsParam["update-restriction-level"], + locale: LocaleParam, +): Promise { + const reportLogChannel = checkLogChannel( + interaction.guild, + await getGuildSetting(interaction.guildId, SettingsKeys.ReportChannelId), + ); + + if (!reportLogChannel) { + throw new Error(i18next.t("common.errors.no_report_channel", { lng: locale })); + } + + if (args.level !== ReportsRestrictionLevel.Enabled && !args.reason?.length) { + throw new Error(i18next.t("command.mod.reports.update_restriction_level.errors.no_reason", { lng: locale })); + } + + const status = await getGuildSetting( + interaction.guildId, + SettingsKeys.ReportsRestrictionLevel, + ); + + if (status === args.level) { + throw new Error(i18next.t("command.mod.reports.update_restriction_level.errors.no_change", { lng: locale })); + } + + const reason = args.level === ReportsRestrictionLevel.Enabled ? null : args.reason; + + await updateGuildSetting(interaction.guildId, SettingsKeys.ReportsRestrictionLevel, args.level); + await updateGuildSetting(interaction.guildId, SettingsKeys.ReportsRestrictionReason, reason); + + await interaction.editReply({ + content: [ + i18next.t("command.mod.reports.update_restriction_level.base", { lng: locale, level: args.level }), + i18next.t(`command.mod.reports.update_restriction_level.${args.level}`, { lng: locale, reason }), + ].join("\n"), + }); +} diff --git a/apps/yuudachi/src/functions/settings/getGuildSetting.ts b/apps/yuudachi/src/functions/settings/getGuildSetting.ts index 0a4b848bc..98081cd13 100644 --- a/apps/yuudachi/src/functions/settings/getGuildSetting.ts +++ b/apps/yuudachi/src/functions/settings/getGuildSetting.ts @@ -18,6 +18,8 @@ export enum SettingsKeys { ReportChannelId = "report_channel_id", ReportStatusTags = "report_status_tags", ReportTypeTags = "report_type_tags", + ReportsRestrictionLevel = "reports_restriction_level", + ReportsRestrictionReason = "reports_restriction_reason", SponsorRoleId = "sponsor_role_id", } diff --git a/apps/yuudachi/src/functions/settings/updateGuildSetting.ts b/apps/yuudachi/src/functions/settings/updateGuildSetting.ts new file mode 100644 index 000000000..fdb7bba85 --- /dev/null +++ b/apps/yuudachi/src/functions/settings/updateGuildSetting.ts @@ -0,0 +1,22 @@ +import { kSQL } from "@yuudachi/framework"; +import type { Snowflake } from "discord.js"; +import type { SerializableParameter, Sql } from "postgres"; +import { container } from "tsyringe"; +import type { SettingsKeys, ReportStatusTagTuple, ReportTypeTagTuple } from "./getGuildSetting.js"; + +export async function updateGuildSetting( + guildId: Snowflake, + prop: SettingsKeys, + newValue: T, + table = "guild_settings", +) { + const sql = container.resolve>(kSQL); + + const [data] = await sql.unsafe<[{ value: ReportStatusTagTuple | ReportTypeTagTuple | boolean | string | null }?]>( + `update ${table} set ${prop} = $2 + where guild_id = $1`, + [guildId, newValue] as SerializableParameter[], + ); + + return (data?.value ?? null) as unknown as T; +} diff --git a/apps/yuudachi/src/interactions/moderation/reports.ts b/apps/yuudachi/src/interactions/moderation/reports.ts index a0d5326fe..01d40ff18 100644 --- a/apps/yuudachi/src/interactions/moderation/reports.ts +++ b/apps/yuudachi/src/interactions/moderation/reports.ts @@ -1,4 +1,5 @@ import { ApplicationCommandOptionType } from "discord-api-types/v10"; +import { REPORTS_RESTRICTION_REASON_MAX_LENGTH, REPORTS_RESTRICTION_REASON_MIN_LENGTH } from "../../Constants.js"; export const ReportUtilsCommand = { name: "reports", @@ -49,6 +50,31 @@ export const ReportUtilsCommand = { }, ], }, + { + name: "update-restriction-level", + description: "Update the restriction level all the future reports will have", + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: "level", + description: "The new restriction level", + type: ApplicationCommandOptionType.String, + choices: [ + { name: "Enabled", value: "enabled" }, + { name: "Restricted (require confirmation)", value: "restricted" }, + { name: "Blocked", value: "blocked" }, + ], + required: true, + }, + { + name: "reason", + description: "The reason that will be shown to the user on restricted or blocked report attempts", + min_length: REPORTS_RESTRICTION_REASON_MIN_LENGTH, + max_length: REPORTS_RESTRICTION_REASON_MAX_LENGTH, + type: ApplicationCommandOptionType.String, + }, + ], + }, ], default_member_permissions: "0", } as const;