Compare commits

..

6 Commits

Author SHA1 Message Date
Harry 54845033f1 Update discord/events/MessageReactionRemove.ts
Switched error messages to ephermal: true

Signed-off-by: Harry <harry@harryrosestudios.com>
2024-12-20 20:34:31 -05:00
Harry 65f4bd92dc Update discord/events/MessageReactionAdd.ts
switched error messages to Ephemeral: true

Signed-off-by: Harry <harry@harryrosestudios.com>
2024-12-20 20:33:31 -05:00
Harry e6dae3ae48 MessageReactionAdd.ts
Pins a message when a partner adds a 📌 reaction to it

Signed-off-by: Harry <harry@harryrosestudios.com>
2024-12-20 18:02:28 -05:00
Harry 9374a1bca6 MessageReactionRemove.ts
Unpins a message when a partner removes the 📌 reaction from it

Signed-off-by: Harry <harry@harryrosestudios.com>
2024-12-20 18:00:58 -05:00
Harry b37ac915c2 MessageReactionAdd.ts
Pins messages in a channel when a partner adds a 📌 reaction

Signed-off-by: Harry <harry@harryrosestudios.com>
2024-12-20 17:54:35 -05:00
Harry 6f561f3b44 MessagePinReactionHandler.ts
/**
 * Handles both 'messageReactionAdd' and 'messageReactionRemove' events.
 * When a reaction is added or removed:
 * - If the reaction matches the specified emoji and the user has the required role,
 *   the message will be pinned or unpinned in the channel accordingly.
 */

Signed-off-by: Harry <harry@harryrosestudios.com>
2024-12-20 02:12:27 -05:00
15 changed files with 248 additions and 657 deletions

View File

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

38
MessageReactionAdd.ts Normal file
View File

@ -0,0 +1,38 @@
import DiscordEvent from "../../util/DiscordEvent";
import { MessageReaction, PartialMessageReaction, PartialUser, User, Events } from "discord.js";
import MemberUtil from "../../util/MemberUtil";
const PUSH_PIN_EMOJI = "📌"; // Unicode
export default class MessageReactionAdd extends DiscordEvent {
constructor(client) {
super(Events.MessageReactionAdd, client);
}
public async execute(reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser): Promise<void> {
try {
// Fetch partials if necessary
if (reaction.partial) await reaction.fetch();
// Condensed check for guild existence and emoji name
if (!reaction.message.guild || reaction.emoji.name !== PUSH_PIN_EMOJI) return;
const guild = reaction.message.guild;
const member = await guild.members.fetch(user.id);
// Combine all partner roles from PartnerDiscordRoleMap
const allPartnerRoles = Object.values(MemberUtil.PartnerDiscordRoleMap).flat();
// Check if the user has any of the partner roles
if (!member.roles.cache.some(role => allPartnerRoles.includes(role.id))) return;
// Attempt to pin the message
await reaction.message.pin();
const channel = reaction.message.channel;
await channel.send(`Pinned message: ${reaction.message.id}`);
} catch (error) {
const channel = reaction.message.channel;
await channel.send(`Error pinning message: ${error}`);
}
}
}

View File

@ -1,7 +1,9 @@
# Community Relations v2 Gamma Edition System - CRRA/G
# Community Relations Alpha Edition System - CRRA
[![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
@ -86,4 +88,4 @@ __Library of Code Department of Engineering__ @:
- [Email](mailto:engineering@libraryofcode.org)
---
Thank you for checking out CRv2!
Thank you for checking out CRRA!

View File

@ -1,4 +1,4 @@
import { prop, getModelForClass, Ref } from "@typegoose/typegoose";
import { prop, getModelForClass } from "@typegoose/typegoose";
import Member from "./Member";
/* TODO
@ -14,7 +14,7 @@ export type PartnerTitle =
| "Deputy Director of Engineering"
| "Deputy Director of Operations"
| "Services Manager"
| "Community Manager"
| "Project Manager"
| "Engineering Core Partner"
| "Operations Core Partner"
| "Community Moderator"
@ -51,9 +51,6 @@ export default class Partner implements SharedMemberAttributes {
@prop({ required: true })
public roleType: PartnerRoleType | undefined;
@prop()
public isKeyHolder: boolean | undefined;
@prop({ required: true })
public commissionType: PartnerCommissionType | undefined;
@ -63,9 +60,9 @@ export default class Partner implements SharedMemberAttributes {
@prop({ required: true })
public title: PartnerTitle | "Partner" | undefined;
@prop({ ref: () => Partner })
@prop()
//
public directReport?: Ref<Partner> | string | undefined;
public directReport: Partner | string | undefined;
@prop()
// this field dictates if the partner is able to perform developer commands, such as "eval"

View File

@ -2,11 +2,10 @@ 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.");
@ -26,13 +25,9 @@ export default class Eval extends DiscordInteractionCommand {
option.setName("depth").setDescription("The depth of the inspection.").setRequired(false)
);
// 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);
}
});
this.listOfAllowedIDs = [
"278620217221971968", // Matthew
];
}
public async execute(interaction: ChatInputCommandInteraction) {

View File

@ -1,293 +0,0 @@
import DiscordInteractionCommand from "../../util/DiscordInteractionCommand";
import { ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import axios from "axios";
interface CertificateDetails {
bitLength: number;
connection: {
cipherSuite: string;
tlsVersion: string;
};
emailAddresses: [];
extendedKeyUsage: number[];
extendedKeyUsageAsText: string[];
fingerprint: string;
issuer: {
commonName: string;
country: string[];
locality: never; // TODO: needs clarification
province: string[];
organization: string[];
organizationalUnit: never; // TODO: needs clarification
};
keyUsageAsText: string[];
notAfter: Date;
notBefore: Date;
publicKeyAlgorithm: string;
san: string[];
serialNumber: string;
signatureAlgorithm: string;
status: boolean;
subject: {
commonName: string;
country: string[];
locality: never; // TODO: needs clarification
province: string[];
organization: string[];
organizationalUnit: never; // TODO: needs clarification
};
validationType: "DV" | "OV" | "EV";
}
// Define an enum for security levels
enum SecurityLevel {
MostSecure,
Secure,
LessSecure,
NotSecure,
}
interface CipherSuite {
cipher: string;
securityLevel: SecurityLevel;
}
const CipherSuites: CipherSuite[] = [
// Most Secure (TLS 1.3 AEAD Ciphers)
{ cipher: "TLS_AES_256_GCM_SHA384", securityLevel: SecurityLevel.MostSecure },
{ cipher: "TLS_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.MostSecure },
{ cipher: "TLS_AES_128_GCM_SHA256", securityLevel: SecurityLevel.MostSecure },
// Secure (TLS 1.2 AEAD Ciphers with Forward Secrecy)
{ cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure },
{ cipher: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", securityLevel: SecurityLevel.Secure },
// Less Secure (CBC with TLS 1.2 and SHA-256/SHA-384)
{ cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", securityLevel: SecurityLevel.LessSecure },
{ cipher: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", securityLevel: SecurityLevel.LessSecure },
{ cipher: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", securityLevel: SecurityLevel.LessSecure },
{ cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure },
{ cipher: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure },
{ cipher: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure },
{ cipher: "TLS_RSA_WITH_AES_256_CBC_SHA256", securityLevel: SecurityLevel.LessSecure },
{ cipher: "TLS_RSA_WITH_AES_128_CBC_SHA256", securityLevel: SecurityLevel.LessSecure },
// Not Secure (CBC with TLS 1.0/1.1 or SHA-1)
{ cipher: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_RSA_WITH_AES_256_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_RSA_WITH_AES_128_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_RSA_WITH_3DES_EDE_CBC_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_RSA_WITH_RC4_128_SHA", securityLevel: SecurityLevel.NotSecure },
{ cipher: "TLS_RSA_WITH_RC4_128_MD5", securityLevel: SecurityLevel.NotSecure },
];
export default class TLS extends DiscordInteractionCommand {
constructor() {
super("tls", "Receives TLS information about an HTTP server with a FQDN.");
this.builder.addStringOption((option) => {
return option
.setName("fqdn")
.setDescription(
"The Fully Qualified Domain Name (FQDN) for the server you want to perform the TLS/SSL lookup for."
)
.setRequired(true)
.setMinLength(3);
});
}
public getCipherSecurityLevel(cipher: string): SecurityLevel | null {
const result = CipherSuites.find((entry) => entry.cipher === cipher);
return result ? result.securityLevel : null;
}
public async execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply({ ephemeral: false });
try {
const certAPIReq = await axios.get(`https://certapi.libraryofcode.org/`, {
params: { q: interaction.options.getString("fqdn", true) },
});
if (certAPIReq.status !== 200) {
return interaction.editReply({
content:
"Could not fetch information for this FQDN's HTTP server. Please check the FQDN, its server, and try again.",
});
}
const certData: CertificateDetails = certAPIReq.data;
if (!certData.status) {
return interaction.editReply({
content: "Issue when fetching this FQDN's TLS certificate. Please try again later.",
});
}
const embed = new EmbedBuilder();
embed.setAuthor({
name: interaction.options.getString("fqdn", true),
iconURL: `https://${interaction.options.getString("fqdn", true)}/favicon.ico`,
});
let desc = "";
if (certData.validationType === "EV") {
desc += `**Certificate issued to:** __${certData.subject.organization[0]} [${certData.subject.country[0]}]__\n**Verified by:** __${certData.issuer.organization[0]}__\n\n`;
} else if (certData.issuer.organization) {
desc += `**Verified by:** __${certData.issuer.organization[0]}__\n\n`;
}
if (certData.subject) {
desc += "## Subject\n";
if (certData.subject.organization && certData.issuer.commonName) {
desc += `__**${certData.subject.organization[0]} (${certData.subject.commonName})**__\n`;
}
if (certData.subject.commonName) {
desc += `**Common Name:** ${certData.subject.commonName}\n`;
}
if (certData.subject.organization) {
desc += `**Organization:** ${certData.subject.organization[0]}\n`;
}
if (certData.subject.organizationalUnit) {
desc += `**Organizational Unit:** ${certData.subject.organizationalUnit[0]}\n`;
}
if (certData.subject.locality) {
desc += `**Locality:** ${certData.subject.locality[0]}\n`;
}
if (certData.subject.province) {
desc += `**State/Province:** ${certData.subject.province[0]}\n`;
}
if (certData.subject.country) {
desc += `**Country:** ${certData.subject.country[0]}\n`;
}
}
if (certData.issuer) {
desc += "## Issuer\n";
if (certData.issuer.organization && certData.issuer.commonName) {
desc += `__**${certData.issuer.organization[0]} (${certData.issuer.commonName})**__\n`;
}
if (certData.issuer.commonName) {
desc += `**Common Name:** ${certData.issuer.commonName}\n`;
}
if (certData.issuer.organization) {
desc += `**Organization:** ${certData.issuer.organization[0]}\n`;
}
if (certData.issuer.organizationalUnit) {
desc += `**Organizational Unit:** ${certData.issuer.organizationalUnit[0]}\n`;
}
if (certData.subject.locality) {
desc += `**Locality:** ${certData.subject.locality[0]}\n`;
}
if (certData.issuer.province) {
desc += `**State/Province:** ${certData.issuer.province[0]}\n`;
}
if (certData.issuer.country) {
desc += `**Country:** ${certData.issuer.country[0]}\n`;
}
}
embed.setDescription(desc);
let validationType:
| "Domain Validation (DV)"
| "Organization Validation (OV)"
| ":lock: Extended Validation (EV)"
| string;
switch (certData.validationType) {
case "DV":
validationType = "Domain Validation (DV)";
break;
case "OV":
validationType = "Organization Validation (OV)";
embed.setColor("#4287f5");
break;
case "EV":
embed.setColor("#42f554");
validationType = ":lock: Extended Validation (EV)";
break;
default:
validationType = "N/A';";
break;
}
let cipherSuiteText: string = "";
switch (this.getCipherSecurityLevel(certData.connection.cipherSuite)) {
case SecurityLevel.MostSecure:
cipherSuiteText = `:green_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`;
break;
case SecurityLevel.Secure:
cipherSuiteText = `:yellow_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`;
break;
case SecurityLevel.LessSecure:
cipherSuiteText = `:orange_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`;
break;
case SecurityLevel.NotSecure:
cipherSuiteText = `:red_circle: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`;
break;
default:
cipherSuiteText = `:grey_question: ${certData.connection.cipherSuite} (${certData.connection.tlsVersion})`;
break;
}
embed.addFields(
{
name: "Validation Type",
value: validationType,
inline: true,
},
{
name: "Cipher Suite",
value: cipherSuiteText,
inline: false,
},
{
name: "Public Key Algorithm",
value: `${certData.publicKeyAlgorithm} ${certData.bitLength}`,
inline: true,
},
{
name: "Signature Algorithm",
value: certData.signatureAlgorithm,
inline: true,
},
{ name: "Not Before", value: new Date(certData.notBefore).toUTCString(), inline: true },
{ name: "Not After", value: new Date(certData.notAfter).toUTCString(), inline: true },
{
name: "Serial Number",
value: certData.serialNumber,
inline: true,
}
);
console.log(certData); // TODO: Remove after testing.
if (certData.keyUsageAsText?.length)
embed.addFields({
name: "Key Usages",
value: certData.keyUsageAsText.join(", "),
inline: true,
});
if (certData.extendedKeyUsageAsText?.length)
embed.addFields({
name: "Extended Key Usages",
value: certData.extendedKeyUsageAsText.join(", "),
inline: true,
});
// embed.addField('Common Name', x509.data.subject.commonName, true);
// embed.addField('Issuer', x509.data.issuer.commonName, true);
// embed.addBlankField();
// embed.addField('Public Key Algorithm', x509.data.publicKeyAlgorithm, true);
// embed.addField('Not Before', new Date(x509.data.notBefore).toUTCString(), true);
// embed.addField('Not After', new Date(x509.data.notAfter).toUTCString(), true);
// if (x509.data.keyUsageAsText.length) embed.addField('Key Usages', x509.data.keyUsageAsText.join(', '), true);
// if (x509.data.extendedKeyUsageAsText.length) embed.addField('Extended Key Usages', x509.data.extendedKeyUsageAsText.join(', '), true);
return await interaction.editReply({ embeds: [embed] });
} catch (err) {
return interaction.editReply({ content: `Error processing retrieval from FQDN: ${err}` });
}
}
}

View File

@ -1,15 +1,14 @@
import DiscordInteractionCommand from "../../util/DiscordInteractionCommand";
import { MemberAdditionalAcknowledgement, MemberModel } from "../../database/Member";
import { MemberModel } from "../../database/Member";
import Partner, {
PartnerCommissionType,
PartnerDepartment,
PartnerModel,
PartnerRoleType,
} from "../../database/Partner";
import { ChatInputCommandInteraction, EmbedBuilder, GuildMember, Snowflake } from "discord.js";
import { ChatInputCommandInteraction, EmbedBuilder, GuildMember } 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() {
@ -37,7 +36,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 = Formatters.formatName(guildMember, partner);
const formattedName = MemberUtil.formatName(guildMember, partner);
embed.setAuthor({ name: formattedName.text, iconURL: formattedName.iconURL });
// set the thumbnail to the user's avatar
embed.setThumbnail(guildMember.user.displayAvatarURL());
@ -80,23 +79,16 @@ export default class Whois extends DiscordInteractionCommand {
break;
}
if (partner.directReport) {
// 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`;
if (partner.directReport instanceof Partner) {
embedDescription += `**Direct Report**: ${partner.directReport.title}\n`;
}
}
}
embed.setColor(guildMember.displayColor);
if (embedDescription?.length > 0)
embed.setDescription(`${embedDescription}\n\n<@${guildMember.id}>`);
if (embedDescription?.length > 0) embed.setDescription(embedDescription);
// 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 });
@ -107,7 +99,7 @@ export default class Whois extends DiscordInteractionCommand {
case "dnd":
embed.addFields({ name: "Status", value: "Do Not Disturb", inline: true });
break;
case "offline":
case "offline" || "invisible":
embed.addFields({ name: "Status", value: "Online", inline: true });
break;
default:
@ -115,87 +107,9 @@ 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
? `<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({
text: `Discord ID: ${guildMember.id}${databaseMember ? `| Internal ID: ${databaseMember?._id}` : ""}${partner ? ` | Partner ID: ${partner?.id}` : ""}`,
text: `Discord ID: ${guildMember.id}${databaseMember ? `Internal ID: ${databaseMember?._id}` : ""}`,
});
return await interaction.editReply({ embeds: [embed] });

View File

@ -0,0 +1,44 @@
import DiscordEvent from "../../util/DiscordEvent";
import { MessageReaction, PartialMessageReaction, PartialUser, User, Events } from "discord.js";
import MemberUtil from "../../util/MemberUtil";
const PUSH_PIN_EMOJI = "📌"; // Unicode
export default class MessageReactionAdd extends DiscordEvent {
constructor(client) {
super(Events.MessageReactionAdd, client);
}
public async execute(reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser): Promise<void> {
try {
// Fetch partials if necessary
if (reaction.partial) await reaction.fetch();
// Condensed check for guild existence and emoji name
if (!reaction.message.guild || reaction.emoji.name !== PUSH_PIN_EMOJI) return;
const guild = reaction.message.guild;
const member = await guild.members.fetch(user.id);
// Combine all partner roles from PartnerDiscordRoleMap
const allPartnerRoles = Object.values(MemberUtil.PartnerDiscordRoleMap).flat();
// Check if the user has any of the partner roles
if (!member.roles.cache.some(role => allPartnerRoles.includes(role.id))) return;
// Attempt to pin the message
await reaction.message.pin();
console.log(`Pinned message: ${reaction.message.id}`);
} catch (error) {
try {
const dmChannel = await user.createDM();
await dmChannel.send({
content: `There was an error pinning the message: ${error.message}`,
ephemeral: true,
});
} catch (dmError) {
console.error(`Failed to send ephemeral error message to ${user.tag}: ${dmError}`);
}
}
}
}

View File

@ -0,0 +1,44 @@
import DiscordEvent from "../../util/DiscordEvent";
import { MessageReaction, PartialMessageReaction, PartialUser, User, Events } from "discord.js";
import MemberUtil from "../../util/MemberUtil";
const PUSH_PIN_EMOJI = "📌"; // Unicode
export default class MessageReactionRemove extends DiscordEvent {
constructor(client) {
super(Events.MessageReactionRemove, client);
}
public async execute(reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser): Promise<void> {
try {
// Fetch partials if necessary
if (reaction.partial) await reaction.fetch();
// Condensed check for guild existence and emoji name
if (!reaction.message.guild || reaction.emoji.name !== PUSH_PIN_EMOJI) return;
const guild = reaction.message.guild;
const member = await guild.members.fetch(user.id);
// Combine all partner roles from PartnerDiscordRoleMap
const allPartnerRoles = Object.values(MemberUtil.PartnerDiscordRoleMap).flat();
// Check if the user has any of the partner roles
if (!member.roles.cache.some(role => allPartnerRoles.includes(role.id))) return;
// Attempt to unpin the message
await reaction.message.unpin();
console.log(`Unpinned message: ${reaction.message.id}`);
} catch (error) {
try {
const dmChannel = await user.createDM();
await dmChannel.send({
content: `There was an error unpinning the message: ${error.message}`,
ephemeral: true,
});
} catch (dmError) {
console.error(`Failed to send ephemeral error message to ${user.tag}: ${dmError}`);
}
}
}
}

105
index.ts
View File

@ -1,5 +1,5 @@
import { Client, GatewayIntentBits, Partials, REST, Routes } from "discord.js";
import { discordBotToken, discordClientID, mongoDBConnectionURI } from "./config.json";
import { discordBotToken, discordClientID } from "./config.json"
import Collection from "./util/Collection";
import DiscordInteractionCommand from "./util/DiscordInteractionCommand";
import DiscordEvent from "./util/DiscordEvent";
@ -12,65 +12,66 @@ export const DiscordEvents: Collection<DiscordEvent> = new Collection();
// Instantiates a new Discord client
const discordClient = new Client({
intents: [
GatewayIntentBits.DirectMessages,
GatewayIntentBits.GuildIntegrations,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildModeration,
],
partials: [Partials.GuildMember, Partials.Message, Partials.User, Partials.Channel],
intents: [
GatewayIntentBits.DirectMessages,
GatewayIntentBits.GuildIntegrations,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildInvites,
GatewayIntentBits.GuildModeration,
],
partials: [ Partials.GuildMember, Partials.Message, Partials.User, Partials.Channel, ],
});
const discordREST = new REST().setToken(discordBotToken);
// const stripeClient = new Stripe(stripeToken, { typescript: true });
export async function main() {
// Connect to the databases
try {
mongoose.connection.once("open", () => {
console.info("[Info - Database] Connected to MongoDB");
});
// TODO: Fetch the MongoDB URI from the config file
await mongoose.connect(mongoDBConnectionURI, {});
} catch (error) {
console.error(`[Error - Database] Failed to connect to MongoDB: ${error}`);
process.exit(1);
}
// Load Discord interaction commands
for (const Command of Object.values(DiscordInteractionCommandsIndex)) {
const instance = new Command();
DiscordInteractionCommands.add(instance.name, instance);
console.info(`[Info - Discord] Loaded interaction command: ${instance.name}`);
}
// Load Discord events
for (const Event of Object.values(DiscordEventsIndex)) {
const instance = new Event(discordClient);
DiscordEvents.add(instance.name, instance);
discordClient.on(instance.name, instance.execute);
console.info(`[Info - Discord] Loaded event: ${instance.name}`);
}
await discordClient.login(discordBotToken);
try {
console.log(`Started refreshing ${DiscordInteractionCommands.size} application (/) commands.`);
const interactionCommandsData = [];
for (const command of DiscordInteractionCommands.values()) {
interactionCommandsData.push(command.builder.toJSON());
// Connect to the databases
try {
mongoose.connection.once("open", () => {
console.info("[Info - Database] Connected to MongoDB");
})
// TODO: Fetch the MongoDB URI from the config file
await mongoose.connect("mongodb://localhost:27017/crra-main", {});
} catch (error) {
console.error(`[Error - Database] Failed to connect to MongoDB: ${error}`);
process.exit(1);
}
// Load Discord interaction commands
for (const Command of Object.values(DiscordInteractionCommandsIndex)) {
const instance = new Command();
DiscordInteractionCommands.add(instance.name, instance);
console.info(`[Info - Discord] Loaded interaction command: ${instance.name}`);
}
// Load Discord events
for (const Event of Object.values(DiscordEventsIndex)) {
const instance = new Event(discordClient);
DiscordEvents.add(instance.name, instance);
discordClient.on(instance.name, instance.execute);
console.info(`[Info - Discord] Loaded event: ${instance.name}`);
}
await discordClient.login(discordBotToken);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await discordREST.put(Routes.applicationCommands(discordClientID), {
body: interactionCommandsData,
});
try {
console.log(`Started refreshing ${DiscordInteractionCommands.size} application (/) commands.`);
const interactionCommandsData = [];
for (const command of DiscordInteractionCommands.values()) {
interactionCommandsData.push(command.builder.toJSON());
}
// @ts-ignore
console.log(`Successfully reloaded ${data?.length} application (/) commands.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
console.error(error);
}
// The put method is used to fully refresh all commands in the guild with the current set
const data = await discordREST.put(
Routes.applicationCommands(discordClientID),
{ body: interactionCommandsData },
);
// @ts-ignore
console.log(`Successfully reloaded ${data?.length} application (/) commands.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
console.error(error);
}
}
main();

134
package-lock.json generated
View File

@ -1,15 +1,11 @@
{
"name": "crv2",
"name": "CRRA",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "crv2",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@typegoose/typegoose": "^12.2.0",
"auth0": "^4.12.0",
"axios": "^1.7.8",
"discord.js": "^14.14.1",
"mongoose": "^8.2.2",
"stripe": "^14.21.0",
@ -329,9 +325,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz",
"integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz",
"integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==",
"dev": true,
"dependencies": {
"levn": "^0.4.1"
@ -937,39 +933,6 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/auth0": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/auth0/-/auth0-4.12.0.tgz",
"integrity": "sha512-5WDAHb8EvWSmRyA9D+FTBrHdEL1RM48PTPHVPxSmzbiAXrhR4pSgwSJyoGjia2+rvMR2NMXhtMfuRRqosEp7PA==",
"dependencies": {
"jose": "^4.13.2",
"undici-types": "^6.15.0",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/auth0/node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"node_modules/axios": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1337,17 +1300,6 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@ -1370,9 +1322,9 @@
"devOptional": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -1425,14 +1377,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -1927,25 +1871,6 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
@ -1961,19 +1886,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -2322,14 +2234,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jose": {
"version": "4.15.9",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -2714,25 +2618,6 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@ -3229,11 +3114,6 @@
"node": ">=6.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

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

View File

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

View File

@ -1,64 +0,0 @@
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 partner is designated as a KeyHolder, add a [k] to the end of the name
// if the partner exists, set the iconURL to the organizational logo
if (partner?.isKeyHolder) {
return {
text: `${target.displayName} [k]`,
iconURL: target.displayAvatarURL(),
};
} else if (partner?.roleType === PartnerRoleType.MANAGERIAL) {
// if a partner is of RoleType MANAGERIAL, add [m] to their name
return {
text: `${target.displayName} [m]`,
iconURL:
target instanceof GuildMember
? target.user.displayAvatarURL()
: 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,14 +8,13 @@ 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: Ref<Partner>;
directReport: Partner | string;
}
export interface FormatNameOptions {
@ -26,7 +25,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
Engineering: [
"Director of Engineering": [
"1077646568091570236",
"1077646956890951690",
"446104438969466890",
@ -35,7 +34,7 @@ export const PartnerDiscordRoleMap = {
"1014978134573064293",
],
// Director of Operations, Management, Staff, Moderator, Core Team, Play Caller
Operations: [
"Director of Operations": [
"1077647072163020840",
"1077646956890951690",
"446104438969466890",
@ -80,4 +79,40 @@ 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(),
};
}
}
}