Pull upates to upstream #3

Merged
pax merged 7 commits from engineering/crv2:dev into master 2024-11-17 01:42:55 -05:00
9 changed files with 172 additions and 61 deletions

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,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] });

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(),
};
}
}
} }