411 lines
No EOL
14 KiB
TypeScript
411 lines
No EOL
14 KiB
TypeScript
import {
|
|
AttachmentBuilder,
|
|
AutocompleteInteraction,
|
|
ChatInputCommandInteraction,
|
|
CommandInteraction,
|
|
EmbedBuilder,
|
|
GuildMember,
|
|
MessageFlags,
|
|
SlashCommandBuilder,
|
|
time
|
|
} from "discord.js";
|
|
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
|
|
import {Container} from "../../Container/Container";
|
|
import {GroupSelection} from "../CommandPartials/GroupSelection";
|
|
import {UserError} from "../UserError";
|
|
import {PlaydateModel} from "../../Database/Models/PlaydateModel";
|
|
import {PlaydateRepository} from "../../Database/Repositories/PlaydateRepository";
|
|
import {GroupModel} from "../../Database/Models/GroupModel";
|
|
import * as ics from 'ics';
|
|
import ical from 'node-ical';
|
|
import {GroupConfigurationRepository} from "../../Database/Repositories/GroupConfigurationRepository";
|
|
import {GroupRepository} from "../../Database/Repositories/GroupRepository";
|
|
import {
|
|
GroupConfigurationProvider,
|
|
RuntimeGroupConfiguration
|
|
} from "../../Configuration/Groups/GroupConfigurationProvider";
|
|
import {ConfigurationHandler} from "../../Configuration/ConfigurationHandler";
|
|
import {PermissionError} from "../PermissionError";
|
|
import {EmbedLibrary, EmbedType} from "../EmbedLibrary";
|
|
import {GroupConfigurationModel} from "../../Database/Models/GroupConfigurationModel";
|
|
import parser from "any-date-parser";
|
|
|
|
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
|
|
definition(): SlashCommandBuilder {
|
|
// @ts-expect-error Command builder is improperly marked as incomplete.
|
|
return new SlashCommandBuilder()
|
|
.setName("playdates")
|
|
.setDescription("Manage your playdates")
|
|
.addSubcommand((subcommand) => subcommand
|
|
.setName("create")
|
|
.setDescription("Creates a new playdate")
|
|
.addIntegerOption(GroupSelection.createOptionSetup())
|
|
.addStringOption((option) => option
|
|
.setName("from")
|
|
.setDescription("Defines the start date & time. Your desired format is probably support.")
|
|
)
|
|
.addStringOption((option) => option
|
|
.setName("to")
|
|
.setDescription("Defines the end date & time. Your desired format is probably support.")
|
|
)
|
|
)
|
|
.addSubcommand((subcommand) => subcommand
|
|
.setName("list")
|
|
.setDescription("Lists all playdates")
|
|
.addIntegerOption(GroupSelection.createOptionSetup())
|
|
)
|
|
.addSubcommand((subcommand) => subcommand
|
|
.setName("remove")
|
|
.setDescription("Removes a playdate")
|
|
.addIntegerOption(GroupSelection.createOptionSetup())
|
|
.addIntegerOption((option) => option
|
|
.setName("playdate")
|
|
.setDescription("Selects a playdate")
|
|
.setRequired(true)
|
|
.setAutocomplete(true)
|
|
)
|
|
)
|
|
.addSubcommand(subcommand => subcommand
|
|
.setName('import')
|
|
.setDescription("Imports playdates from iCal files")
|
|
.addIntegerOption(GroupSelection.createOptionSetup())
|
|
.addAttachmentOption(attachment => attachment
|
|
.setName("file")
|
|
.setDescription("The iCal File to import")
|
|
.setRequired(true)
|
|
)
|
|
)
|
|
.addSubcommand((subcommand) => subcommand
|
|
.setName("export")
|
|
.setDescription("Exports a playdate as iCal file.")
|
|
.addIntegerOption(GroupSelection.createOptionSetup())
|
|
.addIntegerOption((option) => option
|
|
.setName("playdate")
|
|
.setDescription("Selects a playdate to export")
|
|
.setAutocomplete(true)
|
|
)
|
|
.addBooleanOption(option => option
|
|
.setName("future-dates")
|
|
.setDescription("Exports the next playdates as a ical file")
|
|
)
|
|
);
|
|
}
|
|
|
|
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
const group = GroupSelection.getGroup(interaction);
|
|
|
|
switch (interaction.options.getSubcommand()) {
|
|
case "create":
|
|
await this.create(interaction, group);
|
|
break;
|
|
case "remove":
|
|
await this.delete(interaction, group);
|
|
break;
|
|
case "list":
|
|
await this.list(interaction, group);
|
|
break;
|
|
case "import":
|
|
await this.import(interaction, group);
|
|
break;
|
|
case "export":
|
|
await this.export(interaction, group);
|
|
break;
|
|
default:
|
|
throw new UserError("This subcommand is not yet implemented.");
|
|
}
|
|
}
|
|
|
|
async create(interaction: CommandInteraction, group: GroupModel): Promise<void> {
|
|
if (!this.interactionIsAllowedToManage(<ChatInputCommandInteraction>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 = parser.fromString(<string>interaction.options.get("from")?.value ?? '');
|
|
const toDate = parser.fromString(<string>interaction.options.get("to")?.value ?? '');
|
|
|
|
if (!fromDate.isValid()) {
|
|
throw new UserError("No date or invalid date format for the from parameter.");
|
|
}
|
|
|
|
if (!fromDate.isValid()) {
|
|
throw new UserError("No date or invalid date format for the to parameter.");
|
|
}
|
|
|
|
if (fromDate.getTime() > toDate.getTime()) {
|
|
throw new UserError("The to-date can't be earlier than the from-date");
|
|
}
|
|
|
|
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
|
|
|
const collidingTimes = playdateRepo.findPlaydatesInRange(fromDate, toDate, group);
|
|
if (collidingTimes.length > 0) {
|
|
throw new UserError("The playdate collides with another playdate. Please either remove the old one or choose a different time.")
|
|
}
|
|
|
|
const playdate: Partial<PlaydateModel> = {
|
|
group: group,
|
|
from_time: fromDate,
|
|
to_time: toDate,
|
|
}
|
|
|
|
playdateRepo.create(playdate);
|
|
|
|
const embed = EmbedLibrary.playdate(
|
|
group,
|
|
"Created a play-date.",
|
|
":white_check_mark: Your playdate has been created! You and your group get notified, when its time.",
|
|
EmbedType.Success
|
|
).setFields({
|
|
name: "Created playdate",
|
|
value: `${time(new Date(fromDate), 'F')} - ${time(new Date(toDate), 'F')}`,
|
|
})
|
|
|
|
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);
|
|
return;
|
|
}
|
|
|
|
if (option.name != 'playdate') {
|
|
return;
|
|
}
|
|
|
|
const group = GroupSelection.getGroup(interaction);
|
|
|
|
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
|
await interaction.respond(
|
|
playdates.map(playdate => {
|
|
return {
|
|
name: `${playdate.from_time.toLocaleString()} - ${playdate.to_time.toLocaleString()}`,
|
|
value: <number>playdate.id
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
private async list(interaction: ChatInputCommandInteraction, group: GroupModel) {
|
|
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
|
|
|
const embed = EmbedLibrary.playdate(
|
|
group,
|
|
"Created a play-date.",
|
|
null,
|
|
EmbedType.Info
|
|
).setFields(
|
|
playdates.map((playdate) => {
|
|
return {
|
|
name: `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`,
|
|
value: `${time(playdate.from_time, 'R')}`
|
|
}
|
|
})
|
|
)
|
|
|
|
await interaction.reply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
flags: MessageFlags.Ephemeral,
|
|
})
|
|
}
|
|
|
|
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
|
if (!this.interactionIsAllowedToManage(<ChatInputCommandInteraction>interaction, group)) {
|
|
throw new PermissionError(
|
|
"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>(PlaydateRepository.name);
|
|
const selected = repo.getById(playdateId);
|
|
if (!selected) {
|
|
throw new UserError("No playdate found");
|
|
}
|
|
|
|
if (selected.group?.id != group.id) {
|
|
throw new UserError("No playdate found");
|
|
}
|
|
|
|
repo.delete(playdateId);
|
|
|
|
const embed = EmbedLibrary.playdate(
|
|
group,
|
|
"Playdate deleted",
|
|
`:x: Deleted ${time(selected.from_time, 'F')} - ${time(selected.to_time, 'F')}`,
|
|
EmbedType.Success
|
|
);
|
|
await interaction.reply({
|
|
embeds: [
|
|
embed
|
|
],
|
|
flags: MessageFlags.Ephemeral,
|
|
})
|
|
}
|
|
|
|
private async import(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
|
if (!this.interactionIsAllowedToManage(<ChatInputCommandInteraction>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") {
|
|
throw new UserError(`Invalid ical file. Got: ${mimeType}`, "Providing a valid iCal file");
|
|
}
|
|
|
|
await interaction.deferReply({
|
|
flags: MessageFlags.Ephemeral
|
|
});
|
|
|
|
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
|
|
|
const icalFile = await ical.async.fromURL(file.url);
|
|
const playdates: PlaydateModel[] = [];
|
|
for (const event of Object.values(icalFile)) {
|
|
if (event.type !== 'VEVENT') {
|
|
continue;
|
|
}
|
|
|
|
const playdate: Partial<PlaydateModel> = {
|
|
group: group,
|
|
from_time: event.start,
|
|
to_time: event.end
|
|
}
|
|
|
|
const id = playdateRepo.create(playdate);
|
|
playdates.push(<PlaydateModel>{
|
|
id,
|
|
...playdate
|
|
});
|
|
}
|
|
|
|
const embed = EmbedLibrary.playdate(
|
|
group,
|
|
"Imported play-dates",
|
|
`:white_check_mark: Your ${playdates.length} playdates has been created! You and your group get notified, when its time.`,
|
|
EmbedType.Success
|
|
).setFields({
|
|
name: "Created playdates",
|
|
value: playdates.map((playdate) => `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`).join('\n')
|
|
})
|
|
|
|
interaction.followUp({
|
|
embeds: [embed],
|
|
flags: MessageFlags.Ephemeral
|
|
})
|
|
}
|
|
|
|
private async export(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
|
const groupConfig = new ConfigurationHandler<
|
|
GroupConfigurationModel,
|
|
RuntimeGroupConfiguration
|
|
>(
|
|
new GroupConfigurationProvider(
|
|
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
|
|
group
|
|
)
|
|
).getCompleteConfiguration();
|
|
|
|
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 {
|
|
start: ics.convertTimestampToArray(playdate.from_time.getTime(), ''),
|
|
startInputType: 'utc',
|
|
startOutputType: 'utc',
|
|
end: ics.convertTimestampToArray(playdate.to_time.getTime(), ''),
|
|
endInputType: 'utc',
|
|
endOutputType: 'utc',
|
|
title: groupConfig.calendar.title ?? `PnP with ${group.name}`,
|
|
description: groupConfig.calendar.description ?? undefined,
|
|
location: groupConfig.calendar.location ?? undefined,
|
|
status: "CONFIRMED",
|
|
busyStatus: "FREE",
|
|
categories: ['PnP']
|
|
}
|
|
})
|
|
);
|
|
if (!result.value) {
|
|
throw new Error("Failed creating ics file...")
|
|
}
|
|
const attachment = new AttachmentBuilder(Buffer.from(result.value), {
|
|
name: "ICSExport.ics",
|
|
description: "Your export :)"
|
|
});
|
|
interaction.reply({
|
|
files: [attachment],
|
|
flags: MessageFlags.Ephemeral
|
|
})
|
|
}
|
|
|
|
private getExportTargets(interaction: ChatInputCommandInteraction, group: GroupModel): PlaydateModel[] {
|
|
|
|
const repo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
|
const nextPlaydates = interaction.options.getBoolean("future-dates") ?? false;
|
|
if (nextPlaydates) {
|
|
return repo.findPlaydatesInRange(new Date());
|
|
}
|
|
|
|
const playdateId = interaction.options.getInteger('playdate');
|
|
if (!playdateId) {
|
|
throw new UserError("Nothing to export", "Please specify what you want to export.");
|
|
}
|
|
|
|
const playdate = repo.getById(playdateId);
|
|
if (!playdate) {
|
|
throw new UserError("Specified playdate id is invalid");
|
|
}
|
|
|
|
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>(GroupRepository.name);
|
|
if (!groupRepo.isMemberInGroup(<GuildMember>interaction.member, group)) {
|
|
return false;
|
|
}
|
|
|
|
const config = new ConfigurationHandler(
|
|
new GroupConfigurationProvider(
|
|
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
|
|
group
|
|
)
|
|
);
|
|
return config.getConfigurationByPath("permissions.allowMemberManagingPlaydates").value === true;
|
|
}
|
|
} |