diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 305c1f4..532c414 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 we’re excited to collaborate with you. diff --git a/README.md b/README.md index 2da88fd..612ebef 100644 --- a/README.md +++ b/README.md @@ -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) -A brief description of what your project does, what problem it solves, and why it’s useful. - --- ## Table of Contents @@ -88,4 +86,4 @@ __Library of Code Department of Engineering__ @: - [Email](mailto:engineering@libraryofcode.org) --- -Thank you for checking out CRRA! +Thank you for checking out CRv2! diff --git a/database/Partner.ts b/database/Partner.ts index f5022f7..d84a9f0 100644 --- a/database/Partner.ts +++ b/database/Partner.ts @@ -1,4 +1,4 @@ -import { prop, getModelForClass } from "@typegoose/typegoose"; +import { prop, getModelForClass, Ref } from "@typegoose/typegoose"; import Member from "./Member"; /* TODO @@ -60,9 +60,9 @@ export default class Partner implements SharedMemberAttributes { @prop({ required: true }) public title: PartnerTitle | "Partner" | undefined; - @prop() + @prop({ ref: () => Partner }) // - public directReport: Partner | string | undefined; + public directReport?: Ref | string | undefined; @prop() // this field dictates if the partner is able to perform developer commands, such as "eval" diff --git a/discord/commands/Eval.ts b/discord/commands/Eval.ts index d3ea6fb..0a8453d 100644 --- a/discord/commands/Eval.ts +++ b/discord/commands/Eval.ts @@ -2,10 +2,11 @@ import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; import { ChatInputCommandInteraction } from "discord.js"; import { inspect } from "util"; import { discordBotToken } from "../../config.json"; +import { PartnerModel } from "../../database/Partner"; export default class Eval extends DiscordInteractionCommand { // This is a list of IDs that are allowed to use this command. - private listOfAllowedIDs: string[]; + private listOfAllowedIDs: string[] = []; constructor() { 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) ); - this.listOfAllowedIDs = [ - "278620217221971968", // Matthew - ]; + // this checks against the database and adds all of the partners that are "allowed to perform dev commands" + // 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) { diff --git a/discord/commands/Whois.ts b/discord/commands/Whois.ts index 6ff1582..8cd9ab2 100644 --- a/discord/commands/Whois.ts +++ b/discord/commands/Whois.ts @@ -1,14 +1,15 @@ import DiscordInteractionCommand from "../../util/DiscordInteractionCommand"; -import { MemberModel } from "../../database/Member"; +import { MemberAdditionalAcknowledgement, MemberModel } from "../../database/Member"; import Partner, { PartnerCommissionType, PartnerDepartment, PartnerModel, PartnerRoleType, } from "../../database/Partner"; -import { ChatInputCommandInteraction, EmbedBuilder, GuildMember } from "discord.js"; +import { ChatInputCommandInteraction, EmbedBuilder, GuildMember, Snowflake } from "discord.js"; import MemberUtil from "../../util/MemberUtil"; import EmojiConfig from "../../util/EmojiConfig"; +import Formatters from "../../util/Formatters"; export default class Whois extends DiscordInteractionCommand { constructor() { @@ -36,7 +37,7 @@ export default class Whois extends DiscordInteractionCommand { const embed = new EmbedBuilder(); // 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 - const formattedName = MemberUtil.formatName(guildMember, partner); + const formattedName = Formatters.formatName(guildMember, partner); embed.setAuthor({ name: formattedName.text, iconURL: formattedName.iconURL }); // set the thumbnail to the user's avatar embed.setThumbnail(guildMember.user.displayAvatarURL()); @@ -79,16 +80,23 @@ export default class Whois extends DiscordInteractionCommand { break; } if (partner.directReport) { - if (partner.directReport instanceof Partner) { - embedDescription += `**Direct Report**: ${partner.directReport.title}\n`; + // fetch direct report object ref + 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); - if (embedDescription?.length > 0) embed.setDescription(embedDescription); + if (embedDescription?.length > 0) + embed.setDescription(`${embedDescription}\n\n<@${guildMember.id}>`); // add status to embed if (guildMember.presence?.status) { - // TODO: this currently doesn't work for some reason switch (guildMember.presence.status) { case "online": embed.addFields({ name: "Status", value: "Online", inline: true }); @@ -99,7 +107,7 @@ export default class Whois extends DiscordInteractionCommand { case "dnd": embed.addFields({ name: "Status", value: "Do Not Disturb", inline: true }); break; - case "offline" || "invisible": + case "offline": embed.addFields({ name: "Status", value: "Online", inline: true }); break; default: @@ -107,9 +115,87 @@ export default class Whois extends DiscordInteractionCommand { embed.addFields({ name: "Status", value: "", inline: true }); break; } + } else { + embed.addFields({ name: "Status", value: "Offline", inline: true }); + } + + // calculations for joined / created at + embed.addFields( + { + name: "Joined At", + value: guildMember.joinedTimestamp + ? `` + : "Invalid Date", + inline: true, + }, + { + name: "Created At", + value: guildMember.user.createdTimestamp + ? `` + : "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({ - 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] }); diff --git a/package.json b/package.json index 6b17fce..42b40a9 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "crv2", "license": "AGPL-3.0-or-later", "devDependencies": { "@eslint/js": "^9.13.0", @@ -18,6 +19,7 @@ }, "dependencies": { "@typegoose/typegoose": "^12.2.0", + "auth0": "^4.12.0", "discord.js": "^14.14.1", "mongoose": "^8.2.2", "stripe": "^14.21.0", diff --git a/util/DiscordEvent.ts b/util/DiscordEvent.ts index e75f161..2259961 100644 --- a/util/DiscordEvent.ts +++ b/util/DiscordEvent.ts @@ -8,5 +8,5 @@ export default abstract class DiscordEvent { this.client = client; this.execute = this.execute.bind(this); } - public abstract execute(...args: any[]): Error | Promise; + public abstract execute(...args: never[]): Error | Promise; } diff --git a/util/Formatters.ts b/util/Formatters.ts new file mode 100644 index 0000000..7e37c19 --- /dev/null +++ b/util/Formatters.ts @@ -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(), + }; + } + } +} diff --git a/util/MemberUtil.ts b/util/MemberUtil.ts index 9b7bf2e..dbd182b 100644 --- a/util/MemberUtil.ts +++ b/util/MemberUtil.ts @@ -8,13 +8,14 @@ import Partner, { import Member, { MemberAdditionalAcknowledgement, MemberModel } from "../database/Member"; import { Client, GuildMember, User } from "discord.js"; import { guildID } from "../config.json"; +import { Ref } from "@typegoose/typegoose"; export interface PartnerOptions { roleType: PartnerRoleType; commissionType: PartnerCommissionType; department: PartnerDepartment; title: PartnerTitle; - directReport: Partner | string; + directReport: Ref; } export interface FormatNameOptions { @@ -25,7 +26,7 @@ export interface FormatNameOptions { // TODO: Add the rest of the remaining role configurations export const PartnerDiscordRoleMap = { // Director of Engineering, Management, Staff, Technician, Core Team, Play Caller - "Director of Engineering": [ + Engineering: [ "1077646568091570236", "1077646956890951690", "446104438969466890", @@ -34,7 +35,7 @@ export const PartnerDiscordRoleMap = { "1014978134573064293", ], // Director of Operations, Management, Staff, Moderator, Core Team, Play Caller - "Director of Operations": [ + Operations: [ "1077647072163020840", "1077646956890951690", "446104438969466890", @@ -79,40 +80,4 @@ export default class MemberUtil { { $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(), - }; - } - } }