Adds eslint and linted & improved routing for interactions

This commit is contained in:
Michel Fedde 2025-06-17 20:37:53 +02:00
parent 83209f642c
commit 441715675c
35 changed files with 2091 additions and 463 deletions

15
eslint.config.mjs Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View 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

View 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

View file

@ -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

Before After
Before After

View file

@ -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

View 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

View file

@ -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

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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}.`);
}
}

View file

@ -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))

View file

@ -1,5 +1,4 @@
import {ChatInputCommandInteraction, Interaction, SlashCommandBuilder} from "discord.js";
import Commands from "./Commands";
export interface Command {
definition(): SlashCommandBuilder;

View file

@ -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,
})
}
}

View file

@ -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?',
]

View file

@ -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.")

View file

@ -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;
}
}

View 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);
}
}
}

View file

@ -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
) {
}
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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)
)
)

View file

@ -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";

View file

@ -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 ]

View file

@ -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);
}
)

View file

@ -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) => {

View file

@ -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 {

View file

@ -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> {

View file

@ -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;

View file

@ -1,4 +1,3 @@
import Commands from "./Discord/Commands/Commands";
import {Environment} from "./Environment";
import {DatabaseConnection} from "./Database/DatabaseConnection";
import {DatabaseUpdater} from "./Database/DatabaseUpdater";

View file

@ -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";