diff --git a/source/Discord/Commands/Playdates.ts b/source/Discord/Commands/Playdates.ts index ce855d8..d5bf60b 100644 --- a/source/Discord/Commands/Playdates.ts +++ b/source/Discord/Commands/Playdates.ts @@ -2,7 +2,15 @@ import { SlashCommandBuilder, CommandInteraction, AutocompleteInteraction, - EmbedBuilder, MessageFlags, ChatInputCommandInteraction, time, AttachmentBuilder, ActivityFlagsBitField, Options + EmbedBuilder, + MessageFlags, + ChatInputCommandInteraction, + time, + AttachmentBuilder, + ActivityFlagsBitField, + Options, + User, + GuildMember } from "discord.js"; import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command"; import {Container} from "../../Container/Container"; @@ -13,6 +21,10 @@ import {PlaydateRepository} from "../../Repositories/PlaydateRepository"; import {GroupModel} from "../../Models/GroupModel"; import * as ics from 'ics'; import ical from 'node-ical'; +import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler"; +import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository"; +import {privateDecrypt} from "node:crypto"; +import {GroupRepository} from "../../Repositories/GroupRepository"; export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand { definition(): SlashCommandBuilder { @@ -100,6 +112,13 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter } async create(interaction: CommandInteraction, group: GroupModel): Promise { + if (!this.interactionIsAllowedToManage(interaction, group)) { + throw new UserError( + "You are not allowed to create playdates for this group.", + "Ask your Game Master to add the playdate or ask him to allow everyone to do so." + ) + } + const fromDate = Date.parse(interaction.options.get("from")?.value ?? ''); const toDate = Date.parse(interaction.options.get("to")?.value ?? ''); @@ -199,6 +218,13 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter } private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise { + if (!this.interactionIsAllowedToManage(interaction, group)) { + throw new UserError( + "You are not allowed to delete playdates for this group.", + "Ask your Game Master to delete the playdate or ask him to allow everyone to do so." + ) + } + const playdateId = interaction.options.getInteger("playdate", true) const repo = Container.get(PlaydateRepository.name); @@ -231,6 +257,13 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter } private async import(interaction: ChatInputCommandInteraction, group: GroupModel): Promise { + if (!this.interactionIsAllowedToManage(interaction, group)) { + throw new UserError( + "You are not allowed to create playdates for this group.", + "Ask your Game Master to add the playdate or ask him to allow everyone to do so." + ) + } + const file = interaction.options.getAttachment('file', true); const mimeType = file.contentType?.split(';')[0]; if (mimeType !== "text/calendar") { @@ -281,8 +314,25 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter } private async export(interaction: ChatInputCommandInteraction, group: GroupModel): Promise { + const groupConfig = new GroupConfigurationHandler( + Container.get(GroupConfigurationRepository.name), + group + ).getConfiguration(); + const playdates = this.getExportTargets(interaction, group); + if (playdates.length < 1) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle("Export failed") + .setDescription(`:x: No playdates found under those filters.`) + ], + flags: MessageFlags.Ephemeral + }); + return + } + const result = ics.createEvents( playdates.map((playdate) => { return { @@ -292,7 +342,9 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter end: ics.convertTimestampToArray(playdate.to_time.getTime(), ''), endInputType: 'utc', endOutputType: 'utc', - title: `PnP with ${group.name}`, + title: groupConfig.calendar.title ?? `PnP with ${group.name}`, + description: groupConfig.calendar.description ?? undefined, + location: groupConfig.calendar.location ?? undefined, status: "CONFIRMED", busyStatus: "FREE", categories: ['PnP'] @@ -332,4 +384,22 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter return [playdate]; } + + private interactionIsAllowedToManage(interaction: ChatInputCommandInteraction, group: GroupModel): boolean { + const interactionMemberId = interaction.member?.user.id; + if (group.leader.memberid === interactionMemberId) { + return true; + } + + const groupRepo = Container.get(GroupRepository.name); + if (!groupRepo.isMemberInGroup(interaction.member, group)) { + return false; + } + + const config = new GroupConfigurationHandler( + Container.get(GroupConfigurationRepository.name), + group + ); + return config.getConfiguration().permissions.allowMemberManagingPlaydates; + } } \ No newline at end of file diff --git a/source/Discord/InteractionRouter.ts b/source/Discord/InteractionRouter.ts index 74b91a5..918284e 100644 --- a/source/Discord/InteractionRouter.ts +++ b/source/Discord/InteractionRouter.ts @@ -13,6 +13,7 @@ import {Container} from "../Container/Container"; import {EventHandler} from "../Events/EventHandler"; import {ModalInteractionEvent} from "../Events/EventClasses/ModalInteractionEvent"; import {ComponentInteractionEvent} from "../Events/EventClasses/ComponentInteractionEvent"; +import {log} from "node:util"; enum InteractionRoutingType { Unrouted, @@ -91,9 +92,9 @@ export class InteractionRouter { await command.execute?.call(command, interaction); } catch (e: any) { - this.logger.error(e) - + let userMessage = ":x: There was an error while executing this command!"; + let logErrorMessage = true; if (e.constructor.name === UserError.name) { userMessage = `:x: \`${e.message}\` - Please validate your request!` if (e.tryInstead) { @@ -102,7 +103,14 @@ export class InteractionRouter { You can try the following: ${inlineCode(e.tryInstead)}` } + + logErrorMessage = false; } + + if (logErrorMessage) { + this.logger.error(e) + } + if (interaction.replied || interaction.deferred) { await interaction.followUp({content: userMessage, flags: MessageFlags.Ephemeral}); } else { diff --git a/source/Groups/ConfigurationMenuHandler.ts b/source/Groups/ConfigurationMenuHandler.ts index 85fedf9..48b54f9 100644 --- a/source/Groups/ConfigurationMenuHandler.ts +++ b/source/Groups/ConfigurationMenuHandler.ts @@ -1,7 +1,7 @@ import { AnyMenuItem, FieldMenuItem, - FieldMenuItemContext, FieldMenuItemSaveValue, - MenuItemType, + FieldMenuItemContext, FieldMenuItemSaveValue, MenuItem, + MenuItemType, PromptMenuItem, RowBuilderFieldMenuItemContext } from "../Menu/MenuRenderer.types"; import {GroupConfigurationTransformers} from "./GroupConfigurationTransformers"; @@ -18,6 +18,7 @@ import { } from "discord.js"; import {ChannelId} from "../types/DiscordTypes"; import {MessageActionRowComponentBuilder} from "@discordjs/builders"; +import {Prompt} from "../Menu/Modals/Prompt"; export class ConfigurationMenuHandler { @@ -56,15 +57,6 @@ export class ConfigurationMenuHandler { } ] }, - { - traversalKey: "locale", - label: "Locale", - description: "Provides locale to be used for this group.", - type: MenuItemType.Field, - getCurrentValue: this.getLocaleValue.bind(this), - getActionRowBuilder: this.getLocaleMenuBuilder.bind(this), - setValue: this.setValue.bind(this) - }, { traversalKey: "permissions", label: "Permissions", @@ -88,20 +80,45 @@ export class ConfigurationMenuHandler { description: "Provides settings for the metadata contained in the playdate exports.", type: MenuItemType.Collection, children: [ - { + this.createStringMenuItem({ traversalKey: "title", label: "Title", description: "Defines how the calendar entry should be called.", - type: MenuItemType.Prompt, - getCurrentValue: this.getStringValue.bind(this), - getActionRowBuilder: this.getStringBuilder.bind(this), - setValue: this.setValue.bind(this) - } + }), + this.createTextareaMenuItem({ + traversalKey: "description", + label: "Description", + description: "Sets the description for the calendar entry.", + }), + this.createStringMenuItem({ + traversalKey: "location", + label: "Location", + description: "Sets the location where the calendar should point to." + }), ] }, ] } + private createStringMenuItem(metadata: MenuItem): PromptMenuItem { + return { + ...metadata, + type: MenuItemType.Prompt, + getCurrentValue: this.getStringValue.bind(this), + getActionRowBuilder: this.getStringBuilder.bind(this), + setValue: this.setValue.bind(this) + }; + } + private createTextareaMenuItem(metadata: MenuItem): PromptMenuItem { + return { + ...metadata, + type: MenuItemType.Prompt, + getCurrentValue: this.getStringValue.bind(this), + getActionRowBuilder: this.getTextareaBuilder.bind(this), + setValue: this.setValue.bind(this) + }; + } + private getChannelValue(context: FieldMenuItemContext): string { const value = this.configuration.getConfigurationByPath(context.path.join('.')); if (value === undefined) { @@ -193,6 +210,11 @@ export class ConfigurationMenuHandler { return new TextInputBuilder() .setStyle(TextInputStyle.Short) } + private getTextareaBuilder(context: FieldMenuItemContext): TextInputBuilder { + return new TextInputBuilder() + .setStyle(TextInputStyle.Paragraph) + } + private setValue(value: FieldMenuItemSaveValue[]|string, context: FieldMenuItemContext): void { const savedValue = typeof value !== 'string' ? diff --git a/source/Groups/GroupConfigurationHandler.ts b/source/Groups/GroupConfigurationHandler.ts index ae7efac..a2f37bd 100644 --- a/source/Groups/GroupConfigurationHandler.ts +++ b/source/Groups/GroupConfigurationHandler.ts @@ -12,12 +12,13 @@ import {isPlainObject} from "is-plain-object"; export class GroupConfigurationHandler { static DEFAULT_CONFIGURATION: RuntimeGroupConfiguration = { channels: null, - locale: new Intl.Locale('en-GB'), permissions: { allowMemberManagingPlaydates: false }, calendar: { - title: null + title: null, + description: null, + location: null } } diff --git a/source/Groups/GroupConfigurationTransformers.ts b/source/Groups/GroupConfigurationTransformers.ts index e2017f8..c856165 100644 --- a/source/Groups/GroupConfigurationTransformers.ts +++ b/source/Groups/GroupConfigurationTransformers.ts @@ -16,7 +16,7 @@ type GroupConfigurationTransformer = { } export type GroupConfigurationResult = - ChannelId | Intl.Locale | boolean | string + ChannelId | Intl.Locale | boolean | string | null export class GroupConfigurationTransformers { static TRANSFORMERS: GroupConfigurationTransformer[] = [ @@ -28,10 +28,6 @@ export class GroupConfigurationTransformers { path: ['channels', 'playdateReminders'], type: TransformerType.Channel, }, - { - path: ['locale'], - type: TransformerType.Locale, - }, { path: ['permissions', 'allowMemberManagingPlaydates'], type: TransformerType.PermissionBoolean @@ -39,6 +35,14 @@ export class GroupConfigurationTransformers { { path: ['calendar', 'title'], type: TransformerType.String + }, + { + path: ['calendar', 'description'], + type: TransformerType.String, + }, + { + path: ['calendar', 'location'], + type: TransformerType.String } ]; diff --git a/source/Groups/RuntimeGroupConfiguration.d.ts b/source/Groups/RuntimeGroupConfiguration.d.ts index 2909dde..edd611f 100644 --- a/source/Groups/RuntimeGroupConfiguration.d.ts +++ b/source/Groups/RuntimeGroupConfiguration.d.ts @@ -3,7 +3,6 @@ import {Nullable} from "../types/Nullable"; export type RuntimeGroupConfiguration = { channels: Nullable, - locale: Intl.Locale, permissions: PermissionRuntimeGroupConfiguration, calendar: CalendarRuntimeGroupConfiguration }; @@ -18,5 +17,7 @@ export type PermissionRuntimeGroupConfiguration = { } export type CalendarRuntimeGroupConfiguration = { - title: null|string + title: null|string, + description: null|string, + location: null|string } \ No newline at end of file diff --git a/source/Repositories/GroupRepository.ts b/source/Repositories/GroupRepository.ts index 770c50d..bd2db3a 100644 --- a/source/Repositories/GroupRepository.ts +++ b/source/Repositories/GroupRepository.ts @@ -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 {GuildMember} from "discord.js"; +import {GuildMember, UserFlagsBitField} from "discord.js"; import {Nullable} from "../types/Nullable"; import {PlaydateRepository} from "./PlaydateRepository"; import {Container} from "../Container/Container"; @@ -16,7 +16,6 @@ export class GroupRepository extends Repository { database, Groups ); - } public findGroupByName(name: string): Nullable { @@ -60,6 +59,19 @@ export class GroupRepository extends Repository { return group.leader.memberid === member.id; }) } + + public isMemberInGroup(member: Nullable, group: GroupModel): boolean + { + if (!member) { + throw new Error("Can't find member for guild: none given"); + } + + const groups = this.findGroupsByRoles(member.guild.id, [...member.roles.cache.keys()]) + + return groups.some((dbGroup) => { + return group.id === dbGroup.id; + }) + } public deleteGroup(group: GroupModel): void { this.delete(group.id);