diff --git a/.gitignore b/.gitignore index bb53d7a..5a82afb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ ### Node template # Configurations config.json - +.vscode # Logs logs *.log diff --git a/discord/commands/Partner.ts b/discord/commands/Partner.ts index fc4dd36..0f43f8e 100644 --- a/discord/commands/Partner.ts +++ b/discord/commands/Partner.ts @@ -1,13 +1,311 @@ +import { MemberModel } from "../../database/Member"; +import { + PartnerModel, + PartnerCommissionType, + PartnerRoleType, + PartnerDepartment, + PartnerTitle, +} from "../../database/Partner"; import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; -import { ChatInputCommandInteraction } from "discord.js"; +import { + ChatInputCommandInteraction, + InteractionContextType, + PermissionFlagsBits, +} from "discord.js"; +//TODO: ad email validation -export default class Ping extends DiscordInteractionCommand { +//TODO: cover all partner model properties in cooresponding sub commands +const partnerTitles: PartnerTitle[] = [ + "Director of Engineering", + "Director of Operations", + "Deputy Director of Engineering", + "Deputy Director of Operations", + "Services Manager", + "Project Manager", + "Engineering Core Partner", + "Operations Core Partner", + "Community Moderator", + "Technician", +]; + +export default class Partner extends DiscordInteractionCommand { constructor() { super("partner", "Manipulates partner information."); + this.builder + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setContexts(InteractionContextType.Guild); + this.builder.addSubcommand((c) => + c + .setName("add") + .setDescription("test") + .addUserOption((option) => + option.setName("partner").setDescription("the partner you want to add.").setRequired(true) + ) + .addStringOption((option) => + option.setName("email").setDescription("their email address.").setRequired(true) + ) + .addStringOption((option) => + option + .setName("role-type") + .setDescription("their roleType.") + .setChoices(this.formatOptionsForDiscordFromEnum(PartnerRoleType)) + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("commission-type") + .setDescription("their commissionType.") + .setChoices(this.formatOptionsForDiscordFromEnum(PartnerCommissionType)) + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("title") + .setDescription("their title.") + .setChoices(this.formatPartnerTitlesArrayForDiscord()) + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("department") + .setDescription("their department.") + .setChoices(this.formatOptionsForDiscordFromEnum(PartnerDepartment)) + .setRequired(true) + ) + ); + this.builder.addSubcommand((c) => + c + .setName("get") + .setDescription("get") + .addUserOption((option) => + option + .setName("partner") + .setDescription("the partner you want to get info abouts.") + .setRequired(true) + ) + ); + this.builder.addSubcommand((c) => + c + .setName("delete") + .setDescription("delete") + .addUserOption((option) => + option + .setName("partner") + .setDescription("the partner you want to delete.") + .setRequired(true) + ) + ); + this.builder.addSubcommand((c) => + c + .setName("update") + .setDescription("update") + .addUserOption((option) => + option + .setName("partner") + .setDescription("the partner you want to update.") + .setRequired(true) + ) + .addStringOption((option) => option.setName("email").setDescription("their email address.")) + .addStringOption((option) => + option + .setName("role-type") + .setDescription("their roleType.") + .setChoices(this.formatOptionsForDiscordFromEnum(PartnerRoleType)) + ) + .addStringOption((option) => + option + .setName("commission-type") + .setDescription("their commissionType.") + .setChoices(this.formatOptionsForDiscordFromEnum(PartnerCommissionType)) + ) + .addStringOption((option) => + option + .setName("title") + .setDescription("their title.") + .setChoices(this.formatPartnerTitlesArrayForDiscord()) + ) + .addStringOption((option) => + option + .setName("department") + .setDescription("their department.") + .setChoices(this.formatOptionsForDiscordFromEnum(PartnerDepartment)) + ) + ); } public async execute(interaction: ChatInputCommandInteraction): Promise { - if (interaction.options?.getSubcommand(true) === "add") { + const subcommandName = interaction.options.getSubcommand(true); + switch (subcommandName) { + case "get": + await this.handleGetSubcommand(interaction); + break; + + case "add": + await this.handleAddSubcommand(interaction); + break; + + case "delete": + await this.handleDeleteSubcommand(interaction); + break; + + case "update": + await this.handleUpdateSubcommand(interaction); + break; + default: + //discord does not allow parent/main commands to be excutable, this range is limited to options entered via commandbuiler + break; } } + + async handleAddSubcommand(interaction: ChatInputCommandInteraction) { + const partnerOption = interaction.options.getUser("partner", true); + const partnerOptionEmailAddress = interaction.options.getString("email", true); + const partnerOptionRoleType = interaction.options.getString("role-type", true); + const partnerOptionCommisioComissionType = interaction.options.getString( + "commission-type", + true + ); + const partnerOptionDepartment = interaction.options.getString("department", true); + const partnerOptionTitle = interaction.options.getString("title", true); + + const partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec(); + if (partner) + return interaction.reply({ + content: "The specified user already has a partner entry.", + ephemeral: false, + }); + /* + const member = await MemberModel.findOne({ discordID: partnerOption.id }).exec(); + if (!member) + return interaction.reply({ + content: "The specified partner does not have a base member entry.", + ephemeral: false, + }); + */ + let newPartner = new PartnerModel({ + discordID: partnerOption.id, + emailAddress: partnerOptionEmailAddress, + roleType: partnerOptionRoleType, + commissionType: partnerOptionCommisioComissionType, + department: partnerOptionDepartment, + title: partnerOptionTitle, + }); + + await newPartner.save(); + return interaction.reply({ + content: `\`\`\`\n${JSON.stringify(newPartner, null, 2)}\n\`\`\``, + ephemeral: false, + }); + } + + async handleGetSubcommand(interaction: ChatInputCommandInteraction) { + const partnerOption = interaction.options.getUser("partner", true); + const partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec(); + if (!partner) + return interaction.reply({ + content: "The specified partner does not an entry in the database.", + ephemeral: false, + }); + + return interaction.reply({ + content: `Raw entry \`\`\`\n${JSON.stringify(partner, null, 2)}\n\`\`\``, + ephemeral: false, + }); + } + + async handleDeleteSubcommand(interaction: ChatInputCommandInteraction) { + const partnerOption = interaction.options.getUser("partner", true); + const partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec(); + if (!partner) + return interaction.reply({ + content: "The specified user does not have an entry.", + ephemeral: false, + }); + + await PartnerModel.findByIdAndDelete(partner.id); + return interaction.reply({ + content: `removed partner entry from the database.`, + ephemeral: false, + }); + } + + async handleUpdateSubcommand(interaction: ChatInputCommandInteraction) { + const partnerOption = interaction.options.getUser("partner", true); + const partnerOptionEmailAddress = interaction.options.getString("email"); + const partnerOptionRoleType = interaction.options.getString("role-type"); + const partnerOptionCommisioComissionType = interaction.options.getString("commission-type"); + const partnerOptionDepartment = interaction.options.getString("department"); + const partnerOptionTitle = interaction.options.getString("title"); + if ( + !partnerOptionEmailAddress && + !partnerOptionEmailAddress && + !partnerOptionRoleType && + !partnerOptionCommisioComissionType && + !partnerOptionDepartment && + !partnerOptionTitle + ) { + return interaction.reply({ + content: "You need to select atleast one option to update", + ephemeral: false, + }); + } + + const partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec(); + if (!partner) + return interaction.reply({ + content: "The specified partner does not have an entry.", + ephemeral: false, + }); + let updateObj = { + discordID: partnerOption.id, + emailAddress: partnerOptionEmailAddress, + roleType: partnerOptionRoleType, + commissionType: partnerOptionCommisioComissionType, + department: partnerOptionDepartment, + title: partnerOptionTitle, + }; + + try { + let updatedPartner = await this.updatePartnerInfo(partner.id, updateObj); + return interaction.reply({ + content: `updated partner!\n\n\`\`\`\n${JSON.stringify(updatedPartner, null, 2)}\n\`\`\``, + ephemeral: false, + }); + } catch (error) { + return interaction.reply({ + content: `an error occured !\n\n\`\`\`\n${JSON.stringify(error, null, 2)}\n\`\`\``, + ephemeral: false, + }); + } + } + + private formatPartnerTitlesArrayForDiscord(): { name: PartnerTitle; value: string }[] { + return partnerTitles.map((title) => ({ + name: title, + value: title, + })); + } + private formatOptionsForDiscordFromEnum(args: any): { name: string; value: string }[] { + return Object.entries(args) + .filter(([key, value]) => typeof value === "number") // Filter out reverse mappings + .map(([key, value]) => ({ + name: key, + value: (value as number).toString(), + })); + } + private async updatePartnerInfo(id: string, updateObj: Partial) { + // Remove keys with falsy values (undefined, null, etc.) + const filteredUpdate = Object.fromEntries( + Object.entries(updateObj).filter(([key, value]) => key !== "discordID" && value) + ); + + if (Object.keys(filteredUpdate).length === 0) { + throw new Error( + "Error in Partner update command, no options specified on update, you can safely ignore this error." + ); + } + + // Find and update the document by ID with only the valid fields + return await PartnerModel.findByIdAndUpdate(id, filteredUpdate, { new: true }); + } } diff --git a/discord/commands/PartnerAdd.ts b/discord/commands/PartnerAdd.ts deleted file mode 100644 index 8ec9927..0000000 --- a/discord/commands/PartnerAdd.ts +++ /dev/null @@ -1,34 +0,0 @@ -import DiscordInteractionCommand, { - DiscordInteractionCommandSkeleton, -} from "../../util/DiscordInteractionCommand"; -import { guildID } from "../../config.json"; -import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; -import { MemberModel } from "../../database/Member"; -import { PartnerModel } from "../../database/Partner"; - -export default class PartnerAdd implements DiscordInteractionCommandSkeleton { - public GUILD_ID: string; - public name: string; - public description: string; - public builder: SlashCommandBuilder; - constructor() { - this.name = "partner"; - this.description = "Creates a new partner entry."; - this.builder = new SlashCommandBuilder(); - this.GUILD_ID = guildID; - } - - public async execute(interaction: ChatInputCommandInteraction) { - const member = MemberModel.findOne({ discordID: interaction.user.id }); - if (!member) - return interaction.reply({ - content: "The specified partner does not have a base member entry.", - ephemeral: true, - }); - if (!(await PartnerModel.findOne({ discordID: interaction.user.id }))) - return interaction.reply({ - content: "The specified partner already has a partner entry.", - ephemeral: true, - }); - } -} diff --git a/package-lock.json b/package-lock.json index 407e07e..61097ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,10 @@ { - "name": "CRRA", + "name": "crra", "lockfileVersion": 3, "requires": true, "packages": { "": { + "license": "AGPL-3.0-or-later", "dependencies": { "@typegoose/typegoose": "^12.2.0", "discord.js": "^14.14.1",