import {GroupConfigurationTransformers, TransformerType} from "./GroupConfigurationTransformers"; import {GroupConfigurationHandler} from "./GroupConfigurationHandler"; import { ActionRowBuilder, AnySelectMenuInteraction, ButtonBuilder, ButtonStyle, channelMention, ChannelSelectMenuBuilder, ChannelType, ChatInputCommandInteraction, EmbedBuilder, inlineCode, Interaction, InteractionReplyOptions, InteractionUpdateOptions, italic, MessageFlags, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, UserSelectMenuBuilder } from "discord.js"; import {Logger} from "log4js"; import {Container} from "../Container/Container"; import {Nullable} from "../types/Nullable"; import { MentionableSelectMenuBuilder, MessageActionRowComponentBuilder, RoleSelectMenuBuilder } from "@discordjs/builders"; import {ChannelId} from "../types/DiscordTypes"; import {IconCache} from "../Icons/IconCache"; type UIElementCollection = Record; type UIElement = { label: string, key: string, description: string, childrenElements?: UIElementCollection, isConfiguration?: true } export class GroupConfigurationRenderer { private static MOVETO_COMMAND = 'moveto_'; private static SETVALUE_COMMAND = 'setvalue_'; private static MOVEBACK_COMMAND = 'back'; private breadcrumbs: string[] = []; private static UI_ELEMENTS: UIElementCollection = { channels: { label: 'Channels', key: 'channels', description: "Provides settings to define in what channels the bot sends messages, when not directly interacting with it.", childrenElements: { newPlaydates: { label: 'New Playdates', key: 'newPlaydates', description: "Sets the channel, where the group get notified when new Playdates are set.", isConfiguration: true }, playdateReminders: { label: 'Playdate Reminders', key: 'playdateReminders', description: "Sets the channel, where the group gets reminded of upcoming playdates.", isConfiguration: true } } }, locale: { label: "Locale", key: 'locale', description: "Provides locale to be used for this group. This mostly sets how the dates are displayed, but this can also be later used for translations.", isConfiguration: true }, permissions: { label: "Permissions", key: "permissions", description: "Allows customization, how the members are allowed to interact with the data stored in the group.", childrenElements: { allowMemberManagingPlaydates: { label: "Manage Playdates", key: "allowMemberManagingPlaydates", description: "Defines if the members are allowed to manage playdates like adding or deleting them.", isConfiguration: true } } } } constructor( private readonly configurationHandler: GroupConfigurationHandler, private readonly transformers: GroupConfigurationTransformers, ) { } public async setup(interaction: ChatInputCommandInteraction) { let response = await interaction.reply(this.getReplyOptions()); let exit = false; let eventResponse; const filter = (i: Interaction) => i.user.id === interaction.user.id; do { if (eventResponse) { response = await eventResponse.update(this.getReplyOptions()); } try { eventResponse = await response.resource?.message?.awaitMessageComponent({ dispose: true, filter: filter, time: 60_000 }); } catch (_: unknown) { break; } if (!eventResponse || eventResponse.customId === 'exit') { exit = true; continue; } if (eventResponse.customId === GroupConfigurationRenderer.MOVEBACK_COMMAND) { this.breadcrumbs.pop() continue; } if (eventResponse.customId.startsWith(GroupConfigurationRenderer.MOVETO_COMMAND)) { this.breadcrumbs.push( eventResponse.customId.substring(GroupConfigurationRenderer.MOVETO_COMMAND.length) ) continue; } if (eventResponse.customId.startsWith(GroupConfigurationRenderer.SETVALUE_COMMAND)) { this.handleSelection(eventResponse); continue; } } while (!exit); if (eventResponse) { try { await eventResponse.update( this.getReplyOptions() ); } catch (_) { } await eventResponse.deleteReply(); return; } const message = response.resource?.message if (!message) { return; } if (message.deletable) { await message.delete() } } private getReplyOptions(): InteractionUpdateOptions & InteractionReplyOptions & { withResponse: true } { const embed = this.createEmbed(); const icons = Container.get(IconCache.name); embed.setAuthor({ name: "/ " + this.breadcrumbs.join(" / ") }); const exitButton = new ButtonBuilder() .setLabel("Exit") .setStyle(ButtonStyle.Danger) .setCustomId("exit") .setEmoji(icons.get("door_open_solid_white") ?? ''); const actionrow = new ActionRowBuilder() if (this.breadcrumbs.length > 0) { const backButton = new ButtonBuilder() .setLabel("Back") .setStyle(ButtonStyle.Secondary) .setCustomId(GroupConfigurationRenderer.MOVEBACK_COMMAND) .setEmoji(icons.get("angle_left_solid") ?? ''); actionrow.addComponents(backButton) } actionrow.addComponents(exitButton) return { content: "", embeds: [embed], components: [...this.createActionRowBuildersForMenu(), actionrow], withResponse: true, flags: MessageFlags.Ephemeral }; } private createEmbed(): EmbedBuilder { const {currentElement} = this.findCurrentUI(); if (currentElement === null) { return new EmbedBuilder() .setTitle("Group Configuration") .setDescription("This UI allows you to change settings for your group.") } const embed = new EmbedBuilder() .setTitle(currentElement?.label ?? '') .setDescription(currentElement?.description ?? ''); if (currentElement?.isConfiguration ?? false) { embed.addFields( {name: "Current Value", value: this.getCurrentValueAsUI(), inline: false} ) } return embed; } private getCurrentValueAsUI(): string { const path = this.breadcrumbs.join("."); const value = this.configurationHandler.getConfigurationByPath(path); if (value === undefined) return italic("None"); const type = this.transformers.getTransformerType(path); if (type === undefined) { throw new Error("Could not find the type for " + path); } const displaynames = new Intl.DisplayNames(["en"], {type: "language"}); switch (type) { case TransformerType.Locale: if (!value) { return inlineCode("Default"); } return displaynames.of((value)?.baseName) ?? "Unknown"; case TransformerType.Channel: if (!value) { return inlineCode("None"); } return channelMention(value); case TransformerType.PermissionBoolean: return value ? "Allowed" : "Disallowed" default: return "None"; } } private createActionRowBuildersForMenu(): ActionRowBuilder[] { const {currentCollection, currentElement} = this.findCurrentUI(); const icons = Container.get(IconCache.name); if (currentElement?.isConfiguration ?? false) { return [ new ActionRowBuilder() .addComponents(this.getSelectForBreadcrumbs()) ] } return [ new ActionRowBuilder() .setComponents( ...Object.values(currentCollection).map(elem => new ButtonBuilder() .setLabel(` ${elem.label}`) .setStyle(ButtonStyle.Primary) .setCustomId(GroupConfigurationRenderer.MOVETO_COMMAND + elem.key) .setEmoji(icons.get(elem.isConfiguration ? 'pen_solid' : "folder_solid") ?? '') ) ) ] } private getSelectForBreadcrumbs(): ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder { const breadcrumbPath = this.breadcrumbs.join('.') const transformerType = this.transformers.getTransformerType(breadcrumbPath); if (transformerType === undefined) { throw new Error(`Can not find transformer type for ${breadcrumbPath}`) } switch (transformerType) { case TransformerType.Locale: const options = [ 'en-US', 'fr-FR', 'it-IT', 'de-DE' ] const displaynames = new Intl.DisplayNames(["en"], {type: "language"}); return new StringSelectMenuBuilder() .setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath) .setOptions( options.map(intl => new StringSelectMenuOptionBuilder() .setLabel(displaynames.of(intl) ?? '') .setValue(intl) ) ) case TransformerType.Channel: return new ChannelSelectMenuBuilder() .setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath) .setChannelTypes(ChannelType.GuildText) .setPlaceholder("New Value"); case TransformerType.PermissionBoolean: return new StringSelectMenuBuilder() .setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath) .setOptions( [ { label: "Allow", value: "1" }, { label: "Disallow", value: "0" } ] ) default: return new StringSelectMenuBuilder() .setCustomId("...") .setOptions( new StringSelectMenuOptionBuilder() .setLabel("Nothing to see here") .setValue("0") ) } } private handleSelection(interaction: AnySelectMenuInteraction) { const path = interaction.customId.substring(GroupConfigurationRenderer.SETVALUE_COMMAND.length); const savingValue = this.getSaveValue(interaction, path); Container.get("logger").debug(`Saving '${savingValue}' to '${path}'`); this.configurationHandler.saveConfiguration(path, savingValue); } private getSaveValue(interaction: AnySelectMenuInteraction, path: string): string { const transformerType = this.transformers.getTransformerType(path); if (transformerType === undefined || transformerType === null) { throw new Error(`Can not find transformer type for ${path}`) } switch (transformerType) { case TransformerType.Locale: case TransformerType.Channel: case TransformerType.PermissionBoolean: return interaction.values.join('; '); default: throw new Error("Unhandled select menu"); } } private findCurrentUI(): { currentElement: Nullable, currentCollection: UIElementCollection } { let currentCollection: UIElementCollection = GroupConfigurationRenderer.UI_ELEMENTS; let currentElement: Nullable = null; for (const breadcrumb of this.breadcrumbs) { currentElement = currentCollection[breadcrumb]; if (currentElement.isConfiguration ?? false) { break; } currentCollection = currentElement.childrenElements ?? {}; } return { currentElement, currentCollection, } } }