Adds group configuration
This commit is contained in:
parent
0d9cf6a370
commit
154002f6f3
16 changed files with 633 additions and 20 deletions
|
|
@ -5,12 +5,13 @@ export class Container {
|
|||
|
||||
public set<T extends {constructor: {name: string}}>(instance: T, name: string|null = null): void
|
||||
{
|
||||
this.instances.set(name ?? instance.constructor.name, instance);
|
||||
const settingName = name ?? instance.constructor.name;
|
||||
this.instances.set(settingName.toLowerCase(), instance);
|
||||
}
|
||||
|
||||
public get<T>(name: string): T
|
||||
{
|
||||
return <T>this.instances.get(name);
|
||||
return <T>this.instances.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
static getInstance(): Container {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import path from "node:path";
|
|||
import {GroupRepository} from "../Repositories/GroupRepository";
|
||||
import {PlaydateRepository} from "../Repositories/PlaydateRepository";
|
||||
import {GuildEmojiRoleManager} from "discord.js";
|
||||
import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository";
|
||||
|
||||
export enum ServiceHint {
|
||||
App,
|
||||
|
|
@ -56,5 +57,6 @@ export class Services {
|
|||
const db = container.get<DatabaseConnection>(DatabaseConnection.name);
|
||||
container.set<GroupRepository>(new GroupRepository(db));
|
||||
container.set<PlaydateRepository>(new PlaydateRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
container.set<GroupConfigurationRepository>(new GroupConfigurationRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import Groups from "./tables/Groups";
|
||||
import {DatabaseDefinition} from "./DatabaseDefinition";
|
||||
import Playdate from "./tables/Playdate";
|
||||
import GroupConfiguration from "./tables/GroupConfiguration";
|
||||
|
||||
const definitions = new Set<DatabaseDefinition>([
|
||||
Groups,
|
||||
Playdate
|
||||
Playdate,
|
||||
GroupConfiguration
|
||||
]);
|
||||
|
||||
export default definitions;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import {DatabaseDefinition} from "../DatabaseDefinition";
|
||||
|
||||
export type DBGroup = {
|
||||
export type DBGroupConfiguration = {
|
||||
id: number;
|
||||
groupid: number;
|
||||
key: string;
|
||||
|
|
@ -8,7 +8,7 @@ export type DBGroup = {
|
|||
}
|
||||
|
||||
const dbDefinition: DatabaseDefinition = {
|
||||
name: "groups",
|
||||
name: "groupConfiguration",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ import {Container} from "../../Container/Container";
|
|||
import {GroupSelection} from "../CommandPartials/GroupSelection";
|
||||
import {UserError} from "../UserError";
|
||||
import {ArrayUtils} from "../../Utilities/ArrayUtils";
|
||||
import {GroupConfigurationRenderer} from "../../Groups/GroupConfigurationRenderer";
|
||||
import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler";
|
||||
import {GroupConfigurationTransformers} from "../../Groups/GroupConfigurationTransformers";
|
||||
import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository";
|
||||
|
||||
export class GroupCommand implements Command, ChatInteractionCommand, AutocompleteCommand {
|
||||
private static GOODBYE_MESSAGES: string[] = [
|
||||
|
|
@ -57,7 +61,8 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
);
|
||||
}
|
||||
execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
|
||||
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "create":
|
||||
this.create(interaction);
|
||||
|
|
@ -66,10 +71,11 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
this.list(interaction);
|
||||
break;
|
||||
case "remove":
|
||||
this.remove(interaction);
|
||||
await this.remove(interaction);
|
||||
break;
|
||||
case "config":
|
||||
this.runConfigurator(interaction);
|
||||
await this.runConfigurator(interaction);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported command");
|
||||
}
|
||||
|
|
@ -159,9 +165,17 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
}
|
||||
}
|
||||
|
||||
private runConfigurator(interaction: ChatInputCommandInteraction) {
|
||||
private async runConfigurator(interaction: ChatInputCommandInteraction) {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
const configurationRenderer = new GroupConfigurationRenderer(
|
||||
new GroupConfigurationHandler(
|
||||
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
|
||||
group
|
||||
),
|
||||
new GroupConfigurationTransformers(),
|
||||
)
|
||||
|
||||
await configurationRenderer.setup(interaction);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,13 +38,6 @@ export class DiscordClient {
|
|||
})
|
||||
|
||||
this.client.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
|
||||
if (command === null) {
|
||||
Container.get<Logger>("logger").error(`Could not find command for '${interaction.commandName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const method = this.findCommandMethod(interaction);
|
||||
if (!method) {
|
||||
Container.get<Logger>("logger").error(`Could not find method for '${interaction.commandName}'`);
|
||||
|
|
@ -77,7 +70,7 @@ export class DiscordClient {
|
|||
try {
|
||||
await command.execute(interaction)
|
||||
}
|
||||
catch (e: Error) {
|
||||
catch (e: any) {
|
||||
Container.get<Logger>("logger").error(e)
|
||||
|
||||
let userMessage = ":x: There was an error while executing this command!";
|
||||
|
|
|
|||
68
source/Groups/GroupConfigurationHandler.ts
Normal file
68
source/Groups/GroupConfigurationHandler.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import {RuntimeGroupConfiguration} from "./RuntimeGroupConfiguration";
|
||||
import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository";
|
||||
import {GroupModel} from "../Models/GroupModel";
|
||||
import {GroupConfigurationResult, GroupConfigurationTransformers} from "./GroupConfigurationTransformers";
|
||||
// @ts-ignore
|
||||
import setPath from 'object-path-set';
|
||||
import deepmerge from "deepmerge";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
|
||||
export class GroupConfigurationHandler {
|
||||
private static DEFAULT_CONFIGURATION: RuntimeGroupConfiguration = {
|
||||
channels: null,
|
||||
locale: new Intl.Locale('en-GB'),
|
||||
}
|
||||
|
||||
private readonly transformers: GroupConfigurationTransformers = new GroupConfigurationTransformers();
|
||||
|
||||
constructor(
|
||||
private readonly repository: GroupConfigurationRepository,
|
||||
private readonly group: GroupModel
|
||||
) { }
|
||||
|
||||
|
||||
public saveConfiguration(path: string, value: string): void {
|
||||
const configuration = this.repository.findConfigurationByPath(this.group, path);
|
||||
|
||||
if (configuration) {
|
||||
this.repository.update(
|
||||
{
|
||||
...configuration,
|
||||
value: value
|
||||
}
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
this.repository.create({
|
||||
group: this.group,
|
||||
key: path,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration(): RuntimeGroupConfiguration {
|
||||
return deepmerge(GroupConfigurationHandler.DEFAULT_CONFIGURATION, this.getDatabaseConfiguration());
|
||||
}
|
||||
|
||||
public getConfigurationByPath(path: string): Nullable<GroupConfigurationResult> {
|
||||
const configuration = this.repository.findConfigurationByPath(this.group, path);
|
||||
if (!configuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.transformers.getValue(configuration);
|
||||
}
|
||||
|
||||
private getDatabaseConfiguration(): Partial<RuntimeGroupConfiguration> {
|
||||
const values = this.repository.findGroupConfigurations(this.group);
|
||||
const configuration: Partial<RuntimeGroupConfiguration> = {};
|
||||
|
||||
values.forEach((configValue) => {
|
||||
const value = this.transformers.getValue(configValue);
|
||||
setPath(configuration, configValue.key, value);
|
||||
})
|
||||
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
61
source/Groups/GroupConfigurationTransformers.ts
Normal file
61
source/Groups/GroupConfigurationTransformers.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import {ChannelId} from "../types/DiscordTypes";
|
||||
import {GroupConfigurationModel} from "../Models/GroupConfigurationModel";
|
||||
import {config} from "dotenv";
|
||||
import {transform} from "esbuild";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {ArrayUtils} from "../Utilities/ArrayUtils";
|
||||
|
||||
export enum TransformerType {
|
||||
Locale,
|
||||
Channel,
|
||||
}
|
||||
|
||||
type GroupConfigurationTransformer = {
|
||||
path: string[];
|
||||
type: TransformerType,
|
||||
}
|
||||
|
||||
export type GroupConfigurationResult =
|
||||
ChannelId | Intl.Locale
|
||||
|
||||
export class GroupConfigurationTransformers {
|
||||
static TRANSFORMERS: GroupConfigurationTransformer[] = [
|
||||
{
|
||||
path: ['channels', 'newPlaydates'],
|
||||
type: TransformerType.Channel,
|
||||
},
|
||||
{
|
||||
path: ['channels', 'playdateReminders'],
|
||||
type: TransformerType.Channel,
|
||||
},
|
||||
{
|
||||
path: ['locale'],
|
||||
type: TransformerType.Locale,
|
||||
}
|
||||
];
|
||||
|
||||
public getValue(configValue: GroupConfigurationModel): GroupConfigurationResult {
|
||||
const transformerType = this.getTransformerType(configValue.key);
|
||||
if (transformerType === undefined || transformerType === null) {
|
||||
throw new Error(`Can't find transformer for ${configValue.key}`);
|
||||
}
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
return new Intl.Locale(configValue.value)
|
||||
case TransformerType.Channel:
|
||||
return <ChannelId>configValue.value;
|
||||
}
|
||||
}
|
||||
|
||||
public getTransformerType(configKey: string): Nullable<TransformerType> {
|
||||
const path = configKey.split('.');
|
||||
return GroupConfigurationTransformers.TRANSFORMERS.find(
|
||||
transformer => {
|
||||
return ArrayUtils.arraysEqual(transformer.path, path);
|
||||
}
|
||||
)?.type;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
9
source/Groups/RuntimeGroupConfiguration.d.ts
vendored
Normal file
9
source/Groups/RuntimeGroupConfiguration.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export type RuntimeGroupConfiguration = {
|
||||
channels: Nullable<ChannelRuntimeGroupConfiguration>,
|
||||
locale: Intl.Locale,
|
||||
};
|
||||
|
||||
export type ChannelRuntimeGroupConfiguration = {
|
||||
newPlaydates: ChannelId,
|
||||
playdateReminders: ChannelId
|
||||
}
|
||||
65
source/Repositories/GroupConfigurationRepository.ts
Normal file
65
source/Repositories/GroupConfigurationRepository.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import {Repository} from "./Repository";
|
||||
import GroupConfiguration, {DBGroupConfiguration} from "../Database/tables/GroupConfiguration";
|
||||
import {GroupConfigurationModel} from "../Models/GroupConfigurationModel";
|
||||
import { GroupModel } from "../Models/GroupModel";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {DatabaseConnection} from "../Database/DatabaseConnection";
|
||||
import {GroupRepository} from "./GroupRepository";
|
||||
|
||||
export class GroupConfigurationRepository extends Repository<GroupConfigurationModel, DBGroupConfiguration> {
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
private readonly groupRepository: GroupRepository,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
GroupConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
public findGroupConfigurations(group: GroupModel): GroupConfigurationModel[] {
|
||||
return this.database.fetchAll<number, DBGroupConfiguration>(`
|
||||
SELECT * FROM groupConfiguration WHERE groupid = ?`,
|
||||
group.id
|
||||
).map((config) => {
|
||||
return this.convertToModelType(config, group);
|
||||
})
|
||||
}
|
||||
|
||||
public findConfigurationByPath(group: GroupModel, path: string): Nullable<GroupConfigurationModel> {
|
||||
const result = this.database.fetch<number, DBGroupConfiguration>(`
|
||||
SELECT * FROM groupConfiguration WHERE groupid = ? AND key = ?`,
|
||||
group.id,
|
||||
path
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.convertToModelType(result, group);
|
||||
}
|
||||
|
||||
|
||||
protected convertToModelType(intermediateModel: DBGroupConfiguration | undefined, group: Nullable<GroupModel> = null): GroupConfigurationModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("No intermediate model provided");
|
||||
}
|
||||
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
group: group ?? this.groupRepository.getById(intermediateModel.id),
|
||||
key: intermediateModel.key,
|
||||
value: intermediateModel.value,
|
||||
}
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<GroupConfigurationModel>): object {
|
||||
return {
|
||||
groupid: instance.group?.id ?? undefined,
|
||||
key: instance.key ?? undefined,
|
||||
value: instance.value ?? undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,30 @@ export class Repository<ModelType extends Model, IntermediateModelType = unknown
|
|||
return result.lastInsertRowid;
|
||||
}
|
||||
|
||||
public update(instance: Partial<ModelType>&{id: number}): boolean {
|
||||
const columnNames = this.schema.columns.filter((column) => {
|
||||
return !column.primaryKey
|
||||
}).map((column) => {
|
||||
return column.name;
|
||||
});
|
||||
|
||||
const createObject = this.convertToCreateObject(instance);
|
||||
const keys = Object.keys(createObject);
|
||||
const missingColumns = columnNames.filter((columnName) => {
|
||||
return !keys.includes(columnName);
|
||||
})
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Error("Can't create instance, due to missing column values: " + missingColumns);
|
||||
}
|
||||
|
||||
const sql = `UPDATE ${this.schema.name}
|
||||
SET ${Object.keys(createObject).map((key) => `${key} = ?`).join(',')}
|
||||
WHERE id = ?`;
|
||||
const result = this.database.execute(sql, ...Object.values(createObject), instance.id);
|
||||
return result.lastInsertRowid;
|
||||
}
|
||||
|
||||
public getById(id: number): Nullable<ModelType> {
|
||||
const sql = `SELECT * FROM ${this.schema.name} WHERE id = ? LIMIT 1`;
|
||||
return this.convertToModelType(this.database.fetch<number, IntermediateModelType>(sql, id));
|
||||
|
|
|
|||
|
|
@ -3,4 +3,15 @@ export class ArrayUtils {
|
|||
const index = Math.floor(Math.random() * array.length);
|
||||
return array[index];
|
||||
}
|
||||
|
||||
public static arraysEqual<T>(a: Array<T>, b: Array<T>): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
for (var i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,4 +6,6 @@ export type GuildMember = {
|
|||
export type Role = {
|
||||
server: string;
|
||||
roleid: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ChannelId = string;
|
||||
Loading…
Add table
Add a link
Reference in a new issue