Adds initial progress

This commit is contained in:
Michel Fedde 2025-03-28 23:19:54 +01:00
commit a0b668cb90
34 changed files with 2680 additions and 0 deletions

View file

@ -0,0 +1,46 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
CommandInteraction,
GuildMember,
SlashCommandStringOption
} from "discord.js";
import {Container} from "../../Container/Container";
import {GroupRepository} from "../../Repositories/GroupRepository";
import {GroupModel} from "../../Models/GroupModel";
import {UserError} from "../UserError";
export class GroupSelection {
public static createOptionSetup(): SlashCommandStringOption {
return new SlashCommandStringOption()
.setName("group")
.setDescription("Defines the group you want to manage the playdates for")
.setRequired(true)
.setAutocomplete(true)
}
public static async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
const value = interaction.options.getFocused();
const repo = Container.get<GroupRepository>(GroupRepository.name);
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
await interaction.respond(
groups
.filter((group) => group.name.startsWith(value))
.map((group) => ({name: group.name, value: group.name }))
)
}
public static getGroup(interaction: CommandInteraction): GroupModel {
const groupname = interaction.options.get("group");
if (!groupname) {
throw new UserError("No group name provided");
}
const group = Container.get<GroupRepository>(GroupRepository.name).findGroupByName((groupname.value ?? '').toString());
if (!group) {
throw new UserError("No group found");
}
return <GroupModel>group;
}
}

View file

@ -0,0 +1,17 @@
import {ChatInputCommandInteraction, Interaction, SlashCommandBuilder} from "discord.js";
import Commands from "./Commands";
export interface Command {
definition(): SlashCommandBuilder;
}
export interface ChatInteractionCommand {
execute(interaction: ChatInputCommandInteraction): Promise<void>;
}
export interface AutocompleteCommand {
handleAutocomplete(interaction: Interaction): Promise<void>;
}
export type CommandUnion =
Command | Partial<ChatInteractionCommand> | Partial<AutocompleteCommand>;

View file

@ -0,0 +1,41 @@
import {HelloWorldCommand} from "./HelloWorldCommand";
import {Command, CommandUnion} from "./Command";
import {GroupCommand} from "./Groups";
import {PlaydatesCommand} from "./Playdates";
const commands: Set<Command> = new Set<Command>([
new HelloWorldCommand(),
new GroupCommand(),
new PlaydatesCommand()
]);
export default class Commands {
private mappedCommands: Map<string, Command> = new Map<string, Command>();
constructor() {
this.mappedCommands = this.getMap();
}
public getCommand(commandName: string): CommandUnion|undefined {
if (!this.mappedCommands.has(commandName)) {
throw new Error(`Unknown command: ${commandName}`);
}
return this.mappedCommands.get(commandName);
}
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;
}
}

View file

@ -0,0 +1,100 @@
import {
SlashCommandBuilder,
Interaction,
CommandInteraction,
ChatInputCommandInteraction,
MessageFlags, GuildMemberRoleManager, InteractionReplyOptions, GuildMember
} from "discord.js";
import {ChatInteractionCommand, Command} from "./Command";
import {GroupModel} from "../../Models/GroupModel";
import {GroupRepository} from "../../Repositories/GroupRepository";
import {DatabaseConnection} from "../../Database/DatabaseConnection";
import {Container} from "../../Container/Container";
export class GroupCommand implements Command, ChatInteractionCommand {
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)
)
.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.")
);
}
execute(interaction: ChatInputCommandInteraction): Promise<void> {
switch (interaction.options.getSubcommand()) {
case "create":
this.create(interaction);
break;
case "list":
this.list(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");
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 list(interaction: ChatInputCommandInteraction) {
const repo = Container.get<GroupRepository>(GroupRepository.name);
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
const reply: InteractionReplyOptions = {
embeds: [
{
title: "Your groups on this server:",
fields: groups.map((group) => {
return {
name: group.name,
value: ""
}
})
}
],
flags: MessageFlags.Ephemeral
}
interaction.reply(reply);
}
}

View file

@ -0,0 +1,25 @@
import {SlashCommandBuilder, Interaction, 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 adventurerer 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();
}
}

View file

@ -0,0 +1,206 @@
import {
SlashCommandBuilder,
Interaction,
CommandInteraction,
AutocompleteInteraction,
GuildMember,
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, ModalSubmitFields
} 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 = [
]
definition(): SlashCommandBuilder {
// @ts-ignore
return new SlashCommandBuilder()
.setName("playdates")
.setDescription("Manage your playdates")
.addSubcommand((subcommand) => subcommand
.setName("create")
.setDescription("Creates a new playdate")
.addStringOption(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")
)
.addAttachmentOption((option) => option
.setName("calendar-entry")
.setDescription("Optional, you can upload a iCal file and the from and to-values are read from it.")
)
)
.addSubcommand((subcommand) => subcommand
.setName("list")
.setDescription("Lists all playdates")
.addStringOption(GroupSelection.createOptionSetup())
)
.addSubcommand((subcommand) => subcommand
.setName("remove")
.setDescription("Removes a playdate")
.addStringOption(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.");
}
const playdate: Partial<PlaydateModel> = {
group: group,
from_time: new Date(fromDate),
to_time: new Date(toDate),
}
const id = Container.get<PlaydateRepository>(PlaydateRepository.name).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.")
.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 groupname = interaction.options.getString("group")
const group = Container.get<GroupRepository>(GroupRepository.name).findGroupByName((groupname ?? '').toString());
if (!group) {
throw new UserError("No group found");
}
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
}
})
)
}
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: `${playdate.from_time.toLocaleString()} - ${playdate.to_time.toLocaleString()}`,
value: ``
}
})
)
.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");
}
console.log(selected, group);
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,
})
}
}

View file

@ -0,0 +1,120 @@
import {
Client,
GatewayIntentBits,
Events,
Interaction,
ChatInputCommandInteraction,
MessageFlags,
Activity,
ActivityType
} from "discord.js";
import Commands from "./Commands/Commands";
import {Container} from "../Container/Container";
import {Logger} from "log4js";
import {UserError} from "./UserError";
export class DiscordClient {
private readonly client: Client;
private commands: Commands;
public get Client (): Client {
return this.client;
}
constructor() {
this.client = new Client({
intents: [GatewayIntentBits.Guilds]
})
this.commands = new Commands();
}
applyEvents() {
this.client.once(Events.ClientReady, () => {
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.InteractionCreate, async (interaction: Interaction) => {
const command = this.commands.getCommand(interaction.commandName);
if (command === null) {
Container.get<Logger>("logger").error(`Could not find command for '${interaction.commandName}'`);
return;
}
const method = this.findCommandMethod(interaction);
if (!method) {
Container.get<Logger>("logger").error(`Could not find method for '${interaction.commandName}'`);
return;
}
await method();
})
}
connect(token: string) {
this.client.login(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: Error) {
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 (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,3 @@
export class UserError extends Error {
}