diff --git a/source/Database/tables/GroupConfiguration.ts b/source/Database/tables/GroupConfiguration.ts new file mode 100644 index 0000000..82e3be0 --- /dev/null +++ b/source/Database/tables/GroupConfiguration.ts @@ -0,0 +1,34 @@ +import {DatabaseDefinition} from "../DatabaseDefinition"; + +export type DBGroup = { + id: number; + groupid: number; + key: string; + value: string; +} + +const dbDefinition: DatabaseDefinition = { + name: "groups", + columns: [ + { + name: "id", + type: "INTEGER", + autoIncrement: true, + primaryKey: true, + }, + { + name: "groupid", + type: "VARCHAR(32)", + }, + { + name: "key", + type: "VARCHAR(32)", + }, + { + name: "value", + type: "VARCHAR(128)", + } + ] +} + +export default dbDefinition; \ No newline at end of file diff --git a/source/Discord/CommandPartials/GroupSelection.ts b/source/Discord/CommandPartials/GroupSelection.ts index 84f0a0c..b12bf42 100644 --- a/source/Discord/CommandPartials/GroupSelection.ts +++ b/source/Discord/CommandPartials/GroupSelection.ts @@ -2,7 +2,7 @@ import { AutocompleteInteraction, ChatInputCommandInteraction, CommandInteraction, - GuildMember, + GuildMember, SlashCommandIntegerOption, SlashCommandStringOption } from "discord.js"; import {Container} from "../../Container/Container"; @@ -11,32 +11,32 @@ import {GroupModel} from "../../Models/GroupModel"; import {UserError} from "../UserError"; export class GroupSelection { - public static createOptionSetup(): SlashCommandStringOption { - return new SlashCommandStringOption() + public static createOptionSetup(): SlashCommandIntegerOption { + return new SlashCommandIntegerOption() .setName("group") .setDescription("Defines the group you want to manage the playdates for") .setRequired(true) .setAutocomplete(true) } - public static async handleAutocomplete(interaction: AutocompleteInteraction): Promise { + public static async handleAutocomplete(interaction: AutocompleteInteraction, onlyLeaders: boolean = false): Promise { const value = interaction.options.getFocused(); const repo = Container.get(GroupRepository.name); - const groups = repo.findGroupsByMember(interaction.member); + let groups = repo.findGroupsByMember(interaction.member, onlyLeaders); await interaction.respond( groups .filter((group) => group.name.startsWith(value)) - .map((group) => ({name: group.name, value: group.name })) + .map((group) => ({name: group.name, value: group.id })) ) } public static getGroup(interaction: CommandInteraction): GroupModel { - const groupname = interaction.options.get("group"); + const groupname = interaction.options.get("group", true); if (!groupname) { throw new UserError("No group name provided"); } - const group = Container.get(GroupRepository.name).findGroupByName((groupname.value ?? '').toString()); + const group = Container.get(GroupRepository.name).getById((groupname.value ?? 0)); if (!group) { throw new UserError("No group found"); } diff --git a/source/Discord/Commands/Groups.ts b/source/Discord/Commands/Groups.ts index 809a35e..0ba6948 100644 --- a/source/Discord/Commands/Groups.ts +++ b/source/Discord/Commands/Groups.ts @@ -3,15 +3,25 @@ import { Interaction, CommandInteraction, ChatInputCommandInteraction, - MessageFlags, GuildMemberRoleManager, InteractionReplyOptions, GuildMember + MessageFlags, GuildMemberRoleManager, InteractionReplyOptions, GuildMember, EmbedBuilder, AutocompleteInteraction } from "discord.js"; -import {ChatInteractionCommand, Command} from "./Command"; +import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command"; import {GroupModel} from "../../Models/GroupModel"; import {GroupRepository} from "../../Repositories/GroupRepository"; import {DatabaseConnection} from "../../Database/DatabaseConnection"; import {Container} from "../../Container/Container"; +import {GroupSelection} from "../CommandPartials/GroupSelection"; +import {UserError} from "../UserError"; +import {ArrayUtils} from "../../Utilities/ArrayUtils"; -export class GroupCommand implements Command, ChatInteractionCommand { +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, good luck on your next adventures.', + ] + definition(): SlashCommandBuilder { // @ts-ignore return new SlashCommandBuilder() @@ -35,8 +45,17 @@ export class GroupCommand implements Command, ChatInteractionCommand { 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()) ); - } execute(interaction: ChatInputCommandInteraction): Promise { switch (interaction.options.getSubcommand()) { @@ -46,6 +65,11 @@ export class GroupCommand implements Command, ChatInteractionCommand { case "list": this.list(interaction); break; + case "remove": + this.remove(interaction); + break; + case "config": + this.runConfigurator(interaction); default: throw new Error("Unsupported command"); } @@ -79,22 +103,65 @@ export class GroupCommand implements Command, ChatInteractionCommand { const repo = Container.get(GroupRepository.name); const groups = repo.findGroupsByMember(interaction.member); + const embed = new EmbedBuilder() + .setTitle("Your groups on this server:") + .setFields( + groups.map(group => { + return { + name: group.name, + value: ` + Role: <@&${group.role.roleid}> + ` + } + }) + ) + const reply: InteractionReplyOptions = { embeds: [ - { - title: "Your groups on this server:", - - fields: groups.map((group) => { - return { - name: group.name, - value: "" - } - }) - } + embed ], + allowedMentions: { roles: [] }, flags: MessageFlags.Ephemeral } interaction.reply(reply); } + + private async remove(interaction: ChatInputCommandInteraction) { + const group = GroupSelection.getGroup(interaction); + + const repo = Container.get(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 { + const option = interaction.options.getFocused(true); + if (option.name == "group") { + await GroupSelection.handleAutocomplete(interaction, true); + return; + } + } + + private runConfigurator(interaction: ChatInputCommandInteraction) { + const group = GroupSelection.getGroup(interaction); + + + } } \ No newline at end of file diff --git a/source/Discord/Commands/Playdates.ts b/source/Discord/Commands/Playdates.ts index 2e7c5b5..0425a05 100644 --- a/source/Discord/Commands/Playdates.ts +++ b/source/Discord/Commands/Playdates.ts @@ -31,7 +31,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter .addSubcommand((subcommand) => subcommand .setName("create") .setDescription("Creates a new playdate") - .addStringOption(GroupSelection.createOptionSetup()) + .addIntegerOption(GroupSelection.createOptionSetup()) .addStringOption((option) => option .setName("from") .setDescription("Defines the start date & time. Format: YYYY-MM-DD HH:mm") @@ -48,12 +48,12 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter .addSubcommand((subcommand) => subcommand .setName("list") .setDescription("Lists all playdates") - .addStringOption(GroupSelection.createOptionSetup()) + .addIntegerOption(GroupSelection.createOptionSetup()) ) .addSubcommand((subcommand) => subcommand .setName("remove") .setDescription("Removes a playdate") - .addStringOption(GroupSelection.createOptionSetup()) + .addIntegerOption(GroupSelection.createOptionSetup()) .addIntegerOption((option) => option .setName("playdate") .setDescription("Selects a playdate") @@ -179,8 +179,6 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter throw new UserError("No playdate found"); } - console.log(selected, group); - if (selected.group?.id != group.id) { throw new UserError("No playdate found"); } diff --git a/source/Models/GroupConfigurationModel.ts b/source/Models/GroupConfigurationModel.ts new file mode 100644 index 0000000..46831d1 --- /dev/null +++ b/source/Models/GroupConfigurationModel.ts @@ -0,0 +1,8 @@ +import {Model} from "./Model"; +import {GroupModel} from "./GroupModel"; + +export interface GroupConfigurationModel extends Model { + group: GroupModel; + key: string; + value: string; +} \ No newline at end of file diff --git a/source/Repositories/GroupRepository.ts b/source/Repositories/GroupRepository.ts index 6b4c433..b5dd29d 100644 --- a/source/Repositories/GroupRepository.ts +++ b/source/Repositories/GroupRepository.ts @@ -4,6 +4,8 @@ import Groups, {DBGroup} from "../Database/tables/Groups"; import {DatabaseConnection} from "../Database/DatabaseConnection"; import {CacheType, CacheTypeReducer, Guild, GuildMember, GuildMemberRoleManager} from "discord.js"; import {Nullable} from "../types/Nullable"; +import {PlaydateRepository} from "./PlaydateRepository"; +import {Container} from "../Container/Container"; export class GroupRepository extends Repository { @@ -43,12 +45,31 @@ export class GroupRepository extends Repository { return dbResult.map((result) => this.convertToModelType(result)); } - public findGroupsByMember(member: GuildMember) { + public findGroupsByMember(member: GuildMember, onlyLeader: boolean = false) { if (!member) { throw new Error("Can't find member for guild: none given"); } - return this.findGroupsByRoles(member.guild.id, [...member.roles.cache.keys()]) + const groups = this.findGroupsByRoles(member.guild.id, [...member.roles.cache.keys()]) + + if (!onlyLeader) { + return groups; + } + + return groups.filter((group: GroupModel) => { + return group.leader.memberid === member.id; + }) + } + + public deleteGroup(group: GroupModel): void { + this.delete(group.id); + + debugger + const repo = Container.get(PlaydateRepository.name); + const playdates = repo.findFromGroup(group, true) + playdates.forEach((playdate) => { + repo.delete(playdate.id); + }) } protected convertToModelType(intermediateModel: DBGroup | undefined): GroupModel { diff --git a/source/Repositories/PlaydateRepository.ts b/source/Repositories/PlaydateRepository.ts index f0290fb..dd29a48 100644 --- a/source/Repositories/PlaydateRepository.ts +++ b/source/Repositories/PlaydateRepository.ts @@ -5,7 +5,6 @@ import {DatabaseConnection} from "../Database/DatabaseConnection"; import {GroupRepository} from "./GroupRepository"; import {GroupModel} from "../Models/GroupModel"; import {Nullable} from "../types/Nullable"; -import playdate from "../Database/tables/Playdate"; export class PlaydateRepository extends Repository { @@ -19,16 +18,22 @@ export class PlaydateRepository extends Repository { ); } - findFromGroup(group: GroupModel) { + findFromGroup(group: GroupModel, all = false) { + let sql = `SELECT * FROM ${this.schema.name} WHERE groupid = ?`; + const params = [group.id]; + + if (!all) { + sql += " AND time_from > ?" + params.push(new Date().getTime()) + } + const finds = this.database.fetchAll( - `SELECT * FROM ${this.schema.name} WHERE groupid = ? AND time_from > ?`, - group.id, - new Date().getTime() + sql, + ...params ); return finds.map((playdate) => this.convertToModelType(playdate, group)); } - protected convertToModelType(intermediateModel: DBPlaydate | undefined, fixedGroup: Nullable = null): PlaydateModel { if (!intermediateModel) { throw new Error("Unable to convert the playdate model"); diff --git a/source/Utilities/ArrayUtils.ts b/source/Utilities/ArrayUtils.ts new file mode 100644 index 0000000..d3aacbd --- /dev/null +++ b/source/Utilities/ArrayUtils.ts @@ -0,0 +1,6 @@ +export class ArrayUtils { + public static chooseRandom(array: Array):T { + const index = Math.floor(Math.random() * array.length); + return array[index]; + } +} \ No newline at end of file