Adds group configuration
This commit is contained in:
parent
0d9cf6a370
commit
154002f6f3
16 changed files with 633 additions and 20 deletions
334
source/Groups/GroupConfigurationRenderer.ts
Normal file
334
source/Groups/GroupConfigurationRenderer.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
import {GroupConfigurationTransformers, TransformerType} from "./GroupConfigurationTransformers";
|
||||
import {GroupConfigurationHandler} from "./GroupConfigurationHandler";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
AnyComponentBuilder, AnySelectMenuInteraction,
|
||||
APISelectMenuComponent,
|
||||
ButtonBuilder,
|
||||
ButtonStyle, channelMention,
|
||||
ChannelSelectMenuBuilder, ChannelSelectMenuInteraction,
|
||||
ChannelType,
|
||||
ChatInputCommandInteraction,
|
||||
EmbedBuilder,
|
||||
InteractionCallbackResponse,
|
||||
InteractionEditReplyOptions,
|
||||
InteractionReplyOptions,
|
||||
InteractionUpdateOptions, italic,
|
||||
SelectMenuBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
UserSelectMenuBuilder
|
||||
} from "discord.js";
|
||||
import {Logger} from "log4js";
|
||||
import {Container} from "../Container/Container";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import GroupConfiguration from "../Database/tables/GroupConfiguration";
|
||||
import {
|
||||
BaseSelectMenuBuilder,
|
||||
MentionableSelectMenuBuilder,
|
||||
MessageActionRowComponentBuilder,
|
||||
RoleSelectMenuBuilder
|
||||
} from "@discordjs/builders";
|
||||
import {unwatchFile} from "node:fs";
|
||||
import {UserError} from "../Discord/UserError";
|
||||
import {RuntimeGroupConfiguration} from "./RuntimeGroupConfiguration";
|
||||
import {ChannelId} from "../types/DiscordTypes";
|
||||
|
||||
type UIElementCollection = Record<string, UIElement>;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 => 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 (e) {
|
||||
Container.get<Logger>("logger").error("awaiting message component failed: ", e)
|
||||
}
|
||||
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 (e) {
|
||||
|
||||
}
|
||||
await eventResponse.deleteReply();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (interaction.replied) {
|
||||
await interaction.deleteReply();
|
||||
}
|
||||
}
|
||||
|
||||
private getReplyOptions(): InteractionUpdateOptions & InteractionReplyOptions & { withResponse: true } {
|
||||
const embed = this.createEmbed();
|
||||
embed.setAuthor({
|
||||
name: "/ " + this.breadcrumbs.join(" / ")
|
||||
});
|
||||
|
||||
const exitButton = new ButtonBuilder()
|
||||
.setLabel("Exit")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setCustomId("exit");
|
||||
|
||||
const actionrow = new ActionRowBuilder<ButtonBuilder>()
|
||||
|
||||
if (this.breadcrumbs.length > 0) {
|
||||
const backButton = new ButtonBuilder()
|
||||
.setLabel("Back")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVEBACK_COMMAND);
|
||||
|
||||
actionrow.addComponents(backButton)
|
||||
}
|
||||
actionrow.addComponents(exitButton)
|
||||
|
||||
return {
|
||||
content: "",
|
||||
embeds: [embed],
|
||||
components: [...this.createActionRowBuildersForMenu(), actionrow],
|
||||
withResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
private createEmbed(): EmbedBuilder {
|
||||
const {currentCollection, 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:
|
||||
return displaynames.of((<Intl.Locale>value).baseName) ?? "Unknown";
|
||||
case TransformerType.Channel:
|
||||
return channelMention(<ChannelId>value);
|
||||
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
}
|
||||
|
||||
private createActionRowBuildersForMenu() : ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
||||
const {currentCollection, currentElement} = this.findCurrentUI();
|
||||
|
||||
if (currentElement?.isConfiguration ?? false) {
|
||||
return [
|
||||
new ActionRowBuilder<ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder>()
|
||||
.addComponents(this.getSelectForBreadcrumbs(<UIElement>currentElement))
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
new ActionRowBuilder<ButtonBuilder>()
|
||||
.setComponents(
|
||||
...Object.values(currentCollection).map(elem => new ButtonBuilder()
|
||||
.setLabel(elem.label)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVETO_COMMAND + elem.key)
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private getSelectForBreadcrumbs(currentElement: UIElement): 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");
|
||||
|
||||
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>("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:
|
||||
return interaction.values.join('; ');
|
||||
default:
|
||||
throw new Error("Unhandled select menu");
|
||||
}
|
||||
}
|
||||
|
||||
private findCurrentUI(): {currentElement: Nullable<UIElement>, currentCollection: UIElementCollection } {
|
||||
let currentCollection: UIElementCollection = GroupConfigurationRenderer.UI_ELEMENTS;
|
||||
let currentElement: Nullable<UIElement> = null;
|
||||
|
||||
for (const breadcrumb of this.breadcrumbs) {
|
||||
currentElement = currentCollection[breadcrumb];
|
||||
|
||||
if (currentElement.isConfiguration ?? false) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentCollection = currentElement.childrenElements ?? {};
|
||||
}
|
||||
|
||||
return {
|
||||
currentElement,
|
||||
currentCollection,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue