diff --git a/src/commands/slash/Main/hub/announce.ts b/src/commands/slash/Main/hub/announce.ts new file mode 100644 index 000000000..9c4b71a5e --- /dev/null +++ b/src/commands/slash/Main/hub/announce.ts @@ -0,0 +1,66 @@ +import { emojis } from '#main/config/Constants.js'; +import db from '#main/utils/Db.js'; +import { InfoEmbed } from '#main/utils/EmbedUtils.js'; +import { isHubManager, sendToHub } from '#main/utils/hub/utils.js'; +import { + ActionRowBuilder, + ChatInputCommandInteraction, + ModalBuilder, + ModalSubmitInteraction, + TextInputBuilder, + TextInputStyle, +} from 'discord.js'; +import HubCommand from './index.js'; +import { CustomID } from '#main/utils/CustomID.js'; +import { RegisterInteractionHandler } from '#main/decorators/Interaction.js'; + +export default class AnnounceCommand extends HubCommand { + readonly cooldown = 1 * 60 * 1000; + async execute(interaction: ChatInputCommandInteraction) { + const hubName = interaction.options.getString('hub', true); + const hub = await db.hub.findFirst({ where: { name: hubName } }); + + if (!hub || !isHubManager(interaction.user.id, hub)) { + await this.replyEmbed(interaction, 'hub.notFound_mod', { ephemeral: true }); + return; + } + + const isOnCooldown = await this.checkOrSetCooldown(interaction); + if (isOnCooldown) return; + + const modal = new ModalBuilder() + .setCustomId(new CustomID('hub_announce', [hub.id]).toString()) + .setTitle('Announce something to all connected servers') + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('announcement') + .setLabel('Announcement') + .setPlaceholder('Enter your announcement here') + .setRequired(true) + .setMinLength(5) + .setStyle(TextInputStyle.Paragraph), + ), + ); + + await interaction.showModal(modal); + } + + @RegisterInteractionHandler('hub_announce') + async handleAnnounceModal(interaction: ModalSubmitInteraction) { + await interaction.reply(`${emojis.loading} Sending announcement to all connected servers...`); + const [hubId] = CustomID.parseCustomId(interaction.customId).args; + const announcement = interaction.fields.getTextInputValue('announcement'); + + await sendToHub(hubId, { + embeds: [ + new InfoEmbed() + .setTitle('📢 Official Hub Announcement') + .setDescription(announcement) + .setTimestamp(), + ], + }); + + await interaction.editReply(`${emojis.yes} Announcement sent to all connected servers.`); + } +} diff --git a/src/commands/slash/Main/hub/appeal.ts b/src/commands/slash/Main/hub/appeal.ts index c32408f28..a946316f4 100644 --- a/src/commands/slash/Main/hub/appeal.ts +++ b/src/commands/slash/Main/hub/appeal.ts @@ -43,15 +43,8 @@ export default class AppealCommand extends HubCommand { } private async runHubChecks(interaction: ChatInputCommandInteraction) { - const hubName = interaction.options.getString('hub') ?? undefined; - const hub = await db.hub.findFirst({ - where: { - OR: [ - { name: hubName }, - { moderators: { some: { OR: [{ position: 'manager' }, { position: 'network_mod' }] } } }, - ], - }, - }); + const hubName = interaction.options.getString('hub', true); + const hub = await db.hub.findFirst({ where: { name: hubName } }); if (!hub || !isHubMod(interaction.user.id, hub)) { await this.replyEmbed( diff --git a/src/commands/slash/Main/hub/index.ts b/src/commands/slash/Main/hub/index.ts index 8e5481168..ebc320a9a 100644 --- a/src/commands/slash/Main/hub/index.ts +++ b/src/commands/slash/Main/hub/index.ts @@ -42,13 +42,7 @@ export default class HubCommand extends BaseCommand { name: 'browse', description: '🔍 Browse public hubs and join them!', options: [ - { - type: ApplicationCommandOptionType.String, - name: 'hub', - description: 'Search for a hub.', - required: false, - autocomplete: true, - }, + { ...hubOption, required: false }, { type: ApplicationCommandOptionType.String, name: 'sort', @@ -85,7 +79,7 @@ export default class HubCommand extends BaseCommand { ChannelType.PrivateThread, ], }, - { ...hubOption, required: false }, + { ...hubOption }, { type: ApplicationCommandOptionType.String, name: 'invite', @@ -276,7 +270,7 @@ export default class HubCommand extends BaseCommand { name: 'list', description: '🔎 List all the settings of the hub.', type: ApplicationCommandOptionType.Subcommand, - options: [{ ...hubOption, required: false }], + options: [{ ...hubOption }], }, { name: 'toggle', @@ -290,7 +284,7 @@ export default class HubCommand extends BaseCommand { required: true, choices: Object.keys(HubSettingsBits).map((s) => ({ name: s, value: s })), }, - { ...hubOption, required: false }, + { ...hubOption }, ], }, ], @@ -304,7 +298,7 @@ export default class HubCommand extends BaseCommand { type: ApplicationCommandOptionType.Subcommand, name: 'view', description: '🔎 View the current log channel & role configuration.', - options: [{ ...hubOption, required: false }], + options: [{ ...hubOption }], }, { name: 'set_channel', @@ -324,7 +318,7 @@ export default class HubCommand extends BaseCommand { type: ApplicationCommandOptionType.Channel, required: true, }, - { ...hubOption, required: false }, + { ...hubOption }, ], }, { @@ -345,7 +339,7 @@ export default class HubCommand extends BaseCommand { type: ApplicationCommandOptionType.Role, required: true, }, - { ...hubOption, required: false }, + { ...hubOption }, ], }, ], @@ -366,7 +360,7 @@ export default class HubCommand extends BaseCommand { description: 'The duration. Eg. 1h, 1d, 1w, 1mo', required: true, }, - { ...hubOption, required: false }, + { ...hubOption }, ], }, ], @@ -411,13 +405,16 @@ export default class HubCommand extends BaseCommand { type: ApplicationCommandOptionType.Subcommand, name: 'edit', description: '📝 Edit an existing blocked word rule in your hub.', - options: [hubOption, { - type: ApplicationCommandOptionType.String, - name: 'rule', - description: 'The name of the rule you want to edit.', - required: true, - autocomplete: true, - }], + options: [ + hubOption, + { + type: ApplicationCommandOptionType.String, + name: 'rule', + description: 'The name of the rule you want to edit.', + required: true, + autocomplete: true, + }, + ], }, { type: ApplicationCommandOptionType.Subcommand, @@ -427,6 +424,12 @@ export default class HubCommand extends BaseCommand { }, ], }, + { + type: ApplicationCommandOptionType.Subcommand, + name: 'announce', + description: '📢 Send an announcement to a hub you moderate.', + options: [hubOption], + }, ], }; @@ -442,7 +445,16 @@ export default class HubCommand extends BaseCommand { } async autocomplete(interaction: AutocompleteInteraction): Promise { - const managerCmds = ['edit', 'settings', 'invite', 'moderator', 'logging', 'appeal', 'blockwords']; + const managerCmds = [ + 'edit', + 'settings', + 'invite', + 'moderator', + 'logging', + 'appeal', + 'blockwords', + 'announce', + ]; const modCmds = ['servers']; const subcommand = interaction.options.getSubcommand(); diff --git a/src/core/BaseCommand.ts b/src/core/BaseCommand.ts index 7647d8715..5a6c6b3e9 100644 --- a/src/core/BaseCommand.ts +++ b/src/core/BaseCommand.ts @@ -1,6 +1,7 @@ import { emojis } from '#main/config/Constants.js'; import { MetadataHandler } from '#main/core/FileLoader.js'; import { InteractionFunction } from '#main/decorators/Interaction.js'; +import { TranslationKeys } from '#main/types/locale.js'; import { InfoEmbed } from '#utils/EmbedUtils.js'; import { supportedLocaleCodes, t } from '#utils/Locale.js'; import Logger from '#utils/Logger.js'; @@ -125,10 +126,11 @@ export default abstract class BaseCommand { } } - async replyEmbed( + async replyEmbed( interaction: RepliableInteraction | MessageComponentInteraction, - desc: string, + desc: K | (string & NonNullable), opts?: { + t?: { [Key in TranslationKeys[K]]: string }; content?: string; title?: string; color?: ColorResolvable; @@ -141,7 +143,14 @@ export default abstract class BaseCommand { edit?: boolean; }, ): Promise { - const embed = new InfoEmbed().setDescription(desc).setTitle(opts?.title); + let description = desc as string; + + if (t(desc as K, 'en', opts?.t)) { + const locale = await this.getLocale(interaction); + description = t(desc as keyof TranslationKeys, locale); + } + + const embed = new InfoEmbed().setDescription(description).setTitle(opts?.title); const message = { content: opts?.content, embeds: [embed], components: opts?.components }; if (opts?.edit) return await interaction.editReply(message); @@ -177,7 +186,9 @@ export default abstract class BaseCommand { Logger.debug(`Finished adding interactions for command: ${command.data.name}`); } - protected async getLocale(interaction: Interaction): Promise { + protected async getLocale( + interaction: Interaction | MessageComponentInteraction, + ): Promise { const { userManager } = interaction.client; return await userManager.getUserLocale(interaction.user.id); }