Adds eslint and linted & improved routing for interactions
15
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { defineConfig } from "eslint/config";
|
||||
|
||||
|
||||
export default defineConfig([
|
||||
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"] },
|
||||
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: globals.browser } },
|
||||
tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
}
|
||||
}
|
||||
]);
|
||||
1599
package-lock.json
generated
|
|
@ -28,5 +28,11 @@
|
|||
"node-cron": "^4.0.7",
|
||||
"object-path-set": "^1.0.2",
|
||||
"svg2img": "^1.0.0-beta.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
"eslint": "^9.29.0",
|
||||
"globals": "^16.2.0",
|
||||
"typescript-eslint": "^8.34.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
public/icons/angle-left-solid.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"/></svg>
|
||||
|
After Width: | Height: | Size: 399 B |
1
public/icons/door-open-solid-white.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#ffffff" d="M320 32c0-9.9-4.5-19.2-12.3-25.2S289.8-1.4 280.2 1l-179.9 45C79 51.3 64 70.5 64 92.5L64 448l-32 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 192 0 32 0 0-32 0-448zM256 256c0 17.7-10.7 32-24 32s-24-14.3-24-32s10.7-32 24-32s24 14.3 24 32zm96-128l96 0 0 352c0 17.7 14.3 32 32 32l64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-32 0 0-320c0-35.3-28.7-64-64-64l-96 0 0 64z"/></svg>
|
||||
|
After Width: | Height: | Size: 605 B |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="white" d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z"/></svg>
|
||||
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 419 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#9a9996" d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32l0 96L0 384c0 35.3 28.7 64 64 64l192 0 0-64L64 384l0-224 192 0 0-64L64 96l0-64zM288 192c0 17.7 14.3 32 32 32l224 0c17.7 0 32-14.3 32-32l0-128c0-17.7-14.3-32-32-32l-98.7 0c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4L320 0c-17.7 0-32 14.3-32 32l0 160zm0 288c0 17.7 14.3 32 32 32l224 0c17.7 0 32-14.3 32-32l0-128c0-17.7-14.3-32-32-32l-98.7 0c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4L320 288c-17.7 0-32 14.3-32 32l0 160z"/></svg>
|
||||
|
Before Width: | Height: | Size: 732 B |
1
public/icons/pen-solid.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="white" d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 504 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#FFD43B" d="M72 88a56 56 0 1 1 112 0A56 56 0 1 1 72 88zM64 245.7C54 256.9 48 271.8 48 288s6 31.1 16 42.3l0-84.7zm144.4-49.3C178.7 222.7 160 261.2 160 304c0 34.3 12 65.8 32 90.5l0 21.5c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32-14.3-32-32l0-26.8C26.2 371.2 0 332.7 0 288c0-61.9 50.1-112 112-112l32 0c24 0 46.2 7.5 64.4 20.3zM448 416l0-21.5c20-24.7 32-56.2 32-90.5c0-42.8-18.7-81.3-48.4-107.7C449.8 183.5 472 176 496 176l32 0c61.9 0 112 50.1 112 112c0 44.7-26.2 83.2-64 101.2l0 26.8c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32-14.3-32-32zm8-328a56 56 0 1 1 112 0A56 56 0 1 1 456 88zM576 245.7l0 84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM320 32a64 64 0 1 1 0 128 64 64 0 1 1 0-128zM240 304c0 16.2 6 31 16 42.3l0-84.7c-10 11.3-16 26.1-16 42.3zm144-42.3l0 84.7c10-11.3 16-26.1 16-42.3s-6-31.1-16-42.3zM448 304c0 44.7-26.2 83.2-64 101.2l0 42.8c0 17.7-14.3 32-32 32l-64 0c-17.7 0-32-14.3-32-32l0-42.8c-37.8-18-64-56.5-64-101.2c0-61.9 50.1-112 112-112l32 0c61.9 0 112 50.1 112 112z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,15 +1,16 @@
|
|||
import {Environment} from "../Environment";
|
||||
import {Container} from "./Container";
|
||||
import {DatabaseConnection} from "../Database/DatabaseConnection";
|
||||
import {getLogger, configure, Logger} from "log4js";
|
||||
import {configure, getLogger, Logger} from "log4js";
|
||||
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";
|
||||
import {DiscordClient} from "../Discord/DiscordClient";
|
||||
import {IconCache} from "../Icons/IconCache";
|
||||
import {EventHandler} from "../Events/EventHandler";
|
||||
import {InteractionRouter} from "../Discord/InteractionRouter";
|
||||
import Commands from "../Discord/Commands/Commands";
|
||||
|
||||
export enum ServiceHint {
|
||||
App,
|
||||
|
|
@ -21,19 +22,35 @@ export class Services {
|
|||
const env = new Environment();
|
||||
env.setup();
|
||||
container.set<Environment>(env);
|
||||
|
||||
const logger = this.setupLogger(hint);
|
||||
container.set<Logger>(logger, 'logger');
|
||||
|
||||
const database = new DatabaseConnection(env.database);
|
||||
container.set<DatabaseConnection>(database);
|
||||
|
||||
const discordClient = new DiscordClient(env.discord.clientId);
|
||||
const discordClient = new DiscordClient(
|
||||
env.discord.clientId,
|
||||
new InteractionRouter(new Commands(), logger)
|
||||
);
|
||||
container.set<DiscordClient>(discordClient);
|
||||
|
||||
const iconCache = new IconCache(discordClient);
|
||||
container.set<IconCache>(iconCache);
|
||||
|
||||
container.set<EventHandler>(new EventHandler());
|
||||
|
||||
// @ts-ignore
|
||||
this.setupRepositories(container);
|
||||
}
|
||||
|
||||
private static setupRepositories(container: Container) {
|
||||
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)))
|
||||
}
|
||||
|
||||
private static setupLogger(hint: ServiceHint): Logger {
|
||||
|
||||
configure({
|
||||
appenders: {
|
||||
out: { type: "stdout" },
|
||||
|
|
@ -46,7 +63,7 @@ export class Services {
|
|||
deploy: { appenders: ["out", "deployLogFile"], level: "debug" },
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
let loggername = '';
|
||||
switch (hint) {
|
||||
case ServiceHint.App:
|
||||
|
|
@ -57,17 +74,6 @@ export class Services {
|
|||
break;
|
||||
|
||||
}
|
||||
const logger = getLogger(loggername);
|
||||
|
||||
container.set<Logger>(logger, 'logger');
|
||||
|
||||
this.setupRepositories(container);
|
||||
}
|
||||
|
||||
private static setupRepositories(container: Container) {
|
||||
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)))
|
||||
return getLogger(loggername);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,20 @@
|
|||
import {DatabaseEnvironment, Environment} from "../Environment";
|
||||
import {DatabaseEnvironment} from "../Environment";
|
||||
import Sqlite3 from "better-sqlite3";
|
||||
import Database from "better-sqlite3";
|
||||
import {Container} from "../Container/Container";
|
||||
import {Logger} from "log4js";
|
||||
|
||||
export class DatabaseConnection {
|
||||
private static connection: DatabaseConnection;
|
||||
|
||||
private database: Sqlite3.Database;
|
||||
|
||||
constructor(private readonly env: DatabaseEnvironment) {
|
||||
constructor(env: DatabaseEnvironment) {
|
||||
this.database = new Database(env.path, {
|
||||
nativeBinding: "node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||
})
|
||||
this.database.pragma('journal_mode = WAL');
|
||||
}
|
||||
|
||||
public execute(query: string, ...args: any[]): Sqlite3.RunResult {
|
||||
public execute(query: string, ...args: unknown[]): Sqlite3.RunResult {
|
||||
try {
|
||||
const preparedQuery = this.database.prepare(query);
|
||||
return preparedQuery.run(args);
|
||||
|
|
@ -26,11 +24,11 @@ export class DatabaseConnection {
|
|||
}
|
||||
}
|
||||
|
||||
public fetch<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: any[]): Result|undefined {
|
||||
public fetch<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: unknown[]): Result|undefined {
|
||||
const preparedQuery = this.database.prepare<BindParameters, Result>(query);
|
||||
return preparedQuery.get(args);
|
||||
}
|
||||
public fetchAll<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: any[]): Result[] {
|
||||
public fetchAll<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: unknown[]): Result[] {
|
||||
const preparedQuery = this.database.prepare<BindParameters, Result>(query);
|
||||
return preparedQuery.all(args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class DatabaseUpdater {
|
|||
}
|
||||
|
||||
private ensureTableColumns(definition: DatabaseDefinition) {
|
||||
const DBSQLColumns = this.database.fetchAll<{}, {name: string, type: string}>(
|
||||
const DBSQLColumns = this.database.fetchAll<object, {name: string, type: string}>(
|
||||
`PRAGMA table_info("${definition.name}")`
|
||||
);
|
||||
|
||||
|
|
@ -76,6 +76,6 @@ export class DatabaseUpdater {
|
|||
}).join(', ');
|
||||
|
||||
const sql = `CREATE TABLE IF NOT EXISTS ${definition.name} (${columnsSQL})`;
|
||||
const result = this.database.execute(sql);
|
||||
this.database.execute(sql);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ export class CommandDeployer {
|
|||
}
|
||||
|
||||
public async deployAvailableServers() {
|
||||
const commandInfos = [];
|
||||
const commandInfos: object[] = [];
|
||||
this.client.Commands.allCommands.forEach((command) => {
|
||||
commandInfos.push(command.definition().toJSON())
|
||||
})
|
||||
|
|
@ -27,11 +27,10 @@ export class CommandDeployer {
|
|||
this.logger.log(`Started refreshing ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
|
||||
// The put method is used to fully refresh all commands in the guild with the current set
|
||||
const data = await this.client.RESTClient.put(
|
||||
Routes.applicationGuildCommands(this.client.ApplicationId, serverId),
|
||||
{ body: commandInfos },
|
||||
await this.client.RESTClient.put(
|
||||
Routes.applicationGuildCommands(this.client.ApplicationId, serverId),
|
||||
{ body: commandInfos },
|
||||
);
|
||||
|
||||
this.logger.log(`Successfully reloaded ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import {
|
||||
AutocompleteInteraction,
|
||||
ChatInputCommandInteraction,
|
||||
CommandInteraction,
|
||||
GuildMember, SlashCommandIntegerOption,
|
||||
SlashCommandStringOption
|
||||
} from "discord.js";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupRepository} from "../../Repositories/GroupRepository";
|
||||
|
|
@ -22,7 +20,7 @@ export class GroupSelection {
|
|||
public static async handleAutocomplete(interaction: AutocompleteInteraction, onlyLeaders: boolean = false): Promise<void> {
|
||||
const value = interaction.options.getFocused();
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
let groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
|
||||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
|
||||
await interaction.respond(
|
||||
groups
|
||||
.filter((group) => group.name.startsWith(value))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {ChatInputCommandInteraction, Interaction, SlashCommandBuilder} from "discord.js";
|
||||
import Commands from "./Commands";
|
||||
|
||||
export interface Command {
|
||||
definition(): SlashCommandBuilder;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
SlashCommandBuilder,
|
||||
ChatInputCommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
ChatInputCommandInteraction,
|
||||
MessageFlags,
|
||||
InteractionReplyOptions,
|
||||
GuildMember,
|
||||
EmbedBuilder,
|
||||
AutocompleteInteraction,
|
||||
roleMention, time, userMention
|
||||
roleMention, time, userMention, GuildMemberRoleManager
|
||||
} from "discord.js";
|
||||
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
|
||||
import {GroupModel} from "../../Models/GroupModel";
|
||||
|
|
@ -19,265 +19,264 @@ import {GroupConfigurationRenderer} from "../../Groups/GroupConfigurationRendere
|
|||
import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler";
|
||||
import {GroupConfigurationTransformers} from "../../Groups/GroupConfigurationTransformers";
|
||||
import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository";
|
||||
import {IconCache} from "../../Icons/IconCache";
|
||||
import {PlaydateRepository} from "../../Repositories/PlaydateRepository";
|
||||
import {Nullable} from "../../types/Nullable";
|
||||
|
||||
export class GroupCommand implements Command, ChatInteractionCommand, AutocompleteCommand {
|
||||
private static GOODBYE_MESSAGES: string[] = [
|
||||
'Sad to see you go.',
|
||||
'May your next adventure be fruitful.',
|
||||
'I hope, I served you well.',
|
||||
'I wish you and your group good luck on your next adventures.',
|
||||
]
|
||||
|
||||
private static INVALID_CHARACTER_SEQUENCES: string[] = [
|
||||
"http://",
|
||||
"https://"
|
||||
]
|
||||
|
||||
definition(): SlashCommandBuilder {
|
||||
// @ts-ignore
|
||||
return new SlashCommandBuilder()
|
||||
.setName('groups')
|
||||
.setDescription(`Manages groups`)
|
||||
.addSubcommand(create =>
|
||||
create.setName("create")
|
||||
.setDescription("Creates a new group, with executing user being the leader")
|
||||
.addStringOption((option) =>
|
||||
option.setName("name")
|
||||
.setDescription("Defines the name for the group.")
|
||||
.setRequired(true)
|
||||
.setMaxLength(64)
|
||||
)
|
||||
.addRoleOption((builder) =>
|
||||
builder.setName("role")
|
||||
.setDescription("Defines the role, where all the members are located in.")
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(listCommand =>
|
||||
listCommand
|
||||
.setName("list")
|
||||
.setDescription("Displays the groups you are apart of.")
|
||||
)
|
||||
.addSubcommand(command => command
|
||||
.setName('config')
|
||||
.setDescription("Starts the config manager for the group.")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
)
|
||||
.addSubcommand(command => command
|
||||
.setName("remove")
|
||||
.setDescription("Deletes a group you are the leader for.")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
)
|
||||
.addSubcommand(command => command
|
||||
.setName("transfer")
|
||||
.setDescription("Transfers leadership of a group to a different person")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup)
|
||||
.addUserOption(option => option
|
||||
.setName("target")
|
||||
.setDescription("The member, that is the new leader")
|
||||
.setRequired(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
private static GOODBYE_MESSAGES: string[] = [
|
||||
'Sad to see you go.',
|
||||
'May your next adventure be fruitful.',
|
||||
'I hope, I served you well.',
|
||||
'I wish you and your group good luck on your next adventures.',
|
||||
]
|
||||
|
||||
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "create":
|
||||
this.create(interaction);
|
||||
break;
|
||||
case "list":
|
||||
this.list(interaction);
|
||||
break;
|
||||
case "remove":
|
||||
await this.remove(interaction);
|
||||
break;
|
||||
case "config":
|
||||
await this.runConfigurator(interaction);
|
||||
break;
|
||||
case "transfer":
|
||||
await this.transferLeadership(interaction);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported command");
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private create(interaction: ChatInputCommandInteraction): void {
|
||||
const name = interaction.options.getString("name") ?? '';
|
||||
const role = interaction.options.getRole("role", true);
|
||||
|
||||
if (role.id === interaction.guildId) {
|
||||
throw new UserError("Creating a group for everyone is not permitted!");
|
||||
}
|
||||
|
||||
if (!interaction.member?.roles.cache.has(role?.id) ?? true) {
|
||||
throw new UserError(
|
||||
"You are not part of the role, you try to create a group for.",
|
||||
"Add yourself to the group or ask your admin to do so."
|
||||
);
|
||||
}
|
||||
|
||||
const validName = this.validateGroupName(name);
|
||||
if (name !== true) {
|
||||
throw new UserError(`Your group name contains one or more invalid character sequences: ${validName}`)
|
||||
}
|
||||
|
||||
const group: GroupModel = {
|
||||
id: -1,
|
||||
name: name,
|
||||
leader: {
|
||||
server: interaction.guildId ?? '',
|
||||
memberid: interaction.member?.user.id ?? ''
|
||||
},
|
||||
role: {
|
||||
server: interaction.guildId ?? '',
|
||||
roleid: role?.id ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
Container.get<GroupRepository>(GroupRepository.name).create(group);
|
||||
|
||||
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral })
|
||||
}
|
||||
|
||||
private validateGroupName(name: string): true|string{
|
||||
const lowercaseName = name.toLowerCase();
|
||||
for (let invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
|
||||
if (!lowercaseName.includes(invalidcharactersequence)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return invalidcharactersequence
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static INVALID_CHARACTER_SEQUENCES: string[] = [
|
||||
"http://",
|
||||
"https://"
|
||||
]
|
||||
|
||||
private list(interaction: ChatInputCommandInteraction) {
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
|
||||
|
||||
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
|
||||
const iconCache = Container.get<IconCache>(IconCache.name);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Your groups on this server:")
|
||||
.setFields(
|
||||
groups.map(group => {
|
||||
const nextPlaydate = playdateRepo.getNextPlaydateForGroup(group);
|
||||
|
||||
const values = [
|
||||
`Role: ${iconCache.getEmoji("people_group_solid")} ${roleMention(group.role.roleid)}`,
|
||||
`Leader/GM: ${userMention(group.leader.memberid)}`
|
||||
];
|
||||
|
||||
if (nextPlaydate) {
|
||||
values.push(
|
||||
`Next Playdate: ${iconCache.getEmoji("calendar_days_solid")} ${time(nextPlaydate.from_time, "F")}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
name: group.name,
|
||||
value: values.join("\n")
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const reply: InteractionReplyOptions = {
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
allowedMentions: { roles: [] },
|
||||
flags: MessageFlags.Ephemeral
|
||||
}
|
||||
|
||||
interaction.reply(reply);
|
||||
}
|
||||
|
||||
private async remove(interaction: ChatInputCommandInteraction) {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
if (group.leader.memberid != interaction.member?.user.id) {
|
||||
throw new UserError("Can't remove group. You are not the leader.");
|
||||
}
|
||||
|
||||
repo.deleteGroup(group);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Group deleted.")
|
||||
.setDescription(
|
||||
`:x: Deleted \`${group.name}\`. ${ArrayUtils.chooseRandom(GroupCommand.GOODBYE_MESSAGES)}`
|
||||
)
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
|
||||
const option = interaction.options.getFocused(true);
|
||||
if (option.name == "group") {
|
||||
await GroupSelection.handleAutocomplete(interaction, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async runConfigurator(interaction: ChatInputCommandInteraction) {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
const configurationRenderer = new GroupConfigurationRenderer(
|
||||
new GroupConfigurationHandler(
|
||||
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
|
||||
group
|
||||
),
|
||||
new GroupConfigurationTransformers(),
|
||||
definition(): SlashCommandBuilder {
|
||||
// @ts-expect-error Slash command expects more than needed.
|
||||
return new SlashCommandBuilder()
|
||||
.setName('groups')
|
||||
.setDescription(`Manages groups`)
|
||||
.addSubcommand(create =>
|
||||
create.setName("create")
|
||||
.setDescription("Creates a new group, with executing user being the leader")
|
||||
.addStringOption((option) =>
|
||||
option.setName("name")
|
||||
.setDescription("Defines the name for the group.")
|
||||
.setRequired(true)
|
||||
.setMaxLength(64)
|
||||
)
|
||||
.addRoleOption((builder) =>
|
||||
builder.setName("role")
|
||||
.setDescription("Defines the role, where all the members are located in.")
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(listCommand =>
|
||||
listCommand
|
||||
.setName("list")
|
||||
.setDescription("Displays the groups you are apart of.")
|
||||
)
|
||||
.addSubcommand(command => command
|
||||
.setName('config')
|
||||
.setDescription("Starts the config manager for the group.")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
)
|
||||
.addSubcommand(command => command
|
||||
.setName("remove")
|
||||
.setDescription("Deletes a group you are the leader for.")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
)
|
||||
.addSubcommand(command => command
|
||||
.setName("transfer")
|
||||
.setDescription("Transfers leadership of a group to a different person")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup)
|
||||
.addUserOption(option => option
|
||||
.setName("target")
|
||||
.setDescription("The member, that is the new leader")
|
||||
.setRequired(true)
|
||||
)
|
||||
|
||||
await configurationRenderer.setup(interaction);
|
||||
);
|
||||
}
|
||||
|
||||
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "create":
|
||||
this.create(interaction);
|
||||
break;
|
||||
case "list":
|
||||
this.list(interaction);
|
||||
break;
|
||||
case "remove":
|
||||
await this.remove(interaction);
|
||||
break;
|
||||
case "config":
|
||||
await this.runConfigurator(interaction);
|
||||
break;
|
||||
case "transfer":
|
||||
await this.transferLeadership(interaction);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported command");
|
||||
}
|
||||
|
||||
private async transferLeadership(interaction: ChatInputCommandInteraction) {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
if (group.leader.memberid != interaction.member?.user.id) {
|
||||
throw new UserError(
|
||||
"Can't transfer leadership. You are not the leader."
|
||||
);
|
||||
}
|
||||
|
||||
const newLeader = <GuildMember>interaction.options.getMember("target", true);
|
||||
if (!newLeader.roles.cache.has(group.role.roleid)) {
|
||||
throw new UserError(
|
||||
"Can't transfer leadership: The target member is not part of your group.",
|
||||
"Add the user to the role this group is part in or ask your server admin to do it."
|
||||
);
|
||||
}
|
||||
|
||||
group.leader.memberid = newLeader.id
|
||||
repo.update(group);
|
||||
private create(interaction: ChatInputCommandInteraction): void {
|
||||
const name = interaction.options.getString("name") ?? '';
|
||||
const role = interaction.options.getRole("role", true);
|
||||
|
||||
if (role.id === interaction.guildId) {
|
||||
throw new UserError("Creating a group for everyone is not permitted!");
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Leadership transfered")
|
||||
.setDescription(
|
||||
`Leadership was successfully transfered to ${userMention(newLeader.user.id)}`
|
||||
if (!(<Nullable<GuildMemberRoleManager>>interaction.member?.roles)?.cache.has(role?.id)) {
|
||||
throw new UserError(
|
||||
"You are not part of the role, you try to create a group for.",
|
||||
"Add yourself to the group or ask your admin to do so."
|
||||
);
|
||||
}
|
||||
|
||||
const validName = this.validateGroupName(name);
|
||||
// @ts-expect-error Is correct, since the valid name can return either true or a string and the error should only be thrown if it's a string.
|
||||
if (name !== true) {
|
||||
throw new UserError(`Your group name contains one or more invalid character sequences: ${validName}`)
|
||||
}
|
||||
|
||||
const group: GroupModel = {
|
||||
id: -1,
|
||||
name: name,
|
||||
leader: {
|
||||
server: interaction.guildId ?? '',
|
||||
memberid: interaction.member?.user.id ?? ''
|
||||
},
|
||||
role: {
|
||||
server: interaction.guildId ?? '',
|
||||
roleid: role?.id ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
Container.get<GroupRepository>(GroupRepository.name).create(group);
|
||||
|
||||
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral })
|
||||
}
|
||||
|
||||
private validateGroupName(name: string): true|string{
|
||||
const lowercaseName = name.toLowerCase();
|
||||
for (const invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
|
||||
if (!lowercaseName.includes(invalidcharactersequence)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return invalidcharactersequence
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private list(interaction: ChatInputCommandInteraction) {
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
|
||||
|
||||
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Your groups on this server:")
|
||||
.setFields(
|
||||
groups.map(group => {
|
||||
const nextPlaydate = playdateRepo.getNextPlaydateForGroup(group);
|
||||
|
||||
const values = [
|
||||
`Role: ${roleMention(group.role.roleid)}`,
|
||||
`Leader/GM: ${userMention(group.leader.memberid)}`
|
||||
];
|
||||
|
||||
if (nextPlaydate) {
|
||||
values.push(
|
||||
`Next Playdate: ${time(nextPlaydate.from_time, "F")}`
|
||||
)
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
return {
|
||||
name: group.name,
|
||||
value: values.join("\n")
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const reply: InteractionReplyOptions = {
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
allowedMentions: { roles: [] },
|
||||
flags: MessageFlags.Ephemeral
|
||||
}
|
||||
|
||||
interaction.reply(reply);
|
||||
}
|
||||
|
||||
private async remove(interaction: ChatInputCommandInteraction) {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
if (group.leader.memberid != interaction.member?.user.id) {
|
||||
throw new UserError("Can't remove group. You are not the leader.");
|
||||
}
|
||||
|
||||
repo.deleteGroup(group);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Group deleted.")
|
||||
.setDescription(
|
||||
`:x: Deleted \`${group.name}\`. ${ArrayUtils.chooseRandom(GroupCommand.GOODBYE_MESSAGES)}`
|
||||
)
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
|
||||
const option = interaction.options.getFocused(true);
|
||||
if (option.name == "group") {
|
||||
await GroupSelection.handleAutocomplete(interaction, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async transferLeadership(interaction: ChatInputCommandInteraction) {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
if (group.leader.memberid != interaction.member?.user.id) {
|
||||
throw new UserError(
|
||||
"Can't transfer leadership. You are not the leader."
|
||||
);
|
||||
}
|
||||
|
||||
const newLeader = <GuildMember>interaction.options.getMember("target");
|
||||
if (!newLeader.roles.cache.has(group.role.roleid)) {
|
||||
throw new UserError(
|
||||
"Can't transfer leadership: The target member is not part of your group.",
|
||||
"Add the user to the role this group is part in or ask your server admin to do it."
|
||||
);
|
||||
}
|
||||
|
||||
group.leader.memberid = newLeader.id
|
||||
repo.update(group);
|
||||
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Leadership transferred")
|
||||
.setDescription(
|
||||
`Leadership was successfully transferred to ${userMention(newLeader.user.id)}`
|
||||
)
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {SlashCommandBuilder, Interaction, CommandInteraction} from "discord.js";
|
||||
import {SlashCommandBuilder, CommandInteraction} from "discord.js";
|
||||
import {Command} from "./Command";
|
||||
|
||||
export class HelloWorldCommand implements Command {
|
||||
|
|
@ -6,7 +6,7 @@ export class HelloWorldCommand implements Command {
|
|||
'Hello :)',
|
||||
'zzzZ... ZzzzZ... huh? I am awake. I am awake!',
|
||||
'Roll for initiative!',
|
||||
'I was an adventurerer like you...',
|
||||
'I was an adventurer like you...',
|
||||
'Hello :) How are you?',
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,16 @@
|
|||
import {
|
||||
SlashCommandBuilder,
|
||||
Interaction,
|
||||
CommandInteraction,
|
||||
AutocompleteInteraction,
|
||||
GuildMember,
|
||||
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, ModalSubmitFields, time, User
|
||||
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, time
|
||||
} from "discord.js";
|
||||
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupRepository} from "../../Repositories/GroupRepository";
|
||||
import {GroupSelection} from "../CommandPartials/GroupSelection";
|
||||
import {setFlagsFromString} from "node:v8";
|
||||
import {UserError} from "../UserError";
|
||||
import Playdate from "../../Database/tables/Playdate";
|
||||
import {PlaydateModel} from "../../Models/PlaydateModel";
|
||||
import {PlaydateRepository} from "../../Repositories/PlaydateRepository";
|
||||
import {GroupModel} from "../../Models/GroupModel";
|
||||
import playdate from "../../Database/tables/Playdate";
|
||||
|
||||
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
|
||||
static REGEX = [
|
||||
|
|
@ -24,7 +18,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
]
|
||||
|
||||
definition(): SlashCommandBuilder {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error Command builder is improperly marked as incomplete.
|
||||
return new SlashCommandBuilder()
|
||||
.setName("playdates")
|
||||
.setDescription("Manage your playdates")
|
||||
|
|
@ -106,7 +100,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
to_time: new Date(toDate),
|
||||
}
|
||||
|
||||
const id = playdateRepo.create(playdate);
|
||||
playdateRepo.create(playdate);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Created a play-date.")
|
||||
|
|
|
|||
|
|
@ -2,142 +2,66 @@ import {
|
|||
Client,
|
||||
GatewayIntentBits,
|
||||
Events,
|
||||
Interaction,
|
||||
ChatInputCommandInteraction,
|
||||
MessageFlags,
|
||||
Activity,
|
||||
ActivityType, REST, inlineCode
|
||||
ActivityType, REST
|
||||
} from "discord.js";
|
||||
import Commands from "./Commands/Commands";
|
||||
import {Container} from "../Container/Container";
|
||||
import {Logger} from "log4js";
|
||||
import {UserError} from "./UserError";
|
||||
import {InteractionRouter} from "./InteractionRouter";
|
||||
|
||||
export class DiscordClient {
|
||||
private readonly client: Client;
|
||||
private commands: Commands;
|
||||
private readonly restClient: REST;
|
||||
|
||||
public get Client (): Client {
|
||||
|
||||
public get Client(): Client {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public get Commands(): Commands {
|
||||
return this.commands
|
||||
|
||||
public get Commands(): Commands {
|
||||
return this.router.commands
|
||||
}
|
||||
|
||||
|
||||
public get RESTClient(): REST {
|
||||
return this.restClient;
|
||||
}
|
||||
|
||||
|
||||
public get ApplicationId(): string {
|
||||
return this.applicationId;
|
||||
}
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly applicationId: string
|
||||
private readonly applicationId: string,
|
||||
private readonly router: InteractionRouter,
|
||||
private readonly restClient: REST = new REST()
|
||||
) {
|
||||
this.client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds]
|
||||
})
|
||||
|
||||
this.commands = new Commands();
|
||||
this.restClient = new REST();
|
||||
}
|
||||
|
||||
|
||||
applyEvents() {
|
||||
this.client.once(Events.ClientReady, () => {
|
||||
if (!this.client.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
Container.get<Logger>("logger").info(`Ready! Logged in as ${this.client.user.tag}`);
|
||||
this.client.user.setActivity('your PnP playdates', {
|
||||
type: ActivityType.Watching,
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
this.client.on(Events.GuildAvailable, () => {
|
||||
Container.get<Logger>("logger").info("Joined Guild?")
|
||||
})
|
||||
|
||||
this.client.on(Events.InteractionCreate, async (interaction: Interaction) => {
|
||||
const method = this.findCommandMethod(interaction);
|
||||
if (!method) {
|
||||
Container.get<Logger>("logger").error(`Could not find method for '${interaction.commandName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
await method();
|
||||
})
|
||||
|
||||
this.client.on(Events.InteractionCreate, this.router.route.bind(this.router));
|
||||
}
|
||||
|
||||
|
||||
connect(token: string) {
|
||||
this.client.login(token);
|
||||
}
|
||||
|
||||
|
||||
connectRESTClient(token: string) {
|
||||
this.restClient.setToken(token);
|
||||
}
|
||||
|
||||
private findCommandMethod(interaction: Interaction) {
|
||||
if (interaction.isChatInputCommand()) {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!('execute' in command)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return async () => {
|
||||
Container.get<Logger>("logger").debug(`Found chat command ${interaction.commandName}: running...`);
|
||||
|
||||
try {
|
||||
await command.execute(interaction)
|
||||
}
|
||||
catch (e: any) {
|
||||
Container.get<Logger>("logger").error(e)
|
||||
|
||||
let userMessage = ":x: There was an error while executing this command!";
|
||||
if (e.constructor.name === UserError.name) {
|
||||
userMessage = `:x: \`${e.message}\` - Please validate your request!`
|
||||
if (e.tryInstead) {
|
||||
userMessage += `
|
||||
|
||||
You can try the following:
|
||||
${inlineCode(e.tryInstead)}`
|
||||
}
|
||||
}
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
} else {
|
||||
await interaction.reply({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.isAutocomplete()) {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!('handleAutocomplete' in command)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return async () => {
|
||||
Container.get<Logger>("logger").debug(`Found command ${interaction.commandName} for autocomplete: handling...`);
|
||||
|
||||
try {
|
||||
await command.handleAutocomplete(interaction);
|
||||
} catch (e: any) {
|
||||
Container.get<Logger>('logger').error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
109
source/Discord/InteractionRouter.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import {
|
||||
AutocompleteInteraction,
|
||||
ChatInputCommandInteraction,
|
||||
inlineCode,
|
||||
Interaction,
|
||||
MessageFlags,
|
||||
} from "discord.js";
|
||||
import Commands from "./Commands/Commands";
|
||||
import {Logger} from "log4js";
|
||||
import {UserError} from "./UserError";
|
||||
import {Container} from "../Container/Container";
|
||||
|
||||
enum InteractionRoutingType {
|
||||
Unrouted,
|
||||
Command,
|
||||
AutoComplete,
|
||||
}
|
||||
|
||||
export class InteractionRouter {
|
||||
constructor(
|
||||
public readonly commands: Commands,
|
||||
public readonly logger: Logger
|
||||
) {
|
||||
}
|
||||
|
||||
async route(interaction: Interaction) {
|
||||
const interactionType = this.findInteractionType(interaction);
|
||||
|
||||
switch (interactionType) {
|
||||
case InteractionRoutingType.Unrouted:
|
||||
this.logger.debug("Unroutable interaction found...")
|
||||
break;
|
||||
case InteractionRoutingType.Command:
|
||||
await this.handleCommand(<ChatInputCommandInteraction>interaction);
|
||||
break;
|
||||
case InteractionRoutingType.AutoComplete:
|
||||
await this.handleAutocomplete(<AutocompleteInteraction>interaction)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private findInteractionType(interaction: Interaction): InteractionRoutingType {
|
||||
if (interaction.isChatInputCommand()) {
|
||||
return InteractionRoutingType.Command;
|
||||
}
|
||||
|
||||
if (interaction.isAutocomplete()) {
|
||||
return InteractionRoutingType.AutoComplete;
|
||||
}
|
||||
|
||||
return InteractionRoutingType.Unrouted;
|
||||
}
|
||||
|
||||
private async handleCommand(interaction: ChatInputCommandInteraction) {
|
||||
try {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
if (!command) {
|
||||
throw new UserError(`Requested command not found.`);
|
||||
}
|
||||
|
||||
if (!('execute' in command)) {
|
||||
throw new UserError(`Requested command is not setup for a chat command.`);
|
||||
}
|
||||
|
||||
this.logger.debug(`Found chat command ${interaction.commandName}: running...`);
|
||||
|
||||
await command.execute?.call(command, interaction);
|
||||
} catch (e: any) {
|
||||
this.logger.error(e)
|
||||
|
||||
let userMessage = ":x: There was an error while executing this command!";
|
||||
if (e.constructor.name === UserError.name) {
|
||||
userMessage = `:x: \`${e.message}\` - Please validate your request!`
|
||||
if (e.tryInstead) {
|
||||
userMessage += `
|
||||
|
||||
You can try the following:
|
||||
${inlineCode(e.tryInstead)}`
|
||||
}
|
||||
}
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
} else {
|
||||
await interaction.reply({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAutocomplete(interaction: AutocompleteInteraction) {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!('handleAutocomplete' in command)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Container.get<Logger>("logger").debug(`Found command ${interaction.commandName} for autocomplete: handling...`);
|
||||
|
||||
try {
|
||||
await command.handleAutocomplete?.call(command, interaction);
|
||||
} catch (e: unknown) {
|
||||
Container.get<Logger>('logger').error(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ export class ElementCreatedEvent<T extends Model = Model> {
|
|||
constructor(
|
||||
public readonly tableName: string,
|
||||
public readonly instanceValues: Partial<T>,
|
||||
public readonly instanceId: number
|
||||
public readonly instanceId: number|bigint
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import cron from "node-cron";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {Class, ClassNamed} from "../types/Class";
|
||||
import {Class} from "../types/Class";
|
||||
|
||||
export type EventConfiguration = {
|
||||
name: string,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import {ElementCreatedEvent} from "../ElementCreatedEvent";
|
||||
import {DefaultHandler} from "../DefaultEvents";
|
||||
import {PlaydateModel} from "../../Models/PlaydateModel";
|
||||
import PlaydateTableConfiguration from "../../Database/tables/Playdate";
|
||||
import {EmbedBuilder, roleMention, time} from "discord.js";
|
||||
import {ArrayUtils} from "../../Utilities/ArrayUtils";
|
||||
import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupConfigurationRenderer} from "../../Groups/GroupConfigurationRenderer";
|
||||
import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository";
|
||||
import {DiscordClient} from "../../Discord/DiscordClient";
|
||||
|
||||
|
|
@ -37,7 +35,7 @@ export async function sendCreatedNotificationEventHandler(event: ElementCreatedE
|
|||
return;
|
||||
}
|
||||
|
||||
const channel = await Container.get<DiscordClient>(DiscordClient.name).Client.channels.fetch(targetChannel)
|
||||
const channel = await Container.get<DiscordClient>(DiscordClient.name).Client.channels.fetch(<string>targetChannel)
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import {CronExpression, Event, EventConfiguration, TimedEvent} from "./EventHandler";
|
||||
import {EventConfiguration, TimedEvent} from "./EventHandler";
|
||||
import {Container} from "../Container/Container";
|
||||
import Playdate from "../Database/tables/Playdate";
|
||||
import {PlaydateRepository} from "../Repositories/PlaydateRepository";
|
||||
import {GroupConfigurationHandler} from "../Groups/GroupConfigurationHandler";
|
||||
import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository";
|
||||
import {PlaydateModel} from "../Models/PlaydateModel";
|
||||
import {ChannelId} from "../types/DiscordTypes";
|
||||
import {DiscordClient} from "../Discord/DiscordClient";
|
||||
import {EmbedBuilder, MessageFlags, roleMention, time} from "discord.js";
|
||||
import {EmbedBuilder, roleMention, time} from "discord.js";
|
||||
import {ArrayUtils} from "../Utilities/ArrayUtils";
|
||||
|
||||
export class ReminderEvent implements TimedEvent {
|
||||
|
|
@ -27,9 +26,9 @@ export class ReminderEvent implements TimedEvent {
|
|||
name: "Reminders",
|
||||
}
|
||||
|
||||
cronExpression: CronExpression = "0 9 * * *"
|
||||
cronExpression: string = "0 9 * * *"
|
||||
|
||||
private groupConfigurationRepository: GroupConfigurationRepository
|
||||
private readonly groupConfigurationRepository: GroupConfigurationRepository
|
||||
private playdateRepository: PlaydateRepository
|
||||
private discordClient: DiscordClient
|
||||
|
||||
|
|
@ -72,13 +71,13 @@ export class ReminderEvent implements TimedEvent {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.sendReminder(playdate, targetChannel, config.locale);
|
||||
return this.sendReminder(playdate, targetChannel);
|
||||
}, this)
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async sendReminder(playdate: PlaydateModel, targetChannel: ChannelId, locale: Intl.Locale) {
|
||||
private async sendReminder(playdate: PlaydateModel, targetChannel: ChannelId) {
|
||||
if (!playdate.group) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import {RuntimeGroupConfiguration} from "./RuntimeGroupConfiguration";
|
|||
import {GroupConfigurationRepository} from "../Repositories/GroupConfigurationRepository";
|
||||
import {GroupModel} from "../Models/GroupModel";
|
||||
import {GroupConfigurationResult, GroupConfigurationTransformers} from "./GroupConfigurationTransformers";
|
||||
// @ts-ignore
|
||||
// @ts-expect-error set-path is provided
|
||||
import setPath from 'object-path-set';
|
||||
import deepmerge from "deepmerge";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
// @ts-expect-error Any is fine
|
||||
import {isPlainObject} from "is-plain-object";
|
||||
|
||||
export class GroupConfigurationHandler {
|
||||
|
|
|
|||
|
|
@ -2,41 +2,26 @@ import {GroupConfigurationTransformers, TransformerType} from "./GroupConfigurat
|
|||
import {GroupConfigurationHandler} from "./GroupConfigurationHandler";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
AnyComponentBuilder, AnySelectMenuInteraction,
|
||||
APISelectMenuComponent,
|
||||
AnySelectMenuInteraction,
|
||||
ButtonBuilder,
|
||||
ButtonStyle, channelMention,
|
||||
ChannelSelectMenuBuilder, ChannelSelectMenuInteraction,
|
||||
ChannelType,
|
||||
ChatInputCommandInteraction, codeBlock,
|
||||
EmbedBuilder, inlineCode,
|
||||
InteractionCallbackResponse,
|
||||
InteractionEditReplyOptions,
|
||||
ChannelSelectMenuBuilder, ChannelType,
|
||||
ChatInputCommandInteraction, EmbedBuilder, inlineCode, Interaction,
|
||||
InteractionReplyOptions,
|
||||
InteractionUpdateOptions, italic, MessageFlags,
|
||||
SelectMenuBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder, TextBasedChannel,
|
||||
UserSelectMenuBuilder
|
||||
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";
|
||||
import {IconCache} from "../Icons/IconCache";
|
||||
import {ifError} from "node:assert";
|
||||
import {DiscordClient} from "../Discord/DiscordClient";
|
||||
import {channel} from "node:diagnostics_channel";
|
||||
|
||||
type UIElementCollection = Record<string, UIElement>;
|
||||
type UIElement = {
|
||||
|
|
@ -104,7 +89,7 @@ export class GroupConfigurationRenderer {
|
|||
let response = await interaction.reply(this.getReplyOptions());
|
||||
let exit = false;
|
||||
let eventResponse;
|
||||
const filter = i => i.user.id === interaction.user.id;
|
||||
const filter = (i: Interaction) => i.user.id === interaction.user.id;
|
||||
do {
|
||||
|
||||
if (eventResponse) {
|
||||
|
|
@ -117,7 +102,7 @@ export class GroupConfigurationRenderer {
|
|||
filter: filter,
|
||||
time: 60_000
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (_: unknown) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +124,7 @@ export class GroupConfigurationRenderer {
|
|||
}
|
||||
|
||||
if (eventResponse.customId.startsWith(GroupConfigurationRenderer.SETVALUE_COMMAND)) {
|
||||
this.handleSelection(eventResponse);
|
||||
this.handleSelection(<AnySelectMenuInteraction>eventResponse);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +135,7 @@ export class GroupConfigurationRenderer {
|
|||
await eventResponse.update(
|
||||
this.getReplyOptions()
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
await eventResponse.deleteReply();
|
||||
|
|
@ -170,6 +155,7 @@ export class GroupConfigurationRenderer {
|
|||
|
||||
private getReplyOptions(): InteractionUpdateOptions & InteractionReplyOptions & { withResponse: true } {
|
||||
const embed = this.createEmbed();
|
||||
const icons = Container.get<IconCache>(IconCache.name);
|
||||
embed.setAuthor({
|
||||
name: "/ " + this.breadcrumbs.join(" / ")
|
||||
});
|
||||
|
|
@ -177,7 +163,8 @@ export class GroupConfigurationRenderer {
|
|||
const exitButton = new ButtonBuilder()
|
||||
.setLabel("Exit")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setCustomId("exit");
|
||||
.setCustomId("exit")
|
||||
.setEmoji(icons.get("door_open_solid_white") ?? '');
|
||||
|
||||
const actionrow = new ActionRowBuilder<ButtonBuilder>()
|
||||
|
||||
|
|
@ -185,7 +172,8 @@ export class GroupConfigurationRenderer {
|
|||
const backButton = new ButtonBuilder()
|
||||
.setLabel("Back")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVEBACK_COMMAND);
|
||||
.setCustomId(GroupConfigurationRenderer.MOVEBACK_COMMAND)
|
||||
.setEmoji(icons.get("angle_left_solid") ?? '');
|
||||
|
||||
actionrow.addComponents(backButton)
|
||||
}
|
||||
|
|
@ -201,7 +189,7 @@ export class GroupConfigurationRenderer {
|
|||
}
|
||||
|
||||
private createEmbed(): EmbedBuilder {
|
||||
const {currentCollection, currentElement} = this.findCurrentUI();
|
||||
const {currentElement} = this.findCurrentUI();
|
||||
|
||||
if (currentElement === null) {
|
||||
return new EmbedBuilder()
|
||||
|
|
@ -224,7 +212,7 @@ export class GroupConfigurationRenderer {
|
|||
|
||||
private getCurrentValueAsUI(): string {
|
||||
const path = this.breadcrumbs.join(".");
|
||||
let value = this.configurationHandler.getConfigurationByPath(path);
|
||||
const value = this.configurationHandler.getConfigurationByPath(path);
|
||||
|
||||
if (value === undefined) return italic("None");
|
||||
|
||||
|
|
@ -263,7 +251,7 @@ export class GroupConfigurationRenderer {
|
|||
if (currentElement?.isConfiguration ?? false) {
|
||||
return [
|
||||
new ActionRowBuilder<ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder>()
|
||||
.addComponents(this.getSelectForBreadcrumbs(<UIElement>currentElement))
|
||||
.addComponents(this.getSelectForBreadcrumbs())
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -274,13 +262,13 @@ export class GroupConfigurationRenderer {
|
|||
.setLabel(` ${elem.label}`)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVETO_COMMAND + elem.key)
|
||||
.setEmoji(icons.get("folder_tree_solid") ?? '')
|
||||
.setEmoji(icons.get(elem.isConfiguration ? 'pen_solid' : "folder_solid") ?? '')
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private getSelectForBreadcrumbs(currentElement: UIElement): ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder {
|
||||
private getSelectForBreadcrumbs(): ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder {
|
||||
const breadcrumbPath = this.breadcrumbs.join('.')
|
||||
const transformerType = this.transformers.getTransformerType(breadcrumbPath);
|
||||
if (transformerType === undefined) {
|
||||
|
|
@ -300,7 +288,7 @@ export class GroupConfigurationRenderer {
|
|||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setOptions(
|
||||
options.map(intl => new StringSelectMenuOptionBuilder()
|
||||
.setLabel(displaynames.of(intl))
|
||||
.setLabel(displaynames.of(intl) ?? '')
|
||||
.setValue(intl)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
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";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {formatEmoji, Routes, Snowflake} from "discord.js";
|
||||
import {DiscordClient} from "../Discord/DiscordClient";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
|
||||
export class IconCache {
|
||||
private existingIcons: Map<string, string> | undefined;
|
||||
|
|
@ -22,7 +23,7 @@ export class IconCache {
|
|||
const id = this.get(iconName);
|
||||
|
||||
return formatEmoji({
|
||||
id: id,
|
||||
id,
|
||||
name: iconName
|
||||
});
|
||||
}
|
||||
|
|
@ -47,10 +48,14 @@ export class IconCache {
|
|||
return;
|
||||
}
|
||||
|
||||
const existingEmojis: DiscordIconRequest = await this.client.RESTClient.get(
|
||||
const existingEmojis: Nullable<DiscordIconRequest> = await this.client.RESTClient.get(
|
||||
Routes.applicationEmojis(this.client.ApplicationId)
|
||||
)
|
||||
|
||||
if (!existingEmojis) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.existingIcons = new Map<string, string>(
|
||||
existingEmojis.items.map((item) => {
|
||||
return [ item.name, item.id ]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import {REST, Routes} from "discord.js";
|
||||
import path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
import svg2img from "svg2img";
|
||||
|
|
@ -14,7 +13,7 @@ export class IconDeployer {
|
|||
public async ensureExistance() {
|
||||
const directory = await fs.promises.opendir(IconDeployer.ICON_PATH);
|
||||
const addIconPromises: Promise<void>[] = [];
|
||||
for await (let dirname of directory) {
|
||||
for await (const dirname of directory) {
|
||||
const iconName = path.basename(dirname.name, '.svg').replaceAll('-','_');
|
||||
|
||||
if (this.iconCache.get(iconName) !== null) {
|
||||
|
|
@ -43,7 +42,7 @@ export class IconDeployer {
|
|||
}
|
||||
}
|
||||
},
|
||||
function (err, buffer) {
|
||||
function (_err, buffer) {
|
||||
resolve(buffer);
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {Repository} from "./Repository";
|
|||
import {GroupModel} from "../Models/GroupModel";
|
||||
import Groups, {DBGroup} from "../Database/tables/Groups";
|
||||
import {DatabaseConnection} from "../Database/DatabaseConnection";
|
||||
import {CacheType, CacheTypeReducer, Guild, GuildMember, GuildMemberRoleManager} from "discord.js";
|
||||
import {GuildMember} from "discord.js";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {PlaydateRepository} from "./PlaydateRepository";
|
||||
import {Container} from "../Container/Container";
|
||||
|
|
@ -33,7 +33,7 @@ export class GroupRepository extends Repository<GroupModel, DBGroup> {
|
|||
}
|
||||
|
||||
public findGroupsByRoles(server: string, roleIds: string[]): GroupModel[] {
|
||||
const template = roleIds.map(roleId => '?').join(',');
|
||||
const template = roleIds.map(_roleId => '?').join(',');
|
||||
|
||||
const dbResult = this.database.fetchAll<number[], DBGroup>(`
|
||||
SELECT * FROM groups WHERE server = ? AND role IN (${template})
|
||||
|
|
@ -64,7 +64,6 @@ export class GroupRepository extends Repository<GroupModel, DBGroup> {
|
|||
public deleteGroup(group: GroupModel): void {
|
||||
this.delete(group.id);
|
||||
|
||||
debugger
|
||||
const repo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
const playdates = repo.findFromGroup(group, true)
|
||||
playdates.forEach((playdate) => {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export class PlaydateRepository extends Repository<PlaydateModel, DBPlaydate> {
|
|||
}
|
||||
|
||||
getNextPlaydateForGroup(group: GroupModel): PlaydateModel | null {
|
||||
let sql = `SELECT * FROM ${this.schema.name} WHERE groupid = ? AND time_from > ? ORDER BY time_from ASC LIMIT 1`;
|
||||
const sql = `SELECT * FROM ${this.schema.name} WHERE groupid = ? AND time_from > ? ORDER BY time_from LIMIT 1`;
|
||||
|
||||
const find = this.database.fetch<number, DBPlaydate>(
|
||||
sql,
|
||||
|
|
@ -78,14 +78,12 @@ export class PlaydateRepository extends Repository<PlaydateModel, DBPlaydate> {
|
|||
if (!intermediateModel) {
|
||||
throw new Error("Unable to convert the playdate model");
|
||||
}
|
||||
const result: PlaydateModel = {
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
group: fixedGroup ?? this.groupRepository.getById(intermediateModel.groupid),
|
||||
from_time: new Date(intermediateModel.time_from),
|
||||
to_time: new Date(intermediateModel.time_to),
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<PlaydateModel>): object {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {DatabaseConnection} from "../Database/DatabaseConnection";
|
|||
import {Model} from "../Models/Model";
|
||||
import { Nullable } from "../types/Nullable";
|
||||
import {DatabaseDefinition} from "../Database/DatabaseDefinition";
|
||||
import {debug} from "node:util";
|
||||
import {Container} from "../Container/Container";
|
||||
import {EventHandler} from "../Events/EventHandler";
|
||||
import {ElementCreatedEvent} from "../Events/ElementCreatedEvent";
|
||||
|
|
@ -62,7 +61,7 @@ export class Repository<ModelType extends Model, IntermediateModelType = unknown
|
|||
SET ${Object.keys(createObject).map((key) => `${key} = ?`).join(',')}
|
||||
WHERE id = ?`;
|
||||
const result = this.database.execute(sql, ...Object.values(createObject), instance.id);
|
||||
return result.lastInsertRowid;
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
public getById(id: number): Nullable<ModelType> {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export class ArrayUtils {
|
|||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
for (var i = 0; i < a.length; ++i) {
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Commands from "./Discord/Commands/Commands";
|
||||
import {Environment} from "./Environment";
|
||||
import {DatabaseConnection} from "./Database/DatabaseConnection";
|
||||
import {DatabaseUpdater} from "./Database/DatabaseUpdater";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import {DiscordClient} from "./Discord/DiscordClient";
|
||||
import {Environment} from "./Environment";
|
||||
import {Container} from "./Container/Container";
|
||||
import {DatabaseConnection} from "./Database/DatabaseConnection";
|
||||
import {ServiceHint, Services} from "./Container/Services";
|
||||
import {IconCache} from "./Icons/IconCache";
|
||||
import {DefaultEvents} from "./Events/DefaultEvents";
|
||||
|
|
|
|||