Compare commits

..

8 Commits

13 changed files with 211 additions and 446 deletions

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
### Node template ### Node template
# Configurations # Configurations
config.json config.json
.vscode
# Logs # Logs
logs logs
*.log *.log

View File

@ -1,4 +1,4 @@
# Contributing to LOC's Community Relations Alpha Edition System # Contributing to LOC's Community Relations Gamma Edition System
Thank you for considering contributing to this project! Your contributions are highly valued, and were excited to collaborate with you. Thank you for considering contributing to this project! Your contributions are highly valued, and were excited to collaborate with you.

View File

@ -1,9 +1,7 @@
# Community Relations Alpha Edition System - CRRA # Community Relations v2 Gamma Edition System - CRRA/G
[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](LICENSE) [![License](https://img.shields.io/badge/license-AGPLv3-blue.svg)](LICENSE)
A brief description of what your project does, what problem it solves, and why its useful.
--- ---
## Table of Contents ## Table of Contents
@ -88,4 +86,4 @@ __Library of Code Department of Engineering__ @:
- [Email](mailto:engineering@libraryofcode.org) - [Email](mailto:engineering@libraryofcode.org)
--- ---
Thank you for checking out CRRA! Thank you for checking out CRv2!

View File

@ -1,4 +1,4 @@
import { prop, getModelForClass } from "@typegoose/typegoose"; import { prop, getModelForClass, Ref } from "@typegoose/typegoose";
import Member from "./Member"; import Member from "./Member";
/* TODO /* TODO
@ -60,9 +60,9 @@ export default class Partner implements SharedMemberAttributes {
@prop({ required: true }) @prop({ required: true })
public title: PartnerTitle | "Partner" | undefined; public title: PartnerTitle | "Partner" | undefined;
@prop() @prop({ ref: () => Partner })
// //
public directReport: Partner | string | undefined; public directReport?: Ref<Partner> | string | undefined;
@prop() @prop()
// this field dictates if the partner is able to perform developer commands, such as "eval" // this field dictates if the partner is able to perform developer commands, such as "eval"

View File

@ -2,10 +2,11 @@ import DiscordInteractionCommand from "../../util/DiscordInteractionCommand";
import { ChatInputCommandInteraction } from "discord.js"; import { ChatInputCommandInteraction } from "discord.js";
import { inspect } from "util"; import { inspect } from "util";
import { discordBotToken } from "../../config.json"; import { discordBotToken } from "../../config.json";
import { PartnerModel } from "../../database/Partner";
export default class Eval extends DiscordInteractionCommand { export default class Eval extends DiscordInteractionCommand {
// This is a list of IDs that are allowed to use this command. // This is a list of IDs that are allowed to use this command.
private listOfAllowedIDs: string[]; private listOfAllowedIDs: string[] = [];
constructor() { constructor() {
super("eval", "Executes arbitrary JS code and returns the output."); super("eval", "Executes arbitrary JS code and returns the output.");
@ -25,9 +26,13 @@ export default class Eval extends DiscordInteractionCommand {
option.setName("depth").setDescription("The depth of the inspection.").setRequired(false) option.setName("depth").setDescription("The depth of the inspection.").setRequired(false)
); );
this.listOfAllowedIDs = [ // this checks against the database and adds all of the partners that are "allowed to perform dev commands"
"278620217221971968", // Matthew // doing the database check in the initialization prevents us from having to check the database every time this command is ran
]; PartnerModel.find({ canPerformDevCommands: true }).then((partners) => {
for (const partner of partners) {
this.listOfAllowedIDs.push(partner.discordID as string);
}
});
} }
public async execute(interaction: ChatInputCommandInteraction) { public async execute(interaction: ChatInputCommandInteraction) {

View File

@ -1,392 +1,13 @@
import { MemberModel } from "../../database/Member";
import Partner, {
PartnerModel,
PartnerCommissionType,
PartnerRoleType,
PartnerDepartment,
PartnerTitle,
} from "../../database/Partner";
import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; import DiscordInteractionCommand from "../../util/DiscordInteractionCommand";
import { import { ChatInputCommandInteraction } from "discord.js";
ChatInputCommandInteraction,
InteractionContextType,
PermissionFlagsBits,
} from "discord.js";
//TODO: ad email validation
//TODO: cover all partner model properties in cooresponding sub commands export default class Ping extends DiscordInteractionCommand {
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 PartnerCommand extends DiscordInteractionCommand {
constructor() { constructor() {
super("partner", "Manipulates partner information."); 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)
)
.addUserOption((option) =>
option.setName("direct-report").setDescription("their direct report.")
)
);
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)
)
.addUserOption((option) =>
option.setName("direct-report").setDescription("their direct report.")
)
.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<void> { public async execute(interaction: ChatInputCommandInteraction): Promise<void> {
const subcommandName = interaction.options.getSubcommand(true); if (interaction.options?.getSubcommand(true) === "add") {
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 directReport = interaction.options.getUser("direct-report", false);
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 directReportPartnerDocumentFromDb;
if (directReport) {
directReportPartnerDocumentFromDb = await PartnerModel.findOne({
discordID: directReport.id,
}).exec();
if (!directReportPartnerDocumentFromDb)
return interaction.reply({
content: `the specified directReport ${directReport.username} does not have an entry in partner database, please add them first them before assigning subordinates`,
ephemeral: false,
});
}
let newPartner = new PartnerModel({
discordID: partnerOption.id,
emailAddress: partnerOptionEmailAddress,
roleType: partnerOptionRoleType,
commissionType: partnerOptionCommisioComissionType,
department: partnerOptionDepartment,
title: partnerOptionTitle,
directReport:
directReport && directReportPartnerDocumentFromDb
? directReportPartnerDocumentFromDb._id
: null,
});
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);
let 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,
});
if (partner.directReport) await partner.populate("directReport");
if (partner.directReport && partner.directReport instanceof Partner) {
console.log(partner.directReport);
return interaction.reply({
content: `Raw entry \`\`\`\n${JSON.stringify(partner, null, 2)}\n\`\`\`\n\nDirect report: \`\`\`\n${JSON.stringify(partner.directReport, null, 2)}\n\`\`\``,
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 })
.populate("directReport")
.exec();
if (!partner)
return interaction.reply({
content: "The specified user does not have an entry.",
ephemeral: false,
});
if (
partner.directReport &&
partner.directReport instanceof Partner &&
interaction.user.id !== partner.directReport.discordID
)
return interaction.reply({
content:
"You're not authorized to delete this partner's information, only their direct report can.",
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 directReport = interaction.options.getUser("direct-report");
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 (
!directReport &&
!partnerOptionEmailAddress &&
!partnerOptionEmailAddress &&
!partnerOptionRoleType &&
!partnerOptionCommisioComissionType &&
!partnerOptionDepartment &&
!partnerOptionTitle
) {
return interaction.reply({
content: "You need to select atleast one option to update",
ephemeral: false,
});
}
let partner = await PartnerModel.findOne({ discordID: partnerOption.id }).exec();
if (!partner)
return interaction.reply({
content: "The specified partner does not have an entry.",
ephemeral: false,
});
if (partner.directReport) partner = await partner.populate("directReport");
console.log(partner.directReport);
if (
partner.directReport instanceof PartnerModel &&
interaction.user.id !== partner.directReport.discordID
)
return interaction.reply({
content:
"You're not authorized to update this partner's information, only their direct report can.",
ephemeral: false,
});
let directReportPartnerDocumentFromDb;
if (directReport) {
directReportPartnerDocumentFromDb = await PartnerModel.findOne({
discordID: directReport.id,
}).exec();
if (!directReportPartnerDocumentFromDb)
return interaction.reply({
content: `the specified directReport ${directReport.username} does not have an entry in partner database, please add them first them before assigning subordinates`,
ephemeral: false,
});
}
let updateObj = {
discordID: partnerOption.id,
emailAddress: partnerOptionEmailAddress,
roleType: partnerOptionRoleType,
commissionType: partnerOptionCommisioComissionType,
department: partnerOptionDepartment,
title: partnerOptionTitle,
directReport:
directReport && directReportPartnerDocumentFromDb
? directReportPartnerDocumentFromDb.id
: null,
};
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<T>(id: string, updateObj: Partial<T>) {
// 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 });
}
} }

View File

@ -0,0 +1,34 @@
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,
});
}
}

View File

@ -1,14 +1,15 @@
import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; import DiscordInteractionCommand from "../../util/DiscordInteractionCommand";
import { MemberModel } from "../../database/Member"; import { MemberAdditionalAcknowledgement, MemberModel } from "../../database/Member";
import Partner, { import Partner, {
PartnerCommissionType, PartnerCommissionType,
PartnerDepartment, PartnerDepartment,
PartnerModel, PartnerModel,
PartnerRoleType, PartnerRoleType,
} from "../../database/Partner"; } from "../../database/Partner";
import { ChatInputCommandInteraction, EmbedBuilder, GuildMember } from "discord.js"; import { ChatInputCommandInteraction, EmbedBuilder, GuildMember, Snowflake } from "discord.js";
import MemberUtil from "../../util/MemberUtil"; import MemberUtil from "../../util/MemberUtil";
import EmojiConfig from "../../util/EmojiConfig"; import EmojiConfig from "../../util/EmojiConfig";
import Formatters from "../../util/Formatters";
export default class Whois extends DiscordInteractionCommand { export default class Whois extends DiscordInteractionCommand {
constructor() { constructor() {
@ -36,7 +37,7 @@ export default class Whois extends DiscordInteractionCommand {
const embed = new EmbedBuilder(); const embed = new EmbedBuilder();
// if the role type is managerial, add a [k] to the end of the name // if the role type is managerial, add a [k] to the end of the name
// if the partner exists, set the iconURL to the organizational logo // if the partner exists, set the iconURL to the organizational logo
const formattedName = MemberUtil.formatName(guildMember, partner); const formattedName = Formatters.formatName(guildMember, partner);
embed.setAuthor({ name: formattedName.text, iconURL: formattedName.iconURL }); embed.setAuthor({ name: formattedName.text, iconURL: formattedName.iconURL });
// set the thumbnail to the user's avatar // set the thumbnail to the user's avatar
embed.setThumbnail(guildMember.user.displayAvatarURL()); embed.setThumbnail(guildMember.user.displayAvatarURL());
@ -79,16 +80,23 @@ export default class Whois extends DiscordInteractionCommand {
break; break;
} }
if (partner.directReport) { if (partner.directReport) {
if (partner.directReport instanceof Partner) { // fetch direct report object ref
embedDescription += `**Direct Report**: ${partner.directReport.title}\n`; await partner.populate("directReport");
// ensures that the population propagated correctly before adding to embed
if (partner.directReport instanceof PartnerModel) {
const directReportGuildMember = await guild?.members.fetch(
partner.directReport.discordID as Snowflake
);
// fetches GuildMember for the direct report
embedDescription += `**Direct Report**: ${directReportGuildMember ? Formatters.formatName(directReportGuildMember, partner.directReport).text + ", " : ""}${partner.directReport.title}\n`;
} }
} }
} }
embed.setColor(guildMember.displayColor); embed.setColor(guildMember.displayColor);
if (embedDescription?.length > 0) embed.setDescription(embedDescription); if (embedDescription?.length > 0)
embed.setDescription(`${embedDescription}\n\n<@${guildMember.id}>`);
// add status to embed // add status to embed
if (guildMember.presence?.status) { if (guildMember.presence?.status) {
// TODO: this currently doesn't work for some reason
switch (guildMember.presence.status) { switch (guildMember.presence.status) {
case "online": case "online":
embed.addFields({ name: "Status", value: "Online", inline: true }); embed.addFields({ name: "Status", value: "Online", inline: true });
@ -99,7 +107,7 @@ export default class Whois extends DiscordInteractionCommand {
case "dnd": case "dnd":
embed.addFields({ name: "Status", value: "Do Not Disturb", inline: true }); embed.addFields({ name: "Status", value: "Do Not Disturb", inline: true });
break; break;
case "offline" || "invisible": case "offline":
embed.addFields({ name: "Status", value: "Online", inline: true }); embed.addFields({ name: "Status", value: "Online", inline: true });
break; break;
default: default:
@ -107,9 +115,87 @@ export default class Whois extends DiscordInteractionCommand {
embed.addFields({ name: "Status", value: "", inline: true }); embed.addFields({ name: "Status", value: "", inline: true });
break; break;
} }
} else {
embed.addFields({ name: "Status", value: "Offline", inline: true });
}
// calculations for joined / created at
embed.addFields(
{
name: "Joined At",
value: guildMember.joinedTimestamp
? `<t:${Math.floor(guildMember.joinedTimestamp / 1000)}>`
: "Invalid Date",
inline: true,
},
{
name: "Created At",
value: guildMember.user.createdTimestamp
? `<t:${Math.floor(guildMember.user.createdTimestamp / 1000)}>`
: "Invalid Date",
inline: true,
}
);
// TODO: calculations for commscore
embed.addFields({ name: "CommScore™", value: "[PLACEHOLDER]", inline: false });
// role calculation (sorting roles by their position)
let roleString = "";
for (const role of guildMember.roles.valueOf().sort((a, b) => b.position - a.position)) {
roleString += `<@&${role[1].id}> `;
}
if (roleString) {
embed.addFields({ name: "Roles", value: roleString });
} else {
embed.addFields({ name: "Roles", value: "None" });
}
// listing permissions
const serializedPermissions = guildMember.permissions.serialize();
const permissionsArray: string[] = [];
// adding serialized string representation of permissions to array to use in embed field
if (serializedPermissions.Administrator) permissionsArray.push("Administrator");
if (serializedPermissions.ManageGuild) permissionsArray.push("Manage Guild");
if (serializedPermissions.ManageChannels) permissionsArray.push("Manage Channels");
if (serializedPermissions.ManageRoles) permissionsArray.push("Manage Roles");
if (serializedPermissions.ManageMessages) permissionsArray.push("Manage Messages");
if (serializedPermissions.ManageEvents) permissionsArray.push("Manage Events");
if (serializedPermissions.ManageNicknames) permissionsArray.push("Manage Nicknames");
if (serializedPermissions.ManageEmojisAndStickers) permissionsArray.push("Manage Emojis");
if (serializedPermissions.ManageWebhooks) permissionsArray.push("Manage Webhooks");
if (serializedPermissions.ModerateMembers) permissionsArray.push("Moderate Members");
if (serializedPermissions.BanMembers) permissionsArray.push("Ban Members");
if (serializedPermissions.KickMembers) permissionsArray.push("Kick Members");
if (serializedPermissions.DeafenMembers) permissionsArray.push("Deafen Members");
// setting key permissions embed field
if (permissionsArray?.length > 0) {
embed.addFields({ name: "Key Permissions", value: permissionsArray.join(", ") });
}
// determining acknowledgements: MemberAdditionalAcknowledgement || "Guild Owner", "Guild Admin", "Guild Manager", "Guild Moderator"
const acknowledgementsArray: MemberAdditionalAcknowledgement[] = [];
if (guildMember.id === guildMember.guild.ownerId) {
acknowledgementsArray.push("Guild Owner");
} else if (serializedPermissions.Administrator) {
acknowledgementsArray.push("Guild Admin");
} else if (serializedPermissions.ManageGuild) {
acknowledgementsArray.push("Guild Manager");
} else if (serializedPermissions.ModerateMembers || serializedPermissions.ManageMessages) {
acknowledgementsArray.push("Guild Moderator");
}
if (partner?.canPerformDevCommands) {
acknowledgementsArray.push("System Developer");
}
// adding acknowledgements to embed
if (acknowledgementsArray.length > 0) {
embed.addFields({ name: "Acknowledgements", value: acknowledgementsArray.join(", ") });
} }
embed.setFooter({ embed.setFooter({
text: `Discord ID: ${guildMember.id}${databaseMember ? `Internal ID: ${databaseMember?._id}` : ""}`, text: `Discord ID: ${guildMember.id}${databaseMember ? `| Internal ID: ${databaseMember?._id}` : ""}${partner ? ` | Partner ID: ${partner?.id}` : ""}`,
}); });
return await interaction.editReply({ embeds: [embed] }); return await interaction.editReply({ embeds: [embed] });

3
package-lock.json generated
View File

@ -1,10 +1,9 @@
{ {
"name": "crra", "name": "CRRA",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@typegoose/typegoose": "^12.2.0", "@typegoose/typegoose": "^12.2.0",
"discord.js": "^14.14.1", "discord.js": "^14.14.1",

View File

@ -1,4 +1,5 @@
{ {
"name": "crv2",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.13.0", "@eslint/js": "^9.13.0",
@ -18,6 +19,7 @@
}, },
"dependencies": { "dependencies": {
"@typegoose/typegoose": "^12.2.0", "@typegoose/typegoose": "^12.2.0",
"auth0": "^4.12.0",
"discord.js": "^14.14.1", "discord.js": "^14.14.1",
"mongoose": "^8.2.2", "mongoose": "^8.2.2",
"stripe": "^14.21.0", "stripe": "^14.21.0",

View File

@ -8,5 +8,5 @@ export default abstract class DiscordEvent {
this.client = client; this.client = client;
this.execute = this.execute.bind(this); this.execute = this.execute.bind(this);
} }
public abstract execute(...args: any[]): Error | Promise<void>; public abstract execute(...args: never[]): Error | Promise<void>;
} }

55
util/Formatters.ts Normal file
View File

@ -0,0 +1,55 @@
import { GuildMember, User } from "discord.js";
import Partner, { PartnerCommissionType, PartnerRoleType } from "../database/Partner";
import { FormatNameOptions } from "./MemberUtil";
export default class Formatters {
public static formatStandardDate(date: Date | string | number): string {
const resolvedDate = new Date(date);
if (!resolvedDate) return "";
const year = resolvedDate.getFullYear();
const month = String(resolvedDate.getMonth() + 1).padStart(2, "0");
const day = String(resolvedDate.getDate()).padStart(2, "0");
const hours = String(resolvedDate.getHours()).padStart(2, "0");
const minutes = String(resolvedDate.getMinutes()).padStart(2, "0");
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return `${year}-${month}-${day} @ ${hours}:${minutes} (${timeZone})`;
}
// TODO: comments and extended formatting
public static formatName(
target: GuildMember | User,
partner?: Partner | null
): FormatNameOptions {
console.debug(
`[MemberUtil] Formatting name for ${target.displayName} at url ${target instanceof GuildMember ? target.user.displayAvatarURL() : target.displayAvatarURL()}`
);
// if the role type is managerial, add a [k] to the end of the name
// if the partner exists, set the iconURL to the organizational logo
if (partner?.roleType == PartnerRoleType.MANAGERIAL) {
return {
text: `${target.displayName} [k]`,
iconURL: target.displayAvatarURL(),
};
} else if (partner?.commissionType == PartnerCommissionType.CONTRACTUAL) {
// if the commission type is contractual, add a [c] to the end of the name
return {
text: `${target.displayName} [c]`,
iconURL:
target instanceof GuildMember
? target.user.displayAvatarURL()
: target.displayAvatarURL(),
};
} else {
// otherwise, just set the author to the member's display name
return {
text: target.displayName,
iconURL:
target instanceof GuildMember
? target.user.displayAvatarURL()
: target.displayAvatarURL(),
};
}
}
}

View File

@ -8,13 +8,14 @@ import Partner, {
import Member, { MemberAdditionalAcknowledgement, MemberModel } from "../database/Member"; import Member, { MemberAdditionalAcknowledgement, MemberModel } from "../database/Member";
import { Client, GuildMember, User } from "discord.js"; import { Client, GuildMember, User } from "discord.js";
import { guildID } from "../config.json"; import { guildID } from "../config.json";
import { Ref } from "@typegoose/typegoose";
export interface PartnerOptions { export interface PartnerOptions {
roleType: PartnerRoleType; roleType: PartnerRoleType;
commissionType: PartnerCommissionType; commissionType: PartnerCommissionType;
department: PartnerDepartment; department: PartnerDepartment;
title: PartnerTitle; title: PartnerTitle;
directReport: Partner | string; directReport: Ref<Partner>;
} }
export interface FormatNameOptions { export interface FormatNameOptions {
@ -25,7 +26,7 @@ export interface FormatNameOptions {
// TODO: Add the rest of the remaining role configurations // TODO: Add the rest of the remaining role configurations
export const PartnerDiscordRoleMap = { export const PartnerDiscordRoleMap = {
// Director of Engineering, Management, Staff, Technician, Core Team, Play Caller // Director of Engineering, Management, Staff, Technician, Core Team, Play Caller
"Director of Engineering": [ Engineering: [
"1077646568091570236", "1077646568091570236",
"1077646956890951690", "1077646956890951690",
"446104438969466890", "446104438969466890",
@ -34,7 +35,7 @@ export const PartnerDiscordRoleMap = {
"1014978134573064293", "1014978134573064293",
], ],
// Director of Operations, Management, Staff, Moderator, Core Team, Play Caller // Director of Operations, Management, Staff, Moderator, Core Team, Play Caller
"Director of Operations": [ Operations: [
"1077647072163020840", "1077647072163020840",
"1077646956890951690", "1077646956890951690",
"446104438969466890", "446104438969466890",
@ -79,40 +80,4 @@ export default class MemberUtil {
{ $push: { additionalAcknowledgement: acknowledgement } } { $push: { additionalAcknowledgement: acknowledgement } }
); );
} }
// TODO: comments and extended formatting
public static formatName(
target: GuildMember | User,
partner?: Partner | null
): FormatNameOptions {
console.log(
`[MemberUtil] Formatting name for ${target.displayName} at url ${target instanceof GuildMember ? target.user.displayAvatarURL() : target.displayAvatarURL()}`
);
// if the role type is managerial, add a [k] to the end of the name
// if the partner exists, set the iconURL to the organizational logo
if (partner?.roleType == PartnerRoleType.MANAGERIAL) {
return {
text: `${target.displayName} [k]`,
iconURL: target.displayAvatarURL(),
};
} else if (partner?.commissionType == PartnerCommissionType.CONTRACTUAL) {
// if the commission type is contractual, add a [c] to the end of the name
return {
text: `${target.displayName} [c]`,
iconURL:
target instanceof GuildMember
? target.user.displayAvatarURL()
: target.displayAvatarURL(),
};
} else {
// otherwise, just set the author to the member's display name
return {
text: target.displayName,
iconURL:
target instanceof GuildMember
? target.user.displayAvatarURL()
: target.displayAvatarURL(),
};
}
}
} }