Adds ICS
This commit is contained in:
parent
441715675c
commit
a79898b2e9
48 changed files with 2062 additions and 1503 deletions
|
|
@ -1,36 +1,28 @@
|
|||
import {DiscordClient} from "./DiscordClient";
|
||||
import {Logger} from "log4js";
|
||||
import {Routes, Snowflake} from "discord.js";
|
||||
import {REST, Routes, Snowflake} from "discord.js";
|
||||
import Commands from "./Commands/Commands";
|
||||
|
||||
export class CommandDeployer {
|
||||
constructor(
|
||||
private readonly client: DiscordClient,
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
}
|
||||
|
||||
public async deployAvailableServers() {
|
||||
const commandInfos: object[] = [];
|
||||
this.client.Commands.allCommands.forEach((command) => {
|
||||
commandInfos.push(command.definition().toJSON())
|
||||
})
|
||||
|
||||
const guilds = await this.client.RESTClient.get(Routes.userGuilds());
|
||||
const deployments = guilds.map(guild => {
|
||||
return this.deployServer(commandInfos, guild.id)
|
||||
})
|
||||
|
||||
await Promise.all(deployments);
|
||||
}
|
||||
|
||||
private async deployServer(commandInfos: object[], serverId: Snowflake) {
|
||||
this.logger.log(`Started refreshing ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
constructor(
|
||||
private readonly applicationId: string,
|
||||
private readonly restClient: REST,
|
||||
private readonly commands: Commands,
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
}
|
||||
|
||||
// The put method is used to fully refresh all commands in the guild with the current set
|
||||
await this.client.RESTClient.put(
|
||||
Routes.applicationGuildCommands(this.client.ApplicationId, serverId),
|
||||
{ body: commandInfos },
|
||||
);
|
||||
this.logger.log(`Successfully reloaded ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
}
|
||||
public async deployServer(serverId: Snowflake) {
|
||||
const commandInfos = this.commands.getJsonDefinitions();
|
||||
|
||||
this.logger.debug(`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
|
||||
await this.restClient.put(
|
||||
Routes.applicationGuildCommands(this.applicationId, serverId),
|
||||
{body: commandInfos},
|
||||
);
|
||||
|
||||
this.logger.debug(`Successfully reloaded ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
AutocompleteInteraction,
|
||||
CommandInteraction,
|
||||
GuildMember, SlashCommandIntegerOption,
|
||||
AutocompleteInteraction,
|
||||
CommandInteraction,
|
||||
GuildMember, SlashCommandIntegerOption,
|
||||
} from "discord.js";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupRepository} from "../../Repositories/GroupRepository";
|
||||
|
|
@ -9,36 +9,36 @@ import {GroupModel} from "../../Models/GroupModel";
|
|||
import {UserError} from "../UserError";
|
||||
|
||||
export class GroupSelection {
|
||||
public static createOptionSetup(): SlashCommandIntegerOption {
|
||||
return new SlashCommandIntegerOption()
|
||||
.setName("group")
|
||||
.setDescription("Defines the group this action is for")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
public static createOptionSetup(): SlashCommandIntegerOption {
|
||||
return new SlashCommandIntegerOption()
|
||||
.setName("group")
|
||||
.setDescription("Defines the group this action is for")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
}
|
||||
|
||||
public static async handleAutocomplete(interaction: AutocompleteInteraction, onlyLeaders: boolean = false): Promise<void> {
|
||||
const value = interaction.options.getFocused();
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
|
||||
await interaction.respond(
|
||||
groups
|
||||
.filter((group) => group.name.startsWith(value))
|
||||
.map((group) => ({name: group.name, value: group.id}))
|
||||
)
|
||||
}
|
||||
|
||||
public static getGroup(interaction: CommandInteraction | AutocompleteInteraction): GroupModel {
|
||||
const groupname = interaction.options.get("group", true);
|
||||
if (!groupname) {
|
||||
throw new UserError("No group name provided");
|
||||
}
|
||||
|
||||
public static async handleAutocomplete(interaction: AutocompleteInteraction, onlyLeaders: boolean = false): Promise<void> {
|
||||
const value = interaction.options.getFocused();
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
|
||||
await interaction.respond(
|
||||
groups
|
||||
.filter((group) => group.name.startsWith(value))
|
||||
.map((group) => ({name: group.name, value: group.id }))
|
||||
)
|
||||
}
|
||||
|
||||
public static getGroup(interaction: CommandInteraction|AutocompleteInteraction): GroupModel {
|
||||
const groupname = interaction.options.get("group", true);
|
||||
if (!groupname) {
|
||||
throw new UserError("No group name provided");
|
||||
}
|
||||
|
||||
const group = Container.get<GroupRepository>(GroupRepository.name).getById(<number>(groupname.value ?? 0));
|
||||
if (!group) {
|
||||
throw new UserError("No group found");
|
||||
}
|
||||
|
||||
return <GroupModel>group;
|
||||
|
||||
const group = Container.get<GroupRepository>(GroupRepository.name).getById(<number>(groupname.value ?? 0));
|
||||
if (!group) {
|
||||
throw new UserError("No group found");
|
||||
}
|
||||
|
||||
return <GroupModel>group;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
import {ChatInputCommandInteraction, Interaction, SlashCommandBuilder} from "discord.js";
|
||||
|
||||
export interface Command {
|
||||
definition(): SlashCommandBuilder;
|
||||
definition(): SlashCommandBuilder;
|
||||
}
|
||||
|
||||
export interface ChatInteractionCommand {
|
||||
execute(interaction: ChatInputCommandInteraction): Promise<void>;
|
||||
execute(interaction: ChatInputCommandInteraction): Promise<void>;
|
||||
}
|
||||
|
||||
export interface AutocompleteCommand {
|
||||
handleAutocomplete(interaction: Interaction): Promise<void>;
|
||||
handleAutocomplete(interaction: Interaction): Promise<void>;
|
||||
}
|
||||
|
||||
export type CommandUnion =
|
||||
Command | Partial<ChatInteractionCommand> | Partial<AutocompleteCommand>;
|
||||
export type CommandUnion =
|
||||
Command | Partial<ChatInteractionCommand> | Partial<AutocompleteCommand>;
|
||||
|
|
@ -2,40 +2,54 @@ import {HelloWorldCommand} from "./HelloWorldCommand";
|
|||
import {Command, CommandUnion} from "./Command";
|
||||
import {GroupCommand} from "./Groups";
|
||||
import {PlaydatesCommand} from "./Playdates";
|
||||
import {RESTPostAPIChatInputApplicationCommandsJSONBody} from "discord.js";
|
||||
import {Nullable} from "../../types/Nullable";
|
||||
|
||||
const commands: Set<Command> = new Set<Command>([
|
||||
new HelloWorldCommand(),
|
||||
new GroupCommand(),
|
||||
new PlaydatesCommand()
|
||||
new HelloWorldCommand(),
|
||||
new GroupCommand(),
|
||||
new PlaydatesCommand()
|
||||
]);
|
||||
|
||||
export default class Commands {
|
||||
private mappedCommands: Map<string, Command> = new Map<string, Command>();
|
||||
|
||||
constructor() {
|
||||
this.mappedCommands = this.getMap();
|
||||
private mappedCommands: Map<string, Command> = new Map<string, Command>();
|
||||
private definitions: Nullable<RESTPostAPIChatInputApplicationCommandsJSONBody[]>;
|
||||
|
||||
constructor() {
|
||||
this.mappedCommands = this.getMap();
|
||||
}
|
||||
|
||||
public getCommand(commandName: string): CommandUnion | undefined {
|
||||
if (!this.mappedCommands.has(commandName)) {
|
||||
throw new Error(`Unknown command: ${commandName}`);
|
||||
}
|
||||
|
||||
public getCommand(commandName: string): CommandUnion|undefined {
|
||||
if (!this.mappedCommands.has(commandName)) {
|
||||
throw new Error(`Unknown command: ${commandName}`);
|
||||
}
|
||||
|
||||
return this.mappedCommands.get(commandName);
|
||||
|
||||
return this.mappedCommands.get(commandName);
|
||||
}
|
||||
|
||||
public get allCommands(): Set<Command> {
|
||||
return commands;
|
||||
}
|
||||
|
||||
public getJsonDefinitions(): RESTPostAPIChatInputApplicationCommandsJSONBody[] {
|
||||
if (this.definitions) {
|
||||
return this.definitions;
|
||||
}
|
||||
|
||||
public get allCommands(): Set<Command> {
|
||||
return commands;
|
||||
}
|
||||
|
||||
private getMap(): Map<string, Command>
|
||||
{
|
||||
const map = new Map<string, Command>();
|
||||
for (const command of commands) {
|
||||
const definition = command.definition()
|
||||
map.set(definition.name, command);
|
||||
}
|
||||
|
||||
return map;
|
||||
|
||||
this.definitions = [];
|
||||
commands.forEach((command) => {
|
||||
this.definitions?.push(command.definition().toJSON())
|
||||
})
|
||||
return this.definitions;
|
||||
}
|
||||
|
||||
private getMap(): Map<string, Command> {
|
||||
const map = new Map<string, Command>();
|
||||
for (const command of commands) {
|
||||
const definition = command.definition()
|
||||
map.set(definition.name, command);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
SlashCommandBuilder,
|
||||
ChatInputCommandInteraction,
|
||||
MessageFlags,
|
||||
InteractionReplyOptions,
|
||||
GuildMember,
|
||||
EmbedBuilder,
|
||||
AutocompleteInteraction,
|
||||
roleMention, time, userMention, GuildMemberRoleManager
|
||||
MessageFlags,
|
||||
InteractionReplyOptions,
|
||||
GuildMember,
|
||||
EmbedBuilder,
|
||||
AutocompleteInteraction,
|
||||
roleMention, time, userMention, GuildMemberRoleManager
|
||||
} from "discord.js";
|
||||
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
|
||||
import {GroupModel} from "../../Models/GroupModel";
|
||||
|
|
@ -142,10 +142,10 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
|
||||
Container.get<GroupRepository>(GroupRepository.name).create(group);
|
||||
|
||||
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral })
|
||||
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral})
|
||||
}
|
||||
|
||||
private validateGroupName(name: string): true|string{
|
||||
private validateGroupName(name: string): true | string {
|
||||
const lowercaseName = name.toLowerCase();
|
||||
for (const invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
|
||||
if (!lowercaseName.includes(invalidcharactersequence)) {
|
||||
|
|
@ -162,7 +162,7 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
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(
|
||||
|
|
@ -191,7 +191,7 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
embeds: [
|
||||
embed
|
||||
],
|
||||
allowedMentions: { roles: [] },
|
||||
allowedMentions: {roles: []},
|
||||
flags: MessageFlags.Ephemeral
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,24 @@ import {SlashCommandBuilder, CommandInteraction} from "discord.js";
|
|||
import {Command} from "./Command";
|
||||
|
||||
export class HelloWorldCommand implements Command {
|
||||
private static RESPONSES: string[] = [
|
||||
'Hello :)',
|
||||
'zzzZ... ZzzzZ... huh? I am awake. I am awake!',
|
||||
'Roll for initiative!',
|
||||
'I was an adventurer like you...',
|
||||
'Hello :) How are you?',
|
||||
]
|
||||
|
||||
definition(): SlashCommandBuilder
|
||||
{
|
||||
return new SlashCommandBuilder()
|
||||
.setName("hello")
|
||||
.setDescription("Displays a random response. (commonly used to test if the bot is online)")
|
||||
}
|
||||
async execute(interaction: CommandInteraction): Promise<void> {
|
||||
const random = Math.floor(Math.random() * HelloWorldCommand.RESPONSES.length);
|
||||
|
||||
await interaction.reply(HelloWorldCommand.RESPONSES[random]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
private static RESPONSES: string[] = [
|
||||
'Hello :)',
|
||||
'zzzZ... ZzzzZ... huh? I am awake. I am awake!',
|
||||
'Roll for initiative!',
|
||||
'I was an adventurer like you...',
|
||||
'Hello :) How are you?',
|
||||
]
|
||||
|
||||
definition(): SlashCommandBuilder {
|
||||
return new SlashCommandBuilder()
|
||||
.setName("hello")
|
||||
.setDescription("Displays a random response. (commonly used to test if the bot is online)")
|
||||
}
|
||||
|
||||
async execute(interaction: CommandInteraction): Promise<void> {
|
||||
const random = Math.floor(Math.random() * HelloWorldCommand.RESPONSES.length);
|
||||
|
||||
await interaction.reply(HelloWorldCommand.RESPONSES[random]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
SlashCommandBuilder,
|
||||
CommandInteraction,
|
||||
AutocompleteInteraction,
|
||||
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, time
|
||||
SlashCommandBuilder,
|
||||
CommandInteraction,
|
||||
AutocompleteInteraction,
|
||||
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, time, AttachmentBuilder, ActivityFlagsBitField, Options
|
||||
} from "discord.js";
|
||||
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
|
||||
import {Container} from "../../Container/Container";
|
||||
|
|
@ -11,195 +11,325 @@ import {UserError} from "../UserError";
|
|||
import {PlaydateModel} from "../../Models/PlaydateModel";
|
||||
import {PlaydateRepository} from "../../Repositories/PlaydateRepository";
|
||||
import {GroupModel} from "../../Models/GroupModel";
|
||||
import * as ics from 'ics';
|
||||
import ical from 'node-ical';
|
||||
|
||||
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
|
||||
static REGEX = [
|
||||
|
||||
]
|
||||
|
||||
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. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
.addStringOption((option) => option
|
||||
.setName("to")
|
||||
.setDescription("Defines the end date & time. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
)
|
||||
.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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
default:
|
||||
throw new UserError("This subcommand is not yet implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
async create(interaction: CommandInteraction, group: GroupModel): Promise<void> {
|
||||
const fromDate = Date.parse(<string>interaction.options.get("from")?.value ?? '');
|
||||
const toDate = Date.parse(<string>interaction.options.get("to")?.value ?? '');
|
||||
|
||||
if (isNaN(fromDate)) {
|
||||
throw new UserError("No date or invalid date format for the from parameter.");
|
||||
}
|
||||
|
||||
if (isNaN(toDate)) {
|
||||
throw new UserError("No date or invalid date format for the to parameter.");
|
||||
}
|
||||
|
||||
if (fromDate > toDate) {
|
||||
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: new Date(fromDate),
|
||||
to_time: new Date(toDate),
|
||||
}
|
||||
|
||||
playdateRepo.create(playdate);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Created a play-date.")
|
||||
.setDescription(":white_check_mark: Your playdate has been created! You and your group get notified, when its time.")
|
||||
.setFields({
|
||||
name: "Created playdate",
|
||||
value: `${time(new Date(fromDate),'F')} - ${time(new Date(toDate), 'F')}`,
|
||||
})
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
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: playdate.id
|
||||
}
|
||||
})
|
||||
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. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
.addStringOption((option) => option
|
||||
.setName("to")
|
||||
.setDescription("Defines the end date & time. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
)
|
||||
.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> {
|
||||
const fromDate = Date.parse(<string>interaction.options.get("from")?.value ?? '');
|
||||
const toDate = Date.parse(<string>interaction.options.get("to")?.value ?? '');
|
||||
|
||||
if (isNaN(fromDate)) {
|
||||
throw new UserError("No date or invalid date format for the from parameter.");
|
||||
}
|
||||
|
||||
private async list(interaction: ChatInputCommandInteraction, group: GroupModel) {
|
||||
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
||||
if (isNaN(toDate)) {
|
||||
throw new UserError("No date or invalid date format for the to parameter.");
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("The next playdates:")
|
||||
.setFields(
|
||||
playdates.map((playdate) =>
|
||||
{
|
||||
return {
|
||||
name: `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`,
|
||||
value: `${time(playdate.from_time, 'R')}`
|
||||
}
|
||||
})
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
if (fromDate > toDate) {
|
||||
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: new Date(fromDate),
|
||||
to_time: new Date(toDate),
|
||||
}
|
||||
|
||||
playdateRepo.create(playdate);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Created a play-date.")
|
||||
.setDescription(":white_check_mark: Your playdate has been created! You and your group get notified, when its time.")
|
||||
.setFields({
|
||||
name: "Created playdate",
|
||||
value: `${time(new Date(fromDate), 'F')} - ${time(new Date(toDate), 'F')}`,
|
||||
})
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
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 = new EmbedBuilder()
|
||||
.setTitle("The next playdates:")
|
||||
.setFields(
|
||||
playdates.map((playdate) => {
|
||||
return {
|
||||
name: `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`,
|
||||
value: `${time(playdate.from_time, 'R')}`
|
||||
}
|
||||
})
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
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 = new EmbedBuilder()
|
||||
.setTitle("Playdate deleted")
|
||||
.setDescription(
|
||||
`:x: Deleted \`${selected.from_time.toLocaleString()} - ${selected.to_time.toLocaleString()}\``
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
private async import(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
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");
|
||||
}
|
||||
|
||||
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
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 = new EmbedBuilder()
|
||||
.setTitle("Playdate deleted")
|
||||
.setDescription(
|
||||
`:x: Deleted \`${selected.from_time.toLocaleString()} - ${selected.to_time.toLocaleString()}\``
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
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 = new EmbedBuilder()
|
||||
.setTitle("Imported play-dates.")
|
||||
.setDescription(`:white_check_mark: Your ${playdates.length} playdates has been created! You and your group get notified, when its time.`)
|
||||
.setFields({
|
||||
name: "Created playdates",
|
||||
value: playdates.map((playdate) => `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`).join('\n')
|
||||
})
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
interaction.followUp({
|
||||
embeds: [embed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
})
|
||||
}
|
||||
|
||||
private async export(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
const playdates = this.getExportTargets(interaction, group);
|
||||
|
||||
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: `PnP with ${group.name}`,
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +1,77 @@
|
|||
import {
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
Events,
|
||||
ActivityType, REST
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
Events,
|
||||
ActivityType, REST
|
||||
} from "discord.js";
|
||||
import Commands from "./Commands/Commands";
|
||||
import {Container} from "../Container/Container";
|
||||
import {Logger} from "log4js";
|
||||
import {InteractionRouter} from "./InteractionRouter";
|
||||
import {CommandDeployer} from "./CommandDeployer";
|
||||
|
||||
export class DiscordClient {
|
||||
private readonly client: Client;
|
||||
private readonly client: Client;
|
||||
|
||||
public get Client(): Client {
|
||||
return this.client;
|
||||
}
|
||||
public get Client(): Client {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public get Commands(): Commands {
|
||||
return this.router.commands
|
||||
}
|
||||
public get Commands(): Commands {
|
||||
return this.router.commands
|
||||
}
|
||||
|
||||
public get RESTClient(): REST {
|
||||
return this.restClient;
|
||||
}
|
||||
public get RESTClient(): REST {
|
||||
return this.restClient;
|
||||
}
|
||||
|
||||
public get ApplicationId(): string {
|
||||
return this.applicationId;
|
||||
}
|
||||
public get ApplicationId(): string {
|
||||
return this.applicationId;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly applicationId: string,
|
||||
private readonly router: InteractionRouter,
|
||||
private readonly restClient: REST = new REST()
|
||||
) {
|
||||
this.client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds]
|
||||
})
|
||||
}
|
||||
constructor(
|
||||
private readonly applicationId: string,
|
||||
private readonly router: InteractionRouter,
|
||||
private readonly deployer: CommandDeployer,
|
||||
private readonly logger: Logger,
|
||||
private readonly restClient: REST = new REST()
|
||||
) {
|
||||
this.client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds]
|
||||
})
|
||||
}
|
||||
|
||||
applyEvents() {
|
||||
this.client.once(Events.ClientReady, () => {
|
||||
if (!this.client.user) {
|
||||
return;
|
||||
}
|
||||
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.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.GuildCreate, (guild) => {
|
||||
this.logger.info(`Joined ${guild.name}`);
|
||||
this.deployer.deployServer(guild.id);
|
||||
})
|
||||
this.client.on(Events.GuildDelete, (guild) => {
|
||||
this.logger.info(`Left ${guild.name}`);
|
||||
})
|
||||
this.client.on(Events.GuildAvailable, (guild) => {
|
||||
this.deployer.deployServer(guild.id);
|
||||
})
|
||||
|
||||
this.client.on(Events.InteractionCreate, this.router.route.bind(this.router));
|
||||
}
|
||||
this.client.on(Events.InteractionCreate, this.router.route.bind(this.router));
|
||||
}
|
||||
|
||||
connect(token: string) {
|
||||
this.client.login(token);
|
||||
}
|
||||
connect(token: string) {
|
||||
this.client.login(token);
|
||||
}
|
||||
|
||||
connectRESTClient(token: string) {
|
||||
this.restClient.setToken(token);
|
||||
}
|
||||
connectRESTClient(token: string) {
|
||||
this.restClient.setToken(token);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,20 +50,20 @@ export class InteractionRouter {
|
|||
|
||||
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)
|
||||
|
|
@ -79,13 +79,13 @@ ${inlineCode(e.tryInstead)}`
|
|||
}
|
||||
}
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
await interaction.followUp({content: userMessage, flags: MessageFlags.Ephemeral});
|
||||
} else {
|
||||
await interaction.reply({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
await interaction.reply({content: userMessage, flags: MessageFlags.Ephemeral});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async handleAutocomplete(interaction: AutocompleteInteraction) {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
|
||||
|
|
@ -104,6 +104,6 @@ ${inlineCode(e.tryInstead)}`
|
|||
} catch (e: unknown) {
|
||||
Container.get<Logger>('logger').error(e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
export class UserError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly tryInstead: string|null = null
|
||||
) {
|
||||
super(message);
|
||||
|
||||
}
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly tryInstead: string | null = null
|
||||
) {
|
||||
super(message);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue